Initial commit: Tonemark PWA
Some checks failed
Build & Push Docker Image / build-and-push (push) Failing after 11s
Some checks failed
Build & Push Docker Image / build-and-push (push) Failing after 11s
Tonemark is a SvelteKit PWA for transcribing YouTube videos, audio and video files, and microphone recordings using a local Whisper backend. Features: - Dark glassmorphic UI with electric-lime accent (5 switchable themes) - Rail nav (desktop) / tab bar (mobile) layout - Drop zone, YouTube URL input, and live audio recording inputs - Audio mode waveform cards (none / standard / aggressive / auto) - Real-time transcription progress with animated waveform - Job queue with SSE streaming updates - Push notifications on job completion - PWA with native SvelteKit service worker - SRT / TXT / MD / JSON transcript downloads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
91
src/lib/server/whisper.ts
Normal file
91
src/lib/server/whisper.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function whisperUrl() {
|
||||
return process.env.WHISPER_URL ?? 'http://localhost:8080';
|
||||
}
|
||||
|
||||
/** Submit an audio file to whisper-rtx2080. Returns the whisper job id. */
|
||||
export async function submitJob(
|
||||
wavPath: string,
|
||||
webhookUrl: string,
|
||||
language?: string
|
||||
): Promise<string> {
|
||||
const FormData = (await import('form-data')).default;
|
||||
const { createReadStream } = await import('fs');
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
|
||||
const form = new FormData();
|
||||
form.append('audio', createReadStream(wavPath));
|
||||
form.append('task', 'transcribe');
|
||||
form.append('webhook_url', webhookUrl);
|
||||
if (language) form.append('language', language);
|
||||
|
||||
const res = await fetch(`${whisperUrl()}/jobs`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: form.getHeaders()
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`whisper /jobs returned ${res.status}: ${text}`);
|
||||
}
|
||||
|
||||
const json = (await res.json()) as { job_id: string };
|
||||
return json.job_id;
|
||||
}
|
||||
|
||||
/** Open an SSE stream from whisper and call onProgress/onDone callbacks. */
|
||||
export async function streamJob(
|
||||
whisperJobId: string,
|
||||
onProgress: (percent: number, chunk: number, total: number) => void,
|
||||
onDone: () => void,
|
||||
onError: (msg: string) => void
|
||||
): Promise<void> {
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
const res = await fetch(`${whisperUrl()}/jobs/${whisperJobId}/stream`);
|
||||
if (!res.ok || !res.body) throw new Error(`SSE stream returned ${res.status}`);
|
||||
|
||||
let buf = '';
|
||||
for await (const chunk of res.body) {
|
||||
buf += chunk.toString();
|
||||
const lines = buf.split('\n');
|
||||
buf = lines.pop() ?? '';
|
||||
|
||||
let eventType = '';
|
||||
let dataLine = '';
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event:')) eventType = line.slice(6).trim();
|
||||
else if (line.startsWith('data:')) dataLine = line.slice(5).trim();
|
||||
}
|
||||
|
||||
if (!dataLine) continue;
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(dataLine);
|
||||
if (payload.type === 'progress') {
|
||||
onProgress(payload.percent ?? 0, payload.chunk ?? 0, payload.total ?? 0);
|
||||
} else if (payload.type === 'done') {
|
||||
onDone();
|
||||
return;
|
||||
} else if (payload.type === 'error') {
|
||||
onError(payload.message ?? 'unknown error');
|
||||
return;
|
||||
}
|
||||
} catch { /* ignore parse errors */ }
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if the whisper server is healthy. */
|
||||
export async function checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
const res = await fetch(`${whisperUrl()}/health`, { signal: AbortSignal.timeout(3000) });
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user