Spaces:
Runtime error
Runtime error
File size: 6,664 Bytes
aacc29a | 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 | """CLI: bundle a gated GEC adapter into a self-describing serve package.
The serving API must not import ``gec.*`` (research/serving stay
decoupled). So this offline script freezes everything serving needs into a
``serve_manifest.json``: base model, the variant's ``use_retrieval`` flag, and
the DARAG prompt strings. The bundle also carries the adapter, its tokenizer,
and the enriched datastore, so a server only needs the bundle directory.
python scribe/training/scripts/export_serve.py \
--adapter-dir artifacts/gec_lora/qwen3/full/seed-13 \
--datastore artifacts/retrieval/term_datastore.json \
--output artifacts/gec_serve
Set ``LLM_PROVIDER=gec_local`` and ``GEC_BUNDLE_PATH=<output>`` to serve it.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
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.config import FALLBACK_BASE_MODEL # noqa: E402
from gec.gate import run_gate # noqa: E402
from gec.prompts import IM_END, IM_START, SYSTEM_PROMPT # noqa: E402
def _read_variant(adapter_dir: Path) -> dict:
marker = adapter_dir / "darag_variant.json"
if marker.exists():
try:
return json.loads(marker.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
pass
return {"variant": "full", "use_retrieval": True}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--adapter-dir", required=True, help="trained LoRA adapter directory")
parser.add_argument("--datastore", required=True, help="enriched term datastore JSON")
parser.add_argument("--output", default="artifacts/gec_serve", help="bundle output dir")
parser.add_argument(
"--base-model",
default=None,
help="optional expected model; actual export identity comes from the adapter revision lock",
)
parser.add_argument("--fallback-base-model", default=FALLBACK_BASE_MODEL)
parser.add_argument("--max-new-tokens", type=int, default=256)
parser.add_argument("--normal-gate-report", required=True)
parser.add_argument("--frozen-gate-report", required=True)
parser.add_argument("--frozen-safety-report", required=True)
args = parser.parse_args()
adapter_dir = Path(args.adapter_dir)
if not adapter_dir.exists():
raise SystemExit(f"Adapter dir not found: {adapter_dir}. Train + gate one first.")
datastore = Path(args.datastore)
if not datastore.exists():
raise SystemExit(f"Datastore not found: {datastore}. Build it first.")
if (adapter_dir / "adapter_config.json").exists() and json.loads(
(adapter_dir / "adapter_config.json").read_text(encoding="utf-8")
).get("mock"):
raise SystemExit("mock adapters can never produce an accepted serve bundle")
revision_lock = json.loads(
(adapter_dir / "base_revision.json").read_text(encoding="utf-8")
)
revision = str(revision_lock.get("revision", ""))
actual_base_model = str(revision_lock.get("model", ""))
if not actual_base_model or len(revision) != 40:
raise SystemExit("adapter lacks an exact base/tokenizer revision lock")
if args.base_model and actual_base_model != args.base_model:
raise SystemExit("adapter base model differs from the explicit export expectation")
normal_path = Path(args.normal_gate_report)
frozen_path = Path(args.frozen_gate_report)
safety_path = Path(args.frozen_safety_report)
normal = json.loads(normal_path.read_text(encoding="utf-8"))
frozen = json.loads(frozen_path.read_text(encoding="utf-8"))
safety = json.loads(safety_path.read_text(encoding="utf-8"))
normal_accepted, _ = run_gate(normal)
frozen_accepted, _ = run_gate(
frozen,
candidate="gec_pred",
baselines=("raw_asr",),
splits=("frozen",),
safety_report=safety,
safety_split="frozen",
)
if not normal_accepted or not frozen_accepted:
raise SystemExit("normal and frozen GEC gates must both be accepted before export")
bundle = Path(args.output)
bundle.mkdir(parents=True, exist_ok=True)
# Copy the adapter (+ its tokenizer + darag_variant.json) and datastore in,
# so the bundle is portable from Drive to a serving box on its own.
adapter_out = bundle / "adapter"
if adapter_out.exists():
shutil.rmtree(adapter_out)
shutil.copytree(adapter_dir, adapter_out)
shutil.copy2(datastore, bundle / "term_datastore.json")
variant = _read_variant(adapter_dir)
manifest = {
"schema": "carepath.gec.serve/1",
"base_model": actual_base_model,
"base_revision": revision,
"tokenizer_revision": revision,
"fallback_base_model": args.fallback_base_model,
"variant": variant.get("variant", "full"),
"use_retrieval": bool(variant.get("use_retrieval", True)),
"max_new_tokens": args.max_new_tokens,
"adapter_dir": "adapter",
"datastore": "term_datastore.json",
"gate_accepted": True,
"gate_evidence": {
"normal_report_sha256": hashlib.sha256(normal_path.read_bytes()).hexdigest(),
"frozen_report_sha256": hashlib.sha256(frozen_path.read_bytes()).hexdigest(),
"frozen_safety_report_sha256": hashlib.sha256(safety_path.read_bytes()).hexdigest(),
"adapter_files": {
str(path.relative_to(adapter_dir)): hashlib.sha256(path.read_bytes()).hexdigest()
for path in sorted(adapter_dir.rglob("*"))
if path.is_file()
},
},
# Frozen DARAG prompt so serving reproduces the training format with no
# dependency on gec.prompts.
"prompt": {
"system": SYSTEM_PROMPT,
"im_start": IM_START,
"im_end": IM_END,
"best_label": "Best hypothesis: ",
"others_label": "Other hypotheses:",
"entities_label": "Named entities: ",
},
}
(bundle / "serve_manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"Wrote serve bundle -> {bundle}")
print(f" variant={manifest['variant']} use_retrieval={manifest['use_retrieval']} "
f"base={manifest['base_model']}")
print(f"Serve with: LLM_PROVIDER=gec_local GEC_BUNDLE_PATH={bundle}")
if __name__ == "__main__":
main()
|