@breeze.blue / sdk
微风蓝开发者API的ESM-first TypeScript SDK。涵盖了 文本转语音,语音管理,语音预览生成,历史音频, 模型和帐户使用情况。
在Node、浏览器和edge运行时中导入根入口点是安全的。当地的
音频回放助手只存在于node中 @breeze.blue/sdk/node 子路径。
TTS指导接受1-10;如果省略,则使用默认的1。声音设计接受1-10分;省略指导或传递空样本均匀在1-4之间为每个预览,四舍五入到一个小数点。HTTP速度和容量按每个请求计算,默认为1。使用 voiceSettings: { useSpeakerBoost: true } 为了与参考声音更接近(默认为false):非空指令和大于1的指导被忽略,否则指导被应用为1;指令总是被应用。实时修复了会话创建时的增强;当boost应用于上面1所请求的指导时,创建一个新会话来添加指令。通过 removeBackgroundNoise: true 对克隆样本进行降噪(默认为false)。
安装
pnpm add @breeze.blue/sdk
或者:
npm install @breeze.blue/sdk
API密匙
在Breeze Blue Developer Console中创建一个API密钥,然后导出它:
export BREEZE_API_KEY=brz_...
SDK发送密钥时使用 xi-api-key 头。它还发送
x-breeze-sdk 对于服务器端的可观察性。
每个REST方法都需要API密钥。唯一的例外是
textToSpeech.realtime.connect(...) 与一个 clientSecret,这让
浏览器客户端在没有密钥的情况下运行(请参阅下面的实时部分)。
快速入门
import { BreezeBlueClient } from "@breeze.blue/sdk";
import { save } from "@breeze.blue/sdk/node";
const client = new BreezeBlueClient();
const audio = await client.textToSpeech.convert(
"voc_...",
{ text: "Hello from Breeze Blue." },
{ outputFormat: "mp3" },
);
await save(audio, "hello.mp3");
console.log(audio.contentType);
console.log(audio.historyItemId);
new BreezeBlueClient() 读取 BREEZE_API_KEY 默认情况下,并将请求发送到
https://api.breeze.blue. 指向另一个环境,pass baseUrl 或者
集 BREEZE_BASE_URL.
const client = new BreezeBlueClient({
apiKey: "brz_...",
baseUrl: "https://api.breeze.blue",
timeout: 120_000,
});
每个请求选项接受 timeout, signal,以及额外的 headers.
文本到语音
const audio = await client.textToSpeech.convert("voc_...", {
text: "Render this line.",
});
const audioStream = await client.textToSpeech.stream("voc_...", {
text: "Stream this line.",
});
const enhanced = await client.textToSpeech.enhance({
instruction: "Calm, warm, bedtime narration.",
languageCode: "en",
});
对于长文本、大量引用的语音或批处理,使用异步文本到语音 生产中,调用者不应该保持HTTP连接打开:
const job = await client.textToSpeech.createJob(
"voc_...",
{ text: "Render this longer script." },
{ outputFormat: "mp3" },
);
const status = await client.generationJobs.get(job.generationJobId);
if (status.status === "ready") {
const audio = await client.generationJobs.downloadAudio(job.generationJobId);
await save(audio, "async.mp3");
}
如果作业仍处于活动状态, downloadAudio(...) 拒绝与
BreezeBlueGenerationNotReadyError;读 error.retryAfter 之前进行重试。
当一个WebSocket连接应该处理多个连接时,使用实时文本到语音
谈话。实时支持PCM(默认)或MP3在24千赫单声道。
PCM使用原始签名的16位小端采样。通过 outputFormat: "mp3" 来
realtime.createSession, realtime.connect,或者 realtime.connectManaged 选择“MP3”。
每个回合最后的MP3字节到达之前 turn.done.
实时需要 WebSocket 实现。浏览器、边缘运行时和
Node 22+提供了一个全局的;在Node 20上,传递一个构造函数(例如
ws 包的 WebSocket)通过 new BreezeBlueClient({ webSocket }).
在添加文本之前开始消费。音频可以在 flush() 和5月
后继续 endTurn();保持单个消费者运行,直到 turn.done.
const connection = await client.textToSpeech.realtime.connect("voc_...", {
modelId: "breeze-tts-2",
});
const pcmChunks: Uint8Array[] = [];
const consumer = (async () => {
for await (const message of connection) {
if (message.type === "audio") {
// Forward this chunk to your playback or transport layer immediately.
pcmChunks.push(message.audio);
} else if (message.type === "error") {
throw new Error(`Realtime TTS failed: ${message.code}: ${message.message}`);
} else if (message.type === "turn.done") {
return;
}
}
throw new Error("Realtime session closed before turn.done");
})();
connection.startTurn("turn_1");
connection.appendText("Hello from Breeze.");
connection.flush();
connection.endTurn();
await consumer;
connection.close();
始终使用连接(或其属性) audio() / events() 迭代器)
回合是活跃的,呼叫 close() 当你做完的时候。当SDK检测到
连接失败-无效的服务器帧,或超过16 MiB的消息
堆积未消耗的连接——关闭连接,迭代器拒绝使用
一个 BreezeBlueRealtimeError。这个 audio() Helper也会抛出
BreezeBlueRealtimeError 对于服务器 error 事件和异常WebSocket
关闭而不是无声地结束。
当 BreezeBlueRealtimeError.recoverable 是 true,同样的连接
仍然是可用的。大多数可恢复的转弯命令错误取消了活动的转弯。
一个active-turn session.update 拒绝是例外:turn keep
运行。如果 connection.audio() 抛出错误,重新开始 audio()
迭代器立即在同一连接上继续收集
当前的转变;不要开始替换转弯。
事件是一个类型区分联合(session.ready, session.updated, turn.started,
audio.started, turn.done, turn.cancelled, usage.committed,
session.expiring, session.closed, error, pong),还有骆驼的田野
如 turnId, historyItemId, expiresAt, ttfaMs. 把工会看作
非穷举:服务器可能添加事件类型,SDK交付未知
JSON事件不变——忽略你不认识的事件类型
切换详尽。
在回合之间,更新合成指令而不替换
WebSocket。 updateInstructions(...) 只发送请求;匹配的
session.updated 确认仍然在同一个有序迭代器上。等待
在开始下一个转弯之前:
connection.updateInstructions("Speak faster and with more energy.");
for await (const message of connection) {
if (message.type === "session.updated") {
if (message.instructions !== "Speak faster and with more energy.") {
throw new Error("Unexpected instructions acknowledgement");
}
break;
}
}
connection.startTurn("turn_2");
指令必须为不超过1000个字符的非空字符串。只有一个
更新可能等待确认,并且只有在没有轮询时才接受更新
是活跃的。在一个回合中尝试的更新将被拒绝而不会中断
这把。保持一个流消费者:如果一个长寿命的消费者拥有
迭代器,让它在 session.updated 到达
而不是开始第二个迭代器。
在会话内部发送一个keepalive ping inactivityTimeoutSeconds 窗口;
服务器返回一个 pong 事件。在主动转弯时也要保持运行。
没有音频的长TTFA或上游失速不会暂停空闲截止日期。
const keepalive = setInterval(() => connection.ping(), 10_000);
// ... run turns ...
clearInterval(keepalive);
connection.close();
对于长逻辑会话,请使用可选择的托管连接。它源于
从的安全存活间隔 session.ready 并且需要服务器响应
在每次ping之后。丢失的确认将替换一个空闲的半打开的套接字;
如果一个回合处于活动状态,它就会报告 TURN_INTERRUPTED 而且从不重放命令。
之后,管理器还会标记一个物理WebSocket以进行回合边界旋转
默认情况下为10分钟(或在服务器截止日期要求时更早)。一个
主动转向可以延迟超过该阈值的切换;的server-advertised
艰苦的一生依然适用。经理
每个中断执行最多三次意外的空闲重新连接尝试
新鲜的会话和紧张的指数回退:
const connection = await client.textToSpeech.realtime.connectManaged("voc_...", {
modelId: "breeze-tts-2",
});
const consumer = (async () => {
for await (const message of connection) {
if (message.type === "audio") {
// Play or forward this PCM chunk immediately.
} else if (message.type === "session.ready") {
// A new physical epoch is ready; the logical connection remains the same.
} else if (message.type === "turn.done") {
return;
}
}
})();
await connection.startTurnWhenReady("turn_1");
connection.appendText("Hello from a long-running conversation.");
connection.endTurn();
await consumer;
connection.close();
使用 heartbeatTimeoutMs 调整确认截止日期和
maxPhysicalSessionMs 来调优客户端epoch上限
maxPhysicalSessionMs: 0 只有当你有意想要独自依靠的时候
服务器发布的截止日期。
当空闲的重新连接被成功处理时,逻辑迭代器将保留
打开,抑制被替换的物理套接字的终端事件,并发出
新时代的 session.ready. 替换设备发送第一次心跳
立即并在入站确认后恢复派生的节奏。
每次中断都限制重新连接尝试。 startTurnWhenReady(...)
只在这些空闲替换中的一个已经在进行时等待
turn.start 没有被派遣;在发送任何其他回合命令之前等待它。
经过一次有计划的轮转,一个具有突出意义的旧时代 usage.committed
事件最多并行消耗5秒,并在所有事件结束后立即关闭
已知已完成的回合。这个活动是尽最大努力的;使用历史和用法
api是真理的持久来源。
托管连接从不缓冲转内容或重放命令。这个
有界的 startTurnWhenReady(...) wait发生在它的第一个WebSocket之前
写。如果WebSocket在活动时被中断,它的迭代器拒绝使用
BreezeBlueRealtimeError 和 error.code === "TURN_INTERRUPTED";决定从
您的应用程序会话说明是否以及如何开始一个新的回合。
将经理视为一个活动的实时呼叫,而不是一个状态通道。总是
呼叫 connection.close() 当通话结束时,用户离开,或离开页面
进入一个长期存在的背景状态;否则它的心跳是故意的
占用服务器WebSocket插槽。
为了缩短第一个音频的时间,可以提前创建会话(例如,在第一个音频出现时)
你的应用程序还在准备转弯,并与其连接 clientSecret 当
第一个文本已经准备好了-只剩下WebSocket握手:
const session = await client.textToSpeech.realtime.createSession("voc_...", {
modelId: "breeze-tts-2",
});
// Later, when the first text is ready:
const connection = await client.textToSpeech.realtime.connect("voc_...", {
clientSecret: session.clientSecret,
websocketUrl: session.websocketUrl,
directWebsocketUrl: session.directWebsocketUrl,
});
当会话响应包含 directWebsocketUrl, SDK更喜欢它的
无需查询的来源,并使用WebSocket子协议进行身份验证。它落
回 websocketUrl 对于未启用直接传输的服务。
相同的 clientSecret 切换让浏览器连接而不需要看到你的
API密钥:手动在服务器上创建会话 session.clientSecret 到
页面,并在那里构建一个无密钥客户端:
// Browser — no API key required for clientSecret connections.
const browserClient = new BreezeBlueClient();
const connection = await browserClient.textToSpeech.realtime.connect("voc_...", {
clientSecret, // received from your server
});
对于托管浏览器连接,提供 sessionFactory. SDK调用它
对于每个物理纪元,所以回调必须从
您的后端而不是缓存第一个响应。会话配置属于
在使用自定义工厂的后端请求中:
import { BreezeBlueRealtimeError } from "@breeze.blue/sdk";
const connection = await browserClient.textToSpeech.realtime.connectManaged("voc_...", {
sessionFactory: async ({ signal }) => {
const response = await fetch("/api/breeze-realtime-session", {
method: "POST",
signal,
});
if (!response.ok) {
const reconnect =
[408, 425, 429].includes(response.status) || response.status >= 500;
throw new BreezeBlueRealtimeError("Could not create realtime session", {
code: "SESSION_FACTORY_ERROR",
reconnect,
});
}
return response.json(); // { clientSecret, websocketUrl?, directWebsocketUrl? }
},
});
尊重工厂 signal:当逻辑连接关闭时,SDK将终止它
或者物理纪元超时过期。的自定义回调
信号仍然必须强制它自己的有界请求超时。
在收到动态指令更新后 session.updated,托管SDK
将确认的值传递到以后的物理时代。SDK-created替代品
将其包含在会话请求中;工厂创建的替代品接收一个
自动 session.update 在下一个之前 session.ready 暴露在
逻辑迭代器。
modelId, languageCode, voiceSettings, inactivityTimeoutSeconds,
enableLogging 在创建会话时固定。最初的
instructions 价值也在创建时提供,但可以稍后提供
匝间替换为 updateInstructions(...). connect 忽略了
初始会话选项 clientSecret 或者 websocketUrl 提供和
记录一个警告。
如果实时WebSocket因网络变化而中断,服务部署,
或者上游实时工作者重启, connectManaged(...) 处理有界
只有在没有主动回合时才会重新连接。与较低层次的
connect(...)、处理 error.meta.reconnect === true 或者一个 session.closed
事件与 reconnect === true 通过创建新会话并启动新会话
离开你自己的谈话状态。主动回合永远不会原地恢复。
在托管重连期间,瞬态会话创建响应(408,425,429,
和5xx)共享相同的有界重连预算;其他4xx响应失败
逻辑连接立即。一个自定义 sessionFactory 应该保留它
与的区别 new BreezeBlueRealtimeError(message, { reconnect });一个普通
Error 不能通信终端策略响应,并被视为
在同一有限预算内的瞬态。 GENERATION_CAPACITY_EXCEEDED 是
轮作用域:等待以下内容 turn.cancelled,停止使用
meta.retryAfterSeconds,并在相同的托管逻辑上开始新的打开
不回放文本的连接。
API在以下情况下使用默认的文本到语音模型 modelId 是省略了。如果
您需要显式地选择一个模型,调用 client.models.list() 然后通过一个
归来的人 modelId 值。
所有字段名都遵循TypeScript约定(modelId, voiceSettings,
outputFormat, historyItemId,……)。SDK将它们转换为
snake_case发送时的HTTP连线格式,并将JSON响应转换回
骆驼箱子收到了。
音频响应暴露了方便的助手:
await audio.arrayBuffer();
await audio.bytes();
await audio.blob();
audio.contentType;
audio.historyItemId;
节点回放帮助程序:
import { play, save, stream } from "@breeze.blue/sdk/node";
await play(audio); // ffplay, with macOS afplay fallback
await save(audio, "x.mp3");
await stream(audioStream); // mpv
流式文本到语音默认为 pcm 减少到第一个音频的时间。通过
{ outputFormat: "wav" } 或者 { outputFormat: "mp3" } 当你需要电线的时候
显式格式。
同步和异步文本到语音也采用采样率,对于有损编码,采用比特率,例如
{ outputFormat: "wav_24000" }. 携带采样率的配置文件适用于同步和异步转换;流使用赤裸的编码名称。
的声音
搜索该账户可用的所有声音,并检查单个声音。公共 目录结果将语义相似性与精确、前缀和子字符串结合起来 名称匹配;保存的个人声音仍然可以通过姓名,描述, 或语音识别。
const voices = await client.voices.search({ search: "calm documentary narrator" });
const newestFirst = await client.voices.search();
const dailyTrend = await client.voices.search({
voiceType: "default",
sort: "trend",
languageCode: "zh",
gender: ["female", "neutral"],
tone: ["warm"],
});
const taggedFavorites = await client.voices.search({
favoritesOnly: true,
tags: ["narration", "novel=三体"],
});
const firstVoiceId = voices.voices[0].voiceId;
const voice = await client.voices.get(firstVoiceId);
const randomVoice = await client.voices.random();
console.log(randomVoice.voiceId, randomVoice.name);
// Narrow the random pool by language and/or voice source.
const randomCatalogVoice = await client.voices.random({ languageCode: "zh", voiceType: "default" });
// Discover the current code-only Voice Metadata contract before building a form.
const metadataOptions = await client.voices.metadataOptions();
console.log(metadataOptions.languageCodes);
console.log(metadataOptions.accentCodesByLanguage.en);
语音列表保持已发布的默认稳定创建时间顺序
从最新到最老。使用 { sort: "trend", voiceType: "default" } 对于共享的
每日趋势订单。尚未排名的合格声音跟随快照成员;
返回的页令牌固定初始补充候选。搜索查询保持相关度排名;趋势只是一种
同一关联层中的弱先验。
微风的声音创作总是两个步骤:产生一个 预览,让…… 用户接受它,然后 保存预览 作为一个真实的声音。三种方法 制作预览:
克隆样本接受MP3, WAV, M4A, FLAC, AAC, OGG, Opus, WebM音频,WMA,
或AIFF,至少3秒,最大5mb。克隆调用是同步的。
提供匹配 filename 或者 contentType 上传。
import { readFile } from "node:fs/promises";
// Option A — clone preview from an audio sample
const clonePreview = await client.voices.createClonePreview({
name: "Demo voice",
file: {
data: await readFile("sample.wav"),
filename: "sample.wav",
contentType: "audio/wav",
},
});
let generatedVoiceId = clonePreview.generatedVoiceId;
// Option B — design preview from a text description (no audio)
const design = await client.voices.createDesignPreview({
voiceDescription: "Warm documentary narrator with clear articulation.",
});
generatedVoiceId = design.previews[0].generatedVoiceId;
选项C保留现有的声音,并使其说另一种语言。
本地化作为后台作业运行:启动它,然后轮询,直到状态为
ready. name 默认为源语音名。
const job = await client.voices.createLocalizePreview({
voiceId: firstVoiceId,
languageCode: "es",
name: "Documentary narrator (Spanish)",
});
let localized = await client.voices.getLocalizePreview(job.generationJobId);
while (localized.status !== "ready") {
if (localized.status === "failed" || localized.status === "cancelled") {
throw new Error(localized.error?.detail ?? localized.status);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
localized = await client.voices.getLocalizePreview(job.generationJobId);
}
generatedVoiceId = localized.generatedVoiceId!;
Voice Remix从拥有的或官方的声音中创建一个相同语言的候选人 以及自定义描述或预设。每个成功的候选人都使用语音设计候选人 价格。源不变。
const remixJob = await client.voices.createRemixPreview({
voiceId: firstVoiceId,
prompt: "Make the voice warmer and more expressive.",
guidanceScale: 4,
});
let remix = await client.voices.getRemixPreview(remixJob.generationJobId);
while (remix.status !== "ready") {
if (remix.status === "failed" || remix.status === "cancelled") {
throw new Error(remix.error?.detail ?? remix.status);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
remix = await client.voices.getRemixPreview(remixJob.generationJobId);
}
generatedVoiceId = remix.generatedVoiceId!;
使用 streamPreview 和 savePreview 使用生成的ID进行试听和保存
私人的、独立的声音。客户端超时使服务器作业继续运行。
自定义输入被增强为源代码中的指令和匹配脚本
语言。列出模板 client.voices.listRemixPresets() 并通过
presetId 而不是 prompt 用一个。编辑模板得到增强;
只有当源语言不同时,才会翻译未更改的模板。
要比较优势,打电话 client.voices.prepareRemixPreview(...) 第一次和
通过它 preparationToken 每个具有相同输入的create调用。令牌
持续24小时。数值2 / 4 / 6 / 8代表稳定/富有表现力/创造性/强烈;
4为默认值。Web Auto比较4、6和8的候选项。
使用 options.headers["Idempotency-Key"] 要安全地重试不确定的提交,
每个候选人都有一个不同的键。状态包括 comparisonAudioUrl,
comparisonText, sourceVoice. 节省不 voiceName 或者
voiceDescription 使用源值。
看到 语音混音指南.
对于最快的第一个音频,生成一个设计预览作为一个活24千赫单声道
PCM的回应。的节点 stream() Helper直接使用响应体;
干净的结束方式 generatedVoiceId 准备保存:
import { stream } from "@breeze.blue/sdk/node";
const livePreview = await client.voices.streamDesignPreview({
voiceDescription: "Warm documentary narrator with clear articulation.",
text: "This is a short preview script.",
});
await stream(livePreview);
generatedVoiceId = livePreview.generatedVoiceId!;
files 也只接受一个项目。
下载一个完整的预览,以便用户可以试听,然后保存他们的一个 选择:
const audio = await client.voices.streamPreview(generatedVoiceId);
await save(audio, "preview.mp3");
const saved = await client.voices.savePreview({
generatedVoiceId,
voiceName: "Documentary narrator",
languageCode: "en",
gender: "neutral",
age: "middle_aged",
tone: ["warm", "articulate"],
accent: "american",
tags: ["narration", "novel=三体"],
});
保存预览对您的帐户是私有的。编辑语音的元数据可以保持其可见性。
编辑已保存语音的名称、描述、标签、语音元数据或删除:
await client.voices.edit(saved.voiceId, {
name: "Renamed narrator",
languageCode: "en",
tone: ["calm", "measured"],
});
// null clears nullable metadata; [] clears tone. Omitted fields remain unchanged.
await client.voices.edit(saved.voiceId, { accent: null, tone: [] });
await client.voices.updateTags(firstVoiceId, ["narration", "novel=三体"]);
await client.voices.updateTags(firstVoiceId, []); // Resume owner tag inheritance.
await client.voices.delete(saved.voiceId);
语音元数据使用稳定的代码值。 gender, age, accent 可能是
null; tone 包含最多三个不同的代码。中英文用法
不同的重音码集,与其他语言要求不同 accent: null.
呼叫 client.voices.metadataOptions() 去发现每一个被接受的代码和
运行时的语言到口音级联。
历史、模型和账目
const models = await client.models.list();
const balance = await client.account.balance();
const entitlements = await client.account.entitlements();
console.log(entitlements.planCode, entitlements.maxConcurrentGenerations);
const currentKey = (await client.account.currentApiKey()).apiKey;
console.log(currentKey.status, currentKey.creditRemaining);
const usage = await client.account.usage({ days: 7 });
const keyUsage = await client.account.usage({
apiKeyId: "key_01hprod",
clientType: "sdk",
});
const history = await client.history.list({ pageSize: 10 });
const item = await client.history.get(history.history[0].historyItemId);
const audio = await client.history.downloadAudio(item.historyItemId);
错误
API失败抛出 BreezeBlueAPIError 子类:
BreezeBlueAuthenticationErrorBreezeBlueBadRequestErrorBreezeBlueConflictErrorBreezeBlueForbiddenErrorBreezeBlueNotFoundErrorBreezeBlueValidationErrorBreezeBlueRateLimitErrorBreezeBlueInsufficientCreditsErrorBreezeBlueUpstreamError对于上游发电/存储/模型故障, 常见的502/504BreezeBlueServiceUnavailableError容量或服务配置 失败,通常是503次
import { BreezeBlueClient, BreezeBlueRateLimitError } from "@breeze.blue/sdk";
try {
await new BreezeBlueClient().models.list();
} catch (error) {
if (error instanceof BreezeBlueRateLimitError) {
console.log(error.headers.get("retry-after"));
}
}
每个API错误都会暴露 status, code, detail, meta, headers.
克隆预览会自动用检测到的参考音频语言生成一个简短的脚本。它们接受参考样本、名称和可选的描述。将预览保存为语音,然后对自定义脚本、说明或目标语言使用“文本到语音”。
HTTP TTS速度
通过 voiceSettings: { speed: 1.25 } 在不改变音高的情况下控制语速,从0.5到2.0。遗漏总是意味着1.0。同步、异步和HTTP流支持此参数;而实时则不然。
通过 voiceSettings: { volume: 1.5 } 应用请求级线性幅度乘法器。取值范围从0.01到2.0;遗漏意味着1.0。高于1.0的值可能会削去峰值。同步、异步和HTTP流支持此参数;而实时则不然。
词时机
流音频和单词/令牌时间戳 client.textToSpeech.streamWithTimestamps(...)。使用 for await 使用包含以下内容的块 audioBase64 和 wordTimestamps;中断迭代将关闭流。分别解码音频并合并重复的单词索引。看到 演讲时间 获取完整的示例和计时语义。
要获得完整的响应,请使用 client.textToSpeech.convertWithTimestamps(...). 它返回base64音频、其内容类型和完整的单词/令牌时间戳列表。在保存或播放之前解码音频。看到 演讲时间 和 使用时间戳进行转换.
对于带有时序的后台生成,使用 client.textToSpeech.createJobWithTimestamps(...). 调查与 client.generationJobs.get(jobId);现成的工作包括 wordTimestamps 以及音频下载网址。
克隆预览创建接受省略的名称。服务器使用文件名干或 Cloned Voice;保存没有名称的克隆预览将保留其预览名称。其他预览类型仍然需要保存名称。长度超过80个Unicode字符的显式名称将被拒绝。
工作室管理
client.studio 公开 projects, chapters, clips, tracks, instances, files. 导入已有语音id的脚本,编辑继承的指令和静音,上传MP3/WAV素材,整理背景音频。项目写入需要最新的编辑令牌和UUID操作ID。冲突保留错误元数据,不会自动重试。
看到 工作室初始化指南 对于一个完整的工作流。签名上传使用单独的请求而不需要API密钥。管理操作不生成语音或导出项目。
client.studio.files.upload(blob, { operationId, name, projectId }) 创建文件记录,上传到签名URL,完成上传并等待 ready;失败拒绝 BreezeBlueStudioFileError. 时间线位置、修剪和长度是48 kHz帧;使用 secondsToFrames 和 framesToSeconds 转换。