Initial commit: ASCILINE Engine - Modular & Optimized

This commit is contained in:
YusufB5 2026-05-02 14:36:22 +03:00
commit 7cd84b657b
7 changed files with 1016 additions and 0 deletions

23
.gitignore vendored Normal file
View file

@ -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-*

67
README.md Normal file
View file

@ -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) <!-- Replace with your actual GIF -->
## 🚀 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!

288
app.js Normal file
View file

@ -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);
});

302
ascii_video_player2.py Normal file
View file

@ -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()

38
index.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASCILINE | Real-Time ASCII Engine</title>
<!-- Core Styles -->
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<!-- Live Status Indicator -->
<div id="status" class="status">Ready</div>
<!-- Header Section -->
<h1>ASCILINE</h1>
<p class="subtitle">real-time ascii streaming engine</p>
<!-- Main Player Container -->
<div id="player-container">
<!-- Text-based rendering (B&W Mode) -->
<pre id="ascii-player"></pre>
<!-- Canvas-based rendering (Color Modes) -->
<canvas id="ascii-canvas"></canvas>
<!-- Tap to Play Interaction Overlay -->
<div id="play-overlay">
<div class="play-btn"></div>
<span class="play-label">Tap to Play</span>
</div>
</div>
<!-- Core Engine Logic -->
<script src="/static/app.js"></script>
</body>
</html>

131
stream_server.py Normal file
View file

@ -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)

167
style.css Normal file
View file

@ -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);
}
}