Download scripts/run_baselines.py from suvradeepp/tiny-hinglish-turn-detector: direct link, hf CLI and curl.
- Browser
- Download file 9.63 kB
-
https://huggingface.co/suvradeepp/tiny-hinglish-turn-detector/resolve/main/scripts/run_baselines.py
- Command line
-
hf download hf://suvradeepp/tiny-hinglish-turn-detector/scripts/run_baselines.py
-
curl -L -o run_baselines.py https://huggingface.co/suvradeepp/tiny-hinglish-turn-detector/resolve/main/scripts/run_baselines.py
9.63 kB
| #!/usr/bin/env python3 | |
| """Train the interpretable acoustic baseline or sweep fixed silence policies.""" | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| import sys | |
| from dataclasses import replace | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| SRC = ROOT / "src" | |
| if str(SRC) not in sys.path: | |
| sys.path.insert(0, str(SRC)) | |
| def _path(value: str) -> Path: | |
| candidate = Path(value) | |
| return candidate if candidate.is_absolute() else ROOT / candidate | |
| def _sha256(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as handle: | |
| for block in iter(lambda: handle.read(1024 * 1024), b""): | |
| digest.update(block) | |
| return digest.hexdigest() | |
| def _portable_path(path: Path) -> str: | |
| """Prefer a repository-relative path without rejecting external outputs.""" | |
| resolved = path.resolve() | |
| try: | |
| return resolved.relative_to(ROOT.resolve()).as_posix() | |
| except ValueError: | |
| return str(resolved) | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| subparsers = parser.add_subparsers(dest="command", required=True) | |
| audio = subparsers.add_parser("audio", help="fit waveform-statistics logistic regression") | |
| audio.add_argument("--manifest", default="data/processed/partial-iid-splits.jsonl") | |
| audio.add_argument( | |
| "--source-root", | |
| default=".", | |
| help="base for source_file paths stored in the audited manifest", | |
| ) | |
| audio.add_argument("--train-split", default="train") | |
| audio.add_argument("--validation-split", default="validation") | |
| audio.add_argument( | |
| "--revision", | |
| default="e564e2ac567f774d1880aa1db6ce97afb8c519b7", | |
| help="immutable upstream revision represented by the manifest", | |
| ) | |
| audio.add_argument("--output", default="artifacts/partial-baseline") | |
| audio.add_argument( | |
| "--summary-report", | |
| help="optional second location for the compact metrics JSON (for tracked reports)", | |
| ) | |
| audio.add_argument("--max-examples", type=int, help="debug cap per split") | |
| audio.add_argument("--max-seconds", type=float, default=4.0) | |
| audio.add_argument("--epochs", type=int, default=800) | |
| audio.add_argument("--learning-rate", type=float, default=0.05) | |
| audio.add_argument("--l2", type=float, default=1e-3) | |
| audio.add_argument("--fpr-budget", type=float, default=0.02) | |
| timeout = subparsers.add_parser("timeouts", help="evaluate fixed VAD silence timeouts") | |
| timeout.add_argument("--input", required=True, help="PauseCheckpoint JSONL") | |
| timeout.add_argument("--output", required=True) | |
| timeout.add_argument( | |
| "--timeouts-ms", | |
| default="200,400,600,800,1200", | |
| help="comma-separated timeout policies", | |
| ) | |
| return parser.parse_args() | |
| def _audio(args: argparse.Namespace) -> int: | |
| from turn_detection.baselines import extract_manifest_features, fit_logistic_baseline | |
| from turn_detection.data import read_manifest, write_json, write_manifest | |
| from turn_detection.training.metrics import ( | |
| binary_classification_metrics, | |
| reliability_bins, | |
| threshold_at_max_fpr, | |
| ) | |
| manifest = _path(args.manifest) | |
| source_root = _path(args.source_root) | |
| output = _path(args.output) | |
| output.mkdir(parents=True, exist_ok=True) | |
| all_rows = list(read_manifest(manifest)) | |
| train_rows = [row for row in all_rows if row.get("split") == args.train_split] | |
| validation_rows = [row for row in all_rows if row.get("split") == args.validation_split] | |
| if not train_rows or not validation_rows: | |
| raise SystemExit("both requested train and validation splits must be non-empty") | |
| train_groups = {row.get("group_id") for row in train_rows if row.get("group_id")} | |
| validation_groups = {row.get("group_id") for row in validation_rows if row.get("group_id")} | |
| overlap = train_groups & validation_groups | |
| if overlap: | |
| raise SystemExit(f"refusing leaky manifest: {len(overlap)} group IDs cross splits") | |
| all_groups = train_groups | validation_groups | |
| observed_group_rows: dict[str, int] = {} | |
| for row in all_rows: | |
| group_id = row.get("group_id") | |
| if group_id: | |
| observed_group_rows[str(group_id)] = observed_group_rows.get(str(group_id), 0) + 1 | |
| revisions = { | |
| str(row["source_revision"]) for row in all_rows if row.get("source_revision") is not None | |
| } | |
| train_x, train_y, train_info = extract_manifest_features( | |
| train_rows, | |
| source_root=source_root, | |
| max_examples=args.max_examples, | |
| max_seconds=args.max_seconds, | |
| ) | |
| validation_x, validation_y, validation_info = extract_manifest_features( | |
| validation_rows, | |
| source_root=source_root, | |
| max_examples=args.max_examples, | |
| max_seconds=args.max_seconds, | |
| ) | |
| model = fit_logistic_baseline( | |
| train_x, | |
| train_y, | |
| epochs=args.epochs, | |
| learning_rate=args.learning_rate, | |
| l2=args.l2, | |
| ) | |
| validation_probabilities = model.predict_proba(validation_x) | |
| selected = threshold_at_max_fpr( | |
| validation_y.tolist(), | |
| validation_probabilities.tolist(), | |
| max_false_positive_rate=args.fpr_budget, | |
| ) | |
| model = replace(model, threshold=float(selected["threshold"])) | |
| train_probabilities = model.predict_proba(train_x) | |
| model_path = output / "model.json" | |
| write_json(model_path, model.to_dict()) | |
| report = { | |
| "status": "development_only", | |
| "scope": "audited local training shard; official test remains sealed", | |
| "manifest": args.manifest, | |
| "manifest_sha256": _sha256(manifest), | |
| "model": { | |
| "path": _portable_path(model_path), | |
| "bytes": model_path.stat().st_size, | |
| "sha256": _sha256(model_path), | |
| }, | |
| "data_revision": next(iter(revisions)) if len(revisions) == 1 else args.revision, | |
| "source_root": args.source_root, | |
| "train_split": args.train_split, | |
| "validation_split": args.validation_split, | |
| "train_examples": int(len(train_y)), | |
| "validation_examples": int(len(validation_y)), | |
| "group_overlap_count": 0, | |
| "group_count": len(all_groups), | |
| "largest_group_rows": max(observed_group_rows.values(), default=0), | |
| "grouping_note": ( | |
| "All observed groups are singleton rows; speaker/conversation/voice identity " | |
| "separation is not established." | |
| if observed_group_rows and max(observed_group_rows.values()) == 1 | |
| else "Groups contain repeated observed linkage keys." | |
| ), | |
| "hyperparameters": { | |
| "epochs": args.epochs, | |
| "learning_rate": args.learning_rate, | |
| "l2": args.l2, | |
| "max_seconds": args.max_seconds, | |
| "fpr_budget": args.fpr_budget, | |
| }, | |
| "threshold_selection": selected, | |
| "train_metrics_at_validation_threshold": binary_classification_metrics( | |
| train_y.tolist(), train_probabilities.tolist(), model.threshold | |
| ), | |
| "validation_metrics": binary_classification_metrics( | |
| validation_y.tolist(), validation_probabilities.tolist(), model.threshold | |
| ), | |
| "validation_reliability": reliability_bins( | |
| validation_y.tolist(), validation_probabilities.tolist() | |
| ), | |
| "limitations": [ | |
| "One of 83 upstream training shards was locally available.", | |
| "This is an interpretable sanity baseline, not the submitted neural model.", | |
| "The threshold was selected on this validation split and must not be tuned on test.", | |
| ], | |
| } | |
| write_json(output / "metrics.json", report) | |
| if args.summary_report: | |
| write_json(_path(args.summary_report), report) | |
| predictions = [] | |
| for info, probability in zip(validation_info, validation_probabilities.tolist(), strict=True): | |
| predictions.append( | |
| { | |
| **info, | |
| "probability": float(probability), | |
| "prediction": int(probability >= model.threshold), | |
| "threshold": model.threshold, | |
| } | |
| ) | |
| write_manifest(output / "validation_predictions.jsonl", predictions) | |
| # Keep an auditable list of the exact training IDs without copying raw audio. | |
| write_manifest(output / "train_examples.jsonl", train_info) | |
| print( | |
| json.dumps({"model": model.to_dict(), "validation": report["validation_metrics"]}, indent=2) | |
| ) | |
| return 0 | |
| def _timeouts(args: argparse.Namespace) -> int: | |
| from turn_detection.baselines import ( | |
| fixed_timeout_sweep, | |
| load_checkpoints_jsonl, | |
| ) | |
| from turn_detection.data import write_json | |
| try: | |
| timeouts = [float(value.strip()) for value in args.timeouts_ms.split(",") if value.strip()] | |
| except ValueError as exc: | |
| raise SystemExit("--timeouts-ms must contain comma-separated numbers") from exc | |
| if not timeouts: | |
| raise SystemExit("provide at least one fixed timeout") | |
| checkpoints = load_checkpoints_jsonl(_path(args.input)) | |
| results = fixed_timeout_sweep(checkpoints, timeouts) | |
| output = _path(args.output) | |
| write_json( | |
| output, | |
| { | |
| "baseline": "fixed_silence_timeout", | |
| "checkpoint_source": args.input, | |
| "results": results, | |
| }, | |
| ) | |
| print(json.dumps(results, indent=2)) | |
| return 0 | |
| def main() -> int: | |
| args = parse_args() | |
| return _audio(args) if args.command == "audio" else _timeouts(args) | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |