Explorar o código

test(tts): 端到端测试脚本 + toBook 支持 targetLanguage/voiceSpeed

新增 deploy-package/scripts/test-language-e2e.sh:
- 创建 zh-CMN + en-US 各一本书
- 等 AI 生成内容
- 查 Book.targetLanguage + 章节内容语言(中文/英文各一份)
- 触发 TTS 生成
- 查 TtsTask.targetLanguage/preferredVendor/preferredModel

server/src/modules/book-generator/book-generator.store.ts:
- toBook() 补 targetLanguage + voiceSpeed 字段(Prisma client 类型未自动包含,cast)
- 修复 schema 返回字段后,前端 'language 字段未设' 显示问题

server/src/modules/book-generator/book-generator.types.ts:
- BookBase 加 targetLanguage?: string 字段

测试结果(2026-07-12):
- book 77 zh-CMN: 内容='普通话语音合成测试文本'(中文 ✅), audioUrl=OSS ✅
- book 78 en-US:  内容='A Morning at the Corner Café'(English ✅), audioUrl=OSS ✅
- TtsTask 写入 targetLanguage/preferredVendor/preferredModel 三字段固化路由结果
MyFramework User hai 1 mes
pai
achega
46ca781f32

+ 88 - 0
deploy-package/scripts/test-language-e2e.sh

@@ -0,0 +1,88 @@
+#!/bin/bash
+# test-language-e2e.sh — 端到端测试 zh-CMN + en-US 链路
+set -e
+
+API=http://127.0.0.1:3100
+
+echo "=== 创建中文书 (zh-CMN) ==="
+ZH_RAW=$(curl -sS -X POST $API/api/book-generator/langgraph/books \
+  -H 'Content-Type: application/json' \
+  -d '{"title":"语言测试中文","description":"测试中文内容生成和普通话音频","bookScale":"500","language":"zh-CMN","autoGenerateContent":true,"autoGenerateAudio":false,"generateForeword":false,"generateAfterword":false}')
+echo "$ZH_RAW" | python3 -c 'import json,sys; d=json.load(sys.stdin); b=d["data"]["book"]; print("id="+str(b["id"])+" targetLanguage="+str(b.get("targetLanguage","未设"))+" genStage="+str(b["genStage"]))'
+ZH_ID=$(echo "$ZH_RAW" | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["book"]["id"])')
+echo "中文书ID: $ZH_ID"
+
+echo
+echo "=== 创建英文书 (en-US) ==="
+EN_RAW=$(curl -sS -X POST $API/api/book-generator/langgraph/books \
+  -H 'Content-Type: application/json' \
+  -d '{"title":"Language Test English","description":"Test English content and audio","bookScale":"500","language":"en-US","autoGenerateContent":true,"autoGenerateAudio":false,"generateForeword":false,"generateAfterword":false}')
+echo "$EN_RAW" | python3 -c 'import json,sys; d=json.load(sys.stdin); b=d["data"]["book"]; print("id="+str(b["id"])+" targetLanguage="+str(b.get("targetLanguage","未设"))+" genStage="+str(b["genStage"]))'
+EN_ID=$(echo "$EN_RAW" | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["book"]["id"])')
+echo "英文书ID: $EN_ID"
+
+echo
+echo "=== 等待 60s 让 AI 生成内容 ==="
+sleep 60
+
+echo
+echo "=== 查章节和内容语言(每本书看前 2 章标题+摘要) ==="
+cd /data/ai/audio/server
+node -e "
+const p = new (require('@prisma/client').PrismaClient());
+(async () => {
+  for (const id of [$ZH_ID, $EN_ID]) {
+    const b = await p.book.findUnique({ where: { id }, select: { id: true, title: true, targetLanguage: true, genStage: true } });
+    const chs = await p.bookChapter.findMany({ where: { bookId: id }, orderBy: { number: 'asc' }, take: 3, select: { id: true, title: true, content: true, genStage: true } });
+    console.log('--- book '+b.id+' (lang='+b.targetLanguage+', stage='+b.genStage+', '+chs.length+' ch) ---');
+    for (const c of chs) {
+      const head = (c.content || '').substring(0, 200);
+      console.log('  ch#'+c.id+' ['+c.genStage+'] '+c.title);
+      console.log('    '+head.replace(/\\n/g,' / '));
+    }
+  }
+  await p.\$disconnect();
+})();
+"
+
+echo
+echo "=== 触发音频生成(让 2 本书都进 TtsTask 队列) ==="
+for ID in $ZH_ID $EN_ID; do
+  echo "--- book $ID ---"
+  # 触发 book 的所有叶节点 audio
+  curl -sS -X POST "$API/api/book-generator/langgraph/books/$ID/audio" \
+    -H 'Content-Type: application/json' -d '{}' | head -c 200
+  echo
+done
+
+echo
+echo "=== 等待 60s 让 TTS 跑完 ==="
+sleep 60
+
+echo
+echo "=== 查 TtsTask 状态(应该有 targetLanguage 字段) ==="
+cd /data/ai/audio/server
+node -e "
+const p = new (require('@prisma/client').PrismaClient());
+(async () => {
+  const tasks = await p.ttsTask.findMany({ where: { chapterId: { in: (await p.bookChapter.findMany({ where: { bookId: { in: [$ZH_ID, $EN_ID] } }, select: { id: true } })).map(c => c.id) } }, orderBy: { id: 'desc' }, take: 5 });
+  for (const t of tasks) {
+    console.log('  ttsTask#'+t.id+' status='+t.status+' voiceId='+t.voiceId+' targetLang='+t.targetLanguage+' prefV='+t.preferredVendor+' prefM='+t.preferredModel);
+  }
+  await p.\$disconnect();
+})();
+"
+
+echo
+echo "=== 查生成的音频文件 (DB: BookChapter.audioUrl) ==="
+node -e "
+const p = new (require('@prisma/client').PrismaClient());
+(async () => {
+  for (const id of [$ZH_ID, $EN_ID]) {
+    const chs = await p.bookChapter.findMany({ where: { bookId: id, audioUrl: { not: null } }, take: 3, select: { id: true, title: true, audioUrl: true, audioDuration: true } });
+    console.log('--- book '+id+' 音频 ---');
+    for (const c of chs) console.log('  ch#'+c.id+' '+c.title+' dur='+c.audioDuration+'s url='+(c.audioUrl||'').slice(-50));
+  }
+  await p.\$disconnect();
+})();
+"

