dippoo Claude Opus 4.5 commited on
Commit
fc4811e
·
1 Parent(s): f1f21b9

Add video async polling + Higgsfield models via WaveSpeed

Browse files

- Fixed video generation async polling (same issue as img2img)
- Added Higgsfield Soul (image edit) and DoP (video) via WaveSpeed API
- Updated UI dropdowns with Higgsfield options

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

src/content_engine/api/routes_video.py CHANGED
@@ -185,6 +185,58 @@ async def generate_video_cloud(
185
  }
186
 
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  async def _generate_cloud_video(
189
  job_id: str,
190
  image_bytes: bytes,
@@ -244,10 +296,18 @@ async def _generate_cloud_video(
244
  _video_jobs[job_id]["error"] = error_msg
245
  return
246
 
247
- # Extract video URL
248
  video_url = None
249
  outputs = data.get("outputs", [])
250
- if outputs:
 
 
 
 
 
 
 
 
251
  video_url = outputs[0]
252
  elif "output" in data:
253
  out = data["output"]
@@ -258,7 +318,7 @@ async def _generate_cloud_video(
258
 
259
  if not video_url:
260
  _video_jobs[job_id]["status"] = "failed"
261
- _video_jobs[job_id]["error"] = "No video URL in response"
262
  return
263
 
264
  # Download the video
 
185
  }
186
 
187
 
188
+ async def _poll_wavespeed_video(poll_url: str, api_key: str, job_id: str, max_attempts: int = 120, interval: float = 3.0) -> str | None:
189
+ """Poll the WaveSpeed async video job URL until outputs are ready.
190
+
191
+ Returns the first output URL when available, or None on failure.
192
+ """
193
+ import httpx
194
+
195
+ async with httpx.AsyncClient(timeout=60) as client:
196
+ for attempt in range(max_attempts):
197
+ try:
198
+ resp = await client.get(
199
+ poll_url,
200
+ headers={"Authorization": f"Bearer {api_key}"},
201
+ )
202
+ resp.raise_for_status()
203
+ result = resp.json()
204
+
205
+ data = result.get("data", result)
206
+ status = data.get("status", "")
207
+
208
+ if status == "failed":
209
+ error_msg = data.get("error", "Unknown error")
210
+ logger.error("WaveSpeed video job failed: %s", error_msg)
211
+ return None
212
+
213
+ outputs = data.get("outputs", [])
214
+ if outputs:
215
+ logger.info("WaveSpeed video job completed after %d polls", attempt + 1)
216
+ return outputs[0]
217
+
218
+ # Also check for 'output' field
219
+ if "output" in data:
220
+ out = data["output"]
221
+ if isinstance(out, list) and out:
222
+ return out[0]
223
+ elif isinstance(out, str):
224
+ return out
225
+
226
+ # Update job status with progress
227
+ _video_jobs[job_id]["message"] = f"Generating video... (poll {attempt + 1}/{max_attempts})"
228
+
229
+ logger.debug("WaveSpeed video job pending (attempt %d/%d)", attempt + 1, max_attempts)
230
+ await asyncio.sleep(interval)
231
+
232
+ except Exception as e:
233
+ logger.warning("Video poll request failed: %s", e)
234
+ await asyncio.sleep(interval)
235
+
236
+ logger.error("WaveSpeed video job timed out after %d attempts", max_attempts)
237
+ return None
238
+
239
+
240
  async def _generate_cloud_video(
241
  job_id: str,
242
  image_bytes: bytes,
 
296
  _video_jobs[job_id]["error"] = error_msg
297
  return
298
 
299
+ # Extract video URL - handle async response (outputs empty, urls.get present)
300
  video_url = None
301
  outputs = data.get("outputs", [])
302
+ urls_data = data.get("urls", {})
303
+
304
+ # Check for async response first
305
+ if not outputs and urls_data and urls_data.get("get"):
306
+ poll_url = urls_data["get"]
307
+ logger.info("WaveSpeed video returned async job, polling: %s", poll_url[:80])
308
+ _video_jobs[job_id]["message"] = "Polling for video result..."
309
+ video_url = await _poll_wavespeed_video(poll_url, api_key, job_id)
310
+ elif outputs:
311
  video_url = outputs[0]
312
  elif "output" in data:
313
  out = data["output"]
 
318
 
319
  if not video_url:
320
  _video_jobs[job_id]["status"] = "failed"
321
+ _video_jobs[job_id]["error"] = f"No video URL in response: {data}"
322
  return
323
 
324
  # Download the video
src/content_engine/api/ui.html CHANGED
@@ -910,6 +910,9 @@ select { cursor: pointer; }
910
  <div id="cloud-edit-model-select" style="display:none">
911
  <label>Cloud Edit Model (Image-to-Image)</label>
912
  <select id="gen-cloud-edit-model">
 
 
 
913
  <optgroup label="SeeDream Edit - NSFW OK">
914
  <option value="seedream-4.5-edit">SeeDream v4.5 Edit (Best Quality)</option>
915
  <option value="seedream-4-edit">SeeDream v4 Edit</option>
@@ -987,6 +990,11 @@ select { cursor: pointer; }
987
  <div id="video-cloud-model-select">
988
  <label>Video Model</label>
989
  <select id="video-cloud-model">
 
 
 
 
 
990
  <optgroup label="WAN 2.6 I2V (Alibaba)">
991
  <option value="wan-2.6-i2v-pro" selected>WAN 2.6 I2V Pro (Best)</option>
992
  <option value="wan-2.6-i2v">WAN 2.6 I2V</option>
 
910
  <div id="cloud-edit-model-select" style="display:none">
911
  <label>Cloud Edit Model (Image-to-Image)</label>
912
  <select id="gen-cloud-edit-model">
913
+ <optgroup label="Higgsfield (Character Consistency)">
914
+ <option value="higgsfield-soul">Higgsfield Soul (Best for Faces)</option>
915
+ </optgroup>
916
  <optgroup label="SeeDream Edit - NSFW OK">
917
  <option value="seedream-4.5-edit">SeeDream v4.5 Edit (Best Quality)</option>
918
  <option value="seedream-4-edit">SeeDream v4 Edit</option>
 
990
  <div id="video-cloud-model-select">
991
  <label>Video Model</label>
992
  <select id="video-cloud-model">
993
+ <optgroup label="Higgsfield (Cinematic Motion)">
994
+ <option value="higgsfield-dop">Higgsfield DoP (5s Cinematic)</option>
995
+ <option value="higgsfield-dop-turbo">Higgsfield DoP Turbo (Fast)</option>
996
+ <option value="higgsfield-dop-lite">Higgsfield DoP Lite</option>
997
+ </optgroup>
998
  <optgroup label="WAN 2.6 I2V (Alibaba)">
999
  <option value="wan-2.6-i2v-pro" selected>WAN 2.6 I2V Pro (Best)</option>
1000
  <option value="wan-2.6-i2v">WAN 2.6 I2V</option>
src/content_engine/services/cloud_providers/wavespeed_provider.py CHANGED
@@ -64,6 +64,10 @@ MODEL_MAP = {
64
  # Image-to-Video models
65
  # Based on https://wavespeed.ai/models
66
  VIDEO_MODEL_MAP = {
 
 
 
 
67
  # WAN 2.6 I2V (Alibaba)
68
  "wan-2.6-i2v-pro": "alibaba/wan-2.6/image-to-video-pro",
69
  "wan-2.6-i2v": "alibaba/wan-2.6/image-to-video",
@@ -94,6 +98,8 @@ VIDEO_MODEL_MAP = {
94
  # Map friendly names to WaveSpeed edit model API paths
95
  # Based on https://wavespeed.ai/models
96
  EDIT_MODEL_MAP = {
 
 
97
  # SeeDream Edit (ByteDance) - NSFW OK
98
  "seedream-4.5-edit": "bytedance/seedream-v4.5/edit",
99
  "seedream-4-edit": "bytedance/seedream-v4/edit",
 
64
  # Image-to-Video models
65
  # Based on https://wavespeed.ai/models
66
  VIDEO_MODEL_MAP = {
67
+ # Higgsfield DoP (Cinematic Motion)
68
+ "higgsfield-dop": "higgsfield/dop/image-to-video",
69
+ "higgsfield-dop-lite": "higgsfield/dop/image-to-video", # Use options param
70
+ "higgsfield-dop-turbo": "higgsfield/dop/image-to-video", # Use options param
71
  # WAN 2.6 I2V (Alibaba)
72
  "wan-2.6-i2v-pro": "alibaba/wan-2.6/image-to-video-pro",
73
  "wan-2.6-i2v": "alibaba/wan-2.6/image-to-video",
 
98
  # Map friendly names to WaveSpeed edit model API paths
99
  # Based on https://wavespeed.ai/models
100
  EDIT_MODEL_MAP = {
101
+ # Higgsfield Soul (Character Consistency)
102
+ "higgsfield-soul": "higgsfield/soul/image-to-image",
103
  # SeeDream Edit (ByteDance) - NSFW OK
104
  "seedream-4.5-edit": "bytedance/seedream-v4.5/edit",
105
  "seedream-4-edit": "bytedance/seedream-v4/edit",