tts-voice-creator-clone-and.../core/provenance.py
mARTin-B78 781bf6e7f2 feat(voices): consent and provenance field per voice
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:39:32 +02:00

56 lines
2.4 KiB
Python

"""Consent/provenance sidecar field for voices (stored in <voice>.meta.json)."""
from __future__ import annotations
import re
from datetime import datetime, timezone
from urllib.parse import urlsplit
BASES = ("own_voice", "consented", "licensed", "public_domain", "unknown")
CONSENT_EXTS = {".webm", ".ogg", ".opus", ".wav", ".mp3", ".m4a"}
CONSENT_MIME = {".webm": "audio/webm", ".ogg": "audio/ogg", ".opus": "audio/ogg", ".wav": "audio/wav",
".mp3": "audio/mpeg", ".m4a": "audio/mp4"}
MAX_CONSENT_BYTES = 10 * 1024 * 1024
_LIMITS = {"source": 500, "licence": 300, "note": 1000}
_CTRL = re.compile(r"[\x00-\x08\x0b-\x1f\x7f]")
def _clean(value, limit: int) -> str:
if value is None:
return ""
if not isinstance(value, str):
raise ValueError("Provenance text fields must be strings")
value = _CTRL.sub("", value).strip()
if len(value) > limit:
raise ValueError(f"Provenance text is too long (max {limit})")
return value
def validate_provenance(data: dict, previous: dict | None = None) -> dict:
"""Return a clean provenance dict or raise ValueError (-> HTTP 400)."""
if not isinstance(data, dict):
raise ValueError("Provenance must be an object")
basis = data.get("basis", "unknown")
if basis not in BASES:
raise ValueError("basis must be one of: " + ", ".join(BASES))
out = {"basis": basis}
for field, limit in _LIMITS.items():
out[field] = _clean(data.get(field), limit)
src = out["source"]
if re.match(r"^[a-z][a-z0-9+.-]*:", src, re.I): # looks like a URL: http(s) only, no credentials
parts = urlsplit(src)
if parts.scheme.lower() not in {"http", "https"} or not parts.hostname or parts.username or parts.password:
raise ValueError("source URL must be http(s) without credentials")
prev = previous or {}
if prev.get("recorded_consent"): # only the consent routes change the recording
out["recorded_consent"] = prev["recorded_consent"]
out["set_at"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
return out
def import_provenance(source_url: str) -> dict:
"""Prefill for imports from a known source; basis stays unknown until a human decides."""
try:
return validate_provenance({"basis": "unknown", "source": source_url})
except ValueError:
return {"basis": "unknown", "source": "", "licence": "", "note": ""}