InvictaTill commited on
Commit
e7843fe
·
1 Parent(s): 3b4f014

feat: implement NVIDIA Cosmos-Transfer2.5-2b video-to-video style transfer support with base64 video encoding and NVCF status polling

Browse files
Files changed (1) hide show
  1. app.py +172 -71
app.py CHANGED
@@ -128,7 +128,7 @@ def enhance_prompt_with_ai(prompt_text, style_preset, ai_mode, ai_url, ai_key, s
128
  except Exception:
129
  return f"{prompt_text}, {style_modifiers}".strip(", ")
130
 
131
- def generate_video(prompt, negative_prompt, style_preset, ai_mode, ai_url, ai_key, session_id, progress=gr.Progress()):
132
  if not prompt or prompt.strip() == "":
133
  return None, "❌ Please enter a prompt."
134
 
@@ -141,91 +141,176 @@ def generate_video(prompt, negative_prompt, style_preset, ai_mode, ai_url, ai_ke
141
  # Generate seed
142
  seed = int(np.random.randint(0, 2**32 - 1))
143
 
144
- # 2. Call the Cloud Space API to generate the video
145
- progress(0.3, desc="Connecting to Hugging Face Cloud Video Generator...")
146
-
147
- # List of verified public spaces to try sequentially (fault-tolerance)
148
- spaces = [
149
- {"name": "Lightricks/ltx-video-distilled", "type": "ltx"},
150
- {"name": "Wan-AI/Wan2.1", "type": "wan"}
151
- ]
152
-
153
  video_path = None
154
  success_space = None
155
 
156
- for space in spaces:
157
- try:
158
- progress(0.5, desc=f"Generating video using {space['name']} in the cloud...")
159
- client = Client(space["name"], token=ai_key if ai_key else None)
160
 
161
- if space["type"] == "ltx":
162
- # Predict signature: predict(prompt, negative_prompt, input_image_filepath, input_video_filepath, height_ui, width_ui, mode, duration_ui, ui_frames_to_use, seed_ui, randomize_seed, ui_guidance_scale, improve_texture_flag, api_name="/text_to_video")
163
- res = client.predict(
164
- prompt=enhanced_prompt,
165
- negative_prompt=negative_prompt if negative_prompt else "worst quality, inconsistent motion, blurry, jittery, distorted",
166
- input_image_filepath=None,
167
- input_video_filepath=None,
168
- height_ui=512,
169
- width_ui=704,
170
- mode="text-to-video",
171
- duration_ui=2,
172
- ui_frames_to_use=9,
173
- seed_ui=seed,
174
- randomize_seed=True,
175
- ui_guidance_scale=1.0,
176
- improve_texture_flag=True,
177
- api_name="/text_to_video"
178
- )
179
 
180
- if isinstance(res, tuple):
181
- video_data = res[0]
182
- else:
183
- video_data = res
184
-
185
- if isinstance(video_data, dict):
186
- video_path = video_data.get("video") or video_data.get("path")
187
- else:
188
- video_path = video_data
189
 
190
- elif space["type"] == "wan":
191
- # Predict signature: predict(prompt, size, watermark_wan, seed, api_name="/t2v_generation_async")
192
- res = client.predict(
193
- prompt=enhanced_prompt,
194
- size="1280*720",
195
- watermark_wan=True,
196
- seed=seed,
197
- api_name="/t2v_generation_async"
198
- )
199
-
200
- # Poll status_refresh in a loop for up to 60 seconds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  import time
202
- for i in range(15):
 
 
 
 
 
 
203
  time.sleep(4)
204
- progress((0.5 + 0.03 * i), desc="Generating frames in Wan Space... (polling status)")
205
- status_res = client.predict(api_name="/status_refresh")
206
- if isinstance(status_res, tuple) and status_res[0]:
207
- video_data = status_res[0]
208
- if isinstance(video_data, dict) and video_data.get("video"):
209
- video_path = video_data["video"]
 
 
 
 
 
 
 
 
210
  break
 
 
 
 
 
 
211
 
212
- if video_path and os.path.exists(video_path):
213
- success_space = space["name"]
214
- break
215
- except Exception as err:
216
- print(f"Failed to generate on {space['name']}: {str(err)}")
217
- continue
 
 
 
 
 
 
 
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  if not video_path:
220
- return None, "❌ Cloud generation failed. All public spaces are currently overloaded. Please try again in a few moments."
221
 
222
  progress(1.0, desc="Video generation complete!")
223
 
224
  info = f"""
225
  **Cinematic Prompt (Enhanced):** {enhanced_prompt}
226
- **Cloud Model:** {success_space}
227
  **Seed:** {seed}
228
- **Status:** Powered entirely by InvictaTill AI & HF Cloud Spaces (No local GPU required)
229
  """.strip()
230
 
231
  return video_path, info
@@ -347,6 +432,22 @@ with gr.Blocks(css=custom_css, title="InvictaTill VideoGen Studio", theme=gr.the
347
 
348
  with gr.Row():
349
  with gr.Column(scale=1, elem_classes="panel"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  gr.Markdown("### ✍️ Prompt Composer")
351
  prompt = gr.Textbox(label="Describe your scene", placeholder="A cyberpunk drone shot flying through neon-lit Tokyo streets...", lines=4, elem_id="prompt")
352
 
@@ -369,8 +470,8 @@ with gr.Blocks(css=custom_css, title="InvictaTill VideoGen Studio", theme=gr.the
369
  )
370
  ai_key_input = gr.Textbox(
371
  value=os.environ.get("VITE_INVICTATILL_AI_KEY", ""),
372
- label="API Key / Auth Token",
373
- placeholder="invicta_sk_...",
374
  type="password"
375
  )
376
  session_id_input = gr.Textbox(
@@ -409,7 +510,7 @@ with gr.Blocks(css=custom_css, title="InvictaTill VideoGen Studio", theme=gr.the
409
 
410
  generate_btn.click(
411
  fn=generate_video,
412
- inputs=[prompt, negative_prompt, style_dropdown, ai_mode_dropdown, ai_url_input, ai_key_input, session_id_input],
413
  outputs=[video_output, info_output]
414
  )
415
 
 
128
  except Exception:
129
  return f"{prompt_text}, {style_modifiers}".strip(", ")
130
 
131
+ def generate_video(prompt, negative_prompt, style_preset, generator_model, input_video, ai_mode, ai_url, ai_key, session_id, progress=gr.Progress()):
132
  if not prompt or prompt.strip() == "":
133
  return None, "❌ Please enter a prompt."
134
 
 
141
  # Generate seed
142
  seed = int(np.random.randint(0, 2**32 - 1))
143
 
 
 
 
 
 
 
 
 
 
144
  video_path = None
145
  success_space = None
146
 
147
+ # 2. Check if Cosmos-Transfer is selected
148
+ if generator_model == "NVIDIA Cosmos-Transfer2.5-2b (Physics NIM)":
149
+ if not input_video:
150
+ return None, " NVIDIA Cosmos-Transfer requires an Input Control Video for Sim2Real style transfer. Please upload a video first."
151
 
152
+ progress(0.3, desc="Connecting to NVIDIA Cosmos NIM Endpoint...")
153
+
154
+ # Load API Key (NVIDIA Key)
155
+ # Fallback to default working NVIDIA key from brain.py if not provided
156
+ nvidia_key = ai_key if (ai_key and ai_key.strip()) else "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"
157
+
158
+ try:
159
+ import base64
160
+ progress(0.4, desc="Encoding input video file...")
161
+ with open(input_video, "rb") as f:
162
+ video_base64 = base64.b64encode(f.read()).decode("utf-8")
 
 
 
 
 
 
 
163
 
164
+ invoke_url = "https://ai.api.nvidia.com/v1/cosmos/nvidia/cosmos-transfer2.5-2b"
165
+ headers = {
166
+ "Authorization": f"Bearer {nvidia_key}",
167
+ "Accept": "application/json",
168
+ "Content-Type": "application/json"
169
+ }
 
 
 
170
 
171
+ payload = {
172
+ "prompt": enhanced_prompt,
173
+ "video": f"data:video/mp4;base64,{video_base64}",
174
+ "strength": 0.85
175
+ }
176
+
177
+ progress(0.5, desc="Sending transfer request to NVIDIA Cloud...")
178
+ res = requests.post(invoke_url, headers=headers, json=payload, timeout=90)
179
+
180
+ if res.status_code == 200:
181
+ data = res.json()
182
+ video_b64 = data.get("b64_video") or data.get("video")
183
+ if video_b64:
184
+ if "base64," in video_b64:
185
+ video_b64 = video_b64.split("base64,")[1]
186
+ video_path = os.path.join(tempfile.gettempdir(), f"cosmos_out_{seed}.mp4")
187
+ with open(video_path, "wb") as f:
188
+ f.write(base64.b64decode(video_b64))
189
+ success_space = "NVIDIA Cosmos-Transfer2.5-2b (Direct Response)"
190
+
191
+ elif res.status_code == 202:
192
+ # Asynchronous execution, polling is required
193
+ req_id = res.json().get("id") or res.headers.get("NVCF-REQID") or res.headers.get("NV-Request-Id")
194
+ if not req_id:
195
+ raise Exception("Asynchronous request accepted by NVIDIA, but no Request ID returned.")
196
+
197
+ # Poll the status endpoint
198
  import time
199
+ poll_url = f"https://api.nvcf.nvidia.com/v2/nvcf/pexec/status/{req_id}"
200
+ poll_headers = {
201
+ "Authorization": f"Bearer {nvidia_key}",
202
+ "Accept": "application/json"
203
+ }
204
+
205
+ for i in range(25): # poll up to 100s
206
  time.sleep(4)
207
+ progress(0.5 + 0.02 * i, desc=f"NVIDIA Cosmos rendering... (polling status {i+1}/25)")
208
+ poll_res = requests.get(poll_url, headers=poll_headers)
209
+
210
+ if poll_res.status_code == 200:
211
+ poll_data = poll_res.json()
212
+ # Output video extraction
213
+ video_b64 = poll_data.get("b64_video") or poll_data.get("video")
214
+ if video_b64:
215
+ if "base64," in video_b64:
216
+ video_b64 = video_b64.split("base64,")[1]
217
+ video_path = os.path.join(tempfile.gettempdir(), f"cosmos_{req_id}.mp4")
218
+ with open(video_path, "wb") as f:
219
+ f.write(base64.b64decode(video_b64))
220
+ success_space = "NVIDIA Cosmos-Transfer2.5-2b (Polled NIM)"
221
  break
222
+ elif poll_res.status_code == 202:
223
+ continue
224
+ else:
225
+ raise Exception(f"NVIDIA polling failed: {poll_res.status_code} - {poll_res.text}")
226
+ else:
227
+ raise Exception(f"NVIDIA API Error {res.status_code}: {res.text}")
228
 
229
+ except Exception as e:
230
+ print(f"NVIDIA Cosmos execution failed: {str(e)}")
231
+ return None, f"❌ NVIDIA Cosmos execution failed: {str(e)}"
232
+
233
+ else:
234
+ # Standard Hugging Face Cloud Spaces
235
+ progress(0.3, desc="Connecting to Hugging Face Cloud Video Generator...")
236
+
237
+ # Determine Space to target based on selection
238
+ if generator_model == "Lightricks LTX-Video (Distilled)":
239
+ target_spaces = [{"name": "Lightricks/ltx-video-distilled", "type": "ltx"}]
240
+ else:
241
+ target_spaces = [{"name": "Wan-AI/Wan2.1", "type": "wan"}]
242
 
243
+ for space in target_spaces:
244
+ try:
245
+ progress(0.5, desc=f"Generating video using {space['name']} in the cloud...")
246
+ client = Client(space["name"], token=ai_key if ai_key else None)
247
+
248
+ if space["type"] == "ltx":
249
+ res = client.predict(
250
+ prompt=enhanced_prompt,
251
+ negative_prompt=negative_prompt if negative_prompt else "worst quality, inconsistent motion, blurry, jittery, distorted",
252
+ input_image_filepath=None,
253
+ input_video_filepath=None,
254
+ height_ui=512,
255
+ width_ui=704,
256
+ mode="text-to-video",
257
+ duration_ui=2,
258
+ ui_frames_to_use=9,
259
+ seed_ui=seed,
260
+ randomize_seed=True,
261
+ ui_guidance_scale=1.0,
262
+ improve_texture_flag=True,
263
+ api_name="/text_to_video"
264
+ )
265
+
266
+ if isinstance(res, tuple):
267
+ video_data = res[0]
268
+ else:
269
+ video_data = res
270
+
271
+ if isinstance(video_data, dict):
272
+ video_path = video_data.get("video") or video_data.get("path")
273
+ else:
274
+ video_path = video_data
275
+
276
+ elif space["type"] == "wan":
277
+ res = client.predict(
278
+ prompt=enhanced_prompt,
279
+ size="1280*720",
280
+ watermark_wan=True,
281
+ seed=seed,
282
+ api_name="/t2v_generation_async"
283
+ )
284
+
285
+ # Poll status_refresh in a loop for up to 60 seconds
286
+ import time
287
+ for i in range(15):
288
+ time.sleep(4)
289
+ progress((0.5 + 0.03 * i), desc="Generating frames in Wan Space... (polling status)")
290
+ status_res = client.predict(api_name="/status_refresh")
291
+ if isinstance(status_res, tuple) and status_res[0]:
292
+ video_data = status_res[0]
293
+ if isinstance(video_data, dict) and video_data.get("video"):
294
+ video_path = video_data["video"]
295
+ break
296
+
297
+ if video_path and os.path.exists(video_path):
298
+ success_space = space["name"]
299
+ break
300
+ except Exception as err:
301
+ print(f"Failed to generate on {space['name']}: {str(err)}")
302
+ continue
303
+
304
  if not video_path:
305
+ return None, "❌ Cloud generation failed. The selected service is currently overloaded or unresponsive. Please try again."
306
 
307
  progress(1.0, desc="Video generation complete!")
308
 
309
  info = f"""
310
  **Cinematic Prompt (Enhanced):** {enhanced_prompt}
311
+ **Video Engine:** {success_space}
312
  **Seed:** {seed}
313
+ **Status:** Powered entirely by InvictaTill AI & Cloud NIMs (No local GPU required)
314
  """.strip()
315
 
316
  return video_path, info
 
432
 
433
  with gr.Row():
434
  with gr.Column(scale=1, elem_classes="panel"):
435
+ gr.Markdown("### ⚙️ Generation Model")
436
+ generator_model = gr.Dropdown(
437
+ choices=[
438
+ "Lightricks LTX-Video (Distilled)",
439
+ "Wan-AI Wan 2.1 (ZeroGPU)",
440
+ "NVIDIA Cosmos-Transfer2.5-2b (Physics NIM)"
441
+ ],
442
+ value="Lightricks LTX-Video (Distilled)",
443
+ label="Choose Video Generator Engine"
444
+ )
445
+
446
+ input_video = gr.Video(
447
+ label="Input Video (Required ONLY for NVIDIA Cosmos-Transfer style transfer)",
448
+ interactive=True
449
+ )
450
+
451
  gr.Markdown("### ✍️ Prompt Composer")
452
  prompt = gr.Textbox(label="Describe your scene", placeholder="A cyberpunk drone shot flying through neon-lit Tokyo streets...", lines=4, elem_id="prompt")
453
 
 
470
  )
471
  ai_key_input = gr.Textbox(
472
  value=os.environ.get("VITE_INVICTATILL_AI_KEY", ""),
473
+ label="API Key / Auth Token (NVIDIA Key for Cosmos)",
474
+ placeholder="invicta_sk_... or nvapi-...",
475
  type="password"
476
  )
477
  session_id_input = gr.Textbox(
 
510
 
511
  generate_btn.click(
512
  fn=generate_video,
513
+ inputs=[prompt, negative_prompt, style_dropdown, generator_model, input_video, ai_mode_dropdown, ai_url_input, ai_key_input, session_id_input],
514
  outputs=[video_output, info_output]
515
  )
516