const express = require('express'); const cors = require('cors'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const https = require('https'); const http = require('http'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 7860; // 中间件配置 app.use(cors()); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); app.use(express.static(path.join(__dirname, 'public'))); // API key 存储文件路径 const API_KEY_FILE = path.join(__dirname, 'stored_api_key.json'); // 视频存储目录 const VIDEO_DIR = path.join(__dirname, 'video'); // 文件上传配置 const storage = multer.diskStorage({ destination: (req, file, cb) => { const uploadDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } cb(null, uploadDir); }, filename: (req, file, cb) => { cb(null, Date.now() + '-' + file.originalname); } }); const upload = multer({ storage: storage, limits: { fileSize: 50 * 1024 * 1024 } }); // 读取存储的 API key function getStoredApiKey() { try { if (fs.existsSync(API_KEY_FILE)) { const data = fs.readFileSync(API_KEY_FILE, 'utf8'); const parsed = JSON.parse(data); return parsed.apiKey || null; } } catch (error) { console.log('读取存储的 API key 失败:', error.message); } return null; } // 保存 API key function saveApiKey(apiKey) { try { const data = { apiKey, savedAt: new Date().toISOString() }; fs.writeFileSync(API_KEY_FILE, JSON.stringify(data, null, 2)); return true; } catch (error) { console.error('保存 API key 失败:', error); return false; } } // 获取有效的 API key function getValidApiKey(userApiKey = null) { // 优先级:用户提供的 > 环境变量 > 存储的 return userApiKey || process.env.FAL_KEY || getStoredApiKey(); } // 下载并保存视频文件 function downloadVideo(videoUrl, filename) { return new Promise((resolve, reject) => { // 确保视频目录存在 if (!fs.existsSync(VIDEO_DIR)) { fs.mkdirSync(VIDEO_DIR, { recursive: true }); } const filepath = path.join(VIDEO_DIR, filename); const file = fs.createWriteStream(filepath); const protocol = videoUrl.startsWith('https:') ? https : http; protocol.get(videoUrl, (response) => { if (response.statusCode !== 200) { reject(new Error(`下载失败,状态码: ${response.statusCode}`)); return; } response.pipe(file); file.on('finish', () => { file.close(); console.log(`视频已保存到: ${filepath}`); resolve(filepath); }); file.on('error', (err) => { fs.unlink(filepath, () => {}); // 删除不完整的文件 reject(err); }); }).on('error', (err) => { reject(err); }); }); } // 生成唯一的文件名 function generateVideoFilename(type = 'video') { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); return `${type}-${timestamp}.mp4`; } // FAL AI 客户端初始化 let fal; try { fal = require('@fal-ai/client'); // 如果是对象形式的导入,尝试获取 fal 属性 if (fal && typeof fal === 'object' && fal.fal) { fal = fal.fal; } } catch (error) { console.error('FAL AI 客户端初始化失败:', error); } // 路由 app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // API key 管理接口 app.post('/api/save-key', (req, res) => { const { apiKey } = req.body; if (!apiKey || !apiKey.trim()) { return res.status(400).json({ error: 'API key 不能为空' }); } if (saveApiKey(apiKey.trim())) { res.json({ success: true, message: 'API key 保存成功' }); } else { res.status(500).json({ error: 'API key 保存失败' }); } }); app.get('/api/check-key', (req, res) => { const storedKey = getStoredApiKey(); const hasEnvKey = !!process.env.FAL_KEY; res.json({ hasStoredKey: !!storedKey, hasEnvKey: hasEnvKey, keySource: hasEnvKey ? 'environment' : (storedKey ? 'stored' : 'none') }); }); // 图片到视频生成 API(支持模型切换:WAN v2.2-a14b 与 Bytedance Seedance 1.0 Pro Fast) app.post('/api/image-to-video', upload.single('image'), async (req, res) => { try { const userApiKey = req.body.userApiKey; const apiKey = getValidApiKey(userApiKey); if (!apiKey) { return res.status(400).json({ error: '请提供 FAL AI API 密钥' }); } const prompt = req.body.prompt; if (!prompt) { return res.status(400).json({ error: '请提供文本提示' }); } // 解析图片输入(文件或 URL) let image_url; if (req.file) { const file = fs.readFileSync(req.file.path); const blob = new Blob([file], { type: req.file.mimetype }); image_url = await fal.storage.upload(blob); } else if (req.body.image_url) { image_url = req.body.image_url; } else { return res.status(400).json({ error: '请提供图片文件或图片URL' }); } // 模型选择:默认 WAN,支持 seedance-pro-fast const model = req.body.model || 'wan-v2.2-a14b'; // 设置临时环境变量 const originalFalKey = process.env.FAL_KEY; process.env.FAL_KEY = apiKey; let result; if (model === 'seedance-pro-fast') { // Bytedance Seedance 1.0 Pro Fast 入参映射 const { aspect_ratio = 'auto', resolution = '1080p', duration = '5', camera_fixed = false, seed = -1, enable_safety_checker = true, } = req.body; result = await fal.subscribe("fal-ai/bytedance/seedance/v1/pro/fast/image-to-video", { input: { prompt, image_url, aspect_ratio, resolution, duration, camera_fixed: typeof camera_fixed === 'string' ? camera_fixed === 'true' : !!camera_fixed, seed: parseInt(seed, 10), enable_safety_checker: typeof enable_safety_checker === 'string' ? enable_safety_checker === 'true' : !!enable_safety_checker, }, logs: true, onQueueUpdate: (update) => { if (update.status === "IN_PROGRESS") { console.log('生成进度(Seedance I2V):', update.logs?.map(log => log.message).join('\n')); } }, }); } else { // WAN v2.2-a14b 入参映射(保持原逻辑) const { negative_prompt = "", num_frames = 81, frames_per_second = 16, resolution = "720p", aspect_ratio = "auto", video_quality = "high", enable_safety_checker = "true" } = req.body; result = await fal.subscribe("fal-ai/wan/v2.2-a14b/image-to-video", { input: { image_url, prompt, negative_prompt, num_frames: parseInt(num_frames), frames_per_second: parseInt(frames_per_second), resolution, aspect_ratio, video_quality, num_inference_steps: 27, enable_safety_checker: enable_safety_checker === "true", acceleration: "regular", guidance_scale: 3.5, shift: 5 }, logs: true, onQueueUpdate: (update) => { if (update.status === "IN_PROGRESS") { console.log('生成进度(WAN I2V):', update.logs?.map(log => log.message).join('\n')); } }, }); } // 恢复原始环境变量 if (originalFalKey) { process.env.FAL_KEY = originalFalKey; } // 下载并保存视频到本地 let localVideoPath = null; if (result.data && result.data.video && result.data.video.url) { try { const filename = generateVideoFilename('image-to-video'); localVideoPath = await downloadVideo(result.data.video.url, filename); console.log(`图片转视频完成,已保存到: ${localVideoPath}`); } catch (downloadError) { console.error('视频下载失败:', downloadError); // 不影响主要功能,继续返回结果 } } if (req.file && fs.existsSync(req.file.path)) { fs.unlinkSync(req.file.path); } res.json({ success: true, data: { ...result.data, localPath: localVideoPath ? path.relative(__dirname, localVideoPath) : null }, requestId: result.requestId }); } catch (error) { console.error('视频生成错误:', error); if (req.file && fs.existsSync(req.file.path)) { fs.unlinkSync(req.file.path); } res.status(500).json({ success: false, error: error.message || '视频生成失败' }); } }); // 文本到视频生成 API(支持模型切换:WAN v2.2-a14b 与 Bytedance Seedance 1.0 Pro Fast) app.post('/api/text-to-video', async (req, res) => { try { const userApiKey = req.body.userApiKey; const apiKey = getValidApiKey(userApiKey); if (!apiKey) { return res.status(400).json({ error: '请提供 FAL AI API 密钥' }); } const prompt = req.body.prompt; if (!prompt) { return res.status(400).json({ error: '请提供文本提示' }); } // 模型选择:默认 WAN,支持 seedance-pro-fast const model = req.body.model || 'wan-v2.2-a14b'; // 设置临时环境变量 const originalFalKey = process.env.FAL_KEY; process.env.FAL_KEY = apiKey; let result; if (model === 'seedance-pro-fast') { // Bytedance Seedance 1.0 Pro Fast 入参映射 const { aspect_ratio = '16:9', resolution = '1080p', duration = '5', camera_fixed = false, seed = -1, enable_safety_checker = true, } = req.body; result = await fal.subscribe("fal-ai/bytedance/seedance/v1/pro/fast/text-to-video", { input: { prompt, aspect_ratio, resolution, duration, camera_fixed: !!camera_fixed, seed: parseInt(seed, 10), enable_safety_checker: !!enable_safety_checker, }, logs: true, onQueueUpdate: (update) => { if (update.status === "IN_PROGRESS") { console.log('生成进度(Seedance):', update.logs?.map(log => log.message).join('\n')); } }, }); } else { // WAN v2.2-a14b 入参映射(保持原逻辑) const { negative_prompt = "", num_frames = 81, frames_per_second = 16, resolution = "720p", aspect_ratio = "16:9", video_quality = "high", enable_safety_checker = true } = req.body; result = await fal.subscribe("fal-ai/wan/v2.2-a14b/text-to-video", { input: { prompt, negative_prompt, num_frames: parseInt(num_frames), frames_per_second: parseInt(frames_per_second), resolution, aspect_ratio, video_quality, num_inference_steps: 27, enable_safety_checker: enable_safety_checker, acceleration: "regular", guidance_scale: 3.5, shift: 5 }, logs: true, onQueueUpdate: (update) => { if (update.status === "IN_PROGRESS") { console.log('生成进度(WAN):', update.logs?.map(log => log.message).join('\n')); } }, }); } // 恢复原始环境变量 if (originalFalKey) { process.env.FAL_KEY = originalFalKey; } // 下载并保存视频到本地 let localVideoPath = null; if (result.data && result.data.video && result.data.video.url) { try { const filename = generateVideoFilename('text-to-video'); localVideoPath = await downloadVideo(result.data.video.url, filename); console.log(`文本转视频完成,已保存到: ${localVideoPath}`); } catch (downloadError) { console.error('视频下载失败:', downloadError); // 不影响主要功能,继续返回结果 } } res.json({ success: true, data: { ...result.data, localPath: localVideoPath ? path.relative(__dirname, localVideoPath) : null }, requestId: result.requestId }); } catch (error) { console.error('视频生成错误:', error); res.status(500).json({ success: false, error: error.message || '视频生成失败' }); } }); // 获取本地视频列表 app.get('/api/videos', (req, res) => { try { if (!fs.existsSync(VIDEO_DIR)) { return res.json({ videos: [] }); } const files = fs.readdirSync(VIDEO_DIR) .filter(file => file.endsWith('.mp4')) .map(file => { const filepath = path.join(VIDEO_DIR, file); const stats = fs.statSync(filepath); return { filename: file, path: path.join('video', file), size: stats.size, created: stats.birthtime, modified: stats.mtime }; }) .sort((a, b) => new Date(b.created) - new Date(a.created)); // 按创建时间倒序 res.json({ videos: files }); } catch (error) { console.error('获取视频列表失败:', error); res.status(500).json({ error: '获取视频列表失败' }); } }); // 提供视频文件访问 app.use('/video', express.static(path.join(__dirname, 'video'))); // 测试 API 密钥连接(使用免费的健康检查端点) app.post('/api/test-key', async (req, res) => { try { const { apiKey } = req.body; const testKey = apiKey || getValidApiKey(); if (!testKey) { return res.status(400).json({ error: 'API 密钥未提供', success: false }); } // 使用 FAL AI 的免费健康检查端点测试连接 const https = require('https'); const testPromise = new Promise((resolve, reject) => { // 使用 FAL AI 的状态检查端点(不消耗配额) const options = { hostname: 'fal.run', port: 443, path: '/status', method: 'GET', headers: { 'Authorization': `Key ${testKey}`, 'User-Agent': 'FAL-Video-Generator/1.0' }, timeout: 5000 }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { if (res.statusCode === 401) { reject(new Error('Unauthorized - API 密钥无效')); } else if (res.statusCode === 403) { reject(new Error('Forbidden - API 密钥被禁用')); } else if (res.statusCode === 429) { reject(new Error('Rate Limited - 请求过于频繁')); } else if (res.statusCode >= 200 && res.statusCode < 300) { resolve({ statusCode: res.statusCode, data }); } else { // 其他状态码,但至少证明密钥被识别了 resolve({ statusCode: res.statusCode, data }); } }); }); req.on('error', (err) => { if (err.code === 'ENOTFOUND') { reject(new Error('Network - 无法连接到 FAL AI 服务器')); } else if (err.code === 'ECONNREFUSED') { reject(new Error('Connection - 连接被拒绝')); } else { reject(new Error(`Network - ${err.message}`)); } }); req.on('timeout', () => { req.destroy(); reject(new Error('Timeout - 连接超时')); }); req.end(); }); try { const result = await testPromise; res.json({ success: true, message: '✅ 连接成功!API 密钥有效', keyFormat: testKey.substring(0, 8) + '...' + testKey.substring(testKey.length - 4), statusCode: result.statusCode, note: '🆓 使用免费健康检查端点,不消耗配额' }); } catch (testError) { let errorMessage = '❌ 连接测试失败'; let errorType = 'error'; if (testError.message.includes('Unauthorized')) { errorMessage = '❌ API 密钥无效或已过期'; errorType = 'auth'; } else if (testError.message.includes('Forbidden')) { errorMessage = '❌ API 密钥被禁用'; errorType = 'auth'; } else if (testError.message.includes('Rate Limited')) { errorMessage = '⚠️ 请求过于频繁,请稍后重试'; errorType = 'rate'; } else if (testError.message.includes('Network')) { errorMessage = '🌐 网络连接问题'; errorType = 'network'; } else if (testError.message.includes('Timeout')) { errorMessage = '⏱️ 连接超时,请重试'; errorType = 'timeout'; } res.status(errorType === 'auth' ? 401 : 500).json({ success: false, error: errorMessage, errorType: errorType, details: testError.message }); } } catch (error) { console.error('API 密钥连接测试失败:', error); res.status(500).json({ success: false, error: '测试过程中发生错误', details: error.message }); } }); app.get('/api/health', (req, res) => { const apiKey = getValidApiKey(); res.json({ status: 'healthy', timestamp: new Date().toISOString(), hasApiKey: !!apiKey, keyPreview: apiKey ? apiKey.substring(0, 8) + '...' : null }); }); app.listen(PORT, '0.0.0.0', () => { console.log(`服务器运行在端口 ${PORT}`); const apiKey = getValidApiKey(); console.log(`API Key 状态: ${apiKey ? '已配置' : '未配置'}`); }); module.exports = app;