| import asyncio |
| import base64 |
| import tempfile |
| from pathlib import Path |
|
|
| from fastapi import APIRouter, HTTPException, UploadFile, Query |
| from pydantic import BaseModel |
|
|
| from .asr_drivers import get_asr_registry, AsrDriver, AsrResult |
| from app.plugins.run_log import get_run_log_service, RunStatus |
|
|
| router = APIRouter() |
| plugin = None |
|
|
|
|
| def set_plugin_instance(plugin_instance): |
| global plugin |
| plugin = plugin_instance |
|
|
|
|
| class TranscribeBase64Request(BaseModel): |
| audio_base64: str |
| suffix: str = ".wav" |
| language: str | None = None |
| provider: str | None = None |
|
|
|
|
| @router.get("/status") |
| async def get_status(): |
| if plugin is None: |
| return { |
| "name": "audio", |
| "enabled": False, |
| "message": "插件未加载", |
| } |
| return plugin.get_status() |
|
|
|
|
| @router.get("/providers") |
| async def list_providers(): |
| """列出所有 ASR provider 及其可用性""" |
| registry = get_asr_registry() |
| availability = registry.probe_all() |
| return { |
| "providers": [ |
| { |
| "name": name, |
| "status": status.value, |
| } |
| for name, status in availability.items() |
| ], |
| "default": registry.get_default_provider(), |
| } |
|
|
|
|
| def _transcribe_and_finish( |
| run_id: str, |
| driver: AsrDriver, |
| audio_path: str, |
| language: str | None, |
| provider_name: str, |
| availability_value: str, |
| ) -> None: |
| """同步执行转录并写入事件/完成 run,最后清理临时文件。 |
| |
| 供 wait 模式(to_thread 同步等待)和非 wait 模式(后台任务)共用, |
| 保证事件写入逻辑只有一份。 |
| """ |
| run_service = get_run_log_service() |
| try: |
| |
| run_service.add_event( |
| run_id=run_id, |
| stage="model_probe", |
| message=f"模型探测: {driver.model_id}", |
| detail=f"provider={provider_name}, status={availability_value}", |
| ) |
|
|
| |
| run_service.add_event( |
| run_id=run_id, |
| stage="transcribe", |
| message="开始转录", |
| ) |
|
|
| |
| result: AsrResult = driver.transcribe(audio_path, language) |
|
|
| if result.success: |
| |
| run_service.add_event( |
| run_id=run_id, |
| stage="transcribe", |
| message=f"转录完成: {len(result.text)} 字符", |
| detail=f"timings={result.timings}", |
| ) |
| run_service.finish_run( |
| run_id=run_id, |
| status=RunStatus.SUCCEEDED, |
| result={ |
| "text": result.text, |
| "segments": result.segments, |
| "provider": result.provider, |
| "model": result.model, |
| "timings": result.timings, |
| }, |
| ) |
| else: |
| |
| run_service.add_event( |
| run_id=run_id, |
| stage="transcribe", |
| message=f"转录失败: {result.error}", |
| level="error", |
| ) |
| run_service.finish_run( |
| run_id=run_id, |
| status=RunStatus.FAILED, |
| error=result.error, |
| ) |
| except Exception as e: |
| |
| run_service.add_event( |
| run_id=run_id, |
| stage="transcribe", |
| message=f"转录异常: {str(e)}", |
| level="error", |
| ) |
| run_service.finish_run( |
| run_id=run_id, |
| status=RunStatus.FAILED, |
| error=str(e), |
| ) |
| finally: |
| |
| Path(audio_path).unlink(missing_ok=True) |
|
|
|
|
| @router.post("/upload") |
| async def transcribe_upload( |
| file: UploadFile, |
| language: str | None = None, |
| provider: str | None = None, |
| wait: bool = Query(False, description="是否同步等待转录完成"), |
| ): |
| """上传音频文件并转录 |
| |
| 默认异步返回 run_id,前端轮询 run 事件获取实时进度; |
| 设置 wait=true 同步等待结果。 |
| """ |
| if plugin is None or not plugin.enabled: |
| raise HTTPException(status_code=400, detail="插件未启用") |
|
|
| |
| registry = get_asr_registry() |
| provider_name = provider or registry.get_default_provider() |
| driver = registry.get_driver(provider_name) |
| if driver is None: |
| raise HTTPException(status_code=400, detail=f"未知的 ASR provider: {provider_name}") |
|
|
| |
| availability = driver.check_availability() |
| if availability.value != "available": |
| raise HTTPException( |
| status_code=400, |
| detail=f"ASR provider {provider_name} 不可用: {availability.value}", |
| ) |
|
|
| |
| run_service = get_run_log_service() |
| run = run_service.create_run("audio") |
|
|
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="upload", |
| message=f"音频文件已上传: {file.filename}", |
| detail=f"provider={provider_name}, language={language}", |
| ) |
|
|
| |
| content = await file.read() |
| suffix = "." + file.filename.rsplit(".", 1)[-1] if "." in file.filename else ".wav" |
| with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file: |
| tmp_file.write(content) |
| tmp_path = tmp_file.name |
|
|
| if wait: |
| |
| await asyncio.to_thread( |
| _transcribe_and_finish, |
| run.run_id, driver, tmp_path, language, provider_name, availability.value, |
| ) |
| |
| finished = run_service.get_run(run.run_id) |
| return { |
| "run_id": run.run_id, |
| "status": finished.status.value if finished else "failed", |
| "provider": provider_name, |
| "model": driver.model_id, |
| "text": (finished.result or {}).get("text", "") if finished else "", |
| "segments": (finished.result or {}).get("segments", []) if finished else [], |
| "timings": (finished.result or {}).get("timings") if finished else None, |
| "error": finished.error if finished else "运行记录缺失", |
| } |
|
|
| |
| asyncio.create_task( |
| asyncio.to_thread( |
| _transcribe_and_finish, |
| run.run_id, driver, tmp_path, language, provider_name, availability.value, |
| ) |
| ) |
| return { |
| "run_id": run.run_id, |
| "status": "running", |
| "provider": provider_name, |
| "model": driver.model_id, |
| } |
|
|
|
|
| @router.post("/audio-base64") |
| async def transcribe_base64(request: TranscribeBase64Request): |
| """Base64 音频转录(兼容旧接口,同步返回结果)""" |
| if plugin is None or not plugin.enabled: |
| raise HTTPException(status_code=400, detail="插件未启用") |
|
|
| |
| registry = get_asr_registry() |
| provider_name = request.provider or registry.get_default_provider() |
| driver = registry.get_driver(provider_name) |
| if driver is None: |
| raise HTTPException(status_code=400, detail=f"未知的 ASR provider: {provider_name}") |
|
|
| |
| availability = driver.check_availability() |
| if availability.value != "available": |
| raise HTTPException( |
| status_code=400, |
| detail=f"ASR provider {provider_name} 不可用: {availability.value}", |
| ) |
|
|
| |
| run_service = get_run_log_service() |
| run = run_service.create_run("audio") |
|
|
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="upload", |
| message="Base64 音频已接收", |
| detail=f"provider={provider_name}, language={request.language}", |
| ) |
|
|
| |
| try: |
| audio_bytes = base64.b64decode(request.audio_base64) |
| except Exception as e: |
| run_service.finish_run(run_id=run.run_id, status=RunStatus.FAILED, error=f"Base64 解码失败: {e}") |
| raise HTTPException(status_code=400, detail=f"Base64 解码失败: {e}") |
|
|
| suffix = request.suffix if request.suffix.startswith(".") else f".{request.suffix}" |
| with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file: |
| tmp_file.write(audio_bytes) |
| tmp_path = tmp_file.name |
|
|
| |
| await asyncio.to_thread( |
| _transcribe_and_finish, |
| run.run_id, driver, tmp_path, request.language, provider_name, availability.value, |
| ) |
| finished = run_service.get_run(run.run_id) |
| return { |
| "run_id": run.run_id, |
| "status": finished.status.value if finished else "failed", |
| "provider": provider_name, |
| "model": driver.model_id, |
| "text": (finished.result or {}).get("text", "") if finished else "", |
| "segments": (finished.result or {}).get("segments", []) if finished else [], |
| "timings": (finished.result or {}).get("timings") if finished else None, |
| "error": finished.error if finished else None, |
| } |
|
|