feat(dataset-review): 添加歌词三分类测试集查看页面与接口
Showing
7 changed files
with
339 additions
and
0 deletions
dataset_review.html
0 → 100644
This diff is collapsed.
Click to expand it.
datasets/lyric_reuse_review_v1_GUIDE.md
0 → 100644
| 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 计算各分层召回率、误报率和政策分流结果。 |
datasets/lyric_reuse_review_v1_stats.json
0 → 100644
| 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 | } |
datasets/lyric_reuse_review_v2_GUIDE.md
0 → 100644
| 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 指标。 |
datasets/lyric_reuse_review_v2_stats.json
0 → 100644
| 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 | ... | ... |
-
Please register or sign in to post a comment