添加 文生图

This commit is contained in:
root
2025-07-11 13:47:27 +08:00
parent e2ffc87d53
commit fb5cf0f8f1
12 changed files with 3229 additions and 5 deletions

View 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 或联系开发团队。

View 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)
}

View File

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

View File

@@ -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' },

View File

@@ -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: '项目库' },

View 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

View File

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

View 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
}
}

View 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

View 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

View 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

View 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 {}