textmachine/eval/promptlang/build_projects.py

296 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""$0 стройка проектов-проб для пакета «язык промптов (Р6) + fidelity-хвост P4».
Ничего не вызывает у провайдеров. Собирает под `~/books/gu-zhenren/promptlang/`:
A-ru / A-en — задача A: КОПИЯ БД холодного прогона (тот же book_id, тот же снапшот ⇒ черновые
чекпойнты попадают, волна не перекупается), различие ровно одно — `prompts_root`
пар-конфига указывает на дерево с ru- или en-терминологом. Полезная нагрузка роли
(кандидаты, KWIC, черновики, батчинг) при этом байт-идентична по построению:
она собирается из ОДНОЙ И ТОЙ ЖЕ БД одним и тем же кодом.
B-a0/a1/a2 — задача B: ЧИСТЫЕ БД на СВЕЖЕМ срезе (главы 1116, вне среза холодного прогона),
одна черновая стадия, три варианта `translator.md` через `prompt_override`.
Стендовые артефакты не трогаются: всё живёт в отдельном каталоге, БД холодного прогона копируется.
"""
from __future__ import annotations
import shutil
import subprocess
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
BOOKS = Path.home() / "books" / "gu-zhenren"
PL = BOOKS / "promptlang"
COLD = BOOKS / "coldrun-a"
BACKEND = REPO / "backend"
SLICE_B = PL / "slice" / "guzhenren-s11-16.gb18030.txt"
PAIR_YAML = """# Пар-слой zh→ru — ПРОГОННАЯ КОПИЯ ратифицированного `backend/configs/pairs/zh-ru.yaml`.
# Отличие ровно одно и оно и есть предмет эксперимента: prompts_root показывает на дерево промптов
# арма. Числа нарезки и коридор len_ratio скопированы побайтно — иначе сравнивались бы две вещи.
pair: zh-ru
prompts_root: {prompts_root}
segmentation:
draft_budget_out: 1797
edit_ceiling_out: 3200
fertility:
cjk: 1.1978
other: 0.3852
coverage:
len_ratio_bounds: [2.2, 4.2]
"""
# ── задача A ────────────────────────────────────────────────────────────────────────────────────
A_PIPELINE = """# Задача A (Р6, «язык промптов»), арм {arm}. Ран-конфиг — КОПИЯ конфига холодного прогона.
# Изменены ровно два числа, и оба — предохранители, а не предмет замера:
# gates.terminology.budget_usd 0.05 → 0.07 (роль на этой БД уже потратила $0.030254; при 0.05
# арм оборвался бы на середине и сравнивать было бы
# нечего — усечённый арм это не результат, а брак)
# Всё, что входит в СНАПШОТ (стадии, модели, температуры, промпт черновика, гейты банкноты),
# оставлено побайтно — иначе черновая волна была бы перекуплена.
core: C1
version: 1
defaults:
max_output_ratio: 2.2
min_max_tokens: 2048
context:
glossary_injection: selective
glossary_token_budget: 800
cache_ttl: "5m"
waves:
workers: 4
retries:
regenerate_before_escalate: 1
stages:
- name: draft
role: translator
model: deepseek-v4-flash
prompt_override: /home/ubuntu/projects/textmachine/backend/prompts/zh-ru/translator-banknote.md
prompt_version: v1-reflow-banknote
temperature: 0.3
reasoning: "off"
escalate_to: deepseek-v4-pro
- name: edit
role: editor
model: deepseek-v4-pro
prompt_version: v3-discourse-reflow
temperature: 0.4
reasoning: "off"
few_shot: false
gates:
coverage:
enabled: false
sent_cov_min: 0.75
min_chunk_chars: 500
sanitizer:
enabled: true
regression_guard:
enabled: true
banknote:
enabled: true
terminology:
enabled: true
model: deepseek-v4-flash
budget_usd: 0.07
target_script: Cyrillic
voice:
enabled: true
escalation:
chains:
default: [deepseek-v4-pro, glm-5.1, gemini-3.1-pro-preview]
adult: [grok-4.3]
budget_usd: 0.10
fanout:
candidates: 1
mining:
contrast_path: /home/ubuntu/projects/textmachine/eval/exp16/data/jieba_dict_general_zh.txt
"""
A_BOOK = """# Задача A, арм {arm}. book_id СОХРАНЁН (`guzhenren-coldrun-a`): он входит в RequestHash
# (render.go:269-283), и его смена перекупила бы всю черновую волну. Срез, сид (его нет), langpack,
# models.yaml — те же файлы, что у холодного прогона; изменены только пути проекта и потолок.
book_id: guzhenren-coldrun-a
title: 蛊真人
source_lang: zh
target_lang: ru
genre: вебновелла
audience: взрослые читатели вебновелл
adult: false
venuti: 0.6
honorifics: keep
transcription: palladius
footnotes: minimal
pipeline: {dir}/pipeline.yaml
models: /home/ubuntu/projects/textmachine/backend/configs/models.yaml
source_file: /home/ubuntu/books/gu-zhenren/coldrun-a/guzhenren-ch1-10.gb18030.txt
encoding: gb18030
langpack_root: /home/ubuntu/projects/textmachine/backend/configs/langpacks
langpack_extend: /home/ubuntu/books/gu-zhenren/langpack-extend
mined_delta: {dir}/mined-delta.yaml
mined_rejects: {dir}/mined-rejects.yaml
project_db: {dir}/db.sqlite
# committed на копии = $0.126068 (наследство холодного прогона). Потолок 0.16 оставляет $0.0339 —
# больше ожидаемого прохода ($0.013), но физически способен выстрелить.
ceilings:
book_usd: 0.16
day_usd: 0.16
"""
# ── задача B ────────────────────────────────────────────────────────────────────────────────────
B_PIPELINE = """# Задача B (D21 п.6, анти-эхо), арм {arm}: {arm_note}
# ОДНА стадия — черновик. Редактуры нет: предмет замера — выход ЧЕРНОВОГО канала, а edit-волна
# стоила бы в четыре раза дороже всего пакета.
# escalate_to НЕТ и regenerate_before_escalate=0 СОЗНАТЕЛЬНО: эхо здесь ИЗМЕРЯЕТСЯ, а не чинится.
# Ремонт (эскалация на dspro) — боевое поведение, но он превратил бы один чанк в два-три вызова и
# смешал бы частоту эха с ценой его лечения.
core: C0
version: 1
defaults:
max_output_ratio: 2.2
min_max_tokens: 2048
context:
glossary_injection: selective
glossary_token_budget: 800
cache_ttl: "5m"
waves:
workers: 4
retries:
regenerate_before_escalate: 0
stages:
- name: draft
role: translator
model: deepseek-v4-flash
prompt_override: {arm_prompt}
prompt_version: promptlang-{arm}
temperature: 0.3
reasoning: "off"
gates:
coverage:
enabled: false
sent_cov_min: 0.75
min_chunk_chars: 500
sanitizer:
enabled: true
regression_guard:
enabled: true
banknote:
enabled: false
terminology:
enabled: false
voice:
enabled: false
fanout:
candidates: 1
"""
B_BOOK = """# Задача B, арм {arm}. Срез — главы 1116 蛊真人 (строки 837..1293 utf8-копии), НЕ
# пересекается со срезом холодного прогона (главы 110): «свежая выборка» петли D21 п.6.
# Сида НЕТ: банк пуст, инъекция глоссария пуста во ВСЕХ трёх армах ⇒ инъекции байт-идентичны
# тривиально, и различие армов — ровно текст промпта.
book_id: promptlang-{arm}
title: 蛊真人
source_lang: zh
target_lang: ru
genre: вебновелла
audience: взрослые читатели вебновелл
adult: false
venuti: 0.6
honorifics: keep
transcription: palladius
footnotes: minimal
pipeline: {dir}/pipeline.yaml
models: /home/ubuntu/projects/textmachine/backend/configs/models.yaml
source_file: {slice}
encoding: gb18030
langpack_root: /home/ubuntu/projects/textmachine/backend/configs/langpacks
langpack_extend: /home/ubuntu/books/gu-zhenren/langpack-extend
project_db: {dir}/db.sqlite
ceilings:
book_usd: 0.02
day_usd: 0.02
"""
ARM_NOTES = {
"a0": "БОЕВОЙ translator.md побайтно (базлайн; sha совпадает с боевым файлом).",
"a1": "боевой + ПОСТИНСТРУКЦИЯ после {{text}} (хвост USER-части) — дословно строка P4.",
"a2": "боевой + МИКРО-FEW-SHOT в штатный блок ---FEWSHOT--- (хвост system, D38.4) — дословно пары P4.",
}
def write(p: Path, s: str) -> None:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(s, encoding="utf-8")
def build_a() -> None:
for arm, root in (("ru", PL / "prompts-ru"), ("en", PL / "prompts-en")):
d = PL / f"A-{arm}"
d.mkdir(parents=True, exist_ok=True)
shutil.copy2(COLD / "guzhenren-coldrun-a.db", d / "db.sqlite")
shutil.copy2(COLD / "mined-delta.yaml", d / "mined-delta.yaml")
shutil.copy2(COLD / "mined-rejects.yaml", d / "mined-rejects.yaml")
write(d / "pipeline.yaml", A_PIPELINE.format(arm=arm))
write(d / "book.yaml", A_BOOK.format(arm=arm, dir=d))
write(d / "pairs" / "zh-ru.yaml", PAIR_YAML.format(prompts_root=root))
print(f"A-{arm}: собран, prompts_root={root}")
def build_b() -> None:
for arm in ("a0", "a1", "a2"):
d = PL / f"B-{arm}"
d.mkdir(parents=True, exist_ok=True)
for stale in ("db.sqlite", "db.sqlite.lock"):
(d / stale).unlink(missing_ok=True)
write(d / "pipeline.yaml", B_PIPELINE.format(
arm=arm, arm_note=ARM_NOTES[arm], arm_prompt=PL / "arms" / f"translator-{arm}.md"))
write(d / "book.yaml", B_BOOK.format(arm=arm, dir=d, slice=SLICE_B))
write(d / "pairs" / "zh-ru.yaml", PAIR_YAML.format(prompts_root=BACKEND / "prompts"))
print(f"B-{arm}: собран, промпт={PL / 'arms' / f'translator-{arm}.md'}")
def main() -> int:
if not SLICE_B.exists():
print(f"нет среза {SLICE_B}", file=sys.stderr)
return 2
build_a()
build_b()
# Байт-парити ru-дерева промптов с боевым (арм A обязан быть РЕПЛИКОЙ, а не пересказом).
r = subprocess.run(["diff", "-r", str(BACKEND / "prompts"), str(PL / "prompts-ru")],
capture_output=True, text=True)
print("\nprompts-ru vs backend/prompts:", "БАЙТ-ИДЕНТИЧНЫ" if r.returncode == 0 else r.stdout[:500])
return 0
if __name__ == "__main__":
raise SystemExit(main())