@speechos/react 1.0.9 → 1.0.11

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.
@@ -6,8 +6,7 @@
6
6
  */
7
7
  import React, { type ReactNode } from "react";
8
8
  import { type SpeechOSCoreConfig, type SpeechOSState, type SpeechOSEventMap, type UnsubscribeFn, type CommandDefinition, type CommandResult } from "@speechos/core";
9
- import type { FormDetectorInterface } from "@speechos/client";
10
- import type { TextInputHandlerInterface } from "@speechos/client";
9
+ import type { FormDetectorInterface, TextInputHandlerInterface, ReadAloudConfig } from "@speechos/client";
11
10
  /**
12
11
  * Context value exposed by SpeechOSProvider
13
12
  */
@@ -19,8 +18,8 @@ export interface SpeechOSContextValue {
19
18
  stopDictation: () => Promise<string>;
20
19
  edit: (text: string) => Promise<string>;
21
20
  stopEdit: () => Promise<string>;
22
- command: (commands: CommandDefinition[]) => Promise<CommandResult | null>;
23
- stopCommand: () => Promise<CommandResult | null>;
21
+ command: (commands: CommandDefinition[]) => Promise<CommandResult[]>;
22
+ stopCommand: () => Promise<CommandResult[]>;
24
23
  cancel: () => Promise<void>;
25
24
  on: <K extends keyof SpeechOSEventMap>(event: K, callback: (payload: SpeechOSEventMap[K]) => void) => UnsubscribeFn;
26
25
  off: <K extends keyof SpeechOSEventMap>(event: K, callback: (payload: SpeechOSEventMap[K]) => void) => void;
@@ -55,6 +54,13 @@ export interface SpeechOSReactConfig extends SpeechOSCoreConfig {
55
54
  * Default: false
56
55
  */
57
56
  useExternalSettings?: boolean;
57
+ /**
58
+ * Read-aloud behavior for selected text.
59
+ * - true (default): enable with defaults
60
+ * - false: disable read-aloud
61
+ * - ReadAloudConfig: customize behavior
62
+ */
63
+ readAloud?: boolean | ReadAloudConfig;
58
64
  }
59
65
  /**
60
66
  * Props for SpeechOSProvider
package/dist/context.d.ts CHANGED
@@ -6,8 +6,7 @@
6
6
  */
7
7
  import React, { type ReactNode } from "react";
8
8
  import { type SpeechOSCoreConfig, type SpeechOSState, type SpeechOSEventMap, type UnsubscribeFn, type CommandDefinition, type CommandResult } from "@speechos/core";
9
- import type { FormDetectorInterface } from "@speechos/client";
10
- import type { TextInputHandlerInterface } from "@speechos/client";
9
+ import type { FormDetectorInterface, TextInputHandlerInterface, ReadAloudConfig } from "@speechos/client";
11
10
  /**
12
11
  * Context value exposed by SpeechOSProvider
13
12
  */
@@ -19,8 +18,8 @@ export interface SpeechOSContextValue {
19
18
  stopDictation: () => Promise<string>;
20
19
  edit: (text: string) => Promise<string>;
21
20
  stopEdit: () => Promise<string>;
22
- command: (commands: CommandDefinition[]) => Promise<CommandResult | null>;
23
- stopCommand: () => Promise<CommandResult | null>;
21
+ command: (commands: CommandDefinition[]) => Promise<CommandResult[]>;
22
+ stopCommand: () => Promise<CommandResult[]>;
24
23
  cancel: () => Promise<void>;
25
24
  on: <K extends keyof SpeechOSEventMap>(event: K, callback: (payload: SpeechOSEventMap[K]) => void) => UnsubscribeFn;
26
25
  off: <K extends keyof SpeechOSEventMap>(event: K, callback: (payload: SpeechOSEventMap[K]) => void) => void;
@@ -55,6 +54,13 @@ export interface SpeechOSReactConfig extends SpeechOSCoreConfig {
55
54
  * Default: false
56
55
  */
57
56
  useExternalSettings?: boolean;
57
+ /**
58
+ * Read-aloud behavior for selected text.
59
+ * - true (default): enable with defaults
60
+ * - false: disable read-aloud
61
+ * - ReadAloudConfig: customize behavior
62
+ */
63
+ readAloud?: boolean | ReadAloudConfig;
58
64
  }
59
65
  /**
60
66
  * Props for SpeechOSProvider
@@ -9,4 +9,5 @@ export { useSpeechOSEvents } from "./useSpeechOSEvents.js";
9
9
  export { useDictation, type UseDictationResult } from "./useDictation.js";
10
10
  export { useEdit, type UseEditResult } from "./useEdit.js";
11
11
  export { useCommand, type UseCommandResult } from "./useCommand.js";
12
+ export { useTTS, type UseTTSResult, type SpeakOptions } from "./useTTS.js";
12
13
  export { useSpeechOSWidget, type UseSpeechOSWidgetResult } from "./useSpeechOSWidget.js";
@@ -9,4 +9,5 @@ export { useSpeechOSEvents } from "./useSpeechOSEvents.js";
9
9
  export { useDictation, type UseDictationResult } from "./useDictation.js";
10
10
  export { useEdit, type UseEditResult } from "./useEdit.js";
11
11
  export { useCommand, type UseCommandResult } from "./useCommand.js";
12
+ export { useTTS, type UseTTSResult, type SpeakOptions } from "./useTTS.js";
12
13
  export { useSpeechOSWidget, type UseSpeechOSWidgetResult } from "./useSpeechOSWidget.js";
@@ -10,24 +10,25 @@ import type { CommandDefinition, CommandResult } from "@speechos/core";
10
10
  export interface UseCommandResult {
11
11
  /** Start command listening - begins recording */
12
12
  start: (commands: CommandDefinition[]) => Promise<void>;
13
- /** Stop command listening - ends recording and returns matched command */
14
- stop: () => Promise<CommandResult | null>;
13
+ /** Stop command listening - ends recording and returns matched commands */
14
+ stop: () => Promise<CommandResult[]>;
15
15
  /** Whether currently listening for commands */
16
16
  isListening: boolean;
17
17
  /** Whether processing the command */
18
18
  isProcessing: boolean;
19
- /** The matched command result (null if no match or not yet processed) */
20
- result: CommandResult | null;
19
+ /** The matched command results (empty array if no matches or not yet processed) */
20
+ results: CommandResult[];
21
21
  /** Any error that occurred */
22
22
  error: string | null;
23
- /** Clear the result and error state */
23
+ /** Clear the results and error state */
24
24
  clear: () => void;
25
25
  }
26
26
  /**
27
27
  * Simplified hook for voice command workflows
28
28
  *
29
29
  * Provides an easy-to-use interface for voice command matching
30
- * with automatic state management.
30
+ * with automatic state management. Supports matching multiple
31
+ * commands from a single voice input.
31
32
  *
32
33
  * @example
33
34
  * ```tsx
@@ -42,7 +43,7 @@ export interface UseCommandResult {
42
43
  * ];
43
44
  *
44
45
  * function VoiceCommands() {
45
- * const { start, stop, isListening, isProcessing, result, error } = useCommand();
46
+ * const { start, stop, isListening, isProcessing, results, error } = useCommand();
46
47
  *
47
48
  * const handleCommand = async () => {
48
49
  * await start(commands);
@@ -50,9 +51,9 @@ export interface UseCommandResult {
50
51
  *
51
52
  * const handleStop = async () => {
52
53
  * const matched = await stop();
53
- * if (matched) {
54
- * console.log('Matched command:', matched.name, matched.arguments);
55
- * }
54
+ * matched.forEach(cmd => {
55
+ * console.log('Matched command:', cmd.name, cmd.arguments);
56
+ * });
56
57
  * };
57
58
  *
58
59
  * return (
@@ -61,7 +62,11 @@ export interface UseCommandResult {
61
62
  * {isListening ? 'Execute' : 'Say Command'}
62
63
  * </button>
63
64
  * {isProcessing && <span>Processing...</span>}
64
- * {result && <p>Command: {result.name}</p>}
65
+ * {results.length > 0 && (
66
+ * <div>
67
+ * {results.map((cmd, i) => <p key={i}>Command: {cmd.name}</p>)}
68
+ * </div>
69
+ * )}
65
70
  * {error && <p style={{ color: 'red' }}>{error}</p>}
66
71
  * </div>
67
72
  * );
@@ -10,24 +10,25 @@ import type { CommandDefinition, CommandResult } from "@speechos/core";
10
10
  export interface UseCommandResult {
11
11
  /** Start command listening - begins recording */
12
12
  start: (commands: CommandDefinition[]) => Promise<void>;
13
- /** Stop command listening - ends recording and returns matched command */
14
- stop: () => Promise<CommandResult | null>;
13
+ /** Stop command listening - ends recording and returns matched commands */
14
+ stop: () => Promise<CommandResult[]>;
15
15
  /** Whether currently listening for commands */
16
16
  isListening: boolean;
17
17
  /** Whether processing the command */
18
18
  isProcessing: boolean;
19
- /** The matched command result (null if no match or not yet processed) */
20
- result: CommandResult | null;
19
+ /** The matched command results (empty array if no matches or not yet processed) */
20
+ results: CommandResult[];
21
21
  /** Any error that occurred */
22
22
  error: string | null;
23
- /** Clear the result and error state */
23
+ /** Clear the results and error state */
24
24
  clear: () => void;
25
25
  }
