140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
import logging
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import aiofiles
|
|
from fastapi import APIRouter, UploadFile, File, HTTPException
|
|
from fastapi.responses import FileResponse
|
|
|
|
from app.models.video import VideoUploadResponse, FullTranscriptResponse
|
|
from app.services.video_service import VideoService
|
|
from app.services.asr_client import ASRClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["video"])
|
|
|
|
|
|
def _get_video_service() -> VideoService:
|
|
from app.core.config import get_settings
|
|
|
|
s = get_settings()
|
|
return VideoService(
|
|
upload_dir=s.video_upload_dir,
|
|
max_size_mb=s.max_video_size_mb,
|
|
supported_formats=s.supported_video_formats,
|
|
)
|
|
|
|
|
|
@router.post("/video/upload", response_model=VideoUploadResponse)
|
|
async def upload_video(file: UploadFile = File(...)):
|
|
service = _get_video_service()
|
|
filename = file.filename or "unknown"
|
|
ext = Path(filename).suffix.lower()
|
|
upload_start = time.monotonic()
|
|
|
|
logger.info("upload-started filename=%s content_type=%s", filename, file.content_type)
|
|
|
|
total_size = 0
|
|
video_id = uuid.uuid4().hex[:12]
|
|
dest_path = service.upload_dir / f"{video_id}{ext}"
|
|
|
|
try:
|
|
async with aiofiles.open(dest_path, "wb") as out:
|
|
while chunk := await file.read(1024 * 1024):
|
|
total_size += len(chunk)
|
|
if total_size > service.max_size_bytes:
|
|
raise HTTPException(
|
|
status_code=413,
|
|
detail=f"File exceeds {service.max_size_bytes // 1024 // 1024}MB limit",
|
|
)
|
|
await out.write(chunk)
|
|
except HTTPException:
|
|
dest_path.unlink(missing_ok=True)
|
|
raise
|
|
except Exception:
|
|
dest_path.unlink(missing_ok=True)
|
|
raise HTTPException(status_code=500, detail="Upload failed")
|
|
|
|
service.validate_video(filename, file.content_type, total_size)
|
|
upload_duration = time.monotonic() - upload_start
|
|
logger.info(
|
|
"upload-completed video_id=%s filename=%s size=%d duration=%.2fs",
|
|
video_id,
|
|
filename,
|
|
total_size,
|
|
upload_duration,
|
|
)
|
|
|
|
return VideoUploadResponse(
|
|
video_id=video_id,
|
|
filename=filename,
|
|
size_bytes=total_size,
|
|
url=f"/api/v1/video/{video_id}",
|
|
)
|
|
|
|
|
|
@router.get("/video/{video_id}")
|
|
async def serve_video(video_id: str):
|
|
service = _get_video_service()
|
|
video_path = service.get_video_path(video_id)
|
|
ext = video_path.suffix.lower()
|
|
media_types = {
|
|
".mp4": "video/mp4",
|
|
".webm": "video/webm",
|
|
".mov": "video/quicktime",
|
|
".avi": "video/x-msvideo",
|
|
".mkv": "video/x-matroska",
|
|
}
|
|
return FileResponse(str(video_path), media_type=media_types.get(ext, "application/octet-stream"))
|
|
|
|
|
|
@router.post("/video/{video_id}/transcribe", response_model=FullTranscriptResponse)
|
|
async def transcribe_video(video_id: str, language: str = "yue"):
|
|
from app.core.config import get_settings
|
|
settings = get_settings()
|
|
|
|
provider = settings.asr_provider
|
|
if provider == "dashscope" and not settings.dashscope_api_key:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="DASHSCOPE_API_KEY is not configured. Set it in .env to enable transcription.",
|
|
)
|
|
if provider == "openrouter" and not settings.openrouter_api_key:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="OPENROUTER_API_KEY is not configured. Set it in .env to enable OpenRouter ASR.",
|
|
)
|
|
|
|
transcribe_start = time.monotonic()
|
|
logger.info("transcribe-started video_id=%s language=%s provider=%s", video_id, language, provider)
|
|
|
|
service = _get_video_service()
|
|
wav_path = await service.extract_audio(video_id)
|
|
|
|
try:
|
|
audio_bytes = wav_path.read_bytes()
|
|
logger.debug("audio-extracted video_id=%s wav_size=%d", video_id, len(audio_bytes))
|
|
asr = ASRClient(settings)
|
|
text = await asr.transcribe_full(audio_bytes, language=language)
|
|
except Exception as e:
|
|
logger.error("transcribe-failed video_id=%s error=%s", video_id, e)
|
|
raise HTTPException(status_code=500, detail=f"Transcription failed: {str(e)}")
|
|
finally:
|
|
if wav_path.exists():
|
|
wav_path.unlink(missing_ok=True)
|
|
|
|
transcribe_duration = time.monotonic() - transcribe_start
|
|
logger.info(
|
|
"transcribe-completed video_id=%s text_len=%d duration=%.2fs",
|
|
video_id,
|
|
len(text),
|
|
transcribe_duration,
|
|
)
|
|
|
|
return FullTranscriptResponse(
|
|
text=text,
|
|
language=language,
|
|
duration_seconds=None,
|
|
)
|