fix: make conversion trait chips instant
This commit is contained in:
@@ -1,32 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "58c3e5c",
|
||||
"message": "feat: manage subject view thumbnails",
|
||||
"ts": "2026-05-18T06:38:01+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 2,
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:feat: manage subject view thumbnails",
|
||||
"ts": "2026-05-17T22:43:44Z",
|
||||
"type": "session-heartbeat"
|
||||
},
|
||||
{
|
||||
"files_changed": 3,
|
||||
"hash": "a33287f",
|
||||
"message": "auto-save 2026-05-18 06:44 (~3)",
|
||||
"ts": "2026-05-18T06:44:09+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 2,
|
||||
"hash": "e07af78",
|
||||
"message": "fix: use image edits for gpt references",
|
||||
"ts": "2026-05-18T06:48:19+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:fix: use image edits for gpt references",
|
||||
@@ -3250,6 +3223,33 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 4 项未提交变更 · 最近提交:auto-save 2026-05-20 17:00 (~2)",
|
||||
"files_changed": 4
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T17:06:19+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-20 17:06 (~4)",
|
||||
"hash": "32a68be",
|
||||
"files_changed": 4
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T17:07:12+08:00",
|
||||
"type": "commit",
|
||||
"message": "feat: simplify subject conversion composer",
|
||||
"hash": "10d955c",
|
||||
"files_changed": 4
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T17:09:06+08:00",
|
||||
"type": "commit",
|
||||
"message": "docs: record conversion composer control deployment",
|
||||
"hash": "fc3e64d",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T09:15:30Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:docs: record conversion composer control deployment",
|
||||
"files_changed": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
RULES.md
2
RULES.md
@@ -11,7 +11,7 @@
|
||||
- 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解
|
||||
- 风格:`04-Dark-Gallery-Ambient`(路径:`~/Projects/research/20260305-网页风格库/04-Dark-Gallery-Ambient.md`)
|
||||
- 第一冲刺:步骤 1-4(下载 / 拆轨 / 关键帧 / ASR+翻译)
|
||||
- 当前产品方向(2026-05-20 再确认):信息流广告快速复刻默认进入“三字段候选生成”工作流。主界面为“左侧素材输入列 + 右侧信息流复刻工作表”。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动两条路:音频文案路提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效;视频视觉路自动抽取参考帧。源视频工作区右侧主体链路是“参考帧池 → 转换层 → 主体元素”:参考帧池竖向排列;转换层是轻量对话式生图确认区,参考图可通过左侧缩略图 `+`、参考帧拖拽、胶片拖拽或本地图片拖入进入转换层,用户选择 GPT/Gemini 套件后先分析参考图,再在下方消息输入区发送复刻/创新/卡通和画面要求,生成数量通过发送区旁边的张数控件控制;后端返回英文出图 prompt 后必须弹窗确认,用户点确认才生成对应数量的统一多角度套图。右侧主体元素区的套图输出、文件夹分组、单张重生、删除和 hover 预览逻辑保持不变。旧下方“相似主体 / 主体模板库”不再作为主路径。波形下方的画面胶片只是临时预览,点击只跳转原视频时间点,双击或拖进参考帧池才正式加入关键帧,已加入的胶片直接显示“已添加”。产品图上传后独立形成产品资产包,自动识别视角/结构/比例并补缺角度。分镜工作台按逐句时间轴默认只露“文案 / 场景一句话 / 人物+产品+动作”,产品素材池、批量控制、三字段、视频候选和高级区都必须可折叠;视频候选无内容时默认不占大面积,有候选时默认只显示迷你缩略条,展开后才显示 4-grid。单条默认生成 4 个视频候选,顶部支持整片批量生成候选;首尾帧、视觉规划、产品出现方式和旧 6 字段保留在“高级”抽屉与后端 quick-plan 自动展开中,不能再作为客户默认闸门。
|
||||
- 当前产品方向(2026-05-20 再确认):信息流广告快速复刻默认进入“三字段候选生成”工作流。主界面为“左侧素材输入列 + 右侧信息流复刻工作表”。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动两条路:音频文案路提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效;视频视觉路自动抽取参考帧。源视频工作区右侧主体链路是“参考帧池 → 转换层 → 主体元素”:参考帧池竖向排列;转换层是轻量对话式生图确认区,参考图可通过左侧缩略图 `+`、参考帧拖拽、胶片拖拽或本地图片拖入进入转换层,用户选择 GPT/Gemini 套件后先分析参考图;识别结果里的特征 chip 只作为“保留元素”本地选择,点亮=保留、再点取消,点击不立即请求模型,随下一条发送消息提交;用户再在下方消息输入区发送复刻/创新/卡通和画面要求,生成数量通过发送区旁边的张数控件控制;后端返回英文出图 prompt 后必须弹窗确认,用户点确认才生成对应数量的统一多角度套图。右侧主体元素区的套图输出、文件夹分组、单张重生、删除和 hover 预览逻辑保持不变。旧下方“相似主体 / 主体模板库”不再作为主路径。波形下方的画面胶片只是临时预览,点击只跳转原视频时间点,双击或拖进参考帧池才正式加入关键帧,已加入的胶片直接显示“已添加”。产品图上传后独立形成产品资产包,自动识别视角/结构/比例并补缺角度。分镜工作台按逐句时间轴默认只露“文案 / 场景一句话 / 人物+产品+动作”,产品素材池、批量控制、三字段、视频候选和高级区都必须可折叠;视频候选无内容时默认不占大面积,有候选时默认只显示迷你缩略条,展开后才显示 4-grid。单条默认生成 4 个视频候选,顶部支持整片批量生成候选;首尾帧、视觉规划、产品出现方式和旧 6 字段保留在“高级”抽屉与后端 quick-plan 自动展开中,不能再作为客户默认闸门。
|
||||
|
||||
## 部署事实
|
||||
- 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3323,6 +3323,7 @@ function SourceSubjectPipeline({
|
||||
const [agentQuantity, setAgentQuantity] = useState(() => job.subject_agent?.quantity ?? 6)
|
||||
const [agentRequirement, setAgentRequirement] = useState(() => job.subject_agent?.requirements_zh ?? "")
|
||||
const [agentPrompt, setAgentPrompt] = useState(() => job.subject_agent?.generation_prompt_en ?? "")
|
||||
const [agentSelectedTraits, setAgentSelectedTraits] = useState<string[]>(() => job.subject_agent?.selected_traits ?? [])
|
||||
const [agentInput, setAgentInput] = useState("")
|
||||
const [subjectAgentBusy, setSubjectAgentBusy] = useState<"analyze" | "message" | null>(null)
|
||||
const [promptConfirmOpen, setPromptConfirmOpen] = useState(false)
|
||||
@@ -3344,6 +3345,10 @@ function SourceSubjectPipeline({
|
||||
.filter((frame): frame is KeyFrame => !!frame),
|
||||
[agentReferenceFrameIndices, frames],
|
||||
)
|
||||
const persistedAgentSelectedTraits = job.subject_agent?.selected_traits ?? []
|
||||
const agentSelectedTraitsDirty = agentSelectedTraits.length !== persistedAgentSelectedTraits.length
|
||||
|| agentSelectedTraits.some((trait) => !persistedAgentSelectedTraits.includes(trait))
|
||||
|| persistedAgentSelectedTraits.some((trait) => !agentSelectedTraits.includes(trait))
|
||||
const actorSources = useMemo(() => {
|
||||
const items: Array<{ frame: KeyFrame; element: KeyElement; mode: SubjectReconstructionMode }> = []
|
||||
for (const frame of frames) {
|
||||
@@ -3422,6 +3427,7 @@ function SourceSubjectPipeline({
|
||||
setAgentQuantity(job.subject_agent?.quantity ?? 6)
|
||||
setAgentRequirement(job.subject_agent?.requirements_zh ?? "")
|
||||
setAgentPrompt(job.subject_agent?.generation_prompt_en ?? "")
|
||||
setAgentSelectedTraits(job.subject_agent?.selected_traits ?? [])
|
||||
setAgentInput("")
|
||||
setSubjectAgentBusy(null)
|
||||
setPromptConfirmOpen(false)
|
||||
@@ -3440,6 +3446,7 @@ function SourceSubjectPipeline({
|
||||
setAgentQuantity(agent?.quantity ?? 6)
|
||||
setAgentRequirement(agent?.requirements_zh ?? "")
|
||||
setAgentPrompt(agent?.generation_prompt_en ?? "")
|
||||
setAgentSelectedTraits(agent?.selected_traits ?? [])
|
||||
}, [job.id, job.subject_agent?.updated_at])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -3789,6 +3796,7 @@ function SourceSubjectPipeline({
|
||||
source_frame_indices: agentReferenceFrameIndices,
|
||||
})
|
||||
onJobUpdate(updated)
|
||||
setAgentSelectedTraits(updated.subject_agent?.selected_traits ?? [])
|
||||
toast.success("转换层分析完成")
|
||||
} catch (e) {
|
||||
toast.error("转换层分析失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
@@ -3799,8 +3807,8 @@ function SourceSubjectPipeline({
|
||||
|
||||
const sendSubjectAgentRequirement = async (message = agentInput) => {
|
||||
const text = message.trim()
|
||||
if (!text && !agentRequirement.trim()) {
|
||||
toast.warning("先写一句要怎么生成,或者点快捷选项。")
|
||||
if (!text && !agentRequirement.trim() && !agentSelectedTraits.length && !agentSelectedTraitsDirty) {
|
||||
toast.warning("先写一句要怎么生成,或者选择要保留的识别元素。")
|
||||
return
|
||||
}
|
||||
setSubjectAgentBusy("message")
|
||||
@@ -3809,7 +3817,7 @@ function SourceSubjectPipeline({
|
||||
model_bundle: subjectModelBundle,
|
||||
source_frame_indices: agentReferenceFrameIndices,
|
||||
selected_mode: agentMode,
|
||||
selected_traits: job.subject_agent?.selected_traits ?? [],
|
||||
selected_traits: agentSelectedTraits,
|
||||
requirements_zh: agentRequirement,
|
||||
message: text,
|
||||
quantity: agentQuantity,
|
||||
@@ -3822,6 +3830,7 @@ function SourceSubjectPipeline({
|
||||
setAgentRequirement(nextAgent.requirements_zh)
|
||||
setAgentPrompt(nextAgent.generation_prompt_en)
|
||||
setAgentReferenceFrameIndices(nextAgent.source_frame_indices)
|
||||
setAgentSelectedTraits(nextAgent.selected_traits ?? [])
|
||||
}
|
||||
setAgentInput("")
|
||||
setPromptConfirmOpen(true)
|
||||
@@ -3833,18 +3842,9 @@ function SourceSubjectPipeline({
|
||||
}
|
||||
|
||||
const toggleSubjectAgentTrait = (trait: string) => {
|
||||
const selected = job.subject_agent?.selected_traits ?? []
|
||||
const next = selected.includes(trait) ? selected.filter((item) => item !== trait) : [...selected, trait].slice(0, 24)
|
||||
void sendSubjectAgentMessage(job.id, {
|
||||
model_bundle: subjectModelBundle,
|
||||
source_frame_indices: agentReferenceFrameIndices,
|
||||
selected_mode: agentMode,
|
||||
selected_traits: next,
|
||||
requirements_zh: agentRequirement,
|
||||
message: next.includes(trait) ? `保留或强调:${trait}` : `不再强制:${trait}`,
|
||||
quantity: agentQuantity,
|
||||
}).then(onJobUpdate).catch((e) => {
|
||||
toast.error("特征选择失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
setAgentSelectedTraits((current) => {
|
||||
if (current.includes(trait)) return current.filter((item) => item !== trait)
|
||||
return [...current, trait].slice(0, 24)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3852,7 +3852,7 @@ function SourceSubjectPipeline({
|
||||
const agentAnalysis = subjectAgent?.analysis ?? null
|
||||
const agentMessages = subjectAgent?.messages ?? []
|
||||
const agentTraits = agentAnalysis?.trait_chips ?? []
|
||||
const selectedAgentTraits = subjectAgent?.selected_traits ?? []
|
||||
const selectedAgentTraits = agentSelectedTraits
|
||||
const effectiveAgentMode = subjectAgent?.selected_mode ?? agentMode
|
||||
const effectiveAgentQuantity = agentQuantity
|
||||
const effectiveAgentViews = subjectViewsForQuantity(effectiveAgentQuantity)
|
||||
@@ -4065,28 +4065,52 @@ function SourceSubjectPipeline({
|
||||
|
||||
{agentAnalysis ? (
|
||||
<div className="mt-2 rounded-md border border-emerald-200/18 bg-emerald-300/[0.055] p-2">
|
||||
<div className="text-[10px] font-semibold text-emerald-50/76">识别结果</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] font-semibold text-emerald-50/76">识别结果</span>
|
||||
<span className="text-[9px] text-white/34">
|
||||
点亮=保留 · 再点取消
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 max-h-16 overflow-auto text-[9.5px] leading-snug text-white/58">{agentAnalysis.summary_zh}</p>
|
||||
{agentTraits.length ? (
|
||||
<div className="mt-2 flex max-h-16 flex-wrap gap-1 overflow-auto">
|
||||
{agentTraits.slice(0, 12).map((trait) => {
|
||||
const active = selectedAgentTraits.includes(trait)
|
||||
return (
|
||||
<>
|
||||
<div className="mt-2 flex items-center justify-between gap-2 text-[9px]">
|
||||
<span className={selectedAgentTraitsDirty ? "text-cyan-100/56" : "text-white/34"}>
|
||||
保留元素 {selectedAgentTraits.length} 个{selectedAgentTraitsDirty ? " · 待发送" : ""}
|
||||
</span>
|
||||
{selectedAgentTraits.length ? (
|
||||
<button
|
||||
key={trait}
|
||||
type="button"
|
||||
onClick={() => toggleSubjectAgentTrait(trait)}
|
||||
className={`rounded-full border px-2 py-0.5 text-[9px] transition ${
|
||||
active
|
||||
? "border-emerald-100/60 bg-emerald-300/15 text-emerald-50"
|
||||
: "border-white/10 bg-black/26 text-white/46 hover:border-white/22 hover:text-white/70"
|
||||
}`}
|
||||
onClick={() => setAgentSelectedTraits([])}
|
||||
className="rounded border border-white/10 bg-black/24 px-1.5 py-0.5 text-white/42 transition hover:border-white/24 hover:text-white/70"
|
||||
>
|
||||
{trait}
|
||||
清空
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-1.5 flex max-h-[72px] flex-wrap gap-1 overflow-auto">
|
||||
{agentTraits.slice(0, 12).map((trait) => {
|
||||
const active = selectedAgentTraits.includes(trait)
|
||||
return (
|
||||
<button
|
||||
key={trait}
|
||||
type="button"
|
||||
onClick={() => toggleSubjectAgentTrait(trait)}
|
||||
aria-pressed={active}
|
||||
title={active ? "已作为保留元素,再点取消" : "点一下加入保留元素"}
|
||||
className={`inline-flex min-h-[22px] cursor-pointer items-center gap-1 rounded-full border px-2 py-0.5 text-[9px] transition ${
|
||||
active
|
||||
? "border-emerald-100/65 bg-emerald-300/16 text-emerald-50 shadow-[0_0_0_1px_rgba(167,243,208,0.12)]"
|
||||
: "border-white/10 bg-black/26 text-white/46 hover:border-white/22 hover:text-white/70"
|
||||
}`}
|
||||
>
|
||||
{active ? <Check className="h-2.5 w-2.5" /> : null}
|
||||
{trait}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -4168,7 +4192,7 @@ function SourceSubjectPipeline({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void sendSubjectAgentRequirement()}
|
||||
disabled={!!subjectAgentBusy || (!agentInput.trim() && !agentRequirement.trim())}
|
||||
disabled={!!subjectAgentBusy || (!agentInput.trim() && !agentRequirement.trim() && !selectedAgentTraits.length && !agentSelectedTraitsDirty)}
|
||||
className="skg-primary-action inline-flex h-8 flex-1 items-center justify-center gap-1.5 px-2 text-[10.5px] font-semibold transition disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{subjectAgentBusy === "message" ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />}
|
||||
|
||||
Reference in New Issue
Block a user