Compare commits

...

4 Commits

Author SHA1 Message Date
f38c524cbe docs: record video panel deploy 2026-05-22 13:02:12 +08:00
7abbb7d532 fix: dedupe suffixed video tasks 2026-05-22 12:59:12 +08:00
20d2d8fa68 auto-save 2026-05-22 12:50 (~2) 2026-05-22 12:50:09 +08:00
335231fc07 auto-save 2026-05-22 09:01 (~2) 2026-05-22 09:03:04 +08:00
5 changed files with 1868 additions and 1455 deletions

View File

@@ -1,6 +1,6 @@
# 项目接力 # 项目接力
- 生成时间May 21, 2026 at 21:43 - 生成时间May 22, 2026 at 12:48
- 项目AI玩具专利生成工作流 - 项目AI玩具专利生成工作流
- 路径:/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow - 路径:/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow
- 状态active - 状态active
@@ -9,7 +9,7 @@
## 最近助手会话概览 ## 最近助手会话概览
- Claudedf7c3755-a4d2-4e32-b68b-42bbaebc2fda · 时间未知 - Claudedf7c3755-a4d2-4e32-b68b-42bbaebc2fda · 时间未知
- Codex019e40f8-2f9a-73d0-af47-129bc741af46 · 时间未知 - Codex019e4ac6-ab79-74c1-8fc2-26545c4863df · 时间未知
- Cursor未找到匹配当前项目的最近会话 - Cursor未找到匹配当前项目的最近会话
## Claude 最近会话 ## Claude 最近会话
@@ -49,56 +49,52 @@
## Codex 最近会话 ## Codex 最近会话
- Session ID019e40f8-2f9a-73d0-af47-129bc741af46 - Session ID019e4ac6-ab79-74c1-8fc2-26545c4863df
- Transcript/Users/kangwan/.codex/sessions/2026/05/20/rollout-2026-05-20T00-01-07-019e40f8-2f9a-73d0-af47-129bc741af46.jsonl - Transcript/Users/kangwan/.codex/sessions/2026/05/21/rollout-2026-05-21T21-43-14-019e4ac6-ab79-74c1-8fc2-26545c4863df.jsonl
- 工作目录:/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow - 工作目录:/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow
- 分支master - 分支master
- 敏感字段:已对 token / key / password / secret 做脱敏 - 敏感字段:已对 token / key / password / secret 做脱敏
### 最近用户要求 ### 最近用户要求
- 排版麻烦你全部给我打乱,重新来 ,是有给你强制的结构框架构么?不需要现有的结构 你给我重新设计明白么? - 可以改原始的文件名
- 流程错误了 每一个先从专利包开始 都是一步做完才能做下一步 ,不能一按键全包 - OK 开始出一套吧 这应该是混合式ip了
- 视频模型先接上2315eb28-4db5-4907-a2bb-dcbb23b106f3 https://ark.cn-beijing.volces.com/api/v3 - 全称是这个“有你家族”猪小懂IP系列家族
### Codex 最近回复 ### Codex 最近回复
> 已把文档修正整理成语义 commit 并推送。生产运行态和生产 env 都确认是已验证可提交的 `doubao-seedance-2-0-260128`,没有使用那个无效 UUID。最后我再查公网入口和 git 状态 > 文字模板生成也在跑 GPT 文本模型,通常比图片快,等返回后我会做最终汇总验收
> 这个 UUID 我接不上,已经实测过了: > 已出完一套,生产项目是 `s_mpfnrr3k_f8e03a`,名称已统一为 `“有你家族”猪小懂IP系列家族`。
> >
> - `2315eb28-4db5-4907-a2bb-dcbb23b106f3` 提交到 Ark 返回:`InvalidEndpointOrModel.NotFound` > 完成内容:
> - `/models` 能 200说明 base 和 key 是通的,但这个 UUID 不在当前账号可访问模型里 > - 图片包:专利 12 张、配件 13 张、生产打样 18 张、宣发 21 张,全部 `complete`
> - 可见的 `doubao-seedance-1-5-pro-251215` 也返回“账号未激活该模型” > - 文字模板18 个,已生成
> - 当前已验证可提交的是原来的 `doubao-seedance-2-0-260128`6 秒最小任务提交成功:`cgt-20260520160907-wjgl8` > - 视频5 条成片,均约 `60.3s`
> - 360 度旋转展示
> - 开箱短片
> - 触感细节
> - 角色故事介绍
> - 工厂预览短片
> >
> 我没有把这个无效 UUID 留在本地或生产运行态,已经回退到可用模型 > 视频文件名也改成好识别的形式,例如
> `youni_zhuxiaodong_360_turntable_60s.mp4`、`youni_zhuxiaodong_unboxing_60s.mp4` 等。
> >
> - 本地 `.env.local``SEEDANCE_MODEL=doubao-seedance-2-0-260128` > 验证过:
> - 生产 `deploy/.env.production`:已回退 > - 5 条视频 `ffprobe` 都是约 60 秒
> - 生产容器运行态:`doubao-seedance-2-0-260128` > - 登录状态下视频 Range 返回 `206`,线上拖动/预览应该正常
> - 生产容器healthy > - 抽帧联系表不是黑屏5 条都有画面
> - 公网登录页200 > - 没嵌入权志龙真实歌曲;视频提示里用的是“原创非版权 K-pop/electro 氛围”,后期要上 GD 热门歌需要你在剪辑平台里用已授权音源替换
> >
> 验证已做: > 线上看这里:`https://ai-toy.kang-kang.com`
> - `npm run build` 通过
> - `docker compose up -d --build` 通过,本地容器 healthy
> - 生产容器 healthy
> - 代码无功能改动,只修正了 handoff 文档里的 Seedance 默认模型引用
>
> 已提交并推送:
> `b6f7a44 docs: align seedance model reference`
> >
> 最终 `git status -sb` > 最终 `git status -sb`
> ```text > ```text
> ## master...origin/master > ## master...origin/master
> M .memory/worklog.json
> ``` > ```
> >
> 要用你给的这个 UUID需要在 Ark Console 里确认它是当前 `SEEDANCE_API_KEY` 所属账号可访问/已激活的模型或 endpoint > 这次没有改仓库代码,所以没有做代码 commit只是在生产数据里生成并回填了这一套素材
>
> <oai-mem-citation>
> <citation_entries>
> MEMORY.md:196-198|note=…
## Cursor 最近会话 ## Cursor 最近会话
@@ -108,7 +104,7 @@
- 当前分支master - 当前分支master
- 未提交变更1 项 - 未提交变更1 项
- 最近提交auto-save 2026-05-21 08:45 (~2) - 最近提交auto-save 2026-05-22 09:01 (~2)
- 变更文件: - 变更文件:
- M .memory/worklog.json - M .memory/worklog.json

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +1,71 @@
{ {
"name": "AI玩具专利生成工作流", "created" : "2026-05-18",
"description": "批量生成毛绒玩具IP意向→快速筛选→自动出多角度/尺寸图,喂给专利申请", "credentials" : [
"status": "active",
"kind": "tool",
"created": "2026-05-18",
"urls": [
{ {
"type": "app", "env" : "OPENAI_API_KEY",
"url": "https://ai-toy.kang-kang.com", "name" : "OPENAI_API_KEY",
"label": "VPS 生产" "note" : "GPT 文本\/结构化\/图片生成;没填则图片 mock"
}, },
{ {
"type": "app", "env" : "SEEDANCE_API_KEY",
"url": "https://ai-toy.kang-kang.com/login", "name" : "SEEDANCE_API_KEY",
"label": "VPS 登录" "note" : "Seedance 视频生成;没填则视频接口不可用"
}, },
{ {
"type": "app", "env" : "WEB_AUTH_USERNAME\/WEB_AUTH_PASSWORD\/WEB_AUTH_SESSION_SECRET",
"url": "http://localhost:4560", "name" : "WEB_LOGIN",
"label": "本地 Docker" "note" : "网页登录;生产值只放 VPS deploy\/.env.production 和 \/root\/ai-toy-patent-workflow-login.txt"
},
{
"type": "repo",
"label": "git",
"url": "https://git.kang-kang.com/kangwan/ai-toy-patent-workflow"
} }
], ],
"credentials": [ "description" : "批量生成毛绒玩具IP意向→快速筛选→自动出多角度\/尺寸图,喂给专利申请",
"kind" : "tool",
"name" : "AI玩具专利生成工作流",
"ownership" : "personal",
"pin_order" : 1779411563,
"pinned" : true,
"ports" : [
{ {
"name": "OPENAI_API_KEY", "fixed" : true,
"env": "OPENAI_API_KEY", "label" : "dev",
"note": "GPT 文本/结构化/图片生成;没填则图片 mock" "port" : 4560
},
{
"name": "SEEDANCE_API_KEY",
"env": "SEEDANCE_API_KEY",
"note": "Seedance 视频生成;没填则视频接口不可用"
},
{
"name": "WEB_LOGIN",
"env": "WEB_AUTH_USERNAME/WEB_AUTH_PASSWORD/WEB_AUTH_SESSION_SECRET",
"note": "网页登录;生产值只放 VPS deploy/.env.production 和 /root/ai-toy-patent-workflow-login.txt"
} }
], ],
"ports": [ "quick_login" : {
{ "label" : "AI Toy Patent \/ 登录",
"port": 4560, "password" : "22668050fb50f6e95cb5e32c",
"label": "dev", "url" : "https:\/\/ai-toy.kang-kang.com\/login",
"fixed": true "username" : "kangwan"
}
],
"worklog": {
"path": ".memory/worklog.json",
"auto": true
}, },
"stack": [ "stack" : [
"Next.js + GPT + Seedance", "Next.js + GPT + Seedance",
"Docker Compose local/prod parity", "Docker Compose local\/prod parity",
"Coolify Traefik" "Coolify Traefik"
], ],
"ownership": "personal", "status" : "active",
"quick_login": { "urls" : [
"label": "AI Toy Patent / 登录", {
"url": "https://ai-toy.kang-kang.com/login", "label" : "VPS 生产",
"username": "kangwan", "type" : "app",
"password": "22668050fb50f6e95cb5e32c" "url" : "https:\/\/ai-toy.kang-kang.com"
},
{
"label" : "VPS 登录",
"type" : "app",
"url" : "https:\/\/ai-toy.kang-kang.com\/login"
},
{
"label" : "本地 Docker",
"type" : "app",
"url" : "http:\/\/localhost:4560"
},
{
"label" : "git",
"type" : "repo",
"url" : "https:\/\/git.kang-kang.com\/kangwan\/ai-toy-patent-workflow"
}
],
"worklog" : {
"auto" : true,
"path" : ".memory\/worklog.json"
} }
} }

