diff --git a/packages/voice-production/.gitignore b/packages/voice-production/.gitignore index 4f99d35..c5401c7 100644 --- a/packages/voice-production/.gitignore +++ b/packages/voice-production/.gitignore @@ -7,5 +7,11 @@ node_modules/ # voice data/voice/* + +# 本地部署拷贝目录(由 deploy-assets 生成) +pipi-assets/ + +# 生成的 nginx 配置(含机器相关路径,每次运行脚本会重新生成) +scripts/nginx-pipi-assets.conf # 输出文件(可选,如果不想提交生成的数据) # data/char/char_introduce.json diff --git a/packages/voice-production/README.md b/packages/voice-production/README.md new file mode 100644 index 0000000..aa3ef12 --- /dev/null +++ b/packages/voice-production/README.md @@ -0,0 +1,25 @@ +# 生成资源 + +pnpm generate-introduce 生成简介。混元生成的简介质量不高,自己使用Cursor的tab自动补全,反而质量更高一些 + +pnpm generate-voice 根据 generate-introduce 生成的简介,来生成音频。 + +# 本地开发(拷贝到 pipi-assets,并生成 nginx 配置) + +pnpm run deploy-assets + +# 或 + +pnpm run deploy-assets:local + +# 服务器部署前(拷贝到 /var/www/pipi-assets,并生成对应 nginx 配置) + +pnpm run deploy-assets:server + +# 自定义目标目录 + +node scripts/deploy-assets.js --target /path/to/dir + +# 指定 nginx 配置输出路径 + +node scripts/deploy-assets.js --server --nginx /etc/nginx/sites-available/pipi-assets diff --git a/packages/voice-production/package.json b/packages/voice-production/package.json index 3b73d83..1c140cc 100644 --- a/packages/voice-production/package.json +++ b/packages/voice-production/package.json @@ -6,7 +6,10 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "generate-introduce": "cross-env node scripts/generate_char_introduce.js", - "generate-voice": "node scripts/generate_char_voice.js" + "generate-voice": "node scripts/generate_char_voice.js", + "deploy-assets": "node scripts/deploy-assets.js", + "deploy-assets:local": "node scripts/deploy-assets.js --local", + "deploy-assets:server": "node scripts/deploy-assets.js --server" }, "keywords": [], "author": "", diff --git a/packages/voice-production/scripts/NGINX-本地测试说明.md b/packages/voice-production/scripts/NGINX-本地测试说明.md new file mode 100644 index 0000000..c4489ca --- /dev/null +++ b/packages/voice-production/scripts/NGINX-本地测试说明.md @@ -0,0 +1,129 @@ +# 用本机 Nginx 测试 pipi-assets 配置 + +按顺序做下面几步即可。 + +--- + +## 1. 检查是否已安装 Nginx + +在终端执行: + +```bash +nginx -v +``` + +- **有版本号**(如 `nginx version: nginx/1.27.5`)→ 已安装,跳到第 2 步。 +- **报错 command not found** → 需要先安装。Mac 推荐用 Homebrew: + ```bash + brew install nginx + ``` + +--- + +## 2. 确认资源目录存在 + +本配置会使用项目下的 `pipi-assets` 目录。若还没有,先执行一次: + +```bash +cd platform-pipi/packages/voice-production +pnpm run deploy-assets:local +``` + +这样会生成 `pipi-assets/`(内含 `char/`、`voice/` 等)。 + +--- + +## 3. 让主 Nginx 加载本项目的配置 + +你的 Nginx 主配置在 **Homebrew** 安装时一般是: + +- Apple Silicon:`/opt/homebrew/etc/nginx/nginx.conf` +- Intel:`/usr/local/etc/nginx/nginx.conf` + +任选一种方式,让主配置把「本项目里的 pipi-assets 配置」包含进去。 + +### 方式 A:在主配置里加一行 include(推荐) + +1. 用编辑器打开主配置,例如: + + ```bash + open -e /opt/homebrew/etc/nginx/nginx.conf + ``` + + (Intel 机器把路径改成 `/usr/local/etc/nginx/nginx.conf`) + +2. 在 `http { ... }` 块里找合适位置(例如在 `include servers/*;` 那一行**上面**),加一行: + + ```nginx + include /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/scripts/nginx-pipi-assets-local.conf; + ``` + + 注意把路径改成你本机实际的项目路径。 + +3. 保存退出。 + +### 方式 B:放到 Nginx 的 servers 目录 + +1. 若没有 `servers` 目录,先建一个: + + ```bash + sudo mkdir -p /opt/homebrew/etc/nginx/servers + ``` + + (Intel 用 `/usr/local/etc/nginx/servers`) + +2. 做一份软链,指向项目里的本地配置: + ```bash + sudo ln -sf /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/scripts/nginx-pipi-assets-local.conf /opt/homebrew/etc/nginx/servers/pipi-assets.conf + ``` + 主配置里已有 `include servers/*;` 时会自动加载。 + +--- + +## 4. 检查配置并重载 Nginx + +```bash +# 检查语法是否正确 +nginx -t + +# 若无报错,重载 Nginx(不中断已有连接) +nginx -s reload +``` + +若你是用 `brew services start nginx` 启用的,可以改为: + +```bash +brew services restart nginx +``` + +--- + +## 5. 在浏览器或终端里测试 + +本地站点监听 **8081 端口**,用 `localhost` 即可访问(无需改 hosts)。 + +- 健康检查: + **http://localhost:8081/health** + 应看到页面内容:`ok` + +- 角色 JSON: + **http://localhost:8081/char/char_introduce.json** + 应返回 JSON 内容 + +- 语音文件(示例): + **http://localhost:8081/voice/一\_4e00.mp3** + 应能播放或下载 + +终端快速验证: + +```bash +curl -s http://localhost:8081/health +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/char/char_introduce.json +``` + +--- + +## 说明 + +- **端口**:使用 8081 是为了不和 80 端口上已有的其他站点(如 simulation.data-platform.local 等)冲突。 +- **配置文件**:`nginx-pipi-assets.conf` 是给服务器部署用的(80 端口 + 二级域名);`nginx-pipi-assets-local.conf` 是给本机测试用的(8081 + localhost),路径已指向项目下的 `pipi-assets`。 diff --git a/packages/voice-production/scripts/deploy-assets.js b/packages/voice-production/scripts/deploy-assets.js new file mode 100644 index 0000000..89f6704 --- /dev/null +++ b/packages/voice-production/scripts/deploy-assets.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node +/** + * 将 data 目录内容拷贝到指定目录(本地开发或服务器),并生成对应 nginx 配置。 + * + * 用法: + * node scripts/deploy-assets.js # 默认:拷贝到本地 pipi-assets + * node scripts/deploy-assets.js --local # 同上 + * node scripts/deploy-assets.js --server # 拷贝到 /var/www/pipi-assets/ + * node scripts/deploy-assets.js --target /path/to/dir # 自定义目标目录 + * + * 每次运行都会生成 nginx 配置(root 为当前目标目录),默认输出到 scripts/nginx-pipi-assets.conf。 + * 使用 --nginx /path/to/file 可指定 nginx 配置输出路径。 + */ + +const fs = require('fs'); +const path = require('path'); + +const PACKAGE_ROOT = path.resolve(__dirname, '..'); +const DATA_DIR = path.join(PACKAGE_ROOT, 'data'); +const LOCAL_TARGET = path.join(PACKAGE_ROOT, 'pipi-assets'); +const SERVER_TARGET = '/var/www/pipi-assets'; +const DEFAULT_NGINX_OUTPUT = path.join(__dirname, 'nginx-pipi-assets.conf'); + +function parseArgs() { + const args = process.argv.slice(2); + let target = null; + let nginxOutput = null; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--local' || args[i] === '--server') { + if (target) { + console.warn('警告: 多次指定目标,以最后一次为准'); + } + target = args[i] === '--local' ? LOCAL_TARGET : SERVER_TARGET; + } else if (args[i] === '--target' && args[i + 1]) { + target = path.resolve(args[++i]); + } else if (args[i] === '--nginx' && args[i + 1] && !args[i + 1].startsWith('--')) { + nginxOutput = path.resolve(args[++i]); + } + } + + if (target === null) { + target = LOCAL_TARGET; + } + return { target, nginxOutput }; +} + +function copyDirRecursive(src, dest) { + if (!fs.existsSync(src)) return; + fs.mkdirSync(dest, { recursive: true }); + const entries = fs.readdirSync(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + if (entry.isDirectory()) { + copyDirRecursive(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + +function deployDataToTarget(targetDir) { + if (!fs.existsSync(DATA_DIR)) { + console.error('错误: data 目录不存在:', DATA_DIR); + process.exit(1); + } + + const entries = fs.readdirSync(DATA_DIR, { withFileTypes: true }); + if (entries.length === 0) { + console.log('data 目录为空,跳过拷贝'); + return; + } + + fs.mkdirSync(targetDir, { recursive: true }); + + for (const entry of entries) { + const srcPath = path.join(DATA_DIR, entry.name); + const destPath = path.join(targetDir, entry.name); + if (entry.isDirectory()) { + if (fs.existsSync(destPath)) { + fs.rmSync(destPath, { recursive: true }); + } + copyDirRecursive(srcPath, destPath); + console.log(' 已拷贝:', entry.name + '/'); + } else { + fs.copyFileSync(srcPath, destPath); + console.log(' 已拷贝:', entry.name); + } + } + + console.log('资源已拷贝到:', targetDir); +} + +function generateNginxConfig(rootPath, serverName = 'assets.yourdomain.com') { + return `# pipi-assets 静态资源站点 +# 生成后请将 server_name 改为你的二级域名,并视需放到 /etc/nginx/sites-available/ 后启用 + +server { + listen 80; + server_name ${serverName}; + + root ${rootPath}; + autoindex off; + + location /voice/ { + alias ${rootPath}/voice/; + add_header Cache-Control "public, max-age=86400"; + } + location /char/ { + alias ${rootPath}/char/; + add_header Cache-Control "public, max-age=3600"; + add_header Access-Control-Allow-Origin "*"; + } + location /image/ { + alias ${rootPath}/image/; + add_header Cache-Control "public, max-age=86400"; + } + + location /health { + return 200 "ok"; + add_header Content-Type text/plain; + } +} +`; +} + +function main() { + const { target, nginxOutput } = parseArgs(); + + console.log('目标目录:', target); + deployDataToTarget(target); + + const nginxPath = nginxOutput ?? DEFAULT_NGINX_OUTPUT; + const config = generateNginxConfig(target); + fs.writeFileSync(nginxPath, config, 'utf8'); + console.log('Nginx 配置已写入:', nginxPath); +} + +main(); diff --git a/packages/voice-production/scripts/generate_char_introduce.js b/packages/voice-production/scripts/generate_char_introduce.js index ed53a47..0b57e6e 100644 --- a/packages/voice-production/scripts/generate_char_introduce.js +++ b/packages/voice-production/scripts/generate_char_introduce.js @@ -206,32 +206,41 @@ async function main() { const charString = JSON.parse(fs.readFileSync(charStringPath, 'utf-8')); // 获取所有汉字 - const chars = charString['3500'].split(''); - console.log(`共有 ${chars.length} 个汉字需要处理`); + const allChars = charString['3500'].split(''); + console.log(`共有 ${allChars.length} 个汉字需要处理`); // 检查是否有已存在的进度文件 const outputPath = path.join(__dirname, '../data/char/char_introduce.json'); - let results = []; - let startIndex = 0; + let existingResults = []; + const generatedSet = new Set(); if (fs.existsSync(outputPath)) { try { - results = JSON.parse(fs.readFileSync(outputPath, 'utf-8')); - startIndex = results.length; - console.log(`发现已有进度,从第 ${startIndex + 1} 个汉字继续`); + existingResults = JSON.parse(fs.readFileSync(outputPath, 'utf-8')); + existingResults.forEach((item) => generatedSet.add(item.char)); + console.log(`发现已有 ${existingResults.length} 个汉字介绍`); } catch (e) { console.log('已有文件解析失败,从头开始'); } } + // 过滤出未生成的汉字 + const charsToGenerate = allChars.filter((char) => !generatedSet.has(char)); + console.log(`其中 ${generatedSet.size} 个已生成,${charsToGenerate.length} 个待生成`); + + if (charsToGenerate.length === 0) { + console.log('所有汉字介绍已生成完毕,无需处理'); + return; + } + // 批量处理配置 const BATCH_SIZE = 20; // 每次 API 调用处理 20 个汉字 const DELAY_BETWEEN_BATCHES = 1000; // 批次间延迟1秒 - for (let i = startIndex; i < chars.length; i += BATCH_SIZE) { - const batch = chars.slice(i, Math.min(i + BATCH_SIZE, chars.length)); + for (let i = 0; i < charsToGenerate.length; i += BATCH_SIZE) { + const batch = charsToGenerate.slice(i, Math.min(i + BATCH_SIZE, charsToGenerate.length)); console.log( - `\n处理第 ${i + 1} - ${Math.min(i + BATCH_SIZE, chars.length)} 个汉字(共 ${batch.length} 个)...` + `\n处理第 ${i + 1} - ${Math.min(i + BATCH_SIZE, charsToGenerate.length)} 个汉字(共 ${batch.length} 个)...` ); console.log(`汉字列表: ${batch.join('')}`); @@ -243,19 +252,21 @@ async function main() { console.log(` ${item.char}: ${item.introduce}`); } - results.push(...batchResults); + // 合并到已有结果 + existingResults.push(...batchResults); + batchResults.forEach((item) => generatedSet.add(item.char)); // 每批处理完后保存进度 - fs.writeFileSync(outputPath, JSON.stringify(results, null, 2), 'utf-8'); - console.log(`已保存进度:${results.length}/${chars.length}`); + fs.writeFileSync(outputPath, JSON.stringify(existingResults, null, 2), 'utf-8'); + console.log(`已保存进度:${existingResults.length}/${allChars.length}`); // 批次间延迟,避免API限流 - if (i + BATCH_SIZE < chars.length) { + if (i + BATCH_SIZE < charsToGenerate.length) { await delay(DELAY_BETWEEN_BATCHES); } } - console.log(`\n完成!共生成 ${results.length} 个汉字介绍`); + console.log(`\n完成!共生成 ${existingResults.length} 个汉字介绍`); console.log(`结果已保存到: ${outputPath}`); } diff --git a/packages/voice-production/scripts/nginx-pipi-assets-local.conf b/packages/voice-production/scripts/nginx-pipi-assets-local.conf new file mode 100644 index 0000000..e9fead8 --- /dev/null +++ b/packages/voice-production/scripts/nginx-pipi-assets-local.conf @@ -0,0 +1,29 @@ +# pipi-assets 本地测试(端口 8081,避免与 80 端口其他站点冲突) +# 使用方式见同目录下的 NGINX-本地测试说明.md + +server { + listen 8081; + server_name localhost; + + root /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/pipi-assets; + autoindex off; + + location /voice/ { + alias /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/pipi-assets/voice/; + add_header Cache-Control "public, max-age=86400"; + } + location /char/ { + alias /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/pipi-assets/char/; + add_header Cache-Control "public, max-age=3600"; + add_header Access-Control-Allow-Origin "*"; + } + location /image/ { + alias /Users/joey-xd/sites/pipi-learn/platform-pipi/packages/voice-production/pipi-assets/image/; + add_header Cache-Control "public, max-age=86400"; + } + + location /health { + return 200 "ok"; + add_header Content-Type text/plain; + } +}