mirror of
https://github.com/chatfire-AI/huobao-drama.git
synced 2026-08-28 18:07:48 +08:00
refactor(storyboard): 分镜字段收敛,action/dialogue 并入 description 后删列
- storyboards 表删除 action/dialogue 两列;启动迁移带守卫(SHOW COLUMNS), 先把存量数据合并进 description(action 直接追加、dialogue 以「对白:」前缀追加)再 DROP - 画面描述统一按【镜头N】承载子镜头画面与台词/旁白(「角色名说:「…」」「旁白:…」) - storyboard_breaker prompt/skill 字段清单收敛为 description+atmosphere, 对白下限改为按 description 中台词字数计算的台词时长下限 - video-prompt skill 与 prompt_generator 系统提示词新增映射规则: 每个【镜头N】→ 1-2 个连续 3 秒段,台词从 description 原位提取,不新创台词 - 工作台分镜面板移除「动作」编辑框与「添加对白/旁白」入口,只保留画面描述与氛围 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,18 @@
|
||||
/**
|
||||
* 导出初始化 SQL — 把 src/db/mysql-schema.ts 中的内嵌 DDL/DML 生成为独立 init.sql
|
||||
* 导出初始化 SQL — 把 src/db/mysql-schema.ts 中的内嵌 DDL/种子生成为独立 init.sql
|
||||
*
|
||||
* 用法: npx tsx scripts/export-init-sql.ts [输出路径]
|
||||
* 默认输出: <repo>/docker/init.sql
|
||||
*
|
||||
* 说明:
|
||||
* - 应用启动时本就会自动执行同样的初始化(initMySqlSchema),该文件是可选产物,
|
||||
* - 应用启动时本就会自动执行同样的初始化(initMySqlSchema: 建表 + 种子),该文件是可选产物,
|
||||
* 用于 DBA 审核、预建表或挂到 MySQL 容器的 /docker-entrypoint-initdb.d/
|
||||
* - 列补齐(backfill)语句在代码里是幂等的(SHOW COLUMNS 检查后才 ALTER),
|
||||
* MySQL 不支持 ADD COLUMN IF NOT EXISTS,且新建表已包含全部列,故仅以注释形式导出
|
||||
*/
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import {
|
||||
mysqlSchemaStatements,
|
||||
mysqlColumnBackfillStatements,
|
||||
mysqlDropTableStatements,
|
||||
mysqlDataCleanupStatements,
|
||||
mysqlDataSeedStatements,
|
||||
} from '../src/db/mysql-schema.js'
|
||||
|
||||
@@ -58,31 +53,12 @@ ${mysqlSchemaStatements.map(s => `${s};`).join('\n\n')}
|
||||
`)
|
||||
|
||||
sections.push(`-- ----------------------------------------------------------------------------
|
||||
-- 2. 废弃表清理
|
||||
-- ----------------------------------------------------------------------------
|
||||
${mysqlDropTableStatements.map(s => `${s};`).join('\n')}
|
||||
`)
|
||||
|
||||
sections.push(`-- ----------------------------------------------------------------------------
|
||||
-- 3. 历史数据迁移清理(对全新数据库为 no-op,与应用启动行为保持一致)
|
||||
-- ----------------------------------------------------------------------------
|
||||
${mysqlDataCleanupStatements.map(s => `${inlineParams(s.sql, s.params)};`).join('\n')}
|
||||
`)
|
||||
|
||||
sections.push(`-- ----------------------------------------------------------------------------
|
||||
-- 4. 种子数据: 风格预设(幂等,只补缺失行)
|
||||
-- 2. 种子数据: 风格预设(幂等,只补缺失行)
|
||||
-- ----------------------------------------------------------------------------
|
||||
${mysqlDataSeedStatements.map(s => `${inlineParams(s.sql, s.params)};`).join('\n')}
|
||||
`)
|
||||
|
||||
sections.push(`-- ----------------------------------------------------------------------------
|
||||
-- 附: 列补齐语句(仅用于老库升级;新建表已包含全部列,无需执行)
|
||||
-- 代码中通过 SHOW COLUMNS 检查后按需执行,MySQL 不支持 ADD COLUMN IF NOT EXISTS
|
||||
-- ----------------------------------------------------------------------------
|
||||
${mysqlColumnBackfillStatements.map(s => `-- ${s.sql};`).join('\n')}
|
||||
`)
|
||||
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true })
|
||||
fs.writeFileSync(outPath, sections.join('\n'), 'utf8')
|
||||
console.log(`✅ 已导出: ${outPath}`)
|
||||
console.log(` 建表 ${mysqlSchemaStatements.length} 条, 清理 ${mysqlDataCleanupStatements.length} 条, 种子 ${mysqlDataSeedStatements.length} 条`)
|
||||
console.log(` 建表 ${mysqlSchemaStatements.length} 条, 种子 ${mysqlDataSeedStatements.length} 条`)
|
||||
|
||||
@@ -87,22 +87,20 @@ export const DEFAULT_PROMPTS: Record<string, { name: string; instructions: strin
|
||||
- prop_ids:当前段落出现的关键道具 ID 列表(道具在画面中被看到、使用或特写时绑定),可以为空;必须从 props 中选择
|
||||
- scene_id:若可匹配到 scenes 中已有场景,必须填写正确 scene_id;无匹配时置空
|
||||
- duration:段落总时长 8-15 秒
|
||||
- action:按【镜头1】【镜头2】…逐子镜头写角色动作与表演(谁+怎么做+肢体细节+表情)
|
||||
- description:按【镜头N】逐子镜头描述观众实际看到的画面内容
|
||||
- dialogue:该段落实际发生的对白或旁白;旁白可写为“旁白:内容”
|
||||
- description:画面描述,按【镜头1】【镜头2】…逐子镜头描述观众实际看到和听到的内容——画面(谁+具体动作+肢体细节+表情)写在前;该子镜头有台词时以「角色名说:「台词」」写在对应【镜头N】内,旁白写「旁白:内容」
|
||||
- atmosphere:氛围、光线、色调、环境感受
|
||||
|
||||
时长规则(硬约束):
|
||||
- 总量锚定:目标总时长 = 剧本字数 ÷ 500字/分钟,段落数 ≈ 目标总时长 ÷ 12秒,允许 ±20% 浮动
|
||||
- 节奏分层:过渡段(赶路/空镜/转场)8-10 秒;叙事段 10-15 秒;爆点段(特写/规则揭示/情感爆发/反转)12-15 秒且子镜头节奏放慢
|
||||
- 对白下限:段落时长 ≥ 段内对白总字数 ÷ 4.5字/秒 + 2秒表演余量,装不下的对白拆到下一个段落
|
||||
- 台词下限:段落时长 ≥ 段内台词与旁白总字数(写在 description 中的部分)÷ 4.5字/秒 + 2秒表演余量,装不下的台词拆到下一个段落
|
||||
|
||||
额外要求:
|
||||
- 优先复用 read_storyboard_context 返回的 scene_id,不要凭空创造新场景
|
||||
- 段落角色绑定必须来自 read_storyboard_context 返回的角色列表;无角色的空镜段落可传空数组
|
||||
- 段落道具绑定必须来自 read_storyboard_context 返回的道具列表;道具被使用、特写、交接或在画面中明显可见时绑定,与剧情无关的背景物品不要绑定;没有道具出现可传空数组
|
||||
- 段落描述必须能支撑后续视频生成和导出流程
|
||||
- 若一个段落没有对白,可将 dialogue 置空,但 description / action / atmosphere 仍必须完整
|
||||
- 若一个段落没有台词,description 中不写台词即可,但画面描述与 atmosphere 仍必须完整
|
||||
- 如果已有 existing_storyboards,仅在用户明确要求增量修改时参考;默认按当前剧本重新完整生成并保存整集分镜。`,
|
||||
},
|
||||
prompt_generator: {
|
||||
@@ -125,8 +123,8 @@ export const DEFAULT_PROMPTS: Record<string, { name: string; instructions: strin
|
||||
用户请求会告知要为哪个分镜生成视频提示词(附带分镜 ID)。
|
||||
|
||||
工作流程:
|
||||
1. 调用 read_storyboard_context 读取该分镜的 action、description、dialogue、atmosphere、duration 及绑定的场景/角色
|
||||
2. 据此生成 video_prompt:按 3 秒为一段、每段单独一行换行分隔;提到场景用 @场景名、提到角色用 @角色名(名字必须与列表完全一致);内容覆盖动作、画面描述、对白/旁白、氛围。一个分镜段落内允许切镜(换景别/角度/对象),段与段之间可以是不同镜头,但不跨场景;切镜点对齐分镜 action 的【镜头N】结构
|
||||
1. 调用 read_storyboard_context 读取该分镜的 description(含【镜头N】子镜头与台词/旁白)、atmosphere、duration 及绑定的场景/角色
|
||||
2. 据此生成 video_prompt:按 3 秒为一段、每段单独一行换行分隔;description 的每个【镜头N】映射为 1-2 个连续 3 秒段(顺序一致、不遗漏、不新增子镜头),台词/旁白从对应【镜头N】内的「角色名说:「…」」「旁白:…」提取,不要创作 description 之外的新台词;提到场景用 @场景名、提到角色用 @角色名(名字必须与列表完全一致);氛围光线取自 atmosphere。一个分镜段落内允许切镜(换景别/角度/对象),段与段之间可以是不同镜头,但不跨场景;切镜点对齐分镜 description 的【镜头N】结构
|
||||
3. 生成时会自动把 @名字 替换为对应参考图片标记(如 @志远 → @图片1志远),因此名字必须精确匹配场景/角色列表,不要缩写或加额外符号
|
||||
4. 调用 update_storyboard 仅更新该分镜的 video_prompt 字段,不要改动其他字段,不要重新拆分整集
|
||||
|
||||
|
||||
@@ -192,9 +192,7 @@ const readStoryboardContext = createTool({
|
||||
prop_ids: sbPropLinks.map(link => link.propId),
|
||||
shot_type: sb.shotType || '',
|
||||
duration: sb.duration || 0,
|
||||
action: sb.action || '',
|
||||
description: sb.description || '',
|
||||
dialogue: sb.dialogue || '',
|
||||
atmosphere: sb.atmosphere || '',
|
||||
video_prompt: sb.videoPrompt || '',
|
||||
}
|
||||
@@ -238,8 +236,6 @@ const saveStoryboards = createTool({
|
||||
movement: z.string().optional(),
|
||||
location: z.string().optional(),
|
||||
time: z.string().optional(),
|
||||
action: z.string().optional(),
|
||||
dialogue: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
result: z.string().optional(),
|
||||
atmosphere: z.string().optional(),
|
||||
@@ -284,7 +280,6 @@ const saveStoryboards = createTool({
|
||||
title: sb.title, shotType: sb.shot_type,
|
||||
angle: sb.angle, movement: sb.movement,
|
||||
location: sb.location, time: sb.time,
|
||||
action: sb.action, dialogue: sb.dialogue,
|
||||
description: sb.description, result: sb.result,
|
||||
atmosphere: sb.atmosphere, imagePrompt: sb.image_prompt,
|
||||
videoPrompt: sb.video_prompt, bgmPrompt: sb.bgm_prompt,
|
||||
@@ -321,7 +316,6 @@ const updateStoryboard = createTool({
|
||||
movement: z.string().optional(),
|
||||
location: z.string().optional(),
|
||||
time: z.string().optional(),
|
||||
action: z.string().optional(),
|
||||
result: z.string().optional(),
|
||||
atmosphere: z.string().optional(),
|
||||
image_prompt: z.string().optional(),
|
||||
@@ -329,7 +323,6 @@ const updateStoryboard = createTool({
|
||||
bgm_prompt: z.string().optional(),
|
||||
sound_effect: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
dialogue: z.string().optional(),
|
||||
scene_id: z.number().nullable().optional(),
|
||||
character_ids: z.array(z.number()).optional(),
|
||||
prop_ids: z.array(z.number()).optional(),
|
||||
@@ -374,7 +367,6 @@ const updateStoryboard = createTool({
|
||||
if ('movement' in fields) updates.movement = fields.movement
|
||||
if ('location' in fields) updates.location = fields.location
|
||||
if ('time' in fields) updates.time = fields.time
|
||||
if ('action' in fields) updates.action = fields.action
|
||||
if ('result' in fields) updates.result = fields.result
|
||||
if ('atmosphere' in fields) updates.atmosphere = fields.atmosphere
|
||||
if ('image_prompt' in fields) updates.imagePrompt = fields.image_prompt
|
||||
@@ -382,7 +374,6 @@ const updateStoryboard = createTool({
|
||||
if ('bgm_prompt' in fields) updates.bgmPrompt = fields.bgm_prompt
|
||||
if ('sound_effect' in fields) updates.soundEffect = fields.sound_effect
|
||||
if ('description' in fields) updates.description = fields.description
|
||||
if ('dialogue' in fields) updates.dialogue = fields.dialogue
|
||||
if ('scene_id' in fields) updates.sceneId = fields.scene_id
|
||||
if ('duration' in fields) updates.duration = fields.duration
|
||||
await db.update(schema.storyboards).set(updates).where(eq(schema.storyboards.id, storyboard_id))
|
||||
|
||||
@@ -6,7 +6,7 @@ export const mysqlSchemaStatements = [
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
genre TEXT,
|
||||
style VARCHAR(64) DEFAULT 'realistic',
|
||||
style VARCHAR(64) DEFAULT '3d',
|
||||
aspect_ratio VARCHAR(16) DEFAULT '16:9',
|
||||
total_episodes INT DEFAULT 1,
|
||||
total_duration INT DEFAULT 0,
|
||||
@@ -88,14 +88,12 @@ export const mysqlSchemaStatements = [
|
||||
shot_type TEXT,
|
||||
angle TEXT,
|
||||
movement TEXT,
|
||||
action TEXT,
|
||||
result TEXT,
|
||||
atmosphere TEXT,
|
||||
image_prompt TEXT,
|
||||
video_prompt TEXT,
|
||||
bgm_prompt TEXT,
|
||||
sound_effect TEXT,
|
||||
dialogue TEXT,
|
||||
description TEXT,
|
||||
duration INT DEFAULT 0,
|
||||
composed_image TEXT,
|
||||
@@ -285,106 +283,15 @@ export const mysqlSchemaStatements = [
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||
]
|
||||
|
||||
export const mysqlColumnBackfillStatements = [
|
||||
{ table: 'dramas', column: 'aspect_ratio', sql: "ALTER TABLE `dramas` ADD COLUMN `aspect_ratio` VARCHAR(16) DEFAULT '16:9'" },
|
||||
{ table: 'episodes', column: 'resolution', sql: "ALTER TABLE `episodes` ADD COLUMN `resolution` VARCHAR(16) DEFAULT '720p'" },
|
||||
{ table: 'characters', column: 'styling', sql: 'ALTER TABLE `characters` ADD COLUMN `styling` TEXT' },
|
||||
{ table: 'characters', column: 'final_prompt', sql: 'ALTER TABLE `characters` ADD COLUMN `final_prompt` TEXT' },
|
||||
{ table: 'characters', column: 'personality', sql: 'ALTER TABLE `characters` ADD COLUMN `personality` TEXT' },
|
||||
{ table: 'props', column: 'final_prompt', sql: 'ALTER TABLE `props` ADD COLUMN `final_prompt` TEXT' },
|
||||
{ table: 'scenes', column: 'lighting', sql: 'ALTER TABLE `scenes` ADD COLUMN `lighting` TEXT' },
|
||||
{ table: 'scenes', column: 'final_prompt', sql: 'ALTER TABLE `scenes` ADD COLUMN `final_prompt` TEXT' },
|
||||
]
|
||||
|
||||
// 废弃表清理:image_generations / video_generations 已并入 sys_task(不迁移历史);
|
||||
// ai_voices 为 TTS 功能移除后的孤儿表
|
||||
export const mysqlDropTableStatements = [
|
||||
'DROP TABLE IF EXISTS `image_generations`',
|
||||
'DROP TABLE IF EXISTS `video_generations`',
|
||||
'DROP TABLE IF EXISTS `ai_voices`',
|
||||
]
|
||||
|
||||
export const mysqlDataCleanupStatements = [
|
||||
// 悬空配置引用清理:集锁定的 image/video 配置已被删除时置空,让生成回退到当前启用配置
|
||||
{
|
||||
sql: 'UPDATE `episodes` e LEFT JOIN `ai_service_configs` c ON e.`image_config_id` = c.`id` SET e.`image_config_id` = NULL WHERE e.`image_config_id` IS NOT NULL AND c.`id` IS NULL',
|
||||
params: [],
|
||||
},
|
||||
{
|
||||
sql: 'UPDATE `episodes` e LEFT JOIN `ai_service_configs` c ON e.`video_config_id` = c.`id` SET e.`video_config_id` = NULL WHERE e.`video_config_id` IS NOT NULL AND c.`id` IS NULL',
|
||||
params: [],
|
||||
},
|
||||
// 厂商收敛:彻底移除 minimax(含历史遗留的 audio 配置,按 provider 全量清理)
|
||||
{
|
||||
sql: 'UPDATE `episodes` e JOIN `ai_service_configs` c ON e.`image_config_id` = c.`id` SET e.`image_config_id` = NULL WHERE c.`provider` = ?',
|
||||
params: ['minimax'],
|
||||
},
|
||||
{
|
||||
sql: 'UPDATE `episodes` e JOIN `ai_service_configs` c ON e.`video_config_id` = c.`id` SET e.`video_config_id` = NULL WHERE c.`provider` = ?',
|
||||
params: ['minimax'],
|
||||
},
|
||||
{
|
||||
sql: 'DELETE FROM `ai_service_configs` WHERE `provider` = ?',
|
||||
params: ['minimax'],
|
||||
},
|
||||
{
|
||||
sql: 'DELETE FROM `ai_service_providers` WHERE `provider` = ?',
|
||||
params: ['minimax'],
|
||||
},
|
||||
// 厂商收敛:仅保留 openai / gemini / volcengine,移除 deepseek、ali、vidu
|
||||
{
|
||||
sql: 'UPDATE `episodes` e JOIN `ai_service_configs` c ON e.`image_config_id` = c.`id` SET e.`image_config_id` = NULL WHERE c.`provider` IN (?, ?)',
|
||||
params: ['ali', 'vidu'],
|
||||
},
|
||||
{
|
||||
sql: 'UPDATE `episodes` e JOIN `ai_service_configs` c ON e.`video_config_id` = c.`id` SET e.`video_config_id` = NULL WHERE c.`provider` IN (?, ?)',
|
||||
params: ['ali', 'vidu'],
|
||||
},
|
||||
{
|
||||
sql: 'DELETE FROM `ai_service_configs` WHERE `provider` IN (?, ?, ?)',
|
||||
params: ['deepseek', 'ali', 'vidu'],
|
||||
},
|
||||
{
|
||||
sql: 'DELETE FROM `ai_service_providers` WHERE `provider` IN (?, ?, ?)',
|
||||
params: ['deepseek', 'ali', 'vidu'],
|
||||
},
|
||||
// 旧硬编码风格值归并:cinematic 并入写实电影
|
||||
{
|
||||
sql: 'UPDATE `dramas` SET `style` = ? WHERE `style` = ?',
|
||||
params: ['realistic', 'cinematic'],
|
||||
},
|
||||
// 视频提示词格式收敛:旧的 <n> 时间段分隔符统一替换为换行
|
||||
{
|
||||
sql: "UPDATE `storyboards` SET `video_prompt` = REPLACE(`video_prompt`, '<n>', CHAR(10)) WHERE `video_prompt` LIKE '%<n>%'",
|
||||
params: [],
|
||||
},
|
||||
// agent_configs 表已废弃:Agent prompt 改为 workspace/prompts/*.md 文件维护,清理老库
|
||||
{
|
||||
sql: 'DROP TABLE IF EXISTS `agent_configs`',
|
||||
params: [],
|
||||
},
|
||||
// 视频模型收敛:Seedance 2.0 三个官方型号,默认 doubao-seedance-2-0-fast-260128(数组首位即生效模型)
|
||||
{
|
||||
sql: 'UPDATE `ai_service_configs` SET `model` = ? WHERE `service_type` = ? AND `provider` = ? AND `model` NOT LIKE ?',
|
||||
params: [
|
||||
'["doubao-seedance-2-0-fast-260128","doubao-seedance-2-0-260128","doubao-seedance-2-0-mini-260615"]',
|
||||
'video',
|
||||
'volcengine',
|
||||
'%doubao-seedance-2-0%',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 风格预设种子数据 — value 存入 dramas.style,prompt 注入生图提示词
|
||||
*/
|
||||
export const stylePresetSeeds = [
|
||||
{ name: '3D', value: '3d', sortOrder: 1, prompt: '3D render, Pixar-style animation, soft studio lighting, high detail, subsurface scattering', description: '三维渲染卡通质感,适合轻松明快的短剧' },
|
||||
{ name: '动漫', value: 'anime', sortOrder: 2, prompt: 'anime style, cel shading, vibrant colors, clean line art, Japanese animation', description: '日式赛璐璐动画风格' },
|
||||
{ name: '写实电影', value: 'realistic', sortOrder: 3, prompt: 'photorealistic, cinematic film still, 35mm, natural lighting, shallow depth of field, high detail', description: '电影级真人写实质感' },
|
||||
{ name: '吉卜力', value: 'ghibli', sortOrder: 4, prompt: 'Studio Ghibli style, hand-painted, soft watercolor background, warm nostalgic tone', description: '吉卜力手绘治愈风' },
|
||||
{ name: '水彩', value: 'watercolor', sortOrder: 5, prompt: 'watercolor illustration, soft washes, visible paper texture, delicate brush strokes', description: '水彩插画质感' },
|
||||
{ name: '漫画', value: 'comic', sortOrder: 6, prompt: 'comic book style, bold outlines, halftone shading, dynamic colors, flat graphic look', description: '美式漫画粗线条风格' },
|
||||
{ name: '3D 漫剧', value: '3d', sortOrder: 1, prompt: '3D CG animation style, game-engine quality render, semi-realistic stylized characters, refined facial features, detailed materials and textures, cinematic lighting, high detail', description: '游戏引擎级 3D 渲染,半写实角色,当前短剧主流的 3D 漫剧质感' },
|
||||
{ name: '日漫赛璐璐', value: 'anime', sortOrder: 2, prompt: 'Japanese anime style, cel shading, clean crisp line art, vivid saturated colors, expressive character designs, detailed painted backgrounds', description: '日式赛璐璐动画风格' },
|
||||
{ name: '吉卜力手绘', value: 'ghibli', sortOrder: 3, prompt: 'Studio Ghibli style, hand-drawn animation, soft watercolor painted backgrounds, warm nostalgic lighting, gentle natural palette, whimsical cozy atmosphere', description: '吉卜力手绘治愈风' },
|
||||
{ name: '水彩绘本', value: 'watercolor', sortOrder: 4, prompt: 'watercolor illustration style, soft translucent washes, visible paper texture, delicate fluid brushwork, light airy atmosphere, hand-painted storybook feel', description: '水彩插画质感' },
|
||||
{ name: '美式漫画', value: 'comic', sortOrder: 5, prompt: 'Western comic book style, bold black ink outlines, halftone dot shading, dynamic saturated colors, dramatic contrast lighting, flat graphic novel look', description: '美式漫画粗线条风格' },
|
||||
]
|
||||
|
||||
// INSERT ... SELECT WHERE NOT EXISTS → 幂等:只补缺失行,不覆盖用户编辑,
|
||||
@@ -394,25 +301,10 @@ export const mysqlDataSeedStatements = stylePresetSeeds.map((s) => ({
|
||||
params: [s.name, s.value, s.prompt, s.description, s.sortOrder, new Date().toISOString(), new Date().toISOString(), s.value],
|
||||
}))
|
||||
|
||||
export async function ensureMySqlColumn(pool: Pool, table: string, column: string, alterSql: string) {
|
||||
const [rows] = await pool.query(`SHOW COLUMNS FROM \`${table}\` LIKE ?`, [column]) as any[]
|
||||
if (Array.isArray(rows) && rows.length) return
|
||||
await pool.query(alterSql)
|
||||
}
|
||||
|
||||
export async function initMySqlSchema(pool: Pool) {
|
||||
for (const statement of mysqlSchemaStatements) {
|
||||
await pool.query(statement)
|
||||
}
|
||||
for (const statement of mysqlColumnBackfillStatements) {
|
||||
await ensureMySqlColumn(pool, statement.table, statement.column, statement.sql)
|
||||
}
|
||||
for (const statement of mysqlDropTableStatements) {
|
||||
await pool.query(statement)
|
||||
}
|
||||
for (const cleanup of mysqlDataCleanupStatements) {
|
||||
await pool.query(cleanup.sql, cleanup.params)
|
||||
}
|
||||
for (const seed of mysqlDataSeedStatements) {
|
||||
await pool.query(seed.sql, seed.params)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const dramas = mysqlTable('dramas', {
|
||||
title: text('title').notNull(),
|
||||
description: text('description'),
|
||||
genre: text('genre'),
|
||||
style: varchar('style', { length: 64 }).default('realistic'),
|
||||
style: varchar('style', { length: 64 }).default('3d'),
|
||||
aspectRatio: varchar('aspect_ratio', { length: 16 }).default('16:9'),
|
||||
totalEpisodes: int('total_episodes').default(1),
|
||||
totalDuration: int('total_duration').default(0),
|
||||
@@ -114,14 +114,12 @@ export const storyboards = mysqlTable('storyboards', {
|
||||
shotType: text('shot_type'),
|
||||
angle: text('angle'),
|
||||
movement: text('movement'),
|
||||
action: text('action'),
|
||||
result: text('result'),
|
||||
atmosphere: text('atmosphere'),
|
||||
imagePrompt: text('image_prompt'),
|
||||
videoPrompt: text('video_prompt'),
|
||||
bgmPrompt: text('bgm_prompt'),
|
||||
soundEffect: text('sound_effect'),
|
||||
dialogue: text('dialogue'),
|
||||
description: text('description'),
|
||||
duration: int('duration').default(0),
|
||||
composedImage: text('composed_image'),
|
||||
|
||||
@@ -93,8 +93,6 @@ app.post('/', async (c) => {
|
||||
storyboardNumber: body.storyboard_number || 1,
|
||||
title: body.title,
|
||||
description: body.description,
|
||||
action: body.action,
|
||||
dialogue: body.dialogue,
|
||||
sceneId: body.scene_id,
|
||||
duration: body.duration || 10,
|
||||
createdAt: ts,
|
||||
@@ -131,8 +129,8 @@ app.put('/:id', async (c) => {
|
||||
|
||||
const fieldMap: Record<string, string> = {
|
||||
title: 'title', description: 'description', shot_type: 'shotType',
|
||||
angle: 'angle', movement: 'movement', action: 'action',
|
||||
dialogue: 'dialogue', duration: 'duration', video_prompt: 'videoPrompt',
|
||||
angle: 'angle', movement: 'movement', duration: 'duration',
|
||||
video_prompt: 'videoPrompt',
|
||||
image_prompt: 'imagePrompt', scene_id: 'sceneId', location: 'location',
|
||||
time: 'time', atmosphere: 'atmosphere', result: 'result',
|
||||
bgm_prompt: 'bgmPrompt', sound_effect: 'soundEffect',
|
||||
|
||||
@@ -73,7 +73,7 @@ export async function startVideoPromptBatch(
|
||||
await agent.generate([{
|
||||
role: 'user',
|
||||
content: `请为分镜 #${sb.storyboardNumber}(ID:${sb.id})生成视频提示词(video_prompt)。视频模型:${videoLabel},请根据该模型的特性和时长限制生成。
|
||||
请先调用 read_storyboard_context 获取该分镜的画面描述、动作、氛围、对白/旁白及时长,据此生成 video_prompt(按 3 秒分段换行、用 @角色名/@场景名/@道具名 引用参考素材;段落内允许多镜头切镜,段与段可以是不同景别/角度/对象,但不跨场景,切镜点对齐分镜 action 的【镜头N】结构),然后调用 update_storyboard 保存到分镜 ID:${sb.id}。只更新 video_prompt 字段,不要改动其他字段,不要重新拆分整集。`,
|
||||
请先调用 read_storyboard_context 获取该分镜的画面描述(含【镜头N】子镜头与台词/旁白)、氛围及时长,据此生成 video_prompt(按 3 秒分段换行、用 @角色名/@场景名/@道具名 引用参考素材;段落内允许多镜头切镜,段与段可以是不同景别/角度/对象,但不跨场景,切镜点对齐分镜 description 的【镜头N】结构),然后调用 update_storyboard 保存到分镜 ID:${sb.id}。只更新 video_prompt 字段,不要改动其他字段,不要重新拆分整集。`,
|
||||
}], { maxSteps: 8, requestContext })
|
||||
// 以实际落库为准判定成败
|
||||
const [fresh] = await db.select().from(schema.storyboards).where(eq(schema.storyboards.id, sb.id))
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 视频提示词规范 — 根据分镜段落内容生成按时间
|
||||
|
||||
# 视频提示词(分镜段落 → video_prompt)
|
||||
|
||||
根据单个分镜段落的 action / description / dialogue / atmosphere / duration,生成驱动 AI 视频生成的 `video_prompt`。**一个分镜段落 = 一段 8-15 秒的视频,内部允许切镜**:段与段之间可以是不同镜头(换景别/角度/对象),用硬切衔接;但**全程不跨场景**、不闪回。
|
||||
根据单个分镜段落的 description(含【镜头N】子镜头结构与台词/旁白)/ atmosphere / duration,生成驱动 AI 视频生成的 `video_prompt`。**一个分镜段落 = 一段 8-15 秒的视频,内部允许切镜**:段与段之间可以是不同镜头(换景别/角度/对象),用硬切衔接;但**全程不跨场景**、不闪回。
|
||||
|
||||
## 格式
|
||||
|
||||
@@ -17,6 +17,14 @@ description: 视频提示词规范 — 根据分镜段落内容生成按时间
|
||||
6-9秒:切回中景,@小红微笑着走向小明坐下,小明说:「你终于来了。」
|
||||
```
|
||||
|
||||
## 与分镜描述的映射
|
||||
|
||||
`description` 是 video_prompt 的唯一内容来源(画面、动作、台词、旁白都在其中),转换规则:
|
||||
|
||||
- `description` 的每个 `【镜头N】` 映射为 **1-2 个连续的 3 秒段**,顺序一致、不遗漏、不合并、不新增子镜头
|
||||
- 台词/旁白从对应 `【镜头N】` 内的「角色名说:「…」」「旁白:…」提取,分配到该子镜头映射的段;**不要创作 description 之外的新台词**
|
||||
- 画面动作以 `description` 为准;`atmosphere` 只用于补各段的光线、色调与氛围描写
|
||||
|
||||
## 段内结构
|
||||
|
||||
每一段按此顺序组织内容(可省略无内容的项,但动作/画面必须有):
|
||||
@@ -24,7 +32,7 @@ description: 视频提示词规范 — 根据分镜段落内容生成按时间
|
||||
**时间范围 + 场景@引用 + 景别/运镜 + 角色@引用+主体动作·表情 + 对白/旁白 + 氛围光线**
|
||||
|
||||
- **第一段必须建立空间**:场景 + 机位 + 角色的位置与状态,让观众一眼知道在哪、看谁
|
||||
- **切镜**:切镜后的段开头用"切到/切回"等衔接词,并重新交代景别与主体;切镜点应对齐分镜 `action` 中的 `【镜头N】` 结构
|
||||
- **切镜**:切镜后的段开头用"切到/切回"等衔接词,并重新交代景别与主体;切镜点应对齐分镜 `description` 中的 `【镜头N】` 结构
|
||||
- **景别/运镜**:每段一个镜头状态(近景/中景/全景/特写;固定/推/拉/摇/跟);单个子镜头内运镜连续,切镜后可更换运镜方式
|
||||
- **动作**:每段一个主动作,动词具体可见(走、转身、抬头、攥紧、停顿)
|
||||
- **情绪全部转为可见描写**:不要"他很伤心/气氛紧张"这类抽象词,写成"他低下头、手指攥紧杯沿、呼吸变重"
|
||||
|
||||
@@ -31,11 +31,11 @@ description: 分镜拆解专业规范 — 将剧本拆分为可承载多个子
|
||||
| 叙事段 | 10-15 秒 | 常规剧情推进、对话 |
|
||||
| 爆点段 | 12-15 秒 | 特写、规则揭示、情感爆发、反转;子镜头节奏放慢,单个子镜头可停留 4-6 秒 |
|
||||
|
||||
## 对白时长下限(硬规则)
|
||||
## 台词时长下限(硬规则)
|
||||
|
||||
**段落时长 ≥ 段内对白总字数 ÷ 4.5字/秒 + 2秒表演余量**
|
||||
**段落时长 ≥ 段内台词与旁白总字数(写在 description 中的部分)÷ 4.5字/秒 + 2秒表演余量**
|
||||
|
||||
装不下的对白必须拆到下一个段落,不允许把演不完的对白塞进一个段落。
|
||||
装不下的台词必须拆到下一个段落,不允许把演不完的台词塞进一个段落。
|
||||
|
||||
## 镜头要素
|
||||
|
||||
@@ -45,15 +45,13 @@ description: 分镜拆解专业规范 — 将剧本拆分为可承载多个子
|
||||
4. **景别**:段落内主导景别;多景别段落写组合,如"中景+特写"
|
||||
5. **角度**:平视/仰视/俯视/侧面/背面
|
||||
6. **运镜**:固定/推镜/拉镜/摇镜/跟镜/移镜(段落内不同子镜头可不同)
|
||||
7. **动作** `action`:按 `【镜头1】…【镜头2】…` 逐子镜头组织,每个子镜头写清谁 + 具体怎么做 + 肢体细节 + 表情
|
||||
8. **对话** `dialogue`:该段落的完整对话
|
||||
9. **画面结果** `result`:段落结尾的即时后果 + 视觉细节
|
||||
10. **氛围** `atmosphere`:光线 + 色调 + 声音 + 整体氛围
|
||||
11. **时长** `duration`:段落总时长 8-15 秒,且满足对白时长下限
|
||||
12. **画面描述** `description`:按 `【镜头N】` 逐子镜头描述观众实际看到的画面
|
||||
13. **场景关联**:若能匹配已有场景,必须填写 `scene_id`
|
||||
14. **角色关联**:填写 `character_ids`,绑定当前段落涉及的 0 到多个角色
|
||||
15. **道具关联**:填写 `prop_ids`,绑定当前段落出现的关键道具(0 到多个)
|
||||
7. **画面描述** `description`:按 `【镜头1】…【镜头2】…` 逐子镜头描述观众实际看到和听到的内容——画面(谁 + 具体动作 + 肢体细节 + 表情)写在前;该子镜头有台词时以「角色名说:「台词」」写在对应 `【镜头N】` 内,旁白写「旁白:内容」
|
||||
8. **画面结果** `result`:段落结尾的即时后果 + 视觉细节
|
||||
9. **氛围** `atmosphere`:光线 + 色调 + 声音 + 整体氛围
|
||||
10. **时长** `duration`:段落总时长 8-15 秒,且满足台词时长下限
|
||||
11. **场景关联**:若能匹配已有场景,必须填写 `scene_id`
|
||||
12. **角色关联**:填写 `character_ids`,绑定当前段落涉及的 0 到多个角色
|
||||
13. **道具关联**:填写 `prop_ids`,绑定当前段落出现的关键道具(0 到多个)
|
||||
|
||||
## 场景关联规则
|
||||
|
||||
@@ -79,8 +77,7 @@ description: 分镜拆解专业规范 — 将剧本拆分为可承载多个子
|
||||
|
||||
## 质量要求
|
||||
|
||||
- `description` 要适合人读,按子镜头详细描述观众实际看到的画面内容
|
||||
- `description` 要适合人读,按子镜头详细描述观众实际看到和听到的内容;台词/旁白直接写在对应 `【镜头N】` 内
|
||||
- `image_prompt` 要突出单帧构图、角色外观、环境和光线(对应段落第一个子镜头)
|
||||
- `bgm_prompt` 和 `sound_effect` 用简洁短语即可,但不能空泛到只有“紧张”“悲伤”
|
||||
- 若存在旁白,统一写入 `dialogue`,格式为 `旁白:内容`
|
||||
- 如需调整,调用 `update_storyboard` 修改具体段落
|
||||
|
||||
+8
-49
@@ -1,7 +1,7 @@
|
||||
-- ============================================================================
|
||||
-- Huobao Drama 初始化 SQL
|
||||
-- 由 backend/scripts/export-init-sql.ts 从 backend/src/db/mysql-schema.ts 生成
|
||||
-- 生成时间: 2026-08-08T14:35:02.119Z
|
||||
-- 生成时间: 2026-08-08T15:00:54.313Z
|
||||
--
|
||||
-- 注意: 应用启动时会自动执行同等初始化(幂等),本文件不是部署必需,
|
||||
-- 仅供 DBA 审核或在应用外预建表使用
|
||||
@@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS dramas (
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
genre TEXT,
|
||||
style VARCHAR(64) DEFAULT 'realistic',
|
||||
style VARCHAR(64) DEFAULT '3d',
|
||||
aspect_ratio VARCHAR(16) DEFAULT '16:9',
|
||||
total_episodes INT DEFAULT 1,
|
||||
total_duration INT DEFAULT 0,
|
||||
@@ -99,14 +99,12 @@ CREATE TABLE IF NOT EXISTS storyboards (
|
||||
shot_type TEXT,
|
||||
angle TEXT,
|
||||
movement TEXT,
|
||||
action TEXT,
|
||||
result TEXT,
|
||||
atmosphere TEXT,
|
||||
image_prompt TEXT,
|
||||
video_prompt TEXT,
|
||||
bgm_prompt TEXT,
|
||||
sound_effect TEXT,
|
||||
dialogue TEXT,
|
||||
description TEXT,
|
||||
duration INT DEFAULT 0,
|
||||
composed_image TEXT,
|
||||
@@ -296,49 +294,10 @@ CREATE TABLE IF NOT EXISTS assets (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 2. 废弃表清理
|
||||
-- 2. 种子数据: 风格预设(幂等,只补缺失行)
|
||||
-- ----------------------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS `image_generations`;
|
||||
DROP TABLE IF EXISTS `video_generations`;
|
||||
DROP TABLE IF EXISTS `ai_voices`;
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 3. 历史数据迁移清理(对全新数据库为 no-op,与应用启动行为保持一致)
|
||||
-- ----------------------------------------------------------------------------
|
||||
UPDATE `episodes` e LEFT JOIN `ai_service_configs` c ON e.`image_config_id` = c.`id` SET e.`image_config_id` = NULL WHERE e.`image_config_id` IS NOT NULL AND c.`id` IS NULL;
|
||||
UPDATE `episodes` e LEFT JOIN `ai_service_configs` c ON e.`video_config_id` = c.`id` SET e.`video_config_id` = NULL WHERE e.`video_config_id` IS NOT NULL AND c.`id` IS NULL;
|
||||
UPDATE `episodes` e JOIN `ai_service_configs` c ON e.`image_config_id` = c.`id` SET e.`image_config_id` = NULL WHERE c.`provider` = 'minimax';
|
||||
UPDATE `episodes` e JOIN `ai_service_configs` c ON e.`video_config_id` = c.`id` SET e.`video_config_id` = NULL WHERE c.`provider` = 'minimax';
|
||||
DELETE FROM `ai_service_configs` WHERE `provider` = 'minimax';
|
||||
DELETE FROM `ai_service_providers` WHERE `provider` = 'minimax';
|
||||
UPDATE `episodes` e JOIN `ai_service_configs` c ON e.`image_config_id` = c.`id` SET e.`image_config_id` = NULL WHERE c.`provider` IN ('ali', 'vidu');
|
||||
UPDATE `episodes` e JOIN `ai_service_configs` c ON e.`video_config_id` = c.`id` SET e.`video_config_id` = NULL WHERE c.`provider` IN ('ali', 'vidu');
|
||||
DELETE FROM `ai_service_configs` WHERE `provider` IN ('deepseek', 'ali', 'vidu');
|
||||
DELETE FROM `ai_service_providers` WHERE `provider` IN ('deepseek', 'ali', 'vidu');
|
||||
UPDATE `dramas` SET `style` = 'realistic' WHERE `style` = 'cinematic';
|
||||
UPDATE `storyboards` SET `video_prompt` = REPLACE(`video_prompt`, '<n>', CHAR(10)) WHERE `video_prompt` LIKE '%<n>%';
|
||||
DROP TABLE IF EXISTS `agent_configs`;
|
||||
UPDATE `ai_service_configs` SET `model` = '["doubao-seedance-2-0-fast-260128","doubao-seedance-2-0-260128","doubao-seedance-2-0-mini-260615"]' WHERE `service_type` = 'video' AND `provider` = 'volcengine' AND `model` NOT LIKE '%doubao-seedance-2-0%';
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 4. 种子数据: 风格预设(幂等,只补缺失行)
|
||||
-- ----------------------------------------------------------------------------
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '3D', '3d', '3D render, Pixar-style animation, soft studio lighting, high detail, subsurface scattering', '三维渲染卡通质感,适合轻松明快的短剧', 1, 1, '2026-08-08T14:35:02.118Z', '2026-08-08T14:35:02.119Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = '3d');
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '动漫', 'anime', 'anime style, cel shading, vibrant colors, clean line art, Japanese animation', '日式赛璐璐动画风格', 2, 1, '2026-08-08T14:35:02.119Z', '2026-08-08T14:35:02.119Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = 'anime');
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '写实电影', 'realistic', 'photorealistic, cinematic film still, 35mm, natural lighting, shallow depth of field, high detail', '电影级真人写实质感', 3, 1, '2026-08-08T14:35:02.119Z', '2026-08-08T14:35:02.119Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = 'realistic');
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '吉卜力', 'ghibli', 'Studio Ghibli style, hand-painted, soft watercolor background, warm nostalgic tone', '吉卜力手绘治愈风', 4, 1, '2026-08-08T14:35:02.119Z', '2026-08-08T14:35:02.119Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = 'ghibli');
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '水彩', 'watercolor', 'watercolor illustration, soft washes, visible paper texture, delicate brush strokes', '水彩插画质感', 5, 1, '2026-08-08T14:35:02.119Z', '2026-08-08T14:35:02.119Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = 'watercolor');
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '漫画', 'comic', 'comic book style, bold outlines, halftone shading, dynamic colors, flat graphic look', '美式漫画粗线条风格', 6, 1, '2026-08-08T14:35:02.119Z', '2026-08-08T14:35:02.119Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = 'comic');
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 附: 列补齐语句(仅用于老库升级;新建表已包含全部列,无需执行)
|
||||
-- 代码中通过 SHOW COLUMNS 检查后按需执行,MySQL 不支持 ADD COLUMN IF NOT EXISTS
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- ALTER TABLE `dramas` ADD COLUMN `aspect_ratio` VARCHAR(16) DEFAULT '16:9';
|
||||
-- ALTER TABLE `episodes` ADD COLUMN `resolution` VARCHAR(16) DEFAULT '720p';
|
||||
-- ALTER TABLE `characters` ADD COLUMN `styling` TEXT;
|
||||
-- ALTER TABLE `characters` ADD COLUMN `final_prompt` TEXT;
|
||||
-- ALTER TABLE `characters` ADD COLUMN `personality` TEXT;
|
||||
-- ALTER TABLE `props` ADD COLUMN `final_prompt` TEXT;
|
||||
-- ALTER TABLE `scenes` ADD COLUMN `lighting` TEXT;
|
||||
-- ALTER TABLE `scenes` ADD COLUMN `final_prompt` TEXT;
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '3D 漫剧', '3d', '3D CG animation style, game-engine quality render, semi-realistic stylized characters, refined facial features, detailed materials and textures, cinematic lighting, high detail', '游戏引擎级 3D 渲染,半写实角色,当前短剧主流的 3D 漫剧质感', 1, 1, '2026-08-08T15:00:54.312Z', '2026-08-08T15:00:54.313Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = '3d');
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '日漫赛璐璐', 'anime', 'Japanese anime style, cel shading, clean crisp line art, vivid saturated colors, expressive character designs, detailed painted backgrounds', '日式赛璐璐动画风格', 2, 1, '2026-08-08T15:00:54.313Z', '2026-08-08T15:00:54.313Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = 'anime');
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '吉卜力手绘', 'ghibli', 'Studio Ghibli style, hand-drawn animation, soft watercolor painted backgrounds, warm nostalgic lighting, gentle natural palette, whimsical cozy atmosphere', '吉卜力手绘治愈风', 3, 1, '2026-08-08T15:00:54.313Z', '2026-08-08T15:00:54.313Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = 'ghibli');
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '水彩绘本', 'watercolor', 'watercolor illustration style, soft translucent washes, visible paper texture, delicate fluid brushwork, light airy atmosphere, hand-painted storybook feel', '水彩插画质感', 4, 1, '2026-08-08T15:00:54.313Z', '2026-08-08T15:00:54.313Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = 'watercolor');
|
||||
INSERT INTO `style_presets` (`name`, `value`, `prompt`, `description`, `sort_order`, `is_active`, `created_at`, `updated_at`) SELECT '美式漫画', 'comic', 'Western comic book style, bold black ink outlines, halftone dot shading, dynamic saturated colors, dramatic contrast lighting, flat graphic novel look', '美式漫画粗线条风格', 5, 1, '2026-08-08T15:00:54.313Z', '2026-08-08T15:00:54.313Z' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `style_presets` WHERE `value` = 'comic');
|
||||
|
||||
@@ -510,7 +510,6 @@
|
||||
<span v-else class="shot-avatars-empty">0 角色</span>
|
||||
</div>
|
||||
<div class="shot-flags">
|
||||
<span class="shot-flag flag-dialogue" :class="{ on: !!sb.dialogue }" :title="sb.dialogue ? '有对白' : '无对白'"><i class="dot"></i>白</span>
|
||||
<span class="shot-flag flag-video" :class="{ on: hasVid(sb) }" :title="hasVid(sb) ? '已生成视频' : '未生成视频'"><i class="dot"></i>视</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -569,27 +568,13 @@
|
||||
<span class="detail-section-title">分镜描述</span>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span class="field-label">画面描述</span>
|
||||
<textarea :value="selectedSb.description || ''" class="textarea" rows="4" @blur="updateField(selectedSb, 'description', $event.target.value)" placeholder="分镜画面描述" />
|
||||
<span class="field-label">画面描述 <span class="dim">(按【镜头1】【镜头2】…逐子镜头描述;台词写「角色名说:「台词」」,旁白写「旁白:内容」)</span></span>
|
||||
<textarea :value="selectedSb.description || ''" class="textarea" rows="8" @blur="updateField(selectedSb, 'description', $event.target.value)" placeholder="分镜画面描述" />
|
||||
</label>
|
||||
<div class="field-grid field-grid-2">
|
||||
<label class="field">
|
||||
<span class="field-label">动作</span>
|
||||
<textarea :value="selectedSb.action || ''" class="textarea" rows="3" @blur="updateField(selectedSb, 'action', $event.target.value)" placeholder="角色动作与表演" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">氛围</span>
|
||||
<textarea :value="selectedSb.atmosphere || ''" class="textarea" rows="3" @blur="updateField(selectedSb, 'atmosphere', $event.target.value)" placeholder="光线、色调、空气感、环境氛围" />
|
||||
</label>
|
||||
</div>
|
||||
<label v-if="dialogueOpen || selectedSb.dialogue" class="field">
|
||||
<span class="field-label">对白 / 旁白</span>
|
||||
<textarea :value="selectedSb.dialogue || ''" class="textarea" rows="2" @blur="updateField(selectedSb, 'dialogue', $event.target.value)" placeholder="角色名:台词内容 或 旁白:内容" />
|
||||
<label class="field">
|
||||
<span class="field-label">氛围</span>
|
||||
<textarea :value="selectedSb.atmosphere || ''" class="textarea" rows="3" @blur="updateField(selectedSb, 'atmosphere', $event.target.value)" placeholder="光线、色调、空气感、环境氛围" />
|
||||
</label>
|
||||
<button v-else type="button" class="dialogue-add" @click="dialogueOpen = true">
|
||||
<MessageSquarePlus :size="12" />
|
||||
添加对白 / 旁白
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
@@ -605,7 +590,7 @@
|
||||
{{ (selectedSb.video_prompt || selectedSb.videoPrompt) ? '重新生成' : 'AI 生成' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="detail-section-copy">根据当前分镜的画面描述、动作、氛围以及对白 / 旁白生成</div>
|
||||
<div class="detail-section-copy">根据当前分镜的画面描述(含台词/旁白)与氛围生成</div>
|
||||
<MentionTextarea
|
||||
:model-value="selectedSb.video_prompt || selectedSb.videoPrompt || ''"
|
||||
:options="mentionOptions"
|
||||
@@ -1535,7 +1520,7 @@
|
||||
import { toast } from 'vue-sonner'
|
||||
import {
|
||||
Users, Video, FileText, FolderKanban, Clapperboard, Download, Loader2,
|
||||
MapPin, Play, MessageSquarePlus, Plus, X, ListTodo,
|
||||
MapPin, Play, Plus, X, ListTodo,
|
||||
} from 'lucide-vue-next'
|
||||
import { dramaAPI, episodeAPI, storyboardAPI, characterAPI, sceneAPI, propAPI, taskAPI, mergeAPI, aiConfigAPI, uploadAPI } from '~/composables/useApi'
|
||||
import { useAgent } from '~/composables/useAgent'
|
||||
@@ -2507,7 +2492,6 @@ const sceneOptions = computed(() => [
|
||||
...scenes.value.map(s => ({ label: `${s.location} · ${s.time || '未设时间'}`, value: s.id })),
|
||||
])
|
||||
|
||||
const dialogueOpen = ref(false)
|
||||
|
||||
function sceneShotCount(sceneId) {
|
||||
return sbs.value.filter(sb => String(sb?.scene_id || sb?.sceneId || '') === String(sceneId)).length
|
||||
@@ -2743,7 +2727,7 @@ function genVideoPrompt(sb) {
|
||||
|
||||
该分镜信息:时长 ${sb.duration || 10}s;场景:${getSceneName(sb) || '未绑定'};角色:${charNames};道具:${propNames}。
|
||||
|
||||
请先调用 read_storyboard_context 获取该分镜的画面描述、动作、氛围、对白/旁白,据此生成 video_prompt(按 3 秒分段换行、用 @角色名/@场景名/@道具名 引用参考素材),然后调用 update_storyboard 保存到分镜 ID:${sb.id}。只更新 video_prompt 字段,不要改动其他字段,不要重新拆分整集。`, dramaId, epId.value, refresh, chatModelOverride(), chatConfigId())
|
||||
请先调用 read_storyboard_context 获取该分镜的画面描述(含【镜头N】子镜头与台词/旁白)、氛围及时长,据此生成 video_prompt(按 3 秒分段换行、用 @角色名/@场景名/@道具名 引用参考素材;段落内允许多镜头切镜,但不跨场景,切镜点对齐 description 的【镜头N】结构),然后调用 update_storyboard 保存到分镜 ID:${sb.id}。只更新 video_prompt 字段,不要改动其他字段,不要重新拆分整集。`, dramaId, epId.value, refresh, chatModelOverride(), chatConfigId())
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
@@ -3147,7 +3131,6 @@ watch(selectedSb, (sb) => {
|
||||
videoRefAudioUrls.value = []
|
||||
videoRefImageUrls.value = []
|
||||
videoDuration.value = Number(sb?.duration || 10)
|
||||
dialogueOpen.value = false
|
||||
})
|
||||
|
||||
function pickFile(accept, cb) {
|
||||
@@ -4128,27 +4111,6 @@ onMounted(async () => { await refresh(); loadConfigs(); syncExtractStatus() })
|
||||
}
|
||||
.storyboard-ref-thumb:disabled { cursor: default; }
|
||||
.storyboard-ref-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
/* 对白折叠入口 */
|
||||
.dialogue-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
border: 1px dashed var(--surface-outline);
|
||||
border-radius: var(--radius);
|
||||
background: transparent;
|
||||
color: var(--text-3);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: color 0.16s var(--ease-out), border-color 0.16s var(--ease-out), background 0.16s var(--ease-out);
|
||||
}
|
||||
.dialogue-add:hover {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
}
|
||||
.storyboard-ref-main { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||
.storyboard-ref-line { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
||||
.storyboard-ref-name {
|
||||
@@ -4268,14 +4230,7 @@ onMounted(async () => { await refresh(); loadConfigs(); syncExtractStatus() })
|
||||
}
|
||||
.shot-flag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--bg-3); flex-shrink: 0; }
|
||||
.shot-flag.on { color: var(--text-2); }
|
||||
.shot-flag.flag-dialogue.on .dot { background: var(--warning); }
|
||||
.shot-flag.flag-video.on .dot { background: var(--info); }
|
||||
.shot-dialogue {
|
||||
font-size: 10px; color: var(--text-3); margin-top: 2px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
padding-left: 2px; border-left: 2px solid var(--border);
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.detail-panel { flex: 1; display: flex; flex-direction: column; overflow-y: auto; min-width: 0; }
|
||||
.detail-head { display: flex; align-items: center; gap: 8px; padding: 9px 14px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
||||
|
||||
Reference in New Issue
Block a user