From c5f724bac467cbe0a1808244034c36f2e4720eab Mon Sep 17 00:00:00 2001 From: sylyx Date: Sat, 16 May 2026 10:41:42 +0200 Subject: [PATCH] Add FastAPI web dashboard with live logs and video gallery Dark industrial-themed UI with WebSocket log streaming, bot trigger button, settings panel (subreddits, Whisper model, TTS voice, thresholds), and video gallery with inline player. Settings are passed to the bot subprocess via env vars so the pipeline respects UI config at runtime. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + requirements.txt | 2 + src/reddit_client.py | 10 +- src/voice_gen.py | 11 +- templates/index.html | 455 +++++++++++++++++++++++++++++++++++++++++++ web.py | 137 +++++++++++++ 6 files changed, 606 insertions(+), 10 deletions(-) create mode 100644 templates/index.html create mode 100644 web.py diff --git a/.gitignore b/.gitignore index 361b468..feed1af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +bot_settings.json __pycache__/ *.pyc *.pyo diff --git a/requirements.txt b/requirements.txt index 81190df..9014377 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ edge-tts>=6.1.9 openai-whisper>=20231117 moviepy>=1.0.3 python-dotenv>=1.0.0 +fastapi>=0.111.0 +uvicorn[standard]>=0.29.0 diff --git a/src/reddit_client.py b/src/reddit_client.py index 24a1e84..523ba6b 100644 --- a/src/reddit_client.py +++ b/src/reddit_client.py @@ -1,13 +1,15 @@ +import os import sqlite3 import praw from config import REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USER_AGENT DB_PATH = "processed.db" -SUBREDDITS = ["AITAH", "relationship_advice", "confessions", "tifu", "AmItheAsshole"] -MIN_SCORE = 1000 -MIN_WORDS = 200 -MAX_WORDS = 1500 +_env_subs = os.environ.get("BOT_SUBREDDITS", "") +SUBREDDITS = _env_subs.split(",") if _env_subs else ["AITAH", "relationship_advice", "confessions", "tifu", "AmItheAsshole"] +MIN_SCORE = int(os.environ.get("BOT_MIN_SCORE", 1000)) +MIN_WORDS = int(os.environ.get("BOT_MIN_WORDS", 200)) +MAX_WORDS = int(os.environ.get("BOT_MAX_WORDS", 1500)) def _init_db(): diff --git a/src/voice_gen.py b/src/voice_gen.py index 17add6d..86ea78d 100644 --- a/src/voice_gen.py +++ b/src/voice_gen.py @@ -1,14 +1,13 @@ import asyncio +import os import edge_tts -VOICE = "en-US-ChristopherNeural" - -async def _synthesize(text: str, output_path: str): - communicate = edge_tts.Communicate(text, VOICE) +async def _synthesize(text: str, output_path: str, voice: str): + communicate = edge_tts.Communicate(text, voice) await communicate.save(output_path) def generate_audio(text: str, output_path: str): - """Generate an MP3 from text using Edge TTS and save to output_path.""" - asyncio.run(_synthesize(text, output_path)) + voice = os.environ.get("BOT_VOICE", "en-US-ChristopherNeural") + asyncio.run(_synthesize(text, output_path, voice)) diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..be0b732 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,455 @@ + + + + + + Reddit Video Bot + + + + + + + +
+
+ + + + + +
+

REDDIT VIDEO BOT

