mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 21:50:28 +02:00
feat: Implement entity recognition settings and UI updates across multiple components
This commit is contained in:
@@ -1107,6 +1107,13 @@ def _sync_pronunciation_overrides(pending: PendingJob) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _refresh_entity_summary(pending: PendingJob, chapters: Iterable[Mapping[str, Any]]) -> 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"
|
language = pending.language or "en"
|
||||||
result = extract_entities(chapters, language=language)
|
result = extract_entities(chapters, language=language)
|
||||||
summary = dict(result.summary)
|
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]:
|
def _pending_entities_payload(pending: PendingJob) -> Dict[str, Any]:
|
||||||
|
settings = _load_settings()
|
||||||
|
recognition_enabled = bool(settings.get("enable_entity_recognition", True))
|
||||||
return {
|
return {
|
||||||
"summary": pending.entity_summary or {},
|
"summary": pending.entity_summary or {},
|
||||||
"manual_overrides": pending.manual_overrides or [],
|
"manual_overrides": pending.manual_overrides or [],
|
||||||
"pronunciation_overrides": pending.pronunciation_overrides or [],
|
"pronunciation_overrides": pending.pronunciation_overrides or [],
|
||||||
"cache_key": pending.entity_cache_key,
|
"cache_key": pending.entity_cache_key,
|
||||||
"language": pending.language or "en",
|
"language": pending.language or "en",
|
||||||
|
"recognition_enabled": recognition_enabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1681,6 +1691,7 @@ BOOLEAN_SETTINGS = {
|
|||||||
"merge_chapters_at_end",
|
"merge_chapters_at_end",
|
||||||
"save_as_project",
|
"save_as_project",
|
||||||
"generate_epub3",
|
"generate_epub3",
|
||||||
|
"enable_entity_recognition",
|
||||||
}
|
}
|
||||||
|
|
||||||
FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay"}
|
FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay"}
|
||||||
@@ -1707,6 +1718,7 @@ def _settings_defaults() -> Dict[str, Any]:
|
|||||||
"chapter_intro_delay": 0.5,
|
"chapter_intro_delay": 0.5,
|
||||||
"max_subtitle_words": 50,
|
"max_subtitle_words": 50,
|
||||||
"chunk_level": "paragraph",
|
"chunk_level": "paragraph",
|
||||||
|
"enable_entity_recognition": True,
|
||||||
"generate_epub3": False,
|
"generate_epub3": False,
|
||||||
"speaker_analysis_threshold": _DEFAULT_ANALYSIS_THRESHOLD,
|
"speaker_analysis_threshold": _DEFAULT_ANALYSIS_THRESHOLD,
|
||||||
"speaker_pronunciation_sentence": "This is {{name}} speaking.",
|
"speaker_pronunciation_sentence": "This is {{name}} speaking.",
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ const initPrepare = (root = document) => {
|
|||||||
const uploadModal =
|
const uploadModal =
|
||||||
document.querySelector('[data-role="new-job-modal"]') ||
|
document.querySelector('[data-role="new-job-modal"]') ||
|
||||||
document.querySelector('[data-role="upload-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 openUploadTriggers = Array.from(document.querySelectorAll('[data-role="open-upload-modal"]'));
|
||||||
|
|
||||||
const showWizardModal = () => {
|
const showWizardModal = () => {
|
||||||
@@ -59,22 +56,6 @@ const initPrepare = (root = document) => {
|
|||||||
document.addEventListener("upload-modal:close", showWizardModal);
|
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 parseJSONScript = (id) => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (!el) return null;
|
if (!el) return null;
|
||||||
@@ -1083,6 +1064,7 @@ const initPrepare = (root = document) => {
|
|||||||
const languageCode = form.dataset.language || "en";
|
const languageCode = form.dataset.language || "en";
|
||||||
const defaultSpeed = form.dataset.speed || "1.0";
|
const defaultSpeed = form.dataset.speed || "1.0";
|
||||||
const useGpuDefault = form.dataset.useGpu || "true";
|
const useGpuDefault = form.dataset.useGpu || "true";
|
||||||
|
let entitiesEnabled = form.dataset.entitiesEnabled !== "false";
|
||||||
|
|
||||||
const entityState = {
|
const entityState = {
|
||||||
summary: entitySummaryData && typeof entitySummaryData === "object" ? entitySummaryData : {},
|
summary: entitySummaryData && typeof entitySummaryData === "object" ? entitySummaryData : {},
|
||||||
@@ -1177,6 +1159,18 @@ const initPrepare = (root = document) => {
|
|||||||
|
|
||||||
function renderEntitySummary() {
|
function renderEntitySummary() {
|
||||||
if (!entityListNode) return;
|
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 summary = entityState.summary || {};
|
||||||
const stats = summary.stats || {};
|
const stats = summary.stats || {};
|
||||||
const errors = Array.isArray(summary.errors) ? summary.errors : [];
|
const errors = Array.isArray(summary.errors) ? summary.errors : [];
|
||||||
@@ -1207,7 +1201,7 @@ const initPrepare = (root = document) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.slice(0, 120).forEach((entity) => {
|
entries.forEach((entity) => {
|
||||||
const item = cloneTemplate(entityRowTemplate);
|
const item = cloneTemplate(entityRowTemplate);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const normalized = entity.normalized || entity.label || entity.token || "";
|
const normalized = entity.normalized || entity.label || entity.token || "";
|
||||||
@@ -1385,6 +1379,14 @@ const initPrepare = (root = document) => {
|
|||||||
if (typeof payload.cache_key === "string") {
|
if (typeof payload.cache_key === "string") {
|
||||||
entityState.cacheKey = payload.cache_key;
|
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) {
|
if (options.highlightId) {
|
||||||
highlightedOverrideId = options.highlightId;
|
highlightedOverrideId = options.highlightId;
|
||||||
|
|||||||
+43
-13
@@ -120,10 +120,18 @@ const updateStepIndicators = (modal, activeStep) => {
|
|||||||
const step = normalizeStep(indicator.dataset.step || "book");
|
const step = normalizeStep(indicator.dataset.step || "book");
|
||||||
indicator.classList.remove("is-active", "is-complete");
|
indicator.classList.remove("is-active", "is-complete");
|
||||||
const index = STEP_ORDER.indexOf(step);
|
const index = STEP_ORDER.indexOf(step);
|
||||||
if (index < activeIndex) {
|
const isComplete = index > -1 && index < activeIndex;
|
||||||
indicator.classList.add("is-complete");
|
const isActive = index === activeIndex;
|
||||||
} else if (index === activeIndex) {
|
indicator.classList.toggle("is-complete", isComplete);
|
||||||
indicator.classList.add("is-active");
|
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 });
|
dispatchWizardEvent(modal, "cancel", { pendingId });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackToStep = (button) => {
|
const navigateToWizardStep = (targetStep, pendingOverride) => {
|
||||||
const modal = ensureModalRef();
|
const modal = ensureModalRef();
|
||||||
if (!modal) return;
|
if (!modal || wizardState.submitting) {
|
||||||
const targetStep = normalizeStep(button.dataset.targetStep || "book");
|
return;
|
||||||
if (targetStep === "book") {
|
|
||||||
return; // handled by prepare.js to reopen upload modal
|
|
||||||
}
|
}
|
||||||
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 || "";
|
const template = modal.dataset.prepareUrlTemplate || "";
|
||||||
if (!pendingId || !template) {
|
if (!pendingId || !template) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = new URL(template.replace("__pending__", encodeURIComponent(pendingId)), window.location.origin);
|
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");
|
url.searchParams.set("format", "json");
|
||||||
requestWizardStep(url.toString(), { method: "GET" });
|
requestWizardStep(url.toString(), { method: "GET" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBackToStep = (button) => {
|
||||||
|
const targetStep = normalizeStep(button.dataset.targetStep || "book");
|
||||||
|
navigateToWizardStep(targetStep, button.dataset.pendingId);
|
||||||
|
};
|
||||||
|
|
||||||
const handleWizardClick = (event) => {
|
const handleWizardClick = (event) => {
|
||||||
const modal = ensureModalRef();
|
const modal = ensureModalRef();
|
||||||
if (!modal) return;
|
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"]');
|
const cancelButton = event.target.closest('[data-role="wizard-cancel"]');
|
||||||
if (cancelButton) {
|
if (cancelButton) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -390,10 +413,17 @@ const handleWizardClick = (event) => {
|
|||||||
const backButton = event.target.closest('[data-role="wizard-back"]');
|
const backButton = event.target.closest('[data-role="wizard-back"]');
|
||||||
if (backButton) {
|
if (backButton) {
|
||||||
const targetStep = normalizeStep(backButton.dataset.targetStep || "book");
|
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.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleBackToStep(backButton);
|
navigateToWizardStep(indicator.dataset.step || "book");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -144,46 +144,42 @@
|
|||||||
<div class="modal__body wizard-card__body upload-form__sections">
|
<div class="modal__body wizard-card__body upload-form__sections">
|
||||||
<section class="form-section">
|
<section class="form-section">
|
||||||
<h3 class="form-section__title">Manuscript & subtitles</h3>
|
<h3 class="form-section__title">Manuscript & subtitles</h3>
|
||||||
<div class="form-section__layout">
|
<div class="field-grid field-grid--compact">
|
||||||
<div class="field-grid field-grid--two">
|
<div class="field field--file field--span-2">
|
||||||
<div class="field field--file">
|
<label for="source_file">Source file</label>
|
||||||
<label for="source_file">Source file</label>
|
<input type="file" id="source_file" name="source_file" accept=".txt,.pdf,.epub,.md,.markdown" {{ 'disabled' if readonly else '' }}>
|
||||||
<input type="file" id="source_file" name="source_file" accept=".txt,.pdf,.epub,.md,.markdown" {{ 'disabled' if readonly else '' }}>
|
{% if pending %}
|
||||||
{% if pending %}
|
<p class="hint">Current file: {{ pending.original_filename }}</p>
|
||||||
<p class="hint">Current file: {{ pending.original_filename }}</p>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="language">Language</label>
|
|
||||||
<select id="language" name="language" {{ 'disabled' if readonly else '' }}>
|
|
||||||
{% for key, label in options.languages.items() %}
|
|
||||||
<option value="{{ key }}" {% if key == language_value %}selected{% endif %}>{{ label }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field-grid field-grid--two">
|
<div class="field">
|
||||||
<div class="field">
|
<label for="language">Language</label>
|
||||||
<label for="subtitle_mode">Subtitle mode</label>
|
<select id="language" name="language" {{ 'disabled' if readonly else '' }}>
|
||||||
<select id="subtitle_mode" name="subtitle_mode" {{ 'disabled' if readonly else '' }}>
|
{% for key, label in options.languages.items() %}
|
||||||
{% set subtitle_options = ['Disabled', 'Sentence', 'Sentence + Comma', 'Sentence + Highlighting'] %}
|
<option value="{{ key }}" {% if key == language_value %}selected{% endif %}>{{ label }}</option>
|
||||||
{% for option in subtitle_options %}
|
{% endfor %}
|
||||||
<option value="{{ option }}" {% if subtitle_value == option %}selected{% endif %}>{{ option }}</option>
|
</select>
|
||||||
{% endfor %}
|
</div>
|
||||||
{% for i in range(1, 11) %}
|
<div class="field">
|
||||||
{% set label = i ~ ' ' ~ ('word' if i == 1 else 'words') %}
|
<label for="subtitle_mode">Subtitle mode</label>
|
||||||
<option value="{{ label }}" {% if subtitle_value == label %}selected{% endif %}>{{ label }}</option>
|
<select id="subtitle_mode" name="subtitle_mode" {{ 'disabled' if readonly else '' }}>
|
||||||
{% endfor %}
|
{% set subtitle_options = ['Disabled', 'Sentence', 'Sentence + Comma', 'Sentence + Highlighting'] %}
|
||||||
</select>
|
{% for option in subtitle_options %}
|
||||||
</div>
|
<option value="{{ option }}" {% if subtitle_value == option %}selected{% endif %}>{{ option }}</option>
|
||||||
<div class="field field--stack">
|
{% endfor %}
|
||||||
<span class="field__caption">Additional outputs</span>
|
{% for i in range(1, 11) %}
|
||||||
<label class="toggle-pill">
|
{% set label = i ~ ' ' ~ ('word' if i == 1 else 'words') %}
|
||||||
<input type="checkbox" name="generate_epub3" value="true" {% if generate_epub3 %}checked{% endif %} {{ 'disabled' if readonly else '' }}>
|
<option value="{{ label }}" {% if subtitle_value == label %}selected{% endif %}>{{ label }}</option>
|
||||||
<span>Generate EPUB 3 (experimental)</span>
|
{% endfor %}
|
||||||
</label>
|
</select>
|
||||||
<p class="hint">Creates a synchronized EPUB alongside audio output.</p>
|
</div>
|
||||||
</div>
|
<div class="field field--stack field--span-2">
|
||||||
|
<span class="field__caption">Additional outputs</span>
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="checkbox" name="generate_epub3" value="true" {% if generate_epub3 %}checked{% endif %} {{ 'disabled' if readonly else '' }}>
|
||||||
|
<span>Generate EPUB 3 (experimental)</span>
|
||||||
|
</label>
|
||||||
|
<p class="hint">Creates a synchronized EPUB alongside audio output.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -238,8 +234,8 @@
|
|||||||
|
|
||||||
<section class="form-section">
|
<section class="form-section">
|
||||||
<h3 class="form-section__title">Entities & casting</h3>
|
<h3 class="form-section__title">Entities & casting</h3>
|
||||||
<div class="form-section__layout">
|
<div class="field-grid field-grid--compact">
|
||||||
<div class="field field--stack">
|
<div class="field field--stack field--span-2">
|
||||||
<label for="speaker_config">Speaker preset</label>
|
<label for="speaker_config">Speaker preset</label>
|
||||||
<select id="speaker_config" name="speaker_config" {{ 'disabled' if readonly else '' }}>
|
<select id="speaker_config" name="speaker_config" {{ 'disabled' if readonly else '' }}>
|
||||||
<option value="">None</option>
|
<option value="">None</option>
|
||||||
@@ -249,21 +245,19 @@
|
|||||||
</select>
|
</select>
|
||||||
<p class="hint">Reuse a saved roster to keep character voices consistent.</p>
|
<p class="hint">Reuse a saved roster to keep character voices consistent.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-grid field-grid--two">
|
<div class="field">
|
||||||
<div class="field">
|
<label for="chunk_level">Chunk granularity</label>
|
||||||
<label for="chunk_level">Chunk granularity</label>
|
<select id="chunk_level" name="chunk_level" {{ 'disabled' if readonly else '' }}>
|
||||||
<select id="chunk_level" name="chunk_level" {{ 'disabled' if readonly else '' }}>
|
{% for option in options.chunk_levels %}
|
||||||
{% for option in options.chunk_levels %}
|
<option value="{{ option.value }}" {% if chunk_level_value == option.value %}selected{% endif %}>{{ option.label }}</option>
|
||||||
<option value="{{ option.value }}" {% if chunk_level_value == option.value %}selected{% endif %}>{{ option.label }}</option>
|
{% endfor %}
|
||||||
{% endfor %}
|
</select>
|
||||||
</select>
|
<p class="hint">Paragraphs work well for long-form narration; sentences give finer subtitle sync.</p>
|
||||||
<p class="hint">Paragraphs work well for long-form narration; sentences give finer subtitle sync.</p>
|
</div>
|
||||||
</div>
|
<div class="field">
|
||||||
<div class="field">
|
<label for="speaker_analysis_threshold">Speaker analysis minimum mentions</label>
|
||||||
<label for="speaker_analysis_threshold">Speaker analysis minimum mentions</label>
|
<input type="number" min="1" max="25" id="speaker_analysis_threshold" name="speaker_analysis_threshold" value="{{ analysis_threshold_value }}" {{ 'disabled' if readonly else '' }}>
|
||||||
<input type="number" min="1" max="25" id="speaker_analysis_threshold" name="speaker_analysis_threshold" value="{{ analysis_threshold_value }}" {{ 'disabled' if readonly else '' }}>
|
<p class="hint">Entities appearing less often fall back to the narrator voice.</p>
|
||||||
<p class="hint">Entities appearing less often fall back to the narrator voice.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="chapter_intro_delay">Pause after chapter titles (seconds)</label>
|
<label for="chapter_intro_delay">Pause after chapter titles (seconds)</label>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% set embed_scripts = embed_scripts if embed_scripts is defined else True %}
|
{% set embed_scripts = embed_scripts if embed_scripts is defined else True %}
|
||||||
|
{% set recognition_enabled = settings.enable_entity_recognition if settings is defined and settings.enable_entity_recognition is not none else True %}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ url_for('web.finalize_job', pending_id=pending.id) }}"
|
action="{{ url_for('web.finalize_job', pending_id=pending.id) }}"
|
||||||
class="prepare-form"
|
class="prepare-form"
|
||||||
@@ -16,7 +17,8 @@
|
|||||||
data-language="{{ pending.language }}"
|
data-language="{{ pending.language }}"
|
||||||
data-base-voice="{{ pending.voice }}"
|
data-base-voice="{{ pending.voice }}"
|
||||||
data-speed="{{ '%.2f'|format(pending.speed) }}"
|
data-speed="{{ '%.2f'|format(pending.speed) }}"
|
||||||
data-use-gpu="{{ 'true' if pending.use_gpu else 'false' }}">
|
data-use-gpu="{{ 'true' if pending.use_gpu else 'false' }}"
|
||||||
|
data-entities-enabled="{{ 'true' if recognition_enabled else 'false' }}">
|
||||||
<input type="hidden" name="active_step" value="entities" data-role="active-step-input">
|
<input type="hidden" name="active_step" value="entities" data-role="active-step-input">
|
||||||
|
|
||||||
<div class="modal__body wizard-card__body">
|
<div class="modal__body wizard-card__body">
|
||||||
@@ -28,6 +30,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="entity-tabs" data-role="entity-tabs">
|
<div class="entity-tabs" data-role="entity-tabs">
|
||||||
|
{% if not recognition_enabled %}
|
||||||
|
<div class="alert alert--info entity-tabs__notice">
|
||||||
|
Entity recognition is disabled in Settings. Enable it to populate detected people and entities automatically.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="entity-tabs__nav" role="tablist" aria-label="Entity categories">
|
<div class="entity-tabs__nav" role="tablist" aria-label="Entity categories">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="entity-tabs__tab is-active"
|
class="entity-tabs__tab is-active"
|
||||||
@@ -95,7 +102,7 @@
|
|||||||
formaction="{{ url_for('web.analyze_pending_job', pending_id=pending.id) }}"
|
formaction="{{ url_for('web.analyze_pending_job', pending_id=pending.id) }}"
|
||||||
formmethod="post"
|
formmethod="post"
|
||||||
formnovalidate
|
formnovalidate
|
||||||
{% if not speaker_configs %}disabled{% endif %}>
|
{% if not options.speaker_configs %}disabled{% endif %}>
|
||||||
Apply preset
|
Apply preset
|
||||||
</button>
|
</button>
|
||||||
<label class="toggle-pill">
|
<label class="toggle-pill">
|
||||||
@@ -340,7 +347,7 @@
|
|||||||
<h2>Detected entities</h2>
|
<h2>Detected entities</h2>
|
||||||
<p class="hint">Assign pronunciations and voices for recurring names and terminology across the manuscript.</p>
|
<p class="hint">Assign pronunciations and voices for recurring names and terminology across the manuscript.</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="button button--ghost button--small" data-role="entities-refresh">Refresh</button>
|
<button type="button" class="button button--ghost button--small" data-role="entities-refresh" {% if not recognition_enabled %}disabled aria-disabled="true"{% endif %}>Refresh</button>
|
||||||
</header>
|
</header>
|
||||||
<dl class="entity-summary__stats" data-role="entity-stats"></dl>
|
<dl class="entity-summary__stats" data-role="entity-stats"></dl>
|
||||||
<ol class="entity-summary__list" data-role="entity-list"></ol>
|
<ol class="entity-summary__list" data-role="entity-list"></ol>
|
||||||
|
|||||||
@@ -43,10 +43,14 @@
|
|||||||
{% for slug, label in navigation_labels.items() %}
|
{% for slug, label in navigation_labels.items() %}
|
||||||
{% set item_index = step_number[slug] %}
|
{% set item_index = step_number[slug] %}
|
||||||
{% set state = 'is-active' if slug == current_step else ('is-complete' if item_index < current_index else '') %}
|
{% set state = 'is-active' if slug == current_step else ('is-complete' if item_index < current_index else '') %}
|
||||||
<span class="step-indicator__item {{ state }}" data-role="wizard-step-indicator" data-step="{{ slug }}">
|
<button type="button"
|
||||||
|
class="step-indicator__item {{ state }}"
|
||||||
|
data-role="wizard-step-indicator"
|
||||||
|
data-step="{{ slug }}"
|
||||||
|
aria-current="{{ 'step' if slug == current_step else 'false' }}">
|
||||||
<span class="step-indicator__index">{{ item_index }}</span>
|
<span class="step-indicator__index">{{ item_index }}</span>
|
||||||
<span class="step-indicator__label">{{ label }}</span>
|
<span class="step-indicator__label">{{ label }}</span>
|
||||||
</span>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</nav>
|
</nav>
|
||||||
<h2 class="modal__title" id="new-job-modal-title">{{ step_titles[current_step] }}</h2>
|
<h2 class="modal__title" id="new-job-modal-title">{{ step_titles[current_step] }}</h2>
|
||||||
|
|||||||
@@ -41,6 +41,13 @@
|
|||||||
<input type="text" id="speaker_pronunciation_sentence" name="speaker_pronunciation_sentence" value="{{ settings.speaker_pronunciation_sentence }}" placeholder="This is {{ '{{name}}' }} speaking.">
|
<input type="text" id="speaker_pronunciation_sentence" name="speaker_pronunciation_sentence" value="{{ settings.speaker_pronunciation_sentence }}" placeholder="This is {{ '{{name}}' }} speaking.">
|
||||||
<p class="hint">Include <code>{{ '{{name}}' }}</code> where the speaker name should be inserted.</p>
|
<p class="hint">Include <code>{{ '{{name}}' }}</code> where the speaker name should be inserted.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field field--stack">
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="checkbox" name="enable_entity_recognition" value="true" {% if settings.enable_entity_recognition %}checked{% endif %}>
|
||||||
|
<span>Automatically detect entities for new jobs</span>
|
||||||
|
</label>
|
||||||
|
<p class="hint">Disable if you prefer to skip entity extraction in the job wizard.</p>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="speaker_random_languages">Randomizer Languages</label>
|
<label for="speaker_random_languages">Randomizer Languages</label>
|
||||||
{% set selected_languages = settings.speaker_random_languages or [] %}
|
{% set selected_languages = settings.speaker_random_languages or [] %}
|
||||||
|
|||||||
Reference in New Issue
Block a user