168 lines
8.3 KiB
Python
168 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
"""exp15/Q4a — paired 2x2 generator (B-hybrid, orchestrator-ratified 2026-07-18).
|
|
|
|
Generates the full square {flash, pro} x {raw draft, +glm-editor}, x2 draft seeds, on the 7 exp14
|
|
meaning-trap chunks. PAIR INTEGRITY IS SUPREME (orchestrator pin): both flash and pro drafts get
|
|
BYTE-IDENTICAL prompts + injection — the exp14 injected_ids reconstruction (bilingual_glossary for
|
|
the draft, editor_constraint for the editor), imperfect (no sticky) but SHARED by both sides; we do
|
|
NOT "improve" it with the S2' mem_select port. The ONLY variable in the draft row is the draft model.
|
|
|
|
Assembly (exp14 harness, matches the saved flash-draft provenance):
|
|
draft : messages_with_injection(translator_system(), bilingual_glossary, translator_user(src))
|
|
deepseek-v4-{flash|pro}, temp 0.3, thinking-ON (never disabled — echo-mine mandate)
|
|
editor : messages_with_injection(editor_system("discourse"), editor_constraint, editor_user(src,draft))
|
|
glm-5, temp 0.4, thinking-OFF (same editor for both sides -> editor-delta is model-clean)
|
|
|
|
Why regenerate the flash side too (vs reuse on-disk r["draft"]/arm C): the on-disk flash came from
|
|
the Go backend (real sticky injection); regenerating flash with the SAME reconstruction assembly as
|
|
pro GUARANTEES the flash<->pro pairing the orchestrator ranks above reuse. On-disk backend flash is
|
|
retained (records.json / exp14b arm C) as a provenance cross-check, DET-scored in q4a_score.py.
|
|
|
|
Valley guard: deepseek-v4-pro calls are BLOCKED in DeepSeek peak windows (UTC 01-04 & 06-10, the
|
|
x2 surge) — abort with a defer message; flash allowed anytime. Per-call predicted-cost gate + own
|
|
ledger (every attempt incl. failures persisted, D30.10). Resumable (skip non-empty existing files).
|
|
"""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "exp14"))
|
|
import exp14_common as C
|
|
import exp14_prompts as P
|
|
|
|
SD = Path("/home/ubuntu/books/gu-zhenren/exp15")
|
|
OUT = SD / "q4a"
|
|
LEDGER = SD / "q4a_gen_costs.jsonl"
|
|
RECS = {(r["chapter"], r["chunk_idx"]): r
|
|
for r in json.load(open("/home/ubuntu/books/gu-zhenren/rerun/records.json"))}
|
|
INJ = json.load(open("/home/ubuntu/books/gu-zhenren/exp14/injection_blocks.json"))
|
|
|
|
TRAP_CHUNKS = ["6.0", "7.0", "9.1", "10.1", "16.0", "17.0", "19.0"] # 9 meaning-traps live here
|
|
SIDES = {"flash": "deepseek-v4-flash", "pro": "deepseek-v4-pro"}
|
|
SEEDS = [0, 1, 2]
|
|
DRAFT_TEMP, EDIT_TEMP = 0.3, 0.4
|
|
# DRAFT_MAX raised 8000->16000: deepseek-v4-pro reasoning can consume the whole 8000 cap and TRUNCATE the
|
|
# translation (seen: pro_raw/7.0.s0 finish=length, reasoning starved content -> a false "omission" of the
|
|
# lost tail; adversarial verification 2026-07-18). finish=length is now REJECTED in _gen (never accepted).
|
|
DRAFT_MAX, EDIT_MAX = 16000, 12000
|
|
PEAK_UTC_HOURS = {1, 2, 3, 6, 7, 8, 9} # DeepSeek surge windows (01-04 & 06-10 UTC) — pro forbidden
|
|
GEN_HARD_STOP = 1.10 # cumulative gen ceiling ($; leaves room for judging under $2)
|
|
|
|
CAPS = {"deepseek-v4-flash": 0.03, "deepseek-v4-pro": 0.06, "glm-5": 0.08}
|
|
|
|
|
|
def _now_utc_hour():
|
|
return datetime.now(timezone.utc).hour
|
|
|
|
|
|
def _src_inj(cid):
|
|
ch, ck = cid.split("."); k = (int(ch), int(ck))
|
|
b = INJ.get(f"{ch}/{ck}", {})
|
|
return RECS[k]["source"], b.get("bilingual_glossary", ""), b.get("editor_constraint", "")
|
|
|
|
|
|
def _ledger_total():
|
|
if not LEDGER.exists():
|
|
return 0.0
|
|
t = 0.0
|
|
for line in LEDGER.read_text(encoding="utf-8").splitlines():
|
|
try:
|
|
t += json.loads(line).get("cost", 0.0)
|
|
except Exception:
|
|
pass
|
|
return t
|
|
|
|
|
|
def _gen(sp, model, messages, temp, max_out, meta):
|
|
"""Gate -> call -> persist EVERY attempt (incl. gate-block/error). Returns text or None."""
|
|
in_est = sum(len(m["content"]) for m in messages) // 2
|
|
ok, pred = sp.guard(model, in_est, max_out)
|
|
if not ok:
|
|
sp.record({**meta, "model": model, "keyenv": model, "usage": {},
|
|
"err": f"per-call gate pred ${pred:.4f} > cap ${CAPS.get(model)}", "gate_block": True})
|
|
print(f" [GATE BLOCK] {meta} pred ${pred:.4f}")
|
|
return None
|
|
total = _ledger_total()
|
|
if total + pred > GEN_HARD_STOP:
|
|
print(f" [GEN HARD-STOP] {total:.3f}+{pred:.4f} > {GEN_HARD_STOP} — stopping generation")
|
|
return "__STOP__"
|
|
s = C.spec(model, temp=temp, max_tokens=max_out)
|
|
text, err, usage = C.call(s, messages)
|
|
fin = usage.get("finish_reason")
|
|
if not err and fin == "length": # truncated output (reasoning ate the cap) — NOT a complete draft
|
|
err = f"truncated|finish=length|completion={usage.get('completion_tokens')}"
|
|
sp.record({**meta, "model": model, "keyenv": model, "usage": usage, "err": err})
|
|
if err or not text:
|
|
print(f" [BAD] {meta} err={err} finish={fin}")
|
|
return None
|
|
return text
|
|
|
|
|
|
def run(limit_chunks=0, sanity=False):
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
for cell in ("flash_raw", "pro_raw", "flash_glm", "pro_glm"):
|
|
(OUT / cell).mkdir(exist_ok=True)
|
|
sp = C.Spender(LEDGER, CAPS)
|
|
# sanity gate: FIRST chunk, seed 0 only, but BOTH sides -> flash+pro draft AND editor (4 cells), so
|
|
# the pro/deepseek path (content non-empty, thinking-ON) is exercised before the full valley run.
|
|
chunks = TRAP_CHUNKS[:1] if sanity else (TRAP_CHUNKS[:limit_chunks] if limit_chunks else TRAP_CHUNKS)
|
|
seeds = [0] if sanity else SEEDS
|
|
ed_sys = P.editor_system("discourse")
|
|
tr_sys = P.translator_system()
|
|
made, skipped = 0, 0
|
|
for cid in chunks:
|
|
src, gloss, edconstr = _src_inj(cid)
|
|
tr_user = P.translator_user(src)
|
|
for side, dmodel in SIDES.items():
|
|
for seed in seeds:
|
|
# ---- draft ----
|
|
dpath = OUT / f"{side}_raw" / f"{cid}.s{seed}.txt"
|
|
if dpath.exists() and dpath.read_text(encoding="utf-8").strip():
|
|
draft = dpath.read_text(encoding="utf-8"); skipped += 1
|
|
else:
|
|
if dmodel == "deepseek-v4-pro" and _now_utc_hour() in PEAK_UTC_HOURS:
|
|
print(f" [VALLEY DEFER] pro draft {cid} s{seed}: UTC hour {_now_utc_hour()} in peak "
|
|
f"{sorted(PEAK_UTC_HOURS)} — rerun in valley."); return
|
|
msgs = C.messages_with_injection(tr_sys, gloss, tr_user)
|
|
draft = _gen(sp, dmodel, msgs, DRAFT_TEMP, DRAFT_MAX,
|
|
{"cell": f"{side}_raw", "chunk": cid, "seed": seed, "stage": "draft"})
|
|
if draft == "__STOP__":
|
|
return
|
|
if not draft:
|
|
continue
|
|
dpath.write_text(draft, encoding="utf-8"); made += 1
|
|
print(f" {side}_raw {cid} s{seed}: {len(draft)}c gen-cum ${_ledger_total():.4f}")
|
|
# ---- editor over that draft ----
|
|
epath = OUT / f"{side}_glm" / f"{cid}.s{seed}.txt"
|
|
if epath.exists() and epath.read_text(encoding="utf-8").strip():
|
|
skipped += 1
|
|
else:
|
|
emsgs = C.messages_with_injection(ed_sys, edconstr, P.editor_user(src, draft))
|
|
final = _gen(sp, "glm-5", emsgs, EDIT_TEMP, EDIT_MAX,
|
|
{"cell": f"{side}_glm", "chunk": cid, "seed": seed, "stage": "edit"})
|
|
if final == "__STOP__":
|
|
return
|
|
if not final:
|
|
continue
|
|
epath.write_text(final, encoding="utf-8"); made += 1
|
|
print(f" {side}_glm {cid} s{seed}: {len(final)}c gen-cum ${_ledger_total():.4f}")
|
|
print(f"\ngen done: {made} made, {skipped} reused. gen total ${_ledger_total():.4f}")
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--limit-chunks", type=int, default=0)
|
|
ap.add_argument("--sanity", action="store_true",
|
|
help="gate: 1 chunk, seed 0, BOTH sides (flash+pro draft+editor = 4 cells)")
|
|
a = ap.parse_args()
|
|
print(f"[q4a-gen] UTC hour {_now_utc_hour()} (peaks {sorted(PEAK_UTC_HOURS)}); "
|
|
f"gen ledger total ${_ledger_total():.4f}; hard-stop ${GEN_HARD_STOP}")
|
|
run(a.limit_chunks, a.sanity)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|