2
0

apply_chapters.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. <?php
  2. /**
  3. * 按 align_chapters.php 的对齐结果给 book_name 加章后缀,产出下一版表。
  4. *
  5. * 加后缀是为了让 (book_name, cs_para) 这个配对键重新变得唯一:回绕使同一个 key 在
  6. * 几十个章里重复,index() 一次查询就把几十章的段落全捞回来。后缀必须两侧同时加、
  7. * 用同一套章号,只给一侧加会把配对整个打断——所以只处理 align 判定为可信的 book_name,
  8. * 其余原样留给补丁/LLM。
  9. *
  10. * 光靠 align 的置信度不够。实测 vin8 的 books 200/201 都覆盖 cs 1..3183(是并列的两侧,
  11. * 不是上下册),却被当成顺序拼接标成了「第 1 章 / 第 2 章」,该名的跨文件配对 100% 报废;
  12. * dict 的 books 9↔21 同理。侧别判定要做到永不出错是不现实的,所以每个 book_name 改名
  13. * 前后都实测一次跨文件配对段数,掉了就整名回滚——指标说了算,不是规则说了算。
  14. *
  15. * 用法:
  16. * php scripts/related/apply_chapters.php --from=related_paragraphs_v1 --to=related_paragraphs_v2
  17. */
  18. require __DIR__.'/lib/boot.php';
  19. use Illuminate\Support\Facades\DB;
  20. [$options] = rp_args($argv);
  21. $from = rp_assert_table($options['from'] ?? 'related_paragraphs_v1');
  22. $to = rp_assert_table($options['to'] ?? 'related_paragraphs_v2');
  23. $log = new RpLogger('apply_chapters', rp_report_dir());
  24. $log->info("源表:{$from}");
  25. $log->info("目标表:{$to}");
  26. $alignment = rp_json_read(rp_report_dir().'/chapter_alignment.json');
  27. if ($alignment['table'] !== $from) {
  28. $log->error("对齐表是针对 {$alignment['table']} 算的,与源表 {$from} 不符,先重跑 align_chapters.php");
  29. $log->finish();
  30. exit(1);
  31. }
  32. $log->info('复制源表…');
  33. DB::statement("drop table if exists {$to}");
  34. DB::statement("create table {$to} as select * from {$from}");
  35. DB::statement("create index on {$to} (book, para)");
  36. $log->info('行数:'.DB::table($to)->count());
  37. $applied = 0;
  38. $rowsChanged = 0;
  39. $reverted = 0;
  40. /**
  41. * 某个 book_name(含其所有章后缀变体)下,有多少 (book, para) 通过**可信的**配对键
  42. * 跨文件命中。可信 = 该键在任何单本书里命中的段落数不超过 $maxParas。
  43. *
  44. * 为什么要排除膨胀键:回绕让 kn10:1 这样的键在 56 个章里重复,一次查询捞回上千段,
  45. * 那不是配对是噪声。直接数「配对段数」会把这些噪声算成资产——实测 kn10 加后缀后该数
  46. * 从 23757 掉到 6279,看着像灾难,其实掉的几乎全是 book 144 章 43-56 的虚假配对
  47. * (那几章本来就没有义注,是靠撞在一起的 kn10:1 假装配上的)。只数非膨胀键,
  48. * 加对了后缀这个数会涨(膨胀键裂成一批干净键),加错了才会掉。
  49. */
  50. $pairedParagraphs = function (string $table, string $baseName, int $maxParas = 10): int {
  51. $pattern = str_replace(['_', '%'], ['\\_', '\\%'], $baseName);
  52. return (int) DB::selectOne("
  53. with mine as (
  54. select book, para, book_name, cs_para
  55. from {$table}
  56. where cs_para > 0 and (book_name = ? or book_name like ?)
  57. ),
  58. per_book as (
  59. select book_name, cs_para, book, count(distinct para) as paras
  60. from mine group by book_name, cs_para, book
  61. ),
  62. k as (
  63. select book_name, cs_para from per_book
  64. group by book_name, cs_para
  65. having count(*) >= 2 and max(paras) <= ?
  66. )
  67. select count(distinct (m.book, m.para))::int as n
  68. from mine m join k on k.book_name = m.book_name and k.cs_para = m.cs_para
  69. ", [$baseName, $pattern.'\_%', $maxParas])->n;
  70. };
  71. foreach ($alignment['names'] as $name => $entry) {
  72. if (! $entry['applicable']) {
  73. $log->count('name.skipped_low_confidence');
  74. $log->unresolved('chapter_align_low_confidence', [
  75. 'book_name' => $name, 'confidence' => $entry['confidence'],
  76. 'chapters' => $entry['chapters'], 'wraps' => $entry['wraps'],
  77. 'detail' => '跨侧对齐置信不足,未加后缀,待补丁或 LLM 判定',
  78. ]);
  79. continue;
  80. }
  81. if (str_ends_with($name, '_A')) {
  82. // 序言名不分章:它本就不对应正文段落,加后缀没有意义
  83. $log->count('name.skipped_prologue');
  84. continue;
  85. }
  86. $before = $pairedParagraphs($to, $name);
  87. $changed = 0;
  88. foreach ($entry['assignments'] as $a) {
  89. $changed += DB::table($to)
  90. ->where('book', $a['book'])
  91. ->where('book_name', $name)
  92. ->whereBetween('para', [$a['para_from'], $a['para_to']])
  93. ->update(['book_name' => $name.'_'.$a['chapter']]);
  94. }
  95. $after = $pairedParagraphs($to, $name);
  96. // 加对了后缀这个数应当上升或持平;下跌就是把真配对拆散了,整名回滚。
  97. $tolerance = (int) max(5, $before * 0.02);
  98. if ($after < $before - $tolerance) {
  99. foreach ($entry['assignments'] as $a) {
  100. DB::table($to)
  101. ->where('book', $a['book'])
  102. ->where('book_name', $name.'_'.$a['chapter'])
  103. ->whereBetween('para', [$a['para_from'], $a['para_to']])
  104. ->update(['book_name' => $name]);
  105. }
  106. $reverted++;
  107. $log->warn(sprintf('%-8s 回滚:可信配对 %d → %d 段,跌幅超容差 %d', $name, $before, $after, $tolerance));
  108. $log->unresolved('chapter_suffix_reverted', [
  109. 'book_name' => $name, 'paired_before' => $before, 'paired_after' => $after,
  110. 'chapters' => $entry['chapters'], 'confidence' => $entry['confidence'],
  111. 'detail' => '加章后缀后可信配对大幅下跌,已回滚;多半是并列的两本书被当成了顺序拼接,待补丁或 LLM 判定',
  112. ]);
  113. $log->count('name.reverted');
  114. continue;
  115. }
  116. $applied++;
  117. $rowsChanged += $changed;
  118. $log->info(sprintf('%-8s %d 章,改名 %-6d 行,可信配对 %d → %d 段',
  119. $name, $entry['chapters'], $changed, $before, $after));
  120. $log->count('rows.renamed', $changed);
  121. }
  122. $log->info("加后缀的 book_name:{$applied} 个,共改名 {$rowsChanged} 行;因配对下跌回滚 {$reverted} 个");
  123. $log->info('建索引中…');
  124. DB::statement("create index on {$to} (book)");
  125. DB::statement("create index on {$to} (para)");
  126. DB::statement("create index on {$to} (book_name)");
  127. DB::statement("create index on {$to} (book_name, cs_para)");
  128. DB::statement("create index on {$to} (book, para, cs_para)");
  129. $log->finish();