From 104b6311fc703e5f41d646b9cae957221ac5bd9f Mon Sep 17 00:00:00 2001 From: Karthikeyan_A Date: Tue, 16 Jun 2026 12:34:15 +0530 Subject: [PATCH 1/4] Add files via upload add pixel export, web embed widget, and requirements.txt --- export_ascii.py | 227 +++++++++++ final_viewr.html | 992 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 + 3 files changed, 1224 insertions(+) create mode 100644 export_ascii.py create mode 100644 final_viewr.html create mode 100644 requirements.txt diff --git a/export_ascii.py b/export_ascii.py new file mode 100644 index 0000000..f76f625 --- /dev/null +++ b/export_ascii.py @@ -0,0 +1,227 @@ +""" +export_ascii.py — Export ASCILINE video frames to static files +============================================================== +Outputs files from a video in two modes: + + ASCII mode (default): + 1. .txt — Plain ASCII text, one frame per section + 2. .ascjson — JSON with char+RGB per cell + + PIXEL mode (--pixel): + 1. .txt — Full-block █ with ANSI 24-bit color codes + 2. .ascjson — Compact JSON, RGB only (3 bytes/cell, 16M colors) + Structure: {"meta":{cols,rows,fps,mode:"pixel"}, + "frames":[[r,g,b,...]...]} + +Usage: + python export_ascii.py myvideo.mp4 + python export_ascii.py myvideo.mp4 --pixel --cols 320 --max-fps 15 + python export_ascii.py myvideo.mp4 --pixel --no-color # txt only + +Dependencies: opencv-python numpy +Place this file next to ascii_video_player2.py +""" + +import sys +import os +import json +import argparse +import numpy as np + +# ── resolve ASCILINE root ───────────────────────────────────────────────────── +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, SCRIPT_DIR) + +try: + from ascii_video_player2 import VideoDecoder, AsciiMapper +except ImportError: + print("ERROR: ascii_video_player2.py not found. Place export_ascii.py in the ASCILINE folder.") + sys.exit(1) + +# ── palette (mirrors ASCILINE default) ─────────────────────────────────────── +PALETTE = list(" `.-':_,^=;><+!rc*/z?sLTv)J7(|Fi{C}fI31tlu[neoZ5Yxjya]2ESwqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@") +FRAME_SEP = "─" * 80 + +# ── ANSI 24-bit color helpers ──────────────────────────────────────────────── +ANSI_FG = "\033[38;2;{};{};{}m" +ANSI_BG = "\033[48;2;{};{};{}m" +ANSI_RESET = "\033[0m" +FULL_BLOCK = "\u2588" # █ + + +def calc_rows(cols, vid_w, vid_h): + """Match ASCILINE's auto-row formula (chars are ~2× taller than wide).""" + ratio = vid_w / max(vid_h, 1) + return max(1, round(cols / ratio / 2)) + + +def export(video_path: str, output_stem: str, cols: int, rows: int | None, + no_color: bool, max_fps: float, progress: bool, pixel_mode: bool): + + # ── Open once to get dimensions ─────────────────────────────────────── + probe = VideoDecoder(video_path, cols, rows or 1) + vid_w, vid_h = probe.vid_w, probe.vid_h + probe.release() + + if rows is None: + rows = calc_rows(cols, vid_w, vid_h) + + decoder = VideoDecoder(video_path, cols, rows) + fps = min(decoder.fps, max_fps) + + skip_n = max(1, round(decoder.fps / fps)) if decoder.fps > max_fps else 1 + effective_fps = decoder.fps / skip_n + + mode_label = "PIXEL" if pixel_mode else "ASCII" + + # ── ASCII mode setup ────────────────────────────────────────────────── + if not pixel_mode: + mapper = AsciiMapper(PALETTE) + n = mapper._n + lut = mapper._lut + char_byte_lut = np.array([ord(c) for c in lut], dtype=np.uint8) + + txt_path = output_stem + ".txt" + json_path = output_stem + ".ascjson" + + print(f"Video : {video_path}") + print(f"Grid : {cols}×{rows} FPS: {effective_fps:.1f} Mode: {mode_label}") + print(f"Output: {txt_path}") + if not no_color: + print(f" {json_path}") + + txt_frames = [] + json_frames = [] + frame_idx = 0 + + try: + while True: + # FPS decimation + for _ in range(skip_n - 1): + if not decoder.grab(): + decoder.release() + break + + try: + gray, bgr = next(decoder) + except StopIteration: + break + + if pixel_mode: + # ═══════════════════════════════════════════════════════════ + # PIXEL MODE — solid color blocks, 16M color fidelity + # ═══════════════════════════════════════════════════════════ + rgb = bgr[:, :, ::-1].copy() # BGR → RGB, shape (rows, cols, 3) + + # ── .txt with ANSI 24-bit color ─────────────────────────── + lines = [] + for r in range(rows): + parts = [] + for c in range(cols): + pr, pg, pb = int(rgb[r, c, 0]), int(rgb[r, c, 1]), int(rgb[r, c, 2]) + parts.append(f"{ANSI_FG.format(pr, pg, pb)}{FULL_BLOCK}") + parts.append(ANSI_RESET) + lines.append(''.join(parts)) + txt_frames.append(f"FRAME {frame_idx}\n" + '\n'.join(lines)) + + # ── .ascjson — RGB only, 3 values per cell ──────────────── + if not no_color: + json_frames.append(rgb.flatten().tolist()) + + else: + # ═══════════════════════════════════════════════════════════ + # ASCII MODE — character density + color (original) + # ═══════════════════════════════════════════════════════════ + indices = np.floor_divide(gray, max(1, 256 // n)) + np.clip(indices, 0, n - 1, out=indices) + + char_matrix = lut[indices] + lines = [''.join(row) for row in char_matrix] + txt_frames.append(f"FRAME {frame_idx}\n" + '\n'.join(lines)) + + if not no_color: + char_codes = char_byte_lut[indices] + rgb = bgr[:, :, ::-1].copy() + frame_buf = np.empty((rows, cols, 4), dtype=np.uint8) + frame_buf[:, :, 0] = char_codes + frame_buf[:, :, 1:] = rgb + json_frames.append(frame_buf.flatten().tolist()) + + frame_idx += 1 + if progress and frame_idx % 50 == 0: + total = decoder.frame_count + pct = min(100, round(frame_idx * skip_n / total * 100)) + print(f" … {frame_idx} frames ({pct}%)", end='\r') + + except KeyboardInterrupt: + print("\nInterrupted — saving partial export…") + finally: + decoder.release() + + # ── Write .txt ──────────────────────────────────────────────────────────── + header = ( + f"ASCILINE {mode_label} Export\n" + f"cols={cols} rows={rows} fps={effective_fps:.3f} " + f"frames={frame_idx} mode={mode_label.lower()}\n" + f"{FRAME_SEP}\n" + ) + with open(txt_path, "w", encoding="utf-8") as f: + f.write(header) + f.write(f"\n{FRAME_SEP}\n".join(txt_frames)) + print(f"\n✓ Saved {frame_idx} frames → {txt_path} ({os.path.getsize(txt_path)//1024} KB)") + + # ── Write .ascjson ─────────────────────────────────────────────────────── + if not no_color: + meta = { + "cols": cols, + "rows": rows, + "fps": round(effective_fps, 3), + "mode": "pixel" if pixel_mode else "ascii", + } + with open(json_path, "w") as f: + json.dump({"meta": meta, "frames": json_frames}, f, separators=(',', ':')) + print(f"✓ Saved {frame_idx} frames → {json_path} ({os.path.getsize(json_path)//1024} KB)") + + +# ── CLI ─────────────────────────────────────────────────────────────────────── +def main(): + ap = argparse.ArgumentParser( + description="Export ASCILINE video to .txt and .ascjson", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Mode comparison: + ASCII (default) — Character density + color, artistic look + PIXEL (--pixel) — Solid █ blocks + 16M color RGB, ultra-high fidelity + """) + ap.add_argument("video", help="Path to video file") + ap.add_argument("-o", "--output", default=None, + help="Output file stem (default: same as video name)") + ap.add_argument("--cols", type=int, default=240, help="ASCII columns (default 240, pixel 320)") + ap.add_argument("--rows", type=int, default=None, help="ASCII rows (auto if omitted)") + ap.add_argument("--max-fps", type=float, default=12, help="Cap output FPS (default 12)") + ap.add_argument("--pixel", action="store_true", + help="PIXEL mode: solid color blocks, 16M colors, ultra fidelity") + ap.add_argument("--no-color", action="store_true", help="Skip .ascjson, txt only") + ap.add_argument("--quiet", action="store_true", help="Suppress progress output") + args = ap.parse_args() + + # Pixel mode defaults to higher resolution + cols = args.cols + if args.pixel and cols == 240: + cols = 320 # pixel mode benefits from more columns + + stem = args.output or os.path.splitext(os.path.basename(args.video))[0] + export( + video_path = args.video, + output_stem = stem, + cols = cols, + rows = args.rows, + no_color = args.no_color, + max_fps = args.max_fps, + progress = not args.quiet, + pixel_mode = args.pixel, + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/final_viewr.html b/final_viewr.html new file mode 100644 index 0000000..0653db0 --- /dev/null +++ b/final_viewr.html @@ -0,0 +1,992 @@ + + + + + +ASCILINE Player + + + + + + + +
+
+ +
pixel
+
+
+
+ + +
+
+