View File

@@ -9,7 +9,7 @@
## 部署事实 ## 部署事实
- 平台:个人 VPS `76.13.31.179`Docker Compose接入现有 Coolify Traefik - 平台:个人 VPS `76.13.31.179`Docker Compose接入现有 Coolify Traefik
- 发布状态VPS 生产已发布,仅个人使用 - 发布状态VPS 生产已发布,仅个人使用
- 最近生产部署2026-05-21媒体阅览性能修复视频文件改为流式传输并正确支持 HTTP Range / 206图片接口改为流式响应前端缩略图加 lazy/async 加载;对应代码提交 `b6d7feb` - 最近生产部署2026-05-22视频面板修复 60 秒成片任务 ID 映射;`video_turntable_60s` 等已完成视频会替代对应默认模板卡片,不再重复显示不可播放的空视频项;对应代码提交 `7abbb7d`
- 服务名 / 容器名:`ai-toy-patent-workflow` - 服务名 / 容器名:`ai-toy-patent-workflow`
- 服务器路径:`/opt/ai-toy-patent-workflow` - 服务器路径:`/opt/ai-toy-patent-workflow`
- 主站 / 前端https://ai-toy.kang-kang.com - 主站 / 前端https://ai-toy.kang-kang.com

View File

@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import type { AssetPack, AssetTemplate, GenImage, GenSession, PackKind, ToyAsset } from '@/lib/types'; import type { AssetPack, AssetTemplate, GenImage, GenSession, PackKind, ToyAsset, VideoTask } from '@/lib/types';
import { PACK_LABELS, PACK_ORDER, PACK_TEMPLATES, TEXT_TEMPLATES, VIDEO_TEMPLATES } from '@/lib/templates'; import { PACK_LABELS, PACK_ORDER, PACK_TEMPLATES, TEXT_TEMPLATES, VIDEO_TEMPLATES } from '@/lib/templates';
import { HoverImagePreview, HoverVideoPreview } from './HoverImagePreview'; import { HoverImagePreview, HoverVideoPreview } from './HoverImagePreview';
@@ -35,6 +35,30 @@ function aspectCss(aspectRatio: AssetTemplate['aspectRatio'] | string | undefine
return aspectRatio.replace(':', ' / '); return aspectRatio.replace(':', ' / ');
} }
function canonicalVideoTemplateId(templateId: string) {
return VIDEO_TEMPLATES.find(template => (
templateId === template.id || templateId.startsWith(`${template.id}_`)
))?.id ?? templateId;
}
function videoTaskScore(task: VideoTask) {
let score = 0;
if (task.videoUrl) score += 8;
if (task.status === 'succeeded') score += 4;
if (task.status === 'processing' || task.status === 'submitted') score += 2;
return score;
}
function selectVideoTaskForTemplate(tasks: VideoTask[], templateId: string) {
return tasks
.filter(task => canonicalVideoTemplateId(task.templateId) === templateId)
.sort((a, b) => {
const scoreDiff = videoTaskScore(b) - videoTaskScore(a);
if (scoreDiff) return scoreDiff;
return (b.updatedAt || b.submittedAt || 0) - (a.updatedAt || a.submittedAt || 0);
})[0];
}
type AssetDetail = { type AssetDetail = {
template: AssetTemplate; template: AssetTemplate;
asset: ToyAsset | undefined; asset: ToyAsset | undefined;
@@ -379,20 +403,23 @@ function VideoSection({ videoLoading, primaryImage, locked, session, onGenerateV
onRefreshVideo: (taskId: string) => void; onRefreshVideo: (taskId: string) => void;
}) { }) {
const [showPromptId, setShowPromptId] = useState<string | null>(null); const [showPromptId, setShowPromptId] = useState<string | null>(null);
const videoTasks = session.videoTasks ?? []; const videoTasks = (session.videoTasks ?? []).filter(task => !/_part[12]$/.test(task.templateId));
const byTemplate = new Map(videoTasks.map(task => [task.templateId, task]));
const builtInIds = new Set<string>(VIDEO_TEMPLATES.map(template => template.id)); const builtInIds = new Set<string>(VIDEO_TEMPLATES.map(template => template.id));
const extraTasks = videoTasks.filter(task => !builtInIds.has(task.templateId) && !/_part[12]$/.test(task.templateId)); const extraTasks = videoTasks.filter(task => !builtInIds.has(canonicalVideoTemplateId(task.templateId)));
const videoItems = [ const videoItems = [
...VIDEO_TEMPLATES.map(template => ({ ...VIDEO_TEMPLATES.map(template => {
id: template.id, const task = selectVideoTaskForTemplate(videoTasks, template.id);
title: template.title, return {
description: template.description, id: template.id,
duration: template.duration, title: task?.title ?? template.title,
ratio: template.ratio, description: task?.description ?? template.description,
promptTemplate: template.promptTemplate, duration: task?.duration ?? template.duration,
template, ratio: task?.ratio ?? template.ratio,
})), promptTemplate: task?.prompt ?? template.promptTemplate,
template,
task,
};
}),
...extraTasks.map(task => ({ ...extraTasks.map(task => ({
id: task.templateId, id: task.templateId,
title: task.title, title: task.title,
@@ -401,9 +428,10 @@ function VideoSection({ videoLoading, primaryImage, locked, session, onGenerateV
ratio: task.ratio, ratio: task.ratio,
promptTemplate: task.prompt, promptTemplate: task.prompt,
template: null, template: null,
task,
})), })),
]; ];
const submittedCount = videoItems.filter(item => byTemplate.has(item.id)).length; const submittedCount = videoItems.filter(item => item.task).length;
const totalCount = Math.max(videoItems.length, 1); const totalCount = Math.max(videoItems.length, 1);
return ( return (
@@ -430,8 +458,8 @@ function VideoSection({ videoLoading, primaryImage, locked, session, onGenerateV
<div className="grid grid-cols-1 gap-2 border-t border-white/[0.05] p-3 2xl:grid-cols-2"> <div className="grid grid-cols-1 gap-2 border-t border-white/[0.05] p-3 2xl:grid-cols-2">
{videoItems.map(item => { {videoItems.map(item => {
const isOpen = showPromptId === item.id; const isOpen = showPromptId === item.id;
const task = byTemplate.get(item.id); const task = item.task;
const loadingThis = videoLoading === item.id; const loadingThis = videoLoading === item.id || videoLoading === task?.templateId;
return ( return (
<div key={item.id} className="grid min-w-0 grid-cols-[128px_minmax(0,1fr)] gap-3 rounded-[8px] bg-white/[0.025] p-2.5 ring-1 ring-white/[0.05] transition-all hover:ring-white/[0.12]"> <div key={item.id} className="grid min-w-0 grid-cols-[128px_minmax(0,1fr)] gap-3 rounded-[8px] bg-white/[0.025] p-2.5 ring-1 ring-white/[0.05] transition-all hover:ring-white/[0.12]">
<div className="relative h-24 overflow-hidden rounded-[8px] bg-black/70"> <div className="relative h-24 overflow-hidden rounded-[8px] bg-black/70">