playkit-sdk 1.4.0-beta.1 → 1.4.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,6 +68,33 @@ await chat.chatStream(
68
68
  );
69
69
  ```
70
70
 
71
+ ### Reasoning (Thinking)
72
+
73
+ Reasoning-capable models can think before answering. Enable it with the
74
+ `thinking` option (set the `effort` level) and read the model's reasoning
75
+ separately from its answer.
76
+
77
+ ```typescript
78
+ // Non-streaming: reasoning is returned on `result.reasoning`
79
+ const result = await chat.textGeneration({
80
+ messages: [{ role: 'user', content: 'Solve: 17 * 24, show your work.' }],
81
+ thinking: { effort: 'high' },
82
+ });
83
+
84
+ console.log('Answer:', result.content);
85
+ console.log('Reasoning:', result.reasoning);
86
+
87
+ // Streaming: reasoning arrives via the `onReasoning` callback,
88
+ // kept separate from the answer text in `onChunk`
89
+ await chat.textGenerationStream({
90
+ messages: [{ role: 'user', content: 'Solve: 17 * 24, show your work.' }],
91
+ thinking: { effort: 'high' },
92
+ onReasoning: (chunk) => process.stdout.write(`[thinking] ${chunk}`),
93
+ onChunk: (chunk) => process.stdout.write(chunk),
94
+ onComplete: (fullText) => console.log('\nComplete:', fullText),
95
+ });
96
+ ```
97
+
71
98
  ### Image Generation
72
99
 
73
100
  ```typescript
@@ -256,6 +283,10 @@ This SDK is proprietary software owned by Agentland Lab. Use of this SDK is subj
256
283
 
257
284
  ## Changelog
258
285
 
286
+ ### 1.4.0-beta.3
287
+ - Added `thinking` reasoning-effort option on chat (`thinking: { effort: 'high' }`)
288
+ - Surface model reasoning: `result.reasoning` (non-streaming) and the `onReasoning` callback (streaming)
289
+
259
290
  ### 1.0.0-beta.1
260
291
  - Initial public beta release
261
292
  - AI chat support (text generation)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * playkit-sdk v1.4.0-beta.1
2
+ * playkit-sdk v1.4.0-beta.3
3
3
  * PlayKit SDK for JavaScript
4
4
  * @license SEE LICENSE IN LICENSE
5
5
  */
@@ -830,7 +830,7 @@ class TokenStorage {
830
830
  }
831
831
 
832
832
  const SDK_TYPE = 'Javascript';
