Нарезка аудиокниги по главам: треки с ID3-тегами вместо одного слитного файла
This commit is contained in:
@@ -89,11 +89,75 @@ def test_verify_detects_empty_file(tmp: Path):
|
||||
assert a.cmd_verify(ns) is False, "пустой файл должен завалить проверку"
|
||||
|
||||
|
||||
def build_book(tmp: Path, chapters=2, paras=3):
|
||||
"""Готовый синтез: подготовленные главы + фрагменты в temp_audio/."""
|
||||
tts, ab = tmp / "tts", tmp / "audiobook"
|
||||
tts.mkdir()
|
||||
temp = ab / "temp_audio"
|
||||
temp.mkdir(parents=True)
|
||||
for n in range(chapters):
|
||||
write_chapter(tts, n, ["х" * 100] * paras)
|
||||
mp3(temp / ("chapter_%03d_intro.mp3" % n), 1)
|
||||
for i in range(paras):
|
||||
mp3(temp / ("chapter_%03d_para_%04d.mp3" % (n, i)), 2)
|
||||
return tts, ab
|
||||
|
||||
|
||||
def split_ns(tts, ab, out, **kw):
|
||||
fields = {"translations": tts, "audiobook": ab, "output": out,
|
||||
"album": "Книга", "author": "Автор", "gap": 0.3, "force": False}
|
||||
fields.update(kw)
|
||||
return type("ns", (), fields)
|
||||
|
||||
|
||||
def test_split_makes_chapter_tracks(tmp: Path):
|
||||
tts, ab = build_book(tmp, chapters=2, paras=3)
|
||||
out = tmp / "tracks"
|
||||
assert a.cmd_split(split_ns(tts, ab, out)) is True
|
||||
tracks = sorted(out.glob("*.mp3"))
|
||||
assert len(tracks) == 2, "по треку на главу"
|
||||
assert tracks[0].name.startswith("000 - "), tracks[0].name
|
||||
assert not (out / ".tmp").exists(), "временный каталог должен убираться"
|
||||
|
||||
# длительность: 1 вводный + 3 абзаца + 3 паузы = 1 + 6 + 0.9 ≈ 7.9 с
|
||||
d = a.duration(tracks[0])
|
||||
assert 7.0 < d < 9.0, d
|
||||
|
||||
# теги проставлены
|
||||
r = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-show_entries",
|
||||
"format_tags=album,artist,track,title", "-of", "default=nw=1",
|
||||
str(tracks[0])], capture_output=True, text=True)
|
||||
assert "Книга" in r.stdout and "Автор" in r.stdout, r.stdout
|
||||
assert "track=1/2" in r.stdout, r.stdout
|
||||
|
||||
|
||||
def test_split_skips_incomplete_chapter(tmp: Path):
|
||||
tts, ab = build_book(tmp, chapters=2, paras=3)
|
||||
(ab / "temp_audio" / "chapter_001_para_0002.mp3").unlink()
|
||||
out = tmp / "tracks"
|
||||
assert a.cmd_split(split_ns(tts, ab, out)) is False, "неполная глава должна пропускаться"
|
||||
assert len(list(out.glob("*.mp3"))) == 1, "целая глава всё равно нарезается"
|
||||
|
||||
out2 = tmp / "forced"
|
||||
assert a.cmd_split(split_ns(tts, ab, out2, force=True)) is True
|
||||
assert len(list(out2.glob("*.mp3"))) == 2, "--force режет обе"
|
||||
|
||||
|
||||
def test_split_without_gap(tmp: Path):
|
||||
tts, ab = build_book(tmp, chapters=1, paras=2)
|
||||
out = tmp / "tracks"
|
||||
assert a.cmd_split(split_ns(tts, ab, out, gap=0)) is True
|
||||
d = a.duration(next(out.glob("*.mp3")))
|
||||
assert 4.5 < d < 5.6, d # 1 + 2 + 2 без пауз
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if subprocess.run(["which", "ffmpeg"], capture_output=True).returncode:
|
||||
sys.exit("нужен ffmpeg")
|
||||
for fn in (test_prep, test_verify_detects_gap, test_verify_clean,
|
||||
test_verify_detects_empty_file):
|
||||
test_verify_detects_empty_file, test_split_makes_chapter_tracks,
|
||||
test_split_skips_incomplete_chapter, test_split_without_gap):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
print("---", fn.__name__)
|
||||
fn(Path(d))
|
||||
|
||||
Reference in New Issue
Block a user