feat: make subject conversion dialog-driven

This commit is contained in:
2026-05-20 13:45:31 +08:00
parent b3cc0aa83c
commit d82175f0f3
6 changed files with 978 additions and 984 deletions

View File

@@ -1,6 +1,6 @@
# 项目接力 # 项目接力
- 生成时间May 19, 2026 at 08:44 - 生成时间May 20, 2026 at 13:43
- 项目SKG Marketing Studio / SKG 营销内容工作台 - 项目SKG Marketing Studio / SKG 营销内容工作台
- 路径:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证 - 路径:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证
- 状态active - 状态active
@@ -9,8 +9,8 @@
## 最近助手会话概览 ## 最近助手会话概览
- Claudebe53a031-9311-4ee8-b822-d4cfb4f5e78b · 时间未知 - Claudebe53a031-9311-4ee8-b822-d4cfb4f5e78b · 时间未知
- Codex019e3dae-6045-7203-bf4e-8fbeae65cadf · 时间未知 - Codex019e3db1-012e-7163-bc78-acf7cde326e7 · 时间未知
- Cursor3e4af548-3b65-45a5-8698-6e75510f24b5 · May 19, 2026 at 08:43 - Cursor未找到匹配当前项目的最近会话
## Claude 最近会话 ## Claude 最近会话
@@ -44,64 +44,37 @@
## Codex 最近会话 ## Codex 最近会话
- Session ID019e3dae-6045-7203-bf4e-8fbeae65cadf - Session ID019e3db1-012e-7163-bc78-acf7cde326e7
- Transcript/Users/kangwan/.codex/sessions/2026/05/19/rollout-2026-05-19T08-41-38-019e3dae-6045-7203-bf4e-8fbeae65cadf.jsonl - Transcript/Users/kangwan/.codex/sessions/2026/05/19/rollout-2026-05-19T08-44-30-019e3db1-012e-7163-bc78-acf7cde326e7.jsonl
- 工作目录:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证 - 工作目录:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证
- 分支main - 分支main
- 敏感字段:已对 token / key / password / secret 做脱敏 - 敏感字段:已对 token / key / password / secret 做脱敏
### 最近用户要求 ### 最近用户要求
- # AGENTS.md instructions for /Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证 <INSTRUCTIONS> # SKG AI 素材管线 - TK 二创验证 Agent Rules ## Must Read First - `.project.json` 是机器真源:公网链接、快捷登录、凭证引用都以它为准 - `RULES.md` 是人工规则和部署事实:启动命令、平台、域名、注意事项都写这里 - 工作看板全局规则是本项目的开发收口默认规则;本地规则缺失时按 `/Users/kangwan/Projects/code/20260317-rules-dashboard/RULES.md``SCHEMA.md``rules/03-Git约定.md``rules/04-版本发布规则.md` 补齐 - 不允许编造不存在的域名、账号、密码;未知就保持空白并明确标记待补充 ## Development Ledger Contract - 开发任务结束前必须执行并汇报 `git status -sb` - 功能、修复、规则或部署元数据变更完成后,必须创建人工语义 commit`auto-save` 只算安全快照 - Gitea 是主远端,`origin` 必须指向 Gitea - 可以
- 你现在是在 Codex CLI 里的项目承接会话。 这不是对话立项台;项目目录已经存在,当前目标是接住上下文并等待用户下一步。 项目路径:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证 承接步骤: - 先读取 `.memory/assistant-handoff.md` - 再阅读 `.project.json``RULES.md``AGENTS.md` - 结合当前仓库状态建立上下文 要求: - 不要要求用户重新粘贴 Claude / Codex / Cursor 的总结 - 先理解最近进度、未完成项和下一步候选 - 不要立即修改代码、继续上一个任务或自行执行命令 - 等待用户给出下一条明确指令后再开始动手 - 如果后续需要 Cursor提醒用户从看板打开 Cursor GUI 到当前项目目录 - 继续
- <image name=[Image #1]> </image> [Image #1] 那会这个是不是就不用了 转换层 这里要重构样式 ,直接对话式 那个数量直接就在对话中完成
### Codex 最近回复 ### Codex 最近回复
> 本地状态和 handoff 里的快照有一处漂移:现在除了 `.memory/*``.project.json` 也处于未提交修改状态。我再补一眼远端、最新提交和领先/落后计数,只做状态确认 > 我准备动两个关键点:后端让 `/subject-agent/message` 从中文对话里识别“方向 + 数量”,前端删除四个模式卡和数量按钮,只展示当前识别出的方向/张数作为摘要。这样你后面说“卡通,生成 8 张”或“形象锁定6 张”就直接改变状态
> 已完成承接,只读,没有改代码、没有启动服务、没有继续上个任务 > 前端显式卡片已经先移掉了,同时我顺手避免了“卡通默认 3D 风格”硬覆盖对话里的风格要求。接下来补文档和运行验证
>
> 当前理解:
> - 项目是 `SKG Marketing Studio / SKG 营销内容工作台`,生产入口 `https://marketing.skg.com`,后端走 `/api`,主远端是 Gitea。
> - 当前产品方向:先做信息流广告快速复刻第一步,源视频下载优先;下载后并行做音频文案分析和视觉参考帧准备;直接提交视频模型目前是暂停状态。
> - 最近 Codex 进度:资源库 / 提示词库线上 API 链路已测通登录、健康检查、创建、搜索、预览、删除、recent 都验证过;最新提交是 `3b7a062 chore: persist resource libraries in production`。
> - 最近候选问题:音频处理卡片过大且空展示不美观、需要说明用途和调用模型;缩略图下方横向滚动条需要更明显、更好拖。
> - 后续媒体 UI 改动要复用 `web/components/media-asset-tile.tsx`;影响产品理解、节点职责、接口、数据模型或操作路径的改动,要同步更新 `docs/source-analysis.html`。
>
> 当前仓库状态:
> ```text
> ## main...origin/main
> M .memory/assistant-handoff.md
> M .memory/worklog.json
> M .project.json
> ```
>
> `main` 对 `origin/main` 领先/落后是 `0/0`。`.project.json` 当前未提交 diff 看起来主要是格式化变化,但也观察到 `urls` 里原来的 `source-analysis` 文档项被替换/移除了,这个我不会处理,等你下一条明确指令。
>
> 如果后续需要 Cursor请从看板打开 Cursor GUI 到这个精确目录:`/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证`。
>
> 我现在暂停,等你下一步指令。
>
> <…
## Cursor 最近会话 ## Cursor 最近会话
- Session ID3e4af548-3b65-45a5-8698-6e75510f24b5 - 未在本机找到匹配这个项目的 Cursor transcript。
- Transcript/Users/kangwan/.cursor/projects/Users-kangwan-Projects-business-20260512-20260512-skg-tk/agent-transcripts/3e4af548-3b65-45a5-8698-6e75510f24b5/3e4af548-3b65-45a5-8698-6e75510f24b5.jsonl
- 工作目录:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证
- 最后活动May 19, 2026 at 08:43
- 敏感字段:已对 token / key / password / secret 做脱敏
- 内容隔离:公司项目默认不写入 Cursor transcript 的用户要求和助手回复;这里只记录 Cursor 曾在该项目下工作。
## 当前仓库状态 ## 当前仓库状态
- 当前分支main - 当前分支main
- 未提交变更:2 - 未提交变更:8
- 最近提交:chore: persist resource libraries in production - 最近提交:docs: record image timeout deployment
- 变更文件: - 变更文件:
- M .memory/assistant-handoff.md
- M .memory/worklog.json - M .memory/worklog.json
- M .project.json - M api/main.py
- M web/components/ad-recreation-board.tsx
## 统一接力要求 ## 统一接力要求

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@
## 部署事实 ## 部署事实
- 平台VPS `76.13.31.179`Ubuntu 24.04 / Docker Compose / Coolify Traefik - 平台VPS `76.13.31.179`Ubuntu 24.04 / Docker Compose / Coolify Traefik
- 发布状态已部署并验证2026-05-19,主体元素改为按套图文件夹分组展示,主体生成接口提交后立即返回 queued 占位并后台逐视角生成、逐张回填;转换层为真人重构 / 卡通重构 / 元素重构 / 自主描述四个入口,每个入口最多 3 张参考帧;拖入只加入参考队列,点击生成后固定生成全新 6 视图;胶片双击/拖拽加入参考帧池 + 胶片缓存复用 + 音频解析失败可重试,右侧三栏主体管线:竖向参考帧池 + 转换层 + 主体元素,参考帧缩略图保持小尺寸 9:16 比例 + hover 左侧紧凑预览 + 转换层多参考滚动,旧主体模板区移出主路径 + 逐句时间轴移到原版视频下方并支持双行文案 + 波形同框时间对齐画面胶片 + 胶片密度按钮上移波形顶部 + 去分隔线 + 胶片上下错落 + body 顶层原位大放大 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401认证后首页 200容器内 `/health` 返回 `ok:true` - 发布状态已部署并验证2026-05-20,主体元素按套图文件夹分组展示,主体生成接口提交后立即返回 queued 占位并后台逐视角生成、逐张回填;右侧三栏主体管线为竖向参考帧池 + 对话式转换层 + 主体元素;转换层只保留一个参考区和生图对话,不再显示方向卡片、卡通风格下拉或单独数量按钮,方向、数量、风格、服装统一、人物占比和保留/删除元素都由对话识别后写入 `Job.subject_agent`;胶片双击/拖拽加入参考帧池 + 胶片缓存复用 + 音频解析失败可重试,参考帧缩略图保持小尺寸 9:16 比例 + hover 左侧紧凑预览 + 转换层多参考滚动,旧主体模板区移出主路径 + 逐句时间轴移到原版视频下方并支持双行文案 + 波形同框时间对齐画面胶片 + 胶片密度按钮上移波形顶部 + 去分隔线 + 胶片上下错落 + body 顶层原位大放大 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401认证后首页 200容器内 `/health` 返回 `ok:true`
- 最近部署验证2026-05-19`fd794e3` 已推送并部署到 `/opt/skg-marketing-studio`;生产 `/health` 显示 `image=gpt-image-2``subject_image=gpt-image-2``image_request_timeout_seconds=60``image_base_url=https://ai.skg.com/ezlink/v1`。容器内最小文字生图探针在 20 秒限制下返回 `ReadTimeout`,说明当前阻塞点是 `https://ai.skg.com/ezlink/v1``gpt-image-2` 上游通道超时,服务端不会更换图片模型。 - 最近部署验证2026-05-19`fd794e3` 已推送并部署到 `/opt/skg-marketing-studio`;生产 `/health` 显示 `image=gpt-image-2``subject_image=gpt-image-2``image_request_timeout_seconds=60``image_base_url=https://ai.skg.com/ezlink/v1`。容器内最小文字生图探针在 20 秒限制下返回 `ReadTimeout`,说明当前阻塞点是 `https://ai.skg.com/ezlink/v1``gpt-image-2` 上游通道超时,服务端不会更换图片模型。
- 最近部署验证2026-05-19`3756259` 已推送并部署到 `/opt/skg-marketing-studio`;生产 `/health` 显示 `image=gpt-image-2``image_fallbacks=['gemini-3-pro-image-preview']``subject_image_fallbacks=['gpt-image-2','gemini-3-pro-image-preview']`、短时熔断阈值 2 次 / 600 秒。线上真实探针确认 `gpt-image-2` 读超时后同次调用可自动兜底到 `gemini-3-pro-image-preview` 并返回图片;模拟探针确认连续 2 次主模型失败后第三次直接走 Gemini。 - 最近部署验证2026-05-19`3756259` 已推送并部署到 `/opt/skg-marketing-studio`;生产 `/health` 显示 `image=gpt-image-2``image_fallbacks=['gemini-3-pro-image-preview']``subject_image_fallbacks=['gpt-image-2','gemini-3-pro-image-preview']`、短时熔断阈值 2 次 / 600 秒。线上真实探针确认 `gpt-image-2` 读超时后同次调用可自动兜底到 `gemini-3-pro-image-preview` 并返回图片;模拟探针确认连续 2 次主模型失败后第三次直接走 Gemini。
- 最近部署验证2026-05-20`c245bff` 已推送并部署到 `/opt/skg-marketing-studio`;本地 `python3 -m py_compile api/main.py``web/npm run build` 通过,生产 Docker 重建后 `./scripts/verify-prod-docker.sh` 通过web 容器 Up、API 容器 Up、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok`)。容器内模型偏好探针确认转换层 `image_model_preference` 路由:`auto -> ['gpt-image-2','gemini-3-pro-image-preview']``gpt-image-2 -> ['gpt-image-2']``gemini-3-pro-image-preview -> ['gemini-3-pro-image-preview']` - 最近部署验证2026-05-20`c245bff` 已推送并部署到 `/opt/skg-marketing-studio`;本地 `python3 -m py_compile api/main.py``web/npm run build` 通过,生产 Docker 重建后 `./scripts/verify-prod-docker.sh` 通过web 容器 Up、API 容器 Up、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok`)。容器内模型偏好探针确认转换层 `image_model_preference` 路由:`auto -> ['gpt-image-2','gemini-3-pro-image-preview']``gpt-image-2 -> ['gpt-image-2']``gemini-3-pro-image-preview -> ['gemini-3-pro-image-preview']`

