feat: Enhance upload modal and chapter preparation templates for improved user experience

This commit is contained in:
JB
2025-10-11 11:57:50 -07:00
parent ccf5a11222
commit 4a62363064
7 changed files with 303 additions and 165 deletions
+21 -1
View File
@@ -188,6 +188,8 @@ document.addEventListener("DOMContentLoaded", () => {
const enabled = row.querySelector('[data-role=chapter-enabled]');
const inputs = Array.from(row.querySelectorAll("input[type=text], select, textarea"));
const warning = row.querySelector('[data-role=chapter-warning]');
const snippet = row.querySelector('[data-role="chapter-snippet"]');
const details = row.querySelector('[data-role="chapter-details"]');
const isChecked = enabled ? enabled.checked : true;
row.dataset.disabled = isChecked ? "false" : "true";
@@ -212,6 +214,16 @@ document.addEventListener("DOMContentLoaded", () => {
const select = row.querySelector("select[data-role=voice-select]");
toggleFormula(select);
if (snippet) {
snippet.hidden = !isChecked;
snippet.setAttribute("aria-hidden", isChecked ? "false" : "true");
}
if (details) {
details.hidden = !isChecked;
details.setAttribute("aria-hidden", isChecked ? "false" : "true");
}
if (warning) {
warning.hidden = isChecked;
warning.setAttribute("aria-hidden", isChecked ? "true" : "false");
@@ -337,7 +349,15 @@ document.addEventListener("DOMContentLoaded", () => {
});
const speakerItems = Array.from(form.querySelectorAll(".speaker-list__item"));
speakerItems.forEach((item) => initialiseSpeakerItem(item));
speakerItems.forEach((item) => {
initialiseSpeakerItem(item);
const pronunciationInput = item.querySelector('[data-role="speaker-pronunciation"]');
if (pronunciationInput) {
const sync = () => syncPronunciationPreview(item);
pronunciationInput.addEventListener("input", sync);
pronunciationInput.addEventListener("change", sync);
}
});
const activeStepInput = form.querySelector('[data-role="active-step-input"]');
const analysisButtons = Array.from(form.querySelectorAll('[data-role="submit-speaker-analysis"]'));
+16 -1
View File
@@ -26,6 +26,21 @@ const stopCurrentPlayback = () => {
}
};
const resolvePreviewText = (button) => {
const source = (button.dataset.previewSource || "").toLowerCase();
if (source === "pronunciation") {
const container = button.closest(".speaker-list__item");
if (container) {
const input = container.querySelector('[data-role="speaker-pronunciation"]');
const fallback = (container.dataset.defaultPronunciation || "").trim();
const value = (input?.value || "").trim() || fallback;
button.dataset.previewText = value;
return value;
}
}
return (button.dataset.previewText || "").trim();
};
audioElement.addEventListener("ended", () => {
stopCurrentPlayback();
});
@@ -37,7 +52,7 @@ audioElement.addEventListener("pause", () => {
});
const playPreview = async (button) => {
const text = (button.dataset.previewText || "").trim();
const text = resolvePreviewText(button);
const voice = (button.dataset.voice || "").trim();
const language = (button.dataset.language || "a").trim() || "a";
const speedRaw = button.dataset.speed || "1";
+10
View File
@@ -618,6 +618,16 @@ body {
border: 1px solid rgba(148, 163, 184, 0.18);
}
button.step-indicator__item {
cursor: pointer;
font: inherit;
}
button.step-indicator__item:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.step-indicator__index {
width: 1.75rem;
height: 1.75rem;
+1 -157
View File
@@ -17,163 +17,7 @@
</div>
</section>
<div class="modal" data-role="upload-modal" hidden>
<div class="modal__overlay" data-role="upload-modal-close" tabindex="-1"></div>
<div class="modal__content card card--modal" role="dialog" aria-modal="true" aria-labelledby="upload-modal-title">
<header class="modal__header">
<div class="modal__heading">
<p class="modal__eyebrow">Step 1 of 3</p>
<h2 class="modal__title" id="upload-modal-title">Upload &amp; settings</h2>
<p class="hint">Choose your source file or paste text, then set the defaults used for chapter analysis and speaker casting.</p>
</div>
<button type="button" class="icon-button" data-role="upload-modal-close" aria-label="Close upload settings"></button>
</header>
<form action="{{ url_for('web.enqueue_job') }}" method="post" enctype="multipart/form-data" class="upload-form">
<div class="modal__body upload-form__sections">
<section class="form-section">
<h3 class="form-section__title">Manuscript &amp; subtitles</h3>
<div class="form-section__layout">
<div class="field-grid field-grid--two">
<div class="field field--file">
<label for="source_file">Source file</label>
<input type="file" id="source_file" name="source_file" accept=".txt,.pdf,.epub,.md,.markdown">
</div>
<div class="field">
<label for="language">Language</label>
<select id="language" name="language">
{% for key, label in options.languages.items() %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="field-grid field-grid--two">
<div class="field">
<label for="subtitle_mode">Subtitle mode</label>
<select id="subtitle_mode" name="subtitle_mode">
<option value="Disabled">Disabled</option>
<option value="Sentence">Sentence</option>
<option value="Sentence + Comma">Sentence + Comma</option>
<option value="Sentence + Highlighting">Sentence + Highlighting</option>
{% for i in range(1, 11) %}
<option value="{{ i }} {% if i == 1 %}word{% else %}words{% endif %}">{{ i }} {% if i == 1 %}word{% else %}words{% endif %}</option>
{% endfor %}
</select>
</div>
<div class="field field--stack">
<span class="field__caption">Additional outputs</span>
<label class="toggle-pill">
<input type="checkbox" name="generate_epub3" value="true" {% if settings.generate_epub3 %}checked{% endif %}>
<span>Generate EPUB 3 (experimental)</span>
</label>
<p class="hint">Creates a synchronized EPUB alongside audio output.</p>
</div>
</div>
</div>
</section>
<section class="form-section">
<h3 class="form-section__title">Narrator defaults</h3>
<div class="form-section__layout form-section__layout--split">
<div class="form-section__group">
<div class="field">
<label for="voice_profile">Voice profile</label>
<select id="voice_profile" name="voice_profile" data-role="voice-profile">
<option value="__standard" {% if not options.voice_profile_options %}selected{% endif %}>Standard voice</option>
<option value="__formula">Custom voice formula</option>
{% if options.voice_profile_options %}
<optgroup label="Saved mixes">
{% for profile in options.voice_profile_options %}
<option value="{{ profile.name }}" data-language="{{ profile.language }}" data-formula="{{ profile.formula|e }}" {% if loop.first %}selected{% endif %}>{{ profile.name }}{% if profile.language %} ({{ profile.language|upper }}){% endif %}</option>
{% endfor %}
</optgroup>
{% endif %}
</select>
</div>
<div class="field" data-role="voice-field" {% if options.voice_profile_options %}hidden aria-hidden="true"{% endif %}>
<label for="voice">Voice</label>
<select id="voice" name="voice" data-role="voice-select" data-default="{{ settings.default_voice }}">
{% for voice in options.voices %}
<option value="{{ voice }}" {% if settings.default_voice == voice %}selected{% endif %}>{{ voice }}</option>
{% endfor %}
</select>
</div>
<div class="field" data-conditional="formula" data-role="formula-field" hidden aria-hidden="true">
<label for="voice_formula">Custom voice formula</label>
<input type="text" id="voice_formula" name="voice_formula" placeholder="af_nova*0.4+am_liam*0.6" data-role="voice-formula">
</div>
</div>
<div class="form-section__group">
<div class="field field--slider">
<label for="speed">Speed <span class="tag" id="speed_value">1.00×</span></label>
<input type="range" id="speed" name="speed" min="0.6" max="1.4" step="0.05" value="1.0" oninput="document.getElementById('speed_value').textContent = Number.parseFloat(this.value).toFixed(2) + '×';">
</div>
<div class="field field--with-action field--preview" data-role="voice-preview">
<div class="field__label-row">
<span class="field__label">Preview</span>
<button type="button" class="button button--ghost button--small" data-role="voice-preview-button">Preview voice</button>
</div>
<p class="hint field__status" data-role="voice-preview-status" aria-live="polite" hidden></p>
<audio class="voice-preview__audio" data-role="voice-preview-audio" controls preload="none" hidden></audio>
</div>
</div>
</div>
</section>
<section class="form-section">
<h3 class="form-section__title">Speakers &amp; casting</h3>
<div class="form-section__layout">
<div class="field-grid field-grid--two">
<div class="field field--stack">
<label for="speaker_config">Speaker preset</label>
<select id="speaker_config" name="speaker_config">
<option value="">None</option>
{% for config in options.speaker_configs %}
<option value="{{ config.name }}">{{ config.name }} · {{ config.speakers|length }} speaker{% if config.speakers|length != 1 %}s{% endif %}</option>
{% endfor %}
</select>
<p class="hint">Reuse a saved roster to keep character voices consistent.</p>
</div>
<div class="field">
<label for="speaker_mode">Speaker mode</label>
<select id="speaker_mode" name="speaker_mode">
{% for option in options.speaker_modes %}
<option value="{{ option.value }}" {% if settings.speaker_mode == option.value %}selected{% endif %}>{{ option.label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="field-grid field-grid--two">
<div class="field">
<label for="chunk_level">Chunk granularity</label>
<select id="chunk_level" name="chunk_level">
{% for option in options.chunk_levels %}
<option value="{{ option.value }}" {% if settings.chunk_level == option.value %}selected{% endif %}>{{ option.label }}</option>
{% endfor %}
</select>
<p class="hint">Paragraphs work well for long-form narration; sentences give finer subtitle sync.</p>
</div>
<div class="field">
<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="{{ settings.speaker_analysis_threshold }}">
<p class="hint">Speakers appearing less often fall back to the narrator voice.</p>
</div>
</div>
<div class="field">
<label for="chapter_intro_delay">Pause after chapter titles (seconds)</label>
<input type="number" step="0.1" min="0" id="chapter_intro_delay" name="chapter_intro_delay" value="{{ '%.2f'|format(settings.chapter_intro_delay) }}">
<p class="hint">Set to 0 to disable the pause after speaking each chapter title.</p>
</div>
</div>
</section>
</div>
<footer class="modal__footer">
<button type="button" class="button button--ghost" data-role="upload-modal-close">Cancel</button>
<button type="submit" class="button">Chapter Selection</button>
</footer>
</form>
</div>
</div>
{% include "partials/upload_modal.html" with pending=None, readonly=False, active_step='settings' %}
{% include "partials/reader_modal.html" %}
@@ -0,0 +1,235 @@
{% set pending = pending if pending is defined else None %}
{% set readonly = readonly if readonly is defined else False %}
{% set modal_step = (active_step if active_step is defined else 'settings') or 'settings' %}
{% set settings_dict = settings if settings is defined else {} %}
{% set is_multi = False %}
{% if pending %}
{% set is_multi = pending.speaker_mode == 'multi' %}
{% elif settings_dict %}
{% set is_multi = settings_dict.get('speaker_mode', 'single') == 'multi' %}
{% endif %}
{% set language_value = pending.language if pending else settings_dict.get('language', '') %}
{% if not language_value %}
{% set sorted_languages = options.languages|dictsort %}
{% if sorted_languages %}
{% set language_value = sorted_languages[0][0] %}
{% endif %}
{% endif %}
{% set subtitle_value = pending.subtitle_mode if pending else settings_dict.get('subtitle_mode', 'Disabled') %}
{% set generate_epub3 = pending.generate_epub3 if pending else settings_dict.get('generate_epub3', False) %}
{% set speaker_mode_value = pending.speaker_mode if pending else settings_dict.get('speaker_mode', 'single') %}
{% set chunk_level_value = pending.chunk_level if pending else settings_dict.get('chunk_level', 'paragraph') %}
{% set analysis_threshold_value = pending.speaker_analysis_threshold if pending else settings_dict.get('speaker_analysis_threshold', 3) %}
{% set chapter_delay_value = pending.chapter_intro_delay if pending else settings_dict.get('chapter_intro_delay', 0.5) %}
{% set selected_config = pending.applied_speaker_config if pending else '' %}
{% set narrator_voice = pending.voice if pending else settings_dict.get('default_voice', options.voices[0] if options.voices else '') %}
{% set narrator_profile = pending.voice_profile if pending else '' %}
{% set narrator_speed = pending.speed if pending else 1.0 %}
{% set voice_formula_value = '' %}
{% set profile_value = '__standard' %}
{% if narrator_profile %}
{% set profile_value = narrator_profile %}
{% else %}
{% if narrator_voice and ('+' in narrator_voice or '*' in narrator_voice) %}
{% set profile_value = '__formula' %}
{% set voice_formula_value = narrator_voice %}
{% else %}
{% set profile_value = '__standard' %}
{% endif %}
{% endif %}
{% if not narrator_voice and options.voices %}
{% set narrator_voice = options.voices[0] %}
{% endif %}
{% set speed_display = '%.2f'|format(narrator_speed) %}
{% set step1_state = 'is-active' if modal_step in ['settings', 'upload'] else ('is-complete' if modal_step in ['chapters', 'speakers'] else '') %}
{% set step2_state = 'is-active' if modal_step == 'chapters' else ('is-complete' if modal_step == 'speakers' else '') %}
{% set step3_state = 'is-active' if modal_step == 'speakers' else '' %}
<div class="modal" data-role="upload-modal" hidden>
<div class="modal__overlay" data-role="upload-modal-close" tabindex="-1"></div>
<div class="modal__content card card--modal wizard-card" role="dialog" aria-modal="true" aria-labelledby="upload-modal-title">
<header class="modal__header wizard-card__header">
<div class="wizard-card__headline">
<nav class="step-indicator" aria-label="Audiobook workflow">
<span class="step-indicator__item {{ step1_state }}">
<span class="step-indicator__index">1</span>
<span class="step-indicator__label">Upload &amp; settings</span>
</span>
<span class="step-indicator__item {{ step2_state }}">
<span class="step-indicator__index">2</span>
<span class="step-indicator__label">Chapters</span>
</span>
{% if is_multi %}
<span class="step-indicator__item {{ step3_state }}">
<span class="step-indicator__index">3</span>
<span class="step-indicator__label">Speakers</span>
</span>
{% endif %}
</nav>
<p class="modal__eyebrow">Step 1 of 3</p>
<h2 class="modal__title" id="upload-modal-title">Upload &amp; settings</h2>
<p class="hint">Choose your source file or paste text, then set the defaults used for chapter analysis and speaker casting.</p>
</div>
<div class="wizard-card__aside">
<button type="button" class="icon-button wizard-card__close" data-role="upload-modal-close" aria-label="Close upload settings"></button>
{% if pending %}
<p class="wizard-card__filename" title="{{ pending.original_filename }}">{{ pending.original_filename }}</p>
{% endif %}
</div>
</header>
<form action="{{ url_for('web.enqueue_job') if not readonly else '#' }}"
method="post"
{% if not readonly %}enctype="multipart/form-data"{% endif %}
class="upload-form">
<div class="modal__body wizard-card__body upload-form__sections">
<section class="form-section">
<h3 class="form-section__title">Manuscript &amp; subtitles</h3>
<div class="form-section__layout">
<div class="field-grid field-grid--two">
<div class="field field--file">
<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 '' }}>
{% if pending %}
<p class="hint">Current file: {{ pending.original_filename }}</p>
{% 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 class="field-grid field-grid--two">
<div class="field">
<label for="subtitle_mode">Subtitle mode</label>
<select id="subtitle_mode" name="subtitle_mode" {{ 'disabled' if readonly else '' }}>
{% set subtitle_options = ['Disabled', 'Sentence', 'Sentence + Comma', 'Sentence + Highlighting'] %}
{% for option in subtitle_options %}
<option value="{{ option }}" {% if subtitle_value == option %}selected{% endif %}>{{ option }}</option>
{% endfor %}
{% for i in range(1, 11) %}
{% set label = i ~ ' ' ~ ('word' if i == 1 else 'words') %}
<option value="{{ label }}" {% if subtitle_value == label %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="field field--stack">
<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>
</section>
<section class="form-section">
<h3 class="form-section__title">Narrator defaults</h3>
<div class="form-section__layout form-section__layout--split">
<div class="form-section__group">
<div class="field">
<label for="voice_profile">Voice profile</label>
<select id="voice_profile" name="voice_profile" data-role="voice-profile" {{ 'disabled' if readonly else '' }}>
<option value="__standard" {% if profile_value == '__standard' %}selected{% endif %}>Standard voice</option>
<option value="__formula" {% if profile_value == '__formula' %}selected{% endif %}>Custom voice formula</option>
{% if options.voice_profile_options %}
<optgroup label="Saved mixes">
{% for profile in options.voice_profile_options %}
<option value="{{ profile.name }}" data-language="{{ profile.language }}" data-formula="{{ profile.formula|e }}" {% if profile_value == profile.name %}selected{% endif %}>{{ profile.name }}{% if profile.language %} ({{ profile.language|upper }}){% endif %}</option>
{% endfor %}
</optgroup>
{% endif %}
</select>
</div>
<div class="field" data-role="voice-field" {% if profile_value != '__standard' %}hidden aria-hidden="true"{% endif %}>
<label for="voice">Voice</label>
<select id="voice" name="voice" data-role="voice-select" data-default="{{ narrator_voice or settings_dict.get('default_voice', '') }}" {{ 'disabled' if readonly else '' }}>
{% for voice in options.voices %}
<option value="{{ voice }}" {% if narrator_voice == voice and profile_value == '__standard' %}selected{% endif %}>{{ voice }}</option>
{% endfor %}
</select>
</div>
<div class="field" data-conditional="formula" data-role="formula-field" {% if profile_value != '__formula' %}hidden aria-hidden="true"{% endif %}>
<label for="voice_formula">Custom voice formula</label>
<input type="text" id="voice_formula" name="voice_formula" placeholder="af_nova*0.4+am_liam*0.6" data-role="voice-formula" value="{{ voice_formula_value }}" {{ 'disabled' if readonly else '' }}>
</div>
</div>
<div class="form-section__group">
<div class="field field--slider">
<label for="speed">Speed <span class="tag" id="speed_value">{{ speed_display }}×</span></label>
<input type="range" id="speed" name="speed" min="0.6" max="1.4" step="0.05" value="{{ speed_display }}" {{ 'disabled' if readonly else '' }} oninput="document.getElementById('speed_value').textContent = Number.parseFloat(this.value).toFixed(2) + '×';">
</div>
<div class="field field--with-action field--preview" data-role="voice-preview">
<div class="field__label-row">
<span class="field__label">Preview</span>
<button type="button" class="button button--ghost button--small" data-role="voice-preview-button" {{ 'disabled' if readonly else '' }}>Preview voice</button>
</div>
<p class="hint field__status" data-role="voice-preview-status" aria-live="polite" hidden></p>
<audio class="voice-preview__audio" data-role="voice-preview-audio" controls preload="none" hidden></audio>
</div>
</div>
</div>
</section>
<section class="form-section">
<h3 class="form-section__title">Speakers &amp; casting</h3>
<div class="form-section__layout">
<div class="field-grid field-grid--two">
<div class="field field--stack">
<label for="speaker_config">Speaker preset</label>
<select id="speaker_config" name="speaker_config" {{ 'disabled' if readonly else '' }}>
<option value="">None</option>
{% for config in options.speaker_configs %}
<option value="{{ config.name }}" {% if selected_config == config.name %}selected{% endif %}>{{ config.name }} · {{ config.speakers|length }} speaker{% if config.speakers|length != 1 %}s{% endif %}</option>
{% endfor %}
</select>
<p class="hint">Reuse a saved roster to keep character voices consistent.</p>
</div>
<div class="field">
<label for="speaker_mode">Speaker mode</label>
<select id="speaker_mode" name="speaker_mode" {{ 'disabled' if readonly else '' }}>
{% for option in options.speaker_modes %}
<option value="{{ option.value }}" {% if speaker_mode_value == option.value %}selected{% endif %}>{{ option.label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="field-grid field-grid--two">
<div class="field">
<label for="chunk_level">Chunk granularity</label>
<select id="chunk_level" name="chunk_level" {{ 'disabled' if readonly else '' }}>
{% for option in options.chunk_levels %}
<option value="{{ option.value }}" {% if chunk_level_value == option.value %}selected{% endif %}>{{ option.label }}</option>
{% endfor %}
</select>
<p class="hint">Paragraphs work well for long-form narration; sentences give finer subtitle sync.</p>
</div>
<div class="field">
<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 '' }}>
<p class="hint">Speakers appearing less often fall back to the narrator voice.</p>
</div>
</div>
<div class="field">
<label for="chapter_intro_delay">Pause after chapter titles (seconds)</label>
<input type="number" step="0.1" min="0" id="chapter_intro_delay" name="chapter_intro_delay" value="{{ '%.2f'|format(chapter_delay_value) }}" {{ 'disabled' if readonly else '' }}>
<p class="hint">Set to 0 to disable the pause after speaking each chapter title.</p>
</div>
</div>
</section>
</div>
<footer class="modal__footer wizard-card__footer">
<button type="button" class="button button--ghost" data-role="upload-modal-close">Cancel</button>
{% if readonly %}
<button type="button" class="button" data-role="upload-modal-close">Close</button>
{% else %}
<button type="submit" class="button">Chapter Selection</button>
{% endif %}
</footer>
</form>
</div>
</div>
+13 -4
View File
@@ -10,10 +10,12 @@
<header class="modal__header wizard-card__header">
<div class="wizard-card__headline">
<nav class="step-indicator" aria-label="Audiobook workflow">
<a class="step-indicator__item is-complete" href="{{ url_for('web.index') }}">
<button type="button"
class="step-indicator__item is-complete"
data-role="open-upload-modal">
<span class="step-indicator__index">1</span>
<span class="step-indicator__label">Upload &amp; settings</span>
</a>
</button>
<a class="step-indicator__item is-active"
aria-current="step"
href="{{ url_for('web.prepare_job', pending_id=pending.id, step='chapters') }}">
@@ -90,11 +92,15 @@
{% if is_enabled %}checked{% endif %}>
<span>Chapter {{ loop.index }} · {{ chapter.title }}</span>
</label>
<p class="chapter-card__snippet">
<p class="chapter-card__snippet"
data-role="chapter-snippet"
{% if not is_enabled %}hidden aria-hidden="true"{% else %}aria-hidden="false"{% endif %}>
{{ excerpt }}{% if raw_excerpt|length > 240 %}…{% endif %}
</p>
</header>
<div class="chapter-card__details" data-role="chapter-details">
<div class="chapter-card__details"
data-role="chapter-details"
{% if not is_enabled %}hidden aria-hidden="true"{% else %}aria-hidden="false"{% endif %}>
<div class="chapter-card__field">
<label for="chapter-{{ loop.index0 }}-title">Title</label>
<input type="text" id="chapter-{{ loop.index0 }}-title" name="chapter-{{ loop.index0 }}-title" value="{{ chapter.title }}">
@@ -165,12 +171,15 @@
<form method="post" action="{{ url_for('web.cancel_pending_job', pending_id=pending.id) }}" id="cancel-form"></form>
</div>
</section>
{% include "partials/upload_modal.html" with pending=pending, readonly=True, active_step='chapters' %}
{% endblock %}
{% block scripts %}
{{ super() }}
<script id="voice-sample-texts" type="application/json">{{ options.sample_voice_texts | tojson }}</script>
<script id="voice-catalog-data" type="application/json">{{ options.voice_catalog | tojson }}</script>
<script id="voice-language-map" type="application/json">{{ options.languages | tojson }}</script>
<script type="module" src="{{ url_for('static', filename='speakers.js') }}"></script>
<script type="module" src="{{ url_for('static', filename='prepare.js') }}"></script>
<script type="module" src="{{ url_for('static', filename='dashboard.js') }}"></script>
{% endblock %}
+7 -2
View File
@@ -14,10 +14,12 @@
<header class="modal__header wizard-card__header">
<div class="wizard-card__headline">
<nav class="step-indicator" aria-label="Audiobook workflow">
<a class="step-indicator__item is-complete" href="{{ url_for('web.index') }}">
<button type="button"
class="step-indicator__item is-complete"
data-role="open-upload-modal">
<span class="step-indicator__index">1</span>
<span class="step-indicator__label">Upload &amp; settings</span>
</a>
</button>
<a class="step-indicator__item is-complete"
href="{{ url_for('web.prepare_job', pending_id=pending.id, step='chapters') }}">
<span class="step-indicator__index">2</span>
@@ -409,12 +411,15 @@
</div>
</div>
</section>
{% include "partials/upload_modal.html" with pending=pending, readonly=True, active_step='speakers' %}
{% endblock %}
{% block scripts %}
{{ super() }}
<script id="voice-sample-texts" type="application/json">{{ options.sample_voice_texts | tojson }}</script>
<script id="voice-catalog-data" type="application/json">{{ options.voice_catalog | tojson }}</script>
<script id="voice-language-map" type="application/json">{{ options.languages | tojson }}</script>
<script type="module" src="{{ url_for('static', filename='speakers.js') }}"></script>
<script type="module" src="{{ url_for('static', filename='prepare.js') }}"></script>
<script type="module" src="{{ url_for('static', filename='dashboard.js') }}"></script>
{% endblock %}