#!/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()