diff --git a/.gitignore b/.gitignore index 97b62d3..dbe3665 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ logs config.json -pack-dist \ No newline at end of file +pack-dist + +videos/output* \ No newline at end of file diff --git a/package.json b/package.json index 37b9cf9..607217e 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@kevisual/mark": "0.0.7", "@kevisual/router": "0.0.13", "@kevisual/types": "^0.0.9", - "@kevisual/use-config": "^1.0.11", + "@kevisual/use-config": "^1.0.12", "@types/bun": "^1.2.11", "@types/crypto-js": "^4.2.2", "@types/formidable": "^3.4.5", @@ -71,6 +71,6 @@ "tape": "^5.9.0", "tiktoken": "^1.0.21", "typescript": "^5.8.3", - "vite": "^6.3.3" + "vite": "^6.3.4" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97b2633..05dd41b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: devDependencies: '@kevisual/code-center-module': specifier: 0.0.18 - version: 0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.13)(@kevisual/use-config@1.0.11(dotenv@16.5.0))(ioredis@5.6.1)(pg@8.14.1)(sequelize@6.37.7(pg@8.14.1)) + version: 0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.13)(@kevisual/use-config@1.0.12(dotenv@16.5.0))(ioredis@5.6.1)(pg@8.14.1)(sequelize@6.37.7(pg@8.14.1)) '@kevisual/mark': specifier: 0.0.7 version: 0.0.7(dotenv@16.5.0)(esbuild@0.25.2) @@ -21,8 +21,8 @@ importers: specifier: ^0.0.9 version: 0.0.9 '@kevisual/use-config': - specifier: ^1.0.11 - version: 1.0.11(dotenv@16.5.0) + specifier: ^1.0.12 + version: 1.0.12(dotenv@16.5.0) '@types/bun': specifier: ^1.2.11 version: 1.2.11 @@ -40,7 +40,7 @@ importers: version: 22.15.3 '@vitejs/plugin-basic-ssl': specifier: ^2.0.0 - version: 2.0.0(vite@6.3.3(@types/node@22.15.3)(tsx@4.19.3)) + version: 2.0.0(vite@6.3.4(@types/node@22.15.3)(tsx@4.19.3)) cookie: specifier: ^1.0.2 version: 1.0.2 @@ -102,8 +102,8 @@ importers: specifier: ^5.8.3 version: 5.8.3 vite: - specifier: ^6.3.3 - version: 6.3.3(@types/node@22.15.3)(tsx@4.19.3) + specifier: ^6.3.4 + version: 6.3.4(@types/node@22.15.3)(tsx@4.19.3) packages: @@ -307,8 +307,8 @@ packages: '@kevisual/types@0.0.9': resolution: {integrity: sha512-SDJ7GMbOx7Ghz2kreHqym56ccAJS3t93y+NS0+afTLxcq2+cKcoEy2F8WXEv0mnJ6EsDp5AbA7Jv5TZA1Jbc3A==} - '@kevisual/use-config@1.0.11': - resolution: {integrity: sha512-ccilQTRZTpO075L67ZBXhr8Lp3i73/W5cCMT5enMjVrnJT5K0i5JH5IbzBhF6WY5Rj8dmVsAyyjJe24ClyM7Eg==} + '@kevisual/use-config@1.0.12': + resolution: {integrity: sha512-PNoZqj6vdhv6DvjRMNwoGH9HJupm7QvjkvtCEYW2ryK7J8sI73r2ThCl4OEbXdRYVgl1EeK/e2IJh0Rf51bVwA==} peerDependencies: dotenv: ^16.4.7 @@ -1054,14 +1054,6 @@ packages: fclone@1.0.11: resolution: {integrity: sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==} - fdir@6.4.3: - resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.4.4: resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} peerDependencies: @@ -2194,8 +2186,8 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite@6.3.3: - resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} + vite@6.3.4: + resolution: {integrity: sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -2431,11 +2423,11 @@ snapshots: '@kevisual/auth@1.0.5': {} - '@kevisual/code-center-module@0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.13)(@kevisual/use-config@1.0.11(dotenv@16.5.0))(ioredis@5.6.1)(pg@8.14.1)(sequelize@6.37.7(pg@8.14.1))': + '@kevisual/code-center-module@0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.13)(@kevisual/use-config@1.0.12(dotenv@16.5.0))(ioredis@5.6.1)(pg@8.14.1)(sequelize@6.37.7(pg@8.14.1))': dependencies: '@kevisual/auth': 1.0.5 '@kevisual/router': 0.0.13 - '@kevisual/use-config': 1.0.11(dotenv@16.5.0) + '@kevisual/use-config': 1.0.12(dotenv@16.5.0) ioredis: 5.6.1 nanoid: 5.1.5 pg: 8.14.1 @@ -2456,7 +2448,7 @@ snapshots: '@kevisual/auth': 1.0.5 '@kevisual/rollup-tools': 0.0.1(esbuild@0.25.2) '@kevisual/router': 0.0.7 - '@kevisual/use-config': 1.0.11(dotenv@16.5.0) + '@kevisual/use-config': 1.0.12(dotenv@16.5.0) cookie: 1.0.2 nanoid: 5.1.5 pg: 8.14.1 @@ -2516,7 +2508,7 @@ snapshots: '@kevisual/types@0.0.9': {} - '@kevisual/use-config@1.0.11(dotenv@16.5.0)': + '@kevisual/use-config@1.0.12(dotenv@16.5.0)': dependencies: '@kevisual/load': 0.0.6 dotenv: 16.5.0 @@ -2607,7 +2599,7 @@ snapshots: '@rollup/pluginutils': 5.1.4(rollup@4.40.1) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.4.3(picomatch@4.0.2) + fdir: 6.4.4(picomatch@4.0.2) is-reference: 1.2.1 magic-string: 0.30.17 picomatch: 4.0.2 @@ -2778,9 +2770,9 @@ snapshots: '@types/validator@13.12.3': {} - '@vitejs/plugin-basic-ssl@2.0.0(vite@6.3.3(@types/node@22.15.3)(tsx@4.19.3))': + '@vitejs/plugin-basic-ssl@2.0.0(vite@6.3.4(@types/node@22.15.3)(tsx@4.19.3))': dependencies: - vite: 6.3.3(@types/node@22.15.3)(tsx@4.19.3) + vite: 6.3.4(@types/node@22.15.3)(tsx@4.19.3) abort-controller@3.0.0: dependencies: @@ -3304,10 +3296,6 @@ snapshots: fclone@1.0.11: {} - fdir@6.4.3(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - fdir@6.4.4(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -4616,7 +4604,7 @@ snapshots: vary@1.1.2: {} - vite@6.3.3(@types/node@22.15.3)(tsx@4.19.3): + vite@6.3.4(@types/node@22.15.3)(tsx@4.19.3): dependencies: esbuild: 0.25.2 fdir: 6.4.4(picomatch@4.0.2) diff --git a/src/provider/chat-adapter/siliconflow.ts b/src/provider/chat-adapter/siliconflow.ts index 5685e34..775a361 100644 --- a/src/provider/chat-adapter/siliconflow.ts +++ b/src/provider/chat-adapter/siliconflow.ts @@ -1,7 +1,7 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts'; import { OpenAI } from 'openai'; -type SiliconFlowOptions = Partial; +export type SiliconFlowOptions = Partial; type SiliconFlowUsageData = { id: string; diff --git a/src/provider/core/chat.ts b/src/provider/core/chat.ts index 359db92..f537a88 100644 --- a/src/provider/core/chat.ts +++ b/src/provider/core/chat.ts @@ -107,4 +107,11 @@ export class BaseChat implements BaseChatInterface, BaseChatUsageInterface { completion_tokens: this.completion_tokens, }; } + getHeaders(headers?: Record) { + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + ...headers, + }; + } } diff --git a/src/provider/media/index.ts b/src/provider/media/index.ts new file mode 100644 index 0000000..82f6ce0 --- /dev/null +++ b/src/provider/media/index.ts @@ -0,0 +1 @@ +export * from './video/siliconflow.ts'; diff --git a/src/provider/media/video/siliconflow.ts b/src/provider/media/video/siliconflow.ts new file mode 100644 index 0000000..0bf22ff --- /dev/null +++ b/src/provider/media/video/siliconflow.ts @@ -0,0 +1,37 @@ +import { SiliconFlow } from './../../chat-adapter/siliconflow.ts'; +export class VideoSiliconFlow extends SiliconFlow { + constructor(opts: any) { + super(opts); + } + + async uploadAudioVoice(audioBase64: string | Blob | File) { + const pathname = 'uploads/audio/voice'; + const url = `${this.baseURL}/${pathname}`; + const headers = { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${this.apiKey}`, + }; + const formData = new FormData(); + // formData.append('audio', 'data:audio/mpeg;base64,aGVsbG93b3JsZA=='); + // formData.append('audio', audioBase64); + formData.append('file', audioBase64); + formData.append('model', 'FunAudioLLM/CosyVoice2-0.5B'); + formData.append('customName', 'test_name'); + formData.append('text', '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始'); + + const res = await fetch(url, { + method: 'POST', + headers, + body: formData, + }).then((res) => res.json()); + console.log('uploadAudioVoice', res); + } + async audioSpeech() { + this.openai.audio.speech.create({ + model: 'FunAudioLLM/CosyVoice2-0.5B', + voice: 'alloy', + input: '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始', + response_format: 'mp3', + }); + } +} diff --git a/src/test/siliconflow/videos/index.ts b/src/test/siliconflow/videos/index.ts new file mode 100644 index 0000000..ca313f0 --- /dev/null +++ b/src/test/siliconflow/videos/index.ts @@ -0,0 +1,100 @@ +import { SiliconFlow } from '../../../provider/chat-adapter/siliconflow.ts'; +import { VideoSiliconFlow } from '../../../provider/media/video/siliconflow.ts'; +import dotenv from 'dotenv'; +import fs from 'fs'; +import path from 'path'; +import Stream from 'stream'; + +dotenv.config(); +const siliconflow = new SiliconFlow({ + apiKey: process.env.SILICONFLOW_API_KEY, + model: 'Qwen/Qwen2-7B-Instruct', +}); +const videoSiliconflow = new VideoSiliconFlow({ + apiKey: process.env.SILICONFLOW_API_KEY, + model: 'Qwen/Qwen2-7B-Instruct', +}); + +const main = async () => { + const usage = await siliconflow.getUsageInfo(); + console.log(usage); +}; + +// main(); +const mainChat = async () => { + const test2=`我永远记得那个改变一切的下午。十八岁生日后的第三天,我正坐在自家后院的老橡树杈上,用平板电脑调试我最新设计的森林动物追踪程序。我的红发——妈妈总说像"燃烧的枫叶"——在午后的阳光下泛着铜色的光泽,有几缕不听话的发丝被微风拂过我的脸颊。 + +"芮薇!"妈妈的声音从厨房窗口传来,"外婆发来加密信息,说需要你马上过去一趟。" + +我差点从树上掉下来。外婆从不发加密信息——除非情况紧急。作为退休的网络安全专家,外婆一直教导我"过度谨慎总比后悔莫及"。` + try { + const res = await siliconflow.openai.audio.speech.create({ + model: 'FunAudioLLM/CosyVoice2-0.5B', + // voice: 'FunAudioLLM/CosyVoice2-0.5B:diana', + voice: 'speech:test:h36jngt7ms:zarwclhblfjfyonslejr', + // input: '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始', + // input: '这是一个新的轮回,非常有趣的故事。', + input: test2, + response_format: 'mp3', + }); + + console.log(res); + + const dir = path.join(process.cwd(), 'videos'); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const filePath = path.join(dir, `output-${Date.now()}.mp3`); + + // 假设 res 是一个可读流 + if (res instanceof Stream.Readable) { + const writeStream = fs.createWriteStream(filePath); + res.pipe(writeStream); + + return new Promise((resolve, reject) => { + writeStream.on('finish', () => { + console.log('文件已保存至:', filePath); + resolve(filePath); + }); + writeStream.on('error', reject); + }); + } + // 假设 res 是一个 ArrayBuffer 或 Buffer + else if (res.arrayBuffer) { + const buffer = Buffer.from(await res.arrayBuffer()); + fs.writeFileSync(filePath, buffer); + console.log('文件已保存至:', filePath); + return filePath; + } + // 假设 res 是一个包含 blob 的对象 + else if (res.blob) { + // @ts-ignore + const buffer = Buffer.from(res.blob, 'base64'); + fs.writeFileSync(filePath, buffer); + console.log('文件已保存至:', filePath); + return filePath; + } else { + throw new Error('无法识别的响应格式'); + } + } catch (error) { + console.error('保存音频文件时出错:', error); + throw error; + } +}; + +mainChat(); + +const vidioUpload = async () => { + const filePath = path.join(process.cwd(), 'videos', 'my_speech_text.mp3'); + const fileBuffer = fs.readFileSync(filePath); + const fileBase64 = 'data:audio/mpeg;base64,' + fileBuffer.toString('base64'); + console.log('fileBase64', fileBase64.slice(0, 100)); + const fileBlob = new Blob([fileBuffer], { type: 'audio/wav' }); + const file = new File([fileBlob], 'my_speech_text.mp3', { type: 'audio/mp3' }); + const res = await videoSiliconflow.uploadAudioVoice(file); + // console.log('vidioUpload', res); + // uri:speech:test:h36jngt7ms:zarwclhblfjfyonslejr + return res; +}; +// vidioUpload(); diff --git a/tsconfig.json b/tsconfig.json index 0333030..1bdfd4e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,25 +1,7 @@ { + "extends": "@kevisual/types/json/backend.json", "compilerOptions": { - "module": "nodenext", - "target": "esnext", - "noImplicitAny": false, - "outDir": "./dist", - "sourceMap": false, - "allowJs": true, - "newLine": "LF", "baseUrl": "./", - "typeRoots": [ - "node_modules/@types", - "node_modules/@kevisual/types" - ], - "declaration": true, - "noEmit": false, - "allowImportingTsExtensions": true, - "emitDeclarationOnly": true, - "moduleResolution": "NodeNext", - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "esModuleInterop": true, "paths": { "@/*": [ "src/*" diff --git a/videos/my_speech_text.mp3 b/videos/my_speech_text.mp3 new file mode 100644 index 0000000..42f2ba9 Binary files /dev/null and b/videos/my_speech_text.mp3 differ diff --git a/videos/my_speech_text.txt b/videos/my_speech_text.txt new file mode 100644 index 0000000..7511273 --- /dev/null +++ b/videos/my_speech_text.txt @@ -0,0 +1 @@ +在一无所知中, 梦里的一天结束了,一个新的轮回便会开始 \ No newline at end of file diff --git a/videos/my_speech_text.wav b/videos/my_speech_text.wav new file mode 100644 index 0000000..97922be Binary files /dev/null and b/videos/my_speech_text.wav differ