import json import modal import logging import tempfile import os from suno_utils.worker.schema import QueueItem import subprocess import boto3 import requests s3_client = boto3.client( "s3", aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), ) HOOT_BUCKET_NAME = "suno-data-uploads" HOOT_FOLDER_NAME = "studio/uploads" AUDIO_BUCKET_NAME = "suno-data-uploads" AUDIO_FOLDER_NAME = "studio/uploads" logger = logging.getLogger(__name__) logging.basicConfig() logger.setLevel(logging.INFO) ############## CHANGE THESE ############## DEPLOYMENT_TYPE = "dev" ########################################## aws_secret = modal.Secret.from_name("studio-aws") SECRETS = [ aws_secret, modal.Secret.from_name("openai-secret"), modal.Secret.from_name("api-callback-token"), ] PYTHON_VERSION = "3.11" def get_modal_base_image(): return ( modal.Image.from_registry("ubuntu:22.04", add_python="3.11") .apt_install( "curl", "git", "ffmpeg", "fonts-freefont-ttf", "build-essential", # 🧩 Critical Chrome dependencies: "ca-certificates", "fonts-liberation", "libappindicator3-1", "libasound2", "libatk-bridge2.0-0", "libatk1.0-0", "libc6", "libcairo2", "libcups2", "libdbus-1-3", "libexpat1", "libfontconfig1", "libgcc1", "libglib2.0-0", "libgtk-3-0", "libnspr4", "libnss3", "libpango-1.0-0", "libu2f-udev", "libv4l-0", "libx11-6", "libxcomposite1", "libxdamage1", "libxrandr2", "xdg-utils", "wget", ) .run_commands( # Install Node.js "curl -fsSL https://deb.nodesource.com/setup_18.x | bash -", "apt-get install -y nodejs", ) .pip_install( "openai", "modal", "boto3", "requests", # other Python deps ) ) def clone_remotion_repo(): git_token = os.environ["GITHUB_TOKEN"] clone_url = f"https://{git_token}@github.com/suno-ai/video-gen.git" subprocess.run(["git", "clone", "--depth=1", clone_url, "/remotion-project"], check=True) subprocess.run(["npm", "install"], cwd="/remotion-project", check=True) subprocess.run(["ls", "-l"], cwd="/remotion-project", check=True) image = ( get_modal_base_image() .add_local_python_source("suno_utils", copy=True) .run_function(clone_remotion_repo, secrets=[modal.Secret.from_name("victor-modal-github-token")]) ) app = modal.App( "remotion-video-render", image=image, secrets=[ modal.Secret.from_name("openai-secret", required_keys=["OPENAI_API_KEY"]), ], ) @app.cls( cpu=10, secrets=SECRETS, timeout=4000, scaledown_window=1200, retries=modal.Retries( max_retries=2, backoff_coefficient=2.0, initial_delay=5.0, ), memory=250000, min_containers=2, ) @modal.concurrent(max_inputs=60) class VideoRenderStub: def __init__(self): pass @modal.method() def generate_video_from_script( self, queue_item_json: str, ): queue_item = QueueItem.parse_raw(queue_item_json) video_id = queue_item.id with tempfile.TemporaryDirectory() as temp_dir: output_path = os.path.join(temp_dir, f"clip-{video_id}.mp4") props_path = os.path.join(temp_dir, f"scene_{video_id}.json") try: # Write props to a temporary JSON file to avoid escaping issues # download props json from s3 # download video from cdn response = requests.get(f"https://cdn1.suno.ai/scene_{video_id}.json") props_data = json.loads(response.content.decode("utf-8")) # Write the downloaded content to props_path with open(props_path, "w", encoding="utf-8") as f: json.dump(props_data, f, ensure_ascii=False) result = subprocess.run( [ "npx", "remotion", "render", "test", output_path, "--props", props_path, "--bundle-cache", "false", "--concurrency", "6", "--ffg-output-args", "-movflags +faststart", ], cwd="/remotion-project", capture_output=True, text=True, ) # 🧩 Add this for full visibility! print("========== REMOTION STDOUT ==========") print(result.stdout) print("========== REMOTION STDERR ==========") print(result.stderr) if result.returncode != 0: raise RuntimeError(f"Remotion render failed with exit code {result.returncode}") s3_client.upload_file( output_path, "suno-data-uploads", props_data["video_s3_url"], ExtraArgs={ "ContentType": "video/mp4", "ContentDisposition": "inline", }, ) # Remove the props JSON file after successful processing if os.path.exists(props_path): os.remove(props_path) if os.path.exists(output_path): os.remove(output_path) queue_item.notify_progress( { "id": video_id, "video_id": props_data["video_id"], "video_url": props_data["video_output_url"], "status": "success", "message": "Video generation completed", } ) except subprocess.CalledProcessError as e: logger.error(f"Error rendering scene {video_id}:\n{e.stderr}") queue_item.notify_progress( { "id": video_id, "video_id": props_data["video_id"], "video_url": props_data["video_output_url"], "status": "failed", "message": f"Video generation completed: {e.stderr}", } ) raise return output_path @app.local_entrypoint() def main() -> None: app = VideoRenderStub()