feat 添加AI试题讲解生成

This commit is contained in:
cc 2026-03-30 21:07:44 +08:00
parent 081630f732
commit 9bc5332033
18 changed files with 8000 additions and 7 deletions

View File

@ -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
- 用途:分析试题复杂度、拆分讲解点、生成讲解文本和图片提示词
- APIhttps://ark.cn-beijing.volces.com/api/v3/chat/completions
- 认证:使用 DOUBAO_KEY
**图片生成模型**doubao-seedream-5-0-260128
- 用途:批量生成讲解图片(支持组图生成)
- APIhttps://ark.cn-beijing.volces.com/api/v3/images/generations
- 认证:使用 DOUBAO_KEY
- 特性sequential_image_generation="auto"max_images=8
**语音合成模型**qwen3-tts-flash
- 用途:为讲解文本生成音频
- APIhttps://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效果 + 点击反馈
- 平滑过渡:图片切换使用淡入淡出动画

View File

@ -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. **视频预览区**:合成完成后显示视频播放器和下载按钮

View File

@ -5,7 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI 英语学习辅助平台</title> <title>AI 英语学习辅助平台</title>
<script defer src="https://umami.23544.com/script.js" data-website-id="758c5bd3-4189-4dcb-a2a8-21531c92dcd8"></script> <!-- COEP 策略下暂时禁用 umami需要服务端支持 CORP 响应头) -->
<!-- <script defer src="https://umami.23544.com/script.js" data-website-id="758c5bd3-4189-4dcb-a2a8-21531c92dcd8"></script> -->
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -9,8 +9,6 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2",
"axios": "^1.13.6", "axios": "^1.13.6",
"marked": "^17.0.5", "marked": "^17.0.5",
"pako": "^2.1.0", "pako": "^2.1.0",

73
server/README.md Normal file
View File

@ -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`

33
server/index.js Normal file
View File

@ -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`);
});

1020
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
server/package.json Normal file
View File

@ -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"
}
}

284
server/routes/video.js Normal file
View File

@ -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;

View File

@ -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 = "试题讲解_";

1189
src/md/ali-audio-tts.md Normal file

File diff suppressed because it is too large Load Diff

722
src/md/ali-image.md Normal file
View File

@ -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 Keyhttps://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 Keyhttps://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<String, Object> 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 Keyhttps://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 Keyhttps://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; }

2150
src/md/doubao_image.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ import ProblemSolving from '../views/ProblemSolving.vue'
import QuestionGenerator from '../views/QuestionGenerator.vue' import QuestionGenerator from '../views/QuestionGenerator.vue'
import QuestionVariant from '../views/QuestionVariant.vue' import QuestionVariant from '../views/QuestionVariant.vue'
import AudioToText from '../views/AudioToText.vue' import AudioToText from '../views/AudioToText.vue'
import QuestionExplanation from '../views/QuestionExplanation.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -63,6 +64,11 @@ const router = createRouter({
name: 'audio-to-text', name: 'audio-to-text',
component: AudioToText component: AudioToText
}, },
{
path: '/question-explanation',
name: 'question-explanation',
component: QuestionExplanation
},
] ]
}) })

139
src/utils/videoComposer.js Normal file
View File

@ -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>} 视频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);
}

View File

@ -85,6 +85,14 @@ const features = ref([
icon: "file-audio", icon: "file-audio",
route: "/audio-to-text", route: "/audio-to-text",
}, },
{
id: 11,
title: "AI试题讲解生成",
desc: "智能分析试题复杂度,自动生成图文并茂的幻灯片式讲解,每张图片配语音讲解,让解题过程清晰易懂。",
class: "card-11",
icon: "presentation",
route: "/question-explanation",
},
]); ]);
// Hover effect for glassmorphism glare // 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" d="M11.25 12.75l2.25-1.5v4.5l-2.25-1.5v-1.5z"
/> />
</svg> </svg>
<!-- Presentation Icon (Slides/Presentation) -->
<svg
v-else-if="feature.icon === 'presentation'"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5m.75-9l3-3 2.148 2.148A12.061 12.061 0 0116.5 7.605"
/>
</svg>
</div> </div>
<h2 class="card-title">{{ feature.title }}</h2> <h2 class="card-title">{{ feature.title }}</h2>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@ -17,11 +17,12 @@ export default defineConfig({
}, },
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
proxy: { proxy: {
// 后端 API 代理
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
'/tts-api': { '/tts-api': {
target: 'https://openspeech.bytedance.com', target: 'https://openspeech.bytedance.com',
changeOrigin: true, changeOrigin: true,
@ -51,6 +52,29 @@ export default defineConfig({
proxyReq.setHeader('X-Api-Connect-Id', crypto.randomUUID()) 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'] = '*'
})
}
} }
} }
} }