generated from tailored/router-template
	Compare commits
	
		
			6 Commits
		
	
	
		
			767e436eb8
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9e94a4d898 | |||
| d4475cb2f2 | |||
| 5603d09e80 | |||
| 78cc6dcf55 | |||
| 8047577165 | |||
| e4596b4fde | 
							
								
								
									
										100
									
								
								examples/batch-send-files.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								examples/batch-send-files.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
import { EventEmitter } from 'eventemitter3';
 | 
			
		||||
import { VideoWS, VideoWsResult, sleep } from '../src/asr/provider/funasr/ws.ts';
 | 
			
		||||
import fs from 'node:fs';
 | 
			
		||||
import path from 'node:path';
 | 
			
		||||
type BatchSendOptions = {
 | 
			
		||||
  vws: VideoWS;
 | 
			
		||||
  files: string[];
 | 
			
		||||
  matchText?: string;
 | 
			
		||||
  emitter?: EventEmitter;
 | 
			
		||||
};
 | 
			
		||||
export class BatchSendFiles {
 | 
			
		||||
  files: string[];
 | 
			
		||||
  vws: VideoWS;
 | 
			
		||||
  emitter: EventEmitter;
 | 
			
		||||
  constructor({ vws, files, emitter }: BatchSendOptions) {
 | 
			
		||||
    this.files = files;
 | 
			
		||||
    this.vws = vws;
 | 
			
		||||
    this.emitter = emitter || vws.emitter;
 | 
			
		||||
  }
 | 
			
		||||
  async init() {
 | 
			
		||||
    const isConnected = await this.vws.isConnected();
 | 
			
		||||
    if (!isConnected) {
 | 
			
		||||
      console.error('链接失败:', isConnected);
 | 
			
		||||
    }
 | 
			
		||||
    this.send();
 | 
			
		||||
  }
 | 
			
