Commit 4f7a9366 4f7a93667fdbbcf19c8c9db4260719617d077f5c by 沈秋雨

feat(dataset-review): 添加歌词三分类测试集查看页面与接口

1 parent a52c778d
1 # 歌词复用测试集 v1 审核说明
2
3 ## 文件
4
5 - `lyric_reuse_review_v1.csv`:320 对盲审样本,实际审核使用这个文件。
6 - `lyric_reuse_review_v1_manifest.csv`:抽样分层和旧系统弱标签。完成盲审前不要打开,避免旧结论影响判断。
7 - `lyric_reuse_review_v1_stats.json`:构建参数、候选池规模和分层数量。
8
9 盲审表已经随机打散,不包含抽样分层、旧去重结论或旧置信度。
10
11 ## 歌词字段
12
13 `query_lyrics_url_or_text``candidate_lyrics_url_or_text` 根据对应的 `*_lyrics_source_type` 解释:
14
15 - `inline`:字段内容本身就是歌词原文。
16 - `url`:字段是歌词地址,需要访问地址查看原文。
17
18 ## 审核顺序
19
20 先判断内容关系,再看标题、歌手和词曲作者判断作品关系,最后给出业务政策标签。不要因为标题或作者相同就改变内容关系标签。
21
22 ### 1. review_content_relation
23
24 只判断歌词内容事实,填写以下一个值:
25
26 - `near_identical`:主体歌词基本一致,差异主要是格式、重复次数或极少改动。
27 - `substantial_local_reuse`:存在较长或多段实质复用,但整首并非基本一致。
28 - `limited_fragment`:存在可确认的小段复用,覆盖范围有限。
29 - `weak_common_or_translation_only`:只有常见短句、副歌套话或翻译层面的弱重合。
30 - `no_meaningful_reuse`:没有值得关注的歌词复用。
31 - `cannot_judge`:歌词缺失、语言或文本质量导致无法判断。
32
33 ### 2. review_metadata_relation
34
35 结合标题、歌手和词曲作者填写:
36
37 - `same_work_same_version`
38 - `same_work_different_version`
39 - `different_work`
40 - `unknown`
41
42 这里判断的是作品/版本关系,不代表已经确认授权。
43
44 ### 3. review_policy_label
45
46 填写建议的业务队列:
47
48 - `suspected_wash_reuse`:内容存在实质复用,且元数据不支持合理的同作品/版本关系。
49 - `duplicate_or_same_record`:同一作品且歌词基本重复,适合按重复记录处理。
50 - `version_or_rights_review`:可能是翻唱、Remix、同词不同曲、公共文本或其他版本/权利问题。
51 - `no_reuse_action`:不需要因歌词复用采取动作。
52 - `insufficient_evidence`:证据不足,暂时无法分流。
53
54 政策标签不是侵权裁定;授权状态仍需版权信息确认。
55
56 ### 4. review_confidence
57
58 - `high`
59 - `medium`
60 - `low`
61
62 ### 5. review_notes
63
64 自由填写关键依据,例如:
65
66 - 复用发生在哪一段
67 - 仅制作名单相同
68 - 仅译文相同
69 - 同标题但词作者不同
70 - 疑似古诗词或公共文本
71 - URL 无法访问
72
73 ## 建议审核方式
74
75 - 第一轮先审核全部样本,不查看 manifest。
76 -`cannot_judge``unknown` 和低置信度记录进行第二轮复核。
77 - 完成后保留原始 `sample_id`,不要重新排序或删除行。
78 - 将填好的盲审 CSV 返回后,再结合 manifest 计算各分层召回率、误报率和政策分流结果。
1 {
2 "generated_at": "2026-07-22T01:57:04.122612+00:00",
3 "source_csv": "hk_songs_import_staging.csv",
4 "seed": "lyric-review-v1-20260722",
5 "sample_size_per_stratum": 40,
6 "total_pairs": 320,
7 "stratum_counts": {
8 "chorus_overlap": 40,
9 "exact_or_near_duplicate": 40,
10 "fragment_reuse": 40,
11 "general_borderline": 40,
12 "negative_control": 40,
13 "suspected_wash": 40,
14 "translation_overlap": 40,
15 "version_or_same_work": 40
16 },
17 "control_counts": {
18 "random_different_songs": 20,
19 "same_artist_different_title": 20
20 },
21 "source_pool_counts": {
22 "chorus_overlap": 248,
23 "exact_or_near_duplicate": 301,
24 "fragment_reuse": 123,
25 "general_borderline": 618,
26 "suspected_wash": 72,
27 "translation_overlap": 114,
28 "version_or_same_work": 601
29 },
30 "streaming": {
31 "first_pass_rows": 120873,
32 "second_pass_rows": 120873,
33 "candidate_ids_requested": 381,
34 "candidate_rows_found": 381
35 },
36 "outputs": {
37 "blind_review_csv": "datasets/lyric_reuse_review_v1.csv",
38 "sampling_manifest_csv": "datasets/lyric_reuse_review_v1_manifest.csv"
39 }
40 }
1 # 歌词 duplicate / review / new 三分类测试集 v2
2
3 ## 测试集定位
4
5 `lyric_reuse_review_v2.csv` 共 300 对,按原系统结果分层抽样:
6
7 - `duplicate`:100
8 - `review`:100
9 - `new`:100
10
11 表中的 `sampling_class``legacy_dedup_decision` 是抽样依据及原系统结果,不是人工真值。请在 `review_final_class` 中填写你的独立判断。
12
13 ## 需要特别注意的字段
14
15 - `sampling_class`:本次三分类抽样主类。
16 - `sampling_subtype`:类内细分场景。
17 - `legacy_pair_source`:旧系统是否实际比较过这一对。
18 - `legacy_dedup_decision`:原系统的 `new/duplicate/review` 结果。
19 - `legacy_dedup_confidence``legacy_dedup_reason`:原系统依据。
20
21 `duplicate``review` 都来自原系统真实的 `matched_song_id` 对,`legacy_pair_source=existing_matched_pair`
22
23 `new` 类由于原 CSV 没有保存被判 new 时的候选对,因此由两条原系统分别判为 new 的歌曲构造:
24
25 ```text
26 legacy_pair_source=synthetic_pair_from_two_legacy_new_records
27 ```
28
29 这表示两首歌各自的原结果是 new,不表示旧系统曾直接比较过这两个对象。
30
31 ## 类内细分
32
33 ### duplicate:100
34
35 - `exact_hash`:40
36 - `high_literal_overlap`:54
37 - `high_primary_translation_ignored`:6(候选池仅有 6 条,全部纳入)
38
39 ### review:100
40
41 - `suspected_wash`:17
42 - `version_or_same_work`:17
43 - `fragment_reuse`:17
44 - `chorus_overlap`:17
45 - `translation_overlap`:16
46 - `general_borderline`:16
47
48 ### new:100
49
50 - `same_artist_different_title`:25
51 - `same_lyricist_different_title`:25
52 - `same_composer_different_title`:25
53 - `random_different_songs`:25
54
55 ## 歌词字段
56
57 根据对应 `*_lyrics_source_type` 读取:
58
59 - `inline``*_lyrics_url_or_text` 本身就是歌词。
60 - `url``*_lyrics_url_or_text` 是歌词地址。
61
62 ## 审核字段
63
64 ### review_final_class
65
66 这是三分类评估的主要人工标签:
67
68 - `duplicate`:主体歌词足以作为目录重复或近重复处理。
69 - `review`:存在局部复用、版本、翻唱、翻译、公共文本或权利不确定性,不能自动判 duplicate/new。
70 - `new`:没有足以进入 duplicate 或 review 的歌词复用信号。
71 - `cannot_judge`:歌词缺失、无法访问或文本质量不足。
72
73 ### review_content_relation
74
75 - `near_identical`
76 - `substantial_local_reuse`
77 - `limited_fragment`
78 - `weak_common_or_translation_only`
79 - `no_meaningful_reuse`
80 - `cannot_judge`
81
82 ### review_metadata_relation
83
84 - `same_work_same_version`
85 - `same_work_different_version`
86 - `different_work`
87 - `unknown`
88
89 ### review_policy_label
90
91 - `suspected_wash_reuse`
92 - `duplicate_or_same_record`
93 - `version_or_rights_review`
94 - `no_reuse_action`
95 - `insufficient_evidence`
96
97 ### review_confidence
98
99 - `high`
100 - `medium`
101 - `low`
102
103 `review_notes` 可记录复用段落、公共文本、制作名单误报、URL 异常或判断依据。
104
105 ## 审核建议
106
107 - 即使能看到旧结果,也请先阅读双方歌词,再填写人工标签。
108 - `sampling_class` 与人工判断不一致是测试集最有价值的部分,不需要迁就旧系统。
109 - 不要删除行或修改 `sample_id`
110 - 完成后返回原 CSV,即可计算三分类混淆矩阵和各 subtype 指标。
1 {
2 "generated_at": "2026-07-22T02:32:59.642717+00:00",
3 "source_csv": "hk_songs_import_staging.csv",
4 "seed": "lyric-three-way-v2-20260722",
5 "total_pairs": 300,
6 "class_counts": {
7 "duplicate": 100,
8 "new": 100,
9 "review": 100
10 },
11 "subtype_counts": {
12 "chorus_overlap": 17,
13 "exact_hash": 40,
14 "fragment_reuse": 17,
15 "general_borderline": 16,
16 "high_literal_overlap": 54,
17 "high_primary_translation_ignored": 6,
18 "random_different_songs": 25,
19 "same_artist_different_title": 25,
20 "same_composer_different_title": 25,
21 "same_lyricist_different_title": 25,
22 "suspected_wash": 17,
23 "translation_overlap": 16,
24 "version_or_same_work": 17
25 },
26 "legacy_decision_counts": {
27 "duplicate": 100,
28 "new": 100,
29 "review": 100
30 },
31 "source_pool_counts": {
32 "chorus_overlap": 248,
33 "exact_hash": 69,
34 "fragment_reuse": 123,
35 "general_borderline": 618,
36 "high_literal_overlap": 226,
37 "high_primary_translation_ignored": 6,
38 "suspected_wash": 72,
39 "translation_overlap": 114,
40 "version_or_same_work": 601
41 },
42 "streaming": {
43 "first_pass_rows": 120873,
44 "second_pass_rows": 120873,
45 "candidate_ids_requested": 332,
46 "candidate_rows_found": 332
47 },
48 "output": "datasets/lyric_reuse_review_v2.csv"
49 }
...@@ -29,6 +29,8 @@ from dotenv import load_dotenv ...@@ -29,6 +29,8 @@ from dotenv import load_dotenv
29 ROOT = Path(__file__).resolve().parent 29 ROOT = Path(__file__).resolve().parent
30 REPORT_DIR = ROOT / "output" / "reports" 30 REPORT_DIR = ROOT / "output" / "reports"
31 DASHBOARD = ROOT / "l2_review_dashboard.html" 31 DASHBOARD = ROOT / "l2_review_dashboard.html"
32 DATASET_REVIEW_PAGE = ROOT / "dataset_review.html"
33 DATASETS_DIR = ROOT / "datasets"
32 REPORT_DIR.mkdir(parents=True, exist_ok=True) 34 REPORT_DIR.mkdir(parents=True, exist_ok=True)
33 GROUP_INDEX_CACHE: dict[tuple[str, int, int, str], list[dict[str, object]]] = {} 35 GROUP_INDEX_CACHE: dict[tuple[str, int, int, str], list[dict[str, object]]] = {}
34 load_dotenv(ROOT / ".env") 36 load_dotenv(ROOT / ".env")
...@@ -1412,6 +1414,21 @@ class Handler(BaseHTTPRequestHandler): ...@@ -1412,6 +1414,21 @@ class Handler(BaseHTTPRequestHandler):
1412 except Exception as exc: # noqa: BLE001 - local diagnostic API 1414 except Exception as exc: # noqa: BLE001 - local diagnostic API
1413 _error(self, str(exc), status=404) 1415 _error(self, str(exc), status=404)
1414 return 1416 return
1417 if parsed.path == "/dataset-review":
1418 self._serve_file(DATASET_REVIEW_PAGE)
1419 return
1420 if parsed.path == "/api/dataset-review/samples":
1421 self._dataset_review_samples()
1422 return
1423 if parsed.path == "/api/dataset-review/manifest":
1424 self._dataset_review_manifest()
1425 return
1426 if parsed.path == "/api/dataset-review/stats":
1427 self._dataset_review_stats()
1428 return
1429 if parsed.path == "/api/dataset-review/guide":
1430 self._dataset_review_guide()
1431 return
1415 if parsed.path.startswith("/static/"): 1432 if parsed.path.startswith("/static/"):
1416 self._serve_file(ROOT / parsed.path.lstrip("/")) 1433 self._serve_file(ROOT / parsed.path.lstrip("/"))
1417 return 1434 return
...@@ -1538,6 +1555,50 @@ class Handler(BaseHTTPRequestHandler): ...@@ -1538,6 +1555,50 @@ class Handler(BaseHTTPRequestHandler):
1538 except Exception as exc: # noqa: BLE001 - local diagnostic API 1555 except Exception as exc: # noqa: BLE001 - local diagnostic API
1539 _error(self, str(exc), status=400) 1556 _error(self, str(exc), status=400)
1540 1557
1558 def _dataset_review_samples(self) -> None:
1559 csv_path = DATASETS_DIR / "lyric_reuse_review_v2.csv"
1560 if not csv_path.exists():
1561 _error(self, "测试集文件不存在: lyric_reuse_review_v2.csv", status=404)
1562 return
1563 try:
1564 samples = _read_csv(csv_path)
1565 _json_response(self, {"total": len(samples), "samples": samples})
1566 except Exception as exc: # noqa: BLE001
1567 _error(self, str(exc), status=500)
1568
1569 def _dataset_review_manifest(self) -> None:
1570 csv_path = DATASETS_DIR / "lyric_reuse_review_v2_manifest.csv"
1571 if not csv_path.exists():
1572 _json_response(self, {"rows": []})
1573 return
1574 try:
1575 rows = _read_csv(csv_path)
1576 _json_response(self, {"total": len(rows), "rows": rows})
1577 except Exception as exc: # noqa: BLE001
1578 _error(self, str(exc), status=500)
1579
1580 def _dataset_review_stats(self) -> None:
1581 stats_path = DATASETS_DIR / "lyric_reuse_review_v2_stats.json"
1582 if not stats_path.exists():
1583 _json_response(self, {})
1584 return
1585 try:
1586 data = json.loads(stats_path.read_text(encoding="utf-8"))
1587 _json_response(self, data)
1588 except Exception as exc: # noqa: BLE001
1589 _error(self, str(exc), status=500)
1590
1591 def _dataset_review_guide(self) -> None:
1592 guide_path = DATASETS_DIR / "lyric_reuse_review_v2_GUIDE.md"
1593 if not guide_path.exists():
1594 _json_response(self, {"text": ""})
1595 return
1596 try:
1597 text = guide_path.read_text(encoding="utf-8")
1598 _json_response(self, {"text": text})
1599 except Exception as exc: # noqa: BLE001
1600 _error(self, str(exc), status=500)
1601
1541 def _serve_file(self, path: Path) -> None: 1602 def _serve_file(self, path: Path) -> None:
1542 if not path.exists() or not path.is_file(): 1603 if not path.exists() or not path.is_file():
1543 _error(self, "not found", status=404) 1604 _error(self, "not found", status=404)
...@@ -1567,6 +1628,7 @@ def main() -> None: ...@@ -1567,6 +1628,7 @@ def main() -> None:
1567 _ensure_review_schema(apply_migration=args.migrate_review_schema) 1628 _ensure_review_schema(apply_migration=args.migrate_review_schema)
1568 server = ThreadingHTTPServer((args.host, args.port), Handler) 1629 server = ThreadingHTTPServer((args.host, args.port), Handler)
1569 print(f"L2 dashboard: http://{args.host}:{args.port}") 1630 print(f"L2 dashboard: http://{args.host}:{args.port}")
1631 print(f"Dataset review: http://{args.host}:{args.port}/dataset-review")
1570 server.serve_forever() 1632 server.serve_forever()
1571 1633
1572 1634
......