+
AUTOMATED CONTENT PIPELINE
+
+
+
+ + IDLE +
+
+ + +
+
+
+ + +
+ + +
+ + +
+ CONTROL + +
READY
+
+ + +
+
+ SETTINGS + +
+ + +
+
SUBREDDITS
+
+
+ + +
+
+ + +
+
WHISPER MODEL
+ +
+ + +
+
TTS VOICE
+ +
+ + +
+
+
MIN SCORE
+ +
+
+
MIN WORDS
+ +
+
+
+
MAX WORDS
+ +
+
+
+ + +
+ + +
+
+ LIVE LOG + +
+
+
// Waiting for run…
+
+
+ + +
+
+ OUTPUT VIDEOS + +
+ +
+
+
+ + + + diff --git a/web.py b/web.py new file mode 100644 index 0000000..093cc2e --- /dev/null +++ b/web.py @@ -0,0 +1,137 @@ +import asyncio +import json +import os +from pathlib import Path + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import FileResponse, HTMLResponse + +app = FastAPI() + +OUTPUT_DIR = Path("output") +SETTINGS_FILE = Path("bot_settings.json") + +DEFAULT_SETTINGS = { + "subreddits": ["AITAH", "relationship_advice", "confessions", "tifu"], + "whisper_model": "base", + "voice": "en-US-ChristopherNeural", + "min_score": 1000, + "min_words": 200, + "max_words": 1500, +} + +log_clients: list[WebSocket] = [] +bot_running = False + + +def load_settings() -> dict: + if SETTINGS_FILE.exists(): + return json.loads(SETTINGS_FILE.read_text()) + return DEFAULT_SETTINGS.copy() + + +def save_settings(data: dict): + SETTINGS_FILE.write_text(json.dumps(data, indent=2)) + + +@app.get("/", response_class=HTMLResponse) +async def index(): + return open("templates/index.html").read() + + +@app.get("/api/settings") +async def get_settings(): + return load_settings() + + +@app.post("/api/settings") +async def post_settings(data: dict): + save_settings(data) + return {"ok": True} + + +@app.get("/api/videos") +async def list_videos(): + OUTPUT_DIR.mkdir(exist_ok=True) + videos = [] + for f in sorted(OUTPUT_DIR.glob("*.mp4"), key=lambda x: x.stat().st_mtime, reverse=True): + videos.append({ + "name": f.name, + "size": f.stat().st_size, + "url": f"/videos/{f.name}", + }) + return videos + + +@app.get("/videos/{filename}") +async def serve_video(filename: str): + path = OUTPUT_DIR / filename + if not path.exists(): + return {"error": "not found"} + return FileResponse(path, media_type="video/mp4") + + +async def broadcast(message: str): + dead = [] + for ws in log_clients: + try: + await ws.send_text(message) + except Exception: + dead.append(ws) + for ws in dead: + log_clients.remove(ws) + + +@app.post("/api/run") +async def run_bot(): + global bot_running + if bot_running: + return {"error": "Bot is already running"} + bot_running = True + asyncio.create_task(_run_bot_task()) + return {"ok": True} + + +async def _run_bot_task(): + global bot_running + settings = load_settings() + env = os.environ.copy() + env["WHISPER_MODEL"] = settings.get("whisper_model", "base") + env["BOT_VOICE"] = settings.get("voice", "en-US-ChristopherNeural") + env["BOT_SUBREDDITS"] = ",".join(settings.get("subreddits", [])) + env["BOT_MIN_SCORE"] = str(settings.get("min_score", 1000)) + env["BOT_MIN_WORDS"] = str(settings.get("min_words", 200)) + env["BOT_MAX_WORDS"] = str(settings.get("max_words", 1500)) + + try: + proc = await asyncio.create_subprocess_exec( + "python", "main.py", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + env=env, + ) + async for line in proc.stdout: + await broadcast(line.decode().rstrip()) + await proc.wait() + await broadcast(f"__DONE__{proc.returncode}") + except Exception as e: + await broadcast(f"[ERROR] {e}") + await broadcast("__DONE__1") + finally: + bot_running = False + + +@app.websocket("/ws/logs") +async def ws_logs(websocket: WebSocket): + await websocket.accept() + log_clients.append(websocket) + if bot_running: + await websocket.send_text("__RUNNING__") + try: + while True: + await asyncio.sleep(30) + except WebSocketDisconnect: + pass + finally: + if websocket in log_clients: + log_clients.remove(websocket)