This commit is contained in:
2026-06-30 15:02:20 +08:00
commit 3948b5a48a
306 changed files with 77275 additions and 0 deletions
@@ -0,0 +1,237 @@
# 分镜角度视角 + 四宫格序列图 实现计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 修复分镜图背景角度固定问题,并支持四宫格序列图生成模式。
**Architecture:**
- 功能一:在 `framePromptService.js``buildStoryboardContext()` 中,将 `angle` 字段翻译为带透视含义的完整描述注入上下文,让 AI 根据相机角度生成正确视角的背景。
- 功能二:前端增加全局"四宫格序列图"开关;开启后图片生成时传 `frame_type: 'quad_grid'`;后端 `imageService.js` 检测到该标记后先用 AI 生成 4 个帧提示词,拼成四宫格格式提示词,再调用图片生成 API 生成一张四宫格图。
- 利用现有 `frame_type` 字段存储标记,无需 DB migration。
**Tech Stack:** Node.js, better-sqlite3, Vue 3, Element Plus
---
## Task 1:角度描述扩展(功能一)
**Files:**
- Modify: `backend-node/src/services/framePromptService.js`
**Step 1: 在 `buildStoryboardContext()` 前新增角度映射辅助函数**
在文件顶部(`const loadConfig` 之后)新增:
```js
function expandAngleDescription(angle, isEnglish) {
if (!angle) return null;
const a = String(angle).trim().toLowerCase();
if (isEnglish) {
if (a.includes('low') || a === '仰视') return 'camera angle: low-angle upward shot, scene background shows sky/ceiling/treetops from below, strong upward perspective';
if (a.includes('high') || a === '俯视') return 'camera angle: high-angle downward shot, bird\'s eye view perspective, background shows ground/floor/scene from above';
if (a.includes('side') || a === '侧面') return 'camera angle: side-angle shot, profile composition, background extends laterally';
if (a.includes('back') || a === '背面') return 'camera angle: rear shot, character\'s back to camera, background scene stretches ahead';
return `camera angle: eye-level horizontal shot, normal perspective`;
} else {
if (a.includes('仰') || a.includes('low')) return '相机角度:低角度仰拍,背景呈现天空/天花板/树冠的仰视透视效果,视角向上倾斜';
if (a.includes('俯') || a.includes('high')) return '相机角度:高角度俯拍,鸟瞰视角,背景呈现地面/场景从上方向下看的俯视透视效果';
if (a.includes('侧') || a.includes('side')) return '相机角度:侧面视角,侧向构图,背景向两侧延展';
if (a.includes('背') || a.includes('back')) return '相机角度:从角色背后拍摄,角色背对镜头,背景场景在角色前方延伸';
return '相机角度:平视水平拍摄,正常透视构图';
}
}
```
**Step 2: 修改 `buildStoryboardContext()` 中 angle 处理部分**
找到原来的:
```js
if (sb.angle) {
parts.push(promptI18n.formatUserPrompt(cfg, 'angle_label', sb.angle));
}
```
替换为:
```js
if (sb.angle) {
const isEn = promptI18n.isEnglish(cfg);
const angleDesc = expandAngleDescription(sb.angle, isEn);
if (angleDesc) parts.push(angleDesc);
}
```
**Step 3: 同样修改 `episodeStoryboardService.js` 的 `generateImagePrompt()`**
`episodeStoryboardService.js` 中的 `generateImagePrompt()` 是生成初始 image_prompt 的函数,也需要加入角度信息。找到:
```js
function generateImagePrompt(sb, style) {
const parts = [];
if (sb.location) {
let locationDesc = sb.location;
if (sb.time) locationDesc += ', ' + sb.time;
parts.push(locationDesc);
}
...
const styleText = style && String(style).trim();
if (styleText) parts.push(styleText + ', first frame');
else parts.push('first frame');
return parts.length ? parts.join(', ') : (styleText ? styleText + ', first frame' : 'first frame');
}
```
`parts.push(locationDesc)` 之后,加入角度信息:
```js
if (sb.angle) {
const a = String(sb.angle).trim().toLowerCase();
if (a.includes('仰') || a.includes('low')) parts.push('low-angle upward shot');
else if (a.includes('俯') || a.includes('high')) parts.push('high-angle downward shot, bird\'s eye view');
else if (a.includes('侧') || a.includes('side')) parts.push('side-angle shot');
else if (a.includes('背') || a.includes('back')) parts.push('rear shot from behind character');
else parts.push('eye-level shot');
}
```
**Step 4: 提交**
```bash
git add backend-node/src/services/framePromptService.js backend-node/src/services/episodeStoryboardService.js
git commit -m "feat: expand camera angle to perspective description in storyboard image prompts"
```
---
## Task 2:四宫格图片生成(功能二后端)
**Files:**
- Modify: `backend-node/src/services/imageService.js`
**Step 1: 在 `imageService.js` 中新增 `buildQuadGridPrompt()` 函数**
在文件顶部 `const path = require('path')` 之后,引入 framePromptService(注意避免循环依赖,在函数内部 require)。
`processImageGeneration` 函数之前添加:
```js
async function buildQuadGridPrompt(db, log, cfg, storyboardId, model) {
const framePromptService = require('./framePromptService');
const sb = framePromptService.loadStoryboard(db, storyboardId);
if (!sb) return null;
const scene = framePromptService.loadScene(db, sb.scene_id);
const characterNames = framePromptService.loadStoryboardCharacterNames(db, storyboardId);
const [first, key1, key2, last] = await Promise.all([
framePromptService.generateSingleFrameExported(db, log, cfg, sb, scene, characterNames, model, 'first'),
framePromptService.generateSingleFrameExported(db, log, cfg, sb, scene, characterNames, model, 'key'),
framePromptService.generateSingleFrameExported(db, log, cfg, sb, scene, characterNames, model, 'key'),
framePromptService.generateSingleFrameExported(db, log, cfg, sb, scene, characterNames, model, 'last'),
]);
const style = cfg?.style?.default_style || '';
const styleNote = style ? `, consistent style: ${style}` : '';
return `Generate a 2x2 four-panel storyboard grid image (comic strip layout, clear panel borders). Each panel shows a different moment in sequence${styleNote}:
Panel 1 (top-left, first frame): ${first.prompt}
Panel 2 (top-right, key moment): ${key1.prompt}
Panel 3 (bottom-left, key moment): ${key2.prompt}
Panel 4 (bottom-right, last frame): ${last.prompt}
All panels have consistent character appearance and scene. Clear visible borders between panels. Sequential action progression.`;
}
```
**Step 2: 在 `framePromptService.js` 中导出 `generateSingleFrame`**
`framePromptService.js``module.exports` 中添加:
```js
generateSingleFrameExported: generateSingleFrame,
```
**Step 3: 在 `processImageGeneration()` 中添加四宫格分支**
`processImageGeneration` 中,找到 `// ── Step 1: 获取 AI 配置 ──` 之前,添加四宫格处理:
```js
// ── 四宫格模式:先生成4帧提示词,再拼装组合提示词 ──────────────────
if (row.frame_type === 'quad_grid' && row.storyboard_id) {
try {
const quadPrompt = await buildQuadGridPrompt(db, log, cfg, row.storyboard_id, row.model);
if (quadPrompt) {
db.prepare('UPDATE image_generations SET prompt = ?, updated_at = ? WHERE id = ?')
.run(quadPrompt, new Date().toISOString(), imageGenId);
row.prompt = quadPrompt;
}
} catch (quadErr) {
log.warn('[图生] 四宫格提示词生成失败,使用原始提示词', { error: quadErr.message });
}
}
```
这段代码放在 `// ── Step 1: 获取 AI 配置 ──` 注释之前。
**Step 4: 提交**
```bash
git add backend-node/src/services/imageService.js backend-node/src/services/framePromptService.js
git commit -m "feat: add quad-grid storyboard image generation support"
```
---
## Task 3:四宫格前端 UI(功能二前端)
**Files:**
- Modify: `frontweb/src/views/FilmCreate.vue`
**Step 1: 添加 `quadGridMode` 响应式变量**
在文件中找到 `const resourcePanelCollapsed = ref(false)` 附近(约第 1432 行),添加:
```js
const quadGridMode = ref(false)
```
**Step 2: 在分镜配置行添加四宫格开关 UI**
找到分镜配置区(约 511 行的 `<div class="sb-config-row">`),在最后一个 `</label>` 之后、`</div>` 之前添加:
```html
<span class="sb-config-divider"></span>
<label class="sb-config-item">
<span class="sb-config-label">四宫格序列图</span>
<el-switch v-model="quadGridMode" />
<span class="sb-config-hint">生成含4帧的序列参考图</span>
</label>
```
**Step 3: 修改 `onGenerateSbImage()` 传参**
找到 `onGenerateSbImage` 函数(约 1744 行),修改 `imagesAPI.create()` 调用:
```js
const res = await imagesAPI.create({
storyboard_id: sb.id,
drama_id: dramaId.value,
prompt: sb.image_prompt || sb.description || '',
model: undefined,
style: getSelectedStyle(),
frame_type: quadGridMode.value ? 'quad_grid' : undefined,
})
```
**Step 4: 修改批量生成分镜图也透传 quad_grid**
找到 `startBatchImageGeneration` 函数(约 1527 行),找到其中调用 `imagesAPI.create` 的地方,同样加上:
```js
frame_type: quadGridMode.value ? 'quad_grid' : undefined,
```
**Step 5: 提交**
```bash
git add frontweb/src/views/FilmCreate.vue
git commit -m "feat: add quad-grid mode switch to storyboard UI"
```
---
## 验证步骤
1. 重启后端服务
2. 打开一个剧集的分镜页
3. **验证角度**:检查一个带有"俯视"角度的分镜,点击生成分镜图,观察 AI 生成的图片背景是否呈现俯视透视
4. **验证四宫格**:打开四宫格开关,点击某个分镜的"生成分镜"按钮,等待后确认生成的图片是 2×2 四格布局
5. **验证批量**:开启四宫格模式后点击"批量生成分镜图",确认所有分镜均生成四宫格图