Skip to content

第 15 章 · 语音转写与合成

本章目标:

  • 掌握 transcribe 函数把音频转写为文本的用法
  • 了解实验性的流式转写 experimental_streamTranscribe 及其适用场景
  • 掌握 generateSpeech 从文本生成语音的用法
  • 学会处理下载限制、中止信号与语音相关错误类型

15.1 音频转写(Transcription)

AI SDK 提供 transcribe 函数,使用 transcription model 转写音频。

ts
import { transcribe, createGateway } from 'ai';
import { readFile } from 'fs/promises';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

const transcript = await transcribe({
  model: gateway.transcription('openai/whisper-1'),
  audio: await readFile('audio.mp3'),
});

audio 属性可以是 Uint8ArrayArrayBufferBuffer、字符串(base64 编码的音频数据)或 URL

访问生成的转写结果:

ts
const text = transcript.text; // transcript text e.g. "Hello, world!"
const segments = transcript.segments; // array of segments with start and end times, if available
const language = transcript.language; // language of the transcript e.g. "en", if available
const durationInSeconds = transcript.durationInSeconds; // duration of the transcript in seconds, if available

15.2 流式转写(实验性)

流式转写是实验特性。

当你持有实时原始音频、需要在完整音频流结束前就拿到转写更新时,使用 experimental_streamTranscribe。该函数使用支持流式的转写模型;provider 专属行为通过 providerOptions 配置,流式操作本身由函数选择:

ts
import { createGateway } from 'ai';
import { experimental_streamTranscribe as streamTranscribe } from 'ai';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

const result = streamTranscribe({
  model: gateway.transcription('openai/gpt-realtime-whisper'),
  audio: audioStream, // ReadableStream<Uint8Array | string>
  inputAudioFormat: { type: 'audio/pcm', rate: 24000 },
  providerOptions: {
    openai: {
      language: 'en',
      streaming: {
        delay: 'low',
      },
    },
  },
});

for await (const part of result.fullStream) {
  if (part.type === 'transcript-delta') {
    process.stdout.write(part.delta);
  }

  if (part.type === 'transcript-partial') {
    console.log('partial:', part.text);
  }

  if (part.type === 'transcript-final') {
    console.log('final:', part.text);
  }
}

console.log(await result.text);

fullStream 是单消费者的实时流,只能被访问一次。当你同时需要流分片和最终结果时,先访问 fullStream,在消费过程中或之后再 await 结果 promise。如果先访问了结果 promise,流会被内部消费,fullStream 就不再可用——这避免了为实时音频保留无界的回放缓冲区。

获取最终转写元数据:

ts
const text = await result.text; // final transcript text
const segments = await result.segments; // final segments with timing, if available
const language = await result.language; // language of the transcript, if available
const durationInSeconds = await result.durationInSeconds; // duration in seconds, if available

audio 流必须包含原始音频分片:Uint8Array 分片是原始字节,字符串分片是 base64 编码的原始字节。务必设置 inputAudioFormat 与你发送的分片匹配。

官方文档指出:字符串模型 ID 会经全局默认 provider(AI Gateway 默认)解析。AI Gateway 为支持的模型提供流式转写(如 openai/gpt-realtime-whisperelevenlabs/eleven-scribe-2-realtimexai/grok-stt),因此字符串 ID 可用。不同厂商的流式转写模型各不相同:OpenAI 用 gpt-realtime-whisper;Cartesia 用 Ink 2(cartesia.transcription('ink-2'));ElevenLabs 用 Scribe v2 Realtime;xAI 用同一 xai.transcription() 模型,experimental_streamTranscribe 会选择其 WebSocket STT 传输。

ts
import { xai } from '@ai-sdk/xai';
import { experimental_streamTranscribe as streamTranscribe } from 'ai';

const result = streamTranscribe({
  model: xai.transcription(),
  audio: audioStream,
  inputAudioFormat: { type: 'audio/pcm', rate: 16000 },
  providerOptions: {
    xai: {
      language: 'en',
      keyterm: ['AI SDK', 'Grok'],
      streaming: {
        interimResults: true,
        endpointing: 500,
      },
    },
  },
});

一些 provider 的直连流式 STT 需要 WebSocket headers——在这类运行环境中,创建 provider 时需传入 provider 专属的 webSocket 实现。

15.3 转写设置

Provider 专属设置

转写模型通常有 provider 或模型专属设置,通过 providerOptions 参数配置:

ts
import { transcribe, createGateway } from 'ai';
import { readFile } from 'fs/promises';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

const transcript = await transcribe({
  model: gateway.transcription('openai/whisper-1'),
  audio: await readFile('audio.mp3'),
  providerOptions: {
    openai: {
      timestampGranularities: ['word'],
    },
  },
});

下载大小限制

audio 是 URL 时,SDK 以默认 2 GiB 上限下载文件。可通过 createDownload 自定义:

ts
import { transcribe, createDownload, createGateway } from 'ai';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

const transcript = await transcribe({
  model: gateway.transcription('openai/whisper-1'),
  audio: new URL('https://example.com/audio.mp3'),
  download: createDownload({ maxBytes: 50 * 1024 * 1024 }), // 50 MB limit
});

也可以提供完全自定义的下载函数:

