fix(music): 缩写点号还原与包含/演出后缀弱匹配修复失配

- metamusic: 连续单字母空格序列还原为点号缩写(S H E -> S.H.E),
  在场景点分之后执行避免点号再被压平
- metamusic: 规格段残留判定收紧,发布组短词仅在格式词无空格连字符
  形态(FLAC-HHWEB)放行,修复自然语言曲名(Ashielf Alpen)被误剥
- musicbrainz: 专辑候选新增包含关系弱匹配(原声带条目带额外前缀)
  与演出后缀弱匹配(S.H.E十七音乐会 -> 条目「十七」)
- 补充 metamusic 缩写还原与 musicbrainz 弱匹配测试案例
This commit is contained in:
jxxghp
2026-08-11 10:24:45 +08:00
parent 19937b210b
commit ae393456bd
4 changed files with 116 additions and 7 deletions
+34 -5
View File
@@ -208,14 +208,14 @@ _MUSIC_QUALITY_TOKEN_RE = re.compile(
# 演唱会/音乐视频种子的视频编码标记:分辨率、编码、容器与声道描述,
# 不是音乐文本信息,不剔除会污染曲名并阻断艺术家/曲名拆分
_MUSIC_VIDEO_TOKEN_RE = re.compile(rf"\b(?:{_MUSIC_VIDEO_TOKEN_ALT})\b", re.IGNORECASE)
# 尾部规格段判定:整段仅由格式词、视频标记、位深采样无损声明词与发布组标签组成,
# 曲名含任何自然语言文本时判定失败,保证「曲名 (注释) - WEB-DL」不被误剥
# 尾部规格段判定:整段仅由格式词、视频标记、位深采样无损声明词组成,
# 曲名含任何自然语言文本时判定失败,保证「曲名 (注释) - WEB-DL」不被误剥
# 发布组标签(HHWEB/FHDMv)不是固定词表,由 _strip_spec_segments 的短词规则另行放行
_MUSIC_SPEC_SEGMENT_RE = re.compile(
rf"^(?:\b(?:{_MUSIC_FORMAT_TOKEN_ALT}|{_MUSIC_VIDEO_TOKEN_ALT}|single|ep|album)\b"
r"|\d{1,3}\s*-?\s*bits?|\d{2,4}(?:(?:[.]|\s)\d)?\s*k(?:hz|bps?)"
r"|lossless|无损音质|无损|分轨|整轨|合集|精选"
r"|[\s\-–—−-/+]+(?![\s\-–—−-/+])"
r"|(?:(?=[A-Za-z0-9]*[A-Za-z])[A-Za-z0-9]{3,})"
r"|[\s\-–—−-/+]"
r")+$",
re.IGNORECASE,
)
@@ -364,6 +364,18 @@ def _normalize_scene_dots(value: str) -> str:
return _SCENE_DOT_RE.sub(" ", value)
# 连续单字母空格序列是缩写点号被全角归一/场景点分压平的结果(S.H.E -> S H E),
# 还原为点号缩写(S.H.E)才能与 MusicBrainz 条目署名比对
_LETTER_RUN_RE = re.compile(r"(?<![A-Za-z])((?:[A-Za-z] ){2,}[A-Za-z])(?![A-Za-z])")
def _restore_letter_abbrev(value: str) -> str:
"""把连续单字母空格序列还原为点号缩写(S H E -> S.H.E)。"""
return _LETTER_RUN_RE.sub(
lambda m: ".".join(m.group(1).split(" ")), str(value or "")
)
class MetaMusic(MetaBase):
"""音乐文件名及音频标签解析结果,作为 MetaBase 的音乐分支实现。"""
@@ -598,6 +610,8 @@ class MetaMusic(MetaBase):
text = _to_halfwidth(str(value or "")).replace("_", " ")
# 场景命名用点号分隔单词(Shan.Ge.Liao.Zai.2023),归一为空格便于拆分检索
text = _normalize_scene_dots(text)
# 缩写点号被全角归一/场景点分压平成单字母空格序列,在点分之后还原(S H E -> S.H.E
text = _restore_letter_abbrev(text)
return _MUSIC_SPACES_RE.sub(" ", text).strip()
@classmethod
@@ -645,7 +659,7 @@ class MetaMusic(MetaBase):
# 加号是 APE+CUE 类格式联合写法的分隔符,占位为空格后再判定
probe = _MUSIC_VIDEO_TOKEN_RE.sub(" ", _MUSIC_QUALITY_TOKEN_RE.sub(" ", probe))
residue = probe.replace("+", " ").strip()
if residue and not _MUSIC_SPEC_SEGMENT_RE.fullmatch(residue):
if residue and not cls._is_spec_residue(residue, bool(prefix)):
return text
# 不含任何规格词的纯标签段,仅在紧跟格式词的无空格形态下才是发布组标签;
# 「艺术家 - 单词曲名」的曲名段不含规格词,必须保留
@@ -653,6 +667,21 @@ class MetaMusic(MetaBase):
return text
text = text[: segment_match.start()].rstrip()
@classmethod
def _is_spec_residue(cls, residue: str, has_prefix: bool) -> bool:
"""判定规格词替换后的残留是否为发布组标签而非自然语言曲名。
发布组标签只出现在格式词无空格连字符形态(FLAC-HHWEB、AAC-FHDMv),
此时短字母数字组合可放行;空白连字符分隔的段(艺术家 - 曲名 FLAC)
残留可能是自然语言曲名,仅严格规格词表可过。
"""
if _MUSIC_SPEC_SEGMENT_RE.fullmatch(residue):
return True
tokens = residue.split()
return has_prefix and bool(tokens) and all(
len(token) <= 8 and re.fullmatch(r"[A-Za-z0-9]+", token) for token in tokens
)
@classmethod
def _strip_quality_tokens(cls, value: str) -> str:
"""剥离音频格式、视频编码、规格参数与年份括号,保留有效的艺术家与曲名文本。"""