Initial commit: Tonemark PWA
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:
Giancarmine Salucci
2026-05-06 16:41:25 +02:00
co-authored by Copilot
commit 13a96b6efa
68 changed files with 9712 additions and 0 deletions
+137
View File
@@ -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);
}