313 lines
10 KiB
Python
313 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright 2026 Umbra Tools (umbratools.com)
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""
|
|
Test script for Qwen3-TTS voice synthesis.
|
|
Tests the TTS server with character voice prompts.
|
|
"""
|
|
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from umbra_audio.core.tts_client import QwenTTSClient
|
|
|
|
|
|
def test_tts_connection():
|
|
"""Test connection to TTS server."""
|
|
print("=" * 80)
|
|
print("TESTING TTS CONNECTION")
|
|
print("=" * 80)
|
|
|
|
client = QwenTTSClient()
|
|
|
|
try:
|
|
# Test with a simple text
|
|
print("\n1. Testing basic synthesis...")
|
|
start_time = time.time()
|
|
audio_data = client.synthesize_speech(
|
|
text="Hello, this is a test.",
|
|
voice_prompt="A clear, neutral voice."
|
|
)
|
|
elapsed = time.time() - start_time
|
|
|
|
print(f" Success! Audio data: {len(audio_data)} bytes")
|
|
print(f" Time taken: {elapsed:.2f} seconds")
|
|
|
|
# Save the test audio
|
|
output_path = Path(__file__).parent.parent / "test_audio_basic.wav"
|
|
with open(output_path, 'wb') as f:
|
|
f.write(audio_data)
|
|
print(f" Saved to: {output_path}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f" Error: {e}")
|
|
return False
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_character_voices():
|
|
"""Test synthesis with character voice prompts."""
|
|
print("\n" + "=" * 80)
|
|
print("TESTING CHARACTER VOICES")
|
|
print("=" * 80)
|
|
|
|
client = QwenTTSClient()
|
|
|
|
# Test characters with voice prompts
|
|
test_characters = [
|
|
{
|
|
"name": "Princess Kainda",
|
|
"voice_prompt": "A young adult female in her early 20s with a clear, bright mid-range voice. Speaks with a curious, confident cadence and a refined, royal accent.",
|
|
"test_line": "I will not be trapped in my cabin any longer. I want to see what's out there."
|
|
},
|
|
{
|
|
"name": "Captain Hunter",
|
|
"voice_prompt": "A middle-aged male in his 40s with a gravelly, low-pitched baritone. Speaks with a slow, deliberate pace, rough texture, and authoritative delivery.",
|
|
"test_line": "Orders, Captain. We need to move fast before the salvagers arrive."
|
|
},
|
|
{
|
|
"name": "King Kabir",
|
|
"voice_prompt": "A senior male in his 60s with a resonant, regal baritone. Speaks with a slow, dignified pace and an authoritative, paternal tone.",
|
|
"test_line": "My daughter has always been headstrong. Perhaps that is what we need in these troubled times."
|
|
},
|
|
{
|
|
"name": "Nanny (Robot)",
|
|
"voice_prompt": "A synthesized female voice with a smooth, neutral tone. Speaks with a steady, measured cadence and precise, matter-of-fact delivery.",
|
|
"test_line": "Princess, please put this on quickly. We are under attack by pirates."
|
|
},
|
|
{
|
|
"name": "Aunt Vicky",
|
|
"voice_prompt": "A mature female in her 50s with a warm, concerned alto. Speaks with a gentle, maternal cadence and a worried, caring tone.",
|
|
"test_line": "Who is the captain of this ship? I need to speak with him immediately."
|
|
}
|
|
]
|
|
|
|
results = []
|
|
|
|
for i, char in enumerate(test_characters):
|
|
print(f"\n{i+1}. Testing {char['name']}...")
|
|
|
|
try:
|
|
start_time = time.time()
|
|
audio_data = client.synthesize_speech(
|
|
text=char['test_line'],
|
|
voice_prompt=char['voice_prompt']
|
|
)
|
|
elapsed = time.time() - start_time
|
|
|
|
# Save audio
|
|
safe_name = char['name'].replace(' ', '_').replace('(', '').replace(')', '')
|
|
output_path = Path(__file__).parent.parent / f"test_audio_{safe_name}.wav"
|
|
with open(output_path, 'wb') as f:
|
|
f.write(audio_data)
|
|
|
|
results.append({
|
|
"name": char['name'],
|
|
"success": True,
|
|
"audio_size": len(audio_data),
|
|
"time_seconds": elapsed,
|
|
"output_path": str(output_path)
|
|
})
|
|
|
|
print(f" Success! Audio: {len(audio_data)} bytes, Time: {elapsed:.2f}s")
|
|
print(f" Saved to: {output_path}")
|
|
|
|
except Exception as e:
|
|
results.append({
|
|
"name": char['name'],
|
|
"success": False,
|
|
"error": str(e)
|
|
})
|
|
print(f" Error: {e}")
|
|
|
|
client.close()
|
|
|
|
# Summary
|
|
print("\n" + "=" * 80)
|
|
print("CHARACTER VOICE TEST SUMMARY")
|
|
print("=" * 80)
|
|
|
|
successful = sum(1 for r in results if r['success'])
|
|
print(f"Successful: {successful}/{len(results)}")
|
|
|
|
for r in results:
|
|
status = "PASS" if r['success'] else "FAIL"
|
|
print(f" {r['name']}: {status}")
|
|
if r['success']:
|
|
print(f" Size: {r['audio_size']} bytes, Time: {r['time_seconds']:.2f}s")
|
|
else:
|
|
print(f" Error: {r.get('error', 'Unknown')}")
|
|
|
|
return successful == len(results)
|
|
|
|
|
|
def test_dialogue_extraction():
|
|
"""Test synthesis with actual dialogue from the manuscript."""
|
|
print("\n" + "=" * 80)
|
|
print("TESTING DIALOGUE SYNTHESIS")
|
|
print("=" * 80)
|
|
|
|
client = QwenTTSClient()
|
|
|
|
# Real dialogue from DealHunterShip
|
|
dialogues = [
|
|
{
|
|
"speaker": "Kainda",
|
|
"text": "Where is the ship that brought me? Did you fire on that ship?",
|
|
"voice_prompt": "A young adult female in her early 20s with a clear, bright mid-range voice. Speaks with a curious, confident cadence and a refined, royal accent."
|
|
},
|
|
{
|
|
"speaker": "Hunter",
|
|
"text": "Orders, Captain.",
|
|
"voice_prompt": "A middle-aged male in his 40s with a gravelly, low-pitched baritone. Speaks with a slow, deliberate pace, rough texture, and authoritative delivery."
|
|
},
|
|
{
|
|
"speaker": "Kabir",
|
|
"text": "Or I can let her run loose and watch the fun.",
|
|
"voice_prompt": "A senior male in his 60s with a resonant, regal baritone. Speaks with a slow, dignified pace and an authoritative, paternal tone."
|
|
}
|
|
]
|
|
|
|
results = []
|
|
|
|
for i, d in enumerate(dialogues):
|
|
print(f"\n{i+1}. Synthesizing {d['speaker']}'s dialogue...")
|
|
|
|
try:
|
|
start_time = time.time()
|
|
audio_data = client.synthesize_speech(
|
|
text=d['text'],
|
|
voice_prompt=d['voice_prompt']
|
|
)
|
|
elapsed = time.time() - start_time
|
|
|
|
# Save audio
|
|
output_path = Path(__file__).parent.parent / f"test_dialogue_{d['speaker'].lower()}.wav"
|
|
with open(output_path, 'wb') as f:
|
|
f.write(audio_data)
|
|
|
|
results.append({
|
|
"speaker": d['speaker'],
|
|
"success": True,
|
|
"audio_size": len(audio_data),
|
|
"time_seconds": elapsed
|
|
})
|
|
|
|
print(f" Success! Audio: {len(audio_data)} bytes, Time: {elapsed:.2f}s")
|
|
|
|
except Exception as e:
|
|
results.append({
|
|
"speaker": d['speaker'],
|
|
"success": False,
|
|
"error": str(e)
|
|
})
|
|
print(f" Error: {e}")
|
|
|
|
client.close()
|
|
|
|
# Summary
|
|
print("\n" + "=" * 80)
|
|
print("DIALOGUE SYNTHESIS SUMMARY")
|
|
print("=" * 80)
|
|
|
|
successful = sum(1 for r in results if r['success'])
|
|
print(f"Successful: {successful}/{len(results)}")
|
|
|
|
for r in results:
|
|
status = "PASS" if r['success'] else "FAIL"
|
|
print(f" {r['speaker']}: {status}")
|
|
|
|
return successful == len(results)
|
|
|
|
|
|
def test_voice_prompt_validation():
|
|
"""Test voice prompt validation."""
|
|
print("\n" + "=" * 80)
|
|
print("TESTING VOICE PROMPT VALIDATION")
|
|
print("=" * 80)
|
|
|
|
client = QwenTTSClient()
|
|
|
|
test_prompts = [
|
|
"A young adult female in her early 20s with a clear, bright mid-range voice. Speaks with a curious, confident cadence and a refined, royal accent.",
|
|
"A middle-aged male with a gravelly, low-pitched baritone.",
|
|
"This is a very long prompt that exceeds the forty word limit and should fail validation because it contains too many words for the voice synthesis system to handle properly.",
|
|
"A voice that sounds like a mysterious philosopher from ancient times."
|
|
]
|
|
|
|
for i, prompt in enumerate(test_prompts):
|
|
print(f"\n{i+1}. Validating prompt: {prompt[:50]}...")
|
|
result = client.validate_voice_prompt(prompt)
|
|
print(f" Valid: {result['valid']}")
|
|
print(f" Word count: {result['word_count']}")
|
|
if result['issues']:
|
|
print(f" Issues: {result['issues']}")
|
|
|
|
client.close()
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Run all TTS tests."""
|
|
print("UmbraAudio Voice Synthesis Tests")
|
|
print("=" * 80)
|
|
|
|
results = {}
|
|
|
|
try:
|
|
results['connection'] = test_tts_connection()
|
|
except Exception as e:
|
|
print(f"ERROR in connection test: {e}")
|
|
results['connection'] = False
|
|
|
|
try:
|
|
results['character_voices'] = test_character_voices()
|
|
except Exception as e:
|
|
print(f"ERROR in character voice test: {e}")
|
|
results['character_voices'] = False
|
|
|
|
try:
|
|
results['dialogue'] = test_dialogue_extraction()
|
|
except Exception as e:
|
|
print(f"ERROR in dialogue test: {e}")
|
|
results['dialogue'] = False
|
|
|
|
try:
|
|
results['validation'] = test_voice_prompt_validation()
|
|
except Exception as e:
|
|
print(f"ERROR in validation test: {e}")
|
|
results['validation'] = False
|
|
|
|
# Final summary
|
|
print("\n" + "=" * 80)
|
|
print("FINAL TEST SUMMARY")
|
|
print("=" * 80)
|
|
|
|
all_passed = True
|
|
for test_name, passed in results.items():
|
|
status = "PASS" if passed else "FAIL"
|
|
print(f" {test_name}: {status}")
|
|
if not passed:
|
|
all_passed = False
|
|
|
|
print("\n" + "=" * 80)
|
|
if all_passed:
|
|
print("ALL TESTS PASSED!")
|
|
else:
|
|
print("SOME TESTS FAILED!")
|
|
print("=" * 80)
|
|
|
|
return 0 if all_passed else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|