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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LIVE LOG
+
+
+
+
+
+
+
+
+ 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)