feat: Add editor enhancements - export video, audio preview, publish panel, configurable ports

This commit is contained in:
empty
2026-01-06 17:29:43 +08:00
parent 79a6c2ef3e
commit 96eacf06ba
18 changed files with 2946 additions and 1701 deletions

View File

@@ -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"]
)