speaking-midi / tests /test_app.py
k-l-lambda's picture
Add instrumental fitting mode and support 30-second clips
f5a62b9
Raw History Blame Contribute Delete
3.77 kB
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
import app
import numpy as np
import soundfile as sf
from app import prepare_audio, run, example
class AppTests(unittest.TestCase):
def test_audio_validation_and_prefix(self):
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'source.wav'; out=Path(tmp)/'out.wav'
sf.write(src,np.sin(np.arange(16000)*.1),16000)
self.assertAlmostEqual(prepare_audio(src,out,.5),.5)
sf.write(src,np.zeros(16000),16000)
with self.assertRaises(Exception): prepare_audio(src,out,1)
def test_cpu_audio_pipeline(self):
events=list(run('Audio → MIDI','examples/en/source.wav','','English','',.5,200))
self.assertEqual(len(events),2)
self.assertIn('Complete',events[-1][0])
for path in (events[-1][1], events[-1][3]): self.assertTrue(Path(path).exists())
self.assertGreater(len(json.loads(events[-1][2])['notes']), 0)
report=json.loads((Path(events[-1][3]).parent/'report.json').read_text())
self.assertEqual(report['optimization']['device'],'cpu')
self.assertGreater(report['notes'],0)
def test_zerogpu_speech_dispatch_keeps_fitting_on_cpu(self):
wave = .05 * np.sin(2*np.pi*220*np.arange(12000)/24000)
with patch.object(app, 'DEVICE', 'zerogpu'), patch.object(
app, 'generate_zero_speech', return_value=(wave, 24000), create=True) as generate:
events = list(run('Text → speech → MIDI', None, 'Hello.', 'English',
app.VOICE, .5, 200))
generate.assert_called_once_with('Hello.', 'English', app.VOICE)
output = Path(events[-1][3]).parent
self.assertTrue((output/'speech.wav').exists())
self.assertFalse((output/'tts.log').exists())
report = json.loads((output/'report.json').read_text())
self.assertEqual(report['optimization']['device'], 'cpu')
def test_instrumental_pipeline_at_30_second_limit(self):
with tempfile.TemporaryDirectory() as tmp:
source = Path(tmp)/'long.wav'
rate = 8000
wave = .05*np.sin(2*np.pi*220*np.arange(31*rate)/rate)
sf.write(source, wave, rate)
events = list(run('Audio → MIDI', str(source), '', 'English', '',
30, 200, 'Instrumental (sparse)'))
final = events[-1]
self.assertIn('Instrumental (sparse)', final[0])
self.assertEqual(Path(final[3]).name, 'instrumental.mid')
self.assertAlmostEqual(sf.info(final[1]).duration, 30)
payload = json.loads(final[2])
self.assertAlmostEqual(payload['duration'], 30.5)
self.assertTrue(payload['notes'])
self.assertTrue(any(n['duration'] > .02 for n in payload['notes']))
self.assertTrue(all(n['start']+n['duration'] <= 30.001 for n in payload['notes']))
report = json.loads((Path(final[3]).parent/'report.json').read_text())
self.assertEqual(report['device'], 'cpu')
self.assertEqual(len(report['chunks']), 3)
def test_reject_duration_and_style(self):
for duration, style in [(30.5, 'Speech (dense)'), (30, 'unknown')]:
with self.assertRaises(Exception):
list(run('Audio → MIDI', 'examples/en/source.wav', '', 'English', '',
duration, 200, style))
def test_cached_examples(self):
for language in ['en','zh']:
output=example(language)
for path in (output[1], output[3]): self.assertTrue(Path(path).exists())
self.assertTrue(json.loads(output[2])['audio'].startswith('data:audio/wav;base64,'))
if __name__=='__main__': unittest.main()