From 9bc53320331f2e7631ca392a114695fd998a455e Mon Sep 17 00:00:00 2001 From: cc <94575594@qq.com> Date: Mon, 30 Mar 2026 21:07:44 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E6=B7=BB=E5=8A=A0AI=E8=AF=95=E9=A2=98?= =?UTF-8?q?=E8=AE=B2=E8=A7=A3=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/试题讲解生成页面开发_34fbcd9a.md | 250 ++ .../试题讲解视频生成功能开发_c0649647.md | 144 ++ index.html | 3 +- package.json | 2 - server/README.md | 73 + server/index.js | 33 + server/package-lock.json | 1020 ++++++++ server/package.json | 16 + server/routes/video.js | 284 +++ src/config/index.js | 73 + src/md/ali-audio-tts.md | 1189 +++++++++ src/md/ali-image.md | 722 ++++++ src/md/doubao_image.md | 2150 +++++++++++++++++ src/router/index.js | 6 + src/utils/videoComposer.js | 139 ++ src/views/HomePage.vue | 24 + src/views/QuestionExplanation.vue | 1847 ++++++++++++++ vite.config.js | 32 +- 18 files changed, 8000 insertions(+), 7 deletions(-) create mode 100644 .codebuddy/plans/试题讲解生成页面开发_34fbcd9a.md create mode 100644 .codebuddy/plans/试题讲解视频生成功能开发_c0649647.md create mode 100644 server/README.md create mode 100644 server/index.js create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/routes/video.js create mode 100644 src/md/ali-audio-tts.md create mode 100644 src/md/ali-image.md create mode 100644 src/md/doubao_image.md create mode 100644 src/utils/videoComposer.js create mode 100644 src/views/QuestionExplanation.vue diff --git a/.codebuddy/plans/试题讲解生成页面开发_34fbcd9a.md b/.codebuddy/plans/试题讲解生成页面开发_34fbcd9a.md new file mode 100644 index 0000000..50685e9 --- /dev/null +++ b/.codebuddy/plans/试题讲解生成页面开发_34fbcd9a.md @@ -0,0 +1,250 @@ +--- +name: 试题讲解生成页面开发 +overview: 开发一个试题讲解生成页面,用户输入试题后,系统调用AI文本模型分析试题复杂度并拆分为多个讲解点,然后为每个讲解点调用豆包图片生成模型创建讲解图片,最后调用阿里云语音合成模型为每张图片生成讲解音频,实现幻灯片式自动播放。 +design: + architecture: + framework: vue + styleKeywords: + - Glassmorphism + - Dark Theme + - Modern + - Smooth Animations + fontSystem: + fontFamily: PingFang SC + heading: + size: 32px + weight: 600 + subheading: + size: 18px + weight: 500 + body: + size: 16px + weight: 400 + colorSystem: + primary: + - "#8b5cf6" + - "#6366f1" + background: + - "#0f172a" + - "#1e293b" + text: + - "#f8fafc" + - "#94a3b8" + functional: + - "#10b981" + - "#ef4444" + - "#f59e0b" +todos: + - id: add-api-config + content: 在config/index.js中添加试题讲解API配置(文本分析、图片生成、音频合成) + status: completed + - id: create-page-component + content: 创建QuestionExplanation.vue页面组件,实现试题输入和状态管理 + status: completed + dependencies: + - add-api-config + - id: implement-analysis-logic + content: 实现试题分析逻辑,调用文本模型拆分讲解点并生成内容 + status: completed + dependencies: + - create-page-component + - id: implement-resource-generation + content: 实现图片生成和音频合成的批量生成逻辑 + status: completed + dependencies: + - implement-analysis-logic + - id: implement-slideshow-player + content: 实现幻灯片播放器和音频同步播放逻辑 + status: completed + dependencies: + - implement-resource-generation + - id: add-route-and-card + content: 在router添加路由配置并在HomePage添加功能卡片 + status: completed + dependencies: + - create-page-component +--- + +## 产品概述 + +开发一个智能试题讲解生成页面,实现根据试题内容自动生成图文并茂的幻灯片式讲解。 + +## 核心功能 + +- 试题输入:提供文本框供用户输入试题内容 +- 智能分析:调用AI文本模型分析试题复杂度,动态决定讲解点数量(3-8个) +- 内容生成:为每个讲解点生成详细讲解文本和对应的图片生成提示词 +- 图片生成:调用豆包AI图片生成模型,批量生成讲解图片(支持组图生成) +- 音频合成:调用阿里云语音合成模型,为每个讲解文本生成配套音频 +- 幻灯片播放:实现自动播放逻辑,图片与音频同步,音频播完自动切换下一张 +- 播放控制:支持播放/暂停、上一张/下一张、进度指示器等交互控制 + +## 技术栈 + +- 前端框架:Vue 3 + Composition API +- 路由管理:Vue Router +- HTTP客户端:Axios +- 样式方案:Scoped CSS + Glassmorphism UI风格(与现有项目保持一致) +- API调用:前端直接调用(演示模式) + +## 技术架构 + +### 系统流程 + +```mermaid +graph TD + A[用户输入试题] --> B[调用文本模型分析] + B --> C{分析试题复杂度} + C -->|简单| D[生成3个讲解点] + C -->|中等| E[生成5个讲解点] + C -->|复杂| F[生成7-8个讲解点] + D --> G[为每个讲解点生成详细文本和图片提示词] + E --> G + F --> G + G --> H[批量生成图片] + G --> I[批量合成音频] + H --> J[加载图片资源] + I --> K[加载音频资源] + J --> L[幻灯片播放] + K --> L + L --> M{音频播放完成?} + M -->|是| N[切换下一张] + N --> L + M -->|否| L +``` + +### 模块划分 + +1. **输入模块**:试题文本输入框、提交按钮、清空按钮 +2. **处理模块**:试题分析、讲解文本生成、图片prompt生成 +3. **资源生成模块**:图片生成API调用、音频合成API调用 +4. **播放模块**:幻灯片展示、音频播放、自动切换逻辑 +5. **控制模块**:播放/暂停、上下切换、进度指示 + +### API集成 + +**文本分析模型**:doubao-seed-2-0-lite-260215 + +- 用途:分析试题复杂度、拆分讲解点、生成讲解文本和图片提示词 +- API:https://ark.cn-beijing.volces.com/api/v3/chat/completions +- 认证:使用 DOUBAO_KEY + +**图片生成模型**:doubao-seedream-5-0-260128 + +- 用途:批量生成讲解图片(支持组图生成) +- API:https://ark.cn-beijing.volces.com/api/v3/images/generations +- 认证:使用 DOUBAO_KEY +- 特性:sequential_image_generation="auto",max_images=8 + +**语音合成模型**:qwen3-tts-flash + +- 用途:为讲解文本生成音频 +- API:https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation +- 认证:使用 BAILIAN_API_KEY +- 音色:Cherry(芊悦,阳光积极的小姐姐音色) + +## 实现要点 + +### 性能优化 + +- 图片生成采用组图模式,一次API调用生成所有图片,减少网络请求 +- 音频合成采用并发请求,提升生成速度 +- 资源预加载:在播放第一张时预加载后续图片和音频 +- 使用URL.createObjectURL管理音频资源,避免内存泄漏 + +### 错误处理 + +- API调用失败重试机制(最多重试2次) +- 部分资源生成失败时,显示占位图/文本提示,不影响其他讲解点播放 +- 超时处理:图片生成60秒超时,音频生成30秒超时 + +### 播放逻辑 + +- 使用HTML5 Audio API监听'ended'事件触发自动切换 +- 当前音频播放完毕后立即切换到下一张图片并播放对应音频 +- 播放完成后显示结束状态,提供重新播放按钮 + +### 状态管理 + +``` +- idle: 初始状态 +- analyzing: 正在分析试题 +- generating: 正在生成图片和音频 +- ready: 资源准备完成,等待播放 +- playing: 正在播放 +- paused: 暂停 +- completed: 播放完成 +- error: 错误状态 +``` + +## 目录结构 + +### 新增文件 + +``` +src/ +├── views/ +│ └── QuestionExplanation.vue # [NEW] 试题讲解页面主组件 +├── config/ +│ └── index.js # [MODIFY] 添加新API配置 +├── router/ +│ └── index.js # [MODIFY] 添加路由配置 +└── views/ + └── HomePage.vue # [MODIFY] 添加功能卡片 +``` + +### 详细文件说明 + +**QuestionExplanation.vue** - 试题讲解页面主组件 + +- 实现试题输入界面 +- 调用文本模型分析试题并生成讲解内容 +- 调用图片生成API批量生成讲解图片 +- 调用语音合成API生成配套音频 +- 实现幻灯片式自动播放逻辑 +- 提供播放控制(播放/暂停、上下切换、进度指示) +- 采用glassmorphism UI风格,深色主题 + +**config/index.js** - 添加API配置 + +- 试题讲解文本分析API配置 +- 图片生成API配置(已有部分配置,可能需要补充) +- 语音合成API配置(已有部分配置,可能需要补充) + +**router/index.js** - 添加路由 + +- 路径:/question-explanation +- 组件:QuestionExplanation + +**HomePage.vue** - 添加功能卡片 + +- 标题:AI试题讲解生成 +- 描述:智能分析试题,自动生成图文讲解,幻灯片式播放,让解题过程清晰易懂 +- 图标:presentation/slides相关图标 +- 路由:/question-explanation + +## 设计风格 + +采用现代深色主题 + Glassmorphism(玻璃态)设计风格,与现有项目保持视觉一致性。 + +## 页面布局 + +采用垂直布局,分为三个主要区域: + +1. **顶部输入区**:标题 + 试题输入框 + 操作按钮 +2. **中间展示区**:幻灯片播放区域(图片 + 讲解文本) +3. **底部控制区**:播放控制按钮 + 进度指示器 + +## 视觉特点 + +- 深色背景(#0f172a)配合玻璃态卡片 +- 渐变色彩点缀(紫色/蓝色渐变) +- 流畅的过渡动画 +- 响应式设计,适配不同屏幕尺寸 + +## 交互设计 + +- 加载状态:骨架屏 + 进度提示 +- 播放状态:当前幻灯片高亮,进度条动态更新 +- 控制按钮:hover效果 + 点击反馈 +- 平滑过渡:图片切换使用淡入淡出动画 \ No newline at end of file diff --git a/.codebuddy/plans/试题讲解视频生成功能开发_c0649647.md b/.codebuddy/plans/试题讲解视频生成功能开发_c0649647.md new file mode 100644 index 0000000..fad3438 --- /dev/null +++ b/.codebuddy/plans/试题讲解视频生成功能开发_c0649647.md @@ -0,0 +1,144 @@ +--- +name: 试题讲解视频生成功能开发 +overview: 在 QuestionExplanation.vue 页面添加视频生成功能,将已生成的图片和音频合成为完整视频,支持在线预览、下载,并显示详细的生成进度。 +design: + architecture: + framework: vue + styleKeywords: + - Dark Theme + - Glassmorphism + - Minimalist + fontSystem: + fontFamily: PingFang SC + heading: + size: 1.25rem + weight: 600 + subheading: + size: 1rem + weight: 500 + body: + size: 0.875rem + weight: 400 + colorSystem: + primary: + - "#8b5cf6" + - "#3b82f6" + background: + - "#0f172a" + - "#1e293b" + text: + - "#ffffff" + - rgba(255,255,255,0.6) + functional: + - "#22c55e" + - "#ef4444" +todos: + - id: create-composer + content: 创建 videoComposer.js 封装 ffmpeg.wasm 合成逻辑 + status: completed + - id: add-video-config + content: 在 config/index.js 添加视频相关配置 + status: completed + - id: implement-compose-ui + content: 在 QuestionExplanation.vue 添加合成按钮和进度显示 + status: completed + dependencies: + - create-composer + - add-video-config + - id: implement-preview-download + content: 实现视频预览和下载功能 + status: completed + dependencies: + - implement-compose-ui + - id: integrate-workflow + content: 整合视频合成到现有生成流程 + status: completed + dependencies: + - implement-preview-download +--- + +## 产品概述 + +在现有试题讲解页面基础上,新增视频合成功能,将已生成的图片和音频合成为完整的教学讲解视频。 + +## 核心功能 + +- 将多个讲解点的图片和音频按顺序合成为单一视频文件 +- 视频生成过程中显示详细进度(资源下载、片段合成、视频编码等步骤) +- 支持在线预览生成的视频 +- 支持下载视频到本地 + +## 技术栈 + +- 视频合成:ffmpeg.wasm(已安装 @ffmpeg/ffmpeg@0.12.15) +- 现有框架:Vue 3 + Vite +- HTTP 请求:axios(已有) + +## 技术架构 + +### 实现方案 + +使用 ffmpeg.wasm 在浏览器端完成视频合成,无需后端服务: + +```mermaid +flowchart LR + A[slides数据] --> B[下载图片/音频] + B --> C[写入FFmpeg虚拟文件系统] + C --> D[生成视频片段] + D --> E[合并所有片段] + E --> F[输出MP4视频] + F --> G[预览/下载] +``` + +### 核心流程 + +1. **资源准备阶段**:遍历 slides 数组,fetch 所有图片和音频到内存 +2. **文件系统写入**:将资源写入 ffmpeg.wasm 的虚拟文件系统 +3. **片段生成**:每个 slide 生成一个视频片段(图片+音频) +4. **视频合并**:使用 concat demuxer 将所有片段合并为完整视频 +5. **输出展示**:生成 Blob URL 用于预览和下载 + +### 性能考量 + +- 音频时长检测:需要预先获取每个音频时长,用于视频片段时长控制 +- 内存管理:大文件处理时注意释放内存 +- 进度反馈:通过 ffmpeg 日志解析当前进度 + +## 目录结构 + +``` +src/ +├── views/ +│ └── QuestionExplanation.vue # [MODIFY] 添加视频合成功能 +├── utils/ +│ └── videoComposer.js # [NEW] ffmpeg.wasm 封装模块 +└── config/ + └── index.js # [MODIFY] 添加视频相关配置 +``` + +## 实现要点 + +### videoComposer.js 核心接口 + +- `initFFmpeg()`: 初始化并加载 ffmpeg.wasm +- `composeVideo(slides, onProgress)`: 主合成函数,返回视频 Blob +- `downloadVideo(blob, filename)`: 触发下载 +- `getAudioDuration(audioUrl)`: 获取音频时长 + +### 状态管理 + +新增状态: + +- `videoStatus`: idle | preparing | composing | ready | error +- `videoProgress`: { stage: string, current: number, total: number, message: string } +- `videoBlobUrl`: 合成完成的视频 URL + +## 设计说明 + +在现有播放器界面基础上,新增视频合成功能区,位于播放控制区域下方。采用深色主题风格与现有界面保持一致。 + +## 新增区块 + +1. **视频合成按钮区**:在播放控制区底部添加"生成视频"按钮 +2. **进度显示区**:合成过程中显示分步骤进度条和当前状态描述 +3. **视频预览区**:合成完成后显示视频播放器和下载按钮 \ No newline at end of file diff --git a/index.html b/index.html index 9af5473..f5b7660 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,8 @@ AI 英语学习辅助平台 - + +
diff --git a/package.json b/package.json index aa8c0e3..4ad0153 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,6 @@ "preview": "vite preview" }, "dependencies": { - "@ffmpeg/ffmpeg": "^0.12.15", - "@ffmpeg/util": "^0.12.2", "axios": "^1.13.6", "marked": "^17.0.5", "pako": "^2.1.0", diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..ed3ca38 --- /dev/null +++ b/server/README.md @@ -0,0 +1,73 @@ +# AI Demo - 后端服务 + +视频合成后端服务,使用 FFmpeg 处理视频合成任务。 + +## 前置要求 + +- Node.js 18+ +- **FFmpeg** - 需要安装到系统环境变量中 + +### 安装 FFmpeg + +#### Windows +1. 下载 FFmpeg: https://www.gyan.dev/ffmpeg/builds/ +2. 解压到 `C:\ffmpeg` +3. 添加 `C:\ffmpeg\bin` 到系统环境变量 PATH + +#### macOS +```bash +brew install ffmpeg +``` + +#### Linux +```bash +sudo apt update && sudo apt install ffmpeg +``` + +## 安装依赖 + +```bash +cd server +npm install +``` + +## 启动服务 + +```bash +npm run dev # 开发模式(自动重启) +npm start # 生产模式 +``` + +服务将在 http://localhost:3001 启动。 + +## API 接口 + +### POST /api/video/compose + +合成视频 + +**请求体:** +```json +{ + "slides": [ + { + "imageUrl": "https://example.com/image1.png", + "audioUrl": "https://example.com/audio1.mp3" + } + ], + "width": 1920, + "height": 1080 +} +``` + +**响应:** `video/mp4` 格式的视频文件 + +### GET /api/video/health + +检查视频服务状态 + +## 前端配置 + +前端 Vite 开发服务器会自动代理 `/api` 请求到后端服务。 + +确保后端服务运行在 `http://localhost:3001`。 diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..dbca74c --- /dev/null +++ b/server/index.js @@ -0,0 +1,33 @@ +import express from 'express'; +import cors from 'cors'; +import videoRoutes from './routes/video.js'; + +const app = express(); +const PORT = process.env.PORT || 3001; + +// 中间件 +app.use(cors()); +app.use(express.json({ limit: '50mb' })); +app.use(express.urlencoded({ extended: true, limit: '50mb' })); + +// 路由 +app.use('/api/video', videoRoutes); + +// 健康检查 +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// 错误处理 +app.use((err, req, res, next) => { + console.error('Server error:', err); + res.status(500).json({ + error: 'Internal server error', + message: err.message + }); +}); + +app.listen(PORT, () => { + console.log(`🚀 Server running on http://localhost:${PORT}`); + console.log(`📁 Video API: http://localhost:${PORT}/api/video`); +}); diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..9a98b4b --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,1020 @@ +{ + "name": "ai-demo-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-demo-server", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.0", + "cors": "^2.8.5", + "express": "^4.21.0", + "fluent-ffmpeg": "^2.1.3", + "uuid": "^10.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmmirror.com/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", + "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "async": "^0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..f1b653a --- /dev/null +++ b/server/package.json @@ -0,0 +1,16 @@ +{ + "name": "ai-demo-server", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "express": "^4.21.0", + "cors": "^2.8.5", + "fluent-ffmpeg": "^2.1.3", + "axios": "^1.7.0", + "uuid": "^10.0.0" + } +} diff --git a/server/routes/video.js b/server/routes/video.js new file mode 100644 index 0000000..f0289f5 --- /dev/null +++ b/server/routes/video.js @@ -0,0 +1,284 @@ +import express from 'express'; +import ffmpeg from 'fluent-ffmpeg'; +import axios from 'axios'; +import { v4 as uuidv4 } from 'uuid'; +import { promises as fs } from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const router = express.Router(); + +// 临时文件目录 +const TEMP_DIR = path.join(__dirname, '../temp'); + +// 确保临时目录存在 +async function ensureTempDir() { + try { + await fs.mkdir(TEMP_DIR, { recursive: true }); + } catch (err) { + console.error('创建临时目录失败:', err); + } +} + +ensureTempDir(); + +/** + * 下载资源到本地 + */ +async function downloadResource(url, outputPath) { + // 处理 base64 图片 (data:image/svg+xml,...) + if (url.startsWith('data:')) { + const matches = url.match(/^data:image\/(\w+);base64,(.+)$/); + if (matches) { + const buffer = Buffer.from(matches[2], 'base64'); + await fs.writeFile(outputPath, buffer); + return outputPath; + } + // 处理 URL 编码的 SVG + if (url.startsWith('data:image/svg+xml,')) { + const svgContent = decodeURIComponent(url.replace('data:image/svg+xml,', '')); + await fs.writeFile(outputPath, svgContent); + return outputPath; + } + throw new Error('不支持的 base64 格式'); + } + + // 处理普通 HTTP URL + const response = await axios({ + method: 'GET', + url: url, + responseType: 'arraybuffer', + timeout: 60000, + }); + + await fs.writeFile(outputPath, response.data); + return outputPath; +} + +/** + * 获取音频时长 + */ +function getAudioDuration(audioPath) { + return new Promise((resolve, reject) => { + ffmpeg.ffprobe(audioPath, (err, metadata) => { + if (err) { + reject(err); + return; + } + const duration = metadata.format.duration; + resolve(duration); + }); + }); +} + +/** + * 合成单个视频片段 + */ +async function composeSegment(imagePath, audioPath, outputPath, width, height) { + const audioDuration = await getAudioDuration(audioPath); + + return new Promise((resolve, reject) => { + ffmpeg() + .input(imagePath) + .loop(1) + .input(audioPath) + .outputOptions([ + '-tune stillimage', + '-c:a aac', + '-b:a 192k', + '-pix_fmt yuv420p', + '-shortest' + ]) + .videoFilter(`scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`) + .duration(audioDuration + 0.5) + .output(outputPath) + .on('start', (commandLine) => { + console.log('FFmpeg 命令:', commandLine); + }) + .on('end', () => resolve(outputPath)) + .on('error', (err, stdout, stderr) => { + console.error('FFmpeg 错误输出:', stderr); + reject(err); + }) + .run(); + }); +} + +/** + * 合并多个视频片段 + */ +async function mergeSegments(segmentPaths, outputPath) { + const listPath = path.join(TEMP_DIR, `concat_${uuidv4()}.txt`); + // Windows 路径需要转义反斜杠或使用正斜杠 + const listContent = segmentPaths.map(p => { + // 将反斜杠替换为正斜杠,FFmpeg 更兼容 + const normalizedPath = p.replace(/\\/g, '/'); + return `file '${normalizedPath}'`; + }).join('\n'); + + await fs.writeFile(listPath, listContent); + console.log('合并列表内容:\n', listContent); + + return new Promise((resolve, reject) => { + ffmpeg() + .input(listPath) + .inputOptions(['-f concat', '-safe 0']) + .outputOptions(['-c copy']) + .output(outputPath) + .on('start', (commandLine) => { + console.log('FFmpeg 合并命令:', commandLine); + }) + .on('end', async () => { + // 清理列表文件 + try { + await fs.unlink(listPath); + } catch (e) { + console.error('清理列表文件失败:', e); + } + resolve(outputPath); + }) + .on('error', (err, stdout, stderr) => { + console.error('FFmpeg 合并错误:', stderr); + reject(err); + }) + .run(); + }); +} + +/** + * POST /api/video/compose + * 合成视频 + * + * Body: { + * slides: [{ imageUrl, audioUrl }], + * width: 1920, + * height: 1080 + * } + */ +router.post('/compose', async (req, res) => { + const { slides, width = 1920, height = 1080 } = req.body; + + if (!slides || !Array.isArray(slides) || slides.length === 0) { + return res.status(400).json({ error: '缺少 slides 参数或格式错误' }); + } + + const taskId = uuidv4(); + const taskDir = path.join(TEMP_DIR, taskId); + + console.log(`[${taskId}] 开始处理视频合成任务,共 ${slides.length} 个片段`); + + try { + // 创建任务目录 + await fs.mkdir(taskDir, { recursive: true }); + + const segmentPaths = []; + + // 逐个处理片段 + for (let i = 0; i < slides.length; i++) { + const slide = slides[i]; + console.log(`[${taskId}] 处理第 ${i + 1}/${slides.length} 个片段`); + + if (!slide.imageUrl || !slide.audioUrl) { + console.warn(`[${taskId}] 第 ${i + 1} 个片段缺少 imageUrl 或 audioUrl,跳过`); + continue; + } + + // 下载图片和音频 + const imagePath = path.join(taskDir, `image_${i}.png`); + const audioPath = path.join(taskDir, `audio_${i}.mp3`); + const segmentPath = path.join(taskDir, `segment_${i}.mp4`); + + console.log(`[${taskId}] 下载图片: ${slide.imageUrl.substring(0, 50)}...`); + console.log(`[${taskId}] 下载音频: ${slide.audioUrl.substring(0, 50)}...`); + + await downloadResource(slide.imageUrl, imagePath); + await downloadResource(slide.audioUrl, audioPath); + + // 检查文件是否成功创建 + try { + const imageStats = await fs.stat(imagePath); + const audioStats = await fs.stat(audioPath); + console.log(`[${taskId}] 图片大小: ${imageStats.size} bytes, 音频大小: ${audioStats.size} bytes`); + } catch (e) { + console.error(`[${taskId}] 文件检查失败:`, e); + } + + // 合成片段 + await composeSegment(imagePath, audioPath, segmentPath, width, height); + segmentPaths.push(segmentPath); + + console.log(`[${taskId}] 第 ${i + 1} 个片段合成完成`); + } + + if (segmentPaths.length === 0) { + throw new Error('没有有效的片段可以合并'); + } + + // 合并所有片段 + console.log(`[${taskId}] 开始合并 ${segmentPaths.length} 个片段`); + const finalPath = path.join(taskDir, 'output.mp4'); + await mergeSegments(segmentPaths, finalPath); + + console.log(`[${taskId}] 视频合成完成: ${finalPath}`); + + // 返回视频文件 + res.setHeader('Content-Type', 'video/mp4'); + res.setHeader('Content-Disposition', 'attachment; filename="question-explanation.mp4"'); + + const videoBuffer = await fs.readFile(finalPath); + res.send(videoBuffer); + + // 清理临时文件(延迟删除,确保响应已发送) + setTimeout(async () => { + try { + await fs.rm(taskDir, { recursive: true, force: true }); + console.log(`[${taskId}] 临时文件已清理`); + } catch (e) { + console.error(`[${taskId}] 清理临时文件失败:`, e); + } + }, 1000); + + } catch (error) { + console.error(`[${taskId}] 视频合成失败:`, error); + + // 清理临时文件 + try { + await fs.rm(taskDir, { recursive: true, force: true }); + } catch (e) { + // 忽略清理错误 + } + + res.status(500).json({ + error: '视频合成失败', + message: error.message + }); + } +}); + +/** + * GET /api/video/health + * 检查视频服务状态 + */ +router.get('/health', (req, res) => { + // 检查 ffmpeg 是否可用 + ffmpeg.getAvailableFormats((err, formats) => { + if (err) { + return res.status(500).json({ + status: 'error', + message: 'FFmpeg 不可用', + error: err.message + }); + } + + res.json({ + status: 'ok', + message: '视频合成服务正常运行', + formats: formats ? Object.keys(formats).length : 'unknown' + }); + }); +}); + +export default router; diff --git a/src/config/index.js b/src/config/index.js index d959bfa..9882338 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -226,3 +226,76 @@ export const AUDIO_TEXT_OPTIMIZE_PROMPT = `你是一位专业的文字编辑和 - 只输出优化后的文本,不要添加"优化后文本:"等前缀 - 保持原文语言(中文/英文/日语) - 如果原文已经很好,只需做必要的格式调整`; + +// ── 试题讲解生成配置 ── +// 文本分析模型(用于分析试题复杂度、拆分讲解点、生成讲解文本和图片提示词) +export const QUESTION_EXPLANATION_ANALYSIS_URL = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"; +export const QUESTION_EXPLANATION_ANALYSIS_MODEL = "doubao-seed-2-0-lite-260215"; +export const QUESTION_EXPLANATION_ANALYSIS_TEMPERATURE = 0.7; +export const QUESTION_EXPLANATION_ANALYSIS_MAX_TOKENS = 4096; + +// 试题分析 System Prompt +export const QUESTION_EXPLANATION_ANALYSIS_PROMPT = `你是一位经验丰富的教学专家,擅长将复杂的试题拆解为清晰的讲解步骤。 + +【任务目标】 +分析试题复杂度,将其拆分为2-8个讲解点,并为每个讲解点生成详细的讲解文本和对应的图片生成提示词。 + +【分析流程】 +1. 评估试题复杂度: + - 简单题(单一知识点):生成2-3个讲解点 + - 中等题(多个知识点):生成4-5个讲解点 + - 复杂题(综合应用):生成6-8个讲解点 + +2. 拆分讲解点:按照解题逻辑顺序拆分,确保每个讲解点聚焦一个核心概念或步骤 + +3. 为每个讲解点生成: + - 标题:简洁明了,概括该步骤的核心内容 + - 讲解文本:详细的解释说明,语言通俗易懂,适合语音朗读(控制在80-150字之间) + - 图片提示词:用于生成配套讲解图片,要求视觉化呈现该步骤的核心内容 + +【输出格式 - 严格按JSON格式输出】 +必须只输出纯JSON,不要包含任何其他文字、说明或markdown标记。格式如下: +{"complexity":"简单|中等|复杂","totalPoints":数字,"explanationPoints":[{"order":1,"title":"讲解点标题","explanationText":"详细的讲解文本内容...","imagePrompt":"清晰、具体的图片生成提示词,包含视觉元素描述..."}]} + +【重要规则】 +1. 【最高优先级】只输出JSON,不要有任何其他文字 +2. 必须严格按JSON格式输出,所有字符串值必须用双引号包裹 +3. 字符串内的双引号必须转义为 \\" +4. 字符串内的换行符必须转义为 \\n +5. 讲解文本要口语化,适合语音朗读,避免过于书面化 +6. 图片提示词要具体、形象,便于AI生成直观的教学图示 +7. 讲解点之间要有逻辑连贯性,形成完整的解题思路 +8. 确保JSON格式正确,所有引号、逗号、括号匹配 + +【示例输出】 +{"complexity":"简单","totalPoints":2,"explanationPoints":[{"order":1,"title":"理解题目","explanationText":"首先我们来看这道题目,题目要求我们...","imagePrompt":"一个清晰的题目展示卡片..."},{"order":2,"title":"解题思路","explanationText":"接下来我们分析解题思路...","imagePrompt":"解题步骤流程图..."}]}`; + +// 图片生成模型配置(阿里云百炼) +export const QUESTION_EXPLANATION_IMAGE_URL = "/dashscope-api/api/v1/services/aigc/multimodal-generation/generation"; +export const QUESTION_EXPLANATION_IMAGE_MODEL = "qwen-image-2.0-pro"; // 千问文生图模型,擅长文字渲染 +export const QUESTION_EXPLANATION_IMAGE_SIZE = "1920*1080"; // 16:9 比例 + +// 图片统一风格描述(确保所有生成的图片风格一致) +export const QUESTION_EXPLANATION_IMAGE_STYLE = `【统一视觉风格要求】 +1. 背景:使用深蓝色渐变背景(从深蓝色#1e293b到更深的蓝黑色#0f172a),营造专业学术氛围 +2. 配色方案:主色调为深蓝色系,辅助色为紫色和蓝色(色值范围:紫色#8b5cf6到蓝色#3b82f6),强调色使用白色和淡蓝色 +3. 字体样式:所有文字使用清晰易读的无衬线字体,标题文字使用白色或淡蓝色,重点内容可用亮色标注 +4. 图形元素:使用简洁的几何图形、流程图、示意图等,线条清晰,颜色协调 +5. 整体风格:现代简约、专业严谨的教学图示风格,避免过于花哨或复杂的装饰 +6. 布局:采用居中对称或左右分栏布局,保持视觉平衡 +7. 光影效果:适当使用柔和的光晕效果或渐变叠加,增强层次感 +8. 所有图片必须严格遵循以上风格要求,确保整个讲解系列的视觉连贯性和专业性`; + +// 语音合成模型配置(阿里云百炼) +export const QUESTION_EXPLANATION_TTS_URL = "/dashscope-api/api/v1/services/aigc/multimodal-generation/generation"; +export const QUESTION_EXPLANATION_TTS_MODEL = "qwen3-tts-flash"; +export const QUESTION_EXPLANATION_TTS_VOICE = "Cherry"; // 芊悦:阳光积极的小姐姐音色 +export const QUESTION_EXPLANATION_TTS_LANGUAGE = "zh-CN"; +export const QUESTION_EXPLANATION_TTS_FORMAT = "mp3"; +export const QUESTION_EXPLANATION_TTS_SAMPLE_RATE = 24000; + +// ── 视频合成配置 ── +export const VIDEO_WIDTH = 1920; +export const VIDEO_HEIGHT = 1080; +export const VIDEO_OUTPUT_FORMAT = "mp4"; +export const VIDEO_FILENAME_PREFIX = "试题讲解_"; diff --git a/src/md/ali-audio-tts.md b/src/md/ali-audio-tts.md new file mode 100644 index 0000000..5692cf0 --- /dev/null +++ b/src/md/ali-audio-tts.md @@ -0,0 +1,1189 @@ +语音合成-千问提供多种拟人音色,支持多语言及方言,并可在同一音色下输出多语言内容。系统可自适应语气,流畅处理复杂文本。 + +## **核心功能** + +- 支持流式输出,可以边合成边播放 + +- 覆盖多种语言,包含中文方言 + +- 提供丰富音色,满足场景需求 + +- 提供[声音复刻](https://help.aliyun.com/zh/model-studio/qwen-tts-voice-cloning)与[声音设计](https://help.aliyun.com/zh/model-studio/qwen-tts-voice-design)两种音色定制方式 + +- 支持[指令控制](#12884a10929p9),可通过自然语言指令控制语音表现力 + + +## **适用范围** + +**支持的模型:** + +## 中国内地 + +在[中国内地部署模式](https://help.aliyun.com/zh/model-studio/regions/#080da663a75xh)下,接入点与数据存储均位于**北京地域**,模型推理计算资源仅限于中国内地。 + +调用以下模型时,请选择北京地域的[API Key](https://bailian.console.aliyun.com/?tab=model#/api-key): + +- **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash(稳定版,当前等同qwen3-tts-instruct-flash-2026-01-26)、qwen3-tts-instruct-flash-2026-01-26(最新快照版) + +- **千问3-TTS-VD****:**qwen3-tts-vd-2026-01-26(最新快照版) + +- **千问3-TTS-VC****:**qwen3-tts-vc-2026-01-22(最新快照版) + +- **千问3-TTS-Flash**:qwen3-tts-flash(稳定版,当前等同qwen3-tts-flash-2025-11-27)、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 + +- **千问-TTS**:qwen-tts(稳定版,当前等同qwen-tts-2025-04-10)、qwen-tts-latest(最新版,当前等同qwen-tts-2025-05-22)、qwen-tts-2025-05-22(快照版)、qwen-tts-2025-04-10(快照版) + + +## 国际 + +在[国际部署模式](https://help.aliyun.com/zh/model-studio/regions/#080da663a75xh)下,接入点与数据存储均位于**新加坡地域**,模型推理计算资源在全球范围内动态调度(不含中国内地)。 + +调用以下模型时,请选择新加坡地域的[API Key](https://modelstudio.console.aliyun.com/?tab=dashboard#/api-key): + +- **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash(稳定版,当前等同qwen3-tts-instruct-flash-2026-01-26)、qwen3-tts-instruct-flash-2026-01-26(最新快照版) + +- **千问3-TTS-VD****:**qwen3-tts-vd-2026-01-26(最新快照版) + +- **千问3-TTS-VC****:**qwen3-tts-vc-2026-01-22(最新快照版) + +- **千问3-TTS-Flash**:qwen3-tts-flash(稳定版,当前等同qwen3-tts-flash-2025-11-27)、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 + + +更多信息请参见[模型列表](https://help.aliyun.com/zh/model-studio/models) + +## **模型选型** + +| **场景** | **推荐模型** | **推荐理由** | +| --- | --- | --- | +| **品牌形象、专属声音、扩展系统音色等语音定制(基于文本描述)** | qwen3-tts-vd-2026-01-26 | 支持声音设计,无需音频样本,通过文本描述创建定制化音色,适合从零开始设计品牌专属声音 | +| **品牌形象、专属声音、扩展系统音色等语音定制(基于音频样本)** | qwen3-tts-vc-2026-01-22 | 支持声音复刻,基于真实音频样本快速复刻音色,打造拟人化品牌声纹,确保音色高度还原与一致性 | +| **情感化内容生产(有声书、广播剧、游戏/动画配音)** | qwen3-tts-instruct-flash | 支持指令控制,通过自然语言描述精确控制音调、语速、情感、角色性格,适合需要丰富表现力和角色塑造的场景 | +| **移动端导航/通知播报** | qwen3-tts-flash | 按字符计费简单透明,适合短文本高频调用场景 | +| **在线教育课件配音** | qwen3-tts-flash | 支持多语种与方言,满足地域化教学需求 | +| **有声读物批量生产** | qwen3-tts-flash | 成本可控,多音色选择丰富内容表现力 | + +更多说明请参见[模型功能特性对比](#6e3883d028fqq) + +## **快速开始** + +**准备工作** + +- 已[配置 API Key](https://help.aliyun.com/zh/model-studio/get-api-key)并[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。 + +- 如果通过 DashScope SDK 进行调用,需要[安装最新版SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。DashScope Java SDK 版本需要不低于 2.21.9,DashScope Python SDK 版本需要不低于 1.24.6。 + + **说明** + + DashScope Python SDK中的`SpeechSynthesizer`接口已统一为`MultiModalConversation`,使用新接口只需替换接口名称即可,其他参数完全兼容。 + + +## 使用系统音色进行语音合成 + +以下示例演示如何使用[系统音色](#bac280ddf5a1u)进行语音合成。 + +## 非流式输出 + +通过返回的`url`来获取合成的语音。URL 有效期为24 小时。 + +## Python + +``` +import os +import dashscope + +# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 +dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1' + +text = "那我来给大家推荐一款T恤,这款呢真的是超级好看,这个颜色呢很显气质,而且呢也是搭配的绝佳单品,大家可以闭眼入,真的是非常好看,对身材的包容性也很好,不管啥身材的宝宝呢,穿上去都是很好看的。推荐宝宝们下单哦。" +# SpeechSynthesizer接口使用方法:dashscope.audio.qwen_tts.SpeechSynthesizer.call(...) +response = dashscope.MultiModalConversation.call( + # 如需使用指令控制功能,请将model替换为qwen3-tts-instruct-flash + model="qwen3-tts-flash", + # 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + # 若没有配置环境变量,请用百炼API Key将下行替换为:api_key = "sk-xxx" + api_key=os.getenv("DASHSCOPE_API_KEY"), + text=text, + voice="Cherry", + language_type="Chinese", # 建议与文本语种一致,以获得正确的发音和自然的语调。 + # 如需使用指令控制功能,请取消下方注释,并将model替换为qwen3-tts-instruct-flash + # instructions='语速较快,带有明显的上扬语调,适合介绍时尚产品。', + # optimize_instructions=True, + stream=False +) +print(response) +``` + +## Java + +需要导入Gson依赖,若是使用Maven或者Gradle,添加依赖方式如下: + +## Maven + +在`pom.xml`中添加如下内容: + +``` + + + com.google.code.gson + gson + 2.13.1 + +``` + +## Gradle + +在`build.gradle`中添加如下内容: + +``` +// https://mvnrepository.com/artifact/com.google.code.gson/gson +implementation("com.google.code.gson:gson:2.13.1") +``` + +``` +import com.alibaba.dashscope.aigc.multimodalconversation.AudioParameters; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.protocol.Protocol; +import com.alibaba.dashscope.utils.Constants; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.URL; + +public class Main { + // 如需使用指令控制功能,请将MODEL替换为qwen3-tts-instruct-flash + private static final String MODEL = "qwen3-tts-flash"; + public static void call() throws ApiException, NoApiKeyException, UploadFileException { + MultiModalConversation conv = new MultiModalConversation(); + MultiModalConversationParam param = MultiModalConversationParam.builder() + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx") + .apiKey(System.getenv("DASHSCOPE_API_KEY")) + .model(MODEL) + .text("Today is a wonderful day to build something people love!") + .voice(AudioParameters.Voice.CHERRY) + .languageType("English") // 建议与文本语种一致,以获得正确的发音和自然的语调。 + // 如需使用指令控制功能,请取消下方注释,并将model替换为qwen3-tts-instruct-flash + // .parameter("instructions","语速较快,带有明显的上扬语调,适合介绍时尚产品。") + // .parameter("optimize_instructions",true) + .build(); + MultiModalConversationResult result = conv.call(param); + String audioUrl = result.getOutput().getAudio().getUrl(); + System.out.print(audioUrl); + + // 下载音频文件到本地 + try (InputStream in = new URL(audioUrl).openStream(); + FileOutputStream out = new FileOutputStream("downloaded_audio.wav")) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + System.out.println("\n音频文件已下载到本地: downloaded_audio.wav"); + } catch (Exception e) { + System.out.println("\n下载音频文件时出错: " + e.getMessage()); + } + } + public static void main(String[] args) { + try { + // 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 + Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1"; + call(); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + System.out.println(e.getMessage()); + } + System.exit(0); + } +} +``` + +## cURL + +``` +# ======= 重要提示 ======= +# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation +# 新加坡地域和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +# === 执行时请删除该注释 === + +curl -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \ +-H "Authorization: Bearer $DASHSCOPE_API_KEY" \ +-H 'Content-Type: application/json' \ +-d '{ + "model": "qwen3-tts-flash", + "input": { + "text": "那我来给大家推荐一款T恤,这款呢真的是超级好看,这个颜色呢很显气质,而且呢也是搭配的绝佳单品,大家可以闭眼入,真的是非常好看,对身材的包容性也很好,不管啥身材的宝宝呢,穿上去都是很好看的。推荐宝宝们下单哦。", + "voice": "Cherry", + "language_type": "Chinese" + } +}' +``` + +## 流式输出 + +可以流式地将音频数据以 Base64 格式进行输出,此时最后一个数据包中包含完整音频的 URL。 + +## Python + +``` +# coding=utf-8 +# +# Installation instructions for pyaudio: +# APPLE Mac OS X +# brew install portaudio +# pip install pyaudio +# Debian/Ubuntu +# sudo apt-get install python-pyaudio python3-pyaudio +# or +# pip install pyaudio +# CentOS +# sudo yum install -y portaudio portaudio-devel && pip install pyaudio +# Microsoft Windows +# python -m pip install pyaudio + +import os +import dashscope +import pyaudio +import time +import base64 +import numpy as np + +# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 +dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1' + +p = pyaudio.PyAudio() +# 创建音频流 +stream = p.open(format=pyaudio.paInt16, + channels=1, + rate=24000, + output=True) + + +text = "你好啊,我是千问" +response = dashscope.MultiModalConversation.call( + # 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + # 若没有配置环境变量,请用百炼API Key将下行替换为:api_key = "sk-xxx" + api_key=os.getenv("DASHSCOPE_API_KEY"), + # 如需使用指令控制功能,请将model替换为qwen3-tts-instruct-flash + model="qwen3-tts-flash", + text=text, + voice="Cherry", + language_type="Chinese", # 建议与文本语种一致,以获得正确的发音和自然的语调。 + # 如需使用指令控制功能,请取消下方注释,并将model替换为qwen3-tts-instruct-flash + # instructions='语速较快,带有明显的上扬语调,适合介绍时尚产品。', + # optimize_instructions=True, + stream=True +) + +for chunk in response: + if chunk.output is not None: + audio = chunk.output.audio + if audio.data is not None: + wav_bytes = base64.b64decode(audio.data) + audio_np = np.frombuffer(wav_bytes, dtype=np.int16) + # 直接播放音频数据 + stream.write(audio_np.tobytes()) + if chunk.output.finish_reason == "stop": + print("finish at: {} ", chunk.output.audio.expires_at) +time.sleep(0.8) +# 清理资源 +stream.stop_stream() +stream.close() +p.terminate() +``` + +## Java + +需要导入Gson依赖,若是使用Maven或者Gradle,添加依赖方式如下: + +### Maven + +在`pom.xml`中添加如下内容: + +``` + + + com.google.code.gson + gson + 2.13.1 + +``` + +### Gradle + +在`build.gradle`中添加如下内容: + +``` +// https://mvnrepository.com/artifact/com.google.code.gson/gson +implementation("com.google.code.gson:gson:2.13.1") +``` + +``` +import com.alibaba.dashscope.aigc.multimodalconversation.AudioParameters; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.protocol.Protocol; +import com.alibaba.dashscope.utils.Constants; +import io.reactivex.Flowable; +import javax.sound.sampled.*; +import java.util.Base64; + +public class Main { + // 如需使用指令控制功能,请将MODEL替换为qwen3-tts-instruct-flash + private static final String MODEL = "qwen3-tts-flash"; + public static void streamCall() throws ApiException, NoApiKeyException, UploadFileException { + MultiModalConversation conv = new MultiModalConversation(); + MultiModalConversationParam param = MultiModalConversationParam.builder() + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx") + .apiKey(System.getenv("DASHSCOPE_API_KEY")) + .model(MODEL) + .text("Today is a wonderful day to build something people love!") + .voice(AudioParameters.Voice.CHERRY) + .languageType("English") // 建议与文本语种一致,以获得正确的发音和自然的语调。 + // 如需使用指令控制功能,请取消下方注释,并将model替换为qwen3-tts-instruct-flash + // .parameter("instructions","语速较快,带有明显的上扬语调,适合介绍时尚产品。") + // .parameter("optimize_instructions",true) + .build(); + Flowable result = conv.streamCall(param); + result.blockingForEach(r -> { + try { + // 1. 获取Base64编码的音频数据 + String base64Data = r.getOutput().getAudio().getData(); + byte[] audioBytes = Base64.getDecoder().decode(base64Data); + + // 2. 配置音频格式(根据API返回的音频格式调整) + AudioFormat format = new AudioFormat( + AudioFormat.Encoding.PCM_SIGNED, + 24000, // 采样率(需与API返回格式一致) + 16, // 采样位数 + 1, // 声道数 + 2, // 帧大小(位数/字节数) + 24000, // 数据传输率(需与采样率一致) + false // 是否压缩 + ); + + // 3. 实时播放音频数据 + DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); + try (SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info)) { + if (line != null) { + line.open(format); + line.start(); + line.write(audioBytes, 0, audioBytes.length); + line.drain(); + } + } + } catch (LineUnavailableException e) { + e.printStackTrace(); + } + }); + } + public static void main(String[] args) { + // 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 + Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1"; + try { + streamCall(); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + System.out.println(e.getMessage()); + } + System.exit(0); + } +} +``` + +## cURL + +``` +# ======= 重要提示 ======= +# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation +# 新加坡地域和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +# === 执行时请删除该注释 === + +curl -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \ +-H "Authorization: Bearer $DASHSCOPE_API_KEY" \ +-H 'Content-Type: application/json' \ +-H 'X-DashScope-SSE: enable' \ +-d '{ + "model": "qwen3-tts-flash", + "input": { + "text": "那我来给大家推荐一款T恤,这款呢真的是超级好看,这个颜色呢很显气质,而且呢也是搭配的绝佳单品,大家可以闭眼入,真的是非常好看,对身材的包容性也很好,不管啥身材的宝宝呢,穿上去都是很好看的。推荐宝宝们下单哦。", + "voice": "Cherry", + "language_type": "Chinese" + } +}' +``` + +## 使用声音复刻音色进行语音合成 + +声音复刻服务不提供预览音频。需将复刻生成的音色应用于语音合成后,才能试听并评估效果。 + +以下示例演示了如何在语音合成中使用声音复刻生成的专属音色,实现与原音高度相似的输出效果。这里参考了使用系统音色进行语音合成DashScope SDK的“非流式输出”示例代码,将`voice`参数替换为复刻生成的专属音色进行语音合成。 + +- **关键原则**:声音复刻时使用的模型 (`target_model`) 必须与后续进行语音合成时使用的模型 (`model`) 保持一致,否则会导致合成失败。 + +- 示例使用本地音频文件 `voice.mp3` 进行声音复刻,运行代码时,请注意替换。 + + +### Python + +``` +import os +import requests +import base64 +import pathlib +import dashscope + +# ======= 常量配置 ======= +DEFAULT_TARGET_MODEL = "qwen3-tts-vc-2026-01-22" # 声音复刻、语音合成要使用相同的模型 +DEFAULT_PREFERRED_NAME = "guanyu" +DEFAULT_AUDIO_MIME_TYPE = "audio/mpeg" +VOICE_FILE_PATH = "voice.mp3" # 用于声音复刻的本地音频文件的相对路径 + + +def create_voice(file_path: str, + target_model: str = DEFAULT_TARGET_MODEL, + preferred_name: str = DEFAULT_PREFERRED_NAME, + audio_mime_type: str = DEFAULT_AUDIO_MIME_TYPE) -> str: + """ + 创建音色,并返回 voice 参数 + """ + # 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + # 若没有配置环境变量,请用百炼API Key将下行替换为:api_key = "sk-xxx" + api_key = os.getenv("DASHSCOPE_API_KEY") + + file_path_obj = pathlib.Path(file_path) + if not file_path_obj.exists(): + raise FileNotFoundError(f"音频文件不存在: {file_path}") + + base64_str = base64.b64encode(file_path_obj.read_bytes()).decode() + data_uri = f"data:{audio_mime_type};base64,{base64_str}" + + # 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization + url = "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization" + payload = { + "model": "qwen-voice-enrollment", # 不要修改该值 + "input": { + "action": "create", + "target_model": target_model, + "preferred_name": preferred_name, + "audio": {"data": data_uri} + } + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + resp = requests.post(url, json=payload, headers=headers) + if resp.status_code != 200: + raise RuntimeError(f"创建 voice 失败: {resp.status_code}, {resp.text}") + + try: + return resp.json()["output"]["voice"] + except (KeyError, ValueError) as e: + raise RuntimeError(f"解析 voice 响应失败: {e}") + + +if __name__ == '__main__': + # 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 + dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1' + + text = "今天天气怎么样?" + # SpeechSynthesizer接口使用方法:dashscope.audio.qwen_tts.SpeechSynthesizer.call(...) + response = dashscope.MultiModalConversation.call( + model=DEFAULT_TARGET_MODEL, + # 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + # 若没有配置环境变量,请用百炼API Key将下行替换为:api_key = "sk-xxx" + api_key=os.getenv("DASHSCOPE_API_KEY"), + text=text, + voice=create_voice(VOICE_FILE_PATH), # 将voice参数替换为复刻生成的专属音色 + stream=False + ) + print(response) +``` + +### Java + +需要导入Gson依赖,若是使用Maven或者Gradle,添加依赖方式如下: + +#### Maven + +在`pom.xml`中添加如下内容: + +``` + + + com.google.code.gson + gson + 2.13.1 + +``` + +#### Gradle + +在`build.gradle`中添加如下内容: + +``` +// https://mvnrepository.com/artifact/com.google.code.gson/gson +implementation("com.google.code.gson:gson:2.13.1") +``` + +**重要** + +使用声音复刻生成的专属音色进行语音合成时,必须按照如下方式设置音色: + +``` +MultiModalConversationParam param = MultiModalConversationParam.builder() + .parameter("voice", "your_voice") // 将voice参数替换为复刻生成的专属音色 + .build(); +``` + +``` +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; +import com.alibaba.dashscope.utils.Constants; +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.*; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class Main { + // ===== 常量定义 ===== + // 声音复刻、语音合成要使用相同的模型 + private static final String TARGET_MODEL = "qwen3-tts-vc-2026-01-22"; + private static final String PREFERRED_NAME = "guanyu"; + // 用于声音复刻的本地音频文件的相对路径 + private static final String AUDIO_FILE = "voice.mp3"; + private static final String AUDIO_MIME_TYPE = "audio/mpeg"; + + // 生成 data URI + public static String toDataUrl(String filePath) throws IOException { + byte[] bytes = Files.readAllBytes(Paths.get(filePath)); + String encoded = Base64.getEncoder().encodeToString(bytes); + return "data:" + AUDIO_MIME_TYPE + ";base64," + encoded; + } + + // 调用 API 创建 voice + public static String createVoice() throws Exception { + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:String apiKey = "sk-xxx" + String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + String jsonPayload = + "{" + + "\"model\": \"qwen-voice-enrollment\"," // 不要修改该值 + + "\"input\": {" + + "\"action\": \"create\"," + + "\"target_model\": \"" + TARGET_MODEL + "\"," + + "\"preferred_name\": \"" + PREFERRED_NAME + "\"," + + "\"audio\": {" + + "\"data\": \"" + toDataUrl(AUDIO_FILE) + "\"" + + "}" + + "}" + + "}"; + + // 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization + String url = "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization"; + HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Authorization", "Bearer " + apiKey); + con.setRequestProperty("Content-Type", "application/json"); + con.setDoOutput(true); + + try (OutputStream os = con.getOutputStream()) { + os.write(jsonPayload.getBytes(StandardCharsets.UTF_8)); + } + + int status = con.getResponseCode(); + System.out.println("HTTP 状态码: " + status); + + try (BufferedReader br = new BufferedReader( + new InputStreamReader(status >= 200 && status < 300 ? con.getInputStream() : con.getErrorStream(), + StandardCharsets.UTF_8))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + System.out.println("返回内容: " + response); + + if (status == 200) { + JsonObject jsonObj = new Gson().fromJson(response.toString(), JsonObject.class); + return jsonObj.getAsJsonObject("output").get("voice").getAsString(); + } + throw new IOException("创建语音失败: " + status + " - " + response); + } + } + + public static void call() throws Exception { + MultiModalConversation conv = new MultiModalConversation(); + MultiModalConversationParam param = MultiModalConversationParam.builder() + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx") + .apiKey(System.getenv("DASHSCOPE_API_KEY")) + .model(TARGET_MODEL) + .text("今天天气怎么样?") + .parameter("voice", createVoice()) // 将voice参数替换为复刻生成的专属音色 + .build(); + MultiModalConversationResult result = conv.call(param); + String audioUrl = result.getOutput().getAudio().getUrl(); + System.out.print(audioUrl); + + // 下载音频文件到本地 + try (InputStream in = new URL(audioUrl).openStream(); + FileOutputStream out = new FileOutputStream("downloaded_audio.wav")) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + System.out.println("\n音频文件已下载到本地: downloaded_audio.wav"); + } catch (Exception e) { + System.out.println("\n下载音频文件时出错: " + e.getMessage()); + } + } + public static void main(String[] args) { + try { + // 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 + Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1"; + call(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + System.exit(0); + } +} +``` + +## 使用声音设计音色进行语音合成 + +使用声音设计功能时,服务会返回预览音频数据。建议先试听该预览音频,确认效果符合预期后再用于语音合成,降低调用成本。 + +1. 生成专属音色并试听效果,若对效果满意,进行下一步;否则重新生成。 + + ### Python + + ``` + import requests + import base64 + import os + + def create_voice_and_play(): + # 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + # 若没有配置环境变量,请用百炼API Key将下行替换为:api_key = "sk-xxx" + api_key = os.getenv("DASHSCOPE_API_KEY") + + if not api_key: + print("错误: 未找到DASHSCOPE_API_KEY环境变量,请先设置API Key") + return None, None, None + + # 准备请求数据 + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + data = { + "model": "qwen-voice-design", + "input": { + "action": "create", + "target_model": "qwen3-tts-vd-2026-01-26", + "voice_prompt": "沉稳的中年男性播音员,音色低沉浑厚,富有磁性,语速平稳,吐字清晰,适合用于新闻播报或纪录片解说。", + "preview_text": "各位听众朋友,大家好,欢迎收听晚间新闻。", + "preferred_name": "announcer", + "language": "zh" + }, + "parameters": { + "sample_rate": 24000, + "response_format": "wav" + } + } + + # 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization + url = "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization" + + try: + # 发送请求 + response = requests.post( + url, + headers=headers, + json=data, + timeout=60 # 添加超时设置 + ) + + if response.status_code == 200: + result = response.json() + + # 获取音色名称 + voice_name = result["output"]["voice"] + print(f"音色名称: {voice_name}") + + # 获取预览音频数据 + base64_audio = result["output"]["preview_audio"]["data"] + + # 解码Base64音频数据 + audio_bytes = base64.b64decode(base64_audio) + + # 保存音频文件到本地 + filename = f"{voice_name}_preview.wav" + + # 将音频数据写入本地文件 + with open(filename, 'wb') as f: + f.write(audio_bytes) + + print(f"音频已保存到本地文件: {filename}") + print(f"文件路径: {os.path.abspath(filename)}") + + return voice_name, audio_bytes, filename + else: + print(f"请求失败,状态码: {response.status_code}") + print(f"响应内容: {response.text}") + return None, None, None + + except requests.exceptions.RequestException as e: + print(f"网络请求发生错误: {e}") + return None, None, None + except KeyError as e: + print(f"响应数据格式错误,缺少必要的字段: {e}") + print(f"响应内容: {response.text if 'response' in locals() else 'No response'}") + return None, None, None + except Exception as e: + print(f"发生未知错误: {e}") + return None, None, None + + if __name__ == "__main__": + print("开始创建语音...") + voice_name, audio_data, saved_filename = create_voice_and_play() + + if voice_name: + print(f"\n成功创建音色 '{voice_name}'") + print(f"音频文件已保存: '{saved_filename}'") + print(f"文件大小: {os.path.getsize(saved_filename)} 字节") + else: + print("\n音色创建失败") + ``` + + ### Java + + 需要导入Gson依赖,若是使用Maven或者Gradle,添加依赖方式如下: + + #### Maven + + 在`pom.xml`中添加如下内容: + + ``` + + + com.google.code.gson + gson + 2.13.1 + + ``` + + #### Gradle + + 在`build.gradle`中添加如下内容: + + ``` + // https://mvnrepository.com/artifact/com.google.code.gson/gson + implementation("com.google.code.gson:gson:2.13.1") + ``` + + **重要** + + 使用声音设计生成的专属音色进行语音合成时,必须按照如下方式设置音色: + + ``` + MultiModalConversationParam param = MultiModalConversationParam.builder() + .parameter("voice", "your_voice") // 将voice参数替换为声音设计生成的专属音色 + .build(); + ``` + + ``` + import com.google.gson.JsonObject; + import com.google.gson.JsonParser; + import java.io.*; + import java.net.HttpURLConnection; + import java.net.URL; + import java.util.Base64; + + public class Main { + public static void main(String[] args) { + Main example = new Main(); + example.createVoice(); + } + + public void createVoice() { + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:String apiKey = "sk-xxx" + String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + // 创建JSON请求体字符串 + String jsonBody = "{\n" + + " \"model\": \"qwen-voice-design\",\n" + + " \"input\": {\n" + + " \"action\": \"create\",\n" + + " \"target_model\": \"qwen3-tts-vd-2026-01-26\",\n" + + " \"voice_prompt\": \"沉稳的中年男性播音员,音色低沉浑厚,富有磁性,语速平稳,吐字清晰,适合用于新闻播报或纪录片解说。\",\n" + + " \"preview_text\": \"各位听众朋友,大家好,欢迎收听晚间新闻。\",\n" + + " \"preferred_name\": \"announcer\",\n" + + " \"language\": \"zh\"\n" + + " },\n" + + " \"parameters\": {\n" + + " \"sample_rate\": 24000,\n" + + " \"response_format\": \"wav\"\n" + + " }\n" + + "}"; + + HttpURLConnection connection = null; + try { + // 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization + URL url = new URL("https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization"); + connection = (HttpURLConnection) url.openConnection(); + + // 设置请求方法和头部 + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", "Bearer " + apiKey); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + connection.setDoInput(true); + + // 发送请求体 + try (OutputStream os = connection.getOutputStream()) { + byte[] input = jsonBody.getBytes("UTF-8"); + os.write(input, 0, input.length); + os.flush(); + } + + // 获取响应 + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + // 读取响应内容 + StringBuilder response = new StringBuilder(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(connection.getInputStream(), "UTF-8"))) { + String responseLine; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + } + + // 解析JSON响应 + JsonObject jsonResponse = JsonParser.parseString(response.toString()).getAsJsonObject(); + JsonObject outputObj = jsonResponse.getAsJsonObject("output"); + JsonObject previewAudioObj = outputObj.getAsJsonObject("preview_audio"); + + // 获取音色名称 + String voiceName = outputObj.get("voice").getAsString(); + System.out.println("音色名称: " + voiceName); + + // 获取Base64编码的音频数据 + String base64Audio = previewAudioObj.get("data").getAsString(); + + // 解码Base64音频数据 + byte[] audioBytes = Base64.getDecoder().decode(base64Audio); + + // 保存音频到本地文件 + String filename = voiceName + "_preview.wav"; + saveAudioToFile(audioBytes, filename); + + System.out.println("音频已保存到本地文件: " + filename); + + } else { + // 读取错误响应 + StringBuilder errorResponse = new StringBuilder(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(connection.getErrorStream(), "UTF-8"))) { + String responseLine; + while ((responseLine = br.readLine()) != null) { + errorResponse.append(responseLine.trim()); + } + } + + System.out.println("请求失败,状态码: " + responseCode); + System.out.println("错误响应: " + errorResponse.toString()); + } + + } catch (Exception e) { + System.err.println("请求发生错误: " + e.getMessage()); + e.printStackTrace(); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + private void saveAudioToFile(byte[] audioBytes, String filename) { + try { + File file = new File(filename); + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(audioBytes); + } + System.out.println("音频已保存到: " + file.getAbsolutePath()); + } catch (IOException e) { + System.err.println("保存音频文件时发生错误: " + e.getMessage()); + e.printStackTrace(); + } + } + } + ``` + +2. 使用上一步生成的专属音色进行语音合成(非流式合成)。 + + 这里参考了使用系统音色进行语音合成DashScope SDK的“非流式输出”示例代码,将`voice`参数替换为声音设计生成的专属音色进行语音合成。单向流式合成请参见[语音合成-千问](https://help.aliyun.com/zh/model-studio/qwen-tts#c204937c02gsb)。 + + **关键原则**:声音设计时使用的模型 (`target_model`) 必须与后续进行语音合成时使用的模型 (`model`) 保持一致,否则会导致合成失败。 + + ### Python + + ``` + import os + import dashscope + + + if __name__ == '__main__': + # 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 + dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1' + + text = "今天天气怎么样?" + # SpeechSynthesizer接口使用方法:dashscope.audio.qwen_tts.SpeechSynthesizer.call(...) + response = dashscope.MultiModalConversation.call( + model="qwen3-tts-vd-2026-01-26", + # 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + # 若没有配置环境变量,请用百炼API Key将下行替换为:api_key = "sk-xxx" + api_key=os.getenv("DASHSCOPE_API_KEY"), + text=text, + voice="myvoice", # 将voice参数替换为声音设计生成的专属音色 + stream=False + ) + print(response) + ``` + + ### Java + + ``` + import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; + import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; + import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; + import com.alibaba.dashscope.exception.ApiException; + import com.alibaba.dashscope.exception.NoApiKeyException; + import com.alibaba.dashscope.exception.UploadFileException; + + import com.alibaba.dashscope.utils.Constants; + import java.io.FileOutputStream; + import java.io.InputStream; + import java.net.URL; + + public class Main { + private static final String MODEL = "qwen3-tts-vd-2026-01-26"; + public static void call() throws ApiException, NoApiKeyException, UploadFileException { + MultiModalConversation conv = new MultiModalConversation(); + MultiModalConversationParam param = MultiModalConversationParam.builder() + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx") + .apiKey(System.getenv("DASHSCOPE_API_KEY")) + .model(MODEL) + .text("Today is a wonderful day to build something people love!") + .parameter("voice", "myvoice") // 将voice参数替换为声音设计生成的专属音色 + .build(); + MultiModalConversationResult result = conv.call(param); + String audioUrl = result.getOutput().getAudio().getUrl(); + System.out.print(audioUrl); + + // 下载音频文件到本地 + try (InputStream in = new URL(audioUrl).openStream(); + FileOutputStream out = new FileOutputStream("downloaded_audio.wav")) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + System.out.println("\n音频文件已下载到本地: downloaded_audio.wav"); + } catch (Exception e) { + System.out.println("\n下载音频文件时出错: " + e.getMessage()); + } + } + public static void main(String[] args) { + try { + // 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 + Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1"; + call(); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + System.out.println(e.getMessage()); + } + System.exit(0); + } + } + ``` + + +## **指令控制** + +指令控制是一项高级语音合成功能,通过自然语言描述的方式精确控制语音的表达效果。您可以使用简单的文字描述,让合成语音呈现出特定的音调、语速、情感、音色特点,无需调整复杂的音频参数。 + +**支持的模型**:仅支持千问3-TTS-Instruct-Flash系列模型。 + +**使用方式**:通过`instructions`参数指定指令内容,例如“语速较快,带有明显的上扬语调,适合介绍时尚产品”。 + +**支持语言**:描述文本仅支持中文和英文。 + +**长度限制**:长度不得超过 1600 Token。 + +**适用场景**: + +- 有声书和广播剧配音 + +- 广告和宣传片配音 + +- 游戏角色和动画配音 + +- 情感化的智能语音助手 + +- 纪录片和新闻播报 + + +**如何编写高质量的声音描述:** + +- 核心原则: + + 1. 具体而非模糊:使用能够描绘具体声音特质的词语,如“低沉”、“清脆”、“语速偏快”。避免使用“好听”、“普通”等主观且缺乏信息量的词汇。 + + 2. 多维而非单一:优秀的描述通常结合多个维度(如下文所述的音调、语速、情感等)。单一维度的描述(如仅“高音”)过于宽泛,难以生成特色鲜明的效果。 + + 3. 客观而非主观:专注于声音本身的物理和感知特征,而不是个人的喜好。例如,用“音调偏高,带有活力”代替“我最喜欢的声音”。 + + 4. 原创而非模仿:请描述声音的特质,而不是要求模仿特定人物(如名人、演员)。此类请求涉及版权风险且模型不支持直接模仿。 + + 5. 简洁而非冗余:确保每个词都有其意义。避免重复使用同义词或无意义的强调词(如“非常非常棒的声音”)。 + +- 描述维度参考:可以组合多个维度,创造更丰富的表达效果。 + + | **维度** | **描述示例** | + | --- | --- | + | 音调 | 高音、中音、低音、偏高、偏低 | + | 语速 | 快速、中速、缓慢、偏快、偏慢 | + | 情感 | 开朗、沉稳、温柔、严肃、活泼、冷静、治愈 | + | 特点 | 有磁性、清脆、沙哑、圆润、甜美、浑厚、有力 | + | 用途 | 新闻播报、广告配音、有声书、动画角色、语音助手、纪录片解说 | + +- 示例: + + - 标准播音风格:吐字清晰精准,字正腔圆 + + - 情绪递进效果:音量由正常对话迅速增强至高喊,性格直率,情绪易激动且外露 + + - 特殊情感状态:哭腔导致发音略微含糊,略显沙哑,带有明显哭腔的紧张感 + + - 广告配音风格:音调偏高,语速中等,充满活力和感染力,适合广告配音 + + - 温柔治愈风格:语速偏慢,音调温柔甜美,语气治愈温暖,像贴心朋友般关怀 + + +## **API 参考** + +[语音合成-千问API参考](https://help.aliyun.com/zh/model-studio/qwen-tts-api) + +[声音复刻-API参考](https://help.aliyun.com/zh/model-studio/qwen-tts-voice-cloning) + +[声音设计-API参考](https://help.aliyun.com/zh/model-studio/qwen-tts-voice-design) + +## **模型功能特性对比** + +| **功能/特性** | **千问3-TTS-Instruct-Flash** | **千问3-TTS-VD** | **千问3-TTS-VC** | **千问3-TTS-Flash** | **千问-TTS** | +| --- | --- | --- | --- | --- | --- | +| **支持语言** | 因[音色](#bac280ddf5a1u)而异:中文(普通话)、英文、西班牙语、俄语、意大利语、法语、韩语、日语、德语、葡萄牙语 | 中文(普通话)、英文、西班牙语、俄语、意大利语、法语、韩语、日语、德语、葡萄牙语 | | 因[音色](#bac280ddf5a1u)而异:中文(普通话、上海话、北京话、四川话、南京话、陕西话、闽南语、天津话)、粤语、英文、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | 因[音色](#bac280ddf5a1u)而异:中文(普通话、上海话、北京话、四川话)、英文 | +| **音频格式** | - wav:非流式输出时 - pcm:流式输出时,Base64编码 | | | | | +| **音频采样率** | 24kHz | | | | | +| **声音复刻** | 不支持 | | 支持 | 不支持 | | +| **声音设计** | 不支持 | 支持 | 不支持 | | | +| **SSML** | 不支持 | | | | | +| **LaTeX** | 不支持 | | | | | +| **音量调节** | 支持 > 可通过[指令控制](#12884a10929p9)调节 | 不支持 | | | | +| **语速调节** | 支持 > 可通过[指令控制](#12884a10929p9)调节 | 不支持 | | | | +| **语调(音高)调节** | 支持 > 可通过[指令控制](#12884a10929p9)调节 | 不支持 | | | | +| **码率调节** | 不支持 | | | | | +| **时间戳** | 不支持 | | | | | +| **指令控制(Instruct)** | 支持 | 不支持 | | | | +| **流式输入** | 不支持 | | | | | +| **流式输出** | 支持 | | | | | +| **限流** | 每分钟调用次数(RPM):180 | 每分钟调用次数(RPM):180 | 每分钟调用次数(RPM):180 | 每分钟调用次数(RPM)因模型而异: - qwen3-tts-flash、qwen3-tts-flash-2025-11-27:180 - qwen3-tts-flash-2025-09-18:10 | 每分钟调用次数(RPM):10 每分钟消耗Token数(TPM,含输入与输出Token):100,000 | +| **接入方式** | Java/Python SDK、WebSocket API | | | | | +| **价格** | 中国内地:0.8元/万字符 国际:0.8元/万字符 | 中国内地:0.8元/万字符 国际:0.8元/万字符 | 中国内地:0.8元/万字符 国际:0.8元/万字符 | 中国内地:0.8元/万字符 国际:0.733924元/万字符 | 中国内地: - 输入成本:0.0016元/千Token - 输出成本:0.01元/千Token 音频转换为 Token 的规则:每1秒的音频对应 50个 Token ;若音频时长不足1秒,则按 50个 Token 计算 | + +## **支持的系统音色** + +不同模型支持的音色有所差异,使用时将请求参数`voice`设置为音色列表中**voice参数**列对应的值。 + +| `**voice**`**参数** | **详情** | **支持语种** | **支持模型** | +| `Cherry` | **音色名**:芊悦 **描述**:阳光积极、亲切自然小姐姐(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 - **千问-TTS**:qwen-tts、qwen-tts-2025-04-10、qwen-tts-latest、qwen-tts-2025-05-22 | +| `Serena` | **音色名**:苏瑶 **描述**:温柔小姐姐(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 - **千问-TTS**:qwen-tts、qwen-tts-2025-04-10、qwen-tts-latest、qwen-tts-2025-05-22 | +| `Ethan` | **音色名**:晨煦 **描述**:标准普通话,带部分北方口音。阳光、温暖、活力、朝气(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 - **千问-TTS**:qwen-tts、qwen-tts-2025-04-10、qwen-tts-latest、qwen-tts-2025-05-22 | +| `Chelsie` | **音色名**:千雪 **描述**:二次元虚拟女友(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 - **千问-TTS**:qwen-tts、qwen-tts-2025-04-10、qwen-tts-latest、qwen-tts-2025-05-22 | +| `Momo` | **音色名**:茉兔 **描述**:撒娇搞怪,逗你开心(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Vivian` | **音色名**:十三 **描述**:拽拽的、可爱的小暴躁(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Moon` | **音色名**:月白 **描述**:率性帅气的月白(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Maia` | **音色名**:四月 **描述**:知性与温柔的碰撞(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Kai` | **音色名**:凯 **描述**:耳朵的一场SPA(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Nofish` | **音色名**:不吃鱼 **描述**:不会翘舌音的设计师(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Bella` | **音色名**:萌宝 **描述**:喝酒不打醉拳的小萝莉(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Jennifer` | **音色名**:詹妮弗 **描述**:品牌级、电影质感般美语女声(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Ryan` | **音色名**:甜茶 **描述**:节奏拉满,戏感炸裂,真实与张力共舞(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Katerina` | **音色名**:卡捷琳娜 **描述**:御姐音色,韵律回味十足(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Aiden` | **音色名**:艾登 **描述**:精通厨艺的美语大男孩(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Eldric Sage` | **音色名**:沧明子 **描述**:沉稳睿智的老者,沧桑如松却心明如镜(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Mia` | **音色名**:乖小妹 **描述**:温顺如春水,乖巧如初雪(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Mochi` | **音色名**:沙小弥 **描述**:聪明伶俐的小大人,童真未泯却早慧如禅(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Bellona` | **音色名**:燕铮莺 **描述**:声音洪亮,吐字清晰,人物鲜活,听得人热血沸腾;金戈铁马入梦来,字正腔圆间尽显千面人声的江湖(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Vincent` | **音色名**:田叔 **描述**:一口独特的沙哑烟嗓,一开口便道尽了千军万马与江湖豪情(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Bunny` | **音色名**:萌小姬 **描述**:“萌属性”爆棚的小萝莉(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Neil` | **音色名**:阿闻 **描述**:平直的基线语调,字正腔圆的咬字发音,这就是最专业的新闻主持人(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Elias` | **音色名**:墨讲师 **描述**:既保持学科严谨性,又通过叙事技巧将复杂知识转化为可消化的认知模块(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Arthur` | **音色名**:徐大爷 **描述**:被岁月和旱烟浸泡过的质朴嗓音,不疾不徐地摇开了满村的奇闻异事(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Nini` | **音色名**:邻家妹妹 **描述**:糯米糍一样又软又黏的嗓音,那一声声拉长了的“哥哥”,甜得能把人的骨头都叫酥了(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Ebona` | **音色名**:诡婆婆 **描述**:她的低语像一把生锈的钥匙,缓慢转动你内心最深处的幽暗角落——那里藏着所有你不敢承认的童年阴影与未知恐惧(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Seren` | **音色名**:小婉 **描述**:温和舒缓的声线,助你更快地进入睡眠,晚安,好梦(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Pip` | **音色名**:顽屁小孩 **描述**:调皮捣蛋却充满童真的他来了,这是你记忆中的小新吗(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Stella` | **音色名**:少女阿月 **描述**:平时是甜到发腻的迷糊少女音,但在喊出“代表月亮消灭你”时,瞬间充满不容置疑的爱与正义(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Instruct-Flash**:qwen3-tts-instruct-flash、qwen3-tts-instruct-flash-2026-01-26 - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Bodega` | **音色名**:博德加 **描述**:热情的西班牙大叔(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Sonrisa` | **音色名**:索尼莎 **描述**:热情开朗的拉美大姐(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Alek` | **音色名**:阿列克 **描述**:一开口,是战斗民族的冷,也是毛呢大衣下的暖(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Dolce` | **音色名**:多尔切 **描述**:慵懒的意大利大叔(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Sohee` | **音色名**:素熙 **描述**:温柔开朗,情绪丰富的韩国欧尼(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Ono Anna` | **音色名**:小野杏 **描述**:鬼灵精怪的青梅竹马(女性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Lenn` | **音色名**:莱恩 **描述**:理性是底色,叛逆藏在细节里——穿西装也听后朋克的德国青年(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Emilien` | **音色名**:埃米尔安 **描述**:浪漫的法国大哥哥(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Andre` | **音色名**:安德雷 **描述**:声音磁性,自然舒服、沉稳男生(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Radio Gol` | **音色名**:拉迪奥·戈尔 **描述**:足球诗人Rádio Gol!今天我要用名字为你们解说足球(男性) | 中文(普通话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27 | +| `Jada` | **音色名**:上海-阿珍 **描述**:风风火火的沪上阿姐(女性) | 中文(上海话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 - **千问-TTS**:qwen-tts-latest、qwen-tts-2025-05-22 | +| `Dylan` | **音色名**:北京-晓东 **描述**:北京胡同里长大的少年(男性) | 中文(北京话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 - **千问-TTS**:qwen-tts-latest、qwen-tts-2025-05-22 | +| `Li` | **音色名**:南京-老李 **描述**:耐心的瑜伽老师(男性) | 中文(南京话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Marcus` | **音色名**:陕西-秦川 **描述**:面宽话短,心实声沉——老陕的味道(男性) | 中文(陕西话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Roy` | **音色名**:闽南-阿杰 **描述**:诙谐直爽、市井活泼的台湾哥仔形象(男性) | 中文(闽南语)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Peter` | **音色名**:天津-李彼得 **描述**:天津相声,专业捧哏(男性) | 中文(天津话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Sunny` | **音色名**:四川-晴儿 **描述**:甜到你心里的川妹子(女性) | 中文(四川话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 - **千问-TTS**:qwen-tts-latest、qwen-tts-2025-05-22 | +| `Eric` | **音色名**:四川-程川 **描述**:一个跳脱市井的四川成都男子(男性) | 中文(四川话)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Rocky` | **音色名**:粤语-阿强 **描述**:幽默风趣的阿强,在线陪聊(男性) | 中文(粤语)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | +| `Kiki` | **音色名**:粤语-阿清 **描述**:甜美的港妹闺蜜(女性) | 中文(粤语)、英语、法语、德语、俄语、意大利语、西班牙语、葡萄牙语、日语、韩语 | - **千问3-TTS-Flash**:qwen3-tts-flash、qwen3-tts-flash-2025-11-27、qwen3-tts-flash-2025-09-18 | + +## **常见问题** + +### **Q:音频文件链接的有效期是多久?** + +A:24小时后音频文件链接将失效。 + +/\* 让引用上下间距调小,避免内容显示过于稀疏 \*/ .unionContainer .markdown-body blockquote { margin: 4px 0; } .aliyun-docs-content table.qwen blockquote { border-left: none; /\* 添加这一行来移除表格里的引用文字的左侧边框 \*/ padding-left: 5px; /\* 左侧内边距 \*/ margin: 4px 0; } /\* 支持吸顶 \*/ div:has(.aliyun-docs-content), .aliyun-docs-content .markdown-body { overflow: visible; } .stick-top { position: sticky; top: 46px; } + + span.aliyun-docs-icon { color: transparent !important; font-size: 0 !important; } span.aliyun-docs-icon:before { color: black; font-size: 16px; } span.aliyun-docs-icon.icon-size-20:before { font-size: 20px; } span.aliyun-docs-icon.icon-size-22:before { font-size: 22px; } span.aliyun-docs-icon.icon-size-24:before { font-size: 24px; } span.aliyun-docs-icon.icon-size-26:before { font-size: 26px; } span.aliyun-docs-icon.icon-size-28:before { font-size: 28px; } \ No newline at end of file diff --git a/src/md/ali-image.md b/src/md/ali-image.md new file mode 100644 index 0000000..23ed949 --- /dev/null +++ b/src/md/ali-image.md @@ -0,0 +1,722 @@ +通过文生图API,您可以基于文本描述创造出全新的图像。阿里云百炼提供两大系列模型: + +- 千问(Qwen-Image): 擅长渲染复杂的中英文文本。 + +- 万相(Wan系列): 用于生成写实图像和摄影级视觉效果。 + + +**在线体验**:[北京](https://bailian.console.aliyun.com/?tab=model#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=qwen-image)|[新加坡](https://modelstudio.console.aliyun.com/?tab=dashboard#/efm/model_experience_center/vision?currentTab=imageGenerate) + +## **模型效果** + +#### **千问(Qwen-image)** + +| **复杂布局** ![image (10)-2026-03-10-15-57-40](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/9024463771/p1058364.webp) | **超长段落** ![image (11)-2026-03-10-15-57-40](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/9024463771/p1058368.webp) | **写实人像** ![image (13)-2026-03-10-15-57-39](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/9024463771/p1058369.webp) | +| --- | --- | --- | +| **自然景观** ![image (12)-2026-03-10-15-57-39](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/9024463771/p1058371.webp) | **逻辑架构** ![image (14)-2026-03-10-15-57-38](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/9024463771/p1058372.webp) | **电商海报** ![fcd74cd8-c0f6-454b-93b1-e95f337127af-2026-03-10-16-25-42](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=) | + +**点击查看提示词** + +**复杂布局**:冬日北京的都市街景,青灰瓦顶、朱红色外墙的两间相邻中式商铺比肩而立,檐下悬挂印有剪纸马的暖光灯笼,在阴天漫射光中投下柔和光晕,映照湿润鹅卵石路面泛起细腻反光。左侧为书法店:靛蓝色老旧的牌匾上以遒劲行书刻着“**文字渲染**”。店门口的玻璃上挂着一幅字,自上而下,用田英章硬笔写着“**专业幻灯片 中英文海报 高级信息图**”,落款印章为“**1k token**”朱砂印。店内的墙上,可以模糊的辨认有三幅竖排的书法作品,第一幅写着着“**阿里巴巴**”,第二幅写着“**通义千问**”,第三福写着“**图像生成**”。一位白发苍苍的老人背对着镜头观赏。右侧为花店,牌匾上以鲜花做成文字“**真实质感**”;店内多层花架陈列红玫瑰、粉洋牡丹和绿植,门上贴了一个圆形花边标识,标识上写着“**2k resolution**”,门口摆放了一个彩色霓虹灯,上面写着“**细腻刻画 人物 自然 建筑**”。两家店中间堆放了一个雪人,举了一老式小黑板,上面用粉笔字写着“**Qwen-Image-2.0 正式发布**”。街道左侧,年轻情侣依偎在一起,女孩是瘦脸,身穿米白色羊绒大衣,肉色光腿神器。女孩举着心形透明气球,气球印有白色的字:“**生图编辑二合一**”。里面有一个毛茸茸的卡皮巴拉玩偶。男孩身着剪裁合体的深灰色呢子外套,内搭浅色高领毛衣。街道右侧,一个后背上写着“**更小模型,更快速度**”的骑手疾驰而过。整条街光影交织、动静相宜。 + +**超长段落**:中国古典水墨长卷风格,竖幅构图,画面自上而下、自右向左以行书题写柳永《雨霖铃·寒蝉凄切》全文(共12行,含标点与换行):“**寒蝉凄切,对长亭晚,骤雨初歇。都门帐饮无绪,留恋处、兰舟催发。执手相看泪眼,竟无语凝噎。念去去,千里烟波,暮霭沉沉楚天阔。多情自古伤离别,更那堪、冷落清秋节!今宵酒醒何处?杨柳岸,晓风残月。此去经年,应是良辰好景虚设。便纵有千种风情,更与何人说**?”书法墨色浓淡相宜,飞白自然,笔锋遒劲中见婉转,行气连贯如流水;字迹略带微洇,仿宣纸渗透效果。背景为极简留白水墨意境:右下角绘一叶孤舟泊于浅滩,舟头微翘,缆绳轻系枯柳;左侧远景以淡墨晕染出层叠低垂的暮霭与空阔楚天,天际线处一抹青灰远山若隐若现;近景岸边斜出三两枝细柳,枝条纤柔,叶已疏落,承袭清秋萧瑟之气;柳梢悬一弯将隐未隐的残月,清冷微光映照薄雾中拂面的晓风痕迹(以几缕轻扬的柳丝与水纹示意)。整幅画气息沉郁隽永,哀而不伤,严格遵循宋词意境与传统文人画\*\*“诗书画一体”\*\*范式,无印章、无题跋、无现代元素。 + +**写实人像**:一位约20岁出头的亚洲年轻女性,留着齐刘海、乌黑光滑的长直发,自然垂落于双肩两侧。她侧坐于一张复古碎花布艺沙发上,沙发图案为米白底色配粉色与绿色花卉,质地略显陈旧,带有生活感。她身穿一件宽松的浅绿色马海毛针织毛衣,质感蓬松柔软,下身搭配浅灰蓝色亚麻长裙,整体造型清新自然、慵懒随性。右手轻轻握住一颗红色番茄,抬至下巴附近,姿态随意,眼神直视镜头,神情平静、略带冷淡,带有一种漫不经心的疏离感。沙发右侧放有一个浅色陶盘,盘内盛放着三至四颗饱满鲜红的番茄,带有绿色蒂头,色彩鲜艳,与画面整体的冷绿色调形成强烈对比。背景为做旧的青绿色墙面,斑驳而有质感。窗外射入的自然光形成明显的光束,斜斜打在人物与背景上,光影层次丰富。窗台及背景角落摆有数盆绿植,左侧隐约可见一个深棕色老式木柜。整张照片色调偏冷绿,叠有明显的胶片颗粒感与轻微漏光效果,构图饱满,氛围静谧、文艺,带有强烈的复古胶片人文摄影风格。 + +**自然景观**:一幅写实风格的夏日森林场景,画面中央是一片幽深静谧的林间空地,高大挺拔的橡树与山毛榉构成主体乔木层,其浓密树冠呈现深邃厚重的墨绿色,叶片表面带有细微的蜡质反光;树冠间隙中透下柔和而强烈的阳光,在空气中形成清晰可见的丁达尔光束,光束边缘略带暖金色调,与冷调绿影形成微妙对比。中景处一丛新生的枫树嫩枝舒展着鲜亮明快的翠绿色叶片,叶脉清晰、半透明感强,边缘微微卷曲,仿佛刚经历晨露洗礼。前景左侧低矮的冬青与荚蒾灌木丛披覆着哑光柔和的橄榄绿色,枝叶交错,纹理细腻,部分叶片背面泛出浅灰绿光泽。地面覆盖着厚实湿润的苔藓层,由多种苔类组成:近处是绒状垂穗藓,呈现饱满润泽的青绿色,表面凝结细小露珠;稍远处为鳞叶藓与泥炭藓交织,显出微带蓝调的灰青绿与棕绿过渡;腐叶层隐约可见,呈深褐与墨绿混融的有机质感。所有植被表面均带有自然微湿反光,空气中有极细微的悬浮微粒在光束中浮动。背景林区渐次虚化,保留层次但不抢主体,远景融入一层薄薄的蓝绿雾霭。整体光影为上午10点左右的斜射日光,明暗对比适中,绿色系通过23种以上不同明度、饱和度、冷暖倾向与材质表现(如蜡质、绒面、革质、胶质)精确区分,毫无重复感,营造出丰饶、呼吸感强烈、充满生物细节与生态真实性的夏日森林秘境。 + +**逻辑架构:**一幅充满生活气息的插画,采用细腻、柔和的画风,色彩鲜艳且层次丰富,呈现出阳光明媚的街头景象,整体氛围轻松愉快。画面中是一条热闹的商业街道,天空湛蓝,点缀着几朵蓬松白云,几只海鸥在空中自由翱翔,为画面增添动感与生机。街道两旁的建筑风格现代而富有特色,外墙色彩明快,墙上悬挂着多个招牌,分别写着“**阿里巴巴**”、“**百炼**”、“**文生图**”,字体清晰可辨,排列错落有致,营造出浓厚的商业氛围。街道上人流如织,展现出繁忙而温馨的日常场景。画面前景中,一个穿着白色衬衫和短裤的男孩正站在一个摆满商品的货摊前专注挑选。他神情认真,身体微微前倾,体现出对商品的兴趣。货摊上陈列着各类饮料、零食和日用品,摆放整齐,细节丰富。摊主是一名中年男子,身穿深色围裙,神情专注地整理商品或与顾客交流,展现出市井生活的亲切感。货摊上方悬挂着一块木质标牌,清晰写着“**Qwen-Image**”,字体为手写风格,增添艺术感。整个场景通过细腻的描绘和温暖的色调,展现了日常生活中那些简单却美好的瞬间。画面风格为现实主义插画风格,带有轻微的手绘质感,强调光影与细节表现,整体构图饱满,空间感强。 + +**电商海报:**一张高质量的 C4D 风格电商海报,清新蓝色调。画面顶部为巨大的立体艺术字体 “**天猫 双十一 预售来了**”,极具视觉张力。主体是一袋蓝色包装的 “**萌宠家园**” 宠物粮,包装袋有透明窗口展示诱人的肉块,旁边有一只可爱的3D建模小猫。场景中点缀着精致的动物小模型和蓝色的科技感机械装置,营造热闹的促销氛围。底部醒目显示红色立体字 “**全场满 399 元减 99**”。明亮的商业工作室灯光,超高清,渲染细腻,质感通透,构图严谨。" + +#### **万相** + +| **人像写真** ![p1023408-转换自-png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=) | **写实摄影** ![p1023409-转换自-png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=) | **绘画流派** ![p1023411](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=) | +| --- | --- | --- | +| **文字生成** ![p1023399](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=) | **海报设计** ![image.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=) | **组图生成** ![p1023424-转换自-png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=) | + +**点击查看提示词** + +**人像写真**:照片,摄影人像,写实人像:背景是故宫红墙,女子身穿黑色旗袍,手握扇子,长曝光摄影,王家卫电影感,故事感,人来人往,迷离的光线,形成迷离的轨迹,柔焦摄影,眼神深邃神秘,艺术气息十足。 + +**写实摄影**:写实摄影,一只狐狸在森林中凝视镜头,鱼眼视角带来强烈的透视效果,毛发细节清晰,背景树木呈圆形拉伸,水彩风格,柔和色调。 + +**绘画流派**:一束野花插在旧陶罐中,背景是乡村厨房,印象派风格,柔和笔触,温暖光线,油画质感 + +**文字生成:**毛笔水墨画风格,宣纸纹理清晰可见,淡墨晕染出朦胧的客厅轮廓。一位身着素色长裙的东方少女盘坐于虚化边缘的旧式布艺沙发上,侧脸低垂,手持一卷展开的诗稿,窗外竹影婆娑,微风拂动帘栊。画面大量留白,右侧题有小楷诗句“闲坐悲双鬓,幽梦入青烟”,左下角钤朱文印章。墨色浓淡相宜,飞白笔触勾勒出光影流动感,意境空寂深远,似有古琴余音缭绕其间。 + +**海报设计:**扁平几何插画风格,一张端午节海报,杂志封面,色调与背景:以粉色渐变为主色调,营造出柔和且富有节日氛围的背景,奠定温馨且传统的基调。 文字元素:绿色字体搭配阴影效果,主文案突出"DRAGONBOAT FESTIVAL"与"端午"分两行不分开,正文信息下方"2025/05/31"、"农历五月初五"突出端午数字时间信息"2025/05/31"。 主体图案: 一艘绿色龙身搭配粉色龙鳍的龙船,高饱和色调,色彩对比强烈,高周围点缀祥云元素,船上坐着人物,进一步呼应赛龙舟的场景,增添节日活力。 细节点缀:添加 "中国传统节日" 字样,搭配小型粽子图标,丰富文化细节。高级简约排版方式,大师杰出作品。简约,时尚,大气,新中式传统海报,字体不要有阴影样式。 + +**组图生成**:四宫格日系Q版漫画,赛璐璐风格。第一格:戴黑框眼镜的程序员面对屏幕弹出的红色报错,瞳孔地震,冷汗飞溅,背景变为裂开的像素深渊。第二格:他撸起袖子敲击键盘,自信挑眉,头顶冒出“这不过是五行代码的事!”对话框。第三格:屏幕布满混乱的报错符号,他头发炸立,眼圈发黑,椅子后仰45度,天花板飘满废弃的流程图。第四格:误删一行灰色注释后,绿色对勾闪现,他歪头呆滞,屏幕上浮起问号气泡:“……所以它只是个幻觉?” + +## **支持的模型** + +- [千问文生图](https://help.aliyun.com/zh/model-studio/models#34e47bbcf57v1) + +- [万相文生图](https://help.aliyun.com/zh/model-studio/models#b4eb59e706n17) + + +## **模型选型** + +- **复杂文字渲染**(如海报、对联):首选`**qwen-image-2.0-pro**`**、**`**wan2.6-t2i**`。 + +- **写实场景和摄影风格**(通用场景):可选万相模型,如`**wan2.6-t2i**`、`**wan2.5-t2i-preview**`。 + +- **需要自定义输出图像分辨率:**推荐`**qwen-image-2.0**`系列或万相模型。qwen-image-2.0系列支持自由设置宽高,输出图像总像素在\[512\*512, 2048\*2048\]之间;万相模型如`**wan2.6-t2i**`,输出图像总像素在\[1280\*1280, 1440\*1440\]之间。 + + > qwen-image-max、qwen-image-plus系列模型仅支持5种固定尺寸:1664\*928(16:9)、928\*1664(9:16)、1328\*1328(1:1)、1472\*1104(4:3)、1104\*1472(3:4)。 + +- **成本极度敏感,可接受基础质量:**可选择`**wanx2.0-t2i-turbo**`,价格较低,请参见[计费与限流](#a585cbf27dck8)。 + + +## 快速开始 + +#### **前提条件** + +在调用前,请[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key),再[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。如果通过DashScope SDK进行调用,还需要[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +#### **示例代码** + +**调用方式说明**: + +- 千问文生图模型均支持同步调用,其中qwen-image-plus、qwen-image模型支持异步调用,详情请参见[千问-文生图](https://help.aliyun.com/zh/model-studio/qwen-image-api)。 + +- 万相文生图模型均支持异步调用,其中wan2.6-t2i支持同步调用,详情请参见[万相-文生图V2](https://help.aliyun.com/zh/model-studio/text-to-image-v2-api-reference)。 + + +## 同步调用 + +## Python + +### **请求示例** + +``` +import json +import os +import dashscope +from dashscope import MultiModalConversation + +# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 +dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1' + +messages = [ + { + "role": "user", + "content": [ + {"text": "冬日北京的都市街景,青灰瓦顶、朱红色外墙的两间相邻中式商铺比肩而立,檐下悬挂印有剪纸马的暖光灯笼,在阴天漫射光中投下柔和光晕,映照湿润鹅卵石路面泛起细腻反光。左侧为书法店:靛蓝色老旧的牌匾上以遒劲行书刻着“文字渲染”。店门口的玻璃上挂着一幅字,自上而下,用田英章硬笔写着“专业幻灯片 中英文海报 高级信息图”,落款印章为“1k token”朱砂印。店内的墙上,可以模糊的辨认有三幅竖排的书法作品,第一幅写着着“阿里巴巴”,第二幅写着“通义千问”,第三福写着“图像生成”。一位白发苍苍的老人背对着镜头观赏。右侧为花店,牌匾上以鲜花做成文字“真实质感”;店内多层花架陈列红玫瑰、粉洋牡丹和绿植,门上贴了一个圆形花边标识,标识上写着“2k resolution”,门口摆放了一个彩色霓虹灯,上面写着“细腻刻画 人物 自然 建筑”。两家店中间堆放了一个雪人,举了一老式小黑板,上面用粉笔字写着“Qwen-Image-2.0 正式发布”。街道左侧,年轻情侣依偎在一起,女孩是瘦脸,身穿米白色羊绒大衣,肉色光腿神器。女孩举着心形透明气球,气球印有白色的字:“生图编辑二合一”。里面有一个毛茸茸的卡皮巴拉玩偶。男孩身着剪裁合体的深灰色呢子外套,内搭浅色高领毛衣。街道右侧,一个后背上写着“更小模型,更快速度”的骑手疾驰而过。整条街光影交织、动静相宜。"} + ] + } +] + +# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +api_key = os.getenv("DASHSCOPE_API_KEY") + +response = MultiModalConversation.call( + api_key=api_key, + model="qwen-image-2.0-pro", + messages=messages, + result_format='message', + stream=False, + watermark=False, + prompt_extend=True, + negative_prompt="低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。", + size='2048*2048' +) + +if response.status_code == 200: + print(json.dumps(response, ensure_ascii=False)) +else: + print(f"HTTP返回码:{response.status_code}") + print(f"错误码:{response.code}") + print(f"错误信息:{response.message}") + print("请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code") +``` + +### **响应示例** + +> 图像链接的有效期为24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "d2d1a8c0-325f-9b9d-8b90-xxxxxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/xxx.png?Expires=xxx" + } + ] + } + } + ] + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "width": 2048, + "image_count": 1, + "height": 2048 + } +} +``` + +## Java + +### **请求示例** + +``` +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; +import com.alibaba.dashscope.common.MultiModalMessage; +import com.alibaba.dashscope.common.Role; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class QwenImage { + + static { + // 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1 + Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1"; + } + + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:static String apiKey ="sk-xxx" + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public static void call() throws ApiException, NoApiKeyException, UploadFileException, IOException { + + MultiModalConversation conv = new MultiModalConversation(); + + MultiModalMessage userMessage = MultiModalMessage.builder().role(Role.USER.getValue()) + .content(Arrays.asList( + Collections.singletonMap("text", "冬日北京的都市街景,青灰瓦顶、朱红色外墙的两间相邻中式商铺比肩而立,檐下悬挂印有剪纸马的暖光灯笼,在阴天漫射光中投下柔和光晕,映照湿润鹅卵石路面泛起细腻反光。左侧为书法店:靛蓝色老旧的牌匾上以遒劲行书刻着“文字渲染”。店门口的玻璃上挂着一幅字,自上而下,用田英章硬笔写着“专业幻灯片 中英文海报 高级信息图”,落款印章为“1k token”朱砂印。店内的墙上,可以模糊的辨认有三幅竖排的书法作品,第一幅写着着“阿里巴巴”,第二幅写着“通义千问”,第三福写着“图像生成”。一位白发苍苍的老人背对着镜头观赏。右侧为花店,牌匾上以鲜花做成文字“真实质感”;店内多层花架陈列红玫瑰、粉洋牡丹和绿植,门上贴了一个圆形花边标识,标识上写着“2k resolution”,门口摆放了一个彩色霓虹灯,上面写着“细腻刻画 人物 自然 建筑”。两家店中间堆放了一个雪人,举了一老式小黑板,上面用粉笔字写着“Qwen-Image-2.0 正式发布”。街道左侧,年轻情侣依偎在一起,女孩是瘦脸,身穿米白色羊绒大衣,肉色光腿神器。女孩举着心形透明气球,气球印有白色的字:“生图编辑二合一”。里面有一个毛茸茸的卡皮巴拉玩偶。男孩身着剪裁合体的深灰色呢子外套,内搭浅色高领毛衣。街道右侧,一个后背上写着“更小模型,更快速度”的骑手疾驰而过。整条街光影交织、动静相宜。") + )).build(); + + Map parameters = new HashMap<>(); + parameters.put("watermark", false); + parameters.put("prompt_extend", true); + parameters.put("negative_prompt", "低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。"); + parameters.put("size", "2048*2048"); + + MultiModalConversationParam param = MultiModalConversationParam.builder() + .apiKey(apiKey) + .model("qwen-image-2.0-pro") + .messages(Collections.singletonList(userMessage)) + .parameters(parameters) + .build(); + + MultiModalConversationResult result = conv.call(param); + System.out.println(JsonUtils.toJson(result)); + } + + public static void main(String[] args) { + try { + call(); + } catch (ApiException | NoApiKeyException | UploadFileException | IOException e) { + System.out.println(e.getMessage()); + } + System.exit(0); + } +} +``` + +### **响应示例** + +> 图像链接的有效期为24小时,请及时下载图像。 + +``` +{ + "requestId": "5b6f2d04-b019-40db-a5cc-xxxxxx", + "usage": { + "image_count": 1, + "width": 2048, + "height": 2048 + }, + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/xxx.png?Expires=xxx" + } + ] + } + } + ] + } +} +``` + +## curl + +##### **请求示例** + +``` +curl --location 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \ +--header 'Content-Type: application/json' \ +--header "Authorization: Bearer $DASHSCOPE_API_KEY" \ +--data '{ + "model": "qwen-image-2.0-pro", + "input": { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "冬日北京的都市街景,青灰瓦顶、朱红色外墙的两间相邻中式商铺比肩而立,檐下悬挂印有剪纸马的暖光灯笼,在阴天漫射光中投下柔和光晕,映照湿润鹅卵石路面泛起细腻反光。左侧为书法店:靛蓝色老旧的牌匾上以遒劲行书刻着“文字渲染”。店门口的玻璃上挂着一幅字,自上而下,用田英章硬笔写着“专业幻灯片 中英文海报 高级信息图”,落款印章为“1k token”朱砂印。店内的墙上,可以模糊的辨认有三幅竖排的书法作品,第一幅写着着“阿里巴巴”,第二幅写着“通义千问”,第三福写着“图像生成”。一位白发苍苍的老人背对着镜头观赏。右侧为花店,牌匾上以鲜花做成文字“真实质感”;店内多层花架陈列红玫瑰、粉洋牡丹和绿植,门上贴了一个圆形花边标识,标识上写着“2k resolution”,门口摆放了一个彩色霓虹灯,上面写着“细腻刻画 人物 自然 建筑”。两家店中间堆放了一个雪人,举了一老式小黑板,上面用粉笔字写着“Qwen-Image-2.0 正式发布”。街道左侧,年轻情侣依偎在一起,女孩是瘦脸,身穿米白色羊绒大衣,肉色光腿神器。女孩举着心形透明气球,气球印有白色的字:“生图编辑二合一”。里面有一个毛茸茸的卡皮巴拉玩偶。男孩身着剪裁合体的深灰色呢子外套,内搭浅色高领毛衣。街道右侧,一个后背上写着“更小模型,更快速度”的骑手疾驰而过。整条街光影交织、动静相宜。" + } + ] + } + ] + }, + "parameters": { + "negative_prompt": "低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。", + "prompt_extend": true, + "watermark": false, + "size": "2048*2048" + } +}' +``` + +``` +curl --location 'https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \ +--header 'Content-Type: application/json' \ +--header "Authorization: Bearer $DASHSCOPE_API_KEY" \ +--data '{ + "model": "qwen-image-2.0-pro", + "input": { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Healing-style hand-drawn poster featuring three puppies playing with a ball on lush green grass, adorned with decorative elements such as birds and stars. The main title “Come Play Ball!” is prominently displayed at the top in bold, blue cartoon font. Below it, the subtitle “Come [Show Off Your Skills]!” appears in green font. A speech bubble adds playful charm with the text: “Hehe, watch me amaze my little friends next!” At the bottom, supplementary text reads: “We get to play ball with our friends again!” The color palette centers on fresh greens and blues, accented with bright pink and yellow tones to highlight a cheerful, childlike atmosphere." + } + ] + } + ] + }, + "parameters": { + "negative_prompt": "低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。", + "prompt_extend": true, + "watermark": false, + "size": "2048*2048" + } +}' +``` + +##### **响应示例** + +``` +{ + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "content": [ + { + "image": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxx.png?Expires=xxx" + } + ], + "role": "assistant" + } + } + ] + }, + "usage": { + "height": 2048, + "image_count": 1, + "width": 2048 + }, + "request_id": "d0250a3d-b07f-49e1-bdc8-6793f4929xxx" +} +``` + +## 异步调用 + +> SDK 在底层封装了异步处理逻辑,上层接口表现为同步调用(即单次请求并等待最终结果返回);而 curl 示例则对应两个独立的异步 API 接口:一个用于提交任务,另一个用于查询结果。 + +## Python + +### **请求示例** + +``` +import os +import dashscope +from dashscope.aigc.image_generation import ImageGeneration +from dashscope.api_entities.dashscope_response import Message + +# 以下为北京地域url,各地域的base_url不同 +dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1' + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + +message = Message( + role="user", + content=[ + { + 'text': '一间有着精致窗户的花店,漂亮的木质门,摆放着花朵' + } + ] +) +print("----sync call, please wait a moment----") +rsp = ImageGeneration.call( + model="wan2.6-t2i", + api_key=api_key, + messages=[message], + negative_prompt="", + prompt_extend=True, + watermark=False, + n=1, + size="1280*1280" +) +print(rsp) +``` + +### 响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "820dd0db-eb42-4e05-8d6a-1ddb4axxxxxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "audio": null, + "finished": true + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "characters": 0, + "image_count": 1, + "size": "1280*1280", + "total_tokens": 0 + } +} +``` + +## Java + +### **请求示例** + +``` +import com.alibaba.dashscope.aigc.imagegeneration.*; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; +import java.util.Collections; + +public class Main { + + static { + // 以下为北京地域url,各地域的base_url不同 + Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1"; + } + + // 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx" + // 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public static void basicCall() throws ApiException, NoApiKeyException, UploadFileException { + ImageGenerationMessage message = ImageGenerationMessage.builder() + .role("user") + .content(Collections.singletonList( + Collections.singletonMap("text", "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵") + )).build(); + + ImageGenerationParam param = ImageGenerationParam.builder() + .apiKey(apiKey) + .model("wan2.6-t2i") + .n(1) + .size("1280*1280") + .negativePrompt("") + .promptExtend(true) + .watermark(false) + .messages(Collections.singletonList(message)) + .build(); + + ImageGeneration imageGeneration = new ImageGeneration(); + ImageGenerationResult result = null; + try { + System.out.println("---sync call, please wait a moment----"); + result = imageGeneration.call(param); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + throw new RuntimeException(e.getMessage()); + } + System.out.println(JsonUtils.toJson(result)); + } + + public static void main(String[] args) { + try { + basicCall(); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + System.out.println(e.getMessage()); + } + } +} +``` + +### 响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "50b57166-eaaa-4f17-b1e0-35a5ca88672c", + "code": "", + "message": "", + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxx.png?Expires=xxx", + "type": "image" + } + ] + } + } + ], + "finished": true + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "image_count": 1, + "size": "1280*1280", + "total_tokens": 0 + } +} +``` + +## curl + +**说明** + +- 异步调用必须设置 Header 参数`X-DashScope-Async` 为`enable`。 + +- 异步任务的 `task_id` 查询有效期为 24 小时,过期后任务状态将变为 `UNKNOWN`。 + +- 适用于所有模型,新手建议使用 [Postman](https://help.aliyun.com/zh/model-studio/first-call-to-image-and-video-api)调用API。 + + +##### **步骤1:发起创建任务请求** + +该请求会返回一个任务ID(`task_id`)。 + +``` +curl --location 'https://dashscope.aliyuncs.com/api/v1/services/aigc/image-generation/generation' \ +--header 'Content-Type: application/json' \ +--header "Authorization: Bearer $DASHSCOPE_API_KEY" \ +--header 'X-DashScope-Async: enable' \ +--data '{ + "model": "wan2.6-t2i", + "input": { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵" + } + ] + } + ] + }, + "parameters": { + "prompt_extend": true, + "watermark": false, + "n": 1, + "negative_prompt": "", + "size": "1280*1280" + } +}' +``` + +##### **步骤2:根据任务ID查询结果** + +使用上一步获取的 `task_id`,通过接口轮询任务状态,直到 `task_status` 变为 SUCCEEDED 或 FAILED。 + +将`{task_id}`完整替换为上一步接口返回的`task_id`的值。`task_id`查询有效期为24小时。 + +``` +curl -X GET https://dashscope.aliyuncs.com/api/v1/tasks/{task_id} \ +--header "Authorization: Bearer $DASHSCOPE_API_KEY" +``` + +## 关键能力 + +### **1\. 指令遵循(提示词)** + +参数:`input.prompt`(必选)、`input.negative_prompt`(可选)。 + +- **prompt(正向提示词)**:描述希望在画面中看到的内容、主体、场景、风格、光照和构图。文生图的核心控制参数。 + +- **negative\_prompt(反向提示词)**:描述不希望在画面中出现的内容,如“模糊”、“多余的手指”等。仅用于辅助优化生成质量。 + + +撰写技巧:一个结构化的 Prompt 通常能带来更好的效果,撰写技巧请参见[文生图Prompt指南](https://help.aliyun.com/zh/model-studio/text-to-image-prompt)。 + +### **2\. 开启prompt智能改写** + +参数: `parameters.prompt_extend` (bool, **默认为 true**)。 + +此功能可自动扩展和优化**较短的Prompt**,提升出图效果。开启此功能额外耗时 3-5 秒。此耗时为使用大模型改写文本。 + +实践建议: + +- 建议开启:当输入 Prompt 较简洁或宽泛时,此功能可显著提升图像效果。 + +- 建议关闭:若需控制画面细节、或已提供详细描述,或对响应延迟敏感。请将参数 `prompt_extend` **显式设为** `false`。 + + +### **3\. 设置输出图像分辨率** + +参数: parameters.size (string),格式为 `**"宽*高"**`。 + +**qwen-image-2.0 系列**:支持自由设置宽高,输出图像总像素需在512\*512至2048\*2048之间。默认分辨率为2048\*2048。推荐分辨率: + +- `2688*1536` :16:9 + +- `1536*2688` :9:16 + +- `2048*2048`(默认值):1:1 + +- `2368*1728` :4:3 + +- `1728*2368` :3:4 + + +**qwen-image-max、qwen-image-plus 系列**:仅支持以下 5 种固定的分辨率: + +- `1664*928`(默认值):16:9 + +- `1472*1104`:4:3 + +- `1328*1328`:1:1 + +- `1104*1472`:3:4 + +- `928*1664`:9:16 + + +**万相 V2 版模型 (2.0 及以上版本)**:支持在 `[512, 1440]` 像素范围内任意组合宽高,总像素不超过 1440\*1440。常用分辨率: + +- `1024*1024`(默认值):1:1 + +- `1440*810`: 16:9 + +- `810*1440`: 9:16 + +- `1440*1080`: 4:3 + +- `1080*1440`: 3:4 + + +## **应用于生产环境** + +- **容错策略** + + - **处理限流**:当 API 返回 `Throttling` 错误码或 HTTP 429 状态码时,表明已触发限流,限流处理请参见[限流](https://help.aliyun.com/zh/model-studio/rate-limit)。 + + - **异步任务轮询**:轮询查询异步任务结果时,建议采用合理的轮询策略(如前30秒每3秒一次,之后拉长间隔),避免因过于频繁的请求而触发限流。为任务设置一个最终超时时间(如 2 分钟),超时后标记为失败。 + +- **风险防范** + + - **结果持久化**:API 返回的图片 URL 有 24 小时有效期。生产系统必须在获取 URL 后立即下载图片,并转存至您自己的持久化存储服务中(如阿里云对象存储 OSS)。 + + - **内容安全审核**:所有 `prompt` 和 `negative_prompt` 都会经过内容安全审核。若输入内容不合规,请求将被拦截并返回 `DataInspectionFailed` 错误。 + + - **生成内容的版权与合规风险**:请确保您的提示词内容符合相关法律法规。生成包含品牌商标、名人肖像、受版权保护的 IP 形象等内容可能涉及侵权风险,请您自行评估并承担相应责任。 + + +## **API文档** + +- [千问 Qwen-Image](https://help.aliyun.com/zh/model-studio/qwen-image-api) + +- [万相-文生图V2](https://help.aliyun.com/zh/model-studio/text-to-image-v2-api-reference) + + +## **计费与限流** + +- 模型免费额度和计费单价请参见[模型价格](https://help.aliyun.com/zh/model-studio/models#4611ffaa38hnp)。 + +- 模型限流请参见[限流-图像生成](https://help.aliyun.com/zh/model-studio/rate-limit#5998fd159df49)。 + +- 计费说明:按成功生成的 **图像张数** 计费。模型调用失败或处理错误不产生任何费用,也不消耗[新人免费额度](https://help.aliyun.com/zh/model-studio/new-free-quota)。 + + +## **错误码** + +如果模型调用失败并返回报错信息,请参见[错误信息](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 + +## **常见问题** + +**Q: 图片 URL 多久会失效?我应该如何永久保存图片?** + +A: 图片 URL 的有效期为 24 小时。您必须在获取到 URL 后,立即通过程序下载图片,并将其保存到您自己的持久化存储中,例如本地服务器或阿里云对象存储 OSS。 + +**Q: 调用API返回DataInspectionFailed错误,如何处理?** + +A: 该错误表示输入文本触发了内容安全审核。请检查并修改prompt或negative\_prompt中的文本,移除可能违规的内容后重试。 + +**Q: prompt\_extend参数应该开启还是关闭?** + +A: 当输入的prompt比较简洁或希望模型发挥更多创意时,建议保持开启(默认)。当prompt已经非常详细、专业,或对API响应延迟有严格要求时,建议显式设置为false。 + +**Q: 如何提升图像中文字的生成效果?** + +A: 推荐使用`qwen-image-2.0-pro`模型,它具备更专业的文字渲染能力。 + +/\*表格图片设置为块元素(独占一行),居中展示,鼠标放在图片上可以点击查看原图\*/ .unionContainer .markdown-body .image.break { margin: 0px; display: inline-block; vertical-align: middle } /\* 让表格显示成类似钉钉文档的分栏卡片 \*/ table.help-table-card td { border: 10px solid #FFF !important; background: #F4F6F9; padding: 16px !important; vertical-align: top; } /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\* 表格中的引用上下间距调小,避免内容显示过于稀疏 \*/ .unionContainer .markdown-body table blockquote { margin: 4px 0 0 0; } \ No newline at end of file diff --git a/src/md/doubao_image.md b/src/md/doubao_image.md new file mode 100644 index 0000000..e8a59a6 --- /dev/null +++ b/src/md/doubao_image.md @@ -0,0 +1,2150 @@ +Seedream 4.0\-5.0 原生支持文本、单图和多图输入,实现基于主体一致性的多图融合创作、图像编辑、组图生成等多样玩法,让图像创作更加自由可控。本文以 Seedream 5.0 lite 为例介绍如何调用 [Image generation API](https://www.volcengine.com/docs/82379/1541523) 进行图像创作。如需使用 Seedream 4.5/4.0 模型,将下文代码示例中的 model 字段替换为对应的 Model ID 即可。 +:::tip +方舟平台的新用户?获取 API Key 及 开通模型等准备工作,请参见 [快速入门](/docs/82379/1399008)。 +::: + +# 模型效果 +更多效果示例见 [效果预览](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedream-5-0)。 + + +|场景 |输入 |输出 | +|---|---|---| +|文生图 `联网搜索`|制作一张上海未来5日的天气预报图,采用现代扁平化插画风格,清晰展示每日天气、温度和穿搭建议。 整体为横向排版,标题为“上海未来5日天气预报“,包含5个等宽的垂直卡片,从左到右依次排列。 整体风格为现代、干净、友好的扁平化矢量插画风格,线条清晰,色彩柔和。 人物形象采用年轻男女的卡通插画,表情自然,姿态放松,服装细节清晰。 |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/56e0e5cc24ff40559c9e934e5d744393~tplv-goo7wpa0wc-image.image =158x) |\ +|> Seedream 5.0 lite 模型可通过联网搜索功能,融合实时网络信息,提升生图时效性。 | | | +|多参考图生图|![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/2198d4bef000400bbfea18025850ed82~tplv-goo7wpa0wc-image.image =160x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/db71316f709243ceb69a629cd48598ff~tplv-goo7wpa0wc-image.image =158x) |\ +|> 输入多张参考图,融合它们的风格、元素等特征来生成新图像。 |> 将图1的服装换为图2的服装 | | +|组图生成|![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/a215e8241dd94f50901948790da121e1~tplv-goo7wpa0wc-image.image =160x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/fc505a263049499cb70af3f262f63078~tplv-goo7wpa0wc-image.image =158x) |\ +|> 基于用户输入的文字和图片,生成一组内容关联的图像 |> 参考图1,生成四图片,图中人物分别带着墨镜,骑着摩托,带着帽子,拿着棒棒糖 | | + + +# 模型能力 + + +|模型名称 ||[Doubao-Seedream-5.0-lite](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedream-5-0) |[Doubao-Seedream-4.5](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedream-4-5) |[Doubao-Seedream-4.0](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedream-4-0) | +|---|---|---|---|---| +|模型 ID (Model ID) ||doubao\-seedream\-5\-0\-260128 (同时支持:doubao\-seedream\-5\-0\-lite\-260128) |doubao\-seedream\-4\-5\-251128 |doubao\-seedream\-4\-0\-250828 | +|文生图 ||![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) | +|文生组图 ||![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) | +|单 / 多图生图 ||![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) | +|单 / 多图生组图 ||![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) | +|流式输出 ||![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) | +|联网搜索 ||![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/733f5c4e2c954d0f9f25c47e91c7fc9d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/1907ef06afcb468ab116acf4b16c972d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/1907ef06afcb468ab116acf4b16c972d~tplv-goo7wpa0wc-image.image =20x) | +|模型参数 |分辨率 |2K, 3K |2K, 4K |1K, 2K, 4K | +|^^|输出格式 |png, jpeg |jpeg |jpeg | +|^^|提示词优化模式 |标准模式 |标准模式 |标准模式, 极速模式 | +|^^|生成数量 |输入的参考图数量 + 最终生成的图片数量 ≤ 15张 ||| +|限流 IPM(张 / 分钟) ||500 |500 |500 | + + +# 快速体验 +您可在火山方舟平台 [API Explorer](https://api.volcengine.com/api-explorer/?action=ImageGenerations&groupName=%E5%9B%BE%E7%89%87%E7%94%9F%E6%88%90API&serviceCode=ark&tab=2&version=2024-01-01#N4IgTgpgzgDg9gOyhA+gMzmAtgQwC4gBcIArmADYgA0IUAlgF4REgBMA0tSAO74TY4wAayJoc5ZDSxwAJhErEZcEgCMccALTIIMyDiwaALBoAMG1gFYTADlbWuMMHCwwCxQPhmgUTTA-l6Ao2MAw-4CLeYB4tkHBgDOJgE2KgF+KgABygGHxgNf6gPSmgN2egCwegHEegCFugLCagCfKgOhKgGbx-oBFRoBjkYCTkZGA34qA2Ur+gKyugI76gOSagOJO-oDU5oCnpoBHphWA+Ib+gBVKI4Cf2oAr1oBOQf5wAMaATHaAy+b+gJKKgP1+gL-xgFRxY4CABoCEVoBTPv6A9maAj7b+gKGxgA3OgHnagNxygJJy-peAuyH+gNyugEbpgFgJgHH4wBjfoBvQOygAY5QAz2tkZoBLfUAQjqAQmtAIoagAIEp6AZXlAHBygC51c7+QAUsUNAPjuD38gHSzQKAOYzADMB52y6xagAlTQA55oBSELR0UA2DaAF7V-IAXU0xgB9FQDuioAvIMA9OaAbz1AM8GI0AHJqAAn1soB-PUAS5GAeASKmz-IAAAPW-kAs8qAEB1-IBA80AL4GMlr+QBc+oBUfUagDwVQA2aiAAL5AA),快速体验图片生成功能,支持自定义参数(例如设置图片水印、控制输出图片大小等),方便您直观感受其效果和性能。 +![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/5d1932f325e245249d9f955fe709ad85~tplv-goo7wpa0wc-image.image =2278x) + +# 基础使用 + +## 文生图(纯文本输入单图输出) +通过给模型提供清晰准确的文字指令,即可快速获得符合描述的高质量单张图片。 + + +|提示词 |输出 | +|---|---| +|充满活力的特写编辑肖像,模特眼神犀利,头戴雕塑感帽子,色彩拼接丰富,眼部焦点锐利,景深较浅,具有Vogue杂志封面的美学风格,采用中画幅拍摄,工作室灯光效果强烈。 |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/2ff811eb52bf47a6972bf3da0d5a99c9~tplv-goo7wpa0wc-image.image =173x) | + + +```mixin-react +return ( + + + + +); +``` + + +## 图文生图(单图输入单图输出) +基于已有图片,结合文字指令进行图像编辑,包括图像元素增删、风格转化、材质替换、色调迁移、改变背景/视角/尺寸等。 + + +|提示词 |输入图 |输出 | +|---|---|---| +|保持模特姿势和液态服装的流动形状不变。将服装材质从银色金属改为完全透明的清水(或玻璃)。透过液态水流,可以看到模特的皮肤细节。光影从反射变为折射。 |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/816153e67d3c4478886276154d78b22e~tplv-goo7wpa0wc-image.image =183x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/579ed507e0aa4647be9f1890d23e638e~tplv-goo7wpa0wc-image.image =183x) | + + +```mixin-react +return ( + + + + +); +``` + + +## 多图融合(多图输入单图输出) +根据您输入的文本描述和多张参考图片,融合它们的风格、元素等特征来生成新图像。如衣裤鞋帽与模特图融合成穿搭图,人物与风景融合为人物风景图等。 + + +|提示词 |输入图1 |输入图2 |输出 | +|---|---|---|---| +|将图1的服装换为图2的服装 |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/4b4464161cf3463db6f9463b10939178~tplv-goo7wpa0wc-image.image =163x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/c23d1b0528a14cb08b684307eabdcc9b~tplv-goo7wpa0wc-image.image =158x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/db71316f709243ceb69a629cd48598ff~tplv-goo7wpa0wc-image.image =158x) | + + +```mixin-react +return ( + + + + +); +``` + + +## 组图输出(多图输出) +支持通过一张或者多张图片和文字信息,生成漫画分镜、品牌视觉等一组内容关联的图片。 +需指定参数 **sequential_image_generation** 为`auto`。 + +### 文生组图 + + +|提示词 |输出(实际会输出4张图片) | +|---|---| +|生成一组电影级科幻写实风的4张影视分镜:|![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/e2f46207ee5e4c42b2cb988dced7cf82~tplv-goo7wpa0wc-image.image =173x) |\ +|场景1为宇航员在空间站维修飞船,空间站外部精密机械结构,深邃星空 + 银河背景,宇航员身穿细节完整的白色宇航服,手持专业维修工具,专注检修飞船外壳,中全景构图,侧逆光勾勒轮廓,冷色调科幻光影,空间站灯光点缀,失重环境,金属质感细腻,画面静谧严谨。| |\ +|场景2为:突然遇到陨石带袭击,广角史诗镜头,大量大小不一的陨石高速袭来,陨石表面纹理清晰,带燃烧尾焰,动态模糊体现速度感,陨石带压迫感拉满,飞船与空间站在画面一侧,太空黑暗深邃,光影强烈对比,紧张灾难氛围,画面冲击力十足。| |\ +|场景3为:宇航员紧急躲避,近景动态抓拍,宇航员失重状态下极速侧身躲避,肢体动作张力拉满,伸手抓握固定扶手,背景陨石飞掠而过,轻微镜头晃动增强临场感,宇航服褶皱、管线细节清晰,急促紧张,冷冽光影,主体突出不杂乱。| |\ +|场景4为:受伤后惊险逃回飞船,中近景叙事镜头,宇航员宇航服带轻微破损划痕,略显狼狈却坚毅,踉跄冲向开启的飞船舱门,舱内暖光与太空冷光形成对比,背景陨石逐渐远去,惊险逃生氛围,细节真实,情绪饱满。 | | + + +```mixin-react +return ( + + + + +); +``` + + +### 单张图生组图 + + +|提示词 |输入图 |输出(实际会输出4张图片) | +|---|---|---| +|参考这个LOGO,做一套户外运动品牌视觉设计,品牌名称为“GREEN”,包括包装袋、帽子、卡片、挂绳等。绿色视觉主色调,趣味、简约现代风格。 |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/c724450228a94a909580c0400fbf503b~tplv-goo7wpa0wc-image.image =173x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/acf7079c229a4029b4e25bc9c9697992~tplv-goo7wpa0wc-image.image =173x) | + + +```mixin-react +return ( + + + + +); +``` + + +### 多参考图生组图 + + +|提示词 |输入图1 |输入图2 |输出(实际会输出3张图片) | +|---|---|---|---| +|生成3张女孩和奶牛玩偶在游乐园开心地坐过山车的图片,涵盖早晨、中午、晚上 |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/77024d8e03f24862b066bfc385301120~tplv-goo7wpa0wc-image.image =154x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/2cbc5cf5a68d44899fc52f177fb9cf51~tplv-goo7wpa0wc-image.image =154x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/d5f8ffecd482410b8624689889f714cb~tplv-goo7wpa0wc-image.image =154x) | + + +```mixin-react +return ( + + + + +); +``` + + +## **提示词建议** + +* 建议用**简洁连贯**的自然语言写明 **主体 + 行为 + 环境**,若对画面美学有要求,可用自然语言或短语补充 **风格**、**色彩**、**光影**、**构图** 等美学元素。详情可参见 [Seedream 4.0-4.5 提示词指南](/docs/82379/1829186)。 +* 文本提示词(prompt)建议不超过300个汉字或600个英文单词。字数过多信息容易分散,模型可能因此忽略细节,只关注重点,造成图片缺失部分元素。 + + +# 进阶使用 + +## 联网搜索 +Seedream 5.0 lite 新增支持调用联网搜索工具,通过配置 tools.**type** 参数为 `web_search` 即可开启联网搜索。 + +* 开启联网搜索后,模型会根据用户的提示词自主判断是否搜索互联网内容(如商品、天气等),提升生成图片的时效性,但也会增加一定的时延。 +* 实际搜索次数可通过字段 usage.tool_usage.**web_search** 查询,如果为 **0** 表示未搜索。 + + + +|提示词 |输出 | +|---|---| +|制作一张上海未来5日的天气预报图,采用现代扁平化插画风格,清晰展示每日天气、温度和穿搭建议。 整体为横向排版,标题为“上海未来5日天气预报”,包含5个等宽的垂直卡片,从左到右依次排列。 整体风格为现代、干净、友好的扁平化矢量插画风格,线条清晰,色彩柔和。人物形象采用年轻男女的卡通插画,表情自然,姿态放松,服装细节清晰。 |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/56e0e5cc24ff40559c9e934e5d744393~tplv-goo7wpa0wc-image.image =173x) | + + +```mixin-react +return ( + + + tools = Arrays.asList(contentGenerationTool); + + GenerateImagesRequest generateRequest = GenerateImagesRequest.builder() + .model("doubao-seedream-5-0-260128") // Replace with Model ID + .prompt("制作一张上海未来5日的天气预报图,采用现代扁平化插画风格,清晰展示每日天气、温度和穿搭建议。 整体为横向排版,标题为“上海未来5日天气预报”,包含5个等宽的垂直卡片,从左到右依次排列。 整体风格为现代、干净、友好的扁平化矢量插画风格,线条清晰,色彩柔和。人物形象采用年轻男女的卡通插画,表情自然,姿态放松,服装细节清晰。") + .size("2048x2048") + .outputFormat("png") + .responseFormat("url") + .watermark(false) + .tools(tools) + .build(); + + ImagesResponse imagesResponse = service.generateImages(generateRequest); + System.out.println(imagesResponse.getData().get(0).getUrl()); + + service.shutdownExecutor(); + } +} +\`\`\` + +`}> +); +``` + + +## 流式输出 +模型支持流式图像生成,当生成完任一图片后即返回结果,让您能更快浏览到生成的图像,改善等待体验。 +通过配置 **stream** 参数为`true`,即可开启流式输出模式。 +![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/643230864ffc43a8a37ef775cd51ac30~tplv-goo7wpa0wc-image.image =2036x) + +```mixin-react +return ( + + + { + if (choice == null) return; + if ("image_generation.partial_failed".equals(choice.getType())) { + if (choice.getError() != null) { + System.err.println("Stream generate images error: " + choice.getError()); + if (choice.getError().getCode() != null && choice.getError().getCode().equals("InternalServiceError")) { + throw new RuntimeException("Server error, terminating stream."); + } + } + } + else if ("image_generation.partial_succeeded".equals(choice.getType())) { + if (choice.getError() == null && choice.getUrl() != null && !choice.getUrl().isEmpty()) { + System.out.printf("recv.Size: %s, recv.Url: %s%n", choice.getSize(), choice.getUrl()); + } + } + else if ("image_generation.completed".equals(choice.getType())) { + if (choice.getError() == null && choice.getUsage() != null) { + System.out.println("recv.Usage: " + choice.getUsage().toString()); + } + } + } + ); + service.shutdownExecutor(); + } +} +\`\`\` + +`}> + +); +``` + + +## 提示词优化控制 +通过设置 **optimize_prompt_options.mode** 参数,您可以在 `standard` 模式和 `fast` 模式之间进行选择,以根据自身对图片质量和生成速度的不同需求来优化提示词。 + +* 为平衡生成速度与图像质量,Seedream 4.0 支持将 **optimize_prompt_options.mode** 设置为 `fast` 模式以显著提升生成速度,但会在一定程度上牺牲图片质量。 +* Seedream 5.0 lite/4.5 专注于高质量图片输出,仅支持 `standard` 模式。 + + +```mixin-react +return ( + + + + +); +``` + + +## 自定义图片输出规格 +您可以配置以下参数来控制图片输出规格: + +* **size** :指定输出图像的尺寸大小。 +* **response_format** :指定生成图像的返回格式。 +* **output_format:** 指定生成图像的文件格式。 +* **watermark** :指定是否为输出图片添加水印。 + + +### 图像输出尺寸 +支持两种尺寸设置方式,不可混用。 + +* 方式 1 :指定生成图像的分辨率,并在 prompt 中用自然语言描述图片宽高比、图片形状或图片用途,最终由模型判断生成图片的大小。可选值: + * Seedream 5.0 lite:`2K`、`3K` + * Seedream 4.5:`2K`、`4K` + * Seedream 4.0:`1K`、`2K`、`4K` +* 方式2 :指定生成图像的宽高像素值。 + * 默认值:`2048x2048` + * 宽高比取值范围:[1/16, 16] + * 总像素取值范围: + * Seedream 5.0 lite:[`2560x1440=3686400`, `3072x3072x1.1025=10404496`] + * Seedream 4.5:[`2560x1440=3686400`, `4096x4096=16777216`] + * Seedream 4.0:[`1280x720=921600`, `4096x4096=16777216`] + + + +|方式1 |方式2 | +|---|---| +|```JSON|```JSON|\ +|{|{|\ +| "prompt": "生成一组共4张海报,核心为同一庭院一角的四季变迁,以统一风格展现四季独特色彩、元素与氛围", // prompt 中用自然语言描述图片宽高比、图片形状或图片用途| "prompt": "生成一组共4张连贯插画,核心为同一庭院一角的四季变迁,以统一风格展现四季独特色彩、元素与氛围",|\ +| "size": "2K" // 通过参数 size 指定生成图像的分辨率| "size": "2048x2048" // 通过参数 size 指定生成图像的宽高像素值|\ +|}|}|\ +|```|```|\ +| | | + +推荐宽高像素值: + + +| |1K |2K |3K |4K | +|---|---|---|---|---| +|Seedream 5.0 lite |暂不支持 |`1:1`:2048x2048|`1:1`:3072x3072|暂不支持 |\ +| | |`3:4`:1728x2304|`3:4`:2592x3456| |\ +| | |`4:3`:2304x1728|`4:3`:3456x2592| |\ +| | |`16:9`:2848x1600|`16:9`:4096x2304| |\ +| | |`9:16`:1600x2848|`9:16`:2304x4096| |\ +| | |`3:2`:2496x1664|`2:3`:2496x3744| |\ +| | |`2:3`:1664x2496|`3:2`:3744x2496| |\ +| | |`21:9`:3136x1344 |`21:9`:4704x2016 | | +|Seedream 4.5 |暂不支持 |`1:1`:2048x2048|暂不支持 |`1:1`:4096x4096|\ +| | |`3:4`:1728x2304| |`3:4`:3520x4704|\ +| | |`4:3`:2304x1728| |`4:3`:4704x3520|\ +| | |`16:9`:2848x1600| |`16:9`:5504x3040|\ +| | |`9:16`:1600x2848| |`9:16`:3040x5504|\ +| | |`3:2`:2496x1664| |`2:3`:3328x4992|\ +| | |`2:3`:1664x2496| |`3:2`:4992x3328|\ +| | |`21:9`:3136x1344 | |`21:9`:6240x2656 | +|Seedream 4.0 |`1:1`:1024x1024|`1:1`:2048x2048|暂不支持 |`1:1`:4096x4096|\ +| |`3:4`:864x1152|`3:4`:1728x2304| |`3:4`:3520x4704|\ +| |`4:3`:1152x864|`4:3`:2304x1728| |`4:3`:4704x3520|\ +| |`16:9`:1312x736|`16:9`:2848x1600| |`16:9`:5504x3040|\ +| |`9:16`:736x1312|`9:16`:1600x2848| |`9:16`:3040x5504|\ +| |`2:3`:832x1248|`3:2`:2496x1664| |`2:3`:3328x4992|\ +| |`3:2`:1248x832|`2:3`:1664x2496| |`3:2`:4992x3328|\ +| |`21:9`:1568x672 |`21:9`:3136x1344 | |`21:9`:6240x2656 | + + +### 图像输出方式 +通过设置 **response_format** 参数,可以指定生成图像的返回方式: + +* `url`:返回图片下载链接。 +* `b64_json`:以 Base64 编码字符串的 JSON 格式返回图像数据。 + +```JSON +{ + "response_format": "url" +} +``` + + +### 图像文件格式 +Seedream 4.5/4.0 生成的图像格式默认为`jpeg`,不支持自定义设置。 +Seedream 5.0 lite 可通过设置 **output_format** 参数,指定生成图像文件的格式: + +* `png` +* `jpeg` + +```JSON +{ + "output_format": "png" +} +``` + + +### 图像中添加水印 +通过设置 **watermark** 参数,来控制是否在生成的图片中添加水印。 + +* `false`:不添加水印。 +* `true`:在图片右下角添加“AI生成”字样的水印标识。 + +```JSON +{ + "watermark": true +} +``` + + +# 使用限制 +**SDK 版本升级** +为保证模型功能的正常使用,请务必升级至最新 SDK 版本。相关步骤可参考 [安装及升级 SDK](/docs/82379/1541595)。 +**图片传入限制** + +* 图片格式:jpeg、png、webp、bmp、tiff、gif +* 宽高比(宽/高)范围:[1/16, 16] +* 宽高长度(px) \> 14 +* 大小:不超过 10 MB +* 总像素:不超过 `6000x6000=36000000` px (对单张图宽度和高度的像素乘积限制,而不是对宽度或高度的单独值进行限制) +* 最多支持传入 14 张参考图 + +**保存时间** +任务数据(如任务状态、图片URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图片。 +**限流说明** + +* RPM 限流:账号下同模型(区分模型版本)每分钟生成图片数量上限。若超过该限制,生成图片时会报错。 +* 不同模型的限制值不同,详见 [图片生成能力](/docs/82379/1330310#d3e5e0eb)。 + + +# 附:故事书/连环画制作 +[火山方舟大模型体验中心](https://www.volcengine.com/experience/ark?mode=vision&model=doubao-seedream-4-0-250828) 提供了故事书和连环画功能,该功能结合了 doubao\-seed\-1.6 模型和 doubao\-seedream\-4.0 模型,可实现一句话生成动漫、连环画、故事书,满足用户多样化的创作需求。 +连环画的实现过程与故事书类似,本文以故事书为例,为您介绍生成故事书的工作流和技术实现步骤,方便您在本地快速复现。 +![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/d590e440ff7447feaed8fa8f4d91e746~tplv-goo7wpa0wc-image.image =1712x) + +## 工作流 +故事书生成的工作流如下: +![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/0177dd5714aa4750aebc27bbc02ea9f6~tplv-goo7wpa0wc-image.image =2164x) + +## 技术实现 + +1. 根据用户提供的提示词和参考图,调用 doubao\-seed\-1.6 模型,进行故事创作 \> 故事分镜拆解 \> 生成分镜的文案和画面描述 \> 生成书名 \> 生成故事总结,并汇总成 JSON 格式输出。 + +System Prompt 如下: +```Plain +# 角色 + +你是一位**绘本创作大师**。 + +## 任务 + +贴合用户指定的**读者群(儿童/青少年/成人/全年龄)**,创作**情节线性连贯的、生动有趣的、充满情绪价值和温度的、有情感共鸣的、分镜-文案-画面严格顺序对应的绘本内容**: +- 核心约束:**分镜拆分→文案(scenes)→画面描述(scenes_detail)必须1:1顺序绑定**,从故事开头到结尾,像「放电影」一样按时间线推进,绝无错位。 + +## 工作流程 + +1. 充分理解用户诉求。 优先按照用户的创作细节要求执行(如果有) +2. **故事构思:** 创作一个能够精准回应用户诉求、提供情感慰藉的故事脉络。整个故事必须围绕“共情”和“情绪价值”展开。 +3. **分镜结构与数量:** + * 将故事浓缩成 **5~10** 个关键分镜,最多10个(不能超过10个)。 + * 必须遵循清晰的叙事弧线:开端 → 发展 → 高潮 → 结局。 +4. **文案与画面 (一一对应):** + * **文案 ("scenes"字段):** 为每个分镜创作具备情感穿透力的文案。文案必须与画面描述紧密贴合,共同服务于情绪的传递。**禁止在文案中使用任何英文引号 ("")**。不能超过10个。 + * **画面 ("scenes_detail"字段):** 为每个分镜构思详细的画面。画风必须贴合用户诉求和故事氛围。描述需包含构图、光影、色彩、角色神态等关键视觉要素,达到可直接用于图片生成的标准。 +5. **书名 ("title"字段):** + * 构思一个简洁、好记、有创意的书名。 + * 书名必须能巧妙地概括故事精髓,并能瞬间“戳中”目标用户的情绪共鸣点。 +6. **故事总结 ("summary"字段):** + * 创作一句**不超过30个汉字**的总结。 + * 总结需高度凝练故事的核心思想与情感价值。 +7. 整合输出:将所有内容按指定 JSON 格式整理输出。 + +## 安全限制 +生成的内容必须严格遵守以下规定: +1. **禁止暴力与血腥:** 不得包含任何详细的暴力、伤害、血腥或令人不适的画面描述。 +2. **禁止色情内容:** 不得包含任何色情、性暗示或不适宜的裸露内容。 +3. **禁止仇恨与歧视:** 不得包含针对任何群体(基于种族、宗教、性别、性取向等)的仇恨、歧视或攻击性言论。 +4. **禁止违法与危险行为:** 不得描绘或鼓励任何非法活动、自残或危险行为。 +5. **确保普遍适宜性:** 整体内容应保持在社会普遍接受的艺术创作范围内,避免极端争议性话题。 + +## 输出格式要求 +整理成以下JSON格式,scenes 和 scenes_detail 要与分镜保持顺序一致,一一对应,最多10个(不能超过10个): +{ + "title": "书名", + "summary": "30字内的总结", + "scenes": [ + "分镜1的文案,用50字篇幅传递情绪和情感,引发读者共鸣,语言风格需符合设定。", + "分镜2的文案" + ], + "scenes_detail": [ + "图片1:这是第一页的画面描述。必须以'图片'+序号开头。要有强烈的视觉感,详细描述构图(如特写、远景)、光影、色彩、角色表情、动作和环境细节,符合生图提示词的要求。", + "图片2:" + ] +} +``` + + +2. 提取返回结果 JSON 中的 scenes_detail 字段,作为图片生成的 Prompt 。 +3. 处理图片生成的 Prompt: + 1. 将数组转化成字符串 + 2. 在 prompt 末尾补充"最后,为故事书创作一个封面。 再检查所有图片,去除图片中的文字"。 + 3. 在 prompt 开头添加用户输入的提示词。 +4. 根据图片生成的 Prompt 和用户提供的参考图,调用 doubao\-seedream\-4.0 模型的生成组图能力,为故事的所有分镜文案生成配图。 +5. 按照顺序拼装图片和文字即可得到故事书内容 ,用户按需进行展示即可。 + + + diff --git a/src/router/index.js b/src/router/index.js index 7b7dd24..b3f7945 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -9,6 +9,7 @@ import ProblemSolving from '../views/ProblemSolving.vue' import QuestionGenerator from '../views/QuestionGenerator.vue' import QuestionVariant from '../views/QuestionVariant.vue' import AudioToText from '../views/AudioToText.vue' +import QuestionExplanation from '../views/QuestionExplanation.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -63,6 +64,11 @@ const router = createRouter({ name: 'audio-to-text', component: AudioToText }, + { + path: '/question-explanation', + name: 'question-explanation', + component: QuestionExplanation + }, ] }) diff --git a/src/utils/videoComposer.js b/src/utils/videoComposer.js new file mode 100644 index 0000000..b49472d --- /dev/null +++ b/src/utils/videoComposer.js @@ -0,0 +1,139 @@ +/** + * 视频合成模块 - 后端 FFmpeg 方案 + */ + +// 后端 API 地址 +const API_BASE = '/api/video'; + +/** + * 合成视频 - 调用后端 API + * @param {Array} slides 讲解点数组,每个包含 imageUrl 和 audioUrl + * @param {Object} options 配置选项 + * @param {Function} onProgress 进度回调 { stage, current, total, message } + * @returns {Promise} 视频Blob + */ +export async function composeVideo(slides, options = {}, onProgress = null) { + const { + width = 1920, + height = 1080, + } = options; + + // 过滤有效的 slide,并使用原始完整 URL + const validSlides = slides + .filter(s => (s.originalImageUrl || s.imageUrl) && (s.originalAudioUrl || s.audioUrl)) + .map(s => ({ + // 优先使用原始完整 URL,否则使用当前 URL + imageUrl: s.originalImageUrl || s.imageUrl, + audioUrl: s.originalAudioUrl || s.audioUrl, + })); + + if (validSlides.length === 0) { + throw new Error('没有有效的讲解内容'); + } + + // 检查 URL 格式 + const invalidUrls = validSlides.filter(s => + !s.imageUrl.startsWith('http') && !s.imageUrl.startsWith('data:') + ); + if (invalidUrls.length > 0) { + console.warn('部分 URL 格式不正确:', invalidUrls); + } + + // 阶段1: 准备请求 + if (onProgress) { + onProgress({ + stage: 'preparing', + current: 0, + total: validSlides.length, + message: '正在准备视频合成请求...', + }); + } + + try { + // 阶段2: 发送请求到后端 + if (onProgress) { + onProgress({ + stage: 'composing', + current: 1, + total: validSlides.length, + message: `服务器正在合成视频 (${validSlides.length} 个片段)...`, + }); + } + + const response = await fetch(`${API_BASE}/compose`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + slides: validSlides, + width, + height, + }), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: '请求失败' })); + throw new Error(error.message || `服务器错误: ${response.status}`); + } + + // 阶段3: 接收视频 + if (onProgress) { + onProgress({ + stage: 'merging', + current: validSlides.length, + total: validSlides.length, + message: '正在接收合成后的视频...', + }); + } + + const videoBlob = await response.blob(); + + if (onProgress) { + onProgress({ + stage: 'completed', + current: 1, + total: 1, + message: '视频合成完成!', + }); + } + + return videoBlob; + } catch (error) { + console.error('视频合成失败:', error); + throw new Error(`视频合成失败: ${error.message}`); + } +} + +/** + * 下载视频 + * @param {Blob} blob 视频Blob + * @param {string} filename 文件名 + */ +export function downloadVideo(blob, filename = 'question-explanation.mp4') { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +/** + * 创建预览URL + * @param {Blob} blob 视频Blob + * @returns {string} Blob URL + */ +export function createPreviewUrl(blob) { + return URL.createObjectURL(blob); +} + +/** + * 释放预览URL + * @param {string} url Blob URL + */ +export function revokePreviewUrl(url) { + URL.revokeObjectURL(url); +} diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue index dd4b01c..58575b5 100644 --- a/src/views/HomePage.vue +++ b/src/views/HomePage.vue @@ -85,6 +85,14 @@ const features = ref([ icon: "file-audio", route: "/audio-to-text", }, + { + id: 11, + title: "AI试题讲解生成", + desc: "智能分析试题复杂度,自动生成图文并茂的幻灯片式讲解,每张图片配语音讲解,让解题过程清晰易懂。", + class: "card-11", + icon: "presentation", + route: "/question-explanation", + }, ]); // Hover effect for glassmorphism glare @@ -319,6 +327,22 @@ onUnmounted(() => { d="M11.25 12.75l2.25-1.5v4.5l-2.25-1.5v-1.5z" /> + + + + +

{{ feature.title }}

diff --git a/src/views/QuestionExplanation.vue b/src/views/QuestionExplanation.vue new file mode 100644 index 0000000..22e235f --- /dev/null +++ b/src/views/QuestionExplanation.vue @@ -0,0 +1,1847 @@ + + + + + diff --git a/vite.config.js b/vite.config.js index 5ee9f12..a5e6c7b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -17,11 +17,12 @@ export default defineConfig({ }, server: { host: '0.0.0.0', - headers: { - 'Cross-Origin-Opener-Policy': 'same-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', - }, proxy: { + // 后端 API 代理 + '/api': { + target: 'http://localhost:3001', + changeOrigin: true, + }, '/tts-api': { target: 'https://openspeech.bytedance.com', changeOrigin: true, @@ -51,6 +52,29 @@ export default defineConfig({ proxyReq.setHeader('X-Api-Connect-Id', crypto.randomUUID()) }) } + }, + // 阿里云OSS音频代理:解决COEP策略问题(支持多个OSS域名) + '/oss-audio': { + target: 'https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/oss-audio/, ''), + configure: (proxy) => { + proxy.on('proxyRes', (proxyRes) => { + // 添加CORS头以符合COEP要求 + proxyRes.headers['access-control-allow-origin'] = '*' + }) + } + }, + // 阿里云OSS图片代理(支持多个OSS域名) + '/oss-image': { + target: 'https://dashscope-7c2c.oss-accelerate.aliyuncs.com', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/oss-image/, ''), + configure: (proxy) => { + proxy.on('proxyRes', (proxyRes) => { + proxyRes.headers['access-control-allow-origin'] = '*' + }) + } } } }