| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- // 讯飞录音文件转写大模型 WebAPI 客户端
- // API文档: https://www.xfyun.cn/doc/spark/asr_llm/Ifasr_llm.html
- import crypto from 'crypto';
- interface AsrConfig {
- appId: string;
- accessKeyId: string;
- apiSecret: string;
- host: string;
- }
- interface CreateTaskParams {
- audio: Buffer;
- fileName: string;
- language?: string;
- }
- interface TaskResult {
- orderId: string;
- status: 'waiting' | 'processing' | 'completed' | 'failed';
- text?: string;
- error?: string;
- }
- function randomString(len: number): string {
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- return Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
- }
- function formatDateTime(d: Date): string {
- const pad = (n: number) => String(n).padStart(2, '0');
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}+0800`;
- }
- export class IflytekAsr {
- private config: AsrConfig;
- private baseUrl: string;
- constructor(config: AsrConfig) {
- this.config = config;
- this.baseUrl = config.host.replace(/\/$/, '');
- }
- // 标准签名: 对所有URL参数(除signature)排序 → URL编码 → &拼接 → HMAC-SHA1 → Base64
- private sign(params: Record<string, string>): string {
- // 1. 排除 signature,按 key 排序
- const sorted = Object.keys(params)
- .filter(k => k !== 'signature' && params[k] != null && params[k] !== '')
- .sort()
- .map(k => {
- const encodedKey = encodeURIComponent(k);
- const encodedVal = encodeURIComponent(params[k]);
- return `${encodedKey}=${encodedVal}`;
- })
- .join('&');
- console.log('[ASR] sign raw:', sorted);
- // 2. HMAC-SHA1 → Base64
- const hmac = crypto.createHmac('sha1', this.config.apiSecret);
- return hmac.update(sorted).digest('base64');
- }
- // 创建转写任务
- async createTask(params: CreateTaskParams): Promise<string> {
- const dateTime = formatDateTime(new Date());
- const signatureRandom = randomString(16);
- const fileSize = params.audio.length.toString();
- const language = params.language || 'autodialect';
- // 构建所有参数
- const queryParams: Record<string, string> = {
- appId: this.config.appId,
- accessKeyId: this.config.accessKeyId,
- dateTime,
- signatureRandom,
- fileSize,
- fileName: params.fileName,
- language,
- durationCheckDisable: 'true', // 关闭时长校验
- };
- // 生成签名 (不含 signature 字段)
- const signature = this.sign(queryParams);
- // 构建 URL — 使用与签名相同的编码方式
- const sortedKeys = Object.keys(queryParams).filter(k => queryParams[k] != null && queryParams[k] !== '').sort();
- const qs = sortedKeys.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`).join('&');
- const url = `${this.baseUrl}/v2/upload?${qs}`;
- console.log('[ASR] upload URL:', url);
- const res = await fetch(url, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/octet-stream',
- 'signature': signature,
- },
- body: new Uint8Array(params.audio),
- });
- const text = await res.text();
- console.log('[ASR] upload response:', text.slice(0, 500));
- let json: any;
- try { json = JSON.parse(text); } catch { throw new Error(`响应解析失败: ${text.slice(0, 200)}`); }
- if (json.code !== '000000') {
- throw new Error(`讯飞ASR上传失败: [${json.code}] ${json.descInfo || ''}`);
- }
- const orderId = json.content?.orderId;
- if (!orderId) throw new Error('未获取到 orderId');
- return orderId;
- }
- // 查询转写结果
- async getResult(orderId: string): Promise<TaskResult> {
- const dateTime = formatDateTime(new Date());
- const signatureRandom = randomString(16);
- const queryParams: Record<string, string> = {
- appId: this.config.appId,
- accessKeyId: this.config.accessKeyId,
- dateTime,
- signatureRandom,
- orderId,
- resultType: 'transfer',
- };
- const signature = this.sign(queryParams);
- const sortedKeys = Object.keys(queryParams).filter(k => queryParams[k] != null && queryParams[k] !== '').sort();
- const qs = sortedKeys.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`).join('&');
- const url = `${this.baseUrl}/v2/getResult?${qs}`;
- const res = await fetch(url, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'signature': signature,
- },
- });
- const text = await res.text();
- let json: any;
- try { json = JSON.parse(text); } catch { throw new Error(`结果解析失败: ${text.slice(0, 200)}`); }
- if (json.code !== '000000') {
- if (json.code === '000001') return { orderId, status: 'processing' };
- return { orderId, status: 'failed', error: `[${json.code}] ${json.descInfo || ''}` };
- }
- const content = json.content || {};
- let textResult = '';
- if (content.orderResult) {
- try {
- const resultObj = typeof content.orderResult === 'string'
- ? JSON.parse(content.orderResult) : content.orderResult;
- if (resultObj.lattice && Array.isArray(resultObj.lattice)) {
- textResult = resultObj.lattice
- .map((seg: any) => {
- const jb = seg.json_1best || '';
- try {
- const b = JSON.parse(jb);
- return b.st?.rt?.[0]?.ws?.map((w: any) => w.cw?.[0]?.w || '').join('') || '';
- } catch { return seg.onebest || ''; }
- })
- .join('');
- }
- } catch {
- textResult = typeof content.orderResult === 'string' ? content.orderResult : '';
- }
- }
- return { orderId, status: 'completed', text: textResult || content.descInfo || '' };
- }
- }
|