Files
abogen/abogen/web/templates/settings.html
T

328 lines
18 KiB
HTML

{% extends "base.html" %}
{% block title %}abogen · Settings{% endblock %}
{% block content %}
<section class="card settings">
<h1 class="card__title">Application Settings</h1>
<p class="tag">Settings apply to new jobs you queue from the dashboard.</p>
{% if saved %}
<div class="alert alert--success">Settings saved successfully.</div>
{% endif %}
<div class="settings-layout">
<nav class="settings-nav" aria-label="Settings sections">
<button type="button" class="settings-nav__item is-active" data-section="narration">Narration</button>
<button type="button" class="settings-nav__item" data-section="audio">Audio &amp; Delivery</button>
<button type="button" class="settings-nav__item" data-section="subtitles">Subtitles &amp; Text</button>
<button type="button" class="settings-nav__item" data-section="performance">Performance</button>
<button type="button" class="settings-nav__item{% if not llm_ready %} is-disabled{% endif %}" data-section="llm">LLM</button>
<button type="button" class="settings-nav__item" data-section="normalization">Text Normalization</button>
</nav>
<form action="{{ url_for('web.settings_page') }}" method="post" class="settings__form">
<div class="settings-panels">
<section class="settings-panel is-active" data-section="narration">
<fieldset class="settings__section">
<legend>Narration Defaults</legend>
<div class="field">
<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="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 field--wide">
<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 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 field--wide">
<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>
</section>
<section class="settings-panel" data-section="audio">
<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>
{% endfor %}
</select>
</div>
<div class="field">
<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>
{% endfor %}
</select>
<p class="tag">Default output: <code>{{ default_output_dir }}</code></p>
</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 field--choices">
<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>
</label>
<label class="toggle-pill">
<input type="checkbox" name="merge_chapters_at_end" value="true" {% if settings.merge_chapters_at_end %}checked{% endif %}>
<span>Also Create Merged Audiobook</span>
</label>
<label class="toggle-pill">
<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>
<div class="field field--stack">
<label class="toggle-pill">
<input type="checkbox" name="read_title_intro" value="true" {% if settings.read_title_intro %}checked{% endif %}>
<span>Read Book Title Before Narration</span>
</label>
<p class="hint">When enabled, the narrator speaks the title, optional subtitle, and author names before chapter one.</p>
</div>
<div class="field field--stack">
<label class="toggle-pill">
<input type="checkbox" name="auto_prefix_chapter_titles" value="true" {% if settings.auto_prefix_chapter_titles %}checked{% endif %}>
<span>Add "Chapter" before numeric chapter titles</span>
</label>
<p class="hint">Ensures the spoken chapter heading starts with "Chapter" when source titles begin with only a number or numeral.</p>
</div>
</fieldset>
</section>
<section class="settings-panel" data-section="subtitles">
<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>
</fieldset>
</section>
<section class="settings-panel" data-section="performance">
<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>
</section>
<section class="settings-panel" data-section="llm">
<fieldset class="settings__section">
<legend>Endpoint</legend>
<div class="field">
<label for="llm_base_url">Base URL</label>
<input type="url" id="llm_base_url" name="llm_base_url" value="{{ settings.llm_base_url }}" placeholder="https://localhost:11434/v1">
<p class="hint">Point to an OpenAI-compatible endpoint such as Ollama or a proxy.</p>
</div>
<div class="field">
<label for="llm_api_key">API Key</label>
<input type="text" id="llm_api_key" name="llm_api_key" value="{{ settings.llm_api_key }}" autocomplete="off" placeholder="ollama">
<p class="hint">Leave blank or use <code>ollama</code> for local servers that do not require keys.</p>
</div>
<div class="field field--inline">
<div class="field__group">
<label for="llm_model">Default Model</label>
<select id="llm_model" name="llm_model" data-current-model="{{ settings.llm_model }}">
{% if settings.llm_model %}
<option value="{{ settings.llm_model }}" selected>{{ settings.llm_model }}</option>
{% else %}
<option value="" selected disabled>Select a model</option>
{% endif %}
</select>
</div>
<div class="field__group">
<label for="llm_timeout">Timeout (seconds)</label>
<input type="number" step="1" min="1" id="llm_timeout" name="llm_timeout" value="{{ settings.llm_timeout }}">
</div>
<button type="button" class="button button--ghost" data-action="llm-refresh-models">Refresh models</button>
</div>
</fieldset>
<fieldset class="settings__section">
<legend>Normalization Prompt</legend>
<div class="field">
<label for="llm_prompt">Prompt Template</label>
<textarea id="llm_prompt" name="llm_prompt" rows="6">{{ settings.llm_prompt }}</textarea>
<p class="hint">Use <code>{{ '{{sentence}}' }}</code> for the active sentence. <code>{{ '{{paragraph}}' }}</code> remains available for legacy prompts.</p>
</div>
<div class="field field--choices">
<span class="field__label">Context Mode</span>
<div class="choices choices--inline">
{% for option in llm_context_options %}
<label class="radio-pill">
<input type="radio" name="llm_context_mode" value="{{ option.value }}" {% if settings.llm_context_mode == option.value %}checked{% endif %}>
<span>{{ option.label }}</span>
</label>
{% endfor %}
</div>
</div>
<div class="preview-card" data-preview="llm">
<label for="llm_preview_text">Try the prompt</label>
<textarea id="llm_preview_text" rows="3">I've been waiting all day.</textarea>
<div class="preview-card__actions">
<button type="button" class="button" data-action="llm-preview">Preview</button>
<span class="preview-card__status" data-role="llm-preview-status"></span>
</div>
<div class="preview-card__output" data-role="llm-preview-output"></div>
</div>
</fieldset>
</section>
<section class="settings-panel" data-section="normalization">
<fieldset class="settings__section">
<legend>Normalization Rules</legend>
<div class="field field--choices">
<label class="toggle-pill">
<input type="checkbox" name="normalization_numbers" value="true" {% if settings.normalization_numbers %}checked{% endif %}>
<span>Convert grouped numbers to words</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_titles" value="true" {% if settings.normalization_titles %}checked{% endif %}>
<span>Expand titles and suffixes (Dr., St., Jr., …)</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_terminal" value="true" {% if settings.normalization_terminal %}checked{% endif %}>
<span>Ensure sentences end with terminal punctuation</span>
</label>
<label class="toggle-pill">
<input type="checkbox" name="normalization_phoneme_hints" value="true" {% if settings.normalization_phoneme_hints %}checked{% endif %}>
<span>Add phoneme hints for possessives</span>
</label>
</div>
<div class="field">
<span class="field__label">Apostrophe strategy</span>
<div class="choices choices--inline">
{% for option in apostrophe_modes %}
<label class="radio-pill">
<input type="radio" name="normalization_apostrophe_mode" value="{{ option.value }}" {% if settings.normalization_apostrophe_mode == option.value %}checked{% endif %}>
<span>{{ option.label }}</span>
</label>
{% endfor %}
</div>
{% if settings.normalization_apostrophe_mode == 'llm' and not llm_ready %}
<p class="hint hint--warning">Configure the LLM connection before using it for audiobook runs.</p>
{% endif %}
</div>
</fieldset>
<fieldset class="settings__section">
<legend>Sample &amp; Preview</legend>
<div class="field field--inline">
<div class="field__group">
<label for="normalization_sample_select">Sample</label>
<select id="normalization_sample_select">
{% for key, text in normalization_samples.items() %}
<option value="{{ text }}" {% if loop.first %}selected{% endif %}>{{ key|capitalize }}</option>
{% endfor %}
</select>
</div>
<div class="field__group">
<label for="normalization_sample_voice">Voice</label>
<select id="normalization_sample_voice">
{% for voice in options.voices %}
<option value="{{ voice }}" {% if settings.default_voice == voice %}selected{% endif %}>{{ voice }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="field">
<label for="normalization_sample_text">Sample text</label>
<textarea id="normalization_sample_text" rows="4">{{ normalization_samples['apostrophes'] }}</textarea>
</div>
<div class="preview-card" data-preview="normalization">
<div class="preview-card__actions">
<button type="button" class="button" data-action="normalization-preview">Preview with current settings</button>
<span class="preview-card__status" data-role="normalization-preview-status"></span>
</div>
<pre class="preview-card__output" data-role="normalization-preview-output"></pre>
<audio controls class="preview-card__audio" data-role="normalization-preview-audio" hidden></audio>
</div>
</fieldset>
</section>
</div>
<div class="settings__actions">
<button type="submit" class="button">Save Settings</button>
</div>
</form>
</div>
</section>
{% endblock %}
{% block scripts %}
{{ super() }}
<script type="module" src="{{ url_for('static', filename='settings.js') }}"></script>
{% endblock %}