64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
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<string>} = {};
|
|
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<any>} Extracted text
|
|
*/
|
|
asr(path: string, model: string = this.whisperModel): {abort: () => void, response: Promise<string | null>} {
|
|
if(!this.ai.options.whisper?.binary) throw new Error('Whisper not configured');
|
|
let abort: any = () => {};
|
|
const response = new Promise<string | null>((resolve, reject) => {
|
|
this.downloadAsrModel(model).then(m => {
|
|
let output = '';
|
|
const proc = spawn(<string>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<string>} Absolute path to model file, resolves once downloaded
|
|
*/
|
|
async downloadAsrModel(model: string = this.whisperModel): Promise<string> {
|
|
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];
|
|
}
|
|
}
|