"""Consent/provenance sidecar field for voices (stored in .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": ""}