Kleineres Modell
This commit is contained in:
62
tests/test_http.py
Normal file
62
tests/test_http.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""REST test for Qwen3-TTS /v1/audio/speech endpoint."""
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8091"
|
||||
OUTPUT_FILE = "test_output.wav"
|
||||
|
||||
|
||||
def check_health():
|
||||
try:
|
||||
r = requests.get(f"{BASE_URL}/health", timeout=5)
|
||||
r.raise_for_status()
|
||||
body = r.json() if r.content else "(leer)"
|
||||
print(f"[OK] /health → HTTP {r.status_code} {body}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[FAIL] /health: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_tts(text: str, voice: str = "Ryan", language: str = "German"):
|
||||
payload = {
|
||||
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
|
||||
"input": text,
|
||||
"voice": voice,
|
||||
"language": language,
|
||||
"response_format": "wav",
|
||||
}
|
||||
print(f"\n[→] POST /v1/audio/speech")
|
||||
print(f" text: {text!r}")
|
||||
print(f" voice: {voice}, language: {language}")
|
||||
|
||||
t0 = time.perf_counter()
|
||||
r = requests.post(f"{BASE_URL}/v1/audio/speech", json=payload, timeout=60)
|
||||
elapsed = time.perf_counter() - t0
|
||||
|
||||
if r.status_code != 200:
|
||||
print(f"[FAIL] HTTP {r.status_code}: {r.text[:300]}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(OUTPUT_FILE, "wb") as f:
|
||||
f.write(r.content)
|
||||
|
||||
kb = len(r.content) / 1024
|
||||
print(f"[OK] {elapsed:.2f}s → {kb:.1f} KB WAV saved to {OUTPUT_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Testing Qwen3-TTS at {BASE_URL}\n")
|
||||
|
||||
if not check_health():
|
||||
sys.exit(1)
|
||||
|
||||
test_tts(
|
||||
text="Hallo! Ich bin ein KI-Sprachassistent. Wie kann ich Ihnen helfen?",
|
||||
voice="Ryan",
|
||||
language="German",
|
||||
)
|
||||
|
||||
print("\nAll tests passed.")
|
||||
119
tests/test_ws.py
Normal file
119
tests/test_ws.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""WebSocket streaming test for Qwen3-TTS /v1/audio/speech/stream endpoint."""
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import wave
|
||||
import struct
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
try:
|
||||
import websockets
|
||||
except ImportError:
|
||||
print("Installing websockets...")
|
||||
import subprocess
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "websockets"])
|
||||
import websockets
|
||||
|
||||
WS_URL = "ws://localhost:8091/v1/audio/speech/stream"
|
||||
OUTPUT_FILE = "test_stream_output.wav"
|
||||
SAMPLE_RATE = 24000
|
||||
WORD_RE = re.compile(r"\S+\s*")
|
||||
|
||||
|
||||
def iter_words(text: str):
|
||||
"""Yield complete words, preserving trailing whitespace.
|
||||
|
||||
The WebSocket client must not forward raw LLM tokens such as partial
|
||||
subwords. It buffers to words and lets the server detect sentence ends.
|
||||
"""
|
||||
for match in WORD_RE.finditer(text):
|
||||
yield match.group(0)
|
||||
|
||||
|
||||
async def stream_tts(
|
||||
text: str,
|
||||
voice: str = "Ryan",
|
||||
language: str = "German",
|
||||
instructions: str = "Eine warme, ruhige deutsche Stimme, klar artikuliert, freundlich und natuerlich.",
|
||||
seed: int = 42,
|
||||
):
|
||||
print(f"[→] WebSocket connect: {WS_URL}")
|
||||
print(f" text: {text!r}")
|
||||
print(f" voice: {voice}, language: {language}, seed: {seed}")
|
||||
print(f" instruct: {instructions!r}")
|
||||
|
||||
pcm_chunks = []
|
||||
total_bytes = 0
|
||||
t0 = time.perf_counter()
|
||||
first_chunk_time = None
|
||||
|
||||
async with websockets.connect(WS_URL) as ws:
|
||||
# Send session config
|
||||
config = {
|
||||
"type": "session.config",
|
||||
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
|
||||
"task_type": "VoiceDesign",
|
||||
"voice": voice,
|
||||
"language": language,
|
||||
"instructions": instructions,
|
||||
"seed": seed,
|
||||
"response_format": "pcm",
|
||||
"stream_audio": True,
|
||||
"split_granularity": "sentence",
|
||||
}
|
||||
print("[cfg]", json.dumps(config, ensure_ascii=False, sort_keys=True), flush=True)
|
||||
await ws.send(json.dumps(config))
|
||||
|
||||
# Send word-by-word. Do not split into sentences client-side; the
|
||||
# server still decides when a complete sentence is ready for audio.
|
||||
for word in iter_words(text):
|
||||
await ws.send(json.dumps({
|
||||
"type": "input.text",
|
||||
"text": word,
|
||||
}))
|
||||
await ws.send(json.dumps({"type": "input.done"}))
|
||||
|
||||
# Receive
|
||||
async for message in ws:
|
||||
if isinstance(message, bytes):
|
||||
if first_chunk_time is None:
|
||||
first_chunk_time = time.perf_counter() - t0
|
||||
print(f"[OK] First audio chunk in {first_chunk_time:.3f}s")
|
||||
pcm_chunks.append(message)
|
||||
total_bytes += len(message)
|
||||
elif isinstance(message, str):
|
||||
msg = json.loads(message)
|
||||
msg_type = msg.get("type", "")
|
||||
if msg_type == "session.done":
|
||||
break
|
||||
elif msg_type == "error":
|
||||
print(f"[FAIL] Server error: {msg.get('message')}")
|
||||
sys.exit(1)
|
||||
elif msg_type in ("audio.start", "audio.done"):
|
||||
print(f" [{msg_type}] sentence {msg.get('sentence_index', '?')}")
|
||||
|
||||
elapsed = time.perf_counter() - t0
|
||||
|
||||
# Write PCM chunks as WAV
|
||||
raw_pcm = b"".join(pcm_chunks)
|
||||
with wave.open(OUTPUT_FILE, "wb") as wf:
|
||||
wf.setnchannels(1)
|
||||
wf.setsampwidth(2) # int16
|
||||
wf.setframerate(SAMPLE_RATE)
|
||||
wf.writeframes(raw_pcm)
|
||||
|
||||
audio_duration = (total_bytes / 2) / SAMPLE_RATE
|
||||
print(f"[OK] Done: {elapsed:.2f}s total, {total_bytes/1024:.1f} KB PCM")
|
||||
print(f" Audio duration: {audio_duration:.2f}s → saved to {OUTPUT_FILE}")
|
||||
if first_chunk_time:
|
||||
print(f" First-chunk latency: {first_chunk_time:.3f}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(stream_tts(
|
||||
text="Herzlich willkommen! Dieses System nutzt Qwen3-TTS für deutsche Sprachausgabe.",
|
||||
voice="Ryan",
|
||||
language="German",
|
||||
))
|
||||
Reference in New Issue
Block a user