Files
UmbraAudio/umbraaudio/voice_maintenance.py
B.D. Murphy b09e324431 Multi-pass attribution + web voice-maintenance banner + qwen demo branding
Attribution (from prior session, now committed):
- llm_attributor: mechanical narration evidence, unnamed-role rules,
  wider context (12->20 lines, 8->40 utterances), uncertain flag,
  verify pass with 20-item batches and invented-name gate
- reattribute_all: --min-id/--max-id, --dry-run, --no-verify,
  --verify-rounds, qa_accepted skip guard, per-chapter elapsed times
- test_multipass_attribution: 19 tests

Web banner:
- voice_maintenance module: pid-liveness-checked marker file so a
  crashed run self-heals the banner away
- routes/base.html: amber banner on every page while the LLM
  re-attribution run has Qwen3-TTS offline
- reattribute_all: sets banner before switching models, clears in
  finally (also on failure)
- test_voice_maintenance: 8 tests

qwen3-tts submodule: UmbraAudio Voice Studio branding in web demo
(icon data-uri + favicon logo).
2026-09-22 19:12:50 -07:00

59 lines
1.9 KiB
Python

"""Web-UI maintenance banner state for long-running offline phases.
During the LLM speaker re-attribution run the Mac's Qwen3-TTS models must
stay unloaded (16 GB RAM), so voice re-generation is temporarily offline.
The run script records that state here, the web routes surface it through
a template global, and this module's pid-liveness check makes a crashed
run self-heal the banner away instead of lingering until a human notices.
"""
from __future__ import annotations
import contextlib
import json
import os
import time
from umbraaudio.config import PROJECT_BASE
MAINTENANCE_FILE = PROJECT_BASE / ".umbra" / "voice_maintenance.json"
def set_voice_maintenance(reason: str) -> None:
"""Record that voice re-generation is temporarily unavailable."""
payload = {
"pid": os.getpid(),
"started_at": int(time.time()),
"reason": reason,
}
MAINTENANCE_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp = MAINTENANCE_FILE.with_suffix(".json.tmp")
tmp.write_text(json.dumps(payload, indent=2))
tmp.replace(MAINTENANCE_FILE)
def clear_voice_maintenance() -> None:
"""Remove the maintenance marker so the banner disappears."""
with contextlib.suppress(FileNotFoundError):
MAINTENANCE_FILE.unlink()
def active_voice_maintenance() -> dict | None:
"""Return the marker dict while a live process owns it, else None.
The pid check means a run that died without clearing the file stops
showing the banner as soon as its process is gone.
"""
try:
with open(MAINTENANCE_FILE) as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError, OSError):
return None
pid = data.get("pid")
if not isinstance(pid, int) or pid <= 0:
return None
try:
os.kill(pid, 0)
except (ProcessLookupError, PermissionError):
return None
return data