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:
JB
2025-10-10 16:12:51 -07:00
parent f35b35c7a9
commit be37f03109
9 changed files with 673 additions and 190 deletions
+101 -89
View File
@@ -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 &amp; 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 &amp; 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>