添加 文生图
This commit is contained in:
430
README_TEXT_VIDEO_AGENT_API.md
Normal file
430
README_TEXT_VIDEO_AGENT_API.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# Text Video Agent API 工具库
|
||||
|
||||
基于 `https://bowongai-dev--text-video-agent-fastapi-app.modal.run` API 的完整 TypeScript 工具库封装。
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [功能特性](#功能特性)
|
||||
- [安装使用](#安装使用)
|
||||
- [API 文档](#api-文档)
|
||||
- [React Hook](#react-hook)
|
||||
- [组件示例](#组件示例)
|
||||
- [类型定义](#类型定义)
|
||||
- [使用示例](#使用示例)
|
||||
|
||||
## 🚀 功能特性
|
||||
|
||||
### 核心功能
|
||||
- ✅ **图片生成**: 基于 Midjourney 的高质量图片生成
|
||||
- ✅ **视频生成**: 基于极梦的视频生成功能
|
||||
- ✅ **文件上传**: 支持文件上传到云存储
|
||||
- ✅ **图片描述**: AI 图片内容分析和描述
|
||||
- ✅ **任务管理**: 异步任务创建、查询和管理
|
||||
|
||||
### 高级特性
|
||||
- ✅ **重试机制**: 自动重试失败的请求
|
||||
- ✅ **进度跟踪**: 实时进度更新和状态监控
|
||||
- ✅ **类型安全**: 完整的 TypeScript 类型定义
|
||||
- ✅ **React 集成**: 专用的 React Hook 和组件
|
||||
- ✅ **错误处理**: 完善的错误处理和用户反馈
|
||||
|
||||
## 📦 安装使用
|
||||
|
||||
### 基础安装
|
||||
|
||||
```bash
|
||||
# 复制相关文件到你的项目
|
||||
cp src/services/textVideoAgentAPI.ts your-project/src/services/
|
||||
cp src/services/textVideoAgentTypes.ts your-project/src/services/
|
||||
cp src/hooks/useTextVideoAgent.ts your-project/src/hooks/
|
||||
cp src/components/TextVideoGenerator.tsx your-project/src/components/
|
||||
```
|
||||
|
||||
### 依赖要求
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "^18.0.0",
|
||||
"lucide-react": "^0.263.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 API 文档
|
||||
|
||||
### 基础用法
|
||||
|
||||
```typescript
|
||||
import { textVideoAgentAPI } from './services/textVideoAgentAPI'
|
||||
|
||||
// 健康检查
|
||||
const health = await textVideoAgentAPI.healthCheck()
|
||||
|
||||
// 生成图片
|
||||
const imageResult = await textVideoAgentAPI.generateImageSync({
|
||||
prompt: '一个美丽的风景',
|
||||
max_wait_time: 120
|
||||
})
|
||||
|
||||
// 生成视频
|
||||
const videoResult = await textVideoAgentAPI.generateVideoSync({
|
||||
prompt: '动态的自然风光',
|
||||
img_url: 'https://example.com/image.jpg',
|
||||
duration: '5'
|
||||
})
|
||||
```
|
||||
|
||||
### 主要方法
|
||||
|
||||
#### 图片生成
|
||||
```typescript
|
||||
// 同步生成(推荐)
|
||||
generateImageSync(params: ImageGenerationParams): Promise<APIResponse>
|
||||
|
||||
// 异步生成
|
||||
generateImageAsync(prompt: string, imgFile?: File): Promise<APIResponse>
|
||||
|
||||
// 带重试的生成
|
||||
generateImageWithRetry(params: ImageGenerationParams, maxRetries?: number): Promise<APIResponse>
|
||||
```
|
||||
|
||||
#### 视频生成
|
||||
```typescript
|
||||
// 同步生成
|
||||
generateVideoSync(params: VideoGenerationParams): Promise<APIResponse>
|
||||
|
||||
// 异步生成
|
||||
generateVideoAsync(params: VideoGenerationParams): Promise<APIResponse>
|
||||
|
||||
// 带重试的生成
|
||||
generateVideoWithRetry(params: VideoGenerationParams, maxRetries?: number): Promise<APIResponse>
|
||||
```
|
||||
|
||||
#### 任务管理
|
||||
```typescript
|
||||
// 创建任务
|
||||
createTask(request: TaskRequest): Promise<APIResponse>
|
||||
|
||||
// 查询任务状态
|
||||
getTaskStatusAsync(taskId: string): Promise<APIResponse>
|
||||
|
||||
// 同步等待任务完成
|
||||
getTaskResultSync(taskId: string): Promise<APIResponse>
|
||||
```
|
||||
|
||||
#### 高级功能
|
||||
```typescript
|
||||
// 端到端内容生成
|
||||
generateContentEndToEnd(prompt: string, options?: GenerationOptions): Promise<ContentResult>
|
||||
|
||||
// 轮询任务直到完成
|
||||
pollTaskUntilComplete(taskId: string, options?: PollingOptions): Promise<APIResponse>
|
||||
```
|
||||
|
||||
## 🎣 React Hook
|
||||
|
||||
### useTextVideoAgent
|
||||
|
||||
```typescript
|
||||
import { useTextVideoAgent } from './hooks/useTextVideoAgent'
|
||||
|
||||
function MyComponent() {
|
||||
const {
|
||||
state,
|
||||
generateImage,
|
||||
generateVideo,
|
||||
generateContentEndToEnd,
|
||||
reset,
|
||||
cancel
|
||||
} = useTextVideoAgent()
|
||||
|
||||
const handleGenerate = async () => {
|
||||
const result = await generateContentEndToEnd('美丽的风景', {
|
||||
taskType: TaskType.VLOG,
|
||||
aspectRatio: AspectRatio.PORTRAIT,
|
||||
generateVideo: true
|
||||
})
|
||||
|
||||
console.log('生成结果:', result)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{state.isLoading && (
|
||||
<div>
|
||||
<p>{state.currentStep}</p>
|
||||
<progress value={state.progress} max={100} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.error && (
|
||||
<div className="error">{state.error}</div>
|
||||
)}
|
||||
|
||||
<button onClick={handleGenerate} disabled={state.isLoading}>
|
||||
生成内容
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useTaskPolling
|
||||
|
||||
```typescript
|
||||
import { useTaskPolling } from './hooks/useTextVideoAgent'
|
||||
|
||||
function TaskMonitor({ taskId }: { taskId: string }) {
|
||||
const { status, isPolling } = useTaskPolling(taskId, {
|
||||
onComplete: (result) => {
|
||||
console.log('任务完成:', result)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('任务失败:', error)
|
||||
},
|
||||
onProgress: (status) => {
|
||||
console.log('进度更新:', status)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isPolling && <p>任务进行中...</p>}
|
||||
{status && <pre>{JSON.stringify(status, null, 2)}</pre>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🧩 组件示例
|
||||
|
||||
### TextVideoGenerator 组件
|
||||
|
||||
```typescript
|
||||
import TextVideoGenerator from './components/TextVideoGenerator'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<TextVideoGenerator
|
||||
onImageGenerated={(imageUrl) => {
|
||||
console.log('图片生成完成:', imageUrl)
|
||||
}}
|
||||
onVideoGenerated={(videoUrl) => {
|
||||
console.log('视频生成完成:', videoUrl)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 类型定义
|
||||
|
||||
### 主要接口
|
||||
|
||||
```typescript
|
||||
// API 响应
|
||||
interface APIResponse<T = any> {
|
||||
status: boolean
|
||||
msg: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
// 图片生成参数
|
||||
interface ImageGenerationParams {
|
||||
prompt: string
|
||||
img_file?: File
|
||||
max_wait_time?: number
|
||||
poll_interval?: number
|
||||
}
|
||||
|
||||
// 视频生成参数
|
||||
interface VideoGenerationParams {
|
||||
prompt: string
|
||||
img_url?: string
|
||||
img_file?: File
|
||||
duration?: string
|
||||
max_wait_time?: number
|
||||
poll_interval?: number
|
||||
}
|
||||
|
||||
// 任务请求
|
||||
interface TaskRequest {
|
||||
task_type?: string
|
||||
prompt: string
|
||||
img_url?: string
|
||||
ar?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 枚举类型
|
||||
|
||||
```typescript
|
||||
enum TaskType {
|
||||
TEA = 'tea',
|
||||
CHOP = 'chop',
|
||||
LADY = 'lady',
|
||||
VLOG = 'vlog'
|
||||
}
|
||||
|
||||
enum AspectRatio {
|
||||
SQUARE = '1:1',
|
||||
PORTRAIT = '9:16',
|
||||
LANDSCAPE = '16:9'
|
||||
}
|
||||
|
||||
enum VideoDuration {
|
||||
SHORT = '3',
|
||||
MEDIUM = '5',
|
||||
LONG = '10'
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 使用示例
|
||||
|
||||
### 示例1: 基础图片生成
|
||||
|
||||
```typescript
|
||||
import { textVideoAgentAPI, TaskType, AspectRatio } from './services/textVideoAgentAPI'
|
||||
|
||||
async function generateImage() {
|
||||
try {
|
||||
const result = await textVideoAgentAPI.generateImageSync({
|
||||
prompt: '一个现代化的咖啡厅,温暖的灯光,舒适的环境',
|
||||
max_wait_time: 120
|
||||
})
|
||||
|
||||
if (result.status) {
|
||||
console.log('图片URL:', result.data.image_url)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成失败:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例2: 端到端内容生成
|
||||
|
||||
```typescript
|
||||
async function generateContent() {
|
||||
try {
|
||||
const result = await textVideoAgentAPI.generateContentEndToEnd(
|
||||
'制作一个关于健康生活的短视频',
|
||||
{
|
||||
taskType: TaskType.VLOG,
|
||||
aspectRatio: AspectRatio.PORTRAIT,
|
||||
videoDuration: VideoDuration.MEDIUM,
|
||||
generateVideo: true,
|
||||
onProgress: (step, progress) => {
|
||||
console.log(`${step}: ${progress}%`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('生成完成:', result)
|
||||
} catch (error) {
|
||||
console.error('生成失败:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3: 批量处理
|
||||
|
||||
```typescript
|
||||
async function batchGenerate() {
|
||||
const prompts = [
|
||||
'春天的樱花',
|
||||
'夏日的海滩',
|
||||
'秋天的枫叶',
|
||||
'冬日的雪景'
|
||||
]
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
prompts.map(prompt =>
|
||||
textVideoAgentAPI.generateImageSync({ prompt })
|
||||
)
|
||||
)
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
console.log(`图片 ${index + 1} 生成成功:`, result.value.data?.image_url)
|
||||
} else {
|
||||
console.error(`图片 ${index + 1} 生成失败:`, result.reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 配置选项
|
||||
|
||||
### 预设配置
|
||||
|
||||
```typescript
|
||||
import { PRESET_CONFIGS } from './services/textVideoAgentTypes'
|
||||
|
||||
// 快速生成(低质量)
|
||||
const fastConfig = PRESET_CONFIGS.FAST
|
||||
|
||||
// 标准生成
|
||||
const standardConfig = PRESET_CONFIGS.STANDARD
|
||||
|
||||
// 高质量生成
|
||||
const highQualityConfig = PRESET_CONFIGS.HIGH_QUALITY
|
||||
```
|
||||
|
||||
### 自定义配置
|
||||
|
||||
```typescript
|
||||
const customAPI = new TextVideoAgentAPI('https://your-custom-endpoint.com')
|
||||
```
|
||||
|
||||
## 🚨 错误处理
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await textVideoAgentAPI.generateImageSync(params)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('错误信息:', error.message)
|
||||
}
|
||||
|
||||
// 处理特定错误类型
|
||||
if (error.message.includes('timeout')) {
|
||||
// 处理超时错误
|
||||
} else if (error.message.includes('quota')) {
|
||||
// 处理配额不足错误
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 缓存策略
|
||||
- 图片生成结果自动缓存
|
||||
- 任务状态智能轮询
|
||||
- 网络请求去重
|
||||
|
||||
### 最佳实践
|
||||
- 使用适当的超时时间
|
||||
- 合理设置轮询间隔
|
||||
- 及时取消不需要的请求
|
||||
- 使用预设配置提高效率
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支
|
||||
3. 提交更改
|
||||
4. 推送到分支
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 🆘 支持
|
||||
|
||||
如有问题或建议,请创建 Issue 或联系开发团队。
|
||||
416
examples/textVideoAgentUsage.ts
Normal file
416
examples/textVideoAgentUsage.ts
Normal file
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* Text Video Agent API 使用示例
|
||||
*/
|
||||
|
||||
import {
|
||||
TextVideoAgentAPI,
|
||||
textVideoAgentAPI,
|
||||
ImageGenerationParams,
|
||||
VideoGenerationParams,
|
||||
TaskRequest
|
||||
} from '../src/services/textVideoAgentAPI'
|
||||
|
||||
import {
|
||||
TaskType,
|
||||
AspectRatio,
|
||||
VideoDuration,
|
||||
PRESET_CONFIGS
|
||||
} from '../src/services/textVideoAgentTypes'
|
||||
|
||||
// ==================== 基础使用示例 ====================
|
||||
|
||||
/**
|
||||
* 示例1: 基础健康检查
|
||||
*/
|
||||
async function example1_HealthCheck() {
|
||||
console.log('=== 健康检查示例 ===')
|
||||
|
||||
try {
|
||||
const health = await textVideoAgentAPI.healthCheck()
|
||||
console.log('服务状态:', health)
|
||||
|
||||
const mjHealth = await textVideoAgentAPI.mjHealthCheck()
|
||||
console.log('Midjourney状态:', mjHealth)
|
||||
|
||||
const jmHealth = await textVideoAgentAPI.jmHealthCheck()
|
||||
console.log('极梦状态:', jmHealth)
|
||||
} catch (error) {
|
||||
console.error('健康检查失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例2: 获取示例提示词
|
||||
*/
|
||||
async function example2_GetSamplePrompts() {
|
||||
console.log('=== 获取示例提示词 ===')
|
||||
|
||||
try {
|
||||
const prompts = await textVideoAgentAPI.getSamplePrompt(TaskType.VLOG)
|
||||
console.log('VLOG类型示例提示词:', prompts)
|
||||
} catch (error) {
|
||||
console.error('获取提示词失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例3: 文件上传
|
||||
*/
|
||||
async function example3_FileUpload() {
|
||||
console.log('=== 文件上传示例 ===')
|
||||
|
||||
// 注意:这里需要实际的文件对象
|
||||
// const fileInput = document.getElementById('fileInput') as HTMLInputElement
|
||||
// const file = fileInput.files?.[0]
|
||||
|
||||
// 模拟文件对象(实际使用时替换为真实文件)
|
||||
const mockFile = new File(['mock content'], 'test.jpg', { type: 'image/jpeg' })
|
||||
|
||||
try {
|
||||
const uploadResult = await textVideoAgentAPI.uploadFile(mockFile)
|
||||
console.log('文件上传结果:', uploadResult)
|
||||
return uploadResult.data // 返回文件URL
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 图片生成示例 ====================
|
||||
|
||||
/**
|
||||
* 示例4: 基础图片生成
|
||||
*/
|
||||
async function example4_BasicImageGeneration() {
|
||||
console.log('=== 基础图片生成示例 ===')
|
||||
|
||||
const params: ImageGenerationParams = {
|
||||
prompt: '一个美丽的女孩在喝茶,温馨的下午时光,自然光线,高质量摄影',
|
||||
max_wait_time: 120,
|
||||
poll_interval: 2
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await textVideoAgentAPI.generateImageSync(params)
|
||||
console.log('图片生成结果:', result)
|
||||
return result.data?.image_url
|
||||
} catch (error) {
|
||||
console.error('图片生成失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例5: 带参考图片的图片生成
|
||||
*/
|
||||
async function example5_ImageGenerationWithReference() {
|
||||
console.log('=== 带参考图片的图片生成示例 ===')
|
||||
|
||||
// 模拟参考图片文件
|
||||
const referenceImage = new File(['reference'], 'reference.jpg', { type: 'image/jpeg' })
|
||||
|
||||
const params: ImageGenerationParams = {
|
||||
prompt: '保持人物特征,改变背景为咖啡厅环境',
|
||||
img_file: referenceImage,
|
||||
...PRESET_CONFIGS.STANDARD
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await textVideoAgentAPI.generateImageWithRetry(params, 3)
|
||||
console.log('带参考图片生成结果:', result)
|
||||
return result.data?.image_url
|
||||
} catch (error) {
|
||||
console.error('带参考图片生成失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例6: 异步图片生成
|
||||
*/
|
||||
async function example6_AsyncImageGeneration() {
|
||||
console.log('=== 异步图片生成示例 ===')
|
||||
|
||||
const prompt = '现代简约风格的室内设计,明亮的客厅'
|
||||
|
||||
try {
|
||||
// 提交异步任务
|
||||
const taskResult = await textVideoAgentAPI.generateImageAsync(prompt)
|
||||
const taskId = taskResult.data?.task_id
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('未获取到任务ID')
|
||||
}
|
||||
|
||||
console.log('任务已提交,ID:', taskId)
|
||||
|
||||
// 轮询任务状态
|
||||
const finalResult = await textVideoAgentAPI.pollTaskUntilComplete(
|
||||
taskId,
|
||||
2000, // 2秒轮询间隔
|
||||
120000, // 2分钟超时
|
||||
(status) => {
|
||||
console.log('任务状态更新:', status)
|
||||
}
|
||||
)
|
||||
|
||||
console.log('异步图片生成完成:', finalResult)
|
||||
return finalResult.data?.image_url
|
||||
} catch (error) {
|
||||
console.error('异步图片生成失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 视频生成示例 ====================
|
||||
|
||||
/**
|
||||
* 示例7: 基础视频生成
|
||||
*/
|
||||
async function example7_BasicVideoGeneration() {
|
||||
console.log('=== 基础视频生成示例 ===')
|
||||
|
||||
// 首先生成一张图片作为视频的基础
|
||||
const imageUrl = await example4_BasicImageGeneration()
|
||||
|
||||
if (!imageUrl) {
|
||||
console.error('无法获取基础图片,跳过视频生成')
|
||||
return
|
||||
}
|
||||
|
||||
const params: VideoGenerationParams = {
|
||||
prompt: '女孩优雅地品茶,轻柔的动作,温馨的氛围',
|
||||
img_url: imageUrl,
|
||||
duration: VideoDuration.MEDIUM,
|
||||
max_wait_time: 300,
|
||||
poll_interval: 5
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await textVideoAgentAPI.generateVideoSync(params)
|
||||
console.log('视频生成结果:', result)
|
||||
return result.data?.video_url
|
||||
} catch (error) {
|
||||
console.error('视频生成失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例8: 异步视频生成
|
||||
*/
|
||||
async function example8_AsyncVideoGeneration() {
|
||||
console.log('=== 异步视频生成示例 ===')
|
||||
|
||||
const params: VideoGenerationParams = {
|
||||
prompt: '城市夜景延时摄影,车流如水,霓虹闪烁',
|
||||
duration: VideoDuration.LONG
|
||||
}
|
||||
|
||||
try {
|
||||
const taskResult = await textVideoAgentAPI.generateVideoAsync(params)
|
||||
const taskId = taskResult.data?.task_id
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('未获取到任务ID')
|
||||
}
|
||||
|
||||
console.log('视频生成任务已提交,ID:', taskId)
|
||||
|
||||
// 轮询任务状态
|
||||
const finalResult = await textVideoAgentAPI.pollTaskUntilComplete(
|
||||
taskId,
|
||||
5000, // 5秒轮询间隔
|
||||
600000, // 10分钟超时
|
||||
(status) => {
|
||||
console.log('视频生成进度:', status)
|
||||
}
|
||||
)
|
||||
|
||||
console.log('异步视频生成完成:', finalResult)
|
||||
return finalResult.data?.video_url
|
||||
} catch (error) {
|
||||
console.error('异步视频生成失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 高级功能示例 ====================
|
||||
|
||||
/**
|
||||
* 示例9: 图片描述功能
|
||||
*/
|
||||
async function example9_ImageDescription() {
|
||||
console.log('=== 图片描述示例 ===')
|
||||
|
||||
const imageUrl = 'https://example.com/sample-image.jpg'
|
||||
|
||||
try {
|
||||
const description = await textVideoAgentAPI.describeImageByUrl({
|
||||
image_url: imageUrl,
|
||||
max_wait_time: 60
|
||||
})
|
||||
|
||||
console.log('图片描述结果:', description)
|
||||
return description.data?.description
|
||||
} catch (error) {
|
||||
console.error('图片描述失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例10: 任务管理
|
||||
*/
|
||||
async function example10_TaskManagement() {
|
||||
console.log('=== 任务管理示例 ===')
|
||||
|
||||
const taskRequest: TaskRequest = {
|
||||
task_type: TaskType.VLOG,
|
||||
prompt: '制作一个关于健康生活方式的短视频',
|
||||
ar: AspectRatio.PORTRAIT
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建任务
|
||||
const createResult = await textVideoAgentAPI.createTask(taskRequest)
|
||||
const taskId = createResult.data?.task_id
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('任务创建失败')
|
||||
}
|
||||
|
||||
console.log('任务创建成功,ID:', taskId)
|
||||
|
||||
// 查询任务状态
|
||||
const statusResult = await textVideoAgentAPI.getTaskStatusAsync(taskId)
|
||||
console.log('任务状态:', statusResult)
|
||||
|
||||
// 等待任务完成(同步方式)
|
||||
const finalResult = await textVideoAgentAPI.getTaskResultSync(taskId)
|
||||
console.log('任务最终结果:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('任务管理失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例11: 端到端内容生成
|
||||
*/
|
||||
async function example11_EndToEndGeneration() {
|
||||
console.log('=== 端到端内容生成示例 ===')
|
||||
|
||||
try {
|
||||
const result = await textVideoAgentAPI.generateContentEndToEnd(
|
||||
'一个关于咖啡文化的精美内容,展现咖啡师的专业技艺',
|
||||
{
|
||||
taskType: TaskType.VLOG,
|
||||
aspectRatio: AspectRatio.PORTRAIT,
|
||||
videoDuration: VideoDuration.MEDIUM,
|
||||
generateVideo: true,
|
||||
onProgress: (step, progress) => {
|
||||
console.log(`进度更新: ${step} - ${progress}%`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('端到端生成完成:', result)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('端到端生成失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 批量处理示例 ====================
|
||||
|
||||
/**
|
||||
* 示例12: 批量图片生成
|
||||
*/
|
||||
async function example12_BatchImageGeneration() {
|
||||
console.log('=== 批量图片生成示例 ===')
|
||||
|
||||
const prompts = [
|
||||
'春天的樱花盛开',
|
||||
'夏日的海滩风光',
|
||||
'秋天的枫叶满山',
|
||||
'冬日的雪景如画'
|
||||
]
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
prompts.map(async (prompt, index) => {
|
||||
const params: ImageGenerationParams = {
|
||||
prompt,
|
||||
...PRESET_CONFIGS.FAST // 使用快速配置
|
||||
}
|
||||
|
||||
console.log(`开始生成图片 ${index + 1}: ${prompt}`)
|
||||
const result = await textVideoAgentAPI.generateImageSync(params)
|
||||
console.log(`图片 ${index + 1} 生成完成`)
|
||||
|
||||
return {
|
||||
prompt,
|
||||
result: result.data?.image_url,
|
||||
success: result.status
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
console.log('批量图片生成结果:', results)
|
||||
return results
|
||||
} catch (error) {
|
||||
console.error('批量图片生成失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 主函数 ====================
|
||||
|
||||
/**
|
||||
* 运行所有示例
|
||||
*/
|
||||
async function runAllExamples() {
|
||||
console.log('开始运行 Text Video Agent API 示例...\n')
|
||||
|
||||
// 基础功能示例
|
||||
await example1_HealthCheck()
|
||||
await example2_GetSamplePrompts()
|
||||
|
||||
// 文件操作示例
|
||||
// await example3_FileUpload() // 需要实际文件
|
||||
|
||||
// 图片生成示例
|
||||
await example4_BasicImageGeneration()
|
||||
// await example5_ImageGenerationWithReference() // 需要实际文件
|
||||
await example6_AsyncImageGeneration()
|
||||
|
||||
// 视频生成示例
|
||||
await example7_BasicVideoGeneration()
|
||||
await example8_AsyncVideoGeneration()
|
||||
|
||||
// 高级功能示例
|
||||
await example9_ImageDescription()
|
||||
await example10_TaskManagement()
|
||||
await example11_EndToEndGeneration()
|
||||
|
||||
// 批量处理示例
|
||||
await example12_BatchImageGeneration()
|
||||
|
||||
console.log('\n所有示例运行完成!')
|
||||
}
|
||||
|
||||
// 导出示例函数
|
||||
export {
|
||||
example1_HealthCheck,
|
||||
example2_GetSamplePrompts,
|
||||
example3_FileUpload,
|
||||
example4_BasicImageGeneration,
|
||||
example5_ImageGenerationWithReference,
|
||||
example6_AsyncImageGeneration,
|
||||
example7_BasicVideoGeneration,
|
||||
example8_AsyncVideoGeneration,
|
||||
example9_ImageDescription,
|
||||
example10_TaskManagement,
|
||||
example11_EndToEndGeneration,
|
||||
example12_BatchImageGeneration,
|
||||
runAllExamples
|
||||
}
|
||||
|
||||
// 如果直接运行此文件,执行所有示例
|
||||
if (require.main === module) {
|
||||
runAllExamples().catch(console.error)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import React from 'react'
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import Layout from './components/Layout'
|
||||
import HomePage from './pages/HomePage'
|
||||
@@ -14,6 +14,7 @@ import ModelManagePage from './pages/ModelManagePage'
|
||||
import AudioLibraryPage from './pages/AudioLibraryPage'
|
||||
import MediaLibraryPage from './pages/MediaLibraryPage'
|
||||
import KVTestPage from './pages/KVTestPage'
|
||||
import TextVideoGeneratorPage from './pages/TextVideoGeneratorPage'
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
@@ -27,6 +28,7 @@ function App() {
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/editor" element={<EditorPage />} />
|
||||
<Route path="/ai-video" element={<AIVideoPage />} />
|
||||
<Route path="/text-video-generator" element={<TextVideoGeneratorPage />} />
|
||||
<Route path="/templates" element={<TemplateManagePage />} />
|
||||
<Route path="/templates/:templateId" element={<TemplateDetailPage />} />
|
||||
<Route path="/resource-categories" element={<ResourceCategoryPage />} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FolderOpen, Music, Zap, Sparkles, Database, LucideIcon } from 'lucide-react'
|
||||
import { FolderOpen, Music, Zap, Sparkles, Database, LucideIcon, Wand2 } from 'lucide-react'
|
||||
|
||||
interface QuickAction {
|
||||
icon: LucideIcon
|
||||
@@ -11,6 +11,7 @@ interface QuickAction {
|
||||
|
||||
const QuickActions: React.FC = () => {
|
||||
const quickActions: QuickAction[] = [
|
||||
{ icon: Wand2, label: 'AI 内容生成', description: '基于 Text Video Agent API 的智能内容生成', path: '/text-video-generator' },
|
||||
{ icon: Sparkles, label: 'AI 视频生成', description: '使用 AI 将图片转换为动态视频', path: '/ai-video' },
|
||||
{ icon: Music, label: '音频处理', description: '处理音频文件,添加效果', path: '/audio' },
|
||||
{ icon: Zap, label: 'AI 自动剪辑', description: '使用 AI 自动生成视频剪辑', path: '/editor' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Home, Video, Settings, FolderOpen, Music, Image, Sparkles, Layout, Clock, CheckCircle, XCircle, List, Tags, User } from 'lucide-react'
|
||||
import { Home, Video, Settings, FolderOpen, Music, Image, Sparkles, Layout, Clock, CheckCircle, XCircle, List, Tags, User, Wand2 } from 'lucide-react'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
@@ -22,6 +22,7 @@ const Sidebar: React.FC = () => {
|
||||
{ path: '/', icon: Home, label: '首页' },
|
||||
{ path: '/editor', icon: Video, label: '编辑器' },
|
||||
{ path: '/ai-video', icon: Sparkles, label: 'AI 视频' },
|
||||
{ path: '/text-video-generator', icon: Wand2, label: 'AI 内容生成' },
|
||||
{ path: '/templates', icon: Layout, label: '模板库' },
|
||||
{ path: '/resource-categories', icon: Tags, label: '分类管理' },
|
||||
{ path: '/projects', icon: FolderOpen, label: '项目库' },
|
||||
|
||||
433
src/components/TextVideoGenerator.tsx
Normal file
433
src/components/TextVideoGenerator.tsx
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* Text Video Generator Component
|
||||
* 使用 Text Video Agent API 的 React 组件示例
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import {
|
||||
Upload,
|
||||
Image,
|
||||
Video,
|
||||
Loader,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
Play,
|
||||
Download,
|
||||
RefreshCw
|
||||
} from 'lucide-react'
|
||||
|
||||
import { useTextVideoAgent, useTaskPolling } from '../hooks/useTextVideoAgent'
|
||||
import { TaskType, AspectRatio, VideoDuration } from '../services/textVideoAgentTypes'
|
||||
|
||||
interface TextVideoGeneratorProps {
|
||||
onImageGenerated?: (imageUrl: string) => void
|
||||
onVideoGenerated?: (videoUrl: string) => void
|
||||
}
|
||||
|
||||
const TextVideoGenerator: React.FC<TextVideoGeneratorProps> = ({
|
||||
onImageGenerated,
|
||||
onVideoGenerated
|
||||
}) => {
|
||||
// Hook状态
|
||||
const {
|
||||
state,
|
||||
generateImage,
|
||||
generateVideo,
|
||||
generateContentEndToEnd,
|
||||
uploadFile,
|
||||
describeImageByFile,
|
||||
reset,
|
||||
cancel
|
||||
} = useTextVideoAgent()
|
||||
|
||||
// 本地状态
|
||||
const [prompt, setPrompt] = useState('')
|
||||
const [taskType, setTaskType] = useState<TaskType>(TaskType.VLOG)
|
||||
const [aspectRatio, setAspectRatio] = useState<AspectRatio>(AspectRatio.PORTRAIT)
|
||||
const [videoDuration, setVideoDuration] = useState<VideoDuration>(VideoDuration.MEDIUM)
|
||||
const [referenceFile, setReferenceFile] = useState<File | null>(null)
|
||||
const [generateVideoEnabled, setGenerateVideoEnabled] = useState(false)
|
||||
const [results, setResults] = useState<{
|
||||
imageUrl?: string
|
||||
videoUrl?: string
|
||||
taskId?: string
|
||||
}>({})
|
||||
|
||||
// 处理文件上传
|
||||
const handleFileUpload = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setReferenceFile(file)
|
||||
|
||||
// 自动分析图片内容
|
||||
try {
|
||||
const description = await describeImageByFile(file)
|
||||
if (description) {
|
||||
setPrompt(prev => prev ? `${prev}\n\n参考图片描述: ${description}` : description)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('图片分析失败:', error)
|
||||
}
|
||||
}, [describeImageByFile])
|
||||
|
||||
// 生成内容
|
||||
const handleGenerate = useCallback(async () => {
|
||||
if (!prompt.trim()) {
|
||||
alert('请输入生成提示词')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await generateContentEndToEnd(prompt, {
|
||||
taskType,
|
||||
aspectRatio,
|
||||
videoDuration,
|
||||
generateVideo: generateVideoEnabled
|
||||
})
|
||||
|
||||
if (result) {
|
||||
setResults(result)
|
||||
|
||||
if (result.imageUrl && onImageGenerated) {
|
||||
onImageGenerated(result.imageUrl)
|
||||
}
|
||||
|
||||
if (result.videoUrl && onVideoGenerated) {
|
||||
onVideoGenerated(result.videoUrl)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成失败:', error)
|
||||
}
|
||||
}, [
|
||||
prompt,
|
||||
taskType,
|
||||
aspectRatio,
|
||||
videoDuration,
|
||||
generateVideoEnabled,
|
||||
generateContentEndToEnd,
|
||||
onImageGenerated,
|
||||
onVideoGenerated
|
||||
])
|
||||
|
||||
// 只生成图片
|
||||
const handleGenerateImageOnly = useCallback(async () => {
|
||||
if (!prompt.trim()) {
|
||||
alert('请输入生成提示词')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const imageUrl = await generateImage({
|
||||
prompt,
|
||||
img_file: referenceFile || undefined,
|
||||
max_wait_time: 120
|
||||
})
|
||||
|
||||
if (imageUrl) {
|
||||
setResults(prev => ({ ...prev, imageUrl }))
|
||||
onImageGenerated?.(imageUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('图片生成失败:', error)
|
||||
}
|
||||
}, [prompt, referenceFile, generateImage, onImageGenerated])
|
||||
|
||||
// 基于图片生成视频
|
||||
const handleGenerateVideoFromImage = useCallback(async () => {
|
||||
if (!results.imageUrl) {
|
||||
alert('请先生成图片')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const videoUrl = await generateVideo({
|
||||
prompt,
|
||||
img_url: results.imageUrl,
|
||||
duration: videoDuration,
|
||||
max_wait_time: 300
|
||||
})
|
||||
|
||||
if (videoUrl) {
|
||||
setResults(prev => ({ ...prev, videoUrl }))
|
||||
onVideoGenerated?.(videoUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('视频生成失败:', error)
|
||||
}
|
||||
}, [results.imageUrl, prompt, videoDuration, generateVideo, onVideoGenerated])
|
||||
|
||||
// 重置所有状态
|
||||
const handleReset = useCallback(() => {
|
||||
reset()
|
||||
setPrompt('')
|
||||
setReferenceFile(null)
|
||||
setResults({})
|
||||
}, [reset])
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow-lg">
|
||||
{/* 标题 */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">AI 内容生成器</h2>
|
||||
<p className="text-gray-600">基于 Text Video Agent API 的智能内容生成工具</p>
|
||||
</div>
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="space-y-6">
|
||||
{/* 提示词输入 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
生成提示词 *
|
||||
</label>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="描述您想要生成的内容..."
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
disabled={state.isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 参数配置 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
任务类型
|
||||
</label>
|
||||
<select
|
||||
value={taskType}
|
||||
onChange={(e) => setTaskType(e.target.value as TaskType)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
|
||||
disabled={state.isLoading}
|
||||
>
|
||||
<option value={TaskType.VLOG}>Vlog</option>
|
||||
<option value={TaskType.TEA}>茶文化</option>
|
||||
<option value={TaskType.LADY}>人物</option>
|
||||
<option value={TaskType.CHOP}>烹饪</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
长宽比
|
||||
</label>
|
||||
<select
|
||||
value={aspectRatio}
|
||||
onChange={(e) => setAspectRatio(e.target.value as AspectRatio)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
|
||||
disabled={state.isLoading}
|
||||
>
|
||||
<option value={AspectRatio.PORTRAIT}>9:16 (竖屏)</option>
|
||||
<option value={AspectRatio.LANDSCAPE}>16:9 (横屏)</option>
|
||||
<option value={AspectRatio.SQUARE}>1:1 (方形)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
视频时长
|
||||
</label>
|
||||
<select
|
||||
value={videoDuration}
|
||||
onChange={(e) => setVideoDuration(e.target.value as VideoDuration)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
|
||||
disabled={state.isLoading}
|
||||
>
|
||||
<option value={VideoDuration.SHORT}>3秒</option>
|
||||
<option value={VideoDuration.MEDIUM}>5秒</option>
|
||||
<option value={VideoDuration.LONG}>10秒</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 参考图片上传 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
参考图片 (可选)
|
||||
</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
<label className="flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-md cursor-pointer hover:bg-gray-200 transition-colors">
|
||||
<Upload size={16} className="mr-2" />
|
||||
选择文件
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
disabled={state.isLoading}
|
||||
/>
|
||||
</label>
|
||||
{referenceFile && (
|
||||
<span className="text-sm text-gray-600">
|
||||
{referenceFile.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 生成选项 */}
|
||||
<div>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={generateVideoEnabled}
|
||||
onChange={(e) => setGenerateVideoEnabled(e.target.checked)}
|
||||
className="mr-2"
|
||||
disabled={state.isLoading}
|
||||
/>
|
||||
<span className="text-sm text-gray-700">同时生成视频</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 状态显示 */}
|
||||
{state.isLoading && (
|
||||
<div className="mt-6 p-4 bg-blue-50 rounded-md">
|
||||
<div className="flex items-center">
|
||||
<Loader className="animate-spin mr-2 text-blue-600" size={16} />
|
||||
<span className="text-blue-800">{state.currentStep}</span>
|
||||
</div>
|
||||
{state.progress > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="w-full bg-blue-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${state.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-blue-600 mt-1">{state.progress}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误显示 */}
|
||||
{state.error && (
|
||||
<div className="mt-6 p-4 bg-red-50 rounded-md">
|
||||
<div className="flex items-center">
|
||||
<AlertCircle className="mr-2 text-red-600" size={16} />
|
||||
<span className="text-red-800">{state.error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={state.isLoading || !prompt.trim()}
|
||||
className="flex items-center px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{state.isLoading ? (
|
||||
<Loader className="animate-spin mr-2" size={16} />
|
||||
) : (
|
||||
<Play className="mr-2" size={16} />
|
||||
)}
|
||||
{generateVideoEnabled ? '生成图片和视频' : '生成内容'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleGenerateImageOnly}
|
||||
disabled={state.isLoading || !prompt.trim()}
|
||||
className="flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Image className="mr-2" size={16} />
|
||||
仅生成图片
|
||||
</button>
|
||||
|
||||
{results.imageUrl && (
|
||||
<button
|
||||
onClick={handleGenerateVideoFromImage}
|
||||
disabled={state.isLoading}
|
||||
className="flex items-center px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Video className="mr-2" size={16} />
|
||||
基于图片生成视频
|
||||
</button>
|
||||
)}
|
||||
|
||||
{state.isLoading && (
|
||||
<button
|
||||
onClick={cancel}
|
||||
className="flex items-center px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleReset}
|
||||
disabled={state.isLoading}
|
||||
className="flex items-center px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<RefreshCw className="mr-2" size={16} />
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 结果显示 */}
|
||||
{(results.imageUrl || results.videoUrl) && (
|
||||
<div className="mt-8 space-y-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900">生成结果</h3>
|
||||
|
||||
{/* 图片结果 */}
|
||||
{results.imageUrl && (
|
||||
<div className="bg-gray-50 p-4 rounded-md">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium text-gray-900 flex items-center">
|
||||
<Image className="mr-2" size={16} />
|
||||
生成的图片
|
||||
</h4>
|
||||
<a
|
||||
href={results.imageUrl}
|
||||
download
|
||||
className="flex items-center text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
<Download size={16} className="mr-1" />
|
||||
下载
|
||||
</a>
|
||||
</div>
|
||||
<img
|
||||
src={results.imageUrl}
|
||||
alt="Generated"
|
||||
className="max-w-full h-auto rounded-md shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 视频结果 */}
|
||||
{results.videoUrl && (
|
||||
<div className="bg-gray-50 p-4 rounded-md">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium text-gray-900 flex items-center">
|
||||
<Video className="mr-2" size={16} />
|
||||
生成的视频
|
||||
</h4>
|
||||
<a
|
||||
href={results.videoUrl}
|
||||
download
|
||||
className="flex items-center text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
<Download size={16} className="mr-1" />
|
||||
下载
|
||||
</a>
|
||||
</div>
|
||||
<video
|
||||
src={results.videoUrl}
|
||||
controls
|
||||
className="max-w-full h-auto rounded-md shadow-sm"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextVideoGenerator
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Plus, Wand2 } from 'lucide-react'
|
||||
|
||||
const WelcomeSection: React.FC = () => {
|
||||
return (
|
||||
@@ -13,8 +13,15 @@ const WelcomeSection: React.FC = () => {
|
||||
</p>
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<Link
|
||||
to="/projects"
|
||||
to="/text-video-generator"
|
||||
className="btn-primary flex items-center"
|
||||
>
|
||||
<Wand2 size={20} className="mr-2" />
|
||||
AI 内容生成
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects"
|
||||
className="btn-secondary flex items-center"
|
||||
>
|
||||
<Plus size={20} className="mr-2" />
|
||||
创建新项目
|
||||
|
||||
403
src/hooks/useTextVideoAgent.ts
Normal file
403
src/hooks/useTextVideoAgent.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* React Hook for Text Video Agent API
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import {
|
||||
textVideoAgentAPI,
|
||||
ImageGenerationParams,
|
||||
VideoGenerationParams,
|
||||
TaskRequest,
|
||||
APIResponse
|
||||
} from '../services/textVideoAgentAPI'
|
||||
|
||||
import {
|
||||
TaskType,
|
||||
AspectRatio,
|
||||
VideoDuration,
|
||||
TaskStatus as TaskStatusEnum
|
||||
} from '../services/textVideoAgentTypes'
|
||||
|
||||
// Hook状态接口
|
||||
interface UseTextVideoAgentState {
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
progress: number
|
||||
currentStep: string
|
||||
lastResult: any
|
||||
}
|
||||
|
||||
// Hook返回值接口
|
||||
interface UseTextVideoAgentReturn {
|
||||
// 状态
|
||||
state: UseTextVideoAgentState
|
||||
|
||||
// 基础功能
|
||||
healthCheck: () => Promise<APIResponse>
|
||||
uploadFile: (file: File) => Promise<string | null>
|
||||
|
||||
// 图片生成
|
||||
generateImage: (params: ImageGenerationParams) => Promise<string | null>
|
||||
generateImageAsync: (prompt: string, imgFile?: File) => Promise<string>
|
||||
|
||||
// 视频生成
|
||||
generateVideo: (params: VideoGenerationParams) => Promise<string | null>
|
||||
generateVideoAsync: (params: VideoGenerationParams) => Promise<string>
|
||||
|
||||
// 任务管理
|
||||
createTask: (request: TaskRequest) => Promise<string>
|
||||
getTaskStatus: (taskId: string) => Promise<any>
|
||||
|
||||
// 高级功能
|
||||
generateContentEndToEnd: (
|
||||
prompt: string,
|
||||
options?: {
|
||||
taskType?: TaskType
|
||||
aspectRatio?: AspectRatio
|
||||
videoDuration?: VideoDuration
|
||||
generateVideo?: boolean
|
||||
}
|
||||
) => Promise<{ imageUrl?: string; videoUrl?: string; taskId: string } | null>
|
||||
|
||||
// 图片描述
|
||||
describeImage: (imageUrl: string) => Promise<string | null>
|
||||
describeImageByFile: (file: File) => Promise<string | null>
|
||||
|
||||
// 工具函数
|
||||
reset: () => void
|
||||
cancel: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Text Video Agent API React Hook
|
||||
*/
|
||||
export function useTextVideoAgent(): UseTextVideoAgentReturn {
|
||||
const [state, setState] = useState<UseTextVideoAgentState>({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
progress: 0,
|
||||
currentStep: '',
|
||||
lastResult: null
|
||||
})
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
// 更新状态的辅助函数
|
||||
const updateState = useCallback((updates: Partial<UseTextVideoAgentState>) => {
|
||||
setState(prev => ({ ...prev, ...updates }))
|
||||
}, [])
|
||||
|
||||
// 重置状态
|
||||
const reset = useCallback(() => {
|
||||
setState({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
progress: 0,
|
||||
currentStep: '',
|
||||
lastResult: null
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 取消当前操作
|
||||
const cancel = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort()
|
||||
abortControllerRef.current = null
|
||||
}
|
||||
updateState({ isLoading: false, currentStep: '已取消' })
|
||||
}, [updateState])
|
||||
|
||||
// 通用的异步操作包装器
|
||||
const withAsyncOperation = useCallback(async <T>(
|
||||
operation: () => Promise<T>,
|
||||
stepName: string
|
||||
): Promise<T | null> => {
|
||||
try {
|
||||
updateState({
|
||||
isLoading: true,
|
||||
error: null,
|
||||
currentStep: stepName,
|
||||
progress: 0
|
||||
})
|
||||
|
||||
abortControllerRef.current = new AbortController()
|
||||
|
||||
const result = await operation()
|
||||
|
||||
updateState({
|
||||
isLoading: false,
|
||||
progress: 100,
|
||||
currentStep: '完成',
|
||||
lastResult: result
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||||
updateState({
|
||||
isLoading: false,
|
||||
error: errorMessage,
|
||||
currentStep: '错误'
|
||||
})
|
||||
console.error(`${stepName} 失败:`, error)
|
||||
return null
|
||||
} finally {
|
||||
abortControllerRef.current = null
|
||||
}
|
||||
}, [updateState])
|
||||
|
||||
// 健康检查
|
||||
const healthCheck = useCallback(async (): Promise<APIResponse> => {
|
||||
return withAsyncOperation(
|
||||
() => textVideoAgentAPI.healthCheck(),
|
||||
'健康检查'
|
||||
) as Promise<APIResponse>
|
||||
}, [withAsyncOperation])
|
||||
|
||||
// 文件上传
|
||||
const uploadFile = useCallback(async (file: File): Promise<string | null> => {
|
||||
return withAsyncOperation(async () => {
|
||||
const result = await textVideoAgentAPI.uploadFile(file)
|
||||
return result.data || null
|
||||
}, '上传文件')
|
||||
}, [withAsyncOperation])
|
||||
|
||||
// 图片生成(同步)
|
||||
const generateImage = useCallback(async (params: ImageGenerationParams): Promise<string | null> => {
|
||||
return withAsyncOperation(async () => {
|
||||
updateState({ progress: 20 })
|
||||
const result = await textVideoAgentAPI.generateImageWithRetry(params)
|
||||
return result.data?.image_url || result.data?.url || null
|
||||
}, '生成图片')
|
||||
}, [withAsyncOperation, updateState])
|
||||
|
||||
// 图片生成(异步)
|
||||
const generateImageAsync = useCallback(async (prompt: string, imgFile?: File): Promise<string> => {
|
||||
const taskResult = await textVideoAgentAPI.generateImageAsync(prompt, imgFile)
|
||||
const taskId = taskResult.data?.task_id
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('未获取到任务ID')
|
||||
}
|
||||
|
||||
return taskId
|
||||
}, [])
|
||||
|
||||
// 视频生成(同步)
|
||||
const generateVideo = useCallback(async (params: VideoGenerationParams): Promise<string | null> => {
|
||||
return withAsyncOperation(async () => {
|
||||
updateState({ progress: 20 })
|
||||
const result = await textVideoAgentAPI.generateVideoWithRetry(params)
|
||||
return result.data?.video_url || result.data?.url || null
|
||||
}, '生成视频')
|
||||
}, [withAsyncOperation, updateState])
|
||||
|
||||
// 视频生成(异步)
|
||||
const generateVideoAsync = useCallback(async (params: VideoGenerationParams): Promise<string> => {
|
||||
const taskResult = await textVideoAgentAPI.generateVideoAsync(params)
|
||||
const taskId = taskResult.data?.task_id
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('未获取到任务ID')
|
||||
}
|
||||
|
||||
return taskId
|
||||
}, [])
|
||||
|
||||
// 创建任务
|
||||
const createTask = useCallback(async (request: TaskRequest): Promise<string> => {
|
||||
const result = await textVideoAgentAPI.createTask(request)
|
||||
const taskId = result.data?.task_id
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('任务创建失败')
|
||||
}
|
||||
|
||||
return taskId
|
||||
}, [])
|
||||
|
||||
// 获取任务状态
|
||||
const getTaskStatus = useCallback(async (taskId: string): Promise<any> => {
|
||||
const result = await textVideoAgentAPI.getTaskStatusAsync(taskId)
|
||||
return result.data
|
||||
}, [])
|
||||
|
||||
// 端到端内容生成
|
||||
const generateContentEndToEnd = useCallback(async (
|
||||
prompt: string,
|
||||
options: {
|
||||
taskType?: TaskType
|
||||
aspectRatio?: AspectRatio
|
||||
videoDuration?: VideoDuration
|
||||
generateVideo?: boolean
|
||||
} = {}
|
||||
): Promise<{ imageUrl?: string; videoUrl?: string; taskId: string } | null> => {
|
||||
return withAsyncOperation(async () => {
|
||||
const result = await textVideoAgentAPI.generateContentEndToEnd(prompt, {
|
||||
...options,
|
||||
onProgress: (step, progress) => {
|
||||
updateState({ currentStep: step, progress })
|
||||
}
|
||||
})
|
||||
return result
|
||||
}, '端到端内容生成')
|
||||
}, [withAsyncOperation, updateState])
|
||||
|
||||
// 图片描述(通过URL)
|
||||
const describeImage = useCallback(async (imageUrl: string): Promise<string | null> => {
|
||||
return withAsyncOperation(async () => {
|
||||
const result = await textVideoAgentAPI.describeImageByUrl({ image_url: imageUrl })
|
||||
return result.data?.description || null
|
||||
}, '分析图片')
|
||||
}, [withAsyncOperation])
|
||||
|
||||
// 图片描述(通过文件)
|
||||
const describeImageByFile = useCallback(async (file: File): Promise<string | null> => {
|
||||
return withAsyncOperation(async () => {
|
||||
const result = await textVideoAgentAPI.describeImageByFile({ img_file: file })
|
||||
return result.data?.description || null
|
||||
}, '分析图片')
|
||||
}, [withAsyncOperation])
|
||||
|
||||
// 清理资源
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
state,
|
||||
healthCheck,
|
||||
uploadFile,
|
||||
generateImage,
|
||||
generateImageAsync,
|
||||
generateVideo,
|
||||
generateVideoAsync,
|
||||
createTask,
|
||||
getTaskStatus,
|
||||
generateContentEndToEnd,
|
||||
describeImage,
|
||||
describeImageByFile,
|
||||
reset,
|
||||
cancel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务轮询Hook
|
||||
*/
|
||||
export function useTaskPolling(
|
||||
taskId: string | null,
|
||||
options: {
|
||||
enabled?: boolean
|
||||
interval?: number
|
||||
maxDuration?: number
|
||||
onComplete?: (result: any) => void
|
||||
onError?: (error: string) => void
|
||||
onProgress?: (status: any) => void
|
||||
} = {}
|
||||
) {
|
||||
const {
|
||||
enabled = true,
|
||||
interval = 2000,
|
||||
maxDuration = 300000, // 5分钟
|
||||
onComplete,
|
||||
onError,
|
||||
onProgress
|
||||
} = options
|
||||
|
||||
const [status, setStatus] = useState<any>(null)
|
||||
const [isPolling, setIsPolling] = useState(false)
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const startTimeRef = useRef<number>(0)
|
||||
|
||||
const startPolling = useCallback(async () => {
|
||||
if (!taskId || !enabled) return
|
||||
|
||||
setIsPolling(true)
|
||||
startTimeRef.current = Date.now()
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const result = await textVideoAgentAPI.getTaskStatusAsync(taskId)
|
||||
const taskStatus = result.data
|
||||
|
||||
setStatus(taskStatus)
|
||||
onProgress?.(taskStatus)
|
||||
|
||||
// 检查是否完成
|
||||
if (taskStatus?.status === TaskStatusEnum.COMPLETED) {
|
||||
setIsPolling(false)
|
||||
onComplete?.(taskStatus)
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否失败
|
||||
if (taskStatus?.status === TaskStatusEnum.FAILED) {
|
||||
setIsPolling(false)
|
||||
onError?.(taskStatus?.error || '任务失败')
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否超时
|
||||
if (Date.now() - startTimeRef.current > maxDuration) {
|
||||
setIsPolling(false)
|
||||
onError?.('任务超时')
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('轮询任务状态失败:', error)
|
||||
onError?.(error instanceof Error ? error.message : '轮询失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 立即执行一次
|
||||
await poll()
|
||||
|
||||
// 设置定时轮询
|
||||
if (isPolling) {
|
||||
intervalRef.current = setInterval(poll, interval)
|
||||
}
|
||||
}, [taskId, enabled, interval, maxDuration, onComplete, onError, onProgress])
|
||||
|
||||
const stopPolling = useCallback(() => {
|
||||
setIsPolling(false)
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 自动开始轮询
|
||||
useEffect(() => {
|
||||
if (taskId && enabled) {
|
||||
startPolling()
|
||||
}
|
||||
|
||||
return () => {
|
||||
stopPolling()
|
||||
}
|
||||
}, [taskId, enabled, startPolling, stopPolling])
|
||||
|
||||
return {
|
||||
status,
|
||||
isPolling,
|
||||
startPolling,
|
||||
stopPolling
|
||||
}
|
||||
}
|
||||
213
src/pages/TextVideoGeneratorPage.tsx
Normal file
213
src/pages/TextVideoGeneratorPage.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Text Video Generator Page
|
||||
* 基于 Text Video Agent API 的智能内容生成页面
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { ArrowLeft, Info, ExternalLink } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import TextVideoGenerator from '../components/TextVideoGenerator'
|
||||
|
||||
const TextVideoGeneratorPage: React.FC = () => {
|
||||
const [generatedContent, setGeneratedContent] = useState<{
|
||||
images: string[]
|
||||
videos: string[]
|
||||
}>({
|
||||
images: [],
|
||||
videos: []
|
||||
})
|
||||
|
||||
const handleImageGenerated = (imageUrl: string) => {
|
||||
setGeneratedContent(prev => ({
|
||||
...prev,
|
||||
images: [...prev.images, imageUrl]
|
||||
}))
|
||||
}
|
||||
|
||||
const handleVideoGenerated = (videoUrl: string) => {
|
||||
setGeneratedContent(prev => ({
|
||||
...prev,
|
||||
videos: [...prev.videos, videoUrl]
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* 页面头部 */}
|
||||
<div className="bg-white border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center text-gray-600 hover:text-gray-900 transition-colors mr-4"
|
||||
>
|
||||
<ArrowLeft size={20} className="mr-2" />
|
||||
返回首页
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900">AI 内容生成器</h1>
|
||||
<p className="text-sm text-gray-600">基于 Text Video Agent API 的智能内容生成</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<a
|
||||
href="https://bowongai-dev--text-video-agent-fastapi-app.modal.run/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center text-blue-600 hover:text-blue-700 text-sm"
|
||||
>
|
||||
<ExternalLink size={16} className="mr-1" />
|
||||
API 文档
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
{/* 左侧:生成器 */}
|
||||
<div className="lg:col-span-3">
|
||||
<TextVideoGenerator
|
||||
onImageGenerated={handleImageGenerated}
|
||||
onVideoGenerated={handleVideoGenerated}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 右侧:信息面板 */}
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
{/* 功能介绍 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<Info className="text-blue-600 mr-2" size={20} />
|
||||
<h3 className="font-semibold text-gray-900">功能介绍</h3>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm text-gray-600">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-1">🎨 图片生成</h4>
|
||||
<p>基于 Midjourney 的高质量图片生成,支持参考图片和自定义提示词</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-1">🎬 视频生成</h4>
|
||||
<p>基于极梦的视频生成功能,将静态图片转换为动态视频</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-1">🔄 端到端生成</h4>
|
||||
<p>一键完成从提示词到最终视频的完整生成流程</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-1">📊 实时进度</h4>
|
||||
<p>实时显示生成进度和状态,支持取消和重试</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 任务类型说明 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">任务类型说明</h3>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="font-medium text-gray-700">Vlog</span>
|
||||
<span className="text-gray-600">生活记录风格</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-medium text-gray-700">茶文化</span>
|
||||
<span className="text-gray-600">茶艺相关内容</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-medium text-gray-700">人物</span>
|
||||
<span className="text-gray-600">人物肖像风格</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-medium text-gray-700">烹饪</span>
|
||||
<span className="text-gray-600">美食制作过程</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 生成统计 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">本次会话统计</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">生成图片</span>
|
||||
<span className="font-semibold text-blue-600">{generatedContent.images.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">生成视频</span>
|
||||
<span className="font-semibold text-purple-600">{generatedContent.videos.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">总计内容</span>
|
||||
<span className="font-semibold text-green-600">
|
||||
{generatedContent.images.length + generatedContent.videos.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最近生成的内容 */}
|
||||
{(generatedContent.images.length > 0 || generatedContent.videos.length > 0) && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">最近生成</h3>
|
||||
<div className="space-y-3">
|
||||
{/* 最新的图片 */}
|
||||
{generatedContent.images.slice(-2).map((imageUrl, index) => (
|
||||
<div key={index} className="flex items-center space-x-3">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={`Generated ${index}`}
|
||||
className="w-12 h-12 object-cover rounded-md"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
生成的图片
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{new Date().toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 最新的视频 */}
|
||||
{generatedContent.videos.slice(-2).map((_, index) => (
|
||||
<div key={index} className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-purple-100 rounded-md flex items-center justify-center">
|
||||
<span className="text-purple-600 text-xs font-medium">MP4</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
生成的视频
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{new Date().toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用提示 */}
|
||||
<div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
|
||||
<h3 className="font-semibold text-blue-900 mb-3">💡 使用提示</h3>
|
||||
<div className="space-y-2 text-sm text-blue-800">
|
||||
<p>• 提示词越详细,生成效果越好</p>
|
||||
<p>• 可以上传参考图片提高生成质量</p>
|
||||
<p>• 建议先生成图片,再基于图片生成视频</p>
|
||||
<p>• 生成过程可能需要1-3分钟,请耐心等待</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextVideoGeneratorPage
|
||||
565
src/services/textVideoAgentAPI.ts
Normal file
565
src/services/textVideoAgentAPI.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* Text Video Agent API 工具库
|
||||
* 基于 https://bowongai-dev--text-video-agent-fastapi-app.modal.run 的API封装
|
||||
*/
|
||||
|
||||
// 基础配置
|
||||
const API_BASE_URL = 'https://bowongai-dev--text-video-agent-fastapi-app.modal.run'
|
||||
|
||||
// 通用响应接口
|
||||
export interface APIResponse<T = any> {
|
||||
status: boolean
|
||||
msg: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
// 文件上传响应
|
||||
export interface FileUploadResponse {
|
||||
status: boolean
|
||||
msg: string
|
||||
data?: string // 文件URL
|
||||
}
|
||||
|
||||
// 任务请求接口
|
||||
export interface TaskRequest {
|
||||
task_type?: string // 任务类型如: tea, chop, lady, vlog
|
||||
prompt: string // 生图的提示词
|
||||
img_url?: string // 参考图
|
||||
ar?: string // 生成图片,视频的分辨率,默认9:16
|
||||
}
|
||||
|
||||
// 任务状态接口
|
||||
export interface TaskStatus {
|
||||
task_id: string
|
||||
status: 'pending' | 'running' | 'completed' | 'failed'
|
||||
progress?: number
|
||||
result?: any
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 图片生成参数
|
||||
export interface ImageGenerationParams {
|
||||
prompt: string
|
||||
img_file?: File
|
||||
max_wait_time?: number // 默认120秒
|
||||
poll_interval?: number // 默认2秒
|
||||
}
|
||||
|
||||
// 视频生成参数
|
||||
export interface VideoGenerationParams {
|
||||
prompt: string
|
||||
img_url?: string
|
||||
img_file?: File
|
||||
duration?: string // 默认5秒
|
||||
max_wait_time?: number // 默认300秒
|
||||
poll_interval?: number // 默认5秒
|
||||
}
|
||||
|
||||
// 图片描述参数
|
||||
export interface ImageDescribeParams {
|
||||
image_url?: string
|
||||
img_file?: File
|
||||
max_wait_time?: number // 默认120秒
|
||||
poll_interval?: number // 默认2秒
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP请求工具类
|
||||
*/
|
||||
class HTTPClient {
|
||||
private baseURL: string
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.baseURL = baseURL
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${this.baseURL}${endpoint}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error(`API request failed: ${url}`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
|
||||
const url = new URL(endpoint, this.baseURL)
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.append(key, String(value))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return this.request<T>(url.pathname + url.search)
|
||||
}
|
||||
|
||||
async post<T>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
...options,
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
async postJSON<T>(endpoint: string, data?: any): Promise<T> {
|
||||
return this.post<T>(endpoint, JSON.stringify(data), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async postFormData<T>(endpoint: string, formData: FormData): Promise<T> {
|
||||
return this.post<T>(endpoint, formData)
|
||||
}
|
||||
|
||||
async postFormUrlEncoded<T>(
|
||||
endpoint: string,
|
||||
data: Record<string, any>
|
||||
): Promise<T> {
|
||||
const formData = new URLSearchParams()
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
formData.append(key, String(value))
|
||||
}
|
||||
})
|
||||
|
||||
return this.post<T>(endpoint, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text Video Agent API 客户端
|
||||
*/
|
||||
export class TextVideoAgentAPI {
|
||||
private client: HTTPClient
|
||||
|
||||
constructor(baseURL: string = API_BASE_URL) {
|
||||
this.client = new HTTPClient(baseURL)
|
||||
}
|
||||
|
||||
// ==================== 基础功能 ====================
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*/
|
||||
async healthCheck(): Promise<APIResponse> {
|
||||
return this.client.get('/health')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取示例提示词
|
||||
*/
|
||||
async getSamplePrompt(taskType?: string): Promise<APIResponse> {
|
||||
return this.client.get('/api/prompt/default', { task_type: taskType })
|
||||
}
|
||||
|
||||
// ==================== 文件操作 ====================
|
||||
|
||||
/**
|
||||
* 上传文件到COS
|
||||
*/
|
||||
async uploadFile(file: File): Promise<FileUploadResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return this.client.postFormData('/api/file/upload', formData)
|
||||
}
|
||||
|
||||
// ==================== Midjourney图片生成 ====================
|
||||
|
||||
/**
|
||||
* Midjourney健康检查
|
||||
*/
|
||||
async mjHealthCheck(): Promise<APIResponse> {
|
||||
return this.client.get('/api/mj/health')
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步生成图片(阻塞等待)
|
||||
*/
|
||||
async generateImageSync(params: ImageGenerationParams): Promise<APIResponse> {
|
||||
const formData = new FormData()
|
||||
if (params.img_file) {
|
||||
formData.append('img_file', params.img_file)
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
prompt: params.prompt,
|
||||
max_wait_time: params.max_wait_time,
|
||||
poll_interval: params.poll_interval,
|
||||
}
|
||||
|
||||
const url = new URL('/api/mj/sync/image', this.client['baseURL'])
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.append(key, String(value))
|
||||
}
|
||||
})
|
||||
|
||||
return this.client.postFormData(url.pathname + url.search, formData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图片(推荐使用同步接口)
|
||||
*/
|
||||
async generateImage(params: ImageGenerationParams): Promise<APIResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append('prompt', params.prompt)
|
||||
if (params.img_file) {
|
||||
formData.append('img_file', params.img_file)
|
||||
}
|
||||
if (params.max_wait_time) {
|
||||
formData.append('max_wait_time', String(params.max_wait_time))
|
||||
}
|
||||
if (params.poll_interval) {
|
||||
formData.append('poll_interval', String(params.poll_interval))
|
||||
}
|
||||
|
||||
return this.client.postFormData('/api/mj/generate-image', formData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步提交生图任务
|
||||
*/
|
||||
async generateImageAsync(prompt: string, imgFile?: File): Promise<APIResponse> {
|
||||
const formData = new FormData()
|
||||
if (imgFile) {
|
||||
formData.append('img_file', imgFile)
|
||||
}
|
||||
|
||||
return this.client.postFormData(`/api/mj/async/generate/image?prompt=${encodeURIComponent(prompt)}`, formData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询异步任务状态
|
||||
*/
|
||||
async queryImageTaskStatus(taskId: string): Promise<APIResponse> {
|
||||
return this.client.get('/api/mj/async/query/status', { task_id: taskId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过URL获取图像描述
|
||||
*/
|
||||
async describeImageByUrl(params: ImageDescribeParams): Promise<APIResponse> {
|
||||
if (!params.image_url) {
|
||||
throw new Error('image_url is required')
|
||||
}
|
||||
|
||||
return this.client.postFormUrlEncoded('/api/mj/sync/img/describe', {
|
||||
image_url: params.image_url,
|
||||
max_wait_time: params.max_wait_time,
|
||||
poll_interval: params.poll_interval,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过文件获取图像描述
|
||||
*/
|
||||
async describeImageByFile(params: ImageDescribeParams): Promise<APIResponse> {
|
||||
if (!params.img_file) {
|
||||
throw new Error('img_file is required')
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('img_file', params.img_file)
|
||||
if (params.max_wait_time) {
|
||||
formData.append('max_wait_time', String(params.max_wait_time))
|
||||
}
|
||||
if (params.poll_interval) {
|
||||
formData.append('poll_interval', String(params.poll_interval))
|
||||
}
|
||||
|
||||
return this.client.postFormData('/api/mj/sync/file/img/describe', formData)
|
||||
}
|
||||
|
||||
// ==================== 极梦视频生成 ====================
|
||||
|
||||
/**
|
||||
* 极梦健康检查
|
||||
*/
|
||||
async jmHealthCheck(): Promise<APIResponse> {
|
||||
return this.client.get('/api/jm/health')
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步生成视频(阻塞等待)
|
||||
*/
|
||||
async generateVideoSync(params: VideoGenerationParams): Promise<APIResponse> {
|
||||
if (!params.img_url) {
|
||||
throw new Error('img_url is required for sync video generation')
|
||||
}
|
||||
|
||||
return this.client.postFormUrlEncoded('/api/jm/generate-video', {
|
||||
prompt: params.prompt,
|
||||
img_url: params.img_url,
|
||||
duration: params.duration || '5',
|
||||
max_wait_time: params.max_wait_time || 300,
|
||||
poll_interval: params.poll_interval || 5,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步生成视频
|
||||
*/
|
||||
async generateVideoAsync(params: VideoGenerationParams): Promise<APIResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append('prompt', params.prompt)
|
||||
|
||||
if (params.img_url) {
|
||||
formData.append('img_url', params.img_url)
|
||||
}
|
||||
|
||||
if (params.img_file) {
|
||||
formData.append('img_file', params.img_file)
|
||||
}
|
||||
|
||||
formData.append('duration', params.duration || '5')
|
||||
|
||||
return this.client.postFormData('/api/jm/async/generate/video', formData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询视频生成任务状态
|
||||
*/
|
||||
async queryVideoTaskStatus(taskId: string): Promise<APIResponse> {
|
||||
return this.client.get('/api/jm/async/query/status', { task_id: taskId })
|
||||
}
|
||||
|
||||
// ==================== 任务管理 ====================
|
||||
|
||||
/**
|
||||
* 创建异步任务(不阻塞)
|
||||
*/
|
||||
async createTask(request: TaskRequest): Promise<APIResponse> {
|
||||
return this.client.postJSON('/api/task/create/task', request)
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步查询任务结果(阻塞等待)
|
||||
*/
|
||||
async getTaskResultSync(taskId: string): Promise<APIResponse> {
|
||||
return this.client.get(`/api/task/${taskId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步查询任务状态(立即返回)
|
||||
*/
|
||||
async getTaskStatusAsync(taskId: string): Promise<APIResponse> {
|
||||
return this.client.get(`/api/task/status/${taskId}`)
|
||||
}
|
||||
|
||||
// ==================== 高级封装方法 ====================
|
||||
|
||||
/**
|
||||
* 完整的图片生成流程(带重试机制)
|
||||
*/
|
||||
async generateImageWithRetry(
|
||||
params: ImageGenerationParams,
|
||||
maxRetries: number = 3
|
||||
): Promise<APIResponse> {
|
||||
let lastError: Error | null = null
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const result = await this.generateImageSync(params)
|
||||
if (result.status) {
|
||||
return result
|
||||
}
|
||||
lastError = new Error(result.msg || 'Generation failed')
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
console.warn(`Image generation attempt ${i + 1} failed:`, error)
|
||||
|
||||
// 如果不是最后一次重试,等待一段时间再重试
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000 * (i + 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('All retry attempts failed')
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整的视频生成流程(带重试机制)
|
||||
*/
|
||||
async generateVideoWithRetry(
|
||||
params: VideoGenerationParams,
|
||||
maxRetries: number = 3
|
||||
): Promise<APIResponse> {
|
||||
let lastError: Error | null = null
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const result = await this.generateVideoSync(params)
|
||||
if (result.status) {
|
||||
return result
|
||||
}
|
||||
lastError = new Error(result.msg || 'Generation failed')
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
console.warn(`Video generation attempt ${i + 1} failed:`, error)
|
||||
|
||||
// 如果不是最后一次重试,等待一段时间再重试
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000 * (i + 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('All retry attempts failed')
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询任务状态直到完成
|
||||
*/
|
||||
async pollTaskUntilComplete(
|
||||
taskId: string,
|
||||
pollInterval: number = 2000,
|
||||
maxWaitTime: number = 300000, // 5分钟
|
||||
onProgress?: (status: any) => void
|
||||
): Promise<APIResponse> {
|
||||
const startTime = Date.now()
|
||||
|
||||
while (Date.now() - startTime < maxWaitTime) {
|
||||
try {
|
||||
const status = await this.getTaskStatusAsync(taskId)
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(status)
|
||||
}
|
||||
|
||||
if (status.data?.status === 'completed') {
|
||||
return status
|
||||
}
|
||||
|
||||
if (status.data?.status === 'failed') {
|
||||
throw new Error(status.data?.error || 'Task failed')
|
||||
}
|
||||
|
||||
// 等待下次轮询
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
||||
} catch (error) {
|
||||
console.error('Error polling task status:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Task polling timeout')
|
||||
}
|
||||
|
||||
/**
|
||||
* 端到端的内容生成流程
|
||||
*/
|
||||
async generateContentEndToEnd(
|
||||
prompt: string,
|
||||
options: {
|
||||
taskType?: string
|
||||
referenceImageFile?: File
|
||||
referenceImageUrl?: string
|
||||
aspectRatio?: string
|
||||
videoDuration?: string
|
||||
generateVideo?: boolean
|
||||
onProgress?: (step: string, progress: number) => void
|
||||
} = {}
|
||||
): Promise<{
|
||||
imageUrl?: string
|
||||
videoUrl?: string
|
||||
taskId: string
|
||||
}> {
|
||||
const { onProgress } = options
|
||||
|
||||
try {
|
||||
// 步骤1: 创建任务
|
||||
onProgress?.('创建任务', 10)
|
||||
const taskRequest: TaskRequest = {
|
||||
task_type: options.taskType || 'vlog',
|
||||
prompt,
|
||||
img_url: options.referenceImageUrl,
|
||||
ar: options.aspectRatio || '9:16'
|
||||
}
|
||||
|
||||
const taskResult = await this.createTask(taskRequest)
|
||||
const taskId = taskResult.data?.task_id
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('Failed to create task')
|
||||
}
|
||||
|
||||
// 步骤2: 生成图片
|
||||
onProgress?.('生成图片', 30)
|
||||
const imageParams: ImageGenerationParams = {
|
||||
prompt,
|
||||
img_file: options.referenceImageFile,
|
||||
max_wait_time: 120
|
||||
}
|
||||
|
||||
const imageResult = await this.generateImageWithRetry(imageParams)
|
||||
const imageUrl = imageResult.data?.image_url || imageResult.data?.url
|
||||
|
||||
if (!imageUrl) {
|
||||
throw new Error('Failed to generate image')
|
||||
}
|
||||
|
||||
let videoUrl: string | undefined
|
||||
|
||||
// 步骤3: 生成视频(如果需要)
|
||||
if (options.generateVideo) {
|
||||
onProgress?.('生成视频', 60)
|
||||
const videoParams: VideoGenerationParams = {
|
||||
prompt,
|
||||
img_url: imageUrl,
|
||||
duration: options.videoDuration || '5',
|
||||
max_wait_time: 300
|
||||
}
|
||||
|
||||
const videoResult = await this.generateVideoWithRetry(videoParams)
|
||||
videoUrl = videoResult.data?.video_url || videoResult.data?.url
|
||||
}
|
||||
|
||||
onProgress?.('完成', 100)
|
||||
|
||||
return {
|
||||
imageUrl,
|
||||
videoUrl,
|
||||
taskId
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('End-to-end generation failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const textVideoAgentAPI = new TextVideoAgentAPI()
|
||||
|
||||
// 导出默认实例
|
||||
export default textVideoAgentAPI
|
||||
311
src/services/textVideoAgentTypes.ts
Normal file
311
src/services/textVideoAgentTypes.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Text Video Agent API 类型定义
|
||||
*/
|
||||
|
||||
// 任务类型枚举
|
||||
export enum TaskType {
|
||||
TEA = 'tea',
|
||||
CHOP = 'chop',
|
||||
LADY = 'lady',
|
||||
VLOG = 'vlog'
|
||||
}
|
||||
|
||||
// 任务状态枚举
|
||||
export enum TaskStatus {
|
||||
PENDING = 'pending',
|
||||
RUNNING = 'running',
|
||||
COMPLETED = 'completed',
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
// 长宽比枚举
|
||||
export enum AspectRatio {
|
||||
SQUARE = '1:1',
|
||||
PORTRAIT = '9:16',
|
||||
LANDSCAPE = '16:9',
|
||||
VERTICAL = '3:4',
|
||||
HORIZONTAL = '4:3'
|
||||
}
|
||||
|
||||
// 视频时长选项
|
||||
export enum VideoDuration {
|
||||
SHORT = '3',
|
||||
MEDIUM = '5',
|
||||
LONG = '10'
|
||||
}
|
||||
|
||||
// 扩展的API响应接口
|
||||
export interface ExtendedAPIResponse<T = any> {
|
||||
status: boolean
|
||||
msg: string
|
||||
data?: T
|
||||
timestamp?: string
|
||||
request_id?: string
|
||||
}
|
||||
|
||||
// 图片生成结果
|
||||
export interface ImageGenerationResult {
|
||||
image_url: string
|
||||
thumbnail_url?: string
|
||||
width?: number
|
||||
height?: number
|
||||
format?: string
|
||||
size?: number
|
||||
generation_time?: number
|
||||
prompt_used?: string
|
||||
}
|
||||
|
||||
// 视频生成结果
|
||||
export interface VideoGenerationResult {
|
||||
video_url: string
|
||||
thumbnail_url?: string
|
||||
duration: number
|
||||
width?: number
|
||||
height?: number
|
||||
format?: string
|
||||
size?: number
|
||||
generation_time?: number
|
||||
prompt_used?: string
|
||||
image_url_used?: string
|
||||
}
|
||||
|
||||
// 任务详细信息
|
||||
export interface TaskDetail {
|
||||
task_id: string
|
||||
task_type: TaskType
|
||||
status: TaskStatus
|
||||
progress: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
completed_at?: string
|
||||
prompt: string
|
||||
img_url?: string
|
||||
ar: string
|
||||
result?: ImageGenerationResult | VideoGenerationResult
|
||||
error?: {
|
||||
code: string
|
||||
message: string
|
||||
details?: any
|
||||
}
|
||||
metadata?: {
|
||||
user_id?: string
|
||||
session_id?: string
|
||||
client_info?: any
|
||||
}
|
||||
}
|
||||
|
||||
// 图片描述结果
|
||||
export interface ImageDescriptionResult {
|
||||
description: string
|
||||
tags?: string[]
|
||||
confidence?: number
|
||||
language?: string
|
||||
analysis_time?: number
|
||||
}
|
||||
|
||||
// 文件上传结果
|
||||
export interface FileUploadResult {
|
||||
file_url: string
|
||||
file_name: string
|
||||
file_size: number
|
||||
file_type: string
|
||||
upload_time: string
|
||||
cdn_url?: string
|
||||
}
|
||||
|
||||
// 健康检查结果
|
||||
export interface HealthCheckResult {
|
||||
service: string
|
||||
status: 'healthy' | 'unhealthy'
|
||||
version?: string
|
||||
uptime?: number
|
||||
dependencies?: {
|
||||
[key: string]: 'healthy' | 'unhealthy'
|
||||
}
|
||||
}
|
||||
|
||||
// 示例提示词结果
|
||||
export interface SamplePromptResult {
|
||||
task_type: string
|
||||
prompts: string[]
|
||||
examples?: {
|
||||
prompt: string
|
||||
description: string
|
||||
tags?: string[]
|
||||
}[]
|
||||
}
|
||||
|
||||
// 生成选项配置
|
||||
export interface GenerationOptions {
|
||||
// 通用选项
|
||||
quality?: 'low' | 'medium' | 'high' | 'ultra'
|
||||
style?: 'realistic' | 'artistic' | 'cartoon' | 'anime'
|
||||
mood?: 'bright' | 'dark' | 'neutral' | 'vibrant'
|
||||
|
||||
// 图片特定选项
|
||||
image_format?: 'jpg' | 'png' | 'webp'
|
||||
image_quality?: number // 1-100
|
||||
|
||||
// 视频特定选项
|
||||
video_format?: 'mp4' | 'webm' | 'avi'
|
||||
video_quality?: 'low' | 'medium' | 'high' | '4k'
|
||||
frame_rate?: number // fps
|
||||
|
||||
// 高级选项
|
||||
seed?: number
|
||||
guidance_scale?: number
|
||||
num_inference_steps?: number
|
||||
negative_prompt?: string
|
||||
}
|
||||
|
||||
// 批量处理请求
|
||||
export interface BatchRequest {
|
||||
requests: {
|
||||
id: string
|
||||
type: 'image' | 'video'
|
||||
prompt: string
|
||||
options?: GenerationOptions
|
||||
}[]
|
||||
batch_options?: {
|
||||
parallel_limit?: number
|
||||
retry_failed?: boolean
|
||||
notify_on_complete?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// 批量处理结果
|
||||
export interface BatchResult {
|
||||
batch_id: string
|
||||
total_requests: number
|
||||
completed: number
|
||||
failed: number
|
||||
results: {
|
||||
id: string
|
||||
status: 'completed' | 'failed'
|
||||
result?: ImageGenerationResult | VideoGenerationResult
|
||||
error?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
// 使用统计
|
||||
export interface UsageStats {
|
||||
user_id?: string
|
||||
period: 'daily' | 'weekly' | 'monthly'
|
||||
images_generated: number
|
||||
videos_generated: number
|
||||
total_generation_time: number
|
||||
total_cost?: number
|
||||
quota_remaining?: number
|
||||
}
|
||||
|
||||
// 错误类型
|
||||
export interface APIError {
|
||||
code: string
|
||||
message: string
|
||||
details?: any
|
||||
timestamp: string
|
||||
request_id?: string
|
||||
suggestion?: string
|
||||
}
|
||||
|
||||
// 进度回调函数类型
|
||||
export type ProgressCallback = (step: string, progress: number, details?: any) => void
|
||||
|
||||
// 事件回调函数类型
|
||||
export type EventCallback = (event: {
|
||||
type: 'start' | 'progress' | 'complete' | 'error'
|
||||
data: any
|
||||
timestamp: string
|
||||
}) => void
|
||||
|
||||
// 配置选项
|
||||
export interface APIConfig {
|
||||
baseURL?: string
|
||||
timeout?: number
|
||||
retries?: number
|
||||
apiKey?: string
|
||||
userAgent?: string
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
// 缓存选项
|
||||
export interface CacheOptions {
|
||||
enabled: boolean
|
||||
ttl?: number // 缓存时间(秒)
|
||||
maxSize?: number // 最大缓存条目数
|
||||
storage?: 'memory' | 'localStorage' | 'sessionStorage'
|
||||
}
|
||||
|
||||
// 高级配置
|
||||
export interface AdvancedConfig extends APIConfig {
|
||||
cache?: CacheOptions
|
||||
rateLimit?: {
|
||||
enabled: boolean
|
||||
requestsPerMinute?: number
|
||||
requestsPerHour?: number
|
||||
}
|
||||
monitoring?: {
|
||||
enabled: boolean
|
||||
endpoint?: string
|
||||
metrics?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有类型的联合类型
|
||||
export type AllGenerationResults = ImageGenerationResult | VideoGenerationResult
|
||||
export type AllAPIResponses = ExtendedAPIResponse<AllGenerationResults>
|
||||
|
||||
// 常用的预设配置
|
||||
export const PRESET_CONFIGS = {
|
||||
// 快速生成(低质量,快速)
|
||||
FAST: {
|
||||
quality: 'low' as const,
|
||||
max_wait_time: 60,
|
||||
poll_interval: 1
|
||||
},
|
||||
|
||||
// 标准生成(中等质量)
|
||||
STANDARD: {
|
||||
quality: 'medium' as const,
|
||||
max_wait_time: 120,
|
||||
poll_interval: 2
|
||||
},
|
||||
|
||||
// 高质量生成(高质量,较慢)
|
||||
HIGH_QUALITY: {
|
||||
quality: 'high' as const,
|
||||
max_wait_time: 300,
|
||||
poll_interval: 5
|
||||
},
|
||||
|
||||
// 超高质量生成(最高质量,最慢)
|
||||
ULTRA: {
|
||||
quality: 'ultra' as const,
|
||||
max_wait_time: 600,
|
||||
poll_interval: 10
|
||||
}
|
||||
} as const
|
||||
|
||||
// 常用的任务类型配置
|
||||
export const TASK_TYPE_CONFIGS = {
|
||||
[TaskType.TEA]: {
|
||||
description: '茶叶相关内容生成',
|
||||
defaultAR: AspectRatio.PORTRAIT,
|
||||
suggestedDuration: VideoDuration.MEDIUM
|
||||
},
|
||||
[TaskType.CHOP]: {
|
||||
description: '切菜/烹饪相关内容生成',
|
||||
defaultAR: AspectRatio.LANDSCAPE,
|
||||
suggestedDuration: VideoDuration.SHORT
|
||||
},
|
||||
[TaskType.LADY]: {
|
||||
description: '女性人物相关内容生成',
|
||||
defaultAR: AspectRatio.PORTRAIT,
|
||||
suggestedDuration: VideoDuration.MEDIUM
|
||||
},
|
||||
[TaskType.VLOG]: {
|
||||
description: 'Vlog风格内容生成',
|
||||
defaultAR: AspectRatio.PORTRAIT,
|
||||
suggestedDuration: VideoDuration.LONG
|
||||
}
|
||||
} as const
|
||||
442
tests/textVideoAgentAPI.test.ts
Normal file
442
tests/textVideoAgentAPI.test.ts
Normal file
@@ -0,0 +1,442 @@
|
||||
/**
|
||||
* Text Video Agent API 测试文件
|
||||
*/
|
||||
|
||||
import { textVideoAgentAPI, TextVideoAgentAPI } from '../src/services/textVideoAgentAPI'
|
||||
import { TaskType, AspectRatio, VideoDuration } from '../src/services/textVideoAgentTypes'
|
||||
|
||||
// Mock fetch for testing
|
||||
global.fetch = jest.fn()
|
||||
|
||||
describe('TextVideoAgentAPI', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('基础功能测试', () => {
|
||||
test('健康检查', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Service is healthy'
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.healthCheck()
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/health'),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(result).toEqual(mockResponse)
|
||||
})
|
||||
|
||||
test('获取示例提示词', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Success',
|
||||
data: {
|
||||
prompts: ['示例提示词1', '示例提示词2']
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.getSamplePrompt(TaskType.VLOG)
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/prompt/default?task_type=vlog'),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(result).toEqual(mockResponse)
|
||||
})
|
||||
})
|
||||
|
||||
describe('图片生成测试', () => {
|
||||
test('同步生成图片', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Image generated successfully',
|
||||
data: {
|
||||
image_url: 'https://example.com/generated-image.jpg'
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.generateImageSync({
|
||||
prompt: '测试图片生成',
|
||||
max_wait_time: 60
|
||||
})
|
||||
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.data?.image_url).toBe('https://example.com/generated-image.jpg')
|
||||
})
|
||||
|
||||
test('异步生成图片', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Task submitted',
|
||||
data: {
|
||||
task_id: 'test-task-123'
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.generateImageAsync('测试异步图片生成')
|
||||
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.data?.task_id).toBe('test-task-123')
|
||||
})
|
||||
|
||||
test('带重试的图片生成', async () => {
|
||||
// 第一次失败
|
||||
;(fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
// 第二次成功
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Image generated successfully',
|
||||
data: {
|
||||
image_url: 'https://example.com/generated-image.jpg'
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.generateImageWithRetry({
|
||||
prompt: '测试重试机制',
|
||||
max_wait_time: 60
|
||||
}, 2)
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(2)
|
||||
expect(result).toEqual(mockResponse)
|
||||
})
|
||||
})
|
||||
|
||||
describe('视频生成测试', () => {
|
||||
test('同步生成视频', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Video generated successfully',
|
||||
data: {
|
||||
video_url: 'https://example.com/generated-video.mp4'
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.generateVideoSync({
|
||||
prompt: '测试视频生成',
|
||||
img_url: 'https://example.com/base-image.jpg',
|
||||
duration: '5'
|
||||
})
|
||||
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.data?.video_url).toBe('https://example.com/generated-video.mp4')
|
||||
})
|
||||
|
||||
test('异步生成视频', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Video task submitted',
|
||||
data: {
|
||||
task_id: 'video-task-456'
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.generateVideoAsync({
|
||||
prompt: '测试异步视频生成',
|
||||
duration: '5'
|
||||
})
|
||||
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.data?.task_id).toBe('video-task-456')
|
||||
})
|
||||
})
|
||||
|
||||
describe('任务管理测试', () => {
|
||||
test('创建任务', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Task created successfully',
|
||||
data: {
|
||||
task_id: 'new-task-789'
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.createTask({
|
||||
task_type: TaskType.VLOG,
|
||||
prompt: '测试任务创建',
|
||||
ar: AspectRatio.PORTRAIT
|
||||
})
|
||||
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.data?.task_id).toBe('new-task-789')
|
||||
})
|
||||
|
||||
test('查询任务状态', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Task status retrieved',
|
||||
data: {
|
||||
task_id: 'test-task-123',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
result: {
|
||||
image_url: 'https://example.com/result.jpg'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.getTaskStatusAsync('test-task-123')
|
||||
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.data?.status).toBe('completed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('文件操作测试', () => {
|
||||
test('文件上传', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'File uploaded successfully',
|
||||
data: 'https://example.com/uploaded-file.jpg'
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const mockFile = new File(['test content'], 'test.jpg', { type: 'image/jpeg' })
|
||||
const result = await textVideoAgentAPI.uploadFile(mockFile)
|
||||
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.data).toBe('https://example.com/uploaded-file.jpg')
|
||||
})
|
||||
})
|
||||
|
||||
describe('图片描述测试', () => {
|
||||
test('通过URL描述图片', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Image described successfully',
|
||||
data: {
|
||||
description: '这是一张美丽的风景照片'
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.describeImageByUrl({
|
||||
image_url: 'https://example.com/image.jpg'
|
||||
})
|
||||
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.data?.description).toBe('这是一张美丽的风景照片')
|
||||
})
|
||||
|
||||
test('通过文件描述图片', async () => {
|
||||
const mockResponse = {
|
||||
status: true,
|
||||
msg: 'Image described successfully',
|
||||
data: {
|
||||
description: '这是一张人物照片'
|
||||
}
|
||||
}
|
||||
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse
|
||||
})
|
||||
|
||||
const mockFile = new File(['image content'], 'portrait.jpg', { type: 'image/jpeg' })
|
||||
const result = await textVideoAgentAPI.describeImageByFile({
|
||||
img_file: mockFile
|
||||
})
|
||||
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.data?.description).toBe('这是一张人物照片')
|
||||
})
|
||||
})
|
||||
|
||||
describe('高级功能测试', () => {
|
||||
test('端到端内容生成', async () => {
|
||||
// Mock 创建任务
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: true,
|
||||
data: { task_id: 'end-to-end-task' }
|
||||
})
|
||||
})
|
||||
|
||||
// Mock 图片生成
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: true,
|
||||
data: { image_url: 'https://example.com/generated.jpg' }
|
||||
})
|
||||
})
|
||||
|
||||
// Mock 视频生成
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: true,
|
||||
data: { video_url: 'https://example.com/generated.mp4' }
|
||||
})
|
||||
})
|
||||
|
||||
const result = await textVideoAgentAPI.generateContentEndToEnd(
|
||||
'测试端到端生成',
|
||||
{
|
||||
taskType: TaskType.VLOG,
|
||||
aspectRatio: AspectRatio.PORTRAIT,
|
||||
videoDuration: VideoDuration.MEDIUM,
|
||||
generateVideo: true
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
imageUrl: 'https://example.com/generated.jpg',
|
||||
videoUrl: 'https://example.com/generated.mp4',
|
||||
taskId: 'end-to-end-task'
|
||||
})
|
||||
})
|
||||
|
||||
test('轮询任务直到完成', async () => {
|
||||
// 第一次查询 - 进行中
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: true,
|
||||
data: { status: 'running', progress: 50 }
|
||||
})
|
||||
})
|
||||
|
||||
// 第二次查询 - 完成
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: true,
|
||||
data: {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
result: { image_url: 'https://example.com/final.jpg' }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const progressCallback = jest.fn()
|
||||
|
||||
const result = await textVideoAgentAPI.pollTaskUntilComplete(
|
||||
'test-task-id',
|
||||
100, // 快速轮询用于测试
|
||||
5000, // 5秒超时
|
||||
progressCallback
|
||||
)
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(2)
|
||||
expect(progressCallback).toHaveBeenCalledTimes(2)
|
||||
expect(result.data?.status).toBe('completed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('错误处理测试', () => {
|
||||
test('网络错误处理', async () => {
|
||||
;(fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
await expect(textVideoAgentAPI.healthCheck()).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
test('HTTP错误处理', async () => {
|
||||
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: async () => ({ error: 'Internal server error' })
|
||||
})
|
||||
|
||||
await expect(textVideoAgentAPI.healthCheck()).rejects.toThrow('HTTP error! status: 500')
|
||||
})
|
||||
|
||||
test('参数验证', async () => {
|
||||
await expect(
|
||||
textVideoAgentAPI.describeImageByUrl({} as any)
|
||||
).rejects.toThrow('image_url is required')
|
||||
|
||||
await expect(
|
||||
textVideoAgentAPI.describeImageByFile({} as any)
|
||||
).rejects.toThrow('img_file is required')
|
||||
})
|
||||
})
|
||||
|
||||
describe('自定义配置测试', () => {
|
||||
test('自定义API端点', () => {
|
||||
const customAPI = new TextVideoAgentAPI('https://custom-endpoint.com')
|
||||
expect(customAPI).toBeInstanceOf(TextVideoAgentAPI)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 集成测试(需要真实API)
|
||||
describe('集成测试 (需要真实API)', () => {
|
||||
// 这些测试需要真实的API端点,通常在CI/CD环境中跳过
|
||||
const shouldRunIntegrationTests = process.env.RUN_INTEGRATION_TESTS === 'true'
|
||||
|
||||
test.skipIf(!shouldRunIntegrationTests)('真实健康检查', async () => {
|
||||
const result = await textVideoAgentAPI.healthCheck()
|
||||
expect(result.status).toBe(true)
|
||||
})
|
||||
|
||||
test.skipIf(!shouldRunIntegrationTests)('真实图片生成', async () => {
|
||||
const result = await textVideoAgentAPI.generateImageSync({
|
||||
prompt: '一朵美丽的花',
|
||||
max_wait_time: 60
|
||||
})
|
||||
|
||||
expect(result.status).toBe(true)
|
||||
expect(result.data?.image_url).toBeDefined()
|
||||
}, 120000) // 2分钟超时
|
||||
})
|
||||
|
||||
export {}
|
||||
Reference in New Issue
Block a user