Add lazy piano transcription and optional leased server worker

This commit is contained in:
Jonathan Sykes
2026-10-03 15:47:53 +08:00
parent d4354d220e
commit 0374e19396
20 changed files with 1107 additions and 1 deletions

103
server/piano.test.js Normal file
View File

@@ -0,0 +1,103 @@
import { test, expect, beforeAll, afterAll } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Hono } from 'hono';
const root = mkdtempSync(join(tmpdir(), 'ytp-piano-'));
process.env.DB_PATH = join(root, 'test.db');
const db = await import('./db.js');
const { registerPianoRoutes } = await import('./piano.js');
const ID = '0gfX0dFLaBc',
TOKEN = 'piano-worker-test-'.repeat(3);
let app,
time = 1000;
const post = (path, body = {}, token = TOKEN) =>
app.request(path, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
beforeAll(async () => {
await db.initDb();
await db.upsertMedia(ID, { status: 'ready', duration: 60, gen: 1 });
app = new Hono();
await registerPianoRoutes(app, {
db,
enabled: true,
workerToken: TOKEN,
now: () => time,
adminAuth: async (c, next) =>
c.req.header('x-admin') === 'yes' ? next() : c.json({ ok: false }, 401),
}).ready;
});
afterAll(async () => {
db.db.close();
rmSync(root, { recursive: true, force: true });
});
test('server piano requires admin requests and dedicated worker authentication', async () => {
expect((await post(`/api/media/${ID}/piano`)).status).toBe(401);
expect((await post('/api/piano-worker/claim', {}, 'wrong')).status).toBe(401);
const result = await app.request(`/api/media/${ID}/piano`, {
method: 'POST',
headers: { 'x-admin': 'yes' },
});
expect(result.status).toBe(202);
expect((await result.json()).job.status).toBe('queued');
});
test('claim is exclusive; validated results persist as compact notes with inferred hands', async () => {
const first = await (await post('/api/piano-worker/claim')).json();
expect(first.job.audioPath).toBe(`/api/media/${ID}?a=1`);
expect((await (await post('/api/piano-worker/claim')).json()).job).toBeNull();
expect(
(await post(`/api/piano-worker/${ID}`, { action: 'complete', lease: 'wrong', notes: [] }))
.status,
).toBe(409);
expect(
(
await post(`/api/piano-worker/${ID}`, {
action: 'complete',
lease: first.job.lease,
notes: [{ pitch: 60, start: 2, end: 1, velocity: 80 }],
})
).status,
).toBe(422);
expect(
(
await post(`/api/piano-worker/${ID}`, {
action: 'complete',
lease: first.job.lease,
notes: [{ pitch: 48, start: 1.24, end: 1.78, velocity: 85 }],
})
).status,
).toBe(200);
const result = await (await app.request(`/api/media/${ID}/piano`)).json();
expect(result.job.notes[0]).toEqual({
pitch: 48,
start: 1.24,
end: 1.78,
velocity: 85,
hand: 'left',
});
});
test('changed source invalidates notes, lost leases are re-claimed and stale workers cannot publish', async () => {
await db.upsertMedia(ID, { gen: 2 });
expect((await (await app.request(`/api/media/${ID}/piano`)).json()).job.status).toBe('stale');
await app.request(`/api/media/${ID}/piano`, { method: 'POST', headers: { 'x-admin': 'yes' } });
const old = (await (await post('/api/piano-worker/claim')).json()).job;
time += 120001;
const next = (await (await post('/api/piano-worker/claim')).json()).job;
expect(next.lease).not.toBe(old.lease);
expect(
(await post(`/api/piano-worker/${ID}`, { action: 'complete', lease: old.lease, notes: [] }))
.status,
).toBe(409);
expect(
(await post(`/api/piano-worker/${ID}`, { action: 'heartbeat', lease: next.lease })).status,
).toBe(200);
});
test('optional server pipeline defaults off and rejects malformed ids', async () => {
const disabled = new Hono();
await registerPianoRoutes(disabled, { db, adminAuth: async (c, next) => next() }).ready;
expect((await disabled.request(`/api/media/${ID}/piano`, { method: 'POST' })).status).toBe(503);
expect((await disabled.request('/api/media/bad/piano')).status).toBe(400);
});