v4.3: file-based media storage + manual VACUUM maintenance
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user