@callimacus/thamyr-react-native 4.3.4
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.core.md +1747 -0
- package/CHANGELOG.md +268 -0
- package/LICENSE.md +73 -0
- package/MIGRATION.md +271 -0
- package/README.md +173 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/query.js +9 -0
- package/dist/esm/query.js.map +1 -0
- package/dist/types/index.d.ts +277 -0
- package/dist/types/query.d.ts +8 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @callimacus/thamyr-react-native
|
|
2
|
+
|
|
3
|
+
The Thamyr SDK for React Native (Expo and bare). It wraps
|
|
4
|
+
`@callimacus/thamyr-react`, so the hooks, store, and response types are the same
|
|
5
|
+
ones the web SDK exposes — this package narrows them to what works on native and
|
|
6
|
+
adds a native voice recorder.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @callimacus/thamyr-react-native
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Peer dependencies: `react >=18`, `react-native >=0.72`.
|
|
13
|
+
|
|
14
|
+
## Getting started
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import {useInitThamyr, useThamyr, useConnection} from '@callimacus/thamyr-react-native';
|
|
18
|
+
|
|
19
|
+
function App() {
|
|
20
|
+
useInitThamyr({clientId: 'cal-pk-…'});
|
|
21
|
+
return <Chat />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Chat() {
|
|
25
|
+
const {history, sendEvent} = useThamyr();
|
|
26
|
+
const {status} = useConnection();
|
|
27
|
+
// …
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What differs from the web SDK
|
|
32
|
+
|
|
33
|
+
Everything not listed here behaves identically, because it is literally the same
|
|
34
|
+
code.
|
|
35
|
+
|
|
36
|
+
### Voice recording returns bytes
|
|
37
|
+
|
|
38
|
+
The web recorder hands you a `Blob`; this one hands you a `Uint8Array`. Audio
|
|
39
|
+
travels as `inputEvent.data.file` over socket.io, and only typed arrays survive
|
|
40
|
+
that trip on native: a React Native `Blob` is a handle into the native
|
|
41
|
+
BlobModule whose bytes never exist in JS, so socket.io cannot read it, and
|
|
42
|
+
`new File([...])` — what the web path uses — has no useful native equivalent.
|
|
43
|
+
The server drops the filename and mime type either way (socket.io's binary
|
|
44
|
+
framing strips them), so nothing is lost.
|
|
45
|
+
|
|
46
|
+
The recorder needs an adapter, which you build from `expo-audio`:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import {useMemo} from 'react';
|
|
50
|
+
import * as ExpoAudio from 'expo-audio';
|
|
51
|
+
import {
|
|
52
|
+
createExpoAudioAdapter,
|
|
53
|
+
useVoiceRecorder,
|
|
54
|
+
useThamyr,
|
|
55
|
+
SLInputEventType,
|
|
56
|
+
UserInteractionType,
|
|
57
|
+
} from '@callimacus/thamyr-react-native';
|
|
58
|
+
|
|
59
|
+
function VoiceButton() {
|
|
60
|
+
const {sendEvent} = useThamyr();
|
|
61
|
+
const adapter = useMemo(() => createExpoAudioAdapter(ExpoAudio), []);
|
|
62
|
+
|
|
63
|
+
const {state, audioLevels, startRecording, stopRecording, error} = useVoiceRecorder({
|
|
64
|
+
adapter,
|
|
65
|
+
onRecordingComplete(audio) {
|
|
66
|
+
sendEvent(UserInteractionType.CREATE_ROUND, {
|
|
67
|
+
inputEvent: {type: SLInputEventType.question, data: {value: '', file: audio}},
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// …
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The adapter is passed in rather than defaulted on purpose. If this package
|
|
76
|
+
imported `expo-audio` itself, Metro would resolve it statically for every
|
|
77
|
+
consumer, so an app that never records audio would fail to bundle merely for not
|
|
78
|
+
having installed it. Handing the module in also keeps `expo-audio` out of this
|
|
79
|
+
package's dependency graph entirely, so it typechecks and tests without a native
|
|
80
|
+
toolchain. For a recorder other than `expo-audio`, implement
|
|
81
|
+
`VoiceRecorderAdapter` yourself — `start`/`stop`/`cancel` over any native module.
|
|
82
|
+
|
|
83
|
+
Recording auto-stops after `silenceDuration` (default 1800 ms) below
|
|
84
|
+
`silenceThreshold` (default −45 dBFS), and hard-stops at `maxDuration` (default
|
|
85
|
+
30 s).
|
|
86
|
+
|
|
87
|
+
### Hooks that are not re-exported
|
|
88
|
+
|
|
89
|
+
| Not available | Why | Instead |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `useTtsAudio` | Constructs an `HTMLAudioElement` (`new Audio(url)`) | Play the URL from `extractTtsAudio(block.data)` with an `expo-audio` player |
|
|
92
|
+
| `uiObserver` config | Snapshots mean `querySelectorAll` + `getBoundingClientRect` | Not yet available on native (SOL-1084) |
|
|
93
|
+
| `setConsent`, `setGeo`, `initIntent`, … | `@callimacus/intent`'s browser API | `@callimacus/intent/native` — see below |
|
|
94
|
+
|
|
95
|
+
`extractTtsAudio` and the `TtsAudio` type *are* exported, so you can get the
|
|
96
|
+
audio URL out of a response and play it however you like; only the web playback
|
|
97
|
+
hook is missing.
|
|
98
|
+
|
|
99
|
+
### Intent tracking
|
|
100
|
+
|
|
101
|
+
The intent web build cannot be imported on native at all: it reads
|
|
102
|
+
`localStorage` and `document` at module scope, which throws under Hermes before
|
|
103
|
+
your app renders. On React Native the SDK therefore resolves
|
|
104
|
+
`@callimacus/thamyr-core`'s `react-native` entry, where intent is absent — its
|
|
105
|
+
functions are present as warn-once no-ops so shared code does not crash, but they
|
|
106
|
+
do nothing.
|
|
107
|
+
|
|
108
|
+
Intent has its own native entry with a different shape. Wire it up directly:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import * as RN from 'react-native';
|
|
112
|
+
import {MMKV} from 'react-native-mmkv';
|
|
113
|
+
import {createNativeAdapters, initIntentNative, mmkvStorage} from '@callimacus/intent/native';
|
|
114
|
+
|
|
115
|
+
initIntentNative({
|
|
116
|
+
clientId: 'cal-pk-…',
|
|
117
|
+
consent: true,
|
|
118
|
+
adapters: createNativeAdapters(RN, {storage: mmkvStorage(new MMKV())}),
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Transports, and why voice does not work yet
|
|
123
|
+
|
|
124
|
+
Text chat works. **Sending audio does not**, and the cause is neither this package
|
|
125
|
+
nor your app — it is a TLS mismatch that forces the connection onto a transport
|
|
126
|
+
which cannot carry binary on React Native. The detail matters because the symptom
|
|
127
|
+
is misleading.
|
|
128
|
+
|
|
129
|
+
**React Native cannot open a WebSocket to `api.callimacus.ai`.** The host is
|
|
130
|
+
TLS 1.3-only — it rejects a TLS 1.2 handshake outright with a
|
|
131
|
+
`protocol_version` alert (verified with `openssl s_client -no_tls1_3`). React
|
|
132
|
+
Native's WebSocket fails against it with close code 1006 and
|
|
133
|
+
`OSStatus -9836`, which is Apple's `errSSLPeerProtocolVersion` ("bad protocol
|
|
134
|
+
version"). React Native's *HTTP* stack is fine with the same host, since that
|
|
135
|
+
goes through `NSURLSession`, which does negotiate TLS 1.3 — consistent with the
|
|
136
|
+
WebSocket path using the older SecureTransport route that does not.
|
|
137
|
+
|
|
138
|
+
The SDK therefore falls back to HTTP long-polling, and text works over it.
|
|
139
|
+
|
|
140
|
+
**But binary cannot traverse polling on React Native.** engine.io's polling
|
|
141
|
+
payload is text, so `encodePayload` encodes every packet with
|
|
142
|
+
`supportsBinary: false` ("force base64 encoding for binary packets"), and in
|
|
143
|
+
engine.io-parser's browser build that path does
|
|
144
|
+
`encodeBlobAsBase64(new Blob([data]))` — while React Native refuses to construct
|
|
145
|
+
a Blob from an `ArrayBuffer` at all ("Creating blobs from 'ArrayBuffer' and
|
|
146
|
+
'ArrayBufferView' are not supported"). So the audio bytes are dropped with a
|
|
147
|
+
console error. `forceBase64` cannot help: polling already forces it, and that is
|
|
148
|
+
precisely the branch that builds the Blob.
|
|
149
|
+
|
|
150
|
+
**The fix is to let React Native use the WebSocket transport, by enabling TLS 1.2
|
|
151
|
+
on the API host** (nginx: keep `TLSv1.3` and add `TLSv1.2` with a matching cipher
|
|
152
|
+
suite). Over WebSocket, `encodePacket` runs with `supportsBinary: true` and hands
|
|
153
|
+
the typed array straight to `WebSocket.send()` — no Blob, no base64 — so audio
|
|
154
|
+
works with no further SDK change, and text gets lower latency as a bonus.
|
|
155
|
+
|
|
156
|
+
Until then, recording works and `onRecordingComplete` gives you correct bytes;
|
|
157
|
+
only the send fails.
|
|
158
|
+
|
|
159
|
+
## Things worth knowing
|
|
160
|
+
|
|
161
|
+
- **UUIDs.** Hermes ships no global `crypto`. Core falls back to
|
|
162
|
+
`crypto.getRandomValues` and then `Math.random()`, so nothing throws, but
|
|
163
|
+
installing [`react-native-get-random-values`](https://github.com/LinusU/react-native-get-random-values)
|
|
164
|
+
at the top of your entry file gets you cryptographically random interaction ids.
|
|
165
|
+
- **`gatherClientInfo()`** returns empty strings for `userAgent`, `language`, and
|
|
166
|
+
`screenResolution` on native — there is no `navigator.userAgent` or
|
|
167
|
+
`window.screen`. Read screen size from `Dimensions` if you need it.
|
|
168
|
+
- **Microphone permissions.** Add `NSMicrophoneUsageDescription` (iOS) and
|
|
169
|
+
`RECORD_AUDIO` (Android). Under Expo, `expo-audio`'s config plugin does this.
|
|
170
|
+
- **Socket auth** goes through socket.io's `auth` payload, not an
|
|
171
|
+
`Authorization` header — React Native's WebSocket transport ignores
|
|
172
|
+
`extraHeaders`, and the server reads `handshake.auth.token`. No action needed;
|
|
173
|
+
it is noted because the header being dropped looks alarming in logs.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright © 2025–2026 Solomei AI SRL. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Proprietary software, licensed for use by Callimacus customers only.
|
|
5
|
+
* See LICENSE.md for the full terms.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import{useInitThamyr as e}from"@callimacus/thamyr-react";export{CallimacusEvent,ChapterStatus,ClientEvents,GalleryMode,SLInputEventType,ServerEvents,ThamyrHttpError,ThamyrResponseType,UserInteractionType,callimacusService,extractTtsAudio,isSetProduct,logger,thamyrApi,transformImageUrl,useApi,useCallimacusStore,useCart,useConnection,useOnConnectionChange,useOnError,useOnResponse,useSitemap,useSkesis,useThamyr}from"@callimacus/thamyr-react";import{useState as r,useRef as t,useCallback as n,useEffect as o}from"react";function i(r){e(r)}async function s(e){return new Promise((r,t)=>{const n=new XMLHttpRequest;n.responseType="arraybuffer","arraybuffer"===n.responseType?(n.addEventListener("load",()=>{const o=n.response;o instanceof ArrayBuffer?0!==o.byteLength?r(new Uint8Array(o)):t(new Error(`The recording at ${e} is empty`)):t(new Error(`Reading the recording at ${e} produced ${typeof o}, expected an ArrayBuffer`))}),n.addEventListener("error",()=>{t(new Error(`Failed to read the recording at ${e}`))}),n.open("GET",e,!0),n.send(null)):t(new Error('This runtime does not support XMLHttpRequest responseType "arraybuffer", so the recording cannot be read. Pass a custom adapter that returns bytes.'))})}const a=-45,c=1800,u=3e4;function d(e){if(!Number.isFinite(e))return 0;const r=(e- -60)/60;return Math.min(1,Math.max(0,r))}function l({silenceThreshold:e=a,silenceDuration:i=c,maxDuration:l=u,onRecordingComplete:p,adapter:y}){const[m,f]=r("idle"),[g,h]=r([]),[w,v]=r(void 0),[E,T]=r(void 0),M=t(y);M.current=y;const R=t(void 0),A=t([]),S=t(0),b=t(void 0),C=t(!1),I=t(p);I.current=p;const L=n(()=>{void 0!==b.current&&(clearTimeout(b.current),b.current=void 0)},[]),x=n(()=>{A.current=[],S.current=0,h([])},[]),H=n(()=>{if(C.current)return;C.current=!0,L(),f("processing");(async()=>{const e=R.current;R.current=void 0;try{if(!e)throw new Error("No active recording to stop");const r=await e.stop();T(r.uri),I.current(await s(r.uri))}catch(e){v(e instanceof Error?e.message:"Failed to finish recording")}finally{C.current=!1,f("idle")}})()},[L]),P=n(r=>{if(C.current)return;const t=[...A.current,d(r)];t.length>32&&t.splice(0,t.length-32),A.current=t,h(t),r<e?0===S.current?S.current=Date.now():Date.now()-S.current>i&&H():S.current=0},[e,i,H]),U=n(async()=>{v(void 0),C.current=!1,x();const e=M.current;R.current=e;try{await e.start({onMetering:P}),f("recording"),b.current=setTimeout(()=>{H()},l)}catch(r){R.current=void 0,v(r instanceof Error?r.message:"Microphone access denied"),f("idle"),await e.cancel()}},[P,l,x,H]),D=n(()=>{f("idle"),v(void 0),x()},[x]);return o(()=>()=>{L();const e=R.current;R.current=void 0,e?.cancel()},[L]),{state:m,audioLevels:g,startRecording:U,stopRecording:H,reset:D,error:w,lastRecordingUri:E}}function p(e){let r,t;function n(){void 0!==t&&(clearInterval(t),t=void 0)}return{async start({onMetering:n}){if(!(await e.requestRecordingPermissionsAsync()).granted)throw new Error("Microphone permission denied");await e.setAudioModeAsync({allowsRecording:!0,playsInSilentMode:!0});const o=new e.AudioModule.AudioRecorder({...e.RecordingPresets.HIGH_QUALITY,isMeteringEnabled:!0});await o.prepareToRecordAsync(),o.record(),r=o,t=setInterval(()=>{const e=o.getStatus();"number"==typeof e.metering&&n?.(e.metering)},100)},async stop(){if(!r)throw new Error("No active recording to stop");n();const e=r;r=void 0,await e.stop();const{uri:t}=e;if(null===t||""===t)throw new Error("Recording finished without producing a file");return{uri:t,durationMillis:e.getStatus().durationMillis}},async cancel(){if(n(),!r)return;const e=r;r=void 0;try{await e.stop()}catch{}}}}export{p as createExpoAudioAdapter,s as readRecording,i as useInitThamyr,l as useVoiceRecorder};
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/callimacus/index.ts","../../../src/voice/read-recording.ts","../../../src/voice/use-voice-recorder.ts","../../../src/voice/adapters/expo-audio.ts"],"sourcesContent":[null,null,null,null],"names":["useInitThamyr","config","useInitThamyrWeb","async","readRecording","uri","Promise","resolve","reject","request","XMLHttpRequest","responseType","addEventListener","response","ArrayBuffer","byteLength","Uint8Array","Error","open","send","defaultOptions","normalizeMetering","db","Number","isFinite","level","Math","min","max","useVoiceRecorder","silenceThreshold","silenceDuration","maxDuration","onRecordingComplete","adapter","state","setState","useState","audioLevels","setAudioLevels","error","setError","undefined","lastRecordingUri","setLastRecordingUri","adapterRef","useRef","current","activeAdapterRef","levelsRef","silenceStartRef","maxDurationTimerRef","stoppingRef","onCompleteRef","clearMaxDurationTimer","useCallback","clearTimeout","resetWaveform","stopRecording","active","result","stop","error_","message","finish","handleMetering","next","length","splice","Date","now","startRecording","start","onMetering","setTimeout","cancel","reset","useEffect","createExpoAudioAdapter","expoAudio","recorder","meterTimer","stopMeterTimer","clearInterval","requestRecordingPermissionsAsync","granted","setAudioModeAsync","allowsRecording","playsInSilentMode","AudioModule","AudioRecorder","RecordingPresets","HIGH_QUALITY","isMeteringEnabled","prepareToRecordAsync","record","setInterval","status","getStatus","metering","durationMillis"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright © 2025–2026 Solomei AI SRL. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Proprietary software, licensed for use by Callimacus customers only.
|
|
5
|
+
* See LICENSE.md for the full terms.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
export*from"@callimacus/thamyr-react/query";
|
|
9
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright © 2025–2026 Solomei AI SRL. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Proprietary software, licensed for use by Callimacus customers only.
|
|
5
|
+
* See LICENSE.md for the full terms.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
export { Block, BoundingBox, CallimacusEvent, CartActions, Chapter, ChapterStatus, ClientEvents, ClientInfo, ConnectionStatus, ConnectionStatusCallback, ErrorCallback, ErrorMessage, Gallery, GalleryMode, Image, ImageEdits, ImageFormatEdit, ImageResizeEdit, InteractionContext, InteractionPayloadMap, LocalizedEPArtwork, LocalizedEPChapter, LogLevel, LoggerConfig, ProductMatch, SLDocument, SLDocumentSection, SLInputEvent, SLInputEventType, ServerEvents, ServerResponseCallback, SitemapActions, SkesisActions, Story, ThamyrHttpError, ThamyrResponse, ThamyrResponseType, Topic, TtsAudio, UiElement, UiSnapshot, UserInteraction, UserInteractionType, Video, WhisperPayload, callimacusService, extractTtsAudio, isSetProduct, logger, thamyrApi, transformImageUrl, useApi, useCallimacusStore, useCart, useConnection, useOnConnectionChange, useOnError, useOnResponse, useSitemap, useSkesis, useThamyr } from '@callimacus/thamyr-react';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
Configuration for initializing the Thamyr SDK via `useInitThamyr` on React Native.
|
|
12
|
+
|
|
13
|
+
Intentionally narrower than the web (`@callimacus/thamyr-react`) config: the
|
|
14
|
+
`uiObserver` option is **omitted** because collecting a snapshot means
|
|
15
|
+
`querySelectorAll` + `getBoundingClientRect`, which have no React Native analog.
|
|
16
|
+
The `UiSnapshot` data contract itself is platform-neutral, so responses can still
|
|
17
|
+
carry that context; only the collection side is web-only. React Native
|
|
18
|
+
UI-snapshot support is its own feature (SOL-1084) and will need a different
|
|
19
|
+
design — explicit registration through viewability hooks / `measureInWindow`
|
|
20
|
+
rather than an autonomous observer.
|
|
21
|
+
*/
|
|
22
|
+
type CallimacusInitConfig = {
|
|
23
|
+
/**
|
|
24
|
+
Your Callimacus public client ID (`cal-pk-…`).
|
|
25
|
+
*/
|
|
26
|
+
clientId?: string;
|
|
27
|
+
/**
|
|
28
|
+
Server endpoint URL. Uses the default Thamyr endpoint when omitted.
|
|
29
|
+
*/
|
|
30
|
+
endpoint?: string;
|
|
31
|
+
/**
|
|
32
|
+
Maximum number of messages to retain in history. Set to `0` for unlimited. Defaults to `50`.
|
|
33
|
+
*/
|
|
34
|
+
maxMessages?: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
Initialize the Thamyr SDK for React Native usage.
|
|
39
|
+
|
|
40
|
+
Thin wrapper over the shared initializer from `@callimacus/thamyr-react` that
|
|
41
|
+
narrows the config to the React Native surface (no `uiObserver`). Call this once
|
|
42
|
+
in your root component. When `clientId` or `endpoint` change, the SDK is torn
|
|
43
|
+
down and re-initialized.
|
|
44
|
+
|
|
45
|
+
@param config - SDK configuration (client ID, endpoint, max messages).
|
|
46
|
+
|
|
47
|
+
@example
|
|
48
|
+
```tsx
|
|
49
|
+
function App() {
|
|
50
|
+
useInitThamyr({clientId: 'cal-pk-…'});
|
|
51
|
+
return <RootNavigator />;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
*/
|
|
55
|
+
declare function useInitThamyr(config: CallimacusInitConfig): void;
|
|
56
|
+
|
|
57
|
+
type VoiceRecorderState = 'idle' | 'recording' | 'processing';
|
|
58
|
+
type UseVoiceRecorderOptions = {
|
|
59
|
+
/**
|
|
60
|
+
Metering level (in dBFS, a negative value) below which audio is considered silence.
|
|
61
|
+
Defaults to `-45`.
|
|
62
|
+
*/
|
|
63
|
+
silenceThreshold?: number;
|
|
64
|
+
/**
|
|
65
|
+
How long (ms) the level must stay below the threshold before auto-stopping. Defaults to `1800`.
|
|
66
|
+
*/
|
|
67
|
+
silenceDuration?: number;
|
|
68
|
+
/**
|
|
69
|
+
Hard cap (ms) on a single recording. Defaults to `30_000`.
|
|
70
|
+
*/
|
|
71
|
+
maxDuration?: number;
|
|
72
|
+
/**
|
|
73
|
+
Called once recording finishes, with the captured audio as raw bytes.
|
|
74
|
+
|
|
75
|
+
This is a `Uint8Array` rather than the web recorder's `Blob` on purpose.
|
|
76
|
+
Audio travels as `inputEvent.data.file` over socket.io, which serialises typed
|
|
77
|
+
arrays as binary attachments that arrive server-side as a Node `Buffer`. A
|
|
78
|
+
React Native `Blob` is a handle into the native BlobModule whose bytes never
|
|
79
|
+
exist in JS, so socket.io cannot read it; and `new File([...])` — what the web
|
|
80
|
+
path uses — has no useful React Native equivalent. The server discards the
|
|
81
|
+
filename and mime type anyway (socket.io's binary framing strips them), so
|
|
82
|
+
nothing is lost by sending bytes directly.
|
|
83
|
+
|
|
84
|
+
Pass it to `sendUserInteraction` rather than `sendAudio`, which is typed for
|
|
85
|
+
the web `Blob` — see {@link useVoiceRecorder} for the call.
|
|
86
|
+
*/
|
|
87
|
+
onRecordingComplete: (audio: Uint8Array) => void;
|
|
88
|
+
/**
|
|
89
|
+
The recorder implementation. Build one from `expo-audio` with
|
|
90
|
+
`createExpoAudioAdapter(ExpoAudio)`, or supply your own for a different native
|
|
91
|
+
recorder in bare React Native.
|
|
92
|
+
|
|
93
|
+
Required, rather than defaulting to `expo-audio`: a default would mean this
|
|
94
|
+
package referencing `expo-audio` itself, which Metro resolves statically — so
|
|
95
|
+
apps that never record audio would fail to bundle without it installed. Pass a
|
|
96
|
+
memoized adapter so it keeps a stable identity across renders.
|
|
97
|
+
*/
|
|
98
|
+
adapter: VoiceRecorderAdapter;
|
|
99
|
+
};
|
|
100
|
+
type UseVoiceRecorderReturn = {
|
|
101
|
+
state: VoiceRecorderState;
|
|
102
|
+
/**
|
|
103
|
+
Rolling, normalised (0..1) waveform of recent audio levels, length 32.
|
|
104
|
+
*/
|
|
105
|
+
audioLevels: number[];
|
|
106
|
+
startRecording: () => Promise<void>;
|
|
107
|
+
stopRecording: () => void;
|
|
108
|
+
reset: () => void;
|
|
109
|
+
error: string | undefined;
|
|
110
|
+
/**
|
|
111
|
+
File URI of the most recent completed recording, if any.
|
|
112
|
+
*/
|
|
113
|
+
lastRecordingUri: string | undefined;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
Result of a completed native recording.
|
|
117
|
+
*/
|
|
118
|
+
type RecordingResult = {
|
|
119
|
+
/**
|
|
120
|
+
Local file URI of the recording (e.g. `file:///...`).
|
|
121
|
+
*/
|
|
122
|
+
uri: string;
|
|
123
|
+
/**
|
|
124
|
+
Recording duration in milliseconds, when the adapter reports it.
|
|
125
|
+
*/
|
|
126
|
+
durationMillis?: number;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
Abstraction over a native audio recorder so the hook stays independent of any
|
|
130
|
+
single library. The built-in adapter targets `expo-audio`; bare React Native apps
|
|
131
|
+
can implement this against `react-native-audio-recorder-player` or similar.
|
|
132
|
+
*/
|
|
133
|
+
type VoiceRecorderAdapter = {
|
|
134
|
+
/**
|
|
135
|
+
Begins recording. `onMetering` should be invoked periodically with the current
|
|
136
|
+
input level in dBFS (a negative number; `0` is loudest). It drives both the
|
|
137
|
+
waveform and silence detection.
|
|
138
|
+
*/
|
|
139
|
+
start: (options: {
|
|
140
|
+
onMetering?: (db: number) => void;
|
|
141
|
+
}) => Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
Stops recording and resolves with the resulting file.
|
|
144
|
+
*/
|
|
145
|
+
stop: () => Promise<RecordingResult>;
|
|
146
|
+
/**
|
|
147
|
+
Aborts recording and discards any output. Safe to call when not recording.
|
|
148
|
+
*/
|
|
149
|
+
cancel: () => Promise<void>;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
React Native voice recorder hook. Mirrors the control surface of the web recorder
|
|
154
|
+
in `@callimacus/thamyr-react` (`state`, `audioLevels`, `startRecording`,
|
|
155
|
+
`stopRecording`, `reset`, `error`) so cross-platform call sites stay aligned.
|
|
156
|
+
|
|
157
|
+
It differs in one deliberate way: `onRecordingComplete` receives a `Uint8Array`
|
|
158
|
+
rather than a `Blob`, because that is what survives socket.io's binary framing on
|
|
159
|
+
native. See {@link UseVoiceRecorderOptions.onRecordingComplete}.
|
|
160
|
+
|
|
161
|
+
Recording is delegated to a {@link VoiceRecorderAdapter} — see
|
|
162
|
+
{@link UseVoiceRecorderOptions.adapter} for why it is passed in rather than
|
|
163
|
+
defaulted. Silence detection and the max-duration cap run in JS off the adapter's
|
|
164
|
+
metering stream.
|
|
165
|
+
|
|
166
|
+
@example
|
|
167
|
+
```tsx
|
|
168
|
+
import * as ExpoAudio from 'expo-audio';
|
|
169
|
+
|
|
170
|
+
const {sendEvent} = useThamyr();
|
|
171
|
+
const adapter = useMemo(() => createExpoAudioAdapter(ExpoAudio), []);
|
|
172
|
+
const {state, startRecording, stopRecording} = useVoiceRecorder({
|
|
173
|
+
adapter,
|
|
174
|
+
onRecordingComplete(audio) {
|
|
175
|
+
sendEvent(UserInteractionType.CREATE_ROUND, {
|
|
176
|
+
inputEvent: {type: SLInputEventType.question, data: {value: '', file: audio}},
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
*/
|
|
182
|
+
declare function useVoiceRecorder({ silenceThreshold, silenceDuration, maxDuration, onRecordingComplete, adapter, }: UseVoiceRecorderOptions): UseVoiceRecorderReturn;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
The slice of a recorder instance this adapter drives.
|
|
186
|
+
|
|
187
|
+
Structural rather than `import type {AudioRecorder} from 'expo-audio'` so this
|
|
188
|
+
package neither depends on `expo-audio` nor leaks it into consumers' typechecks.
|
|
189
|
+
*/
|
|
190
|
+
type ExpoAudioRecorderLike = {
|
|
191
|
+
uri: string | null;
|
|
192
|
+
prepareToRecordAsync: () => Promise<unknown>;
|
|
193
|
+
record: () => void;
|
|
194
|
+
stop: () => Promise<unknown>;
|
|
195
|
+
getStatus: () => {
|
|
196
|
+
metering?: number;
|
|
197
|
+
durationMillis?: number;
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
The slice of the `expo-audio` module this adapter uses. `import * as ExpoAudio
|
|
202
|
+
from 'expo-audio'` satisfies it.
|
|
203
|
+
*/
|
|
204
|
+
type ExpoAudioLike = {
|
|
205
|
+
AudioModule: {
|
|
206
|
+
AudioRecorder: new (options: Record<string, unknown>) => ExpoAudioRecorderLike;
|
|
207
|
+
};
|
|
208
|
+
RecordingPresets: Record<string, Record<string, unknown>>;
|
|
209
|
+
setAudioModeAsync: (mode: {
|
|
210
|
+
allowsRecording?: boolean;
|
|
211
|
+
playsInSilentMode?: boolean;
|
|
212
|
+
}) => Promise<unknown>;
|
|
213
|
+
requestRecordingPermissionsAsync: () => Promise<{
|
|
214
|
+
granted: boolean;
|
|
215
|
+
}>;
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
{@link VoiceRecorderAdapter} backed by `expo-audio` — the current Expo audio
|
|
219
|
+
module, and the successor to `expo-av`, which Expo removed in SDK 54.
|
|
220
|
+
|
|
221
|
+
The module is **handed in rather than imported**, which is the whole point of the
|
|
222
|
+
signature:
|
|
223
|
+
|
|
224
|
+
- `require('expo-audio')` inside this package would make Metro resolve it
|
|
225
|
+
statically for every consumer, so an app that never records audio would fail to
|
|
226
|
+
bundle merely for not having installed it. An "optional peer dependency" that
|
|
227
|
+
breaks the build when absent is not optional.
|
|
228
|
+
- `await import('expo-audio')` is worse: Metro serves no async bundle splits at
|
|
229
|
+
runtime, so it fails with "Could not load bundle".
|
|
230
|
+
- Keeping `expo-audio` out of this package's dependency graph also means it
|
|
231
|
+
typechecks and tests with no native toolchain, against a plain object.
|
|
232
|
+
|
|
233
|
+
Metering is delivered by polling `recorder.getStatus().metering`, since
|
|
234
|
+
`expo-audio` exposes no push-based metering stream.
|
|
235
|
+
|
|
236
|
+
@param expoAudio - The `expo-audio` module namespace.
|
|
237
|
+
|
|
238
|
+
@example
|
|
239
|
+
```tsx
|
|
240
|
+
import * as ExpoAudio from 'expo-audio';
|
|
241
|
+
import {createExpoAudioAdapter, useVoiceRecorder} from '@callimacus/thamyr-react-native';
|
|
242
|
+
|
|
243
|
+
const adapter = useMemo(() => createExpoAudioAdapter(ExpoAudio), []);
|
|
244
|
+
const recorder = useVoiceRecorder({adapter, onRecordingComplete: sendAudio});
|
|
245
|
+
```
|
|
246
|
+
*/
|
|
247
|
+
declare function createExpoAudioAdapter(expoAudio: ExpoAudioLike): VoiceRecorderAdapter;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
Reads a local recording file into raw bytes.
|
|
251
|
+
|
|
252
|
+
`XMLHttpRequest` with `responseType = 'arraybuffer'` is the route that actually
|
|
253
|
+
works on React Native, and the choice is worth recording because the two obvious
|
|
254
|
+
alternatives do not:
|
|
255
|
+
|
|
256
|
+
- `fetch(uri).then(r => r.arrayBuffer())` — React Native's fetch is the
|
|
257
|
+
whatwg-fetch polyfill, whose `arrayBuffer()` goes through
|
|
258
|
+
`FileReader.readAsArrayBuffer` on a Blob. That works on current React Native
|
|
259
|
+
but adds a Blob round-trip for no benefit.
|
|
260
|
+
- `new Blob([arrayBuffer])` — React Native forbids it outright ("Creating blobs
|
|
261
|
+
from 'ArrayBuffer' and 'ArrayBufferView' are not supported"), so any path that
|
|
262
|
+
tries to rebuild a Blob from bytes is a dead end.
|
|
263
|
+
|
|
264
|
+
React Native's XHR maps `responseType = 'arraybuffer'` onto a native base64
|
|
265
|
+
response and decodes it into a real `ArrayBuffer` (see
|
|
266
|
+
`react-native/Libraries/Network/XMLHttpRequest.js`), which is exactly the one
|
|
267
|
+
conversion needed here. `arraybuffer` support is itself conditional on
|
|
268
|
+
`global.ArrayBuffer` in that file, so it is feature-detected below rather than
|
|
269
|
+
assumed.
|
|
270
|
+
|
|
271
|
+
@param uri - A local file URI, as produced by the recorder adapter.
|
|
272
|
+
@returns The file's bytes.
|
|
273
|
+
*/
|
|
274
|
+
declare function readRecording(uri: string): Promise<Uint8Array>;
|
|
275
|
+
|
|
276
|
+
export { createExpoAudioAdapter, readRecording, useInitThamyr, useVoiceRecorder };
|
|
277
|
+
export type { CallimacusInitConfig, ExpoAudioLike, RecordingResult, UseVoiceRecorderOptions, UseVoiceRecorderReturn, VoiceRecorderAdapter, VoiceRecorderState };
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@callimacus/thamyr-react-native",
|
|
3
|
+
"version": "4.3.4",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "React Native package for Thamyr",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
7
|
+
"homepage": "https://docs.callimacus.ai/thamyr-sdk/react-native/1-overview",
|
|
8
|
+
"main": "dist/esm/index.js",
|
|
9
|
+
"module": "dist/esm/index.js",
|
|
10
|
+
"types": "dist/types/index.d.ts",
|
|
11
|
+
"react-native": "dist/esm/index.js",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/esm/index.js",
|
|
15
|
+
"types": "./dist/types/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./query": {
|
|
18
|
+
"import": "./dist/esm/query.js",
|
|
19
|
+
"types": "./dist/types/query.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"CHANGELOG.core.md",
|
|
26
|
+
"MIGRATION.md"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"registry": "https://registry.npmjs.org",
|
|
32
|
+
"@callimacus:registry": "https://registry.npmjs.org"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"prepack": "cp ../../MIGRATION.md ./MIGRATION.md && cp ../core/CHANGELOG.md ./CHANGELOG.core.md",
|
|
36
|
+
"build": "rollup -c",
|
|
37
|
+
"dev": "rollup -c -w",
|
|
38
|
+
"clean": "rm -rf dist",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"lint": "eslint src"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@callimacus/thamyr-react": "^4.3.4"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18",
|
|
47
|
+
"react-native": ">=0.72",
|
|
48
|
+
"@tanstack/react-query": "^5"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@eslint-react/eslint-plugin": "^5.20.8",
|
|
52
|
+
"@testing-library/react": "^16.3.3",
|
|
53
|
+
"@types/react": "^19.3.0",
|
|
54
|
+
"eslint-config-xo": "^4.0.1",
|
|
55
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
56
|
+
"jsdom": "^30.1.1",
|
|
57
|
+
"react": "^19.3.0",
|
|
58
|
+
"react-native": "^0.87.1",
|
|
59
|
+
"vitest": "^5.0.2",
|
|
60
|
+
"@tanstack/react-query": "^5"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"@tanstack/react-query": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|