fixes for the service layer. Updated readme.

This commit is contained in:
2026-06-13 21:29:25 -07:00
parent bfad71e96a
commit e9ff109f0a
3 changed files with 71 additions and 16 deletions
+18 -16
View File
@@ -215,18 +215,24 @@ docker tag guardian-newscast truenas.local:30095/guardian-newscast:latest
docker push 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 ```bash
docker pull truenas.local:30095/guardian-newscast:latest docker buildx build --platform linux/amd64 -t guardian-newscast:amd64 .
docker run -d \ ```
--name guardian-newscast \
--network host \ For ARM (aarch64 / Apple Silicon):
-v /path/to/data:/app/data \
-e OLLAMA_URL=http://127.0.0.1:11434/api/generate \ ```bash
-e PORT=30095 \ docker buildx build --platform linux/arm64 -t guardian-newscast:arm64 .
--restart unless-stopped \ ```
truenas.local:30095/guardian-newscast:latest
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 ### Docker Compose
@@ -242,14 +248,10 @@ docker compose --profile full up -d
### GPU Acceleration ### GPU Acceleration
The default Dockerfile uses `python:3.11-slim` (Debian), which has glibc — fully compatible with NVIDIA CUDA. The default Dockerfile uses `python:3.11-slim` (Debian), which has glibc — fully compatible with NVIDIA CUDA.
1. Build with CUDA-enabled torch:
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:
```bash ```bash
docker build --target gpu -t guardian-newscast:gpu . 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`) (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`. 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.
+13
View File
@@ -37,8 +37,21 @@ from server.app import run_server as start_server
logger.add("pipeline.log", rotation="5 MB", retention="7 days") 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: async def run_pipeline() -> bool:
"""Run all three pipeline stages sequentially. Returns True on success.""" """Run all three pipeline stages sequentially. Returns True on success."""
clear_json_data()
logger.info("=" * 60) logger.info("=" * 60)
logger.info("Stage 1: Scraping Guardian articles...") logger.info("Stage 1: Scraping Guardian articles...")
try: try:
+40
View File
@@ -1,5 +1,6 @@
"""HTTP server that serves the Guardian Daily Newscast MP3 index page and audio files.""" """HTTP server that serves the Guardian Daily Newscast MP3 index page and audio files."""
import glob
import os import os
import json import json
import threading import threading
@@ -65,6 +66,41 @@ class NewscastHandler(SimpleHTTPRequestHandler):
"""Suppress per-request logs to avoid noisy output.""" """Suppress per-request logs to avoid noisy output."""
pass 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): def _generate_index(mp3_files, scripts_map):
"""Generate the HTML index page for the MP3 newscast.""" """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; }} .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 {{ 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; }} .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; }}
</style> </style>
<script> <script>
function refresh() {{ location.reload(); }} function refresh() {{ location.reload(); }}
function clearAudio() {{ fetch('/clear', {{ method: 'POST' }}).then(() => location.reload()); }}
setInterval(refresh, 300000); setInterval(refresh, 300000);
</script> </script>
</head> </head>
@@ -118,6 +157,7 @@ def _generate_index(mp3_files, scripts_map):
<h1>Guardian Daily Newscast</h1> <h1>Guardian Daily Newscast</h1>
<p class="subtitle">Daily US news — spoken by AI</p> <p class="subtitle">Daily US news — spoken by AI</p>
<button class="refresh-btn" onclick="refresh()">Refresh</button> <button class="refresh-btn" onclick="refresh()">Refresh</button>
<button class="clear-btn" onclick="clearAudio()">Clear Audio</button>
{audio_entries} {audio_entries}
</div> </div>
</body> </body>