234 lines
7.1 KiB
Python
234 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Fun-ASR-Nano Python WebSocket Client.
|
|
|
|
Supports real-time microphone recording and audio file streaming.
|
|
|
|
Usage:
|
|
# Microphone mode
|
|
python client_python.py --server ws://localhost:10095 --mic
|
|
|
|
# File mode
|
|
python client_python.py --server ws://localhost:10095 --file audio.wav
|
|
|
|
# With hotwords
|
|
python client_python.py --server ws://localhost:10095 --file audio.wav --hotwords "张三,李四,北京"
|
|
|
|
# Disable speaker diarization display
|
|
python client_python.py --server ws://localhost:10095 --mic --no-spk
|
|
"""
|
|
|
|
import asyncio
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import numpy as np
|
|
|
|
try:
|
|
import websockets
|
|
except ImportError:
|
|
print("Please install websockets: pip install websockets")
|
|
sys.exit(1)
|
|
|
|
|
|
SAMPLE_RATE = 16000
|
|
CHUNK_DURATION_MS = 100
|
|
CHUNK_SAMPLES = int(SAMPLE_RATE * CHUNK_DURATION_MS / 1000)
|
|
|
|
SPK_COLORS = [
|
|
"\033[36m", "\033[35m", "\033[33m", "\033[32m",
|
|
"\033[34m", "\033[91m", "\033[96m", "\033[95m",
|
|
]
|
|
RESET = "\033[0m"
|
|
GRAY = "\033[90m"
|
|
GREEN = "\033[92m"
|
|
|
|
|
|
def format_time(ms):
|
|
s = ms / 1000
|
|
return f"{s:.1f}s"
|
|
|
|
|
|
def print_result(data, show_spk=True):
|
|
"""Print ASR result to terminal."""
|
|
sentences = data.get("sentences", [])
|
|
partial = data.get("partial", "")
|
|
partial_start = data.get("partial_start_ms", 0)
|
|
is_final = data.get("is_final", False)
|
|
|
|
sys.stdout.write("\033[2J\033[H")
|
|
|
|
print(f"{GREEN}Fun-ASR-Nano Streaming ASR{RESET}")
|
|
print(f"{GRAY}{'─' * 60}{RESET}")
|
|
|
|
for s in sentences:
|
|
start = s.get("start", s.get("start_ms", 0))
|
|
end = s.get("end", s.get("end_ms", 0))
|
|
spk = s.get("spk", -1)
|
|
text = s["text"]
|
|
|
|
time_str = f"{GRAY}[{format_time(start)}-{format_time(end)}]{RESET}"
|
|
spk_str = ""
|
|
if show_spk and spk >= 0:
|
|
color = SPK_COLORS[spk % len(SPK_COLORS)]
|
|
spk_str = f" {color}SPK{spk}{RESET}"
|
|
|
|
print(f" {time_str}{spk_str} {text}")
|
|
|
|
if partial:
|
|
print(f" {GRAY}[{format_time(partial_start)}-...] {partial}{RESET}")
|
|
|
|
if is_final:
|
|
print(f"\n{GRAY}{'─' * 60}{RESET}")
|
|
print(f"{GREEN}Done.{RESET} {len(sentences)} sentences")
|
|
else:
|
|
print(f"\n{GRAY}Recording... Press Ctrl+C to stop{RESET}")
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
async def run_mic(args):
|
|
"""Stream from microphone."""
|
|
try:
|
|
import sounddevice as sd
|
|
except ImportError:
|
|
print("Please install sounddevice: pip install sounddevice")
|
|
sys.exit(1)
|
|
|
|
print(f"Connecting to {args.server}...")
|
|
async with websockets.connect(args.server, ping_interval=None) as ws:
|
|
await ws.send("START")
|
|
resp = await ws.recv()
|
|
event = json.loads(resp)
|
|
if event.get("event") != "started":
|
|
print(f"Unexpected response: {resp}")
|
|
return
|
|
|
|
if args.hotwords:
|
|
await ws.send(f"HOTWORDS:{args.hotwords}")
|
|
await ws.recv()
|
|
|
|
print("Recording... Press Ctrl+C to stop\n")
|
|
|
|
audio_queue = asyncio.Queue()
|
|
|
|
def audio_callback(indata, frames, time_info, status):
|
|
audio_queue.put_nowait(indata.copy())
|
|
|
|
stream = sd.InputStream(
|
|
samplerate=SAMPLE_RATE, channels=1, dtype='int16',
|
|
blocksize=CHUNK_SAMPLES, callback=audio_callback,
|
|
)
|
|
|
|
async def send_audio():
|
|
with stream:
|
|
while True:
|
|
chunk = await audio_queue.get()
|
|
await ws.send(chunk.tobytes())
|
|
|
|
async def recv_results():
|
|
async for msg in ws:
|
|
data = json.loads(msg)
|
|
if "sentences" in data:
|
|
print_result(data, show_spk=args.spk)
|
|
if data.get("is_final") or data.get("event") == "stopped":
|
|
break
|
|
|
|
send_task = asyncio.create_task(send_audio())
|
|
recv_task = asyncio.create_task(recv_results())
|
|
|
|
try:
|
|
await asyncio.gather(send_task, recv_task)
|
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
pass
|
|
finally:
|
|
send_task.cancel()
|
|
if ws.open:
|
|
await ws.send("STOP")
|
|
async for msg in ws:
|
|
data = json.loads(msg)
|
|
if "sentences" in data:
|
|
print_result(data, show_spk=args.spk)
|
|
if data.get("is_final") or data.get("event") == "stopped":
|
|
break
|
|
|
|
|
|
async def run_file(args):
|
|
"""Stream an audio file."""
|
|
try:
|
|
import soundfile as sf
|
|
except ImportError:
|
|
print("Please install soundfile: pip install soundfile")
|
|
sys.exit(1)
|
|
|
|
audio, sr = sf.read(args.file)
|
|
if sr != SAMPLE_RATE:
|
|
try:
|
|
import librosa
|
|
audio = librosa.resample(audio, orig_sr=sr, target_sr=SAMPLE_RATE)
|
|
except ImportError:
|
|
print(f"Audio is {sr}Hz, need 16kHz. Install librosa: pip install librosa")
|
|
sys.exit(1)
|
|
if audio.ndim > 1:
|
|
audio = audio[:, 0]
|
|
audio = audio.astype(np.float32)
|
|
|
|
duration = len(audio) / SAMPLE_RATE
|
|
print(f"File: {args.file} ({duration:.1f}s)")
|
|
print(f"Connecting to {args.server}...")
|
|
|
|
async with websockets.connect(args.server, ping_interval=None) as ws:
|
|
await ws.send("START")
|
|
await ws.recv()
|
|
|
|
if args.hotwords:
|
|
await ws.send(f"HOTWORDS:{args.hotwords}")
|
|
await ws.recv()
|
|
|
|
int16 = (audio * 32768).clip(-32768, 32767).astype(np.int16)
|
|
|
|
chunk_size = CHUNK_SAMPLES
|
|
total_chunks = (len(int16) + chunk_size - 1) // chunk_size
|
|
|
|
async def send_audio():
|
|
for i in range(0, len(int16), chunk_size):
|
|
chunk = int16[i:i+chunk_size]
|
|
await ws.send(chunk.tobytes())
|
|
await asyncio.sleep(CHUNK_DURATION_MS / 1000 * 0.5)
|
|
await ws.send("STOP")
|
|
|
|
async def recv_results():
|
|
async for msg in ws:
|
|
data = json.loads(msg)
|
|
if "sentences" in data:
|
|
print_result(data, show_spk=args.spk)
|
|
if data.get("is_final") or data.get("event") == "stopped":
|
|
break
|
|
|
|
await asyncio.gather(send_audio(), recv_results())
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Fun-ASR-Nano Python Client")
|
|
parser.add_argument("--server", type=str, default="ws://localhost:10095")
|
|
parser.add_argument("--mic", action="store_true", help="Use microphone input")
|
|
parser.add_argument("--file", type=str, help="Audio file to transcribe")
|
|
parser.add_argument("--hotwords", type=str, default="", help="Hotwords (comma-separated)")
|
|
parser.add_argument("--spk", action="store_true", default=True, help="Show speaker IDs")
|
|
parser.add_argument("--no-spk", dest="spk", action="store_false")
|
|
args = parser.parse_args()
|
|
|
|
if not args.mic and not args.file:
|
|
parser.error("Specify --mic or --file")
|
|
|
|
try:
|
|
if args.mic:
|
|
asyncio.run(run_mic(args))
|
|
else:
|
|
asyncio.run(run_file(args))
|
|
except KeyboardInterrupt:
|
|
print(f"\n{RESET}Interrupted.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|