import {spawn} from 'node:child_process'; import fs from 'node:fs/promises'; import Path from 'node:path'; import {Ai} from './ai.ts'; export class Audio { private downloads: {[key: string]: Promise} = {}; private whisperModel!: string; constructor(private ai: Ai) { if(ai.options.whisper?.binary) { this.whisperModel = ai.options.whisper?.model.endsWith('.bin') ? ai.options.whisper?.model : ai.options.whisper?.model + '.bin'; this.downloadAsrModel(); } } /** * Convert audio to text using Auditory Speech Recognition * @param {string} path Path to audio * @param model Whisper model * @returns {Promise} Extracted text */ asr(path: string, model: string = this.whisperModel): {abort: () => void, response: Promise} { if(!this.ai.options.whisper?.binary) throw new Error('Whisper not configured'); let abort: any = () => {}; const response = new Promise((resolve, reject) => { this.downloadAsrModel(model).then(m => { let output = ''; const proc = spawn(this.ai.options.whisper?.binary, ['-nt', '-np', '-m', m, '-f', path], {stdio: ['ignore', 'pipe', 'ignore']}); abort = () => proc.kill('SIGTERM'); proc.on('error', (err: Error) => reject(err)); proc.stdout.on('data', (data: Buffer) => output += data.toString()); proc.on('close', (code: number) => { if(code === 0) resolve(output.trim() || null); else reject(new Error(`Exit code ${code}`)); }); }); }); return {response, abort}; } /** * Downloads the specified Whisper model if it is not already present locally. * * @param {string} model Whisper model that will be downloaded * @return {Promise} Absolute path to model file, resolves once downloaded */ async downloadAsrModel(model: string = this.whisperModel): Promise { if(!this.ai.options.whisper?.binary) throw new Error('Whisper not configured'); if(!model.endsWith('.bin')) model += '.bin'; const p = Path.join(this.ai.options.whisper.path, model); if(await fs.stat(p).then(() => true).catch(() => false)) return p; if(!!this.downloads[model]) return this.downloads[model]; this.downloads[model] = fetch(`https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${model}`) .then(resp => resp.arrayBuffer()) .then(arr => Buffer.from(arr)).then(async buffer => { await fs.writeFile(p, buffer); delete this.downloads[model]; return p; }); return this.downloads[model]; } }