aivox
Version:
A lightweight CLI tool for translating voice to text using Whisper, seamlessly piping the transcribed text to any Unix-like command for versatile integration.
174 lines (170 loc) • 6.4 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/worker.ts
var import_node_wav = require("node-wav");
var import_node_record_lpcm16 = __toESM(require("node-record-lpcm16"));
var import_wav_headers = __toESM(require("wav-headers"));
var import_smart_whisper = require("smart-whisper");
// src/download_model.ts
var import_node_fs = require("fs");
var import_node_path = require("path");
var src = "https://huggingface.co/ggerganov/whisper.cpp";
var pfx = "resolve/main/ggml";
var model = "base.en";
var downloadModel = (modelsPath = "./models") => __async(void 0, null, function* () {
const modelFileName = `ggml-${model}.bin`;
const modelFilePath = (0, import_node_path.join)(modelsPath, modelFileName);
if (!(0, import_node_fs.existsSync)(modelsPath)) {
(0, import_node_fs.mkdirSync)(modelsPath, { recursive: true });
}
if ((0, import_node_fs.existsSync)(modelFilePath)) {
return;
}
const url = `${src}/${pfx}-${model}.bin`;
try {
const response = yield fetch(url);
if (!response.ok) {
throw new Error(`Failed to download ggml model ${model}`);
}
const arrayBuffer = yield response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const fileStream = (0, import_node_fs.createWriteStream)(modelFilePath);
fileStream.write(buffer);
fileStream.end();
} catch (error) {
console.error(error.message);
console.error(
"Please try again later or download the original Whisper model files and convert them yourself."
);
process.exit(1);
}
});
var download_model_default = downloadModel;
// src/worker.ts
var import_node_os = require("os");
var import_node_path2 = require("path");
var import_node_process = require("process");
var import_node_readline = __toESM(require("readline"));
function main() {
return __async(this, null, function* () {
const homeDir = (0, import_node_os.homedir)();
const directoryName = ".aivox/models";
const fullPath = (0, import_node_path2.join)(homeDir, directoryName);
const dl = download_model_default(fullPath);
const model2 = (0, import_node_path2.join)(fullPath, "ggml-base.en.bin");
const recording = import_node_record_lpcm16.default.record({
sampleRate: 16e3,
channels: 1,
encoding: "pcm16"
});
const chunks = [];
recording.stream().on("data", (chunk) => chunks.push(chunk));
import_node_readline.default.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
let recordingFinished = false;
const stopRecordingAndProcess = () => __async(this, null, function* () {
if (recordingFinished) return;
recordingFinished = true;
recording.stop();
const rawData = Buffer.concat(chunks);
if (rawData.length < 16e3) {
console.log("Not enough audio data recorded. Please try again and speak for a longer duration.");
(0, import_node_process.exit)(0);
}
const header = (0, import_wav_headers.default)({
sampleRate: 16e3,
channels: 1,
bitDepth: 16,
dataLength: rawData.length
});
const wavFile = Buffer.concat([header, rawData]);
try {
yield dl;
const whisper = new import_smart_whisper.Whisper(model2, { gpu: true });
const pcm = readWavFromBuffer(wavFile);
const task = yield whisper.transcribe(pcm, {
language: "en",
print_special: false,
print_progress: false,
print_realtime: false,
print_timestamps: false
});
const result = yield task.result;
if (result && result.length > 0 && result[0].text) {
console.log(result[0].text);
} else {
console.log("No transcription result. The audio might be too short or silent.");
}
yield whisper.free();
(0, import_node_process.exit)(0);
} catch (error) {
console.error("Error during transcription:", error);
(0, import_node_process.exit)(1);
}
});
const timeoutId = setTimeout(stopRecordingAndProcess, 5e3);
process.stdin.on("keypress", (str, key) => {
if (key.name === "space" || key.ctrl && key.name === "c") {
clearTimeout(timeoutId);
stopRecordingAndProcess();
}
});
});
}
function readWavFromBuffer(buffer) {
const { sampleRate, channelData } = (0, import_node_wav.decode)(buffer);
if (sampleRate !== 16e3) {
throw new Error(`Invalid sample rate: ${sampleRate}`);
}
if (channelData.length !== 1) {
throw new Error(`Invalid channel count: ${channelData.length}`);
}
return channelData[0];
}
main().catch((error) => {
console.error("Error:", error);
process.exit(1);
});