A high-performance ASCII video rendering engine featuring real-time WebSocket binary streaming and an isolated compiler for serverless static generation. Built
A high-performance ASCII video rendering engine featuring real-time WebSocket binary streaming and an isolated compiler for serverless static generation. Built
█████╗ ███████╗ ██████╗██╗██╗ ██╗███╗ ██╗███████╗ ██
██╔══██╗██╔════╝██╔════╝██║██║ ██║████╗ ██║██╔════╝ █████
███████║███████╗██║ ██║██║ ██║██╔██╗ ██║█████╗ ████████
██╔══██║╚════██║██║ ██║██║ ██║██║╚██╗██║██╔══╝ ████████
██║ ██║███████║╚██████╗██║███████╗██║██║ ╚████║███████╗ █████
╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ ██
ASCILINE is a high-performance, cross-platform real-time ASCII video rendering engine. It maps pixels to text-based representations and streams the result over a low-overhead binary protocol, turning the browser canvas into a typographic display surface.
| Output | Details |
|---|---|
| Original Source Standard MP4 video file. |
|
| ASCII Mode Rendered using Mode 4 (32K colors) from a 30fps source. |
|
| PIXEL Mode Rendered using the --pixel flag for high fidelity colored blocks █ . |
<video> tag, no browser-side codec decoding, no autoplay restrictions. To the browser, it's just text on a canvas.Roadmap idea, not implemented yet: because ASCII output is already a compact, structured text representation, it could in principle serve as a lightweight input for downstream text/LLM processing instead of feeding raw pixel streams to a vision model. Nothing in the current codebase does this — flagging it here as a direction, not a shipped feature.
Uint8Array straight to the canvas.INIT handshake negotiates resolution/FPS, followed by the binary frame stream.ASCILINE uses a modular architecture that separates the core rendering engine from its delivery methods.
…The original protocol re-sends the full grid every frame. An opt-in adaptive codec picks the smallest of several encodings per frame and tags it with a 1-byte header, without changing the rendered output:
| tag | encoding | best for |
|---|---|---|
0 RAW |
framebuffer as-is (legacy) | incompressible frames |
1 ZLIB |
zlib(framebuffer) |
general motion |
2 DELTA |
only the cells that changed since the last frame | static / low-motion |
3 RLE_FULL |
run-length encoded framebuffer | large flat-color regions |
4 DCT |
Discrete Cosine Transform | High-ratio spatial compression. Used exclusively by the static player. Automatically enforces --pixel output. |
Clients opt in with /ws?codec=adaptive; omit it and you get the original protocol byte-for-byte, so existing clients are unaffected. A keyframe is forced periodically so dropped packets / late joiners resync.
codec.js (the shared decoder used by both the live player and the test suite) understands all four tags. Not every encoder produces all four, though: the Python side (codec.py, used by the live server and by compiler.py) can emit RLE_FULL when it wins the size comparison. The browser-side JS encoder (static_player/studio/encoder.js, used by the client-only Studio compiler) intentionally only emits RAW/ZLIB/DELTA — it doesn't implement RLE run-building, to keep the in-browser encoder simple. RAW/ZLIB/DELTA already cover most cases reasonably well, so this is a deliberate simplicity/size trade-off, not a bug — decoders stay permissive, encoders stay conservative.
Measured wire savings (mode 6, 200×80 grid):
| content | vs. legacy |
|---|---|
| static screen / slideshow | 0.3% (≈375×) |
| high-motion / full-frame change | 63% (never worse than legacy) |
An optional --quality {lossless,high,balanced,low} enables lossy temporal delta: a color cell is only re-sent once it drifts past a tolerance from what the viewer already sees (the character plane stays exact), cutting the hard cases a further ~15–30% at imperceptible quality. Default is lossless (bit-exact).
Monitor bandwidth in real time: pass --debug when launching the server to see live RAW vs WIRE byte comparisons and the compression ratio in your terminal.
Verified two independent ways, both bit-exact: Python-encoded vectors decoded by
codec.jsin Node (experiments/gen_vectors.py→experiments/check_vectors.js), and a liveadaptive-vs-legacyWebSocket diff (experiments/test_e2e.js). Generate test clips withexperiments/make_test_clips.sh.
LAN / network streaming: use --host to expose the server on your network.
python stream_server.py video.mp4 --host 0.0.0.0ASCILINE can compile a video into a self-contained .ascf (ASCII Compressed Format) file and play it back with a static HTML page — no Python backend at runtime, hostable anywhere (GitHub Pages, Vercel, Netlify).
Trade-off: compiled
.ascffiles are naturally larger than standard.mp4. In exchange you get true DOM-level interaction, pixel-perfect text selection, and no dependency on the browser's video codecs.
There are two ways to produce a .ascf file:
python compiler.py your_video.mp4 --cols 250 --pixel --quantize 2--quantize 0-3: drops color bits to reduce file size (0 = lossless, 3 = aggressive).--profile: Enables Discrete Cosine Transform (Tag 4) spatial compression. Provides the engine's highest compression ratio, significantly reducing the final .ascf payload size at the cost of higher encode times and lossy quantization. Automatically enforces --pixel.--qf 1-100: Quality factor for the DCT profile (default: 70). Higher means better quality and larger file.--tolerance: color drift tolerance before a pixel update is sent, to skip invisible changes.--hard: max zlib compression (level 9) — slower to compile, smaller output.This is what powers the live demo at asciline.dev: the static clips there are compiled with this Python path.
static_player/studio/ is a standalone page (index.html + encoder.js, using pako from a CDN) that compiles a video to .ascf entirely client-side — drop a video in, get a .ascf out, nothing ever leaves your browser, no Python required.
u can try it from - https://yusufb5.github.io/ASCILINE/static_player/studio/
The page includes a built-in preview with a custom seekbar, allowing you to instantly scrub through your compiled clip. Because it shares the main codec.js, this studio player natively decodes all advanced compression tags (including Tag 4 DCT).
(Note: While it can play all tags, the client-side encoder itself is conservative and only emits RAW/ZLIB/DELTA for speed. For production output or maximum compression with RLE/DCT, use the Python compiler).
For the full experience — audio sync and ASCII/pixel mode support — use the main player at static_player/index.html.
Method A: Drag & Drop (No server needed!)
Simply open static_player/index.html in your browser and drag your .ascf file (along with an optional .mp3 file for audio) directly onto the page. Playback starts instantly, completely bypassing browser CORS restrictions with zero backend required.
Method B: Local File Server If you prefer to load files via URL instead of drag-and-drop, serve the folder through a plain static server:
python -m http.serverInfinite Playback & Low RAM: The static player uses an aggressive rolling buffer (~3 seconds). Rendered frames are instantly garbage-collected, allowing continuous playback with no duration limit and a near-zero memory footprint.
git clone https://github.com/YusufB5/ASCILINE.git
cd ASCILINEASCILINE's dependencies are defined in pyproject.toml. Install the base package with:
pip install .Or, if you prefer the plain requirements file:
pip install fastapi uvicorn opencv-python numpy websocketsRunning headless (server / no display, e.g. a VPS or container)? opencv-python-headless is a lighter drop-in replacement for opencv-python and avoids pulling in GUI dependencies you won't use.
Optional — play from YouTube (and other yt-dlp sites):
pip install ".[ytdlp]"This installs the ytdlp extra defined in pyproject.toml, pulling in yt-dlp for URL streaming. Only needed if you pass a URL instead of a local file — local playback works without it. URL playback also uses FFmpeg (see below) to normalize downloads.
Package manager (recommended):
winget install ffmpegbrew install ffmpegsudo apt install ffmpegManual (Windows): if you hit a FileNotFoundError o
No open issues yet, or sync has not completed.