158 lines
6.1 KiB
Python
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()
|