| import os |
| import sys |
| import json |
| import base64 |
| import requests |
| import random |
| import time |
| import io |
|
|
| |
| start_time = time.time() |
|
|
| |
| |
| import httpx |
| original_client_init = httpx.Client.__init__ |
| def patched_client_init(self, *args, **kwargs): |
| kwargs['timeout'] = httpx.Timeout(250.0) |
| original_client_init(self, *args, **kwargs) |
| httpx.Client.__init__ = patched_client_init |
|
|
| original_async_init = httpx.AsyncClient.__init__ |
| def patched_async_init(self, *args, **kwargs): |
| kwargs['timeout'] = httpx.Timeout(250.0) |
| original_async_init(self, *args, **kwargs) |
| httpx.AsyncClient.__init__ = patched_async_init |
| |
|
|
| from gradio_client import Client, handle_file |
| from PIL import Image |
|
|
| raw_prompt = os.environ.get('PROMPT', '') |
| run_id = os.environ.get('RUN_ID', '') |
| space_url = os.environ.get('SPACE_URL', '') |
| github_run_id = os.environ.get('GITHUB_RUN_ID', '') |
|
|
| |
| if not raw_prompt.startswith("VOICECONFIG_LTX_"): |
| err_str = "Error: Invalid LTX configuration payload." |
| print(err_str) |
| sys.exit(1) |
|
|
| try: |
| encoded_data = raw_prompt[len("VOICECONFIG_LTX_"):] |
| config = json.loads(base64.b64decode(encoded_data).decode('utf-8')) |
| |
| original_run_id = config.get("original_run_id", run_id) |
| except Exception as e: |
| err_str = f"Decode Error: {e}" |
| print(err_str) |
| sys.exit(1) |
|
|
| def report_failure(error_msg): |
| for webhook_attempt in range(3): |
| try: |
| res = requests.post( |
| f"{space_url}/api/webhook/fail", |
| json={ |
| "run_id": run_id, |
| "error": error_msg, |
| "event_type": "ltxvideo", |
| "client_payload": { |
| "prompt": raw_prompt, |
| "run_id": run_id, |
| "space_url": space_url |
| }, |
| "github_run_id": github_run_id |
| }, |
| timeout=20 |
| ) |
| print(f" -> Webhook response status: {res.status_code} - Server Response: {res.text}", flush=True) |
| if res.status_code == 200: |
| |
| if "Max retries reached" in res.text: |
| try: |
| cycle_count = int(config.get("cycle_count", 1)) |
| |
| |
| if cycle_count < 3: |
| next_cycle = cycle_count + 1 |
| print(f"🔄 Cycle {cycle_count} failed after 20 retries. Automatically starting Cycle {next_cycle} out of 3...", flush=True) |
| |
| |
| config["cycle_count"] = next_cycle |
| config["original_run_id"] = original_run_id |
| |
| new_encoded = base64.b64encode(json.dumps(config).encode('utf-8')).decode('utf-8') |
| new_raw_prompt = f"VOICECONFIG_LTX_{new_encoded}" |
| |
| |
| with open("processing.txt", "w", encoding="utf-8") as pf: |
| pf.write("processing") |
| requests.post( |
| f"{space_url}/api/webhook/upload", |
| data={'run_id': f"{original_run_id}_processing", 'github_run_id': github_run_id, 'ext': 'txt'}, |
| files={'file': open("processing.txt", "rb")}, |
| timeout=30 |
| ) |
| |
| spawn_res = requests.post( |
| f"{space_url}/api/generate", |
| json={ |
| "prompt": new_raw_prompt, |
| "action_name": "ltxvideo" |
| }, |
| timeout=30 |
| ) |
| print(f" -> Spawned new cycle status: {spawn_res.status_code} - Server Resp: {spawn_res.text}", flush=True) |
| else: |
| |
| print(f"❌ Max cycles reached ({cycle_count}/3). Mapping final error card back to original ID: {original_run_id}", flush=True) |
| |
| with open("processing.txt", "w", encoding="utf-8") as pf: |
| pf.write("failed") |
| requests.post( |
| f"{space_url}/api/webhook/upload", |
| data={'run_id': f"{original_run_id}_processing", 'github_run_id': github_run_id, 'ext': 'txt'}, |
| files={'file': open("processing.txt", "rb")}, |
| timeout=30 |
| ) |
| |
| time.sleep(5) |
| error_card_url = f"{space_url}/static/images/{run_id}.png" |
| err_req = requests.get(error_card_url, timeout=20) |
| if err_req.status_code == 200: |
| requests.post( |
| f"{space_url}/api/webhook/upload", |
| data={'run_id': original_run_id, 'github_run_id': github_run_id, 'ext': 'png'}, |
| files={'file': ('error.png', err_req.content)}, |
| timeout=30 |
| ) |
| print(f" -> Successfully mapped error card to {original_run_id}.png", flush=True) |
| except Exception as spawn_err: |
| print(f"⚠️ Failed to spawn next cycle automatically: {spawn_err}", flush=True) |
| break |
| except Exception as e: |
| print(f" -> Webhook failed to send, retrying... {e}", flush=True) |
| time.sleep(3) |
|
|
| print(f"🚀 Starting LTX Video runner. Run ID: {run_id} (Original User ID: {original_run_id})") |
|
|
| try: |
| client = Client("Fighterdan/LTX-2.3-10Eros_I2V") |
| except Exception as e: |
| err_str = f"Failed to connect to Gradio Space: {e}" |
| print(err_str) |
| report_failure(err_str) |
| sys.exit(1) |
|
|
| |
| image_url = config.get("image_path") |
| local_img = "gen_input.png" |
| download_success = False |
| max_download_attempts = 5 |
|
|
| print("1. Downloading input image...") |
| for dl_attempt in range(max_download_attempts): |
| try: |
| r = requests.get(image_url, timeout=45) |
| if r.status_code == 200: |
| img_data = io.BytesIO(r.content) |
| img = Image.open(img_data) |
| img.verify() |
| |
| img_data.seek(0) |
| clean_img = Image.open(img_data).convert('RGB') |
| clean_img.save(local_img, format='PNG') |
| download_success = True |
| print(" -> Image successfully downloaded and verified.") |
| break |
| else: |
| print(f" -> Download attempt {dl_attempt + 1} failed with status: {r.status_code}") |
| except Exception as e: |
| print(f" -> Download attempt {dl_attempt + 1} connection failed: {e}") |
| |
| if dl_attempt < max_download_attempts - 1: |
| print(" -> Waiting 8 seconds before retrying image download...") |
| time.sleep(8.0) |
|
|
| if not download_success: |
| err_str = "ZeroGPU quota hit during image download. Aborting and switching runner." |
| print(f"CRITICAL ERROR: {err_str}", flush=True) |
| report_failure(err_str) |
| time.sleep(3) |
| sys.exit(1) |
|
|
| base_seed = int(config.get("seed", -1)) |
| seconds = float(config.get("seconds", 5.0)) |
| prompt = config.get("prompt", "") |
|
|
| print(f"🎬 Generation configurations - Duration: {seconds}s") |
|
|
| video_path = None |
| max_attempts = 4 |
|
|
| |
| for attempt in range(max_attempts): |
| elapsed_time = time.time() - start_time |
| if elapsed_time > 450.0: |
| print(f"⏳ WARNING: Elapsed time is {elapsed_time:.1f}s. Aborting this run to trigger a clean retry on a new runner.") |
| report_failure("TIMEOUT_VOLUNTARY_ABORT") |
| sys.exit(1) |
|
|
| seed = base_seed |
| if seed == -1: |
| seed = random.randint(1, 2147483647) |
|
|
| print(f" -> Generation Attempt {attempt + 1} of {max_attempts} with Seed: {seed}...") |
| |
| try: |
| result = client.predict( |
| image_path=handle_file(local_img), |
| prompt=prompt, |
| negative_prompt="captions, music, transition, bad quality, subtitles, text, watermark, cartoon, ugly, blur, static, noise, mutant, horror", |
| seconds=seconds, |
| preset="tuned", |
| seed=seed, |
| randomize_seed=False, |
| max_width=1120, |
| max_height=1344, |
| target_mp=1.15, |
| snap_multiple=64, |
| custom_res_enabled=False, |
| mode="anchor only", |
| face_bbox="", |
| likeness_strength=0.9, |
| likeness_anchor_strength=0.15, |
| latent_anchor_strength=0.08, |
| first_frame_strength=0.82, |
| sulphur_lora_strength=0.15, |
| sulphur_v1_lora_strength=0.15, |
| vbvr_lora_strength=0.5, |
| dreamly_lora_strength=0.6, |
| synth_lora_strength=0.0, |
| plora_lora_strength=0.0, |
| singularity_lora_strength=0.3, |
| omninft_lora_strength=0.8, |
| omninft_bf16_lora_strength=0.0, |
| better_motion_lora_strength=0.0, |
| physics_v2_lora_strength=0.0, |
| hardcut_lora_strength=0.0, |
| transition_lora_strength=0.15, |
| sulphur_audio_strength=0.15, |
| sulphur_v1_audio_strength=0.15, |
| vbvr_audio_strength=0.5, |
| dreamly_audio_strength=0.6, |
| synth_audio_strength=0.0, |
| plora_audio_strength=0.0, |
| singularity_audio_strength=0.3, |
| omninft_audio_strength=0.8, |
| omninft_bf16_audio_strength=0.0, |
| better_motion_audio_strength=0.0, |
| physics_v2_audio_strength=0.0, |
| hardcut_audio_strength=0.0, |
| transition_audio_strength=0.0, |
| cache_at_step=0, |
| cache_warmup=400, |
| energy_threshold=0.3, |
| anchor_similarity_threshold=0.3, |
| sigma_string="0.4824, 0.2412, 0.0", |
| skip_refine=False, |
| gen_budget=0, |
| profile_name="", |
| input_mode="single image (i2v)", |
| msr_ref2=None, |
| msr_ref3=None, |
| msr_ref4=None, |
| msr_background=None, |
| msr_frame_count=41, |
| msr_guide_strength=1.0, |
| msr_lora_strength=0.7, |
| prompt_relay_enabled=False, |
| prompt_segments="", |
| scene_chain_enabled=False, |
| scene_chain_prompt="", |
| scene_chain_max_scenes=2, |
| scene_chain_frame_overlap=8, |
| scene_chain_mid_guide=True, |
| scene_chain_mid_guide_strength=0.25, |
| kv_enabled=False, |
| kv_strength=1.0, |
| audio_ref_enabled=False, |
| audio_ref_file=None, |
| audio_ref_guidance_scale=3.0, |
| audio_ref_stem_sep=False, |
| audio_ref_normalize=True, |
| kf_strength=0.82, |
| kf_last_image=None, |
| kf_mid_enabled=False, |
| kf_mid_1_image=None, |
| kf_mid_1_pos=50, |
| kf_mid_2_image=None, |
| kf_mid_2_pos=50, |
| kf_mid_3_image=None, |
| kf_mid_3_pos=50, |
| kf_mid_4_image=None, |
| kf_mid_4_pos=50, |
| kf_mid_5_image=None, |
| kf_mid_5_pos=50, |
| api_name="/generate" |
| ) |
|
|
| if isinstance(result, (tuple, list)): |
| video_data = result[0] |
| else: |
| video_data = result |
|
|
| if isinstance(video_data, dict): |
| video_path = video_data.get('video') or video_data.get('path') |
| else: |
| video_path = str(video_data) |
|
|
| if video_path and os.path.exists(video_path): |
| print(" -> Success! Video file created successfully.") |
| break |
|
|
| except Exception as e: |
| err_msg = str(e) |
| print(f" -> Failed on attempt {attempt + 1}: {err_msg}") |
| |
| if 'ZeroGPU quota' in err_msg or 'quota' in err_msg.lower(): |
| print(" -> ZeroGPU quota hit. Aborting internal attempts to trigger webhook fail and switch runner.") |
| report_failure(err_msg) |
| sys.exit(1) |
| |
| if attempt < max_attempts - 1: |
| print(" -> Waiting 5 seconds before next internal attempt...") |
| time.sleep(5.0) |
|
|
| if not video_path or not os.path.exists(video_path): |
| err_str = "Failed to generate video after all internal attempts." |
| print(f"CRITICAL ERROR: {err_str}") |
| report_failure(err_str) |
| sys.exit(1) |
|
|
| |
| print(f"Uploading video back to space using ID: {original_run_id}...") |
| ext = 'mp4' |
| if video_path.endswith('.webm'): |
| ext = 'webm' |
|
|
| try: |
| |
| with open("processing.txt", "w", encoding="utf-8") as pf: |
| pf.write("done") |
| requests.post( |
| f"{space_url}/api/webhook/upload", |
| data={'run_id': f"{original_run_id}_processing", 'github_run_id': github_run_id, 'ext': 'txt'}, |
| files={'file': open("processing.txt", "rb")}, |
| timeout=30 |
| ) |
|
|
| with open(video_path, 'rb') as f: |
| requests.post( |
| f"{space_url}/api/webhook/upload", |
| data={'run_id': original_run_id, 'github_run_id': github_run_id, 'ext': ext}, |
| files={'file': f}, |
| timeout=60 |
| ) |
|
|
| print("Uploading Seed Metadata...") |
| meta_data = {"seed": seed} |
| with open("meta.json", "w", encoding="utf-8") as mf: |
| json.dump(meta_data, mf) |
| |
| with open("meta.json", "rb") as mf: |
| requests.post( |
| f"{space_url}/api/webhook/upload", |
| data={'run_id': f"{original_run_id}_meta", 'github_run_id': github_run_id, 'ext': 'json'}, |
| files={'file': mf}, |
| timeout=30 |
| ) |
|
|
| print("✅ SUCCESS!") |
|
|
| except Exception as up_err: |
| err_str = f"Upload to main server failed: {up_err}" |
| print(err_str) |
| report_failure(err_str) |
| sys.exit(1) |