Files
2026-07-13 13:25:10 +08:00

158 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Browser demo for the FunASR OpenAI-compatible API."""
from __future__ import annotations
import argparse
import json
import mimetypes
import os
from pathlib import Path
import urllib.error
import urllib.request
import uuid
DEFAULT_BASE_URL = "http://localhost:8000"
DEFAULT_MODEL = "sensevoice"
def request_json(url: str, timeout: float) -> dict:
request = urllib.request.Request(url, headers={"Accept": "application/json"})
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def multipart_body(audio_path: Path, model: str, response_format: str) -> tuple[bytes, str]:
boundary = f"----funasr-gradio-{uuid.uuid4().hex}"
content_type = mimetypes.guess_type(str(audio_path))[0] or "application/octet-stream"
parts: list[bytes] = []
def add_text(name: str, value: str) -> None:
parts.append(
(
f"--{boundary}\r\n"
f"Content-Disposition: form-data; name=\"{name}\"\r\n\r\n"
f"{value}\r\n"
).encode("utf-8")
)
parts.append(
(
f"--{boundary}\r\n"
f"Content-Disposition: form-data; name=\"file\"; filename=\"{audio_path.name}\"\r\n"
f"Content-Type: {content_type}\r\n\r\n"
).encode("utf-8")
)
parts.append(audio_path.read_bytes())
parts.append(b"\r\n")
add_text("model", model)
add_text("response_format", response_format)
parts.append(f"--{boundary}--\r\n".encode("utf-8"))
return b"".join(parts), boundary
def transcribe_audio(base_url: str, audio_path: str | None, model: str, response_format: str, timeout: float) -> tuple[str, str]:
if not audio_path:
return "", "Upload or record an audio file first."
base_url = base_url.rstrip("/")
path = Path(audio_path)
body, boundary = multipart_body(path, model, response_format)
request = urllib.request.Request(
f"{base_url}/v1/audio/transcriptions",
data=body,
method="POST",
headers={
"Accept": "application/json",
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(len(body)),
},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
payload = json.loads(response.read().decode("utf-8"))
text = payload.get("text", "")
return text, json.dumps(payload, ensure_ascii=False, indent=2)
def check_service(base_url: str, timeout: float) -> str:
base_url = base_url.rstrip("/")
health = request_json(f"{base_url}/health", timeout)
models = request_json(f"{base_url}/v1/models", timeout)
return json.dumps({"health": health, "models": models}, ensure_ascii=False, indent=2)
def safe_transcribe(base_url: str, audio_path: str | None, model: str, response_format: str, timeout: float) -> tuple[str, str]:
try:
return transcribe_audio(base_url, audio_path, model, response_format, timeout)
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
return "", f"HTTP {error.code} from {error.url}: {detail}"
except Exception as error:
return "", f"Transcription failed: {error}"
def safe_check(base_url: str, timeout: float) -> str:
try:
return check_service(base_url, timeout)
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
return f"HTTP {error.code} from {error.url}: {detail}"
except Exception as error:
return f"Service check failed: {error}"
def build_app(default_base_url: str, default_timeout: float):
try:
import gradio as gr
except ImportError as error:
raise SystemExit("Install Gradio first: pip install gradio") from error
with gr.Blocks(title="FunASR OpenAI API Demo") as demo:
gr.Markdown("# FunASR OpenAI-Compatible API Demo")
gr.Markdown("Start `python server.py --model sensevoice --device cuda --port 8000`, then upload or record audio here.")
with gr.Row():
base_url = gr.Textbox(label="API base URL", value=default_base_url)
model = gr.Dropdown(
label="Model alias",
choices=["sensevoice", "paraformer", "paraformer-en", "fun-asr-nano"],
value=DEFAULT_MODEL,
)
response_format = gr.Radio(label="Response format", choices=["json", "verbose_json"], value="verbose_json")
timeout = gr.Number(label="Timeout seconds", value=default_timeout, precision=0)
audio = gr.Audio(label="Audio", sources=["upload", "microphone"], type="filepath")
with gr.Row():
check_button = gr.Button("Check service")
transcribe_button = gr.Button("Transcribe", variant="primary")
transcript = gr.Textbox(label="Transcript", lines=6)
raw_json = gr.Code(label="Raw JSON or status", language="json")
check_button.click(fn=safe_check, inputs=[base_url, timeout], outputs=raw_json)
transcribe_button.click(
fn=safe_transcribe,
inputs=[base_url, audio, model, response_format, timeout],
outputs=[transcript, raw_json],
)
return demo
def main() -> None:
parser = argparse.ArgumentParser(description="Run a Gradio demo for the FunASR OpenAI-compatible API")
parser.add_argument("--base-url", default=os.getenv("BASE_URL", DEFAULT_BASE_URL), help="FunASR API base URL")
parser.add_argument("--host", default=os.getenv("GRADIO_HOST", "127.0.0.1"), help="Gradio bind host")
parser.add_argument("--port", type=int, default=int(os.getenv("GRADIO_PORT", "7860")), help="Gradio bind port")
parser.add_argument("--timeout", type=float, default=float(os.getenv("TIMEOUT", "300")), help="HTTP timeout in seconds")
parser.add_argument("--share", action="store_true", help="Create a temporary Gradio share link")
args = parser.parse_args()
app = build_app(args.base_url, args.timeout)
app.launch(server_name=args.host, server_port=args.port, share=args.share)
if __name__ == "__main__":
main()