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

Add Higgsfield provider for Kling 3.0, Sora 2, Veo 3.1

Browse files

- Created HiggsFieldProvider using higgsfield-client SDK
- Added Kling 3.0 Pro and standard models for video generation
- Added Sora 2 and Veo 3.1 via Higgsfield platform
- Updated UI with premium model options
- Added higgsfield-client to requirements

Requires HIGGSFIELD_API_KEY or HF_API_KEY env var to use.

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

requirements.txt CHANGED
@@ -2,15 +2,14 @@ fastapi>=0.109.0
2
  uvicorn[standard]>=0.27.0
3
  aiohttp>=3.9.0
4
  sqlalchemy>=2.0.0
 
5
  aiosqlite>=0.19.0
6
  pydantic>=2.5.0
7
  pydantic-settings>=2.1.0
8
  jinja2>=3.1.0
9
  Pillow>=10.2.0
 
10
  httpx>=0.26.0
11
  pyyaml>=6.0
12
  python-multipart>=0.0.6
13
- python-dotenv>=1.0.0
14
- runpod>=1.6.0
15
- paramiko>=3.4.0
16
- wavespeed>=0.1.0
 
2
  uvicorn[standard]>=0.27.0
3
  aiohttp>=3.9.0
4
  sqlalchemy>=2.0.0
5
+ alembic>=1.13.0
6
  aiosqlite>=0.19.0
7
  pydantic>=2.5.0
8
  pydantic-settings>=2.1.0
9
  jinja2>=3.1.0
10
  Pillow>=10.2.0
11
+ apscheduler>=3.10.0
12
  httpx>=0.26.0
13
  pyyaml>=6.0
14
  python-multipart>=0.0.6
15
+ higgsfield-client>=0.1.0
 
 
 
src/content_engine/api/routes_video.py CHANGED
@@ -21,14 +21,20 @@ router = APIRouter(prefix="/api/video", tags=["video"])
21
  # Video jobs tracking
22
  _video_jobs: dict[str, dict] = {}
23
 
24
- # WaveSpeed provider (initialized from main.py)
25
  _wavespeed_provider = None
 
26
 
27
  def init_wavespeed(provider):
28
  """Initialize WaveSpeed provider for cloud video generation."""
29
  global _wavespeed_provider
30
  _wavespeed_provider = provider
31
 
 
 
 
 
 
32
  # Pod state is shared from routes_pod
33
  def _get_pod_state():
34
  from content_engine.api.routes_pod import _pod_state
@@ -145,11 +151,22 @@ async def generate_video_cloud(
145
  num_frames: int = Form(81),
146
  fps: int = Form(24),
147
  seed: int = Form(-1),
 
148
  ):
149
- """Generate a video using WaveSpeed cloud API (Kling, WAN I2V, Veo, etc)."""
150
  import random
151
  import httpx
152
 
 
 
 
 
 
 
 
 
 
 
153
  if not _wavespeed_provider:
154
  raise HTTPException(500, "WaveSpeed API not configured")
155
 
@@ -185,6 +202,134 @@ async def generate_video_cloud(
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
 
 
21
  # Video jobs tracking
22
  _video_jobs: dict[str, dict] = {}
23
 
24
+ # Cloud providers (initialized from main.py)
25
  _wavespeed_provider = None
26
+ _higgsfield_provider = None
27
 
28
  def init_wavespeed(provider):
29
  """Initialize WaveSpeed provider for cloud video generation."""
30
  global _wavespeed_provider
31
  _wavespeed_provider = provider
32
 
33
+ def init_higgsfield(provider):
34
+ """Initialize Higgsfield provider for Kling 3.0, Sora 2, etc."""
35
+ global _higgsfield_provider
36
+ _higgsfield_provider = provider
37
+
38
  # Pod state is shared from routes_pod
39
  def _get_pod_state():
40
  from content_engine.api.routes_pod import _pod_state
 
151
  num_frames: int = Form(81),
152
  fps: int = Form(24),
153
  seed: int = Form(-1),
154
+ backend: str = Form("wavespeed"), # wavespeed or higgsfield
155
  ):