View File

@@ -107,6 +107,7 @@ IMAGE_MODEL = GPT_IMAGE_MODEL
PRODUCT_VIEW_MODEL = GPT_IMAGE_MODEL PRODUCT_VIEW_MODEL = GPT_IMAGE_MODEL
SUBJECT_ASSET_IMAGE_MODEL = GPT_IMAGE_MODEL SUBJECT_ASSET_IMAGE_MODEL = GPT_IMAGE_MODEL
SubjectModelBundle = Literal["gpt", "gemini"] SubjectModelBundle = Literal["gpt", "gemini"]
SubjectAgentMode = Literal["realistic", "cartoon", "elements", "custom"]
SUBJECT_AGENT_GPT_MODEL = gpt_model_env("SUBJECT_AGENT_GPT_MODEL", VISION_MODEL) SUBJECT_AGENT_GPT_MODEL = gpt_model_env("SUBJECT_AGENT_GPT_MODEL", VISION_MODEL)
SUBJECT_AGENT_GEMINI_MODEL = os.getenv("SUBJECT_AGENT_GEMINI_MODEL", "gemini-2.5-flash").strip() or "gemini-2.5-flash" SUBJECT_AGENT_GEMINI_MODEL = os.getenv("SUBJECT_AGENT_GEMINI_MODEL", "gemini-2.5-flash").strip() or "gemini-2.5-flash"
SUBJECT_ASSET_IMAGE_MODELS = [GPT_IMAGE_MODEL] + ( SUBJECT_ASSET_IMAGE_MODELS = [GPT_IMAGE_MODEL] + (
@@ -766,7 +767,7 @@ class SubjectAgentState(BaseModel):
source_frame_indices: list[int] = Field(default_factory=list) source_frame_indices: list[int] = Field(default_factory=list)
analysis: SubjectAgentAnalysis | None = None analysis: SubjectAgentAnalysis | None = None
messages: list[SubjectAgentMessage] = Field(default_factory=list) messages: list[SubjectAgentMessage] = Field(default_factory=list)
selected_mode: Literal["realistic", "cartoon", "elements", "custom"] = "custom" selected_mode: SubjectAgentMode = "custom"
selected_traits: list[str] = Field(default_factory=list) selected_traits: list[str] = Field(default_factory=list)
requirements_zh: str = "" requirements_zh: str = ""
generation_prompt_en: str = "" generation_prompt_en: str = ""
@@ -4103,27 +4104,85 @@ def _subject_agent_analysis(job_id: str, source_indices: list[int], bundle: Subj
) )
def _subject_agent_message_update(state: SubjectAgentState, user_message: str) -> tuple[str, str, str, int, list[str]]: _SUBJECT_AGENT_MODES: set[str] = {"realistic", "cartoon", "elements", "custom"}
def _subject_agent_quantity_from_text(text: str, fallback: int) -> int:
quantity = max(1, min(10, int(fallback or 6)))
text = text or ""
if re.fullmatch(r"\s*\d{1,2}\s*", text):
return max(1, min(10, int(text.strip())))
digit_match = re.search(r"(\d{1,2})\s*(?:张|个|视图|张图|图|views?)", text, flags=re.I)
if digit_match:
return max(1, min(10, int(digit_match.group(1))))
cn_numbers = {
"": 1,
"": 2,
"": 2,
"": 3,
"": 4,
"": 5,
"": 6,
"": 7,
"": 8,
"": 9,
"": 10,
}
cn_match = re.search(r"([一二两三四五六七八九十])\s*(?:张|个|视图|张图|图)", text)
if cn_match:
return max(1, min(10, cn_numbers.get(cn_match.group(1), quantity)))
return quantity
def _subject_agent_mode_from_text(text: str, fallback: SubjectAgentMode = "custom") -> SubjectAgentMode:
compact = re.sub(r"\s+", "", text or "").lower()
if re.search(r"卡通|动画|插画|公仔|潮玩|二次元|cartoon|anime|illustration|toy|stylized", compact):
return "cartoon"
if re.search(r"创意复刻|创意模式|元素|参考创新|不像|换人|全新主体|全新人物|不同人|newperson|newactor|concept|element", compact):
return "elements"
if re.search(r"形象锁定|复刻这个人|复刻形象|同一主体|同一个人|保持这个人|保持原主体|完全复刻|source locked|same subject|sameperson", compact):
return "realistic"
if re.search(r"自主描述|只按文字|不依赖|不用参考|按描述|fromdescription|custom", compact):
return "custom"
return fallback
def _subject_agent_mode_from_value(value: object, fallback: SubjectAgentMode) -> SubjectAgentMode:
text = str(value or "").strip()
return text if text in _SUBJECT_AGENT_MODES else fallback
def _subject_agent_message_update(state: SubjectAgentState, user_message: str) -> tuple[str, str, str, int, list[str], SubjectAgentMode]:
current_req = state.requirements_zh.strip() current_req = state.requirements_zh.strip()
selected_traits = state.selected_traits[:20] selected_traits = state.selected_traits[:20]
quantity = max(1, min(10, int(state.quantity or 6))) quantity = _subject_agent_quantity_from_text(user_message, int(state.quantity or 6))
qty_match = re.search(r"(\d{1,2})\s*张", user_message) selected_mode = _subject_agent_mode_from_text(user_message, state.selected_mode)
if qty_match:
quantity = max(1, min(10, int(qty_match.group(1))))
fallback_req = "".join(part for part in [current_req, user_message.strip()] if part).strip("") fallback_req = "".join(part for part in [current_req, user_message.strip()] if part).strip("")
mode_label = {
"realistic": "source-locked same visible subject reconstruction",
"cartoon": "cartoon or stylized reconstruction",
"elements": "creative element reconstruction with a different new subject",
"custom": "custom description driven subject generation",
}.get(selected_mode, "custom description driven subject generation")
fallback_prompt = _ensure_english( fallback_prompt = _ensure_english(
"Subject image generation requirements: " "Subject image generation requirements: "
+ (fallback_req or "create a consistent SKG ad subject pack") + (fallback_req or "create a consistent SKG ad subject pack")
+ f". Direction mode: {mode_label}."
+ f" Generate exactly {quantity} separate views."
+ ". Keep one identity and one outfit bible across all generated views. " + ". Keep one identity and one outfit bible across all generated views. "
+ (f"Selected traits: {', '.join(selected_traits)}." if selected_traits else "") + (f"Selected traits: {', '.join(selected_traits)}." if selected_traits else "")
) )
if not LLM_API_KEY: if not LLM_API_KEY:
return "已记录这条生图要求。继续补充要保留/删除的元素,确认后我会按当前要求生成。", fallback_req, fallback_prompt, quantity, selected_traits return "已记录这条生图要求。继续补充要保留/删除的元素,确认后我会按当前要求生成。", fallback_req, fallback_prompt, quantity, selected_traits, selected_mode
system = ( system = (
"You are an SKG subject image-generation requirements agent. Your scope is only image generation for a subject view pack. " "You are an SKG subject image-generation requirements agent. Your scope is only image generation for a subject view pack. "
"Do not answer unrelated video, audio, download, coding, copywriting, or general chat requests; redirect to subject image requirements. " "Do not answer unrelated video, audio, download, coding, copywriting, or general chat requests; redirect to subject image requirements. "
"Normalize the user's fuzzy Chinese request into precise generation constraints. " "Normalize the user's fuzzy Chinese request into precise generation constraints. "
"Return strict JSON with keys: assistant_message_zh, updated_requirements_zh, generation_prompt_en, quantity, selected_traits. " "Infer selected_mode from the conversation. Allowed selected_mode values are realistic, cartoon, elements, custom. "
"Use realistic when the user wants to lock or replicate the visible reference subject; cartoon for stylized/cartoon/toy/illustration; "
"elements when the user wants the creative logic but a different new subject; custom when the user wants free text generation without relying on references. "
"Infer quantity from Chinese or English requests such as 4张, 六视图, generate 8 views. "
"Return strict JSON with keys: assistant_message_zh, updated_requirements_zh, generation_prompt_en, quantity, selected_traits, selected_mode. "
"generation_prompt_en must be English and must enforce: one consistent identity, one consistent outfit bible, neck/shoulder readability, no text/watermarks/UI, and legal-safe reconstruction." "generation_prompt_en must be English and must enforce: one consistent identity, one consistent outfit bible, neck/shoulder readability, no text/watermarks/UI, and legal-safe reconstruction."
) )
user_payload = { user_payload = {
@@ -4153,12 +4212,13 @@ def _subject_agent_message_update(state: SubjectAgentState, user_message: str) -
assistant = str(data.get("assistant_message_zh") or "已记录这条生图要求。").strip()[:1200] assistant = str(data.get("assistant_message_zh") or "已记录这条生图要求。").strip()[:1200]
updated_req = str(data.get("updated_requirements_zh") or fallback_req).strip()[:2200] updated_req = str(data.get("updated_requirements_zh") or fallback_req).strip()[:2200]
prompt_en = _ensure_english(str(data.get("generation_prompt_en") or fallback_prompt).strip())[:2600] prompt_en = _ensure_english(str(data.get("generation_prompt_en") or fallback_prompt).strip())[:2600]
out_quantity = max(1, min(10, int(data.get("quantity") or quantity))) out_quantity = _subject_agent_quantity_from_text(str(data.get("quantity") or ""), quantity)
out_traits = _list_of_strings(data.get("selected_traits"), 24) or selected_traits out_traits = _list_of_strings(data.get("selected_traits"), 24) or selected_traits
return assistant, updated_req, prompt_en, out_quantity, out_traits out_mode = _subject_agent_mode_from_value(data.get("selected_mode"), selected_mode)
return assistant, updated_req, prompt_en, out_quantity, out_traits, out_mode
except Exception as e: except Exception as e:
print(f"[subject agent message failed] bundle={state.model_bundle} error={e}", flush=True) print(f"[subject agent message failed] bundle={state.model_bundle} error={e}", flush=True)
return "已先按本地规则记录这条要求;模型回复失败时仍可直接生成。", fallback_req, fallback_prompt, quantity, selected_traits return "已先按本地规则记录这条要求;模型回复失败时仍可直接生成。", fallback_req, fallback_prompt, quantity, selected_traits, selected_mode
# ---------- API 路由 ---------- # ---------- API 路由 ----------
@@ -4179,7 +4239,7 @@ class SubjectAgentMessageReq(BaseModel):
model_bundle: SubjectModelBundle = "gpt" model_bundle: SubjectModelBundle = "gpt"
source_frame_indices: list[int] = Field(default_factory=list) source_frame_indices: list[int] = Field(default_factory=list)
selected_mode: Literal["realistic", "cartoon", "elements", "custom"] = "custom" selected_mode: SubjectAgentMode = "custom"
selected_traits: list[str] = Field(default_factory=list) selected_traits: list[str] = Field(default_factory=list)
requirements_zh: str = "" requirements_zh: str = ""
message: str = "" message: str = ""
@@ -4666,7 +4726,7 @@ def analyze_subject_agent(job_id: str, req: SubjectAgentAnalyzeReq) -> Job:
state = job.subject_agent.model_copy(deep=True) state = job.subject_agent.model_copy(deep=True)
assistant_text = ( assistant_text = (
f"我已用 {req.model_bundle.upper()} 套件分析这些参考帧。" f"我已用 {req.model_bundle.upper()} 套件分析这些参考帧。"
"你可以选择形象锁定、创意复刻、元素混合或自主描述,也可以继续告诉我要改数量、风格、服装人物大小。" "接下来直接告诉我要复刻形象、卡通化、参考创意换新人,还是只按文字生成;数量、风格、服装人物大小也都写在对话里"
) )
messages = (state.messages + [SubjectAgentMessage(role="assistant", content=assistant_text, created_at=time.time())])[-30:] messages = (state.messages + [SubjectAgentMessage(role="assistant", content=assistant_text, created_at=time.time())])[-30:]
state = state.model_copy(update={ state = state.model_copy(update={
@@ -4689,10 +4749,11 @@ def message_subject_agent(job_id: str, req: SubjectAgentMessageReq) -> Job:
raise HTTPException(404, "job not found") raise HTTPException(404, "job not found")
state = job.subject_agent.model_copy(deep=True) state = job.subject_agent.model_copy(deep=True)
source_indices = [idx for idx in req.source_frame_indices if any(frame.index == idx for frame in job.frames)][:8] source_indices = [idx for idx in req.source_frame_indices if any(frame.index == idx for frame in job.frames)][:8]
fallback_mode = req.selected_mode or state.selected_mode
state = state.model_copy(update={ state = state.model_copy(update={
"model_bundle": req.model_bundle, "model_bundle": req.model_bundle,
"source_frame_indices": source_indices or state.source_frame_indices, "source_frame_indices": source_indices or state.source_frame_indices,
"selected_mode": req.selected_mode, "selected_mode": fallback_mode,
"selected_traits": [str(item).strip()[:80] for item in req.selected_traits if str(item).strip()][:24], "selected_traits": [str(item).strip()[:80] for item in req.selected_traits if str(item).strip()][:24],
"requirements_zh": req.requirements_zh.strip()[:2200] or state.requirements_zh, "requirements_zh": req.requirements_zh.strip()[:2200] or state.requirements_zh,
"quantity": max(1, min(10, int(req.quantity or state.quantity or 6))), "quantity": max(1, min(10, int(req.quantity or state.quantity or 6))),
@@ -4700,7 +4761,7 @@ def message_subject_agent(job_id: str, req: SubjectAgentMessageReq) -> Job:
user_message = req.message.strip() user_message = req.message.strip()
if not user_message: if not user_message:
user_message = state.requirements_zh or "按当前设置准备主体套图生成要求" user_message = state.requirements_zh or "按当前设置准备主体套图生成要求"
assistant_text, requirements_zh, prompt_en, quantity, selected_traits = _subject_agent_message_update(state, user_message) assistant_text, requirements_zh, prompt_en, quantity, selected_traits, selected_mode = _subject_agent_message_update(state, user_message)
messages = ( messages = (
state.messages state.messages
+ [SubjectAgentMessage(role="user", content=user_message, created_at=time.time())] + [SubjectAgentMessage(role="user", content=user_message, created_at=time.time())]
@@ -4709,6 +4770,7 @@ def message_subject_agent(job_id: str, req: SubjectAgentMessageReq) -> Job:
state = state.model_copy(update={ state = state.model_copy(update={
"requirements_zh": requirements_zh, "requirements_zh": requirements_zh,
"generation_prompt_en": prompt_en, "generation_prompt_en": prompt_en,
"selected_mode": selected_mode,
"quantity": quantity, "quantity": quantity,
"selected_traits": selected_traits, "selected_traits": selected_traits,
"messages": messages, "messages": messages,

File diff suppressed because one or more lines are too long

View File

@@ -1147,7 +1147,9 @@ function buildReconstructionDirection(
} else if (mode === "cartoon") { } else if (mode === "cartoon") {
common.push( common.push(
"Direction mode: cartoon reconstruction.", "Direction mode: cartoon reconstruction.",
`Cartoon style: ${style.label}; ${style.prompt}.`, trimmed
? `Cartoon style: follow the user's requested style from the direction text; if no explicit cartoon style is specified, use ${style.label}; ${style.prompt}.`
: `Cartoon style: ${style.label}; ${style.prompt}.`,
"Transform broad pose, emotion, body-readability, and ad energy into a fully original stylized character, not a realistic human and not a traced version of the source.", "Transform broad pose, emotion, body-readability, and ad energy into a fully original stylized character, not a realistic human and not a traced version of the source.",
) )
} else if (mode === "elements") { } else if (mode === "elements") {
@@ -3316,8 +3318,7 @@ function SourceSubjectPipeline({
const [agentInput, setAgentInput] = useState("") const [agentInput, setAgentInput] = useState("")
const [subjectAgentBusy, setSubjectAgentBusy] = useState<"analyze" | "message" | null>(null) const [subjectAgentBusy, setSubjectAgentBusy] = useState<"analyze" | "message" | null>(null)
const [promptMemoryByMode, setPromptMemoryByMode] = useState<Record<SubjectReconstructionMode, string[]>>(() => loadSubjectPromptMemory(job.id)) const [promptMemoryByMode, setPromptMemoryByMode] = useState<Record<SubjectReconstructionMode, string[]>>(() => loadSubjectPromptMemory(job.id))
const [cartoonStyle, setCartoonStyle] = useState<CartoonReconstructionStyle>("3d_animation") const [cartoonStyle] = useState<CartoonReconstructionStyle>("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) const [subjectBusyFor, setSubjectBusyFor] = useState<{ jobId: string; jobLabel: string; mode: SubjectReconstructionMode; viewCount: number; sourceCount: number; profileLabel: string; modelLabel: string } | null>(null)
const [subjectAssetBusy, setSubjectAssetBusy] = useState<string | null>(null) const [subjectAssetBusy, setSubjectAssetBusy] = useState<string | null>(null)
const [expandedSubjectPackKey, setExpandedSubjectPackKey] = useState<string | null>(null) const [expandedSubjectPackKey, setExpandedSubjectPackKey] = useState<string | null>(null)
@@ -3417,7 +3418,6 @@ function SourceSubjectPipeline({
setLastSubjectProfile(null) setLastSubjectProfile(null)
setSubjectBusyFor(null) setSubjectBusyFor(null)
setSubjectAssetBusy(null) setSubjectAssetBusy(null)
setCartoonStyleOpen(false)
setExpandedSubjectPackKey(null) setExpandedSubjectPackKey(null)
}, [job.id]) }, [job.id])
@@ -3917,72 +3917,20 @@ function SourceSubjectPipeline({
</button> </button>
</div> </div>
<div className="mt-2 grid grid-cols-2 gap-1"> <div className="mt-2 rounded-md border border-[#d6b36a]/25 bg-[#d6b36a]/[0.065] px-2.5 py-2">
{RECONSTRUCTION_MODES.map((modeConfig) => ( <div className="flex flex-wrap items-center gap-1.5">
<button <span className="rounded-full border border-[#d6b36a]/35 bg-black/28 px-2 py-0.5 text-[9.5px] font-semibold text-[#f4dd98]">
key={modeConfig.value} {reconstructionModeConfig(agentMode).label}
type="button" </span>
onClick={() => setAgentMode(modeConfig.value)} <span className="rounded-full border border-white/10 bg-black/28 px-2 py-0.5 text-[9.5px] font-semibold text-white/58">
className={`rounded-md border px-2 py-1.5 text-left transition ${ {selectedSubjectViews.length}
agentMode === modeConfig.value </span>
? "border-[#d6b36a]/70 bg-[#d6b36a]/14 text-white" <span className="rounded-full border border-white/10 bg-black/28 px-2 py-0.5 text-[9.5px] text-white/42">
: "border-white/10 bg-black/24 text-white/48 hover:border-white/22 hover:text-white"
}`} </span>
title={modeConfig.subtitle}
>
<span className="block text-[10.5px] font-semibold">{modeConfig.label}</span>
<span className="mt-0.5 block truncate text-[8.5px] text-white/34">{modeConfig.subtitle}</span>
</button>
))}
</div>
{agentMode === "cartoon" ? (
<div className="mt-2 rounded-md border border-white/10 bg-black/24 p-1.5">
<button
type="button"
onClick={() => setCartoonStyleOpen((open) => !open)}
className="inline-flex h-7 w-full items-center justify-between rounded-md border border-white/10 bg-black/28 px-2 text-[10px] font-semibold text-white/58 transition hover:border-white/20 hover:text-white"
>
<span>{cartoonStyleConfig(cartoonStyle).label}</span>
<ChevronDown className={`h-3.5 w-3.5 transition ${cartoonStyleOpen ? "rotate-180" : ""}`} />
</button>
{cartoonStyleOpen ? (
<div className="mt-1 grid grid-cols-2 gap-1">
{CARTOON_RECONSTRUCTION_STYLES.map((style) => (
<button
key={style.value}
type="button"
onClick={() => {
setCartoonStyle(style.value)
setCartoonStyleOpen(false)
}}
className={`h-7 rounded-md border px-1.5 text-[9.5px] font-semibold transition ${
cartoonStyle === style.value ? "border-white bg-white text-black" : "border-white/10 bg-black/24 text-white/45 hover:text-white"
}`}
>
{style.label}
</button>
))}
</div>
) : null}
</div> </div>
) : null} <div className="mt-1.5 text-[9.5px] leading-snug text-white/42">
/ / /
<div className="mt-2 flex items-center justify-between gap-2 rounded-md border border-white/10 bg-black/24 px-2 py-1.5">
<span className="text-[10px] font-semibold text-white/58"></span>
<div className="flex gap-1">
{[4, 6, 8, 10].map((count) => (
<button
key={count}
type="button"
onClick={() => setAgentQuantity(count)}
className={`h-6 rounded-full border px-2 text-[9.5px] font-semibold transition ${
agentQuantity === count ? "border-white bg-white text-black" : "border-white/10 bg-black/24 text-white/45 hover:text-white"
}`}
>
{count}
</button>
))}
</div> </div>
</div> </div>