sam12345324/renderapi
0
1from fastapi import FastAPI, UploadFile, File, Form, HTTPException, BackgroundTasks2from fastapi.responses import StreamingResponse3import tempfile4import shutil5import os6import logging7import traceback8from moviepy.editor import (9 VideoFileClip,10 concatenate_videoclips,11 AudioFileClip,12 CompositeAudioClip,13 concatenate_audioclips,14 vfx,15)16import io17from PIL import Image18 19# Configure logging20logging.basicConfig(level=logging.DEBUG)21logger = logging.getLogger("main")22 23# Enable MoviePy verbose logging24from moviepy.config import change_settings25change_settings({"VERBOSE": True})26 27# Safe video clip loader28def safe_video_clip(path, target_width=1280, reference_aspect_ratio=None):29 try:30 clip = VideoFileClip(path)31 if clip.reader is None or clip.duration is None or clip.duration <= 0:32 clip.close()33 raise ValueError(f"Invalid or empty video file: {os.path.basename(path)}")34 logger.debug(f"Loaded video: {path}, duration: {clip.duration}s, resolution: {clip.size}, aspect ratio: {clip.w / clip.h}")35 36 # Get video aspect ratio37 current_aspect_ratio = clip.w / clip.h38 39 # Use reference aspect ratio if provided, else use current video's aspect ratio40 target_aspect_ratio = reference_aspect_ratio if reference_aspect_ratio else current_aspect_ratio41 42 # Check if aspect ratios are compatible (within a small tolerance)43 if reference_aspect_ratio and abs(current_aspect_ratio - target_aspect_ratio) > 0.05:44 logger.warning(f"Video {path} has different aspect ratio ({current_aspect_ratio:.2f}) than reference ({target_aspect_ratio:.2f}). Adjusting to match.")45 46 # Calculate target resolution to preserve aspect ratio47 target_height = int(target_width / target_aspect_ratio)48 # Ensure height is even (required by libx264 codec)49 target_height = target_height if target_height % 2 == 0 else target_height + 150 51 try:52 clip = clip.resize((target_width, target_height))53 logger.debug(f"Resized video {path} to {target_width}x{target_height}, aspect ratio: {target_width / target_height}")54 except Exception as e:55 logger.warning(f"Failed to resize video {path}: {e}")56 # Continue without resizing if it fails (may cause concatenation issues)57 58 return clip, target_aspect_ratio59 except Exception as e:60 raise ValueError(f"Failed to load video file '{os.path.basename(path)}': {str(e)}")61 62# Safe audio clip loader63def safe_audio_clip(path):64 try:65 clip = AudioFileClip(path)66 if clip.reader is None or clip.duration is None or clip.duration <= 0:67 clip.close()68 raise ValueError(f"Invalid or empty audio file: {os.path.basename(path)}")69 logger.debug(f"Loaded audio: {path}, duration: {clip.duration}s")70 return clip71 except Exception as e:72 raise ValueError(f"Failed to load audio file '{os.path.basename(path)}': {str(e)}")73 74def merge_videos_and_audios(video_paths, audio_paths, output_path, temp_dir, orig_vol=1.0, music_vol=0.5, audio_mode="concatenate"):75 logger.debug(f"Merging {len(video_paths)} videos and {len(audio_paths)} audios to {output_path}")76 77 # Load video clips78 video_clips = []79 reference_aspect_ratio = None80 for vp in video_paths:81 try:82 clip, aspect_ratio = safe_video_clip(vp, reference_aspect_ratio=reference_aspect_ratio)83 if reference_aspect_ratio is None:84 reference_aspect_ratio = aspect_ratio # Set reference from first valid video85 video_clips.append(clip)86 except Exception as e:87 logger.warning(f"Skipping invalid video file {vp}: {e}")88 89 if not video_clips:90 raise ValueError("No valid video files provided")91 92 # Load audio clips93 audio_clips = []94 for ap in audio_paths:95 try:96 clip = safe_audio_clip(ap)97 clip = clip.volumex(music_vol)98 audio_clips.append(clip)99 except Exception as e:100 logger.warning(f"Skipping invalid audio file {ap}: {e}")101 102 # Concatenate video clips103 final_video = concatenate_videoclips(video_clips, method="compose")104 logger.debug(f"Concatenated video duration: {final_video.duration}s, resolution: {final_video.size}")105 106 # Handle original video audio107 original_audio = None108 if final_video.audio:109 original_audio = final_video.audio.volumex(orig_vol)110 logger.debug(f"Original video audio preserved, duration: {original_audio.duration}s")111 112 # Combine audio clips113 final_audio = None114 if audio_clips:115 if audio_mode == "concatenate":116 final_audio = concatenate_audioclips(audio_clips)117 logger.debug(f"Concatenated audio duration: {final_audio.duration}s")118 else:119 final_audio = CompositeAudioClip(audio_clips)120 logger.debug(f"Composite audio duration: {final_audio.duration}s")121 122 # Match audio duration to video123 if final_audio and final_audio.duration < final_video.duration:124 logger.debug(f"Looping audio to match video duration: {final_video.duration}s")125 final_audio = final_audio.fx(vfx.loop, duration=final_video.duration)126 elif final_audio and final_audio.duration > final_video.duration:127 logger.debug(f"Trimming audio to match video duration: {final_video.duration}s")128 final_audio = final_audio.subclip(0, final_video.duration)129 130 if original_audio:131 final_audio = CompositeAudioClip([final_audio, original_audio])132 logger.debug(f"Combined audio (original + uploaded) duration: {final_audio.duration}s")133 134 # Set the audio for the final video135 if final_audio:136 final_video = final_video.set_audio(final_audio)137 elif original_audio:138 final_video = final_video.set_audio(original_audio)139 else:140 logger.warning("No audio (uploaded or original) available for the final video")141 142 # Write output143 logger.debug(f"Writing output to {output_path}")144 try:145 final_video.write_videofile(146 output_path,147 codec="libx264",148 audio_codec="aac",149 temp_audiofile=os.path.join(temp_dir, "temp-audio.m4a"),150 remove_temp=True,151 verbose=True,152 logger='bar'153 )154 except Exception as e:155 logger.error(f"MoviePy failed to write output: {str(e)}")156 raise157 158 # Close clips159 final_video.close()160 for vc in video_clips:161 vc.close()162 for ac in audio_clips:163 ac.close()164 if original_audio:165 original_audio.close()166 167app = FastAPI()168 169@app.post("/merge")170async def merge_endpoint(171 files: list[UploadFile] = File(...),172 orig_vol: float = Form(1.0),173 music_vol: float = Form(0.5),174 audio_mode: str = Form("concatenate"),175 background_tasks: BackgroundTasks = BackgroundTasks(),176):177 temp_dir = tempfile.mkdtemp()178 logger.debug(f"Created temporary directory: {temp_dir}")179 180 try:181 saved_files = []182 for uploaded_file in files:183 if not uploaded_file.filename:184 logger.warning("Skipping file with no filename")185 continue186 file_path = os.path.join(temp_dir, uploaded_file.filename)187 with open(file_path, "wb") as out_file:188 content = await uploaded_file.read()189 if not content:190 logger.warning(f"Empty file uploaded: {uploaded_file.filename}")191 continue192 out_file.write(content)193 saved_files.append(file_path)194 logger.debug(f"Saved file: {file_path}")195 196 video_files = [f for f in saved_files if f.lower().endswith(".mp4")]197 audio_files = [f for f in saved_files if f.lower().endswith((".mp3", ".wav", ".aac", ".m4a", ".ogg"))]198 logger.debug(f"Video files: {video_files}")199 logger.debug(f"Audio files: {audio_files}")200 201 if len(video_files) < 1:202 raise HTTPException(status_code=400, detail="Please upload at least one valid video file")203 204 output_path = os.path.join(temp_dir, "merged_output.mp4")205 merge_videos_and_audios(video_files, audio_files, output_path, temp_dir, orig_vol, music_vol, audio_mode)206 207 if not os.path.exists(output_path):208 logger.error(f"Output file not found: {output_path}")209 raise HTTPException(status_code=500, detail="Failed to create merged video file")210 211 logger.debug(f"Reading output file: {output_path}")212 with open(output_path, "rb") as file:213 content = file.read()214 if not content:215 logger.error(f"Output file is empty: {output_path}")216 raise HTTPException(status_code=500, detail="Output file is empty")217 218 background_tasks.add_task(shutil.rmtree, temp_dir, ignore_errors=True)219 logger.debug("Sending streaming response")220 return StreamingResponse(221 io.BytesIO(content),222 media_type="video/mp4",223 headers={"Content-Disposition": 'attachment; filename="merged_output.mp4"'}224 )225 226 except Exception as e:227 error_msg = f"Error: {str(e)}\n\n{traceback.format_exc()}"228 logger.error(error_msg)229 raise HTTPException(status_code=500, detail=error_msg)230 231 finally:232 logger.debug(f"Cleaning up temporary directory: {temp_dir}")233 shutil.rmtree(temp_dir, ignore_errors=True)234 235# Log public URL236def log_api_url():237 url = os.getenv("SPACE_PUBLIC_URL")238 if url:239 logger.info(f"API is available at: {url}/merge")240 else:241 logger.info("SPACE_PUBLIC_URL environment variable not found")242 243log_api_url()