added share function
This commit is contained in:
@@ -4,7 +4,8 @@ import logging
|
||||
import mimetypes
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, BinaryIO, Dict, Optional
|
||||
from typing import Any, BinaryIO, Dict, List, Optional
|
||||
import json
|
||||
|
||||
from fastapi import (
|
||||
BackgroundTasks,
|
||||
@@ -51,6 +52,7 @@ SHA256_PATH = ApiPath(
|
||||
description="SHA-256 hex digest of the exact uploaded video bytes",
|
||||
)
|
||||
ALLOWED_VIDEO_EXTENSIONS = {".mp4", ".m4v", ".mov", ".mkv", ".webm"}
|
||||
ALLOWED_AUDIO_EXTENSIONS = {".m4a", ".mp4", ".wav"}
|
||||
|
||||
|
||||
class UploadTooLargeError(ValueError):
|
||||
@@ -380,6 +382,116 @@ def create_app(
|
||||
if not service_settings.keep_attempt_audio or not completed:
|
||||
temporary_path.unlink(missing_ok=True)
|
||||
|
||||
@application.post("/api/v1/dub-shares", status_code=201)
|
||||
async def create_dub_share(
|
||||
request: Request,
|
||||
video_hash: str = Form(...),
|
||||
title: str = Form(default="我的口语配音"),
|
||||
segments: str = Form(...),
|
||||
files: List[UploadFile] = File(...),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
segment_items = json.loads(segments)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail="segments must be valid JSON.") from exc
|
||||
if not isinstance(segment_items, list) or not segment_items:
|
||||
raise HTTPException(status_code=400, detail="At least one dubbing segment is required.")
|
||||
if len(segment_items) != len(files):
|
||||
raise HTTPException(status_code=400, detail="Segment count does not match audio file count.")
|
||||
document = find_document(video_hash.lower())
|
||||
if document is None:
|
||||
raise HTTPException(status_code=404, detail="Video or sentence boundaries were not found.")
|
||||
|
||||
prepared_segments: List[Dict[str, Any]] = []
|
||||
saved_files: List[Path] = []
|
||||
try:
|
||||
for item, upload in zip(segment_items, files):
|
||||
sentence_index = int(item["sentence_index"])
|
||||
boundary = next((s for s in document.sentences if s.index == sentence_index), None)
|
||||
if boundary is None:
|
||||
raise HTTPException(status_code=404, detail=f"Sentence {sentence_index} was not found.")
|
||||
suffix = Path(upload.filename or "dub.m4a").suffix.lower()
|
||||
if suffix == ".mp4":
|
||||
suffix = ".m4a"
|
||||
if suffix not in ALLOWED_AUDIO_EXTENSIONS:
|
||||
raise HTTPException(status_code=415, detail=f"Unsupported audio extension: {suffix or '(none)'}")
|
||||
audio_name = f"{uuid.uuid4().hex}{suffix}"
|
||||
audio_path = service_settings.dub_shares_dir / audio_name
|
||||
await run_in_threadpool(_persist_upload, upload.file, audio_path, 50 * 1024 * 1024)
|
||||
saved_files.append(audio_path)
|
||||
prepared_segments.append({
|
||||
"sentence_index": sentence_index,
|
||||
"start_ms": boundary.start_ms,
|
||||
"end_ms": boundary.end_ms,
|
||||
"text": boundary.text,
|
||||
"audio_filename": audio_name,
|
||||
"audio_size_bytes": audio_path.stat().st_size,
|
||||
})
|
||||
share = video_repository.create_dub_share(
|
||||
video_hash=video_hash.lower(),
|
||||
title=title,
|
||||
segments=prepared_segments,
|
||||
)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Video was not found.")
|
||||
return {
|
||||
**share,
|
||||
"share_url": f"/dub-shares/{share['share_id']}",
|
||||
}
|
||||
except UploadTooLargeError as exc:
|
||||
raise HTTPException(status_code=413, detail=str(exc)) from exc
|
||||
except HTTPException:
|
||||
raise
|
||||
finally:
|
||||
for upload in files:
|
||||
await upload.close()
|
||||
# Files intentionally remain until share deletion because the web page references them.
|
||||
del saved_files
|
||||
|
||||
@application.get("/api/v1/dub-shares/{share_id}")
|
||||
def get_dub_share(share_id: str) -> Dict[str, Any]:
|
||||
share = video_repository.get_dub_share(share_id)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Dubbing share was not found.")
|
||||
return {
|
||||
"share_id": share["share_id"],
|
||||
"title": share["title"],
|
||||
"created_at": share["created_at"],
|
||||
"segments": [
|
||||
{
|
||||
"index": row["sentence_index"],
|
||||
"start_ms": row["start_ms"],
|
||||
"end_ms": row["end_ms"],
|
||||
"text": row["text"],
|
||||
"audio_url": f"/api/v1/dub-shares/{share_id}/audio/{row['audio_filename']}",
|
||||
}
|
||||
for row in share["segments"]
|
||||
],
|
||||
}
|
||||
|
||||
@application.get("/api/v1/dub-shares/{share_id}/audio/{filename}")
|
||||
def get_dub_share_audio(share_id: str, filename: str) -> FileResponse:
|
||||
share = video_repository.get_dub_share(share_id)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Dubbing share was not found.")
|
||||
safe_name = Path(filename).name
|
||||
row = next((item for item in share["segments"] if item["audio_filename"] == safe_name), None)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Audio was not found.")
|
||||
media_path = service_settings.dub_shares_dir / safe_name
|
||||
if not media_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="Stored audio file is missing.")
|
||||
return FileResponse(media_path, media_type="audio/mp4", headers={"Cache-Control": "public, max-age=31536000"})
|
||||
|
||||
@application.get("/dub-shares/{share_id}", include_in_schema=False)
|
||||
def dub_share_page(share_id: str) -> FileResponse:
|
||||
if video_repository.get_dub_share(share_id) is None:
|
||||
raise HTTPException(status_code=404, detail="Dubbing share was not found.")
|
||||
page = static_dir / "dub-share.html"
|
||||
if not page.is_file():
|
||||
raise HTTPException(status_code=404, detail="Dubbing share UI is not installed.")
|
||||
return FileResponse(page)
|
||||
|
||||
return application
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user