@aj-archipelago/cortex 1.3.5 → 1.3.7
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/helper-apps/cortex-autogen/agents.py +31 -2
- package/helper-apps/cortex-realtime-voice-server/.env.sample +6 -0
- package/helper-apps/cortex-realtime-voice-server/README.md +22 -0
- package/helper-apps/cortex-realtime-voice-server/bun.lockb +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/bun.lockb +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/index.html +12 -0
- package/helper-apps/cortex-realtime-voice-server/client/package.json +65 -0
- package/helper-apps/cortex-realtime-voice-server/client/postcss.config.js +6 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/favicon.ico +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/index.html +43 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/logo192.png +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/logo512.png +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/manifest.json +25 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/robots.txt +3 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/sounds/connect.mp3 +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/sounds/disconnect.mp3 +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/App.test.tsx +9 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/App.tsx +126 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/SettingsModal.tsx +207 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/Chat.tsx +553 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubble.tsx +22 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleLeft.tsx +22 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleRight.tsx +21 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessage.tsx +27 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessageInput.tsx +74 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatTile.tsx +211 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/SoundEffects.ts +56 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavPacker.ts +112 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavRecorder.ts +571 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavStreamPlayer.ts +290 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/AudioAnalysis.ts +186 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/constants.ts +59 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/AudioProcessor.ts +214 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/StreamProcessor.ts +183 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/AudioVisualizer.tsx +151 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/CopyButton.tsx +32 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ImageOverlay.tsx +166 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/MicrophoneVisualizer.tsx +95 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ScreenshotCapture.tsx +116 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/hooks/useWindowResize.ts +27 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/utils/audio.ts +33 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/index.css +20 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/index.tsx +19 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/logo.svg +1 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/react-app-env.d.ts +1 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/reportWebVitals.ts +15 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/setupTests.ts +5 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/utils/logger.ts +45 -0
- package/helper-apps/cortex-realtime-voice-server/client/tailwind.config.js +14 -0
- package/helper-apps/cortex-realtime-voice-server/client/tsconfig.json +30 -0
- package/helper-apps/cortex-realtime-voice-server/client/vite.config.ts +22 -0
- package/helper-apps/cortex-realtime-voice-server/index.ts +19 -0
- package/helper-apps/cortex-realtime-voice-server/package.json +28 -0
- package/helper-apps/cortex-realtime-voice-server/src/ApiServer.ts +35 -0
- package/helper-apps/cortex-realtime-voice-server/src/SocketServer.ts +737 -0
- package/helper-apps/cortex-realtime-voice-server/src/Tools.ts +520 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/expert.ts +29 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/image.ts +29 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +91 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/reason.ts +29 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/search.ts +30 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/style.ts +31 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/utils.ts +95 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/vision.ts +34 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +499 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +279 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/socket.ts +27 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/transcription.ts +75 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/utils.ts +33 -0
- package/helper-apps/cortex-realtime-voice-server/src/utils/logger.ts +45 -0
- package/helper-apps/cortex-realtime-voice-server/src/utils/prompt.ts +81 -0
- package/helper-apps/cortex-realtime-voice-server/tsconfig.json +28 -0
- package/package.json +1 -1
- package/pathways/basePathway.js +3 -1
- package/pathways/system/entity/memory/sys_memory_manager.js +3 -0
- package/pathways/system/entity/memory/sys_memory_update.js +44 -45
- package/pathways/system/entity/memory/sys_read_memory.js +86 -6
- package/pathways/system/entity/memory/sys_search_memory.js +66 -0
- package/pathways/system/entity/shared/sys_entity_constants.js +2 -2
- package/pathways/system/entity/sys_entity_continue.js +2 -1
- package/pathways/system/entity/sys_entity_start.js +10 -0
- package/pathways/system/entity/sys_generator_expert.js +0 -2
- package/pathways/system/entity/sys_generator_memory.js +31 -0
- package/pathways/system/entity/sys_generator_voice_sample.js +36 -0
- package/pathways/system/entity/sys_router_tool.js +13 -10
- package/pathways/system/sys_parse_numbered_object_list.js +1 -1
- package/server/pathwayResolver.js +41 -31
- package/server/plugins/azureVideoTranslatePlugin.js +28 -16
- package/server/plugins/claude3VertexPlugin.js +0 -9
- package/server/plugins/gemini15ChatPlugin.js +18 -5
- package/server/plugins/modelPlugin.js +27 -6
- package/server/plugins/openAiChatPlugin.js +10 -8
- package/server/plugins/openAiVisionPlugin.js +56 -0
- package/tests/memoryfunction.test.js +73 -1
package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/AudioProcessor.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const AudioProcessorWorklet = `
|
|
2
|
+
class AudioProcessor extends AudioWorkletProcessor {
|
|
3
|
+
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
this.port.onmessage = this.receive.bind(this);
|
|
7
|
+
this.initialize();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
initialize() {
|
|
11
|
+
this.foundAudio = false;
|
|
12
|
+
this.recording = false;
|
|
13
|
+
this.chunks = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Concatenates sampled chunks into channels
|
|
18
|
+
* Format is chunk[Left[], Right[]]
|
|
19
|
+
*/
|
|
20
|
+
readChannelData(chunks, channel = -1, maxChannels = 9) {
|
|
21
|
+
let channelLimit;
|
|
22
|
+
if (channel !== -1) {
|
|
23
|
+
if (chunks[0] && chunks[0].length - 1 < channel) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
\`Channel \${channel} out of range: max \${chunks[0].length}\`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
channelLimit = channel + 1;
|
|
29
|
+
} else {
|
|
30
|
+
channel = 0;
|
|
31
|
+
channelLimit = Math.min(chunks[0] ? chunks[0].length : 1, maxChannels);
|
|
32
|
+
}
|
|
33
|
+
const channels = [];
|
|
34
|
+
for (let n = channel; n < channelLimit; n++) {
|
|
35
|
+
const length = chunks.reduce((sum, chunk) => {
|
|
36
|
+
return sum + chunk[n].length;
|
|
37
|
+
}, 0);
|
|
38
|
+
const buffers = chunks.map((chunk) => chunk[n]);
|
|
39
|
+
const result = new Float32Array(length);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
for (let i = 0; i < buffers.length; i++) {
|
|
42
|
+
result.set(buffers[i], offset);
|
|
43
|
+
offset += buffers[i].length;
|
|
44
|
+
}
|
|
45
|
+
channels[n] = result;
|
|
46
|
+
}
|
|
47
|
+
return channels;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Combines parallel audio data into correct format,
|
|
52
|
+
* channels[Left[], Right[]] to float32Array[LRLRLRLR...]
|
|
53
|
+
*/
|
|
54
|
+
formatAudioData(channels) {
|
|
55
|
+
if (channels.length === 1) {
|
|
56
|
+
// Simple case is only one channel
|
|
57
|
+
const float32Array = channels[0].slice();
|
|
58
|
+
const meanValues = channels[0].slice();
|
|
59
|
+
return { float32Array, meanValues };
|
|
60
|
+
} else {
|
|
61
|
+
const float32Array = new Float32Array(
|
|
62
|
+
channels[0].length * channels.length
|
|
63
|
+
);
|
|
64
|
+
const meanValues = new Float32Array(channels[0].length);
|
|
65
|
+
for (let i = 0; i < channels[0].length; i++) {
|
|
66
|
+
const offset = i * channels.length;
|
|
67
|
+
let meanValue = 0;
|
|
68
|
+
for (let n = 0; n < channels.length; n++) {
|
|
69
|
+
float32Array[offset + n] = channels[n][i];
|
|
70
|
+
meanValue += channels[n][i];
|
|
71
|
+
}
|
|
72
|
+
meanValues[i] = meanValue / channels.length;
|
|
73
|
+
}
|
|
74
|
+
return { float32Array, meanValues };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Converts 32-bit float data to 16-bit integers
|
|
80
|
+
*/
|
|
81
|
+
floatTo16BitPCM(float32Array) {
|
|
82
|
+
const buffer = new ArrayBuffer(float32Array.length * 2);
|
|
83
|
+
const view = new DataView(buffer);
|
|
84
|
+
let offset = 0;
|
|
85
|
+
for (let i = 0; i < float32Array.length; i++, offset += 2) {
|
|
86
|
+
let s = Math.max(-1, Math.min(1, float32Array[i]));
|
|
87
|
+
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
|
88
|
+
}
|
|
89
|
+
return buffer;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Retrieves the most recent amplitude values from the audio stream
|
|
94
|
+
* @param {number} channel
|
|
95
|
+
*/
|
|
96
|
+
getValues(channel = -1) {
|
|
97
|
+
const channels = this.readChannelData(this.chunks, channel);
|
|
98
|
+
const { meanValues } = this.formatAudioData(channels);
|
|
99
|
+
return { meanValues, channels };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Exports chunks as an audio/wav file
|
|
104
|
+
*/
|
|
105
|
+
export() {
|
|
106
|
+
const channels = this.readChannelData(this.chunks);
|
|
107
|
+
const { float32Array, meanValues } = this.formatAudioData(channels);
|
|
108
|
+
const audioData = this.floatTo16BitPCM(float32Array);
|
|
109
|
+
return {
|
|
110
|
+
meanValues: meanValues,
|
|
111
|
+
audio: {
|
|
112
|
+
bitsPerSample: 16,
|
|
113
|
+
channels: channels,
|
|
114
|
+
data: audioData,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
receive(e) {
|
|
120
|
+
const { event, id } = e.data;
|
|
121
|
+
let receiptData = {};
|
|
122
|
+
switch (event) {
|
|
123
|
+
case 'start':
|
|
124
|
+
this.recording = true;
|
|
125
|
+
break;
|
|
126
|
+
case 'stop':
|
|
127
|
+
this.recording = false;
|
|
128
|
+
break;
|
|
129
|
+
case 'clear':
|
|
130
|
+
this.initialize();
|
|
131
|
+
break;
|
|
132
|
+
case 'export':
|
|
133
|
+
receiptData = this.export();
|
|
134
|
+
break;
|
|
135
|
+
case 'read':
|
|
136
|
+
receiptData = this.getValues();
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
// Always send back receipt
|
|
142
|
+
this.port.postMessage({ event: 'receipt', id, data: receiptData });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
sendChunk(chunk) {
|
|
146
|
+
const channels = this.readChannelData([chunk]);
|
|
147
|
+
const { float32Array, meanValues } = this.formatAudioData(channels);
|
|
148
|
+
const rawAudioData = this.floatTo16BitPCM(float32Array);
|
|
149
|
+
const monoAudioData = this.floatTo16BitPCM(meanValues);
|
|
150
|
+
this.port.postMessage({
|
|
151
|
+
event: 'chunk',
|
|
152
|
+
data: {
|
|
153
|
+
mono: monoAudioData,
|
|
154
|
+
raw: rawAudioData,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
process(inputList, outputList, parameters) {
|
|
160
|
+
// Copy input to output (e.g. speakers)
|
|
161
|
+
// Note that this creates choppy sounds with Mac products
|
|
162
|
+
const sourceLimit = Math.min(inputList.length, outputList.length);
|
|
163
|
+
for (let inputNum = 0; inputNum < sourceLimit; inputNum++) {
|
|
164
|
+
const input = inputList[inputNum];
|
|
165
|
+
const output = outputList[inputNum];
|
|
166
|
+
const channelCount = Math.min(input.length, output.length);
|
|
167
|
+
for (let channelNum = 0; channelNum < channelCount; channelNum++) {
|
|
168
|
+
input[channelNum].forEach((sample, i) => {
|
|
169
|
+
output[channelNum][i] = sample;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const inputs = inputList[0];
|
|
174
|
+
// There's latency at the beginning of a stream before recording starts
|
|
175
|
+
// Make sure we actually receive audio data before we start storing chunks
|
|
176
|
+
let sliceIndex = 0;
|
|
177
|
+
if (!this.foundAudio) {
|
|
178
|
+
for (const channel of inputs) {
|
|
179
|
+
sliceIndex = 0; // reset for each channel
|
|
180
|
+
if (this.foundAudio) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (channel) {
|
|
184
|
+
for (const value of channel) {
|
|
185
|
+
if (value !== 0) {
|
|
186
|
+
// find only one non-zero entry in any channel
|
|
187
|
+
this.foundAudio = true;
|
|
188
|
+
break;
|
|
189
|
+
} else {
|
|
190
|
+
sliceIndex++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (inputs && inputs[0] && this.foundAudio && this.recording) {
|
|
197
|
+
// We need to copy the TypedArray, because the \`process\`
|
|
198
|
+
// internals will reuse the same buffer to hold each input
|
|
199
|
+
const chunk = inputs.map((input) => input.slice(sliceIndex));
|
|
200
|
+
this.chunks.push(chunk);
|
|
201
|
+
this.sendChunk(chunk);
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
registerProcessor('audio_processor', AudioProcessor);
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
const script = new Blob([AudioProcessorWorklet], {
|
|
211
|
+
type: 'application/javascript',
|
|
212
|
+
});
|
|
213
|
+
const src = URL.createObjectURL(script);
|
|
214
|
+
export const AudioProcessorSrc: string = src;
|
package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/StreamProcessor.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export const StreamProcessorWorklet = `
|
|
2
|
+
class StreamProcessor extends AudioWorkletProcessor {
|
|
3
|
+
constructor() {
|
|
4
|
+
super();
|
|
5
|
+
this.hasStarted = false;
|
|
6
|
+
this.hasInterrupted = false;
|
|
7
|
+
this.outputBuffers = [];
|
|
8
|
+
this.bufferLength = 128;
|
|
9
|
+
this.minBufferSize = 3;
|
|
10
|
+
this.write = { buffer: new Float32Array(this.bufferLength), trackId: null };
|
|
11
|
+
this.writeOffset = 0;
|
|
12
|
+
this.trackSampleOffsets = {};
|
|
13
|
+
this.lastErrorTime = 0;
|
|
14
|
+
this.errorCount = 0;
|
|
15
|
+
this.noBufferCount = 0;
|
|
16
|
+
this.maxNoBufferFrames = 100;
|
|
17
|
+
this.lastUnderrunLog = 0;
|
|
18
|
+
|
|
19
|
+
this.port.onmessage = (event) => {
|
|
20
|
+
try {
|
|
21
|
+
if (event.data) {
|
|
22
|
+
const payload = event.data;
|
|
23
|
+
if (payload.event === 'write') {
|
|
24
|
+
const int16Array = payload.buffer;
|
|
25
|
+
const float32Array = new Float32Array(int16Array.length);
|
|
26
|
+
for (let i = 0; i < int16Array.length; i++) {
|
|
27
|
+
float32Array[i] = int16Array[i] / 0x8000;
|
|
28
|
+
}
|
|
29
|
+
this.writeData(float32Array, payload.trackId);
|
|
30
|
+
} else if (payload.event === 'config') {
|
|
31
|
+
this.minBufferSize = payload.minBufferSize;
|
|
32
|
+
} else if (
|
|
33
|
+
payload.event === 'offset' ||
|
|
34
|
+
payload.event === 'interrupt'
|
|
35
|
+
) {
|
|
36
|
+
const requestId = payload.requestId;
|
|
37
|
+
const trackId = this.write.trackId;
|
|
38
|
+
const offset = this.trackSampleOffsets[trackId] || 0;
|
|
39
|
+
this.port.postMessage({
|
|
40
|
+
event: 'offset',
|
|
41
|
+
requestId,
|
|
42
|
+
trackId,
|
|
43
|
+
offset,
|
|
44
|
+
});
|
|
45
|
+
if (payload.event === 'interrupt') {
|
|
46
|
+
this.hasInterrupted = true;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
throw new Error(\`Unhandled event "\${payload.event}"\`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.handleError(error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handleError(error) {
|
|
59
|
+
const now = currentTime;
|
|
60
|
+
if (now - this.lastErrorTime > 5) {
|
|
61
|
+
// Reset error count if more than 5 seconds have passed
|
|
62
|
+
this.errorCount = 0;
|
|
63
|
+
}
|
|
64
|
+
this.lastErrorTime = now;
|
|
65
|
+
this.errorCount++;
|
|
66
|
+
|
|
67
|
+
if (this.errorCount <= 3) {
|
|
68
|
+
this.port.postMessage({
|
|
69
|
+
event: 'error',
|
|
70
|
+
error: error.message || 'Unknown error in stream processor'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
writeData(float32Array, trackId = null) {
|
|
76
|
+
try {
|
|
77
|
+
let { buffer } = this.write;
|
|
78
|
+
let offset = this.writeOffset;
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < float32Array.length; i++) {
|
|
81
|
+
buffer[offset++] = float32Array[i];
|
|
82
|
+
if (offset >= buffer.length) {
|
|
83
|
+
this.outputBuffers.push(this.write);
|
|
84
|
+
this.write = { buffer: new Float32Array(this.bufferLength), trackId };
|
|
85
|
+
buffer = this.write.buffer;
|
|
86
|
+
offset = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If we have a partial buffer at the end, push it too
|
|
91
|
+
if (offset > 0) {
|
|
92
|
+
const finalBuffer = new Float32Array(this.bufferLength);
|
|
93
|
+
finalBuffer.set(buffer.subarray(0, offset));
|
|
94
|
+
this.outputBuffers.push({ buffer: finalBuffer, trackId });
|
|
95
|
+
this.write = { buffer: new Float32Array(this.bufferLength), trackId };
|
|
96
|
+
this.writeOffset = 0;
|
|
97
|
+
} else {
|
|
98
|
+
this.writeOffset = offset;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.noBufferCount = 0;
|
|
102
|
+
return true;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.handleError(error);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
process(inputs, outputs, parameters) {
|
|
110
|
+
try {
|
|
111
|
+
const output = outputs[0];
|
|
112
|
+
const outputChannelData = output[0];
|
|
113
|
+
const outputBuffers = this.outputBuffers;
|
|
114
|
+
|
|
115
|
+
if (this.hasInterrupted) {
|
|
116
|
+
this.port.postMessage({ event: 'stop' });
|
|
117
|
+
return false;
|
|
118
|
+
} else if (!this.hasStarted && outputBuffers.length < this.minBufferSize) {
|
|
119
|
+
// Wait for more buffers before starting
|
|
120
|
+
outputChannelData.fill(0);
|
|
121
|
+
return true;
|
|
122
|
+
} else if (outputBuffers.length > 0) {
|
|
123
|
+
this.hasStarted = true;
|
|
124
|
+
this.noBufferCount = 0;
|
|
125
|
+
this.lastUnderrunLog = 0;
|
|
126
|
+
|
|
127
|
+
const { buffer, trackId } = outputBuffers.shift();
|
|
128
|
+
outputChannelData.set(buffer);
|
|
129
|
+
|
|
130
|
+
if (trackId) {
|
|
131
|
+
this.trackSampleOffsets[trackId] =
|
|
132
|
+
this.trackSampleOffsets[trackId] || 0;
|
|
133
|
+
this.trackSampleOffsets[trackId] += buffer.length;
|
|
134
|
+
|
|
135
|
+
// If this was the last buffer for this track, notify completion
|
|
136
|
+
if (outputBuffers.length === 0 || outputBuffers[0].trackId !== trackId) {
|
|
137
|
+
this.port.postMessage({
|
|
138
|
+
event: 'track_complete',
|
|
139
|
+
trackId,
|
|
140
|
+
finalOffset: this.trackSampleOffsets[trackId]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
} else if (this.hasStarted) {
|
|
146
|
+
this.noBufferCount++;
|
|
147
|
+
|
|
148
|
+
if (this.noBufferCount >= 5 && currentTime - this.lastUnderrunLog > 1) {
|
|
149
|
+
this.port.postMessage({
|
|
150
|
+
event: 'underrun',
|
|
151
|
+
count: this.noBufferCount,
|
|
152
|
+
bufferSize: this.outputBuffers.length,
|
|
153
|
+
maxBuffers: this.maxNoBufferFrames
|
|
154
|
+
});
|
|
155
|
+
this.lastUnderrunLog = currentTime;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (this.noBufferCount >= this.maxNoBufferFrames) {
|
|
159
|
+
this.port.postMessage({
|
|
160
|
+
event: 'stop',
|
|
161
|
+
reason: 'max_underruns_reached',
|
|
162
|
+
finalCount: this.noBufferCount
|
|
163
|
+
});
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
outputChannelData.fill(0);
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.handleError(error);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
registerProcessor('stream_processor', StreamProcessor);
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
const script = new Blob([StreamProcessorWorklet], {
|
|
180
|
+
type: 'application/javascript',
|
|
181
|
+
});
|
|
182
|
+
const src = URL.createObjectURL(script);
|
|
183
|
+
export const StreamProcessorSrc = src;
|
package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/AudioVisualizer.tsx
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
type AudioVisualizerProps = {
|
|
4
|
+
audioContext: AudioContext | null;
|
|
5
|
+
analyserNode: AnalyserNode | null;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function AudioVisualizer({
|
|
11
|
+
audioContext,
|
|
12
|
+
analyserNode,
|
|
13
|
+
width = 300,
|
|
14
|
+
height = 300
|
|
15
|
+
}: AudioVisualizerProps) {
|
|
16
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
17
|
+
const rotationRef = useRef(0);
|
|
18
|
+
const colorShiftRef = useRef(0);
|
|
19
|
+
const animationFrameRef = useRef<number>();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!audioContext || !analyserNode || !canvasRef.current) return;
|
|
23
|
+
|
|
24
|
+
// Update canvas size when width/height props change
|
|
25
|
+
if (canvasRef.current) {
|
|
26
|
+
canvasRef.current.width = width;
|
|
27
|
+
canvasRef.current.height = height;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const draw = () => {
|
|
31
|
+
const canvas = canvasRef.current;
|
|
32
|
+
if (!canvas || !analyserNode) return;
|
|
33
|
+
|
|
34
|
+
const ctx = canvas.getContext('2d');
|
|
35
|
+
if (!ctx) return;
|
|
36
|
+
|
|
37
|
+
const bufferLength = analyserNode.frequencyBinCount;
|
|
38
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
39
|
+
analyserNode.getByteFrequencyData(dataArray);
|
|
40
|
+
|
|
41
|
+
// Clear with fade effect
|
|
42
|
+
ctx.fillStyle = 'rgba(17, 24, 39, 0.3)';
|
|
43
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
44
|
+
|
|
45
|
+
const centerX = canvas.width / 2;
|
|
46
|
+
const centerY = canvas.height / 2;
|
|
47
|
+
const maxRadius = Math.min(centerX, centerY) - 10;
|
|
48
|
+
|
|
49
|
+
// Draw outer circle
|
|
50
|
+
ctx.beginPath();
|
|
51
|
+
ctx.strokeStyle = `hsl(${210 + Math.sin(colorShiftRef.current) * 20}, 80%, 60%)`;
|
|
52
|
+
ctx.lineWidth = 2;
|
|
53
|
+
ctx.arc(centerX, centerY, maxRadius, 0, Math.PI * 2);
|
|
54
|
+
ctx.stroke();
|
|
55
|
+
|
|
56
|
+
// Define base colors with shifting hues
|
|
57
|
+
const baseHue = 210 + Math.sin(colorShiftRef.current) * 20;
|
|
58
|
+
const waveforms = [
|
|
59
|
+
{
|
|
60
|
+
baseRadius: maxRadius * 0.4,
|
|
61
|
+
color: `hsl(${baseHue}, 90%, 70%)`,
|
|
62
|
+
gradientColors: [`hsla(${baseHue}, 90%, 70%, 0.3)`, `hsla(${baseHue}, 90%, 50%, 0)`],
|
|
63
|
+
rotation: rotationRef.current
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
baseRadius: maxRadius * 0.6,
|
|
67
|
+
color: `hsl(${baseHue + 10}, 85%, 60%)`,
|
|
68
|
+
gradientColors: [`hsla(${baseHue + 10}, 85%, 60%, 0.3)`, `hsla(${baseHue + 10}, 85%, 40%, 0)`],
|
|
69
|
+
rotation: rotationRef.current + (Math.PI * 2 / 3)
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
baseRadius: maxRadius * 0.8,
|
|
73
|
+
color: `hsl(${baseHue + 20}, 80%, 50%)`,
|
|
74
|
+
gradientColors: [`hsla(${baseHue + 20}, 80%, 50%, 0.3)`, `hsla(${baseHue + 20}, 80%, 30%, 0)`],
|
|
75
|
+
rotation: rotationRef.current + (Math.PI * 4 / 3)
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
waveforms.forEach(({ baseRadius, color, gradientColors, rotation }) => {
|
|
80
|
+
const points: [number, number][] = [];
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i <= bufferLength; i++) {
|
|
83
|
+
const amplitude = dataArray[i % bufferLength] / 255.0;
|
|
84
|
+
const angle = (i * 2 * Math.PI) / bufferLength + rotation;
|
|
85
|
+
|
|
86
|
+
const radius = baseRadius + (maxRadius * 0.4 * amplitude);
|
|
87
|
+
const x = centerX + Math.cos(angle) * radius;
|
|
88
|
+
const y = centerY + Math.sin(angle) * radius;
|
|
89
|
+
|
|
90
|
+
points.push([x, y]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create gradient for fill
|
|
94
|
+
const gradient = ctx.createRadialGradient(
|
|
95
|
+
centerX, centerY, baseRadius * 0.8,
|
|
96
|
+
centerX, centerY, baseRadius * 1.2
|
|
97
|
+
);
|
|
98
|
+
gradient.addColorStop(0, gradientColors[0]);
|
|
99
|
+
gradient.addColorStop(1, gradientColors[1]);
|
|
100
|
+
|
|
101
|
+
ctx.beginPath();
|
|
102
|
+
ctx.moveTo(centerX, centerY);
|
|
103
|
+
points.forEach(([x, y]) => {
|
|
104
|
+
ctx.lineTo(x, y);
|
|
105
|
+
});
|
|
106
|
+
ctx.closePath();
|
|
107
|
+
ctx.fillStyle = gradient;
|
|
108
|
+
ctx.fill();
|
|
109
|
+
|
|
110
|
+
ctx.beginPath();
|
|
111
|
+
points.forEach(([x, y], i) => {
|
|
112
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
113
|
+
else ctx.lineTo(x, y);
|
|
114
|
+
});
|
|
115
|
+
ctx.strokeStyle = color;
|
|
116
|
+
ctx.lineWidth = 2;
|
|
117
|
+
ctx.closePath();
|
|
118
|
+
ctx.stroke();
|
|
119
|
+
|
|
120
|
+
ctx.shadowBlur = 15;
|
|
121
|
+
ctx.shadowColor = color;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
rotationRef.current += 0.002;
|
|
125
|
+
colorShiftRef.current += 0.005;
|
|
126
|
+
|
|
127
|
+
animationFrameRef.current = requestAnimationFrame(draw);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
draw();
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
if (animationFrameRef.current) {
|
|
134
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}, [audioContext, analyserNode, width, height]);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="w-full h-full flex items-center justify-center pointer-events-none">
|
|
141
|
+
<div className="aspect-square w-full max-h-full">
|
|
142
|
+
<canvas
|
|
143
|
+
ref={canvasRef}
|
|
144
|
+
width={width}
|
|
145
|
+
height={height}
|
|
146
|
+
className="bg-gray-900 rounded-lg w-full h-full object-contain pointer-events-none"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
3
|
+
import CheckIcon from '@mui/icons-material/Check';
|
|
4
|
+
|
|
5
|
+
interface CopyButtonProps {
|
|
6
|
+
text: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const CopyButton: React.FC<CopyButtonProps> = ({ text, className = '' }) => {
|
|
11
|
+
const [copied, setCopied] = useState(false);
|
|
12
|
+
|
|
13
|
+
const handleCopy = async () => {
|
|
14
|
+
await navigator.clipboard.writeText(text);
|
|
15
|
+
setCopied(true);
|
|
16
|
+
setTimeout(() => setCopied(false), 2000);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
onClick={handleCopy}
|
|
22
|
+
className={`p-1 text-gray-400 hover:text-cyan-400 transition-colors duration-200 ${className}`}
|
|
23
|
+
title="Copy to clipboard"
|
|
24
|
+
>
|
|
25
|
+
{copied ? (
|
|
26
|
+
<CheckIcon sx={{ fontSize: 16 }} />
|
|
27
|
+
) : (
|
|
28
|
+
<ContentCopyIcon sx={{ fontSize: 16 }} />
|
|
29
|
+
)}
|
|
30
|
+
</button>
|
|
31
|
+
);
|
|
32
|
+
};
|