book-generator.service.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. "use strict";
  2. /**
  3. * 书籍生成服务 - 核心业务逻辑
  4. * 使用 Prisma 数据库存储
  5. */
  6. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
  7. if (k2 === undefined) k2 = k;
  8. var desc = Object.getOwnPropertyDescriptor(m, k);
  9. if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
  10. desc = { enumerable: true, get: function() { return m[k]; } };
  11. }
  12. Object.defineProperty(o, k2, desc);
  13. }) : (function(o, m, k, k2) {
  14. if (k2 === undefined) k2 = k;
  15. o[k2] = m[k];
  16. }));
  17. var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
  18. Object.defineProperty(o, "default", { enumerable: true, value: v });
  19. }) : function(o, v) {
  20. o["default"] = v;
  21. });
  22. var __importStar = (this && this.__importStar) || (function () {
  23. var ownKeys = function(o) {
  24. ownKeys = Object.getOwnPropertyNames || function (o) {
  25. var ar = [];
  26. for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
  27. return ar;
  28. };
  29. return ownKeys(o);
  30. };
  31. return function (mod) {
  32. if (mod && mod.__esModule) return mod;
  33. var result = {};
  34. if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
  35. __setModuleDefault(result, mod);
  36. return result;
  37. };
  38. })();
  39. var __importDefault = (this && this.__importDefault) || function (mod) {
  40. return (mod && mod.__esModule) ? mod : { "default": mod };
  41. };
  42. Object.defineProperty(exports, "__esModule", { value: true });
  43. exports.bookGeneratorService = exports.BookGeneratorService = void 0;
  44. const axios_1 = __importDefault(require("axios"));
  45. const config_1 = require("../../config");
  46. const models_1 = require("../../models");
  47. const TtsService = __importStar(require("../tts/tts.service"));
  48. const VideoService = __importStar(require("../video-generator/video-generator.service"));
  49. const book_generator_store_1 = require("./book-generator.store");
  50. // ============ 默认配置 ============
  51. const DEFAULT_CONFIG = {
  52. model: 'qwen3.6-plus',
  53. temperature: 0.7,
  54. maxTokens: 4000,
  55. chapterWordRange: {
  56. min: 800,
  57. max: 2000,
  58. },
  59. retryPolicy: {
  60. maxRetries: 3,
  61. retryDelay: 2000,
  62. },
  63. };
  64. // ============ 提示词模板 ============
  65. const PROMPT_TEMPLATES = {
  66. systemPrompt: `你是专业的书籍作者,擅长撰写结构严谨、内容丰富、通俗易懂的作品。`,
  67. outlinePrompt: `
  68. 请为以下书籍生成详细的章节大纲。
  69. ## 书籍信息
  70. - 书名:{title}
  71. - 副标题:{subtitle}
  72. - 主题:{description}
  73. - 目标受众:{targetAudience}
  74. - 风格:{style}
  75. - 章节数:{totalChapters}章
  76. ## 输出要求
  77. 请生成JSON格式的大纲,包含:
  78. 1. mainTheme: 本书的核心主题
  79. 2. structureLogic: 整体结构逻辑
  80. 3. chapters: 章节大纲数组,每个章节包含:
  81. - number: 章节序号
  82. - title: 章节标题
  83. - summary: 章节概述(1-2句话)
  84. - keyPoints: 核心知识点(3-5个)
  85. - estimatedWords: 预估字数
  86. 请直接输出JSON,不要其他内容:
  87. `,
  88. chapterPrompt: (chapter, context) => `
  89. 请撰写书籍《{bookTitle}》第${chapter.number}章的完整内容。
  90. ## 章节信息
  91. - 章节标题:${chapter.title}
  92. - 章节概述:${chapter.summary}
  93. - 核心知识点:
  94. ${chapter.keyPoints.map((p, i) => ` ${i + 1}. ${p}`).join('\n')}
  95. - 预估字数:${chapter.estimatedWords}字
  96. ## 全书上下文
  97. - 主题主线:${context.mainTheme}
  98. - 结构逻辑:${context.structureLogic}
  99. ## 写作要求
  100. 1. 语言通俗易懂,适合目标受众
  101. 2. 包含引入、正文、总结三个部分
  102. 3. 适当使用小标题划分内容
  103. 4. 长度控制在${chapter.estimatedWords}字左右
  104. 5. 使用markdown格式输出
  105. 请直接输出正文内容:
  106. `,
  107. forewordPrompt: (book) => `
  108. 请为书籍《${book.title}》撰写前言。
  109. 主题:${book.description}
  110. 目标受众:${book.targetAudience}
  111. 写作风格:${book.style}
  112. 长度:300-500字
  113. 请直接输出前言内容:
  114. `,
  115. afterwordPrompt: (book) => `
  116. 请为书籍《${book.title}》撰写后记。
  117. 主题:${book.description}
  118. 写作风格:${book.style}
  119. 长度:300-500字
  120. 请直接输出后记内容:
  121. `,
  122. };
  123. // ============ 核心服务类 ============
  124. class BookGeneratorService {
  125. apiKey = '';
  126. config;
  127. constructor(config) {
  128. this.config = { ...DEFAULT_CONFIG, ...config };
  129. }
  130. setApiKey(apiKey) {
  131. this.apiKey = apiKey;
  132. }
  133. // ============ 书籍 CRUD ============
  134. async createBook(request) {
  135. return book_generator_store_1.bookStore.create({
  136. title: request.title,
  137. subtitle: request.subtitle,
  138. description: request.description,
  139. targetAudience: request.targetAudience,
  140. style: request.style,
  141. totalChapters: request.totalChapters,
  142. });
  143. }
  144. async getBook(id) {
  145. return book_generator_store_1.bookStore.getById(id);
  146. }
  147. async getAllBooks(userId) {
  148. return book_generator_store_1.bookStore.getAllByUser(userId);
  149. }
  150. async deleteBook(id) {
  151. return book_generator_store_1.bookStore.delete(id);
  152. }
  153. // ============ 生成大纲 ============
  154. async generateOutline(bookId) {
  155. const book = await book_generator_store_1.bookStore.getById(bookId);
  156. if (!book)
  157. throw new Error('书籍不存在');
  158. await book_generator_store_1.bookStore.update(bookId, { status: 'planning' });
  159. try {
  160. const outlinePrompt = this.buildOutlinePrompt(book);
  161. const outlineJson = await this.callLLM(outlinePrompt);
  162. const outline = this.parseOutline(outlineJson);
  163. await book_generator_store_1.bookStore.update(bookId, {
  164. outlineJson: JSON.stringify(outline),
  165. estimatedWords: outline.chapters.reduce((sum, c) => sum + c.estimatedWords, 0),
  166. });
  167. // 创建章节记录
  168. await book_generator_store_1.bookStore.createChapters(bookId, outline.chapters.map(c => ({
  169. number: c.number,
  170. title: c.title,
  171. summary: c.summary,
  172. keyPoints: c.keyPoints,
  173. estimatedWords: c.estimatedWords,
  174. })));
  175. return outline;
  176. }
  177. catch (error) {
  178. await book_generator_store_1.bookStore.update(bookId, { status: 'failed', errorMsg: error instanceof Error ? error.message : '生成失败' });
  179. throw error;
  180. }
  181. }
  182. // ============ 生成章节 ============
  183. async generateChapter(bookId, chapterNumber) {
  184. const book = await book_generator_store_1.bookStore.getById(bookId);
  185. if (!book)
  186. throw new Error('书籍不存在');
  187. if (!book.outline)
  188. throw new Error('请先生成大纲');
  189. const outlineChapter = book.outline.chapters.find((c) => c.number === chapterNumber);
  190. if (!outlineChapter)
  191. throw new Error(`第${chapterNumber}章不存在`);
  192. await book_generator_store_1.bookStore.update(bookId, { status: 'generating' });
  193. try {
  194. const chapterPrompt = this.buildChapterPrompt(book, outlineChapter);
  195. const content = await this.callLLM(chapterPrompt);
  196. const wordCount = this.countWords(content);
  197. const chapter = await book_generator_store_1.bookStore.updateChapter(bookId, chapterNumber, {
  198. content,
  199. wordCount,
  200. status: 'completed',
  201. });
  202. // 更新进度
  203. const completedCount = await book_generator_store_1.bookStore.countCompletedChapters(bookId);
  204. const progress = Math.round((completedCount / book.totalChapters) * 100);
  205. await book_generator_store_1.bookStore.update(bookId, {
  206. progress,
  207. status: progress >= 100 ? 'completed' : 'generating',
  208. });
  209. return chapter;
  210. }
  211. catch (error) {
  212. await book_generator_store_1.bookStore.updateChapter(bookId, chapterNumber, {
  213. status: 'failed',
  214. errorMsg: error instanceof Error ? error.message : '生成失败',
  215. });
  216. throw error;
  217. }
  218. }
  219. async generateAllChapters(bookId) {
  220. const book = await book_generator_store_1.bookStore.getById(bookId);
  221. if (!book)
  222. throw new Error('书籍不存在');
  223. if (!book.outline)
  224. throw new Error('请先生成大纲');
  225. const results = [];
  226. for (const outlineChapter of book.outline.chapters) {
  227. const existingChapter = book.chapters.find((c) => c.number === outlineChapter.number);
  228. if (existingChapter?.status === 'completed') {
  229. results.push(existingChapter);
  230. continue;
  231. }
  232. try {
  233. const chapter = await this.generateChapter(bookId, outlineChapter.number);
  234. results.push(chapter);
  235. }
  236. catch (error) {
  237. console.error(`生成第${outlineChapter.number}章失败:`, error);
  238. }
  239. }
  240. return results;
  241. }
  242. // ============ 前言/后记 ============
  243. async generateForeword(bookId) {
  244. const book = await book_generator_store_1.bookStore.getById(bookId);
  245. if (!book)
  246. throw new Error('书籍不存在');
  247. try {
  248. const prompt = PROMPT_TEMPLATES.forewordPrompt(book);
  249. const foreword = await this.callLLM(prompt);
  250. await book_generator_store_1.bookStore.update(bookId, { foreword });
  251. return foreword;
  252. }
  253. catch (error) {
  254. throw new Error(`生成前言失败: ${error instanceof Error ? error.message : error}`);
  255. }
  256. }
  257. async generateAfterword(bookId) {
  258. const book = await book_generator_store_1.bookStore.getById(bookId);
  259. if (!book)
  260. throw new Error('书籍不存在');
  261. try {
  262. const prompt = PROMPT_TEMPLATES.afterwordPrompt(book);
  263. const afterword = await this.callLLM(prompt);
  264. await book_generator_store_1.bookStore.update(bookId, { afterword });
  265. return afterword;
  266. }
  267. catch (error) {
  268. throw new Error(`生成后记失败: ${error instanceof Error ? error.message : error}`);
  269. }
  270. }
  271. // ============ 内容获取 ============
  272. async getFullContent(bookId) {
  273. const book = await book_generator_store_1.bookStore.getById(bookId);
  274. if (!book)
  275. throw new Error('书籍不存在');
  276. const parts = [];
  277. parts.push(`# ${book.title}`);
  278. if (book.subtitle)
  279. parts.push(`## ${book.subtitle}`);
  280. if (book.metadata?.foreword) {
  281. parts.push('\n## 前言\n');
  282. parts.push(book.metadata.foreword);
  283. }
  284. if (book.outline) {
  285. parts.push('\n## 目录\n');
  286. book.outline.chapters.forEach((ch) => {
  287. parts.push(`${ch.number}. ${ch.title}`);
  288. });
  289. }
  290. book.chapters.forEach((ch) => {
  291. parts.push(`\n## 第${ch.number}章 ${ch.title}\n`);
  292. parts.push(ch.content);
  293. });
  294. if (book.metadata?.afterword) {
  295. parts.push('\n## 后记\n');
  296. parts.push(book.metadata.afterword);
  297. }
  298. return parts.join('\n');
  299. }
  300. async getProgress(bookId) {
  301. const book = await book_generator_store_1.bookStore.getById(bookId);
  302. if (!book)
  303. return null;
  304. const completedChapters = book.chapters.filter((c) => c.status === 'completed').length;
  305. return {
  306. bookId,
  307. status: book.status,
  308. progress: book.progress,
  309. completedChapters,
  310. totalChapters: book.totalChapters,
  311. };
  312. }
  313. // ============ 一键生成(同步阻塞) ============
  314. async generateBook(bookId, options) {
  315. const book = await book_generator_store_1.bookStore.getById(bookId);
  316. if (!book)
  317. return { success: false, completedChapters: 0, failedChapters: 0, errors: ['书籍不存在'] };
  318. const errors = [];
  319. try {
  320. if (!book.outline) {
  321. await this.generateOutline(bookId);
  322. }
  323. }
  324. catch (error) {
  325. return { success: false, completedChapters: 0, failedChapters: 0, errors: [`大纲: ${error instanceof Error ? error.message : error}`] };
  326. }
  327. const results = [];
  328. let failedCount = 0;
  329. for (const outlineChapter of book.outline?.chapters || []) {
  330. try {
  331. const chapter = await this.generateChapter(bookId, outlineChapter.number);
  332. results.push(chapter);
  333. options?.onProgress?.(Math.round((results.length / book.totalChapters) * 100), outlineChapter.number);
  334. }
  335. catch (error) {
  336. failedCount++;
  337. errors.push(`第${outlineChapter.number}章: ${error instanceof Error ? error.message : error}`);
  338. }
  339. }
  340. if (options?.generateForeword) {
  341. try {
  342. await this.generateForeword(bookId);
  343. }
  344. catch (error) {
  345. errors.push(`前言: ${error instanceof Error ? error.message : error}`);
  346. }
  347. }
  348. if (options?.generateAfterword) {
  349. try {
  350. await this.generateAfterword(bookId);
  351. }
  352. catch (error) {
  353. errors.push(`后记: ${error instanceof Error ? error.message : error}`);
  354. }
  355. }
  356. const updatedBook = await book_generator_store_1.bookStore.getById(bookId);
  357. return {
  358. success: failedCount === 0 && errors.length === 0,
  359. book: updatedBook || undefined,
  360. completedChapters: results.length,
  361. failedChapters: failedCount,
  362. errors,
  363. };
  364. }
  365. // ============ 一键生成(异步不阻塞) ============
  366. async generateBookAsync(bookId, options) {
  367. const book = await book_generator_store_1.bookStore.getById(bookId);
  368. if (!book)
  369. return;
  370. try {
  371. await book_generator_store_1.bookStore.update(bookId, { status: 'generating', progress: 0 });
  372. if (!book.outline) {
  373. await this.generateOutline(bookId);
  374. }
  375. // 生成章节
  376. for (const outlineChapter of book.outline?.chapters || []) {
  377. try {
  378. await this.generateChapter(bookId, outlineChapter.number);
  379. const updatedBook = await book_generator_store_1.bookStore.getById(bookId);
  380. await book_generator_store_1.bookStore.update(bookId, { progress: updatedBook.progress });
  381. }
  382. catch (error) {
  383. console.error(`生成第${outlineChapter.number}章失败:`, error);
  384. }
  385. }
  386. if (options?.generateForeword) {
  387. await this.generateForeword(bookId);
  388. }
  389. if (options?.generateAfterword) {
  390. await this.generateAfterword(bookId);
  391. }
  392. await book_generator_store_1.bookStore.update(bookId, { status: 'completed', progress: 100 });
  393. }
  394. catch (error) {
  395. await book_generator_store_1.bookStore.update(bookId, { status: 'failed', errorMsg: error instanceof Error ? error.message : '生成失败' });
  396. }
  397. }
  398. // ============ 私有方法 ============
  399. buildOutlinePrompt(book) {
  400. return PROMPT_TEMPLATES.outlinePrompt
  401. .replace('{title}', book.title)
  402. .replace('{subtitle}', book.subtitle || '无')
  403. .replace('{description}', book.description)
  404. .replace('{targetAudience}', book.targetAudience)
  405. .replace('{style}', book.style)
  406. .replace('{totalChapters}', String(book.totalChapters));
  407. }
  408. buildChapterPrompt(book, chapter) {
  409. if (!book.outline)
  410. throw new Error('书籍大纲不存在');
  411. return PROMPT_TEMPLATES.chapterPrompt(chapter, book.outline).replace('{bookTitle}', book.title);
  412. }
  413. parseOutline(jsonStr) {
  414. try {
  415. const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
  416. if (jsonMatch) {
  417. return this.validateOutline(JSON.parse(jsonMatch[0]));
  418. }
  419. }
  420. catch {
  421. console.error('解析大纲 JSON 失败');
  422. }
  423. return this.createDefaultOutline();
  424. }
  425. validateOutline(data) {
  426. if (!data.chapters || !Array.isArray(data.chapters)) {
  427. throw new Error('大纲格式不正确');
  428. }
  429. return {
  430. mainTheme: data.mainTheme || '主题待定',
  431. structureLogic: data.structureLogic || '由浅入深',
  432. chapters: data.chapters.map((c, i) => ({
  433. number: c.number || i + 1,
  434. title: c.title || `第${i + 1}章`,
  435. summary: c.summary || '',
  436. keyPoints: c.keyPoints || [],
  437. estimatedWords: c.estimatedWords || 1000,
  438. stories: c.stories || [],
  439. })),
  440. };
  441. }
  442. createDefaultOutline() {
  443. return {
  444. mainTheme: '核心主题',
  445. structureLogic: '由浅入深',
  446. chapters: [{ number: 1, title: '概述', summary: '介绍', keyPoints: ['基础概念'], estimatedWords: 1000 }],
  447. };
  448. }
  449. countWords(text) {
  450. return (text.match(/[\u4e00-\u9fa5]/g) || []).length;
  451. }
  452. async callLLM(prompt, retryCount = 0, currentModel) {
  453. const modelId = currentModel || this.config.model;
  454. // 从模型配置获取 API Key 和 URL
  455. const modelConfig = config_1.config.models.getModel(modelId);
  456. if (!modelConfig?.apiKey || !modelConfig?.baseUrl) {
  457. throw new Error(`模型 ${modelId} 缺少 API 配置`);
  458. }
  459. const { apiKey, baseUrl } = modelConfig;
  460. try {
  461. const response = await axios_1.default.post(`${baseUrl}/chat/completions`, {
  462. model: modelId,
  463. messages: [{ role: 'user', content: prompt }],
  464. }, {
  465. headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  466. timeout: 120000,
  467. });
  468. const result = response.data?.choices?.[0]?.message?.content || '';
  469. if (!result)
  470. throw new Error('API 返回内容为空');
  471. return result;
  472. }
  473. catch (error) {
  474. const errorMessage = error.response?.data?.message || error.message || 'API 调用失败';
  475. console.log(`[LLM] 模型 ${modelId} 调用失败: ${errorMessage}`);
  476. // 检查是否需要切换模型
  477. if (config_1.config.models.shouldSwitchModel(errorMessage)) {
  478. const nextModel = config_1.config.models.getNextModel(modelId, 'text');
  479. if (nextModel) {
  480. console.log(`[LLM] 自动切换到模型: ${nextModel}`);
  481. return this.callLLM(prompt, 0, nextModel); // 重置 retryCount
  482. }
  483. }
  484. // 重试当前模型
  485. if (retryCount < this.config.retryPolicy.maxRetries) {
  486. await this.delay(this.config.retryPolicy.retryDelay);
  487. return this.callLLM(prompt, retryCount + 1, currentModel);
  488. }
  489. throw new Error(errorMessage);
  490. }
  491. }
  492. delay(ms) {
  493. return new Promise((resolve) => setTimeout(resolve, ms));
  494. }
  495. // ============ 音频生成 ============
  496. /**
  497. * 生成单个章节音频
  498. */
  499. async generateChapterAudio(bookId, chapterNumber, voiceId) {
  500. const book = await book_generator_store_1.bookStore.getById(bookId);
  501. if (!book)
  502. throw new Error('书籍不存在');
  503. const chapter = book.chapters.find((c) => c.number === chapterNumber);
  504. if (!chapter)
  505. throw new Error(`第${chapterNumber}章不存在`);
  506. if (!chapter.content || chapter.content.trim().length === 0) {
  507. throw new Error('章节内容为空,请先生成章节内容');
  508. }
  509. // 从数据库获取章节记录
  510. const dbChapter = await models_1.prisma.bookChapter.findFirst({
  511. where: {
  512. bookId: parseInt(bookId),
  513. number: chapterNumber,
  514. },
  515. });
  516. if (!dbChapter)
  517. throw new Error('章节数据库记录不存在');
  518. const taskId = `audio_${bookId}_${chapterNumber}_${Date.now()}`;
  519. // 异步生成音频
  520. this.processChapterAudio(dbChapter.id, chapter.content, voiceId).catch((err) => {
  521. console.error(`章节 ${chapterNumber} 音频生成失败:`, err);
  522. });
  523. return { taskId, chapterId: String(dbChapter.id) };
  524. }
  525. /**
  526. * 批量生成书籍所有章节音频
  527. */
  528. async generateAllChaptersAudio(bookId, voiceId) {
  529. const book = await book_generator_store_1.bookStore.getById(bookId);
  530. if (!book)
  531. throw new Error('书籍不存在');
  532. const completedChapters = book.chapters.filter((c) => c.status === 'completed' && c.content && c.content.trim().length > 0);
  533. if (completedChapters.length === 0) {
  534. throw new Error('没有可生成音频的章节,请先生成章节内容');
  535. }
  536. const taskId = `audio_book_${bookId}_${Date.now()}`;
  537. // 异步批量生成音频
  538. this.processAllChaptersAudio(bookId, completedChapters, voiceId).catch((err) => {
  539. console.error(`书籍 ${bookId} 批量音频生成失败:`, err);
  540. });
  541. return { taskId, totalChapters: completedChapters.length };
  542. }
  543. /**
  544. * 处理单个章节音频生成
  545. */
  546. async processChapterAudio(chapterId, content, voiceId) {
  547. try {
  548. console.log(`🎵 开始生成章节 ${chapterId} 音频...`);
  549. // 清理内容(移除 markdown 格式)
  550. const cleanContent = content
  551. .replace(/^#{1,6}\s+/gm, '') // 移除标题标记
  552. .replace(/\*\*(.*?)\*\*/g, '$1') // 移除粗体
  553. .replace(/\*(.*?)\*/g, '$1') // 移除斜体
  554. .replace(/`(.*?)`/g, '$1') // 移除行内代码
  555. .replace(/^\s*[-*+]\s+/gm, '') // 移除列表标记
  556. .replace(/^\s*\d+\.\s+/gm, '') // 移除数字列表标记
  557. .trim();
  558. // 调用 TTS 服务生成音频
  559. const result = await TtsService.generateAudio('system', cleanContent, voiceId, { speed: 1.0, pitch: 0, volume: 50 }, async (audioUrl, duration) => {
  560. // 音频生成完成后更新数据库
  561. await models_1.prisma.bookChapter.update({
  562. where: { id: chapterId },
  563. data: {
  564. audioUrl,
  565. audioDuration: duration,
  566. },
  567. });
  568. console.log(`✅ 章节 ${chapterId} 音频生成完成: ${audioUrl}`);
  569. });
  570. console.log(`🎵 章节 ${chapterId} 音频任务已启动: ${result.audioId}`);
  571. }
  572. catch (error) {
  573. console.error(`❌ 章节 ${chapterId} 音频生成失败:`, error);
  574. throw error;
  575. }
  576. }
  577. /**
  578. * 处理批量章节音频生成
  579. */
  580. async processAllChaptersAudio(bookId, chapters, voiceId) {
  581. console.log(`🎵 开始批量生成书籍 ${bookId} 的 ${chapters.length} 个章节音频...`);
  582. // 从数据库获取所有章节记录
  583. const dbChapters = await models_1.prisma.bookChapter.findMany({
  584. where: { bookId: parseInt(bookId) },
  585. });
  586. for (const chapter of chapters) {
  587. const dbChapter = dbChapters.find((c) => c.number === chapter.number);
  588. if (dbChapter && chapter.content) {
  589. try {
  590. await this.processChapterAudio(dbChapter.id, chapter.content, voiceId);
  591. // 每个章节之间稍作延迟,避免请求过于密集
  592. await this.delay(1000);
  593. }
  594. catch (error) {
  595. console.error(`第${chapter.number}章音频生成失败,继续下一个:`, error);
  596. }
  597. }
  598. }
  599. console.log(`✅ 书籍 ${bookId} 批量音频生成任务完成`);
  600. }
  601. // ============ 视频生成 ============
  602. /**
  603. * 生成单个章节视频(从音频转视频)
  604. */
  605. async generateChapterVideo(bookId, chapterNumber) {
  606. const book = await book_generator_store_1.bookStore.getById(bookId);
  607. if (!book)
  608. throw new Error('书籍不存在');
  609. const chapter = book.chapters.find((c) => c.number === chapterNumber);
  610. if (!chapter)
  611. throw new Error(`第${chapterNumber}章不存在`);
  612. if (!chapter.audioUrl) {
  613. throw new Error('章节音频不存在,请先生成音频');
  614. }
  615. // 获取数据库中的章节记录
  616. const dbChapter = await models_1.prisma.bookChapter.findFirst({
  617. where: {
  618. bookId: parseInt(bookId),
  619. number: chapterNumber,
  620. },
  621. });
  622. if (!dbChapter)
  623. throw new Error('章节数据库记录不存在');
  624. // 创建视频项目并生成
  625. const videoProject = await VideoService.createVideoProjectFromBook(parseInt(bookId), dbChapter.id);
  626. if (!videoProject) {
  627. throw new Error('创建视频项目失败');
  628. }
  629. // 异步生成视频
  630. this.processChapterVideo(videoProject.id).catch((err) => {
  631. console.error(`章节 ${chapterNumber} 视频生成失败:`, err);
  632. });
  633. return { projectId: videoProject.id, chapterId: String(dbChapter.id) };
  634. }
  635. /**
  636. * 批量生成书籍所有章节视频
  637. */
  638. async generateAllChaptersVideo(bookId) {
  639. const book = await book_generator_store_1.bookStore.getById(bookId);
  640. if (!book)
  641. throw new Error('书籍不存在');
  642. const chaptersWithAudio = book.chapters.filter((c) => c.status === 'completed' && c.audioUrl);
  643. if (chaptersWithAudio.length === 0) {
  644. throw new Error('没有可生成视频的章节(需要先有音频)');
  645. }
  646. const taskId = `video_book_${bookId}_${Date.now()}`;
  647. // 异步批量生成视频
  648. this.processAllChaptersVideo(bookId, chaptersWithAudio).catch((err) => {
  649. console.error(`书籍 ${bookId} 批量视频生成失败:`, err);
  650. });
  651. return { taskId, totalChapters: chaptersWithAudio.length };
  652. }
  653. /**
  654. * 处理单个章节视频生成
  655. */
  656. async processChapterVideo(projectId) {
  657. try {
  658. console.log(`🎬 开始生成视频项目 ${projectId}...`);
  659. const result = await VideoService.generateVideoForProject(projectId);
  660. if (result.success) {
  661. console.log(`✅ 视频项目 ${projectId} 生成完成: ${result.outputUrl}`);
  662. }
  663. else {
  664. console.error(`❌ 视频项目 ${projectId} 生成失败: ${result.error}`);
  665. }
  666. }
  667. catch (error) {
  668. console.error(`❌ 视频项目 ${projectId} 生成失败:`, error);
  669. throw error;
  670. }
  671. }
  672. /**
  673. * 处理批量章节视频生成
  674. */
  675. async processAllChaptersVideo(bookId, chapters) {
  676. console.log(`🎬 开始批量生成书籍 ${bookId} 的 ${chapters.length} 个章节视频...`);
  677. for (const chapter of chapters) {
  678. try {
  679. const dbChapter = await models_1.prisma.bookChapter.findFirst({
  680. where: {
  681. bookId: parseInt(bookId),
  682. number: chapter.number,
  683. },
  684. });
  685. if (!dbChapter || !chapter.audioUrl) {
  686. console.warn(`第${chapter.number}章跳过:缺少音频或数据库记录`);
  687. continue;
  688. }
  689. // 创建视频项目
  690. const videoProject = await VideoService.createVideoProjectFromBook(parseInt(bookId), dbChapter.id);
  691. if (videoProject) {
  692. // 生成视频
  693. await this.processChapterVideo(videoProject.id);
  694. }
  695. // 每个视频之间稍作延迟
  696. await this.delay(2000);
  697. }
  698. catch (error) {
  699. console.error(`第${chapter.number}章视频生成失败,继续下一个:`, error);
  700. }
  701. }
  702. console.log(`✅ 书籍 ${bookId} 批量视频生成任务完成`);
  703. }
  704. }
  705. exports.BookGeneratorService = BookGeneratorService;
  706. // 导出单例
  707. exports.bookGeneratorService = new BookGeneratorService();
  708. exports.bookGeneratorService.setApiKey(config_1.config.dashscope?.apiKey || '');