mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Enhance entity management with filtering and preview functionality
This commit is contained in:
+31
-7
@@ -3543,8 +3543,10 @@ def entities_page() -> ResponseReturnValue:
|
|||||||
]
|
]
|
||||||
|
|
||||||
status_message = ""
|
status_message = ""
|
||||||
if status_code == "saved":
|
if status_code in {"saved", "updated"}:
|
||||||
status_message = f"Updated override for {status_token or 'override'}."
|
status_message = f"Updated override for {status_token or 'override'}."
|
||||||
|
elif status_code == "created":
|
||||||
|
status_message = f"Added override for {status_token or 'override'}."
|
||||||
elif status_code == "deleted":
|
elif status_code == "deleted":
|
||||||
status_message = f"Deleted override for {status_token or 'override'}."
|
status_message = f"Deleted override for {status_token or 'override'}."
|
||||||
|
|
||||||
@@ -3579,7 +3581,8 @@ def entities_override_update() -> ResponseReturnValue:
|
|||||||
action = (request.form.get("action") or "save").strip().lower()
|
action = (request.form.get("action") or "save").strip().lower()
|
||||||
pronunciation_value = (request.form.get("pronunciation") or "").strip()
|
pronunciation_value = (request.form.get("pronunciation") or "").strip()
|
||||||
voice_value = (request.form.get("voice") or "").strip()
|
voice_value = (request.form.get("voice") or "").strip()
|
||||||
notes_value = (request.form.get("notes") or "").strip()
|
notes_present = "notes" in request.form
|
||||||
|
notes_value = (request.form.get("notes") or "").strip() if notes_present else ""
|
||||||
|
|
||||||
redirect_params: Dict[str, Any] = {"lang": language}
|
redirect_params: Dict[str, Any] = {"lang": language}
|
||||||
state_mappings = (
|
state_mappings = (
|
||||||
@@ -3598,21 +3601,42 @@ def entities_override_update() -> ResponseReturnValue:
|
|||||||
redirect_params["error"] = "Missing override token."
|
redirect_params["error"] = "Missing override token."
|
||||||
return redirect(url_for("web.entities_page", **redirect_params))
|
return redirect(url_for("web.entities_page", **redirect_params))
|
||||||
|
|
||||||
status_code = "saved"
|
normalized_token = normalize_entity_token(token_value)
|
||||||
|
if not normalized_token:
|
||||||
|
redirect_params["status"] = "error"
|
||||||
|
redirect_params["error"] = "Token is too generic to override."
|
||||||
|
return redirect(url_for("web.entities_page", **redirect_params))
|
||||||
|
|
||||||
|
existing_map = load_pronunciation_overrides(language=language, tokens=[token_value])
|
||||||
|
existing_override = existing_map.get(normalized_token)
|
||||||
|
|
||||||
|
if notes_present:
|
||||||
|
notes_payload: Optional[str] = notes_value or None
|
||||||
|
elif existing_override:
|
||||||
|
notes_payload = existing_override.get("notes")
|
||||||
|
else:
|
||||||
|
notes_payload = None
|
||||||
|
|
||||||
|
status_code = "updated"
|
||||||
|
saved_override: Optional[Dict[str, Any]] = None
|
||||||
try:
|
try:
|
||||||
if action == "delete":
|
if action == "delete":
|
||||||
delete_pronunciation_override(language=language, token=token_value)
|
delete_pronunciation_override(language=language, token=token_value)
|
||||||
status_code = "deleted"
|
status_code = "deleted"
|
||||||
else:
|
else:
|
||||||
save_pronunciation_override(
|
saved_override = save_pronunciation_override(
|
||||||
language=language,
|
language=language,
|
||||||
token=token_value,
|
token=token_value,
|
||||||
pronunciation=pronunciation_value or None,
|
pronunciation=pronunciation_value or None,
|
||||||
voice=voice_value or None,
|
voice=voice_value or None,
|
||||||
notes=notes_value or None,
|
notes=notes_payload,
|
||||||
context=None,
|
context=None,
|
||||||
)
|
)
|
||||||
status_code = "saved"
|
status_code = "updated" if existing_override else "created"
|
||||||
|
except ValueError as exc:
|
||||||
|
redirect_params["status"] = "error"
|
||||||
|
redirect_params["error"] = str(exc)
|
||||||
|
return redirect(url_for("web.entities_page", **redirect_params))
|
||||||
except Exception as exc: # pragma: no cover - defensive logging
|
except Exception as exc: # pragma: no cover - defensive logging
|
||||||
current_app.logger.exception("Failed to %s override for token %s", action, token_value)
|
current_app.logger.exception("Failed to %s override for token %s", action, token_value)
|
||||||
redirect_params["status"] = "error"
|
redirect_params["status"] = "error"
|
||||||
@@ -3620,7 +3644,7 @@ def entities_override_update() -> ResponseReturnValue:
|
|||||||
return redirect(url_for("web.entities_page", **redirect_params))
|
return redirect(url_for("web.entities_page", **redirect_params))
|
||||||
|
|
||||||
redirect_params["status"] = status_code
|
redirect_params["status"] = status_code
|
||||||
redirect_params["token"] = token_value
|
redirect_params["token"] = (saved_override or {}).get("token") or token_value
|
||||||
return redirect(url_for("web.entities_page", **redirect_params))
|
return redirect(url_for("web.entities_page", **redirect_params))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
(function () {
|
||||||
|
const root = document.querySelector('[data-override-root]');
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewUrl = root.dataset.previewUrl || "";
|
||||||
|
const defaultLanguage = root.dataset.language || "a";
|
||||||
|
const table = root.querySelector('[data-role="override-table"]');
|
||||||
|
const rows = table ? Array.from(table.querySelectorAll('[data-role="override-row"]')) : [];
|
||||||
|
const filterInput = root.querySelector('[data-role="override-filter"]');
|
||||||
|
const filterClearButton = root.querySelector('[data-role="override-filter-clear"]');
|
||||||
|
const filterEmptyMessage = root.querySelector('[data-role="filter-empty"]');
|
||||||
|
|
||||||
|
function base64ToBlob(base64, mimeType) {
|
||||||
|
const binary = atob(base64);
|
||||||
|
const length = binary.length;
|
||||||
|
const bytes = new Uint8Array(length);
|
||||||
|
for (let index = 0; index < length; index += 1) {
|
||||||
|
bytes[index] = binary.charCodeAt(index);
|
||||||
|
}
|
||||||
|
return new Blob([bytes], { type: mimeType });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getControl(form, selector) {
|
||||||
|
if (!form) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const direct = form.querySelector(selector);
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
if (!form.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return root.querySelector(`${selector}[form="${form.id}"]`) || document.querySelector(`${selector}[form="${form.id}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPreview(container) {
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const messageEl = container.querySelector('[data-role="preview-message"]');
|
||||||
|
const audioEl = container.querySelector('[data-role="preview-audio"]');
|
||||||
|
if (messageEl) {
|
||||||
|
messageEl.textContent = "";
|
||||||
|
messageEl.removeAttribute('data-state');
|
||||||
|
}
|
||||||
|
if (audioEl) {
|
||||||
|
const priorUrl = audioEl.dataset.objectUrl;
|
||||||
|
if (priorUrl) {
|
||||||
|
URL.revokeObjectURL(priorUrl);
|
||||||
|
delete audioEl.dataset.objectUrl;
|
||||||
|
}
|
||||||
|
audioEl.pause();
|
||||||
|
audioEl.removeAttribute('src');
|
||||||
|
audioEl.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPreviewPayload(form) {
|
||||||
|
if (!form) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const tokenInput = getControl(form, 'input[name="token"]');
|
||||||
|
const pronunciationInput = getControl(form, 'input[name="pronunciation"]');
|
||||||
|
const voiceSelect = getControl(form, 'select[name="voice"]');
|
||||||
|
const languageInput = getControl(form, 'input[name="lang"]');
|
||||||
|
|
||||||
|
const token = tokenInput && 'value' in tokenInput ? tokenInput.value.trim() : "";
|
||||||
|
const pronunciation = pronunciationInput && 'value' in pronunciationInput ? pronunciationInput.value.trim() : "";
|
||||||
|
const voice = voiceSelect && 'value' in voiceSelect ? voiceSelect.value.trim() : "";
|
||||||
|
const language = languageInput && 'value' in languageInput ? languageInput.value.trim() : defaultLanguage;
|
||||||
|
|
||||||
|
if (!token && !pronunciation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
pronunciation,
|
||||||
|
voice,
|
||||||
|
language,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestPreview(button) {
|
||||||
|
if (!previewUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formId = button.dataset.formId || "";
|
||||||
|
const form = formId ? document.getElementById(formId) : button.closest('form');
|
||||||
|
const container = button.closest('[data-role="preview-container"]');
|
||||||
|
const messageEl = container ? container.querySelector('[data-role="preview-message"]') : null;
|
||||||
|
const audioEl = container ? container.querySelector('[data-role="preview-audio"]') : null;
|
||||||
|
|
||||||
|
resetPreview(container);
|
||||||
|
|
||||||
|
const payload = buildPreviewPayload(form);
|
||||||
|
if (!payload) {
|
||||||
|
if (messageEl) {
|
||||||
|
messageEl.textContent = "Enter a token or pronunciation first.";
|
||||||
|
messageEl.dataset.state = "error";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
button.setAttribute('data-loading', 'true');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(previewUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok || data.error) {
|
||||||
|
throw new Error(data.error || 'Preview failed.');
|
||||||
|
}
|
||||||
|
if (!data.audio_base64) {
|
||||||
|
throw new Error('Preview did not return audio.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioEl) {
|
||||||
|
const blob = base64ToBlob(data.audio_base64, 'audio/wav');
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
audioEl.src = objectUrl;
|
||||||
|
audioEl.dataset.objectUrl = objectUrl;
|
||||||
|
audioEl.hidden = false;
|
||||||
|
audioEl.load();
|
||||||
|
audioEl.play().catch(() => {
|
||||||
|
/* playback might require user interaction; ignore */
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageEl) {
|
||||||
|
messageEl.textContent = data.normalized_text || data.text || 'Preview ready.';
|
||||||
|
messageEl.dataset.state = "success";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (messageEl) {
|
||||||
|
messageEl.textContent = error instanceof Error ? error.message : 'Preview failed.';
|
||||||
|
messageEl.dataset.state = "error";
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
button.removeAttribute('data-loading');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachPreviewHandlers() {
|
||||||
|
const previewButtons = root.querySelectorAll('[data-role="preview-button"]');
|
||||||
|
previewButtons.forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
requestPreview(button);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilter() {
|
||||||
|
if (!filterInput || rows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const term = filterInput.value.trim().toLowerCase();
|
||||||
|
let visibleCount = 0;
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const token = row.dataset.token || "";
|
||||||
|
const pronunciationInput = row.querySelector('input[name="pronunciation"]');
|
||||||
|
const voiceSelect = row.querySelector('select[name="voice"]');
|
||||||
|
|
||||||
|
const pronunciationValue = pronunciationInput && 'value' in pronunciationInput
|
||||||
|
? pronunciationInput.value.trim().toLowerCase()
|
||||||
|
: "";
|
||||||
|
const voiceOption = voiceSelect && 'selectedIndex' in voiceSelect && voiceSelect.selectedIndex >= 0
|
||||||
|
? voiceSelect.options[voiceSelect.selectedIndex]
|
||||||
|
: null;
|
||||||
|
const voiceValue = voiceOption && voiceOption.textContent
|
||||||
|
? voiceOption.textContent.trim().toLowerCase()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (!term || token.includes(term) || pronunciationValue.includes(term) || voiceValue.includes(term)) {
|
||||||
|
row.hidden = false;
|
||||||
|
visibleCount += 1;
|
||||||
|
} else {
|
||||||
|
row.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filterEmptyMessage) {
|
||||||
|
filterEmptyMessage.hidden = visibleCount !== 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterInput) {
|
||||||
|
filterInput.addEventListener('input', applyFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterClearButton && filterInput) {
|
||||||
|
filterClearButton.addEventListener('click', () => {
|
||||||
|
filterInput.value = "";
|
||||||
|
applyFilter();
|
||||||
|
filterInput.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table) {
|
||||||
|
table.addEventListener('input', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (target && (target.matches('input[name="pronunciation"]') || target.matches('select[name="voice"]'))) {
|
||||||
|
applyFilter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
table.addEventListener('change', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (target && target.matches('select[name="voice"]')) {
|
||||||
|
applyFilter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachPreviewHandlers();
|
||||||
|
applyFilter();
|
||||||
|
})();
|
||||||
@@ -1595,6 +1595,77 @@ button.step-indicator__item:focus-visible {
|
|||||||
background: rgba(56, 189, 248, 0.08);
|
background: rgba(56, 189, 248, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.entity-table-tools {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-table-tools__filter {
|
||||||
|
flex: 1 1 240px;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-table-tools__filter input {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-table-tools__clear {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-create {
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-create__description {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-create__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-preview {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.6rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-preview__controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-preview__audio {
|
||||||
|
width: min(100%, 220px);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-preview__status {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-preview__status[data-state="success"] {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-preview__status[data-state="error"] {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
.overrides-table__token {
|
.overrides-table__token {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
@@ -1639,6 +1710,26 @@ button.step-indicator__item:focus-visible {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button[data-loading="true"][data-role="preview-button"] {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 2.3rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[data-loading="true"][data-role="preview-button"]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0.9rem;
|
||||||
|
top: 50%;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid rgba(148, 163, 184, 0.4);
|
||||||
|
border-right-color: rgba(56, 189, 248, 0.8);
|
||||||
|
animation: spin 0.85s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 9px;
|
height: 9px;
|
||||||
@@ -1872,8 +1963,17 @@ button.step-indicator__item:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button--danger {
|
.button--danger {
|
||||||
background: linear-gradient(135deg, var(--danger), #ef4444);
|
background: rgba(248, 113, 113, 0.16);
|
||||||
box-shadow: 0 12px 30px rgba(239, 68, 68, 0.2);
|
color: var(--danger);
|
||||||
|
border: 1px solid rgba(248, 113, 113, 0.45);
|
||||||
|
box-shadow: 0 10px 24px rgba(248, 113, 113, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--danger:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(248, 113, 113, 0.85), #ef4444);
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(248, 113, 113, 0.65);
|
||||||
|
box-shadow: 0 16px 36px rgba(248, 113, 113, 0.32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
{% block title %}abogen · Entities{% endblock %}
|
{% block title %}abogen · Entities{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="card card--panel">
|
<section
|
||||||
|
class="card card--panel"
|
||||||
|
data-override-root
|
||||||
|
data-preview-url="{{ url_for('api.api_entity_pronunciation_preview') }}"
|
||||||
|
data-language="{{ language }}"
|
||||||
|
>
|
||||||
<h1 class="card__title">Entities & Pronunciation Overrides</h1>
|
<h1 class="card__title">Entities & Pronunciation Overrides</h1>
|
||||||
<p class="card__subtitle">Review and refine stored pronunciations so recurring names sound right in every project.</p>
|
<p class="card__subtitle">Review and refine stored pronunciations so recurring names sound right in every project.</p>
|
||||||
<form class="entity-filter" method="get">
|
<form class="entity-filter" method="get">
|
||||||
@@ -64,24 +69,91 @@
|
|||||||
{{ status_error or status_message }}
|
{{ status_error or status_message }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="entity-table-tools">
|
||||||
|
<label class="field entity-table-tools__filter">
|
||||||
|
<span>Filter visible overrides</span>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
data-role="override-filter"
|
||||||
|
placeholder="Type a name or voice to filter"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="button button--ghost" data-role="override-filter-clear">Clear</button>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
id="override-create-form"
|
||||||
|
class="form-section entity-create"
|
||||||
|
method="post"
|
||||||
|
action="{{ url_for('web.entities_override_update') }}"
|
||||||
|
>
|
||||||
|
<h3 class="form-section__title">Add manual override</h3>
|
||||||
|
<p class="entity-create__description">Create pronunciations or assign default voices without preparing a job.</p>
|
||||||
|
<input type="hidden" name="lang" value="{{ language }}">
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="state_voice" value="{{ voice_filter }}">
|
||||||
|
<input type="hidden" name="state_pronunciation" value="{{ pronunciation_filter }}">
|
||||||
|
<input type="hidden" name="state_limit" value="{{ limit }}">
|
||||||
|
<input type="hidden" name="state_query" value="{{ query }}">
|
||||||
|
<div class="field-grid field-grid--compact">
|
||||||
|
<label class="field">
|
||||||
|
<span>Token</span>
|
||||||
|
<input type="text" name="token" placeholder="e.g. Arrakis" required>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Pronunciation</span>
|
||||||
|
<input type="text" name="pronunciation" placeholder="Optional phonetic spelling">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Voice override</span>
|
||||||
|
<select name="voice">
|
||||||
|
<option value="" selected>No override</option>
|
||||||
|
{% for voice in options.voice_catalog %}
|
||||||
|
<option value="{{ voice.id }}">{{ voice.display_name }} - {{ voice.language_label }} - {{ voice.gender }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="entity-create__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button--ghost"
|
||||||
|
data-role="preview-button"
|
||||||
|
data-form-id="override-create-form"
|
||||||
|
>Preview</button>
|
||||||
|
<button type="submit" class="button">Add override</button>
|
||||||
|
</div>
|
||||||
|
<div class="entity-preview" data-role="preview-container">
|
||||||
|
<div class="entity-preview__status" data-role="preview-message"></div>
|
||||||
|
<audio
|
||||||
|
class="entity-preview__audio"
|
||||||
|
data-role="preview-audio"
|
||||||
|
controls
|
||||||
|
hidden
|
||||||
|
></audio>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% if overrides %}
|
{% if overrides %}
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="jobs-table overrides-table">
|
<table class="jobs-table overrides-table" data-role="override-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Token</th>
|
<th scope="col">Token</th>
|
||||||
<th scope="col">Pronunciation</th>
|
<th scope="col">Pronunciation</th>
|
||||||
<th scope="col">Voice</th>
|
<th scope="col">Voice</th>
|
||||||
<th scope="col">Notes</th>
|
|
||||||
<th scope="col">Usage</th>
|
<th scope="col">Usage</th>
|
||||||
<th scope="col">Last updated</th>
|
<th scope="col">Last updated</th>
|
||||||
|
<th scope="col">Preview</th>
|
||||||
<th scope="col" class="overrides-table__actions-heading">Actions</th>
|
<th scope="col" class="overrides-table__actions-heading">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for override in overrides %}
|
{% for override in overrides %}
|
||||||
{% set form_id = "override-form-" ~ loop.index %}
|
{% set form_id = "override-form-" ~ loop.index %}
|
||||||
<tr>
|
<tr
|
||||||
|
data-role="override-row"
|
||||||
|
data-token="{{ override.token | lower }}"
|
||||||
|
>
|
||||||
<td data-label="Token"><span class="overrides-table__token">{{ override.token }}</span></td>
|
<td data-label="Token"><span class="overrides-table__token">{{ override.token }}</span></td>
|
||||||
<td data-label="Pronunciation">
|
<td data-label="Pronunciation">
|
||||||
<input
|
<input
|
||||||
@@ -107,18 +179,27 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Notes">
|
|
||||||
<input
|
|
||||||
class="overrides-table__input"
|
|
||||||
type="text"
|
|
||||||
name="notes"
|
|
||||||
form="{{ form_id }}"
|
|
||||||
value="{{ override.notes or '' }}"
|
|
||||||
placeholder="Add notes"
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
<td data-label="Usage">{{ override.usage_count }}</td>
|
<td data-label="Usage">{{ override.usage_count }}</td>
|
||||||
<td data-label="Last updated">{{ override.updated_at_label }}</td>
|
<td data-label="Last updated">{{ override.updated_at_label }}</td>
|
||||||
|
<td data-label="Preview">
|
||||||
|
<div class="entity-preview" data-role="preview-container">
|
||||||
|
<div class="entity-preview__controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button--ghost button--small"
|
||||||
|
data-role="preview-button"
|
||||||
|
data-form-id="{{ form_id }}"
|
||||||
|
>Preview</button>
|
||||||
|
<span class="entity-preview__status" data-role="preview-message"></span>
|
||||||
|
</div>
|
||||||
|
<audio
|
||||||
|
class="entity-preview__audio"
|
||||||
|
data-role="preview-audio"
|
||||||
|
controls
|
||||||
|
hidden
|
||||||
|
></audio>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td data-label="Actions">
|
<td data-label="Actions">
|
||||||
<form
|
<form
|
||||||
id="{{ form_id }}"
|
id="{{ form_id }}"
|
||||||
@@ -143,6 +224,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="hint" data-role="filter-empty" hidden>No overrides match your current filter.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="hint">No overrides matched your filters. Try adjusting the search or create overrides from the Entities step while preparing a job.</p>
|
<p class="hint">No overrides matched your filters. Try adjusting the search or create overrides from the Entities step while preparing a job.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -151,4 +233,5 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
<script src="{{ url_for('static', filename='entities.js') }}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user