feat(audio-tools): ✨ Introduce audio validation and subscription-friendly audio splitting utilities
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
8f7e0606ce
commit
5e92fb313d
2 changed files with 136 additions and 10 deletions
78
tools/audio-split-to-subscription.py
Normal file
78
tools/audio-split-to-subscription.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env python3
|
||||
"""One-shot migration: split a per-theme audio.json into the subscription-
|
||||
pattern triple used everywhere else in the codebase.
|
||||
|
||||
Before:
|
||||
public/games/<theme>/data/audio.json
|
||||
{schema_version, sfx: {...}, music: {tracks: [...], victory_pool, defeat_pool,
|
||||
default_track_id, crossfade_seconds}}
|
||||
|
||||
After:
|
||||
public/resources/audio/library.json # shared catalogue
|
||||
{schema_version, sfx: {...}, music: {tracks: [...]}}
|
||||
public/games/<theme>/data/audio/manifest.json # subscription
|
||||
{source: "resources/audio", includes: true, overrides: {}}
|
||||
public/games/<theme>/data/audio/pools.json # per-game routing
|
||||
{default_track_id, crossfade_seconds, victory_pool, defeat_pool}
|
||||
|
||||
The migration assumes the current single audio.json IS the only library —
|
||||
i.e. all entries are subscribed by Game 1. Future games author their own
|
||||
manifest.json with `includes: [whitelist]` or overrides.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
THEME = "age-of-dwarves"
|
||||
SRC = REPO / "public" / "games" / THEME / "data" / "audio.json"
|
||||
LIBRARY = REPO / "public" / "resources" / "audio" / "library.json"
|
||||
DEST_DIR = REPO / "public" / "games" / THEME / "data" / "audio"
|
||||
MANIFEST = DEST_DIR / "manifest.json"
|
||||
POOLS = DEST_DIR / "pools.json"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with open(SRC) as f:
|
||||
old = json.load(f)
|
||||
sfx: dict = old["sfx"]
|
||||
music: dict = old["music"]
|
||||
tracks: list = music.get("tracks", [])
|
||||
|
||||
# Library: schema_version + sfx + music tracks (no per-theme routing).
|
||||
library = {
|
||||
"schema_version": int(old.get("schema_version", 2)),
|
||||
"sfx": sfx,
|
||||
"music": {"tracks": tracks},
|
||||
}
|
||||
|
||||
# Manifest: subscription. Game 1 takes everything.
|
||||
manifest = {
|
||||
"source": "resources/audio",
|
||||
"includes": True,
|
||||
"overrides": {},
|
||||
}
|
||||
|
||||
# Pools: per-theme routing. Pull every per-game-only field out of
|
||||
# the music block.
|
||||
pools: dict = {}
|
||||
for k in ("default_track_id", "crossfade_seconds", "victory_pool",
|
||||
"defeat_pool"):
|
||||
if k in music:
|
||||
pools[k] = music[k]
|
||||
|
||||
DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LIBRARY.parent.mkdir(parents=True, exist_ok=True)
|
||||
LIBRARY.write_text(json.dumps(library, indent=2) + "\n")
|
||||
MANIFEST.write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
POOLS.write_text(json.dumps(pools, indent=2) + "\n")
|
||||
SRC.unlink()
|
||||
print(f"library: {LIBRARY} ({len(sfx)} sfx, {len(tracks)} tracks)")
|
||||
print(f"manifest: {MANIFEST}")
|
||||
print(f"pools: {POOLS}")
|
||||
print(f"removed: {SRC}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -189,20 +189,68 @@ def check_assets(theme: str, refs: set[str], report: Report) -> None:
|
|||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _read_json(path: Path, theme: str, what: str, report: Report) -> dict:
|
||||
if not path.exists():
|
||||
report.fail(f"{theme}: {what} not found at {path}")
|
||||
return {}
|
||||
try:
|
||||
return json.loads(path.read_text())
|
||||
except json.JSONDecodeError as e:
|
||||
report.fail(f"{theme}: {what} JSON parse error: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def _resolve_manifest(library: dict, manifest: dict) -> dict:
|
||||
"""Mirror AudioManager._apply_subscription: filter library by includes,
|
||||
merge per-key overrides. Returns a synthesized full manifest dict in
|
||||
the legacy shape that `validate_schema` and `collect_referenced_streams`
|
||||
already understand.
|
||||
"""
|
||||
includes = manifest.get("includes", True)
|
||||
overrides = manifest.get("overrides", {}) or {}
|
||||
|
||||
def included(key: str) -> bool:
|
||||
if isinstance(includes, bool):
|
||||
return includes
|
||||
if isinstance(includes, list):
|
||||
return key in includes
|
||||
return False
|
||||
|
||||
out: dict = {"schema_version": library.get("schema_version", 2),
|
||||
"sfx": {}, "music": {"tracks": []}}
|
||||
for key, entry in (library.get("sfx", {}) or {}).items():
|
||||
if not included(key):
|
||||
continue
|
||||
merged = dict(entry)
|
||||
if key in overrides and isinstance(overrides[key], dict):
|
||||
merged.update(overrides[key])
|
||||
out["sfx"][key] = merged
|
||||
for track in (library.get("music", {}) or {}).get("tracks", []) or []:
|
||||
if not isinstance(track, dict):
|
||||
continue
|
||||
tid = track.get("id", "")
|
||||
if not tid or not included(tid):
|
||||
continue
|
||||
merged = dict(track)
|
||||
if tid in overrides and isinstance(overrides[tid], dict):
|
||||
merged.update(overrides[tid])
|
||||
out["music"]["tracks"].append(merged)
|
||||
return out
|
||||
|
||||
|
||||
def validate_theme(theme: str) -> Report:
|
||||
report = Report(errors=[], warnings=[])
|
||||
manifest_path = REPO / "public" / "games" / theme / "data" / "audio.json"
|
||||
if not manifest_path.exists():
|
||||
report.fail(f"{theme}: audio.json not found at {manifest_path}")
|
||||
return report
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
except json.JSONDecodeError as e:
|
||||
report.fail(f"{theme}: JSON parse error: {e}")
|
||||
library_path = REPO / "public" / "resources" / "audio" / "library.json"
|
||||
manifest_path = REPO / "public" / "games" / theme / "data" / "audio" / "manifest.json"
|
||||
|
||||
library = _read_json(library_path, theme, "audio library.json", report)
|
||||
manifest = _read_json(manifest_path, theme, "audio manifest.json", report)
|
||||
if report.errors:
|
||||
return report
|
||||
|
||||
validate_schema(manifest, report)
|
||||
refs = collect_referenced_streams(manifest)
|
||||
resolved = _resolve_manifest(library, manifest)
|
||||
validate_schema(resolved, report)
|
||||
refs = collect_referenced_streams(resolved)
|
||||
check_assets(theme, refs, report)
|
||||
return report
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue