批量处理未剪辑的视频

This commit is contained in:
qikongjian 2025-09-29 10:48:25 +08:00
parent 967fe95ccc
commit 88fa136ad7
4 changed files with 1016 additions and 0 deletions

309
scripts/README.md Normal file
View File

@ -0,0 +1,309 @@
# 批量视频导出脚本
这个脚本用于批量处理项目ID生成剪辑计划并调用导出接口实现自动化的视频处理流程。
## 功能特性
- ✅ 批量处理多个项目ID
- ✅ 自动生成剪辑计划(调用 `/edit-plan/generate-by-project` 接口)
- ✅ 自动调用导出接口(调用 `/api/export/stream` 接口)
- ✅ 支持并发处理,提高效率
- ✅ 完整的错误处理和重试机制
- ✅ 实时进度跟踪和日志记录
- ✅ 生成详细的处理报告
- ✅ 支持从命令行或文件读取项目ID
## 文件说明
- `batch-export.js` - JavaScript版本的主脚本推荐使用
- `batch-video-export.ts` - TypeScript版本的主脚本
- `batch-config.example.env` - 配置文件示例
- `projects.example.txt` - 项目ID列表文件示例
## 快速开始
### 1. 配置环境变量
复制配置文件并填入实际值:
```bash
cp scripts/batch-config.example.env scripts/batch-config.env
```
编辑 `batch-config.env` 文件:
```env
# API配置
API_BASE_URL=https://your-api-domain.com
AUTH_TOKEN=your-actual-auth-token
USER_ID=your-actual-user-id
# 处理配置
CONCURRENCY=3 # 并发处理数量
MAX_RETRIES=3 # 最大重试次数
RETRY_DELAY=5000 # 重试间隔(毫秒)
# 导出配置
EXPORT_QUALITY=standard # 导出质量: standard | high | ultra
# 输出配置
OUTPUT_DIR=./batch-export-output # 输出目录
```
### 2. 准备项目ID列表
**方法一:命令行参数**
```bash
node scripts/batch-export.js --projects "project-001,project-002,project-003"
```
**方法二:文件列表**
创建项目ID文件
```bash
cp scripts/projects.example.txt scripts/projects.txt
```
编辑 `projects.txt` 文件:
```text
project-001
project-002
project-003
project-004
# project-005 # 注释行会被忽略
```
然后运行:
```bash
node scripts/batch-export.js --file scripts/projects.txt
```
### 3. 运行脚本
加载环境变量并运行:
```bash
# 加载配置文件
source scripts/batch-config.env
# 运行脚本
node scripts/batch-export.js --projects "project-001,project-002"
```
或者一次性运行:
```bash
API_BASE_URL=https://your-api.com AUTH_TOKEN=your-token node scripts/batch-export.js --projects "project-001,project-002"
```
## 使用方法
### 命令行选项
```bash
# 使用项目ID参数
node scripts/batch-export.js --projects "id1,id2,id3"
# 使用文件列表
node scripts/batch-export.js --file projects.txt
```
### 环境变量配置
| 变量名 | 说明 | 默认值 | 必填 |
|--------|------|--------|------|
| `API_BASE_URL` | API基础URL | - | ✅ |
| `AUTH_TOKEN` | 认证Token | - | ✅ |
| `USER_ID` | 用户ID | - | ✅ |
| `CONCURRENCY` | 并发处理数量 | 3 | ❌ |
| `MAX_RETRIES` | 最大重试次数 | 3 | ❌ |
| `RETRY_DELAY` | 重试间隔(毫秒) | 5000 | ❌ |
| `EXPORT_QUALITY` | 导出质量 | standard | ❌ |
| `OUTPUT_DIR` | 输出目录 | ./batch-export-output | ❌ |
## 工作流程
脚本会按以下步骤处理每个项目:
1. **生成剪辑计划**
- 调用 `/edit-plan/generate-by-project` 接口
- 支持自动重试最多10分钟8秒间隔
- 等待剪辑计划生成完成
2. **构建导出请求**
- 解析剪辑计划中的时间线信息
- 构建符合API规范的导出请求数据
- 包含视频片段、字幕、转场等信息
3. **调用导出接口**
- 调用 `/api/export/stream` 流式导出接口
- 实时处理SSE事件流
- 监控导出进度
4. **轮询导出状态**
- 如果SSE未返回完整结果自动轮询进度
- 调用 `/api/export/task/{taskId}/progress` 接口
- 等待导出完成并获取视频URL
## 输出文件
脚本运行后会在输出目录生成以下文件:
- `batch-export-{timestamp}.log` - 详细日志文件
- `batch-report-{timestamp}.json` - 处理结果报告
### 报告格式示例
```json
{
"timestamp": "2023-12-07T10:30:00.000Z",
"config": {
"concurrency": 3,
"maxRetries": 3,
"exportQuality": "standard"
},
"results": {
"total": 5,
"completed": 4,
"failed": 1,
"errors": [
{
"projectId": "project-005",
"error": "剪辑计划生成失败"
}
]
},
"projects": [
{
"projectId": "project-001",
"status": "completed",
"videoUrl": "https://example.com/video1.mp4",
"duration": 125.5
}
]
}
```
## 错误处理
脚本包含完善的错误处理机制:
### 自动重试
- 剪辑计划生成失败最多重试10分钟
- 导出接口调用失败:根据配置重试
- 网络错误:自动重试
### 错误类型
- **剪辑计划生成失败**API返回错误或超时
- **导出接口错误**:请求格式错误或服务器错误
- **网络连接错误**:网络不稳定或服务不可用
- **认证错误**Token无效或过期
### 故障恢复
- 单个项目失败不影响其他项目处理
- 详细错误日志帮助定位问题
- 支持断点续传(可以只处理失败的项目)
## 性能优化
### 并发控制
- 默认并发数为3可根据服务器性能调整
- 避免同时处理太多项目导致服务器压力
### 内存管理
- 流式处理SSE响应避免内存积累
- 及时释放不需要的数据
### 网络优化
- 合理的重试间隔,避免频繁请求
- 长连接处理SSE流
## 故障排除
### 常见问题
1. **认证失败**
```
错误HTTP 401: Unauthorized
解决:检查 AUTH_TOKEN 是否正确
```
2. **API地址错误**
```
错误ENOTFOUND your-api-domain.com
解决:检查 API_BASE_URL 是否正确
```
3. **剪辑计划生成超时**
```
错误获取剪辑计划超时已重试75次
解决:检查项目状态,可能需要更长等待时间
```
4. **Node.js版本问题**
```
错误fetch is not defined
解决:升级到 Node.js 18+ 或安装 node-fetch
```
### 调试技巧
1. **查看详细日志**
```bash
tail -f batch-export-output/batch-export-*.log
```
2. **测试单个项目**
```bash
node scripts/batch-export.js --projects "single-project-id"
```
3. **检查API连通性**
```bash
curl -H "Authorization: Bearer $AUTH_TOKEN" $API_BASE_URL/health
```
## 高级用法
### 自定义配置
可以通过修改脚本中的配置对象来自定义更多选项:
```javascript
const config = {
apiBaseUrl: process.env.API_BASE_URL,
token: process.env.AUTH_TOKEN,
// 自定义超时时间
requestTimeout: 30000,
// 自定义User-Agent
userAgent: 'BatchVideoExporter/1.0',
// 其他配置...
};
```
### 集成到CI/CD
可以将脚本集成到自动化流程中:
```yaml
# GitHub Actions 示例
- name: Batch Export Videos
env:
API_BASE_URL: ${{ secrets.API_BASE_URL }}
AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }}
USER_ID: ${{ secrets.USER_ID }}
run: |
node scripts/batch-export.js --file projects.txt
```
## 注意事项
1. **资源使用**:批量处理会消耗较多服务器资源,建议在低峰期运行
2. **网络稳定性**:确保网络连接稳定,避免长时间处理中断
3. **存储空间**:确保有足够的存储空间保存日志和报告文件
4. **API限制**注意API的调用频率限制避免被限流
5. **数据备份**:重要项目建议先备份,避免处理过程中数据丢失
## 许可证
MIT License

