feat(audio): add voice selector, voice samples, MediaSession cover art, and fix speed/voice bug
- Fix speed/voice bug: AudioPlayer no longer accepts speed/voice as props;
startPlayback() reads audioStore.voice/speed directly instead of overwriting them
- Add GET /api/voices endpoint (Go) proxying Kokoro, cached in-memory
- Add POST /api/audio/voice-samples endpoint (Go) to pre-generate sample clips
for all voices and store them in MinIO under _voice-samples/{voice}.mp3
- Add GET /api/presign/voice-sample/{voice} endpoint (Go)
- Add SvelteKit proxy routes: /api/voices, /api/presign/voice-sample, /api/audio/voice-samples
- Add presignVoiceSample() helper in minio.ts with proper host rewrite
- Pass book.cover through +page.server.ts -> +page.svelte -> AudioPlayer
- Set navigator.mediaSession.metadata on playback start so cover art,
book title, and chapter title appear on phone lock screen / notification
This commit is contained in:
@@ -61,6 +61,35 @@ export async function presignChapter(slug: string, n: number, rewrite = false):
|
||||
return rewrite ? rewriteHost(data.url) : data.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a presigned URL for a voice sample audio file.
|
||||
* URL is valid for ~1 hour. The URL is returned to the browser for direct streaming.
|
||||
* Throws with { status: 404 } when the sample has not been generated yet.
|
||||
*/
|
||||
export async function presignVoiceSample(voice: string): Promise<string> {
|
||||
log.debug('minio', 'presigning voice sample', { voice });
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${SCRAPER_URL}/api/presign/voice-sample/${encodeURIComponent(voice)}`);
|
||||
} catch (e) {
|
||||
log.error('minio', 'presign voice sample network error', { voice, err: String(e) });
|
||||
throw new Error(`presign voice sample ${voice}: network error`);
|
||||
}
|
||||
if (res.status === 404) {
|
||||
const err = new Error(`presign voice sample ${voice}: not found`) as Error & { status: number };
|
||||
err.status = 404;
|
||||
throw err;
|
||||
}
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '');
|
||||
log.error('minio', 'presign voice sample failed', { voice, status: res.status, body });
|
||||
throw new Error(`presign voice sample ${voice}: ${res.status}`);
|
||||
}
|
||||
const data = (await res.json()) as { url: string };
|
||||
log.debug('minio', 'presign voice sample ok', { voice });
|
||||
return rewriteHost(data.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a presigned URL for an audio file.
|
||||
* URL is valid for ~1 hour. The URL is returned to the browser for direct streaming.
|
||||
|
||||
Reference in New Issue
Block a user