v4.3: file-based media storage + manual VACUUM maintenance

This commit is contained in:
Ashim Kumar
2026-06-12 13:24:00 +06:00
parent 965470853e
commit cc57204aff
10 changed files with 789 additions and 164 deletions

View File

@@ -1,12 +1,12 @@
/**
* Interactive Reader Module — Lazy Audio Loading (v4)
* Interactive Reader Module — File-based Audio (v4.3)
*
* Strategy:
* - Text + transcription are already loaded (from editorBlocks in memory).
* - Audio is fetched on-demand from /api/projects/<id>/audio/<block_id>
* when the user wants to play that block.
* - Smart preload: at 70% of current block, fetch next block's audio.
* - Memory cap: keep at most MAX_AUDIO_LOADED blob URLs alive.
* - Text + transcription loaded from editorBlocks in memory.
* - Audio fetched on-demand from /api/projects/<id>/audio/<block_id>.
* v4.3: endpoint may return binary audio (Content-Type: audio/*) OR
* legacy base64 JSON. Both handled.
* - Smart preload + memory cap (sliding window of blob URLs).
*/
// ============================================
@@ -66,7 +66,6 @@ function renderInteractiveReader() {
isFirstBlockOfChapter = false;
// has_audio comes from server; audio_data may not yet be loaded
if (!isImageBlock && blockData && (blockData.audio_data || blockData.has_audio)) {
hasAudio = true;
}
@@ -106,7 +105,6 @@ function renderInteractiveReader() {
const blockData = block._editorData;
const isImageBlock = block._isImage;
// has_audio is the SOURCE OF TRUTH for whether this block has audio on server
const hasBlockAudio = !isImageBlock && blockData && (blockData.audio_data || blockData.has_audio);
const blockId = blockData ? blockData.id : `reader_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
@@ -447,38 +445,45 @@ function setReaderButtonLoading(isLoading) {
}
// ============================================
// Audio Lazy Loading
// Audio Lazy Loading (v4.3 — binary OR base64)
// ============================================
/**
* Fetch audio for an instance. If already loaded into editorBlocks
* by background loader, use that. Otherwise fetch from API directly.
* Fetch audio for an instance.
* v4.3: endpoint may return binary audio (Content-Type: audio/*)
* → return { audio_url } ; OR legacy base64 JSON → return { audio_data }.
*/
async function fetchAudioForInstance(inst) {
// Path 1: audio_data already in editorBlocks (loaded in background)
// Path 1: audio_data already in editorBlocks (rare, generated this session)
if (inst.blockData && inst.blockData.audio_data) {
return {
audio_data: inst.blockData.audio_data,
audio_format: inst.blockData.audio_format || 'mp3'
};
}
// Path 2: fetch from API
if (!inst.blockData || !inst.blockData.db_id || !currentProject || !currentProject.id) {
throw new Error('Cannot fetch audio: missing block info');
}
const resp = await fetch(`/api/projects/${currentProject.id}/audio/${inst.blockData.db_id}`);
if (!resp.ok) throw new Error('No audio data');
const contentType = resp.headers.get('content-type') || '';
if (contentType.startsWith('audio/')) {
// v4.3: direct binary stream
const blob = await resp.blob();
return { audio_url: URL.createObjectURL(blob), audio_format: 'mp3' };
}
// Legacy base64 JSON
const data = await resp.json();
if (data.error || !data.audio_data) {
throw new Error(data.error || 'No audio data');
}
// Cache into editorBlocks for future use
inst.blockData.audio_data = data.audio_data;
inst.blockData.audio_format = data.audio_format;
return data;
}
@@ -488,8 +493,13 @@ function ensureReaderAudioLoaded(inst) {
inst.audioLoadingPromise = (async () => {
const audioInfo = await fetchAudioForInstance(inst);
const audioBlob = base64ToBlob(audioInfo.audio_data, `audio/${audioInfo.audio_format || 'mp3'}`);
const audioUrl = URL.createObjectURL(audioBlob);
let audioUrl;
if (audioInfo.audio_url) {
audioUrl = audioInfo.audio_url; // v4.3 binary blob URL
} else {
const audioBlob = base64ToBlob(audioInfo.audio_data, `audio/${audioInfo.audio_format || 'mp3'}`);
audioUrl = URL.createObjectURL(audioBlob);
}
const audio = new Audio(audioUrl);
return new Promise((resolve, reject) => {