feat: make subject conversion dialog-driven
This commit is contained in:
@@ -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 @@
|
|||||||
## 最近助手会话概览
|
## 最近助手会话概览
|
||||||
|
|
||||||
- Claude:be53a031-9311-4ee8-b822-d4cfb4f5e78b · 时间未知
|
- Claude:be53a031-9311-4ee8-b822-d4cfb4f5e78b · 时间未知
|
||||||
- Codex:019e3dae-6045-7203-bf4e-8fbeae65cadf · 时间未知
|
- Codex:019e3db1-012e-7163-bc78-acf7cde326e7 · 时间未知
|
||||||
- Cursor:3e4af548-3b65-45a5-8698-6e75510f24b5 · May 19, 2026 at 08:43
|
- Cursor:未找到匹配当前项目的最近会话
|
||||||
|
|
||||||
## Claude 最近会话
|
## Claude 最近会话
|
||||||
|
|
||||||
@@ -44,64 +44,37 @@
|
|||||||
|
|
||||||
## Codex 最近会话
|
## Codex 最近会话
|
||||||
|
|
||||||
- Session ID:019e3dae-6045-7203-bf4e-8fbeae65cadf
|
- Session ID:019e3db1-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 ID:3e4af548-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
|
||||||
|
|
||||||
## 统一接力要求
|
## 统一接力要求
|
||||||
|
|
||||||
|
|||||||
1696
.memory/worklog.json
1696
.memory/worklog.json
File diff suppressed because it is too large
Load Diff
2
RULES.md
2
RULES.md
@@ -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']`。
|
||||||
|
|||||||
92
api/main.py
92
api/main.py
@@ -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
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user