You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
162 lines
4.8 KiB
162 lines
4.8 KiB
#!/usr/bin/env python3
|
|
"""校验 lyric.md 歌词格式是否符合要求。
|
|
|
|
规则:
|
|
- 时间码格式正确:M:SS 或 MM:SS
|
|
- 时间段必须连续,不能有间隙或重叠
|
|
- 时长不能为负或零
|
|
- 纯音乐段不应包含歌词
|
|
- 歌词段至少包含 1 行歌词
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
TIME_RE = re.compile(r"^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$")
|
|
TAG_RE = re.compile(r"^\[(prelude|interlude|outro)\]$")
|
|
|
|
|
|
def parse_time(minutes: str, seconds: str) -> float:
|
|
return int(minutes) * 60 + int(seconds)
|
|
|
|
|
|
def parse_lyric_file(path: str) -> list[dict]:
|
|
"""解析 lyric.md,返回段落列表。"""
|
|
text = Path(path).read_text(encoding="utf-8")
|
|
|
|
# 提取 ``` 代码块内容
|
|
m = re.search(r"```\n(.*?)```", text, re.DOTALL)
|
|
if not m:
|
|
print("ERROR: 未找到 ``` 代码块")
|
|
sys.exit(1)
|
|
|
|
content = m.group(1).strip()
|
|
segments: list[dict] = []
|
|
current_tag: str | None = None
|
|
|
|
for line in content.split("\n"):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
|
|
tag_match = TAG_RE.match(line)
|
|
if tag_match:
|
|
current_tag = tag_match.group(1)
|
|
continue
|
|
|
|
time_match = TIME_RE.match(line)
|
|
if time_match:
|
|
start = parse_time(time_match.group(1), time_match.group(2))
|
|
end = parse_time(time_match.group(3), time_match.group(4))
|
|
segments.append({
|
|
"start": start,
|
|
"end": end,
|
|
"tag": current_tag,
|
|
"lines": [],
|
|
"raw_time": line,
|
|
})
|
|
current_tag = None # tag 只作用于紧跟的段落
|
|
continue
|
|
|
|
# 歌词行
|
|
if segments:
|
|
segments[-1]["lines"].append(line)
|
|
|
|
return segments
|
|
|
|
|
|
def validate(segments: list[dict]) -> list[str]:
|
|
"""校验段落,返回警告/错误列表。"""
|
|
issues: list[str] = []
|
|
|
|
for i, seg in enumerate(segments):
|
|
duration = seg["end"] - seg["start"]
|
|
time_label = seg["raw_time"]
|
|
is_instrumental = seg["tag"] is not None
|
|
line_count = len(seg["lines"])
|
|
|
|
# 时长为负或零
|
|
if duration <= 0:
|
|
issues.append(f"ERROR [{time_label}] 时长无效 ({duration:.0f}s)")
|
|
continue
|
|
|
|
# 单段时长上限检查 (不超过20s)
|
|
if duration > 20:
|
|
issues.append(f"WARN [{time_label}] 时长过长 ({duration:.0f}s > 20s)")
|
|
|
|
if is_instrumental:
|
|
if line_count > 0:
|
|
issues.append(
|
|
f"WARN [{time_label}] [{seg['tag']}] 纯音乐段包含 {line_count} 行歌词"
|
|
)
|
|
else:
|
|
if line_count < 1:
|
|
issues.append(
|
|
f"WARN [{time_label}] 歌词段无歌词内容"
|
|
)
|
|
|
|
# 连续性检查
|
|
if i > 0:
|
|
prev_end = segments[i - 1]["end"]
|
|
if abs(seg["start"] - prev_end) > 0.01:
|
|
issues.append(
|
|
f"ERROR [{time_label}] 与上一段不连续 "
|
|
f"(上段结束 {prev_end:.0f}s, 本段开始 {seg['start']:.0f}s)"
|
|
)
|
|
|
|
return issues
|
|
|
|
|
|
def print_summary(segments: list[dict], issues: list[str]) -> None:
|
|
total = len(segments)
|
|
instrumental = sum(1 for s in segments if s["tag"])
|
|
lyric_segs = total - instrumental
|
|
total_duration = segments[-1]["end"] - segments[0]["start"] if segments else 0
|
|
|
|
print(f"=== 歌词校验报告 ===\n")
|
|
print(f"总时长: {int(total_duration // 60)}:{int(total_duration % 60):02d}")
|
|
print(f"总段数: {total} (歌词段 {lyric_segs}, 纯音乐段 {instrumental})")
|
|
print()
|
|
|
|
# 段落明细
|
|
print(f"{'时间码':<18} {'时长':>5} {'行数':>4} {'类型':<10}")
|
|
print("-" * 45)
|
|
for seg in segments:
|
|
duration = seg["end"] - seg["start"]
|
|
seg_type = f"[{seg['tag']}]" if seg["tag"] else "歌词"
|
|
print(f"{seg['raw_time']:<18} {duration:>4.0f}s {len(seg['lines']):>4} {seg_type:<10}")
|
|
|
|
print()
|
|
if issues:
|
|
print(f"发现 {len(issues)} 个问题:\n")
|
|
for issue in issues:
|
|
print(f" {issue}")
|
|
else:
|
|
print("ALL PASS")
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
# 默认查找所有 lyric.md
|
|
base = Path(__file__).resolve().parent.parent / ".mv"
|
|
files = list(base.glob("*/lyric.md"))
|
|
if not files:
|
|
print("用法: python validate_lyric.py <lyric.md 路径>")
|
|
sys.exit(1)
|
|
else:
|
|
files = [Path(sys.argv[1])]
|
|
|
|
for path in files:
|
|
print(f"\n--- {path} ---\n")
|
|
segments = parse_lyric_file(str(path))
|
|
issues = validate(segments)
|
|
print_summary(segments, issues)
|
|
|
|
# 有 ERROR 则返回非零退出码
|
|
has_error = any("ERROR" in i for i in issues)
|
|
sys.exit(1 if has_error else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|