clay-server 4.3.1-beta.4 → 4.4.0-beta.1

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.
Files changed (37) hide show
  1. package/lib/issues-filing-guidance.js +12 -0
  2. package/lib/issues-mcp-server.js +1 -1
  3. package/lib/project-logs-mcp-server.js +1 -1
  4. package/lib/public/app.js +13 -5
  5. package/lib/public/css/home-hub.css +1 -0
  6. package/lib/public/css/messages.css +1 -2
  7. package/lib/public/css/stt.css +106 -155
  8. package/lib/public/icons/speech/SOURCES.txt +5 -0
  9. package/lib/public/icons/speech/deepgram.svg +1 -0
  10. package/lib/public/icons/speech/elevenlabs.svg +1 -0
  11. package/lib/public/icons/speech/groq.svg +1 -0
  12. package/lib/public/icons/speech/soniox.png +0 -0
  13. package/lib/public/index.html +3 -2
  14. package/lib/public/modules/home-mate-chat.js +8 -0
  15. package/lib/public/modules/input.js +2 -0
  16. package/lib/public/modules/profile.js +0 -4
  17. package/lib/public/modules/stt-audio-worklet.js +32 -0
  18. package/lib/public/modules/stt-audio.js +68 -0
  19. package/lib/public/modules/stt-controller.js +202 -0
  20. package/lib/public/modules/stt-debug.js +7 -0
  21. package/lib/public/modules/stt-meter.js +55 -0
  22. package/lib/public/modules/stt-picker.js +123 -0
  23. package/lib/public/modules/stt-recording.js +130 -0
  24. package/lib/public/modules/stt-settings.js +50 -0
  25. package/lib/public/modules/stt-signal.js +57 -0
  26. package/lib/public/modules/stt.js +113 -252
  27. package/lib/public/modules/user-settings.js +15 -9
  28. package/lib/public/modules/worker-pane-lock.js +1 -0
  29. package/lib/server-speech.js +166 -0
  30. package/lib/server.js +8 -0
  31. package/lib/session-notes-mcp-server.js +4 -3
  32. package/lib/speech-catalog.js +25 -0
  33. package/lib/speech-openai-stream.js +102 -0
  34. package/lib/speech-store.js +71 -0
  35. package/lib/speech-stream.js +108 -0
  36. package/lib/speech-transcribe.js +63 -0
  37. package/package.json +1 -1
@@ -4,8 +4,9 @@ var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
5
  var MEMORY_CONTRACT =
6
6
  "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
7
- "Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository, an Issue, or another note, and the user would likely be glad to find it on the board a week later. " +
8
- "Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; they are not mandatory duplicates of Project Issues. For concrete actionable bugs, improvements, or deferred implementation, an authorized Project Driver should use Project Issues. If you lack Issues authority, route the work through an eligible Project Driver. Check active notes first when a duplicate is plausible. " +
7
+ "Except for confirmed Issue link notes described below, default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository, an Issue, or another note, and the user would likely be glad to find it on the board a week later. " +
8
+ "Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; confirmed Issues also get short link notes as described below. For concrete actionable bugs, improvements, or deferred implementation, an authorized Project Driver should use Project Issues. If you lack Issues authority, route the work through an eligible Project Driver. Check active notes first when a duplicate is plausible. " +
9
+ require("./issues-filing-guidance").NOTE_GUIDANCE +
9
10
  "Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
10
11
  "Updates are visible too: update only when durable state materially changes. " +
11
12
  "A note is an attention item, so finishing it means closing it, never erasing it: call close_note once the thing it is asking for is done, and the note leaves the active board while the record stays and can be reopened. Do not turn a note into a completion log instead of closing it. " +
@@ -23,7 +24,7 @@ function getToolDefs(handlers) {
23
24
  },
