diff --git a/api/main.py b/api/main.py index 1292643..3d39c96 100644 --- a/api/main.py +++ b/api/main.py @@ -5547,7 +5547,7 @@ def _subject_assets_background_worker( req: GenerateSubjectAssetsReq, queued: list[tuple[SubjectView, str, str]], ) -> None: - if req.reconstruction_mode == "similar" and not req.source_subject_brief.strip(): + if not req.source_subject_brief.strip() and _subject_source_indices(req, idx): try: req.source_subject_brief = _describe_source_subject(job_id, _subject_source_indices(req, idx)) except Exception as e: @@ -5738,21 +5738,35 @@ def _generate_subject_assets_sync(job_id: str, idx: int, element_id: str, req: G frame_reference_paths = [p for p in (_source_frame_path(job_id, i) for i in source_indices) if p.exists()] source_subject_brief = ( _ensure_english(req.source_subject_brief.strip()) - if similar_mode and req.source_subject_brief.strip() - else (_describe_source_subject(job_id, source_indices) if similar_mode else "") + if req.source_subject_brief.strip() + else (_describe_source_subject(job_id, source_indices) if source_indices else "") ) source_subject_clause = ( f"Source video role brief from selected keyframes: {source_subject_brief}. " - "Use this brief to preserve role category, creator-ad energy, camera readability, and broad styling, while creating a new non-identical subject. " + + ( + "Use this brief as secondary text evidence while preserving the same visible source subject from the attached reference image(s). " + if req.reconstruction_mode == "same" + else "Use this brief to preserve role category, creator-ad energy, camera readability, and broad styling, while creating a new non-identical subject. " + ) if source_subject_brief else - "Source video role brief unavailable; create a new non-identical ad subject guided by the user direction, template brief, and requested view. " + ( + "Source video role brief unavailable; use the attached source reference image(s) as primary evidence for the same visible subject. " + if req.reconstruction_mode == "same" + else "Source video role brief unavailable; create a new non-identical ad subject guided by the user direction, template brief, and requested view. " + ) ) - if not similar_mode: + if similar_mode: + if character_reference_paths: + remaining = max(0, 10 - len(character_reference_paths)) + model_src = character_reference_paths + frame_reference_paths[:remaining] + elif frame_reference_paths: + model_src = frame_reference_paths[:10] + else: model_src, tmp_focus = _focus_source_for_element(job_id, idx, el) if character_reference_paths: remaining = max(0, 10 - len(character_reference_paths)) model_src = character_reference_paths + frame_reference_paths[:remaining] - elif len(frame_reference_paths) > 1: + elif frame_reference_paths: model_src = frame_reference_paths[:10] try: @@ -5823,14 +5837,25 @@ def _generate_subject_assets_sync(job_id: str, idx: int, element_id: str, req: G "If the reference outfit is useful, inherit its broad wardrobe category and color family, but redraw it as a new non-identical clean commercial outfit. " ) pack_bible_clause = ( - "PACK BIBLE - this exact bible applies to every view in this generated set. " - "Subject bible: one newly designed commercial wellness-ad subject; inherit only broad non-identifying casting traits from the source such as gender presentation, regional/ethnic appearance category, skin-tone family, age range, body-proportion category, hair-length family, posture energy, and neck/shoulder readability. " - "Do not copy the source person's biometric identity, exact face, exact hairstyle, marks, tattoos, captions, logos, or watermarks. " - "Keep the same new face design, same head shape, same hair color and hair silhouette, same skin tone, same body proportions, same height impression, and same character age across front, side, three-quarter, and back views. " - "Wardrobe bible: if the user direction names a specific outfit, use that one outfit uniformly across every view. Otherwise use one clean SKG wellness-ad activewear outfit for the entire pack: fitted short-sleeve performance top with a visible neck/collarbone area, slim athletic pants, and low-profile sneakers. " - "Lock the exact top color, bottom color, shoe color, neckline shape, sleeve/strap structure, seams, trim, fabric finish, fit, and accessories before rendering the first view, then repeat those same clothing decisions in every other view. " - "Never add or remove a jacket, blazer, hoodie, coat, dress, skirt, scarf, hat, bag, jewelry, logo, stripe pattern, or extra layer in only one view. " - "Back and side views must show the same garment wrapping around the same body, not a redesigned outfit. " + ( + "PACK BIBLE - source-locked mode. " + "Subject bible: use the attached source frame(s) as the primary identity and wardrobe reference for one same visible subject. " + "Preserve the visible gender presentation, regional/ethnic appearance category, skin-tone family, age range impression, body-proportion category, hair length/color/silhouette, face-structure impression, posture energy, neck/shoulder readability, outfit category, garment colors, material finish, and accessory logic across every generated view. " + "Do not replace the source subject with a different actor, different body type, different ethnicity, different gender, different hairstyle, different outfit, or generic wellness model. " + "Remove only source-video artifacts such as background, captions, watermarks, platform UI, compression noise, and accidental occlusion; redraw missing angles as the same subject. " + "Lock the exact top color, bottom color, shoe color, neckline shape, sleeve/strap structure, seams, trim, fabric finish, fit, and accessories before rendering the first view, then repeat those same clothing decisions in every other view. " + ) + if req.reconstruction_mode == "same" else + ( + "PACK BIBLE - this exact bible applies to every view in this generated set. " + "Subject bible: one newly designed commercial wellness-ad subject; inherit only broad non-identifying casting traits from the source such as gender presentation, regional/ethnic appearance category, skin-tone family, age range, body-proportion category, hair-length family, posture energy, and neck/shoulder readability. " + "Do not copy the source person's biometric identity, exact face, exact hairstyle, marks, tattoos, captions, logos, or watermarks. " + "Keep the same new face design, same head shape, same hair color and hair silhouette, same skin tone, same body proportions, same height impression, and same character age across front, side, three-quarter, and back views. " + "Wardrobe bible: if the user direction names a specific outfit, use that one outfit uniformly across every view. Otherwise use one clean SKG wellness-ad activewear outfit for the entire pack: fitted short-sleeve performance top with a visible neck/collarbone area, slim athletic pants, and low-profile sneakers. " + "Lock the exact top color, bottom color, shoe color, neckline shape, sleeve/strap structure, seams, trim, fabric finish, fit, and accessories before rendering the first view, then repeat those same clothing decisions in every other view. " + "Never add or remove a jacket, blazer, hoodie, coat, dress, skirt, scarf, hat, bag, jewelry, logo, stripe pattern, or extra layer in only one view. " + "Back and side views must show the same garment wrapping around the same body, not a redesigned outfit. " + ) ) neck_product_clause = ( "This subject pack is for SKG neck-and-shoulder wearable massage device videos. " @@ -5840,6 +5865,7 @@ def _generate_subject_assets_sync(job_id: str, idx: int, element_id: str, req: G ) models = SUBJECT_ASSET_IMAGE_MODELS model_preference = _normalize_image_model_preference(req.image_model_preference) + reference_image_count = len(model_src) if isinstance(model_src, list) else (1 if model_src else 0) generated: list[SubjectAsset] = [] generation_errors: list[str] = [] first_generation_error: RuntimeError | None = None @@ -5872,14 +5898,30 @@ def _generate_subject_assets_sync(job_id: str, idx: int, element_id: str, req: G if closeup_view and req.subject_kind == "living" else "The subject must be complete, centered, full body or full object, head-to-feet visible when applicable, not cropped by the canvas. Make the subject large and readable: it should occupy about 88-94% of the image height, with the head close to the top margin and feet close to the bottom margin. No tiny character, no miniature person, no distant full-body figure, no large empty white margins. " ) - reference_strategy_clause = ( - "Text-only generation mode: no source image is attached to this image request. Use only the written source/video/template briefs below as creative constraints. " - "This is intentionally NOT image editing and NOT identity replication. " - + source_subject_clause - + template_brief_clause - if similar_mode else - "Use the reference image(s) only as visual evidence; do not crop, cut out, paste, trace, or extract pixels from the source. " - ) + if similar_mode and reference_image_count: + reference_strategy_clause = ( + f"Image-conditioned reference reconstruction mode: {reference_image_count} selected source reference image(s) are attached to this request. " + "First read the attached frames and the written source brief, then generate a new similar but non-identical subject. " + "Use the images as visual evidence for broad role, gender presentation, regional/ethnic appearance category, skin-tone family, body proportion, hair family, outfit category/color family, pose language, and creator-ad energy. " + "Do not copy exact face, biometric identity, unique marks, source pixels, captions, watermarks, or background. " + + source_subject_clause + + template_brief_clause + ) + elif similar_mode: + reference_strategy_clause = ( + "Text-only generation mode: no source image is attached to this image request. Use only the written source/video/template briefs below as creative constraints. " + "This is intentionally NOT image editing and NOT identity replication. " + + source_subject_clause + + template_brief_clause + ) + else: + reference_strategy_clause = ( + f"Source-locked image reference mode: {reference_image_count} selected source reference image(s) are attached and are the primary visual evidence. " + "Preserve the visible source subject's identity impression, proportions, silhouette, material, colors, wardrobe, styling, and distinctive non-artifact details across all generated views. " + "Do not crop, cut out, paste, trace, or extract pixels from the source; redraw a clean production-ready asset of the same visible subject. " + + source_subject_clause + + template_brief_clause + ) prompt = ( reference_strategy_clause + @@ -5904,7 +5946,15 @@ def _generate_subject_assets_sync(job_id: str, idx: int, element_id: str, req: G + transparent_character_clause ) try: - if similar_mode: + if similar_mode and model_src is not None: + print( + f"[subject assets] reconstruction_mode=similar endpoint=/images/edits view={view} image_refs={reference_image_count} model_preference={model_preference}", + flush=True, + ) + img_bytes, _mode = _image_edit_call(model_src, prompt, models=models, fallback_text=False, max_attempts=3, max_side=1280, force_fallback_model=pack_force_fallback_model, image_model_preference=model_preference) + if model_preference == "auto" and _mode.endswith(f":{IMAGE_FALLBACK_MODEL}"): + pack_force_fallback_model = True + elif similar_mode: print( f"[subject assets] reconstruction_mode=similar endpoint=/images/generations view={view} image_refs=0 model_preference={model_preference}", flush=True, diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 36a3e47..89f3181 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -569,13 +569,13 @@

业务管线

-

当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧是信息流复刻工作表。后台仍按 01-09 流程顺序计算素材任务、源视频、音频文案、抽帧、主体资产、产品资产、分镜文案、三字段规划和视频候选这些状态,但这些判断不再默认显现在工作区顶部,避免状态提示挤占首屏操作空间。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动音频文案路和视频视觉路。音频文案路提取原音频文案/字幕,分析讲话人、语速节奏、背景音乐/环境声/音效,并为后续新口播和分镜文案提供时间轴;视频视觉路同步抽取参考帧。源视频工作区右侧主体链路是“参考帧池 → 转换层 → 主体元素”:参考帧池只作为竖向原始参考,转换层收敛为真人重构、卡通重构、元素重构、自主描述四个入口,每个入口最多接收 3 张参考帧;拖入只加入对应入口的参考队列,不自动生成,用户放好参考和文字后再点击生成全新 6 视图主体;主体元素区按重构类型分组承接生成结果供后续分镜使用。四种入口都属于参考重构,不抠图、不复制原人、不复刻原画面;旧下方主体模板库不再作为主路径。波形下方的画面胶片由前端临时从源视频截取,密度可调,点击只跳转原视频时间点,双击或拖入参考帧池才调用手动抽帧接口正式写入关键帧;已写入的胶片显示“已添加”,相同素材、相同密度和时长下会复用内存缓存,避免返回页面时重复扫视频。产品图上传后独立形成产品资产包:自动识别视角、左右/上下/内外侧、结构点、比例和风险,并补缺角度。最终分镜规划按逐句时间轴把文案、主体元素和产品资产汇合;每条分镜默认是左侧“文案 / 场景一句话 / 人物+产品+动作”三字段、右侧横向视频候选轨。客户可直接改中文镜像,前端会调用改写/翻译链路自动优化对应英文主值;单条和整片都可选择生成数量,整片按行排队提交。首尾帧、视觉规划、产品出现方式等细节保留在高级抽屉和后端自动展开逻辑里,不再作为客户默认闸门。

+

当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧是信息流复刻工作表。后台仍按 01-09 流程顺序计算素材任务、源视频、音频文案、抽帧、主体资产、产品资产、分镜文案、三字段规划和视频候选这些状态,但这些判断不再默认显现在工作区顶部,避免状态提示挤占首屏操作空间。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动音频文案路和视频视觉路。音频文案路提取原音频文案/字幕,分析讲话人、语速节奏、背景音乐/环境声/音效,并为后续新口播和分镜文案提供时间轴;视频视觉路同步抽取参考帧。源视频工作区右侧主体链路是“参考帧池 → 转换层 → 主体元素”:参考帧池只作为竖向原始参考,转换层收敛为真人重构、卡通重构、元素重构、自主描述四个入口,每个入口最多接收 3 张参考帧;拖入只加入对应入口的参考队列,不自动生成,用户放好参考和文字后再点击生成 6 视图主体;主体元素区按重构类型分组承接生成结果供后续分镜使用。真人/卡通/元素和有文字的自主描述属于参考创新重构,会先识别参考帧再用参考图通道生成;自主描述没有文字时切到源形象锁定模式,按参考帧复刻同一可见主体;旧下方主体模板库不再作为主路径。波形下方的画面胶片由前端临时从源视频截取,密度可调,点击只跳转原视频时间点,双击或拖入参考帧池才调用手动抽帧接口正式写入关键帧;已写入的胶片显示“已添加”,相同素材、相同密度和时长下会复用内存缓存,避免返回页面时重复扫视频。产品图上传后独立形成产品资产包:自动识别视角、左右/上下/内外侧、结构点、比例和风险,并补缺角度。最终分镜规划按逐句时间轴把文案、主体元素和产品资产汇合;每条分镜默认是左侧“文案 / 场景一句话 / 人物+产品+动作”三字段、右侧横向视频候选轨。客户可直接改中文镜像,前端会调用改写/翻译链路自动优化对应英文主值;单条和整片都可选择生成数量,整片按行排队提交。首尾帧、视觉规划、产品出现方式等细节保留在高级抽屉和后端自动展开逻辑里,不再作为客户默认闸门。

01

素材输入

有当前素材任务即通过;输入框只负责创建或切换任务。

02

源视频下载

job.video_url 存在即通过;created/downloading 视为运行中。公开视频默认不带 cookies 下载;只有 TikTok 明确要求登录态时才配置 YTDLP_COOKIES_FILE,生产容器禁止使用 YTDLP_COOKIES_FROM_BROWSER=chrome

03

音频文案

audio_script.source_texttranscript 逐句时间轴有内容即通过。

04

抽帧参考

job.frames.length > 0 即通过;参考帧只做主体重构证据。

-
05

主体重构

关键帧里存在 subject_assets 即通过;真人、卡通、元素和自主描述都生成全新 6 视图主体,不复刻原人。

+
05

主体重构

关键帧里存在 subject_assets 即通过;真人、卡通、元素和有文字的自主描述走参考创新,自主描述空文本走源形象锁定。

06

产品素材池

product_refs 有记录即通过;不限量上传,后续按分镜最多挑 6 张。

07

分镜文案

逐句时间轴生成后进入分镜;新口播可单段或整片改写。

08

三字段规划

客户默认只编辑文案、场景一句话、人物+产品+动作;高级抽屉保留首尾帧和 6 字段。

@@ -593,8 +593,8 @@ web/next.config.mjsNext.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 eslint 顶层配置,避免本地 dev 出现配置 Issue 提示。 web/app/globals.css全局主题变量、登录页视觉样式、信息流工作台同源品牌 token、ReactFlow 样式引用,以及本地开发态 nextjs-portal 遮挡隐藏规则。工作台在 skg-board-theme 内定义 --skg-gold-1--skg-gold-2--skg-cream--skg-bg-*--skg-text-*--skg-radius-* 和按钮阴影等变量,并新增 skg-board-brandskg-stat-cardskg-primary-actionskg-secondary-actionskg-empty-state 等样式。暗色工作台复用登录页金色聚焦、米白主按钮和弱暖光氛围;明亮模式通过 skg-board-theme--light 复用同一套结构,改成暖白底、白色 panel、黑底主 CTA 和深色文本,不另起一套界面。 web/app/page.tsx产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始分析”会把 job 放入并行素材分析队列,下载完成后触发 triggerTranscribe 解析音频,并触发 analyzeJob 自动抽 12 张参考帧,形成“音频文案路 + 视频视觉路”同步推进;音频失败时会忽略失败状态下残留的半成品 transcript,允许再次触发音频解析;底部吸附音频条和旧全局浮动主题按钮不再从主界面渲染,避免和工作台内的明暗模式切换重复。 - web/components/ad-recreation-board.tsx信息流广告复刻工作表:顶部先展示与登录页连续的 SKG brand strip,包含 SKG 字标、“未来健康 · 营销内容工作台”和“营销内容工作台 · TK 二创”;右侧素材/任务/视频/文案统计改为米白 stat 卡片,主动作按钮统一走 skg-primary-action,次动作走 skg-secondary-action,空状态复用 AnimatedLoginCharactersbuildWorkflowSteps 仍统一生成 01-09 流程顺序、状态和判定依据,WorkflowStepBadge / PipelineLane / 分镜列标题也继续共用同一套编号;但完整 WorkflowOrderBar、右侧素材/视频/音频/文案/参考帧需求 chips、文案依据下拉和“音频文案、抽帧参考、主体重构、产品素材池”四个状态条不再默认渲染在工作区顶部。左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧源视频工作区直接进入核心操作。讲话人、节奏和背景音分析仍写入 AudioScript,但不再作为“音频解析结果”卡片默认渲染;主工作区左侧宽度调整为 430-460px,上方是按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧,播放器下方是逐句时间轴,英文和中文都最多显示两行;右侧上方是无标题的波形与切点参考框,下方是三栏主体管线。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部把低/中/高密度按钮和当前播放秒数、总时长、鼠标指针停点秒数直接放在波形上方。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。音频波形下方同框渲染无标题的 TimelineFilmstrip 临时画面胶片,前端按低/中/高密度从源视频 canvas 截取预览缩略图,并按 frame.time / duration 的百分比定位到和波形同一条时间轴上;波形与胶片之间不显示分隔横线,胶片轨道贴近波形,缩略图轻微上下错落并倾斜重叠排列,hover 时用同一张胶片卡在原位置生成固定顶层克隆,约 4.8 倍放大并自动限制在视口内,避免被工作区、滚动容器或相邻面板遮挡;单击胶片只跳转原视频时间,不写入任务数据,双击胶片或拖进参考帧池时才调用手动抽帧并正式加入 job.frames,已加入的胶片显示“已添加”;胶片预览按 job、视频、密度和时长缓存,未切换低/中/高时返回页面不重新扫视频。右侧参考帧池的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化;缩略图按竖版完整比例显示不裁切,点选状态直接叠在参考帧池缩略图上,鼠标停留会通过固定浮层放大展示完整帧。转换层不再暴露“生成 10 张高清图”、透明骨架/真人或完整/常用视图开关,改成真人重构、卡通重构、元素重构、自主描述四个投放区;每区最多 3 张参考帧,拖入只加入该区参考队列,用户放好参考和文字后点击按钮才调用 generateSubjectAssets 固定生成 6 视图,卡通重构可选择具体卡通风格,文字方向会进入 prompt;转换层顶部新增生图模型选择(自动 / GPT / Gemini),选择写入本地 localStorage,只影响主体套图生成;四个方向的提示词输入会记忆常用短语并生成可点击小按键,点击会追加到当前提示词。主体元素区按重构类型分组显示结果;只展示每个 view 的最新一张,缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 replace_views=true 替换同一视角。前端当前对真人/元素/自主描述传 subject_style=source_actor,对卡通重构传 subject_style=cartoon_subject,并使用 reconstruction_mode=similar;后端会把关键帧反推成非身份化文字 brief,再按 image_model_preference 选择自动链路、仅 GPT 或仅 Gemini,自动链路仍以 gpt-image-2 为主,只有主模型超时、429、5xx 或网络错误时才短时熔断并兜底 gemini-3-pro-image-preview,避免复制原人、原脸和原画面。主体生成 prompt 默认继承参考里的性别、人种/肤色、年龄体态和角色气质这些广义特征,同时锁定同一个全新主体、同一套服装和同一套配饰贯穿 6 视图。主体生成完成后会形成 subject_consensus_brief。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写。每条音频分镜默认是左侧三字段、右侧横向视频候选轨;高级区仍保留首尾帧 prompt、产品出现方式和旧 6 字段。ModelTrace 会在音频解析、产品识别/补图、主体重构视图包、脚本改写等入口旁直接展示模型名;生图入口会显示 gpt-image-2 / gemini-3-pro-image-preview 链路和短时熔断规则,点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 - SourceSubjectPipeline源视频工作区右侧主体管线主路径:三栏分别是竖向 参考帧池转换层主体元素。参考帧池保留自动 12 张、胶片拖入正式成帧、点击勾选和删除;参考帧缩略图保持小尺寸固定宽度、aspect-[9/16]object-contain 显示,hover 预览通过 MediaAssetTile 的左侧紧凑浮层显示。转换层取消旧的“透明骨架 / 真人”和“完整 10 / 常用 4”开关,改成真人重构、卡通重构、元素重构、自主描述四个投放区;每个区最多保留 3 张参考帧,拖入只加入参考队列,不自动调用生成;用户放好参考和文字后点击按钮才调用 generateSubjectAssets 生成固定 6 视图。转换层顶部提供 autogpt-image-2gemini-3-pro-image-preview 三档模型选择,偏好存到 localStorage["skg:subject-image-model:v1"];提示词输入存到 localStorage["skg:subject-prompt-memory:v1"],会把旧词解析成短 chip,点击 chip 会追加到当前方向。文字输入会参与 prompt,卡通重构额外提供 3D 动画、潮玩公仔、日系清爽、美式插画、黏土玩具、极简扁平等风格。四种模式都强调参考重构:不抠图区、不复制原人原脸、不复刻原画面;默认继承参考里的性别、人种/肤色、年龄体态和角色气质这些广义特征,但生成的是同一个全新主体。后端会为每次主体套图注入同一份 pack bible,锁定同一张新脸、同一发型、同一体态、同一套 SKG 健康广告服装、同一配色、同一材质、同一剪裁和同一配饰;后处理会裁出白底主体并允许放大到画布高度上限约 96%,实测典型主体有效高度约 90%,避免模型生成“小人 + 大白边”。主体元素区按每次生成的 pack_id 组织成“套图文件夹”:顶部展开当前选中套图,下面是可滚动的套图包列表;同一方向可保留多套,生成中按 pack 显示 2/6 这类进度,单张完成就替换对应占位卡。缩略图复用 MediaAssetTile,支持 hover 放大、单张重生和删除。旧下方 SourceReferenceBuildPanel 不再主路径渲染。 + web/components/ad-recreation-board.tsx信息流广告复刻工作表:顶部先展示与登录页连续的 SKG brand strip,包含 SKG 字标、“未来健康 · 营销内容工作台”和“营销内容工作台 · TK 二创”;右侧素材/任务/视频/文案统计改为米白 stat 卡片,主动作按钮统一走 skg-primary-action,次动作走 skg-secondary-action,空状态复用 AnimatedLoginCharactersbuildWorkflowSteps 仍统一生成 01-09 流程顺序、状态和判定依据,WorkflowStepBadge / PipelineLane / 分镜列标题也继续共用同一套编号;但完整 WorkflowOrderBar、右侧素材/视频/音频/文案/参考帧需求 chips、文案依据下拉和“音频文案、抽帧参考、主体重构、产品素材池”四个状态条不再默认渲染在工作区顶部。左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧源视频工作区直接进入核心操作。讲话人、节奏和背景音分析仍写入 AudioScript,但不再作为“音频解析结果”卡片默认渲染;主工作区左侧宽度调整为 430-460px,上方是按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧,播放器下方是逐句时间轴,英文和中文都最多显示两行;右侧上方是无标题的波形与切点参考框,下方是三栏主体管线。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部把低/中/高密度按钮和当前播放秒数、总时长、鼠标指针停点秒数直接放在波形上方。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。音频波形下方同框渲染无标题的 TimelineFilmstrip 临时画面胶片,前端按低/中/高密度从源视频 canvas 截取预览缩略图,并按 frame.time / duration 的百分比定位到和波形同一条时间轴上;波形与胶片之间不显示分隔横线,胶片轨道贴近波形,缩略图轻微上下错落并倾斜重叠排列,hover 时用同一张胶片卡在原位置生成固定顶层克隆,约 4.8 倍放大并自动限制在视口内,避免被工作区、滚动容器或相邻面板遮挡;单击胶片只跳转原视频时间,不写入任务数据,双击胶片或拖进参考帧池时才调用手动抽帧并正式加入 job.frames,已加入的胶片显示“已添加”;胶片预览按 job、视频、密度和时长缓存,未切换低/中/高时返回页面不重新扫视频。右侧参考帧池的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化;缩略图按竖版完整比例显示不裁切,点选状态直接叠在参考帧池缩略图上,鼠标停留会通过固定浮层放大展示完整帧。转换层不再暴露“生成 10 张高清图”、透明骨架/真人或完整/常用视图开关,改成真人重构、卡通重构、元素重构、自主描述四个投放区;每区最多 3 张参考帧,拖入只加入该区参考队列,用户放好参考和文字后点击按钮才调用 generateSubjectAssets 固定生成 6 视图,卡通重构可选择具体卡通风格,文字方向会进入 prompt;转换层顶部新增生图模型选择(自动 / GPT / Gemini),选择写入当前 job 作用域的 localStorage,只影响当前项目的主体套图生成;四个方向的提示词输入会记忆当前项目常用短语并生成可点击小按键,点击会追加到当前提示词。主体元素区按重构类型分组显示结果;只展示每个 view 的最新一张,缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 replace_views=true 替换同一视角。前端当前对真人/元素/自主描述传 subject_style=source_actor,对卡通重构传 subject_style=cartoon_subject;真人、卡通、元素和有文字的自主描述使用 reconstruction_mode=similar,后端先识别关键帧 brief,再把参考帧作为 /images/edits 的 image refs 一起提交;自主描述空文本使用 reconstruction_mode=same 做源形象锁定。主体生成完成后会形成 subject_consensus_brief。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写。每条音频分镜默认是左侧三字段、右侧横向视频候选轨;高级区仍保留首尾帧 prompt、产品出现方式和旧 6 字段。ModelTrace 会在音频解析、产品识别/补图、主体重构视图包、脚本改写等入口旁直接展示模型名;生图入口会显示 gpt-image-2 / gemini-3-pro-image-preview 链路和短时熔断规则,点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 + SourceSubjectPipeline源视频工作区右侧主体管线主路径:三栏分别是竖向 参考帧池转换层主体元素。参考帧池保留自动 12 张、胶片拖入正式成帧、点击勾选和删除;参考帧缩略图保持小尺寸固定宽度、aspect-[9/16]object-contain 显示,hover 预览通过 MediaAssetTile 的左侧紧凑浮层显示。转换层取消旧的“透明骨架 / 真人”和“完整 10 / 常用 4”开关,改成真人重构、卡通重构、元素重构、自主描述四个投放区;每个区最多保留 3 张参考帧,拖入只加入参考队列,不自动调用生成;用户放好参考和文字后点击按钮才调用 generateSubjectAssets 生成固定 6 视图。转换层顶部提供 autogpt-image-2gemini-3-pro-image-preview 三档模型选择,偏好存到当前 job 作用域的 localStorage["skg:subject-image-model:v1:{jobId}"];提示词输入存到当前 job 作用域的 localStorage["skg:subject-prompt-memory:v1:{jobId}"],会把旧词解析成短 chip,点击 chip 会追加到当前方向但不会跨项目带入。文字输入会参与 prompt,卡通重构额外提供 3D 动画、潮玩公仔、日系清爽、美式插画、黏土玩具、极简扁平等风格。真人、卡通、元素和有文字的自主描述强调参考创新:先把参考帧识别成主体 brief,再把参考帧作为 /images/edits 的 image refs 一起提交,默认继承参考里的性别、人种/肤色、年龄体态和角色气质这些广义特征,但生成的是同一个全新主体;自主描述没有文字时切到 reconstruction_mode=same,使用参考帧作为源形象锁定证据,避免被默认 prompt 随机换成另一个人。后端会为每次主体套图注入同一份 pack bible:参考创新模式锁定同一个全新主体和同一套服装,源形象锁定模式锁定参考帧里的可见主体、体态、发型、服装和配色;后处理会裁出白底主体并允许放大到画布高度上限约 96%,实测典型主体有效高度约 90%,避免模型生成“小人 + 大白边”。主体元素区按每次生成的 pack_id 组织成“套图文件夹”:顶部展开当前选中套图,下面是可滚动的套图包列表;同一方向可保留多套,生成中按 pack 显示 2/6 这类进度,单张完成就替换对应占位卡。缩略图复用 MediaAssetTile,支持 hover 放大、单张重生和删除。旧下方 SourceReferenceBuildPanel 不再主路径渲染。 AudioStoryboardPlanPanel 三字段候选生成当前分镜主路径:每行是左右双栏,左侧默认显示 skg_copy_*scene_one_line_*action_one_line_* 三组中英字段,右侧直接显示视频候选横向轨。用户改中文镜像后,字段失焦会通过 refineStoryboard 优化对应英文主值,失败时退回 translateText;英文仍是后续 prompt 主值。quickPlanStoryboard 把三字段和主体 brief 展开为完整 StoryboardScenegenerateStoryboardVideocount 可由单行数字控件选择,候选新生成后持续向右追加,不再用 4-grid 撑高每行。整片生成同样可选择每行数量,并以 concurrency=1 按行排队提交。产品素材池、批量控制、每行主体区和高级区都可折叠,高级抽屉仍展示旧 6 字段、首尾帧 prompt 和首尾帧资产槽,但客户默认不用先处理首尾帧。 web/components/resource-library/library-drawer.tsx全局资源中心浮窗:由工作台顶部“资源库”按钮打开,叠加在工作台上方但不阻塞主界面;尺寸、位置和当前 Tab 写入 localStorage["skg-resource-library-drawer"]。提示词 Tab 固定 5 列(场景描述、视频描述、主体描述、SKG 文案、产品角度),每列先显示 use_count 排名前 5 的“常用”,再按月份倒序分组;提示词节点常驻复制按钮,hover 可选英文/中文/双语复制,并调用 use 接口。素材 Tab 固定 4 列(主体、产品、场景、视频),节点不可拖动,按月份倒序硬编码排列;“应用到当前 job”只调用后端复制接口,得到普通 ImageRef(kind="asset") 后再写入产品素材池或复制 ID。浮窗顶部最近 24 小时横条混合显示提示词和素材;新建提示词、上传素材、删除前查引用、详情侧栏都在该组件内完成。 AdRecreationBoard 主题切换顶部指标区左侧有“明亮/暗色”按钮,使用 Sun / Moon 图标切换 skg-board-theme--light 类名,并把选择写入 localStorage["skg-board-theme"]。暗色仍是默认模式;明亮模式只改变工作台外观,不改变任务、素材、分镜、模型调用或接口数据。 @@ -986,7 +986,7 @@ ProductRefStateItem { 应用清洗POST /cleanup/applyapplyCleanedFrame物理覆盖 frames/{idx}.jpg,并备份原图。 元素增改删POST/PATCH/DELETE /elementsaddElement/updateElement/deleteElement让用户修正 Vision 错误,避免候选结果锁死。 元素提取POST /elements/{element_id}/cutoutcutoutElement调用图像模型生成独立白底素材图,每次累积一张 cutout。 - 主体资产包POST /elements/{element_id}/subject-assets
DELETE /elements/{element_id}/subject-assets/{asset_id}generateSubjectAssets
deleteSubjectAsset根据转换层里的参考帧重新绘制一个统一主体资产包;前端按真人重构、卡通重构、元素重构、自主描述四个方向分别管理 source_frame_indices,每个方向最多 3 张参考帧,固定请求 frontthree_quarter_leftleftbackrightthree_quarter_right 六个视图,不再暴露完整 10 / 常用 4 选择。当前源视频工作区使用 subject_style=source_actor 承接真人、元素和自主描述,使用 subject_style=cartoon_subject 承接卡通重构;旧 transparent_human 仍为兼容类型但不是当前转换层默认入口。reconstruction_mode=similar 是创新路径:后端先用 VISION_MODEL 把关键帧反推成非身份化文字 brief,再按 image_model_preference 选择自动链路、仅 GPT 或仅 Gemini,自动链路仍优先调用 gpt-image-2/images/generations 文字生图,日志会显示 image_refs=0;这里是参考重构生成套图,不是抠图、复制或 image-edit 复刻。卡通重构在后端额外加入原创卡通/插画主体约束,明确不输出真实人物复制 likeness。生成完成后,后端会把生成视图反推/写入 KeyElement.subject_consensus_brief,作为后续首尾帧的唯一主体身份文字依据。reconstruction_mode=same 仍保留旧 image-edit 路径,用于确实需要精确复刻且有授权的场景。每个 view 单独调用一次生图,明确禁止多视图拼图、contact sheet、多主体、多面板、标签或对比排版。单次图片请求受 IMAGE_REQUEST_TIMEOUT_SECONDS 控制,默认 60 秒;gpt-image-2 超时、429、5xx、DNS 或连接失败时可兜底 gemini-3-pro-image-preview,连续 2 次主模型上游类失败后 600 秒内短时熔断。仅当 image_model_preference=auto 时才启用兜底和熔断;用户显式选择 GPT 或 Gemini 时只走所选模型,方便已知某个上游不可用时直接切换。主体同一套图内一旦触发 Gemini,后续视图沿用 Gemini,避免风格混杂和重复等待主模型超时。主体 prompt 会要求从参考图继承性别、人种/肤色、年龄体态和角色气质等广义特征,但生成同一个全新主体;六视图必须保持同一脸部设定、发型、体态、服装类型、配色、材质、剪裁和配饰,不允许每个视角换衣服。后端新增 pack bible 固定字段,把主体、发型、肤色、体态、服装、鞋、配饰和禁止换装项注入每个视角;_normalize_asset_image(fill_subject=true) 裁白边后会按目标画布放大主体,而不是只用 thumbnail() 缩小,目标是让全身主体占画布高度约 88-94%。后端不再要求整包全成功才写入:单个视图失败时会保留已成功生成的主体图,返回“部分生成完成”,只有一张都没生成出来才返回错误。replace_views=true 时会替换同一视角旧图;删除接口会移除对应 subject asset 记录并删除本地 jpg 文件。 + 主体资产包POST /elements/{element_id}/subject-assets
DELETE /elements/{element_id}/subject-assets/{asset_id}generateSubjectAssets
deleteSubjectAsset根据转换层里的参考帧重新绘制一个统一主体资产包;前端按真人重构、卡通重构、元素重构、自主描述四个方向分别管理 source_frame_indices,每个方向最多 3 张参考帧,固定请求 frontthree_quarter_leftleftbackrightthree_quarter_right 六个视图,不再暴露完整 10 / 常用 4 选择。当前源视频工作区使用 subject_style=source_actor 承接真人、元素和自主描述,使用 subject_style=cartoon_subject 承接卡通重构;旧 transparent_human 仍为兼容类型但不是当前转换层默认入口。reconstruction_mode=similar 是参考创新路径:后端先用 VISION_MODEL 把关键帧反推成主体 brief;只要有参考帧,就把这些帧作为 /images/edits 的 image refs 一起提交,日志会显示 endpoint=/images/editsimage_refs>0,不再偷偷降级为纯文字生图。卡通重构在后端额外加入原创卡通/插画主体约束,明确不输出真实人物复制 likeness。生成完成后,后端会把生成视图反推/写入 KeyElement.subject_consensus_brief,作为后续首尾帧的唯一主体身份文字依据。reconstruction_mode=same 是源形象锁定路径:自主描述空文本时前端使用该路径,后端把参考帧作为 primary visual evidence,尽量保留同一可见主体、体态、发型、服装和配色。每个 view 单独调用一次生图,明确禁止多视图拼图、contact sheet、多主体、多面板、标签或对比排版。单次图片请求受 IMAGE_REQUEST_TIMEOUT_SECONDS 控制,默认 60 秒;gpt-image-2 超时、429、5xx、DNS 或连接失败时可兜底 gemini-3-pro-image-preview,连续 2 次主模型上游类失败后 600 秒内短时熔断。仅当 image_model_preference=auto 时才启用兜底和熔断;用户显式选择 GPT 或 Gemini 时只走所选模型,方便已知某个上游不可用时直接切换。主体同一套图内一旦触发 Gemini,后续视图沿用 Gemini,避免风格混杂和重复等待主模型超时。主体 prompt 会要求从参考图继承性别、人种/肤色、年龄体态和角色气质等广义特征,但生成同一个全新主体;六视图必须保持同一脸部设定、发型、体态、服装类型、配色、材质、剪裁和配饰,不允许每个视角换衣服。后端新增 pack bible 固定字段,把主体、发型、肤色、体态、服装、鞋、配饰和禁止换装项注入每个视角;_normalize_asset_image(fill_subject=true) 裁白边后会按目标画布放大主体,而不是只用 thumbnail() 缩小,目标是让全身主体占画布高度约 88-94%。后端不再要求整包全成功才写入:单个视图失败时会保留已成功生成的主体图,返回“部分生成完成”,只有一张都没生成出来才返回错误。replace_views=true 时会替换同一视角旧图;删除接口会移除对应 subject asset 记录并删除本地 jpg 文件。 主体套图状态SubjectAsset.status
pack_idweb/app/page.tsx
SourceSubjectPipelinegenerateSubjectAssets 现在先写入同一个 pack_id 下的 queued 占位卡并立即返回,后台按视角逐张生成,单张完成就把该占位替换成 completed 图片。前端轮询会把 queued / in_progress 主体资产纳入运行状态;主体元素区按 pack 显示套图文件夹,点击某个文件夹后展开该套图,其他套图顺位进入下方可滚动列表。 首尾帧资产POST /frames/{idx}/scene-assetgenerateSceneAsset同一接口兼容旧场景图和新首尾帧;当前信息流复刻流程传 asset_role=first_frame/last_framesubject_brief 和最多 1-2 张 product_images。首尾帧不再传主体图、不再把主体图和产品图拼成 contact sheet;主体只走文字 brief,允许新动作、新景别、新表情和新环境。若本条需要产品,后端只把产品参考图作为 gpt-image-2 image-edit 的硬视觉真源;若不需要产品,则走纯文字生图。关键帧只作为行数据承载位置。生成结果保存在 scene_assets,前端再写入 StoryboardScene.first_image/last_image。 产品图库GET /product-library/skglistProductLibrary读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。 @@ -1113,6 +1113,19 @@ ProductRefStateItem {

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-20 · 转换层参考帧改为项目隔离和图像参考生图

+ UI + API + Workflow +
+
+

问题:转换层提示词记忆和生图模型选择用全局 localStorage key,切换不同项目时会带入上一项目的偏好;同时 reconstruction_mode=similar 虽然提交了参考帧,但最终走纯文字 /images/generations,导致不同项目容易生成同一套泛化人物。

+

改动:SourceSubjectPipeline 把主体提示词记忆和生图模型偏好改为按 job.id 存储;自主描述为空且已拖入参考帧时,前端切到 reconstruction_mode=same 并提交源形象锁定 prompt。generateSubjectAssets 对有参考帧的 similar 路径先生成 source brief,再把参考帧作为 /images/edits 的 image refs 一起提交;same 路径使用源形象 pack bible,锁定参考帧中的可见主体、体态、发型、服装和配色。

+

影响:不同项目之间不会再共享转换层提示词 chip 或模型选择;真人/卡通/元素重构仍是参考创新,但参考帧会真正进入生图请求;自主描述不写字时表示按拖入参考帧复刻同一主体,而不是随机生成新人。

+
+

2026-05-20 · 主体 6 视图增加 pack bible 和主体占比放大

@@ -1134,7 +1147,7 @@ ProductRefStateItem {

问题:用户已经知道某个生图模型不可用时,需要能在转换层直接切换;同时主体 6 视图容易出现不同衣服、不同人设,提示词每次也要重复输入。

-

改动:SourceSubjectPipeline 新增自动 / GPT / Gemini 三档生图模型选择,偏好写入 localStorage["skg:subject-image-model:v1"] 并通过 GenerateSubjectAssetsReq.image_model_preference 传到后端;提示词输入会保存到 localStorage["skg:subject-prompt-memory:v1"],旧词解析成可点击 chip。后端 _image_model_candidates 支持显式模型偏好,auto 保留 gpt-image-2 主模型、Gemini 兜底和短时熔断,显式 GPT / Gemini 只走所选模型。

+

改动:SourceSubjectPipeline 新增自动 / GPT / Gemini 三档生图模型选择,当时偏好写入全局 localStorage["skg:subject-image-model:v1"],后续已改为当前 job 作用域;提示词输入当时保存到全局 localStorage["skg:subject-prompt-memory:v1"],后续也已改为当前 job 作用域。后端 _image_model_candidates 支持显式模型偏好,auto 保留 gpt-image-2 主模型、Gemini 兜底和短时熔断,显式 GPT / Gemini 只走所选模型。

影响:主体重构默认继承参考图里的性别、人种/肤色、年龄体态和角色气质这些广义特征,但生成同一个全新主体;六视图 prompt 强制统一脸部设定、发型、体态、服装类型、配色、材质、剪裁和配饰,避免一套图里每张衣服都不同。

@@ -1492,7 +1505,7 @@ ProductRefStateItem {

问题:前端模型弹窗和状态文档对音频、产品、相似主体、脚本改写和视频入口的描述过于简化,容易误解为所有模型调用都会成功、都会上传参考图或会直接提交视频。

-

改动:ModelTrace 文案改成真实链路:ASR 是远端 whisper、本机 mlx_whisper、Gemini 多模态三级;翻译失败会保留原文时间轴;音频画像使用 audio.wav + 转写时间轴并有本地估算兜底;产品识别批量失败会单图重试再写本地默认视角;相似主体先用 GPT 视觉 brief,再走 gpt-image-2 文字生图;脚本改写全部模型失败后用本地模板;视频入口当前主工作台暂停直接提交。

+

改动:ModelTrace 文案改成真实链路:ASR 是远端 whisper、本机 mlx_whisper、Gemini 多模态三级;翻译失败会保留原文时间轴;音频画像使用 audio.wav + 转写时间轴并有本地估算兜底;产品识别批量失败会单图重试再写本地默认视角;相似主体先用 GPT 视觉 brief,有参考帧时再带参考图走 /images/edits;脚本改写全部模型失败后用本地模板;视频入口当前主工作台暂停直接提交。

影响:web/components/ad-recreation-board.tsxweb/components/dashboard.tsxapi/README.md.project.json.memory/status.md 和本页同步按实际后端行为描述,后续排查模型问题时优先看弹窗里的“兜底/失败行为”。

@@ -1540,7 +1553,7 @@ ProductRefStateItem {

问题:相似主体生成把最多 10 张源视频帧或模板图作为 image[] 上传给 /images/edits,参考图视觉权重过强,结果容易变成源视频人物或内置模板的复刻品。

-

改动:generate_subject_assetsreconstruction_mode=similar 时先用 VISION_MODEL 把关键帧、内置形象或数据库模板转成非身份化文字 brief,再调用 gpt-image-2 的文字生图路径;same 模式保留旧 image-edit 精确复刻路径。SubjectTemplateItem 和内置形象 manifest 新增 prompt_brief,保存模板时会从生成视图反推 brief。前端 SourceReferenceBuildPanel 改成“用模板 / 不用模板”模式开关、120px 竖排模板卡、底部一行保存表单,以及独立的“生成主体视图”区域,支持全部 10 / 常用 4 / 自定义视图。

+

改动:generate_subject_assets 当时在 reconstruction_mode=similar 时先用 VISION_MODEL 把关键帧、内置形象或数据库模板转成非身份化文字 brief,再调用 gpt-image-2 的文字生图路径;后续当前转换层已有参考帧时会改走 /images/edits 图像参考路径。same 模式保留 image-edit 源形象锁定路径。SubjectTemplateItem 和内置形象 manifest 新增 prompt_brief,保存模板时会从生成视图反推 brief。前端 SourceReferenceBuildPanel 改成“用模板 / 不用模板”模式开关、120px 竖排模板卡、底部一行保存表单,以及独立的“生成主体视图”区域,支持全部 10 / 常用 4 / 自定义视图。

影响:后续说“相似主体”应理解为文字 brief 驱动的创新生图,不是把关键帧或模板图送去 image edit 复制。需要精确复刻时必须显式走 reconstruction_mode=same

diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index 1c1a2e8..d8d24bb 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -653,10 +653,14 @@ function emptySubjectPromptMemory(): Record return { realistic: [], cartoon: [], elements: [], custom: [] } } -function loadSubjectPromptMemory(): Record { +function subjectScopedStorageKey(baseKey: string, jobId: string) { + return `${baseKey}:${jobId}` +} + +function loadSubjectPromptMemory(jobId: string): Record { if (typeof window === "undefined") return emptySubjectPromptMemory() try { - const parsed = JSON.parse(window.localStorage.getItem(SUBJECT_PROMPT_MEMORY_KEY) || "{}") as Partial> + const parsed = JSON.parse(window.localStorage.getItem(subjectScopedStorageKey(SUBJECT_PROMPT_MEMORY_KEY, jobId)) || "{}") as Partial> const next = emptySubjectPromptMemory() for (const mode of Object.keys(next) as SubjectReconstructionMode[]) { next[mode] = Array.isArray(parsed[mode]) ? parsed[mode]!.filter(Boolean).slice(0, SUBJECT_PROMPT_MEMORY_LIMIT) : [] @@ -667,25 +671,25 @@ function loadSubjectPromptMemory(): Record } } -function saveSubjectPromptMemory(memory: Record) { +function saveSubjectPromptMemory(jobId: string, memory: Record) { if (typeof window === "undefined") return try { - window.localStorage.setItem(SUBJECT_PROMPT_MEMORY_KEY, JSON.stringify(memory)) + window.localStorage.setItem(subjectScopedStorageKey(SUBJECT_PROMPT_MEMORY_KEY, jobId), JSON.stringify(memory)) } catch { /* localStorage may be unavailable */ } } -function loadSubjectImageModelPreference(): SubjectImageModelPreference { +function loadSubjectImageModelPreference(jobId: string): SubjectImageModelPreference { if (typeof window === "undefined") return "auto" - const raw = window.localStorage.getItem(SUBJECT_MODEL_MEMORY_KEY) + const raw = window.localStorage.getItem(subjectScopedStorageKey(SUBJECT_MODEL_MEMORY_KEY, jobId)) return SUBJECT_IMAGE_MODEL_OPTIONS.some((item) => item.value === raw) ? raw as SubjectImageModelPreference : "auto" } -function saveSubjectImageModelPreference(value: SubjectImageModelPreference) { +function saveSubjectImageModelPreference(jobId: string, value: SubjectImageModelPreference) { if (typeof window === "undefined") return try { - window.localStorage.setItem(SUBJECT_MODEL_MEMORY_KEY, value) + window.localStorage.setItem(subjectScopedStorageKey(SUBJECT_MODEL_MEMORY_KEY, jobId), value) } catch { /* localStorage may be unavailable */ } @@ -1216,6 +1220,24 @@ function buildSimilarSubjectPrompt( return base.join(" ") } +function buildSourceLockedSubjectPrompt(subjectStyle: SubjectStyleMode) { + const base = [ + "Source-locked subject replication from the selected reference frames.", + "Use the attached reference frame(s) as the primary visual source for the same visible subject: preserve gender presentation, regional/ethnic appearance category, skin-tone family, body proportions, hair length/color/silhouette, face structure impression, wardrobe category, outfit colors, fit, and commercial role as closely as the model allows.", + "Generate separate clean white-background multi-view assets of that same source subject, removing only source video background, platform UI, captions, watermarks, compression artifacts, and accidental occlusions.", + "Do not invent a different actor, different ethnicity, different gender, different body type, different hair design, or different outfit when the reference evidence is visible.", + "If multiple frames are supplied, treat them as evidence for one same subject and build one locked subject bible before rendering every view.", + "Keep the neck, collarbone, shoulders, upper back, and side neck clean and usable for SKG neck-and-shoulder product placement.", + ] + if (subjectStyle === "cartoon_subject") { + base.push("If a cartoon style is requested, convert the same visible source subject into one consistent stylized character while preserving the reference's main appearance and outfit cues.") + } else { + base.push("The subject must remain a believable normal commercial ad actor, not a transparent or skeleton character.") + } + base.push("Output high-definition assets; each image is one requested view of the same unified subject.") + return base.join(" ") +} + function subjectAssetUrl(job: Job, asset: SubjectAsset) { if (!asset.url && asset.status && asset.status !== "completed") return "" return apiAssetUrl(asset.url) || resolveImageRefUrl(job.id, { kind: "asset", frame_idx: 0, element_id: asset.id }) @@ -3287,8 +3309,8 @@ function SourceSubjectPipeline({ const [activeDropMode, setActiveDropMode] = useState(null) const [conversionFrameIndicesByMode, setConversionFrameIndicesByMode] = useState>(() => ({ ...EMPTY_RECONSTRUCTION_FRAME_MAP })) const [reconstructionDirections, setReconstructionDirections] = useState>(() => ({ ...DEFAULT_RECONSTRUCTION_DIRECTIONS })) - const [subjectImageModelPreference, setSubjectImageModelPreference] = useState(() => loadSubjectImageModelPreference()) - const [promptMemoryByMode, setPromptMemoryByMode] = useState>(() => loadSubjectPromptMemory()) + const [subjectImageModelPreference, setSubjectImageModelPreference] = useState(() => loadSubjectImageModelPreference(job.id)) + const [promptMemoryByMode, setPromptMemoryByMode] = useState>(() => loadSubjectPromptMemory(job.id)) const [cartoonStyle, setCartoonStyle] = useState("3d_animation") const [cartoonStyleOpen, setCartoonStyleOpen] = useState(false) const [subjectBusyFor, setSubjectBusyFor] = useState<{ jobId: string; jobLabel: string; mode: SubjectReconstructionMode; viewCount: number; sourceCount: number; profileLabel: string; modelLabel: string } | null>(null) @@ -3383,6 +3405,8 @@ function SourceSubjectPipeline({ useEffect(() => { setConversionFrameIndicesByMode({ ...EMPTY_RECONSTRUCTION_FRAME_MAP }) setReconstructionDirections({ ...DEFAULT_RECONSTRUCTION_DIRECTIONS }) + setSubjectImageModelPreference(loadSubjectImageModelPreference(job.id)) + setPromptMemoryByMode(loadSubjectPromptMemory(job.id)) setLastSubjectProfile(null) setSubjectBusyFor(null) setSubjectAssetBusy(null) @@ -3392,12 +3416,12 @@ function SourceSubjectPipeline({ }, [job.id]) useEffect(() => { - saveSubjectImageModelPreference(subjectImageModelPreference) - }, [subjectImageModelPreference]) + saveSubjectImageModelPreference(job.id, subjectImageModelPreference) + }, [job.id, subjectImageModelPreference]) useEffect(() => { - saveSubjectPromptMemory(promptMemoryByMode) - }, [promptMemoryByMode]) + saveSubjectPromptMemory(job.id, promptMemoryByMode) + }, [job.id, promptMemoryByMode]) useEffect(() => { if (expandedSubjectPackKey && !subjectAssetPacks.some((pack) => pack.key === expandedSubjectPackKey)) { @@ -3455,17 +3479,23 @@ function SourceSubjectPipeline({ const sourceFrames = sourceIndices .map((index) => frames.find((frame) => frame.index === index)) .filter((frame): frame is KeyFrame => !!frame) + const rawDirection = reconstructionDirections[mode].trim() + const sourceLockedReplication = mode === "custom" && !rawDirection if (!sourceFrames.length && mode !== "custom") { toast.warning(`先把参考帧拖到${reconstructionModeConfig(mode).label}。`) return } + if (!sourceFrames.length && sourceLockedReplication) { + toast.warning("自主描述没有文字时,需要先拖入参考帧用于形象复刻。") + return + } const baseFrame = sourceFrames[0] ?? frames[0] if (!baseFrame) { toast.warning("先完成抽帧,或从胶片加入至少一张参考帧。") return } const requestJobId = job.id - const requestProfile = mode === "custom" && reconstructionDirections.custom.trim() + const requestProfile = sourceLockedReplication || (mode === "custom" && rawDirection) ? null : buildSubjectProfileForRequest() const subjectStyle = reconstructionSubjectStyle(mode) @@ -3502,13 +3532,15 @@ function SourceSubjectPipeline({ const updated = await generateSubjectAssets(requestJobId, baseFrame.index, element.id, { subject_kind: "living", subject_style: subjectStyle, - reconstruction_mode: "similar", + reconstruction_mode: sourceLockedReplication ? "same" : "similar", background: "white", size: SUBJECT_ASSET_SIZE, source_frame_indices: sourceFrames.slice(0, RECONSTRUCTION_FRAME_LIMIT).map((frame) => frame.index), views: selectedSubjectViews, subject_profile: requestProfile?.payload ?? null, - prompt: buildSimilarSubjectPrompt(subjectStyle, userDirection, null, requestProfile), + prompt: sourceLockedReplication + ? buildSourceLockedSubjectPrompt(subjectStyle) + : buildSimilarSubjectPrompt(subjectStyle, userDirection, null, requestProfile), image_model_preference: subjectImageModelPreference, replace_views: false, pack_label: `${reconstructionModeConfig(mode).label} ${new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false })}`, @@ -3563,13 +3595,19 @@ function SourceSubjectPipeline({ const sourceIndices = asset.source_frame_indices?.length ? asset.source_frame_indices : conversionFrameIndicesByMode[mode] + const rawDirection = reconstructionDirections[mode].trim() + const sourceLockedReplication = mode === "custom" && !rawDirection if (!sourceIndices.length && mode !== "custom") { toast.warning("转换层没有参考帧,不能重生。") return } + if (!sourceIndices.length && sourceLockedReplication) { + toast.warning("自主描述没有文字时,需要参考帧才能复刻重生。") + return + } setSubjectAssetBusy(`regen:${asset.id}`) try { - const requestProfile = mode === "custom" && reconstructionDirections.custom.trim() + const requestProfile = sourceLockedReplication || (mode === "custom" && rawDirection) ? null : lastSubjectProfile ?? buildSubjectProfileForRequest() const subjectStyle = reconstructionSubjectStyle(mode) @@ -3577,18 +3615,20 @@ function SourceSubjectPipeline({ const updated = await generateSubjectAssets(job.id, frame.index, element.id, { subject_kind: "living", subject_style: subjectStyle, - reconstruction_mode: "similar", + reconstruction_mode: sourceLockedReplication ? "same" : "similar", background: asset.background || "white", size: SUBJECT_ASSET_SIZE, source_frame_indices: sourceIndices.slice(0, RECONSTRUCTION_FRAME_LIMIT), views: [asset.view], subject_profile: requestProfile?.payload ?? null, - prompt: buildSimilarSubjectPrompt( - subjectStyle, - buildReconstructionDirection(mode, reconstructionDirections[mode], cartoonStyle), - null, - requestProfile, - ), + prompt: sourceLockedReplication + ? buildSourceLockedSubjectPrompt(subjectStyle) + : buildSimilarSubjectPrompt( + subjectStyle, + buildReconstructionDirection(mode, reconstructionDirections[mode], cartoonStyle), + null, + requestProfile, + ), image_model_preference: subjectImageModelPreference, replace_views: true, pack_id: asset.pack_id ?? "",