mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-19 22:00:28 +02:00
Add number normalization and enhance UI for voice preview
- Implemented number conversion to words for grouped numbers in text normalization. - Added configuration options for number conversion in ApostropheConfig. - Updated the normalize_apostrophes function to include number normalization. - Enhanced the dashboard UI with new sections for manuscript and narrator defaults. - Improved voice preview functionality with better handling of audio playback and status updates. - Refactored HTML templates for cleaner structure and added new fields for speaker settings. - Updated CSS styles for improved layout and responsiveness. - Added tests to ensure correct spelling of grouped numbers in the normalization process. - Included num2words as a new dependency in pyproject.toml.
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
<a href="{{ url_for('web.index') }}" class="btn{% if endpoint == 'web.index' %} is-active{% endif %}">Dashboard</a>
|
||||
<a href="{{ url_for('web.voice_profiles_page') }}" class="btn{% if endpoint == 'web.voice_profiles_page' %} is-active{% endif %}">Voice Mixer</a>
|
||||
<a href="{{ url_for('web.speaker_configs_page') }}" class="btn{% if endpoint == 'web.speaker_configs_page' %} is-active{% endif %}">Speakers</a>
|
||||
<a href="{{ url_for('web.index', _anchor='queue') }}" class="btn{% if endpoint in ['web.queue_page', 'web.job_detail'] %} is-active{% endif %}">Queue</a>
|
||||
<a href="{{ url_for('web.queue_page') }}" class="btn{% if endpoint in ['web.queue_page', 'web.job_detail'] %} is-active{% endif %}">Queue</a>
|
||||
<a href="{{ url_for('web.settings_page') }}" class="btn{% if endpoint == 'web.settings_page' %} is-active{% endif %}">Settings</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
+136
-97
@@ -22,6 +22,7 @@
|
||||
<p class="card__subtitle">Kick off a fresh conversion with your manuscript or pasted text. You can fine-tune chapters and speakers in the next steps.</p>
|
||||
<div class="card__actions">
|
||||
<button type="button" class="button" data-role="open-upload-modal">Open upload & settings</button>
|
||||
<a class="button button--ghost" href="{{ url_for('web.queue_page') }}">View queue</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -37,97 +38,143 @@
|
||||
<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">
|
||||
<div class="grid grid--two form-grid">
|
||||
<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 class="modal__body upload-form__sections">
|
||||
<section class="form-section">
|
||||
<h3 class="form-section__title">Manuscript & 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>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<h3 class="form-section__title">Speakers & 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>
|
||||
<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 class="field">
|
||||
<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">Optional: reuse a saved roster to keep character voices consistent.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<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">
|
||||
<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">
|
||||
<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">Controls how chapters are split into TTS-ready chunks.</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 class="field">
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
<footer class="modal__footer">
|
||||
<button type="button" class="button button--ghost" data-role="upload-modal-close">Cancel</button>
|
||||
@@ -157,18 +204,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card" id="queue">
|
||||
<div id="jobs-panel"
|
||||
hx-get="{{ url_for('web.jobs_partial') }}"
|
||||
hx-trigger="load, every 3s"
|
||||
hx-target="#jobs-panel"
|
||||
hx-swap="innerHTML">
|
||||
{{ jobs_panel|safe }}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script id="voice-sample-texts" type="application/json">{{ options.sample_voice_texts | tojson }}</script>
|
||||
<script type="module" src="{{ url_for('static', filename='dashboard.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,9 +13,58 @@
|
||||
|
||||
<form action="{{ url_for('web.settings_page') }}" method="post" class="settings__form">
|
||||
<fieldset class="settings__section">
|
||||
<legend>Output Defaults</legend>
|
||||
<legend>Narration Defaults</legend>
|
||||
<div class="field">
|
||||
<label for="output_format">Audio Format</label>
|
||||
<label for="default_voice">Narrator Voice</label>
|
||||
<select id="default_voice" name="default_voice">
|
||||
{% for voice in options.voices %}
|
||||
<option value="{{ voice }}" {% if settings.default_voice == voice %}selected{% endif %}>{{ voice }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="hint">Used whenever “Standard voice” is selected for a new job.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="speaker_mode_default">Speaker Mode</label>
|
||||
<select id="speaker_mode_default" 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 class="field">
|
||||
<label for="chunk_level_default">Chunk Granularity</label>
|
||||
<select id="chunk_level_default" 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>
|
||||
</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 detected fewer times fall back to the narrator voice.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="speaker_pronunciation_sentence">Speaker Pronunciation Preview</label>
|
||||
<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>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="speaker_random_languages">Randomizer Languages</label>
|
||||
{% set selected_languages = settings.speaker_random_languages or [] %}
|
||||
<select id="speaker_random_languages" name="speaker_random_languages" multiple size="6">
|
||||
{% for code, label in options.languages.items() %}
|
||||
<option value="{{ code }}" {% if code in selected_languages %}selected{% endif %}>{{ label }} ({{ code|upper }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="hint">Limits random voice selection for speakers marked as random. Leave empty to allow any language.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="settings__section">
|
||||
<legend>Audio & Delivery</legend>
|
||||
<div class="field">
|
||||
<label for="output_format">Audio Format</label>
|
||||
<select id="output_format" name="output_format">
|
||||
{% for fmt in options.output_formats %}
|
||||
<option value="{{ fmt }}" {% if settings.output_format == fmt %}selected{% endif %}>{{ fmt }}</option>
|
||||
@@ -23,24 +72,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="subtitle_format">Subtitle File Format</label>
|
||||
<select id="subtitle_format" name="subtitle_format">
|
||||
{% for value, text in options.subtitle_formats %}
|
||||
<option value="{{ value }}" {% if settings.subtitle_format == value %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="default_voice">Default Voice</label>
|
||||
<select id="default_voice" name="default_voice">
|
||||
{% for voice in options.voices %}
|
||||
<option value="{{ voice }}" {% if settings.default_voice == voice %}selected{% endif %}>{{ voice }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="hint">Used when “Standard voice” is selected on the dashboard.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="save_mode">Save Location</label>
|
||||
<label for="save_mode">Save Location</label>
|
||||
<select id="save_mode" name="save_mode">
|
||||
{% for location in save_locations %}
|
||||
<option value="{{ location.value }}" {% if settings.save_mode == location.value %}selected{% endif %}>{{ location.label }}</option>
|
||||
@@ -48,19 +80,15 @@
|
||||
</select>
|
||||
<p class="tag">Default output: <code>{{ default_output_dir }}</code></p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="settings__section">
|
||||
<legend>Processing</legend>
|
||||
<div class="field">
|
||||
<label for="separate_chapters_format">Separate Chapter Format</label>
|
||||
<select id="separate_chapters_format" name="separate_chapters_format">
|
||||
{% for fmt in options.separate_formats %}
|
||||
<option value="{{ fmt }}" {% if settings.separate_chapters_format == fmt %}selected{% endif %}>{{ fmt|upper }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field field--choices">
|
||||
<label class="toggle-pill">
|
||||
<input type="checkbox" name="replace_single_newlines" value="true" {% if settings.replace_single_newlines %}checked{% endif %}>
|
||||
<span>Replace Single Newlines</span>
|
||||
</label>
|
||||
<label class="toggle-pill">
|
||||
<input type="checkbox" name="use_gpu" value="true" {% if settings.use_gpu %}checked{% endif %}>
|
||||
<span>Use GPU (When Available)</span>
|
||||
</label>
|
||||
<label class="toggle-pill">
|
||||
<input type="checkbox" name="save_chapters_separately" value="true" {% if settings.save_chapters_separately %}checked{% endif %}>
|
||||
<span>Save Each Chapter Separately</span>
|
||||
@@ -73,72 +101,56 @@
|
||||
<input type="checkbox" name="save_as_project" value="true" {% if settings.save_as_project %}checked{% endif %}>
|
||||
<span>Save as Project With Metadata</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="silence_between_chapters">Silence Between Chapters (Seconds)</label>
|
||||
<input type="number" step="0.5" min="0" id="silence_between_chapters" name="silence_between_chapters" value="{{ settings.silence_between_chapters }}">
|
||||
</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">Inserted between the spoken chapter title and the chapter content. Set to 0 to disable.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="settings__section">
|
||||
<legend>Subtitles & Text</legend>
|
||||
<div class="field">
|
||||
<label for="subtitle_format">Subtitle File Format</label>
|
||||
<select id="subtitle_format" name="subtitle_format">
|
||||
{% for value, text in options.subtitle_formats %}
|
||||
<option value="{{ value }}" {% if settings.subtitle_format == value %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="max_subtitle_words">Max Words Per Subtitle Entry</label>
|
||||
<input type="number" min="1" max="200" id="max_subtitle_words" name="max_subtitle_words" value="{{ settings.max_subtitle_words }}">
|
||||
</div>
|
||||
<div class="field field--choices">
|
||||
<label class="toggle-pill">
|
||||
<input type="checkbox" name="replace_single_newlines" value="true" {% if settings.replace_single_newlines %}checked{% endif %}>
|
||||
<span>Replace Single Newlines</span>
|
||||
</label>
|
||||
<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>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="separate_chapters_format">Separate Chapter Format</label>
|
||||
<select id="separate_chapters_format" name="separate_chapters_format">
|
||||
{% for fmt in options.separate_formats %}
|
||||
<option value="{{ fmt }}" {% if settings.separate_chapters_format == fmt %}selected{% endif %}>{{ fmt|upper }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="chunk_level_default">Chunk Granularity</label>
|
||||
<select id="chunk_level_default" 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>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="speaker_mode_default">Speaker Mode</label>
|
||||
<select id="speaker_mode_default" 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 class="field">
|
||||
<label for="speaker_random_languages">Randomizer Languages</label>
|
||||
{% set selected_languages = settings.speaker_random_languages or [] %}
|
||||
<select id="speaker_random_languages" name="speaker_random_languages" multiple size="6">
|
||||
{% for code, label in options.languages.items() %}
|
||||
<option value="{{ code }}" {% if code in selected_languages %}selected{% endif %}>{{ label }} ({{ code|upper }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="hint">Limits random voice selection for speakers marked as random. Leave unused to allow any language.</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 detected fewer times than this fallback to the narrator voice.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="speaker_pronunciation_sentence">Speaker Pronunciation Preview</label>
|
||||
<input type="text" id="speaker_pronunciation_sentence" name="speaker_pronunciation_sentence" value="{{ settings.speaker_pronunciation_sentence }}" placeholder="This is {{ '{{name}}' }} speaking.">
|
||||
<p class="hint">Sentence template used when previewing name pronunciation. Include <code>{{ '{{name}}' }}</code> where the speaker name should be inserted.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="silence_between_chapters">Silence Between Chapters (Seconds)</label>
|
||||
<input type="number" step="0.5" min="0" id="silence_between_chapters" name="silence_between_chapters" value="{{ settings.silence_between_chapters }}">
|
||||
</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">Inserted between the spoken chapter title and the chapter content. Set to 0 to disable.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="max_subtitle_words">Max Words Per Subtitle Entry</label>
|
||||
<input type="number" min="1" max="200" id="max_subtitle_words" name="max_subtitle_words" value="{{ settings.max_subtitle_words }}">
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="settings__section">
|
||||
<legend>Performance</legend>
|
||||
<div class="field field--choices">
|
||||
<label class="toggle-pill">
|
||||
<input type="checkbox" name="use_gpu" value="true" {% if settings.use_gpu %}checked{% endif %}>
|
||||
<span>Use GPU (when available)</span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="settings__actions">
|
||||
<button type="submit" class="button">Save Settings</button>
|
||||
<button type="submit" class="button">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user