carepath-api / scribe /training /scripts /run_pipeline.py
tranth3truong's picture
Deploy public Scribe-only CarePath Space
cc678b9
Raw
History Blame Contribute Delete
8.05 kB
"""CLI: run the DARAG pipeline end-to-end (or a single stage) for a profile.
# plumbing check (mock ASR, tiny limits, no models needed):
python scribe/training/scripts/run_pipeline.py --profile smoke --stage data
# real ViMedCSS run on a GPU box:
python scribe/training/scripts/run_pipeline.py --profile full --stage all
Stages: ``data`` (datastore + real pairs), ``synth`` (synthetic
transcripts + TTS + synthetic pairs + leakage), ``train`` (augment + QLoRA), and
``eval`` (LLM/RAG baseline + predict + tables + gate). Each stage is independently
runnable and resumable; ``all`` runs them in order. Paths and run-sizes come from
the versioned JSON run config, while ``gec.paths`` derives its artifact names.
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "scribe" / "training"))
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "scribe"))
from gec.cliutil import configure_stdout # noqa: E402
configure_stdout()
from gec.data import read_jsonl # noqa: E402
from gec.manifest import load_manifest, sha256_file # noqa: E402
from gec.paths import ArtifactPaths # noqa: E402
from gec.run_config import load_pipeline_config # noqa: E402
STAGES = ("all", "data", "synth", "train", "eval")
def run(args: list) -> None:
run_env = dict(os.environ)
run_env["PYTHONPATH"] = os.pathsep.join(("scribe/training", "scribe"))
run_env["PYTHONIOENCODING"] = "utf-8"
printable = " ".join(str(a) for a in args)
print("\n>>>", printable, flush=True)
proc = subprocess.run([sys.executable, *map(str, args)], env=run_env)
if proc.returncode != 0:
raise SystemExit(f"step failed ({proc.returncode}): {printable}")
def stage_data(p: ArtifactPaths, prof, dataset: str) -> None:
limit = str(prof.limit_per_split or 0)
run(["scribe/training/scripts/build_datastore.py", "--dataset", dataset,
"--limit-per-split", limit, "--output", str(p.datastore)])
run(["scribe/training/scripts/make_pairs.py", "--dataset", dataset, "--output", str(p.real_pairs),
"--asr-provider", prof.asr_provider, "--datastore", str(p.datastore),
"--retrieval-backend", prof.retrieval_backend, "--limit-per-split", limit,
"--n-best", str(prof.n_best), "--resume"])
def stage_synth(p: ArtifactPaths, prof) -> None:
count = prof.synth_count
if count is None: # paper nsyn = n: match the real train size
count = sum(1 for r in read_jsonl(p.real_pairs) if r.get("split") == "train") or 50
gen = ["scribe/training/scripts/gen_synthetic.py", "--pairs", str(p.real_pairs),
"--output", str(p.synth_clean), "--count", str(count)]
if prof.name != "smoke":
gen.append("--load-in-4bit")
run(gen)
tts = ["scribe/training/scripts/voice_clone_tts.py", "--input", str(p.synth_clean),
"--output", str(p.tts_manifest), "--provider", prof.tts_provider,
"--ref-dataset", "tensorxt/ViMedCSS", "--ref-count", "20", "--resume"]
if prof.synth_tts_limit:
tts += ["--limit", str(prof.synth_tts_limit)]
run(tts)
run(["scribe/training/scripts/make_synth_pairs.py", "--input", str(p.tts_manifest),
"--output", str(p.synth_pairs), "--datastore", str(p.datastore),
"--n-best", str(prof.n_best), "--resume"])
run(["scribe/training/scripts/check_leakage.py", "--synthetic", str(p.synth_clean),
"--real", str(p.real_pairs), "--output", str(p.leakage)])
def stage_train(p: ArtifactPaths, prof) -> None:
real_inputs = [str(p.real_pairs)]
# Learn real ASR confusions (paper Limitation #1) into the datastore, then
# refresh every pair's retrieved NEs so the RAC prompt carries the right term.
harvest_pairs = list(real_inputs)
if p.synth_pairs.exists():
harvest_pairs.append(str(p.synth_pairs))
run(["scribe/training/scripts/harvest_aliases.py", "--datastore", str(p.datastore),
"--pairs", *harvest_pairs, "--refresh", "--backend", prof.retrieval_backend])
run(["scribe/training/scripts/augment.py", "--real", *real_inputs, "--synthetic", str(p.synth_pairs),
"--output", str(p.augmented), "--nsyn-factor", str(prof.nsyn_factor)])
train = ["scribe/training/scripts/train.py", "--pairs", str(p.augmented), "--output-dir", str(p.adapters),
"--max-steps", str(prof.max_steps), "--seeds", *[str(s) for s in prof.seeds]]
train.append("--all-variants" if prof.all_variants else "--variant")
if not prof.all_variants:
train.append("full")
run(train)
def _full_adapter(p: ArtifactPaths, prof) -> str:
"""Path of the 'full' adapter under the variant/seed layout train wrote."""
adir = str(p.adapters)
if prof.all_variants:
adir = f"{adir}/full"
if len(prof.seeds) > 1:
adir = f"{adir}/seed-{prof.seeds[0]}"
return adir
def stage_eval(p: ArtifactPaths, prof, frozen_fixture: Path, frozen_manifest: Path) -> None:
run(["scribe/training/scripts/llm_rag_baseline.py", "--input", str(p.real_pairs), "--output", str(p.llm_rag)])
run(["scribe/training/scripts/predict.py", "--pairs", str(p.llm_rag), "--adapter-dir",
_full_adapter(p, prof), "--output", str(p.darag_preds), "--column", "gec_pred"])
run(["scribe/training/scripts/evaluate.py", "--input", str(p.darag_preds), "--prediction-columns",
"raw_asr", "corrected_text", "gec_pred", "--wer-output", str(p.darag_wer),
"--ne-f1-output", str(p.darag_ne_f1), "--stratified-output", str(p.darag_stratified)])
run(["scribe/training/scripts/gate.py", "--report", str(p.darag_wer)])
frozen = load_manifest(frozen_manifest)
if sha256_file(frozen_fixture) != frozen["sha256"]:
raise ValueError("frozen evaluation fixture hash does not match its manifest")
run(["scribe/training/scripts/predict.py", "--pairs", str(frozen_fixture), "--adapter-dir",
_full_adapter(p, prof), "--output", str(p.frozen_preds), "--column", "gec_pred"])
run(["scribe/training/scripts/evaluate.py", "--input", str(p.frozen_preds), "--prediction-columns",
"raw_asr", "gec_pred", "--wer-output", str(p.frozen_wer), "--ne-f1-output",
str(p.frozen_ne_f1), "--stratified-output", str(p.frozen_stratified)])
run(["scribe/training/scripts/gate.py", "--report", str(p.frozen_wer), "--candidate", "gec_pred",
"--baselines", "raw_asr", "--splits", "frozen", "--safety-report",
str(p.frozen_stratified)])
run(["scribe/training/scripts/export_serve.py", "--adapter-dir", _full_adapter(p, prof),
"--datastore", str(p.datastore), "--output", str(p.serve_bundle),
"--gate-report", str(p.darag_wer)])
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--profile", default="smoke", choices=["smoke", "full"])
parser.add_argument("--config", type=Path, help="versioned JSON run config")
parser.add_argument("--stage", default="all", choices=list(STAGES))
args = parser.parse_args()
config_path = args.config or Path("scribe/training/configs") / f"{args.profile}-v1.json"
config = load_pipeline_config(config_path)
prof = config.profile
paths = ArtifactPaths(root=config.artifact_root, suffix=config.suffix)
wanted = STAGES[1:] if args.stage == "all" else [args.stage]
if "train" in wanted:
load_manifest(config.manifest, require_approved=True)
for stage in wanted:
print(f"\n===== STAGE: {stage} (run={config.run_id}, profile={prof.name}) =====")
if stage == "data":
stage_data(paths, prof, config.dataset)
elif stage == "synth":
stage_synth(paths, prof)
elif stage == "train":
stage_train(paths, prof)
elif stage == "eval":
stage_eval(paths, prof, config.frozen_eval_fixture, config.frozen_eval_manifest)
print("\nPipeline stage(s) complete:", ", ".join(wanted))
if __name__ == "__main__":
main()