Compare commits
4 Commits
8d3128d71c
...
f38c524cbe
| Author | SHA1 | Date | |
|---|---|---|---|
| f38c524cbe | |||
| 7abbb7d532 | |||
| 20d2d8fa68 | |||
| 335231fc07 |
@@ -1,6 +1,6 @@
|
||||
# 项目接力
|
||||
|
||||
- 生成时间:May 21, 2026 at 21:43
|
||||
- 生成时间:May 22, 2026 at 12:48
|
||||
- 项目:AI玩具专利生成工作流
|
||||
- 路径:/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow
|
||||
- 状态:active
|
||||
@@ -9,7 +9,7 @@
|
||||
## 最近助手会话概览
|
||||
|
||||
- Claude:df7c3755-a4d2-4e32-b68b-42bbaebc2fda · 时间未知
|
||||
- Codex:019e40f8-2f9a-73d0-af47-129bc741af46 · 时间未知
|
||||
- Codex:019e4ac6-ab79-74c1-8fc2-26545c4863df · 时间未知
|
||||
- Cursor:未找到匹配当前项目的最近会话
|
||||
|
||||
## Claude 最近会话
|
||||
@@ -49,56 +49,52 @@
|
||||
|
||||
## Codex 最近会话
|
||||
|
||||
- Session ID:019e40f8-2f9a-73d0-af47-129bc741af46
|
||||
- Transcript:/Users/kangwan/.codex/sessions/2026/05/20/rollout-2026-05-20T00-01-07-019e40f8-2f9a-73d0-af47-129bc741af46.jsonl
|
||||
- Session ID:019e4ac6-ab79-74c1-8fc2-26545c4863df
|
||||
- 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
|
||||
- 分支:master
|
||||
- 敏感字段:已对 token / key / password / secret 做脱敏
|
||||
|
||||
### 最近用户要求
|
||||
|
||||
- 排版麻烦你全部给我打乱,重新来 ,是有给你强制的结构框架构么?不需要现有的结构 你给我重新设计明白么?
|
||||
- 流程错误了 每一个先从专利包开始 都是一步做完才能做下一步 ,不能一按键全包
|
||||
- 视频模型先接上2315eb28-4db5-4907-a2bb-dcbb23b106f3 https://ark.cn-beijing.volces.com/api/v3
|
||||
- 可以改原始的文件名
|
||||
- OK 开始出一套吧 这应该是混合式ip了
|
||||
- 全称是这个:“有你家族”猪小懂IP系列家族
|
||||
|
||||
### 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 不在当前账号可访问模型里
|
||||
> - 可见的 `doubao-seedance-1-5-pro-251215` 也返回“账号未激活该模型”
|
||||
> - 当前已验证可提交的是原来的 `doubao-seedance-2-0-260128`,6 秒最小任务提交成功:`cgt-20260520160907-wjgl8`
|
||||
> 完成内容:
|
||||
> - 图片包:专利 12 张、配件 13 张、生产打样 18 张、宣发 21 张,全部 `complete`
|
||||
> - 文字模板:18 个,已生成
|
||||
> - 视频: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`:已回退
|
||||
> - 生产容器运行态:`doubao-seedance-2-0-260128`
|
||||
> - 生产容器:healthy
|
||||
> - 公网登录页:200
|
||||
> 验证过:
|
||||
> - 5 条视频 `ffprobe` 都是约 60 秒
|
||||
> - 登录状态下视频 Range 返回 `206`,线上拖动/预览应该正常
|
||||
> - 抽帧联系表不是黑屏,5 条都有画面
|
||||
> - 没嵌入权志龙真实歌曲;视频提示里用的是“原创非版权 K-pop/electro 氛围”,后期要上 GD 热门歌需要你在剪辑平台里用已授权音源替换
|
||||
>
|
||||
> 验证已做:
|
||||
> - `npm run build` 通过
|
||||
> - `docker compose up -d --build` 通过,本地容器 healthy
|
||||
> - 生产容器 healthy
|
||||
> - 代码无功能改动,只修正了 handoff 文档里的 Seedance 默认模型引用
|
||||
>
|
||||
> 已提交并推送:
|
||||
> `b6f7a44 docs: align seedance model reference`
|
||||
> 线上看这里:`https://ai-toy.kang-kang.com`
|
||||
>
|
||||
> 最终 `git status -sb`:
|
||||
> ```text
|
||||
> ## master...origin/master
|
||||
> M .memory/worklog.json
|
||||
> ```
|
||||
>
|
||||
> 要用你给的这个 UUID,需要在 Ark Console 里确认它是当前 `SEEDANCE_API_KEY` 所属账号可访问/已激活的模型或 endpoint。
|
||||
>
|
||||
> <oai-mem-citation>
|
||||
> <citation_entries>
|
||||
> MEMORY.md:196-198|note=…
|
||||
> 这次没有改仓库代码,所以没有做代码 commit;只是在生产数据里生成并回填了这一套素材。
|
||||
|
||||
## Cursor 最近会话
|
||||
|
||||
@@ -108,7 +104,7 @@
|
||||
|
||||
- 当前分支:master
|
||||
- 未提交变更:1 项
|
||||
- 最近提交:auto-save 2026-05-21 08:45 (~2)
|
||||
- 最近提交:auto-save 2026-05-22 09:01 (~2)
|
||||
- 变更文件:
|
||||
- M .memory/worklog.json
|
||||
|
||||
|
||||
3093
.memory/worklog.json
3093
.memory/worklog.json
File diff suppressed because it is too large
Load Diff
106
.project.json
106
.project.json
@@ -1,69 +1,71 @@
|
||||
{
|
||||
"name": "AI玩具专利生成工作流",
|
||||
"description": "批量生成毛绒玩具IP意向→快速筛选→自动出多角度/尺寸图,喂给专利申请",
|
||||
"status": "active",
|
||||
"kind": "tool",
|
||||
"created": "2026-05-18",
|
||||
"urls": [
|
||||
"created" : "2026-05-18",
|
||||
"credentials" : [
|
||||
{
|
||||
"type": "app",
|
||||
"url": "https://ai-toy.kang-kang.com",
|
||||
"label": "VPS 生产"
|
||||
"env" : "OPENAI_API_KEY",
|
||||
"name" : "OPENAI_API_KEY",
|
||||
"note" : "GPT 文本\/结构化\/图片生成;没填则图片 mock"
|
||||
},
|
||||
{
|
||||
"type": "app",
|
||||
"url": "https://ai-toy.kang-kang.com/login",
|
||||
"label": "VPS 登录"
|
||||
"env" : "SEEDANCE_API_KEY",
|
||||
"name" : "SEEDANCE_API_KEY",
|
||||
"note" : "Seedance 视频生成;没填则视频接口不可用"
|
||||
},
|
||||
{
|
||||
"type": "app",
|
||||
"url": "http://localhost:4560",
|
||||
"label": "本地 Docker"
|
||||
},
|
||||
{
|
||||
"type": "repo",
|
||||
"label": "git",
|
||||
"url": "https://git.kang-kang.com/kangwan/ai-toy-patent-workflow"
|
||||
"env" : "WEB_AUTH_USERNAME\/WEB_AUTH_PASSWORD\/WEB_AUTH_SESSION_SECRET",
|
||||
"name" : "WEB_LOGIN",
|
||||
"note" : "网页登录;生产值只放 VPS deploy\/.env.production 和 \/root\/ai-toy-patent-workflow-login.txt"
|
||||
}
|
||||
],
|
||||
"credentials": [
|
||||
"description" : "批量生成毛绒玩具IP意向→快速筛选→自动出多角度\/尺寸图,喂给专利申请",
|
||||
"kind" : "tool",
|
||||
"name" : "AI玩具专利生成工作流",
|
||||
"ownership" : "personal",
|
||||
"pin_order" : 1779411563,
|
||||
"pinned" : true,
|
||||
"ports" : [
|
||||
{
|
||||
"name": "OPENAI_API_KEY",
|
||||
"env": "OPENAI_API_KEY",
|
||||
"note": "GPT 文本/结构化/图片生成;没填则图片 mock"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"fixed" : true,
|
||||
"label" : "dev",
|
||||
"port" : 4560
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"port": 4560,
|
||||
"label": "dev",
|
||||
"fixed": true
|
||||
}
|
||||
],
|
||||
"worklog": {
|
||||
"path": ".memory/worklog.json",
|
||||
"auto": true
|
||||
"quick_login" : {
|
||||
"label" : "AI Toy Patent \/ 登录",
|
||||
"password" : "22668050fb50f6e95cb5e32c",
|
||||
"url" : "https:\/\/ai-toy.kang-kang.com\/login",
|
||||
"username" : "kangwan"
|
||||
},
|
||||
"stack": [
|
||||
"stack" : [
|
||||
"Next.js + GPT + Seedance",
|
||||
"Docker Compose local/prod parity",
|
||||
"Docker Compose local\/prod parity",
|
||||
"Coolify Traefik"
|
||||
],
|
||||
"ownership": "personal",
|
||||
"quick_login": {
|
||||
"label": "AI Toy Patent / 登录",
|
||||
"url": "https://ai-toy.kang-kang.com/login",
|
||||
"username": "kangwan",
|
||||
"password": "22668050fb50f6e95cb5e32c"
|
||||
"status" : "active",
|
||||
"urls" : [
|
||||
{
|
||||
"label" : "VPS 生产",
|
||||
"type" : "app",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
2
RULES.md
2
RULES.md
@@ -9,7 +9,7 @@
|
||||
## 部署事实
|
||||
- 平台:个人 VPS `76.13.31.179`,Docker Compose,接入现有 Coolify Traefik
|
||||
- 发布状态:VPS 生产已发布,仅个人使用
|
||||
- 最近生产部署:2026-05-21,媒体阅览性能修复;视频文件改为流式传输并正确支持 HTTP Range / 206,图片接口改为流式响应,前端缩略图加 lazy/async 加载;对应代码提交 `b6d7feb`
|
||||
- 最近生产部署:2026-05-22,视频面板修复 60 秒成片任务 ID 映射;`video_turntable_60s` 等已完成视频会替代对应默认模板卡片,不再重复显示不可播放的空视频项;对应代码提交 `7abbb7d`
|
||||
- 服务名 / 容器名:`ai-toy-patent-workflow`
|
||||
- 服务器路径:`/opt/ai-toy-patent-workflow`
|
||||
- 主站 / 前端:https://ai-toy.kang-kang.com
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
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 { HoverImagePreview, HoverVideoPreview } from './HoverImagePreview';
|
||||
|
||||
@@ -35,6 +35,30 @@ function aspectCss(aspectRatio: AssetTemplate['aspectRatio'] | string | undefine
|
||||
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 = {
|
||||
template: AssetTemplate;
|
||||
asset: ToyAsset | undefined;
|
||||
@@ -379,20 +403,23 @@ function VideoSection({ videoLoading, primaryImage, locked, session, onGenerateV
|
||||
onRefreshVideo: (taskId: string) => void;
|
||||
}) {
|
||||
const [showPromptId, setShowPromptId] = useState<string | null>(null);
|
||||
const videoTasks = session.videoTasks ?? [];
|
||||
const byTemplate = new Map(videoTasks.map(task => [task.templateId, task]));
|
||||
const videoTasks = (session.videoTasks ?? []).filter(task => !/_part[12]$/.test(task.templateId));
|
||||
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 = [
|
||||
...VIDEO_TEMPLATES.map(template => ({
|
||||
...VIDEO_TEMPLATES.map(template => {
|
||||
const task = selectVideoTaskForTemplate(videoTasks, template.id);
|
||||
return {
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
description: template.description,
|
||||
duration: template.duration,
|
||||
ratio: template.ratio,
|
||||
promptTemplate: template.promptTemplate,
|
||||
title: task?.title ?? template.title,
|
||||
description: task?.description ?? template.description,
|
||||
duration: task?.duration ?? template.duration,
|
||||
ratio: task?.ratio ?? template.ratio,
|
||||
promptTemplate: task?.prompt ?? template.promptTemplate,
|
||||
template,
|
||||
})),
|
||||
task,
|
||||
};
|
||||
}),
|
||||
...extraTasks.map(task => ({
|
||||
id: task.templateId,
|
||||
title: task.title,
|
||||
@@ -401,9 +428,10 @@ function VideoSection({ videoLoading, primaryImage, locked, session, onGenerateV
|
||||
ratio: task.ratio,
|
||||
promptTemplate: task.prompt,
|
||||
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);
|
||||
|
||||
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">
|
||||
{videoItems.map(item => {
|
||||
const isOpen = showPromptId === item.id;
|
||||
const task = byTemplate.get(item.id);
|
||||
const loadingThis = videoLoading === item.id;
|
||||
const task = item.task;
|
||||
const loadingThis = videoLoading === item.id || videoLoading === task?.templateId;
|
||||
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 className="relative h-24 overflow-hidden rounded-[8px] bg-black/70">
|
||||
|
||||
Reference in New Issue
Block a user