diff --git a/abogen/web/routes.py b/abogen/web/routes.py index 4668642..dfd85ca 100644 --- a/abogen/web/routes.py +++ b/abogen/web/routes.py @@ -1107,6 +1107,13 @@ def _sync_pronunciation_overrides(pending: PendingJob) -> None: def _refresh_entity_summary(pending: PendingJob, chapters: Iterable[Mapping[str, Any]]) -> None: + settings = _load_settings() + if not bool(settings.get("enable_entity_recognition", True)): + pending.entity_summary = {} + pending.entity_cache_key = "" + pending.pronunciation_overrides = pending.pronunciation_overrides or [] + return + language = pending.language or "en" result = extract_entities(chapters, language=language) summary = dict(result.summary) @@ -1289,12 +1296,15 @@ def _search_manual_override_candidates(pending: PendingJob, query: str, *, limit def _pending_entities_payload(pending: PendingJob) -> Dict[str, Any]: + settings = _load_settings() + recognition_enabled = bool(settings.get("enable_entity_recognition", True)) return { "summary": pending.entity_summary or {}, "manual_overrides": pending.manual_overrides or [], "pronunciation_overrides": pending.pronunciation_overrides or [], "cache_key": pending.entity_cache_key, "language": pending.language or "en", + "recognition_enabled": recognition_enabled, } @@ -1681,6 +1691,7 @@ BOOLEAN_SETTINGS = { "merge_chapters_at_end", "save_as_project", "generate_epub3", + "enable_entity_recognition", } FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay"} @@ -1707,6 +1718,7 @@ def _settings_defaults() -> Dict[str, Any]: "chapter_intro_delay": 0.5, "max_subtitle_words": 50, "chunk_level": "paragraph", + "enable_entity_recognition": True, "generate_epub3": False, "speaker_analysis_threshold": _DEFAULT_ANALYSIS_THRESHOLD, "speaker_pronunciation_sentence": "This is {{name}} speaking.", diff --git a/abogen/web/static/prepare.js b/abogen/web/static/prepare.js index a949bac..2950b48 100644 --- a/abogen/web/static/prepare.js +++ b/abogen/web/static/prepare.js @@ -15,9 +15,6 @@ const initPrepare = (root = document) => { const uploadModal = document.querySelector('[data-role="new-job-modal"]') || document.querySelector('[data-role="upload-modal"]'); - const wizardPreviousButtons = Array.from( - document.querySelectorAll('[data-role="wizard-previous"], [data-role="wizard-back"]') - ); const openUploadTriggers = Array.from(document.querySelectorAll('[data-role="open-upload-modal"]')); const showWizardModal = () => { @@ -59,22 +56,6 @@ const initPrepare = (root = document) => { document.addEventListener("upload-modal:close", showWizardModal); } - wizardPreviousButtons.forEach((button) => { - if (!button || button.dataset.prepareBackBound === "true") { - return; - } - const targetStep = (button.dataset.targetStep || "").toLowerCase(); - if (targetStep && targetStep !== "book") { - return; - } - button.dataset.prepareBackBound = "true"; - button.addEventListener("click", (event) => { - event.preventDefault(); - hideWizardModal(); - triggerUploadModal(); - }); - }); - const parseJSONScript = (id) => { const el = document.getElementById(id); if (!el) return null; @@ -1083,6 +1064,7 @@ const initPrepare = (root = document) => { const languageCode = form.dataset.language || "en"; const defaultSpeed = form.dataset.speed || "1.0"; const useGpuDefault = form.dataset.useGpu || "true"; + let entitiesEnabled = form.dataset.entitiesEnabled !== "false"; const entityState = { summary: entitySummaryData && typeof entitySummaryData === "object" ? entitySummaryData : {}, @@ -1177,6 +1159,18 @@ const initPrepare = (root = document) => { function renderEntitySummary() { if (!entityListNode) return; + if (!entitiesEnabled) { + if (entityStatsNode) { + entityStatsNode.textContent = "Entity recognition is turned off in Settings."; + } + entityListNode.innerHTML = ""; + const emptyItem = document.createElement("li"); + emptyItem.className = "entity-summary__empty"; + emptyItem.textContent = "Enable entity recognition to populate detected entities."; + entityListNode.appendChild(emptyItem); + return; + } + const summary = entityState.summary || {}; const stats = summary.stats || {}; const errors = Array.isArray(summary.errors) ? summary.errors : []; @@ -1207,7 +1201,7 @@ const initPrepare = (root = document) => { return; } - entries.slice(0, 120).forEach((entity) => { + entries.forEach((entity) => { const item = cloneTemplate(entityRowTemplate); if (!item) return; const normalized = entity.normalized || entity.label || entity.token || ""; @@ -1385,6 +1379,14 @@ const initPrepare = (root = document) => { if (typeof payload.cache_key === "string") { entityState.cacheKey = payload.cache_key; } + if (Object.prototype.hasOwnProperty.call(payload, "recognition_enabled")) { + entitiesEnabled = payload.recognition_enabled !== false; + form.dataset.entitiesEnabled = entitiesEnabled ? "true" : "false"; + if (entitiesRefreshButton) { + entitiesRefreshButton.disabled = !entitiesEnabled; + entitiesRefreshButton.setAttribute("aria-disabled", entitiesEnabled ? "false" : "true"); + } + } } if (options.highlightId) { highlightedOverrideId = options.highlightId; diff --git a/abogen/web/static/wizard.js b/abogen/web/static/wizard.js index e9e94d1..431409d 100644 --- a/abogen/web/static/wizard.js +++ b/abogen/web/static/wizard.js @@ -120,10 +120,18 @@ const updateStepIndicators = (modal, activeStep) => { const step = normalizeStep(indicator.dataset.step || "book"); indicator.classList.remove("is-active", "is-complete"); const index = STEP_ORDER.indexOf(step); - if (index < activeIndex) { - indicator.classList.add("is-complete"); - } else if (index === activeIndex) { - indicator.classList.add("is-active"); + const isComplete = index > -1 && index < activeIndex; + const isActive = index === activeIndex; + indicator.classList.toggle("is-complete", isComplete); + indicator.classList.toggle("is-active", isActive); + if (indicator instanceof HTMLButtonElement) { + indicator.disabled = !(isComplete); + indicator.setAttribute("aria-current", isActive ? "step" : "false"); + if (isComplete) { + indicator.dataset.state = "clickable"; + } else { + delete indicator.dataset.state; + } } }); }; @@ -359,27 +367,42 @@ const handleCancel = async (button) => { dispatchWizardEvent(modal, "cancel", { pendingId }); }; -const handleBackToStep = (button) => { +const navigateToWizardStep = (targetStep, pendingOverride) => { const modal = ensureModalRef(); - if (!modal) return; - const targetStep = normalizeStep(button.dataset.targetStep || "book"); - if (targetStep === "book") { - return; // handled by prepare.js to reopen upload modal + if (!modal || wizardState.submitting) { + return; } - const pendingId = button.dataset.pendingId || modal.dataset.pendingId || ""; + const normalizedTarget = normalizeStep(targetStep || "book"); + const currentStep = modal.dataset.step ? normalizeStep(modal.dataset.step) : "book"; + if (normalizedTarget === currentStep) { + return; + } + const pendingId = pendingOverride || modal.dataset.pendingId || ""; const template = modal.dataset.prepareUrlTemplate || ""; if (!pendingId || !template) { return; } const url = new URL(template.replace("__pending__", encodeURIComponent(pendingId)), window.location.origin); - url.searchParams.set("step", targetStep); + url.searchParams.set("step", normalizedTarget); url.searchParams.set("format", "json"); requestWizardStep(url.toString(), { method: "GET" }); }; +const handleBackToStep = (button) => { + const targetStep = normalizeStep(button.dataset.targetStep || "book"); + navigateToWizardStep(targetStep, button.dataset.pendingId); +}; + const handleWizardClick = (event) => { const modal = ensureModalRef(); if (!modal) return; + const closeTarget = event.target.closest('[data-role="new-job-modal-close"]'); + if (closeTarget) { + event.preventDefault(); + event.stopPropagation(); + handleCancel(closeTarget); + return; + } const cancelButton = event.target.closest('[data-role="wizard-cancel"]'); if (cancelButton) { event.preventDefault(); @@ -390,10 +413,17 @@ const handleWizardClick = (event) => { const backButton = event.target.closest('[data-role="wizard-back"]'); if (backButton) { const targetStep = normalizeStep(backButton.dataset.targetStep || "book"); - if (targetStep !== "book") { + event.preventDefault(); + event.stopPropagation(); + handleBackToStep(backButton); + return; + } + const indicator = event.target.closest('[data-role="wizard-step-indicator"]'); + if (indicator instanceof HTMLButtonElement) { + if (indicator.classList.contains("is-complete")) { event.preventDefault(); event.stopPropagation(); - handleBackToStep(backButton); + navigateToWizardStep(indicator.dataset.step || "book"); } } }; diff --git a/abogen/web/templates/partials/new_job_step_book.html b/abogen/web/templates/partials/new_job_step_book.html index ee8fe86..680c942 100644 --- a/abogen/web/templates/partials/new_job_step_book.html +++ b/abogen/web/templates/partials/new_job_step_book.html @@ -144,46 +144,42 @@
Current file: {{ pending.original_filename }}
- {% endif %} -Current file: {{ pending.original_filename }}
+ {% endif %}Creates a synchronized EPUB alongside audio output.
-Creates a synchronized EPUB alongside audio output.
Reuse a saved roster to keep character voices consistent.
Paragraphs work well for long-form narration; sentences give finer subtitle sync.
-Entities appearing less often fall back to the narrator voice.
-Paragraphs work well for long-form narration; sentences give finer subtitle sync.
+Entities appearing less often fall back to the narrator voice.