156
+ """Generate a video using cloud API (WaveSpeed or Higgsfield)."""
157
  import random
158
  import httpx
159
 
160
+ # Route to Higgsfield for Kling 3.0 models
161
+ if backend == "higgsfield" or model.startswith("kling-3"):
162
+ return await generate_video_higgsfield(
163
+ image=image,
164
+ prompt=prompt,
165
+ model=model,
166
+ duration=max(3, num_frames // 24), # Convert frames to seconds
167
+ seed=seed,
168
+ )
169
+
170
  if not _wavespeed_provider:
171
  raise HTTPException(500, "WaveSpeed API not configured")
172
 
 
202
  }
203
 
204
 
205
+ @router.post("/generate/higgsfield")
206
+ async def generate_video_higgsfield(
207
+ image: UploadFile = File(...),
208
+ prompt: str = Form("smooth cinematic motion"),
209
+ model: str = Form("kling-3.0"),
210
+ duration: int = Form(5),
211
+ resolution: str = Form("720p"),
212
+ enable_audio: bool = Form(False),
213
+ seed: int = Form(-1),
214
+ ):
215
+ """Generate a video using Higgsfield (Kling 3.0, Sora 2, Veo 3.1)."""
216
+ import random
217
+
218
+ if not _higgsfield_provider:
219
+ raise HTTPException(500, "Higgsfield API not configured - set HIGGSFIELD_API_KEY")
220
+
221
+ job_id = str(uuid.uuid4())[:8]
222
+ seed = seed if seed >= 0 else random.randint(0, 2**32 - 1)
223
+
224
+ # Read the image
225
+ image_bytes = await image.read()
226
+
227
+ # Create job entry
228
+ _video_jobs[job_id] = {
229
+ "status": "running",
230
+ "seed": seed,
231
+ "started_at": time.time(),
232
+ "duration": duration,
233
+ "model": model,
234
+ "backend": "higgsfield",
235
+ "message": "Starting Higgsfield video generation...",
236
+ }
237
+
238
+ logger.info("Higgsfield video generation started: %s (model=%s)", job_id, model)
239
+
240
+ # Start background task
241
+ asyncio.create_task(_generate_higgsfield_video(
242
+ job_id, image_bytes, prompt, model, duration, resolution, enable_audio, seed
243
+ ))
244
+
245
+ return {
246
+ "job_id": job_id,
247
+ "status": "running",
248
+ "seed": seed,
249
+ "model": model,
250
+ "backend": "higgsfield",
251
+ "estimated_time": f"~{duration * 10}-{duration * 20} seconds",
252
+ }
253
+
254
+
255
+ async def _generate_higgsfield_video(
256
+ job_id: str,
257
+ image_bytes: bytes,
258
+ prompt: str,
259
+ model: str,
260
+ duration: int,
261
+ resolution: str,
262
+ enable_audio: bool,
263
+ seed: int,
264
+ ):
265
+ """Background task to generate video via Higgsfield API."""
266
+ try:
267
+ _video_jobs[job_id]["message"] = "Uploading image to Higgsfield..."
268
+
269
+ # Upload image to temp URL
270
+ image_url = await _higgsfield_provider._upload_temp_image(image_bytes) if hasattr(_higgsfield_provider, '_upload_temp_image') else None
271
+
272
+ if not image_url:
273
+ # Fall back to base64 data URL
274
+ import base64
275
+ image_b64 = base64.b64encode(image_bytes).decode("utf-8")
276
+ image_url = f"data:image/png;base64,{image_b64}"
277
+
278
+ _video_jobs[job_id]["message"] = f"Generating video with {model}..."
279
+
280
+ # Generate video
281
+ result = await _higgsfield_provider.generate_video(
282
+ prompt=prompt,
283
+ model=model,
284
+ duration=duration,
285
+ resolution=resolution,
286
+ enable_audio=enable_audio,
287
+ image_url=image_url,
288
+ )
289
+
290
+ video_url = result.get("video_url")
291
+ if not video_url:
292
+ _video_jobs[job_id]["status"] = "failed"
293
+ _video_jobs[job_id]["error"] = "No video URL in response"
294
+ return
295
+
296
+ # Download the video
297
+ _video_jobs[job_id]["message"] = "Downloading generated video..."
298
+ import httpx
299
+ async with httpx.AsyncClient(timeout=120) as client:
300
+ video_resp = await client.get(video_url)
301
+
302
+ if video_resp.status_code != 200:
303
+ _video_jobs[job_id]["status"] = "failed"
304
+ _video_jobs[job_id]["error"] = "Failed to download video"
305
+ return
306
+
307
+ # Save to local output directory
308
+ from content_engine.config import settings
309
+ output_dir = settings.paths.output_dir / "videos"
310
+ output_dir.mkdir(parents=True, exist_ok=True)
311
+
312
+ ext = ".mp4"
313
+ if video_url.endswith(".webm"):
314
+ ext = ".webm"
315
+
316
+ local_path = output_dir / f"video_{job_id}{ext}"
317
+ local_path.write_bytes(video_resp.content)
318
+
319
+ _video_jobs[job_id]["status"] = "completed"
320
+ _video_jobs[job_id]["output_path"] = str(local_path)
321
+ _video_jobs[job_id]["completed_at"] = time.time()
322
+ _video_jobs[job_id]["filename"] = local_path.name
323
+ _video_jobs[job_id]["message"] = "Video generated successfully!"
324
+
325
+ logger.info("Higgsfield video saved: %s", local_path)
326
+
327
+ except Exception as e:
328
+ logger.error("Higgsfield video generation failed: %s", e)
329
+ _video_jobs[job_id]["status"] = "failed"
330
+ _video_jobs[job_id]["error"] = str(e)
331
+
332
+
333
  async def _poll_wavespeed_video(poll_url: str, api_key: str, job_id: str, max_attempts: int = 120, interval: float = 3.0) -> str | None:
