From 73e8ffecc6470a519aec5f670a447214c0b1aeee Mon Sep 17 00:00:00 2001 From: kang Date: Mon, 18 May 2026 21:03:11 +0800 Subject: [PATCH] auto-save 2026-05-18 21:03 (+1, ~3) --- .memory/worklog.json | 27 +- api/main.py | 43 ++ web/components/ad-recreation-board.tsx | 111 ++- .../resource-library/library-drawer.tsx | 678 ++++++++++++++++++ 4 files changed, 839 insertions(+), 20 deletions(-) create mode 100644 web/components/resource-library/library-drawer.tsx diff --git a/.memory/worklog.json b/.memory/worklog.json index ef30469..934f2d3 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,19 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "59687a5", - "message": "auto-save 2026-05-16 15:37 (~1)", - "ts": "2026-05-16T15:37:58+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "3ee9dc8", - "message": "auto-save 2026-05-16 15:43 (~1)", - "ts": "2026-05-16T15:43:44+08:00", - "type": "commit" - }, { "files_changed": 1, "hash": "258bc10", @@ -3213,6 +3199,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:auto-save 2026-05-18 20:51 (~2)", "files_changed": 2 + }, + { + "ts": "2026-05-18T20:57:23+08:00", + "type": "commit", + "message": "auto-save 2026-05-18 20:57 (~3)", + "hash": "32620af", + "files_changed": 3 + }, + { + "ts": "2026-05-18T13:02:20Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 3 项未提交变更 · 最近提交:auto-save 2026-05-18 20:57 (~3)", + "files_changed": 3 } ] } diff --git a/api/main.py b/api/main.py index c3d0530..9bb3869 100644 --- a/api/main.py +++ b/api/main.py @@ -6056,6 +6056,49 @@ def save_subject_template(job_id: str, req: SaveSubjectTemplateReq) -> SubjectTe ) items = [item] + [existing for existing in load_subject_template_items() if existing.id != item.id] save_subject_template_items(items) + try: + library_id = f"lib_subjects_{uuid.uuid4().hex[:12]}" + library_dir = _asset_library_item_dir("subjects", library_id) + library_images: list[AssetLibraryImage] = [] + for image in images: + src = SUBJECT_TEMPLATE_IMAGE_DIR / image.filename + if not src.exists(): + continue + view = re.sub(r"[^a-zA-Z0-9_-]+", "_", image.view or image.id).strip("_") or image.id + dst = library_dir / "images" / f"{view}.jpg" + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + width, height = _library_media_size(dst) + library_images.append(AssetLibraryImage( + id=view, + view=image.view, + label=image.label or image.view, + filename=f"images/{view}.jpg", + width=width or image.width, + height=height or image.height, + created_at=image.created_at or now, + )) + if library_images: + library_item = AssetLibraryItem( + id=library_id, + kind="subjects", + name=name, + name_zh=name, + note=req.note.strip(), + tags=["主体模板"], + source_job_id=job_id, + prompt_brief=prompt_brief, + prompt_brief_zh=prompt_brief_zh, + subject_style=req.subject_style, + images=library_images, + views=library_images, + created_at=now, + updated_at=now, + ) + _hydrate_asset_library_urls(library_item) + _write_asset_item(library_item) + except Exception as e: + print(f"[asset library] subject template mirror failed: {e}", flush=True) return item diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index 326239b..2288307 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -3,7 +3,7 @@ import { type ReactNode, type RefObject, useEffect, useMemo, useRef, useState } from "react" import { createPortal } from "react-dom" import { - AlertTriangle, Check, ChevronDown, Circle, Film, FileText, Image as ImageIcon, Info, Link2, Loader2, + AlertTriangle, BookOpen, Check, ChevronDown, Circle, Film, FileText, Image as ImageIcon, Info, Link2, Loader2, Mic, Moon, Package, PanelRight, Play, Plus, RefreshCw, Scissors, Sparkles, Sun, Trash2, Upload, Wand2, } from "lucide-react" import { toast } from "sonner" @@ -13,6 +13,8 @@ import { type FrameObject, type GeneratedVideo, type ImageRef, + type AssetLibraryItem, + type AssetLibraryKind, type CharacterLibraryItem, type SubjectTemplateItem, type Job, @@ -31,6 +33,8 @@ import { analyzeProductViews, apiAssetUrl, characterLibraryImageUrl, + createAssetLibraryItem, + createPromptLibraryItem, cutoutElement, deleteSubjectAsset, effectiveFrameUrl, @@ -60,6 +64,7 @@ import { import { type NodeData } from "@/components/nodes" import { MediaAssetTile } from "@/components/media-asset-tile" import { AnimatedLoginCharacters } from "@/components/login/animated-login-characters" +import { LibraryDrawer } from "@/components/resource-library/library-drawer" const TARGETS: Array<{ value: FrameExtractTarget; label: string }> = [ { value: "balanced", label: "综合" }, @@ -1705,6 +1710,7 @@ export function AdRecreationBoard({ const [generatingAll, setGeneratingAll] = useState(false) const [runtimeModels, setRuntimeModels] = useState() const [boardTheme, setBoardTheme] = useState("dark") + const [libraryOpen, setLibraryOpen] = useState(false) const fileRef = useRef(null) const selectedFrames = job ? job.frames.filter((frame) => data.selectedFrames.has(frame.index)).sort((a, b) => a.timestamp - b.timestamp) @@ -1806,6 +1812,26 @@ export function AdRecreationBoard({ } } + const applyLibraryAsset = async ( + kind: AssetLibraryKind, + ref: ImageRef, + target: "copy_only" | "product_pool", + item: AssetLibraryItem, + ) => { + if (!job) return + if (target === "product_pool" && kind === "products") { + const existing = job.product_refs ?? [] + const next = [ + ...existing, + createProductRefItem(ref, existing.length, "library", "front", item.note || item.name || "素材库产品图"), + ] + const updated = await saveProductRefs(job.id, next) + data.onJobUpdate(updated) + return + } + toast.success("素材已复制到当前 job;需要入产品池时请选择“应用到产品素材池”。") + } + const addDraftSegment = () => { setDraftSegments((prev) => [ ...prev, @@ -1950,6 +1976,15 @@ export function AdRecreationBoard({
+
+ setLibraryOpen(false)} + onApplyAsset={applyLibraryAsset} + /> ) } @@ -4123,6 +4164,27 @@ function ProductReferenceCard({ const assetWarnings = item.assetMeta?.warnings ?? [] const assetActions = item.assetMeta?.actions ?? [] const orientationText = formatProductOrientation(item.orientation) + const saveProductToLibrary = async () => { + if (!src) return + try { + const response = await fetch(src) + if (!response.ok) throw new Error(`fetch ${response.status}`) + const blob = await response.blob() + const file = new File([blob], `${item.view || "product"}.jpg`, { type: blob.type || "image/jpeg" }) + await createAssetLibraryItem("products", { + name: item.note || productViewLabel(item.view), + name_zh: item.note || productViewLabel(item.view), + note: item.note, + tags: ["产品素材池", productViewLabel(item.view)], + source_job_id: job.id, + product_type: "neck_and_shoulder_massager", + views: [item.view], + }, [file]) + toast.success("产品图已保存到素材库") + } catch (error) { + toast.error("保存到素材库失败:" + (error instanceof Error ? error.message : String(error))) + } + } const previewDetail = ( <> {productViewLabel(item.view)} · {productBackgroundLabel(item.background)} · {tagLabels.join(" / ") || "用途待标注"} @@ -4183,6 +4245,15 @@ function ProductReferenceCard({ > + ) } @@ -4292,15 +4363,43 @@ function EndpointFrameSlot({ const ref = endpointAssetRef(frame, role) const src = ref ? resolveImageRefUrl(job.id, ref) : "" const label = role === "first_frame" ? "首帧" : "尾帧" + const saveEndpointFrameToLibrary = async () => { + if (!src) return + try { + const response = await fetch(src) + if (!response.ok) throw new Error(`fetch ${response.status}`) + const blob = await response.blob() + const file = new File([blob], `${role}.jpg`, { type: blob.type || "image/jpeg" }) + await createAssetLibraryItem("scenes", { + name: `${label} · ${shortId(job.id)}`, + name_zh: `${label} · ${shortId(job.id)}`, + note: subjectBrief || `${label}首尾帧资产`, + tags: [label, "首尾帧"], + source_job_id: job.id, + asset_role: role, + aspect_ratio: "9:16", + }, [file]) + toast.success(`${label}已保存到素材库`) + } catch (error) { + toast.error("保存首尾帧到素材库失败:" + (error instanceof Error ? error.message : String(error))) + } + } return (
{label} - - + + {src ? ( + + ) : null} + + +
void + onApplyAsset?: (kind: AssetLibraryKind, ref: ImageRef, target: LibraryApplyTarget, item: AssetLibraryItem) => Promise | void +} + +const DRAWER_STORAGE_KEY = "skg-resource-library-drawer" +const PROMPT_COLUMNS: Array<{ category: PromptLibraryCategory; label: string; desc: string }> = [ + { category: "scene_desc", label: "场景描述", desc: "首尾帧、场景图、环境描述" }, + { category: "video_desc", label: "视频描述", desc: "视频生成动作、镜头语言" }, + { category: "subject_desc", label: "主体描述", desc: "人物、透明骨架、角色 brief" }, + { category: "skg_script", label: "SKG 文案", desc: "口播、卖点、作者意图" }, + { category: "product_angle", label: "产品角度", desc: "视角、佩戴、结构约束" }, +] +const ASSET_COLUMNS: Array<{ kind: AssetLibraryKind; label: string; icon: ReactNode }> = [ + { kind: "subjects", label: "主体", icon: }, + { kind: "products", label: "产品", icon: }, + { kind: "scenes", label: "场景", icon: }, + { kind: "videos", label: "视频", icon: