commit 7cd84b657b930fb8b6b2586712414c4d8f41e273 Author: YusufB5 Date: Sat May 2 14:36:22 2026 +0300 Initial commit: ASCILINE Engine - Modular & Optimized diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4368d85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Video files (too large for GitHub, user provides their own) +*.mp4 +*.avi +*.mov +*.mkv +*.webm + +# Environment & IDE +.env +.vscode/ +.idea/ + +# Personal notes +mynotes.txt + +# Old versions +*-previous-ver-* diff --git a/README.md b/README.md new file mode 100644 index 0000000..26c6999 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# 🌌 ASCILINE Engine + +**ASCILINE** is a high-performance, real-time ASCII video rendering engine for the web. It streams video frames from a Python backend directly into a web browser at **60 FPS** using binary WebSockets and HTML5 Canvas. + +![ASCILINE Showcase](https://via.placeholder.com/800x450.png?text=Add+Your+Amazing+ASCII+GIF+Here) + +## 🚀 Key Features + +- **Real-Time Streaming**: Low-latency video-to-ASCII conversion. +- **High Performance**: Uses **HTML5 Canvas** for rendering instead of heavy DOM elements, enabling 60 FPS playback. +- **Binary Protocol**: Frames are encoded into `Uint8Array` (binary) for efficient bandwidth usage. +- **Multiple Color Modes**: Supports everything from classic B&W to 16M color ultra-fidelity. +- **Modern Aesthetic**: Premium dark-mode UI with interactive ripple dissolve effects. + +## 🛠️ Architecture + +1. **Backend (Python/FastAPI)**: Decodes video using OpenCV, maps pixels to ASCII characters via NumPy, and streams binary data. +2. **Frontend (Vanilla JS)**: Receives binary frames via WebSockets, manages a jitter buffer, and renders to a Canvas grid. +3. **Communication**: Optimized WebSocket protocol with a custom `INIT` handshake for dynamic resolution/FPS adjustment. + +## 📦 Installation + +### 1. Clone the repository +```bash +git clone https://github.com/yourusername/ASCILINE.git +cd ASCILINE +``` + +### 2. Install dependencies +```bash +pip install fastapi uvicorn opencv-python numpy websockets +``` + +### 3. Run the engine +Place a `video.mp4` in the root directory and start the server: +```bash +python stream_server.py +``` +Open `http://localhost:8000` in your browser. + +## 🎨 Customization + +You can easily customize the look and feel of the engine: + +### Styling +Edit `style.css` to change the accent colors and typography using CSS variables: +```css +:root { + --accent-color: #00ff41; /* Classic Matrix Green */ + --bg-color: #050505; +} +``` + +### Rendering Modes +The engine supports different fidelity levels via the `--mode` flag: +- `1`: Black & White (DOM mode) +- `2`: 512 Colors +- `3`: 32K Colors +- `4`: 262K Colors +- `5`: 16M Colors (Ultra) + +```bash +python stream_server.py --mode 5 --cols 240 --rows 100 +``` + +## 📜 License +MIT License. Feel free to use and abuse for your own ASCII adventures! diff --git a/app.js b/app.js new file mode 100644 index 0000000..a175017 --- /dev/null +++ b/app.js @@ -0,0 +1,288 @@ +/** + * ASCILINE ENGINE - Core Logic + * ========================================= + * Handles WebSocket communication, frame buffering, + * and dual-mode rendering (Canvas/DOM). + */ + +const player = document.getElementById('ascii-player'); +const canvas = document.getElementById('ascii-canvas'); +const ctx = canvas.getContext('2d'); +const statusEl = document.getElementById('status'); +const container = document.getElementById('player-container'); +const overlay = document.getElementById('play-overlay'); + +// ── STATE ── +let state = 'IDLE'; // IDLE | PLAYING | DISSOLVING +let ws = null; +const frameBuffer = []; +const BUFFER_SIZE = 4; +let targetFps = 24; +let frameInterval = 1000 / targetFps; +let renderMode = 1; + +// Grid & Dimensions +let gridCols = 0, gridRows = 0; +let charWidth = 0, charHeight = 0; +let xPos = null, yPos = null; + +// Timing & Metrics +let lastRenderTime = 0; +let frameCount = 0, currentFps = 0, lastFpsUpdate = 0; +let lastFrameView = null; // Stored for the ripple effect + +// Character Lookup Table (optimization) +const CHAR_LUT = new Array(128); +for (let i = 0; i < 128; i++) CHAR_LUT[i] = String.fromCharCode(i); + +/** + * Pre-calculates positions and scales canvas for high-performance rendering. + */ +function buildCanvas(cols, rows) { + gridCols = cols; + gridRows = rows; + ctx.font = 'bold 8px Courier New'; + charWidth = Math.ceil(ctx.measureText('M').width); + charHeight = 8; + + canvas.width = cols * charWidth; + canvas.height = rows * charHeight; + canvas.style.display = 'block'; + player.style.display = 'none'; + + container.style.minWidth = canvas.width + 'px'; + container.style.minHeight = canvas.height + 'px'; + + ctx.font = 'bold 8px Courier New'; + ctx.textBaseline = 'top'; + + xPos = new Float32Array(cols); + yPos = new Float32Array(rows); + for (let c = 0; c < cols; c++) xPos[c] = c * charWidth; + for (let r = 0; r < rows; r++) yPos[r] = r * charHeight; +} + +/** + * Initiates WebSocket connection and stream handling. + */ +function startStream() { + if (state !== 'IDLE') return; + state = 'PLAYING'; + overlay.classList.add('hidden'); + statusEl.textContent = 'Connecting...'; + statusEl.style.color = 'var(--accent-color)'; + + frameBuffer.length = 0; + lastFrameView = null; + frameCount = 0; + currentFps = 0; + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + ws = new WebSocket(`${protocol}//${location.host}/ws`); + ws.binaryType = 'arraybuffer'; + + ws.onmessage = (event) => { + if (state !== 'PLAYING') return; + + if (typeof event.data === 'string') { + if (event.data.startsWith('INIT:')) { + const p = event.data.split(':'); + targetFps = parseFloat(p[1]); + frameInterval = 1000 / targetFps; + renderMode = parseInt(p[2]); + if (renderMode > 1) { + buildCanvas(parseInt(p[3]), parseInt(p[4])); + } else { + player.style.display = 'block'; + canvas.style.display = 'none'; + } + return; + } + frameBuffer.push(event.data); + } else { + frameBuffer.push(event.data); + } + + // Buffer Overflow Protection + while (frameBuffer.length > BUFFER_SIZE * 3) frameBuffer.shift(); + + // Start render loop once buffered + if (frameBuffer.length >= BUFFER_SIZE && lastRenderTime === 0) { + lastRenderTime = performance.now(); + lastFpsUpdate = lastRenderTime; + requestAnimationFrame(renderFrame); + } + }; + + ws.onopen = () => { + statusEl.textContent = 'Buffering...'; + }; + + ws.onclose = () => { + if (state === 'PLAYING') { + statusEl.textContent = 'Stream Ended.'; + statusEl.style.color = '#888'; + setTimeout(() => resetToIdle(), 1500); + } + }; + + ws.onerror = () => { + statusEl.textContent = 'Connection Error!'; + statusEl.style.color = '#ff0000'; + setTimeout(() => resetToIdle(), 2000); + }; +} + +/** + * Main render loop using requestAnimationFrame. + */ +function renderFrame(now) { + if (state !== 'PLAYING') return; + requestAnimationFrame(renderFrame); + + const elapsed = now - lastRenderTime; + if (elapsed < frameInterval) return; + + // FPS Counter + frameCount++; + if (now - lastFpsUpdate >= 1000) { + currentFps = frameCount; + frameCount = 0; + lastFpsUpdate = now; + let modeText = 'B&W'; + const modes = { 2: '512 Color', 3: '32K Color', 4: '262K Color', 5: '16M Ultra' }; + modeText = modes[renderMode] || 'B&W'; + statusEl.textContent = `FPS: ${currentFps}/${Math.round(targetFps)} | Buf: ${frameBuffer.length} | ${modeText}`; + } + + if (frameBuffer.length === 0) return; + lastRenderTime = now; + + const frame = frameBuffer.shift(); + + if (renderMode === 1) { + player.textContent = frame; + } else { + const view = new Uint8Array(frame); + lastFrameView = view; + + ctx.fillStyle = '#050505'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.font = 'bold 8px Courier New'; + ctx.textBaseline = 'top'; + + let col = 0, row = 0, prevPacked = -1; + for (let idx = 0; idx < view.length; idx += 4) { + const packed = (view[idx+1] << 16) | (view[idx+2] << 8) | view[idx+3]; + if (packed !== prevPacked) { + ctx.fillStyle = `rgb(${view[idx+1]},${view[idx+2]},${view[idx+3]})`; + prevPacked = packed; + } + ctx.fillText(CHAR_LUT[view[idx]], xPos[col], yPos[row]); + col++; + if (col >= gridCols) { col = 0; row++; } + } + } +} + +/** + * Visual Effect: Ripple Dissolve + * Triggered on click during playback. + */ +function triggerRipple(clickX, clickY) { + if (!lastFrameView || renderMode === 1) { + resetToIdle(); + return; + } + + state = 'DISSOLVING'; + if (ws) { ws.onclose = null; ws.close(); ws = null; } + + const fv = lastFrameView; + const rippleSpeed = 400; + const waveFront = 70; + const maxShake = 6; + const maxDist = Math.sqrt(canvas.width * canvas.width + canvas.height * canvas.height); + const startTime = performance.now(); + + statusEl.textContent = ''; + + function animateRipple(now) { + const elapsed = (now - startTime) / 1000; + const radius = elapsed * rippleSpeed; + + if (radius > maxDist + waveFront + 20) { + resetToIdle(); + return; + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.font = 'bold 8px Courier New'; + ctx.textBaseline = 'top'; + + let col = 0, row = 0; + + for (let idx = 0; idx < fv.length; idx += 4) { + const px = xPos[col]; + const py = yPos[row]; + const dx = px - clickX; + const dy = py - clickY; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist > radius) { + ctx.globalAlpha = 1; + ctx.fillStyle = `rgb(${fv[idx+1]},${fv[idx+2]},${fv[idx+3]})`; + ctx.fillText(CHAR_LUT[fv[idx]], px, py); + } else if (dist > radius - waveFront) { + const progress = (radius - dist) / waveFront; + const shake = Math.sin(progress * Math.PI) * maxShake; + const ox = (Math.random() - 0.5) * shake * 2; + const oy = (Math.random() - 0.5) * shake * 2; + + ctx.globalAlpha = 1 - progress; + ctx.fillStyle = `rgb(${fv[idx+1]},${fv[idx+2]},${fv[idx+3]})`; + ctx.fillText(CHAR_LUT[fv[idx]], px + ox, py + oy); + } + + col++; + if (col >= gridCols) { col = 0; row++; } + } + + ctx.globalAlpha = 1; + requestAnimationFrame(animateRipple); + } + + requestAnimationFrame(animateRipple); +} + +function resetToIdle() { + state = 'IDLE'; + if (ws) { ws.onclose = null; ws.close(); ws = null; } + frameBuffer.length = 0; + lastRenderTime = 0; + lastFrameView = null; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + player.textContent = ''; + overlay.classList.remove('hidden'); + statusEl.textContent = 'Ready'; + statusEl.style.color = 'rgba(255,255,255,0.6)'; +} + +// ── EVENT LISTENERS ── +overlay.addEventListener('click', (e) => { + e.stopPropagation(); + startStream(); +}); + +container.addEventListener('click', (e) => { + if (state !== 'PLAYING') return; + + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + const clickX = (e.clientX - rect.left) * scaleX; + const clickY = (e.clientY - rect.top) * scaleY; + + triggerRipple(clickX, clickY); +}); diff --git a/ascii_video_player2.py b/ascii_video_player2.py new file mode 100644 index 0000000..f83093c --- /dev/null +++ b/ascii_video_player2.py @@ -0,0 +1,302 @@ +""" +ascii_video_player.py +===================== +Modüler, renkli (True Color / 24-bit ANSI), sıfır titremeli ASCII video oynatıcı. + + - VideoDecoder : Video → (gray, color) kare çifti üretir. + - AsciiMapper : Gri matris → ASCII karakter + ANSI True Color kodu → String. + - TerminalRenderer: Ana döngü, FPS kontrolü, yön tespiti, render. + +Bağımlılıklar: + pip install opencv-python numpy +""" + +import sys +import time +import shutil +import numpy as np +import cv2 +import os + +# PowerShell/CMD (Windows) üzerinde ANSI renk kodlarını aktif etmek için: +os.system("") + + +# ───────────────────────────────────────────── +# MODÜL 1 ─ VideoDecoder +# ───────────────────────────────────────────── +class VideoDecoder: + """ + Video dosyasını açar ve her kare için (gray, bgr) çifti üretir. + + Renkli render için hem gri (karakter seçimi) hem de + orijinal BGR (renk örnekleme) matrisine ihtiyaç var. + İkisi de aynı resize işleminden geçer → boyut tutarlılığı garantili. + """ + + def __init__(self, path: str, cols: int, rows: int) -> None: + self._cap = cv2.VideoCapture(path) + if not self._cap.isOpened(): + raise FileNotFoundError(f"Video açılamadı: {path!r}") + + self.fps : float = self._cap.get(cv2.CAP_PROP_FPS) or 24.0 + self.frame_count : int = int(self._cap.get(cv2.CAP_PROP_FRAME_COUNT)) + self.vid_w : int = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + self.vid_h : int = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + self._size : tuple = (cols, rows) + + def __iter__(self): + return self + + def __next__(self) -> tuple[np.ndarray, np.ndarray]: + """ + :return: (gray[H,W] uint8, bgr[H,W,3] uint8) + """ + ok, frame = self._cap.read() + if not ok: + raise StopIteration + + small = cv2.resize(frame, self._size, interpolation=cv2.INTER_LINEAR) + gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) + return gray, small # small = küçültülmüş BGR karesi + + def release(self): + self._cap.release() + + def __del__(self): + self.release() + + +# ───────────────────────────────────────────── +# MODÜL 2 ─ AsciiMapper +# ───────────────────────────────────────────── +class AsciiMapper: + """ + Gri + BGR matrisini ANSI True Color kodlarıyla renklendirilmiş + ASCII çerçeve dizisine dönüştürür. + + ── True Color ANSI Formatı ───────────────────────────────────────────── + \033[38;2;R;G;Bm{karakter}\033[0m + └─ ön plan rengi ─────────┘ + + ── Renk Kuantizasyonu (Performans Optimizasyonu) ─────────────────────── + Her piksel için ayrı bir escape kodu üretmek yerine renk değerleri + 6-bit'e indirilir (>> 2 << 2, 64 seviye/kanal). + Bu sayede ardışık aynı renkli pikseller tek bir escape koduyla + temsil edilir → string boyutu ve stdout.write yükü azalır. + Gözle algılanabilir renk kaybı olmaz (16M → ~262K renk). + + ── RLE (Run-Length Encoding) ─────────────────────────────────────────── + Aynı renkteki ardışık karakterler için escape kodu tekrar yazılmaz; + yalnızca renk değiştiğinde yeni kod eklenir. + Tipik bir karede %40-60 oranında string küçülmesi sağlar. + """ + + DEFAULT_PALETTE = list( + " `.-':_,^=;><+!rc*/z?sLTv)J7(|Fi{C}fI31tlu[neoZ5Yxjya]2ESwqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@" + ) + + # ANSI sıfırlama + satır başı + _RESET = "\033[0m" + + def __init__(self, palette: list[str] | None = None, quantize_bits: int = 0) -> None: + """ + :param palette: Karakter listesi (None → 93 seviyeli varsayılan) + :param quantize_bits: Renk kuantizasyonu için sağdan kaydırma miktarı. + 2 → 64 seviye/kanal (hızlı), + 0 → tam 8-bit (en yüksek kalite, varsayılan). + """ + p = palette or self.DEFAULT_PALETTE + self._n = len(p) + self._lut = np.array(p, dtype='U1') + self._qb = quantize_bits # kuantizasyon bit kaydırma miktarı + + def convert(self, gray: np.ndarray, bgr: np.ndarray) -> str: + """ + Her piksel için: + 1. Gri değeri → ASCII karakter (yoğunluk LUT) + 2. BGR rengi → ANSI True Color escape kodu (kuantize + RLE) + + :param gray: shape=(H,W) uint8 gri matris + :param bgr: shape=(H,W,3) uint8 BGR renk matrisi + :return: Terminale doğrudan yazılabilecek renkli ASCII dizesi + """ + H, W = gray.shape + + # ── Adım 1: Piksel yoğunluğu → karakter indeksi ────────────────── + indices = np.floor_divide(gray, max(1, 256 // self._n)) + np.clip(indices, 0, self._n - 1, out=indices) + char_matrix = self._lut[indices] # shape=(H,W), dtype='U1' + + # ── Adım 2: Renk kuantizasyonu ──────────────────────────────────── + # BGR → RGB sıralaması (ANSI kodu R,G,B sırasında) + rgb = bgr[:, :, ::-1] # BGR → RGB view, kopyasız + + if self._qb > 0: + # Düşük bitleri sıfırla → renk hassasiyetini düşür, hızı artır + qb = self._qb + rgb = (rgb >> qb) << qb # örn. qb=2: 0b11111100 maskeleme + + # ── Adım 3: RLE ile renkli string birleştirme ───────────────────── + # Saf NumPy ile RLE yapılamadığından bu kısım Python döngüsüdür. + # Ancak satır başına yalnızca renk değişimlerinde escape kodu yazılır; + # tekrarlanan renkler için döngü yükü minimize edilir. + lines = [] + prev_r = prev_g = prev_b = -1 # önceki renk (ilk piksel hep farklı) + + for row_idx in range(H): + row_chars = char_matrix[row_idx] # shape=(W,) char array + row_colors = rgb[row_idx] # shape=(W,3) uint8 array + buf = [] + + for col_idx in range(W): + r, g, b = int(row_colors[col_idx, 0]), \ + int(row_colors[col_idx, 1]), \ + int(row_colors[col_idx, 2]) + + # RLE: sadece renk değişince yeni escape kodu ekle + if r != prev_r or g != prev_g or b != prev_b: + buf.append(f"\033[38;2;{r};{g};{b}m") + prev_r, prev_g, prev_b = r, g, b + + buf.append(row_chars[col_idx]) + + lines.append("".join(buf)) + + return self._RESET + "\n".join(lines) + self._RESET + + +# ───────────────────────────────────────────── +# MODÜL 3 ─ TerminalRenderer +# ───────────────────────────────────────────── +class TerminalRenderer: + """ + VideoDecoder → AsciiMapper → stdout akışını yönetir. + + Ek özellikler (renkli sürüm): + - Başlangıçta terminal arka planını siyaha alır (\033[40m) + → renkli karakterler daha belirgin görünür. + - Her kare sonunda \033[0m ile renk sıfırlanır + → sonraki terminal komutları etkilenmez. + """ + + _CURSOR_HOME = "\033[H" + _HIDE_CURSOR = "\033[?25l" + _SHOW_CURSOR = "\033[?25h" + _BLACK_BG = "\033[40m" # siyah arka plan — kontrast için + _RESET_ALL = "\033[0m" + _CLEAR_SCREEN = "\033[2J" + + CHAR_RATIO = 0.45 # terminal karakter en-boy oranı düzeltmesi + + def __init__( + self, + path : str, + palette : list[str] | None = None, + quantize_bits: int = 0, + ) -> None: + """ + :param path: Video dosyası yolu + :param palette: Özel karakter paleti (None → 93 seviyeli) + :param quantize_bits: Renk kuantizasyonu (0=tam kalite, 2=hızlı) + """ + # ── Video meta bilgisi ──────────────────────────────────────────── + _probe = VideoDecoder(path, 2, 2) + vid_w, vid_h = _probe.vid_w, _probe.vid_h + src_fps = _probe.fps + _probe.release() + + # ── Terminal boyutları ──────────────────────────────────────────── + term = shutil.get_terminal_size(fallback=(220, 50)) + t_cols = term.columns + t_lines = term.lines - 2 + + # ── Yön tespiti & en-boy oranı korumalı boyutlandırma ───────────── + orientation = "portrait" if vid_h > vid_w else "landscape" + aspect = vid_h / vid_w + + if orientation == "landscape": + cols = t_cols + rows = max(1, int(cols * aspect * self.CHAR_RATIO)) + if rows > t_lines: + rows = t_lines + cols = max(1, int(rows / (aspect * self.CHAR_RATIO))) + else: + rows = t_lines + cols = max(1, int(rows / (aspect * self.CHAR_RATIO))) + if cols > t_cols: + cols = t_cols + rows = max(1, int(cols * aspect * self.CHAR_RATIO)) + + # ── Bilgi ekranı ────────────────────────────────────────────────── + print(self._CLEAR_SCREEN) + print( + f"\033[1m[ASCII Player — True Color]\033[0m\n" + f" Yön : {orientation.upper()}\n" + f" Video : {vid_w}×{vid_h}\n" + f" ASCII : {cols}×{rows} karakter\n" + f" FPS : {src_fps:.1f}\n" + f" Kuantizasyon: {2**(8-quantize_bits)} seviye/kanal\n" + f" Çıkış : Ctrl+C\n" + ) + time.sleep(2.0) + + self._decoder = VideoDecoder(path, cols, rows) + self._mapper = AsciiMapper(palette, quantize_bits) + self._fps = self._decoder.fps + self._frame_t = 1.0 / self._fps + + def play(self) -> None: + """Ana oynatma döngüsü.""" + stdout = sys.stdout + + stdout.write(self._HIDE_CURSOR + self._BLACK_BG) + stdout.flush() + + try: + for gray_frame, bgr_frame in self._decoder: + t0 = time.perf_counter() + + ascii_frame = self._mapper.convert(gray_frame, bgr_frame) + + stdout.write(self._CURSOR_HOME + ascii_frame) + stdout.flush() + + wait = self._frame_t - (time.perf_counter() - t0) + if wait > 0: + time.sleep(wait) + + except KeyboardInterrupt: + pass + + finally: + stdout.write(self._SHOW_CURSOR + self._RESET_ALL + "\n") + stdout.flush() + self._decoder.release() + + +# ───────────────────────────────────────────── +# GİRİŞ NOKTASI +# ───────────────────────────────────────────── +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="True Color ANSI ASCII video oynatıcı — sıfır titreme" + ) + parser.add_argument("video", + help="Video dosyası yolu (MP4, AVI, MKV …)") + parser.add_argument("--palette", default=None, + help="Özel karakter paleti, boşlukla ayrılmış") + parser.add_argument("--quality", type=int, choices=[0, 1, 2, 3], default=0, + help="Renk kalitesi: 0=maksimum kalite, 3=maksimum hız (varsayılan: 0)") + args = parser.parse_args() + + custom_palette = args.palette.split() if args.palette else None + + renderer = TerminalRenderer( + path = args.video, + palette = custom_palette, + quantize_bits = args.quality, + ) + renderer.play() diff --git a/index.html b/index.html new file mode 100644 index 0000000..4906ee7 --- /dev/null +++ b/index.html @@ -0,0 +1,38 @@ + + + + + + ASCILINE | Real-Time ASCII Engine + + + + + + + +
Ready
+ + +

