implemented the lower priority features; haven't tested them
This commit is contained in:
@ -8,9 +8,10 @@ from typing import List, Optional
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.video_editor import export_stream_copy, export_reencode, export_reencode_with_subs
|
||||
from services.video_editor import export_stream_copy, export_reencode, export_reencode_with_subs, mix_background_music, concat_clips
|
||||
from services.audio_cleaner import clean_audio
|
||||
from services.caption_generator import generate_srt, generate_ass, save_captions
|
||||
from services.background_removal import remove_background_on_export as remove_bg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@ -36,6 +37,22 @@ class ExportWordModel(BaseModel):
|
||||
confidence: float = 0.0
|
||||
|
||||
|
||||
class ZoomConfigModel(BaseModel):
|
||||
enabled: bool = False
|
||||
zoomFactor: float = 1.0
|
||||
panX: float = 0.0
|
||||
panY: float = 0.0
|
||||
|
||||
|
||||
class BackgroundMusicModel(BaseModel):
|
||||
path: str
|
||||
volumeDb: float = 0.0
|
||||
duckingEnabled: bool = False
|
||||
duckingDb: float = 6.0
|
||||
duckingAttackMs: float = 10.0
|
||||
duckingReleaseMs: float = 200.0
|
||||
|
||||
|
||||
class ExportRequest(BaseModel):
|
||||
input_path: str
|
||||
output_path: str
|
||||
@ -53,6 +70,12 @@ class ExportRequest(BaseModel):
|
||||
captions: str = "none"
|
||||
words: Optional[List[ExportWordModel]] = None
|
||||
deleted_indices: Optional[List[int]] = None
|
||||
zoom: Optional[ZoomConfigModel] = None
|
||||
additional_clips: Optional[List[str]] = None
|
||||
background_music: Optional[BackgroundMusicModel] = None
|
||||
remove_background: bool = False
|
||||
background_replacement: str = "blur"
|
||||
background_replacement_value: str = ""
|
||||
|
||||
|
||||
class TranscriptExportRequest(BaseModel):
|
||||
@ -130,6 +153,29 @@ async def export_video(req: ExportRequest):
|
||||
if not segments and not mute_segments:
|
||||
raise HTTPException(status_code=400, detail="No segments to export")
|
||||
|
||||
# Convert zoom config to dict
|
||||
zoom_dict = None
|
||||
if req.zoom and req.zoom.enabled:
|
||||
zoom_dict = {
|
||||
"enabled": True,
|
||||
"zoomFactor": req.zoom.zoomFactor,
|
||||
"panX": req.zoom.panX,
|
||||
"panY": req.zoom.panY,
|
||||
}
|
||||
|
||||
# Handle additional clips: pre-concat before main editing
|
||||
working_input = req.input_path
|
||||
has_additional = bool(req.additional_clips)
|
||||
if has_additional:
|
||||
try:
|
||||
concat_output = req.output_path + ".concat.mp4"
|
||||
concat_clips(req.input_path, req.additional_clips, concat_output)
|
||||
working_input = concat_output
|
||||
logger.info("Pre-concatenated %d additional clips into %s", len(req.additional_clips), concat_output)
|
||||
except Exception as e:
|
||||
logger.warning(f"Clip concatenation failed (non-fatal): {e}")
|
||||
# Fall back to main input only
|
||||
|
||||
mapped_gain_segments = _map_ranges_to_output_timeline(gain_segments or [], segments)
|
||||
|
||||
has_gain = abs(float(req.global_gain_db)) > 1e-6 or bool(gain_segments)
|
||||
@ -141,7 +187,7 @@ async def export_video(req: ExportRequest):
|
||||
detail="Speed zones currently cannot be combined with mute/gain filters in one export",
|
||||
)
|
||||
|
||||
use_stream_copy = req.mode == "fast" and len(segments) == 1 and not mute_segments and not has_gain and not has_speed
|
||||
use_stream_copy = req.mode == "fast" and len(segments) == 1 and not mute_segments and not has_gain and not has_speed and not zoom_dict and not has_additional
|
||||
needs_reencode_for_subs = req.captions == "burn-in"
|
||||
|
||||
# Burn-in captions or audio filters require re-encode
|
||||
@ -162,10 +208,10 @@ async def export_video(req: ExportRequest):
|
||||
|
||||
try:
|
||||
if use_stream_copy:
|
||||
output = export_stream_copy(req.input_path, req.output_path, segments)
|
||||
output = export_stream_copy(working_input, req.output_path, segments)
|
||||
elif ass_path:
|
||||
output = export_reencode_with_subs(
|
||||
req.input_path,
|
||||
working_input,
|
||||
req.output_path,
|
||||
segments,
|
||||
ass_path,
|
||||
@ -177,10 +223,11 @@ async def export_video(req: ExportRequest):
|
||||
global_gain_db=req.global_gain_db,
|
||||
normalize_loudness=req.normalize_loudness,
|
||||
normalize_target_lufs=req.normalize_target_lufs,
|
||||
zoom_config=zoom_dict,
|
||||
)
|
||||
else:
|
||||
output = export_reencode(
|
||||
req.input_path,
|
||||
working_input,
|
||||
req.output_path,
|
||||
segments,
|
||||
resolution=req.resolution,
|
||||
@ -191,6 +238,7 @@ async def export_video(req: ExportRequest):
|
||||
global_gain_db=req.global_gain_db,
|
||||
normalize_loudness=req.normalize_loudness,
|
||||
normalize_target_lufs=req.normalize_target_lufs,
|
||||
zoom_config=zoom_dict,
|
||||
)
|
||||
finally:
|
||||
if ass_path and os.path.exists(ass_path):
|
||||
@ -209,7 +257,6 @@ async def export_video(req: ExportRequest):
|
||||
os.replace(muxed_path, output)
|
||||
logger.info(f"Audio enhanced and muxed into {output}")
|
||||
|
||||
# Cleanup
|
||||
try:
|
||||
os.remove(cleaned_audio)
|
||||
os.rmdir(tmp_dir)
|
||||
@ -218,6 +265,35 @@ async def export_video(req: ExportRequest):
|
||||
except Exception as e:
|
||||
logger.warning(f"Audio enhancement failed (non-fatal): {e}")
|
||||
|
||||
# Background removal (post-process)
|
||||
if req.remove_background:
|
||||
try:
|
||||
bg_output = output + ".nobg.mp4"
|
||||
remove_bg(output, bg_output, req.background_replacement, req.background_replacement_value)
|
||||
os.replace(bg_output, output)
|
||||
logger.info("Background removed from %s", output)
|
||||
except Exception as e:
|
||||
logger.warning(f"Background removal failed (non-fatal): {e}")
|
||||
|
||||
# Background music mixing (post-process)
|
||||
if req.background_music:
|
||||
try:
|
||||
music_output = output + ".music.mp4"
|
||||
mix_background_music(
|
||||
output,
|
||||
req.background_music.path,
|
||||
music_output,
|
||||
volume_db=req.background_music.volumeDb,
|
||||
ducking_enabled=req.background_music.duckingEnabled,
|
||||
ducking_db=req.background_music.duckingDb,
|
||||
ducking_attack_ms=req.background_music.duckingAttackMs,
|
||||
ducking_release_ms=req.background_music.duckingReleaseMs,
|
||||
)
|
||||
os.replace(music_output, output)
|
||||
logger.info("Background music mixed into %s", output)
|
||||
except Exception as e:
|
||||
logger.warning(f"Background music mixing failed (non-fatal): {e}")
|
||||
|
||||
# Sidecar SRT: generate and save alongside video
|
||||
srt_path = None
|
||||
if req.captions == "sidecar" and words_dicts:
|
||||
@ -226,6 +302,13 @@ async def export_video(req: ExportRequest):
|
||||
save_captions(srt_content, srt_path)
|
||||
logger.info(f"Sidecar SRT saved to {srt_path}")
|
||||
|
||||
# Cleanup pre-concat temp file
|
||||
if has_additional and working_input != req.input_path and os.path.exists(working_input):
|
||||
try:
|
||||
os.remove(working_input)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
result = {"status": "ok", "output_path": output}
|
||||
if srt_path:
|
||||
result["srt_path"] = srt_path
|
||||
|
||||
Reference in New Issue
Block a user