yaver-feedback-react-native 0.8.13 → 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/FeedbackModal.js +318 -61
- package/dist/MachinePickerScreen.js +14 -5
- package/dist/P2PClient.d.ts +31 -1
- package/dist/P2PClient.js +59 -0
- package/dist/VibeChatScreen.d.ts +6 -1
- package/dist/VibeChatScreen.js +204 -1
- package/dist/capture.d.ts +6 -0
- package/dist/capture.js +53 -0
- package/dist/types.d.ts +57 -2
- package/dist/voice.d.ts +61 -0
- package/dist/voice.js +246 -0
- package/package.json +14 -3
- package/src/FeedbackModal.tsx +388 -74
- package/src/MachinePickerScreen.tsx +12 -3
- package/src/P2PClient.ts +79 -0
- package/src/VibeChatScreen.tsx +219 -0
- package/src/capture.ts +51 -0
- package/src/types.ts +53 -2
- package/src/voice.ts +270 -0
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.
|
|
4
|
-
"description": "Visual feedback SDK for Yaver \u2014 bug reports, screen recording, voice
|
|
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": [
|
|
@@ -26,9 +26,20 @@
|
|
|
26
26
|
"expo-document-picker": ">=13.0.0",
|
|
27
27
|
"react-native-view-shot": ">=3.0.0",
|
|
28
28
|
"react-native-record-screen": ">=0.0.10",
|
|
29
|
-
"expo-av": ">=14.0.0"
|
|
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"
|
|
30
35
|
},
|
|
31
36
|
"peerDependenciesMeta": {
|
|
37
|
+
"expo-file-system": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"expo-speech": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
32
43
|
"@expo/config-plugins": {
|
|
33
44
|
"optional": true
|
|
34
45
|
},
|