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
+17 -15
View File
@@ -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.
+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")
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:
+40
View File
@@ -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; }}
</style>
<script>
function refresh() {{ location.reload(); }}
function clearAudio() {{ fetch('/clear', {{ method: 'POST' }}).then(() => location.reload()); }}
setInterval(refresh, 300000);
</script>
</head>
@@ -118,6 +157,7 @@ def _generate_index(mp3_files, scripts_map):
<h1>Guardian Daily Newscast</h1>
<p class="subtitle">Daily US news — spoken by AI</p>
<button class="refresh-btn" onclick="refresh()">Refresh</button>
<button class="clear-btn" onclick="clearAudio()">Clear Audio</button>
{audio_entries}
</div>
</body>