import {Files} from "../file/main"; import {Log} from "../log/main"; import {AppEnv} from "../env"; import {spawn} from "child_process"; import * as fs from "fs"; import * as path from "path"; import {getPythonPath} from "../../lib/python-util"; import {getPythonScriptPath, getFFmpegExecutablePath, resourceExists} from "../../lib/resource-path"; import {spawnPython, createTempConfig, cleanupTempConfig} from "../../lib/python-spawn-util"; import {normalizePath, normalizeOutputPath} from "../../lib/path-util"; import DB from "../db/main"; const TAG = "SubtitleCoverGenerator"; interface GenerateRequest { videoPath: string; scriptText: string; subtitleStyle: string; coverStyle: string; // 新增参数 coverText?: string; coverTextPosition?: string; coverTextColor?: string; coverTextSize?: number; outlineColor?: string; outlineWidth?: number; blurRadius?: number; zoomLevel?: number; } interface GenerateResult { success: boolean; coverPath?: string; srtPath?: string; subtitles?: Array<{ index: number; text: string; start_time: number; end_time: number; style: string; }>; error?: string; message: string; } class SubtitleCoverGenerator { private getPythonScriptPath(): string { // Python脚本路径 return getPythonScriptPath('subtitle_cover_generator_simple.py'); } // 移除重复的 createTempConfig 方法,使用 python-spawn-util 中的统一实现 /** * 执行Python命令 * ⚠️ 只使用程序自带的 Python,不依赖系统环境 */ private async execPython(command: string, timeoutMs: number = 300000): Promise { // 解析command字符串为参数数组 const args = command.match(/(?:[^\s"]+|"[^"]*")+/g) || []; const parsedArgs = args.map(arg => arg.replace(/^"|"$/g, '')); // 移除引号 // 使用统一的 Python 调用工具(支持中文路径) return spawnPython(parsedArgs, { timeout: timeoutMs, addFFmpegToPath: true }); } /** * 生成字幕和封面 * @param request 生成请求参数 * @returns 生成结果 */ async generateSubtitleAndCover(request: GenerateRequest): Promise { try { Log.info(TAG, `Starting generation for video: ${request.videoPath}`); // 验证输入参数 if (!request.videoPath || !request.scriptText) { throw new Error("Video path and script text are required"); } if (!fs.existsSync(request.videoPath)) { throw new Error(`Video file not found: ${request.videoPath}`); } const safeVideoPath = normalizePath(request.videoPath); const hubRoot = await Files.hubRoot(); Log.info(TAG, `Using hubRoot for output: ${hubRoot}`); const outputDir = path.join(hubRoot, `subtitle_cover_${Date.now()}`); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const safeOutputDir = normalizeOutputPath(outputDir); const safeHubRoot = normalizePath(hubRoot); const config = { videoPath: safeVideoPath, scriptText: request.scriptText, subtitleStyle: request.subtitleStyle || "default", coverStyle: request.coverStyle || "default", outputDir: safeOutputDir, hubRoot: safeHubRoot, // 新增参数 coverText: request.coverText || '', coverTextPosition: request.coverTextPosition || 'bottom', coverTextColor: request.coverTextColor || '#FFFFFF', coverTextSize: request.coverTextSize || 50, outlineColor: request.outlineColor || '#FFD700', outlineWidth: request.outlineWidth || 3, blurRadius: request.blurRadius || 15, zoomLevel: request.zoomLevel || 1.0 }; const configPath = await createTempConfig(config); Log.info(TAG, `Created config file: ${configPath}`); // 执行Python脚本 (设置10分钟超时) const pythonCommand = `"${this.getPythonScriptPath()}" "${configPath}"`; Log.info(TAG, `Executing Python command with 10 minute timeout`); const result = await this.execPython(pythonCommand, 600000); // 10分钟超时 Log.info(TAG, `Python execution completed`); // 解析结果 let generateResult: GenerateResult; try { // 查找JSON结果(可能在输出的最后) const lines = result.trim().split('\n'); const jsonLine = lines[lines.length - 1]; generateResult = JSON.parse(jsonLine); } catch (parseError) { Log.error(TAG, `Failed to parse Python result: ${parseError}`); throw new Error(`Invalid result format`); } // 清理临时配置文件 cleanupTempConfig(configPath); Log.info(TAG, `Generation completed: ${generateResult.message}`); return generateResult; } catch (error: any) { Log.error(TAG, `Generation failed: ${error}`); return { success: false, error: error.message || String(error), message: `Generation failed: ${error.message || String(error)}` }; } } /** * 获取支持的字幕样式列表 * @returns 字幕样式列表 */ getSubtitleStyles(): Array<{value: string; label: string; description: string}> { return [ { value: "default", label: "默认样式", description: "白色字体,黑色描边" }, { value: "highlight", label: "高亮样式", description: "黄色高亮背景" }, { value: "card", label: "卡片样式", description: "圆角卡片背景" }, { value: "neon", label: "霓虹样式", description: "霓虹灯效果" }, { value: "minimal", label: "极简样式", description: "简洁现代风格" } ]; } /** * 获取支持的封面样式列表 * @returns 封面样式列表 */ getCoverStyles(): Array<{value: string; label: string; description: string}> { return [ { value: "default", label: "默认封面", description: "标准视频封面" }, { value: "blur-bg", label: "背景虚化", description: "人物清晰,背景虚化" }, { value: "outline", label: "人物描边", description: "人物描边效果" }, { value: "gradient", label: "渐变背景", description: "渐变色背景" }, { value: "split", label: "斜切设计", description: "斜切色块设计" } ]; } /** * 重新生成封面 * @param request 重新生成请求参数 * @returns 生成结果 */ async regenerateCover(request: { videoPath: string; scriptText: string; coverStyle: string; // 新增参数 coverText?: string; coverTextPosition?: string; coverTextColor?: string; coverTextSize?: number; outlineColor?: string; outlineWidth?: number; blurRadius?: number; zoomLevel?: number; }): Promise { try { Log.info(TAG, `Regenerating cover for video: ${request.videoPath}`); // 验证输入参数 if (!request.videoPath) { throw new Error("Video path is required"); } if (!fs.existsSync(request.videoPath)) { throw new Error(`Video file not found: ${request.videoPath}`); } const safeVideoPath = normalizePath(request.videoPath); const hubRoot = await Files.hubRoot(); Log.info(TAG, `Using hubRoot for output: ${hubRoot}`); const outputDir = path.join(hubRoot, `subtitle_cover_${Date.now()}`); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const safeOutputDir = normalizeOutputPath(outputDir); const safeHubRoot = normalizePath(hubRoot); const config = { videoPath: safeVideoPath, scriptText: request.scriptText, subtitleStyle: "default", coverStyle: request.coverStyle || "default", outputDir: safeOutputDir, hubRoot: safeHubRoot, coverOnly: true, // 新增参数 coverText: request.coverText || '', coverTextPosition: request.coverTextPosition || 'bottom', coverTextColor: request.coverTextColor || '#FFFFFF', coverTextSize: request.coverTextSize || 50, outlineColor: request.outlineColor || '#FFD700', outlineWidth: request.outlineWidth || 3, blurRadius: request.blurRadius || 15, zoomLevel: request.zoomLevel || 1.0 }; const configPath = await createTempConfig(config); Log.info(TAG, `Created config file for cover regeneration: ${configPath}`); // 执行Python脚本 const pythonCommand = `"${this.getPythonScriptPath()}" "${configPath}"`; Log.info(TAG, `Executing Python command for cover regeneration`); const result = await this.execPython(pythonCommand); Log.info(TAG, `Python execution completed for cover regeneration`); // 解析结果 let generateResult: GenerateResult; try { // 查找JSON结果(可能在输出的最后) const lines = result.trim().split('\n'); const jsonLine = lines[lines.length - 1]; generateResult = JSON.parse(jsonLine); } catch (parseError) { Log.error(TAG, `Failed to parse Python result: ${parseError}`); throw new Error(`Invalid result format`); } // 清理临时配置文件 cleanupTempConfig(configPath); Log.info(TAG, `Cover regeneration completed: ${generateResult.message}`); return generateResult; } catch (error: any) { Log.error(TAG, `Cover regeneration failed: ${error}`); return { success: false, error: error.message || String(error), message: `Cover regeneration failed: ${error.message || String(error)}` }; } } /** * 生成实时封面预览 * @param request 预览请求参数 * @returns 预览结果 */ async generateCoverPreviews(request: { videoPath: string; scriptText?: string; }): Promise<{ success: boolean; previews?: Array<{ style: string; url: string; }>; error?: string; message: string; }> { try { Log.info(TAG, `Generating cover previews for video: ${request.videoPath}`); // 验证输入参数 if (!request.videoPath) { throw new Error("Video path is required"); } if (!fs.existsSync(request.videoPath)) { throw new Error(`Video file not found: ${request.videoPath}`); } const safeVideoPath = normalizePath(request.videoPath); const outputDir = path.join(await Files.tempRoot(), `cover_previews_${Date.now()}`); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const safeOutputDir = normalizeOutputPath(outputDir); const pythonScript = getPythonScriptPath('cover_preview_generator.py'); if (!fs.existsSync(pythonScript)) { throw new Error(`Preview generator script not found: ${pythonScript}`); } const pythonCommand = `"${pythonScript}" "${safeVideoPath}" --output-dir "${safeOutputDir}"`; Log.info(TAG, `Executing preview generation command`); const result = await this.execPython(pythonCommand, 120000); // 2分钟超时 Log.info(TAG, `Preview generation completed`); // 解析结果 let previewResult: any; try { const lines = result.trim().split('\n'); const jsonLine = lines[lines.length - 1]; previewResult = JSON.parse(jsonLine); } catch (parseError) { Log.error(TAG, `Failed to parse preview result: ${parseError}`); throw new Error(`Invalid result format`); } if (previewResult.error) { throw new Error(previewResult.error); } return { success: true, previews: previewResult.previews || [], message: `Generated ${previewResult.previews?.length || 0} previews` }; } catch (error: any) { Log.error(TAG, `Preview generation failed: ${error}`); return { success: false, error: error.message || String(error), message: `Preview generation failed: ${error.message || String(error)}` }; } } /** * 使用自定义模板生成封面 * @param request 自定义模板生成请求 * @returns 生成结果 */ async generateWithCustomTemplate(request: { videoPath: string; scriptText: string; template: any; // CustomTemplate interface from frontend }): Promise { try { Log.info(TAG, `Generating with custom template for video: ${request.videoPath}`); // 验证输入参数 if (!request.videoPath) { throw new Error("Video path is required"); } if (!request.template) { throw new Error("Custom template is required"); } if (!fs.existsSync(request.videoPath)) { throw new Error(`Video file not found: ${request.videoPath}`); } const safeVideoPath = normalizePath(request.videoPath); const hubRoot = await Files.hubRoot(); Log.info(TAG, `Using hubRoot for output: ${hubRoot}`); const outputDir = path.join(hubRoot, `subtitle_cover_${Date.now()}`); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const safeOutputDir = normalizeOutputPath(outputDir); const safeHubRoot = normalizePath(hubRoot); const config = { videoPath: safeVideoPath, scriptText: request.scriptText, outputDir: safeOutputDir, hubRoot: safeHubRoot, customTemplate: true, template: request.template }; const configPath = await createTempConfig(config); Log.info(TAG, `Created config file with custom template: ${configPath}`); // 执行Python脚本 const pythonCommand = `"${this.getPythonScriptPath()}" "${configPath}"`; Log.info(TAG, `Executing Python command with custom template`); const result = await this.execPython(pythonCommand, 300000); // 5分钟超时 Log.info(TAG, `Python execution completed for custom template`); // 解析结果 let generateResult: GenerateResult; try { // 查找JSON结果(可能在输出的最后) const lines = result.trim().split('\n'); const jsonLine = lines[lines.length - 1]; generateResult = JSON.parse(jsonLine); } catch (parseError) { Log.error(TAG, `Failed to parse Python result: ${parseError}`); throw new Error(`Invalid result format`); } // 清理临时配置文件 cleanupTempConfig(configPath); Log.info(TAG, `Custom template generation completed: ${generateResult.message}`); return generateResult; } catch (error: any) { Log.error(TAG, `Custom template generation failed: ${error}`); return { success: false, error: error.message || String(error), message: `Custom template generation failed: ${error.message || String(error)}` }; } } /** * 检查Python环境和依赖 * @returns 检查结果 */ async checkPythonEnvironment(): Promise<{ pythonAvailable: boolean; scriptExists: boolean; dependenciesInstalled: boolean; missingLibraries?: string[]; message: string; }> { try { // 检查Python是否可用 let pythonAvailable = false; try { await this.execPython('--version'); pythonAvailable = true; } catch (e) { pythonAvailable = false; } // 检查脚本文件是否存在 const scriptExists = fs.existsSync(this.getPythonScriptPath()); // 检查依赖库 const dependencies = ["cv2", "numpy", "PIL"]; const missingLibraries: string[] = []; for (const lib of dependencies) { try { await this.execPython(`-c "import ${lib}"`); } catch (e) { missingLibraries.push(lib); } } const dependenciesInstalled = missingLibraries.length === 0; let message = ""; if (!pythonAvailable) { message = "Python is not available"; } else if (!scriptExists) { message = "Python script not found"; } else if (!dependenciesInstalled) { message = `Missing dependencies: ${missingLibraries.join(", ")}`; } else { message = "Python environment is ready"; } return { pythonAvailable, scriptExists, dependenciesInstalled, missingLibraries: missingLibraries.length > 0 ? missingLibraries : undefined, message }; } catch (error: any) { Log.error(TAG, `Environment check failed: ${error}`); return { pythonAvailable: false, scriptExists: false, dependenciesInstalled: false, message: `Environment check failed: ${error.message || String(error)}` }; } } /** * 获取系统模板列表 */ async getSystemTemplatesList(): Promise<{success: boolean; templates?: any[]; error?: string}> { try { // 从数据库获取系统模板 const subtitleTemplates = await DB.select( `SELECT id, name, description, config, is_system, readonly, created_at, updated_at FROM subtitle_templates WHERE is_system = 1 ORDER BY created_at ASC` ); const coverTemplates = await DB.select( `SELECT id, name, description, config, is_system, readonly, created_at, updated_at FROM cover_templates WHERE is_system = 1 ORDER BY created_at ASC` ); return { success: true, templates: { subtitleTemplates: subtitleTemplates || [], coverTemplates: coverTemplates || [] } }; } catch (error: any) { Log.error(TAG, `Get system templates list failed: ${error}`); return { success: false, error: error.message || String(error) }; } } /** * 保存/编辑系统模板 */ async saveSystemTemplate(template: any): Promise<{success: boolean; id?: string; error?: string; message?: string}> { try { const now = Date.now(); if (template.type === 'subtitle') { // 保存字幕模板 const configJson = JSON.stringify(template.config); const existing = await DB.first( `SELECT id FROM subtitle_templates WHERE id = ?`, [template.id] ); if (existing) { await DB.execute( `UPDATE subtitle_templates SET name = ?, description = ?, config = ?, updated_at = ? WHERE id = ?`, [template.name, template.description, configJson, now, template.id] ); } else { await DB.execute( `INSERT INTO subtitle_templates (id, name, description, config, is_system, readonly, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [template.id, template.name, template.description, configJson, 1, 0, now, now] ); } Log.info(TAG, `Saved subtitle template: ${template.id}`); return {success: true, id: template.id, message: "Subtitle template saved successfully"}; } else if (template.type === 'cover') { // 保存封面模板 const configJson = JSON.stringify(template.config); const existing = await DB.first( `SELECT id FROM cover_templates WHERE id = ?`, [template.id] ); if (existing) { await DB.execute( `UPDATE cover_templates SET name = ?, description = ?, config = ?, thumbnail_path = ?, updated_at = ? WHERE id = ?`, [template.name, template.description, configJson, template.thumbnailPath || null, now, template.id] ); } else { await DB.execute( `INSERT INTO cover_templates (id, name, config, is_system, readonly, created_at, updated_at, thumbnail_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [template.id, template.name, configJson, 1, 0, now, now, template.thumbnailPath || null] ); } Log.info(TAG, `Saved cover template: ${template.id}`); return {success: true, id: template.id, message: "Cover template saved successfully"}; } else { throw new Error("Invalid template type"); } } catch (error: any) { Log.error(TAG, `Save system template failed: ${error}`); return { success: false, error: error.message || String(error) }; } } /** * 删除系统模板 */ async deleteSystemTemplate(templateId: string): Promise<{success: boolean; error?: string; message?: string}> { try { // 检查模板是否存在 const template = await DB.first( `SELECT id, is_system FROM subtitle_templates WHERE id = ? UNION SELECT id, is_system FROM cover_templates WHERE id = ?`, [templateId, templateId] ); if (!template) { throw new Error("Template not found"); } if (template.is_system === 1) { throw new Error("Cannot delete system templates"); } // 从数据库删除 await DB.execute(`DELETE FROM subtitle_templates WHERE id = ?`, [templateId]); await DB.execute(`DELETE FROM cover_templates WHERE id = ?`, [templateId]); Log.info(TAG, `Deleted template: ${templateId}`); return {success: true, message: "Template deleted successfully"}; } catch (error: any) { Log.error(TAG, `Delete system template failed: ${error}`); return { success: false, error: error.message || String(error) }; } } /** * 重置系统模板为默认值 */ async resetSystemTemplates(): Promise<{success: boolean; error?: string; message?: string}> { try { const { initSystemTemplates } = await import('../db/initSystemTemplates'); // 调用初始化函数重置系统模板 await initSystemTemplates(); Log.info(TAG, "System templates reset to defaults"); return {success: true, message: "System templates reset to defaults successfully"}; } catch (error: any) { Log.error(TAG, `Reset system templates failed: ${error}`); return { success: false, error: error.message || String(error) }; } } /** * 保存字幕模板并自动导出(开发模式) */ async saveSubtitleTemplateAndExport(template: any, isDev: boolean = false): Promise<{success: boolean; id?: string; error?: string; message?: string}> { try { const now = Date.now(); const configJson = JSON.stringify(template.config); const existing = await DB.first( `SELECT id FROM subtitle_templates WHERE id = ?`, [template.id] ); if (existing) { await DB.execute( `UPDATE subtitle_templates SET name = ?, description = ?, config = ?, updated_at = ? WHERE id = ?`, [template.name, template.description, configJson, now, template.id] ); } else { await DB.execute( `INSERT INTO subtitle_templates (id, name, description, config, is_system, readonly, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [template.id, template.name, template.description, configJson, 0, 0, now, now] ); } // ✨ 关键:保存后自动导出到JSON(开发模式) if (isDev) { const exportResult = await this.exportSystemTemplatesToFile(); if (!exportResult.success) { Log.warn(TAG, 'Failed to auto-export after saving', { templateId: template.id, error: exportResult.error }); // 不中断保存,只记录警告 } else { Log.info(TAG, 'Auto-exported system template after saving', { templateId: template.id }); } } Log.info(TAG, `Saved subtitle template: ${template.id}`); return { success: true, id: template.id, message: "Subtitle template saved successfully" }; } catch (error: any) { Log.error(TAG, `Save subtitle template failed: ${error}`); return { success: false, error: error.message || String(error) }; } } /** * 保存封面模板并自动导出(开发模式) */ async saveCoverTemplateAndExport(template: any, isDev: boolean = false): Promise<{success: boolean; id?: string; error?: string; message?: string}> { try { const now = Date.now(); const configJson = JSON.stringify(template.config); const existing = await DB.first( `SELECT id FROM cover_templates WHERE id = ?`, [template.id] ); if (existing) { await DB.execute( `UPDATE cover_templates SET name = ?, description = ?, config = ?, thumbnail_path = ?, updated_at = ? WHERE id = ?`, [template.name, template.description, configJson, template.thumbnailPath || null, now, template.id] ); } else { await DB.execute( `INSERT INTO cover_templates (id, name, description, config, thumbnail_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [template.id, template.name, template.description, configJson, template.thumbnailPath || null, now, now] ); } // ✨ 关键:保存后自动导出到JSON(开发模式) if (isDev) { const exportResult = await this.exportSystemTemplatesToFile(); if (!exportResult.success) { Log.warn(TAG, 'Failed to auto-export after saving', { templateId: template.id, error: exportResult.error }); // 不中断保存,只记录警告 } else { Log.info(TAG, 'Auto-exported system template after saving', { templateId: template.id }); } } Log.info(TAG, `Saved cover template: ${template.id}`); return { success: true, id: template.id, message: "Cover template saved successfully" }; } catch (error: any) { Log.error(TAG, `Save cover template failed: ${error}`); return { success: false, error: error.message || String(error) }; } } /** * 导出系统模板配置到 JSON 文件 */ async exportSystemTemplatesToFile(): Promise<{success: boolean; filePath?: string; error?: string; message?: string}> { try { const pathModule = await import('path'); const fsModule = await import('fs'); // 从数据库获取所有系统模板 const subtitleTemplates = await DB.select( `SELECT id, name, description, config, is_system, readonly, created_at, updated_at FROM subtitle_templates WHERE is_system = 1 ORDER BY created_at ASC` ); const coverTemplates = await DB.select( `SELECT id, name, description, config, is_system, readonly, created_at, updated_at, thumbnail_path FROM cover_templates WHERE is_system = 1 ORDER BY created_at ASC` ); // 格式化数据 const subtitleTemplatesFormatted = (subtitleTemplates || []).map((t: any) => ({ id: t.id, name: t.name, description: t.description, isSystem: Boolean(t.is_system), readonly: Boolean(t.readonly), createdAt: t.created_at, config: typeof t.config === 'string' ? JSON.parse(t.config) : t.config })); const coverTemplatesFormatted = (coverTemplates || []).map((t: any) => ({ id: t.id, name: t.name, description: t.description, is_system: t.is_system, readonly: Boolean(t.readonly), config: typeof t.config === 'string' ? JSON.parse(t.config) : t.config, thumbnailPath: t.thumbnail_path, createdAt: t.created_at })); // 生成配置对象 const systemTemplatesConfig = { version: "1.0.0", description: "系统内置模板配置 - 包含8套字幕模板和8套封面模板(开发模式可编辑,生产模式只读)", timestamp: new Date().toISOString(), subtitleTemplates: subtitleTemplatesFormatted, coverTemplates: coverTemplatesFormatted }; // 确定输出路径 const configDir = pathModule.join(__dirname, '../../config'); const configPath = pathModule.join(configDir, 'system-templates.json'); // 确保目录存在 if (!fsModule.existsSync(configDir)) { fsModule.mkdirSync(configDir, { recursive: true }); } // 保存到文件 fsModule.writeFileSync( configPath, JSON.stringify(systemTemplatesConfig, null, 2), 'utf-8' ); Log.info(TAG, `Exported system templates to ${configPath}`, { subtitleCount: subtitleTemplatesFormatted.length, coverCount: coverTemplatesFormatted.length, filePath: configPath }); return { success: true, filePath: configPath, message: `Successfully exported ${subtitleTemplatesFormatted.length} subtitle templates and ${coverTemplatesFormatted.length} cover templates` }; } catch (error: any) { Log.error(TAG, `Export system templates failed: ${error}`); return { success: false, error: error.message || String(error) }; } } } export default SubtitleCoverGenerator;