tts-voice-creator-clone-and.../scratch/patch_audiobook.py
mARTin-B78 b43bacc275
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
chore: add local maintenance utilities
2026-09-09 07:53:13 +02:00

186 lines
8.3 KiB
Python

import re
with open('static/js/audiobook.js', 'r') as f:
content = f.read()
# 1. Change listener
content = content.replace(
"$('reh-tb-audiobook')?.addEventListener('click', audiobookExport);",
"$('reh-tb-audiobook')?.addEventListener('click', audiobookExportDialog);"
)
# 2. Rename audiobookExport to accept opts
content = content.replace(
"async function audiobookExport() {",
"async function audiobookExport(opts = {}) {"
)
# 3. Handle activeBuckets inside audiobookExport
content = content.replace(
"const allIdx = buckets.flatMap(b => b.idx);",
"""let activeBuckets = buckets;
if (opts.selectedChapterIndices) {
activeBuckets = buckets.filter((_, i) => opts.selectedChapterIndices.includes(i));
}
const allIdx = activeBuckets.flatMap(b => b.idx);"""
)
# 4. Modify the loop that creates files
loop_find = """ for (let c = 0; c < buckets.length; c++) {
const blobs = buckets[c].idx.map(i => wavClips.get(i)).filter(Boolean);"""
loop_replace = """ if (opts.format === 'single') {
let masterBlobs = [];
for (let c = 0; c < activeBuckets.length; c++) {
const blobs = activeBuckets[c].idx.map(i => wavClips.get(i)).filter(Boolean);
if (!blobs.length) continue;
if (encMsg) encMsg.textContent = `Merging chapter ${c + 1} / ${activeBuckets.length}…`;
let mergedWav = await mergeWavBlobs(blobs, pauses.paragraphMs);
if (c > 0 && activeBuckets[c].title && (pauses.chapterMs > 0 || _abChapterSfxDataUrl())) {
const ref = _parseWavBytes(new Uint8Array(await mergedWav.arrayBuffer())).fmt;
const lead = [];
const sfx = await _abChapterSfxWavBlob(ref);
if (sfx) lead.push(sfx);
if (pauses.chapterMs > 0) lead.push(_buildWavBlob(_silencePcmBytes(pauses.chapterMs, ref), ref));
if (lead.length) mergedWav = await mergeWavBlobs([...lead, mergedWav]);
}
masterBlobs.push(mergedWav);
}
if (masterBlobs.length) {
if (encMsg) encMsg.textContent = `Encoding full book to MP3…`;
let finalWav = await mergeWavBlobs(masterBlobs, 0);
let blob = finalWav;
try {
const encResp = await fetch('/api/audio/encode-mp3', { method: 'POST', body: finalWav });
if (encResp.ok) blob = await encResp.blob();
} catch (e) {}
const ext = blob === finalWav ? 'wav' : 'mp3';
const name = `${title}.${ext}`;
if (typeof readerDownload === 'function') readerDownload(blob, name);
try {
let exportUrl = `/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}`;
if (opts.folder) exportUrl += `?folder=${encodeURIComponent(opts.folder)}`;
const r = await fetch(exportUrl, { method: 'POST', body: blob });
if (r.ok) savedFiles.push({ name, url: exportUrl });
} catch (e) {}
files++;
}
} else {
for (let c = 0; c < activeBuckets.length; c++) {
const blobs = activeBuckets[c].idx.map(i => wavClips.get(i)).filter(Boolean);"""
content = content.replace(loop_find, loop_replace)
# 5. Fix references inside the else block
content = content.replace(
"encMsg.textContent = `Merging chapter ${c + 1} / ${buckets.length}…`;",
"encMsg.textContent = `Merging chapter ${c + 1} / ${activeBuckets.length}…`;"
)
content = content.replace(
"encMsg.textContent = `Encoding chapter ${c + 1} / ${buckets.length}…`;",
"encMsg.textContent = `Encoding chapter ${c + 1} / ${activeBuckets.length}…`;"
)
content = content.replace(
"if (c > 0 && buckets[c].title && (pauses.chapterMs > 0 || _abChapterSfxDataUrl())) {",
"if (c > 0 && activeBuckets[c].title && (pauses.chapterMs > 0 || _abChapterSfxDataUrl())) {"
)
content = content.replace(
"const ch = buckets[c].title ? ' ' + readerSafeName(buckets[c].title) : '';\n const name = (realChapters || buckets.length > 1) ? `${title} - ${String(c + 1).padStart(2, '0')}${ch}.${ext}` : `${title}.${ext}`;",
"const ch = activeBuckets[c].title ? ' ' + readerSafeName(activeBuckets[c].title) : '';\n const name = (realChapters || activeBuckets.length > 1) ? `${title} - ${String(c + 1).padStart(2, '0')}${ch}.${ext}` : `${title}.${ext}`;"
)
content = content.replace(
"if (r.ok) savedFiles.push({ name, url: `/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}` });",
"if (r.ok) savedFiles.push({ name, url: exportUrl });"
)
# add folder query param in else block
content = content.replace(
"const r = await fetch(`/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}`, { method: 'POST', body: blob });",
"let exportUrl = `/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}`;\n if (opts.folder) exportUrl += `?folder=${encodeURIComponent(opts.folder)}`;\n const r = await fetch(exportUrl, { method: 'POST', body: blob });"
)
# close the else block
close_else = """ files++;
await new Promise(r => setTimeout(r, 400)); // breathing room between chunk encode/uploads
}
} // end else
"""
content = content.replace(
"await new Promise(r => setTimeout(r, 400)); // breathing room between chunk encode/uploads\n }",
close_else
)
# 6. Add audiobookExportDialog
dialog_code = """
window.audiobookExportDialog = function() {
if (_audiobook.running) return;
if (!window.rehState || !(rehState.lines || []).length) { toast('Open a script in the rehearser first', 'error'); return; }
if (!rehState.backend) { toast('Select a TTS backend in the rehearser first', 'error'); return; }
const speakable = i => {
const l = rehState.lines[i];
if (!l || l.ignored || l.hidden) return false;
if (l.type === 'dialog') { const c = rehState.cast[l.speaker]; return !!(c && c.voice && c.voice !== 'me'); }
return !!(rehState.narratorVoice && (l.text || '').trim());
};
const buckets = [];
let cur = null;
rehState.lines.forEach((l, i) => {
if (audiobookIsChapter(l)) { cur = { title: (typeof stripMarkdown === 'function' ? stripMarkdown(l.text) : l.text).trim().slice(0, 50), idx: [] }; buckets.push(cur); }
if (speakable(i)) { if (!cur) { cur = { title: '', idx: [] }; buckets.push(cur); } cur.idx.push(i); }
});
const listEl = document.getElementById('ab-export-chapter-list');
if (listEl) {
listEl.innerHTML = '';
const countEl = document.getElementById('ab-export-chapter-count');
if (countEl) countEl.textContent = buckets.length;
buckets.forEach((b, i) => {
const lbl = document.createElement('label');
lbl.style.display = 'block';
lbl.style.marginBottom = '4px';
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.checked = true;
chk.value = i;
chk.className = 'ab-export-chapter-cb';
lbl.appendChild(chk);
lbl.appendChild(document.createTextNode(' Chapter ' + (i+1) + (b.title ? ': ' + b.title : '')));
listEl.appendChild(lbl);
});
}
const warn = document.getElementById('reh-tone-warn');
const slot = document.getElementById('ab-export-tone-warn-slot');
if (warn && slot) {
warn.hidden = false;
slot.appendChild(warn);
}
if (typeof _abInitPauseUI === 'function') _abInitPauseUI();
const modal = document.getElementById('audiobook-export-modal');
if (modal) modal.hidden = false;
};
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('ab-export-modal-close')?.addEventListener('click', () => document.getElementById('audiobook-export-modal').hidden = true);
document.getElementById('ab-export-modal-cancel')?.addEventListener('click', () => document.getElementById('audiobook-export-modal').hidden = true);
document.getElementById('ab-export-modal-start')?.addEventListener('click', () => {
document.getElementById('audiobook-export-modal').hidden = true;
const formatEl = document.querySelector('input[name="ab_export_format"]:checked');
const format = formatEl ? formatEl.value : 'chapter';
const folderEl = document.getElementById('ab-export-server-folder');
const folder = folderEl ? folderEl.value : '';
const checkboxes = Array.from(document.querySelectorAll('.ab-export-chapter-cb'));
const selectedChapterIndices = checkboxes.filter(cb => cb.checked).map(cb => parseInt(cb.value, 10));
audiobookExport({ format, selectedChapterIndices, folder });
});
});
"""
content += dialog_code
with open('static/js/audiobook.js', 'w') as f:
f.write(content)