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

View File

@@ -0,0 +1,56 @@
const { test, expect } = require('@playwright/test');
const { openClassic } = require('./helpers/classic-fixture');
test('Basic Pitch is lazy and transcribes a short synthetic WAV', async ({ page }) => {
test.setTimeout(120000);
page.on('requestfailed', (req) =>
console.log('Engine request failed:', req.url(), req.failure()?.errorText),
);
const requests = [];
page.on('request', (req) => {
if (/esm\.sh|jsdelivr.*basic-pitch/.test(req.url())) requests.push(req.url());
});
await openClassic(page);
expect(requests).toEqual([]);
const result = await page.evaluate(async () => {
const rate = 22050,
samples = rate * 2,
buffer = new ArrayBuffer(44 + samples * 2),
view = new DataView(buffer);
const text = (offset, value) => {
[...value].forEach((c, i) => view.setUint8(offset + i, c.charCodeAt(0)));
};
text(0, 'RIFF');
view.setUint32(4, 36 + samples * 2, true);
text(8, 'WAVE');
text(12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, rate, true);
view.setUint32(28, rate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
text(36, 'data');
view.setUint32(40, samples * 2, true);
for (let i = 0; i < samples; i++)
view.setInt16(44 + i * 2, Math.sin((2 * Math.PI * 440 * i) / rate) * 16000, true);
const engine = await import('/piano-engine.mjs');
const result = await engine.transcribe(buffer.slice(0), () => {});
const tf = await import('https://esm.sh/@tensorflow/tfjs@4.22.0');
const before = tf.memory().numTensors;
const second = await engine.transcribe(buffer.slice(0), () => {});
return {
engine: result.engine,
notes: PianoCore.fromBasicPitch(result.notes),
secondNotes: second.notes.length,
before,
after: tf.memory().numTensors,
};
});
expect(result.engine).toMatch(/basic-pitch-1.0.1\/(webgpu|webgl|cpu)/);
expect(requests.length).toBeGreaterThan(0);
expect(result.notes.length).toBeGreaterThan(0);
expect(result.notes.some((n) => n.pitch === 69)).toBe(true);
expect(result.secondNotes).toBeGreaterThan(0);
expect(result.after).toBe(result.before);
});

View File

@@ -73,3 +73,11 @@ for (const layout of ['classic', 'glass-stage']) test(`${layout} secondary lyric
await expect(page.locator('#serviceLyricsList .lyric-phonetic')).toHaveText('zhǔ nǐ de ēndiǎn');
expect(await page.locator('#serviceLyricsList .lyric-secondary').first().evaluate(el => parseFloat(getComputedStyle(el).fontSize) < parseFloat(getComputedStyle(el.parentElement).fontSize))).toBe(true);
});
for (const layout of ['classic', 'glass-stage']) for (const width of [390,1440]) test(`${layout} piano settings stay lazy at ${width}px`, async ({ page }) => {
const models=[];page.on('request',req=>{if(/esm\.sh|jsdelivr.*basic-pitch/.test(req.url()))models.push(req.url());});
await page.setViewportSize({width,height:900});await openClassic(page);await page.evaluate(layout=>{data.settings.layout=layout;applyAppearance();current={meta:{id:'0gfX0dFLaBc',title:'Piano fixture',duration:200}};view={type:'settings'};render();},layout);
await page.locator('[data-section="piano"]').click();await expect(page.getByRole('button',{name:'Transcribe in browser',exact:true})).toBeDisabled();
await page.locator('#settings-section-piano input[type="checkbox"]').check();await expect(page.getByRole('button',{name:'Transcribe in browser',exact:true})).toBeEnabled();
await page.getByRole('button',{name:'Add current 8 s to practice',exact:true}).click();await expect(page.locator('.piano-practice button').first()).toHaveText('Loop 0.0–8.0 s');
expect(models).toEqual([]);expect(await page.locator('#settings-section-piano').evaluate(el=>el.scrollWidth<=el.clientWidth+1)).toBe(true);
});