606
scripts/batch-export.js Normal file
View File

@ -0,0 +1,606 @@
#!/usr/bin/env node
/**
* 批量视频导出脚本 - JavaScript版本
* 用于批量处理项目ID生成剪辑计划并调用导出接口
*
* 使用方法:
* node scripts/batch-export.js --projects "project1,project2,project3"
* 或者
* node scripts/batch-export.js --file projects.txt
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
/**
* 批量视频导出处理器
*/
class BatchVideoExporter {
constructor(config) {
this.config = config;
this.projectStatuses = new Map();
this.logFile = path.join(config.outputDir, `batch-export-${Date.now()}.log`);
// 确保输出目录存在
if (!fs.existsSync(config.outputDir)) {
fs.mkdirSync(config.outputDir, { recursive: true });
}
}
/** 记录日志 */
log(message, level = 'info') {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
console.log(logMessage);
fs.appendFileSync(this.logFile, logMessage + '\n');
}
/** HTTP请求封装 */
async makeRequest(endpoint, data = null, method = 'POST') {
return new Promise((resolve, reject) => {
const url = new URL(`${this.config.apiBaseUrl}${endpoint}`);
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
const options = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.token}`,
'X-EASE-ADMIN-TOKEN': this.config.token,
}
};
const req = httpModule.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(responseData));
} catch (e) {
resolve(responseData);
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
if (data && method === 'POST') {
req.write(JSON.stringify(data));
}
req.end();
});
}
/** 生成剪辑计划 */
async generateEditPlan(projectId) {
this.log(`开始为项目 ${projectId} 生成剪辑计划...`);
const maxRetryTime = 10 * 60 * 1000; // 10分钟
const retryInterval = 8 * 1000; // 8秒
const maxAttempts = Math.floor(maxRetryTime / retryInterval);
let attempts = 0;
while (attempts < maxAttempts) {
attempts++;
try {
this.log(`项目 ${projectId}: 第${attempts}次尝试获取剪辑计划...`);
const response = await this.makeRequest('/edit-plan/generate-by-project', {
project_id: projectId
});
if (response.code === 0 && response.data && response.data.success && response.data.editing_plan) {
this.log(`项目 ${projectId}: 剪辑计划生成成功`);
return response.data.editing_plan;
}
if (attempts >= maxAttempts) {
throw new Error(`剪辑计划生成失败: ${response.message || '未知错误'}`);
}
this.log(`项目 ${projectId}: 第${attempts}次尝试失败,${retryInterval/1000}秒后重试...`);
await new Promise(resolve => setTimeout(resolve, retryInterval));
} catch (error) {
if (attempts >= maxAttempts) {
throw new Error(`获取剪辑计划失败,已重试${maxAttempts}次: ${error.message}`);
}
this.log(`项目 ${projectId}: 第${attempts}次尝试出现错误: ${error.message}`);
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
}
throw new Error(`获取剪辑计划超时,已重试${maxAttempts}`);
}
/** 解析时间码为毫秒 */
parseTimecodeToMs(timecode) {
const parts = timecode.split(':');
if (parts.length !== 3) return 0;
const hours = parseInt(parts[0]) || 0;
const minutes = parseInt(parts[1]) || 0;
const secondsParts = parts[2].split('.');
const seconds = parseInt(secondsParts[0]) || 0;
const milliseconds = parseInt((secondsParts[1] || '').padEnd(3, '0').slice(0, 3)) || 0;
return Math.round((hours * 3600 + minutes * 60 + seconds) * 1000 + milliseconds);
}
/** 构建导出请求数据 */
buildExportRequest(projectId, editingPlan) {
this.log(`项目 ${projectId}: 构建导出请求数据...`);
const defaultClipDuration = 8000; // 8秒
let currentStartTime = 0;
const videoElements = [];
// 处理剪辑计划中的时间线信息
if (editingPlan.editing_sequence_plans && editingPlan.editing_sequence_plans.length > 0) {
const timelineClips = editingPlan.editing_sequence_plans[0].timeline_clips || [];
this.log(`项目 ${projectId}: 使用剪辑计划中的 ${timelineClips.length} 个时间线片段`);
timelineClips.forEach((clip, index) => {
const sequenceStartMs = this.parseTimecodeToMs(clip.sequence_start_timecode || "00:00:00.000");
const sourceInMs = this.parseTimecodeToMs(clip.source_in_timecode || "00:00:00.000");
const sourceOutMs = this.parseTimecodeToMs(clip.source_out_timecode || "00:00:08.000");
const clipDurationMs = this.parseTimecodeToMs(clip.clip_duration_in_sequence || "00:00:08.000");
const element = {
id: clip.sequence_clip_id || `video_${index + 1}`,
src: clip.video_url,
start: currentStartTime,
end: currentStartTime + clipDurationMs,
in: sourceInMs,
out: sourceOutMs,
_source_type: (clip.video_url && clip.video_url.startsWith('http')) ? 'remote_url' : 'local'
};
videoElements.push(element);
currentStartTime += clipDurationMs;
});
}
const totalDuration = currentStartTime || defaultClipDuration;
// 处理字幕
const texts = [];
if (editingPlan.finalized_dialogue_track && editingPlan.finalized_dialogue_track.final_dialogue_segments) {
editingPlan.finalized_dialogue_track.final_dialogue_segments.forEach((dialogue, index) => {
texts.push({
id: dialogue.sequence_clip_id || `text_${index + 1}`,
text: dialogue.transcript,
start: this.parseTimecodeToMs(dialogue.start_timecode || "00:00:00.000"),
end: this.parseTimecodeToMs(dialogue.end_timecode || "00:00:02.000"),
style: {
fontFamily: 'Arial',
fontSize: 40,
color: '#FFFFFF',
backgroundColor: 'transparent',
fontWeight: 'normal',
fontStyle: 'normal',
align: 'center',
shadow: true
}
});
});
}
// 构建导出请求
const exportRequest = {
project_id: projectId,
ir: {
width: 1920,
height: 1080,
fps: 30,
duration: totalDuration,
video: videoElements,
texts: texts,
audio: [],
transitions: []
},
options: {
quality: this.config.exportQuality,
codec: 'h264',
subtitleMode: 'hard'
}
};
this.log(`项目 ${projectId}: 导出请求数据构建完成,视频片段: ${videoElements.length}, 字幕片段: ${texts.length}`);
return exportRequest;
}
/** 调用导出流接口 */
async callExportStream(exportRequest) {
const projectId = exportRequest.project_id;
this.log(`项目 ${projectId}: 开始调用导出流接口...`);
// 使用fetch APINode.js 18+支持)
let fetch;
try {
fetch = globalThis.fetch;
} catch {
// 如果没有fetch使用node-fetch或提示升级Node.js
throw new Error('需要Node.js 18+或安装node-fetch包');
}
const response = await fetch(`${this.config.exportApiBaseUrl}/api/export/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
'Authorization': `Bearer ${this.config.token}`,
'X-EASE-ADMIN-TOKEN': this.config.token,
},
body: JSON.stringify(exportRequest)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`导出接口错误: ${response.status} ${errorText}`);
}
this.log(`项目 ${projectId}: 导出接口调用成功开始处理SSE流...`);
// 处理SSE流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder();
let finalResult = null;
let detectedTaskId = null;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const eventData = JSON.parse(line.slice(6));
// 提取任务ID
if (eventData.export_id || eventData.task_id) {
detectedTaskId = eventData.export_id || eventData.task_id;
}
// 处理不同类型的事件
switch (eventData.type) {
case 'start':
this.log(`项目 ${projectId}: 导出开始 - ${eventData.message}`);
if (eventData.export_id || eventData.task_id) {
detectedTaskId = eventData.export_id || eventData.task_id;
}
break;
case 'progress':
const progressPercent = Math.round((eventData.progress || 0) * 100);
this.log(`项目 ${projectId}: 导出进度 ${progressPercent}% - ${eventData.stage || 'processing'} - ${eventData.message}`);
break;
case 'complete':
this.log(`项目 ${projectId}: 导出完成!`);
finalResult = eventData;
if (detectedTaskId && !finalResult.export_id && !finalResult.task_id) {
finalResult.export_id = detectedTaskId;
}
return finalResult;
case 'error':
throw new Error(`导出失败: ${eventData.message}`);
}
} catch (parseError) {
this.log(`项目 ${projectId}: 解析SSE事件失败: ${line}`, 'warn');
}
}
}
}
} finally {
reader.releaseLock();
}
return finalResult;
}
/** 轮询导出进度 */
async pollExportProgress(taskId, projectId) {
this.log(`项目 ${projectId}: 开始轮询导出进度任务ID: ${taskId}`);
const maxAttempts = 120; // 最多轮询10分钟
let attempts = 0;
while (attempts < maxAttempts) {
try {
// 使用导出API基础URL进行轮询
const progressUrl = `${this.config.exportApiBaseUrl}/api/export/task/${taskId}/progress`;
const progressResponse = await fetch(progressUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.token}`,
'X-EASE-ADMIN-TOKEN': this.config.token,
}
});
if (!progressResponse.ok) {
throw new Error(`进度查询失败: ${progressResponse.status} ${progressResponse.statusText}`);
}
const response = await progressResponse.json();
const { status, progress } = response;
if (status === 'completed') {
this.log(`项目 ${projectId}: 导出任务完成视频URL: ${progress && progress.video_url}`);
return {
task_id: taskId,
status: status,
video_url: progress && progress.video_url,
file_size: progress && progress.file_size,
export_id: progress && progress.export_id
};
} else if (status === 'failed') {
throw new Error(`导出任务失败: ${(progress && progress.message) || '未知错误'}`);
} else if (status === 'error') {
throw new Error(`导出任务错误: ${(progress && progress.message) || '未知错误'}`);
} else {
const percentage = (progress && progress.percentage) || 0;
const message = (progress && progress.message) || '处理中...';
this.log(`项目 ${projectId}: 导出进度 ${percentage}% - ${message}`);
}
attempts++;
await new Promise(resolve => setTimeout(resolve, 5000)); // 5秒间隔
} catch (error) {
this.log(`项目 ${projectId}: 轮询进度出错: ${error.message}`, 'error');
attempts++;
if (attempts >= maxAttempts) {
throw error;
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
throw new Error(`轮询超时,已尝试${maxAttempts}`);
}
/** 处理单个项目 */
async processProject(projectId) {
const status = {
projectId,
status: 'pending',
editPlanGenerated: false,
exportStarted: false,
exportCompleted: false,
attempts: 0,
startTime: Date.now()
};
this.projectStatuses.set(projectId, status);
try {
// 1. 生成剪辑计划
status.status = 'generating_plan';
this.log(`项目 ${projectId}: 开始处理...`);
const editingPlan = await this.generateEditPlan(projectId);
status.editPlanGenerated = true;
this.log(`项目 ${projectId}: 剪辑计划生成完成`);
// 2. 构建导出请求
const exportRequest = this.buildExportRequest(projectId, editingPlan);
// 3. 调用导出接口
status.status = 'exporting';
status.exportStarted = true;
let exportResult = await this.callExportStream(exportRequest);
// 4. 如果SSE没有返回完整结果使用轮询
if (!exportResult || !exportResult.video_url) {
const taskId = (exportResult && exportResult.export_id) || (exportResult && exportResult.task_id);
if (taskId) {
exportResult = await this.pollExportProgress(taskId, projectId);
} else {
throw new Error('无法获取任务ID无法轮询进度');
}
}
// 5. 处理完成
status.status = 'completed';
status.exportCompleted = true;
status.videoUrl = exportResult.video_url;
status.endTime = Date.now();
this.log(`项目 ${projectId}: 处理完成视频URL: ${exportResult.video_url}`);
this.log(`项目 ${projectId}: 总耗时: ${((status.endTime - status.startTime) / 1000).toFixed(2)}`);
} catch (error) {
status.status = 'failed';
status.error = error.message;
status.endTime = Date.now();
this.log(`项目 ${projectId}: 处理失败: ${status.error}`, 'error');
throw error;
}
}
/** 批量处理项目 */
async processProjects(projectIds) {
this.log(`开始批量处理 ${projectIds.length} 个项目...`);
this.log(`配置: 并发数=${this.config.concurrency}, 最大重试=${this.config.maxRetries}`);
const results = {
total: projectIds.length,
completed: 0,
failed: 0,
errors: []
};
// 分批并发处理
for (let i = 0; i < projectIds.length; i += this.config.concurrency) {
const batch = projectIds.slice(i, i + this.config.concurrency);
this.log(`处理第 ${Math.floor(i/this.config.concurrency) + 1} 批,项目: ${batch.join(', ')}`);
const promises = batch.map(async (projectId) => {
let attempts = 0;
while (attempts < this.config.maxRetries) {
try {
await this.processProject(projectId);
results.completed++;
break;
} catch (error) {
attempts++;
const errorMsg = error.message;
if (attempts >= this.config.maxRetries) {
this.log(`项目 ${projectId}: 达到最大重试次数,放弃处理`, 'error');
results.failed++;
results.errors.push({ projectId, error: errorMsg });
} else {
this.log(`项目 ${projectId}: 第${attempts}次尝试失败,${this.config.retryDelay/1000}秒后重试...`, 'warn');
await new Promise(resolve => setTimeout(resolve, this.config.retryDelay));
}
}
}
});
await Promise.all(promises);
}
// 生成处理报告
this.generateReport(results);
}
/** 生成处理报告 */
generateReport(results) {
const reportPath = path.join(this.config.outputDir, `batch-report-${Date.now()}.json`);
const report = {
timestamp: new Date().toISOString(),
config: {
concurrency: this.config.concurrency,
maxRetries: this.config.maxRetries,
exportQuality: this.config.exportQuality
},
results: results,
projects: Array.from(this.projectStatuses.values()).map(status => ({
projectId: status.projectId,
status: status.status,
videoUrl: status.videoUrl,
error: status.error,
duration: status.endTime && status.startTime ?
((status.endTime - status.startTime) / 1000) : null
}))
};
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
this.log(`\n=== 批量处理完成 ===`);
this.log(`总项目数: ${results.total}`);
this.log(`成功: ${results.completed}`);
this.log(`失败: ${results.failed}`);
this.log(`处理报告: ${reportPath}`);
this.log(`详细日志: ${this.logFile}`);
if (results.errors.length > 0) {
this.log(`\n失败项目:`);
results.errors.forEach((error) => {
this.log(` - ${error.projectId}: ${error.error}`, 'error');
});
}
}
}
/** 从命令行参数解析项目ID列表 */
function parseProjectIds() {
const args = process.argv.slice(2);
const projectIds = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === '--projects' && i + 1 < args.length) {
// 从命令行参数解析
projectIds.push(...args[i + 1].split(',').map(id => id.trim()).filter(Boolean));
} else if (args[i] === '--file' && i + 1 < args.length) {
// 从文件读取
const filePath = args[i + 1];
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
projectIds.push(...content.split('\n').map(id => id.trim()).filter(id => id && !id.startsWith('#')));
} else {
console.error(`文件不存在: ${filePath}`);
process.exit(1);
}
}
}
if (projectIds.length === 0) {
console.error('请提供项目ID列表:');
console.error(' 使用 --projects "id1,id2,id3"');
console.error(' 或者 --file projects.txt');
process.exit(1);
}
return projectIds;
}
/** 主函数 */
async function main() {
try {
// 解析项目ID列表
const projectIds = parseProjectIds();
// 配置参数(可以通过环境变量或配置文件自定义)
const config = {
apiBaseUrl: process.env.API_BASE_URL || 'https://api.video.movieflow.ai',
exportApiBaseUrl: process.env.EXPORT_API_BASE_URL || 'https://smartcut.api.movieflow.ai',
token: process.env.AUTH_TOKEN || 'your-auth-token',
userId: process.env.USER_ID || 'your-user-id',
concurrency: parseInt(process.env.CONCURRENCY || '3'),
maxRetries: parseInt(process.env.MAX_RETRIES || '3'),
retryDelay: parseInt(process.env.RETRY_DELAY || '5000'),
exportQuality: process.env.EXPORT_QUALITY || 'standard',
outputDir: process.env.OUTPUT_DIR || './batch-export-output'
};
console.log(`开始批量处理 ${projectIds.length} 个项目...`);
console.log(`项目列表: ${projectIds.join(', ')}`);
// 创建处理器并执行
const exporter = new BatchVideoExporter(config);
await exporter.processProjects(projectIds);
} catch (error) {
console.error('批量处理失败:', error);
process.exit(1);
}
}
// 运行主函数
if (require.main === module) {
main();
}
module.exports = { BatchVideoExporter };

View File

@ -0,0 +1,74 @@
# 项目ID列表示例文件
# 每行一个项目ID以#开头的行为注释
107c5fcc-8348-4c3b-b9f3-f7474e24295d
6243a491-d8cd-46cf-8d11-933300f27569
2b296c0f-dcc3-4141-8977-b9f3579bdb04
2238ca87-1c6a-484a-9405-9e25d0999072
481e672d-3a00-454f-bee0-71797d40ea17
de805e6c-2374-4a5d-9f8a-f0b6baa3fad5
64f137e0-06e5-4d30-86e7-28c6b8644de1
8a132503-456a-462f-83e3-367636cd0286
8d81241b-ae7c-431a-b953-85cfd6af77f3
98b53d49-a02d-4fed-aa4e-62b6cc45fae8
f23d558c-6f18-43f3-a373-8e3d1e8d278a
12daaba2-79c0-4fe8-bdc8-5b746b6bee3b
6b7ca8b9-5760-4908-aac6-b7388402652d
19db2b3e-9fb7-408c-90d5-35f9969b3d01
89c20909-4ebf-4ccc-a5e7-401ef91ef242
4de9ef61-cefd-4027-b07e-a2520a62f218
d2e23eaf-e806-44c9-a241-7fc1af53dab4
64941470-7689-4ea1-872c-672f2b5e4628
469c5792-aa1a-4489-bfea-779d3782218c
7c55bd8e-8f16-4caa-a001-1a34497a9711
86dcc7cf-f116-429f-b298-fe0878aa25ef
ded3ed74-b6fa-48fd-ad7e-fbfa717d54f5
96da7e21-7284-4056-900e-59835e99b651
892083d2-a7e7-4436-aac9-91cfdf24feae
52543696-fbe0-4fd9-bbe2-ef31e642fb8e
675ae612-d6ac-4017-98a6-ac9c4fd4eac9
e430dfe1-3cb8-40bd-a6a9-f6b223052ff7
3b12c5d3-7899-4ef6-97c4-8ff07ee5bc8e
35abfaba-d5b8-4caf-bbb7-89feaa007415
71d52c66-b83b-4d51-91a2-be26f88f9eba
8013eeb8-6938-43e1-b5e1-91f68d860eb8
30c8582f-5231-4a31-af08-5bba4b6171f3
0cece1ba-ea3b-43cb-a7f2-10ce0de96936
9a0e2fc9-73c0-43b5-b3d2-d6c1491cf9e5
8659867f-0887-4a59-a5e4-22c1db88fff1
f24bc59e-acbe-40a4-9383-c2d6f238475f
530500f8-8320-4bef-a7d8-7f1114c69a16
8d926de6-4089-49a8-a58b-c71c9c0b9e87
c7a08757-8c78-437c-9a75-10298fbd58e3
af559e8b-2c36-4d21-a60d-3f6e8c9ce9a1
2971aaa9-2d9f-46cb-b09b-e3d8ad0ba9de
63a03433-f1df-4e0f-99f3-58933ee7fe8e
92e0944d-183a-4e42-aad1-c54f2a70a29b
160493c9-235b-4d75-ba59-d97cd41d7bff
3ffeffe3-0191-47a8-8112-bda7bac5c983
1b433f0d-bf02-449d-bb51-0b51ee4ffee9
a563afec-afe3-4eca-b347-861bc6e00a82
3d66b6ff-80ec-439b-a0f8-ffcd31663166
1e5d52d3-c3b4-46c1-b555-8d507cd4b81f
ecae91fc-bd4a-4f3c-a086-a2f8970e2fc0
5c6ca83f-3a32-45ff-baad-0a2036bf2d35
8d725266-f62f-4e1e-984a-17414f8ca937
4e200654-5af5-448a-bac2-9f421cde1272
8574e0a4-10b9-494b-ab7f-6544197480d6
4c1182a2-13cb-4422-a551-b89dc2cc1f0c
f42ad2b3-1f29-45b1-9b25-ba3eed23b03c
e923af63-0df2-4609-b3fa-2a19232f26ae
4e468c8b-1ba3-4fa7-bfc9-2d96aff78d32
57a82669-5fcc-4289-be8a-9179cf535aa1
49915888-c999-4d0c-9504-98146ae2fea1
001c33b6-fefb-4807-b0ef-2c332bd881ca
d963c23c-a5b6-4b43-a6f1-7d801ea7bf34
8e879443-1a98-4a1f-811a-4c98cb1d6e60
d291dc06-15de-49d2-a140-6eef8da8de22
2f7b5b56-e20e-4b29-9e09-6ca9b4dcee1b
5ad180ae-c4a6-435a-8f94-2ae0e081c91f
475f90f4-2a02-4e0b-aaa2-eae68ee4c6ac
9d609d66-51d0-4392-9023-96172eaa94ca
3c46b89d-44b1-47fd-ac2a-61c0b439bc27
35be5718-1036-44e3-89a5-d8431bcb3b50

View File

@ -0,0 +1,27 @@
{
"timestamp": "2025-09-28T15:12:31.624Z",
"config": {
"concurrency": 1,
"maxRetries": 3,
"exportQuality": "standard"
},
"results": {
"total": 1,
"completed": 0,
"failed": 1,
"errors": [
{
"projectId": "107c5fcc-8348-4c3b-b9f3-f7474e24295d",
"error": "无法获取任务ID无法轮询进度"
}
]
},
"projects": [
{
"projectId": "107c5fcc-8348-4c3b-b9f3-f7474e24295d",
"status": "failed",
"error": "无法获取任务ID无法轮询进度",
"duration": 6.201
}
]
}