"use strict"; /** * 书籍生成服务 - 核心业务逻辑 * 使用 Prisma 数据库存储 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.bookGeneratorService = exports.BookGeneratorService = void 0; const axios_1 = __importDefault(require("axios")); const config_1 = require("../../config"); const models_1 = require("../../models"); const TtsService = __importStar(require("../tts/tts.service")); const VideoService = __importStar(require("../video-generator/video-generator.service")); const book_generator_store_1 = require("./book-generator.store"); // ============ 默认配置 ============ const DEFAULT_CONFIG = { model: 'qwen3.6-plus', temperature: 0.7, maxTokens: 4000, chapterWordRange: { min: 800, max: 2000, }, retryPolicy: { maxRetries: 3, retryDelay: 2000, }, }; // ============ 提示词模板 ============ const PROMPT_TEMPLATES = { systemPrompt: `你是专业的书籍作者,擅长撰写结构严谨、内容丰富、通俗易懂的作品。`, outlinePrompt: ` 请为以下书籍生成详细的章节大纲。 ## 书籍信息 - 书名:{title} - 副标题:{subtitle} - 主题:{description} - 目标受众:{targetAudience} - 风格:{style} - 章节数:{totalChapters}章 ## 输出要求 请生成JSON格式的大纲,包含: 1. mainTheme: 本书的核心主题 2. structureLogic: 整体结构逻辑 3. chapters: 章节大纲数组,每个章节包含: - number: 章节序号 - title: 章节标题 - summary: 章节概述(1-2句话) - keyPoints: 核心知识点(3-5个) - estimatedWords: 预估字数 请直接输出JSON,不要其他内容: `, chapterPrompt: (chapter, context) => ` 请撰写书籍《{bookTitle}》第${chapter.number}章的完整内容。 ## 章节信息 - 章节标题:${chapter.title} - 章节概述:${chapter.summary} - 核心知识点: ${chapter.keyPoints.map((p, i) => ` ${i + 1}. ${p}`).join('\n')} - 预估字数:${chapter.estimatedWords}字 ## 全书上下文 - 主题主线:${context.mainTheme} - 结构逻辑:${context.structureLogic} ## 写作要求 1. 语言通俗易懂,适合目标受众 2. 包含引入、正文、总结三个部分 3. 适当使用小标题划分内容 4. 长度控制在${chapter.estimatedWords}字左右 5. 使用markdown格式输出 请直接输出正文内容: `, forewordPrompt: (book) => ` 请为书籍《${book.title}》撰写前言。 主题:${book.description} 目标受众:${book.targetAudience} 写作风格:${book.style} 长度:300-500字 请直接输出前言内容: `, afterwordPrompt: (book) => ` 请为书籍《${book.title}》撰写后记。 主题:${book.description} 写作风格:${book.style} 长度:300-500字 请直接输出后记内容: `, }; // ============ 核心服务类 ============ class BookGeneratorService { apiKey = ''; config; constructor(config) { this.config = { ...DEFAULT_CONFIG, ...config }; } setApiKey(apiKey) { this.apiKey = apiKey; } // ============ 书籍 CRUD ============ async createBook(request) { return book_generator_store_1.bookStore.create({ title: request.title, subtitle: request.subtitle, description: request.description, targetAudience: request.targetAudience, style: request.style, totalChapters: request.totalChapters, }); } async getBook(id) { return book_generator_store_1.bookStore.getById(id); } async getAllBooks(userId) { return book_generator_store_1.bookStore.getAllByUser(userId); } async deleteBook(id) { return book_generator_store_1.bookStore.delete(id); } // ============ 生成大纲 ============ async generateOutline(bookId) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); await book_generator_store_1.bookStore.update(bookId, { status: 'planning' }); try { const outlinePrompt = this.buildOutlinePrompt(book); const outlineJson = await this.callLLM(outlinePrompt); const outline = this.parseOutline(outlineJson); await book_generator_store_1.bookStore.update(bookId, { outlineJson: JSON.stringify(outline), estimatedWords: outline.chapters.reduce((sum, c) => sum + c.estimatedWords, 0), }); // 创建章节记录 await book_generator_store_1.bookStore.createChapters(bookId, outline.chapters.map(c => ({ number: c.number, title: c.title, summary: c.summary, keyPoints: c.keyPoints, estimatedWords: c.estimatedWords, }))); return outline; } catch (error) { await book_generator_store_1.bookStore.update(bookId, { status: 'failed', errorMsg: error instanceof Error ? error.message : '生成失败' }); throw error; } } // ============ 生成章节 ============ async generateChapter(bookId, chapterNumber) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); if (!book.outline) throw new Error('请先生成大纲'); const outlineChapter = book.outline.chapters.find((c) => c.number === chapterNumber); if (!outlineChapter) throw new Error(`第${chapterNumber}章不存在`); await book_generator_store_1.bookStore.update(bookId, { status: 'generating' }); try { const chapterPrompt = this.buildChapterPrompt(book, outlineChapter); const content = await this.callLLM(chapterPrompt); const wordCount = this.countWords(content); const chapter = await book_generator_store_1.bookStore.updateChapter(bookId, chapterNumber, { content, wordCount, status: 'completed', }); // 更新进度 const completedCount = await book_generator_store_1.bookStore.countCompletedChapters(bookId); const progress = Math.round((completedCount / book.totalChapters) * 100); await book_generator_store_1.bookStore.update(bookId, { progress, status: progress >= 100 ? 'completed' : 'generating', }); return chapter; } catch (error) { await book_generator_store_1.bookStore.updateChapter(bookId, chapterNumber, { status: 'failed', errorMsg: error instanceof Error ? error.message : '生成失败', }); throw error; } } async generateAllChapters(bookId) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); if (!book.outline) throw new Error('请先生成大纲'); const results = []; for (const outlineChapter of book.outline.chapters) { const existingChapter = book.chapters.find((c) => c.number === outlineChapter.number); if (existingChapter?.status === 'completed') { results.push(existingChapter); continue; } try { const chapter = await this.generateChapter(bookId, outlineChapter.number); results.push(chapter); } catch (error) { console.error(`生成第${outlineChapter.number}章失败:`, error); } } return results; } // ============ 前言/后记 ============ async generateForeword(bookId) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); try { const prompt = PROMPT_TEMPLATES.forewordPrompt(book); const foreword = await this.callLLM(prompt); await book_generator_store_1.bookStore.update(bookId, { foreword }); return foreword; } catch (error) { throw new Error(`生成前言失败: ${error instanceof Error ? error.message : error}`); } } async generateAfterword(bookId) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); try { const prompt = PROMPT_TEMPLATES.afterwordPrompt(book); const afterword = await this.callLLM(prompt); await book_generator_store_1.bookStore.update(bookId, { afterword }); return afterword; } catch (error) { throw new Error(`生成后记失败: ${error instanceof Error ? error.message : error}`); } } // ============ 内容获取 ============ async getFullContent(bookId) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); const parts = []; parts.push(`# ${book.title}`); if (book.subtitle) parts.push(`## ${book.subtitle}`); if (book.metadata?.foreword) { parts.push('\n## 前言\n'); parts.push(book.metadata.foreword); } if (book.outline) { parts.push('\n## 目录\n'); book.outline.chapters.forEach((ch) => { parts.push(`${ch.number}. ${ch.title}`); }); } book.chapters.forEach((ch) => { parts.push(`\n## 第${ch.number}章 ${ch.title}\n`); parts.push(ch.content); }); if (book.metadata?.afterword) { parts.push('\n## 后记\n'); parts.push(book.metadata.afterword); } return parts.join('\n'); } async getProgress(bookId) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) return null; const completedChapters = book.chapters.filter((c) => c.status === 'completed').length; return { bookId, status: book.status, progress: book.progress, completedChapters, totalChapters: book.totalChapters, }; } // ============ 一键生成(同步阻塞) ============ async generateBook(bookId, options) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) return { success: false, completedChapters: 0, failedChapters: 0, errors: ['书籍不存在'] }; const errors = []; try { if (!book.outline) { await this.generateOutline(bookId); } } catch (error) { return { success: false, completedChapters: 0, failedChapters: 0, errors: [`大纲: ${error instanceof Error ? error.message : error}`] }; } const results = []; let failedCount = 0; for (const outlineChapter of book.outline?.chapters || []) { try { const chapter = await this.generateChapter(bookId, outlineChapter.number); results.push(chapter); options?.onProgress?.(Math.round((results.length / book.totalChapters) * 100), outlineChapter.number); } catch (error) { failedCount++; errors.push(`第${outlineChapter.number}章: ${error instanceof Error ? error.message : error}`); } } if (options?.generateForeword) { try { await this.generateForeword(bookId); } catch (error) { errors.push(`前言: ${error instanceof Error ? error.message : error}`); } } if (options?.generateAfterword) { try { await this.generateAfterword(bookId); } catch (error) { errors.push(`后记: ${error instanceof Error ? error.message : error}`); } } const updatedBook = await book_generator_store_1.bookStore.getById(bookId); return { success: failedCount === 0 && errors.length === 0, book: updatedBook || undefined, completedChapters: results.length, failedChapters: failedCount, errors, }; } // ============ 一键生成(异步不阻塞) ============ async generateBookAsync(bookId, options) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) return; try { await book_generator_store_1.bookStore.update(bookId, { status: 'generating', progress: 0 }); if (!book.outline) { await this.generateOutline(bookId); } // 生成章节 for (const outlineChapter of book.outline?.chapters || []) { try { await this.generateChapter(bookId, outlineChapter.number); const updatedBook = await book_generator_store_1.bookStore.getById(bookId); await book_generator_store_1.bookStore.update(bookId, { progress: updatedBook.progress }); } catch (error) { console.error(`生成第${outlineChapter.number}章失败:`, error); } } if (options?.generateForeword) { await this.generateForeword(bookId); } if (options?.generateAfterword) { await this.generateAfterword(bookId); } await book_generator_store_1.bookStore.update(bookId, { status: 'completed', progress: 100 }); } catch (error) { await book_generator_store_1.bookStore.update(bookId, { status: 'failed', errorMsg: error instanceof Error ? error.message : '生成失败' }); } } // ============ 私有方法 ============ buildOutlinePrompt(book) { return PROMPT_TEMPLATES.outlinePrompt .replace('{title}', book.title) .replace('{subtitle}', book.subtitle || '无') .replace('{description}', book.description) .replace('{targetAudience}', book.targetAudience) .replace('{style}', book.style) .replace('{totalChapters}', String(book.totalChapters)); } buildChapterPrompt(book, chapter) { if (!book.outline) throw new Error('书籍大纲不存在'); return PROMPT_TEMPLATES.chapterPrompt(chapter, book.outline).replace('{bookTitle}', book.title); } parseOutline(jsonStr) { try { const jsonMatch = jsonStr.match(/\{[\s\S]*\}/); if (jsonMatch) { return this.validateOutline(JSON.parse(jsonMatch[0])); } } catch { console.error('解析大纲 JSON 失败'); } return this.createDefaultOutline(); } validateOutline(data) { if (!data.chapters || !Array.isArray(data.chapters)) { throw new Error('大纲格式不正确'); } return { mainTheme: data.mainTheme || '主题待定', structureLogic: data.structureLogic || '由浅入深', chapters: data.chapters.map((c, i) => ({ number: c.number || i + 1, title: c.title || `第${i + 1}章`, summary: c.summary || '', keyPoints: c.keyPoints || [], estimatedWords: c.estimatedWords || 1000, stories: c.stories || [], })), }; } createDefaultOutline() { return { mainTheme: '核心主题', structureLogic: '由浅入深', chapters: [{ number: 1, title: '概述', summary: '介绍', keyPoints: ['基础概念'], estimatedWords: 1000 }], }; } countWords(text) { return (text.match(/[\u4e00-\u9fa5]/g) || []).length; } async callLLM(prompt, retryCount = 0, currentModel) { const modelId = currentModel || this.config.model; // 从模型配置获取 API Key 和 URL const modelConfig = config_1.config.models.getModel(modelId); if (!modelConfig?.apiKey || !modelConfig?.baseUrl) { throw new Error(`模型 ${modelId} 缺少 API 配置`); } const { apiKey, baseUrl } = modelConfig; try { const response = await axios_1.default.post(`${baseUrl}/chat/completions`, { model: modelId, messages: [{ role: 'user', content: prompt }], }, { headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, timeout: 120000, }); const result = response.data?.choices?.[0]?.message?.content || ''; if (!result) throw new Error('API 返回内容为空'); return result; } catch (error) { const errorMessage = error.response?.data?.message || error.message || 'API 调用失败'; console.log(`[LLM] 模型 ${modelId} 调用失败: ${errorMessage}`); // 检查是否需要切换模型 if (config_1.config.models.shouldSwitchModel(errorMessage)) { const nextModel = config_1.config.models.getNextModel(modelId, 'text'); if (nextModel) { console.log(`[LLM] 自动切换到模型: ${nextModel}`); return this.callLLM(prompt, 0, nextModel); // 重置 retryCount } } // 重试当前模型 if (retryCount < this.config.retryPolicy.maxRetries) { await this.delay(this.config.retryPolicy.retryDelay); return this.callLLM(prompt, retryCount + 1, currentModel); } throw new Error(errorMessage); } } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } // ============ 音频生成 ============ /** * 生成单个章节音频 */ async generateChapterAudio(bookId, chapterNumber, voiceId) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); const chapter = book.chapters.find((c) => c.number === chapterNumber); if (!chapter) throw new Error(`第${chapterNumber}章不存在`); if (!chapter.content || chapter.content.trim().length === 0) { throw new Error('章节内容为空,请先生成章节内容'); } // 从数据库获取章节记录 const dbChapter = await models_1.prisma.bookChapter.findFirst({ where: { bookId: parseInt(bookId), number: chapterNumber, }, }); if (!dbChapter) throw new Error('章节数据库记录不存在'); const taskId = `audio_${bookId}_${chapterNumber}_${Date.now()}`; // 异步生成音频 this.processChapterAudio(dbChapter.id, chapter.content, voiceId).catch((err) => { console.error(`章节 ${chapterNumber} 音频生成失败:`, err); }); return { taskId, chapterId: String(dbChapter.id) }; } /** * 批量生成书籍所有章节音频 */ async generateAllChaptersAudio(bookId, voiceId) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); const completedChapters = book.chapters.filter((c) => c.status === 'completed' && c.content && c.content.trim().length > 0); if (completedChapters.length === 0) { throw new Error('没有可生成音频的章节,请先生成章节内容'); } const taskId = `audio_book_${bookId}_${Date.now()}`; // 异步批量生成音频 this.processAllChaptersAudio(bookId, completedChapters, voiceId).catch((err) => { console.error(`书籍 ${bookId} 批量音频生成失败:`, err); }); return { taskId, totalChapters: completedChapters.length }; } /** * 处理单个章节音频生成 */ async processChapterAudio(chapterId, content, voiceId) { try { console.log(`🎵 开始生成章节 ${chapterId} 音频...`); // 清理内容(移除 markdown 格式) const cleanContent = content .replace(/^#{1,6}\s+/gm, '') // 移除标题标记 .replace(/\*\*(.*?)\*\*/g, '$1') // 移除粗体 .replace(/\*(.*?)\*/g, '$1') // 移除斜体 .replace(/`(.*?)`/g, '$1') // 移除行内代码 .replace(/^\s*[-*+]\s+/gm, '') // 移除列表标记 .replace(/^\s*\d+\.\s+/gm, '') // 移除数字列表标记 .trim(); // 调用 TTS 服务生成音频 const result = await TtsService.generateAudio('system', cleanContent, voiceId, { speed: 1.0, pitch: 0, volume: 50 }, async (audioUrl, duration) => { // 音频生成完成后更新数据库 await models_1.prisma.bookChapter.update({ where: { id: chapterId }, data: { audioUrl, audioDuration: duration, }, }); console.log(`✅ 章节 ${chapterId} 音频生成完成: ${audioUrl}`); }); console.log(`🎵 章节 ${chapterId} 音频任务已启动: ${result.audioId}`); } catch (error) { console.error(`❌ 章节 ${chapterId} 音频生成失败:`, error); throw error; } } /** * 处理批量章节音频生成 */ async processAllChaptersAudio(bookId, chapters, voiceId) { console.log(`🎵 开始批量生成书籍 ${bookId} 的 ${chapters.length} 个章节音频...`); // 从数据库获取所有章节记录 const dbChapters = await models_1.prisma.bookChapter.findMany({ where: { bookId: parseInt(bookId) }, }); for (const chapter of chapters) { const dbChapter = dbChapters.find((c) => c.number === chapter.number); if (dbChapter && chapter.content) { try { await this.processChapterAudio(dbChapter.id, chapter.content, voiceId); // 每个章节之间稍作延迟,避免请求过于密集 await this.delay(1000); } catch (error) { console.error(`第${chapter.number}章音频生成失败,继续下一个:`, error); } } } console.log(`✅ 书籍 ${bookId} 批量音频生成任务完成`); } // ============ 视频生成 ============ /** * 生成单个章节视频(从音频转视频) */ async generateChapterVideo(bookId, chapterNumber) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); const chapter = book.chapters.find((c) => c.number === chapterNumber); if (!chapter) throw new Error(`第${chapterNumber}章不存在`); if (!chapter.audioUrl) { throw new Error('章节音频不存在,请先生成音频'); } // 获取数据库中的章节记录 const dbChapter = await models_1.prisma.bookChapter.findFirst({ where: { bookId: parseInt(bookId), number: chapterNumber, }, }); if (!dbChapter) throw new Error('章节数据库记录不存在'); // 创建视频项目并生成 const videoProject = await VideoService.createVideoProjectFromBook(parseInt(bookId), dbChapter.id); if (!videoProject) { throw new Error('创建视频项目失败'); } // 异步生成视频 this.processChapterVideo(videoProject.id).catch((err) => { console.error(`章节 ${chapterNumber} 视频生成失败:`, err); }); return { projectId: videoProject.id, chapterId: String(dbChapter.id) }; } /** * 批量生成书籍所有章节视频 */ async generateAllChaptersVideo(bookId) { const book = await book_generator_store_1.bookStore.getById(bookId); if (!book) throw new Error('书籍不存在'); const chaptersWithAudio = book.chapters.filter((c) => c.status === 'completed' && c.audioUrl); if (chaptersWithAudio.length === 0) { throw new Error('没有可生成视频的章节(需要先有音频)'); } const taskId = `video_book_${bookId}_${Date.now()}`; // 异步批量生成视频 this.processAllChaptersVideo(bookId, chaptersWithAudio).catch((err) => { console.error(`书籍 ${bookId} 批量视频生成失败:`, err); }); return { taskId, totalChapters: chaptersWithAudio.length }; } /** * 处理单个章节视频生成 */ async processChapterVideo(projectId) { try { console.log(`🎬 开始生成视频项目 ${projectId}...`); const result = await VideoService.generateVideoForProject(projectId); if (result.success) { console.log(`✅ 视频项目 ${projectId} 生成完成: ${result.outputUrl}`); } else { console.error(`❌ 视频项目 ${projectId} 生成失败: ${result.error}`); } } catch (error) { console.error(`❌ 视频项目 ${projectId} 生成失败:`, error); throw error; } } /** * 处理批量章节视频生成 */ async processAllChaptersVideo(bookId, chapters) { console.log(`🎬 开始批量生成书籍 ${bookId} 的 ${chapters.length} 个章节视频...`); for (const chapter of chapters) { try { const dbChapter = await models_1.prisma.bookChapter.findFirst({ where: { bookId: parseInt(bookId), number: chapter.number, }, }); if (!dbChapter || !chapter.audioUrl) { console.warn(`第${chapter.number}章跳过:缺少音频或数据库记录`); continue; } // 创建视频项目 const videoProject = await VideoService.createVideoProjectFromBook(parseInt(bookId), dbChapter.id); if (videoProject) { // 生成视频 await this.processChapterVideo(videoProject.id); } // 每个视频之间稍作延迟 await this.delay(2000); } catch (error) { console.error(`第${chapter.number}章视频生成失败,继续下一个:`, error); } } console.log(`✅ 书籍 ${bookId} 批量视频生成任务完成`); } } exports.BookGeneratorService = BookGeneratorService; // 导出单例 exports.bookGeneratorService = new BookGeneratorService(); exports.bookGeneratorService.setApiKey(config_1.config.dashscope?.apiKey || '');