iflytek.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. // 讯飞录音文件转写大模型 WebAPI 客户端
  2. // API文档: https://www.xfyun.cn/doc/spark/asr_llm/Ifasr_llm.html
  3. import crypto from 'crypto';
  4. interface AsrConfig {
  5. appId: string;
  6. accessKeyId: string;
  7. apiSecret: string;
  8. host: string;
  9. }
  10. interface CreateTaskParams {
  11. audio: Buffer;
  12. fileName: string;
  13. language?: string;
  14. }
  15. interface TaskResult {
  16. orderId: string;
  17. status: 'waiting' | 'processing' | 'completed' | 'failed';
  18. text?: string;
  19. error?: string;
  20. }
  21. function randomString(len: number): string {
  22. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  23. return Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
  24. }
  25. function formatDateTime(d: Date): string {
  26. const pad = (n: number) => String(n).padStart(2, '0');
  27. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}+0800`;
  28. }
  29. export class IflytekAsr {
  30. private config: AsrConfig;
  31. private baseUrl: string;
  32. constructor(config: AsrConfig) {
  33. this.config = config;
  34. this.baseUrl = config.host.replace(/\/$/, '');
  35. }
  36. // 标准签名: 对所有URL参数(除signature)排序 → URL编码 → &拼接 → HMAC-SHA1 → Base64
  37. private sign(params: Record<string, string>): string {
  38. // 1. 排除 signature,按 key 排序
  39. const sorted = Object.keys(params)
  40. .filter(k => k !== 'signature' && params[k] != null && params[k] !== '')
  41. .sort()
  42. .map(k => {
  43. const encodedKey = encodeURIComponent(k);
  44. const encodedVal = encodeURIComponent(params[k]);
  45. return `${encodedKey}=${encodedVal}`;
  46. })
  47. .join('&');
  48. console.log('[ASR] sign raw:', sorted);
  49. // 2. HMAC-SHA1 → Base64
  50. const hmac = crypto.createHmac('sha1', this.config.apiSecret);
  51. return hmac.update(sorted).digest('base64');
  52. }
  53. // 创建转写任务
  54. async createTask(params: CreateTaskParams): Promise<string> {
  55. const dateTime = formatDateTime(new Date());
  56. const signatureRandom = randomString(16);
  57. const fileSize = params.audio.length.toString();
  58. const language = params.language || 'autodialect';
  59. // 构建所有参数
  60. const queryParams: Record<string, string> = {
  61. appId: this.config.appId,
  62. accessKeyId: this.config.accessKeyId,
  63. dateTime,
  64. signatureRandom,
  65. fileSize,
  66. fileName: params.fileName,
  67. language,
  68. durationCheckDisable: 'true', // 关闭时长校验
  69. };
  70. // 生成签名 (不含 signature 字段)
  71. const signature = this.sign(queryParams);
  72. // 构建 URL — 使用与签名相同的编码方式
  73. const sortedKeys = Object.keys(queryParams).filter(k => queryParams[k] != null && queryParams[k] !== '').sort();
  74. const qs = sortedKeys.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`).join('&');
  75. const url = `${this.baseUrl}/v2/upload?${qs}`;
  76. console.log('[ASR] upload URL:', url);
  77. const res = await fetch(url, {
  78. method: 'POST',
  79. headers: {
  80. 'Content-Type': 'application/octet-stream',
  81. 'signature': signature,
  82. },
  83. body: new Uint8Array(params.audio),
  84. });
  85. const text = await res.text();
  86. console.log('[ASR] upload response:', text.slice(0, 500));
  87. let json: any;
  88. try { json = JSON.parse(text); } catch { throw new Error(`响应解析失败: ${text.slice(0, 200)}`); }
  89. if (json.code !== '000000') {
  90. throw new Error(`讯飞ASR上传失败: [${json.code}] ${json.descInfo || ''}`);
  91. }
  92. const orderId = json.content?.orderId;
  93. if (!orderId) throw new Error('未获取到 orderId');
  94. return orderId;
  95. }
  96. // 查询转写结果
  97. async getResult(orderId: string): Promise<TaskResult> {
  98. const dateTime = formatDateTime(new Date());
  99. const signatureRandom = randomString(16);
  100. const queryParams: Record<string, string> = {
  101. appId: this.config.appId,
  102. accessKeyId: this.config.accessKeyId,
  103. dateTime,
  104. signatureRandom,
  105. orderId,
  106. resultType: 'transfer',
  107. };
  108. const signature = this.sign(queryParams);
  109. const sortedKeys = Object.keys(queryParams).filter(k => queryParams[k] != null && queryParams[k] !== '').sort();
  110. const qs = sortedKeys.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`).join('&');
  111. const url = `${this.baseUrl}/v2/getResult?${qs}`;
  112. const res = await fetch(url, {
  113. method: 'POST',
  114. headers: {
  115. 'Content-Type': 'application/json',
  116. 'signature': signature,
  117. },
  118. });
  119. const text = await res.text();
  120. let json: any;
  121. try { json = JSON.parse(text); } catch { throw new Error(`结果解析失败: ${text.slice(0, 200)}`); }
  122. if (json.code !== '000000') {
  123. if (json.code === '000001') return { orderId, status: 'processing' };
  124. return { orderId, status: 'failed', error: `[${json.code}] ${json.descInfo || ''}` };
  125. }
  126. const content = json.content || {};
  127. let textResult = '';
  128. if (content.orderResult) {
  129. try {
  130. const resultObj = typeof content.orderResult === 'string'
  131. ? JSON.parse(content.orderResult) : content.orderResult;
  132. if (resultObj.lattice && Array.isArray(resultObj.lattice)) {
  133. textResult = resultObj.lattice
  134. .map((seg: any) => {
  135. const jb = seg.json_1best || '';
  136. try {
  137. const b = JSON.parse(jb);
  138. return b.st?.rt?.[0]?.ws?.map((w: any) => w.cw?.[0]?.w || '').join('') || '';
  139. } catch { return seg.onebest || ''; }
  140. })
  141. .join('');
  142. }
  143. } catch {
  144. textResult = typeof content.orderResult === 'string' ? content.orderResult : '';
  145. }
  146. }
  147. return { orderId, status: 'completed', text: textResult || content.descInfo || '' };
  148. }
  149. }