Single-voice health endpoint (GET /api/voices/{id}/health), problems stored in
meta on import, shown in the Try-out voice panel; architecture note and docs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
71 lines
2.7 KiB
Python
71 lines
2.7 KiB
Python
"""audio_problems: silence gap, clipped burst, level jump, clean signal."""
|
|
import sys
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from pydub import AudioSegment
|
|
from pydub.generators import Sine, Square
|
|
|
|
from core.audio import audio_problems
|
|
|
|
SR = 24000
|
|
|
|
|
|
def tone(ms, gain=-20):
|
|
return Sine(300, sample_rate=SR).to_audio_segment(ms).apply_gain(gain)
|
|
|
|
|
|
def kinds(seg):
|
|
return {p["kind"] for p in audio_problems(seg)}
|
|
|
|
|
|
class AudioProblemTests(unittest.TestCase):
|
|
def test_clean(self):
|
|
self.assertEqual(audio_problems(tone(4000, -18).set_frame_rate(SR)), [])
|
|
|
|
def test_silence_gap(self):
|
|
seg = tone(2000) + AudioSegment.silent(2000, frame_rate=SR) + tone(2000)
|
|
p = [x for x in audio_problems(seg) if x["kind"] == "silence"]
|
|
self.assertEqual(len(p), 1)
|
|
self.assertAlmostEqual(p[0]["start_s"], 2.0, delta=0.1)
|
|
|
|
def test_edge_silence_ignored(self):
|
|
seg = AudioSegment.silent(3000, frame_rate=SR) + tone(2000) + AudioSegment.silent(3000, frame_rate=SR)
|
|
self.assertNotIn("silence", kinds(seg))
|
|
|
|
def test_clipping(self):
|
|
burst = Square(300, sample_rate=SR).to_audio_segment(400).apply_gain(+10)
|
|
seg = tone(2000) + burst + tone(2000)
|
|
p = [x for x in audio_problems(seg) if x["kind"] == "clipping"]
|
|
self.assertTrue(p)
|
|
self.assertAlmostEqual(p[0]["start_s"], 2.0, delta=0.1)
|
|
|
|
def test_level_jump(self):
|
|
seg = tone(2000, -40) + tone(2000, -10)
|
|
p = [x for x in audio_problems(seg) if x["kind"] == "level_jump"]
|
|
self.assertEqual(len(p), 1)
|
|
self.assertAlmostEqual(p[0]["start_s"], 2.0, delta=0.1)
|
|
|
|
|
|
class VoiceHealthTests(unittest.TestCase):
|
|
def test_analyze_and_stored(self):
|
|
import tempfile
|
|
from core.voice import _voice_health, _validate_voice_meta
|
|
with tempfile.TemporaryDirectory() as d:
|
|
wav = Path(d) / "EN_F_test.wav"
|
|
(tone(2000) + AudioSegment.silent(2000, frame_rate=SR) + tone(2000)).export(str(wav), format="wav")
|
|
h = _voice_health(wav, meta={}, transcript="hi", duration=6.0, analyze=True)
|
|
self.assertEqual([p["kind"] for p in h["problems"]], ["silence"])
|
|
self.assertIn("Long silence in reference audio", h["warnings"])
|
|
stored = _voice_health(wav, meta={"audio_problems": h["problems"]}, transcript="hi", duration=6.0)
|
|
self.assertEqual(stored["problems"], h["problems"])
|
|
self.assertEqual(_voice_health(wav, meta={}, transcript="hi", duration=6.0)["problems"], [])
|
|
with self.assertRaises(ValueError):
|
|
_validate_voice_meta({"audio_problems": "x"})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|