AIvoices/server-deno/utils.ts
2025-10-22 11:53:57 +01:00

185 lines
No EOL
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as jose from "https://deno.land/x/jose@v5.9.6/index.ts";
import { getUserByEmail } from "./supabase.ts";
import { SupabaseClient } from "@supabase/supabase-js";
import crypto from "node:crypto";
import { Buffer } from "node:buffer";
import { Encoder } from "@evan/opus";
export const defaultVolume = 50;
// Define your audio parameters
export const SAMPLE_RATE = 24000; // For example, 24000 Hz
const CHANNELS = 1; // Mono (set to 2 if you have stereo)
const FRAME_DURATION = 120; // Frame length in ms
const BYTES_PER_SAMPLE = 2; // 16-bit PCM: 2 bytes per sample
const FRAME_SIZE = (SAMPLE_RATE * FRAME_DURATION / 1000) * CHANNELS *
BYTES_PER_SAMPLE; // 960 bytes for 24000 Hz mono 16-bit
const encoder = new Encoder({
channels: CHANNELS,
sample_rate: SAMPLE_RATE,
application: "voip",
});
encoder.expert_frame_duration = FRAME_DURATION;
encoder.bitrate = 12000;
export const openaiApiKey = Deno.env.get("OPENAI_API_KEY");
export const geminiApiKey = Deno.env.get("GEMINI_API_KEY");
export const elevenLabsApiKey = Deno.env.get("ELEVENLABS_API_KEY");
export const humeApiKey = Deno.env.get('HUME_API_KEY');
export { encoder, FRAME_SIZE };
export const isDev = Deno.env.get("DEV_MODE") === "True";
export const authenticateUser = async (
supabaseClient: SupabaseClient,
authToken: string,
): Promise<IUser> => {
try {
const jwtSecret = Deno.env.get("JWT_SECRET_KEY");
if (!jwtSecret) throw new Error("JWT_SECRET_KEY not configured");
const secretBytes = new TextEncoder().encode(jwtSecret);
const payload = await jose.jwtVerify(authToken, secretBytes);
const { payload: { email } } = payload;
const user = await getUserByEmail(supabaseClient, email as string);
return user;
} catch (error: any) {
throw new Error(error.message || "Failed to authenticate user");
}
};
/**
* Decrypts an encrypted secret with the same master encryption key.
* @param encryptedData - base64 string from the database
* @param iv - base64 IV from the database
* @param masterKey - 32-byte string or buffer
* @returns the original plaintext secret
*/
export function decryptSecret(
encryptedData: string,
iv: string,
masterKey: string,
) {
// Decode the base64 master key
const decodedKey = Buffer.from(masterKey, "base64");
if (decodedKey.length !== 32) {
throw new Error(
"ENCRYPTION_KEY must be 32 bytes when decoded from base64.",
);
}
const decipher = crypto.createDecipheriv(
"aes-256-cbc",
decodedKey, // Use the decoded key instead of raw masterKey
Buffer.from(iv, "base64"),
);
let decrypted = decipher.update(encryptedData, "base64", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
export function boostLimitPCM16LEInPlace(
pcmBytes: Uint8Array, // Buffer is fine (subclass of Uint8Array)
gainDb = 6.0,
ceiling = 0.89, // ≈ 1 dBFS
): void {
const dv = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength);
const g = Math.pow(10, gainDb / 20);
// Pass 1: measure post-gain peak
let peak = 0;
for (let i = 0; i < dv.byteLength; i += 2) {
const s = dv.getInt16(i, true) / 32768; // 16-bit LE → [-1,1]
const y = s * g;
const a = Math.abs(y);
if (a > peak) peak = a;
}
const scale = peak > ceiling && peak > 0 ? (ceiling / peak) : 1;
// Pass 2: apply gain + scale + gentle soft-clip
for (let i = 0; i < dv.byteLength; i += 2) {
let y = (dv.getInt16(i, true) / 32768) * g * scale;
// cubic soft-clip (tanh-ish) for nicer peaks
const y2 = y * y;
y = 0.5 * y * (3 - y2);
if (y > 0.999) y = 0.999;
if (y < -0.999) y = -0.999;
dv.setInt16(i, (y * 32767) | 0, true);
}
}
// Function to downsample PCM audio from 48kHz to 24kHz
export function downsamplePcm(pcmBuffer: Buffer, fromRate: number, toRate: number): Buffer {
if (fromRate === toRate) {
return pcmBuffer;
}
const ratio = fromRate / toRate;
const inputSamples = pcmBuffer.length / 2; // 16-bit = 2 bytes per sample
const outputSamples = Math.floor(inputSamples / ratio);
const outputBuffer = Buffer.alloc(outputSamples * 2);
for (let i = 0; i < outputSamples; i++) {
const sourceIndex = Math.floor(i * ratio) * 2;
const sample = pcmBuffer.readInt16LE(sourceIndex);
outputBuffer.writeInt16LE(sample, i * 2);
}
return outputBuffer;
}
// Function to extract PCM data from WAV file
export function extractPcmFromWav(wavBuffer: Buffer): Buffer | null {
try {
// Check minimum WAV header size
if (wavBuffer.length < 44) {
console.error('WAV file too small');
return null;
}
// Verify RIFF header
const riffHeader = wavBuffer.subarray(0, 4).toString('ascii');
if (riffHeader !== 'RIFF') {
console.error('Not a RIFF file');
return null;
}
// Verify WAVE format
const waveHeader = wavBuffer.subarray(8, 12).toString('ascii');
if (waveHeader !== 'WAVE') {
console.error('Not a WAVE file');
return null;
}
// Find the data chunk
let offset = 12;
while (offset < wavBuffer.length - 8) {
const chunkId = wavBuffer.subarray(offset, offset + 4).toString('ascii');
const chunkSize = wavBuffer.readUInt32LE(offset + 4);
if (chunkId === 'data') {
// Found data chunk, extract PCM data
const pcmData = wavBuffer.subarray(offset + 8, offset + 8 + chunkSize);
return pcmData;
}
// Move to next chunk
offset += 8 + chunkSize;
}
console.error('No data chunk found in WAV file');
return null;
} catch (error) {
console.error('Error extracting PCM from WAV:', error);
return null;
}
}