Озвучка: фрагменты режутся группами (не по абзацу), защита от тихого обрыва склейки, досинтез потерянных фрагментов
This commit is contained in:
+41
-23
@@ -50,12 +50,12 @@ def test_verify_detects_gap(tmp: Path):
|
||||
for n in (0, 1):
|
||||
write_chapter(tts, n, [para] * 4)
|
||||
mp3(temp / ("chapter_%03d_intro.mp3" % n), 1)
|
||||
# у главы 1 намеренно пропущен последний абзац
|
||||
count = 4 if n == 0 else 3
|
||||
# у главы 1 намеренно пропущена последняя группа
|
||||
count = 2 if n == 0 else 1
|
||||
for i in range(count):
|
||||
mp3(temp / ("chapter_%03d_para_%04d.mp3" % (n, i)), 6)
|
||||
mp3(temp / ("chapter_%03d_group_%03d.mp3" % (n, i)), 9)
|
||||
|
||||
ns = type("ns", (), {"translations": tts, "audiobook": ab})
|
||||
ns = type("ns", (), {"translations": tts, "audiobook": ab, "group_size": 3})
|
||||
assert a.cmd_verify(ns) is False, "пропущенный фрагмент должен завалить проверку"
|
||||
|
||||
|
||||
@@ -68,9 +68,8 @@ def test_verify_clean(tmp: Path):
|
||||
for n in (0, 1):
|
||||
write_chapter(tts, n, [para] * 3)
|
||||
mp3(temp / ("chapter_%03d_intro.mp3" % n), 1)
|
||||
for i in range(3):
|
||||
mp3(temp / ("chapter_%03d_para_%04d.mp3" % (n, i)), 6)
|
||||
ns = type("ns", (), {"translations": tts, "audiobook": ab})
|
||||
mp3(temp / ("chapter_%03d_group_000.mp3" % n), 18)
|
||||
ns = type("ns", (), {"translations": tts, "audiobook": ab, "group_size": 3})
|
||||
assert a.cmd_verify(ns) is True, "целая книга должна проходить проверку"
|
||||
|
||||
|
||||
@@ -80,17 +79,17 @@ def test_verify_detects_empty_file(tmp: Path):
|
||||
temp = ab / "temp_audio"
|
||||
temp.mkdir(parents=True)
|
||||
para = "х" * 100
|
||||
write_chapter(tts, 0, [para] * 3)
|
||||
write_chapter(tts, 0, [para] * 9)
|
||||
mp3(temp / "chapter_000_intro.mp3", 1)
|
||||
mp3(temp / "chapter_000_para_0000.mp3", 6)
|
||||
mp3(temp / "chapter_000_para_0001.mp3", 6)
|
||||
(temp / "chapter_000_para_0002.mp3").touch() # нулевой размер
|
||||
ns = type("ns", (), {"translations": tts, "audiobook": ab})
|
||||
mp3(temp / "chapter_000_group_000.mp3", 18)
|
||||
mp3(temp / "chapter_000_group_001.mp3", 18)
|
||||
(temp / "chapter_000_group_002.mp3").touch() # нулевой размер
|
||||
ns = type("ns", (), {"translations": tts, "audiobook": ab, "group_size": 3})
|
||||
assert a.cmd_verify(ns) is False, "пустой файл должен завалить проверку"
|
||||
|
||||
|
||||
def build_book(tmp: Path, chapters=2, paras=3):
|
||||
"""Готовый синтез: подготовленные главы + фрагменты в temp_audio/."""
|
||||
def build_book(tmp: Path, chapters=2, paras=3, group=3, secs=2):
|
||||
"""Готовый синтез: главы + фрагменты, сгруппированные как в чужом скрипте."""
|
||||
tts, ab = tmp / "tts", tmp / "audiobook"
|
||||
tts.mkdir()
|
||||
temp = ab / "temp_audio"
|
||||
@@ -98,14 +97,15 @@ def build_book(tmp: Path, chapters=2, paras=3):
|
||||
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)
|
||||
for i in range(-(-paras // group)):
|
||||
mp3(temp / ("chapter_%03d_group_%03d.mp3" % (n, i)), secs)
|
||||
return tts, ab
|
||||
|
||||
|
||||
def split_ns(tts, ab, out, **kw):
|
||||
fields = {"translations": tts, "audiobook": ab, "output": out,
|
||||
"album": "Книга", "author": "Автор", "gap": 0.3, "force": False}
|
||||
"album": "Книга", "author": "Автор", "gap": 0.3,
|
||||
"force": False, "group_size": 3}
|
||||
fields.update(kw)
|
||||
return type("ns", (), fields)
|
||||
|
||||
@@ -119,9 +119,9 @@ def test_split_makes_chapter_tracks(tmp: Path):
|
||||
assert tracks[0].name.startswith("000 - "), tracks[0].name
|
||||
assert not (out / ".tmp").exists(), "временный каталог должен убираться"
|
||||
|
||||
# длительность: 1 вводный + 3 абзаца + 3 паузы = 1 + 6 + 0.9 ≈ 7.9 с
|
||||
# 1 вводный + 1 группа + 1 пауза = 1 + 2 + 0.3 ≈ 3.3 с
|
||||
d = a.duration(tracks[0])
|
||||
assert 7.0 < d < 9.0, d
|
||||
assert 2.8 < d < 4.0, d
|
||||
|
||||
# теги проставлены
|
||||
r = subprocess.run(
|
||||
@@ -134,7 +134,7 @@ def test_split_makes_chapter_tracks(tmp: Path):
|
||||
|
||||
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()
|
||||
(ab / "temp_audio" / "chapter_001_group_000.mp3").unlink()
|
||||
out = tmp / "tracks"
|
||||
assert a.cmd_split(split_ns(tts, ab, out)) is False, "неполная глава должна пропускаться"
|
||||
assert len(list(out.glob("*.mp3"))) == 1, "целая глава всё равно нарезается"
|
||||
@@ -144,12 +144,29 @@ def test_split_skips_incomplete_chapter(tmp: Path):
|
||||
assert len(list(out2.glob("*.mp3"))) == 2, "--force режет обе"
|
||||
|
||||
|
||||
def test_split_survives_broken_fragment(tmp: Path):
|
||||
"""Регресс: пустой фрагмент обрывал concat молча, ffmpeg возвращал 0."""
|
||||
tts, ab = build_book(tmp, chapters=1, paras=9, secs=4)
|
||||
broken = ab / "temp_audio" / "chapter_000_group_001.mp3"
|
||||
broken.write_bytes(b"") # ровно тот случай, что дал edge-tts
|
||||
out = tmp / "tracks"
|
||||
assert a.cmd_split(split_ns(tts, ab, out)) is False, \
|
||||
"глава с битым фрагментом не должна выдаваться как готовая"
|
||||
assert not list(out.glob("*.mp3")), "обрезанный трек не должен остаться на диске"
|
||||
|
||||
# с --force собирается из уцелевших, но без тихого обрыва посередине
|
||||
out2 = tmp / "forced"
|
||||
assert a.cmd_split(split_ns(tts, ab, out2, force=True, gap=0)) is True
|
||||
d = a.duration(next(out2.glob("*.mp3")))
|
||||
assert 8.5 < d < 9.5, d # вводный 1 с + две уцелевшие группы по 4 с
|
||||
|
||||
|
||||
def test_split_without_gap(tmp: Path):
|
||||
tts, ab = build_book(tmp, chapters=1, paras=2)
|
||||
tts, ab = build_book(tmp, chapters=1, paras=6)
|
||||
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 без пауз
|
||||
assert 4.5 < d < 5.6, d # 1 вводный + 2 группы по 2 с, без пауз
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -157,7 +174,8 @@ if __name__ == "__main__":
|
||||
sys.exit("нужен ffmpeg")
|
||||
for fn in (test_prep, test_verify_detects_gap, test_verify_clean,
|
||||
test_verify_detects_empty_file, test_split_makes_chapter_tracks,
|
||||
test_split_skips_incomplete_chapter, test_split_without_gap):
|
||||
test_split_skips_incomplete_chapter,
|
||||
test_split_survives_broken_fragment, test_split_without_gap):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
print("---", fn.__name__)
|
||||
fn(Path(d))
|
||||
|
||||
Reference in New Issue
Block a user