ai-code-writer
Version:
An AI code writer application using OpenAI APIs for audio transcription and chat completion.
217 lines (216 loc) • 8.4 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const stream_1 = require("stream");
const node_web_audio_api_1 = require("node-web-audio-api");
const node_microphone_1 = __importDefault(require("node-microphone"));
const debug = false;
class RecordingHandler {
constructor(resolve, reject, config, silenceThreshold, newSilenceThresholdCallback) {
this.resolve = resolve;
this.reject = reject;
this.config = config;
this.silenceThreshold = silenceThreshold;
this.newSilenceThresholdCallback = newSilenceThresholdCallback;
this.chunks = [];
this.isStopped = false;
this.passThrough = new stream_1.PassThrough();
this.audioContext = new node_web_audio_api_1.AudioContext();
this.silenceDuration = 0;
this.audioInputDetected = false;
this.isSilenceLevelAdjusted = false;
this.silenceLevels = [];
this.firstFrameSkipped = false;
this.registerEvents();
}
startRecording() {
return __awaiter(this, void 0, void 0, function* () {
this.isSilenceLevelAdjusted = this.silenceThreshold != 0;
try {
this.microphoneInstance = new node_microphone_1.default();
const micStream = this.microphoneInstance.startRecording();
micStream.on('data', (chunk) => {
this.handleData(chunk);
});
micStream.on('end', () => this.streamEnded());
micStream.on('error', (error) => this.onError(error));
this.waitTimeout = setTimeout(() => {
if (!this.audioInputDetected) {
this.chunks = [];
this.stopRecording();
}
}, this.config.MAX_WAIT_DURATION);
}
catch (error) {
this.reject(error);
}
});
}
stopRecording() {
this.isStopped = true;
if (this.microphoneInstance) {
this.microphoneInstance.stopRecording();
this.microphoneInstance = undefined;
}
if (!this.passThrough.destroyed) {
this.passThrough.end();
}
this.audioContext.close().catch((error) => console.error('Error closing audio context:', error));
}
handleData(chunk) {
this.storeChunk(chunk);
if (!this.firstFrameSkipped) {
this.skipFirstFrame();
return;
}
if (this.isStopped)
return;
if (!this.isSilenceLevelAdjusted)
this.adjustSilenceLevel(chunk);
this.processAudioChunk(chunk);
}
storeChunk(chunk) {
this.chunks.push(chunk);
if (this.passThrough.writable && !this.passThrough.writableEnded && !this.passThrough.writableFinished) {
this.passThrough.write(chunk);
}
}
skipFirstFrame() {
this.firstFrameSkipped = true;
}
processAudioChunk(chunk) {
const isSilent = this.isSilenceLevelAdjusted && this.isSilentChunk(chunk);
if (isSilent) {
this.incrementSilenceDuration(chunk);
}
else {
this.resetSilenceDuration();
}
if (this.shouldStopRecording()) {
this.stopRecording();
}
if (debug)
this.logDebugInfo(chunk, isSilent);
}
incrementSilenceDuration(chunk) {
this.silenceDuration += chunk.length / this.audioContext.sampleRate * 1000;
}
resetSilenceDuration() {
this.silenceDuration = 0;
if (!this.audioInputDetected && this.isSilenceLevelAdjusted) {
this.audioInputDetected = true;
clearTimeout(this.waitTimeout);
}
}
shouldStopRecording() {
return this.silenceDuration >= this.config.MAX_SILENCE_DURATION && this.audioInputDetected;
}
logDebugInfo(chunk, isSilent) {
console.log('Audio:', this.getAudioLevel(chunk), 'Threshold:', this.silenceThreshold, 'isSilent:', isSilent, 'audioInputDetected:', this.audioInputDetected);
}
adjustSilenceLevel(chunk) {
if (this.silenceLevels.length < this.config.SILENCE_DETECTION_WINDOW) {
this.silenceLevels.push(this.getAudioLevel(chunk));
}
else {
this.sanitizeHighLevelStartingValue();
this.silenceThreshold = this.silenceLevels.reduce((a, b) => a + b, 0) / this.silenceLevels.length * this.config.silenceThresholdMultiplier;
this.isSilenceLevelAdjusted = true;
this.newSilenceThresholdCallback(this.silenceThreshold);
}
}
sanitizeHighLevelStartingValue() {
this.silenceLevels.shift();
}
isSilentChunk(chunk) {
const avg = this.getAudioLevel(chunk);
return avg <= this.silenceThreshold;
}
getAudioLevel(chunk) {
const int16Array = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.byteLength / Int16Array.BYTES_PER_ELEMENT);
return Math.sqrt(int16Array.reduce((sum, value) => sum + value * value, 0) / int16Array.length);
}
registerEvents() {
this.passThrough.on('end', this.onEnd.bind(this));
this.passThrough.on('error', this.onError.bind(this));
}
streamEnded() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.isStopped) {
this.stopRecording();
}
else {
this.onEnd();
}
});
}
onEnd() {
if (!this.isStopped) {
this.stopRecording();
}
this.resolve(Buffer.concat(this.chunks));
this.isStopped = true;
}
onError(error) {
if (!this.isStopped) {
this.stopRecording();
}
this.reject(error);
}
}
class NodeAudioRecorder {
constructor(config) {
this.config = config;
this.silenceThreshold = 0;
}
startRecording() {
return __awaiter(this, void 0, void 0, function* () {
if (this.currentRecordingHandler) {
this.currentRecordingHandler.stopRecording();
}
return new Promise((resolve, reject) => {
const handler = new RecordingHandler(resolve, reject, this.config, this.silenceThreshold, this.changeSilenceThreshold.bind(this));
this.currentRecordingHandler = handler;
handler.startRecording().catch((error) => {
reject(error);
});
});
});
}
stopRecording() {
return __awaiter(this, void 0, void 0, function* () {
if (this.currentRecordingHandler) {
this.currentRecordingHandler.stopRecording();
this.currentRecordingHandler = undefined;
}
});
}
measureNoiseLevel() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
const handler = new RecordingHandler(() => resolve(), reject, this.config, this.silenceThreshold, this.changeSilenceThreshold.bind(this));
handler.startRecording().catch((error) => {
reject(error);
});
setTimeout(() => {
handler.stopRecording();
}, this.config.NOISE_MEASUREMENT_DURATION);
});
});
}
changeSilenceThreshold(newValue) {
this.silenceThreshold = newValue;
}
}
exports.default = NodeAudioRecorder;