tests/test_composite_name_split.py
Rama master · réplica de lectura del repositorio original.
"""把一格装了多个艺名的别名拆成一人多名,以及不该被拆的四类括号。"""
import sqlite3
import tempfile
import unittest
from pathlib import Path
from peach.entities import split_composite_person_name
from peach.metadata import normalized_performers, split_former_name
from peach.metadata_auto_apply import _apply_performer_candidate
from peach.migrations import upgrade
from scripts.split_composite_aliases import (
CLEANUP_SOURCE, apply_review, apply_rows, collect,
)
ROOT = Path(__file__).resolve().parents[1]
class CompositePersonNameTests(unittest.TestCase):
def test_a_packed_romaji_field_becomes_one_name_per_person(self):
self.assertEqual(
split_composite_person_name("Ako Momona (Kou Akemi, Mari Koizumi)"),
["Ako Momona", "Kou Akemi", "Mari Koizumi"])
self.assertEqual(split_composite_person_name("Nao (Hitomi Koike)"),
["Nao", "Hitomi Koike"])
def test_a_bracket_repeating_the_name_collapses_to_one(self):
# 上游把括号无条件填满,`Rei Mizuna (Rei Mizuna)` 是模板产物不是两个人。
self.assertEqual(split_composite_person_name("Rei Mizuna (Rei Mizuna)"),
["Rei Mizuna"])
def test_names_carrying_dots_apostrophes_and_hyphens_still_split(self):
self.assertEqual(split_composite_person_name("A.J. O'Neil (Mary-Ann Doe)"),
["A.J. O'Neil", "Mary-Ann Doe"])
def test_the_other_four_kinds_of_bracket_are_left_whole(self):
# 括号里是厂牌消歧、角色出处、接稿状态、去重后缀——都不是这个人的另一个名字。
for name in ("AV DEBUT(本物人妻)", "アスナ(SAO)", "快慢扳机(接稿中)",
"kitty(1)", "Mana(23)", "プレステージプレミアム(PRESTIGEPREMIUM)",
"Egami(えがみ)", "Z(指定)ERO"):
self.assertEqual(split_composite_person_name(name), [name])
def test_a_plain_name_comes_back_as_itself(self):
self.assertEqual(split_composite_person_name(" 白石亚子 "), ["白石亚子"])
self.assertEqual(split_composite_person_name(""), [])
class CompositeAliasSplitTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.db = Path(self.tmp.name).resolve() / "ledger.db"
upgrade(self.db, ROOT / "migrations")
self.con = sqlite3.connect(self.db)
self.addCleanup(self.con.close)
self.con.executemany(
"INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)"
" VALUES(?,?,?,?,'t','t')",
[(10, "performer", "藤木美夏", "藤木美夏"),
(11, "performer", "里中静香", "里中静香"),
(12, "series", "AV DEBUT(本物人妻)", "av debut(本物人妻)"),
(13, "tag", "アスナ(SAO)", "アスナ(sao)"),
(14, "performer", "Kou Akemi", "kou akemi"),
(15, "performer", "新井爱丽", "新井爱丽")])
self.con.executemany(
"INSERT INTO entity_alias(entity_id,alias,normalized_alias,source,confidence)"
" VALUES(?,?,?,?,1.0)",
[(10, "Ako Momona (Kou Akemi, Mari Koizumi)",
"ako momona (kou akemi, mari koizumi)", "r18:performer"),
(11, "Rei Mizuna (Rei Mizuna)", "rei mizuna (rei mizuna)", "r18:performer"),
(12, "AV DEBUT (Real Married Woman)", "av debut (real married woman)",
"r18dev:series-localization"),
(15, "Elly Akira (Elly Arai)", "elly akira (elly arai)",
"merge:duplicate-identity")])
self.con.commit()
def _rows(self):
return {str(row["value"]): row for row in collect(self.con)}
def test_only_the_romaji_person_shape_is_marked_auto(self):
rows = self._rows()
self.assertEqual(rows["Elly Akira (Elly Arai)"]["verdict"], "auto")
self.assertEqual(rows["Rei Mizuna (Rei Mizuna)"]["verdict"], "auto")
# 系列别名的括号里是厂牌,标签的括号里是作品出处。
self.assertEqual(rows["AV DEBUT (Real Married Woman)"]["verdict"], "review")
self.assertEqual(rows["AV DEBUT(本物人妻)"]["verdict"], "review")
self.assertEqual(rows["アスナ(SAO)"]["verdict"], "review")
def test_a_part_that_is_another_entitys_canonical_name_goes_to_review(self):
# `Kou Akemi` 已经自己占了一条实体:那意味着可能要合并,不是拆别名顺手能定的。
row = self._rows()["Ako Momona (Kou Akemi, Mari Koizumi)"]
self.assertEqual(row["verdict"], "review")
self.assertIn("撞 entity 14", str(row["note"]))
def test_applying_replaces_the_packed_row_with_one_row_per_name(self):
moved = apply_rows(self.con, collect(self.con))
self.assertEqual(moved, {"added": 3, "removed": 2})
self.assertEqual(
self.con.execute("SELECT alias,source FROM entity_alias WHERE entity_id=15"
" ORDER BY alias").fetchall(),
[("Elly Akira", "merge:duplicate-identity"),
("Elly Arai", "merge:duplicate-identity")])
# 每一段都记在提供它的那个来源名下:这几个罗马字确实是它给的,只是被打包在一个字段里。
self.assertEqual(
self.con.execute("SELECT alias,source FROM entity_alias WHERE entity_id=11")
.fetchall(), [("Rei Mizuna", "r18:performer")])
def test_applying_leaves_every_review_row_untouched(self):
apply_rows(self.con, collect(self.con))
for entity_id, alias in ((10, "Ako Momona (Kou Akemi, Mari Koizumi)"),
(12, "AV DEBUT (Real Married Woman)")):
self.assertEqual(
self.con.execute("SELECT count(*) FROM entity_alias"
" WHERE entity_id=? AND alias=?",
(entity_id, alias)).fetchone()[0], 1)
self.assertEqual(
self.con.execute("SELECT canonical_name FROM entity WHERE id=13")
.fetchone()[0], "アスナ(SAO)")
class ReviewedNameCleanupTests(unittest.TestCase):
"""形状分不开、只有人能判的那些:复核 CSV 填 `verdict` 与 `target` 再执行。"""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.db = Path(self.tmp.name).resolve() / "ledger.db"
upgrade(self.db, ROOT / "migrations")
self.con = sqlite3.connect(self.db)
self.addCleanup(self.con.close)
self.con.executemany(
"INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)"
" VALUES(?,?,?,?,'t','t')",
[(20, "creator", "幼月月@一日目(土)東オ54a", "幼月月@一日目(土)東オ54a"),
(21, "studio", "プレステージプレミアム", "プレステージプレミアム"),
(22, "performer", "白石亚子", "白石亚子"),
(23, "creator", "みどりふう", "みどりふう")])
self.con.execute(
"INSERT INTO entity_alias(entity_id,alias,normalized_alias,source,confidence)"
" VALUES(21,'プレステージプレミアム(PRESTIGEPREMIUM)',"
"'プレステージプレミアム(prestigepremium)','javdb:studio',1.0)")
self.con.executemany(
"INSERT INTO asset(id,location,path,name,medium,creator)"
" VALUES(?,'online',?,?,'image',?)",
[(1, "pixiv/1", "a", "幼月月@一日目(土)東オ54a"),
(2, "pixiv/2", "b", "幼月月@一日目(土)東オ54a"),
(3, "pixiv/3", "c", "みどりふう")])
self.con.execute(
"INSERT INTO asset_entity(asset_id,entity_id,role,source,confidence)"
" VALUES(1,20,'creator','pixiv',1.0)")
self.con.commit()
def _row(self, verdict, entity_id, value, target, kind="creator", source=""):
return {"verdict": verdict, "kind": kind, "entity_id": str(entity_id),
"value": value, "target": target, "source": source}
def test_stripping_a_booth_suffix_renames_the_entity_and_its_assets(self):
done = apply_review(self.con, [
self._row("strip", 20, "幼月月@一日目(土)東オ54a", "幼月月")])
self.assertEqual((done["strip"], done["flat"]), (1, 2))
self.assertEqual(
self.con.execute("SELECT canonical_name,normalized_name FROM entity"
" WHERE id=20").fetchone(), ("幼月月", "幼月月"))
# 扁平投影跟着改,否则卡片还写着旧名、按旧名照样搜得到。
self.assertEqual(
sorted(name for (name,) in self.con.execute(
"SELECT creator FROM asset WHERE creator IS NOT NULL")),
["みどりふう", "幼月月", "幼月月"])
def test_the_stripped_name_stays_as_an_alias_under_the_cleanup_source(self):
apply_review(self.con, [self._row("strip", 20, "幼月月@一日目(土)東オ54a", "幼月月")])
self.assertEqual(
self.con.execute("SELECT alias,source FROM entity_alias WHERE entity_id=20")
.fetchall(), [("幼月月@一日目(土)東オ54a", CLEANUP_SOURCE)])
def test_splitting_a_studio_alias_keeps_the_source_that_gave_it(self):
done = apply_review(self.con, [
self._row("split", 21, "プレステージプレミアム(PRESTIGEPREMIUM)",
"プレステージプレミアム | PRESTIGEPREMIUM",
kind="studio", source="javdb:studio")])
self.assertEqual(done["split"], 1)
self.assertEqual(
sorted(alias for (alias,) in self.con.execute(
"SELECT alias FROM entity_alias WHERE entity_id=21")),
["PRESTIGEPREMIUM", "プレステージプレミアム"])
def test_a_target_taken_by_another_entity_is_refused_not_written(self):
# 撞上另一条实体的规范名意味着两条可能该合并,那是合并要回答的问题。
done = apply_review(self.con, [
self._row("strip", 20, "幼月月@一日目(土)東オ54a", "みどりふう")])
self.assertEqual(done["strip"], 0)
self.assertEqual(len(done["blocked"]), 1)
self.assertIn("撞 entity 23", done["blocked"][0])
self.assertEqual(
self.con.execute("SELECT canonical_name FROM entity WHERE id=20").fetchone()[0],
"幼月月@一日目(土)東オ54a")
def test_rows_left_at_review_do_nothing(self):
done = apply_review(self.con, [
self._row("review", 20, "幼月月@一日目(土)東オ54a", "幼月月"),
self._row("strip", 20, "幼月月@一日目(土)東オ54a", "")])
self.assertEqual((done["strip"], done["split"], done["flat"]), (0, 0, 0))
self.assertEqual(
self.con.execute("SELECT canonical_name FROM entity WHERE id=20").fetchone()[0],
"幼月月@一日目(土)東オ54a")
def test_a_performer_strip_rewrites_the_flat_tag(self):
self.con.execute(
"INSERT INTO asset(id,location,path,name,medium)"
" VALUES(4,'online','pixiv/4','d','image')")
self.con.execute(
"INSERT INTO asset_entity(asset_id,entity_id,role,source,confidence)"
" VALUES(4,22,'performer','javdb',1.0)")
self.con.execute(
"INSERT INTO asset_tag(asset_id,tag,confidence,source)"
" VALUES(4,'演员:白石亚子',1.0,'javdb')")
done = apply_review(self.con, [
self._row("strip", 22, "白石亚子", "白石 あこ", kind="performer")])
self.assertEqual((done["strip"], done["flat"]), (1, 1))
self.assertEqual(
[tag for (tag,) in self.con.execute(
"SELECT tag FROM asset_tag WHERE asset_id=4")], ["演员:白石 あこ"])
class FormerNameTests(unittest.TestCase):
"""女优栏里的「现名(旧名)」:拆成一个人的两个名字,挂到账本里已有的那一位。"""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.db = Path(self.tmp.name).resolve() / "ledger.db"
upgrade(self.db, ROOT / "migrations")
self.con = sqlite3.connect(self.db)
self.con.row_factory = sqlite3.Row
self.addCleanup(self.con.close)
self.con.executemany(
"INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at)"
" VALUES(?,'performer',?,?,'t','t')",
[(20, "美谷朱音", "美谷朱音"), (21, "姬川优奈", "姬川优奈")])
self.con.execute(
"INSERT INTO entity_alias(entity_id,alias,normalized_alias,source,confidence)"
" VALUES(21,'月城らん','月城らん','test',1.0)")
self.con.execute(
"INSERT INTO asset(id,location,path,name,medium,code,size)"
" VALUES(1,'R:','R:\\media\\MIZD-997.mp4','MIZD-997.mp4','video','MIZD-997',1)")
def land(self, raw_name: str, external_id: str = "") -> int:
performers, _warnings = normalized_performers(
[{"japanese_name": raw_name, "dmm_id": external_id, "profile_source": "r18dev"}])
_apply_performer_candidate(self.con, [1], {"value": performers}, source="r18dev",
confidence=0.9, metadata={}, now="t")
return self.con.execute(
"SELECT entity_id FROM asset_entity WHERE asset_id=1 AND role='performer'"
).fetchone()[0]
def test_current_and_former_names_split_apart(self):
self.assertEqual(split_former_name("美谷朱音(美谷朱里)"), ("美谷朱音", "美谷朱里"))
self.assertEqual(split_former_name("七海ひな(旧・七瀬ひな)"), ("七海ひな", "七瀬ひな"))
performers, _warnings = normalized_performers([{"japanese_name": "美谷朱音(美谷朱里)"}])
self.assertEqual((performers[0]["name"], performers[0]["aliases"],
performers[0]["former_names"]),
("美谷朱音", ["美谷朱里"], ["美谷朱里"]))
def test_brackets_that_are_not_a_former_name_stay_whole(self):
# 年龄、描述、罗马字、几个旧名挤在一格:都不是「一个现名配一个旧名」。
for name in ("Mana(23)", "みく(素人)", "Rei Mizuna (Rei Mizuna)",
"美谷朱音(美谷朱里、朱里)", "ゆい(20歳)"):
self.assertEqual(split_former_name(name), (name, ""))
def test_the_current_name_lands_on_the_entity_already_there(self):
self.assertEqual(self.land("美谷朱音(美谷朱里)", "1039982"), 20)
self.assertEqual(self.con.execute(
"SELECT count(*) FROM entity WHERE kind='performer'").fetchone()[0], 2)
self.assertEqual([row[0] for row in self.con.execute(
"SELECT tag FROM asset_tag WHERE asset_id=1")], ["演员:美谷朱音"])
def test_a_new_current_name_lands_on_whoever_holds_the_former_one(self):
self.assertEqual(self.land("姫川ゆうな(月城らん)"), 21)
self.assertEqual(self.con.execute(
"SELECT count(*) FROM entity WHERE kind='performer'").fetchone()[0], 2)
self.assertEqual(self.con.execute(
"SELECT source FROM entity_alias WHERE entity_id=21 AND alias='姫川ゆうな'"
).fetchone()[0], "javinizer:current-name")
if __name__ == "__main__":
unittest.main()