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