334
  """Poll the WaveSpeed async video job URL until outputs are ready.
335
 
src/content_engine/api/ui.html CHANGED
@@ -990,10 +990,17 @@ select { cursor: pointer; }
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>
@@ -1007,8 +1014,8 @@ select { cursor: pointer; }
1007
  <option value="wan-2.2-i2v-1080p">WAN 2.2 I2V 1080p</option>
1008
  <option value="wan-2.2-i2v-720p">WAN 2.2 I2V 720p</option>
1009
  </optgroup>
1010
- <optgroup label="Kling (Kuaishou)">
1011
- <option value="kling-o3-pro">Kling O3 Pro (Highest Quality)</option>
1012
  <option value="kling-o3">Kling O3</option>
1013
  </optgroup>
1014
  <optgroup label="Seedance (ByteDance)">
 
990
  <div id="video-cloud-model-select">
991
  <label>Video Model</label>
992
  <select id="video-cloud-model">
993
+ <optgroup label="⭐ Kling 3.0 (Higgsfield) - Best Quality">
994
+ <option value="kling-3.0-pro">Kling 3.0 Pro (15s, Audio)</option>
995
+ <option value="kling-3.0">Kling 3.0 (Up to 15s)</option>
996
+ </optgroup>
997
+ <optgroup label="⭐ Premium (Higgsfield)">
998
+ <option value="sora-2-hf">Sora 2 (OpenAI via Higgsfield)</option>
999
+ <option value="veo-3.1-hf">Veo 3.1 (Google via Higgsfield)</option>
1000
+ </optgroup>
1001
  <optgroup label="Higgsfield (Cinematic Motion)">
1002
  <option value="higgsfield-dop">Higgsfield DoP (5s Cinematic)</option>
1003
  <option value="higgsfield-dop-turbo">Higgsfield DoP Turbo (Fast)</option>
 
1004
  </optgroup>
1005
  <optgroup label="WAN 2.6 I2V (Alibaba)">
1006
  <option value="wan-2.6-i2v-pro" selected>WAN 2.6 I2V Pro (Best)</option>
 
1014
  <option value="wan-2.2-i2v-1080p">WAN 2.2 I2V 1080p</option>
1015
  <option value="wan-2.2-i2v-720p">WAN 2.2 I2V 720p</option>
1016
  </optgroup>
1017
+ <optgroup label="Kling (WaveSpeed)">
1018
+ <option value="kling-o3-pro">Kling O3 Pro</option>
1019
  <option value="kling-o3">Kling O3</option>
1020
  </optgroup>
1021
  <optgroup label="Seedance (ByteDance)">
src/content_engine/main.py CHANGED
@@ -129,6 +129,19 @@ async def lifespan(app: FastAPI):
129
  else:
130
  logger.info("WaveSpeed not configured — cloud generation disabled")
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  # Initialize route dependencies
133
  routes_generation.init_routes(
134
  local_worker, template_engine, variation_engine, character_profiles,
@@ -138,9 +151,11 @@ async def lifespan(app: FastAPI):
138
  routes_catalog.init_routes(catalog)
139
  routes_system.init_routes(comfyui_client, catalog, template_engine, character_profiles)
140
 
141
- # Initialize video routes with WaveSpeed provider for cloud video generation
142
  if wavespeed_provider:
143
  routes_video.init_wavespeed(wavespeed_provider)
 
 
144
 
145
  # Initialize LoRA trainer (local)
146
  from content_engine.services.lora_trainer import LoRATrainer
 
129
  else:
130
  logger.info("WaveSpeed not configured — cloud generation disabled")
131
 
132
+ # Initialize Higgsfield cloud provider if API key is set
133
+ higgsfield_provider = None
134
+ higgsfield_key = os.environ.get("HIGGSFIELD_API_KEY") or os.environ.get("HF_API_KEY")
135
+ if higgsfield_key:
136
+ try:
137
+ from content_engine.services.cloud_providers.higgsfield_provider import HiggsFieldProvider
138
+ higgsfield_provider = HiggsFieldProvider()
139
+ logger.info("Higgsfield cloud provider initialized (Kling 3.0, Sora 2, Veo 3.1)")
140
+ except Exception as e:
141
+ logger.warning("Failed to initialize Higgsfield provider: %s", e)
142
+ else:
143
+ logger.info("Higgsfield not configured — set HIGGSFIELD_API_KEY for Kling 3.0/Sora 2")
144
+
145
  # Initialize route dependencies
146
  routes_generation.init_routes(
147
  local_worker, template_engine, variation_engine, character_profiles,
 
151
  routes_catalog.init_routes(catalog)
152
  routes_system.init_routes(comfyui_client, catalog, template_engine, character_profiles)
153
 
154
+ # Initialize video routes with cloud providers
155
  if wavespeed_provider:
156
  routes_video.init_wavespeed(wavespeed_provider)
157
+ if higgsfield_provider:
158
+ routes_video.init_higgsfield(higgsfield_provider)
159
 
160
  # Initialize LoRA trainer (local)
161
  from content_engine.services.lora_trainer import LoRATrainer
src/content_engine/services/cloud_providers/higgsfield_provider.py ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Higgsfield.ai cloud provider — access to Kling 3.0, Sora 2, Veo 3.1, and more.
2
+
3
+ Higgsfield provides a unified API for multiple AI video and image generation models
4
+ including Kling 3.0 (Kuaishou), Sora 2 (OpenAI), Veo 3.1 (Google), WAN 2.5 (Alibaba),
5
+ and their own Higgsfield Soul for character consistency.
6
+
7
+ SDK: pip install higgsfield-client
8
+ Docs: https://cloud.higgsfield.ai/
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import base64
15
+ import logging
16
+ import os
17
+ import time
18
+ import uuid
19
+ from typing import Any
20
+
21
+ import httpx
22
+
23
+ from content_engine.services.cloud_providers.base import CloudGenerationResult, CloudProvider
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Model IDs for Higgsfield platform
28
+ # Format: provider/model/task
29
+ TEXT_TO_IMAGE_MODELS = {
30
+ "seedream-4": "bytedance/seedream/v4/text-to-image",
31
+ "seedream-4.5": "bytedance/seedream/v4.5/text-to-image",
32
+ "nano-banana-pro": "google/nano-banana-pro/text-to-image",
33
+ "flux-2": "black-forest-labs/flux-2/text-to-image",
34
+ "gpt-image": "openai/gpt-image/text-to-image",
35
+ "default": "bytedance/seedream/v4/text-to-image",
36
+ }
37
+
38
+ TEXT_TO_VIDEO_MODELS = {
39
+ "kling-3.0": "kuaishou/kling/v3.0/text-to-video",
40
+ "kling-3.0-pro": "kuaishou/kling/v3.0-pro/text-to-video",
41
+ "sora-2": "openai/sora-2/text-to-video",
42
+ "veo-3.1": "google/veo-3.1/text-to-video",
43
+ "wan-2.5": "alibaba/wan-2.5/text-to-video",
44
+ "seedance-pro": "bytedance/seedance/pro/text-to-video",
45
+ "default": "kuaishou/kling/v3.0/text-to-video",
46
+ }
47
+
48
+ IMAGE_TO_VIDEO_MODELS = {
49
+ "kling-3.0": "kuaishou/kling/v3.0/image-to-video",
50
+ "kling-3.0-pro": "kuaishou/kling/v3.0-pro/image-to-video",
51
+ "sora-2": "openai/sora-2/image-to-video",
52
+ "veo-3.1": "google/veo-3.1/image-to-video",
53
+ "wan-2.5": "alibaba/wan-2.5/image-to-video",
54
+ "higgsfield-dop": "higgsfield/dop/image-to-video",
55
+ "default": "kuaishou/kling/v3.0/image-to-video",
56
+ }
57
+
58
+ IMAGE_EDIT_MODELS = {
59
+ "higgsfield-soul": "higgsfield/soul/image-to-image",
60
+ "seedream-4-edit": "bytedance/seedream/v4/edit",
61
+ "default": "higgsfield/soul/image-to-image",
62
+ }
63
+
64
+
65
+ class HiggsFieldProvider(CloudProvider):
66
+ """Cloud provider using Higgsfield.ai for Kling 3.0, Sora 2, Veo 3.1, etc."""
67
+
68
+ def __init__(self, api_key: str = None, api_secret: str = None):
69
+ """Initialize with Higgsfield credentials.
70
+
71
+ Can use either:
72
+ - Combined key (HF_KEY env var)
73
+ - Separate key/secret (HF_API_KEY/HF_API_SECRET env vars)
74
+ """
75
+ self._api_key = api_key or os.getenv("HIGGSFIELD_API_KEY") or os.getenv("HF_API_KEY")
76
+ self._api_secret = api_secret or os.getenv("HIGGSFIELD_API_SECRET") or os.getenv("HF_API_SECRET")
77
+ self._combined_key = os.getenv("HF_KEY")
78
+
79
+ self._http_client = httpx.AsyncClient(timeout=300)
80
+ self._client = None
81
+
82
+ # Try to initialize SDK if available
83
+ try:
84
+ from higgsfield_client import HiggsFieldClient
85
+ if self._combined_key:
86
+ self._client = HiggsFieldClient()
87
+ elif self._api_key and self._api_secret:
88
+ self._client = HiggsFieldClient(api_key=self._api_key, api_secret=self._api_secret)
89
+ logger.info("Higgsfield SDK initialized")
90
+ except ImportError:
91
+ logger.warning("higgsfield-client not installed, using direct API")
92
+ except Exception as e:
93
+ logger.warning("Failed to init Higgsfield SDK: %s", e)
94
+
95
+ @property
96
+ def name(self) -> str:
97
+ return "higgsfield"
98
+
99
+ async def is_available(self) -> bool:
100
+ """Check if Higgsfield API is configured."""
101
+ return bool(self._client or self._api_key)
102
+
103
+ def _resolve_t2i_model(self, model_name: str | None) -> str:
104
+ """Resolve friendly name to Higgsfield model ID for text-to-image."""
105
+ if model_name and model_name in TEXT_TO_IMAGE_MODELS:
106
+ return TEXT_TO_IMAGE_MODELS[model_name]
107
+ if model_name:
108
+ return model_name
109
+ return TEXT_TO_IMAGE_MODELS["default"]
110
+
111
+ def _resolve_t2v_model(self, model_name: str | None) -> str:
112
+ """Resolve friendly name to Higgsfield model ID for text-to-video."""
113
+ if model_name and model_name in TEXT_TO_VIDEO_MODELS:
114
+ return TEXT_TO_VIDEO_MODELS[model_name]
115
+ if model_name:
116
+ return model_name
117
+ return TEXT_TO_VIDEO_MODELS["default"]
118
+
119
+ def _resolve_i2v_model(self, model_name: str | None) -> str:
120
+ """Resolve friendly name to Higgsfield model ID for image-to-video."""
121
+ if model_name and model_name in IMAGE_TO_VIDEO_MODELS:
122
+ return IMAGE_TO_VIDEO_MODELS[model_name]
123
+ if model_name:
124
+ return model_name
125
+ return IMAGE_TO_VIDEO_MODELS["default"]
126
+
127
+ def _resolve_edit_model(self, model_name: str | None) -> str:
128
+ """Resolve friendly name to Higgsfield model ID for image editing."""
129
+ if model_name and model_name in IMAGE_EDIT_MODELS:
130
+ return IMAGE_EDIT_MODELS[model_name]
131
+ if model_name:
132
+ return model_name
133
+ return IMAGE_EDIT_MODELS["default"]
134
+
135
+ async def generate_image(
136
+ self,
137
+ prompt: str,
138
+ model: str | None = None,
139
+ resolution: str = "2K",
140
+ aspect_ratio: str = "16:9",
141
+ **kwargs,
142
+ ) -> CloudGenerationResult:
143
+ """Generate an image using Higgsfield text-to-image models."""
144
+ start = time.time()
145
+ model_id = self._resolve_t2i_model(model)
146
+
147
+ if self._client:
148
+ # Use SDK
149
+ try:
150
+ result = self._client.subscribe(
151
+ model_id,
152
+ {
153
+ "prompt": prompt,
154
+ "resolution": resolution,
155
+ "aspect_ratio": aspect_ratio,
156
+ **kwargs,
157
+ }
158
+ )
159
+
160
+ # Extract image URL
161
+ images = result.get("images", [])
162
+ if not images:
163
+ raise RuntimeError(f"No images in Higgsfield response: {result}")
164
+
165
+ image_url = images[0].get("url") if isinstance(images[0], dict) else images[0]
166
+
167
+ # Download image
168
+ resp = await self._http_client.get(image_url)
169
+ resp.raise_for_status()
170
+
171
+ return CloudGenerationResult(
172
+ job_id=str(uuid.uuid4()),
173
+ image_bytes=resp.content,
174
+ generation_time_seconds=time.time() - start,
175
+ )
176
+ except Exception as e:
177
+ logger.error("Higgsfield image generation failed: %s", e)
178
+ raise
179
+ else:
180
+ raise RuntimeError("Higgsfield SDK not initialized")
181
+
182
+ async def generate_video(
183
+ self,
184
+ prompt: str,
185
+ model: str | None = None,
186
+ duration: int = 5,
187
+ resolution: str = "720p",
188
+ aspect_ratio: str = "16:9",
189
+ enable_audio: bool = False,
190
+ image_url: str | None = None,
191
+ **kwargs,
192
+ ) -> dict:
193
+ """Generate a video using Higgsfield models (Kling 3.0, Sora 2, Veo 3.1, etc.).
194
+
195
+ Args:
196
+ prompt: Text description of desired video
197
+ model: Model to use (kling-3.0, sora-2, veo-3.1, etc.)
198
+ duration: Video duration in seconds (3-15 for Kling 3.0)
199
+ resolution: Output resolution (720p, 1080p)
200
+ aspect_ratio: Aspect ratio (16:9, 9:16, 1:1)
201
+ enable_audio: Enable audio generation (Kling 3.0 supports this)
202
+ image_url: Reference image URL for image-to-video
203
+
204
+ Returns:
205
+ Dict with job_id, status, and video_url when complete
206
+ """
207
+ start = time.time()
208
+
209
+ # Choose model based on whether we have an image
210
+ if image_url:
211
+ model_id = self._resolve_i2v_model(model)
212
+ else:
213
+ model_id = self._resolve_t2v_model(model)
214
+
215
+ if self._client:
216
+ try:
217
+ payload = {
218
+ "prompt": prompt,
219
+ "resolution": resolution,
220
+ "aspect_ratio": aspect_ratio,
221
+ "duration": duration,
222
+ }
223
+
224
+ if enable_audio:
225
+ payload["enable_audio"] = True
226
+
227
+ if image_url:
228
+ payload["image"] = image_url
229
+
230
+ payload.update(kwargs)
231
+
232
+ # Submit and wait for result
233
+ result = self._client.subscribe(model_id, payload)
234
+
235
+ # Extract video URL
236
+ video_url = None
237
+ if "video" in result:
238
+ video_url = result["video"]
239
+ elif "outputs" in result and result["outputs"]:
240
+ video_url = result["outputs"][0]
241
+ elif "output" in result:
242
+ video_url = result["output"]
243
+
244
+ if not video_url:
245
+ raise RuntimeError(f"No video URL in Higgsfield response: {result}")
246
+
247
+ return {
248
+ "job_id": str(uuid.uuid4()),
249
+ "status": "completed",
250
+ "video_url": video_url,
251
+ "generation_time": time.time() - start,
252
+ }
253
+
254
+ except Exception as e:
255
+ logger.error("Higgsfield video generation failed: %s", e)
256
+ raise
257
+ else:
258
+ raise RuntimeError("Higgsfield SDK not initialized")
259
+
260
+ async def submit_generation(
261
+ self,
262
+ positive_prompt: str,
263
+ negative_prompt: str = "",
264
+ model: str | None = None,
265
+ width: int = 1024,
266
+ height: int = 1024,
267
+ seed: int = -1,
268
+ **kwargs,
269
+ ) -> str:
270
+ """Submit an image generation job. Returns job ID."""
271
+ # For now, use synchronous generation
272
+ result = await self.generate_image(
273
+ prompt=positive_prompt,
274
+ model=model,
275
+ **kwargs,
276
+ )
277
+
278
+ # Cache result for get_result
279
+ self._last_result = {
280
+ "job_id": result.job_id,
281
+ "result": result,
282
+ "timestamp": time.time(),
283
+ }
284
+
285
+ return result.job_id
286
+
287
+ async def check_status(self, job_id: str) -> str:
288
+ """Check job status."""
289
+ if hasattr(self, '_last_result') and self._last_result.get("job_id") == job_id:
290
+ return "completed"
291
+ return "unknown"
292
+
293
+ async def get_result(self, job_id: str) -> CloudGenerationResult:
294
+ """Get the generation result."""
295
+ if not hasattr(self, '_last_result') or self._last_result.get("job_id") != job_id:
296
+ raise RuntimeError(f"No cached result for job {job_id}")
297
+ return self._last_result["result"]
298
+
299
+ async def generate(
300
+ self,
301
+ positive_prompt: str,
302
+ negative_prompt: str = "",
303
+ model: str | None = None,
304
+ width: int = 1024,
305
+ height: int = 1024,
306
+ seed: int = -1,
307
+ **kwargs,
308
+ ) -> CloudGenerationResult:
309
+ """Convenience method: generate image in one call."""
310
+ return await self.generate_image(
311
+ prompt=positive_prompt,
312
+ model=model,
313
+ **kwargs,
314
+ )