@ai-matrx/media 0.14.0 → 0.15.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @ai-matrx/media
2
2
 
3
+ ## 0.15.0 — THE one speech engine (`/speech`) + ReadAloudButton compact size
4
+
5
+ - New `@ai-matrx/media/speech`: `speak()`, the app-wide playback queue (`enqueuePlayback`,
6
+ `pausePlayback`, `resumePlayback`, `skipPlayback`, `playPlaybackItem`, `removePlaybackItem`,
7
+ `clearPlayback`, `subscribePlayback`, …), the playback lock (`claimPlayback` / `releasePlayback`),
8
+ the Cartesia streaming lane (`connectCartesiaTts`, `cartesiaAdapter`, `ProviderSessionError`),
9
+ `SinkAwarePlayer` + output-device routing, and the iOS/WebKit unlock (`primeAudioOutput`,
10
+ `installAudioUnlockListeners`) — moved from matrx-frontend's audio system, which is the reference.
11
+ A host wires it once: `configureSpeech({ getCartesiaToken, resolveVoiceSettings,
12
+ reportProviderFailure?, notifyError?, beforeStart?, describeStartRefusal?, onEngage?, adapters?,
13
+ defaultEngine?, completeRequest?, resolvePronunciations?, invalidateCartesiaToken? })`.
14
+ - `@ai-matrx/media/react`: `useSpeech`, `useReadAloud(text)` (the read-aloud button's whole wiring:
15
+ `status`, `error`, `onPress`), `usePlaybackSnapshot`, `readAloudStatusOf` — over the same queue
16
+ instance (`/react` imports `/speech`, never a bundled copy).
17
+ - New dependency `@cartesia/cartesia-js` ^4.2.0.
18
+ - Consumer action: delete the app's own queue / lock / Cartesia connection / player / unlock and
19
+ import them from `/speech`; call `configureSpeech` once at start-up.
20
+
21
+ - `ReadAloudButton` `size?: "default" | "compact"` (`ReadAloudButtonSize` exported): "compact" is the
22
+ message-footer cell (32px box, 28px pill — the tap system's group geometry) instead of the 38px
23
+ default box. Geometry only; it wins over `variant`.
24
+ - Consumer action: a message footer that drew the old 24px speaker passes `size="compact"`.
25
+
3
26
  ## 0.14.0 — Read-aloud lives here (speech text, languages, button, language picker)
4
27
 
5
28
  - `@ai-matrx/media/voices`: `parseMarkdownToText` (+ `normalizeSpeechAbbreviations`,
package/dist/react.cjs CHANGED
@@ -70,7 +70,11 @@ __export(react_exports, {
70
70
  VolumeXIcon: () => VolumeXIcon,
71
71
  XIcon: () => XIcon,
72
72
  cn: () => cn,
73
- useMediaDropzone: () => useMediaDropzone
73
+ readAloudStatusOf: () => readAloudStatusOf,
74
+ useMediaDropzone: () => useMediaDropzone,
75
+ usePlaybackSnapshot: () => usePlaybackSnapshot,
76
+ useReadAloud: () => useReadAloud,
77
+ useSpeech: () => useSpeech
74
78
  });
75
79
  module.exports = __toCommonJS(react_exports);
76
80
 
@@ -2542,10 +2546,12 @@ function ReadAloudButton({
2542
2546
  onPress,
2543
2547
  error,
2544
2548
  disabled = false,
2545
- variant,
2549
+ variant: requestedVariant,
2550
+ size = "default",
2546
2551
  className,
2547
2552
  label = "Read aloud"
2548
2553
  }) {
2554
+ const variant = size === "compact" ? "group" : requestedVariant;
2549
2555
  if (status === "playing") {
2550
2556
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_buttons.PauseTapButton, { variant, onClick: onPress, disabled, ariaLabel: "Pause", className });
2551
2557
  }
@@ -2574,10 +2580,84 @@ function ReadAloudButton({
2574
2580
  );
2575
2581
  }
2576
2582
 
2583
+ // src/react/use-speech.ts
2584
+ var import_react17 = require("react");
2585
+ var import_speech = require("@ai-matrx/media/speech");
2586
+ var cached = (0, import_speech.getPlaybackSnapshot)();
2587
+ function subscribe(onChange) {
2588
+ return (0, import_speech.subscribePlayback)((snapshot) => {
2589
+ cached = snapshot;
2590
+ onChange();
2591
+ });
2592
+ }
2593
+ var read = () => cached;
2594
+ function usePlaybackSnapshot() {
2595
+ return (0, import_react17.useSyncExternalStore)(subscribe, read, read);
2596
+ }
2597
+ function useSpeech({ adoptText, ...request } = {}) {
2598
+ const { items, currentId } = usePlaybackSnapshot();
2599
+ const [itemId, setItemId] = (0, import_react17.useState)(null);
2600
+ const adoptedId = !itemId && adoptText && currentId ? items.find(
2601
+ (i) => i.id === currentId && i.text === adoptText && (i.status === "playing" || i.status === "paused" || i.status === "loading")
2602
+ )?.id ?? null : null;
2603
+ const effectiveId = itemId ?? adoptedId;
2604
+ const { engine, voice, purpose, processMarkdown = true, label, dictionarySurfaceKey, language, speed } = request;
2605
+ const speak = (0, import_react17.useCallback)(
2606
+ (text) => {
2607
+ const { id } = (0, import_speech.speak)({
2608
+ text,
2609
+ engine,
2610
+ voice,
2611
+ purpose,
2612
+ processMarkdown,
2613
+ label,
2614
+ dictionarySurfaceKey,
2615
+ language,
2616
+ speed
2617
+ });
2618
+ setItemId(id);
2619
+ return id;
2620
+ },
2621
+ [engine, voice, purpose, processMarkdown, label, dictionarySurfaceKey, language, speed]
2622
+ );
2623
+ const item = effectiveId ? items.find((i) => i.id === effectiveId) : void 0;
2624
+ const status = item?.status ?? null;
2625
+ return {
2626
+ speak,
2627
+ status,
2628
+ error: item?.error ?? null,
2629
+ itemId,
2630
+ isMine: effectiveId !== null && effectiveId === currentId,
2631
+ pause: import_speech.pausePlayback,
2632
+ resume: import_speech.resumePlayback,
2633
+ skip: import_speech.skipPlayback,
2634
+ remove: import_speech.removePlaybackItem,
2635
+ playItem: import_speech.playPlaybackItem
2636
+ };
2637
+ }
2638
+ function readAloudStatusOf(status) {
2639
+ if (status === "playing") return "playing";
2640
+ if (status === "paused") return "paused";
2641
+ if (status === "loading" || status === "queued") return "loading";
2642
+ if (status === "error") return "error";
2643
+ return "idle";
2644
+ }
2645
+ function useReadAloud(text, options = {}) {
2646
+ const { speak, status, error, pause, resume } = useSpeech({ adoptText: text, ...options });
2647
+ const state = readAloudStatusOf(status);
2648
+ const onPress = (0, import_react17.useCallback)(() => {
2649
+ if (state === "loading") return;
2650
+ if (state === "playing") void pause();
2651
+ else if (state === "paused") void resume();
2652
+ else if (text.trim()) speak(text);
2653
+ }, [state, text, speak, pause, resume]);
2654
+ return { status: state, error, onPress, speak };
2655
+ }
2656
+
2577
2657
  // src/react/read-aloud-language-picker.tsx
2578
2658
  var import_design_system = require("@ai-matrx/design-system");
2579
2659
  var import_lucide_react = require("lucide-react");
2580
- var import_react17 = require("react");
2660
+ var import_react18 = require("react");
2581
2661
 
2582
2662
  // src/voices/read-aloud-languages.ts
2583
2663
  var READ_ALOUD_LANGUAGES = [
@@ -2605,7 +2685,7 @@ function readAloudLanguage(code) {
2605
2685
  // src/react/read-aloud-language-picker.tsx
2606
2686
  var import_jsx_runtime10 = require("react/jsx-runtime");
2607
2687
  function ReadAloudLanguagePicker({ value, onChange, className }) {
2608
- const [open, setOpen] = (0, import_react17.useState)(false);
2688
+ const [open, setOpen] = (0, import_react18.useState)(false);
2609
2689
  const selected = readAloudLanguage(value);
2610
2690
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_design_system.Popover, { open, onOpenChange: setOpen, children: [
2611
2691
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_design_system.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(