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)