Spaces:
Running on Zero
Running on Zero
File size: 7,254 Bytes
a22c748 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | #!/usr/bin/env python3
"""JSON-manifest MAPS alignment for inpaint_pipeline.py --maps.
Reads segment transcript JSON (``02_transcripts.json``), runs MAPS in the
active PhonePaint interpreter (no conda hop, no TensorFlow), and writes
per-segment alignments with PhonePaint ``canonical`` labels.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from textgrid import IntervalTier, TextGrid
MAPS_ROOT = Path(__file__).resolve().parent
REPO_ROOT = MAPS_ROOT.parents[1]
sys.path.insert(0, str(MAPS_ROOT))
sys.path.insert(0, str(REPO_ROOT))
from phone_labels import canonical_phone_label # noqa: E402
from maps_torch.align_runtime import ( # noqa: E402
align_wav,
load_models,
resolve_device,
)
from maps_torch.alignment import load_dictionary # noqa: E402
DEFAULT_MAPS_CKPT = MAPS_ROOT / "torch_models" / "timbuck_eng.pt"
DEFAULT_MAPS_ENSEMBLE = MAPS_ROOT / "torch_models" / "ensemble_model"
_BUNDLED_CMUDICT = MAPS_ROOT / "nltk_data" / "corpora" / "cmudict" / "cmudict"
_NLTK_CMUDICT = Path.home() / "nltk_data" / "corpora" / "cmudict" / "cmudict"
def _default_cmudict() -> Path:
env_value = os.environ.get("PHONEPAINT_CMUDICT")
if env_value:
env_path = Path(env_value).expanduser()
if env_path.is_file():
return env_path
# A stale env var (e.g. an unmounted drive) must not break a good checkout.
print(
f"PHONEPAINT_CMUDICT={env_value} is not a file — ignoring it.",
file=sys.stderr,
)
for candidate in (_BUNDLED_CMUDICT, _NLTK_CMUDICT):
if candidate.is_file():
return candidate
return _BUNDLED_CMUDICT
DEFAULT_MAPS_DICT = _default_cmudict()
def _pick_tiers(tg: TextGrid) -> tuple[IntervalTier | None, IntervalTier | None]:
phones_tier = None
words_tier = None
for tier in tg.tiers:
if not isinstance(tier, IntervalTier):
continue
lname = (tier.name or "").lower()
if "phone" in lname or "segment" in lname:
phones_tier = tier
if "word" in lname:
words_tier = tier
return phones_tier, words_tier
def _tier_to_dict(tier: IntervalTier | None, *, phones: bool) -> dict[str, dict]:
out: dict[str, dict] = {}
if tier is None:
return out
for interval in tier:
text = (interval.mark or "").strip()
if not text:
continue
start = round(float(interval.minTime), 3)
end = round(float(interval.maxTime), 3)
if end <= start:
continue
item: dict = {"xmin": start, "xmax": end, "text": text}
if phones:
item["canonical"] = canonical_phone_label(text)
item["inventory"] = "maps-timit61"
out[str(len(out))] = item
return out
def _textgrid_to_alignment(tg: TextGrid) -> dict:
phones_tier, words_tier = _pick_tiers(tg)
phones = _tier_to_dict(phones_tier, phones=True)
if not phones:
raise RuntimeError("MAPS produced no phone intervals")
return {
"words": _tier_to_dict(words_tier, phones=False),
"phones": phones,
"alignment": {
"backend": "maps",
"inventory": "maps-timit61",
},
}
def _load_transcripts(path: Path) -> dict[str, str]:
raw = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raise TypeError(f"Expected transcript object in {path}")
texts: dict[str, str] = {}
for name, entry in raw.items():
if isinstance(entry, dict):
text = str(entry.get("text") or "").strip()
else:
text = str(entry or "").strip()
if not text:
raise ValueError(f"Missing transcript text for {name} in {path}")
texts[str(name)] = text
return texts
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--audio_dir", type=Path, required=True)
ap.add_argument("--input_json", type=Path, required=True)
ap.add_argument("--output_json", type=Path, required=True)
ap.add_argument(
"--model",
type=Path,
default=None,
help="Single .pt checkpoint or ensemble directory (default: timbuck_eng.pt)",
)
ap.add_argument(
"--dict",
dest="dictionary",
type=Path,
default=None,
help=f"CMUdict path (default: PHONEPAINT_CMUDICT or {DEFAULT_MAPS_DICT})",
)
ap.add_argument(
"--device",
default="auto",
choices=("auto", "cpu", "cuda"),
help="Torch device for MAPS acoustic model (default: auto)",
)
ap.add_argument(
"--no-sil",
action="store_true",
help="Disable silence padding at transcript ends",
)
ap.add_argument(
"--no-interp",
action="store_true",
help="Disable boundary interpolation",
)
ap.add_argument(
"--check-variants",
action="store_true",
help="Evaluate pronunciation variants (slow)",
)
ap.add_argument(
"--ensemble",
action="store_true",
help="Use tools/maps/torch_models/ensemble_model/ (10 checkpoints, median boundaries)",
)
args = ap.parse_args()
model_path = args.model
if model_path is None:
model_path = DEFAULT_MAPS_ENSEMBLE if args.ensemble else DEFAULT_MAPS_CKPT
model_path = model_path.expanduser().resolve()
if not model_path.exists():
raise SystemExit(
f"MAPS checkpoint not found: {model_path}\n"
"Run tools/maps/sync_checkpoints.sh or copy torch_models/ from ~/MAPS."
)
dict_path = (args.dictionary or DEFAULT_MAPS_DICT).expanduser().resolve()
if not dict_path.is_file():
raise SystemExit(
f"CMUdict not found: {dict_path}\n"
"Set PHONEPAINT_CMUDICT or pass --dict."
)
device = resolve_device(args.device)
print(f"MAPS device: {device}", flush=True)
models = load_models(model_path, device)
print(f"Loaded {len(models)} MAPS checkpoint(s) from {model_path}", flush=True)
word2phone = load_dictionary(dict_path)
audio_dir = args.audio_dir.expanduser().resolve()
transcripts = _load_transcripts(args.input_json.expanduser().resolve())
aligned: dict[str, dict] = {}
for name, text in transcripts.items():
wav_path = audio_dir / name
if not wav_path.is_file():
raise FileNotFoundError(f"Segment wav not found: {wav_path}")
print(f"MAPS align: {name}", flush=True)
tg = align_wav(
wav_path,
text,
models=models,
word2phone=word2phone,
device=device,
add_sil=not args.no_sil,
use_interp=not args.no_interp,
check_variants=args.check_variants,
)
aligned[name] = _textgrid_to_alignment(tg)
args.output_json.parent.mkdir(parents=True, exist_ok=True)
args.output_json.write_text(
json.dumps(aligned, indent=2, ensure_ascii=False),
encoding="utf-8",
)
print(f"Wrote {args.output_json} ({len(aligned)} segment(s))", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|