"""Provenance tags for exported generated audio (tier 1) and the AudioSeal stub (tier 2). Tags are written with one ffmpeg stream copy (no re-encode) into a COPY of the MP3; the stored article audio is never modified. Tags never contain paths, user names or secrets; the voice is included only when the caller opts in. See architecture/watermarking.md. """ from __future__ import annotations import json import re import subprocess from datetime import datetime, timezone from pathlib import Path GENERATOR = "tts-voice-creator-clone-and-design-2" _KEYS = ("synthetic_speech", "generator", "generator_version", "generated_date", "voice") _CTRL = re.compile(r"[\x00-\x1f\x7f]") def build_tags(app_version: str, voice: str = "", include_voice: bool = False, today: str = "") -> dict[str, str]: tags = { "synthetic_speech": "true", "generator": GENERATOR, "generator_version": _CTRL.sub("", str(app_version))[:32], "generated_date": today or datetime.now(timezone.utc).strftime("%Y-%m-%d"), } if include_voice and voice: tags["voice"] = _CTRL.sub("", Path(str(voice)).name)[:80] # name only, never a path tags["comment"] = f"Synthetic speech (AI-generated) by {GENERATOR} {tags['generator_version']}" return tags def apply_tags(src: Path, dst: Path, tags: dict[str, str]) -> Path: """Copy src to dst (stream copy) with the tags; raises RuntimeError on ffmpeg failure.""" cmd = ["ffmpeg", "-y", "-v", "error", "-i", str(src), "-map", "0:a", "-map_metadata", "0", "-c", "copy", "-id3v2_version", "3"] for k, v in tags.items(): cmd += ["-metadata", f"{k}={v}"] cmd.append(str(dst)) r = subprocess.run(cmd, capture_output=True, timeout=120) if r.returncode != 0: raise RuntimeError("tagging failed") # ffmpeg stderr may contain paths: not forwarded return dst def read_tags(path: Path) -> dict[str, str]: """Provenance tags found in an audio file ({} if none). Never raises.""" try: r = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format_tags", "-of", "json", str(path)], capture_output=True, timeout=30) found = (json.loads(r.stdout or b"{}").get("format") or {}).get("tags") or {} except (OSError, ValueError, subprocess.SubprocessError): return {} low = {str(k).lower(): str(v) for k, v in found.items()} return {k: low[k] for k in _KEYS if k in low} def watermark(path: Path) -> Path: """Tier 2 (AudioSeal) is not installed in this image: always reports unavailable.""" raise NotImplementedError("Inaudible watermarking (AudioSeal) is not available in this build") def watermark_available() -> bool: return False if __name__ == "__main__": # verify CLI: python -m core.export_provenance file.mp3 import sys found = read_tags(Path(sys.argv[1])) if len(sys.argv) == 2 else {} print(json.dumps(found, indent=2)) sys.exit(0 if found.get("synthetic_speech") == "true" else 1)