feat: 支持视频单图模式切换

This commit is contained in:
XianYuDaXian committed 2026-03-14 13:34:52 +08:00
1 parent 3f41749a7d
commit 94ee740c52
5 files changed
+107 -4

No files matched your search

+11
View File
@@ -102,6 +102,7 @@ async def _new_session(
source_image_url: Optional[str],
reference_items: Optional[List[Dict[str, str]]],
reasoning_effort: Optional[str],
single_image_mode: str = "frame",
# 视频延长相关
is_video_extension: bool = False,
extend_post_id: Optional[str] = None,
@@ -125,6 +126,7 @@ async def _new_session(
"source_image_url": source_image_url,
"reference_items": reference_items or [],
"reasoning_effort": reasoning_effort,
"single_image_mode": single_image_mode,
"is_video_extension": is_video_extension,
"extend_post_id": extend_post_id,
"video_extension_start_time": video_extension_start_time,
@@ -290,6 +292,7 @@ class VideoStartRequest(BaseModel):
video_length: Optional[int] = 6
resolution_name: Optional[str] = "480p"
preset: Optional[str] = "normal"
single_image_mode: Optional[str] = "frame"
concurrent: Optional[int] = Field(1, ge=1, le=4)
image_url: Optional[str] = None
parent_post_id: Optional[str] = None
@@ -343,6 +346,10 @@ async def public_video_start(data: VideoStartRequest):
if concurrent < 1 or concurrent > 4:
raise HTTPException(status_code=400, detail="concurrent must be between 1 and 4")
single_image_mode = str(data.single_image_mode or "frame").strip().lower()
if single_image_mode not in ("frame", "reference"):
raise HTTPException(status_code=400, detail="single_image_mode must be one of ['frame','reference']")
reference_items = _build_reference_items(data)
if len(reference_items) > 7:
raise HTTPException(status_code=400, detail="最多支持 7 张参考图")
@@ -435,6 +442,7 @@ async def public_video_start(data: VideoStartRequest):
source_image_url,
reference_items,
reasoning_effort,
single_image_mode=single_image_mode,
is_video_extension=is_video_extension,
extend_post_id=extend_post_id,
video_extension_start_time=video_extension_start_time,
@@ -453,6 +461,7 @@ async def public_video_start(data: VideoStartRequest):
"reference_count": len(reference_items),
"extend_post_id": extend_post_id,
"file_attachment_id": file_attachment_id or "",
"single_image_mode": single_image_mode,
}
@@ -472,6 +481,7 @@ async def public_video_sse(request: Request, task_id: str = Query("")):
source_image_url = str(session.get("source_image_url") or "").strip() or None
reference_items = session.get("reference_items") or []
reasoning_effort = session.get("reasoning_effort")
single_image_mode = str(session.get("single_image_mode") or "frame").strip() or "frame"
async def event_stream():
try:
@@ -524,6 +534,7 @@ async def public_video_sse(request: Request, task_id: str = Query("")):
stitch_with_extend=stitch_with_extend,
source_image_url=source_image_url,
reference_items=reference_items,
single_image_mode=single_image_mode,
)
async for chunk in stream:
+28 -3
View File
@@ -680,18 +680,31 @@ class VideoService:
) -> AsyncGenerator[bytes, None]:
"""Generate video from image."""
token_tag = _token_tag(token)
normalized_image_url = str(image_url or "").strip()
if normalized_image_url.startswith("data:"):
upload_service = UploadService()
try:
_, file_uri = await upload_service.upload_file(normalized_image_url, token)
normalized_image_url = f"https://assets.grok.com/{file_uri}"
logger.info(
"Image to video source uploaded before generation: "
f"token={token_tag}, asset_url={normalized_image_url}"
)
finally:
await upload_service.close()
# 确定逻辑上的 mode
is_custom = VideoService.is_meaningful_video_prompt(prompt)
official_mode = "custom" if is_custom else VideoService._map_preset_to_mode(preset)
logger.info(
f"Image to video: token={token_tag}, prompt='{prompt[:50]}...', image={image_url[:80]}, mode={official_mode}"
f"Image to video: token={token_tag}, prompt='{prompt[:50]}...', image={normalized_image_url[:80]}, mode={official_mode}"
)
post_id = await self.create_image_post(token, image_url)
post_id = await self.create_image_post(token, normalized_image_url)
message = self._build_video_message(
prompt=prompt,
preset=preset,
source_image_url=image_url,
source_image_url=normalized_image_url,
)
model_config_override = {
"modelMap": {
@@ -1070,6 +1083,7 @@ class VideoService:
stitch_with_extend: bool = True,
source_image_url: str | None = None,
reference_items: list[dict[str, Any]] | None = None,
single_image_mode: str = "frame",
):
"""Video generation entrypoint."""
# Get token via intelligent routing.
@@ -1190,6 +1204,7 @@ class VideoService:
item = reference_items[0]
single_parent_post_id = str(item.get("parent_post_id") or "").strip()
single_source_image_url = str(item.get("source_image_url") or item.get("image_url") or "").strip()
effective_single_image_mode = str(single_image_mode or "frame").strip().lower() or "frame"
if single_parent_post_id:
response = await service.generate_from_parent_post(
token=token,
@@ -1201,6 +1216,16 @@ class VideoService:
resolution=resolution,
preset=preset,
)
elif effective_single_image_mode == "reference":
response = await service.generate_from_reference_items(
token=token,
prompt=prompt,
reference_items=reference_items,
aspect_ratio=aspect_ratio,
video_length=video_length,
resolution=resolution,
preset=preset,
)
else:
response = await service.generate_from_image(
token,
+4 -1
View File
@@ -38,6 +38,7 @@
const resolutionSelect = document.getElementById('resolutionSelect');
const presetSelect = document.getElementById('presetSelect');
const concurrentSelect = document.getElementById('concurrentSelect');
const singleImageModeSelect = document.getElementById('singleImageModeSelect');
const statusText = document.getElementById('statusText');
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
@@ -1414,6 +1415,7 @@
video_length: lengthSelect ? parseInt(lengthSelect.value, 10) : 6,
resolution_name: resolutionSelect ? resolutionSelect.value : '480p',
preset: presetSelect ? presetSelect.value : 'normal',
single_image_mode: singleImageModeSelect ? singleImageModeSelect.value : 'frame',
concurrent: getConcurrentValue()
})
});
@@ -2103,6 +2105,7 @@
video_length: lengthSelect ? parseInt(lengthSelect.value, 10) : 6,
resolution_name: resolutionSelect ? resolutionSelect.value : '480p',
preset: presetSelect ? presetSelect.value : 'custom',
single_image_mode: 'frame',
concurrent,
n: concurrent,
edit_context: editCtx
@@ -3089,7 +3092,7 @@
hideReferenceMentionMenu();
});
[ratioSelect, lengthSelect, resolutionSelect, presetSelect, concurrentSelect]
[ratioSelect, lengthSelect, resolutionSelect, presetSelect, concurrentSelect, singleImageModeSelect]
.filter(Boolean)
.forEach((el) => {
el.addEventListener('change', updateMeta);
+7
View File
@@ -231,6 +231,13 @@
<option value="custom">Custom</option>
</select>
</div>
<div class="settings-block">
<label class="field-label" for="singleImageModeSelect">单图模式</label>
<select id="singleImageModeSelect" class="geist-input">
<option value="frame" selected>作为首帧</option>
<option value="reference">作为参考图</option>
</select>
</div>
<div class="settings-block">
<label class="field-label" for="concurrentSelect">并发视频数</label>
<select id="concurrentSelect" class="geist-input">
+57
View File
@@ -419,6 +419,63 @@ curl http://127.0.0.1:8000/v1/chat/completions \
}'
```
### 单图模式视频
`POST /v1/public/video/start`
单图时可通过 `single_image_mode` 指定这张图的用途:
- `frame`:作为首帧
- `reference`:作为参考图
单图作为首帧:
```bash
curl http://127.0.0.1:8000/v1/public/video/start \
-H "Content-Type: application/json" \
-H "x-public-key: YOUR_PUBLIC_KEY" \
-d '{
"prompt": "两人合唱",
"reference_items": [
{
"image_url": "data:image/png;base64,AAA...",
"source_image_url": "data:image/png;base64,AAA...",
"mention_alias": "Image 1"
}
],
"single_image_mode": "frame",
"aspect_ratio": "3:2",
"video_length": 6,
"resolution_name": "480p",
"preset": "normal",
"concurrent": 1
}'
```
单图作为参考图:
```bash
curl http://127.0.0.1:8000/v1/public/video/start \
-H "Content-Type: application/json" \
-H "x-public-key: YOUR_PUBLIC_KEY" \
-d '{
"prompt": "两人合唱",
"reference_items": [
{
"image_url": "data:image/png;base64,AAA...",
"source_image_url": "data:image/png;base64,AAA...",
"mention_alias": "Image 1"
}
],
"single_image_mode": "reference",
"aspect_ratio": "3:2",
"video_length": 6,
"resolution_name": "480p",
"preset": "normal",
"concurrent": 1
}'
```
### 图片编辑
`POST /v1/images/edits`