auto-save 2026-05-14 07:39 (~4)
This commit is contained in:
@@ -1,19 +1,5 @@
|
|||||||
{
|
{
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "dd3d7b2",
|
|
||||||
"message": "auto-save 2026-05-12 21:38 (~1)",
|
|
||||||
"ts": "2026-05-12T21:38:30+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "d2d1e25",
|
|
||||||
"message": "auto-save 2026-05-12 21:44 (~1)",
|
|
||||||
"ts": "2026-05-12T21:44:22+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"files_changed": 1,
|
"files_changed": 1,
|
||||||
"hash": "f8cd466",
|
"hash": "f8cd466",
|
||||||
@@ -3336,6 +3322,19 @@
|
|||||||
"type": "session-heartbeat",
|
"type": "session-heartbeat",
|
||||||
"message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 07:28 (~6)",
|
"message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 07:28 (~6)",
|
||||||
"files_changed": 1
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-14T07:34:24+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-14 07:34 (~1)",
|
||||||
|
"hash": "19813b5",
|
||||||
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-13T23:38:52Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 07:34 (~1)",
|
||||||
|
"files_changed": 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
57
api/main.py
57
api/main.py
@@ -2540,6 +2540,10 @@ class GenerateStoryboardVideoReq(BaseModel):
|
|||||||
size: str = "720x1280"
|
size: str = "720x1280"
|
||||||
|
|
||||||
|
|
||||||
|
class ProductFusionDescriptionReq(BaseModel):
|
||||||
|
shots: list[ProductFusionShot] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
def video_seconds(duration: float) -> str:
|
def video_seconds(duration: float) -> str:
|
||||||
if video_uses_ark():
|
if video_uses_ark():
|
||||||
if duration <= 0:
|
if duration <= 0:
|
||||||
@@ -2997,6 +3001,59 @@ def create_product_fusion_guide(job_id: str, req: ProductFusionShot) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fallback_product_fusion_descriptions() -> list[str]:
|
||||||
|
return [
|
||||||
|
"人物双手拿起 SKG 颈部按摩仪,准备戴到脖子上,镜头轻微推近产品。",
|
||||||
|
"人物把 SKG 按摩仪贴合到肩颈位置,手部轻轻调整两侧机身角度。",
|
||||||
|
"人物坐在场景中轻按侧边控制区,产品保持在画框指定区域内清晰可见。",
|
||||||
|
"人物闭眼放松,肩颈从紧绷变舒展,产品佩戴位置稳定不漂移。",
|
||||||
|
"镜头靠近展示 SKG 产品材质、按键和内侧触点,手部不要遮挡产品主体。",
|
||||||
|
"使用后的放松状态收尾,人物自然抬头,产品仍保持白色 U 形外观和真实比例。",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/jobs/{job_id}/product-fusion/descriptions")
|
||||||
|
def generate_product_fusion_descriptions(job_id: str, req: ProductFusionDescriptionReq) -> dict:
|
||||||
|
if job_id not in JOBS:
|
||||||
|
raise HTTPException(404, "job not found")
|
||||||
|
fallback = fallback_product_fusion_descriptions()
|
||||||
|
shots = (req.shots or [])[:6]
|
||||||
|
if not LLM_API_KEY:
|
||||||
|
return {"descriptions": fallback, "mode": "fallback"}
|
||||||
|
shot_lines = []
|
||||||
|
for i, shot in enumerate(shots, start=1):
|
||||||
|
product = (shot.product_image or {}).get("label") or "SKG 产品图"
|
||||||
|
person = (shot.person_image or {}).get("label") or "白底人物姿态图"
|
||||||
|
scene = (shot.scene_image or {}).get("label") or "场景图"
|
||||||
|
region = shot.product_region
|
||||||
|
region_text = f"x={region.x:.2f}, y={region.y:.2f}, w={region.w:.2f}, h={region.h:.2f}" if region else "未画区域"
|
||||||
|
shot_lines.append(f"{i}. 产品={product};人物={person};区域={region_text};场景={scene};已有描述={shot.action_text or '空'}")
|
||||||
|
prompt = (
|
||||||
|
"你是 SKG 产品短视频分镜导演。请为 6 条产品融合镜头各写一条中文动作描述,"
|
||||||
|
"每条 20-40 字,必须说明人物在做什么、产品如何佩戴/展示、动作如何自然连续。"
|
||||||
|
"产品是 SKG 白色 U 形颈部/肩颈按摩仪,不要写医疗治疗承诺,不要出现竞品。"
|
||||||
|
"输出 JSON:{\"descriptions\":[\"...\", \"...\"]}。\n\n"
|
||||||
|
+ "\n".join(shot_lines)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
resp = llm().chat.completions.create(
|
||||||
|
model=REWRITE_MODEL,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "只输出合法 JSON,不要解释。"},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
temperature=0.5,
|
||||||
|
)
|
||||||
|
text = resp.choices[0].message.content or ""
|
||||||
|
data = json.loads(text)
|
||||||
|
descriptions = [str(x).strip() for x in data.get("descriptions", []) if str(x).strip()]
|
||||||
|
if len(descriptions) < 6:
|
||||||
|
descriptions = (descriptions + fallback)[:6]
|
||||||
|
return {"descriptions": descriptions[:6], "mode": "llm"}
|
||||||
|
except Exception:
|
||||||
|
return {"descriptions": fallback, "mode": "fallback"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/jobs/{job_id}/assets/{asset_id}.jpg")
|
@app.get("/jobs/{job_id}/assets/{asset_id}.jpg")
|
||||||
def get_storyboard_asset(job_id: str, asset_id: str):
|
def get_storyboard_asset(job_id: str, asset_id: str):
|
||||||
p = job_dir(job_id) / "assets" / f"{asset_id}.jpg"
|
p = job_dir(job_id) / "assets" / f"{asset_id}.jpg"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, Ref
|
|||||||
import {
|
import {
|
||||||
frameUrl, cleanedFrameUrl, apiAssetUrl,
|
frameUrl, cleanedFrameUrl, apiAssetUrl,
|
||||||
describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, updateElement, deleteElement,
|
describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, updateElement, deleteElement,
|
||||||
generateSceneAsset, generateSubjectAssets, resolveImageRefUrl, uploadStoryboardAsset, updateStoryboard,
|
generateSceneAsset, generateSubjectAssets, generateProductFusionDescriptions, resolveImageRefUrl, uploadStoryboardAsset, updateStoryboard,
|
||||||
type AssetBackground, type AssetSize, type KeyFrame, type Job, type ImageRef, type ProductFusionShot, type SceneMode, type SceneStyle, type SubjectKind,
|
type AssetBackground, type AssetSize, type KeyFrame, type Job, type ImageRef, type ProductFusionShot, type SceneMode, type SceneStyle, type SubjectKind,
|
||||||
} from "@/lib/api"
|
} from "@/lib/api"
|
||||||
import { ProductLibraryPicker } from "@/components/product-library-picker"
|
import { ProductLibraryPicker } from "@/components/product-library-picker"
|
||||||
@@ -347,7 +347,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
|||||||
fusionFileInputRef.current?.click()
|
fusionFileInputRef.current?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
const draftFusionDescriptions = () => {
|
const draftFusionDescriptions = async () => {
|
||||||
const actions = [
|
const actions = [
|
||||||
"人物双手拿起 SKG 颈部按摩仪,准备戴到脖子上,镜头轻微推近产品。",
|
"人物双手拿起 SKG 颈部按摩仪,准备戴到脖子上,镜头轻微推近产品。",
|
||||||
"人物把 SKG 按摩仪贴合到肩颈位置,手部轻轻调整两侧机身角度。",
|
"人物把 SKG 按摩仪贴合到肩颈位置,手部轻轻调整两侧机身角度。",
|
||||||
@@ -356,9 +356,16 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
|||||||
"镜头靠近展示 SKG 产品材质、按键和内侧触点,手部不要遮挡产品主体。",
|
"镜头靠近展示 SKG 产品材质、按键和内侧触点,手部不要遮挡产品主体。",
|
||||||
"使用后的放松状态收尾,人物自然抬头,产品仍保持白色 U 形外观和真实比例。",
|
"使用后的放松状态收尾,人物自然抬头,产品仍保持白色 U 形外观和真实比例。",
|
||||||
]
|
]
|
||||||
|
let descriptions = actions
|
||||||
|
try {
|
||||||
|
const result = await generateProductFusionDescriptions(jobId, fusionShots)
|
||||||
|
descriptions = result.descriptions.length ? result.descriptions : actions
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("AI 描述生成失败,已使用本地草稿")
|
||||||
|
}
|
||||||
const next = fusionShots.map((shot, i) => ({
|
const next = fusionShots.map((shot, i) => ({
|
||||||
...shot,
|
...shot,
|
||||||
action_text: shot.action_text?.trim() || actions[i],
|
action_text: shot.action_text?.trim() || descriptions[i] || actions[i],
|
||||||
}))
|
}))
|
||||||
setFusionShots(next)
|
setFusionShots(next)
|
||||||
void persistFusionShots(next)
|
void persistFusionShots(next)
|
||||||
|
|||||||
@@ -177,6 +177,22 @@ export async function createProductFusionGuide(
|
|||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateProductFusionDescriptions(
|
||||||
|
jobId: string,
|
||||||
|
shots: ProductFusionShot[],
|
||||||
|
): Promise<{ descriptions: string[]; mode: "llm" | "fallback" }> {
|
||||||
|
const res = await fetch(`${API_BASE}/jobs/${jobId}/product-fusion/descriptions`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ shots }),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`generateProductFusionDescriptions ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
export interface KeyFrame {
|
export interface KeyFrame {
|
||||||
index: number
|
index: number
|
||||||
timestamp: number
|
timestamp: number
|
||||||
|
|||||||
Reference in New Issue
Block a user