| import os |
| import sys |
| import requests |
| import base64 |
| import time |
| from gradio_client import Client, handle_file |
|
|
| 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', '') |
|
|
| def report_failure(error_msg): |
| try: |
| requests.post( |
| f"{space_url}/api/webhook/fail", |
| json={ |
| "run_id": run_id, |
| "error": error_msg, |
| "event_type": "avatar", |
| "client_payload": { |
| "prompt": raw_prompt, |
| "run_id": run_id, |
| "space_url": space_url |
| }, |
| "github_run_id": github_run_id |
| }, |
| timeout=15 |
| ) |
| except Exception as e: |
| print(f"Failed to report failure: {e}") |
|
|
| print('1. Decoding packed configurations from safe payload...') |
| if not raw_prompt.startswith("AVATARCONFIG_"): |
| err_str = "Error: Invalid avatar configuration payload signature." |
| print(err_str) |
| report_failure(err_str) |
| sys.exit(1) |
|
|
| config_str = raw_prompt[len("AVATARCONFIG_"):] |
| parts = config_str.split("_") |
| config = {} |
| i = 0 |
| while i < len(parts) - 1: |
| key = parts[i] |
| val = parts[i+1] |
| config[key] = val |
| i += 2 |
|
|
| user_run_id = config.get("userRunId", run_id) |
| img_ext = config.get("imgExt", "png") |
| aud_ext = config.get("audExt", "mp3") |
| res = config.get("res", "480p") |
|
|
| try: |
| seed = int(config.get("seed", "42")) |
| except ValueError: |
| seed = 42 |
|
|
| vocal_slug = config.get("vocal", "clean") |
| vocal_mode = "Clean speech (fast)" if vocal_slug == "clean" else "Isolate vocals (quality)" |
|
|
| accel_slug = config.get("accel", "dbfaster") |
| if accel_slug == "dbfast": |
| acceleration = "DBCache fast" |
| elif accel_slug == "exact8": |
| acceleration = "Exact 8-step" |
| else: |
| acceleration = "DBCache faster" |
|
|
| try: |
| b64_prompt = config.get("prompt", "") |
| prompt = base64.b64decode(b64_prompt).decode('utf-8') |
| except Exception as e: |
| prompt = "A person is speaking expressively, looking at the camera." |
|
|
| print(f' -> User Run ID: {user_run_id}') |
| print(f' -> Target prompt: "{prompt}"') |
| print(f' -> Params: Seed={seed}, Res={res}, Vocal={vocal_mode}, Accel={acceleration}') |
|
|
| |
| img_url = f"{space_url}/static/images/{user_run_id}_avatar_img.{img_ext}" |
| aud_url = f"{space_url}/static/images/{user_run_id}_avatar_aud.{aud_ext}" |
| state_url = f"{space_url}/static/images/{user_run_id}_state.pt" |
|
|
| local_img = f"input_img.{img_ext}" |
| local_aud = f"input_aud.{aud_ext}" |
| local_state = "input_state.pt" |
|
|
| print('2. Downloading assets from your Space...') |
| |
| for att in range(3): |
| try: |
| req_img = requests.get(img_url, timeout=45) |
| if req_img.status_code == 200: |
| with open(local_img, 'wb') as f: |
| f.write(req_img.content) |
| break |
| except: |
| time.sleep(2) |
| else: |
| report_failure("Image download failed.") |
| sys.exit(1) |
|
|
| |
| for att in range(3): |
| try: |
| req_aud = requests.get(aud_url, timeout=45) |
| if req_aud.status_code == 200: |
| with open(local_aud, 'wb') as f: |
| f.write(req_aud.content) |
| break |
| except: |
| time.sleep(2) |
| else: |
| report_failure("Audio download failed.") |
| sys.exit(1) |
|
|
| |
| has_state = False |
| try: |
| req_state = requests.get(state_url, timeout=45) |
| if req_state.status_code == 200: |
| with open(local_state, 'wb') as f: |
| f.write(req_state.content) |
| has_state = True |
| print(' -> State file (.pt) found and loaded to resume rendering.') |
| except Exception as e: |
| print(" -> No state file found. Starting fresh generation...") |
|
|
| print('3. Connecting to LongCat-Video-Avatar-1.5 Space on ZeroGPU...') |
| try: |
| client = Client("https://opera8-longcat-video-avatar-1-5-2nd2.hf.space") |
| |
| |
| max_submit_retries = 3 |
| job = None |
| for att in range(max_submit_retries): |
| try: |
| print(f" -> Submitting job to ZeroGPU (Attempt {att+1})...") |
| job = client.submit( |
| handle_file(local_img), |
| handle_file(local_aud), |
| prompt, |
| res, |
| seed, |
| vocal_mode, |
| acceleration, |
| handle_file(local_state) if has_state else None, |
| api_name="/generate" |
| ) |
| break |
| except Exception as e: |
| print(f" -> Submission error: {e}") |
| time.sleep(5) |
| |
| if not job: |
| raise Exception("Failed to connect to ZeroGPU Space after multiple attempts.") |
| |
| while not job.done(): |
| time.sleep(5) |
| |
| result = job.result() |
| video_path = result[0] |
| state_path = result[1] |
| status_msg = result[2] |
| |
| |
| if video_path is not None: |
| print('4. Final video generated successfully! Uploading back to your Space...') |
| actual_video_path = video_path.get('video') or video_path.get('path') if isinstance(video_path, dict) else video_path |
| |
| with open(actual_video_path, 'rb') as f: |
| res_upload = requests.post( |
| f'{space_url}/api/webhook/upload', |
| data={'run_id': user_run_id, 'github_run_id': github_run_id, 'ext': 'mp4'}, |
| files={'file': f}, |
| timeout=120 |
| ) |
|
|
| if res_upload.status_code == 200: |
| print('5. SUCCESS! Final video process complete.') |
| sys.exit(0) |
| else: |
| raise Exception(f"Webhook upload failed. Status code: {res_upload.status_code}") |
| |
| |
| elif state_path is not None: |
| print(f"4. Timeout reached securely. State file (.pt) received. Preparing next phase...") |
| actual_state_path = state_path.get('path') if isinstance(state_path, dict) else state_path |
| |
| |
| with open(actual_state_path, 'rb') as f: |
| res_state_upload = requests.post( |
| f'{space_url}/api/webhook/upload', |
| data={'run_id': f"{user_run_id}_state", 'github_run_id': github_run_id, 'ext': 'pt'}, |
| files={'file': f}, |
| timeout=120 |
| ) |
| |
| if res_state_upload.status_code == 200: |
| print(' -> State file uploaded. Triggering a NEW GitHub Action runner to continue...') |
| |
| |
| dispatch_payload = { |
| "prompt": raw_prompt, |
| "width": 1024, |
| "height": 1024, |
| "action_name": "avatar" |
| } |
| |
| trigger_res = requests.post( |
| f'{space_url}/api/generate', |
| json=dispatch_payload, |
| timeout=20 |
| ) |
| |
| if trigger_res.status_code in [200, 204]: |
| print('5. SUCCESS! Next phase dispatched to a new Action runner.') |
| sys.exit(0) |
| else: |
| raise Exception(f"Failed to trigger next phase. Server returned {trigger_res.status_code}") |
| else: |
| raise Exception(f"Failed to upload state file. Server returned {res_state_upload.status_code}") |
| else: |
| raise Exception(f"Unexpected result from model: {status_msg}") |
|
|
| except Exception as e: |
| err_str = str(e) |
| print(f"CRITICAL ERROR during generation: {err_str}") |
| report_failure(err_str) |
| sys.exit(1) |