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:
@@ -0,0 +1,137 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import type { Job, JobStatus, AudioMode, PushSubscription } from '$lib/types.js';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR ?? join(process.env.HOME ?? '/tmp', '.whisper-pwa');
|
||||
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
const db = new Database(join(DATA_DIR, 'jobs.db'));
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
audio_mode TEXT NOT NULL DEFAULT 'auto',
|
||||
mean_volume REAL,
|
||||
whisper_job_id TEXT,
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
output_dir TEXT,
|
||||
segments_json TEXT,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
endpoint TEXT NOT NULL UNIQUE,
|
||||
p256dh TEXT NOT NULL,
|
||||
auth TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
|
||||
const stmts = {
|
||||
insertJob: db.prepare(`
|
||||
INSERT INTO jobs (id, status, title, source, audio_mode)
|
||||
VALUES (@id, 'pending', @title, @source, @audioMode)
|
||||
`),
|
||||
updateJob: db.prepare(`
|
||||
UPDATE jobs SET
|
||||
status = @status,
|
||||
title = @title,
|
||||
mean_volume = @meanVolume,
|
||||
whisper_job_id = @whisperJobId,
|
||||
progress = @progress,
|
||||
output_dir = @outputDir,
|
||||
segments_json = @segmentsJson,
|
||||
error = @error,
|
||||
updated_at = datetime('now')
|
||||
WHERE id = @id
|
||||
`),
|
||||
setStatus: db.prepare(`
|
||||
UPDATE jobs SET status = @status, progress = @progress, updated_at = datetime('now')
|
||||
WHERE id = @id
|
||||
`),
|
||||
getJob: db.prepare('SELECT * FROM jobs WHERE id = ?'),
|
||||
listJobs: db.prepare('SELECT * FROM jobs ORDER BY created_at DESC, rowid DESC LIMIT 100'),
|
||||
upsertSub: db.prepare(`
|
||||
INSERT INTO push_subscriptions (endpoint, p256dh, auth)
|
||||
VALUES (@endpoint, @p256dh, @auth)
|
||||
ON CONFLICT(endpoint) DO UPDATE SET p256dh = excluded.p256dh, auth = excluded.auth
|
||||
`),
|
||||
allSubs: db.prepare('SELECT * FROM push_subscriptions'),
|
||||
deleteSub: db.prepare('DELETE FROM push_subscriptions WHERE endpoint = ?')
|
||||
};
|
||||
|
||||
function rowToJob(row: Record<string, unknown>): Job {
|
||||
return {
|
||||
id: row.id as string,
|
||||
status: row.status as JobStatus,
|
||||
title: row.title as string,
|
||||
source: row.source as string,
|
||||
audioMode: row.audio_mode as AudioMode,
|
||||
meanVolume: row.mean_volume as number | null,
|
||||
whisperJobId: row.whisper_job_id as string | null,
|
||||
progress: row.progress as number,
|
||||
outputDir: row.output_dir as string | null,
|
||||
segmentsJson: row.segments_json as string | null,
|
||||
error: row.error as string | null,
|
||||
createdAt: row.created_at as string,
|
||||
updatedAt: row.updated_at as string
|
||||
};
|
||||
}
|
||||
|
||||
export function createJob(source: string, title: string, audioMode: AudioMode): Job {
|
||||
const id = randomUUID();
|
||||
stmts.insertJob.run({ id, title, source, audioMode });
|
||||
return getJob(id)!;
|
||||
}
|
||||
|
||||
export function getJob(id: string): Job | null {
|
||||
const row = stmts.getJob.get(id) as Record<string, unknown> | undefined;
|
||||
return row ? rowToJob(row) : null;
|
||||
}
|
||||
|
||||
export function listJobs(): Job[] {
|
||||
return (stmts.listJobs.all() as Record<string, unknown>[]).map(rowToJob);
|
||||
}
|
||||
|
||||
export function updateJob(job: Partial<Job> & { id: string }): void {
|
||||
const current = getJob(job.id);
|
||||
if (!current) return;
|
||||
const merged = { ...current, ...job };
|
||||
stmts.updateJob.run({
|
||||
id: merged.id,
|
||||
status: merged.status,
|
||||
title: merged.title,
|
||||
meanVolume: merged.meanVolume,
|
||||
whisperJobId: merged.whisperJobId,
|
||||
progress: merged.progress,
|
||||
outputDir: merged.outputDir,
|
||||
segmentsJson: merged.segmentsJson,
|
||||
error: merged.error
|
||||
});
|
||||
}
|
||||
|
||||
export function setJobStatus(id: string, status: JobStatus, progress = 0): void {
|
||||
stmts.setStatus.run({ id, status, progress });
|
||||
}
|
||||
|
||||
export function savePushSubscription(sub: { endpoint: string; p256dh: string; auth: string }): void {
|
||||
stmts.upsertSub.run(sub);
|
||||
}
|
||||
|
||||
export function getAllSubscriptions(): PushSubscription[] {
|
||||
return stmts.allSubs.all() as PushSubscription[];
|
||||
}
|
||||
|
||||
export function deletePushSubscription(endpoint: string): void {
|
||||
stmts.deleteSub.run(endpoint);
|
||||
}
|
||||
Reference in New Issue
Block a user