ts
const transcript = await transcribe({
  model: gateway.transcription('openai/whisper-1'),
  audio: new URL('https://example.com/audio.mp3'),
  download: async ({ url }) => {
    const res = await myAuthenticatedFetch(url);
    return {
      data: new Uint8Array(await res.arrayBuffer()),
      mediaType: res.headers.get('content-type') ?? undefined,
    };
  },
});

下载超过大小限制时会抛出 DownloadError

ts
import { transcribe, DownloadError, createGateway } from 'ai';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

try {
  await transcribe({
    model: gateway.transcription('openai/whisper-1'),
    audio: new URL('https://example.com/audio.mp3'),
  });
} catch (error) {
  if (DownloadError.isInstance(error)) {
    console.log('Download failed:', error.message);
  }
}

中止信号与超时

transcribe 接受可选的 AbortSignal 类型 abortSignal 参数,可用于中止转写或设置超时——与 URL 下载组合使用时可防止长时间挂起的请求:

ts
import { transcribe, createGateway } from 'ai';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

const transcript = await transcribe({
  model: gateway.transcription('openai/whisper-1'),
  audio: new URL('https://example.com/audio.mp3'),
  abortSignal: AbortSignal.timeout(5000), // Abort after 5 seconds
});

15.4 语音合成(Speech)

AI SDK 提供 generateSpeech 函数,使用 speech model 从文本生成语音。

ts
import { generateSpeech, createGateway } from 'ai';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

const audio = await generateSpeech({
  model: gateway.speech('openai/tts-1'),
  text: 'Hello, world!',
  voice: 'alloy',
});

语言设置

可以指定语音生成的语言(取决于 provider 支持)。官方示例以 LMNT 为例展示 language 参数:

ts
import { generateSpeech } from 'ai';
import { lmnt } from '@ai-sdk/lmnt';

const audio = await generateSpeech({
  model: lmnt.speech('aurora'),
  text: 'Hola, mundo!',
  language: 'es', // Spanish
});

访问生成的音频数据:

ts
const audioData = result.audio.uint8Array; // audio data as Uint8Array
// or
const audioBase64 = result.audio.base64; // audio data as base64 string

15.5 语音合成设置

Provider 专属设置

providerOptions 设置模型专属选项:

ts
import { generateSpeech, createGateway } from 'ai';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

const audio = await generateSpeech({
  model: gateway.speech('openai/tts-1'),
  text: 'Hello, world!',
  providerOptions: {
    openai: {
      // ...
    },
  },
});

中止信号、自定义 Headers 与警告

generateSpeech 同样接受可选的 abortSignal 参数来中止生成或设置超时;可选的 headers 参数可为请求添加自定义 header;不支持的参数等警告会出现在 warnings 属性中:

ts
import { generateSpeech, createGateway } from 'ai';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

const audio = await generateSpeech({
  model: gateway.speech('openai/tts-1'),
  text: 'Hello, world!',
  abortSignal: AbortSignal.timeout(1000), // Abort after 1 second
});
ts
import { generateSpeech, createGateway } from 'ai';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

const audio = await generateSpeech({
  model: gateway.speech('openai/tts-1'),
  text: 'Hello, world!',
});

const warnings = audio.warnings;

错误处理

generateSpeech 无法生成有效音频时,会抛出 AI_NoSpeechGeneratedError。可能原因:模型未能生成响应,或生成了无法解析的响应。

错误对象保留以下信息便于记录问题:responses(speech 模型响应元数据,含时间戳、模型、headers)与 cause(根因):

ts
import { generateSpeech, NoSpeechGeneratedError, createGateway } from 'ai';
import 'dotenv/config';

const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });

try {
  await generateSpeech({
    model: gateway.speech('openai/tts-1'),
    text: 'Hello, world!',
  });
} catch (error) {
  if (NoSpeechGeneratedError.isInstance(error)) {
    console.log('AI_NoSpeechGeneratedError');
    console.log('Cause:', error.cause);
    console.log('Responses:', error.responses);
  }
}

本章小结

  • transcribe 统一音频转写入口:audio 支持 Uint8Array / ArrayBuffer / Buffer / base64 字符串 / URL,结果含 textsegmentslanguagedurationInSeconds
  • experimental_streamTranscribe 面向实时原始音频,fullStream 单次可消费,注意「先取 fullStream 再 await 结果」的顺序约束;
  • URL 音频下载默认 2 GiB 上限,可用 createDownload({ maxBytes }) 收紧或传入完全自定义的下载函数,超限抛 DownloadError
  • generateSpeech 用 speech model 把文本变语音,支持 voicelanguageproviderOptionsabortSignalheaders
  • 语音生成失败抛出 NoSpeechGeneratedError,用 isInstance 判定并读取 cause / responses

🛠️ 动手实践

  1. 找一段本地 mp3,用 transcribe + AI Gateway 转写,打印完整文本、语言代码和时长,并遍历 segments 输出每段起止时间。
  2. createDownload({ maxBytes }) 把下载上限收紧到 5 MB,然后对一个较大的远程音频发起转写,捕获 DownloadError 并打印错误信息。
  3. generateSpeech 把一段中文文案合成为语音(尝试不同 voice 取值),将 uint8Array 写入本地文件并用播放器试听对比效果。