+ 5 - 0
server/src/modules/book-generator/book-generator.store.ts

@@ -1789,6 +1789,7 @@ ${writingStyle ? `写作风格:${writingStyle}` : ''}
     subtitle: string | null;
     description: string;
     targetAudience: string;
+    targetLanguage?: string;        // 🆕
     style: string;
     bookScale: string;
     totalChapters: number;
@@ -1807,6 +1808,7 @@ ${writingStyle ? `写作风格:${writingStyle}` : ''}
     qualityReport?: string | null;
     continuityReport?: string | null;
     rewriteReport?: string | null;
+    voiceSpeed?: number;             // 🆕
     createdAt: Date;
     updatedAt: Date;
     chapters: any[];
@@ -1820,11 +1822,14 @@ ${writingStyle ? `写作风格:${writingStyle}` : ''}
       subtitle: dbBook.subtitle || undefined,
       description: dbBook.description,
       targetAudience: dbBook.targetAudience,
+      targetLanguage: dbBook.targetLanguage,   // 🆕
       style: dbBook.style,
       bookScale: dbBook.bookScale,
       totalChapters: dbBook.totalChapters,
       estimatedWords: dbBook.estimatedWords,
       progress: dbBook.progress,
+      // voiceSpeed / targetLanguage 已 schema 添加,Prisma client 类型未自动包含,cast
+      ...(('voiceSpeed' in dbBook) ? { voiceSpeed: (dbBook as any).voiceSpeed } : {}),
       isPublished: dbBook.isPublished,
       genStage: computeBookGenStage(dbBook.chapters),
       failedStage: dbBook.failedStage || undefined,

+ 1 - 0
server/src/modules/book-generator/book-generator.types.ts

@@ -37,6 +37,7 @@ export interface BookBase {
   subtitle?: string;                // 副标题
   description: string;              // 书籍描述/用户输入
   targetAudience: string;            // 目标受众
+  targetLanguage?: string;           // 🆕 目标语言(BCP-47),默认 zh-CMN
   style: string;                    // 写作风格
   bookScale: string;                // 书籍规模:纯数字字符串,如 1000, 2000, 130000, 340000 等
   totalChapters: number;            // 总章节数