Spaces:
Running
Running
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|