Commit 4f7a9366 4f7a93667fdbbcf19c8c9db4260719617d077f5c by 沈秋雨

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

1 parent a52c778d
# 歌词复用测试集 v1 审核说明
## 文件
- `lyric_reuse_review_v1.csv`:320 对盲审样本,实际审核使用这个文件。
- `lyric_reuse_review_v1_manifest.csv`:抽样分层和旧系统弱标签。完成盲审前不要打开,避免旧结论影响判断。
- `lyric_reuse_review_v1_stats.json`:构建参数、候选池规模和分层数量。
盲审表已经随机打散,不包含抽样分层、旧去重结论或旧置信度。
## 歌词字段
`query_lyrics_url_or_text``candidate_lyrics_url_or_text` 根据对应的 `*_lyrics_source_type` 解释:
- `inline`:字段内容本身就是歌词原文。
- `url`:字段是歌词地址,需要访问地址查看原文。
## 审核顺序
先判断内容关系,再看标题、歌手和词曲作者判断作品关系,最后给出业务政策标签。不要因为标题或作者相同就改变内容关系标签。
### 1. review_content_relation
只判断歌词内容事实,填写以下一个值:
- `near_identical`:主体歌词基本一致,差异主要是格式、重复次数或极少改动。
- `substantial_local_reuse`:存在较长或多段实质复用,但整首并非基本一致。
- `limited_fragment`:存在可确认的小段复用,覆盖范围有限。
- `weak_common_or_translation_only`:只有常见短句、副歌套话或翻译层面的弱重合。
- `no_meaningful_reuse`:没有值得关注的歌词复用。
- `cannot_judge`:歌词缺失、语言或文本质量导致无法判断。
### 2. review_metadata_relation
结合标题、歌手和词曲作者填写:
- `same_work_same_version`
- `same_work_different_version`
- `different_work`
- `unknown`
这里判断的是作品/版本关系,不代表已经确认授权。
### 3. review_policy_label
填写建议的业务队列:
- `suspected_wash_reuse`:内容存在实质复用,且元数据不支持合理的同作品/版本关系。
- `duplicate_or_same_record`:同一作品且歌词基本重复,适合按重复记录处理。
- `version_or_rights_review`:可能是翻唱、Remix、同词不同曲、公共文本或其他版本/权利问题。
- `no_reuse_action`:不需要因歌词复用采取动作。
- `insufficient_evidence`:证据不足,暂时无法分流。
政策标签不是侵权裁定;授权状态仍需版权信息确认。
### 4. review_confidence
- `high`
- `medium`
- `low`
### 5. review_notes
自由填写关键依据,例如:
- 复用发生在哪一段
- 仅制作名单相同
- 仅译文相同
- 同标题但词作者不同
- 疑似古诗词或公共文本
- URL 无法访问
## 建议审核方式
- 第一轮先审核全部样本,不查看 manifest。
-`cannot_judge``unknown` 和低置信度记录进行第二轮复核。
- 完成后保留原始 `sample_id`,不要重新排序或删除行。
- 将填好的盲审 CSV 返回后,再结合 manifest 计算各分层召回率、误报率和政策分流结果。
{
"generated_at": "2026-07-22T01:57:04.122612+00:00",
"source_csv": "hk_songs_import_staging.csv",
"seed": "lyric-review-v1-20260722",
"sample_size_per_stratum": 40,
"total_pairs": 320,
"stratum_counts": {
"chorus_overlap": 40,
"exact_or_near_duplicate": 40,
"fragment_reuse": 40,
"general_borderline": 40,
"negative_control": 40,
"suspected_wash": 40,
"translation_overlap": 40,
"version_or_same_work": 40
},
"control_counts": {
"random_different_songs": 20,
"same_artist_different_title": 20
},
"source_pool_counts": {
"chorus_overlap": 248,
"exact_or_near_duplicate": 301,
"fragment_reuse": 123,
"general_borderline": 618,
"suspected_wash": 72,
"translation_overlap": 114,
"version_or_same_work": 601
},
"streaming": {
"first_pass_rows": 120873,
"second_pass_rows": 120873,
"candidate_ids_requested": 381,
"candidate_rows_found": 381
},
"outputs": {
"blind_review_csv": "datasets/lyric_reuse_review_v1.csv",
"sampling_manifest_csv": "datasets/lyric_reuse_review_v1_manifest.csv"
}
}
# 歌词 duplicate / review / new 三分类测试集 v2
## 测试集定位
`lyric_reuse_review_v2.csv` 共 300 对,按原系统结果分层抽样:
- `duplicate`:100
- `review`:100
- `new`:100
表中的 `sampling_class``legacy_dedup_decision` 是抽样依据及原系统结果,不是人工真值。请在 `review_final_class` 中填写你的独立判断。
## 需要特别注意的字段
- `sampling_class`:本次三分类抽样主类。
- `sampling_subtype`:类内细分场景。
- `legacy_pair_source`:旧系统是否实际比较过这一对。
- `legacy_dedup_decision`:原系统的 `new/duplicate/review` 结果。
- `legacy_dedup_confidence``legacy_dedup_reason`:原系统依据。
`duplicate``review` 都来自原系统真实的 `matched_song_id` 对,`legacy_pair_source=existing_matched_pair`
`new` 类由于原 CSV 没有保存被判 new 时的候选对,因此由两条原系统分别判为 new 的歌曲构造:
```text
legacy_pair_source=synthetic_pair_from_two_legacy_new_records
```
这表示两首歌各自的原结果是 new,不表示旧系统曾直接比较过这两个对象。
## 类内细分
### duplicate:100
- `exact_hash`:40
- `high_literal_overlap`:54
- `high_primary_translation_ignored`:6(候选池仅有 6 条,全部纳入)
### review:100
- `suspected_wash`:17
- `version_or_same_work`:17
- `fragment_reuse`:17
- `chorus_overlap`:17
- `translation_overlap`:16
- `general_borderline`:16
### new:100
- `same_artist_different_title`:25
- `same_lyricist_different_title`:25
- `same_composer_different_title`:25
- `random_different_songs`:25
## 歌词字段
根据对应 `*_lyrics_source_type` 读取:
- `inline``*_lyrics_url_or_text` 本身就是歌词。
- `url``*_lyrics_url_or_text` 是歌词地址。
## 审核字段
### review_final_class
这是三分类评估的主要人工标签:
- `duplicate`:主体歌词足以作为目录重复或近重复处理。
- `review`:存在局部复用、版本、翻唱、翻译、公共文本或权利不确定性,不能自动判 duplicate/new。
- `new`:没有足以进入 duplicate 或 review 的歌词复用信号。
- `cannot_judge`:歌词缺失、无法访问或文本质量不足。
### review_content_relation
- `near_identical`
- `substantial_local_reuse`
- `limited_fragment`
- `weak_common_or_translation_only`
- `no_meaningful_reuse`
- `cannot_judge`
### review_metadata_relation
- `same_work_same_version`
- `same_work_different_version`
- `different_work`
- `unknown`
### review_policy_label
- `suspected_wash_reuse`
- `duplicate_or_same_record`
- `version_or_rights_review`
- `no_reuse_action`
- `insufficient_evidence`
### review_confidence
- `high`
- `medium`
- `low`
`review_notes` 可记录复用段落、公共文本、制作名单误报、URL 异常或判断依据。
## 审核建议
- 即使能看到旧结果,也请先阅读双方歌词,再填写人工标签。
- `sampling_class` 与人工判断不一致是测试集最有价值的部分,不需要迁就旧系统。
- 不要删除行或修改 `sample_id`
- 完成后返回原 CSV,即可计算三分类混淆矩阵和各 subtype 指标。
{
"generated_at": "2026-07-22T02:32:59.642717+00:00",
"source_csv": "hk_songs_import_staging.csv",
"seed": "lyric-three-way-v2-20260722",
"total_pairs": 300,
"class_counts": {
"duplicate": 100,
"new": 100,
"review": 100
},
"subtype_counts": {
"chorus_overlap": 17,
"exact_hash": 40,
"fragment_reuse": 17,
"general_borderline": 16,
"high_literal_overlap": 54,
"high_primary_translation_ignored": 6,
"random_different_songs": 25,
"same_artist_different_title": 25,
"same_composer_different_title": 25,
"same_lyricist_different_title": 25,
"suspected_wash": 17,
"translation_overlap": 16,
"version_or_same_work": 17
},
"legacy_decision_counts": {
"duplicate": 100,
"new": 100,
"review": 100
},
"source_pool_counts": {
"chorus_overlap": 248,
"exact_hash": 69,
"fragment_reuse": 123,
"general_borderline": 618,
"high_literal_overlap": 226,
"high_primary_translation_ignored": 6,
"suspected_wash": 72,
"translation_overlap": 114,
"version_or_same_work": 601
},
"streaming": {
"first_pass_rows": 120873,
"second_pass_rows": 120873,
"candidate_ids_requested": 332,
"candidate_rows_found": 332
},
"output": "datasets/lyric_reuse_review_v2.csv"
}
......@@ -29,6 +29,8 @@ from dotenv import load_dotenv
ROOT = Path(__file__).resolve().parent
REPORT_DIR = ROOT / "output" / "reports"
DASHBOARD = ROOT / "l2_review_dashboard.html"
DATASET_REVIEW_PAGE = ROOT / "dataset_review.html"
DATASETS_DIR = ROOT / "datasets"
REPORT_DIR.mkdir(parents=True, exist_ok=True)
GROUP_INDEX_CACHE: dict[tuple[str, int, int, str], list[dict[str, object]]] = {}
load_dotenv(ROOT / ".env")
......@@ -1412,6 +1414,21 @@ class Handler(BaseHTTPRequestHandler):
except Exception as exc: # noqa: BLE001 - local diagnostic API
_error(self, str(exc), status=404)
return
if parsed.path == "/dataset-review":
self._serve_file(DATASET_REVIEW_PAGE)
return
if parsed.path == "/api/dataset-review/samples":
self._dataset_review_samples()
return
if parsed.path == "/api/dataset-review/manifest":
self._dataset_review_manifest()
return
if parsed.path == "/api/dataset-review/stats":
self._dataset_review_stats()
return
if parsed.path == "/api/dataset-review/guide":
self._dataset_review_guide()
return
if parsed.path.startswith("/static/"):
self._serve_file(ROOT / parsed.path.lstrip("/"))
return
......@@ -1538,6 +1555,50 @@ class Handler(BaseHTTPRequestHandler):
except Exception as exc: # noqa: BLE001 - local diagnostic API
_error(self, str(exc), status=400)
def _dataset_review_samples(self) -> None:
csv_path = DATASETS_DIR / "lyric_reuse_review_v2.csv"
if not csv_path.exists():
_error(self, "测试集文件不存在: lyric_reuse_review_v2.csv", status=404)
return
try:
samples = _read_csv(csv_path)
_json_response(self, {"total": len(samples), "samples": samples})
except Exception as exc: # noqa: BLE001
_error(self, str(exc), status=500)
def _dataset_review_manifest(self) -> None:
csv_path = DATASETS_DIR / "lyric_reuse_review_v2_manifest.csv"
if not csv_path.exists():
_json_response(self, {"rows": []})
return
try:
rows = _read_csv(csv_path)
_json_response(self, {"total": len(rows), "rows": rows})
except Exception as exc: # noqa: BLE001
_error(self, str(exc), status=500)
def _dataset_review_stats(self) -> None:
stats_path = DATASETS_DIR / "lyric_reuse_review_v2_stats.json"
if not stats_path.exists():
_json_response(self, {})
return
try:
data = json.loads(stats_path.read_text(encoding="utf-8"))
_json_response(self, data)
except Exception as exc: # noqa: BLE001
_error(self, str(exc), status=500)
def _dataset_review_guide(self) -> None:
guide_path = DATASETS_DIR / "lyric_reuse_review_v2_GUIDE.md"
if not guide_path.exists():
_json_response(self, {"text": ""})
return
try:
text = guide_path.read_text(encoding="utf-8")
_json_response(self, {"text": text})
except Exception as exc: # noqa: BLE001
_error(self, str(exc), status=500)
def _serve_file(self, path: Path) -> None:
if not path.exists() or not path.is_file():
_error(self, "not found", status=404)
......@@ -1567,6 +1628,7 @@ def main() -> None:
_ensure_review_schema(apply_migration=args.migrate_review_schema)
server = ThreadingHTTPServer((args.host, args.port), Handler)
print(f"L2 dashboard: http://{args.host}:{args.port}")
print(f"Dataset review: http://{args.host}:{args.port}/dataset-review")
server.serve_forever()
......