24
25
  {
25
26
  name: "write_note",
26
- description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. Before creating, apply this test: would the user likely choose to keep this visible on their board next week? If the answer is unclear, do not call this tool. The 20000-character limit is an abuse guard, not a target.",
27
+ description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. For notes other than confirmed Issue link notes, apply this test: would the user likely choose to keep this visible on their board next week? If the answer is unclear, do not call this tool. The 20000-character limit is an abuse guard, not a target.",
27
28
  inputSchema: buildShape({
28
29
  id: { type: "string", description: "Existing note id to update. Omit to create a note." },
29
30
  text: { type: "string", description: "User-facing sticky-note text with a concise title on the first line and only durable, action-relevant context below it." },
@@ -0,0 +1,25 @@
1
+ // Provider IDs and capabilities are server-owned; clients cannot choose upstream URLs.
2
+ var providers = [
3
+ { id: 'openai', name: 'OpenAI', icon: '/codex-avatar.png', url: 'https://platform.openai.com/api-keys' },
4
+ { id: 'elevenlabs', name: 'ElevenLabs', icon: '/icons/speech/elevenlabs.svg', url: 'https://elevenlabs.io/app/settings/api-keys' },
5
+ { id: 'soniox', name: 'Soniox', icon: '/icons/speech/soniox.png', url: 'https://console.soniox.com/' },
6
+ { id: 'groq', name: 'Groq', icon: '/icons/speech/groq.svg', url: 'https://console.groq.com/keys' },
7
+ { id: 'deepgram', name: 'Deepgram', icon: '/icons/speech/deepgram.svg', url: 'https://console.deepgram.com/' }
8
+ ];
9
+ var models = [
10
+ ['openai-live', 'openai', 'GPT Live Transcribe', 'live', 'gpt-live-transcribe', 'Low-latency transcription with text appearing as you speak.'],
11
+ ['openai-transcribe', 'openai', 'GPT Transcribe', 'recorded', 'gpt-transcribe', 'Transcribes your recording after you stop.'],
12
+ ['openai-whisper', 'openai', 'Whisper', 'recorded', 'whisper-1', 'General-purpose multilingual transcription.'],
13
+ ['browser', 'browser', 'Browser speech', 'live', '', 'Uses your browser’s speech service. No API key required.'],
14
+ ['elevenlabs-live', 'elevenlabs', 'Scribe v2 Realtime', 'live', 'scribe_v2_realtime', 'Live multilingual dictation with partial text as you speak.'],
15
+ ['elevenlabs-file', 'elevenlabs', 'Scribe v2', 'recorded', 'scribe_v2', 'Multilingual transcription of completed recordings.'],
16
+ ['soniox-live', 'soniox', 'Soniox Realtime', 'live', 'stt-rt-v5', 'Live transcription with automatic language recognition.'],
17
+ ['soniox-file', 'soniox', 'Soniox Async', 'recorded', 'stt-async-v5', 'Asynchronous transcription of completed recordings.'],
18
+ ['groq-whisper', 'groq', 'Whisper Large v3', 'recorded', 'whisper-large-v3', 'Multilingual Whisper transcription hosted by Groq.'],
19
+ ['groq-turbo', 'groq', 'Whisper Turbo', 'recorded', 'whisper-large-v3-turbo', 'A faster Whisper variant hosted by Groq.'],
20
+ ['deepgram-live', 'deepgram', 'Nova-3', 'live', 'nova-3', 'Live transcription in your selected language.'],
21
+ ['deepgram-file', 'deepgram', 'Nova-3', 'recorded', 'nova-3', 'Recorded-audio transcription in your selected language.']
22
+ ].map(function (row) { return { id: row[0], provider: row[1], name: row[2], mode: row[3], model: row[4], description: row[5] }; });
23
+ function findModel(id) { return models.find(function (model) { return model.id === id; }); }
24
+ function findProvider(id) { return providers.find(function (provider) { return provider.id === id; }); }
25
+ module.exports = { models: models, providers: providers, findModel: findModel, findProvider: findProvider };
@@ -0,0 +1,102 @@
1
+ var providerError = require('./speech-transcribe').providerError;
2
+
3
+ // Interpolate the shared 16 kHz PCM stream to OpenAI's required 24 kHz,
4
+ // carrying sample position across chunks so chunk boundaries do not lose audio.
5
+ function pcmResampler() {
6
+ var count = 0; var tick = 0; var previous = 0;
7
+ return function (bytes, flush) {
8
+ var samples = [];
9
+ for (var at = 0; at < bytes.length; at += 2) {
10
+ var current = bytes.readInt16LE(at);
11
+ while (tick <= count * 3) {
12
+ var fraction = (tick - (count - 1) * 3) / 3;
13
+ samples.push(count ? Math.round(previous + (current - previous) * fraction) : current);
14
+ tick += 2;
15
+ }
16
+ previous = current; count++;
17
+ }
18
+ if (flush) while (tick < count * 3) { samples.push(previous); tick += 2; }
19
+ var output = Buffer.alloc(samples.length * 2);
20
+ samples.forEach(function (sample, index) { output.writeInt16LE(sample, index * 2); });
21
+ return output;
22
+ };
23
+ }
24
+
25
+ function openOpenAIStream(model, key, language, events, Socket) {
26
+ var socket = new Socket('wss://api.openai.com/v1/realtime?intent=transcription', {
27
+ headers: { Authorization: 'Bearer ' + key }, handshakeTimeout: 15000, maxPayload: 1024 * 1024
28
+ });
29
+ var ended = false; var ready = false; var finishing = false; var timer;
30
+ var partial = ''; var itemId = null; var committedId = null; var completed = new Map();
31
+ var bytesSent = 0; var resample = pcmResampler();
32
+ function send(message) { socket.send(JSON.stringify(message)); }
33
+ function fail(error) {
34
+ if (ended) return;
35
+ ended = true; clearTimeout(timer); socket.terminate(); events.error(error);
36
+ }
37
+ function done(text) {
38
+ if (ended) return;
39
+ ended = true; clearTimeout(timer); events.done(text.trim()); socket.close();
40
+ }
41
+ function failure(message) {
42
+ var code = message.error && message.error.code;
43
+ if (code === 'invalid_api_key') return providerError(401);
44
+ if (code === 'insufficient_quota' || code === 'rate_limit_exceeded') return providerError(429);
45
+ if (code === 'model_not_found') return new Error('GPT Live Transcribe is unavailable for this OpenAI project. Check model access.');
46
+ return new Error('OpenAI could not complete live transcription. Check your API key, model access and connection.');
47
+ }
48
+ function append(bytes) {
49
+ if (bytes.length) send({ type: 'input_audio_buffer.append', audio: bytes.toString('base64') });
50
+ }
51
+ socket.on('open', function () {
52
+ if (ended) return;
53
+ send({ type: 'session.update', session: { type: 'transcription', audio: { input: {
54
+ format: { type: 'audio/pcm', rate: 24000 },
55
+ transcription: { model: model.model, languages: [language.toLowerCase().split('-')[0]] },
56
+ turn_detection: null
57
+ } } } });
58
+ });
59
+ socket.on('message', function (raw) {
60
+ if (ended) return;
61
+ try {
62
+ var message = JSON.parse(raw.toString());
63
+ if (message.type === 'error' || message.type === 'conversation.item.input_audio_transcription.failed') { fail(failure(message)); return; }
64
+ if (message.type === 'session.updated' && !ready) { ready = true; events.ready(); }
65
+ else if (message.type === 'conversation.item.input_audio_transcription.delta') {
66
+ if (!itemId) itemId = message.item_id;
67
+ if (message.item_id !== itemId) return;
68
+ partial += message.delta || ''; events.text(partial);
69
+ } else if (message.type === 'input_audio_buffer.committed') {
70
+ committedId = message.item_id;
71
+ if (finishing && completed.has(committedId)) done(completed.get(committedId));
72
+ } else if (message.type === 'conversation.item.input_audio_transcription.completed') {
73
+ if (!itemId) itemId = message.item_id;
74
+ if (message.item_id !== itemId) return;
75
+ completed.set(message.item_id, message.transcript || '');
76
+ if (finishing && message.item_id === committedId) done(message.transcript || '');
77
+ }
78
+ } catch (error) { fail(providerError(502)); }
79
+ });
80
+ socket.on('error', function () { fail(new Error('Could not connect to OpenAI live transcription. Please try again.')); });
81
+ socket.on('unexpected-response', function (request, response) { response.resume(); fail(providerError(response.statusCode)); });
82
+ socket.on('close', function () { if (!ended) fail(new Error('The OpenAI live connection ended unexpectedly. Your draft has been preserved.')); });
83
+ return {
84
+ audio: function (bytes) {
85
+ if (ended || finishing || !ready || socket.readyState !== 1) return;
86
+ if (socket.bufferedAmount > 1024 * 1024) { fail(new Error('The connection is too slow for live transcription.')); return; }
87
+ bytesSent += bytes.length; append(resample(bytes, false));
88
+ },
89
+ finish: function () {
90
+ if (ended || finishing || !ready) return;
91
+ finishing = true;
92
+ if (!bytesSent) { done(''); return; }
93
+ // OpenAI requires at least 100 ms per commit; pad only very short captures.
94
+ if (bytesSent < 3200) append(resample(Buffer.alloc(3200 - bytesSent), false));
95
+ append(resample(Buffer.alloc(0), true));
96
+ timer = setTimeout(function () { fail(new Error('OpenAI transcription timed out. Your draft has been preserved.')); }, 20000);
97
+ send({ type: 'input_audio_buffer.commit' });
98
+ },
99
+ abort: function () { ended = true; clearTimeout(timer); socket.terminate(); }
100
+ };
101
+ }
102
+ module.exports = { openOpenAIStream: openOpenAIStream, pcmResampler: pcmResampler };
@@ -0,0 +1,71 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+ var crypto = require('crypto');
4
+ var catalog = require('./speech-catalog');
5
+
6
+ function attachSpeechStore(root) {
7
+ var directory = path.join(root, 'speech');
8
+ function prepare() { fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); }
9
+ function filename(owner) {
10
+ if (typeof owner !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(owner)) throw new Error('Invalid owner');
11
+ return path.join(directory, owner + '.json');
12
+ }
13
+ function read(owner) {
14
+ try { return JSON.parse(fs.readFileSync(filename(owner), 'utf8')); }
15
+ catch (error) { if (error.code === 'ENOENT') return { selected: 'browser', keys: {} }; throw error; }
16
+ }
17
+ function masterKey() {
18
+ prepare();
19
+ var file = path.join(directory, '.key');
20
+ try { fs.writeFileSync(file, crypto.randomBytes(32), { flag: 'wx', mode: 0o600 }); }
21
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
22
+ return fs.readFileSync(file);
23
+ }
24
+ function encrypt(owner, provider, text) {
25
+ var iv = crypto.randomBytes(12);
26
+ var cipher = crypto.createCipheriv('aes-256-gcm', masterKey(), iv);
27
+ cipher.setAAD(Buffer.from(owner + ':' + provider));
28
+ var data = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
29
+ return [iv, cipher.getAuthTag(), data].map(function (part) { return part.toString('base64'); }).join('.');
30
+ }
31
+ function key(owner, provider) {
32
+ var value = (read(owner).keys || {})[provider];
33
+ if (!value) return null;
34
+ var parts = value.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
35
+ var cipher = crypto.createDecipheriv('aes-256-gcm', masterKey(), parts[0]);
36
+ cipher.setAAD(Buffer.from(owner + ':' + provider)); cipher.setAuthTag(parts[1]);
37
+ return Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8');
38
+ }
39
+ function view(owner) {
40
+ var data = read(owner);
41
+ return { selected: data.selected || 'browser', language: data.language || 'en-US', configured: Object.keys(data.keys || {}), models: catalog.models, providers: catalog.providers };
42
+ }
43
+ function update(owner, change) {
44
+ var data = read(owner); data.keys = data.keys || {};
45
+ if (change.provider !== undefined) {
46
+ if (!catalog.findProvider(change.provider)) throw new Error('Unknown speech provider');
47
+ if (change.key === null) delete data.keys[change.provider];
48
+ else {
49
+ if (typeof change.key !== 'string' || !change.key.trim() || change.key.length > 4096 || /[\r\n]/.test(change.key)) throw new Error('Enter a valid API key');
50
+ data.keys[change.provider] = encrypt(owner, change.provider, change.key.trim());
51
+ }
52
+ }
53
+ if (change.language !== undefined) {
54
+ if (typeof change.language !== 'string' || !/^[a-z]{2,3}(?:-[a-zA-Z]{2,8})?$/.test(change.language)) throw new Error('Invalid speech language');
55
+ data.language = change.language;
56
+ }
57
+ if (change.selected !== undefined) {
58
+ var model = catalog.findModel(change.selected);
59
+ if (!model || (model.provider !== 'browser' && !data.keys[model.provider])) throw new Error('Connect the provider before selecting this model');
60
+ data.selected = model.id;
61
+ }
62
+ // Keep selection after key removal so there is no silent provider fallback.
63
+ prepare();
64
+ var file = filename(owner); var temporary = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
65
+ try { fs.writeFileSync(temporary, JSON.stringify(data), { mode: 0o600 }); fs.renameSync(temporary, file); }
66
+ finally { try { fs.unlinkSync(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
67
+ return view(owner);
68
+ }
69
+ return { view: view, update: update, key: key };
70
+ }
71
+ module.exports = { attachSpeechStore: attachSpeechStore };
@@ -0,0 +1,108 @@
1
+ var WebSocket = require('ws');
2
+ var providerError = require('./speech-transcribe').providerError;
3
+
4
+ function streamError(model, message) {
5
+ var status = Number(message.error_code) || 502;
6
+ if (model.provider !== 'soniox') return providerError(status);
7
+ var reasons = {
8
+ unauthenticated: 'Soniox rejected the API key. Check the key in Voice input settings.',
9
+ permission_denied: 'The Soniox key does not have permission for live transcription. Check its permissions in Soniox Console.',
10
+ organization_balance_exhausted: 'Your Soniox credit balance is empty. Add credits in Soniox Console to use voice input.',
11
+ organization_monthly_budget_exhausted: 'Your Soniox organization has reached its monthly budget. Check its billing limits.',
12
+ project_monthly_budget_exhausted: 'Your Soniox project has reached its monthly budget. Check its billing limits.',
13
+ model_not_available: 'The selected Soniox model is unavailable for your project or region. Check model access in Soniox Console.',
14
+ invalid_request: 'Soniox rejected the live transcription configuration. Try another voice model.',
15
+ limit_exceeded: 'Your Soniox request limit was reached. Wait for other transcriptions to finish and try again.',
16
+ request_timeout: 'Soniox timed out waiting for microphone audio. Check your microphone and connection, then try again.',
17
+ service_unavailable: 'Soniox is temporarily unavailable. Please try again later.'
18
+ };
19
+ var reason = Object.prototype.hasOwnProperty.call(reasons, message.error_type) ? reasons[message.error_type] : null;
20
+ if (!reason && status === 402) reason = 'Soniox requires available credits and budget. Check billing in Soniox Console.';
21
+ return reason ? new Error(reason) : providerError(status);
22
+ }
23
+
24
+ function openSpeechStream(model, key, language, events, Socket) {
25
+ Socket = Socket || WebSocket;
26
+ if (model.provider === 'openai') return require('./speech-openai-stream').openOpenAIStream(model, key, language, events, Socket);
27
+ var code = language.split('-')[0];
28
+ var url; var options = { handshakeTimeout: 15000, maxPayload: 1024 * 1024 };
29
+ if (model.provider === 'soniox') url = 'wss://stt-rt.soniox.com/transcribe-websocket';
30
+ else if (model.provider === 'elevenlabs') {
31
+ url = 'wss://api.elevenlabs.io/v1/speech-to-text/realtime?model_id=scribe_v2_realtime&audio_format=pcm_16000&commit_strategy=manual&language_code=' + encodeURIComponent(code);
32
+ options.headers = { 'xi-api-key': key };
33
+ } else if (model.provider === 'deepgram') {
34
+ url = 'wss://api.deepgram.com/v1/listen?model=nova-3&encoding=linear16&sample_rate=16000&channels=1&interim_results=true&smart_format=true&language=' + encodeURIComponent(code);
35
+ options.headers = { Authorization: 'Token ' + key };
36
+ } else throw new Error('This model does not support live transcription');
37
+ var socket = new Socket(url, options);
38
+ var finalText = ''; var ended = false; var finishing = false; var finishTimer;
39
+ function diagnostic(stage, details) { if (events.diagnostic) events.diagnostic(stage, details); }
40
+ function fail(error) { if (!ended) { ended = true; clearTimeout(finishTimer); socket.terminate(); events.error(error); } }
41
+ function done() { if (!ended) { ended = true; clearTimeout(finishTimer); events.done(finalText.trim()); socket.close(); } }
42
+ function emit(partial) { events.text((finalText + partial).trim()); }
43
+ socket.on('open', function () {
44
+ if (ended) return;
45
+ diagnostic('upstream-websocket-open');
46
+ if (model.provider === 'soniox') socket.send(JSON.stringify({ api_key: key, model: model.model,
47
+ audio_format: 'pcm_s16le', sample_rate: 16000, num_channels: 1, language_hints: [code], enable_endpoint_detection: true }));
48
+ if (model.provider === 'soniox') diagnostic('soniox-config-sent');
49
+ if (model.provider !== 'elevenlabs') events.ready();
50
+ });
51
+ socket.on('message', function (raw) {
52
+ if (ended) return;
53
+ try {
54
+ var message = JSON.parse(raw.toString());
55
+ if (message.error || message.error_code || message.error_type || message.type === 'Error' || /error|rate_limited|quota_exceeded/.test(message.message_type || '')) {
56
+ diagnostic('upstream-error-response', { status: Number(message.error_code) || 502 });
57
+ fail(streamError(model, message)); return;
58
+ }
59
+ if (model.provider === 'soniox') {
60
+ var partial = '';
61
+ (message.tokens || []).forEach(function (token) {
62
+ if (/^<.*>$/.test(token.text)) return;
63
+ if (token.is_final) finalText += token.text; else partial += token.text;
64
+ });
65
+ emit(partial);
66
+ if (message.finished) done();
67
+ } else if (model.provider === 'elevenlabs') {
68
+ if (message.message_type === 'session_started') events.ready();
69
+ if (message.message_type === 'partial_transcript') emit(message.text || '');
70
+ if (message.message_type === 'committed_transcript') {
71
+ finalText += (message.text || '') + ' '; emit('');
72
+ if (finishing) done();
73
+ }
74
+ } else if (message.type === 'Results') {
75
+ var text = message.channel.alternatives[0].transcript || '';
76
+ if (message.is_final) { finalText += text + (text ? ' ' : ''); emit(''); }
77
+ else emit(text);
78
+ } else if (message.type === 'Metadata' && finishing) done();
79
+ } catch (error) { fail(providerError(502)); }
80
+ });
81
+ socket.on('error', function () { diagnostic('upstream-transport-error'); fail(providerError(502)); });
82
+ socket.on('unexpected-response', function (request, response) { diagnostic('upstream-handshake-rejected', { status: response.statusCode }); response.resume(); fail(providerError(response.statusCode)); });
83
+ socket.on('close', function (code) {
84
+ diagnostic('upstream-websocket-close', { status: code });
85
+ if (!ended) {
86
+ if (finishing && model.provider === 'deepgram') done();
87
+ else fail(new Error('The live connection ended unexpectedly. Your draft has been preserved.'));
88
+ }
89
+ });
90
+ return {
91
+ audio: function (bytes) {
92
+ if (ended || finishing || socket.readyState !== 1) return;
93
+ if (socket.bufferedAmount > 1024 * 1024) { fail(new Error('The connection is too slow for live transcription.')); return; }
94
+ if (model.provider === 'elevenlabs') socket.send(JSON.stringify({ message_type: 'input_audio_chunk', audio_base_64: bytes.toString('base64'), sample_rate: 16000 }));
95
+ else socket.send(bytes);
96
+ },
97
+ finish: function () {
98
+ if (ended || finishing) return;
99
+ finishing = true;
100
+ finishTimer = setTimeout(function () { fail(new Error('Transcription timed out. Your draft has been preserved.')); }, 20000);
101
+ if (model.provider === 'soniox') socket.send('');
102
+ else if (model.provider === 'deepgram') socket.send(JSON.stringify({ type: 'CloseStream' }));
103
+ else socket.send(JSON.stringify({ message_type: 'input_audio_chunk', audio_base_64: '', commit: true, sample_rate: 16000 }));
104
+ },
105
+ abort: function () { ended = true; clearTimeout(finishTimer); socket.terminate(); }
106
+ };
107
+ }
108
+ module.exports = { openSpeechStream: openSpeechStream };
@@ -0,0 +1,63 @@
1
+ // Recorded audio adapters. No provider response body (which may contain private data) is logged.
2
+ function providerError(status) {
3
+ var error = new Error(status === 401 || status === 403 ? 'The provider rejected your API key. Check its permissions in Voice input settings.' :
4
+ status === 429 ? 'The provider limit was reached. Check your quota or try again later.' : 'The transcription provider could not complete the request. Please try again.');
5
+ error.status = 502;
6
+ return error;
7
+ }
8
+ async function transcribe(model, key, audio, language, signal, fetcher) {
9
+ fetcher = fetcher || fetch;
10
+ var languageCode = (language || 'en-US').split('-')[0];
11
+ async function request(url, options) {
12
+ var response = await fetcher(url, Object.assign({}, options, { signal: signal }));
13
+ if (!response.ok) throw providerError(response.status);
14
+ return response;
15
+ }
16
+ function form() { var data = new FormData(); data.append('file', new Blob([audio], { type: 'audio/wav' }), 'voice.wav'); return data; }
17
+ if (model.provider === 'openai' || model.provider === 'groq') {
18
+ var data = form(); data.append('model', model.model); data.append(model.model === 'gpt-transcribe' ? 'languages[]' : 'language', languageCode);
19
+ var endpoint = model.provider === 'openai' ? 'https://api.openai.com/v1/audio/transcriptions' : 'https://api.groq.com/openai/v1/audio/transcriptions';
20
+ var response = await request(endpoint, { method: 'POST', headers: { Authorization: 'Bearer ' + key }, body: data });
21
+ return (await response.json()).text;
22
+ }
23
+ if (model.provider === 'elevenlabs') {
24
+ var data = form(); data.append('model_id', model.model); data.append('language_code', languageCode);
25
+ data.append('tag_audio_events', 'false'); data.append('diarize', 'false');
26
+ var response = await request('https://api.elevenlabs.io/v1/speech-to-text', { method: 'POST', headers: { 'xi-api-key': key }, body: data });
27
+ return (await response.json()).text;
28
+ }
29
+ if (model.provider === 'deepgram') {
30
+ var url = 'https://api.deepgram.com/v1/listen?model=nova-3&smart_format=true&language=' + encodeURIComponent(languageCode);
31
+ var response = await request(url, { method: 'POST', headers: { Authorization: 'Token ' + key, 'Content-Type': 'audio/wav' }, body: audio });
32
+ return (await response.json()).results.channels[0].alternatives[0].transcript;
33
+ }
34
+ if (model.provider !== 'soniox') throw new Error('Unsupported speech provider');
35
+ var base = 'https://api.soniox.com/v1';
36
+ var headers = { Authorization: 'Bearer ' + key };
37
+ var fileId; var transcriptionId;
38
+ try {
39
+ var uploaded = await request(base + '/files', { method: 'POST', headers: headers, body: form() });
40
+ fileId = (await uploaded.json()).id;
41
+ var created = await request(base + '/transcriptions', { method: 'POST', headers: Object.assign({}, headers, { 'Content-Type': 'application/json' }),
42
+ body: JSON.stringify({ model: model.model, file_id: fileId, language_hints: [languageCode] }) });
43
+ transcriptionId = (await created.json()).id;
44
+ for (;;) {
45
+ var result = await request(base + '/transcriptions/' + encodeURIComponent(transcriptionId), { headers: headers });
46
+ var state = await result.json();
47
+ if (state.status === 'error') throw providerError(502);
48
+ if (state.status === 'completed') break;
49
+ await require('timers/promises').setTimeout(750, undefined, { signal: signal });
50
+ }
51
+ var transcript = await request(base + '/transcriptions/' + encodeURIComponent(transcriptionId) + '/transcript', { headers: headers });
52
+ return (await transcript.json()).text;
53
+ } finally {
54
+ // Cleanup uses its own bounded signal, including when the user cancels the transcription.
55
+ var paths = [];
56
+ if (transcriptionId) paths.push('/transcriptions/' + encodeURIComponent(transcriptionId));
57
+ if (fileId) paths.push('/files/' + encodeURIComponent(fileId));
58
+ await Promise.allSettled(paths.map(function (path) {
59
+ return fetcher(base + path, { method: 'DELETE', headers: headers, signal: AbortSignal.timeout(5000) });
60
+ }));
61
+ }
62
+ }
63
+ module.exports = { transcribe: transcribe, providerError: providerError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.3.1-beta.4",
3
+ "version": "4.4.0-beta.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"