# 火山引擎视频生成服务 - 开发计划 ## 📋 项目概述 为 AI Chatbot 系统新增独立的"火山引擎视频"服务商,支持基于豆包 Seedance 模型的视频生成能力,包括文生视频、图生视频等多种场景。 --- ## 🎯 核心功能规划 ### 1️⃣ **文生视频(Text-to-Video)** - **功能描述**:用户输入文本提示词,AI 生成视频 - **支持模型**: - `doubao-seedance-1-0-pro-250528` - `doubao-seedance-1-0-pro-fast-251015` - `doubao-seedance-1-0-lite-t2v-250428` - **参数支持**: - 分辨率:480p / 720p / 1080p - 宽高比:16:9 / 4:3 / 1:1 / 3:4 / 9:16 / 21:9 - 时长:2~12 秒 - 帧率:24 fps - 水印:true / false - 种子:随机数种子 - 固定摄像头:camerafixed ### 2️⃣ **图生视频-首帧模式(Image-to-Video: First Frame)** - **功能描述**:上传首帧图片 + 文本提示词,生成视频 - **支持模型**: - `doubao-seedance-1-0-pro-250528` - `doubao-seedance-1-0-pro-fast-251015` - `doubao-seedance-1-0-lite-i2v-250428` - **使用场景**: - 静态图片动态化 - 图片续写视频故事 ### 3️⃣ **图生视频-首尾帧模式(Image-to-Video: First & Last Frame)** - **功能描述**:上传首帧 + 尾帧图片,AI 生成中间过渡视频 - **支持模型**: - `doubao-seedance-1-0-pro-250528` - `doubao-seedance-1-0-lite-i2v-250428` - **使用场景**: - 两张图片之间的平滑过渡 - 镜头运动轨迹控制(如 360 度环绕) ### 4️⃣ **图生视频-参考图模式(Image-to-Video: Reference Images)** - **功能描述**:上传 1-4 张参考图 + 文本描述,生成包含参考元素的视频 - **支持模型**: - `doubao-seedance-1-0-lite-i2v-250428` - **使用场景**: - 多元素融合视频 - 角色/场景组合(如"[图1]男生和[图2]柯基在[图3]草坪上") ### 5️⃣ **连续视频生成(Sequential Video Generation)** - **功能描述**:自动使用前一个视频的尾帧作为下一个视频的首帧,生成多段连续视频 - **技术实现**: - 启用 `return_last_frame: true` - 循环调用生成任务 - **使用场景**: - 长视频故事创作 - 连续镜头拼接 --- ## 🏗️ 技术架构设计 ### 📁 文件结构 ``` ai-chatbot-main/ ├── lib/ai/providers/ │ └── volcano-video.ts # 🆕 火山视频 Provider 核心逻辑 ├── lib/storage/ │ └── video-archiver.ts # 🆕 视频归档服务(下载+上传S3) ├── lib/jobs/ │ └── video-archive-job.ts # 🆕 视频归档定时任务 ├── components/ │ ├── qianwen-video-loading.tsx # ♻️ 复用:视频生成加载动画(已存在) │ ├── video-preview.tsx # 🆕 视频预览组件(支持播放器) │ └── video-params-bar.tsx # 🆕 视频参数栏(横向布局,类似官方) ├── app/(chat)/ │ ├── page.tsx # ✏️ 扩展:集成视频参数栏到聊天界面 │ ├── components/ │ │ └── chat-input.tsx # ✏️ 扩展:动态图片上传按钮 │ └── api/chat/ │ └── route.ts # ✏️ 扩展:集成视频生成 Tool ├── app/api/ │ ├── admin/ │ │ ├── ai-providers/ │ │ │ └── route.ts # ✏️ 添加:volcano-video 类型验证 │ │ └── ai-models/ │ │ └── route.ts # ✏️ 扩展:模型能力配置(capabilities) │ └── jobs/ │ └── archive-videos/ │ └── route.ts # 🆕 视频归档触发API(用于定时任务) ├── lib/db/ │ ├── schema.ts # ✏️ 更新:Message 表增加视频存储状态字段 │ └── queries.ts # ✏️ 新增:查询待归档视频的方法 └── types/ └── ai.ts # ✏️ 添加:VideoGenerationOptions + ModelCapabilities 类型 ``` --- ## 🔧 详细实现方案 ### **阶段 1:数据库与类型系统扩展** #### 1.1 实际数据库表结构(已部署) **✅ AiProvider 表(已更新)** ```sql -- 表名:AiProvider -- 类型约束已更新,支持 'volcano-video' CHECK (type IN ('alibaba', 'openai', 'custom', 'local', 'gemini', 'qianwen-video', 'volcano-vision', 'volcano-video')) ``` **✅ AiModel 表(已扩展)** ```sql -- 完整表结构 Table "public.AiModel" Column | Type | Nullable | Default ------------------+-----------------------------+----------+------------------- id | uuid | not null | gen_random_uuid() providerId | uuid | not null | modelId | character varying(128) | not null | displayName | character varying(128) | not null | description | text | | supportsVision | boolean | not null | false supportsImageGen | boolean | not null | false isEnabled | boolean | not null | true isDefault | boolean | not null | false sortOrder | integer | not null | 0 createdAt | timestamp without time zone | not null | now() updatedAt | timestamp without time zone | not null | now() capabilities | jsonb | | -- 🆕 新增 defaultParams | jsonb | | -- 🆕 新增 -- 主键 PRIMARY KEY (id) -- 外键 FOREIGN KEY (providerId) REFERENCES AiProvider(id) ON DELETE CASCADE ``` **✅ Message_v2 表(已扩展)** ```sql -- 新增视频存储相关字段 ALTER TABLE "Message_v2" ADD COLUMN "video_storage_status" VARCHAR(20) DEFAULT 'temporary'; ALTER TABLE "Message_v2" ADD COLUMN "temporary_video_url" TEXT; ALTER TABLE "Message_v2" ADD COLUMN "permanent_video_url" TEXT; ALTER TABLE "Message_v2" ADD COLUMN "archived_at" TIMESTAMP; ALTER TABLE "Message_v2" ADD COLUMN "video_metadata" JSONB; -- 新增索引(优化归档查询性能) CREATE INDEX "idx_message_video_archive" ON "Message_v2"("video_storage_status", "createdAt") WHERE "video_storage_status" = 'temporary' AND "permanent_video_url" IS NULL; CREATE INDEX "idx_message_permanent_video" ON "Message_v2"("permanent_video_url") WHERE "permanent_video_url" IS NOT NULL; ``` #### 1.2 Schema 定义(lib/db/schema.ts) ```typescript // 更新服务商类型枚举 export const aiProvider = pgTable("AiProvider", { // ... 其他字段 ... type: varchar("type", { enum: ["alibaba", "openai", "custom", "local", "gemini", "qianwen-video", "volcano-vision", "volcano-video"] // 🆕 已添加 }).notNull().default("custom"), }); // 扩展 AiModel 表结构 export const aiModel = pgTable('AiModel', { // ... 现有字段 ... // 🆕 模型能力配置(JSON 格式存储) capabilities: jsonb('capabilities'), // 🆕 默认参数配置(JSON 格式存储) defaultParams: jsonb('defaultParams'), }); // 扩展 Message_v2 表 export const message = pgTable('Message_v2', { // ... 现有字段 ... // 🆕 视频存储状态(用于归档管理) videoStorageStatus: varchar('video_storage_status', { length: 20 }) .default('temporary'), // 🆕 临时视频URL(火山引擎返回的24小时有效URL) temporaryVideoUrl: text('temporary_video_url'), // 🆕 永久视频URL(S3存储的永久URL) permanentVideoUrl: text('permanent_video_url'), // 🆕 归档时间 archivedAt: timestamp('archived_at'), // 🆕 视频元数据(JSON格式) videoMetadata: jsonb('video_metadata'), }); ``` **注意事项**: - ✅ 迁移脚本:`lib/db/migrations/0018_add_volcano_video_support.sql` - ✅ 已在生产数据库执行完成 - ✅ 表所有者:`AiModel` 由 `ai_chatbot_user` 所有,`Message_v2` 由 `chatbot_user` 所有 - ✅ 索引已创建,优化归档查询性能 #### 1.2 定义 TypeScript 类型 **文件**:`types/ai.ts` ```typescript // 视频生成模式 export type VideoGenerationMode = | 'text-to-video' // 文生视频 | 'first-frame' // 图生视频-首帧 | 'first-last-frame' // 图生视频-首尾帧 | 'reference-images'; // 图生视频-参考图 // 模型能力配置 export interface VideoModelCapabilities { modelId: string; modelName: string; supportedModes: VideoGenerationMode[]; supportedResolutions: ('480p' | '720p' | '1080p')[]; supportedRatios: ('16:9' | '4:3' | '1:1' | '3:4' | '9:16' | '21:9' | 'adaptive')[]; durationRange: { min: number; max: number }; // 时长范围(秒) supportWatermark: boolean; supportCameraFixed: boolean; supportSeed: boolean; supportReturnLastFrame: boolean; maxReferenceImages?: number; // 参考图模式最多支持几张图 } // 视频生成选项 export interface VideoGenerationOptions { prompt: string; model: string; mode: VideoGenerationMode; // 🆕 生成模式 // 图片输入(可选) firstFrameImage?: string; // 首帧图片 URL/Base64 lastFrameImage?: string; // 尾帧图片 URL/Base64 referenceImages?: string[]; // 参考图数组(1-4张) // 视频参数 resolution?: '480p' | '720p' | '1080p'; ratio?: '16:9' | '4:3' | '1:1' | '3:4' | '9:16' | '21:9' | 'adaptive'; duration?: number; // 2-12 秒 fps?: number; // 帧率(默认 24) watermark?: boolean; // 是否包含水印 cameraFixed?: boolean; // 是否固定摄像头 seed?: number; // 随机种子 returnLastFrame?: boolean; // 是否返回尾帧 } // 视频生成结果 export interface VideoGenerationResult { taskId: string; status: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled'; videoUrl?: string; lastFrameUrl?: string; // 尾帧 URL(用于连续生成) error?: { code: string; message: string; }; usage?: { completionTokens: number; totalTokens: number; }; createdAt: number; updatedAt: number; } ``` --- ### **阶段 2:核心 Provider 开发** #### 2.1 创建 `volcano-video.ts` **文件**:`lib/ai/providers/volcano-video.ts` **核心功能模块:** ```typescript // 1. 初始化客户端 async function createVolcanoVideoClient(apiKey: string) { // 配置 ARK SDK 客户端 // Base URL: https://ark.cn-beijing.volces.com/api/v3 } // 2. 创建视频生成任务 async function createVideoTask(options: VideoGenerationOptions) { // 构建 content 数组 // - 文本提示词 // - 图片(首帧/尾帧/参考图) // 发送 POST 请求到 /content_generation/tasks/create // 返回 task_id } // 3. 轮询任务状态 async function pollTaskStatus(taskId: string, maxRetries = 60) { // 每 10 秒查询一次任务状态 // 状态:queued → running → succeeded / failed // 超时时间:10 分钟(视频生成耗时较长) } // 4. 构建参数字符串 function buildParameterString(options: VideoGenerationOptions) { // 生成 --ratio 16:9 --dur 5 --rs 720p 格式的参数 } // 5. 图片处理:上传到S3 + 转换Base64 🆕 async function processImageInput(imageUrl: string) { // 步骤1:下载图片 const response = await fetch(imageUrl); const buffer = await response.arrayBuffer(); // 步骤2:上传到S3获取永久URL(复用项目存储服务) import { uploadFile } from '@/lib/storage/service'; const filename = `volcano-video-input-${Date.now()}.jpg`; const { url: permanentUrl } = await uploadFile(filename, buffer); console.log(`[Volcano Video] 图片已上传到S3:${permanentUrl}`); // 步骤3:转换为Base64(火山引擎API需要) const base64 = Buffer.from(buffer).toString('base64'); return { permanentUrl, // S3永久URL(保存到数据库) base64, // Base64(发送给火山引擎API) }; } // 6. 格式化输出 function formatVideoResponse(result: VideoGenerationResult) { // 生成 Markdown 格式的视频链接 // 支持视频预览组件渲染 } // 7. 主函数:doGenerate export async function doGenerate(options: VideoGenerationOptions) { // 1. 验证输入参数 // 2. 创建任务 // 3. 轮询状态 // 4. 返回结果 } // 8. 流式响应(模拟进度) export async function doGenerateStream(options: VideoGenerationOptions) { // 生成过程中推送进度消息: // - "正在创建视频生成任务..." // - "任务已提交,等待处理..." // - "正在生成视频(预计 1-3 分钟)..." // - "视频生成完成!" } ``` --- ### **阶段 3:视频永久存储系统开发** 🆕 #### 3.1 视频归档服务 **文件**:`lib/storage/video-archiver.ts` **功能需求:下载临时视频并上传到S3** ```typescript import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { db } from '@/lib/db'; import { message } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; interface VideoArchiveOptions { messageId: string; temporaryUrl: string; taskId: string; metadata?: { duration?: number; resolution?: string; ratio?: string; }; } // ✅ 复用项目现有的存储服务 import { uploadFile } from '@/lib/storage/service'; export class VideoArchiver { /** * 上传视频到S3(复用项目现有的统一存储服务) */ private async uploadVideoToStorage( buffer: Buffer, filename: string ): Promise { // 使用项目统一的 uploadFile 方法 // 自动使用:数据库配置的S3 → 环境变量S3 → Vercel Blob const result = await uploadFile(filename, buffer); return result.url; } /** * 归档视频:下载 → 上传S3 → 更新数据库 */ async archiveVideo(options: VideoArchiveOptions): Promise { const { messageId, temporaryUrl, taskId, metadata } = options; try { console.log(`[VideoArchiver] 开始归档视频:${messageId}`); // 1. 更新状态为"归档中" await this.updateVideoStatus(messageId, 'archiving'); // 2. 下载视频 console.log(`[VideoArchiver] 下载视频:${temporaryUrl}`); const videoBuffer = await this.downloadVideo(temporaryUrl); if (!videoBuffer) { throw new Error('Failed to download video from temporary URL'); } // 3. 生成文件名 const filename = this.generateVideoFilename(messageId, taskId); // 4. 上传到S3(使用项目统一存储服务) console.log(`[VideoArchiver] 上传视频:${filename}`); const permanentUrl = await this.uploadVideoToS3(videoBuffer, filename); // 5. 更新数据库 await this.updateVideoUrls(messageId, { permanentUrl, s3Key, fileSize: videoBuffer.length, ...metadata, }); console.log(`[VideoArchiver] ✅ 视频归档完成:${permanentUrl}`); return permanentUrl; } catch (error) { console.error(`[VideoArchiver] ❌ 归档失败:`, error); // 更新状态为"失败" await this.updateVideoStatus(messageId, 'failed'); throw error; } } /** * 下载视频(带重试机制) */ private async downloadVideo(url: string, retries = 3): Promise { for (let i = 0; i < retries; i++) { try { const response = await fetch(url, { headers: { 'User-Agent': 'AI-Chatbot-Video-Archiver/1.0', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); return Buffer.from(arrayBuffer); } catch (error) { console.error(`[VideoArchiver] 下载失败 (尝试 ${i + 1}/${retries}):`, error); if (i === retries - 1) { return null; } // 等待后重试(指数退避) await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000)); } } return null; } /** * 上传视频到S3(使用项目统一存储服务) */ private async uploadVideoToS3(buffer: Buffer, filename: string): Promise { // ✅ 直接使用项目已有的 uploadFile 方法 // 支持:数据库配置的S3 / 环境变量S3 / Vercel Blob const result = await uploadFile(filename, buffer); console.log(`[VideoArchiver] 视频已上传到S3:${result.url}`); return result.url; } /** * 生成视频文件名 * 项目的 uploadFile 会自动生成 uploads/timestamp-random.ext 格式 */ private generateVideoFilename(messageId: string, taskId: string): string { // 使用唯一标识作为文件名 return `video-${messageId}-${taskId}.mp4`; } /** * 更新视频状态 */ private async updateVideoStatus( messageId: string, status: 'temporary' | 'archiving' | 'archived' | 'failed' ) { await db.update(message) .set({ videoStorageStatus: status }) .where(eq(message.id, messageId)); } /** * 更新视频URL和元数据 */ private async updateVideoUrls( messageId: string, data: { permanentUrl: string; s3Key: string; fileSize: number; duration?: number; resolution?: string; ratio?: string; } ) { await db.update(message) .set({ permanentVideoUrl: data.permanentUrl, videoStorageStatus: 'archived', archivedAt: new Date(), videoMetadata: { s3Key: data.s3Key, fileSize: data.fileSize, duration: data.duration, resolution: data.resolution, ratio: data.ratio, }, }) .where(eq(message.id, messageId)); } } // 单例模式 export const videoArchiver = new VideoArchiver(); ``` #### 3.2 定时归档任务 **文件**:`lib/jobs/video-archive-job.ts` **功能需求:8小时后自动触发归档** ```typescript import { db } from '@/lib/db'; import { message } from '@/lib/db/schema'; import { eq, and, lte, isNull } from 'drizzle-orm'; import { videoArchiver } from '@/lib/storage/video-archiver'; /** * 查询需要归档的视频消息 * 条件: * 1. 视频状态为 'temporary'(临时) * 2. 创建时间超过8小时 * 3. 还没有永久URL */ export async function getVideosToArchive() { const eightHoursAgo = new Date(Date.now() - 8 * 60 * 60 * 1000); return await db.query.message.findMany({ where: and( eq(message.videoStorageStatus, 'temporary'), lte(message.createdAt, eightHoursAgo), isNull(message.permanentVideoUrl) ), limit: 20, // 每次处理20个,8小时周期可以处理更多 }); } /** * 执行视频归档任务 */ export async function runVideoArchiveJob() { console.log('[VideoArchiveJob] 开始执行视频归档任务...'); try { // 1. 查询待归档的视频 const videos = await getVideosToArchive(); if (videos.length === 0) { console.log('[VideoArchiveJob] 没有需要归档的视频'); return; } console.log(`[VideoArchiveJob] 发现 ${videos.length} 个待归档视频`); // 2. 并发归档(控制并发数为3) const concurrency = 3; const results = []; for (let i = 0; i < videos.length; i += concurrency) { const batch = videos.slice(i, i + concurrency); const batchPromises = batch.map(async (video) => { try { // 从消息内容中提取视频URL和元数据 const videoData = extractVideoDataFromMessage(video); if (!videoData) { console.warn(`[VideoArchiveJob] 消息 ${video.id} 没有视频数据`); return { success: false, messageId: video.id }; } // 执行归档 const permanentUrl = await videoArchiver.archiveVideo({ messageId: video.id, temporaryUrl: videoData.temporaryUrl, taskId: videoData.taskId, metadata: videoData.metadata, }); return { success: true, messageId: video.id, permanentUrl }; } catch (error) { console.error(`[VideoArchiveJob] 归档失败 (${video.id}):`, error); return { success: false, messageId: video.id, error }; } }); const batchResults = await Promise.allSettled(batchPromises); results.push(...batchResults); } // 3. 统计结果 const succeeded = results.filter(r => r.status === 'fulfilled' && r.value.success ).length; const failed = results.length - succeeded; console.log(`[VideoArchiveJob] ✅ 完成:成功 ${succeeded},失败 ${failed}`); } catch (error) { console.error('[VideoArchiveJob] 任务执行出错:', error); } } /** * 从消息中提取视频数据 */ function extractVideoDataFromMessage(msg: any) { try { // 假设视频数据存储在 message.content 或 message.toolInvocations 中 // 具体结构根据实际情况调整 if (msg.temporaryVideoUrl) { return { temporaryUrl: msg.temporaryVideoUrl, taskId: msg.videoMetadata?.taskId || msg.id, metadata: { duration: msg.videoMetadata?.duration, resolution: msg.videoMetadata?.resolution, ratio: msg.videoMetadata?.ratio, }, }; } return null; } catch (error) { console.error('提取视频数据失败:', error); return null; } } ``` #### 3.3 定时任务触发器 **文件**:`app/api/jobs/archive-videos/route.ts` **功能需求:提供HTTP接口用于Cron触发(每小时执行一次)** ```typescript import { NextResponse } from 'next/server'; import { runVideoArchiveJob } from '@/lib/jobs/video-archive-job'; // 验证请求来源(防止未授权调用) function verifyAuthToken(request: Request): boolean { const authHeader = request.headers.get('authorization'); const token = authHeader?.replace('Bearer ', ''); // 与环境变量中的密钥比对 return token === process.env.CRON_SECRET; } export async function POST(request: Request) { try { // 1. 验证授权 if (!verifyAuthToken(request)) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } // 2. 执行归档任务 await runVideoArchiveJob(); // 3. 返回成功响应 return NextResponse.json({ success: true, message: 'Video archive job completed', timestamp: new Date().toISOString(), }); } catch (error: any) { console.error('[API] Video archive job failed:', error); return NextResponse.json( { success: false, error: error.message, timestamp: new Date().toISOString(), }, { status: 500 } ); } } // 允许GET请求查看任务状态 export async function GET(request: Request) { if (!verifyAuthToken(request)) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // 返回待归档视频统计 const { getVideosToArchive } = await import('@/lib/jobs/video-archive-job'); const videos = await getVideosToArchive(); return NextResponse.json({ pendingCount: videos.length, timestamp: new Date().toISOString(), }); } ``` #### 3.4 配置 Vercel Cron(或其他调度器) **文件**:`vercel.json` ```json { "crons": [ { "path": "/api/jobs/archive-videos", "schedule": "0 * * * *" } ] } ``` **说明:** - `0 * * * *` = 每小时执行一次(整点触发) - 由于归档延迟设为8小时,每小时检查一次即可 - 相比5分钟周期,大幅降低服务器负载 **或使用外部Cron服务(cron-job.org / GitHub Actions):** ```bash # 每小时执行一次 0 * * * * curl -X POST https://yourdomain.com/api/jobs/archive-videos \ -H "Authorization: Bearer YOUR_CRON_SECRET" ``` --- ### **阶段 4:前端参数面板开发** #### 3.1 创建视频参数设置栏(横向设计) **文件**:`components/video-params-bar.tsx` **功能需求:** **📋 参考官方设计 - 横向参数栏:** ``` ┌────────────────────────────────────────────────────────────────┐ │ 输入框区域(上传图片 + 文本输入) │ │ [+ 图片] [+ 尾帧] 输入文字,描述你想创作的画面内容... │ └────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────┐ │ 参数栏(横向排列,类似官方设计) │ │ ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌─────┐ ┌─────┐ ┌────┐│ │ │视频生成 ▼│ │视频 3.0 │ │首尾帧 ▼ │ │16:9 │ │720P │ │5s ││ │ └──────────┘ └─────────┘ └──────────┘ └─────┘ └─────┘ └────┘│ │ ┌──────────┐ ┌─────────────────────────────────────┐ │ │ │运镜:无 ▼│ │ [生成] │ │ │ └──────────┘ └─────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────┘ 点击"首尾帧▼"展开下拉菜单: ┌────────────────┐ │ ✓ 首尾帧 │ │ 智能多帧 New │ │ 主体参考 New │ └────────────────┘ 点击"16:9"或"720P"展开分层面板: ┌──────────────────────────────────────┐ │ 选择比例 │ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐│ │ │21:9│ │16:9│ │4:3│ │1:1│ │3:4│ │9:16││ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘│ │ │ │ 选择分辨率 │ │ ┌─────┐ ┌────────┐ │ │ │720P │ │1080P + │ │ │ └─────┘ └────────┘ │ └──────────────────────────────────────┘ ``` **核心功能:** ```typescript interface VideoParamsBarProps { // 当前选择的模型 currentModel?: VideoModelCapabilities; // 初始参数(来自系统默认配置) initialParams?: Partial; // 参数变化回调 onParamsChange?: (params: VideoGenerationOptions) => void; } export const VideoParamsBar = ({ currentModel, initialParams, onParamsChange }: VideoParamsBarProps) => { // 1. 状态管理 const [mode, setMode] = useState('text-to-video'); const [params, setParams] = useState({ resolution: '720p', ratio: '16:9', duration: 5, watermark: false, cameraFixed: false, ...initialParams }); // 2. 根据模型能力动态显示/禁用选项 const availableModes = currentModel?.supportedModes || []; const availableResolutions = currentModel?.supportedResolutions || []; const availableRatios = currentModel?.supportedRatios || []; // 3. 参数更新(实时同步) const updateParam = ( key: K, value: VideoGenerationOptions[K] ) => { const newParams = { ...params, [key]: value }; setParams(newParams); onParamsChange?.(newParams); }; return (
{/* 模式选择下拉菜单 */} setMode('text-to-video')}> 文生视频 setMode('first-frame')}> 图生视频-首帧 setMode('first-last-frame')}> 首尾帧过渡 setMode('reference-images')}> 参考图融合 New {/* 模型显示(只读) */} {currentModel?.modelName || 'Seedance Pro'} {/* 宽高比 + 分辨率组合选择器 */} {/* 宽高比选择 */}
{['21:9', '16:9', '4:3', '1:1', '3:4', '9:16'].map(ratio => ( ))}
{/* 分辨率选择 */}
{availableResolutions.map(res => ( ))}
{/* 时长滑块 */} updateParam('duration', value)} className="mt-2" />
2秒 12秒
{/* 运镜/摄像头设置 */} updateParam('cameraFixed', false)}> 自由运镜 updateParam('cameraFixed', true)}> 固定视角 {/* 高级选项(折叠) */}
updateParam('watermark', checked)} />
updateParam('returnLastFrame', checked)} />
updateParam('seed', Number(e.target.value) || undefined)} className="mt-1" />
); }; ``` **智能交互逻辑:** 1. **模式联动模型选择**: - 用户选择"首尾帧过渡" → 自动筛选支持该模式的模型 - 如果当前模型不支持,自动切换到 `doubao-seedance-1-0-pro-250528` - 在模式下拉菜单中,不支持的模式显示为禁用状态(灰色) 2. **动态图片上传区域**: - 选择"文生视频" → 隐藏图片上传按钮 - 选择"图生视频-首帧" → 显示 `[+ 图片]` 按钮 - 选择"首尾帧" → 显示 `[+ 首帧] [+ 尾帧]` 两个按钮 - 选择"参考图" → 显示 `[+ 参考图1-4]` 可多次点击,最多4张 3. **参数约束与提示**: - 根据模型能力禁用不支持的选项(按钮显示为灰色 disabled) - 参数冲突时显示 Tooltip 提示: ``` "参考图模式不支持 1080p 分辨率" "Lite 模型不支持固定摄像头功能" ``` - 1080p 选项带 `+` 图标,表示高级功能 4. **实时参数同步**: - 用户调整任何参数 → 立即触发 `onParamsChange` 回调 - 参数实时保存到本地存储(localStorage) - 下次打开时恢复上次的参数设置 5. **视觉反馈**: - 当前选中的参数按钮使用 `variant="default"` 高亮显示 - 悬停时显示详细信息(如:16:9 → "宽屏格式,适合电脑/电视观看") - 参数变更时,按钮有短暂的动画效果 #### 3.2 集成到聊天界面(参考官方布局) **文件**:`app/(chat)/page.tsx` 或相关聊天组件 **实现方案:** ```typescript // 聊天界面布局(完整结构)
{/* 消息列表区域 */}
{/* 输入区域(固定在底部) */}
{/* 图片上传 + 文本输入框 */}
{/* 动态显示图片上传按钮(根据模式) */} {selectedModel?.type === 'volcano-video' && ( <> {videoParams.mode === 'first-frame' && ( )} {videoParams.mode === 'first-last-frame' && ( <> )} {videoParams.mode === 'reference-images' && ( )} )} {/* 已上传图片预览 */} {uploadedImages.length > 0 && (
{uploadedImages.map((img, idx) => (
))}
)} {/* 文本输入框 */}