feat(telegram-remote): ship lib/ code, add voice transcription, chat pagination, stream photos
- Add lib/ (plain-ESM plugin code, no build step) so the published tarball actually contains the plugin; un-ignore lib/ for this package - Sync README: voice/audio transcription via home-lab Whishper (default large-v2, device cuda, language auto), /chats pagination and subagent hiding, whisper* config keys, updated security note
This commit is contained in:
85
packages/telegram-remote/lib/whisper.js
Normal file
85
packages/telegram-remote/lib/whisper.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Zero-dependency client for the home-lab Whishper speech-to-text server
|
||||
* (g-daco/Whishper). Multipart upload + job polling over global fetch/
|
||||
* FormData/Blob (Node >= 22). Server contract (verified against
|
||||
* http://192.168.31.159:8082):
|
||||
* POST {base}/api/transcriptions multipart fields: file, language,
|
||||
* modelSize, device (cuda|cpu), sourceUrl
|
||||
* GET {base}/api/transcriptions/{id}
|
||||
* Job status -1 = queued/running; statuses 0/1/2 = terminal; a successful
|
||||
* terminal job has a non-empty result.text.
|
||||
*/
|
||||
|
||||
const POLL_INTERVAL_MS = 1500;
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transcribe an audio buffer via the Whishper server.
|
||||
* @param {object} opts - { baseUrl, model, device, language, timeoutMs, maxBytes }
|
||||
* @param {Buffer} audioBuffer - audio bytes to transcribe
|
||||
* @param {string} filename - upload filename (e.g. "voice.ogg")
|
||||
* @param {string} [mimeType] - content type for the upload blob
|
||||
* @returns {Promise<string>} the transcribed text
|
||||
* @throws {Error} clear message on empty baseUrl, non-Buffer input, size
|
||||
* overrun, upload HTTP rejection, terminal-without-text, or timeout
|
||||
*/
|
||||
export async function transcribeAudio({ baseUrl, model, device, language, timeoutMs, maxBytes }, audioBuffer, filename, mimeType) {
|
||||
const base = String(baseUrl ?? "").replace(/\/+$/, "");
|
||||
if (!base) throw new Error("whisper: baseUrl is empty");
|
||||
if (!Buffer.isBuffer(audioBuffer)) throw new Error("whisper: audioBuffer must be a Buffer");
|
||||
const max = Number(maxBytes) || 0;
|
||||
if (max > 0 && audioBuffer.length > max) {
|
||||
throw new Error("whisper: audio is " + audioBuffer.length + " bytes, limit is " + max);
|
||||
}
|
||||
const name = String(filename ?? "audio.bin");
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([audioBuffer], mimeType ? { type: mimeType } : {}), name);
|
||||
form.append("language", String(language ?? "").trim() || "auto");
|
||||
form.append("modelSize", String(model ?? "large-v2"));
|
||||
form.append("device", String(device ?? "cuda"));
|
||||
form.append("sourceUrl", "");
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(base + "/api/transcriptions", { method: "POST", body: form });
|
||||
} catch (error) {
|
||||
throw new Error("whisper: upload failed: " + error.message);
|
||||
}
|
||||
if (!res.ok) {
|
||||
const body = (await res.text()).slice(0, 300);
|
||||
throw new Error("whisper: upload rejected (HTTP " + res.status + "): " + body);
|
||||
}
|
||||
let created;
|
||||
try {
|
||||
created = await res.json();
|
||||
} catch {
|
||||
throw new Error("whisper: upload response was not JSON");
|
||||
}
|
||||
const id = created?.id;
|
||||
if (!id) throw new Error("whisper: no job id in upload response");
|
||||
|
||||
const timeout = Math.max(Number(timeoutMs) || 120000, 1000);
|
||||
const deadline = Date.now() + timeout;
|
||||
for (;;) {
|
||||
if (Date.now() >= deadline) {
|
||||
throw new Error("whisper: transcription timed out after " + timeout + "ms");
|
||||
}
|
||||
await sleep(POLL_INTERVAL_MS);
|
||||
let job;
|
||||
try {
|
||||
const pollRes = await fetch(base + "/api/transcriptions/" + id);
|
||||
if (!pollRes.ok) throw new Error("HTTP " + pollRes.status);
|
||||
job = await pollRes.json();
|
||||
} catch (error) {
|
||||
throw new Error("whisper: poll failed: " + error.message);
|
||||
}
|
||||
const text = job?.result?.text;
|
||||
if (typeof text === "string" && text.trim().length > 0) return text;
|
||||
if (job?.status !== -1) {
|
||||
throw new Error("whisper: job finished without text (status " + String(job?.status) + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user