diff --git a/README.md b/README.md
index 7c1176b..294c953 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** Standard MP4 video file. |
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
+
+
+
+
+
+
+
+
+
ASCILINE.studio
+
Video โ Pixel Art Renderer
+
+
+
+
+
๐ฌ
+
Drop your video here
+
or click to browse files
+
+ MP4
+ MOV
+ AVI
+ MKV
+ WEBM
+
+
+
+
+
Pick quality
+
+
+
๐
+
Fast
+
320 cols ยท 8fps
+
+
+
โก
+
Balanced
+
480 cols ยท 12fps
+
+
+
๐ฏ
+
Sharp
+
640 cols ยท 12fps
+
+
+
๐
+
Ultra
+
960 cols ยท 15fps
+
+
+
+
+
Choose a video to start โ
+
+
+
+
+
Renderingโฆ
+
โ
+
+
+
+
+
+
+
+
+
+
ASCILINE
+
โ
+
+
โ
+
โ New video
+
+
+
+
+
+
+
+
โฎ
+
โช
+
โถ
+
โฉ
+
โญ
+
0:00 / 0:00
+
+
Speed
+
+ 0.25ร
+ 0.5ร
+ 1ร
+ 1.5ร
+ 2ร
+ 3ร
+
+
+
+ Grid โ
+ FPS โ
+ Frames โ
+ Duration โ
+
+
+
+
+
+
+
+
+
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..394e4b0
--- /dev/null
+++ b/final_viewr.html
@@ -0,0 +1,1076 @@
+
+
+
+
+
+ASCILINE Player
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Speed
+
+ 0.25x
+ 0.5x
+ 0.75x
+ 1x
+ 1.5x
+ 2x
+ 3x
+ 5x
+
+
+
+
+ Size
+
+ Auto
+ 1x
+ 2x
+ 3x
+ 4x
+
+
+
+
+
+
+
+
+
+
Space Play/Pause
+
โ โ Step frames
+
Home End First/Last
+
+/- Speed
+
F Fullscreen
+
+
+
+
+
+
+
+
+
+
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