@elizaos/capacitor-talkmode 1.0.0 → 2.0.3-beta.2

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/dist/plugin.js CHANGED
@@ -50,8 +50,13 @@ var capacitorTalkMode = (function (exports, core) {
50
50
  this.recognition.interimResults = true;
51
51
  this.recognition.onresult = (event) => {
52
52
  const result = event.results[event.results.length - 1];
53
- const transcript = result[0].transcript;
53
+ const first = result?.[0];
54
+ if (!first || typeof first.transcript !== "string")
55
+ return;
56
+ const transcript = first.transcript;
54
57
  const isFinal = result.isFinal;
58
+ if (!transcript.trim())
59
+ return;
55
60
  this.notifyListeners("transcript", { transcript, isFinal });
56
61
  if (isFinal && transcript.trim()) ;
57
62
  };
@@ -126,7 +131,9 @@ var capacitorTalkMode = (function (exports, core) {
126
131
  // numbers in the wrong language (e.g., Chinese on a Chinese-locale system).
127
132
  utterance.lang = options.directive?.language || "en-US";
128
133
  // Apply directive settings if available
129
- if (options.directive?.speed) {
134
+ if (typeof options.directive?.speed === "number" &&
135
+ Number.isFinite(options.directive.speed) &&
136
+ options.directive.speed > 0) {
130
137
  utterance.rate = options.directive.speed;
131
138
  }
132
139
  utterance.onend = () => {
@@ -160,14 +167,32 @@ var capacitorTalkMode = (function (exports, core) {
160
167
  async isSpeaking() {
161
168
  return { speaking: this.synthesis?.speaking ?? false };
162
169
  }
170
+ async startAudioFrames(_options) {
171
+ // Raw PCM frame capture is a native-only diarization path; on web the Web
172
+ // Speech API gives transcripts only, with no raw-PCM hook.
173
+ return {
174
+ started: false,
175
+ error: "audioFrame capture is not supported on web",
176
+ };
177
+ }
178
+ async stopAudioFrames() {
179
+ // no-op on web
180
+ }
181
+ async isCapturingAudioFrames() {
182
+ return { capturing: false };
183
+ }
163
184
  async checkPermissions() {
164
185
  // Check microphone permission
165
186
  let microphone = "prompt";
166
187
  try {
167
- const result = await navigator.permissions.query({
188
+ const result = await navigator.permissions?.query?.({
168
189
  name: "microphone",
169
190
  });
170
- microphone = result.state;
191
+ if (result?.state === "granted" ||
192
+ result?.state === "denied" ||
193
+ result?.state === "prompt") {
194
+ microphone = result.state;
195
+ }
171
196
  }
172
197
  catch {
173
198
  // Permissions API may not support microphone query
@@ -181,7 +206,11 @@ var capacitorTalkMode = (function (exports, core) {
181
206
  async requestPermissions() {
182
207
  // Request microphone permission by attempting to get user media
183
208
  try {
184
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
209
+ const stream = await navigator.mediaDevices?.getUserMedia?.({
210
+ audio: true,
211
+ });
212
+ if (!stream)
213
+ throw new Error("mediaDevices.getUserMedia unavailable");
185
214
  stream.getTracks().forEach((track) => {
186
215
  track.stop();
187
216
  });
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\nexport * from \"./definitions\";\nconst loadWeb = () => import(\"./web\").then((m) => new m.TalkModeWeb());\nexport const TalkMode = registerPlugin(\"TalkMode\", {\n web: loadWeb,\n});\n","import { WebPlugin } from \"@capacitor/core\";\n/**\n * Web implementation of TalkMode plugin\n *\n * Uses Web Speech API for TTS with limited functionality compared to native.\n * ElevenLabs streaming is not supported on web due to CORS limitations.\n */\nexport class TalkModeWeb extends WebPlugin {\n constructor() {\n super();\n this.config = {};\n this.state = \"idle\";\n this.statusText = \"Off\";\n this.synthesis = null;\n this.currentUtterance = null;\n this.recognition = null;\n this.enabled = false;\n if (typeof window !== \"undefined\" && window.speechSynthesis) {\n this.synthesis = window.speechSynthesis;\n }\n }\n async start(options) {\n if (options?.config) {\n this.config = { ...this.config, ...options.config };\n }\n // Check for Web Speech API support\n const SpeechRecognitionAPI = window.SpeechRecognition ||\n window.webkitSpeechRecognition;\n if (!SpeechRecognitionAPI) {\n return {\n started: false,\n error: \"Speech recognition not supported on this browser\",\n };\n }\n if (!this.synthesis) {\n console.warn(\"[TalkMode] Speech synthesis not available on web\");\n }\n this.enabled = true;\n this.setState(\"listening\", \"Listening\");\n // Initialize speech recognition\n this.recognition = new SpeechRecognitionAPI();\n this.recognition.continuous = true;\n this.recognition.interimResults = true;\n this.recognition.onresult = (event) => {\n const result = event.results[event.results.length - 1];\n const transcript = result[0].transcript;\n const isFinal = result.isFinal;\n this.notifyListeners(\"transcript\", { transcript, isFinal });\n if (isFinal && transcript.trim()) {\n // Note: Full talk mode flow would need Gateway plugin integration\n // For web, we just emit the transcript\n }\n };\n this.recognition.onerror = (event) => {\n this.notifyListeners(\"error\", {\n code: event.error,\n message: event.message || event.error,\n recoverable: event.error !== \"not-allowed\",\n });\n };\n this.recognition.onend = () => {\n if (this.enabled && this.state === \"listening\") {\n // Restart recognition if still enabled\n try {\n this.recognition?.start();\n }\n catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes(\"already started\")) {\n console.warn(\"[TalkMode] Failed to restart recognition:\", msg);\n }\n }\n }\n };\n try {\n this.recognition.start();\n return { started: true };\n }\n catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to start\";\n return { started: false, error: message };\n }\n }\n async stop() {\n this.enabled = false;\n this.recognition?.stop();\n this.recognition = null;\n this.synthesis?.cancel();\n this.currentUtterance = null;\n this.setState(\"idle\", \"Off\");\n }\n async isEnabled() {\n return { enabled: this.enabled };\n }\n async getState() {\n return { state: this.state, statusText: this.statusText };\n }\n async updateConfig(options) {\n this.config = { ...this.config, ...options.config };\n }\n async speak(options) {\n if (!this.synthesis) {\n return {\n completed: false,\n interrupted: false,\n usedSystemTts: false,\n error: \"Speech synthesis not available\",\n };\n }\n // Web can only use system TTS (no ElevenLabs due to CORS)\n const text = options.text.trim();\n if (!text) {\n return { completed: true, interrupted: false, usedSystemTts: true };\n }\n this.setState(\"speaking\", \"Speaking\");\n this.notifyListeners(\"speaking\", { text, isSystemTts: true });\n return new Promise((resolve) => {\n const utterance = new SpeechSynthesisUtterance(text);\n this.currentUtterance = utterance;\n // Always set language — fallback to en-US if directive doesn't specify.\n // Without this, the browser uses the system locale, which may read\n // numbers in the wrong language (e.g., Chinese on a Chinese-locale system).\n utterance.lang = options.directive?.language || \"en-US\";\n // Apply directive settings if available\n if (options.directive?.speed) {\n utterance.rate = options.directive.speed;\n }\n utterance.onend = () => {\n this.currentUtterance = null;\n this.notifyListeners(\"speakComplete\", { completed: true });\n this.setState(\"listening\", \"Listening\");\n resolve({ completed: true, interrupted: false, usedSystemTts: true });\n };\n utterance.onerror = (event) => {\n this.currentUtterance = null;\n this.notifyListeners(\"speakComplete\", { completed: false });\n this.setState(\"idle\", \"Speech error\");\n resolve({\n completed: false,\n interrupted: event.error === \"interrupted\",\n usedSystemTts: true,\n error: event.error,\n });\n };\n this.synthesis?.speak(utterance);\n });\n }\n async stopSpeaking() {\n if (this.synthesis && this.currentUtterance) {\n this.synthesis.cancel();\n this.currentUtterance = null;\n return { interruptedAt: undefined };\n }\n return {};\n }\n async isSpeaking() {\n return { speaking: this.synthesis?.speaking ?? false };\n }\n async checkPermissions() {\n // Check microphone permission\n let microphone = \"prompt\";\n try {\n const result = await navigator.permissions.query({\n name: \"microphone\",\n });\n microphone = result.state;\n }\n catch {\n // Permissions API may not support microphone query\n }\n // Check if speech recognition is supported\n const SpeechRecognitionAPI = window.SpeechRecognition ||\n window.webkitSpeechRecognition;\n const speechRecognition = SpeechRecognitionAPI ? \"prompt\" : \"not_supported\";\n return { microphone, speechRecognition };\n }\n async requestPermissions() {\n // Request microphone permission by attempting to get user media\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n stream.getTracks().forEach((track) => {\n track.stop();\n });\n }\n catch {\n // Permission denied or error\n }\n return this.checkPermissions();\n }\n setState(state, statusText) {\n const previousState = this.state;\n this.state = state;\n this.statusText = statusText;\n this.notifyListeners(\"stateChange\", {\n state,\n previousState,\n statusText,\n usingSystemTts: true,\n });\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\nexport * from \"./definitions\";\nconst loadWeb = () => import(\"./web\").then((m) => new m.TalkModeWeb());\nexport const TalkMode = registerPlugin(\"TalkMode\", {\n web: loadWeb,\n});\n","import { WebPlugin } from \"@capacitor/core\";\n/**\n * Web implementation of TalkMode plugin\n *\n * Uses Web Speech API for TTS with limited functionality compared to native.\n * ElevenLabs streaming is not supported on web due to CORS limitations.\n */\nexport class TalkModeWeb extends WebPlugin {\n constructor() {\n super();\n this.config = {};\n this.state = \"idle\";\n this.statusText = \"Off\";\n this.synthesis = null;\n this.currentUtterance = null;\n this.recognition = null;\n this.enabled = false;\n if (typeof window !== \"undefined\" && window.speechSynthesis) {\n this.synthesis = window.speechSynthesis;\n }\n }\n async start(options) {\n if (options?.config) {\n this.config = { ...this.config, ...options.config };\n }\n // Check for Web Speech API support\n const SpeechRecognitionAPI = window.SpeechRecognition ||\n window.webkitSpeechRecognition;\n if (!SpeechRecognitionAPI) {\n return {\n started: false,\n error: \"Speech recognition not supported on this browser\",\n };\n }\n if (!this.synthesis) {\n console.warn(\"[TalkMode] Speech synthesis not available on web\");\n }\n this.enabled = true;\n this.setState(\"listening\", \"Listening\");\n // Initialize speech recognition\n this.recognition = new SpeechRecognitionAPI();\n this.recognition.continuous = true;\n this.recognition.interimResults = true;\n this.recognition.onresult = (event) => {\n const result = event.results[event.results.length - 1];\n const first = result?.[0];\n if (!first || typeof first.transcript !== \"string\")\n return;\n const transcript = first.transcript;\n const isFinal = result.isFinal;\n if (!transcript.trim())\n return;\n this.notifyListeners(\"transcript\", { transcript, isFinal });\n if (isFinal && transcript.trim()) {\n // Note: Full talk mode flow would need Gateway plugin integration\n // For web, we just emit the transcript\n }\n };\n this.recognition.onerror = (event) => {\n this.notifyListeners(\"error\", {\n code: event.error,\n message: event.message || event.error,\n recoverable: event.error !== \"not-allowed\",\n });\n };\n this.recognition.onend = () => {\n if (this.enabled && this.state === \"listening\") {\n // Restart recognition if still enabled\n try {\n this.recognition?.start();\n }\n catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes(\"already started\")) {\n console.warn(\"[TalkMode] Failed to restart recognition:\", msg);\n }\n }\n }\n };\n try {\n this.recognition.start();\n return { started: true };\n }\n catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to start\";\n return { started: false, error: message };\n }\n }\n async stop() {\n this.enabled = false;\n this.recognition?.stop();\n this.recognition = null;\n this.synthesis?.cancel();\n this.currentUtterance = null;\n this.setState(\"idle\", \"Off\");\n }\n async isEnabled() {\n return { enabled: this.enabled };\n }\n async getState() {\n return { state: this.state, statusText: this.statusText };\n }\n async updateConfig(options) {\n this.config = { ...this.config, ...options.config };\n }\n async speak(options) {\n if (!this.synthesis) {\n return {\n completed: false,\n interrupted: false,\n usedSystemTts: false,\n error: \"Speech synthesis not available\",\n };\n }\n // Web can only use system TTS (no ElevenLabs due to CORS)\n const text = options.text.trim();\n if (!text) {\n return { completed: true, interrupted: false, usedSystemTts: true };\n }\n this.setState(\"speaking\", \"Speaking\");\n this.notifyListeners(\"speaking\", { text, isSystemTts: true });\n return new Promise((resolve) => {\n const utterance = new SpeechSynthesisUtterance(text);\n this.currentUtterance = utterance;\n // Always set language — fallback to en-US if directive doesn't specify.\n // Without this, the browser uses the system locale, which may read\n // numbers in the wrong language (e.g., Chinese on a Chinese-locale system).\n utterance.lang = options.directive?.language || \"en-US\";\n // Apply directive settings if available\n if (typeof options.directive?.speed === \"number\" &&\n Number.isFinite(options.directive.speed) &&\n options.directive.speed > 0) {\n utterance.rate = options.directive.speed;\n }\n utterance.onend = () => {\n this.currentUtterance = null;\n this.notifyListeners(\"speakComplete\", { completed: true });\n this.setState(\"listening\", \"Listening\");\n resolve({ completed: true, interrupted: false, usedSystemTts: true });\n };\n utterance.onerror = (event) => {\n this.currentUtterance = null;\n this.notifyListeners(\"speakComplete\", { completed: false });\n this.setState(\"idle\", \"Speech error\");\n resolve({\n completed: false,\n interrupted: event.error === \"interrupted\",\n usedSystemTts: true,\n error: event.error,\n });\n };\n this.synthesis?.speak(utterance);\n });\n }\n async stopSpeaking() {\n if (this.synthesis && this.currentUtterance) {\n this.synthesis.cancel();\n this.currentUtterance = null;\n return { interruptedAt: undefined };\n }\n return {};\n }\n async isSpeaking() {\n return { speaking: this.synthesis?.speaking ?? false };\n }\n async startAudioFrames(_options) {\n // Raw PCM frame capture is a native-only diarization path; on web the Web\n // Speech API gives transcripts only, with no raw-PCM hook.\n return {\n started: false,\n error: \"audioFrame capture is not supported on web\",\n };\n }\n async stopAudioFrames() {\n // no-op on web\n }\n async isCapturingAudioFrames() {\n return { capturing: false };\n }\n async checkPermissions() {\n // Check microphone permission\n let microphone = \"prompt\";\n try {\n const result = await navigator.permissions?.query?.({\n name: \"microphone\",\n });\n if (result?.state === \"granted\" ||\n result?.state === \"denied\" ||\n result?.state === \"prompt\") {\n microphone = result.state;\n }\n }\n catch {\n // Permissions API may not support microphone query\n }\n // Check if speech recognition is supported\n const SpeechRecognitionAPI = window.SpeechRecognition ||\n window.webkitSpeechRecognition;\n const speechRecognition = SpeechRecognitionAPI ? \"prompt\" : \"not_supported\";\n return { microphone, speechRecognition };\n }\n async requestPermissions() {\n // Request microphone permission by attempting to get user media\n try {\n const stream = await navigator.mediaDevices?.getUserMedia?.({\n audio: true,\n });\n if (!stream)\n throw new Error(\"mediaDevices.getUserMedia unavailable\");\n stream.getTracks().forEach((track) => {\n track.stop();\n });\n }\n catch {\n // Permission denied or error\n }\n return this.checkPermissions();\n }\n setState(state, statusText) {\n const previousState = this.state;\n this.state = state;\n this.statusText = statusText;\n this.notifyListeners(\"stateChange\", {\n state,\n previousState,\n statusText,\n usingSystemTts: true,\n });\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":"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"}
@@ -23,6 +23,7 @@ public class TalkModePlugin: CAPPlugin, CAPBridgedPlugin {
23
23
  ]
24
24
 
25
25
  private static let defaultModelId = "eleven_flash_v2_5"
26
+ private static let localInferenceTtsURL = "eliza-local-agent://ipc/api/tts/local-inference"
26
27
 
27
28
  // MARK: - State
28
29
 
@@ -171,11 +172,18 @@ public class TalkModePlugin: CAPPlugin, CAPBridgedPlugin {
171
172
  }
172
173
 
173
174
  let useSystemTts = call.getBool("useSystemTts") ?? false
175
+ let useLocalInferenceTts = call.getBool("useLocalInferenceTts") ?? false
174
176
  let directive = call.getObject("directive")
175
177
 
176
178
  speakTask?.cancel()
177
179
  speakTask = Task { @MainActor in
178
- await self.speakInternal(text: text, forceSystemTts: useSystemTts, directive: directive, call: call)
180
+ await self.speakInternal(
181
+ text: text,
182
+ forceSystemTts: useSystemTts,
183
+ useLocalInferenceTts: useLocalInferenceTts,
184
+ directive: directive,
185
+ call: call
186
+ )
179
187
  }
180
188
  }
181
189
 
@@ -420,6 +428,7 @@ public class TalkModePlugin: CAPPlugin, CAPBridgedPlugin {
420
428
  private func speakInternal(
421
429
  text: String,
422
430
  forceSystemTts: Bool,
431
+ useLocalInferenceTts: Bool,
423
432
  directive: [String: Any]?,
424
433
  call: CAPPluginCall
425
434
  ) async {
@@ -452,13 +461,15 @@ public class TalkModePlugin: CAPPlugin, CAPBridgedPlugin {
452
461
  let effectiveFormat = Self.validatedOutputFormat(rawFormat) ?? "pcm_24000"
453
462
  let effectiveApiKey = apiKey?.trimmingCharacters(in: .whitespacesAndNewlines)
454
463
 
455
- let canUseElevenLabs = !forceSystemTts
464
+ let canUseLocalInference = useLocalInferenceTts && !forceSystemTts
465
+ let canUseElevenLabs = !canUseLocalInference
466
+ && !forceSystemTts
456
467
  && !(effectiveApiKey ?? "").isEmpty
457
468
  && !(effectiveVoiceId ?? "").isEmpty
458
469
 
459
470
  notifyListeners("speaking", data: [
460
471
  "text": text,
461
- "isSystemTts": !canUseElevenLabs
472
+ "isSystemTts": !(canUseLocalInference || canUseElevenLabs)
462
473
  ])
463
474
 
464
475
  // Enable STT during playback for interrupt detection
@@ -474,7 +485,10 @@ public class TalkModePlugin: CAPPlugin, CAPBridgedPlugin {
474
485
  let language = Self.validatedLanguage(directive?["language"] as? String)
475
486
 
476
487
  do {
477
- if canUseElevenLabs {
488
+ if canUseLocalInference {
489
+ try await streamLocalInferenceTts(text: text, directive: directive)
490
+ interrupted = pcmStopRequested
491
+ } else if canUseElevenLabs {
478
492
  do {
479
493
  try await streamElevenLabsTts(
480
494
  text: text,
@@ -527,6 +541,239 @@ public class TalkModePlugin: CAPPlugin, CAPBridgedPlugin {
527
541
  finishSpeaking()
528
542
  }
529
543
 
544
+ // MARK: - Local Inference TTS
545
+
546
+ private struct LocalInferenceAudioResponse {
547
+ let status: Int
548
+ let statusText: String
549
+ let headers: [String: String]
550
+ let audioBase64: String
551
+ let bodyText: String
552
+ }
553
+
554
+ private struct WavPcmFormat {
555
+ let sampleRate: Int
556
+ let channels: Int
557
+ }
558
+
559
+ private func streamLocalInferenceTts(text: String, directive: [String: Any]?) async throws {
560
+ let response = try await requestLocalInferenceTtsAudio(text: text, directive: directive)
561
+ guard (200..<300).contains(response.status) else {
562
+ let detail = response.bodyText.trimmingCharacters(in: .whitespacesAndNewlines)
563
+ let suffix = detail.isEmpty ? response.statusText : detail
564
+ throw NSError(domain: "TalkMode", code: response.status, userInfo: [
565
+ NSLocalizedDescriptionKey: "Local inference TTS error: \(response.status) \(suffix)"
566
+ ])
567
+ }
568
+
569
+ guard let audioData = Data(base64Encoded: response.audioBase64), !audioData.isEmpty else {
570
+ throw NSError(domain: "TalkMode", code: 2, userInfo: [
571
+ NSLocalizedDescriptionKey: "Local inference TTS returned empty audio"
572
+ ])
573
+ }
574
+
575
+ let format = try Self.parseWavPcmFormat(audioData)
576
+ notifyListeners("playbackStart", data: [
577
+ "provider": "local-inference",
578
+ "sampleRate": format.sampleRate,
579
+ "channels": format.channels
580
+ ])
581
+ try await playBufferedAudio(audioData)
582
+ }
583
+
584
+ private func requestLocalInferenceTtsAudio(
585
+ text: String,
586
+ directive: [String: Any]?
587
+ ) async throws -> LocalInferenceAudioResponse {
588
+ guard let webView = bridge?.webView else {
589
+ throw NSError(domain: "TalkMode", code: 503, userInfo: [
590
+ NSLocalizedDescriptionKey: "iOS local-agent WebView bridge is unavailable"
591
+ ])
592
+ }
593
+ guard #available(iOS 14.0, *) else {
594
+ throw NSError(domain: "TalkMode", code: 503, userInfo: [
595
+ NSLocalizedDescriptionKey: "Local inference TTS requires iOS 14 or later"
596
+ ])
597
+ }
598
+
599
+ let payload = buildLocalInferenceTtsPayload(text: text, directive: directive)
600
+ let source = """
601
+ const response = await fetch(options.url, {
602
+ method: "POST",
603
+ headers: {
604
+ "Accept": "audio/wav",
605
+ "Content-Type": "application/json"
606
+ },
607
+ body: JSON.stringify(options.payload)
608
+ });
609
+ const headers = {};
610
+ response.headers.forEach((value, key) => { headers[key] = value; });
611
+ const buffer = await response.arrayBuffer();
612
+ const bytes = new Uint8Array(buffer);
613
+ let binary = "";
614
+ const chunkSize = 0x8000;
615
+ for (let i = 0; i < bytes.length; i += chunkSize) {
616
+ const chunk = bytes.subarray(i, i + chunkSize);
617
+ binary += String.fromCharCode.apply(null, Array.from(chunk));
618
+ }
619
+ let bodyText = "";
620
+ if (!response.ok && typeof TextDecoder !== "undefined") {
621
+ bodyText = new TextDecoder().decode(bytes.slice(0, 2048));
622
+ }
623
+ return {
624
+ status: response.status,
625
+ statusText: response.statusText,
626
+ headers,
627
+ audioBase64: btoa(binary),
628
+ bodyText
629
+ };
630
+ """
631
+
632
+ return try await withCheckedThrowingContinuation { continuation in
633
+ DispatchQueue.main.async {
634
+ Task { @MainActor in
635
+ do {
636
+ let value = try await webView.callAsyncJavaScript(
637
+ source,
638
+ arguments: [
639
+ "options": [
640
+ "url": Self.localInferenceTtsURL,
641
+ "payload": payload
642
+ ]
643
+ ],
644
+ in: nil,
645
+ contentWorld: .page
646
+ )
647
+ continuation.resume(returning: try self.parseLocalInferenceAudioResponse(value))
648
+ } catch {
649
+ continuation.resume(throwing: error)
650
+ }
651
+ }
652
+ }
653
+ }
654
+ }
655
+
656
+ private func buildLocalInferenceTtsPayload(text: String, directive: [String: Any]?) -> JSObject {
657
+ var payload: JSObject = ["text": text]
658
+ for key in ["voiceId", "voice", "modelId", "model"] {
659
+ if let value = directive?[key] as? String {
660
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
661
+ if !trimmed.isEmpty { payload[key] = trimmed }
662
+ }
663
+ }
664
+ if let speed = directive?["speed"] as? Double, speed.isFinite, speed > 0 {
665
+ payload["speed"] = speed
666
+ } else if let speed = directive?["speed"] as? NSNumber, speed.doubleValue.isFinite, speed.doubleValue > 0 {
667
+ payload["speed"] = speed.doubleValue
668
+ }
669
+ if let outputFormat = directive?["outputFormat"] as? String {
670
+ let trimmed = outputFormat.trimmingCharacters(in: .whitespacesAndNewlines)
671
+ if !trimmed.isEmpty { payload["format"] = trimmed }
672
+ }
673
+ return payload
674
+ }
675
+
676
+ private func parseLocalInferenceAudioResponse(_ value: Any?) throws -> LocalInferenceAudioResponse {
677
+ guard let payload = value as? JSObject else {
678
+ throw NSError(domain: "TalkMode", code: 502, userInfo: [
679
+ NSLocalizedDescriptionKey: "Local inference TTS returned an invalid bridge response"
680
+ ])
681
+ }
682
+ let status = (payload["status"] as? Int)
683
+ ?? (payload["status"] as? NSNumber)?.intValue
684
+ ?? 0
685
+ let headers = payload["headers"] as? [String: String] ?? [:]
686
+ return LocalInferenceAudioResponse(
687
+ status: status,
688
+ statusText: payload["statusText"] as? String ?? "",
689
+ headers: headers,
690
+ audioBase64: payload["audioBase64"] as? String ?? "",
691
+ bodyText: payload["bodyText"] as? String ?? ""
692
+ )
693
+ }
694
+
695
+ private static func parseWavPcmFormat(_ data: Data) throws -> WavPcmFormat {
696
+ guard data.count >= 12,
697
+ asciiString(data, offset: 0, length: 4) == "RIFF",
698
+ asciiString(data, offset: 8, length: 4) == "WAVE" else {
699
+ throw NSError(domain: "TalkMode", code: 3, userInfo: [
700
+ NSLocalizedDescriptionKey: "Local inference TTS returned non-WAV audio"
701
+ ])
702
+ }
703
+
704
+ var offset = 12
705
+ var parsedFormat: WavPcmFormat?
706
+ while offset + 8 <= data.count {
707
+ let id = asciiString(data, offset: offset, length: 4)
708
+ let size = Int(littleEndianUInt32(data, offset: offset + 4))
709
+ let payloadOffset = offset + 8
710
+ guard size >= 0, payloadOffset + size <= data.count else {
711
+ throw NSError(domain: "TalkMode", code: 4, userInfo: [
712
+ NSLocalizedDescriptionKey: "Invalid WAV chunk size"
713
+ ])
714
+ }
715
+
716
+ if id == "fmt " {
717
+ guard size >= 16 else {
718
+ throw NSError(domain: "TalkMode", code: 5, userInfo: [
719
+ NSLocalizedDescriptionKey: "Invalid WAV fmt chunk"
720
+ ])
721
+ }
722
+ let audioFormat = littleEndianUInt16(data, offset: payloadOffset)
723
+ let channels = Int(littleEndianUInt16(data, offset: payloadOffset + 2))
724
+ let sampleRate = Int(littleEndianUInt32(data, offset: payloadOffset + 4))
725
+ let bitsPerSample = littleEndianUInt16(data, offset: payloadOffset + 14)
726
+ guard audioFormat == 1 else {
727
+ throw NSError(domain: "TalkMode", code: 6, userInfo: [
728
+ NSLocalizedDescriptionKey: "Only PCM WAV is supported"
729
+ ])
730
+ }
731
+ guard bitsPerSample == 16 else {
732
+ throw NSError(domain: "TalkMode", code: 7, userInfo: [
733
+ NSLocalizedDescriptionKey: "Only 16-bit PCM WAV is supported"
734
+ ])
735
+ }
736
+ guard channels >= 1, channels <= 2, sampleRate > 0 else {
737
+ throw NSError(domain: "TalkMode", code: 8, userInfo: [
738
+ NSLocalizedDescriptionKey: "Invalid WAV sample rate or channel count"
739
+ ])
740
+ }
741
+ parsedFormat = WavPcmFormat(sampleRate: sampleRate, channels: channels)
742
+ } else if id == "data" {
743
+ guard let parsedFormat else {
744
+ throw NSError(domain: "TalkMode", code: 9, userInfo: [
745
+ NSLocalizedDescriptionKey: "WAV data arrived before fmt chunk"
746
+ ])
747
+ }
748
+ return parsedFormat
749
+ }
750
+
751
+ offset = payloadOffset + size + (size % 2)
752
+ }
753
+
754
+ throw NSError(domain: "TalkMode", code: 10, userInfo: [
755
+ NSLocalizedDescriptionKey: "WAV data chunk was not found"
756
+ ])
757
+ }
758
+
759
+ private static func asciiString(_ data: Data, offset: Int, length: Int) -> String {
760
+ guard offset >= 0, length >= 0, offset + length <= data.count else { return "" }
761
+ return String(data: data.subdata(in: offset..<(offset + length)), encoding: .ascii) ?? ""
762
+ }
763
+
764
+ private static func littleEndianUInt16(_ data: Data, offset: Int) -> UInt16 {
765
+ guard offset + 2 <= data.count else { return 0 }
766
+ return UInt16(data[offset]) | (UInt16(data[offset + 1]) << 8)
767
+ }
768
+
769
+ private static func littleEndianUInt32(_ data: Data, offset: Int) -> UInt32 {
770
+ guard offset + 4 <= data.count else { return 0 }
771
+ return UInt32(data[offset])
772
+ | (UInt32(data[offset + 1]) << 8)
773
+ | (UInt32(data[offset + 2]) << 16)
774
+ | (UInt32(data[offset + 3]) << 24)
775
+ }
776
+
530
777
  /// Clean up after speech and restart recognition if talk mode is still enabled.
531
778
  private func finishSpeaking() {
532
779
  isSpeakingValue = false
@@ -751,17 +998,7 @@ public class TalkModePlugin: CAPPlugin, CAPBridgedPlugin {
751
998
  }
752
999
  }
753
1000
 
754
- /// Download a full audio response (MP3 etc.) and play it with AVAudioPlayer.
755
- private func downloadAndPlayAudio(request: URLRequest) async throws {
756
- let (data, response) = try await URLSession.shared.data(for: request)
757
-
758
- guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
759
- let msg = String(data: data.prefix(2048), encoding: .utf8) ?? "Unknown error"
760
- throw NSError(domain: "TalkMode", code: 2, userInfo: [
761
- NSLocalizedDescriptionKey: "ElevenLabs API error: \(msg)"
762
- ])
763
- }
764
-
1001
+ private func playBufferedAudio(_ data: Data) async throws {
765
1002
  mp3PlaybackStartTime = Date()
766
1003
 
767
1004
  let player = try AVAudioPlayer(data: data)
@@ -772,7 +1009,6 @@ public class TalkModePlugin: CAPPlugin, CAPBridgedPlugin {
772
1009
  let delegate = AudioPlayerDelegate {
773
1010
  continuation.resume()
774
1011
  }
775
- // Retain delegate for the lifetime of playback
776
1012
  objc_setAssociatedObject(player, "delegate", delegate, .OBJC_ASSOCIATION_RETAIN)
777
1013
  player.delegate = delegate
778
1014
  player.play()
@@ -782,6 +1018,20 @@ public class TalkModePlugin: CAPPlugin, CAPBridgedPlugin {
782
1018
  mp3PlaybackStartTime = nil
783
1019
  }
784
1020
 
1021
+ /// Download a full audio response (MP3 etc.) and play it with AVAudioPlayer.
1022
+ private func downloadAndPlayAudio(request: URLRequest) async throws {
1023
+ let (data, response) = try await URLSession.shared.data(for: request)
1024
+
1025
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
1026
+ let msg = String(data: data.prefix(2048), encoding: .utf8) ?? "Unknown error"
1027
+ throw NSError(domain: "TalkMode", code: 2, userInfo: [
1028
+ NSLocalizedDescriptionKey: "ElevenLabs API error: \(msg)"
1029
+ ])
1030
+ }
1031
+
1032
+ try await playBufferedAudio(data)
1033
+ }
1034
+
785
1035
  // MARK: - System TTS
786
1036
 
787
1037
  private func speakWithSystemTts(text: String, language: String? = nil) async throws {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elizaos/capacitor-talkmode",
3
- "version": "1.0.0",
3
+ "version": "2.0.3-beta.2",
4
4
  "description": "Runs voice conversations with live transcription, chat orchestration, and spoken replies.",
5
5
  "keywords": [
6
6
  "voice",
@@ -14,6 +14,8 @@
14
14
  "exports": {
15
15
  ".": {
16
16
  "types": "./dist/esm/index.d.ts",
17
+ "bun": "./src/index.ts",
18
+ "development": "./src/index.ts",
17
19
  "import": "./dist/esm/index.js",
18
20
  "require": "./dist/plugin.cjs.js"
19
21
  },
@@ -25,20 +27,21 @@
25
27
  "android/build.gradle",
26
28
  "dist/",
27
29
  "ios/Sources/",
28
- "*.podspec"
30
+ "*.podspec",
31
+ "dist"
29
32
  ],
30
33
  "scripts": {
31
- "build": "npm run clean && tsc && rollup -c rollup.config.mjs",
32
- "build:docs": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
33
- "clean": "rimraf ./dist",
34
- "prepublishOnly": "npm run build",
35
- "docgen": "docgen --api TalkModePlugin --output-readme README.md --output-json dist/docs.json"
34
+ "build": "node ../../packages/scripts/with-package-build-lock.mjs plugins/plugin-native-talkmode -- bun run build:unlocked",
35
+ "build:docs": "bun run clean && bun run docgen && tsc && bun --bun rollup -c rollup.config.mjs",
36
+ "clean": "node ../../packages/scripts/rm-path-recursive.mjs dist",
37
+ "test": "vitest run",
38
+ "prepublishOnly": "bun run build",
39
+ "docgen": "docgen --api TalkModePlugin --output-readme README.md --output-json dist/docs.json",
40
+ "build:unlocked": "bun run clean && tsc && bunx rollup -c rollup.config.mjs"
36
41
  },
37
42
  "author": "elizaOS",
38
43
  "license": "MIT",
39
- "dependencies": {
40
- "@elizaos/app-core": "2.0.0-alpha.537"
41
- },
44
+ "dependencies": {},
42
45
  "repository": {
43
46
  "type": "git",
44
47
  "url": "https://github.com/elizaOS/eliza.git",
@@ -54,12 +57,12 @@
54
57
  }
55
58
  },
56
59
  "devDependencies": {
57
- "@capacitor/cli": "^8.0.0",
58
60
  "@capacitor/core": "^8.3.1",
59
61
  "@capacitor/docgen": "^0.3.0",
60
- "rimraf": "^6.0.0",
62
+ "@elizaos/native-plugin-shared-types": "2.0.3-beta.2",
61
63
  "rollup": "^4.60.2",
62
- "typescript": "^6.0.0"
64
+ "typescript": "^6.0.3",
65
+ "vitest": "^4.0.0"
63
66
  },
64
67
  "peerDependencies": {
65
68
  "@capacitor/core": "^8.3.1"
@@ -79,5 +82,6 @@
79
82
  "ios": true,
80
83
  "android": true
81
84
  }
82
- }
85
+ },
86
+ "gitHead": "82fe0f44215954c2417328203f5bd6510985c1fc"
83
87
  }