From 8440c646ad945dd7c0821379ad0516a4f812921e Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Sat, 8 Mar 2025 17:24:42 +0800 Subject: [PATCH 1/5] feat: add tts api --- packages/codemate-plugin/package.json | 1 + packages/codemate-plugin/plugins/tts/index.ts | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 packages/codemate-plugin/plugins/tts/index.ts diff --git a/packages/codemate-plugin/package.json b/packages/codemate-plugin/package.json index b6354c3cd4..70c42cb223 100644 --- a/packages/codemate-plugin/package.json +++ b/packages/codemate-plugin/package.json @@ -10,6 +10,7 @@ "lru-cache": "^10.2.0", "nanoid": "^5.0.6", "tencentcloud-sdk-nodejs-captcha": "^4.0.850", + "tencentcloud-sdk-nodejs-tts": "^4.0.1003", "wechatpay-node-v3": "2.1.8" }, "module": "api.ts", diff --git a/packages/codemate-plugin/plugins/tts/index.ts b/packages/codemate-plugin/plugins/tts/index.ts new file mode 100644 index 0000000000..c61dc5f812 --- /dev/null +++ b/packages/codemate-plugin/plugins/tts/index.ts @@ -0,0 +1,50 @@ +import * as TencentCloudSDK from 'tencentcloud-sdk-nodejs-tts'; +import type { Client as TtsClientType } from 'tencentcloud-sdk-nodejs-tts/tencentcloud/services/tts/v20190823/tts_client'; +import { Context, Handler, nanoid, param, SettingModel, SystemModel, Types } from 'hydrooj'; + +declare module 'hydrooj' { + interface Lib { + tts: TtsClientType; + } +} + +class TtsHandler extends Handler { + @param('text', Types.String) + async get(domainId: string, text: string) { + const sessionId = nanoid(); + + const res = await global.Hydro.lib.tts.TextToVoice({ + Text: text, + SessionId: sessionId, + Codec: 'mp3', + VoiceType: 601009, + }); + + this.response.body = { + b64AudioMp3File: res.Audio, + qCloudSessionId: sessionId, + }; + } +} + +export async function apply(ctx: Context) { + ctx.Route('tts', '/tts', TtsHandler); + + ctx.inject(['setting'], (c) => { + c.setting.SystemSetting(SettingModel.Setting('setting_secrets', 'tts.secretId', '', 'text', 'TTS SecretId')); + c.setting.SystemSetting(SettingModel.Setting('setting_secrets', 'tts.secretKey', '', 'text', 'TTS SecretKey')); + }); + + global.Hydro.lib.tts = new TencentCloudSDK.tts.v20190823.Client({ + credential: { + secretId: await SystemModel.get('tts.secretId'), + secretKey: await SystemModel.get('tts.secretKey'), + }, + region: '', + profile: { + httpProfile: { + endpoint: 'tts.tencentcloudapi.com', + }, + }, + }); +} From 07909236b989beacc672456b31cf951693333c8c Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Sat, 8 Mar 2025 20:24:55 +0800 Subject: [PATCH 2/5] feat: storage audio to cos --- packages/codemate-plugin/package.json | 1 + packages/codemate-plugin/plugins/tts/index.ts | 96 ++++++++++++++++++- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/packages/codemate-plugin/package.json b/packages/codemate-plugin/package.json index 70c42cb223..c063f8971e 100644 --- a/packages/codemate-plugin/package.json +++ b/packages/codemate-plugin/package.json @@ -3,6 +3,7 @@ "packageManager": "yarn@4.0.2", "dependencies": { "alipay-sdk": "^4.11.0", + "cos-nodejs-sdk-v5": "^2.14.6", "crypto-js": "^4.2.0", "fs-extra": "^11.2.0", "hydrooj": "workspace:^", diff --git a/packages/codemate-plugin/plugins/tts/index.ts b/packages/codemate-plugin/plugins/tts/index.ts index c61dc5f812..46fd650f06 100644 --- a/packages/codemate-plugin/plugins/tts/index.ts +++ b/packages/codemate-plugin/plugins/tts/index.ts @@ -1,28 +1,107 @@ +import COS from 'cos-nodejs-sdk-v5'; import * as TencentCloudSDK from 'tencentcloud-sdk-nodejs-tts'; import type { Client as TtsClientType } from 'tencentcloud-sdk-nodejs-tts/tencentcloud/services/tts/v20190823/tts_client'; -import { Context, Handler, nanoid, param, SettingModel, SystemModel, Types } from 'hydrooj'; +import { Context, db, Handler, md5, nanoid, ObjectId, param, SettingModel, SystemModel, Types } from 'hydrooj'; + +const TYPE_TTS_DATA = 110; + +export interface TtsDataDoc { + docType: 110; + docId: ObjectId; + domainId: string; + fileId: string; + text: string; + textHash: string; +} declare module 'hydrooj' { + interface DocType { + [TYPE_TTS_DATA]: TtsDataDoc; + } + interface Lib { tts: TtsClientType; + tts_cos: COS; } } +const coll = db.collection('document'); + +const getFileKey = (fileId: string) => `${fileId}.mp3`; + class TtsHandler extends Handler { @param('text', Types.String) async get(domainId: string, text: string) { - const sessionId = nanoid(); + const textMd5 = md5(text); + const doc = await coll.findOne({ docType: TYPE_TTS_DATA, domainId, textHash: textMd5 }); + const bucket = await SystemModel.get('tts.bucket'); + const region = await SystemModel.get('tts.region'); + + if (doc) { + const url = await new Promise((resolve, reject) => { + global.Hydro.lib.tts_cos.getObjectUrl( + { + Bucket: bucket, + Region: region, + Key: getFileKey(doc.fileId), + Sign: true, + }, + (err, data) => (err ? reject(err) : resolve(data.Url)), + ); + }); + + this.response.body = { + audioUrl: url, + }; + + return; + } + + const fileId = nanoid(); const res = await global.Hydro.lib.tts.TextToVoice({ Text: text, - SessionId: sessionId, + SessionId: fileId, Codec: 'mp3', VoiceType: 601009, }); + const buffer = Buffer.from(res.Audio, 'base64'); + await new Promise((resolve, reject) => { + global.Hydro.lib.tts_cos.putObject( + { + Bucket: bucket, + Region: region, + Key: getFileKey(fileId), + Body: buffer, + }, + (err) => (err ? reject(err) : resolve(1)), + ); + }); + + await coll.insertOne({ + docType: TYPE_TTS_DATA, + docId: new ObjectId(), + domainId, + fileId, + text, + textHash: textMd5, + }); + + const url = await new Promise((resolve, reject) => { + global.Hydro.lib.tts_cos.getObjectUrl( + { + Bucket: bucket, + Region: region, + Key: getFileKey(fileId), + Sign: true, + }, + (err, data) => (err ? reject(err) : resolve(data.Url)), + ); + }); + this.response.body = { - b64AudioMp3File: res.Audio, - qCloudSessionId: sessionId, + audioUrl: url, }; } } @@ -33,6 +112,8 @@ export async function apply(ctx: Context) { ctx.inject(['setting'], (c) => { c.setting.SystemSetting(SettingModel.Setting('setting_secrets', 'tts.secretId', '', 'text', 'TTS SecretId')); c.setting.SystemSetting(SettingModel.Setting('setting_secrets', 'tts.secretKey', '', 'text', 'TTS SecretKey')); + c.setting.SystemSetting(SettingModel.Setting('setting_secrets', 'tts.bucket', '', 'text', 'TTS COS bucket')); + c.setting.SystemSetting(SettingModel.Setting('setting_secrets', 'tts.region', '', 'text', 'TTS COS region')); }); global.Hydro.lib.tts = new TencentCloudSDK.tts.v20190823.Client({ @@ -47,4 +128,9 @@ export async function apply(ctx: Context) { }, }, }); + + global.Hydro.lib.tts_cos = new COS({ + SecretId: await SystemModel.get('tts.secretId'), + SecretKey: await SystemModel.get('tts.secretKey'), + }); } From bf963444872df7419aa2881353e860bab437989f Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Sat, 8 Mar 2025 20:26:08 +0800 Subject: [PATCH 3/5] feat: limit tts.generate rate --- packages/codemate-plugin/plugins/tts/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/codemate-plugin/plugins/tts/index.ts b/packages/codemate-plugin/plugins/tts/index.ts index 46fd650f06..be05b1b39f 100644 --- a/packages/codemate-plugin/plugins/tts/index.ts +++ b/packages/codemate-plugin/plugins/tts/index.ts @@ -57,6 +57,8 @@ class TtsHandler extends Handler { return; } + await this.limitRate('tts.generate', 15, 3, true); + const fileId = nanoid(); const res = await global.Hydro.lib.tts.TextToVoice({ From 0d649d203e2def5667131f12e3dcdfdc820f9689 Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Mon, 10 Mar 2025 14:16:00 +0800 Subject: [PATCH 4/5] feat: store voice type to database --- packages/codemate-plugin/plugins/tts/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/codemate-plugin/plugins/tts/index.ts b/packages/codemate-plugin/plugins/tts/index.ts index be05b1b39f..814e93249a 100644 --- a/packages/codemate-plugin/plugins/tts/index.ts +++ b/packages/codemate-plugin/plugins/tts/index.ts @@ -4,6 +4,7 @@ import type { Client as TtsClientType } from 'tencentcloud-sdk-nodejs-tts/tencen import { Context, db, Handler, md5, nanoid, ObjectId, param, SettingModel, SystemModel, Types } from 'hydrooj'; const TYPE_TTS_DATA = 110; +const VOICE_TYPE = 601009; export interface TtsDataDoc { docType: 110; @@ -12,6 +13,7 @@ export interface TtsDataDoc { fileId: string; text: string; textHash: string; + voiceType: number; } declare module 'hydrooj' { @@ -65,7 +67,7 @@ class TtsHandler extends Handler { Text: text, SessionId: fileId, Codec: 'mp3', - VoiceType: 601009, + VoiceType: VOICE_TYPE, }); const buffer = Buffer.from(res.Audio, 'base64'); @@ -88,6 +90,7 @@ class TtsHandler extends Handler { fileId, text, textHash: textMd5, + voiceType: VOICE_TYPE, }); const url = await new Promise((resolve, reject) => { From 509fe93bdcf1691e59169486b59050455c4685dd Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Mon, 10 Mar 2025 14:30:55 +0800 Subject: [PATCH 5/5] refactor: store tts data in a single collection --- packages/codemate-plugin/plugins/tts/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/codemate-plugin/plugins/tts/index.ts b/packages/codemate-plugin/plugins/tts/index.ts index 814e93249a..d74a690e14 100644 --- a/packages/codemate-plugin/plugins/tts/index.ts +++ b/packages/codemate-plugin/plugins/tts/index.ts @@ -25,9 +25,13 @@ declare module 'hydrooj' { tts: TtsClientType; tts_cos: COS; } + + interface Collections { + tts: TtsDataDoc; + } } -const coll = db.collection('document'); +const coll = db.collection('tts'); const getFileKey = (fileId: string) => `${fileId}.mp3`;