		||||
  waitOne() {
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
      this.vws.emitter.once('result', (data) => {
 | 
			
		||||
        resolve(data);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  async checkAudioFile(file: string) {
 | 
			
		||||
    const stats = fs.statSync(file);
 | 
			
		||||
    if (!stats.isFile()) {
 | 
			
		||||
      throw new Error(`File not found: ${file}`);
 | 
			
		||||
    }
 | 
			
		||||
    const ext = path.extname(file).toLowerCase();
 | 
			
		||||
    const validExtensions = ['.wav', '.mp3', '.flac', '.ogg', '.aac'];
 | 
			
		||||
    if (!validExtensions.includes(ext)) {
 | 
			
		||||
      throw new Error(`Invalid file type: ${ext}. Supported types are: ${validExtensions.join(', ')}`);
 | 
			
		||||
    }
 | 
			
		||||
    const fileSize = stats.size;
 | 
			
		||||
    if (fileSize === 0) {
 | 
			
		||||
      throw new Error(`File is empty: ${file}`);
 | 
			
		||||
    }
 | 
			
		||||
    const maxSize = 100 * 1024 * 1024; // 100 MB
 | 
			
		||||
    if (fileSize > maxSize) {
 | 
			
		||||
      throw new Error(`File size exceeds limit: ${fileSize} bytes. Maximum allowed size is ${maxSize} bytes.`);
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      file,
 | 
			
		||||
      ext,
 | 
			
		||||
      size: fileSize,
 | 
			
		||||
      isValid: true,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  async send() {
 | 
			
		||||
    const textList: { file: string; text: string }[] = [];
 | 
			
		||||
    for (const file of this.files) {
 | 
			
		||||
      let wav_format = 'wav';
 | 
			
		||||
      try {
 | 
			
		||||
        const ck = await this.checkAudioFile(file);
 | 
			
		||||
        if (ck.ext !== '.wav') {
 | 
			
		||||
          wav_format = ck.ext.replace('.', '');
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('Error checking file:', error);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      const data = fs.readFileSync(file);
 | 
			
		||||
      const wait = this.waitOne();
 | 
			
		||||
      await this.vws.sendBuffer(data, { wav_format });
 | 
			
		||||
      await sleep(1000);
 | 
			
		||||
      console.log('File sent:', file);
 | 
			
		||||
      const result: VideoWsResult = (await wait) as any;
 | 
			
		||||
      console.log('Result:', result.text);
 | 
			
		||||
      textList.push({ file, text: result.text });
 | 
			
		||||
      console.log('----------------------');
 | 
			
		||||
    }
 | 
			
		||||
    this.emitter.emit('send-done', textList);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
// const batchSend = new BatchSendFiles({
 | 
			
		||||
//   vws: ws,
 | 
			
		||||
//   // files: [audioTestPath],
 | 
			
		||||
//   files: [videoTestPath, audioTestPath],
 | 
			
		||||
// });
 | 
			
		||||
// batchSend.init();
 | 
			
		||||
// batchSend.emitter.on('send-done', (data) => {
 | 
			
		||||
//   const matchText = '在一无所知中,梦里的一天结束了一个新的轮回,便会开始。';
 | 
			
		||||
//   const textList = data as { file: string; text: string }[];
 | 
			
		||||
//   for (const item of textList) {
 | 
			
		||||
//     const getText = item.text || '';
 | 
			
		||||
//     const distance = natural.JaroWinklerDistance(getText, matchText);
 | 
			
		||||
//     console.log(`File: ${item.file}, \nText: ${item.text}\nDistance: ${distance}`);
 | 
			
		||||
//   }
 | 
			
		||||
//   // console.log('Batch processing done:', data);
 | 
			
		||||
// });
 | 
			
		||||
							
								
								
									
										17
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@kevisual/video-tools",
 | 
			
		||||
  "version": "0.0.4",
 | 
			
		||||
  "version": "0.0.5",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "basename": "/root/video-tools",
 | 
			
		||||
@@ -10,10 +10,7 @@
 | 
			
		||||
    "type": "system-app"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "watch": "rollup -c rollup.config.mjs -w",
 | 
			
		||||
    "build": "rollup -c rollup.config.mjs",
 | 
			
		||||
    "dev": "cross-env NODE_ENV=development nodemon --delay 2.5 -e js,cjs,mjs --exec node dist/app.mjs",
 | 
			
		||||
    "dev:watch": "cross-env NODE_ENV=development concurrently -n \"Watch,Dev\" -c \"green,blue\" \"npm run watch\" \"sleep 1 && npm run dev\" ",
 | 
			
		||||
    "dev:bun": "bun run src/dev.ts --watch",
 | 
			
		||||
    "test": "tsx  test/**/*.ts",
 | 
			
		||||
    "clean": "rm -rf dist",
 | 
			
		||||
@@ -39,6 +36,7 @@
 | 
			
		||||
    "@kevisual/use-config": "^1.0.17",
 | 
			
		||||
    "@kevisual/video": "^0.0.2",
 | 
			
		||||
    "cookie": "^1.0.2",
 | 
			
		||||
    "crypto-js": "^4.2.0",
 | 
			
		||||
    "dayjs": "^1.11.13",
 | 
			
		||||
    "eventemitter3": "^5.0.1",
 | 
			
		||||
    "formidable": "^3.5.4",
 | 
			
		||||
@@ -50,12 +48,6 @@
 | 
			
		||||
    "@kevisual/logger": "^0.0.4",
 | 
			
		||||
    "@kevisual/types": "^0.0.10",
 | 
			
		||||
    "@kevisual/use-config": "^1.0.17",
 | 
			
		||||
    "@rollup/plugin-alias": "^5.1.1",
 | 
			
		||||
    "@rollup/plugin-commonjs": "^28.0.3",
 | 
			
		||||
    "@rollup/plugin-json": "^6.1.0",
 | 
			
		||||
    "@rollup/plugin-node-resolve": "^16.0.1",
 | 
			
		||||
    "@rollup/plugin-replace": "^6.0.2",
 | 
			
		||||
    "@rollup/plugin-typescript": "^12.1.2",
 | 
			
		||||
    "@types/crypto-js": "^4.2.2",
 | 
			
		||||
    "@types/formidable": "^3.4.5",
 | 
			
		||||
    "@types/lodash-es": "^4.17.12",
 | 
			
		||||
@@ -72,13 +64,8 @@
 | 
			
		||||
    "pg": "^8.16.0",
 | 
			
		||||
    "pm2": "^6.0.6",
 | 
			
		||||
    "rimraf": "^6.0.1",
 | 
			
		||||
    "rollup": "^4.41.1",
 | 
			
		||||
    "rollup-plugin-copy": "^3.5.0",
 | 
			
		||||
    "rollup-plugin-dts": "^6.2.1",
 | 
			
		||||
    "rollup-plugin-esbuild": "^6.2.1",
 | 
			
		||||
    "sequelize": "^6.37.7",
 | 
			
		||||
    "tape": "^5.9.0",
 | 
			
		||||
    "tsup": "^8.5.0",
 | 
			
		||||
    "tsx": "^4.19.4",
 | 
			
		||||
    "typescript": "^5.8.3",
 | 
			
		||||
    "ws": "npm:@kevisual/ws"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1962
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1962
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,75 +0,0 @@
 | 
			
		||||
import resolve from '@rollup/plugin-node-resolve';
 | 
			
		||||
import commonjs from '@rollup/plugin-commonjs';
 | 
			
		||||
import json from '@rollup/plugin-json';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import esbuild from 'rollup-plugin-esbuild';
 | 
			
		||||
import alias from '@rollup/plugin-alias';
 | 
			
		||||
import replace from '@rollup/plugin-replace';
 | 
			
		||||
import pkgs from './package.json' with {type: 'json'};
 | 
			
		||||
 | 
			
		||||
const isDev = process.env.NODE_ENV === 'development';
 | 
			
		||||
const input = isDev ? './src/dev.ts' : './src/main.ts';
 | 
			
		||||
/**
 | 
			
		||||
 * @type {import('rollup').RollupOptions}
 | 
			
		||||
 */
 | 
			
		||||
const config = {
 | 
			
		||||
  input,
 | 
			
		||||
  output: {
 | 
			
		||||
    dir: './dist',
 | 
			
		||||
    entryFileNames: 'app.mjs',
 | 
			
		||||
    chunkFileNames: '[name]-[hash].mjs',
 | 
			
		||||
    format: 'esm',
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    replace({
 | 
			
		||||
      preventAssignment: true, // 防止意外赋值
 | 
			
		||||
      DEV_SERVER: JSON.stringify(isDev), // 替换 process.env.NODE_ENV
 | 
			
		||||
      APP_VERSION: JSON.stringify(pkgs.version),
 | 
			
		||||
    }),
 | 
			
		||||
    alias({
 | 
			
		||||
      // only esbuild needs to be configured
 | 
			
		||||
      entries: [
 | 
			
		||||
        { find: '@', replacement: path.resolve('src') }, // 配置 @ 为 src 目录
 | 
			
		||||
        { find: 'http', replacement: 'node:http' },
 | 
			
		||||
        { find: 'https', replacement: 'node:https' },
 | 
			
		||||
        { find: 'fs', replacement: 'node:fs' },
 | 
			
		||||
        { find: 'path', replacement: 'node:path' },
 | 
			
		||||
        { find: 'crypto', replacement: 'node:crypto' },
 | 
			
		||||
        { find: 'zlib', replacement: 'node:zlib' },
 | 
			
		||||
        { find: 'stream', replacement: 'node:stream' },
 | 
			
		||||
        { find: 'net', replacement: 'node:net' },
 | 
			
		||||
        { find: 'tty', replacement: 'node:tty' },
 | 
			
		||||
        { find: 'tls', replacement: 'node:tls' },
 | 
			
		||||
        { find: 'buffer', replacement: 'node:buffer' },
 | 
			
		||||
        { find: 'timers', replacement: 'node:timers' },
 | 
			
		||||
        // { find: 'string_decoder', replacement: 'node:string_decoder' },
 | 
			
		||||
        { find: 'dns', replacement: 'node:dns' },
 | 
			
		||||
        { find: 'domain', replacement: 'node:domain' },
 | 
			
		||||
        { find: 'os', replacement: 'node:os' },
 | 
			
		||||
        { find: 'events', replacement: 'node:events' },
 | 
			
		||||
        { find: 'url', replacement: 'node:url' },
 | 
			
		||||
        { find: 'assert', replacement: 'node:assert' },
 | 
			
		||||
        { find: 'util', replacement: 'node:util' },
 | 
			
		||||
      ],
 | 
			
		||||
    }),
 | 
			
		||||
    resolve({
 | 
			
		||||
      preferBuiltins: true, // 强制优先使用内置模块
 | 
			
		||||
    }),
 | 
			
		||||
    commonjs(),
 | 
			
		||||
    esbuild({
 | 
			
		||||
      target: 'node22', //
 | 
			
		||||
      minify: false, // 启用代码压缩
 | 
			
		||||
      tsconfig: 'tsconfig.json',
 | 
			
		||||
    }),
 | 
			
		||||
    json(),
 | 
			
		||||
  ],
 | 
			
		||||
  external: [
 | 
			
		||||
    /@kevisual\/router(\/.*)?/, //, // 路由
 | 
			
		||||
    /@kevisual\/use-config(\/.*)?/, //
 | 
			
		||||
 | 
			
		||||
    'sequelize', // 数据库 orm
 | 
			
		||||
    'ioredis', // redis
 | 
			
		||||
    'pg', // pg
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
export default config;
 | 
			
		||||
@@ -7,10 +7,12 @@ import fs from 'fs';
 | 
			
		||||
// const videoTestPath = path.join(process.cwd(), 'videos/asr_example2.wav');
 | 
			
		||||
// const videoTestPath = path.join(process.cwd(), 'videos/tts_mix.mp3');
 | 
			
		||||
const videoTestPath = path.join(process.cwd(), 'videos/my_speech_text.wav');
 | 
			
		||||
const videoTestPath3 = path.join(process.cwd(), 'funasr_test.wav');
 | 
			
		||||
const name = 'output-1746007775571.mp3';
 | 
			
		||||
const videoTestPath2 = path.join(process.cwd(), 'build', name);
 | 
			
		||||
 | 
			
		||||
const url = 'wss://funasr.xiongxiao.me';
 | 
			
		||||
const url5 = 'https://1.15.101.247:10095'; // pro
 | 
			
		||||
// const ws = new VideoWS({
 | 
			
		||||
//   // url: 'wss://192.168.31.220:10095',
 | 
			
		||||
//   url: 'wss://funasr.xiongxiao.me',
 | 
			
		||||
@@ -54,12 +56,27 @@ const url = 'wss://funasr.xiongxiao.me';
 | 
			
		||||
// server.listen(10096);
 | 
			
		||||
 | 
			
		||||
const ws2 = new VideoWS({
 | 
			
		||||
  url: url,
 | 
			
		||||
  url: url5,
 | 
			
		||||
  mode: '2pass',
 | 
			
		||||
  onConnect: async () => {
 | 
			
		||||
    const data = fs.readFileSync(videoTestPath);
 | 
			
		||||
    await ws2.sendBuffer(data, { wav_format: 'mp3' });
 | 
			
		||||
    await new Promise((resolve) => setTimeout(resolve, 1000));
 | 
			
		||||
    const data2 = fs.readFileSync(videoTestPath2);
 | 
			
		||||
    await ws2.sendBuffer(data2, { wav_format: 'mp3' });
 | 
			
		||||
    const data = fs.readFileSync(videoTestPath3);
 | 
			
		||||
    // await ws2.sendBuffer(data, { wav_format: 'mp3' });
 | 
			
		||||
    // await new Promise((resolve) => setTimeout(resolve, 1000));
 | 
			
		||||
    // const data2 = fs.readFileSync(videoTestPath2);
 | 
			
		||||
    // await ws2.sendBuffer(data2, { wav_format: 'mp3' });
 | 
			
		||||
    ws2.emitter.on('message', (event) => {
 | 
			
		||||
      console.log('message', event.data);
 | 
			
		||||
    });
 | 
			
		||||
    ws2.emitter.on('result', (result) => {
 | 
			
		||||
      if (result.is_final) {
 | 
			
		||||
        console.log('Final result:', result);
 | 
			
		||||
        process.exit(0);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    await ws2.start();
 | 
			
		||||
    await ws2.sendBuffer(data, { online: true });
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      ws2.stop();
 | 
			
		||||
    }, 4000);
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,48 +1,60 @@
 | 
			
		||||
import { VideoWS } from '../ws.ts';
 | 
			
		||||
import path from 'node:path';
 | 
			
		||||
import net from 'net';
 | 
			
		||||
import { Recording } from '../../../../recorder/index.ts';
 | 
			
		||||
import Stream from 'stream';
 | 
			
		||||
import fs from 'node:fs'; // 新增
 | 
			
		||||
 | 
			
		||||
const recorder = new Recording({
 | 
			
		||||
  sampleRate: 16000,
 | 
			
		||||
  channels: 1, //
 | 
			
		||||
  audioType: 'wav',
 | 
			
		||||
  threshold: 0,
 | 
			
		||||
  recorder: 'rec',
 | 
			
		||||
  silence: '1.0',
 | 
			
		||||
  endOnSilence: true,
 | 
			
		||||
});
 | 
			
		||||
const writeFilePath = path.join(process.cwd(), 'funasr_test.wav');
 | 
			
		||||
const fileStream = fs.createWriteStream(writeFilePath, { encoding: 'binary' });
 | 
			
		||||
 | 
			
		||||
const recorder = new Recording();
 | 
			
		||||
const writeStream = new Stream.Writable();
 | 
			
		||||
const url = 'wss://funasr.xiongxiao.me';
 | 
			
		||||
const url3 = 'wss://pro.xiongxiao.me:10095';
 | 
			
		||||
const url4 = 'wss://121.4.112.18:10095'; // aliyun
 | 
			
		||||
const url5 = 'https://1.15.101.247:10095'; // pro
 | 
			
		||||
 | 
			
		||||
const ws = new VideoWS({
 | 
			
		||||
  // url: 'wss://192.168.31.220:10095',
 | 
			
		||||
  url: url,
 | 
			
		||||
  url: url5,
 | 
			
		||||
  isFile: false,
 | 
			
		||||
  // mode: 'online',
 | 
			
		||||
  mode: '2pass',
 | 
			
		||||
  wsOptions: {
 | 
			
		||||
    rejectUnauthorized: false,
 | 
			
		||||
  },
 | 
			
		||||
  onConnect: async () => {
 | 
			
		||||
    console.log('onConnect');
 | 
			
		||||
    ws.start();
 | 
			
		||||
 | 
			
		||||
    recorder.start();
 | 
			
		||||
    let chunks: Buffer = Buffer.alloc(0);
 | 
			
		||||
    var chunk_size = 960; // for asr chunk_size [5, 10, 5]
 | 
			
		||||
    let totalsend = 0;
 | 
			
		||||
    let len = 0;
 | 
			
		||||
    recorder.stream().on('data', (chunk) => {
 | 
			
		||||
      // chunks = Buffer.concat([chunks, chunk]);
 | 
			
		||||
      // if (chunks.length > chunk_size) {
 | 
			
		||||
      //   ws.send(chunks);
 | 
			
		||||
      //   console.log('chunk', chunk.length);
 | 
			
		||||
 | 
			
		||||
      //   totalsend += chunks.length;
 | 
			
		||||
      //   chunks = Buffer.alloc(0);
 | 
			
		||||
      // }
 | 
			
		||||
      ws.send(chunk);
 | 
			
		||||
      // ws.sendBuffer(chunk, { online: true });
 | 
			
		||||
      // console.log('Sending audio chunk:', chunk.length);
 | 
			
		||||
      ws.send(chunk)
 | 
			
		||||
      fileStream.write(chunk); // 新增:将音频数据写入文件
 | 
			
		||||
      len += chunk.length;
 | 
			
		||||
    });
 | 
			
		||||
    ws.start();
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      ws.stop();
 | 
			
		||||
      fileStream.end(); // 新增:关闭文件流
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        process.exit(0);
 | 
			
		||||
      }, 1000);
 | 
			
		||||
      console.log('len', len);
 | 
			
		||||
    }, 10 * 30 * 1000);
 | 
			
		||||
    // }, 5 * 1000);
 | 
			
		||||
    }, 10 * 1000);
 | 
			
		||||
 | 
			
		||||
    ws.emitter.on('message', (event) => {
 | 
			
		||||
      // console.log('message', event.data);
 | 
			
		||||
      console.log('message', event.data);
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
// import WebSocket from 'ws';
 | 
			
		||||
import { EventEmitter } from 'eventemitter3';
 | 
			
		||||
import { WSServer, WSSOptions } from '../../provider/ws-server.ts';
 | 
			
		||||
 | 
			
		||||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
 | 
			
		||||
export type VideoWSOptions = {
 | 
			
		||||
  url?: string;
 | 
			
		||||
  ws?: WebSocket;
 | 
			
		||||
@@ -60,7 +60,7 @@ export class VideoWS extends WSServer {
 | 
			
		||||
 | 
			
		||||
  async start(opts?: Partial<OpenRequest>) {
 | 
			
		||||
    const chunk_size = new Array(5, 10, 5);
 | 
			
		||||
 | 
			
		||||
    console.log('start', chunk_size);
 | 
			
		||||
    const request: OpenRequest = {
 | 
			
		||||
      chunk_size: chunk_size,
 | 
			
		||||
      wav_name: 'h5', //
 | 
			
		||||
@@ -94,15 +94,20 @@ export class VideoWS extends WSServer {
 | 
			
		||||
      this.ws.send(data);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  async sendBuffer(data: Buffer, opts?: { isFile?: boolean; wav_format?: string }) {
 | 
			
		||||
    const { wav_format = 'wav' } = opts || {};
 | 
			
		||||
  /**
 | 
			
		||||
   * 发送音频数据, 离线
 | 
			
		||||
   * @param data 音频数据
 | 
			
		||||
   * @param opts 选项
 | 
			
		||||
   */
 | 
			
		||||
  async sendBuffer(data: Buffer, opts?: { isFile?: boolean; wav_format?: string; online?: boolean }) {
 | 
			
		||||
    const { wav_format = 'wav', online = false } = opts || {};
 | 
			
		||||
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
 | 
			
		||||
      let sampleBuf = new Uint8Array(data);
 | 
			
		||||
      const ws = this;
 | 
			
		||||
      var chunk_size = 960; // for asr chunk_size [5, 10, 5]
 | 
			
		||||
      let totalsend = 0;
 | 
			
		||||
      let len = 0;
 | 
			
		||||
      ws.start({ wav_format });
 | 
			
		||||
      if (!online) ws.start({ wav_format });
 | 
			
		||||
      while (sampleBuf.length >= chunk_size) {
 | 
			
		||||
        const sendBuf = sampleBuf.slice(0, chunk_size);
 | 
			
		||||
        totalsend = totalsend + sampleBuf.length;
 | 
			
		||||
@@ -111,7 +116,7 @@ export class VideoWS extends WSServer {
 | 
			
		||||
        ws.send(sendBuf);
 | 
			
		||||
        len++;
 | 
			
		||||
      }
 | 
			
		||||
      ws.stop();
 | 
			
		||||
      if (!online) ws.stop();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  async onMessage(event: MessageEvent) {
 | 
			
		||||
@@ -123,7 +128,7 @@ export class VideoWS extends WSServer {
 | 
			
		||||
        // console.log('result', result, typeof result);
 | 
			
		||||
        this.emitter.emit('result', result);
 | 
			
		||||
      }
 | 
			
		||||
      console.log('onMessage-result', result);
 | 
			
		||||
      // console.log('onMessage-result', result);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log('error', error);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import * as zlib from 'zlib';
 | 
			
		||||
import { promisify } from 'util';
 | 
			
		||||
import * as zlib from 'node:zlib';
 | 
			
		||||
import { promisify } from 'node:util';
 | 
			
		||||
import { nanoid } from 'nanoid';
 | 
			
		||||
import { VolcEngineBase, uuid } from './base.ts';
 | 
			
		||||
 | 
			
		||||
@@ -61,6 +61,39 @@ function generateBeforePayload(sequence: number): Buffer {
 | 
			
		||||
  return beforePayload;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ParsedMessage = {
 | 
			
		||||
  isLastPackage: boolean;
 | 
			
		||||
  payloadSequence?: number;
 | 
			
		||||
  payloadMsg?: {
 | 
			
		||||
    audio_info?: {
 | 
			
		||||
      duration: number;
 | 
			
		||||
    };
 | 
			
		||||
    result?: {
 | 
			
		||||
      additions?: {
 | 
			
		||||
        log_id?: string;
 | 
			
		||||
      };
 | 
			
		||||
      text?: string;
 | 
			
		||||
      utterances?: Array<{
 | 
			
		||||
        additions?: {
 | 
			
		||||
          fixed_prefix_result?: string;
 | 
			
		||||
        };
 | 
			
		||||
        definite?: boolean;
 | 
			
		||||
        end_time?: number;
 | 
			
		||||
        start_time?: number;
 | 
			
		||||
        text?: string;
 | 
			
		||||
        words?: Array<{
 | 
			
		||||
          end_time: number;
 | 
			
		||||
          start_time: number;
 | 
			
		||||
          text: string;
 | 
			
		||||
        }>;
 | 
			
		||||
      }>;
 | 
			
		||||
    };
 | 
			
		||||
    error?: any;
 | 
			
		||||
  };
 | 
			
		||||
  payloadSize?: number;
 | 
			
		||||
  code?: number;
 | 
			
		||||
  seq?: number;
 | 
			
		||||
};
 | 
			
		||||
/**
 | 
			
		||||
 * Parse response from the WebSocket server
 | 
			
		||||
 */
 | 
			
		||||
@@ -393,10 +426,11 @@ export class AsrWsClient extends VolcEngineBase {
 | 
			
		||||
    // Wait for response
 | 
			
		||||
    await sendVoice(audioData, segmentSize);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async onMessage(event: MessageEvent) {
 | 
			
		||||
    try {
 | 
			
		||||
      const parsed = parseResponse(Buffer.from(event.data as ArrayBuffer));
 | 
			
		||||
      console.log(`Seq ${parsed.payloadSequence} response:`, parsed);
 | 
			
		||||
      // console.log(`Seq ${parsed.payloadSequence} response:`, parsed);
 | 
			
		||||
      if (typeof event.data === 'string') {
 | 
			
		||||
        throw new Error('event.data is string: ' + event.data);
 | 
			
		||||
      }
 | 
			
		||||
@@ -405,10 +439,9 @@ export class AsrWsClient extends VolcEngineBase {
 | 
			
		||||
        this.emitter.emit('error', parsed);
 | 
			
		||||
        this.isError = true;
 | 
			
		||||
      }
 | 
			
		||||
      this.emitter.emit('message', parsed);
 | 
			
		||||
      if (parsed.isLastPackage) {
 | 
			
		||||
        this.emitter.emit('end', parsed);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.emitter.emit('message', parsed);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Error processing response:', error);
 | 
			
		||||
@@ -440,6 +473,14 @@ export class AsrWsClient extends VolcEngineBase {
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  async setIsEnd(isEnd: boolean) {
 | 
			
		||||
      super.setIsEnd(isEnd);
 | 
			
		||||
      if (isEnd) {
 | 
			
		||||
        // 发送空白包
 | 
			
		||||
        const emptyBuffer = Buffer.alloc(10000);
 | 
			
		||||
        this.sendVoiceStream(emptyBuffer);
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 发送语音流, 最小10000
 | 
			
		||||
   * @param data
 | 
			
		||||
 
 | 
			
		||||
@@ -238,7 +238,7 @@ interface AudioItem {
 | 
			
		||||
  id: string | number;
 | 
			
		||||
  path: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 流式语音识别
 | 
			
		||||
export class AsrWsClient extends VolcEngineBase {
 | 
			
		||||
  private audioPath: string;
 | 
			
		||||
  private cluster: string;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										136
									
								
								src/asr/provider/volcengine/auc.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/asr/provider/volcengine/auc.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
			
		||||
// https://git.xiongxiao.me/kevisual/video-tools/raw/branch/main/src/asr/provider/volcengine/auc.ts
 | 
			
		||||
import { nanoid } from "nanoid"
 | 
			
		||||
 | 
			
		||||
export const FlashURL = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash"
 | 
			
		||||
export const AsrBaseURL = 'https://openspeech.bytedance.com/api/v3/auc/bigmodel/submit'
 | 
			
		||||
export const AsrBase = 'volc.bigasr.auc'
 | 
			
		||||
export const AsrTurbo = 'volc.bigasr.auc_turbo'
 | 
			
		||||
 | 
			
		||||
const uuid = () => nanoid()
 | 
			
		||||
 | 
			
		||||
type AsrOptions = {
 | 
			
		||||
  url?: string
 | 
			
		||||
  appid?: string
 | 
			
		||||
  token?: string
 | 
			
		||||
  type?: AsrType
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AsrType = 'flash' | 'standard' | 'turbo'
 | 
			
		||||
export class Asr {
 | 
			
		||||
  url: string = FlashURL
 | 
			
		||||
  appid: string = ""
 | 
			
		||||
  token: string = ""
 | 
			
		||||
  type: AsrType = 'flash'
 | 
			
		||||
  constructor(options: AsrOptions = {}) {
 | 
			
		||||
    this.appid = options.appid || ""
 | 
			
		||||
    this.token = options.token || ""
 | 
			
		||||
    this.type = options.type || 'flash'
 | 
			
		||||
    if (this.type !== 'flash') {
 | 
			
		||||
      this.url = AsrBaseURL
 | 
			
		||||
    }
 | 
			
		||||
    if (!this.appid || !this.token) {
 | 
			
		||||
      throw new Error("VOLCENGINE_Asr_APPID or VOLCENGINE_Asr_TOKEN is not set")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  header() {
 | 
			
		||||
    const model = this.type === 'flash' ? AsrTurbo : AsrBase
 | 
			
		||||
    return {
 | 
			
		||||
      "X-Api-App-Key": this.appid,
 | 
			
		||||
      "X-Api-Access-Key": this.token,
 | 
			
		||||
      "X-Api-Resource-Id": model,
 | 
			
		||||
      "X-Api-Request-Id": uuid(),
 | 
			
		||||
      "X-Api-Sequence": "-1",
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  submit(body: AsrRequest) {
 | 
			
		||||
    if (!body.audio || (!body.audio.url && !body.audio.data)) {
 | 
			
		||||
      throw new Error("audio.url or audio.data is required")
 | 
			
		||||
    }
 | 
			
		||||
    const data: AsrRequest = {
 | 
			
		||||
      ...body,
 | 
			
		||||
    }
 | 
			
		||||
    return fetch(this.url, { method: "POST", headers: this.header(), body: JSON.stringify(data) })
 | 
			
		||||
  }
 | 
			
		||||
  async getText(body: AsrRequest) {
 | 
			
		||||
    const res = await this.submit(body)
 | 
			
		||||
    return res.json()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AsrResponse = {
 | 
			
		||||
  audio_info: {
 | 
			
		||||
    /**
 | 
			
		||||
     * 音频时长,单位为 ms
 | 
			
		||||
     */
 | 
			
		||||
    duration: number;
 | 
			
		||||
  };
 | 
			
		||||
  result: {
 | 
			
		||||
    additions: {
 | 
			
		||||
      duration: string;
 | 
			
		||||
    };
 | 
			
		||||
    text: string;
 | 
			
		||||
    utterances: Array<{
 | 
			
		||||
      end_time: number;
 | 
			
		||||
      start_time: number;
 | 
			
		||||
      text: string;
 | 
			
		||||
      words: Array<{
 | 
			
		||||
        confidence: number;
 | 
			
		||||
        end_time: number;
 | 
			
		||||
        start_time: number;
 | 
			
		||||
        text: string;
 | 
			
		||||
      }>;
 | 
			
		||||
    }>;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
export interface AsrRequest {
 | 
			
		||||
  user?: {
 | 
			
		||||
    uid: string;
 | 
			
		||||
  };
 | 
			
		||||
  audio: {
 | 
			
		||||
    url?: string;
 | 
			
		||||
    data?: string;
 | 
			
		||||
    format?: 'wav' | 'pcm' | 'mp3' | 'ogg';
 | 
			
		||||
    codec?: 'raw' | 'opus'; 	// raw / opus,默认为 raw(pcm) 。
 | 
			
		||||
    rate?: 8000 | 16000; // 采样率,支持 8000 或 16000,默认为 16000 。
 | 
			
		||||
    channel?: 1 | 2; // 声道数,支持 1 或 2,默认为 1。
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  request?: {
 | 
			
		||||
    model_name?: string; // 识别模型名称,如 "bigmodel"
 | 
			
		||||
    enable_words?: boolean; // 是否开启词级别时间戳,默认为 false。
 | 
			
		||||
    enable_sentence_info?: boolean; // 是否开启句子级别时间戳,默认为 false。
 | 
			
		||||
    enable_utterance_info?: boolean; // 是否开启语句级别时间戳,默认为 true。
 | 
			
		||||
    enable_punctuation_prediction?: boolean; // 是否开启标点符号预测,默认为 true。
 | 
			
		||||
    enable_inverse_text_normalization?: boolean; // 是否开启文本规范化,默认为 true。
 | 
			
		||||
    enable_separate_recognition_per_channel?: boolean; // 是否开启声道分离识别,默认为 false。
 | 
			
		||||
    audio_channel_count?: 1 | 2; // 音频声道数,仅在 enable_separate_recognition_per_channel 开启时有效,支持 1 或 2,默认为 1。
 | 
			
		||||
    max_sentence_silence?: number; // 句子最大静音时间,仅在 enable_sentence_info 开启时有效,单位为 ms,默认为 800。
 | 
			
		||||
    custom_words?: string[];
 | 
			
		||||
    enable_channel_split?: boolean; // 是否开启声道分离
 | 
			
		||||
    enable_ddc?: boolean; // 是否开启 DDC(双通道降噪)
 | 
			
		||||
    enable_speaker_info?: boolean; // 是否开启说话人分离
 | 
			
		||||
    enable_punc?: boolean; // 是否开启标点符号预测(简写)
 | 
			
		||||
    enable_itn?: boolean; // 是否开启文本规范化(简写)
 | 
			
		||||
    vad_segment?: boolean; // 是否开启 VAD 断句
 | 
			
		||||
    show_utterances?: boolean; // 是否返回语句级别结果
 | 
			
		||||
    corpus?: {
 | 
			
		||||
      boosting_table_name?: string;
 | 
			
		||||
      correct_table_name?: string;
 | 
			
		||||
      context?: string;
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// const main = async () => {
 | 
			
		||||
//   const base64Audio = wavToBase64(audioPath);
 | 
			
		||||
//   const auc = new Asr({
 | 
			
		||||
//     appid: config.VOLCENGINE_AUC_APPID,
 | 
			
		||||
//     token: config.VOLCENGINE_AUC_TOKEN,
 | 
			
		||||
//   });
 | 
			
		||||
//   const result = await auc.getText({ audio: { data: base64Audio } });
 | 
			
		||||
//   console.log(util.inspect(result, { showHidden: false, depth: null, colors: true }))
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// main();
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import { initWs } from '../../../ws-adapter/index.ts';
 | 
			
		||||
import { WSServer } from '../../provider/ws-server.ts';
 | 
			
		||||
import { nanoid } from 'nanoid';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,15 +7,22 @@ import fs from 'fs';
 | 
			
		||||
const main = async () => {
 | 
			
		||||
  const audioId = '123';
 | 
			
		||||
  const asrClient = new AsrWsClient({
 | 
			
		||||
    appid: config.APP_ID,
 | 
			
		||||
    token: config.TOKEN,
 | 
			
		||||
    appid: config.VOLCENGINE_ASR_MODEL_APPID,
 | 
			
		||||
    token: config.VOLCENGINE_ASR_MODEL_TOKEN,
 | 
			
		||||
  });
 | 
			
		||||
  asrClient.emitter.on('message', (result) => {
 | 
			
		||||
    console.log('识别结果', JSON.stringify(result, null, 2));
 | 
			
		||||
  })
 | 
			
		||||
  asrClient.emitter.on('end', (result) => {
 | 
			
		||||
    console.log('识别结束', JSON.stringify(result, null, 2));
 | 
			
		||||
  })
 | 
			
		||||
  await new Promise((resolve) => setTimeout(resolve, 2000));
 | 
			
		||||
  const data = fs.readFileSync(audioPath);
 | 
			
		||||
  await asrClient.sendVoiceFile(data);
 | 
			
		||||
  await asrClient.sendVoiceFile(fs.readFileSync(blankAudioPath));
 | 
			
		||||
  // await asrClient.sendVoiceFile(fs.readFileSync(blankAudioPath));
 | 
			
		||||
  asrClient.setIsEnd(true);
 | 
			
		||||
  await asrClient.sendVoiceFile(fs.readFileSync(audioPath2));
 | 
			
		||||
  // await asrClient.sendVoiceFile(fs.readFileSync(audioPath2));
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								src/asr/provider/volcengine/test/auc.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/asr/provider/volcengine/test/auc.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import { audioPath, config, sleep } from './common.ts';
 | 
			
		||||
 | 
			
		||||
import { Asr } from '../auc.ts';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import util from 'node:util';
 | 
			
		||||
const wavToBase64 = (filePath: string) => {
 | 
			
		||||
  const data = fs.readFileSync(filePath);
 | 
			
		||||
  return data.toString('base64');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const main = async () => {
 | 
			
		||||
  const base64Audio = wavToBase64(audioPath);
 | 
			
		||||
  const auc = new Asr({
 | 
			
		||||
    appid: config.VOLCENGINE_AUC_APPID,
 | 
			
		||||
    token: config.VOLCENGINE_AUC_TOKEN,
 | 
			
		||||
  });
 | 
			
		||||
  const result = await auc.getText({ audio: { data: base64Audio } });
 | 
			
		||||
  console.log(util.inspect(result, { showHidden: false, depth: null, colors: true }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
const isBrowser = process?.env?.BROWSER === 'true' || (typeof window !== 'undefined' && typeof window.document !== 'undefined');
 | 
			
		||||
const isBrowser = (typeof process === 'undefined') || 
 | 
			
		||||
  (typeof window !== 'undefined' && typeof window.document !== 'undefined') || 
 | 
			
		||||
  (typeof process !== 'undefined' && process?.env?.BROWSER === 'true');
 | 
			
		||||
const chantHttpToWs = (url: string) => {
 | 
			
		||||
  if (url.startsWith('http://')) {
 | 
			
		||||
    return url.replace('http://', 'ws://');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
import { defineConfig } from 'tsup';
 | 
			
		||||
// import glob from 'fast-glob';
 | 
			
		||||
// const services = glob.sync('src/services/*.ts');
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
 | 
			
		||||
const clean = () => {
 | 
			
		||||
  const distDir = 'dist';
 | 
			
		||||
  if (fs.existsSync(distDir)) {
 | 
			
		||||
    fs.rmSync(distDir, { recursive: true, force: true });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
clean();
 | 
			
		||||
 | 
			
		||||
const entrys = ['src/index.ts'];
 | 
			
		||||
const nodeEntrys = ['src/dev.ts'];
 | 
			
		||||
 | 
			
		||||
const getCommonConfig = (opts = {}) => {
 | 
			
		||||
  return {
 | 
			
		||||
    entry: opts.entry,
 | 
			
		||||
    outExtension: ({ format }) => ({
 | 
			
		||||
      js: format === 'esm' ? '.mjs' : '.js',
 | 
			
		||||
    }),
 | 
			
		||||
    splitting: false,
 | 
			
		||||
    sourcemap: false,
 | 
			
		||||
    // clean: true,
 | 
			
		||||
    format: 'esm',
 | 
			
		||||
    external: ['dotenv'],
 | 
			
		||||
    dts: true,
 | 
			
		||||
    outDir: 'dist',
 | 
			
		||||
    tsconfig: 'tsconfig.json',
 | 
			
		||||
    ...opts,
 | 
			
		||||
    define: {
 | 
			
		||||
      'process.env.IS_BROWSER': JSON.stringify(process.env.BROWSER || false),
 | 
			
		||||
      ...opts.define,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
export default defineConfig([
 | 
			
		||||
  // getCommonConfig({ entry: entrys, define: { 'process.env.IS_BROWSER': JSON.stringify(true) } }), // 浏览器
 | 
			
		||||
  getCommonConfig({ entry: nodeEntrys, define: { 'process.env.IS_BROWSER': JSON.stringify(false) } }), // node
 | 
			
		||||
]);
 | 
			
		||||
		Reference in New Issue
	
	Block a user