Team Ai
Apppublic

sam12345324/renderapi

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
main.py243 linesDownload Raw Back to root
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()