168 lines
7.5 KiB
Python
168 lines
7.5 KiB
Python
import os
|
|
import json
|
|
import tempfile
|
|
import subprocess
|
|
import asyncio
|
|
from pathlib import Path
|
|
from appPublic.log import debug, error
|
|
|
|
async def run_synthesize(task_instance, payload):
|
|
"""
|
|
Run KTV/MTV video synthesis using two-step ffmpeg process.
|
|
|
|
Step 1: Create silent looped video track from scene clips
|
|
Step 2a: MTV - single track with original audio + ASS subtitles
|
|
Step 2b: KTV - dual track with accompaniment (default) + original audio
|
|
"""
|
|
try:
|
|
# Extract parameters from payload
|
|
video_files = payload.get('video_files', [])
|
|
original_audio = payload.get('original_audio')
|
|
accompaniment = payload.get('accompaniment')
|
|
subtitle_path = payload.get('subtitle_path')
|
|
output_dir = payload.get('output_dir', '/tmp/ktv-synth-outputs')
|
|
title = payload.get('title', 'output')
|
|
duration = payload.get('duration')
|
|
loops = payload.get('loops')
|
|
output_modes = payload.get('output_modes', ['mtv', 'ktv'])
|
|
|
|
# Validate required parameters
|
|
if not video_files:
|
|
raise ValueError('video_files is required')
|
|
if not original_audio:
|
|
raise ValueError('original_audio is required')
|
|
if not subtitle_path:
|
|
raise ValueError('subtitle_path is required')
|
|
|
|
# Create output directory
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# Create temporary directory for intermediate files
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
temp_video = os.path.join(temp_dir, 'temp_video.mp4')
|
|
concat_list = os.path.join(temp_dir, 'concat_list.txt')
|
|
|
|
# Create concat list for ffmpeg
|
|
with open(concat_list, 'w') as f:
|
|
for video_file in video_files:
|
|
f.write(f"file '{video_file}'\n")
|
|
|
|
# Calculate loops if not provided
|
|
if loops is None:
|
|
# Get duration of first video file to estimate total duration
|
|
probe_cmd = [
|
|
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
|
|
'-of', 'default=noprint_wrappers=1:nokey=1', video_files[0]
|
|
]
|
|
result = subprocess.run(probe_cmd, capture_output=True, text=True)
|
|
clip_duration = float(result.stdout.strip())
|
|
|
|
# If duration not provided, use sum of all clips
|
|
if duration is None:
|
|
total_duration = 0
|
|
for vf in video_files:
|
|
probe_cmd = [
|
|
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
|
|
'-of', 'default=noprint_wrappers=1:nokey=1', vf
|
|
]
|
|
result = subprocess.run(probe_cmd, capture_output=True, text=True)
|
|
total_duration += float(result.stdout.strip())
|
|
duration = total_duration
|
|
|
|
loops = int((duration / (clip_duration * len(video_files))) + 1)
|
|
|
|
debug(f'Starting synthesis: title={title}, duration={duration}, loops={loops}, modes={output_modes}')
|
|
|
|
# Step 1: Create silent looped video track
|
|
debug('Step 1: Creating silent looped video track')
|
|
cmd_step1 = [
|
|
'ffmpeg', '-y', '-f', 'concat', '-safe', '0',
|
|
'-stream_loop', str(loops),
|
|
'-i', concat_list,
|
|
'-t', str(duration),
|
|
'-an', # No audio
|
|
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
|
|
temp_video
|
|
]
|
|
|
|
debug(f'Running: {" ".join(cmd_step1)}')
|
|
result = subprocess.run(cmd_step1, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
error(f'FFmpeg step 1 failed: {result.stderr}')
|
|
raise RuntimeError(f'FFmpeg step 1 failed: {result.stderr}')
|
|
|
|
result_dict = {'duration': duration}
|
|
|
|
# Step 2a: MTV synthesis (if requested)
|
|
if 'mtv' in output_modes:
|
|
debug('Step 2a: Creating MTV (single track)')
|
|
mtv_output = os.path.join(output_dir, f'{title}_MTV.mp4')
|
|
|
|
cmd_mtv = [
|
|
'ffmpeg', '-y',
|
|
'-i', temp_video,
|
|
'-i', original_audio,
|
|
'-map', '0:v',
|
|
'-map', '1:a',
|
|
'-vf', f'ass={subtitle_path},scale=1920:1080:flags=lanczos',
|
|
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
|
|
'-c:a', 'aac', '-b:a', '192k',
|
|
mtv_output
|
|
]
|
|
|
|
debug(f'Running: {" ".join(cmd_mtv)}')
|
|
result = subprocess.run(cmd_mtv, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
error(f'FFmpeg MTV synthesis failed: {result.stderr}')
|
|
raise RuntimeError(f'FFmpeg MTV synthesis failed: {result.stderr}')
|
|
|
|
mtv_size = os.path.getsize(mtv_output) / (1024 * 1024) # Size in MB
|
|
result_dict['mtv_path'] = mtv_output
|
|
result_dict['mtv_size_mb'] = round(mtv_size, 2)
|
|
debug(f'MTV created: {mtv_output} ({mtv_size:.2f} MB)')
|
|
|
|
# Step 2b: KTV synthesis (if requested)
|
|
if 'ktv' in output_modes:
|
|
if not accompaniment:
|
|
raise ValueError('accompaniment is required for KTV output')
|
|
|
|
debug('Step 2b: Creating KTV (dual track)')
|
|
ktv_output = os.path.join(output_dir, f'{title}_KTV.mp4')
|
|
|
|
cmd_ktv = [
|
|
'ffmpeg', '-y',
|
|
'-i', temp_video,
|
|
'-i', accompaniment,
|
|
'-i', original_audio,
|
|
'-map', '0:v',
|
|
'-map', '1:a', # Accompaniment
|
|
'-map', '2:a', # Original
|
|
'-vf', f'ass={subtitle_path},scale=1920:1080:flags=lanczos',
|
|
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
|
|
'-c:a:0', 'aac', '-b:a:0', '192k',
|
|
'-metadata:s:a:0', 'handler_name=伴奏(Accompaniment)',
|
|
'-c:a:1', 'aac', '-b:a:1', '192k',
|
|
'-metadata:s:a:1', 'handler_name=原唱(Original)',
|
|
'-disposition:a:0', 'default',
|
|
'-disposition:a:1', '0',
|
|
ktv_output
|
|
]
|
|
|
|
debug(f'Running: {" ".join(cmd_ktv)}')
|
|
result = subprocess.run(cmd_ktv, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
error(f'FFmpeg KTV synthesis failed: {result.stderr}')
|
|
raise RuntimeError(f'FFmpeg KTV synthesis failed: {result.stderr}')
|
|
|
|
ktv_size = os.path.getsize(ktv_output) / (1024 * 1024) # Size in MB
|
|
result_dict['ktv_path'] = ktv_output
|
|
result_dict['ktv_size_mb'] = round(ktv_size, 2)
|
|
debug(f'KTV created: {ktv_output} ({ktv_size:.2f} MB)')
|
|
|
|
debug(f'Synthesis completed successfully: {json.dumps(result_dict)}')
|
|
return result_dict
|
|
|
|
except Exception as e:
|
|
error(f'Synthesis failed: {str(e)}')
|
|
raise
|