Drop .ascjson file here

+

+ Export from video with + python export_ascii.py video.mp4 --pixel +
+ then load the generated .ascjson file here. +
+ Supports both PIXEL mode (16M color) and ASCII mode. +

+ +
+ + +
+
+

Loading frames...

+
+ + +
+ +
0 / 0
+
+ + +
+ + + + + + +
+
+
+ +
+ + +
+ +
+ + +
+
+ + +
+
+
Mode
+
+
+
+
Resolution
+
+
+
+
FPS
+
+
+
+
Frames
+
+
+
+
File Size
+
+
+
+
Duration
+
+
+
+ + +
+
Space Play/Pause
+
Step frames
+
HomeEnd First/Last
+
+/- Speed
+
F Fullscreen
+
+
+ + +
+ + + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7fffa04 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +opencv-python +numpy +websockets From d1ecdcd76165906bf855fe7d03ae676dd7371aec Mon Sep 17 00:00:00 2001 From: Karthikeyan_A Date: Tue, 16 Jun 2026 12:41:43 +0530 Subject: [PATCH 2/4] Update README with export and web player details Added export functionality and web player widget instructions. --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ae5efad..c10d27a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,61 @@ -# 🌌 ASCILINE Engine +# 🌌 ASCILINE Engine with Export **ASCILINE** is a high-performance, cross-platform real-time ASCII video rendering engine. **Our core objective is to transform the web into a highly dynamic and interactive typographic canvas.** By mapping pixels to text-based representations, we unlock new possibilities for web media delivery. +## 💾 Export to Static Files (`export_ascii.py`) + +Export any video to two portable static files — no server or WebSocket required: + +| File | Contents | +| :--- | :--- | +| `output.txt` | Plain ASCII frames separated by dividers. Human-readable, terminal-pipeable, embeddable in `.md`. | +| `output.ascjson` | Compact JSON with per-cell color data: `{"meta":{cols,rows,fps},"frames":[[char,r,g,b,...]...]}`. Feeds the web player widget directly. | + +Place `export_ascii.py` in the ASCILINE folder (next to `ascii_video_player2.py`), then: + +```bash +# Basic — outputs video.txt + video.ascjson +python export_ascii.py video.mp4 + +# Custom output name +python export_ascii.py video.mp4 -o my_export + +# Custom grid size (rows auto-calculated if omitted) +python export_ascii.py video.mp4 --cols 120 --rows 34 + +# Cap output FPS (default: 24) +python export_ascii.py video.mp4 --max-fps 12 + +# Text only, skip the color JSON +python export_ascii.py video.mp4 --no-color + +# Suppress progress output +python export_ascii.py video.mp4 --quiet +``` + +### Web Player Widget + +Load an `.ascjson` export into any webpage with the included `website_widget.js`: + +```html + +
+ + + + + + +``` + + | Output | Details | | :--- | :--- | | Original Source | **Original Source**
Standard MP4 video file. | From ceadd1594721da77662ebf9f90273c2158436678 Mon Sep 17 00:00:00 2001 From: Karthikeyan_A Date: Tue, 16 Jun 2026 13:59:19 +0530 Subject: [PATCH 3/4] Update fmt.Println message from 'Hello' to 'Goodbye' --- final_viewr.html | 136 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 26 deletions(-) diff --git a/final_viewr.html b/final_viewr.html index 0653db0..394e4b0 100644 --- a/final_viewr.html +++ b/final_viewr.html @@ -607,33 +607,117 @@ function loadFile(file) { loading.classList.add('active'); loadingText.textContent = `Loading ${file.name}...`; - const reader = new FileReader(); - reader.onload = e => { - try { - data = JSON.parse(e.target.result); - meta = data.meta; - if (!meta || !meta.cols || !meta.rows || !data.frames || !data.frames.length) { - throw new Error('Invalid .ascjson structure'); - } - isPixel = meta.mode === 'pixel'; - frameStride = isPixel ? 3 : 4; - currentFrame = 0; - - loading.classList.remove('active'); - initPlayer(file.name); - showToast(`Loaded ${data.frames.length} frames (${isPixel ? 'PIXEL' : 'ASCII'} mode)`, 'success'); - } catch (err) { - loading.classList.remove('active'); - dropzone.style.display = ''; - showToast('Failed to parse file: ' + err.message, 'error'); - } - }; - reader.onerror = () => { + // ── Streaming chunk parser ──────────────────────────────────────────── + // JSON.parse on a 200MB+ string crashes the browser mid-parse. + // Instead we read the file in 4MB chunks via ReadableStream and hand-parse + // the frame array token-by-token so we never hold more than one frame in + // memory at a time. + loadFileStreaming(file).then(() => { + loading.classList.remove('active'); + initPlayer(file.name); + showToast(`Loaded ${data.frames.length} frames (${isPixel ? 'PIXEL' : 'ASCII'} mode)`, 'success'); + }).catch(err => { loading.classList.remove('active'); dropzone.style.display = ''; - showToast('Failed to read file', 'error'); - }; - reader.readAsText(file); + showToast('Failed to parse file: ' + err.message, 'error'); + }); +} + +async function loadFileStreaming(file) { + const frames = []; + const total = file.size; + const decoder = new TextDecoder(); + const stream = file.stream(); + const reader = stream.getReader(); + + let buffer = ''; // rolling text buffer (never holds full file) + let totalRead = 0; + let metaDone = false; + let inFrames = false; + let frameCount = 0; + + // ── read one chunk at a time, never accumulating the full file ──────── + async function readChunk() { + const { done, value } = await reader.read(); + if (done) return false; + totalRead += value.byteLength; + buffer += decoder.decode(value, { stream: true }); + return true; + } + + // ── Phase 1: read until we have the meta block ──────────────────────── + loadingText.textContent = 'Reading header…'; + while (!buffer.includes('"frames":[')) { + const ok = await readChunk(); + if (!ok) break; + } + + const metaMatch = buffer.match(/"meta"\s*:\s*(\{[^}]+\})/); + if (!metaMatch) throw new Error('Could not find meta block'); + meta = JSON.parse(metaMatch[1]); + if (!meta || !meta.cols || !meta.rows) throw new Error('Invalid meta block'); + + // Trim buffer to just after opening '[' of frames array + const fi = buffer.indexOf('"frames":['); + if (fi === -1) throw new Error('Could not find frames array'); + buffer = buffer.slice(fi + '"frames":['.length); + inFrames = true; + + // ── Phase 2: extract frames one by one, streaming chunks as needed ──── + // Each frame is a flat JSON array [n,n,...] — no nested arrays. + // We find the '[' and scan forward for the matching ']'. + // When we run out of buffer, we pull another chunk. + + while (true) { + // Skip commas/whitespace between frames + buffer = buffer.trimStart().replace(/^,+/, '').trimStart(); + + // End of frames array + if (buffer.startsWith(']') || buffer.startsWith(']}')) break; + + // Need more data to determine what's next + if (buffer.length < 2) { + const ok = await readChunk(); + if (!ok) break; + continue; + } + + if (buffer[0] !== '[') { buffer = buffer.slice(1); continue; } + + // Find the closing ']' of this frame — load more chunks until we have it + let closeIdx = -1; + while (true) { + closeIdx = buffer.indexOf(']', 1); + if (closeIdx !== -1) break; + const ok = await readChunk(); + if (!ok) break; + } + if (closeIdx === -1) break; + + // Extract and parse this frame + const frameStr = buffer.slice(0, closeIdx + 1); + buffer = buffer.slice(closeIdx + 1); + + // JSON.parse the frame array, then convert to Uint8Array + // (avoids the string-coercion bug that zeroed all values) + const arr = new Uint8Array(JSON.parse(frameStr)); + frames.push(arr); + frameCount++; + + // Yield to UI thread + update progress every 5 frames + if (frameCount % 5 === 0) { + const pct = Math.round(totalRead / total * 100); + loadingText.textContent = `Parsing frames… ${frameCount} frames (${pct}%)`; + await new Promise(r => setTimeout(r, 0)); + } + } + + if (frames.length === 0) throw new Error('No frames found in file'); + + data = { meta, frames }; + isPixel = meta.mode === 'pixel'; + frameStride = isPixel ? 3 : 4; + currentFrame = 0; } // ── Player init ─────────────────────────────────────────── @@ -989,4 +1073,4 @@ function formatDuration(seconds) { - \ No newline at end of file + From abe6e9d5e9358fac26a15b261156a27350753a22 Mon Sep 17 00:00:00 2001 From: Karthikeyan_A Date: Tue, 16 Jun 2026 15:06:34 +0530 Subject: [PATCH 4/4] Add files via upload --- asciline_studio.html | 1026 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1026 insertions(+) create mode 100644 asciline_studio.html diff --git a/asciline_studio.html b/asciline_studio.html new file mode 100644 index 0000000..71fb1c4 --- /dev/null +++ b/asciline_studio.html @@ -0,0 +1,1026 @@ + + + + + +ASCILINE Studio + + + + + + +
+
+ +
Video → Pixel Art Renderer
+
+ +
+ + 🎬 +
Drop your video here
+
or click to browse files
+
+ MP4 + MOV + AVI + MKV + WEBM +
+
+ +
+ +
+
+ 🚀 +
Fast
+
320 cols · 8fps
+
+
+ +
Balanced
+
480 cols · 12fps
+
+
+ 🎯 +
Sharp
+
640 cols · 12fps
+
+
+ 💎 +
Ultra
+
960 cols · 15fps
+
+
+
+ + +
+ + +
+
Rendering…
+
+ +
+
+
+
+
+
+
0
+
Frames
+
+
+
0%
+
Complete
+
+
+
+
Speed
+
+
+
Initialising…
+
+ +
+ +
LIVE PREVIEW
+
+
+ + +
+
+ +
+
+
+ +
+ +
+ +
0 / 0
+
+ +
+
+
+
+
+ + + + + +
0:00 / 0:00
+
+ + +
+
+ Grid + FPS + Frames + Duration +
+
+
+ +
+ + + +