File size: 6,152 Bytes
9f12a7f | 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 | """
export_distillation.py β turn captured teacher examples into student-ready JSONL.
Offline/admin tool. Reads the `distillation_examples` Firestore collection (written by
distill.py) and emits one JSONL file per task under an output directory:
intent_parse.jsonl / translate.jsonl / codegen.jsonl / coach.jsonl / insight.jsonl
β chat-format SFT {"messages":[{system},{user},{assistant}]}
vision.jsonl β {"image_uri", "instruction", "output"}
asr.jsonl β {"audio_uri", "transcript"}
rejected.jsonl β hard negatives (verdict=rejected), for DPO / filtering
This is sequence-level (response-based) distillation: targets are the teacher's text /
JSON outputs and the human-verified gold label β Gemini exposes no logits.
Usage:
python scripts/export_distillation.py --out ./dataset \
--verdicts confirmed corrected pending
(defaults: SFT uses confirmed+corrected; pass --include-pending to add unlabeled)
Env: FIREBASE (service-account JSON), as used by the app.
"""
import argparse
import json
import os
from collections import Counter, defaultdict
from typing import Any, Dict, List, Optional
def _init_db():
import firebase_admin
from firebase_admin import credentials, firestore
if not firebase_admin._apps:
sa = json.loads(os.environ["FIREBASE"])
cred = credentials.Certificate(sa)
opts = {}
bucket = os.environ.get("FIREBASE_STORAGE_BUCKET")
if bucket:
opts["storageBucket"] = bucket
firebase_admin.initialize_app(cred, opts)
return firestore.client()
# Tasks that train as text chat SFT.
_TEXT_TASKS = {"intent_parse", "translate", "codegen", "coach", "insight"}
def _gold_or_teacher(ex: Dict[str, Any]) -> Optional[str]:
"""Prefer the human-corrected gold output; fall back to the teacher output."""
return ex.get("gold_output") or ex.get("output_raw")
def _to_chat(ex: Dict[str, Any]) -> Optional[Dict[str, Any]]:
inp = ex.get("input") or {}
user = inp.get("text") or inp.get("transcript") or ""
system = inp.get("system_prompt") or ""
target = _gold_or_teacher(ex)
if not user or not target:
return None
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": user})
messages.append({"role": "assistant", "content": target})
return {"messages": messages, "task": ex.get("task"),
"teacher_model": ex.get("teacher_model"),
"prompt_version": ex.get("prompt_version"),
"verdict": ex.get("verdict")}
def _to_vision(ex: Dict[str, Any]) -> Optional[Dict[str, Any]]:
inp = ex.get("input") or {}
uri = inp.get("media_uri")
target = _gold_or_teacher(ex)
if not uri or not target:
return None
return {"image_uri": uri, "instruction": inp.get("caption") or "Extract the bookkeeping transactions as JSON.",
"output": target, "verdict": ex.get("verdict"),
"teacher_model": ex.get("teacher_model")}
def _to_asr(ex: Dict[str, Any]) -> Optional[Dict[str, Any]]:
inp = ex.get("input") or {}
uri = inp.get("media_uri")
transcript = ex.get("output_raw")
if not uri or not transcript:
return None
return {"audio_uri": uri, "transcript": transcript, "teacher_model": ex.get("teacher_model")}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="./dataset", help="output directory")
ap.add_argument("--verdicts", nargs="*", default=["confirmed", "corrected"],
help="verdicts to include for SFT (default: confirmed corrected)")
ap.add_argument("--include-pending", action="store_true",
help="also include unlabeled (pending) examples in SFT")
ap.add_argument("--limit", type=int, default=0, help="max docs (0 = all)")
args = ap.parse_args()
sft_verdicts = set(args.verdicts)
if args.include_pending:
sft_verdicts.add("pending")
db = _init_db()
os.makedirs(args.out, exist_ok=True)
writers: Dict[str, Any] = {}
def _w(name: str):
if name not in writers:
writers[name] = open(os.path.join(args.out, f"{name}.jsonl"), "w", encoding="utf-8")
return writers[name]
counts: Counter = Counter()
# Dedup identical (user,target) pairs per task.
seen: Dict[str, set] = defaultdict(set)
q = db.collection("distillation_examples")
docs = q.limit(args.limit).stream() if args.limit else q.stream()
for doc in docs:
ex = doc.to_dict() or {}
task = ex.get("task") or "other"
verdict = ex.get("verdict") or "pending"
if verdict == "rejected":
rec = _to_chat(ex) or {"input": ex.get("input"), "output": ex.get("output_raw")}
_w("rejected").write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
counts["rejected"] += 1
continue
if task == "asr":
rec = _to_asr(ex)
if rec:
_w("asr").write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
counts["asr"] += 1
continue
if verdict not in sft_verdicts:
continue
if task == "vision":
rec = _to_vision(ex)
if rec:
_w("vision").write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
counts["vision"] += 1
continue
if task in _TEXT_TASKS:
rec = _to_chat(ex)
if not rec:
continue
key = (rec["messages"][-2]["content"], rec["messages"][-1]["content"])
if key in seen[task]:
continue
seen[task].add(key)
_w(task).write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
counts[task] += 1
for f in writers.values():
f.close()
print("Exported examples per file:")
for name, n in sorted(counts.items()):
print(f" {name}.jsonl: {n}")
print(f"Output: {os.path.abspath(args.out)}")
if __name__ == "__main__":
main()
|