info("源表:{$from}"); $log->info("目标表:{$to}"); $alignment = rp_json_read(rp_report_dir().'/chapter_alignment.json'); if ($alignment['table'] !== $from) { $log->error("对齐表是针对 {$alignment['table']} 算的,与源表 {$from} 不符,先重跑 align_chapters.php"); $log->finish(); exit(1); } $log->info('复制源表…'); DB::statement("drop table if exists {$to}"); DB::statement("create table {$to} as select * from {$from}"); DB::statement("create index on {$to} (book, para)"); $log->info('行数:'.DB::table($to)->count()); $applied = 0; $rowsChanged = 0; $reverted = 0; /** * 某个 book_name(含其所有章后缀变体)下,有多少 (book, para) 通过**可信的**配对键 * 跨文件命中。可信 = 该键在任何单本书里命中的段落数不超过 $maxParas。 * * 为什么要排除膨胀键:回绕让 kn10:1 这样的键在 56 个章里重复,一次查询捞回上千段, * 那不是配对是噪声。直接数「配对段数」会把这些噪声算成资产——实测 kn10 加后缀后该数 * 从 23757 掉到 6279,看着像灾难,其实掉的几乎全是 book 144 章 43-56 的虚假配对 * (那几章本来就没有义注,是靠撞在一起的 kn10:1 假装配上的)。只数非膨胀键, * 加对了后缀这个数会涨(膨胀键裂成一批干净键),加错了才会掉。 */ $pairedParagraphs = function (string $table, string $baseName, int $maxParas = 10): int { $pattern = str_replace(['_', '%'], ['\\_', '\\%'], $baseName); return (int) DB::selectOne(" with mine as ( select book, para, book_name, cs_para from {$table} where cs_para > 0 and (book_name = ? or book_name like ?) ), per_book as ( select book_name, cs_para, book, count(distinct para) as paras from mine group by book_name, cs_para, book ), k as ( select book_name, cs_para from per_book group by book_name, cs_para having count(*) >= 2 and max(paras) <= ? ) select count(distinct (m.book, m.para))::int as n from mine m join k on k.book_name = m.book_name and k.cs_para = m.cs_para ", [$baseName, $pattern.'\_%', $maxParas])->n; }; foreach ($alignment['names'] as $name => $entry) { if (! $entry['applicable']) { $log->count('name.skipped_low_confidence'); $log->unresolved('chapter_align_low_confidence', [ 'book_name' => $name, 'confidence' => $entry['confidence'], 'chapters' => $entry['chapters'], 'wraps' => $entry['wraps'], 'detail' => '跨侧对齐置信不足,未加后缀,待补丁或 LLM 判定', ]); continue; } if (str_ends_with($name, '_A')) { // 序言名不分章:它本就不对应正文段落,加后缀没有意义 $log->count('name.skipped_prologue'); continue; } $before = $pairedParagraphs($to, $name); $changed = 0; foreach ($entry['assignments'] as $a) { $changed += DB::table($to) ->where('book', $a['book']) ->where('book_name', $name) ->whereBetween('para', [$a['para_from'], $a['para_to']]) ->update(['book_name' => $name.'_'.$a['chapter']]); } $after = $pairedParagraphs($to, $name); // 加对了后缀这个数应当上升或持平;下跌就是把真配对拆散了,整名回滚。 $tolerance = (int) max(5, $before * 0.02); if ($after < $before - $tolerance) { foreach ($entry['assignments'] as $a) { DB::table($to) ->where('book', $a['book']) ->where('book_name', $name.'_'.$a['chapter']) ->whereBetween('para', [$a['para_from'], $a['para_to']]) ->update(['book_name' => $name]); } $reverted++; $log->warn(sprintf('%-8s 回滚:可信配对 %d → %d 段,跌幅超容差 %d', $name, $before, $after, $tolerance)); $log->unresolved('chapter_suffix_reverted', [ 'book_name' => $name, 'paired_before' => $before, 'paired_after' => $after, 'chapters' => $entry['chapters'], 'confidence' => $entry['confidence'], 'detail' => '加章后缀后可信配对大幅下跌,已回滚;多半是并列的两本书被当成了顺序拼接,待补丁或 LLM 判定', ]); $log->count('name.reverted'); continue; } $applied++; $rowsChanged += $changed; $log->info(sprintf('%-8s %d 章,改名 %-6d 行,可信配对 %d → %d 段', $name, $entry['chapters'], $changed, $before, $after)); $log->count('rows.renamed', $changed); } $log->info("加后缀的 book_name:{$applied} 个,共改名 {$rowsChanged} 行;因配对下跌回滚 {$reverted} 个"); $log->info('建索引中…'); DB::statement("create index on {$to} (book)"); DB::statement("create index on {$to} (para)"); DB::statement("create index on {$to} (book_name)"); DB::statement("create index on {$to} (book_name, cs_para)"); DB::statement("create index on {$to} (book, para, cs_para)"); $log->finish();