833
- const SDK_VERSION = '"1.4.0-beta.1"';
833
+ const SDK_VERSION = '"1.4.0-beta.3"';
834
834
  function getSDKHeaders() {
835
835
  return {
836
836
  'X-SDK-Type': SDK_TYPE,
@@ -3930,6 +3930,9 @@ class ChatProvider {
3930
3930
  stop: chatConfig.stop || null,
3931
3931
  top_p: chatConfig.topP || null,
3932
3932
  };
3933
+ if (chatConfig.thinking) {
3934
+ requestBody.thinking = chatConfig.thinking;
3935
+ }
3933
3936
  try {
3934
3937
  const response = await fetch(`${this.baseURL}${endpoint}`, {
3935
3938
  method: 'POST',
@@ -3987,6 +3990,9 @@ class ChatProvider {
3987
3990
  stop: chatConfig.stop || null,
3988
3991
  top_p: chatConfig.topP || null,
3989
3992
  };
3993
+ if (chatConfig.thinking) {
3994
+ requestBody.thinking = chatConfig.thinking;
3995
+ }
3990
3996
  try {
3991
3997
  const response = await fetch(`${this.baseURL}${endpoint}`, {
3992
3998
  method: 'POST',
@@ -4051,6 +4057,9 @@ class ChatProvider {
4051
4057
  if (chatConfig.tool_choice) {
4052
4058
  requestBody.tool_choice = chatConfig.tool_choice;
4053
4059
  }
4060
+ if (chatConfig.thinking) {
4061
+ requestBody.thinking = chatConfig.thinking;
4062
+ }
4054
4063
  try {
4055
4064
  const response = await fetch(`${this.baseURL}${endpoint}`, {
4056
4065
  method: 'POST',
@@ -4109,6 +4118,9 @@ class ChatProvider {
4109
4118
  if (chatConfig.tool_choice) {
4110
4119
  requestBody.tool_choice = chatConfig.tool_choice;
4111
4120
  }
4121
+ if (chatConfig.thinking) {
4122
+ requestBody.thinking = chatConfig.thinking;
4123
+ }
4112
4124
  try {
4113
4125
  const response = await fetch(`${this.baseURL}${endpoint}`, {
4114
4126
  method: 'POST',
@@ -4418,6 +4430,19 @@ class TranscriptionProvider {
4418
4430
  */
4419
4431
  // @ts-ignore - replaced at build time
4420
4432
  const DEFAULT_BASE_URL$1 = "https://api.playkit.ai";
4433
+ /** Decode a base64 string to an ArrayBuffer (browser + Node). */
4434
+ function base64ToArrayBuffer(b64) {
4435
+ if (typeof atob === 'function') {
4436
+ const bin = atob(b64);
4437
+ const bytes = new Uint8Array(bin.length);
4438
+ for (let i = 0; i < bin.length; i++)
4439
+ bytes[i] = bin.charCodeAt(i);
4440
+ return bytes.buffer;
4441
+ }
4442
+ // Node fallback
4443
+ const buf = globalThis.Buffer.from(b64, 'base64');
4444
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
4445
+ }
4421
4446
  class TTSProvider {
4422
4447
  constructor(authManager, config) {
4423
4448
  this.authManager = authManager;
@@ -4430,69 +4455,69 @@ class TTSProvider {
4430
4455
  setPlayerClient(playerClient) {
4431
4456
  this.playerClient = playerClient;
4432
4457
  }
4433
- /**
4434
- * Synthesize text into speech audio
4435
- */
4436
- async synthesize(ttsConfig) {
4437
- // Ensure token is valid, auto-refresh if needed (browser mode only)
4458
+ /** Build the shared request body from a TTS config (new fields + legacy). */
4459
+ buildRequestBody(ttsConfig) {
4460
+ const model = ttsConfig.model || this.config.defaultTTSModel || 'default-tts-model';
4461
+ const body = { model, text: ttsConfig.text };
4462
+ if (ttsConfig.voice !== undefined)
4463
+ body.voice = ttsConfig.voice;
4464
+ if (ttsConfig.voiceMix !== undefined)
4465
+ body.voice_mix = ttsConfig.voiceMix;
4466
+ if (ttsConfig.voiceSettings !== undefined) {
4467
+ body.voice_settings = ttsConfig.voiceSettings;
4468
+ }
4469
+ if (ttsConfig.outputFormat !== undefined) {
4470
+ body.output_format = ttsConfig.outputFormat;
4471
+ }
4472
+ if (ttsConfig.language !== undefined)
4473
+ body.language = ttsConfig.language;
4474
+ if (ttsConfig.providerOptions !== undefined) {
4475
+ body.provider_options = ttsConfig.providerOptions;
4476
+ }
4477
+ return body;
4478
+ }
4479
+ /** POST to a TTS endpoint; throws a PlayKitError on a non-ok response. */
4480
+ async post(endpoint, body) {
4438
4481
  await this.authManager.ensureValidToken();
4439
4482
  const token = this.authManager.getToken();
4440
4483
  if (!token) {
4441
4484
  throw new PlayKitError('Not authenticated', 'NOT_AUTHENTICATED');
4442
4485
  }
4443
- const model = ttsConfig.model || this.config.defaultTTSModel || 'default-tts-model';
4444
- const endpoint = `/ai/${this.config.gameId}/v2/audio/speech`;
4445
- const requestBody = {
4446
- model,
4447
- text: ttsConfig.text,
4448
- };
4449
- // Add optional parameters (only when defined)
4450
- if (ttsConfig.voice !== undefined) {
4451
- requestBody.voice = ttsConfig.voice;
4452
- }
4453
- if (ttsConfig.speed !== undefined) {
4454
- requestBody.speed = ttsConfig.speed;
4455
- }
4456
- if (ttsConfig.vol !== undefined) {
4457
- requestBody.vol = ttsConfig.vol;
4458
- }
4459
- if (ttsConfig.pitch !== undefined) {
4460
- requestBody.pitch = ttsConfig.pitch;
4461
- }
4462
- if (ttsConfig.emotion !== undefined) {
4463
- requestBody.emotion = ttsConfig.emotion;
4464
- }
4465
- if (ttsConfig.languageBoost !== undefined) {
4466
- requestBody.language_boost = ttsConfig.languageBoost;
4467
- }
4468
- if (ttsConfig.format !== undefined) {
4469
- requestBody.response_format = ttsConfig.format;
4470
- }
4471
- if (ttsConfig.voiceSetting !== undefined) {
4472
- requestBody.voice_setting = ttsConfig.voiceSetting;
4486
+ const response = await fetch(`${this.baseURL}${endpoint}`, {
4487
+ method: 'POST',
4488
+ headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
4489
+ body: JSON.stringify(body),
4490
+ });
4491
+ if (!response.ok) {
4492
+ const error = await response
4493
+ .json()
4494
+ .catch(() => ({ message: 'Speech synthesis failed' }));
4495
+ const playKitError = new PlayKitError(error.message || 'Speech synthesis failed', error.code, response.status);
4496
+ if (error.code === 'INSUFFICIENT_CREDITS' ||
4497
+ error.code === 'PLAYER_INSUFFICIENT_CREDIT' ||
4498
+ response.status === 402) {
4499
+ if (this.playerClient) {
4500
+ await this.playerClient.handleInsufficientCredits(playKitError);
4501
+ }
4502
+ }
4503
+ throw playKitError;
4473
4504
  }
4474
- if (ttsConfig.audioSetting !== undefined) {
4475
- requestBody.audio_setting = ttsConfig.audioSetting;
4505
+ return response;
4506
+ }
4507
+ checkBalanceAfter() {
4508
+ if (this.playerClient) {
4509
+ this.playerClient.checkBalanceAfterApiCall().catch(() => {
4510
+ /* silently fail */
4511
+ });
4476
4512
  }
4513
+ }
4514
+ /**
4515
+ * Synthesize text into speech audio (raw bytes).
4516
+ */
4517
+ async synthesize(ttsConfig) {
4518
+ const endpoint = `/ai/${this.config.gameId}/v2/audio/speech`;
4477
4519
  try {
4478
- const response = await fetch(`${this.baseURL}${endpoint}`, {
4479
- method: 'POST',
4480
- headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
4481
- body: JSON.stringify(requestBody),
4482
- });
4483
- if (!response.ok) {
4484
- const error = await response.json().catch(() => ({ message: 'Speech synthesis failed' }));
4485
- const playKitError = new PlayKitError(error.message || 'Speech synthesis failed', error.code, response.status);
4486
- // Check for insufficient credits error
4487
- if (error.code === 'INSUFFICIENT_CREDITS' ||
4488
- error.code === 'PLAYER_INSUFFICIENT_CREDIT' ||
4489
- response.status === 402) {
4490
- if (this.playerClient) {
4491
- await this.playerClient.handleInsufficientCredits(playKitError);
4492
- }
4493
- }
4494
- throw playKitError;
4495
- }
4520
+ const response = await this.post(endpoint, this.buildRequestBody(ttsConfig));
4496
4521
  // SUCCESS: response is raw audio bytes, NOT JSON.
4497
4522
  const audio = await response.arrayBuffer();
4498
4523
  const contentType = response.headers.get('Content-Type');
@@ -4500,24 +4525,66 @@ class TTSProvider {
4500
4525
  const audioLengthHeader = response.headers.get('X-Audio-Length-Ms');
4501
4526
  const result = {
4502
4527
  audio,
4503
- format: contentType || ttsConfig.format || 'mp3',
4528
+ format: contentType || 'mp3',
4504
4529
  usageCharacters: Number(usageHeader) || 0,
4505
4530
  };
4506
4531
  if (audioLengthHeader !== null) {
4507
4532
  result.audioLengthMs = Number(audioLengthHeader) || 0;
4508
4533
  }
4509
- // Check balance after successful API call
4510
- if (this.playerClient) {
4511
- this.playerClient.checkBalanceAfterApiCall().catch(() => {
4512
- // Silently fail
4513
- });
4514
- }
4534
+ this.checkBalanceAfter();
4515
4535
  return result;
4516
4536
  }
4517
4537
  catch (error) {
4518
- if (error instanceof PlayKitError) {
4538
+ if (error instanceof PlayKitError)
4519
4539
  throw error;
4540
+ throw new PlayKitError(error instanceof Error ? error.message : 'Unknown error', 'TTS_ERROR');
4541
+ }
4542
+ }
4543
+ /**
4544
+ * Synthesize text into speech AND return timestamp alignment. Hits the
4545
+ * `speech-with-timestamps` variant, whose success response is a JSON envelope
4546
+ * (base64 audio + alignment), so it is parsed as JSON — not raw bytes.
4547
+ */
4548
+ async synthesizeWithTimestamps(ttsConfig) {
4549
+ const endpoint = `/ai/${this.config.gameId}/v2/audio/speech-with-timestamps`;
4550
+ const body = this.buildRequestBody(ttsConfig);
4551
+ if (ttsConfig.granularity !== undefined) {
4552
+ body.subtitle_type = ttsConfig.granularity;
4553
+ }
4554
+ try {
4555
+ const response = await this.post(endpoint, body);
4556
+ const json = (await response.json());
4557
+ let alignment = null;
4558
+ if (json.alignment && Array.isArray(json.alignment.items)) {
4559
+ alignment = {
4560
+ granularity: json.alignment.granularity || 'word',
4561
+ items: json.alignment.items.map((it) => {
4562
+ var _a, _b, _c;
4563
+ return ({
4564
+ text: (_a = it.text) !== null && _a !== void 0 ? _a : '',
4565
+ startMs: (_b = it.start_ms) !== null && _b !== void 0 ? _b : 0,
4566
+ endMs: (_c = it.end_ms) !== null && _c !== void 0 ? _c : 0,
4567
+ textStart: it.text_start,
4568
+ textEnd: it.text_end,
4569
+ });
4570
+ }),
4571
+ };
4572
+ }
4573
+ const result = {
4574
+ audio: base64ToArrayBuffer(json.audio_base64),
4575
+ format: json.format || 'mp3',
4576
+ usageCharacters: Number(json.usage_characters) || 0,
4577
+ alignment,
4578
+ };
4579
+ if (json.audio_length_ms != null) {
4580
+ result.audioLengthMs = Number(json.audio_length_ms) || 0;
4520
4581
  }
4582
+ this.checkBalanceAfter();
4583
+ return result;
4584
+ }
4585
+ catch (error) {
4586
+ if (error instanceof PlayKitError)
4587
+ throw error;
4521
4588
  throw new PlayKitError(error instanceof Error ? error.message : 'Unknown error', 'TTS_ERROR');
4522
4589
  }
4523
4590
  }
@@ -4601,6 +4668,7 @@ function createDecoder() {
4601
4668
  class StreamParser {
4602
4669
  /**
4603
4670
  * Parse SSE stream using ReadableStream
4671
+ * Yields typed parts so callers can separate text from reasoning.
4604
4672
  */
4605
4673
  static parseStream(reader) {
4606
4674
  return __asyncGenerator(this, arguments, function* parseStream_1() {
@@ -4631,9 +4699,9 @@ class StreamParser {
4631
4699
  const data = trimmed.substring(6); // Remove 'data: ' prefix
4632
4700
  try {
4633
4701
  const parsed = JSON.parse(data);
4634
- const text = this.extractTextFromChunk(parsed);
4635
- if (text) {
4636
- yield yield __await(text);
4702
+ const part = this.extractPartFromChunk(parsed);
4703
+ if (part) {
4704
+ yield yield __await(part);
4637
4705
  }
4638
4706
  // Stream termination events
4639
4707
  if (parsed.type === 'done' || parsed.type === 'finish' || parsed.finish_reason) {
@@ -4650,7 +4718,7 @@ class StreamParser {
4650
4718
  }
4651
4719
  catch (error) {
4652
4720
  // If JSON parse fails, treat as plain text
4653
- yield yield __await(data);
4721
+ yield yield __await({ kind: 'text', delta: data });
4654
4722
  }
4655
4723
  }
4656
4724
  }
@@ -4662,22 +4730,33 @@ class StreamParser {
4662
4730
  });
4663
4731
  }
4664
4732
  /**
4665
- * Extract text from a stream chunk
4666
- * Supports multiple formats (UI Message Stream and OpenAI)
4733
+ * Extract a typed part (text or reasoning) from a stream chunk
4734
+ * Supports multiple formats (UI Message Stream and OpenAI).
4735
+ * Reasoning is detected before the generic text fallback so thinking
4736
+ * deltas never leak into the text stream.
4667
4737
  */
4668
- static extractTextFromChunk(chunk) {
4669
- var _a, _b;
4670
- // UI Message Stream format: { type: "text-delta", delta: "..." }
4738
+ static extractPartFromChunk(chunk) {
4739
+ var _a, _b, _c, _d;
4740
+ // UI Message Stream reasoning: { type: "reasoning-delta", delta: "..." }
4741
+ if (chunk.type === 'reasoning-delta' && chunk.delta) {
4742
+ return { kind: 'reasoning', delta: chunk.delta };
4743
+ }
4744
+ // UI Message Stream text: { type: "text-delta", delta: "..." }
4671
4745
  if (chunk.type === 'text-delta' && chunk.delta) {
4672
- return chunk.delta;
4746
+ return { kind: 'text', delta: chunk.delta };
4747
+ }
4748
+ // OpenAI reasoning (defensive): { choices: [{ delta: { reasoning_content: "..." } }] }
4749
+ if (chunk.choices && ((_b = (_a = chunk.choices[0]) === null || _a === void 0 ? void 0 : _a.delta) === null || _b === void 0 ? void 0 : _b.reasoning_content)) {
4750
+ return { kind: 'reasoning', delta: chunk.choices[0].delta.reasoning_content };
4673
4751
  }
4674
- // OpenAI format: { choices: [{ delta: { content: "..." } }] }
4675
- if (chunk.choices && ((_b = (_a = chunk.choices[0]) === null || _a === void 0 ? void 0 : _a.delta) === null || _b === void 0 ? void 0 : _b.content)) {
4676
- return chunk.choices[0].delta.content;
4752
+ // OpenAI text: { choices: [{ delta: { content: "..." } }] }
4753
+ if (chunk.choices && ((_d = (_c = chunk.choices[0]) === null || _c === void 0 ? void 0 : _c.delta) === null || _d === void 0 ? void 0 : _d.content)) {
4754
+ return { kind: 'text', delta: chunk.choices[0].delta.content };
4677
4755
  }
4678
- // Direct delta format
4756
+ // Direct delta format (text)
4679
4757
  if (chunk.delta) {
4680
- return typeof chunk.delta === 'string' ? chunk.delta : chunk.delta.content || null;
4758
+ const text = typeof chunk.delta === 'string' ? chunk.delta : chunk.delta.content || null;
4759
+ return text ? { kind: 'text', delta: text } : null;
4681
4760
  }
4682
4761
  return null;
4683
4762
  }
@@ -4691,8 +4770,10 @@ class StreamParser {
4691
4770
  for (var _d = true, _e = __asyncValues(this.parseStream(reader)), _f; _f = await _e.next(), _a = _f.done, !_a; _d = true) {
4692
4771
  _c = _f.value;
4693
4772
  _d = false;
4694
- const chunk = _c;
4695
- fullText += chunk;
4773
+ const part = _c;
4774
+ if (part.kind === 'text') {
4775
+ fullText += part.delta;
4776
+ }
4696
4777
  }
4697
4778
  }
4698
4779
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
@@ -4706,8 +4787,9 @@ class StreamParser {
4706
4787
  }
4707
4788
  /**
4708
4789
  * Stream with callbacks
4790
+ * Text deltas go to onChunk; reasoning (thinking) deltas go to onReasoning.
4709
4791
  */
4710
- static async streamWithCallbacks(reader, onChunk, onComplete, onError) {
4792
+ static async streamWithCallbacks(reader, onChunk, onComplete, onError, onReasoning) {
4711
4793
  var _a, e_2, _b, _c;
4712
4794
  let fullText = '';
4713
4795
  try {
@@ -4715,9 +4797,15 @@ class StreamParser {
4715
4797
  for (var _d = true, _e = __asyncValues(this.parseStream(reader)), _f; _f = await _e.next(), _a = _f.done, !_a; _d = true) {
4716
4798
  _c = _f.value;
4717
4799
  _d = false;
4718
- const chunk = _c;
4719
- fullText += chunk;
4720
- onChunk(chunk);
4800
+ const part = _c;
4801
+ if (part.kind === 'reasoning') {
4802
+ if (onReasoning) {
4803
+ onReasoning(part.delta);
4804
+ }
4805
+ continue;
4806
+ }
4807
+ fullText += part.delta;
4808
+ onChunk(part.delta);
4721
4809
  }
4722
4810
  }
4723
4811
  catch (e_2_1) { e_2 = { error: e_2_1 }; }
@@ -4818,6 +4906,7 @@ class ChatClient {
4818
4906
  : undefined,
4819
4907
  id: response.id,
4820
4908
  created: response.created,
4909
+ reasoning: choice.message.reasoning_content,
4821
4910
  };
4822
4911
  }
4823
4912
  /**
@@ -4826,7 +4915,7 @@ class ChatClient {
4826
4915
  async textGenerationStream(config) {
4827
4916
  const chatConfig = Object.assign(Object.assign({}, config), { model: config.model || this.model });
4828
4917
  const reader = await this.provider.chatCompletionStream(chatConfig);
4829
- await StreamParser.streamWithCallbacks(reader, config.onChunk, config.onComplete, config.onError);
4918
+ await StreamParser.streamWithCallbacks(reader, config.onChunk, config.onComplete, config.onError, config.onReasoning);
4830
4919
  }
4831
4920
  // ===== Structured Output Generation =====
4832
4921
  /**
@@ -4984,6 +5073,7 @@ class ChatClient {
4984
5073
  id: response.id,
4985
5074
  created: response.created,
4986
5075
  tool_calls: choice.message.tool_calls,
5076
+ reasoning: choice.message.reasoning_content,
4987
5077
  };
4988
5078
  }
4989
5079
  /**
@@ -4994,6 +5084,7 @@ class ChatClient {
4994
5084
  const chatConfig = Object.assign(Object.assign({}, config), { model: config.model || this.model });
4995
5085
  const reader = await this.provider.chatCompletionWithToolsStream(chatConfig);
4996
5086
  let fullContent = '';
5087
+ let fullReasoning = '';
4997
5088
  let toolCalls = [];
4998
5089
  await StreamParser.streamWithCallbacks(reader, (chunk) => {
4999
5090
  fullContent += chunk;
@@ -5006,9 +5097,14 @@ class ChatClient {
5006
5097
  model: chatConfig.model || this.model,
5007
5098
  finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
5008
5099
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
5100
+ reasoning: fullReasoning || undefined,
5009
5101
  });
5010
5102
  }
5011
- }, config.onError);
5103
+ }, config.onError, (chunk) => {
5104
+ var _a;
5105
+ fullReasoning += chunk;
5106
+ (_a = config.onReasoning) === null || _a === void 0 ? void 0 : _a.call(config, chunk);
5107
+ });
5012
5108
  }
5013
5109
  }
5014
5110
 
@@ -5236,6 +5332,14 @@ class TTSClient {
5236
5332
  async synthesize(config) {
5237
5333
  return this.provider.synthesize(Object.assign(Object.assign({}, config), { model: config.model || this.model }));
5238
5334
  }
5335
+ /**
5336
+ * Synthesize text into speech AND return timestamp alignment (word/sentence
5337
+ * timings). Returns the audio bytes plus an `alignment` object.
5338
+ * @param config - TTS configuration; `granularity` defaults to 'word'.
5339
+ */
5340
+ async synthesizeWithTimestamps(config) {
5341
+ return this.provider.synthesizeWithTimestamps(Object.assign(Object.assign({}, config), { model: config.model || this.model }));
5342
+ }
5239
5343
  /**
5240
5344
  * Synthesize text into speech and return it as a Blob (browser-friendly)
5241
5345
  * @param config - Full TTS configuration