Нарезка аудиокниги по главам: треки с ID3-тегами вместо одного слитного файла

This commit is contained in:
Илья Поляков
2026-08-03 11:02:32 +03:00
parent 93c40c76bf
commit a0c26b41bf
4 changed files with 227 additions and 7 deletions
+65 -1
View File
@@ -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))