yaver-feedback-react-native 0.8.12 → 0.9.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/dist/capture.js CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.captureScreenshot = captureScreenshot;
19
+ exports.captureScreenshotBase64 = captureScreenshotBase64;
19
20
  exports.pickFeedbackFile = pickFeedbackFile;
20
21
  exports.startVideoRecording = startVideoRecording;
21
22
  exports.stopVideoRecording = stopVideoRecording;
@@ -24,6 +25,9 @@ exports.isVoiceCaptureSupported = isVoiceCaptureSupported;
24
25
  exports.startAudioRecording = startAudioRecording;
25
26
  exports.stopAudioRecording = stopAudioRecording;
26
27
  exports.isAudioRecording = isAudioRecording;
28
+ exports.startPcmRecording = startPcmRecording;
29
+ exports.stopPcmRecording = stopPcmRecording;
30
+ exports.isPcmRecording = isPcmRecording;
27
31
  /**
28
32
  * Capture the current screen as a PNG image.
29
33
  * Requires `react-native-view-shot` to be installed.
@@ -46,6 +50,34 @@ async function captureScreenshot() {
46
50
  String(err));
47
51
  }
48
52
  }
53
+ /**
54
+ * Capture a screenshot AND return it as base64 + mime type, ready to
55
+ * embed in a `/tasks` payload's `images` array. Used by the converged
56
+ * vibe-feedback flow (FeedbackModal → P2PClient.createFeedbackTask).
57
+ *
58
+ * Returns null when capture isn't possible (peer dep missing / user
59
+ * permission denied / running in a context where view-shot can't
60
+ * grab the screen). Caller should treat null as "send without
61
+ * screenshot" rather than aborting the whole feedback.
62
+ */
63
+ async function captureScreenshotBase64() {
64
+ try {
65
+ const ViewShot = require('react-native-view-shot');
66
+ const result = await ViewShot.captureScreen({
67
+ format: 'jpg',
68
+ quality: 0.7,
69
+ result: 'base64',
70
+ });
71
+ if (typeof result === 'string' && result.length > 0) {
72
+ // ViewShot returns a bare base64 string (no `data:` prefix).
73
+ return { base64: result, mimeType: 'image/jpeg' };
74
+ }
75
+ return null;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
49
81
  function classifyPickedFile(name, mimeType) {
50
82
  const lowerName = name.toLowerCase();
51
83
  const lowerMime = (mimeType ?? '').toLowerCase();
@@ -278,3 +310,53 @@ async function stopAudioRecording() {
278
310
  function isAudioRecording() {
279
311
  return audioRecorderActive;
280
312
  }
313
+ // ── Voice-stream recording (raw LPCM WAV) ──────────────────────────────
314
+ // The voice vibe-coding path streams audio to the agent's STT WS, which
315
+ // expects raw 16-bit / 16 kHz mono PCM (we strip the WAV header on the
316
+ // way out). That's a different format from the HIGH_QUALITY m4a recorder
317
+ // above — a compressed .m4a can't be streamed to Deepgram/whisper — so
318
+ // this uses its own recording options, mirroring the Yaver app's
319
+ // AgentVoiceButton.
320
+ let pcmRecorderRef = null;
321
+ let pcmRecorderActive = false;
322
+ // Raw LPCM 16-bit LE, 16 kHz mono. iOS uses lpcm; Android records WAV.
323
+ const PCM_RECORDING_OPTIONS = {
324
+ android: { extension: '.wav', outputFormat: 2, audioEncoder: 3, sampleRate: 16000, numberOfChannels: 1, bitRate: 256000 },
325
+ ios: {
326
+ extension: '.wav', outputFormat: 'lpcm', audioQuality: 0x40, sampleRate: 16000,
327
+ numberOfChannels: 1, bitRate: 256000, linearPCMBitDepth: 16, linearPCMIsBigEndian: false, linearPCMIsFloat: false,
328
+ },
329
+ web: { mimeType: 'audio/wav', bitsPerSecond: 256000 },
330
+ };
331
+ /** Begin a raw-PCM recording for the voice stream. */
332
+ async function startPcmRecording() {
333
+ if (pcmRecorderActive)
334
+ throw new Error('[YaverFeedback] A voice recording is already in progress.');
335
+ const ExpoAv = loadExpoAvOrThrow();
336
+ const { Audio } = ExpoAv;
337
+ const perm = await Audio.requestPermissionsAsync();
338
+ if (!perm.granted) {
339
+ throw new Error('[YaverFeedback] Microphone permission denied. Enable it in Settings ▸ Your App ▸ Microphone.');
340
+ }
341
+ await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true, staysActiveInBackground: false });
342
+ const { recording } = await Audio.Recording.createAsync(PCM_RECORDING_OPTIONS);
343
+ pcmRecorderRef = recording;
344
+ pcmRecorderActive = true;
345
+ }
346
+ /** Stop the voice recording; returns the WAV file:// URI (or null). */
347
+ async function stopPcmRecording() {
348
+ if (!pcmRecorderActive || !pcmRecorderRef)
349
+ return null;
350
+ const recording = pcmRecorderRef;
351
+ pcmRecorderRef = null;
352
+ pcmRecorderActive = false;
353
+ try {
354
+ await recording.stopAndUnloadAsync();
355
+ }
356
+ catch { /* already stopped */ }
357
+ return typeof recording.getURI === 'function' ? recording.getURI() : null;
358
+ }
359
+ /** Whether a voice-stream recording is currently active. */
360
+ function isPcmRecording() {
361
+ return pcmRecorderActive;
362
+ }
@@ -27,3 +27,7 @@ export declare function clearQuickIconDisabled(): Promise<void>;
27
27
  export declare function getQuickIconColorPreset(): Promise<QuickIconColorPreset | null>;
28
28
  export declare function setQuickIconColorPreset(preset: QuickIconColorPreset | null): Promise<void>;
29
29
  export declare function clearQuickIconColorPreset(): Promise<void>;
30
+ export declare function getPreferredRunner(): Promise<string | null>;
31
+ export declare function setPreferredRunner(runner: string | null): Promise<void>;
32
+ export declare function getPreferredModel(): Promise<string | null>;
33
+ export declare function setPreferredModel(model: string | null): Promise<void>;
@@ -21,6 +21,10 @@ exports.clearQuickIconDisabled = clearQuickIconDisabled;
21
21
  exports.getQuickIconColorPreset = getQuickIconColorPreset;
22
22
  exports.setQuickIconColorPreset = setQuickIconColorPreset;
23
23
  exports.clearQuickIconColorPreset = clearQuickIconColorPreset;
24
+ exports.getPreferredRunner = getPreferredRunner;
25
+ exports.setPreferredRunner = setPreferredRunner;
26
+ exports.getPreferredModel = getPreferredModel;
27
+ exports.setPreferredModel = setPreferredModel;
24
28
  let AsyncStorage = null;
25
29
  try {
26
30
  AsyncStorage = require('@react-native-async-storage/async-storage').default;
@@ -137,3 +141,61 @@ async function setQuickIconColorPreset(preset) {
137
141
  async function clearQuickIconColorPreset() {
138
142
  await setQuickIconColorPreset(null);
139
143
  }
144
+ // ── Preferred coding agent + model (used by the standalone feedback
145
+ // SDK's vibe chat to mirror what Yaver mobile's Tasks tab would send.
146
+ // The agent on the remote DOES read userSettings.primaryRunnerByDevice
147
+ // from Convex, but the standalone SDK has no DeviceContext to push the
148
+ // per-device pick. We persist the user's last choice locally; first
149
+ // run picks whatever's signed-in via getRunnerStatus().)
150
+ const PREFERRED_RUNNER_KEY = 'yaver_feedback_preferred_runner';
151
+ const PREFERRED_MODEL_KEY = 'yaver_feedback_preferred_model';
152
+ async function getPreferredRunner() {
153
+ if (!AsyncStorage)
154
+ return null;
155
+ try {
156
+ const v = await AsyncStorage.getItem(PREFERRED_RUNNER_KEY);
157
+ return v && v.trim() ? v.trim() : null;
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ async function setPreferredRunner(runner) {
164
+ if (!AsyncStorage)
165
+ return;
166
+ try {
167
+ if (!runner || !runner.trim()) {
168
+ await AsyncStorage.removeItem(PREFERRED_RUNNER_KEY);
169
+ return;
170
+ }
171
+ await AsyncStorage.setItem(PREFERRED_RUNNER_KEY, runner.trim());
172
+ }
173
+ catch {
174
+ /* best-effort */
175
+ }
176
+ }
177
+ async function getPreferredModel() {
178
+ if (!AsyncStorage)
179
+ return null;
180
+ try {
181
+ const v = await AsyncStorage.getItem(PREFERRED_MODEL_KEY);
182
+ return v && v.trim() ? v.trim() : null;
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ }
188
+ async function setPreferredModel(model) {
189
+ if (!AsyncStorage)
190
+ return;
191
+ try {
192
+ if (!model || !model.trim()) {
193
+ await AsyncStorage.removeItem(PREFERRED_MODEL_KEY);
194
+ return;
195
+ }
196
+ await AsyncStorage.setItem(PREFERRED_MODEL_KEY, model.trim());
197
+ }
198
+ catch {
199
+ /* best-effort */
200
+ }
201
+ }
package/dist/types.d.ts CHANGED
@@ -19,6 +19,53 @@ export interface RunnerBrowserAuthSession {
19
19
  updatedAt: number;
20
20
  completedAt?: number;
21
21
  }
22
+ export interface RunnerAuthStatusRow {
23
+ id: string;
24
+ name: string;
25
+ installed: boolean;
26
+ ready: boolean;
27
+ authConfigured: boolean;
28
+ authSource?: string;
29
+ warning?: string;
30
+ error?: string;
31
+ path?: string;
32
+ detail?: string;
33
+ version?: string;
34
+ }
35
+ export interface OpenCodeProviderSummary {
36
+ id: string;
37
+ name?: string;
38
+ hasApiKey?: boolean;
39
+ baseUrl?: string;
40
+ models?: Array<{
41
+ id: string;
42
+ name?: string;
43
+ provider?: string;
44
+ }>;
45
+ }
46
+ export interface OpenCodeAgentSummary {
47
+ name: string;
48
+ model?: string;
49
+ description?: string;
50
+ isBuiltin?: boolean;
51
+ }
52
+ export interface OpenCodeConfigSummary {
53
+ path: string;
54
+ exists: boolean;
55
+ defaultAgent?: string;
56
+ model?: string;
57
+ smallModel?: string;
58
+ buildModel?: string;
59
+ planModel?: string;
60
+ providers?: OpenCodeProviderSummary[];
61
+ models?: Array<{
62
+ id: string;
63
+ name?: string;
64
+ provider?: string;
65
+ }>;
66
+ agents?: OpenCodeAgentSummary[];
67
+ diagnostics?: string[];
68
+ }
22
69
  export interface IncidentEvent {
23
70
  id: string;
24
71
  timestamp: number;
@@ -118,6 +165,15 @@ export interface FeedbackConfig {
118
165
  preferredDeviceId?: string;
119
166
  /** How feedback collection is triggered */
120
167
  trigger?: 'shake' | 'floating-button' | 'manual';
168
+ /**
169
+ * App slug used by the in-modal Deploy panel when calling the agent's
170
+ * `/fleet/deploy-options` and `/deploy/ship` endpoints. Should match an
171
+ * `apps[].name` entry in the agent's `yaver.workspace.yaml`. When omitted
172
+ * the panel falls back to the last dot-segment of `bundleId` (e.g.
173
+ * `io.yaver.sfmg` → `sfmg`). Set explicitly when the workspace name
174
+ * differs from the bundleId tail.
175
+ */
176
+ deployAppSlug?: string;
121
177
  /**
122
178
  * Non-default escape hatch for host apps that want the SDK without
123
179
  * shake gesture handling. When enabled:
@@ -429,12 +485,20 @@ export interface TestSession {
429
485
  export interface VoiceCapability {
430
486
  /** Always true — mobile can always record and send audio. */
431
487
  voiceInputEnabled: boolean;
432
- /** Speech-to-speech provider (e.g. "personaplex", "openai"), or null. */
488
+ /** Speech-to-speech provider (legacy), or null. */
433
489
  s2sProvider?: string;
434
490
  /** Whether the S2S provider is ready for real-time sessions. */
435
491
  s2sReady?: boolean;
436
- /** Speech-to-text provider for transcription (e.g. "whisper", "openai"). */
492
+ /** Speech-to-text provider for transcription, e.g. "deepgram" for Deepgram Flux. */
437
493
  sttProvider?: string;
438
494
  /** Whether STT is ready (auto-transcription of voice input). */
439
495
  sttReady?: boolean;
496
+ /** Text-to-speech provider for readback, e.g. "cartesia". */
497
+ ttsProvider?: string;
498
+ /** Whether TTS readback is ready. */
499
+ ttsReady?: boolean;
500
+ /** Whether the agent-side hands-free task loop is enabled. */
501
+ enabled?: boolean;
502
+ /** Default project slug used by the agent voice loop. */
503
+ defaultProject?: string;
440
504
  }
@@ -0,0 +1,61 @@
1
+ export interface SDKVoiceStartOpts {
2
+ /** WS URL for the agent voice stream — P2PClient.voiceStreamUrl(). */
3
+ wsUrl: string;
4
+ /** Auth headers — P2PClient.voiceAuthHeaders(). */
5
+ headers: Record<string, string>;
6
+ project?: string;
7
+ model?: string;
8
+ runner?: string;
9
+ /** Surface hint for the agent's prompt wrapper. */
10
+ surface?: string;
11
+ /** Max chars for the spoken readback (Cartesia default ~280). */
12
+ ttsBudget?: number;
13
+ /** Per-session STT engine. "" = agent default. "local" = free
14
+ * whisper.cpp on the host; "deepgram" = Flux nova-3 streaming. */
15
+ sttProvider?: string;
16
+ /** Per-session TTS engine. "" = agent default. "local"/"device" =
17
+ * client synthesizes from the result text; cloud engines stream PCM. */
18
+ ttsProvider?: string;
19
+ }
20
+ export interface SDKVoiceCallbacks {
21
+ /** Active engines, echoed by the agent right after start — lets the UI
22
+ * show "Local" vs "Flux". */
23
+ onProviders?: (stt: string, tts: string) => void;
24
+ onTranscriptPartial?: (text: string) => void;
25
+ onTranscriptFinal?: (text: string) => void;
26
+ onTaskCreated?: (taskId: string) => void;
27
+ onTaskResult?: (taskId: string, text: string, status: string) => void;
28
+ onTTSReady?: (pcm: Uint8Array, sampleRate: number) => void;
29
+ onError?: (msg: string) => void;
30
+ onDone?: () => void;
31
+ }
32
+ /** True when the deps the voice stream needs (expo-file-system + buffer)
33
+ * are installed. The feedback UI hides the mic button otherwise. */
34
+ export declare function isVoiceStreamSupported(): boolean;
35
+ export declare class SDKVoiceSession {
36
+ private ws;
37
+ private callbacks;
38
+ private ttsChunks;
39
+ private ttsTotalBytes;
40
+ private ttsSampleRate;
41
+ private closed;
42
+ constructor(callbacks: SDKVoiceCallbacks);
43
+ /** Open the WS and send the start frame. Resolves once open. */
44
+ start(opts: SDKVoiceStartOpts): Promise<void>;
45
+ /** Stream a recorded WAV/PCM file as binary frames. Records produced by
46
+ * recordPcmWav() are LPCM 16-bit/16kHz mono with a 44-byte RIFF header
47
+ * we strip here — the exact shape the backend → STT expects. */
48
+ streamAudioFile(uri: string, opts?: {
49
+ skipWavHeader?: boolean;
50
+ chunkBytes?: number;
51
+ }): Promise<void>;
52
+ /** Done speaking — flush STT and create the agent task. */
53
+ finalize(): void;
54
+ close(): void;
55
+ private handleMessage;
56
+ }
57
+ /** Wrap raw PCM (signed 16-bit LE) in a minimal WAV container for
58
+ * expo-av playback. 44-byte header + samples. */
59
+ export declare function wrapPCMAsWAV(pcm: Uint8Array, sampleRate: number, channels?: number): Uint8Array;
60
+ /** Write a PCM buffer to a temp WAV file; returns its file:// URI. */
61
+ export declare function pcmToTempWavURI(pcm: Uint8Array, sampleRate: number): Promise<string>;
package/dist/voice.js ADDED
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ // Voice vibe-coding for the feedback SDK — speak a change, the agent
3
+ // acts (full MCP), and reads back a spoken headline. Ported from the
4
+ // Yaver mobile app's AgentVoiceSession (mobile/src/lib/agentVoice.ts) but
5
+ // decoupled from the app: connection details (WS URL + auth headers) come
6
+ // from the SDK's P2PClient, so it works both inside the Yaver container
7
+ // (inherited auth) and standalone in a third-party app.
8
+ //
9
+ // Local vs remote STT/TTS is decided entirely agent-side (free local
10
+ // whisper.cpp by default, Deepgram/OpenAI/etc when keys are configured),
11
+ // so nothing model-related is bundled here — the SDK just streams mic
12
+ // audio and renders the transcript + TTS the agent sends back.
13
+ //
14
+ // expo-file-system + buffer are loaded lazily so a bare-RN app that
15
+ // hasn't installed them can still use the rest of the SDK; the voice
16
+ // button hides itself via isVoiceStreamSupported() when they're absent.
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.SDKVoiceSession = void 0;
19
+ exports.isVoiceStreamSupported = isVoiceStreamSupported;
20
+ exports.wrapPCMAsWAV = wrapPCMAsWAV;
21
+ exports.pcmToTempWavURI = pcmToTempWavURI;
22
+ function loadBuffer() {
23
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
24
+ return require('buffer').Buffer;
25
+ }
26
+ function loadFS() {
27
+ // expo-file-system v16+ moved the classic API under /legacy; fall back
28
+ // to the root export for older installs.
29
+ try {
30
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
31
+ return require('expo-file-system/legacy');
32
+ }
33
+ catch {
34
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
35
+ return require('expo-file-system');
36
+ }
37
+ }
38
+ /** True when the deps the voice stream needs (expo-file-system + buffer)
39
+ * are installed. The feedback UI hides the mic button otherwise. */
40
+ function isVoiceStreamSupported() {
41
+ try {
42
+ loadBuffer();
43
+ const fs = loadFS();
44
+ return typeof fs?.readAsStringAsync === 'function';
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ class SDKVoiceSession {
51
+ constructor(callbacks) {
52
+ this.ws = null;
53
+ this.ttsChunks = [];
54
+ this.ttsTotalBytes = 0;
55
+ this.ttsSampleRate = 22050;
56
+ this.closed = false;
57
+ this.callbacks = callbacks;
58
+ }
59
+ /** Open the WS and send the start frame. Resolves once open. */
60
+ async start(opts) {
61
+ return new Promise((resolve, reject) => {
62
+ let opened = false;
63
+ let ws;
64
+ try {
65
+ // RN-specific 3rd arg carries request headers.
66
+ ws = new WebSocket(opts.wsUrl, undefined, { headers: opts.headers });
67
+ }
68
+ catch (e) {
69
+ reject(e);
70
+ return;
71
+ }
72
+ this.ws = ws;
73
+ ws.onopen = () => {
74
+ opened = true;
75
+ try {
76
+ ws.send(JSON.stringify({
77
+ type: 'start',
78
+ project: opts.project ?? '',
79
+ model: opts.model ?? '',
80
+ runner: opts.runner ?? '',
81
+ surface: opts.surface ?? '',
82
+ ttsBudget: opts.ttsBudget ?? 0,
83
+ sttProvider: opts.sttProvider ?? '',
84
+ ttsProvider: opts.ttsProvider ?? '',
85
+ }));
86
+ resolve();
87
+ }
88
+ catch (e) {
89
+ reject(e);
90
+ }
91
+ };
92
+ ws.onmessage = (e) => this.handleMessage(e);
93
+ ws.onerror = () => {
94
+ if (!opened)
95
+ reject(new Error('voice WS connect failed'));
96
+ else
97
+ this.callbacks.onError?.('voice WS error');
98
+ };
99
+ ws.onclose = () => {
100
+ this.closed = true;
101
+ if (!opened)
102
+ reject(new Error('voice WS closed before open'));
103
+ };
104
+ });
105
+ }
106
+ /** Stream a recorded WAV/PCM file as binary frames. Records produced by
107
+ * recordPcmWav() are LPCM 16-bit/16kHz mono with a 44-byte RIFF header
108
+ * we strip here — the exact shape the backend → STT expects. */
109
+ async streamAudioFile(uri, opts) {
110
+ if (!this.ws || this.closed)
111
+ throw new Error('voice WS not open');
112
+ const Buffer = loadBuffer();
113
+ const FileSystem = loadFS();
114
+ const skipWavHeader = opts?.skipWavHeader ?? true;
115
+ const chunkBytes = opts?.chunkBytes ?? 16384;
116
+ const b64 = await FileSystem.readAsStringAsync(uri, { encoding: 'base64' });
117
+ let buf = Buffer.from(b64, 'base64');
118
+ if (skipWavHeader && buf.length > 44 && buf.slice(0, 4).toString() === 'RIFF') {
119
+ buf = buf.slice(44);
120
+ }
121
+ for (let i = 0; i < buf.length; i += chunkBytes) {
122
+ if (this.closed)
123
+ return;
124
+ const slice = buf.slice(i, Math.min(i + chunkBytes, buf.length));
125
+ this.ws.send(slice.buffer.slice(slice.byteOffset, slice.byteOffset + slice.byteLength));
126
+ await new Promise((r) => setTimeout(r, 0));
127
+ }
128
+ }
129
+ /** Done speaking — flush STT and create the agent task. */
130
+ finalize() {
131
+ if (this.ws && !this.closed) {
132
+ this.ws.send(JSON.stringify({ type: 'stop' }));
133
+ }
134
+ }
135
+ close() {
136
+ this.closed = true;
137
+ try {
138
+ this.ws?.close();
139
+ }
140
+ catch { /* ignore */ }
141
+ this.ws = null;
142
+ }
143
+ handleMessage(e) {
144
+ let msg;
145
+ try {
146
+ msg = JSON.parse(typeof e.data === 'string' ? e.data : '');
147
+ }
148
+ catch {
149
+ return;
150
+ }
151
+ switch (msg.type) {
152
+ case 'providers':
153
+ this.callbacks.onProviders?.(msg.stt ?? '', msg.tts ?? '');
154
+ break;
155
+ case 'transcript-partial':
156
+ this.callbacks.onTranscriptPartial?.(msg.text ?? '');
157
+ break;
158
+ case 'transcript-final':
159
+ this.callbacks.onTranscriptFinal?.(msg.text ?? '');
160
+ break;
161
+ case 'task-created':
162
+ this.callbacks.onTaskCreated?.(msg.taskId ?? '');
163
+ break;
164
+ case 'task-result':
165
+ this.callbacks.onTaskResult?.(msg.taskId ?? '', msg.text ?? '', msg.status ?? '');
166
+ break;
167
+ case 'tts-frame': {
168
+ if (!msg.pcm)
169
+ break;
170
+ const Buffer = loadBuffer();
171
+ const arr = new Uint8Array(Buffer.from(msg.pcm, 'base64'));
172
+ this.ttsChunks.push(arr);
173
+ this.ttsTotalBytes += arr.length;
174
+ if (msg.sampleRate)
175
+ this.ttsSampleRate = msg.sampleRate;
176
+ break;
177
+ }
178
+ case 'done': {
179
+ if (this.ttsTotalBytes > 0) {
180
+ const pcm = new Uint8Array(this.ttsTotalBytes);
181
+ let off = 0;
182
+ for (const c of this.ttsChunks) {
183
+ pcm.set(c, off);
184
+ off += c.length;
185
+ }
186
+ this.callbacks.onTTSReady?.(pcm, this.ttsSampleRate);
187
+ }
188
+ this.callbacks.onDone?.();
189
+ this.close();
190
+ break;
191
+ }
192
+ case 'error':
193
+ this.callbacks.onError?.(msg.error ?? 'voice error');
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ exports.SDKVoiceSession = SDKVoiceSession;
199
+ /** Wrap raw PCM (signed 16-bit LE) in a minimal WAV container for
200
+ * expo-av playback. 44-byte header + samples. */
201
+ function wrapPCMAsWAV(pcm, sampleRate, channels = 1) {
202
+ const bitsPerSample = 16;
203
+ const byteRate = (sampleRate * channels * bitsPerSample) / 8;
204
+ const blockAlign = (channels * bitsPerSample) / 8;
205
+ const dataSize = pcm.length;
206
+ const buf = new Uint8Array(44 + dataSize);
207
+ const dv = new DataView(buf.buffer);
208
+ buf[0] = 0x52;
209
+ buf[1] = 0x49;
210
+ buf[2] = 0x46;
211
+ buf[3] = 0x46; // RIFF
212
+ dv.setUint32(4, 36 + dataSize, true);
213
+ buf[8] = 0x57;
214
+ buf[9] = 0x41;
215
+ buf[10] = 0x56;
216
+ buf[11] = 0x45; // WAVE
217
+ buf[12] = 0x66;
218
+ buf[13] = 0x6d;
219
+ buf[14] = 0x74;
220
+ buf[15] = 0x20; // "fmt "
221
+ dv.setUint32(16, 16, true);
222
+ dv.setUint16(20, 1, true);
223
+ dv.setUint16(22, channels, true);
224
+ dv.setUint32(24, sampleRate, true);
225
+ dv.setUint32(28, byteRate, true);
226
+ dv.setUint16(32, blockAlign, true);
227
+ dv.setUint16(34, bitsPerSample, true);
228
+ buf[36] = 0x64;
229
+ buf[37] = 0x61;
230
+ buf[38] = 0x74;
231
+ buf[39] = 0x61; // data
232
+ dv.setUint32(40, dataSize, true);
233
+ buf.set(pcm, 44);
234
+ return buf;
235
+ }
236
+ /** Write a PCM buffer to a temp WAV file; returns its file:// URI. */
237
+ async function pcmToTempWavURI(pcm, sampleRate) {
238
+ const Buffer = loadBuffer();
239
+ const FileSystem = loadFS();
240
+ const wav = wrapPCMAsWAV(pcm, sampleRate);
241
+ const b64 = Buffer.from(wav).toString('base64');
242
+ const dir = FileSystem.cacheDirectory ?? FileSystem.documentDirectory ?? '';
243
+ const path = `${dir}yaver-voice-tts-${Date.now()}.wav`;
244
+ await FileSystem.writeAsStringAsync(path, b64, { encoding: 'base64' });
245
+ return path;
246
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.8.12",
4
- "description": "Visual feedback SDK for Yaver \u2014 bug reports, screen recording, voice annotations, and local-first developer workflows",
3
+ "version": "0.9.0",
4
+ "description": "Visual feedback SDK for Yaver \u2014 bug reports, screen recording, voice vibe coding, and local-first developer workflows",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
@@ -23,9 +23,23 @@
23
23
  "@react-native-async-storage/async-storage": ">=1.17.0",
24
24
  "expo-web-browser": ">=12.0.0",
25
25
  "expo-apple-authentication": ">=6.0.0",
26
- "expo-document-picker": ">=13.0.0"
26
+ "expo-document-picker": ">=13.0.0",
27
+ "react-native-view-shot": ">=3.0.0",
28
+ "react-native-record-screen": ">=0.0.10",
29
+ "expo-av": ">=14.0.0",
30
+ "expo-file-system": ">=16.0.0",
31
+ "expo-speech": ">=12.0.0"
32
+ },
33
+ "dependencies": {
34
+ "buffer": "^6.0.3"
27
35
  },
28
36
  "peerDependenciesMeta": {
37
+ "expo-file-system": {
38
+ "optional": true
39
+ },
40
+ "expo-speech": {
41
+ "optional": true
42
+ },
29
43
  "@expo/config-plugins": {
30
44
  "optional": true
31
45
  },
@@ -37,6 +51,15 @@
37
51
  },
38
52
  "expo-document-picker": {
39
53
  "optional": true
54
+ },
55
+ "react-native-view-shot": {
56
+ "optional": true
57
+ },
58
+ "react-native-record-screen": {
59
+ "optional": true
60
+ },
61
+ "expo-av": {
62
+ "optional": true
40
63
  }
41
64
  },
42
65
  "devDependencies": {