#!/usr/bin/env python3 """opencode-only teacher label generation - runs in background.""" import json, subprocess, time, sys from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed ARTIFACTS = Path(__file__).resolve().parents[2] / "artifacts" / "tiny_nla" RECORDS_FILE = ARTIFACTS / "records_v2.jsonl" OUTPUT_JSONL = ARTIFACTS / "teacher_labels_v2.jsonl" PROMPT = """\ 你是语言模型内部机制分析专家。用1-2句简洁中文描述Qwen3-0.6B在处理下面这个token时,该位置residual stream激活值编码的语义信息。 上下文:{context} 当前token:「{token}」(位置{pos}/{seq_len}) 模型预测下一个词的候选:{top_tokens} 要求:简洁具体,联系token在句中的实际句法/语义角色,联系预测候选推断编码内容,不超过55字。只输出解释本身。""" def make_prompt(r): return PROMPT.format(context=r["text"], token=r["token_text"], pos=r["pos"], seq_len=r["seq_len"], top_tokens="、".join(r["top_tokens"][:5])) def strip_think(t): return t.split("")[-1].strip() if "" in t else t.strip() def call_opencode(r): res = subprocess.run( ["opencode", "run", "--model", "opencode/deepseek-v4-flash-free", make_prompt(r)], capture_output=True, text=True, timeout=240, ) lines = [l.strip() for l in res.stdout.splitlines() if l.strip() and not l.startswith("\x1b") and "orchestrator" not in l and not l.startswith("{") and not l.startswith('"')] return strip_think("\n".join(lines)) def load_done(): done = set() try: for r in json.load(open(ARTIFACTS / "teacher_labels_v2.json")): done.add((r["text_idx"], r["pos"])) except Exception: pass if OUTPUT_JSONL.exists(): for line in open(OUTPUT_JSONL): try: r = json.loads(line) done.add((r["text_idx"], r["pos"])) except Exception: pass return done def process(idx_rec): idx, rec = idx_rec try: expl = call_opencode(rec) if expl and len(expl) >= 5: return {**rec, "teacher_explanation": expl, "teacher_source": "opencode"} except Exception as e: print(f" err [{idx}]: {e}", flush=True) return None def main(): workers = int(sys.argv[1]) if len(sys.argv) > 1 else 3 records = [json.loads(l) for l in open(RECORDS_FILE)] done = load_done() todo = [(i, r) for i, r in enumerate(records) if (r["text_idx"], r["pos"]) not in done] print(f"Remaining: {len(todo)}/9810, workers: {workers}", flush=True) completed = 0 t0 = time.time() out_f = open(OUTPUT_JSONL, "a", encoding="utf-8", buffering=1) with ThreadPoolExecutor(max_workers=workers) as pool: futures = {pool.submit(process, item): item for item in todo} for fut in as_completed(futures): result = fut.result() if result: out_f.write(json.dumps(result, ensure_ascii=False) + "\n") completed += 1 if completed % 20 == 0: rate = completed / (time.time() - t0) * 60 eta_h = (len(todo) - completed) / (rate / 60) / 3600 print(f" [{len(done)+completed}/{9810}] {rate:.0f}/min ETA={eta_h:.1f}h", flush=True) out_f.close() print(f"Done: {completed} new labels written", flush=True) if __name__ == "__main__": main()