184 lines
5.7 KiB
Go
184 lines
5.7 KiB
Go
// Command minerharness прогоняет БОЕВОЙ майнер банка (`internal/miner`) по произвольной книге,
|
||
// НЕ ТРОГАЯ репозиторий. Прецедент — пакет-6 (`labels/README.md`): отдельный Go-модуль ВНЕ репо с
|
||
// `module textmachine/backend/<имя>` и `replace textmachine/backend => <копия>/backend`; имя обязано
|
||
// начинаться с `textmachine/backend/`, иначе Go запретит импорт `internal/`.
|
||
//
|
||
// Отличие от пакета-6, сознательное: `replace` смотрит не в рабочее дерево, а в КОПИЮ бэкенда,
|
||
// извлечённую `git archive <commit>` — параллельно идёт бэкенд-сессия пака-20, её незакоммиченные
|
||
// правки (в т.ч. `internal/terminology`, `miner_emit.go`) меняли бы результат под руками. Пин к
|
||
// коммиту делает прогон воспроизводимым и снимает вопрос «чей это был код».
|
||
//
|
||
// Вход: JSONL от `eval/pkg7/split_book.py` ({chapter, chunk_idx, source}). Нормализацию делает
|
||
// сам харнесс тем же вызовом, что и прод (`text.NormalizeSourceKey`, pipeline/mining.go:57).
|
||
// Выход: TSV эмитированной дельты + сводка. Ноль сетевых вызовов, ноль моделей, $0.
|
||
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"sort"
|
||
"strings"
|
||
|
||
"textmachine/backend/internal/lang"
|
||
"textmachine/backend/internal/membank"
|
||
"textmachine/backend/internal/miner"
|
||
"textmachine/backend/internal/store"
|
||
"textmachine/backend/internal/text"
|
||
)
|
||
|
||
type row struct {
|
||
Chapter int `json:"chapter"`
|
||
ChunkIdx int `json:"chunk_idx"`
|
||
Source string `json:"source"`
|
||
}
|
||
|
||
func main() {
|
||
var (
|
||
in = flag.String("in", "", "JSONL чанков (chapter, chunk_idx, source)")
|
||
contrastP = flag.String("contrast", "", "контраст-корпус (jieba dict.txt)")
|
||
packRoot = flag.String("langpack", "", "корень langpack (configs/langpacks)")
|
||
srcLang = flag.String("src", "zh", "язык исходника для langpack")
|
||
tgtLang = flag.String("tgt", "ru", "целевой язык для langpack")
|
||
seedPath = flag.String("seed", "", "опциональный сид книги (YAML); пусто = майнить с нуля")
|
||
outTSV = flag.String("out", "", "куда писать TSV дельты (пусто = stdout)")
|
||
outYAML = flag.String("out-yaml", "", "опционально: карта подписи в форме DeltaYAML")
|
||
)
|
||
flag.Parse()
|
||
if *in == "" || *contrastP == "" || *packRoot == "" {
|
||
fmt.Fprintln(os.Stderr, "usage: minerharness -in chunks.jsonl -contrast dict.txt -langpack <root> [-src zh] [-seed seed.yaml] [-out out.tsv]")
|
||
os.Exit(2)
|
||
}
|
||
|
||
chunks, chars, chapters, err := readChunks(*in)
|
||
if err != nil {
|
||
fatal(err)
|
||
}
|
||
cf, err := os.Open(*contrastP)
|
||
if err != nil {
|
||
fatal(err)
|
||
}
|
||
defer cf.Close()
|
||
contrast, err := miner.LoadContrast(cf)
|
||
if err != nil {
|
||
fatal(err)
|
||
}
|
||
pack, err := lang.Load(*packRoot, *srcLang, *tgtLang)
|
||
if err != nil {
|
||
fatal(err)
|
||
}
|
||
var seed []store.GlossaryEntry
|
||
if *seedPath != "" {
|
||
seed, err = membank.LoadGlossarySeed(*seedPath)
|
||
if err != nil {
|
||
fatal(err)
|
||
}
|
||
}
|
||
|
||
mined := miner.MineBank(chunks, contrast, seed, nil, miner.FrozenConfig(), pack)
|
||
|
||
w := os.Stdout
|
||
if *outTSV != "" {
|
||
f, err := os.Create(*outTSV)
|
||
if err != nil {
|
||
fatal(err)
|
||
}
|
||
defer f.Close()
|
||
w = f
|
||
}
|
||
bw := bufio.NewWriter(w)
|
||
defer bw.Flush()
|
||
fmt.Fprintln(bw, "src\ttype\tfreq\tsince_ch\taliases\tevidence")
|
||
byType := map[string]int{}
|
||
byLen := map[int]int{}
|
||
for _, m := range mined {
|
||
byType[m.Type]++
|
||
byLen[len([]rune(m.Src))]++
|
||
fmt.Fprintf(bw, "%s\t%s\t%d\t%d\t%s\t%s\n",
|
||
m.Src, m.Type, m.Freq, m.SinceCh,
|
||
strings.Join(m.Aliases, "|"), strings.Join(m.Evidence, "|"))
|
||
}
|
||
bw.Flush()
|
||
|
||
if *outYAML != "" {
|
||
y, err := miner.DeltaYAML(mined, nil)
|
||
if err != nil {
|
||
fatal(err)
|
||
}
|
||
if err := os.WriteFile(*outYAML, []byte(y), 0o644); err != nil {
|
||
fatal(err)
|
||
}
|
||
}
|
||
|
||
fmt.Fprintf(os.Stderr, "chunks=%d chapters=%d chars=%d pack=%s emitted=%d\n",
|
||
len(chunks), chapters, chars, pack.Version(), len(mined))
|
||
fmt.Fprintf(os.Stderr, "by_type: %s\n", kvSorted(byType))
|
||
fmt.Fprintf(os.Stderr, "by_srclen: %s\n", kvIntSorted(byLen))
|
||
}
|
||
|
||
func readChunks(path string) (out []miner.Chunk, chars, chapters int, err error) {
|
||
f, err := os.Open(path)
|
||
if err != nil {
|
||
return nil, 0, 0, err
|
||
}
|
||
defer f.Close()
|
||
sc := bufio.NewScanner(f)
|
||
sc.Buffer(make([]byte, 0, 1<<20), 1<<26)
|
||
seen := map[int]bool{}
|
||
for sc.Scan() {
|
||
line := strings.TrimSpace(sc.Text())
|
||
if line == "" {
|
||
continue
|
||
}
|
||
var r row
|
||
if err := json.Unmarshal([]byte(line), &r); err != nil {
|
||
return nil, 0, 0, err
|
||
}
|
||
chars += len([]rune(r.Source))
|
||
seen[r.Chapter] = true
|
||
out = append(out, miner.Chunk{
|
||
Chapter: r.Chapter, ChunkIdx: r.ChunkIdx,
|
||
NSource: text.NormalizeSourceKey(r.Source),
|
||
})
|
||
}
|
||
return out, chars, len(seen), sc.Err()
|
||
}
|
||
|
||
func kvSorted(m map[string]int) string {
|
||
keys := make([]string, 0, len(m))
|
||
for k := range m {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
var b strings.Builder
|
||
for i, k := range keys {
|
||
if i > 0 {
|
||
b.WriteString(" ")
|
||
}
|
||
fmt.Fprintf(&b, "%s=%d", k, m[k])
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
func kvIntSorted(m map[int]int) string {
|
||
keys := make([]int, 0, len(m))
|
||
for k := range m {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Ints(keys)
|
||
var b strings.Builder
|
||
for i, k := range keys {
|
||
if i > 0 {
|
||
b.WriteString(" ")
|
||
}
|
||
fmt.Fprintf(&b, "%d=%d", k, m[k])
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
func fatal(err error) {
|
||
fmt.Fprintln(os.Stderr, "minerharness:", err)
|
||
os.Exit(1)
|
||
}
|