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