56 lines
2.4 KiB
Python
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": ""}
|