From 47443c0efeab8f3636f66c8d34298ac4cd225670 Mon Sep 17 00:00:00 2001 From: GouJ Date: Fri, 28 Aug 2026 15:16:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=E5=BD=A2=E6=80=81?= =?UTF-8?q?=E5=9B=BE=E5=BA=93=E4=B8=8E=20Seedream=20=E7=94=9F=E5=9B=BE?= =?UTF-8?q?=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 展示已有主图、候选图和失败记录,支持单图及批量生成、主图选择。 补充正式 ID 校验、费用确认、图片加载占位和回归测试。 依赖后端 eac8dc3 的项目形态图库只读接口。 --- README.md | 27 +- src/App.vue | 10 +- src/components/ui/AppDialog.vue | 3 +- src/components/ui/AssetImage.vue | 40 +++ src/components/ui/index.ts | 1 + src/features/breakdown/BreakdownPage.vue | 1 + .../breakdown/components/SubjectList.vue | 8 +- src/features/projects/ProjectLayout.vue | 5 +- .../storyboard/components/ShotTools.vue | 5 +- src/features/storyboard/model.ts | 18 +- .../subject-images/SubjectImagesPage.vue | 260 ++++++++++++++++++ src/features/subject-images/api.ts | 30 ++ .../components/GenerateImageDialog.vue | 120 ++++++++ .../components/ImageGalleryDialog.vue | 180 ++++++++++++ src/features/subject-images/images.test.ts | 172 ++++++++++++ src/features/subject-images/index.ts | 2 + src/features/subject-images/model.ts | 31 +++ .../subject-images/testing/fixtures.ts | 36 +++ src/features/subject-images/types.ts | 65 +++++ .../subject-images/useSubjectImages.ts | 104 +++++++ src/features/workflows/pages.test.ts | 120 ++++++++ src/lib/assets.ts | 16 ++ src/lib/http.test.ts | 36 +++ src/lib/http.ts | 2 +- src/router/index.ts | 5 + src/styles.css | 66 +++++ 26 files changed, 1338 insertions(+), 25 deletions(-) create mode 100644 src/components/ui/AssetImage.vue create mode 100644 src/features/subject-images/SubjectImagesPage.vue create mode 100644 src/features/subject-images/api.ts create mode 100644 src/features/subject-images/components/GenerateImageDialog.vue create mode 100644 src/features/subject-images/components/ImageGalleryDialog.vue create mode 100644 src/features/subject-images/images.test.ts create mode 100644 src/features/subject-images/index.ts create mode 100644 src/features/subject-images/model.ts create mode 100644 src/features/subject-images/testing/fixtures.ts create mode 100644 src/features/subject-images/types.ts create mode 100644 src/features/subject-images/useSubjectImages.ts create mode 100644 src/lib/assets.ts diff --git a/README.md b/README.md index d26d660..a5cdb6e 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,9 @@ API_PROXY_TARGET=http://localhost:3412 | 分镜检查 | 按剧集、Beat、正式 Shot ID 浏览;导演设计、环境与主体即时状态;单集覆盖率与 JSON 导出 | | 分镜生成 | Direction / VisualState 单集生成、项目批量补齐、覆盖重生成;并发和自动修复次数;部分失败诊断 | | 镜头工具 | 主体参考图与缺失原因、确定性 GenerationSpec 查询与导出、单镜头/批量视频提示词 | +| 形态图片 | 正式形态图库、主图/候选图预览、图片历史与失败记录、Seedream 单图/批量生图、切换主参考图 | -尚未实现:手工编辑剧本或分镜设计(后端暂无对应写接口)、主体图生成、实际视频生成、用户登录。模型输出按纯文本显示,避免执行不可信 HTML。参考图只接受 HTTP(S) 或后端 `/storage/` 路径。 +尚未实现:手工编辑剧本或分镜设计(后端暂无对应写接口)、实际视频生成、用户登录。模型输出按纯文本显示,避免执行不可信 HTML。参考图只接受 HTTP(S) 或后端 `/storage/` 路径。 ## 技术与规范 @@ -65,6 +66,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S | `src/features/create-drama` | 剧本创作 graph 页面 | | `src/features/breakdown` | 拆解 graph 的 API、类型、配置页面、主体与分镜组件 | | `src/features/storyboard` | 分镜 API、类型、生成操作、依赖判断、设计与状态、镜头资源、生成回执 | +| `src/features/subject-images` | 正式形态图库、图片生成参数、历史图片、主图选择与批量回执 | | `src/features/workflows` | checkpoint 选择、执行记录、长请求互斥与操作确认 | | `src/components/ui` | 跨业务使用的 Reka UI 封装与公共展示组件 | | `src/composables` | 具备取消和竞态保护的轮询 | @@ -76,7 +78,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S ## 接口基线 对齐后端 `qianlanse/short-drama-agent` 的 `dev`: -`6ce284b9d93e8c9d0b7ca57d265148e354642095`。 +`eac8dc3b1b2ce1ff2bdfeb01d7281cd838f07285`,包含本次新增的只读 `GET /projects/:projectId/subject-forms` 接口。请同步更新前后端 `dev` 后重启后端。 | HTTP | 路径(以 /api 为前缀) | 用途 | | --- | --- | --- | @@ -100,6 +102,11 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S | GET | `/storyboard-shots/:shotId/generation-spec` | 后端确定性编译结果,不调用模型 | | POST | `/storyboard-shots/:shotId/video-prompt` | 复用已有 Prompt 或调用模型生成;`force` 控制覆盖 | | POST | `/projects/:id/video-prompts/generate` | 项目批量 Prompt;`concurrency`、`force` | +| GET | `/projects/:id/subject-forms` | 正式 SubjectForm ID、主体名称/引用、主图与图片元数据;本次新增只读接口 | +| GET | `/subject-forms/:formId/images` | 此形态的全部图片历史及实际 Prompt | +| POST | `/subject-forms/:formId/images` | 单图生成;显式 `provider: seedream`,可选 Prompt 和成对尺寸,`setPrimary` | +| PUT | `/subject-forms/:formId/images/:imageId/primary` | 选择已完成图片作为主参考图,不调用模型 | +| POST | `/projects/:id/subject-images/generate` | 全项目批量生图;`provider`、`concurrency`、`force` | API 层也提供 `state` 与 `breakdown/latest` 方法。页面通过共享 checkpoint 查询取得阶段状态,避免重复拉取同样内容。 @@ -127,6 +134,18 @@ API 层也提供 `state` 与 `breakdown/latest` 方法。页面通过共享 chec 9. 参考图和 GenerationSpec 按需 GET,切换镜头或设计变化时取消旧查询。GenerationSpec 需要有效的 Direction、VisualState 和形态关联,不生成图片或视频。 10. 后端当前的 Video Prompt 仍使用镜头描述和参考图,**尚未消费 GenerationSpec**。没有独立 GET Prompt 接口,“读取/生成提示词”仍是 POST;不存在 Prompt 时会调用模型,因此必须确认。`force: false` 复用已有正文;批量响应只有计数和失败列表,不能凭计数构造 Prompt 正文。`generated` 是后端处理成功数,可能包含已存在但状态不为 `prompt_ready` 的复用项。 +### 形态图片约定 + +1. 入口为 `/projects/:projectId/subject-images`;项目导航、拆解主体列表和分镜参考图缺失提示均可进入。图库自动读取后端已生成图片,不需要重新生图才能显示。 +2. 生图使用正式数据库 `SubjectForm.id`。Checkpoint 的领域 `formId` 不是数据库 ID,不能直接调用图片接口。新增查询从项目关联读取正式形态,返回轻量图片元数据,不包含图片的 `rawJson` 或长 Prompt;打开历史弹窗后再读取详细记录。 +3. 单图明确发送 `provider: 'seedream'`,避免后端单图入口默认使用 mock。模型和凭证仅配置在后端,前端不保存 API Key。默认使用后端的 generationPrompt,其次 appearancePrompt;可为本次输入自定义 Prompt,详情显示后端最终实际使用的文本。 +4. 单图宽高同时留空使用后端默认 2K,自定义时必须同时提供正整数,具体尺寸限制仍由模型校验。当前 Seedream Provider 没有使用 negativePrompt,所以页面不提供无效的负向提示词输入。 +5. 单图生成新增历史记录,不删除旧图。无主图时默认勾选“生成成功后设为主图”;已有主图时默认不替换。候选图可以在历史弹窗二次确认后通过 PUT 设为主图,只允许已完成图片。 +6. 批量默认跳过已有主图的形态;勾选“已有主图也新增候选图”发送 `force: true`。后端批量 force 只生成新图,**不会替换已有主图**。批量作用于整个项目,不受类型/文字/缺图筛选影响,也不接收单图的尺寸与自定义 Prompt。 +7. 图库每 6 秒读取一次真实记录,弹窗只在打开时查询该形态图片。数据库存在 pending / generating 时暂停再次生图;网络错误不自动重试有费用的请求。HTTP 200 中的批量部分失败单独展示。 +8. 图片使用持久化后的 `/storage/...` 地址,开发代理已配置。生产环境也必须代理 `/storage/`,不能让 SPA 回退返回 index.html。无效 URL、加载失败、无图和后端生成失败有各自提示;不会自动使用可能过期的 Provider 临时地址。 +9. 设为主图后,回到分镜点击“刷新参考图”读取最新绑定。已有视频提示词不会自动重生成,需要按需覆盖。本页面不会调用 `/production/start` 或实际视频生成。 + ### 后端限制 目前 `checkpoints` 接口返回完整历史与 state,没有分页或轻量状态接口。长篇剧本会增加轮询流量。下一步建议后端增加: @@ -182,5 +201,7 @@ server { 6. 进入分镜设计,确认正式镜头 ID 与 Beat 对应;先生成单集 Direction,再生成 VisualState,检查覆盖率和主体状态。 7. 用小型测试项目验证批量补齐、跳过完整集、失败诊断和明确覆盖;确认关闭覆盖后不会重生成完整设计。 8. 给主体形态准备参考图,读取镜头参考图和 GenerationSpec;确认 Prompt 无正文时经确认生成、有正文且不覆盖时复用。不提交实际视频任务。 +9. 打开“形态图片”,检查已有主图自动显示;单个形态新增候选图,查看历史、失败原因和实际 Prompt,再设为主图。 +10. 在小型测试项目中验证批量补齐与“已有主图也新增候选图”;回到分镜刷新参考图,确认使用新选择的主图。 -自动测试使用模拟 API,覆盖两条既有流程及分镜 API 参数、依赖判断、正式 ID 合并、校验与部分失败、切换剧集的竞态、查询错误恢复、Prompt 确认和纯文本展示。不等同于真实模型、MySQL 或浏览器视觉联调。请勿用正式项目随意制造失败以测试恢复。 +自动测试使用模拟 API,覆盖既有流程、分镜接口与竞态,以及形态图片自动读取、正式 ID 生图、主图 PUT、费用确认、尺寸校验、候选图选择、批量部分失败和图片加载错误。不等同于真实模型、MySQL 或浏览器视觉联调。请勿用正式项目随意制造失败以测试恢复。 diff --git a/src/App.vue b/src/App.vue index 9a0a0f5..9ba32b7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,6 +4,7 @@ import { useRoute } from 'vue-router' import { Clapperboard, Camera, + Images, FolderOpen, FileText, Layers, @@ -55,13 +56,20 @@ const apiBase = import.meta.env.VITE_API_BASE_URL || '/api' title="剧本拆解" >剧本拆解02形态图片03分镜设计0304 diff --git a/src/components/ui/AppDialog.vue b/src/components/ui/AppDialog.vue index d8e262b..d62d0d2 100644 --- a/src/components/ui/AppDialog.vue +++ b/src/components/ui/AppDialog.vue @@ -11,7 +11,7 @@ import { import { X } from '@lucide/vue' /** 统一弹窗容器,Reka UI 负责焦点锁定、Escape 和无障碍语义。 */ -defineProps<{ title: string; description: string; busy?: boolean }>() +defineProps<{ title: string; description: string; busy?: boolean; wide?: boolean }>() const open = defineModel('open', { default: false }) @@ -22,6 +22,7 @@ const open = defineModel('open', { default: false }) diff --git a/src/components/ui/AssetImage.vue b/src/components/ui/AssetImage.vue new file mode 100644 index 0000000..2a95481 --- /dev/null +++ b/src/components/ui/AssetImage.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index a25aa2e..b71a11f 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -2,3 +2,4 @@ export { default as AppDialog } from './AppDialog.vue' export { default as EmptyState } from './EmptyState.vue' export { default as StatusBadge } from './StatusBadge.vue' +export { default as AssetImage } from './AssetImage.vue' diff --git a/src/features/breakdown/BreakdownPage.vue b/src/features/breakdown/BreakdownPage.vue index 192dc8d..c329830 100644 --- a/src/features/breakdown/BreakdownPage.vue +++ b/src/features/breakdown/BreakdownPage.vue @@ -341,6 +341,7 @@ function exportResult() {
() +const props = defineProps<{ subjects: SubjectCandidate[]; forms: SubjectForm[]; projectId?: string }>() const search = ref('') const filtered = computed(() => props.subjects.filter(item => @@ -34,6 +34,12 @@ const filtered = computed(() => >

{{ subject.description }}

+ 查看形态图片/生图
外观描述与形态 diff --git a/src/features/projects/ProjectLayout.vue b/src/features/projects/ProjectLayout.vue index 9d1b3d6..1979a3b 100644 --- a/src/features/projects/ProjectLayout.vue +++ b/src/features/projects/ProjectLayout.vue @@ -1,7 +1,7 @@ + + diff --git a/src/features/subject-images/api.ts b/src/features/subject-images/api.ts new file mode 100644 index 0000000..e50513f --- /dev/null +++ b/src/features/subject-images/api.ts @@ -0,0 +1,30 @@ +import { request } from '../../lib/http' +import type { + GenerateFormImageInput, + GenerateProjectImagesInput, + ImageBatchResult, + SubjectFormAsset, + SubjectImage +} from './types' + +/** 对正式 SubjectForm 数据库 ID 编码,不接受按名称拼出的路径。 */ +function formPath(id: string) { + return `/subject-forms/${encodeURIComponent(id)}/images` +} + +/** 形态图库 API:读取与生成分离,生成不自动重试,不设置客户端短超时。 */ +export const subjectImagesApi = { + listForms: (projectId: string, signal?: AbortSignal) => + request(`/projects/${encodeURIComponent(projectId)}/subject-forms`, { signal }), + listImages: (formId: string, signal?: AbortSignal) => request(formPath(formId), { signal }), + generate: (formId: string, input: GenerateFormImageInput) => + request(formPath(formId), { method: 'POST', body: input, timeoutMs: 0 }), + generateProject: (projectId: string, input: GenerateProjectImagesInput) => + request(`/projects/${encodeURIComponent(projectId)}/subject-images/generate`, { + method: 'POST', + body: input, + timeoutMs: 0 + }), + setPrimary: (formId: string, imageId: string) => + request(`${formPath(formId)}/${encodeURIComponent(imageId)}/primary`, { method: 'PUT' }) +} diff --git a/src/features/subject-images/components/GenerateImageDialog.vue b/src/features/subject-images/components/GenerateImageDialog.vue new file mode 100644 index 0000000..78b16f4 --- /dev/null +++ b/src/features/subject-images/components/GenerateImageDialog.vue @@ -0,0 +1,120 @@ + + +