26
26
  /**
27
27
  * Simplified hook for voice command workflows
28
28
  *
29
29
  * Provides an easy-to-use interface for voice command matching
30
- * with automatic state management.
30
+ * with automatic state management. Supports matching multiple
31
+ * commands from a single voice input.
31
32
  *
32
33
  * @example
33
34
  * ```tsx
@@ -42,7 +43,7 @@ export interface UseCommandResult {
42
43
  * ];
43
44
  *
44
45
  * function VoiceCommands() {
45
- * const { start, stop, isListening, isProcessing, result, error } = useCommand();
46
+ * const { start, stop, isListening, isProcessing, results, error } = useCommand();
46
47
  *
47
48
  * const handleCommand = async () => {
48
49
  * await start(commands);
@@ -50,9 +51,9 @@ export interface UseCommandResult {
50
51
  *
51
52
  * const handleStop = async () => {
52
53
  * const matched = await stop();
53
- * if (matched) {
54
- * console.log('Matched command:', matched.name, matched.arguments);
55
- * }
54
+ * matched.forEach(cmd => {
55
+ * console.log('Matched command:', cmd.name, cmd.arguments);
56
+ * });
56
57
  * };
57
58
  *
58
59
  * return (
@@ -61,7 +62,11 @@ export interface UseCommandResult {
61
62
  * {isListening ? 'Execute' : 'Say Command'}
62
63
  * </button>
63
64
  * {isProcessing && <span>Processing...</span>}
64
- * {result && <p>Command: {result.name}</p>}
65
+ * {results.length > 0 && (
66
+ * <div>
67
+ * {results.map((cmd, i) => <p key={i}>Command: {cmd.name}</p>)}
68
+ * </div>
69
+ * )}
65
70
  * {error && <p style={{ color: 'red' }}>{error}</p>}
66
71
  * </div>
67
72
  * );
@@ -0,0 +1,59 @@
1
+ /**
2
+ * useTTS - React hook for text-to-speech
3
+ *
4
+ * Provides a simple interface for TTS synthesis and playback.
5
+ */
6
+ import { type TTSOptions, type TTSResult } from "@speechos/core";
7
+ /**
8
+ * Return type for useTTS hook
9
+ */
10
+ export interface UseTTSResult {
11
+ /** Synthesize text and play audio immediately */
12
+ speak: (text: string, options?: SpeakOptions) => Promise<void>;
13
+ /** Synthesize text and return audio bytes */
14
+ synthesize: (text: string, options?: TTSOptions) => Promise<TTSResult>;
15
+ /** Stop current playback */
16
+ stop: () => void;
17
+ /** Whether currently synthesizing (HTTP request in progress) */
18
+ isSynthesizing: boolean;
19
+ /** Whether audio is currently playing */
20
+ isPlaying: boolean;
21
+ /** The last synthesized audio result (from synthesize()) */
22
+ audioResult: TTSResult | null;
23
+ /** Any error that occurred */
24
+ error: string | null;
25
+ /** Clear error and audio result state */
26
+ clear: () => void;
27
+ }
28
+ /**
29
+ * Options for speaking text (extends TTSOptions with playback options)
30
+ */
31
+ export interface SpeakOptions extends TTSOptions {
32
+ /** Output audio device ID (if supported by browser) */
33
+ audioDeviceId?: string;
34
+ }
35
+ /**
36
+ * React hook for text-to-speech
37
+ *
38
+ * Provides an easy-to-use interface for TTS synthesis and playback
39
+ * with automatic state management.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * function TTSButton() {
44
+ * const { speak, stop, isSynthesizing, isPlaying, error } = useTTS();
45
+ *
46
+ * return (
47
+ * <div>
48
+ * <button onClick={() => speak('Hello!')}>
49
+ * {isSynthesizing ? 'Loading...' : isPlaying ? 'Stop' : 'Speak'}
50
+ * </button>
51
+ * {error && <p style={{ color: 'red' }}>{error}</p>}
52
+ * </div>
53
+ * );
54
+ * }
55
+ * ```
56
+ *
57
+ * @returns TTS controls and state
58
+ */
59
+ export declare function useTTS(): UseTTSResult;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * useTTS - React hook for text-to-speech
3
+ *
4
+ * Provides a simple interface for TTS synthesis and playback.
5
+ */
6
+ import { type TTSOptions, type TTSResult } from "@speechos/core";
7
+ /**
8
+ * Return type for useTTS hook
9
+ */
10
+ export interface UseTTSResult {
11
+ /** Synthesize text and play audio immediately */
12
+ speak: (text: string, options?: SpeakOptions) => Promise<void>;
13
+ /** Synthesize text and return audio bytes */
14
+ synthesize: (text: string, options?: TTSOptions) => Promise<TTSResult>;
15
+ /** Stop current playback */
16
+ stop: () => void;
17
+ /** Whether currently synthesizing (HTTP request in progress) */
18
+ isSynthesizing: boolean;
19
+ /** Whether audio is currently playing */
20
+ isPlaying: boolean;
21
+ /** The last synthesized audio result (from synthesize()) */
22
+ audioResult: TTSResult | null;
23
+ /** Any error that occurred */
24
+ error: string | null;
25
+ /** Clear error and audio result state */
26
+ clear: () => void;
27
+ }
28
+ /**
29
+ * Options for speaking text (extends TTSOptions with playback options)
30
+ */
31
+ export interface SpeakOptions extends TTSOptions {
32
+ /** Output audio device ID (if supported by browser) */
33
+ audioDeviceId?: string;
34
+ }
35
+ /**
36
+ * React hook for text-to-speech
37
+ *
38
+ * Provides an easy-to-use interface for TTS synthesis and playback
39
+ * with automatic state management.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * function TTSButton() {
44
+ * const { speak, stop, isSynthesizing, isPlaying, error } = useTTS();
45
+ *
46
+ * return (
47
+ * <div>
48
+ * <button onClick={() => speak('Hello!')}>
49
+ * {isSynthesizing ? 'Loading...' : isPlaying ? 'Stop' : 'Speak'}
50
+ * </button>
51
+ * {error && <p style={{ color: 'red' }}>{error}</p>}
52
+ * </div>
53
+ * );
54
+ * }
55
+ * ```
56
+ *
57
+ * @returns TTS controls and state
58
+ */
59
+ export declare function useTTS(): UseTTSResult;
package/dist/index.cjs CHANGED
@@ -339,7 +339,8 @@ function useEdit() {
339
339
  * Simplified hook for voice command workflows
340
340
  *
341
341
  * Provides an easy-to-use interface for voice command matching
342
- * with automatic state management.
342
+ * with automatic state management. Supports matching multiple
343
+ * commands from a single voice input.
343
344
  *
344
345
  * @example
345
346
  * ```tsx
@@ -354,7 +355,7 @@ function useEdit() {
354
355
  * ];
355
356
  *
356
357
  * function VoiceCommands() {
357
- * const { start, stop, isListening, isProcessing, result, error } = useCommand();
358
+ * const { start, stop, isListening, isProcessing, results, error } = useCommand();
358
359
  *
359
360
  * const handleCommand = async () => {
360
361
  * await start(commands);
@@ -362,9 +363,9 @@ function useEdit() {
362
363
  *
363
364
  * const handleStop = async () => {
364
365
  * const matched = await stop();
365
- * if (matched) {
366
- * console.log('Matched command:', matched.name, matched.arguments);
367
- * }
366
+ * matched.forEach(cmd => {
367
+ * console.log('Matched command:', cmd.name, cmd.arguments);
368
+ * });
368
369
  * };
369
370
  *
370
371
  * return (
@@ -373,7 +374,11 @@ function useEdit() {
373
374
  * {isListening ? 'Execute' : 'Say Command'}
374
375
  * </button>
375
376
  * {isProcessing && <span>Processing...</span>}
376
- * {result && <p>Command: {result.name}</p>}
377
+ * {results.length > 0 && (
378
+ * <div>
379
+ * {results.map((cmd, i) => <p key={i}>Command: {cmd.name}</p>)}
380
+ * </div>
381
+ * )}
377
382
  * {error && <p style={{ color: 'red' }}>{error}</p>}
378
383
  * </div>
379
384
  * );
@@ -384,7 +389,7 @@ function useEdit() {
384
389
  */
385
390
  function useCommand() {
386
391
  const { state: state$2, command, stopCommand } = useSpeechOSContext();
387
- const [result, setResult] = (0, react.useState)(null);
392
+ const [results, setResults] = (0, react.useState)([]);
388
393
  const [error, setError] = (0, react.useState)(null);
389
394
  const isListening = state$2.recordingState === "recording" && state$2.activeAction === "command";
390
395
  const isProcessing = state$2.recordingState === "processing";
@@ -399,10 +404,10 @@ function useCommand() {
399
404
  }, [command]);
400
405
  const stop = (0, react.useCallback)(async () => {
401
406
  try {
402
- const commandResult = await stopCommand();
403
- setResult(commandResult);
407
+ const commandResults = await stopCommand();
408
+ setResults(commandResults);
404
409
  setError(null);
405
- return commandResult;
410
+ return commandResults;
406
411
  } catch (err) {
407
412
  const message = err instanceof Error ? err.message : "Failed to process command";
408
413
  setError(message);
@@ -410,7 +415,7 @@ function useCommand() {
410
415
  }
411
416
  }, [stopCommand]);
412
417
  const clear = (0, react.useCallback)(() => {
413
- setResult(null);
418
+ setResults([]);
414
419
  setError(null);
415
420
  }, []);
416
421
  return {
@@ -418,7 +423,124 @@ function useCommand() {
418
423
  stop,
419
424
  isListening,
420
425
  isProcessing,
421
- result,
426
+ results,
427
+ error,
428
+ clear
429
+ };
430
+ }
431
+
432
+ //#endregion
433
+ //#region src/hooks/useTTS.ts
434
+ let clientTTS = null;
435
+ async function getClientTTS() {
436
+ if (clientTTS) return clientTTS;
437
+ try {
438
+ const client = await import("@speechos/client");
439
+ clientTTS = client.tts;
440
+ return clientTTS;
441
+ } catch {
442
+ return null;
443
+ }
444
+ }
445
+ /**
446
+ * React hook for text-to-speech
447
+ *
448
+ * Provides an easy-to-use interface for TTS synthesis and playback
449
+ * with automatic state management.
450
+ *
451
+ * @example
452
+ * ```tsx
453
+ * function TTSButton() {
454
+ * const { speak, stop, isSynthesizing, isPlaying, error } = useTTS();
455
+ *
456
+ * return (
457
+ * <div>
458
+ * <button onClick={() => speak('Hello!')}>
459
+ * {isSynthesizing ? 'Loading...' : isPlaying ? 'Stop' : 'Speak'}
460
+ * </button>
461
+ * {error && <p style={{ color: 'red' }}>{error}</p>}
462
+ * </div>
463
+ * );
464
+ * }
465
+ * ```
466
+ *
467
+ * @returns TTS controls and state
468
+ */
469
+ function useTTS() {
470
+ const [isSynthesizing, setIsSynthesizing] = (0, react.useState)(false);
471
+ const [isPlaying, setIsPlaying] = (0, react.useState)(false);
472
+ const [audioResult, setAudioResult] = (0, react.useState)(null);
473
+ const [error, setError] = (0, react.useState)(null);
474
+ (0, react.useEffect)(() => {
475
+ const unsubStart = __speechos_core.events.on("tts:playback:start", () => {
476
+ setIsPlaying(true);
477
+ });
478
+ const unsubComplete = __speechos_core.events.on("tts:playback:complete", () => {
479
+ setIsPlaying(false);
480
+ });
481
+ const unsubStop = __speechos_core.events.on("tts:playback:stop", () => {
482
+ setIsPlaying(false);
483
+ });
484
+ const unsubError = __speechos_core.events.on("tts:error", ({ message, phase }) => {
485
+ if (phase === "playback") setIsPlaying(false);
486
+ setError(message);
487
+ });
488
+ return () => {
489
+ unsubStart();
490
+ unsubComplete();
491
+ unsubStop();
492
+ unsubError();
493
+ };
494
+ }, []);
495
+ const speak = (0, react.useCallback)(async (text, options) => {
496
+ setError(null);
497
+ setIsSynthesizing(true);
498
+ try {
499
+ const client = await getClientTTS();
500
+ if (client) await client.speak(text, options);
501
+ else {
502
+ await __speechos_core.tts.synthesize(text, options);
503
+ console.warn("[useTTS] @speechos/client not available, audio not played");
504
+ }
505
+ } catch (err) {
506
+ const message = err instanceof Error ? err.message : "TTS failed";
507
+ setError(message);
508
+ throw err;
509
+ } finally {
510
+ setIsSynthesizing(false);
511
+ }
512
+ }, []);
513
+ const synthesize = (0, react.useCallback)(async (text, options) => {
514
+ setError(null);
515
+ setIsSynthesizing(true);
516
+ try {
517
+ const result = await __speechos_core.tts.synthesize(text, options);
518
+ setAudioResult(result);
519
+ return result;
520
+ } catch (err) {
521
+ const message = err instanceof Error ? err.message : "Synthesis failed";
522
+ setError(message);
523
+ throw err;
524
+ } finally {
525
+ setIsSynthesizing(false);
526
+ }
527
+ }, []);
528
+ const stop = (0, react.useCallback)(async () => {
529
+ const client = await getClientTTS();
530
+ if (client) client.stop();
531
+ setIsPlaying(false);
532
+ }, []);
533
+ const clear = (0, react.useCallback)(() => {
534
+ setAudioResult(null);
535
+ setError(null);
536
+ }, []);
537
+ return {
538
+ speak,
539
+ synthesize,
540
+ stop,
541
+ isSynthesizing,
542
+ isPlaying,
543
+ audioResult,
422
544
  error,
423
545
  clear
424
546
  };
@@ -599,4 +721,5 @@ exports.useSpeechOS = useSpeechOS;
599
721
  exports.useSpeechOSContext = useSpeechOSContext;
600
722
  exports.useSpeechOSEvents = useSpeechOSEvents;
601
723
  exports.useSpeechOSState = useSpeechOSState;
602
- exports.useSpeechOSWidget = useSpeechOSWidget;
724
+ exports.useSpeechOSWidget = useSpeechOSWidget;
725
+ exports.useTTS = useTTS;
package/dist/index.d.cts CHANGED
@@ -37,8 +37,8 @@
37
37
  *
38
38
  */
39
39
  export { SpeechOSProvider, SpeechOSContext, useSpeechOSContext, type SpeechOSContextValue, type SpeechOSProviderProps, type SpeechOSReactConfig, } from "./context.js";
40
- export { useSpeechOS, useSpeechOSState, useSpeechOSEvents, useDictation, useEdit, useCommand, useSpeechOSWidget, type UseDictationResult, type UseEditResult, type UseCommandResult, type UseSpeechOSWidgetResult, } from "./hooks/index.js";
40
+ export { useSpeechOS, useSpeechOSState, useSpeechOSEvents, useDictation, useEdit, useCommand, useTTS, useSpeechOSWidget, type UseDictationResult, type UseEditResult, type UseCommandResult, type UseTTSResult, type SpeakOptions, type UseSpeechOSWidgetResult, } from "./hooks/index.js";
41
41
  export { SpeechOSWidget, type SpeechOSWidgetProps, } from "./components/index.js";
42
- export type { SpeechOSCoreConfig, SpeechOSState, SpeechOSAction, SpeechOSEventMap, RecordingState, UnsubscribeFn, CommandArgument, CommandDefinition, CommandResult, } from "@speechos/core";
42
+ export type { SpeechOSCoreConfig, SpeechOSState, SpeechOSAction, SpeechOSEventMap, RecordingState, UnsubscribeFn, CommandArgument, CommandDefinition, CommandResult, TTSOptions, TTSResult, } from "@speechos/core";
43
43
  export type { SpeechOSCoreConfig as SpeechOSConfig } from "@speechos/core";
44
44
  export declare const VERSION = "0.1.0";
package/dist/index.d.ts CHANGED
@@ -37,8 +37,8 @@
37
37
  *
38
38
  */
39
39
  export { SpeechOSProvider, SpeechOSContext, useSpeechOSContext, type SpeechOSContextValue, type SpeechOSProviderProps, type SpeechOSReactConfig, } from "./context.js";
40
- export { useSpeechOS, useSpeechOSState, useSpeechOSEvents, useDictation, useEdit, useCommand, useSpeechOSWidget, type UseDictationResult, type UseEditResult, type UseCommandResult, type UseSpeechOSWidgetResult, } from "./hooks/index.js";
40
+ export { useSpeechOS, useSpeechOSState, useSpeechOSEvents, useDictation, useEdit, useCommand, useTTS, useSpeechOSWidget, type UseDictationResult, type UseEditResult, type UseCommandResult, type UseTTSResult, type SpeakOptions, type UseSpeechOSWidgetResult, } from "./hooks/index.js";
41
41
  export { SpeechOSWidget, type SpeechOSWidgetProps, } from "./components/index.js";
42
- export type { SpeechOSCoreConfig, SpeechOSState, SpeechOSAction, SpeechOSEventMap, RecordingState, UnsubscribeFn, CommandArgument, CommandDefinition, CommandResult, } from "@speechos/core";
42
+ export type { SpeechOSCoreConfig, SpeechOSState, SpeechOSAction, SpeechOSEventMap, RecordingState, UnsubscribeFn, CommandArgument, CommandDefinition, CommandResult, TTSOptions, TTSResult, } from "@speechos/core";
43
43
  export type { SpeechOSCoreConfig as SpeechOSConfig } from "@speechos/core";
44
44
  export declare const VERSION = "0.1.0";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
- import { events, speechOS, state } from "@speechos/core";
2
+ import { events, speechOS, state, tts } from "@speechos/core";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
 
5
5
  //#region src/context.tsx
@@ -316,7 +316,8 @@ function useEdit() {
316
316
  * Simplified hook for voice command workflows
317
317
  *
318
318
  * Provides an easy-to-use interface for voice command matching
319
- * with automatic state management.
319
+ * with automatic state management. Supports matching multiple
320
+ * commands from a single voice input.
320
321
  *
321
322
  * @example
322
323
  * ```tsx
@@ -331,7 +332,7 @@ function useEdit() {
331
332
  * ];
332
333
  *
333
334
  * function VoiceCommands() {
334
- * const { start, stop, isListening, isProcessing, result, error } = useCommand();
335
+ * const { start, stop, isListening, isProcessing, results, error } = useCommand();
335
336
  *
336
337
  * const handleCommand = async () => {
337
338
  * await start(commands);
@@ -339,9 +340,9 @@ function useEdit() {
339
340
  *
340
341
  * const handleStop = async () => {
341
342
  * const matched = await stop();
342
- * if (matched) {
343
- * console.log('Matched command:', matched.name, matched.arguments);
344
- * }
343
+ * matched.forEach(cmd => {
344
+ * console.log('Matched command:', cmd.name, cmd.arguments);
345
+ * });
345
346
  * };
346
347
  *
347
348
  * return (
@@ -350,7 +351,11 @@ function useEdit() {
350
351
  * {isListening ? 'Execute' : 'Say Command'}
351
352
  * </button>
352
353
  * {isProcessing && <span>Processing...</span>}
353
- * {result && <p>Command: {result.name}</p>}
354
+ * {results.length > 0 && (
355
+ * <div>
356
+ * {results.map((cmd, i) => <p key={i}>Command: {cmd.name}</p>)}
357
+ * </div>
358
+ * )}
354
359
  * {error && <p style={{ color: 'red' }}>{error}</p>}
355
360
  * </div>
356
361
  * );
@@ -361,7 +366,7 @@ function useEdit() {
361
366
  */
362
367
  function useCommand() {
363
368
  const { state: state$1, command, stopCommand } = useSpeechOSContext();
364
- const [result, setResult] = useState(null);
369
+ const [results, setResults] = useState([]);
365
370
  const [error, setError] = useState(null);
366
371
  const isListening = state$1.recordingState === "recording" && state$1.activeAction === "command";
367
372
  const isProcessing = state$1.recordingState === "processing";
@@ -376,10 +381,10 @@ function useCommand() {
376
381
  }, [command]);
377
382
  const stop = useCallback(async () => {
378
383
  try {
379
- const commandResult = await stopCommand();
380
- setResult(commandResult);
384
+ const commandResults = await stopCommand();
385
+ setResults(commandResults);
381
386
  setError(null);
382
- return commandResult;
387
+ return commandResults;
383
388
  } catch (err) {
384
389
  const message = err instanceof Error ? err.message : "Failed to process command";
385
390
  setError(message);
@@ -387,7 +392,7 @@ function useCommand() {
387
392
  }
388
393
  }, [stopCommand]);
389
394
  const clear = useCallback(() => {
390
- setResult(null);
395
+ setResults([]);
391
396
  setError(null);
392
397
  }, []);
393
398
  return {
@@ -395,7 +400,124 @@ function useCommand() {
395
400
  stop,
396
401
  isListening,
397
402
  isProcessing,
398
- result,
403
+ results,
404
+ error,
405
+ clear
406
+ };
407
+ }
408
+
409
+ //#endregion
410
+ //#region src/hooks/useTTS.ts
411
+ let clientTTS = null;
412
+ async function getClientTTS() {
413
+ if (clientTTS) return clientTTS;
414
+ try {
415
+ const client = await import("@speechos/client");
416
+ clientTTS = client.tts;
417
+ return clientTTS;
418
+ } catch {
419
+ return null;
420
+ }
421
+ }
422
+ /**
423
+ * React hook for text-to-speech
424
+ *
425
+ * Provides an easy-to-use interface for TTS synthesis and playback
426
+ * with automatic state management.
427
+ *
428
+ * @example
429
+ * ```tsx
430
+ * function TTSButton() {
431
+ * const { speak, stop, isSynthesizing, isPlaying, error } = useTTS();
432
+ *
433
+ * return (
434
+ * <div>
435
+ * <button onClick={() => speak('Hello!')}>
436
+ * {isSynthesizing ? 'Loading...' : isPlaying ? 'Stop' : 'Speak'}
437
+ * </button>
438
+ * {error && <p style={{ color: 'red' }}>{error}</p>}
439
+ * </div>
440
+ * );
441
+ * }
442
+ * ```
443
+ *
444
+ * @returns TTS controls and state
445
+ */
446
+ function useTTS() {
447
+ const [isSynthesizing, setIsSynthesizing] = useState(false);
448
+ const [isPlaying, setIsPlaying] = useState(false);
449
+ const [audioResult, setAudioResult] = useState(null);
450
+ const [error, setError] = useState(null);
451
+ useEffect(() => {
452
+ const unsubStart = events.on("tts:playback:start", () => {
453
+ setIsPlaying(true);
454
+ });
455
+ const unsubComplete = events.on("tts:playback:complete", () => {
456
+ setIsPlaying(false);
457
+ });
458
+ const unsubStop = events.on("tts:playback:stop", () => {
459
+ setIsPlaying(false);
460
+ });
461
+ const unsubError = events.on("tts:error", ({ message, phase }) => {
462
+ if (phase === "playback") setIsPlaying(false);
463
+ setError(message);
464
+ });
465
+ return () => {
466
+ unsubStart();
467
+ unsubComplete();
468
+ unsubStop();
469
+ unsubError();
470
+ };
471
+ }, []);
472
+ const speak = useCallback(async (text, options) => {
473
+ setError(null);
474
+ setIsSynthesizing(true);
475
+ try {
476
+ const client = await getClientTTS();
477
+ if (client) await client.speak(text, options);
478
+ else {
479
+ await tts.synthesize(text, options);
480
+ console.warn("[useTTS] @speechos/client not available, audio not played");
481
+ }
482
+ } catch (err) {
483
+ const message = err instanceof Error ? err.message : "TTS failed";
484
+ setError(message);
485
+ throw err;
486
+ } finally {
487
+ setIsSynthesizing(false);
488
+ }
489
+ }, []);
490
+ const synthesize = useCallback(async (text, options) => {
491
+ setError(null);
492
+ setIsSynthesizing(true);
493
+ try {
494
+ const result = await tts.synthesize(text, options);
495
+ setAudioResult(result);
496
+ return result;
497
+ } catch (err) {
498
+ const message = err instanceof Error ? err.message : "Synthesis failed";
499
+ setError(message);
500
+ throw err;
501
+ } finally {
502
+ setIsSynthesizing(false);
503
+ }
504
+ }, []);
505
+ const stop = useCallback(async () => {
506
+ const client = await getClientTTS();
507
+ if (client) client.stop();
508
+ setIsPlaying(false);
509
+ }, []);
510
+ const clear = useCallback(() => {
511
+ setAudioResult(null);
512
+ setError(null);
513
+ }, []);
514
+ return {
515
+ speak,
516
+ synthesize,
517
+ stop,
518
+ isSynthesizing,
519
+ isPlaying,
520
+ audioResult,
399
521
  error,
400
522
  clear
401
523
  };
@@ -565,4 +687,4 @@ function SpeechOSWidget({ apiKey, userId, host, zIndex, debug, onTranscription,
565
687
  const VERSION = "0.1.0";
566
688
 
567
689
  //#endregion
568
- export { SpeechOSContext, SpeechOSProvider, SpeechOSWidget, VERSION, useCommand, useDictation, useEdit, useSpeechOS, useSpeechOSContext, useSpeechOSEvents, useSpeechOSState, useSpeechOSWidget };
690
+ export { SpeechOSContext, SpeechOSProvider, SpeechOSWidget, VERSION, useCommand, useDictation, useEdit, useSpeechOS, useSpeechOSContext, useSpeechOSEvents, useSpeechOSState, useSpeechOSWidget, useTTS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speechos/react",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "React hooks and components for SpeechOS voice integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -48,8 +48,8 @@
48
48
  "author": "SpeechOS",
49
49
  "license": "MIT",
50
50
  "peerDependencies": {
51
- "@speechos/client": "^0.2.10",
52
- "@speechos/core": "^0.2.9",
51
+ "@speechos/client": "^0.2.12",
52
+ "@speechos/core": "^0.2.11",
53
53
  "react": ">=18.0.0"
54
54
  },
55
55
  "peerDependenciesMeta": {
@@ -69,6 +69,6 @@
69
69
  "vitest": "^4.0.16"
70
70
  },
71
71
  "dependencies": {
72
- "@speechos/core": "^0.2.9"
72
+ "@speechos/core": "^0.2.11"
73
73
  }
74
74
  }