feat: Add editor enhancements - export video, audio preview, publish panel, configurable ports
This commit is contained in:
@@ -38,10 +38,19 @@ from api.schemas.editor import (
|
||||
RegenerateAudioResponse,
|
||||
InpaintRequest,
|
||||
InpaintResponse,
|
||||
ExportRequest,
|
||||
ExportResponse,
|
||||
ExportStatusResponse,
|
||||
)
|
||||
from fastapi import BackgroundTasks
|
||||
import asyncio
|
||||
import uuid as uuid_module
|
||||
|
||||
router = APIRouter(prefix="/editor", tags=["Editor"])
|
||||
|
||||
# Export task storage
|
||||
_export_tasks: dict = {}
|
||||
|
||||
|
||||
def _path_to_url(file_path: str, base_url: str = "http://localhost:8000") -> str:
|
||||
"""Convert local file path to URL accessible through API"""
|
||||
@@ -683,3 +692,138 @@ async def inpaint_frame_image(
|
||||
except Exception as e:
|
||||
logger.error(f"Inpainting failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Video Export Endpoints
|
||||
# ============================================================
|
||||
|
||||
@router.post(
|
||||
"/storyboard/{storyboard_id}/export",
|
||||
response_model=ExportResponse
|
||||
)
|
||||
async def export_video(
|
||||
storyboard_id: str = Path(..., description="Storyboard/task ID"),
|
||||
request: ExportRequest = None,
|
||||
background_tasks: BackgroundTasks = None
|
||||
):
|
||||
"""
|
||||
Export edited video
|
||||
|
||||
Concatenates all video segments in order and optionally adds BGM.
|
||||
Returns immediately with task_id for tracking progress.
|
||||
"""
|
||||
if storyboard_id not in _storyboard_cache:
|
||||
raise HTTPException(status_code=404, detail=f"Storyboard {storyboard_id} not found")
|
||||
|
||||
storyboard = _storyboard_cache[storyboard_id]
|
||||
frames = storyboard["frames"]
|
||||
|
||||
# Check if all frames have video segments
|
||||
missing_segments = [f["id"] for f in frames if not f.get("video_segment_path")]
|
||||
if missing_segments:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Missing video segments for frames: {missing_segments}"
|
||||
)
|
||||
|
||||
# Create export task
|
||||
task_id = str(uuid_module.uuid4())
|
||||
_export_tasks[task_id] = {
|
||||
"status": "pending",
|
||||
"progress": 0.0,
|
||||
"video_path": None,
|
||||
"error": None
|
||||
}
|
||||
|
||||
# Run export in background
|
||||
async def run_export():
|
||||
try:
|
||||
_export_tasks[task_id]["status"] = "processing"
|
||||
_export_tasks[task_id]["progress"] = 0.1
|
||||
|
||||
from pixelle_video.services.video import VideoService
|
||||
import os
|
||||
|
||||
video_service = VideoService()
|
||||
|
||||
# Get video segment paths (sorted by order)
|
||||
sorted_frames = sorted(frames, key=lambda f: f.get("order", 0))
|
||||
|
||||
# Convert URLs back to file paths
|
||||
video_segments = []
|
||||
for frame in sorted_frames:
|
||||
path = frame.get("video_segment_path", "")
|
||||
if path.startswith("http"):
|
||||
# Extract path from URL
|
||||
path = path.replace("http://localhost:8000/api/files/", "output/")
|
||||
video_segments.append(path)
|
||||
|
||||
_export_tasks[task_id]["progress"] = 0.3
|
||||
|
||||
# Create output directory
|
||||
output_dir = f"output/{storyboard_id}"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Output path
|
||||
output_path = f"{output_dir}/exported_{task_id[:8]}.mp4"
|
||||
|
||||
# BGM settings
|
||||
bgm_path = request.bgm_path if request else None
|
||||
bgm_volume = request.bgm_volume if request else 0.2
|
||||
|
||||
_export_tasks[task_id]["progress"] = 0.5
|
||||
|
||||
# Concatenate videos
|
||||
video_service.concat_videos(
|
||||
videos=video_segments,
|
||||
output=output_path,
|
||||
method="demuxer",
|
||||
bgm_path=bgm_path,
|
||||
bgm_volume=bgm_volume
|
||||
)
|
||||
|
||||
_export_tasks[task_id]["progress"] = 1.0
|
||||
_export_tasks[task_id]["status"] = "completed"
|
||||
_export_tasks[task_id]["video_path"] = output_path
|
||||
|
||||
logger.info(f"Exported video for storyboard {storyboard_id}: {output_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Export failed: {e}")
|
||||
_export_tasks[task_id]["status"] = "failed"
|
||||
_export_tasks[task_id]["error"] = str(e)
|
||||
|
||||
background_tasks.add_task(run_export)
|
||||
|
||||
return ExportResponse(
|
||||
task_id=task_id,
|
||||
status="pending"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/export/{task_id}/status",
|
||||
response_model=ExportStatusResponse
|
||||
)
|
||||
async def get_export_status(task_id: str = Path(..., description="Export task ID")):
|
||||
"""
|
||||
Get export task status and progress
|
||||
"""
|
||||
if task_id not in _export_tasks:
|
||||
raise HTTPException(status_code=404, detail=f"Export task {task_id} not found")
|
||||
|
||||
task = _export_tasks[task_id]
|
||||
|
||||
download_url = None
|
||||
if task["video_path"]:
|
||||
download_url = _path_to_url(task["video_path"])
|
||||
|
||||
return ExportStatusResponse(
|
||||
task_id=task_id,
|
||||
status=task["status"],
|
||||
progress=task["progress"],
|
||||
video_path=task["video_path"],
|
||||
download_url=download_url,
|
||||
error=task["error"]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user