Commit ·
53706f2
1
Parent(s): e35e038
backend/api/gpt_image_frames.py
CHANGED
|
@@ -5,6 +5,7 @@ Feeds Veo a keyframe that matches 3–4 product reference photos.
|
|
| 5 |
|
| 6 |
from __future__ import annotations
|
| 7 |
|
|
|
|
| 8 |
import io
|
| 9 |
import os
|
| 10 |
from typing import Any, List
|
|
@@ -201,29 +202,19 @@ async def generate_segment_first_frame(body: SegmentFirstFrameRequest):
|
|
| 201 |
size = _aspect_to_size(body.aspect_ratio)
|
| 202 |
|
| 203 |
file_tuples: list[tuple[str, io.BytesIO, str]] = []
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
except ValueError as e:
|
| 218 |
-
raise HTTPException(
|
| 219 |
-
status_code=400,
|
| 220 |
-
detail=(
|
| 221 |
-
f"Invalid reference image {idx + 1} of {len(urls_in_order)} ({url[:160]}…): {e}"
|
| 222 |
-
),
|
| 223 |
-
) from None
|
| 224 |
-
for i, (raw, ctype) in enumerate(downloaded):
|
| 225 |
-
ext = "png" if "png" in ctype else "jpeg"
|
| 226 |
-
file_tuples.append((f"ref_{i}.{ext}", io.BytesIO(raw), ctype))
|
| 227 |
|
| 228 |
client = AsyncOpenAI(api_key=api_key)
|
| 229 |
result = None
|
|
|
|
| 5 |
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
+
import asyncio
|
| 9 |
import io
|
| 10 |
import os
|
| 11 |
from typing import Any, List
|
|
|
|
| 202 |
size = _aspect_to_size(body.aspect_ratio)
|
| 203 |
|
| 204 |
file_tuples: list[tuple[str, io.BytesIO, str]] = []
|
| 205 |
+
try:
|
| 206 |
+
urls_in_order = body.reference_image_urls[:4]
|
| 207 |
+
async with httpx.AsyncClient() as dl:
|
| 208 |
+
downloaded = await asyncio.gather(
|
| 209 |
+
*(_download_image(dl, url) for url in urls_in_order)
|
| 210 |
+
)
|
| 211 |
+
for i, (raw, ctype) in enumerate(downloaded):
|
| 212 |
+
ext = "png" if "png" in ctype else "jpeg"
|
| 213 |
+
file_tuples.append((f"ref_{i}.{ext}", io.BytesIO(raw), ctype))
|
| 214 |
+
except httpx.HTTPError as e:
|
| 215 |
+
raise HTTPException(status_code=502, detail=f"Could not download reference image: {e}")
|
| 216 |
+
except ValueError as e:
|
| 217 |
+
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
client = AsyncOpenAI(api_key=api_key)
|
| 220 |
result = None
|
backend/api/seedance_generation.py
CHANGED
|
@@ -129,8 +129,10 @@ def _replicate_input_from_seedance_urls(
|
|
| 129 |
}
|
| 130 |
if n == 1:
|
| 131 |
inp["image"] = mapped[0]
|
|
|
|
|
|
|
|
|
|
| 132 |
else:
|
| 133 |
-
# Multiple URLs = product reference angles, not first+last-frame (see _seedance_input_from_urls).
|
| 134 |
inp["reference_images"] = mapped[:9]
|
| 135 |
return inp
|
| 136 |
|
|
@@ -253,11 +255,9 @@ def _seedance_input_from_urls(
|
|
| 253 |
"""
|
| 254 |
Seedance modes are mutually exclusive (OpenAPI):
|
| 255 |
- 1 URL → image-to-video (first frame only): first_frame_url
|
| 256 |
-
- 2
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
multiple *product reference* angles (hero + gallery). That old 2-URL branch made
|
| 260 |
-
KIE treat the second image as an end frame and fail fetches (content[1].image_url).
|
| 261 |
"""
|
| 262 |
n = len(urls)
|
| 263 |
base = {
|
|
@@ -270,6 +270,9 @@ def _seedance_input_from_urls(
|
|
| 270 |
}
|
| 271 |
if n == 1:
|
| 272 |
base["first_frame_url"] = urls[0]
|
|
|
|
|
|
|
|
|
|
| 273 |
else:
|
| 274 |
base["reference_image_urls"] = urls[:9]
|
| 275 |
return base
|
|
@@ -357,7 +360,7 @@ def _extract_seedance_callback_state(payload: dict) -> dict:
|
|
| 357 |
|
| 358 |
@router.post("/seedance/create", response_model=SeedanceCreateResponse)
|
| 359 |
async def seedance_create(body: SeedanceCreateBody):
|
| 360 |
-
"""Start a Seedance 2 video task.
|
| 361 |
dur = _clamp_seedance_duration(body.duration)
|
| 362 |
|
| 363 |
allowed_ratio = {"16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"}
|
|
|
|
| 129 |
}
|
| 130 |
if n == 1:
|
| 131 |
inp["image"] = mapped[0]
|
| 132 |
+
elif n == 2:
|
| 133 |
+
inp["image"] = mapped[0]
|
| 134 |
+
inp["last_frame_image"] = mapped[1]
|
| 135 |
else:
|
|
|
|
| 136 |
inp["reference_images"] = mapped[:9]
|
| 137 |
return inp
|
| 138 |
|
|
|
|
| 255 |
"""
|
| 256 |
Seedance modes are mutually exclusive (OpenAPI):
|
| 257 |
- 1 URL → image-to-video (first frame only): first_frame_url
|
| 258 |
+
- 2 URLs → first & last frames: first_frame_url + last_frame_url
|
| 259 |
+
- 3+ URLs → multimodal reference-to-video: reference_image_urls only (max 9)
|
| 260 |
+
Do not mix first/last/reference in one request.
|
|
|
|
|
|
|
| 261 |
"""
|
| 262 |
n = len(urls)
|
| 263 |
base = {
|
|
|
|
| 270 |
}
|
| 271 |
if n == 1:
|
| 272 |
base["first_frame_url"] = urls[0]
|
| 273 |
+
elif n == 2:
|
| 274 |
+
base["first_frame_url"] = urls[0]
|
| 275 |
+
base["last_frame_url"] = urls[1]
|
| 276 |
else:
|
| 277 |
base["reference_image_urls"] = urls[:9]
|
| 278 |
return base
|
|
|
|
| 360 |
|
| 361 |
@router.post("/seedance/create", response_model=SeedanceCreateResponse)
|
| 362 |
async def seedance_create(body: SeedanceCreateBody):
|
| 363 |
+
"""Start a Seedance 2 video task. Maps 1–2 image URLs to first/last frame I2V; 3+ to reference_image_urls."""
|
| 364 |
dur = _clamp_seedance_duration(body.duration)
|
| 365 |
|
| 366 |
allowed_ratio = {"16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"}
|