@saywright/studio 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +63 -2
- package/bin/saywright.mjs +10 -0
- package/dist/THIRD_PARTY_NOTICES.md +2699 -0
- package/dist/axe.min.js +12 -0
- package/dist/bridge.js +6582 -0
- package/dist/cli.mjs +56050 -0
- package/dist/client/assets/Calibration-xY4YDLzU.js +1 -0
- package/dist/client/assets/Calibration-xY4YDLzU.js.br +0 -0
- package/dist/client/assets/Calibration-xY4YDLzU.js.gz +0 -0
- package/dist/client/assets/CodeViews-ytEy2pIT.js +3 -0
- package/dist/client/assets/CodeViews-ytEy2pIT.js.br +0 -0
- package/dist/client/assets/CodeViews-ytEy2pIT.js.gz +0 -0
- package/dist/client/assets/CommandPalette-BA3akF-p.js +1 -0
- package/dist/client/assets/CommandPalette-BA3akF-p.js.br +0 -0
- package/dist/client/assets/CommandPalette-BA3akF-p.js.gz +0 -0
- package/dist/client/assets/HelpDialog-C6qmdEwi.js +1 -0
- package/dist/client/assets/HelpDialog-C6qmdEwi.js.br +0 -0
- package/dist/client/assets/HelpDialog-C6qmdEwi.js.gz +0 -0
- package/dist/client/assets/ReportView-Bj9xxLCi.js +4 -0
- package/dist/client/assets/ReportView-Bj9xxLCi.js.br +0 -0
- package/dist/client/assets/ReportView-Bj9xxLCi.js.gz +0 -0
- package/dist/client/assets/SettingsDialog-VrACcdUX.js +1 -0
- package/dist/client/assets/SettingsDialog-VrACcdUX.js.br +0 -0
- package/dist/client/assets/SettingsDialog-VrACcdUX.js.gz +0 -0
- package/dist/client/assets/asr.worker-FcRNDSsJ.js +17 -0
- package/dist/client/assets/asr.worker-FcRNDSsJ.js.br +0 -0
- package/dist/client/assets/asr.worker-FcRNDSsJ.js.gz +0 -0
- package/dist/client/assets/button-B2NFEpCs.js +18 -0
- package/dist/client/assets/button-B2NFEpCs.js.br +0 -0
- package/dist/client/assets/button-B2NFEpCs.js.gz +0 -0
- package/dist/client/assets/camera-BD9K1f5I.js +1 -0
- package/dist/client/assets/cn-Wq33h1G4.js +1 -0
- package/dist/client/assets/cn-Wq33h1G4.js.br +0 -0
- package/dist/client/assets/cn-Wq33h1G4.js.gz +0 -0
- package/dist/client/assets/confirm-D1U-bFIL.js +41 -0
- package/dist/client/assets/confirm-D1U-bFIL.js.br +0 -0
- package/dist/client/assets/confirm-D1U-bFIL.js.gz +0 -0
- package/dist/client/assets/footprints-DiYtIryA.js +1 -0
- package/dist/client/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/client/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/client/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/client/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/client/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/client/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/client/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/client/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/client/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/client/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/client/assets/index-BakNwOFF.css +2 -0
- package/dist/client/assets/index-BakNwOFF.css.br +0 -0
- package/dist/client/assets/index-BakNwOFF.css.gz +0 -0
- package/dist/client/assets/index-DUAtV_Av.js +18 -0
- package/dist/client/assets/index-DUAtV_Av.js.br +0 -0
- package/dist/client/assets/index-DUAtV_Av.js.gz +0 -0
- package/dist/client/assets/ort-wasm-simd-threaded-DcHrbrbl.wasm +0 -0
- package/dist/client/assets/preload-helper-BLdHbfdN.js +1 -0
- package/dist/client/assets/preload-helper-BLdHbfdN.js.br +0 -0
- package/dist/client/assets/preload-helper-BLdHbfdN.js.gz +0 -0
- package/dist/client/assets/refresh-cw-Dobx2YmO.js +1 -0
- package/dist/client/assets/tracker.worker-DWwj76VA.js +41 -0
- package/dist/client/assets/tracker.worker-DWwj76VA.js.br +0 -0
- package/dist/client/assets/tracker.worker-DWwj76VA.js.gz +0 -0
- package/dist/client/assets/vision_bundle-ByYCzljp.js +41 -0
- package/dist/client/assets/vision_bundle-ByYCzljp.js.br +0 -0
- package/dist/client/assets/vision_bundle-ByYCzljp.js.gz +0 -0
- package/dist/client/favicon.svg +1 -0
- package/dist/client/index.html +27 -0
- package/dist/client/index.html.br +0 -0
- package/dist/client/index.html.gz +0 -0
- package/dist/client/voice-capture-worklet.js +23 -0
- package/dist/sample/index.html +13 -0
- package/dist/sample/package.json +23 -0
- package/dist/sample/playwright.config.ts +20 -0
- package/dist/sample/saywright.config.json +68 -0
- package/dist/sample/src/App.tsx +32 -0
- package/dist/sample/src/components/AddressCard.tsx +16 -0
- package/dist/sample/src/components/CouponField.tsx +50 -0
- package/dist/sample/src/components/Header.tsx +26 -0
- package/dist/sample/src/components/LineItems.tsx +22 -0
- package/dist/sample/src/components/OrderSummary.tsx +32 -0
- package/dist/sample/src/components/ProductCard.tsx +26 -0
- package/dist/sample/src/ds/Badge.tsx +14 -0
- package/dist/sample/src/ds/Button.tsx +16 -0
- package/dist/sample/src/ds/Card.tsx +17 -0
- package/dist/sample/src/ds/Heading.tsx +14 -0
- package/dist/sample/src/ds/ProductGrid.tsx +15 -0
- package/dist/sample/src/ds/SummaryRow.tsx +18 -0
- package/dist/sample/src/ds/TextField.tsx +31 -0
- package/dist/sample/src/ds/index.ts +7 -0
- package/dist/sample/src/lib/cart.ts +20 -0
- package/dist/sample/src/lib/money.ts +7 -0
- package/dist/sample/src/lib/pricing.ts +34 -0
- package/dist/sample/src/lib/route.ts +17 -0
- package/dist/sample/src/main.tsx +10 -0
- package/dist/sample/src/pages/Checkout.tsx +37 -0
- package/dist/sample/src/pages/Shop.tsx +27 -0
- package/dist/sample/src/styles.css +155 -0
- package/dist/sample/tests/smoke.spec.ts +7 -0
- package/dist/sample/tsconfig.json +37 -0
- package/dist/sample/vite.config.ts +8 -0
- package/dist/server/node-BgmEDdmn.mjs +23 -0
- package/dist/server/node-DuXxFg5I.mjs +2501 -0
- package/dist/server/speech-worker.mjs +266 -0
- package/package.json +97 -4
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Speech recognition, in its own process so heavy decoding never stalls the
|
|
2
|
+
// studio server. Talks to the parent over IPC (advanced serialization, so
|
|
3
|
+
// Float32Arrays cross without copying into JSON).
|
|
4
|
+
//
|
|
5
|
+
// → { type: 'load', key, config, tail? } create a recognizer; tail: seconds of silence after speech
|
|
6
|
+
// → { type: 'transcribe', id, key, samples, options, priority }
|
|
7
|
+
// ← { type: 'result', id, text, ms } | { type: 'error', id, message }
|
|
8
|
+
// → { type: 'stream-open', sid, plan, vad } live narration; plan = { primary, arabic?, latin? },
|
|
9
|
+
// each { key, options }: see narrationPlan in catalog.ts
|
|
10
|
+
// → { type: 'stream-push', sid, samples } 16 kHz float samples
|
|
11
|
+
// → { type: 'stream-close', sid }
|
|
12
|
+
// ← { type: 'segment', sid, start, end, text, ms, key } start/end in samples; key: the model that wrote it
|
|
13
|
+
// ← { type: 'stream-closed', sid }
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
let sherpa;
|
|
18
|
+
try {
|
|
19
|
+
sherpa = require('sherpa-onnx-node');
|
|
20
|
+
} catch (err) {
|
|
21
|
+
process.send?.({ type: 'fatal', message: `Speech runtime unavailable: ${err?.message ?? err}` });
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const RATE = 16000;
|
|
26
|
+
const recognizers = new Map(); // key -> Promise<OfflineRecognizer>
|
|
27
|
+
const configs = new Map();
|
|
28
|
+
const tails = new Map(); // key -> seconds of silence to add after the speech
|
|
29
|
+
|
|
30
|
+
function recognizer(key) {
|
|
31
|
+
let r = recognizers.get(key);
|
|
32
|
+
if (!r) {
|
|
33
|
+
const config = configs.get(key);
|
|
34
|
+
if (!config) return Promise.reject(new Error(`Model ${key} is not loaded`));
|
|
35
|
+
r = sherpa.OfflineRecognizer.createAsync(config);
|
|
36
|
+
recognizers.set(key, r);
|
|
37
|
+
r.catch(() => recognizers.delete(key));
|
|
38
|
+
}
|
|
39
|
+
return r;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// One decode at a time keeps latency predictable; a spoken command jumps the queue ahead of narration.
|
|
43
|
+
const queue = [];
|
|
44
|
+
let busy = false;
|
|
45
|
+
|
|
46
|
+
function enqueue(job, priority) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const item = { job, resolve, reject, priority };
|
|
49
|
+
if (priority === 'high') {
|
|
50
|
+
const i = queue.findIndex((q) => q.priority !== 'high');
|
|
51
|
+
queue.splice(i < 0 ? queue.length : i, 0, item);
|
|
52
|
+
} else queue.push(item);
|
|
53
|
+
void pump();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function pump() {
|
|
58
|
+
if (busy) return;
|
|
59
|
+
const item = queue.shift();
|
|
60
|
+
if (!item) return;
|
|
61
|
+
busy = true;
|
|
62
|
+
try {
|
|
63
|
+
item.resolve(await item.job());
|
|
64
|
+
} catch (err) {
|
|
65
|
+
item.reject(err);
|
|
66
|
+
} finally {
|
|
67
|
+
busy = false;
|
|
68
|
+
void pump();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Silence around the speech helps models that clip word edges: a quarter second before, and what the model needs after. */
|
|
73
|
+
function padded(samples, tail = 0.25) {
|
|
74
|
+
const lead = Math.round(RATE * 0.25);
|
|
75
|
+
const out = new Float32Array(lead + samples.length + Math.round(RATE * tail));
|
|
76
|
+
out.set(samples, lead);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Some models write an amount straight after the word before it (“should show$80.80”): put the space back. */
|
|
81
|
+
const tidy = (text) => text.replace(/(\p{L})([$€£])(?=\d)/gu, '$1 $2');
|
|
82
|
+
|
|
83
|
+
async function decode(key, samples, options) {
|
|
84
|
+
const rec = await recognizer(key);
|
|
85
|
+
const stream = rec.createStream();
|
|
86
|
+
for (const [k, v] of Object.entries(options ?? {})) stream.setOption(k, String(v));
|
|
87
|
+
stream.acceptWaveform({ sampleRate: RATE, samples: padded(samples, tails.get(key)) });
|
|
88
|
+
const t = performance.now();
|
|
89
|
+
const result = await rec.decodeAsync(stream);
|
|
90
|
+
return { text: tidy(String(result?.text ?? '').trim()), ms: Math.round(performance.now() - t) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---- live narration: voice activity detection, then a decode per sentence ----
|
|
94
|
+
|
|
95
|
+
const streams = new Map(); // sid -> { vad, plan, pending: Promise[], window: Float32Array, fill, recent: Recent }
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The last half minute of a stream's audio. The voice detector trims a
|
|
99
|
+
* sentence to where it heard speech, which can cut a soft last syllable
|
|
100
|
+
* ("ة", a trailing "s"); each sentence is decoded with the real audio just
|
|
101
|
+
* around it instead.
|
|
102
|
+
*/
|
|
103
|
+
class Recent {
|
|
104
|
+
constructor(seconds) {
|
|
105
|
+
this.buf = new Float32Array(RATE * seconds);
|
|
106
|
+
this.total = 0; // samples ever pushed: the same count the voice detector's positions use
|
|
107
|
+
}
|
|
108
|
+
push(x) {
|
|
109
|
+
for (let i = 0; i < x.length; ) {
|
|
110
|
+
const at = this.total % this.buf.length;
|
|
111
|
+
const n = Math.min(x.length - i, this.buf.length - at);
|
|
112
|
+
this.buf.set(x.subarray(i, i + n), at);
|
|
113
|
+
i += n;
|
|
114
|
+
this.total += n;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
slice(from, to) {
|
|
118
|
+
from = Math.max(from, this.total - this.buf.length, 0);
|
|
119
|
+
to = Math.min(to, this.total);
|
|
120
|
+
const out = new Float32Array(Math.max(0, to - from));
|
|
121
|
+
for (let k = from, o = 0; k < to; ) {
|
|
122
|
+
const at = k % this.buf.length;
|
|
123
|
+
const n = Math.min(to - k, this.buf.length - at);
|
|
124
|
+
out.set(this.buf.subarray(at, at + n), o);
|
|
125
|
+
o += n;
|
|
126
|
+
k += n;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Real audio kept around each sentence: a little before, and more after (it ends after 0.45 s of quiet, so that much has arrived). */
|
|
133
|
+
const BEFORE = Math.round(RATE * 0.2);
|
|
134
|
+
const AFTER = Math.round(RATE * 0.4);
|
|
135
|
+
|
|
136
|
+
/** Arabic or Latin letters: which one most of the text is written in. */
|
|
137
|
+
function script(text) {
|
|
138
|
+
const ar = (text.match(/[\u0600-\u06FF]/g) || []).length;
|
|
139
|
+
const latin = (text.match(/[A-Za-z]/g) || []).length;
|
|
140
|
+
if (ar + latin < 3) return null;
|
|
141
|
+
if (ar >= latin * 3) return 'arabic';
|
|
142
|
+
if (latin >= ar * 3) return 'latin';
|
|
143
|
+
return null; // mixed: the multilingual model's own words are best
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Transcribe one sentence by the plan: the primary model, then the better model for its language when there is one. */
|
|
147
|
+
async function decodePlanned(plan, samples) {
|
|
148
|
+
const first = await decode(plan.primary.key, samples, plan.primary.options);
|
|
149
|
+
const which = script(first.text);
|
|
150
|
+
const better = which && plan[which];
|
|
151
|
+
if (!better) return { ...first, key: plan.primary.key };
|
|
152
|
+
try {
|
|
153
|
+
const second = await decode(better.key, samples, better.options);
|
|
154
|
+
if (second.text) return { text: second.text, ms: first.ms + second.ms, key: better.key };
|
|
155
|
+
} catch {
|
|
156
|
+
/* keep the first transcript */
|
|
157
|
+
}
|
|
158
|
+
return { ...first, key: plan.primary.key };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function openStream(m) {
|
|
162
|
+
const vad = new sherpa.Vad(
|
|
163
|
+
{
|
|
164
|
+
sileroVad: {
|
|
165
|
+
model: m.vad,
|
|
166
|
+
threshold: 0.45,
|
|
167
|
+
minSilenceDuration: 0.45,
|
|
168
|
+
minSpeechDuration: 0.2,
|
|
169
|
+
maxSpeechDuration: m.maxSpeech ?? 12,
|
|
170
|
+
windowSize: 512,
|
|
171
|
+
},
|
|
172
|
+
sampleRate: RATE,
|
|
173
|
+
debug: false,
|
|
174
|
+
numThreads: 1,
|
|
175
|
+
},
|
|
176
|
+
60,
|
|
177
|
+
);
|
|
178
|
+
streams.set(m.sid, { vad, plan: m.plan, pending: [], window: new Float32Array(512), fill: 0, recent: new Recent(30) });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function drain(sid, s) {
|
|
182
|
+
while (!s.vad.isEmpty()) {
|
|
183
|
+
// A copy: the default view points into native memory that pop() frees.
|
|
184
|
+
const seg = s.vad.front(false);
|
|
185
|
+
s.vad.pop();
|
|
186
|
+
const start = seg.start;
|
|
187
|
+
const end = start + seg.samples.length;
|
|
188
|
+
const around = s.recent.slice(start - BEFORE, end + AFTER);
|
|
189
|
+
const samples = around.length >= seg.samples.length ? around : seg.samples;
|
|
190
|
+
const p = enqueue(() => decodePlanned(s.plan, samples), 'low')
|
|
191
|
+
.then((r) => process.send?.({ type: 'segment', sid, start, end, text: r.text, ms: r.ms, key: r.key }))
|
|
192
|
+
.catch((err) => process.send?.({ type: 'segment', sid, start, end, text: '', ms: 0, error: String(err?.message ?? err) }));
|
|
193
|
+
s.pending.push(p);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function pushStream(m) {
|
|
198
|
+
const s = streams.get(m.sid);
|
|
199
|
+
if (!s) return;
|
|
200
|
+
// The VAD wants exact 512-sample windows.
|
|
201
|
+
const x = m.samples;
|
|
202
|
+
let i = 0;
|
|
203
|
+
while (i < x.length) {
|
|
204
|
+
const take = Math.min(512 - s.fill, x.length - i);
|
|
205
|
+
s.window.set(x.subarray(i, i + take), s.fill);
|
|
206
|
+
s.fill += take;
|
|
207
|
+
i += take;
|
|
208
|
+
if (s.fill === 512) {
|
|
209
|
+
s.vad.acceptWaveform(s.window);
|
|
210
|
+
s.recent.push(s.window);
|
|
211
|
+
s.fill = 0;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
drain(m.sid, s);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function closeStream(m) {
|
|
218
|
+
const s = streams.get(m.sid);
|
|
219
|
+
if (!s) return process.send?.({ type: 'stream-closed', sid: m.sid });
|
|
220
|
+
if (s.fill) {
|
|
221
|
+
const rest = new Float32Array(512);
|
|
222
|
+
rest.set(s.window.subarray(0, s.fill));
|
|
223
|
+
s.vad.acceptWaveform(rest);
|
|
224
|
+
s.recent.push(rest);
|
|
225
|
+
}
|
|
226
|
+
s.vad.flush();
|
|
227
|
+
drain(m.sid, s);
|
|
228
|
+
await Promise.allSettled(s.pending);
|
|
229
|
+
streams.delete(m.sid);
|
|
230
|
+
process.send?.({ type: 'stream-closed', sid: m.sid });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
process.on('message', (m) => {
|
|
234
|
+
switch (m?.type) {
|
|
235
|
+
case 'load':
|
|
236
|
+
configs.set(m.key, m.config);
|
|
237
|
+
if (typeof m.tail === 'number') tails.set(m.key, m.tail);
|
|
238
|
+
recognizer(m.key).then(
|
|
239
|
+
() => process.send?.({ type: 'loaded', key: m.key }),
|
|
240
|
+
(err) => process.send?.({ type: 'load-error', key: m.key, message: String(err?.message ?? err) }),
|
|
241
|
+
);
|
|
242
|
+
break;
|
|
243
|
+
case 'transcribe':
|
|
244
|
+
enqueue(() => decode(m.key, m.samples, m.options), m.priority === 'low' ? 'low' : 'high').then(
|
|
245
|
+
(r) => process.send?.({ type: 'result', id: m.id, ...r }),
|
|
246
|
+
(err) => process.send?.({ type: 'error', id: m.id, message: String(err?.message ?? err) }),
|
|
247
|
+
);
|
|
248
|
+
break;
|
|
249
|
+
case 'stream-open':
|
|
250
|
+
try {
|
|
251
|
+
openStream(m);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
process.send?.({ type: 'stream-error', sid: m.sid, message: String(err?.message ?? err) });
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
case 'stream-push':
|
|
257
|
+
pushStream(m);
|
|
258
|
+
break;
|
|
259
|
+
case 'stream-close':
|
|
260
|
+
void closeStream(m);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
process.on('disconnect', () => process.exit(0));
|
|
266
|
+
process.send?.({ type: 'ready', version: sherpa.version });
|
package/package.json
CHANGED
|
@@ -1,6 +1,99 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saywright/studio",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Point at your running web app and say what to change. A studio on your computer for any web app: edits traced to the exact source line, Playwright tests from plain words, session reports, AI testing and accessibility reviews, with the AI you already have.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"developer-tools",
|
|
7
|
+
"devtools",
|
|
8
|
+
"react",
|
|
9
|
+
"vite",
|
|
10
|
+
"nextjs",
|
|
11
|
+
"visual-editing",
|
|
12
|
+
"click-to-source",
|
|
13
|
+
"open-in-editor",
|
|
14
|
+
"playwright",
|
|
15
|
+
"test-generation",
|
|
16
|
+
"e2e-testing",
|
|
17
|
+
"test-automation",
|
|
18
|
+
"session-recording",
|
|
19
|
+
"qa",
|
|
20
|
+
"ai",
|
|
21
|
+
"llm",
|
|
22
|
+
"ai-agent",
|
|
23
|
+
"mcp",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"codex",
|
|
26
|
+
"gemini-cli",
|
|
27
|
+
"accessibility",
|
|
28
|
+
"a11y",
|
|
29
|
+
"wcag",
|
|
30
|
+
"seo",
|
|
31
|
+
"voice-control",
|
|
32
|
+
"speech-to-text"
|
|
33
|
+
],
|
|
34
|
+
"license": "Apache-2.0",
|
|
35
|
+
"homepage": "https://github.com/ahmedbakr/saywright#readme",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/ahmedbakr/saywright.git",
|
|
39
|
+
"directory": "apps/studio"
|
|
40
|
+
},
|
|
41
|
+
"bugs": "https://github.com/ahmedbakr/saywright/issues",
|
|
42
|
+
"type": "module",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"bin": {
|
|
47
|
+
"saywright": "bin/saywright.mjs"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"bin",
|
|
51
|
+
"dist",
|
|
52
|
+
"README.md",
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"NOTICE"
|
|
55
|
+
],
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": "^22.18.0 || >=24.11.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "node build.mjs",
|
|
61
|
+
"prepack": "node build.mjs",
|
|
62
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
63
|
+
},
|
|
64
|
+
"optionalDependencies": {
|
|
65
|
+
"sherpa-onnx-node": "^1.13.8"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@anthropic-ai/sdk": "^0.133.0",
|
|
69
|
+
"@babel/parser": "^8.0.7",
|
|
70
|
+
"@fontsource-variable/geist": "^5.3.0",
|
|
71
|
+
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
72
|
+
"@mediapipe/tasks-vision": "^1.1.0",
|
|
73
|
+
"@saywright/vite": "*",
|
|
74
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
75
|
+
"@types/cross-spawn": "^6.0.6",
|
|
76
|
+
"@types/react": "^19.3.0",
|
|
77
|
+
"@types/react-dom": "^19.3.0",
|
|
78
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
79
|
+
"axe-core": "^4.14.0",
|
|
80
|
+
"clsx": "^2.1.1",
|
|
81
|
+
"cmdk": "^1.1.1",
|
|
82
|
+
"cross-spawn": "^7.0.6",
|
|
83
|
+
"diff": "^9.0.0",
|
|
84
|
+
"launch-editor": "^2.14.2",
|
|
85
|
+
"llama-tokenizer-js": "^1.2.2",
|
|
86
|
+
"lucide-react": "^1.56.0",
|
|
87
|
+
"magic-string": "^1.4.3",
|
|
88
|
+
"motion": "^14.1.1",
|
|
89
|
+
"onnxruntime-web": "^1.30.0",
|
|
90
|
+
"prism-react-renderer": "^2.4.1",
|
|
91
|
+
"radix-ui": "^1.8.1",
|
|
92
|
+
"react": "^19.3.0",
|
|
93
|
+
"react-dom": "^19.3.0",
|
|
94
|
+
"tailwind-merge": "^3.7.0",
|
|
95
|
+
"tailwindcss": "^4.3.3",
|
|
96
|
+
"zod": "^4.6.7",
|
|
97
|
+
"zustand": "^5.0.15"
|
|
98
|
+
}
|
|
99
|
+
}
|