ASCILINE

+

real-time ascii streaming engine

+ + +
+ +

+        
+        
+        
+
+        
+        
+
+ Tap to Play +
+
+ + + + + \ No newline at end of file diff --git a/stream_server.py b/stream_server.py new file mode 100644 index 0000000..b0f4f4a --- /dev/null +++ b/stream_server.py @@ -0,0 +1,131 @@ +""" +stream_server.py +================ +Video to ASCII çekirdek motorunu HTTP/WebSocket üzerinden web'e yayınlar. +Bağımlılıklar: pip install fastapi uvicorn websockets +""" + +import asyncio +import numpy as np +import cv2 +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +import uvicorn +import os +from websockets.exceptions import ConnectionClosed + +# Mevcut motoru import ediyoruz (ascii_video_player2.py) +from ascii_video_player2 import VideoDecoder, AsciiMapper + +app = FastAPI() + +# Serve static files (style.css, app.js) from the project directory +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +app.mount("/static", StaticFiles(directory=BASE_DIR), name="static") + +def get_html_content(): + html_path = os.path.join(os.path.dirname(__file__), "index.html") + with open(html_path, "r", encoding="utf-8") as f: + return f.read() + +@app.get("/") +async def root(): + """İstemciye Frontend (HTML/JS/CSS) dosyasını sunar.""" + return HTMLResponse(get_html_content()) + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + """ + İstemci bağlandığında videoyu decode etmeye başlar, + AsciiMapper ile saf ASCII'ye çevirir ve WebSockets üzerinden gönderir. + """ + await websocket.accept() + + video_path = getattr(app.state, "video_path", "video.mp4") + render_mode = getattr(app.state, "render_mode", 1) + cols = getattr(app.state, "cols", 200) + rows = getattr(app.state, "rows", 80) + + try: + decoder = VideoDecoder(video_path, cols, rows) + except FileNotFoundError: + await websocket.send_text("Hata: Video dosyası bulunamadı!") + await websocket.close() + return + + mapper = AsciiMapper() + fps = decoder.fps + frame_t = 1.0 / fps + + # Karakter → byte kodu lookup tablosu (binary format için) + char_byte_lut = np.array([ord(c) for c in mapper._lut], dtype=np.uint8) + + # Kuantizasyon seviyesini bir kere belirle (render_mode sabit) + qb = {5: 0, 4: 2, 3: 3, 2: 5}.get(render_mode, 0) + + # İstemciye meta bilgilerini yolla (cols/rows grid oluşturmak için) + await websocket.send_text(f"INIT:{fps}:{render_mode}:{cols}:{rows}") + + try: + # Decoder iteratörü her kare için (gray, bgr) döndürüyor + # Binary frame buffer'ı önceden ayır (GC baskısını azalt) + frame_buf = np.empty((rows, cols, 4), dtype=np.uint8) if render_mode > 1 else None + + for gray_frame, bgr_frame in decoder: + t0 = asyncio.get_event_loop().time() + + # Ortak: yoğunluk → karakter indeksi + indices = np.floor_divide(gray_frame, max(1, 256 // mapper._n)) + np.clip(indices, 0, mapper._n - 1, out=indices) + + if render_mode == 1: + # --- SAF ASCII DÖNÜŞÜMÜ (text) --- + char_matrix = mapper._lut[indices] + lines = [''.join(row) for row in char_matrix] + await websocket.send_text('\n'.join(lines)) + else: + # --- RENKLİ BINARY DÖNÜŞÜMÜ (numpy, sıfır Python döngüsü) --- + H, W = gray_frame.shape + char_codes = char_byte_lut[indices] # (H,W) uint8 + + rgb = bgr_frame[:, :, ::-1] # BGR → RGB + if qb > 0: + rgb = (rgb >> qb) << qb + + # [char, R, G, B] interleaved binary frame + frame_buf[:, :, 0] = char_codes + frame_buf[:, :, 1:] = rgb + + await websocket.send_bytes(frame_buf.tobytes()) + + elapsed = asyncio.get_event_loop().time() - t0 + wait = frame_t - elapsed + if wait > 0: + await asyncio.sleep(wait) + + except (WebSocketDisconnect, ConnectionClosed): + print("İstemci yayından ayrıldı.") + finally: + decoder.release() + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description="Gerçek Zamanlı ASCII Web Sunucusu") + parser.add_argument("video", help="Yayınlanacak video dosyası", default="video.mp4", nargs='?') + parser.add_argument("--port", type=int, default=8000, help="Sunucu portu") + parser.add_argument("--mode", type=int, choices=[1, 2, 3, 4, 5], default=1, help="Render Modu: 1=B&W, 2=512renk, 3=32K, 4=262K, 5=16M Ultra") + parser.add_argument("--cols", type=int, default=200, help="Terminal kolon genişliği") + parser.add_argument("--rows", type=int, default=80, help="Terminal satır yüksekliği") + args = parser.parse_args() + + # Argümanları global olarak state içine kaydet + app.state.video_path = args.video + app.state.render_mode = args.mode + app.state.cols = args.cols + app.state.rows = args.rows + + print(f"[{args.video}] yayınlanmaya hazır. Mod: {args.mode}, Çöz: {args.cols}x{args.rows}") + print(f"Sunucu başlatılıyor... Lütfen tarayıcınızdan http://localhost:{args.port} adresine gidin.") + + uvicorn.run(app, host="0.0.0.0", port=args.port, ws_ping_interval=None, ws_ping_timeout=None) diff --git a/style.css b/style.css new file mode 100644 index 0000000..e04ba70 --- /dev/null +++ b/style.css @@ -0,0 +1,167 @@ +/* + ASCILINE ENGINE - Core Styles + ========================================= + Feel free to customize the look using the variables below. +*/ + +:root { + --bg-color: #050505; + --accent-color: #00ff41; + --accent-glow: rgba(0, 255, 65, 0.3); + --text-muted: rgba(255, 255, 255, 0.4); + --player-bg: #030303; + --font-main: 'Courier New', monospace; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: var(--bg-color); + color: var(--accent-color); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + font-family: var(--font-main); + overflow: hidden; +} + +/* ── UI ELEMENTS ─────────────────────────── */ + +.status { + position: fixed; + top: 12px; + left: 16px; + color: rgba(255, 255, 255, 0.6); + font-size: 13px; + font-family: monospace; + z-index: 100; +} + +h1 { + color: var(--accent-color); + font-size: 18px; + letter-spacing: 6px; + text-transform: uppercase; + margin-bottom: 8px; + text-shadow: 0 0 10px var(--accent-glow); +} + +.subtitle { + color: var(--text-muted); + font-size: 11px; + letter-spacing: 3px; + margin-bottom: 24px; +} + +/* ── PLAYER CONTAINER ─────────────────────────── */ + +#player-container { + position: relative; + border: 1px solid rgba(0, 255, 65, 0.2); + box-shadow: 0 0 30px rgba(0, 255, 65, 0.06), + inset 0 0 30px rgba(0, 0, 0, 0.5); + background: var(--player-bg); + cursor: pointer; + min-width: 500px; + min-height: 280px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + transition: border-color 0.4s ease, box-shadow 0.4s ease; +} + +#player-container:hover { + border-color: rgba(0, 255, 65, 0.4); + box-shadow: 0 0 40px rgba(0, 255, 65, 0.1), + inset 0 0 30px rgba(0, 0, 0, 0.5); +} + +#ascii-player { + font-family: var(--font-main); + white-space: pre; + overflow: hidden; + font-size: 8px; + line-height: 8px; + letter-spacing: 0; + font-weight: bold; + contain: content; + display: none; +} + +#ascii-canvas { + display: none; +} + +/* ── OVERLAYS ───────────────────────── */ + +#play-overlay { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(3, 3, 3, 0.97); + z-index: 10; + transition: opacity 0.5s ease; + user-select: none; +} + +#play-overlay.hidden { + opacity: 0; + pointer-events: none; +} + +.play-btn { + width: 72px; + height: 72px; + border: 2px solid rgba(0, 255, 65, 0.5); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + animation: pulse-glow 2.5s ease-in-out infinite; + transition: transform 0.2s ease, border-color 0.2s ease; +} + +.play-btn:hover { + transform: scale(1.08); + border-color: var(--accent-color); +} + +.play-btn::after { + content: ''; + display: block; + width: 0; + height: 0; + border-style: solid; + border-width: 12px 0 12px 22px; + border-color: transparent transparent transparent var(--accent-color); + margin-left: 4px; +} + +.play-label { + color: rgba(0, 255, 65, 0.5); + font-size: 11px; + letter-spacing: 5px; + text-transform: uppercase; +} + +@keyframes pulse-glow { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(0, 255, 65, 0.3), + 0 0 15px rgba(0, 255, 65, 0.05); + } + 50% { + box-shadow: 0 0 0 12px rgba(0, 255, 65, 0), + 0 0 25px rgba(0, 255, 65, 0.12); + } +}