/** * LangGraph 书籍生成 - API 路由 * 支持书籍创建时的预估显示 */ import Router from '@koa/router'; import { Context } from 'koa'; import { langGraphGenerator, getScaleConfig } from './index'; import { queueService, QueueType } from '../../services/queue.service'; import { bookStore } from './book-generator.store'; import { estimateBookWords, estimateAudioMinutesFromWords, checkBookGenerationQuota } from '../subscription/subscription.service'; import { optionalAuth } from '../../middleware/auth'; import { getAllBookTypes, getDetectableTypes, getBookTypeConfig, BOOK_TYPE_CONFIG, DETECTABLE_TYPES } from './book-type-config'; import { callLLMWithMessages, ChatMessage } from '../../services/llm'; import { createVideoProjectFromBook, generateVideoForProject } from '../video-generator/video-generator.service'; // 开发环境测试用户ID const TEST_USER_ID = '1'; const router = new Router(); /** * GET /api/book-generator/langgraph/estimate * 获取书籍规模预估信息 */ router.get('/estimate', async (ctx: Context) => { const { scale, userId } = ctx.query as { scale?: string; userId?: string }; if (!scale) { ctx.status = 400; ctx.body = { code: 1, message: '请提供书籍规模' }; return; } const scaleConfig = getScaleConfig(scale); const avgWords = Math.round((scaleConfig.wordRange.min + scaleConfig.wordRange.max) / 2); const audioMinutes = estimateAudioMinutesFromWords(avgWords); const estimatedChapters = Math.round((scaleConfig.chapterRange.min + scaleConfig.chapterRange.max) / 2); const result: any = { scale, scaleConfig, audioMinutes: { min: estimateAudioMinutesFromWords(scaleConfig.wordRange.min), max: estimateAudioMinutesFromWords(scaleConfig.wordRange.max), avg: audioMinutes }, estimatedChapters }; // 如果提供了 userId,同时检查用户配额 if (userId) { const quotaCheck = await checkBookGenerationQuota(parseInt(userId), scale); result.quotaCheck = quotaCheck; } ctx.body = { code: 0, message: 'success', data: result }; }); /** * GET /api/book-generator/langgraph/book-types * 获取所有书籍类型配置(供前端显示) */ router.get('/book-types', async (ctx: Context) => { const types = getAllBookTypes().map(t => ({ key: t.key, label: t.label, description: t.description, chapters: t.chapters, totalWords: t.totalWords, chapterWords: t.chapterWords, sectionWords: t.sectionWords, structureFormat: t.structureFormat, readingDifficulty: t.readingDifficulty, isShortArticle: t.isShortArticle, })); ctx.body = { code: 0, message: 'success', data: types, }; }); /** * POST /api/book-generator/langgraph/detect-book-type * AI 自动检测书籍类型 */ router.post('/detect-book-type', async (ctx: Context) => { const { title, description } = ctx.request.body as { title?: string; description?: string; }; if (!title) { ctx.status = 400; ctx.body = { code: 1, message: '请提供书籍标题' }; return; } const detectableTypes = DETECTABLE_TYPES; // 构建类型特征描述,帮助 AI 分类 const typeDescriptions = detectableTypes.map(key => { const t = BOOK_TYPE_CONFIG[key]; return `- ${t.key}: ${t.description},${t.chapters.min}-${t.chapters.max}章,总字数${t.totalWords.min}-${t.totalWords.max}字,每章约${t.chapterWords}字,结构形式:${t.structureFormat},阅读难度:${t.readingDifficulty}`; }).join('\n'); const messages: ChatMessage[] = [ { role: 'system', content: `你是一位专业的图书分类编辑。根据用户提供的标题和描述,判断这本书最可能属于以下哪种类型: 可选类型: ${typeDescriptions} ## 分类规则 1. 分析标题中的关键词(如"科普""青少年""专业""小说"等) 2. 分析描述中的目标读者、写作风格、内容深度 3. 匹配最符合的类型 ## 输出格式 必须返回 JSON,不要包含 markdown 代码块标记: { "detectedType": "类型 key", "confidence": 0.85, "reasoning": "分类理由,1-2句话" } confidence 是 0-1 的数值,表示置信程度。`, }, { role: 'user', content: `标题:《${title}》\n描述:${description || '无'}`, }, ]; try { const response = await callLLMWithMessages(messages); const parsed = parseDetectResult(response); if (!parsed) { // 降级到关键词匹配 const fallback = keywordFallback(title, description || ''); ctx.body = { code: 0, message: 'success', data: { detectedType: fallback.detectedType, confidence: 0.5, reasoning: '基于关键词匹配(AI 解析失败,使用降级策略)', config: getBookTypeConfig(fallback.detectedType), }, }; return; } ctx.body = { code: 0, message: 'success', data: { detectedType: parsed.detectedType, confidence: parsed.confidence, reasoning: parsed.reasoning, config: getBookTypeConfig(parsed.detectedType), }, }; } catch (error: any) { // 最终降级:关键词匹配 const fallback = keywordFallback(title, description || ''); ctx.body = { code: 0, message: 'success', data: { detectedType: fallback.detectedType, confidence: 0.3, reasoning: 'LLM 调用失败,使用关键词匹配降级', config: getBookTypeConfig(fallback.detectedType), }, }; } }); function parseDetectResult(text: string): { detectedType: string; confidence: number; reasoning: string } | null { try { const match = text.match(/\{[\s\S]*\}/); if (!match) return null; const data = JSON.parse(match[0]); if (!data.detectedType) return null; // 验证类型是否有效 if (!DETECTABLE_TYPES.includes(data.detectedType)) { // 尝试模糊匹配 const found = DETECTABLE_TYPES.find(t => data.detectedType.includes(t) || t.includes(data.detectedType)); if (!found) return null; data.detectedType = found; } return { detectedType: data.detectedType, confidence: Math.min(1, Math.max(0, data.confidence || 0.5)), reasoning: data.reasoning || '', }; } catch { return null; } } function keywordFallback(title: string, description: string): { detectedType: string } { const text = `${title} ${description}`.toLowerCase(); if (text.includes('小说') || text.includes('故事') || text.includes('fiction')) { if (text.includes('网络') || text.includes('连载') || text.includes('修仙') || text.includes('穿越')) { return { detectedType: '网络小说' }; } return { detectedType: '现代出版长篇小说' }; } if (text.includes('科普') || text.includes('经管') || text.includes('畅销') || text.includes('通俗')) { return { detectedType: '科普经管畅销书' }; } if (text.includes('专业') || text.includes('大学') || text.includes('研究生') || text.includes('算法') || text.includes('操作系统') || text.includes('数据库')) { return { detectedType: '大学专业教材' }; } if (text.includes('中小学') || text.includes('初中') || text.includes('高中') || text.includes('青少年') || text.includes('儿童')) { return { detectedType: '中小学课本' }; } if (text.includes('古典') || text.includes('章回') || text.includes('名著') || text.includes('红楼') || text.includes('西游') || text.includes('三国') || text.includes('水浒')) { return { detectedType: '古典名著' }; } return { detectedType: '中小学课本' }; // 默认 } /** * AI 自动检测书籍类型(后端静默调用,前端无感知) */ async function autoDetectBookType(title: string, description: string): Promise { const detectableTypes = DETECTABLE_TYPES; const typeDescriptions = detectableTypes.map(key => { const t = BOOK_TYPE_CONFIG[key]; return `- ${t.key}: ${t.description},${t.chapters.min}-${t.chapters.max}章,总字数${t.totalWords.min}-${t.totalWords.max}字`; }).join('\n'); const messages: ChatMessage[] = [ { role: 'system', content: `你是一位专业的图书分类编辑。根据标题和描述,判断书籍属于以下哪种类型: 可选类型: ${typeDescriptions} 分类规则: 1. 分析标题中的关键词(如"科普""青少年""专业""小说"等) 2. 分析描述中的目标读者、写作风格、内容深度 3. 匹配最符合的类型 输出格式:只返回类型 key,不要其他内容。`, }, { role: 'user', content: `标题:《${title}》\n描述:${description}`, }, ]; try { const response = await callLLMWithMessages(messages); const trimmed = response.trim(); if (detectableTypes.includes(trimmed)) return trimmed; const found = detectableTypes.find(t => trimmed.includes(t) || t.includes(trimmed)); if (found) return found; return keywordFallback(title, description).detectedType; } catch { return keywordFallback(title, description).detectedType; } } /** * POST /api/book-generator/langgraph/books * 使用 LangGraph 创建并生成书籍(异步,自动生成大纲和内容) * AI 根据标题+描述自动判断书籍类型 */ router.post('/books', async (ctx: Context) => { try { const body = ctx.request.body as { title: string; description: string; bookScale?: string; generateForeword?: boolean; generateAfterword?: boolean; }; if (!body.title || !body.description) { ctx.status = 400; ctx.body = { code: 1, message: '书名和描述不能为空' }; return; } // AI 自动检测书籍类型(如果前端传了 bookScale 则用前端的,否则自动检测) let bookScale = body.bookScale; if (!bookScale) { const detectResult = await autoDetectBookType(body.title, body.description); bookScale = detectResult; console.log(`[LangGraph] AI 自动检测类型: ${body.title} -> ${bookScale}`); } // 创建书籍(预估章节数,实际数量由AI根据字数范围分析后确定) const scaleConfig = getScaleConfig(bookScale); const estimatedChapters = Math.round((scaleConfig.chapterRange.min + scaleConfig.chapterRange.max) / 2); const book = await bookStore.create({ title: body.title, description: body.description, bookScale: bookScale, totalChapters: estimatedChapters, }); // 尝试将生成任务加入队列(队列只是为了改善用户体验) try { const jobId = await queueService.addBookGenerationTask({ bookId: book.id, topic: body.description, bookScale, }); console.log(`[LangGraph] 生成任务已加入队列: bookId=${book.id}, jobId=${jobId}`); ctx.body = { code: 0, message: '书籍创建成功,生成已开始(队列模式)', data: { book, jobId, status: 'generating', mode: 'queue', }, }; } catch (queueError) { // 队列失败时,降级为同步执行(确保核心业务不受影响) console.warn(`[LangGraph] 队列不可用,降级为同步执行: bookId=${book.id}`, queueError); try { // 同步调用生成器,等待完成 await langGraphGenerator.generate(book.id.toString(), body.description, bookScale); console.log(`[LangGraph] 同步执行完成: bookId=${book.id}`); ctx.body = { code: 0, message: '书籍创建成功,生成已完成(同步模式)', data: { book, status: 'completed', mode: 'sync', }, }; } catch (generateError) { // 生成失败,更新书籍状态 console.error(`[LangGraph] 同步执行失败: bookId=${book.id}`, generateError); await bookStore.update(book.id.toString(), { status: 'failed', errorMsg: generateError instanceof Error ? generateError.message : '生成失败', }); ctx.body = { code: 0, message: '书籍创建成功,但生成失败', data: { book, status: 'failed', mode: 'sync', error: generateError instanceof Error ? generateError.message : '生成失败', }, }; } } } catch (error) { console.error('启动失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '启动失败', }; } }); /** * GET /api/book-generator/langgraph/books * 获取书籍列表 * 返回:公开的书籍(有公开音频)+ 当前用户自己的书籍 * 注意:此接口已废弃,请使用 /public-books 或 /my-books */ router.get('/books', optionalAuth, async (ctx: Context) => { try { const userId = ctx.state.user?.userId; const userIdNum = userId ? parseInt(userId as string) : undefined; // 获取公开书籍(有公开音频的书籍) const publicBooks = await bookStore.getPublicBooks(); // 如果用户已登录,获取用户自己的书籍 let userBooks: any[] = []; if (userIdNum) { userBooks = await bookStore.getAllByUser(userIdNum, false); } // 合并并去重(按 id) const bookMap = new Map(); publicBooks.forEach(b => bookMap.set(b.id, b)); userBooks.forEach(b => { if (!bookMap.has(b.id)) { bookMap.set(b.id, b); } }); const books = Array.from(bookMap.values()); ctx.body = { code: 0, message: 'success', data: { books } }; } catch (error) { console.error('查询失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' }; } }); /** * GET /api/book-generator/langgraph/public-books * 获取公开书籍列表(首页专用) * 只返回有公开音频的书籍 */ router.get('/public-books', async (ctx: Context) => { try { const publicBooks = await bookStore.getPublicBooks(); ctx.body = { code: 0, message: 'success', data: { books: publicBooks } }; } catch (error) { console.error('查询公开书籍失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' }; } }); /** * GET /api/book-generator/langgraph/my-books * 获取当前用户自己的书籍列表(管理页专用) * 只返回当前用户创建的书籍 */ router.get('/my-books', optionalAuth, async (ctx: Context) => { try { const userId = ctx.state.user?.userId; if (!userId) { ctx.status = 401; ctx.body = { code: 1, message: '请先登录' }; return; } const userBooks = await bookStore.getAllByUser(parseInt(userId as string), false); ctx.body = { code: 0, message: 'success', data: { books: userBooks } }; } catch (error) { console.error('查询用户书籍失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' }; } }); /** * PUT /api/book-generator/langgraph/books/:id/publish * 切换书籍公开状态 */ router.put('/books/:id/publish', optionalAuth, async (ctx: Context) => { try { const bookId = ctx.params.id; const newStatus = await bookStore.togglePublish(bookId); ctx.body = { code: 0, message: 'success', data: { isPublished: newStatus } }; } catch (error) { console.error('切换发布状态失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '操作失败' }; } }); /** * GET /api/book-generator/langgraph/books/:id * 获取书籍详情 * 支持公开过滤:?filterPublic=true&userId=1 */ router.get('/books/:id', optionalAuth, async (ctx: Context) => { try { const bookId = ctx.params.id as string; const userId = ctx.state.user?.userId || TEST_USER_ID; const filterPublic = ctx.query.filterPublic === 'true'; const book = await bookStore.getById(bookId, filterPublic, parseInt(userId as string)); if (!book) { ctx.status = 404; ctx.body = { code: 1, message: '书籍不存在' }; return; } ctx.body = { code: 0, message: 'success', data: { book } }; } catch (error) { console.error('查询失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' }; } }); /** * GET /api/book-generator/langgraph/books/:id/progress * 获取书籍生成进度 */ router.get('/books/:id/progress', async (ctx: Context) => { try { const bookId = ctx.params.id as string; const book = await bookStore.getById(bookId); if (!book) { ctx.status = 404; ctx.body = { code: 1, message: '书籍不存在' }; return; } // 计算实际进度(基于章节完成情况) const completedChapters = book.chapters.filter((c) => c.status === 'completed').length; const totalChapters = book.outline?.chapters?.length || book.totalChapters || 0; // 如果有大纲,使用大纲章节数计算进度 let progress = book.progress; if (totalChapters > 0 && book.status !== 'completed') { progress = Math.round((completedChapters / totalChapters) * 100); } ctx.body = { code: 0, message: 'success', data: { bookId, status: book.status, progress, completedChapters, totalChapters, }, }; } catch (error) { console.error('查询进度失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' }; } }); /** * DELETE /api/book-generator/langgraph/books/:id * 删除书籍 */ router.delete('/books/:id', async (ctx: Context) => { try { const bookId = ctx.params.id as string; await bookStore.delete(bookId); ctx.body = { code: 0, message: '删除成功' }; } catch (error) { console.error('删除失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '删除失败' }; } }); /** * POST /api/book-generator/langgraph/books/:id/generate * 对已有书籍使用 LangGraph 生成 */ router.post('/books/:id/generate', async (ctx: Context) => { try { const bookId = ctx.params.id as string; console.log(`[LangGraph Generate] 收到请求 bookId=${bookId}`); const body = (ctx.request.body || {}) as { bookScale?: string }; const book = await bookStore.getById(bookId); console.log(`[LangGraph Generate] book对象:`, book ? '存在' : 'null'); if (!book) { ctx.status = 404; ctx.body = { code: 1, message: '书籍不存在' }; return; } // 防止重复生成:如果书籍正在生成中,拒绝请求 if (book.status === 'generating') { console.log(`[LangGraph Generate] 书籍正在生成中,拒绝重复请求 bookId=${bookId}`); ctx.status = 400; ctx.body = { code: 1, message: '书籍正在生成中,请勿重复提交', data: { bookId, status: book.status, progress: book.progress, } }; return; } // 优先使用请求传入的 scale,否则使用书籍保存的 scale,最后默认标准教程 const bookScale = body.bookScale || book.bookScale || '标准教程'; // 尝试将生成任务加入队列(队列只是为了改善用户体验) try { const jobId = await queueService.addBookGenerationTask({ bookId, topic: book.description, bookScale, }); console.log(`[LangGraph] 生成任务已加入队列: bookId=${bookId}, jobId=${jobId}`); ctx.body = { code: 0, message: 'LangGraph 生成任务已启动(队列模式)', data: { bookId, jobId, status: 'queued', mode: 'queue', }, }; } catch (queueError) { // 队列失败时,降级为同步执行(确保核心业务不受影响) console.warn(`[LangGraph] 队列不可用,降级为同步执行: bookId=${bookId}`, queueError); try { // 同步调用生成器,等待完成 await langGraphGenerator.generate(bookId, book.description, bookScale); console.log(`[LangGraph] 同步执行完成: bookId=${bookId}`); ctx.body = { code: 0, message: 'LangGraph 生成任务已完成(同步模式)', data: { bookId, status: 'completed', mode: 'sync', }, }; } catch (generateError) { // 生成失败,更新书籍状态 console.error(`[LangGraph] 同步执行失败: bookId=${bookId}`, generateError); await bookStore.update(bookId, { status: 'failed', errorMsg: generateError instanceof Error ? generateError.message : '生成失败', }); ctx.body = { code: 0, message: 'LangGraph 生成任务失败', data: { bookId, status: 'failed', mode: 'sync', error: generateError instanceof Error ? generateError.message : '生成失败', }, }; } } } catch (error) { console.error('启动失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '启动失败', }; } }); /** * POST /api/book-generator/langgraph/books/:id/audio * 批量生成书籍所有小节的音频 */ router.post('/books/:id/audio', async (ctx: Context) => { try { const bookId = ctx.params.id as string; const { voiceId = 'cherry' } = ctx.request.body as { voiceId?: string }; const book = await bookStore.getById(bookId); if (!book) { ctx.status = 404; ctx.body = { code: 1, message: '书籍不存在' }; return; } // 获取所有小节 (level=3) const chapters = await bookStore.getChapterTree(bookId); const subsections = chapters.filter((c: any) => c.level === 3); // 检查内容状态 const subsectionsWithContent = subsections.filter((c: any) => c.content && c.contentStatus === 'completed'); if (subsections.length === 0) { ctx.body = { code: 1, message: '没有小节' }; return; } if (subsectionsWithContent.length === 0) { ctx.body = { code: 1, message: '没有内容生成完成的小节,请先生成内容', data: { totalSubsections: subsections.length, completedSubsections: 0, } }; return; } if (subsections.length !== subsectionsWithContent.length) { console.log(`[Audio] 书籍 ${bookId} 共有 ${subsections.length} 个小节,其中 ${subsectionsWithContent.length} 个内容已生成完成`); } // 异步生成所有小节音频 for (const sub of subsectionsWithContent) { bookStore.generateChapterAudioById(sub.id, book.userId || 1).catch(err => { console.error(`[Audio] 小节${sub.number}音频生成失败:`, err); }); } ctx.body = { code: 0, message: '音频生成任务已启动', data: { totalSubsections: subsectionsWithContent.length, taskId: `audio_${bookId}_${Date.now()}`, }, }; } catch (error) { console.error('音频生成失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '音频生成失败' }; } }); /** * GET /api/book-generator/langgraph/books/:id/failed-chapters * 获取生成失败的小节列表 */ router.get('/books/:id/failed-chapters', async (ctx: Context) => { try { const bookId = ctx.params.id as string; const book = await bookStore.getById(bookId); if (!book) { ctx.status = 404; ctx.body = { code: 1, message: '书籍不存在' }; return; } // 获取所有失败的小节 const chapters = await bookStore.getChapterTree(bookId); const failedSubsections = chapters.filter(c => c.status === 'failed'); ctx.body = { code: 0, message: 'success', data: { bookId, failedCount: failedSubsections.length, failedChapters: failedSubsections.map(c => ({ id: c.id, number: c.number, title: c.title, level: c.level, errorMsg: c.errorMsg, parentId: c.parentId, })), }, }; } catch (error) { console.error('获取失败小节失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '获取失败' }; } }); /** * POST /api/book-generator/langgraph/books/:id/resume * 从断点处继续生成(重试失败的小节) */ router.post('/books/:id/resume', async (ctx: Context) => { try { const bookId = ctx.params.id as string; const book = await bookStore.getById(bookId); if (!book) { ctx.status = 404; ctx.body = { code: 1, message: '书籍不存在' }; return; } if (book.status === 'completed') { ctx.body = { code: 1, message: '书籍已生成完成,无需继续' }; return; } // 获取失败的小节 const chapters = await bookStore.getChapterTree(bookId); const failedSubsections = chapters.filter(c => c.status === 'failed'); if (failedSubsections.length === 0) { ctx.body = { code: 1, message: '没有失败的小节需要重试' }; return; } // 重置失败小节的状态为 pending for (const sub of failedSubsections) { await bookStore.updateChapterById(sub.id, { status: 'pending', errorMsg: null, content: null, }); } // 将续生成任务加入队列 const jobId = await queueService.addBookGenerationTask({ bookId, topic: book.description || book.title, bookScale: book.bookScale || '标准教程', }); console.log(`[LangGraph] 续生成任务已加入队列: bookId=${bookId}, jobId=${jobId}`); ctx.body = { code: 0, message: `已重启生成,将重试 ${failedSubsections.length} 个失败的小节`, data: { bookId, retryCount: failedSubsections.length, status: 'resuming', }, }; } catch (error) { console.error('继续生成失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '继续生成失败' }; } }); /** * POST /api/book-generator/langgraph/books/:id/retry-chapter * 单独重试某个失败的小节 */ router.post('/books/:id/retry-chapter', async (ctx: Context) => { try { const bookId = ctx.params.id as string; const { chapterId } = ctx.request.body as { chapterId: number }; if (!chapterId) { ctx.status = 400; ctx.body = { code: 1, message: '请提供章节 ID' }; return; } // 获取所有章节查找指定的章节 const chapters = await bookStore.getChapterTree(bookId); const chapter = chapters.find(c => c.id === chapterId); if (!chapter) { ctx.status = 404; ctx.body = { code: 1, message: '章节不存在' }; return; } // 重置章节状态 await bookStore.updateChapterById(chapterId, { status: 'pending', errorMsg: null, content: null, }); ctx.body = { code: 0, message: '章节已重置为待生成状态', data: { chapterId }, }; } catch (error) { console.error('重试章节失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '重试失败' }; } }); /** * POST /api/book-generator/langgraph/books/:id/videos * 批量生成书籍所有章节视频 */ router.post('/books/:id/videos', async (ctx: Context) => { try { const bookId = ctx.params.id as string; const book = await bookStore.getById(bookId); if (!book) { ctx.status = 404; ctx.body = { code: 1, message: '书籍不存在' }; return; } // 获取所有小节 (level=3) const chapters = await bookStore.getChapterTree(bookId); const subsections = chapters.filter((c: any) => c.level === 3); // 过滤出有音频的小节(audioUrl存在且不为空) const subsectionsWithAudio = subsections.filter((c: any) => c.audioUrl && c.audioUrl !== ''); if (subsections.length === 0) { ctx.body = { code: 1, message: '没有小节' }; return; } if (subsectionsWithAudio.length === 0) { ctx.body = { code: 1, message: '没有音频生成完成的小节,请先生成音频', data: { totalSubsections: subsections.length, audioCompletedSubsections: 0, } }; return; } if (subsections.length !== subsectionsWithAudio.length) { console.log(`[Video] 书籍 ${bookId} 共有 ${subsections.length} 个小节,其中 ${subsectionsWithAudio.length} 个音频已生成完成`); } // 异步生成所有章节视频 const taskId = `video_${bookId}_${Date.now()}`; console.log(`[Video] 开始批量生成视频: bookId=${bookId}, taskId=${taskId}, 总数=${subsectionsWithAudio.length}`); for (const sub of subsectionsWithAudio) { // 异步处理每个章节的视频生成 (async () => { try { console.log(`[Video] 开始生成章节 ${sub.number} 的视频: ${sub.title}`); // 从书籍章节创建视频项目 const project = await createVideoProjectFromBook( parseInt(bookId), sub.id, book.userId || 1 ); if (!project) { console.error(`[Video] 章节 ${sub.number} 视频项目创建失败`); return; } console.log(`[Video] 章节 ${sub.number} 视频项目创建成功: projectId=${project.id}`); // 生成视频 const result = await generateVideoForProject(project.id); if (result.success && result.outputUrl) { // 更新章节的视频URL await bookStore.updateChapterById(sub.id, { videoUrl: result.outputUrl, videoDuration: result.duration, }); console.log(`[Video] 章节 ${sub.number} 视频生成成功: ${result.outputUrl}`); } else { console.error(`[Video] 章节 ${sub.number} 视频生成失败:`, result.error); } } catch (error) { console.error(`[Video] 章节 ${sub.number} 视频生成异常:`, error); } })(); } ctx.body = { code: 0, message: '视频生成任务已启动', data: { taskId, totalChapters: subsectionsWithAudio.length, totalSubsections: subsections.length, }, }; } catch (error) { console.error('批量生成视频失败:', error); ctx.status = 500; ctx.body = { code: 1, message: error instanceof Error ? error.message : '批量生成视频失败' }; } }); export default router;