| import base64 |
| import tempfile |
| from pathlib import Path |
|
|
| from fastapi import APIRouter, HTTPException, UploadFile, Query |
| from pydantic import BaseModel |
|
|
| from .core import get_video_audio_service |
| from plugins.audio.asr_drivers import get_asr_registry |
| 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 VideoBase64Request(BaseModel): |
| video_base64: str |
| suffix: str = ".mp4" |
| language: str | None = None |
| provider: str | None = None |
|
|
|
|
| @router.get("/status") |
| async def get_status(): |
| if plugin is None: |
| return { |
| "name": "video", |
| "enabled": False, |
| "message": "插件未加载", |
| } |
| return plugin.get_status() |
|
|
|
|
| @router.post("/upload/transcribe") |
| async def transcribe_video_upload( |
| file: UploadFile, |
| language: str | None = None, |
| provider: str | None = None, |
| wait: bool = Query(False, description="是否同步等待转录完成"), |
| ): |
| """上传视频文件,提取音频并转录 |
| |
| 默认异步返回 run_id,设置 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("video") |
|
|
| |
| 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 ".mp4" |
| with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file: |
| tmp_file.write(content) |
| tmp_path = tmp_file.name |
|
|
| try: |
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="extract_audio", |
| message="开始从视频提取音频", |
| ) |
|
|
| |
| video_service = get_video_audio_service() |
| extraction = video_service.extract_audio(tmp_path) |
|
|
| if not extraction["success"]: |
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="extract_audio", |
| message=f"音频提取失败: {extraction['error']}", |
| level="error", |
| ) |
| run_service.finish_run( |
| run_id=run.run_id, |
| status=RunStatus.FAILED, |
| error=extraction["error"], |
| ) |
| raise HTTPException(status_code=400, detail=extraction["error"]) |
|
|
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="extract_audio", |
| message="音频提取完成", |
| ) |
|
|
| |
| run_service.add_event( |
| run_id=run.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.run_id, |
| stage="transcribe", |
| message="开始转录", |
| ) |
|
|
| |
| audio_path = extraction["audio_path"] |
| result = driver.transcribe(audio_path, language) |
|
|
| |
| Path(audio_path).unlink(missing_ok=True) |
|
|
| if result.success: |
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="transcribe", |
| message=f"转录完成: {len(result.text)} 字符", |
| detail=f"timings={result.timings}", |
| ) |
|
|
| |
| run_service.finish_run( |
| run_id=run.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.run_id, |
| stage="transcribe", |
| message=f"转录失败: {result.error}", |
| level="error", |
| ) |
|
|
| |
| run_service.finish_run( |
| run_id=run.run_id, |
| status=RunStatus.FAILED, |
| error=result.error, |
| ) |
|
|
| |
| if wait: |
| return { |
| "run_id": run.run_id, |
| "status": "succeeded" if result.success else "failed", |
| "provider": result.provider, |
| "model": result.model, |
| "text": result.text, |
| "segments": result.segments, |
| "timings": result.timings, |
| "error": result.error, |
| } |
|
|
| |
| return { |
| "run_id": run.run_id, |
| "status": "running", |
| "provider": provider_name, |
| "model": driver.model_id, |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="transcribe", |
| message=f"转录异常: {str(e)}", |
| level="error", |
| ) |
| run_service.finish_run( |
| run_id=run.run_id, |
| status=RunStatus.FAILED, |
| error=str(e), |
| ) |
| raise HTTPException(status_code=500, detail=f"转录失败: {str(e)}") |
| finally: |
| |
| Path(tmp_path).unlink(missing_ok=True) |
|
|
|
|
| @router.post("/video-base64/transcribe") |
| async def transcribe_video_base64(request: VideoBase64Request): |
| """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("video") |
|
|
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="upload", |
| message="Base64 视频已接收", |
| detail=f"provider={provider_name}, language={request.language}", |
| ) |
|
|
| |
| try: |
| video_bytes = base64.b64decode(request.video_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(video_bytes) |
| tmp_path = tmp_file.name |
|
|
| try: |
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="extract_audio", |
| message="开始从视频提取音频", |
| ) |
|
|
| |
| video_service = get_video_audio_service() |
| extraction = video_service.extract_audio(tmp_path) |
|
|
| if not extraction["success"]: |
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="extract_audio", |
| message=f"音频提取失败: {extraction['error']}", |
| level="error", |
| ) |
| run_service.finish_run( |
| run_id=run.run_id, |
| status=RunStatus.FAILED, |
| error=extraction["error"], |
| ) |
| return { |
| "run_id": run.run_id, |
| "status": "failed", |
| "error": extraction["error"], |
| } |
|
|
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="extract_audio", |
| message="音频提取完成", |
| ) |
|
|
| |
| run_service.add_event( |
| run_id=run.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.run_id, |
| stage="transcribe", |
| message="开始转录", |
| ) |
|
|
| |
| audio_path = extraction["audio_path"] |
| result = driver.transcribe(audio_path, request.language) |
|
|
| |
| Path(audio_path).unlink(missing_ok=True) |
|
|
| if result.success: |
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="transcribe", |
| message=f"转录完成: {len(result.text)} 字符", |
| detail=f"timings={result.timings}", |
| ) |
|
|
| |
| run_service.finish_run( |
| run_id=run.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.run_id, |
| stage="transcribe", |
| message=f"转录失败: {result.error}", |
| level="error", |
| ) |
|
|
| |
| run_service.finish_run( |
| run_id=run.run_id, |
| status=RunStatus.FAILED, |
| error=result.error, |
| ) |
|
|
| return { |
| "run_id": run.run_id, |
| "status": "succeeded" if result.success else "failed", |
| "provider": result.provider, |
| "model": result.model, |
| "text": result.text, |
| "segments": result.segments, |
| "timings": result.timings, |
| "error": result.error, |
| } |
|
|
| except Exception as e: |
| |
| run_service.add_event( |
| run_id=run.run_id, |
| stage="transcribe", |
| message=f"转录异常: {str(e)}", |
| level="error", |
| ) |
| run_service.finish_run( |
| run_id=run.run_id, |
| status=RunStatus.FAILED, |
| error=str(e), |
| ) |
| raise HTTPException(status_code=500, detail=f"转录失败: {str(e)}") |
| finally: |
| |
| Path(tmp_path).unlink(missing_ok=True) |
|
|