auto-save 2026-05-17 21:36 (~4)
This commit is contained in:
@@ -1,24 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 1,
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 12:53 (~1)",
|
||||
"ts": "2026-05-15T04:54:45Z",
|
||||
"type": "session-heartbeat"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "1c75b08",
|
||||
"message": "auto-save 2026-05-15 12:59 (~1)",
|
||||
"ts": "2026-05-15T12:59:28+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 12:59 (~1)",
|
||||
"ts": "2026-05-15T05:04:45Z",
|
||||
"type": "session-heartbeat"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "4df2601",
|
||||
@@ -3261,6 +3242,25 @@
|
||||
"message": "auto-save 2026-05-17 21:09 (~4)",
|
||||
"hash": "252cdf4",
|
||||
"files_changed": 4
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-17T21:14:42+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-17 21:14 (~3)",
|
||||
"hash": "ab2d0a8",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-17T13:18:29Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-17 21:14 (~3)",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-17T13:28:29Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-17 21:14 (~3)",
|
||||
"files_changed": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
18
api/main.py
18
api/main.py
@@ -481,6 +481,7 @@ class Job(BaseModel):
|
||||
audio_script: AudioScript = Field(default_factory=AudioScript)
|
||||
storyboard_images: list[StoryboardImage] = Field(default_factory=list)
|
||||
generated_videos: list[GeneratedVideo] = Field(default_factory=list)
|
||||
product_refs: list[dict] = Field(default_factory=list)
|
||||
error: str = ""
|
||||
|
||||
|
||||
@@ -3453,6 +3454,23 @@ class GenerateSubjectAssetsReq(BaseModel):
|
||||
prompt: str = ""
|
||||
|
||||
|
||||
class UpdateProductRefsReq(BaseModel):
|
||||
items: list[dict] = Field(default_factory=list)
|
||||
|
||||
|
||||
@app.put("/jobs/{job_id}/product-refs", response_model=Job)
|
||||
def update_product_refs(job_id: str, req: UpdateProductRefsReq) -> Job:
|
||||
job = JOBS.get(job_id)
|
||||
if not job:
|
||||
raise HTTPException(404, "job not found")
|
||||
items: list[dict] = []
|
||||
for item in req.items[:300]:
|
||||
if isinstance(item, dict) and isinstance(item.get("ref"), dict):
|
||||
items.append(item)
|
||||
update(job, product_refs=items)
|
||||
return job
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/frames/{idx}/elements", response_model=Job)
|
||||
def add_element(job_id: str, idx: int, req: AddElementReq) -> Job:
|
||||
"""加一条元素 · 若 name_en 缺则自动 zh→en 翻译"""
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type KeyElement,
|
||||
type KeyFrame,
|
||||
type ProductViewAnalysisItem,
|
||||
type ProductRefStateItem,
|
||||
type StoryboardScriptRewriteSegment,
|
||||
type StoryboardScene,
|
||||
type SubjectAsset,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
representativeCutoutUrl,
|
||||
resolveImageRefUrl,
|
||||
rewriteStoryboardScript,
|
||||
saveProductRefs,
|
||||
sourceAudioUrl,
|
||||
updateStoryboard,
|
||||
uploadStoryboardAsset,
|
||||
@@ -90,20 +92,7 @@ type AudioStoryboardRow = {
|
||||
productIntegration: string
|
||||
}
|
||||
|
||||
type ProductRefItem = {
|
||||
id: string
|
||||
ref: ImageRef
|
||||
view: string
|
||||
background: string
|
||||
useTags: string[]
|
||||
orientation?: ProductViewAnalysisItem["orientation"]
|
||||
landmarks: string[]
|
||||
note: string
|
||||
risk: string
|
||||
source: "upload" | "ai"
|
||||
assetMeta?: ImageRef["asset_meta"]
|
||||
confidence?: number
|
||||
}
|
||||
type ProductRefItem = ProductRefStateItem
|
||||
|
||||
const PRODUCT_VIEW_SLOTS = [
|
||||
{ value: "front", label: "正面/外侧", hint: "整体 U 形轮廓、开口宽度、外壳主外观" },
|
||||
@@ -471,6 +460,28 @@ function createProductRefItem(
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStoredProductItem(item: ProductRefItem, index: number): ProductRefItem {
|
||||
const ref = { ...item.ref, asset_meta: item.ref.asset_meta ?? item.assetMeta }
|
||||
const restored = createProductRefItem(
|
||||
ref,
|
||||
index,
|
||||
item.source ?? "upload",
|
||||
item.view,
|
||||
item.note,
|
||||
item.background ?? "unknown",
|
||||
item.useTags,
|
||||
item.orientation,
|
||||
item.landmarks,
|
||||
item.risk ?? "",
|
||||
item.confidence,
|
||||
)
|
||||
return {
|
||||
...restored,
|
||||
id: item.id || restored.id,
|
||||
assetMeta: item.assetMeta ?? restored.assetMeta,
|
||||
}
|
||||
}
|
||||
|
||||
function productReferenceNotes(items: ProductRefItem[]) {
|
||||
if (!items.length) return ""
|
||||
return items
|
||||
@@ -1399,6 +1410,7 @@ function AudioStoryboardPlanPanel({
|
||||
const [authorIntent, setAuthorIntent] = useState("")
|
||||
const [scriptRewriteBusy, setScriptRewriteBusy] = useState<"all" | number | null>(null)
|
||||
const productFileRef = useRef<HTMLInputElement | null>(null)
|
||||
const productPersistSeq = useRef(0)
|
||||
const rows = useMemo(() => buildAudioStoryboardRows(job), [job])
|
||||
const orderedFrames = useMemo(() => job ? [...job.frames].sort((a, b) => a.timestamp - b.timestamp) : [], [job])
|
||||
const selectedReferenceFrames = useMemo(
|
||||
@@ -1408,12 +1420,28 @@ function AudioStoryboardPlanPanel({
|
||||
const rowReferencePool = selectedReferenceFrames.length ? selectedReferenceFrames : orderedFrames
|
||||
|
||||
useEffect(() => {
|
||||
setProductItems([])
|
||||
setProductItems((job?.product_refs ?? []).map(normalizeStoredProductItem))
|
||||
setCopyOverrides({})
|
||||
setAuthorIntent("")
|
||||
setScriptRewriteBusy(null)
|
||||
}, [job?.id])
|
||||
|
||||
const persistProductItems = async (items: ProductRefItem[]) => {
|
||||
if (!job) return
|
||||
const seq = ++productPersistSeq.current
|
||||
try {
|
||||
const updated = await saveProductRefs(job.id, items)
|
||||
if (seq === productPersistSeq.current) onJobUpdate?.(updated)
|
||||
} catch (e) {
|
||||
console.warn("产品素材池保存失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
const setAndPersistProductItems = (items: ProductRefItem[]) => {
|
||||
setProductItems(items)
|
||||
void persistProductItems(items)
|
||||
}
|
||||
|
||||
const copyForRow = (row: AudioStoryboardRow) => copyOverrides[row.index] ?? row.skgCopy
|
||||
|
||||
const patchRowCopy = (rowIndex: number, value: string) => {
|
||||
|
||||
@@ -244,6 +244,19 @@ export async function analyzeProductViews(
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function saveProductRefs(jobId: string, items: ProductRefStateItem[]): Promise<Job> {
|
||||
const res = await fetch(`${API_BASE}/jobs/${jobId}/product-refs`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => "")
|
||||
throw new Error(`saveProductRefs ${res.status} ${txt.slice(0, 300)}`)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function listProductLibrary(): Promise<ProductLibraryItem[]> {
|
||||
const res = await fetch(`${API_BASE}/product-library/skg`)
|
||||
if (!res.ok) {
|
||||
@@ -476,6 +489,21 @@ export interface StoryboardImage {
|
||||
created_at?: number
|
||||
}
|
||||
|
||||
export interface ProductRefStateItem {
|
||||
id: string
|
||||
ref: ImageRef
|
||||
view: string
|
||||
background: string
|
||||
useTags: string[]
|
||||
orientation?: ProductViewAnalysisItem["orientation"]
|
||||
landmarks: string[]
|
||||
note: string
|
||||
risk: string
|
||||
source: "upload" | "ai"
|
||||
assetMeta?: ImageRef["asset_meta"]
|
||||
confidence?: number
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: string
|
||||
url: string
|
||||
@@ -492,6 +520,7 @@ export interface Job {
|
||||
audio_script?: AudioScript
|
||||
storyboard_images?: StoryboardImage[]
|
||||
generated_videos?: GeneratedVideo[]
|
||||
product_refs?: ProductRefStateItem[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user