tts-voice-creator-clone-and.../tests/test_audio_onsets.py
mARTin-B78 63034b07eb fix(magazine): record where speech starts in each sentence so the highlight is not early
The TTS pads every sentence with 0.4-0.9 s of silence; boundaries are exact but the
highlight lit up that long before the words. Record per-sentence speech onsets at render
time, measure them from the decoded MP3 for older audio, and expose them as 'speech' in
/timings.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 14:57:30 +02:00

43 lines
1.7 KiB
Python

"""Speech onsets: the lead-in of silence TTS puts before each sentence is measured and recorded."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from pydub import AudioSegment
from pydub.generators import Sine
from core import magazine as mag
def _clip(lead_ms: int, tone_ms: int = 400) -> AudioSegment:
return AudioSegment.silent(lead_ms, frame_rate=24000) + Sine(300, sample_rate=24000).to_audio_segment(tone_ms).apply_gain(-6)
def test_onset_is_the_leading_silence():
assert abs(mag._speech_onset_ms(_clip(700)) - 700) <= 20
assert mag._speech_onset_ms(_clip(0)) <= 10
def test_silent_or_tiny_clip_has_no_onset():
assert mag._speech_onset_ms(AudioSegment.silent(500, frame_rate=24000)) == 0
assert mag._speech_onset_ms(AudioSegment.silent(20, frame_rate=24000)) == 0
def test_onsets_are_written_per_segment(tmp_path):
mp3 = tmp_path / "a.mp3"
segs = [{"text": "One. Two."}, {"text": "Three."}]
mag.write_audio_timings(mp3, segs, [0, 0, 1], [(0, 1000), (1000, 2000), (2000, 3000)], interview=False, onsets=[700, 650, 500])
rec = mag.read_audio_timings(mp3)
assert [s["onsets"] for s in rec["segments"]] == [[700, 650], [500]]
assert [s["times"] for s in rec["segments"]] == [[[0, 1000], [1000, 2000]], [[2000, 3000]]]
def test_old_audio_onsets_are_measured_from_the_file(tmp_path):
mp3 = tmp_path / "old.mp3"
(_clip(600) + _clip(800)).export(mp3, format="mp3")
total = int(AudioSegment.from_file(mp3).duration_seconds * 1000)
timings = {"version": 1, "segments": [{"chars": 1, "times": [[0, 1000], [1000, total]]}]}
got = mag.measure_speech_onsets(mp3, timings)
assert got is not None and abs(got[0] - 600) <= 60 and abs(got[1] - 800) <= 60