import os import zipfile import logging import shutil import math from pathlib import Path from typing import List, Dict, Any import config logger = logging.getLogger(__name__) def format_timestamp(seconds: float) -> str: """Convert seconds to SRT timestamp format (HH:MM:SS,mmm)""" hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) millis = int((seconds - int(seconds)) * 1000) return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}" def generate_srt(script_data: Dict[str, Any], video_map: Dict[int, str]) -> str: """Generate SRT content from script data""" scenes = script_data.get("scenes", []) srt_content = "" current_time = 0.0 # Need to get durations from actual videos if possible, else estimate from modules import ffmpeg_utils for i, scene in enumerate(scenes): scene_id = scene["id"] # Get duration duration = 5.0 if scene_id in video_map and os.path.exists(video_map[scene_id]): try: info = ffmpeg_utils.get_video_info(video_map[scene_id]) duration = info.get("duration", 5.0) except: pass start_time = current_time end_time = current_time + duration current_time = end_time text = scene.get("subtitle", "") if text: srt_content += f"{i+1}\n" srt_content += f"{format_timestamp(start_time)} --> {format_timestamp(end_time)}\n" srt_content += f"{text}\n\n" return srt_content def create_capcut_package(project_id: str, script_data: Dict[str, Any], assets: Dict[str, str]) -> str: """ Create a ZIP package for CapCut (JianYing) import Contains: - videos/ (scene videos) - audios/ (voiceover, bgm) - images/ (fancy text transparent pngs) - subtitles.srt """ package_dir = config.TEMP_DIR / f"capcut_pkg_{project_id}_{int(os.getpid())}" if package_dir.exists(): shutil.rmtree(package_dir) package_dir.mkdir() (package_dir / "videos").mkdir() (package_dir / "audios").mkdir() (package_dir / "images").mkdir() # 1. Generate SRT # Need to reconstruct video map from assets or script # Assuming 'assets' contains 'scene_videos' map scene_videos = assets.get("scene_videos", {}) srt_content = generate_srt(script_data, scene_videos) with open(package_dir / "subtitles.srt", "w", encoding="utf-8") as f: f.write(srt_content) # 2. Copy Videos scenes = script_data.get("scenes", []) for i, scene in enumerate(scenes): sid = scene["id"] if sid in scene_videos and os.path.exists(scene_videos[sid]): # Rename with sequence number for easy sorting: 01_scene.mp4 ext = Path(scene_videos[sid]).suffix dest_name = f"{i+1:02d}_scene_{sid}{ext}" shutil.copy(scene_videos[sid], package_dir / "videos" / dest_name) # 3. Copy Audio (Voiceover) # We might not have the separate voiceover file easily accessible if it was mixed on the fly. # But usually we generate it to temp. # Option: Re-generate voiceover audio for the whole track or segments? # Better: If we have 'voiceover_segments', generate them or copy if cached. # For now, let's try to find if we have a full voiceover file or just use segments. # Simplest: Re-generate the full voiceover audio file if it doesn't exist as a standalone asset. # Or check if user just wants the pieces. # Let's check if we have a mixed audio file. Usually we don't save the intermediate audio as an asset. # So we might need to re-generate the voiceover audio here. from modules import factory full_vo_text = " ".join([s.get("voiceover", "") for s in scenes if s.get("voiceover")]) if full_vo_text: try: # Assuming default voice voice_type = config.VOLC_TTS_DEFAULT_VOICE vo_path = factory.generate_voiceover_volcengine(full_vo_text, voice_type) shutil.copy(vo_path, package_dir / "audios" / "full_voiceover.mp3") except Exception as e: logger.warning(f"Failed to generate export voiceover: {e}") # Copy BGM # Check settings or script for BGM? BGM is usually a global setting in Composer. # We'll just look for BGM in assets folder or let user drag their own. # Or if we saved the BGM selection in the project, we could copy it. # For now, skip specific BGM unless we know which one was used. # 4. Copy Fancy Text Images # We need to re-render them or find them. # Since they are generated to temp in composer, they might be gone. # Safer to re-render them. from modules.text_renderer import renderer for i, scene in enumerate(scenes): ft = scene.get("fancy_text") if ft: text = ft.get("text", "") if isinstance(ft, dict) else "" style = ft.get("style", "highlight") if isinstance(ft, dict) else "highlight" if text: try: # Render if isinstance(style, str): # Simple mapping or default # We need the full style dict logic from composer ideally # For export, we just use default render pass # Actually, composer logic for style resolution is complex. # Let's just use a simple render here. img_path = renderer.render(text, {"font_size": 60, "font_color": "#FFFFFF"}, cache=False) shutil.copy(img_path, package_dir / "images" / f"{i+1:02d}_text_{scene['id']}.png") except: pass # 5. Zip it zip_path = config.TEMP_DIR / f"capcut_export_{project_id}.zip" with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, dirs, files in os.walk(package_dir): for file in files: file_path = os.path.join(root, file) arcname = os.path.relpath(file_path, package_dir) zipf.write(file_path, arcname) # Cleanup shutil.rmtree(package_dir) return str(zip_path)