diff --git a/README.md b/README.md index 9161559..d522157 100644 --- a/README.md +++ b/README.md @@ -215,18 +215,24 @@ docker tag guardian-newscast truenas.local:30095/guardian-newscast:latest docker push truenas.local:30095/guardian-newscast:latest ``` -### Deploying on TrueNAS +#### Cross-platform builds + +For Intel (x86_64): ```bash -docker pull truenas.local:30095/guardian-newscast:latest -docker run -d \ - --name guardian-newscast \ - --network host \ - -v /path/to/data:/app/data \ - -e OLLAMA_URL=http://127.0.0.1:11434/api/generate \ - -e PORT=30095 \ - --restart unless-stopped \ - truenas.local:30095/guardian-newscast:latest +docker buildx build --platform linux/amd64 -t guardian-newscast:amd64 . +``` + +For ARM (aarch64 / Apple Silicon): + +```bash +docker buildx build --platform linux/arm64 -t guardian-newscast:arm64 . +``` + +To build and push both variants as a single multi-arch manifest: + +```bash +docker buildx build --platform linux/amd64,linux/arm64 -t truenas.local:30095/guardian-newscast:latest --push . ``` ### Docker Compose @@ -242,14 +248,10 @@ docker compose --profile full up -d ### GPU Acceleration The default Dockerfile uses `python:3.11-slim` (Debian), which has glibc — fully compatible with NVIDIA CUDA. - -To run with GPU acceleration on TrueNAS: -1. Install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html). -2. Build with CUDA-enabled torch: +1. Build with CUDA-enabled torch: ```bash docker build --target gpu -t guardian-newscast:gpu . ``` (Add a `FROM python:3.11-slim AS gpu` stage with `pip install torch --index-url https://download.pytorch.org/whl/cu121`) 3. Run with `--gpus all`. -4. Minimum **4 GB VRAM** recommended for Ollama + Kokoro in the same container. - +4. Minimum **4 GB VRAM** recommended for Ollama + Kokoro in the same container. \ No newline at end of file diff --git a/pipeline.py b/pipeline.py index ffad3f3..3d11688 100644 --- a/pipeline.py +++ b/pipeline.py @@ -37,8 +37,21 @@ from server.app import run_server as start_server logger.add("pipeline.log", rotation="5 MB", retention="7 days") +def clear_json_data() -> None: + """Remove stale JSON files at the start of each pipeline cycle.""" + if not os.path.isdir(DATA_DIR): + return + for fname in ("output.json", "scripts.json"): + fpath = os.path.join(DATA_DIR, fname) + if os.path.exists(fpath): + os.remove(fpath) + logger.info(f"Cleared {fpath}") + logger.info("Cleared JSON data at cycle start") + + async def run_pipeline() -> bool: """Run all three pipeline stages sequentially. Returns True on success.""" + clear_json_data() logger.info("=" * 60) logger.info("Stage 1: Scraping Guardian articles...") try: diff --git a/server/app.py b/server/app.py index 724f9a0..3c2d143 100644 --- a/server/app.py +++ b/server/app.py @@ -1,5 +1,6 @@ """HTTP server that serves the Guardian Daily Newscast MP3 index page and audio files.""" +import glob import os import json import threading @@ -65,6 +66,41 @@ class NewscastHandler(SimpleHTTPRequestHandler): """Suppress per-request logs to avoid noisy output.""" pass + def do_POST(self): + if self.path == "/clear": + self.clear_audio() + else: + self.send_error(405, "Method Not Allowed") + + def clear_audio(self): + """Remove all MP3 files from the audio directory.""" + if ".." in self.path: + self.send_error(403, "Forbidden") + return + audio_dir = os.path.join(self.data_dir, "audio") + if not os.path.isdir(audio_dir): + self._json_response({"status": "ok", "cleared": 0}) + return + mp3_paths = glob.glob(os.path.join(audio_dir, "*.mp3")) + count = 0 + for path in mp3_paths: + try: + os.remove(path) + count += 1 + except OSError: + logger.exception(f"Failed to remove {path}") + logger.info(f"Cleared {count} audio files.") + self._json_response({"status": "ok", "cleared": count}) + + def _json_response(self, data): + """Send a JSON response.""" + body = json.dumps(data).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + def _generate_index(mp3_files, scripts_map): """Generate the HTML index page for the MP3 newscast.""" @@ -107,9 +143,12 @@ def _generate_index(mp3_files, scripts_map): .empty {{ text-align: center; color: #8b949e; padding: 3rem 0; }} .refresh-btn {{ background: #21262d; color: #c9d1d9; border: 1px solid #30363d; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.85rem; margin-bottom: 1.5rem; }} .refresh-btn:hover {{ background: #30363d; }} + .clear-btn {{ background: #490202; color: #f85149; border: 1px solid #f85149; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.85rem; margin-bottom: 1.5rem; }} + .clear-btn:hover {{ background: #da3633; color: #fff; }} @@ -118,6 +157,7 @@ def _generate_index(mp3_files, scripts_map):

Guardian Daily Newscast

Daily US news — spoken by AI

+ {audio_entries}