heyhank 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/README.md +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- package/server/ws-bridge.ts +1240 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// ─── Audio Bridge ─────────────────────────────────────────────────────────────
|
|
2
|
+
// Bridges audio between FreeSWITCH (8kHz PCM via mod_audio_fork) and
|
|
3
|
+
// Gemini Live BidiGenerateContent API (16kHz PCM).
|
|
4
|
+
// This is the core of the telephony system — no STT/TTS needed,
|
|
5
|
+
// Gemini handles everything natively.
|
|
6
|
+
|
|
7
|
+
import type { CallState, TranscriptEntry } from "./call-types.js";
|
|
8
|
+
|
|
9
|
+
// Gemini Live WebSocket endpoint
|
|
10
|
+
const GEMINI_WS_BASE = "wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
|
|
11
|
+
const GEMINI_MODEL = "models/gemini-2.0-flash-live-001";
|
|
12
|
+
|
|
13
|
+
export interface AudioBridgeConfig {
|
|
14
|
+
geminiApiKey: string;
|
|
15
|
+
voice: string;
|
|
16
|
+
systemPrompt: string;
|
|
17
|
+
tools: unknown[];
|
|
18
|
+
onTranscript: (entry: TranscriptEntry) => void;
|
|
19
|
+
onStatusChange: (status: CallState["status"]) => void;
|
|
20
|
+
onToolCall: (calls: Array<{ id: string; name: string; args: Record<string, unknown> }>) => Promise<Array<{ id: string; name: string; response: unknown }>>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* AudioBridge manages a single call's audio pipeline:
|
|
25
|
+
* FreeSWITCH PCM (8kHz) → upsample → Gemini Live (16kHz) → downsample → FreeSWITCH
|
|
26
|
+
*/
|
|
27
|
+
export class AudioBridge {
|
|
28
|
+
private geminiWs: WebSocket | null = null;
|
|
29
|
+
private config: AudioBridgeConfig;
|
|
30
|
+
private setupDone = false;
|
|
31
|
+
private callId: string;
|
|
32
|
+
private textBuffer = "";
|
|
33
|
+
|
|
34
|
+
constructor(callId: string, config: AudioBridgeConfig) {
|
|
35
|
+
this.callId = callId;
|
|
36
|
+
this.config = config;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Connect to Gemini Live API */
|
|
40
|
+
async connect(): Promise<void> {
|
|
41
|
+
const url = `${GEMINI_WS_BASE}?key=${this.config.geminiApiKey}`;
|
|
42
|
+
this.geminiWs = new WebSocket(url);
|
|
43
|
+
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const timeout = setTimeout(() => {
|
|
46
|
+
reject(new Error("Gemini connection timeout"));
|
|
47
|
+
}, 15000);
|
|
48
|
+
|
|
49
|
+
this.geminiWs!.onopen = () => {
|
|
50
|
+
// Send setup with telephony-optimized config
|
|
51
|
+
this.geminiWs!.send(JSON.stringify({
|
|
52
|
+
setup: {
|
|
53
|
+
model: GEMINI_MODEL,
|
|
54
|
+
generationConfig: {
|
|
55
|
+
responseModalities: ["AUDIO"],
|
|
56
|
+
speechConfig: {
|
|
57
|
+
voiceConfig: {
|
|
58
|
+
prebuiltVoiceConfig: { voiceName: this.config.voice },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
systemInstruction: {
|
|
63
|
+
parts: [{ text: this.config.systemPrompt }],
|
|
64
|
+
},
|
|
65
|
+
tools: this.config.tools,
|
|
66
|
+
outputAudioTranscription: {},
|
|
67
|
+
inputAudioTranscription: {},
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.geminiWs!.onmessage = async (event: MessageEvent) => {
|
|
73
|
+
try {
|
|
74
|
+
let text: string;
|
|
75
|
+
if (event.data instanceof Blob) {
|
|
76
|
+
text = await event.data.text();
|
|
77
|
+
} else {
|
|
78
|
+
text = event.data as string;
|
|
79
|
+
}
|
|
80
|
+
const msg = JSON.parse(text);
|
|
81
|
+
this.handleGeminiMessage(msg, resolve, clearTimeout.bind(null, timeout));
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore parse errors
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
this.geminiWs!.onerror = () => {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
reject(new Error("Gemini WebSocket error"));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
this.geminiWs!.onclose = () => {
|
|
93
|
+
this.setupDone = false;
|
|
94
|
+
this.flushTextBuffer();
|
|
95
|
+
this.config.onStatusChange("ended");
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private flushTextBuffer(): void {
|
|
101
|
+
if (this.textBuffer.trim()) {
|
|
102
|
+
this.config.onTranscript({
|
|
103
|
+
speaker: "ai",
|
|
104
|
+
text: this.textBuffer.trim(),
|
|
105
|
+
isFinal: true,
|
|
106
|
+
ts: Date.now(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
this.textBuffer = "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private handleGeminiMessage(
|
|
113
|
+
msg: Record<string, unknown>,
|
|
114
|
+
onSetupResolve?: () => void,
|
|
115
|
+
clearSetupTimeout?: () => void,
|
|
116
|
+
): void {
|
|
117
|
+
// Setup complete
|
|
118
|
+
if ("setupComplete" in msg) {
|
|
119
|
+
this.setupDone = true;
|
|
120
|
+
clearSetupTimeout?.();
|
|
121
|
+
onSetupResolve?.();
|
|
122
|
+
this.config.onStatusChange("active");
|
|
123
|
+
this.config.onTranscript({
|
|
124
|
+
speaker: "system",
|
|
125
|
+
text: "AI connected to call",
|
|
126
|
+
isFinal: true,
|
|
127
|
+
ts: Date.now(),
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Tool calls
|
|
133
|
+
if ("toolCall" in msg) {
|
|
134
|
+
const tc = msg.toolCall as {
|
|
135
|
+
functionCalls?: Array<{ id: string; name: string; args?: Record<string, unknown> }>;
|
|
136
|
+
};
|
|
137
|
+
if (tc.functionCalls?.length) {
|
|
138
|
+
const calls = tc.functionCalls.map((fc) => ({
|
|
139
|
+
id: fc.id,
|
|
140
|
+
name: fc.name,
|
|
141
|
+
args: fc.args || {},
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
this.config.onTranscript({
|
|
145
|
+
speaker: "system",
|
|
146
|
+
text: `Tool: ${calls.map((c) => c.name).join(", ")}`,
|
|
147
|
+
isFinal: true,
|
|
148
|
+
ts: Date.now(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Execute tools and send response back
|
|
152
|
+
this.config.onToolCall(calls).then((responses) => {
|
|
153
|
+
this.sendToolResponse(responses);
|
|
154
|
+
}).catch(() => {});
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Server content
|
|
160
|
+
if ("serverContent" in msg) {
|
|
161
|
+
const content = msg.serverContent as Record<string, unknown>;
|
|
162
|
+
|
|
163
|
+
// Output transcription (AI speech as text)
|
|
164
|
+
const outputT = content.outputTranscription as { text?: string } | undefined;
|
|
165
|
+
if (outputT?.text) {
|
|
166
|
+
this.textBuffer += outputT.text;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Input transcription (callee speech as text)
|
|
170
|
+
const inputT = content.inputTranscription as { text?: string } | undefined;
|
|
171
|
+
if (inputT?.text?.trim()) {
|
|
172
|
+
this.config.onTranscript({
|
|
173
|
+
speaker: "callee",
|
|
174
|
+
text: inputT.text.trim(),
|
|
175
|
+
isFinal: true,
|
|
176
|
+
ts: Date.now(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Turn complete
|
|
181
|
+
if (content.turnComplete) {
|
|
182
|
+
this.flushTextBuffer();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Interrupted
|
|
187
|
+
if (content.interrupted) {
|
|
188
|
+
this.flushTextBuffer();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Model turn parts — extract audio to send back to FreeSWITCH
|
|
193
|
+
const modelTurn = content.modelTurn as {
|
|
194
|
+
parts?: Array<{ inlineData?: { data: string; mimeType: string }; text?: string }>;
|
|
195
|
+
} | undefined;
|
|
196
|
+
|
|
197
|
+
if (modelTurn?.parts) {
|
|
198
|
+
for (const part of modelTurn.parts) {
|
|
199
|
+
if (part.inlineData?.data) {
|
|
200
|
+
// This is the AI's audio response — needs to go back to FreeSWITCH
|
|
201
|
+
// The audio is 24kHz PCM from Gemini, needs downsampling to 8kHz for telephony
|
|
202
|
+
this.onGeminiAudio(part.inlineData.data);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Callback for when Gemini produces audio — override to send to FreeSWITCH */
|
|
210
|
+
public onGeminiAudio: (base64Pcm: string) => void = () => {};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Feed audio from FreeSWITCH into Gemini.
|
|
214
|
+
* Input: raw PCM 8kHz 16-bit mono from mod_audio_fork
|
|
215
|
+
* Gemini expects: PCM 16kHz
|
|
216
|
+
*/
|
|
217
|
+
sendCallerAudio(pcm8kHz: Buffer | Uint8Array): void {
|
|
218
|
+
if (!this.geminiWs || this.geminiWs.readyState !== WebSocket.OPEN || !this.setupDone) return;
|
|
219
|
+
|
|
220
|
+
// Upsample 8kHz → 16kHz (simple linear interpolation)
|
|
221
|
+
const upsampled = upsample8to16(pcm8kHz);
|
|
222
|
+
const base64 = bufferToBase64(upsampled);
|
|
223
|
+
|
|
224
|
+
this.geminiWs.send(JSON.stringify({
|
|
225
|
+
realtimeInput: {
|
|
226
|
+
audio: {
|
|
227
|
+
mimeType: "audio/pcm;rate=16000",
|
|
228
|
+
data: base64,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Send tool call results back to Gemini */
|
|
235
|
+
private sendToolResponse(responses: Array<{ id: string; name: string; response: unknown }>): void {
|
|
236
|
+
if (!this.geminiWs || this.geminiWs.readyState !== WebSocket.OPEN || !this.setupDone) return;
|
|
237
|
+
|
|
238
|
+
this.geminiWs.send(JSON.stringify({
|
|
239
|
+
toolResponse: {
|
|
240
|
+
functionResponses: responses.map((r) => ({
|
|
241
|
+
id: r.id,
|
|
242
|
+
name: r.name,
|
|
243
|
+
response: r.response,
|
|
244
|
+
})),
|
|
245
|
+
},
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Disconnect from Gemini */
|
|
250
|
+
disconnect(): void {
|
|
251
|
+
this.flushTextBuffer();
|
|
252
|
+
if (this.geminiWs) {
|
|
253
|
+
this.geminiWs.onclose = null;
|
|
254
|
+
this.geminiWs.close();
|
|
255
|
+
this.geminiWs = null;
|
|
256
|
+
}
|
|
257
|
+
this.setupDone = false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
get isReady(): boolean {
|
|
261
|
+
return this.setupDone && this.geminiWs?.readyState === WebSocket.OPEN;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── Audio Utilities ──────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Upsample 8kHz PCM (16-bit LE) to 16kHz using linear interpolation.
|
|
269
|
+
* Every sample gets doubled with an interpolated sample in between.
|
|
270
|
+
*/
|
|
271
|
+
function upsample8to16(input: Buffer | Uint8Array): Uint8Array {
|
|
272
|
+
const inputView = new DataView(input.buffer, input.byteOffset, input.byteLength);
|
|
273
|
+
const sampleCount = input.byteLength / 2; // 16-bit samples
|
|
274
|
+
const output = new Uint8Array(sampleCount * 4); // 2x samples, 2 bytes each
|
|
275
|
+
const outputView = new DataView(output.buffer);
|
|
276
|
+
|
|
277
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
278
|
+
const sample = inputView.getInt16(i * 2, true); // little-endian
|
|
279
|
+
const nextSample = i + 1 < sampleCount
|
|
280
|
+
? inputView.getInt16((i + 1) * 2, true)
|
|
281
|
+
: sample;
|
|
282
|
+
const interpolated = Math.round((sample + nextSample) / 2);
|
|
283
|
+
|
|
284
|
+
outputView.setInt16(i * 4, sample, true);
|
|
285
|
+
outputView.setInt16(i * 4 + 2, interpolated, true);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return output;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Downsample 24kHz/16kHz PCM to 8kHz for FreeSWITCH.
|
|
293
|
+
* Takes every Nth sample (simple decimation).
|
|
294
|
+
*/
|
|
295
|
+
export function downsampleTo8k(input: Uint8Array, inputRate: number): Uint8Array {
|
|
296
|
+
const ratio = inputRate / 8000;
|
|
297
|
+
const inputView = new DataView(input.buffer, input.byteOffset, input.byteLength);
|
|
298
|
+
const inputSamples = input.byteLength / 2;
|
|
299
|
+
const outputSamples = Math.floor(inputSamples / ratio);
|
|
300
|
+
const output = new Uint8Array(outputSamples * 2);
|
|
301
|
+
const outputView = new DataView(output.buffer);
|
|
302
|
+
|
|
303
|
+
for (let i = 0; i < outputSamples; i++) {
|
|
304
|
+
const srcIdx = Math.floor(i * ratio);
|
|
305
|
+
if (srcIdx * 2 + 1 < input.byteLength) {
|
|
306
|
+
const sample = inputView.getInt16(srcIdx * 2, true);
|
|
307
|
+
outputView.setInt16(i * 2, sample, true);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return output;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Convert Uint8Array/Buffer to base64 string */
|
|
315
|
+
function bufferToBase64(buf: Uint8Array): string {
|
|
316
|
+
let binary = "";
|
|
317
|
+
for (let i = 0; i < buf.byteLength; i++) {
|
|
318
|
+
binary += String.fromCharCode(buf[i]);
|
|
319
|
+
}
|
|
320
|
+
return btoa(binary);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Convert base64 string to Uint8Array */
|
|
324
|
+
export function base64ToBuffer(base64: string): Uint8Array {
|
|
325
|
+
const binary = atob(base64);
|
|
326
|
+
const bytes = new Uint8Array(binary.length);
|
|
327
|
+
for (let i = 0; i < binary.length; i++) {
|
|
328
|
+
bytes[i] = binary.charCodeAt(i);
|
|
329
|
+
}
|
|
330
|
+
return bytes;
|
|
331
|
+
}
|