chore: add local maintenance utilities
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
This commit is contained in:
parent
037f3dd74d
commit
b43bacc275
24
patch_changelog.py
Normal file
24
patch_changelog.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('CHANGELOG.md', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
new_entry = """## [1.22.0] — 2026-08-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Audiobook Export Modal.** Introduced a comprehensive GUI for exporting audiobooks directly from the Rehearser.
|
||||||
|
- **Granular Export Controls.** Users can now select specific chapters to export, define the output format (one MP3 per chapter vs. single book file), and configure custom server-side save locations.
|
||||||
|
- **Pacing Configuration.** Added UI settings to adjust pacing with precise pause durations between paragraphs and chapters.
|
||||||
|
- **Tone Warning Integration.** Warnings for out-of-bounds tone settings are now displayed within the export modal for safety before batch generation.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **UI Synchronization.** Fixed an issue where the Rehearser's green dots for cached audio lines would not synchronize reliably during navigation updates.
|
||||||
|
- **Bundle Stability.** Addressed a script hoisting issue in the production bundle that caused silent initialization failures and a "Loading..." stall.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
content = content.replace("---\n\n## [1.21.1]", "---\n\n" + new_entry + "## [1.21.1]")
|
||||||
|
|
||||||
|
with open('CHANGELOG.md', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
185
scratch/patch_audiobook.py
Normal file
185
scratch/patch_audiobook.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
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)
|
||||||
51
scratch/patch_sync_dots.py
Normal file
51
scratch/patch_sync_dots.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('static/js/rehearser.js', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 1. In buildScriptPage, use serverCachedLines
|
||||||
|
content = content.replace(
|
||||||
|
"const isCached = rehState.synthCache.has(i);",
|
||||||
|
"const isCached = rehState.synthCache.has(i) || (rehState.serverCachedLines && rehState.serverCachedLines.has(i));"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Modify _lineAudioSyncDots to populate serverCachedLines and remove scanId
|
||||||
|
old_sync_dots = """let _lineAudioSyncedFor = null; // `${book}::${lineCount}` — avoid re-running the same scan on every small re-render
|
||||||
|
async function _lineAudioSyncDots() {
|
||||||
|
const book = _lineAudioBookName();
|
||||||
|
const scanId = `${book}::${rehState.lines.length}`;
|
||||||
|
if (_lineAudioSyncedFor === scanId) return;
|
||||||
|
_lineAudioSyncedFor = scanId;
|
||||||
|
_ensureNarrator();"""
|
||||||
|
|
||||||
|
new_sync_dots = """let _lineAudioSyncedFor = null; // unused now, kept for compat
|
||||||
|
async function _lineAudioSyncDots() {
|
||||||
|
if (!rehState.serverCachedLines) rehState.serverCachedLines = new Set();
|
||||||
|
const book = _lineAudioBookName();
|
||||||
|
_ensureNarrator();"""
|
||||||
|
|
||||||
|
content = content.replace(old_sync_dots, new_sync_dots)
|
||||||
|
|
||||||
|
old_foreach = """ const { existing } = await r.json();
|
||||||
|
(existing || []).forEach(key => {
|
||||||
|
const idx = keyToIdx.get(key);
|
||||||
|
if (idx == null) return;
|
||||||
|
rehState.staleLines.delete(idx);
|
||||||
|
_markSynthDot(idx, 'ok');
|
||||||
|
});
|
||||||
|
} catch (_) { /* dots just stay as-is; playback still checks the disk cache lazily either way */ }"""
|
||||||
|
|
||||||
|
new_foreach = """ const { existing } = await r.json();
|
||||||
|
(existing || []).forEach(key => {
|
||||||
|
const idx = keyToIdx.get(key);
|
||||||
|
if (idx == null) return;
|
||||||
|
rehState.staleLines.delete(idx);
|
||||||
|
rehState.serverCachedLines.add(idx);
|
||||||
|
_markSynthDot(idx, 'ok');
|
||||||
|
});
|
||||||
|
} catch (_) { /* dots just stay as-is; playback still checks the disk cache lazily either way */ }"""
|
||||||
|
|
||||||
|
content = content.replace(old_foreach, new_foreach)
|
||||||
|
|
||||||
|
with open('static/js/rehearser.js', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
48
scratch/patch_sync_dots2.py
Normal file
48
scratch/patch_sync_dots2.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('static/js/rehearser.js', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 1. Revert buildScriptPage change
|
||||||
|
content = content.replace(
|
||||||
|
"const isCached = rehState.synthCache.has(i) || (rehState.serverCachedLines && rehState.serverCachedLines.has(i));",
|
||||||
|
"const isCached = rehState.synthCache.has(i);"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Revert serverCachedLines in _lineAudioSyncDots
|
||||||
|
old_sync_dots = """let _lineAudioSyncedFor = null; // unused now, kept for compat
|
||||||
|
async function _lineAudioSyncDots() {
|
||||||
|
if (!rehState.serverCachedLines) rehState.serverCachedLines = new Set();
|
||||||
|
const book = _lineAudioBookName();
|
||||||
|
_ensureNarrator();"""
|
||||||
|
|
||||||
|
new_sync_dots = """let _lineAudioSyncedFor = null; // unused now, kept for compat
|
||||||
|
async function _lineAudioSyncDots() {
|
||||||
|
const book = _lineAudioBookName();
|
||||||
|
_ensureNarrator();"""
|
||||||
|
|
||||||
|
content = content.replace(old_sync_dots, new_sync_dots)
|
||||||
|
|
||||||
|
old_foreach = """ const { existing } = await r.json();
|
||||||
|
(existing || []).forEach(key => {
|
||||||
|
const idx = keyToIdx.get(key);
|
||||||
|
if (idx == null) return;
|
||||||
|
rehState.staleLines.delete(idx);
|
||||||
|
rehState.serverCachedLines.add(idx);
|
||||||
|
_markSynthDot(idx, 'ok');
|
||||||
|
});
|
||||||
|
} catch (_) { /* dots just stay as-is; playback still checks the disk cache lazily either way */ }"""
|
||||||
|
|
||||||
|
new_foreach = """ const { existing } = await r.json();
|
||||||
|
(existing || []).forEach(key => {
|
||||||
|
const idx = keyToIdx.get(key);
|
||||||
|
if (idx == null) return;
|
||||||
|
rehState.staleLines.delete(idx);
|
||||||
|
_markSynthDot(idx, 'ok');
|
||||||
|
});
|
||||||
|
} catch (_) { /* dots just stay as-is; playback still checks the disk cache lazily either way */ }"""
|
||||||
|
|
||||||
|
content = content.replace(old_foreach, new_foreach)
|
||||||
|
|
||||||
|
with open('static/js/rehearser.js', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
22
scratch/test_bundle.js
Normal file
22
scratch/test_bundle.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const { JSDOM } = require("jsdom");
|
||||||
|
const dom = new JSDOM(`<!DOCTYPE html><html><body><div id="status-message"></div></body></html>`);
|
||||||
|
global.window = dom.window;
|
||||||
|
global.document = dom.window.document;
|
||||||
|
global.navigator = dom.window.navigator;
|
||||||
|
global.location = dom.window.location;
|
||||||
|
global.localStorage = { getItem: () => null, setItem: () => null, removeItem: () => null };
|
||||||
|
global.fetch = () => Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
||||||
|
global.$ = (id) => document.getElementById(id) || document.createElement('div');
|
||||||
|
global.toast = () => {};
|
||||||
|
global.escapeHtml = () => '';
|
||||||
|
global.escHtml = () => '';
|
||||||
|
global.debounce = () => {};
|
||||||
|
global.audiobookIsChapter = () => false;
|
||||||
|
global.initCloneSampleText = () => {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
require('../static/dist/main.min.js');
|
||||||
|
console.log("Bundle executed successfully without top-level errors!");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Bundle error:", e);
|
||||||
|
}
|
||||||
13
scratch/test_cache.py
Normal file
13
scratch/test_cache.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import json
|
||||||
|
|
||||||
|
book = "Hohlbein Hennen - DSA - Das Jahr des Greifen 03 - Die Amazone"
|
||||||
|
url = f"http://127.0.0.1:8010/api/line-audio/{urllib.parse.quote(book)}/check"
|
||||||
|
data = json.dumps({"keys": ["c79dbe84b9ed7b1780891ba905951bb6", "ee3559287060ae3530fb9d7f088ff9c8"]}).encode('utf-8')
|
||||||
|
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}, method='POST')
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as f:
|
||||||
|
print(f.read().decode('utf-8'))
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
67
scratch_paginate.js
Normal file
67
scratch_paginate.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// Printable content height inside a page (A4 minus vertical padding)
|
||||||
|
const PAGE_CONTENT = 1027; // 1123 − 56 − 40
|
||||||
|
const TITLE_SPACE = 64; // page-1 title block allowance
|
||||||
|
|
||||||
|
let pageNum = 0, page = null, used = 0;
|
||||||
|
let isFirstPage = true;
|
||||||
|
|
||||||
|
// Pre-scan for the first PDF page number to start numbering correctly
|
||||||
|
if (respectBreaks) {
|
||||||
|
for (const b of blocks) {
|
||||||
|
if (b.classList.contains('reh-block-pagebreak')) {
|
||||||
|
const i = parseInt(b.dataset.index);
|
||||||
|
const l = rehState.lines[i];
|
||||||
|
if (l && l.page) {
|
||||||
|
// If the pagebreak is for page N, the content before it is N or N-1 depending on how breaks are placed.
|
||||||
|
// Usually a page break marker MEANS "Start of Page N".
|
||||||
|
// If the first element is NOT a pagebreak, it means the content belongs to the page BEFORE the first pagebreak.
|
||||||
|
// We'll just set pageNum to l.page - 1 (or 1 if it's 1).
|
||||||
|
pageNum = Math.max(0, l.page - (b === blocks[0] ? 0 : 1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPage = (forcedNum) => {
|
||||||
|
if (forcedNum !== undefined) pageNum = forcedNum;
|
||||||
|
else pageNum++;
|
||||||
|
page = document.createElement('div');
|
||||||
|
page.className = 'reh-paper';
|
||||||
|
const num = document.createElement('div');
|
||||||
|
num.className = 'reh-paper-num';
|
||||||
|
num.textContent = pageNum + '.';
|
||||||
|
page.appendChild(num);
|
||||||
|
used = 0;
|
||||||
|
if (isFirstPage && titleEl) {
|
||||||
|
const t = titleEl.cloneNode(true);
|
||||||
|
t.style.display = '';
|
||||||
|
page.appendChild(t);
|
||||||
|
used += TITLE_SPACE;
|
||||||
|
}
|
||||||
|
isFirstPage = false;
|
||||||
|
wrap.appendChild(page);
|
||||||
|
};
|
||||||
|
newPage();
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
// PDF page-break marker: always start a new page here
|
||||||
|
if (respectBreaks && block.classList.contains('reh-block-pagebreak')) {
|
||||||
|
const i = parseInt(block.dataset.index);
|
||||||
|
const l = rehState.lines[i];
|
||||||
|
newPage(l && l.page ? l.page : undefined);
|
||||||
|
continue; // don't move the marker div into the page
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure block height (must be in DOM to measure → append then check)
|
||||||
|
page.appendChild(block);
|
||||||
|
const cs = getComputedStyle(block);
|
||||||
|
const h = block.offsetHeight + (parseFloat(cs.marginTop) || 0) + (parseFloat(cs.marginBottom) || 0);
|
||||||
|
|
||||||
|
// If adding this block overflows the page (and page already has content), move to a new page
|
||||||
|
if (!respectBreaks && used + h > PAGE_CONTENT && used > (isFirstPage ? TITLE_SPACE : 0)) {
|
||||||
|
newPage();
|
||||||
|
page.appendChild(block);
|
||||||
|
}
|
||||||
|
used += h;
|
||||||
|
}
|
||||||
49
scratchpad_prefetch.js
Normal file
49
scratchpad_prefetch.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
let _rehPrefetchRunning = false;
|
||||||
|
async function _rehPrefetchLines(fromIndex) {
|
||||||
|
if (_rehPrefetchRunning) return;
|
||||||
|
_rehPrefetchRunning = true;
|
||||||
|
try {
|
||||||
|
let prefetched = 0;
|
||||||
|
for (let i = fromIndex; i < rehState.lines.length && prefetched < 2; i++) {
|
||||||
|
if (!rehState.playing) break;
|
||||||
|
const l = rehState.lines[i];
|
||||||
|
if (!l || l.ignored || l.hidden || l.type === 'pagebreak') continue;
|
||||||
|
|
||||||
|
const isDialog = l.type === 'dialog';
|
||||||
|
const cast = isDialog ? (rehState.cast[l.speaker] || { voice: '' }) : null;
|
||||||
|
if (isDialog && (!cast.voice || cast.voice === 'me')) continue;
|
||||||
|
if (!isDialog && (!rehState.narratorVoice || !(l.text || '').trim() || rehState.skipDescriptions)) continue;
|
||||||
|
|
||||||
|
if (rehState.synthCache.has(i) && !rehState.staleLines.has(i)) {
|
||||||
|
prefetched++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const voice = isDialog ? cast.voice : rehState.narratorVoice;
|
||||||
|
const cleanTxt = stripMarkdown(l.text);
|
||||||
|
const instruct = isDialog ? _buildInstruct((cast.instruct || '').trim(), l.emotion, voice) : '';
|
||||||
|
const text = isDialog ? _rehInlineTone(cleanTxt, l.emotion) : cleanTxt;
|
||||||
|
|
||||||
|
const book = _lineAudioBookName();
|
||||||
|
const cacheKey = await _lineAudioCacheKey(text, voice, instruct);
|
||||||
|
let blob = await _lineAudioCacheGet(book, cacheKey);
|
||||||
|
|
||||||
|
if (!blob) {
|
||||||
|
try {
|
||||||
|
blob = await fetchTtsPreviewBlob(voice, text, 'wav', instruct, _ttsBackendForVoice(voice, rehState.backend));
|
||||||
|
_lineAudioCachePut(book, cacheKey, blob);
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('Prefetch failed for line', i, e);
|
||||||
|
continue; // skip counting as prefetched if it failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rehState.synthCache.set(i, blob);
|
||||||
|
rehState.staleLines.delete(i);
|
||||||
|
_markSynthDot(i, 'ok');
|
||||||
|
prefetched++;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_rehPrefetchRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
static/js/dump_state.js
Normal file
15
static/js/dump_state.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
(function() {
|
||||||
|
const data = {
|
||||||
|
linesLength: window.rehState ? (window.rehState.lines || []).length : 'no rehState',
|
||||||
|
linesSample: window.rehState ? (window.rehState.lines || []).slice(0, 5) : [],
|
||||||
|
a4Display: document.getElementById('reh-a4-page') ? document.getElementById('reh-a4-page').style.display : 'no a4',
|
||||||
|
wrapChildrenCount: document.querySelector('.reh-page-wrap') ? document.querySelector('.reh-page-wrap').children.length : 'no wrap',
|
||||||
|
paperHeights: Array.from(document.querySelectorAll('.reh-paper')).map(p => p.offsetHeight),
|
||||||
|
paperChildrenCount: Array.from(document.querySelectorAll('.reh-paper')).map(p => p.children.length),
|
||||||
|
scriptLinesChildrenCount: document.getElementById('reh-script-lines') ? document.getElementById('reh-script-lines').children.length : 'no script-lines',
|
||||||
|
pageMode: window._pageMode || 'unknown',
|
||||||
|
skipDesc: window.rehState ? window.rehState.skipDescriptions : 'unknown'
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
alert(JSON.stringify(data, null, 2));
|
||||||
|
})();
|
||||||
30
workflows/comfyui-character-portrait.json.bak
Normal file
30
workflows/comfyui-character-portrait.json.bak
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"1": {
|
||||||
|
"class_type": "CheckpointLoaderSimple",
|
||||||
|
"inputs": {"ckpt_name": "flux1-dev-fp8.safetensors"}
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"class_type": "CLIPTextEncode",
|
||||||
|
"inputs": {"text": "", "clip": ["1", 1]}
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"class_type": "CLIPTextEncode",
|
||||||
|
"inputs": {"text": "blurry, low resolution, distorted face, duplicate person, extra limbs, malformed hands, cropped head, readable text, logo, watermark", "clip": ["1", 1]}
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"class_type": "EmptyLatentImage",
|
||||||
|
"inputs": {"width": 1024, "height": 1024, "batch_size": 1}
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"class_type": "KSampler",
|
||||||
|
"inputs": {"seed": 0, "steps": 28, "cfg": 3.5, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0, "model": ["1", 0], "positive": ["2", 0], "negative": ["3", 0], "latent_image": ["4", 0]}
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"class_type": "VAEDecode",
|
||||||
|
"inputs": {"samples": ["5", 0], "vae": ["1", 2]}
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"class_type": "SaveImage",
|
||||||
|
"inputs": {"filename_prefix": "voice-creator/portrait", "images": ["6", 0]}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user