feat: Implement logging filter for successful HTTP access logs and enhance modal styling for better usability

This commit is contained in:
JB
2025-10-09 05:51:45 -07:00
parent b0875c7486
commit 63f9474741
4 changed files with 112 additions and 146 deletions
+23
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import atexit import atexit
import logging
import os import os
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any, Optional
@@ -13,6 +14,23 @@ from .conversion_runner import run_conversion_job
from .service import build_service from .service import build_service
class _SuppressSuccessfulAccessFilter(logging.Filter):
"""Filter out successful (HTTP 200) werkzeug access logs."""
def filter(self, record: logging.LogRecord) -> bool: # pragma: no cover - small utility
try:
message = record.getMessage()
except Exception: # pragma: no cover - defensive
return True
# Werkzeug access logs include the status code near the end, e.g.
# "GET /path HTTP/1.1" 200 -
# Treat any 2xx response as success to suppress.
return " 200 " not in message and " 201 " not in message and " 204 " not in message
_access_log_filter_attached = False
def _default_dirs() -> tuple[Path, Path]: def _default_dirs() -> tuple[Path, Path]:
uploads_override = os.environ.get("ABOGEN_UPLOAD_ROOT") uploads_override = os.environ.get("ABOGEN_UPLOAD_ROOT")
outputs_override = os.environ.get("ABOGEN_OUTPUT_ROOT") outputs_override = os.environ.get("ABOGEN_OUTPUT_ROOT")
@@ -64,6 +82,11 @@ def create_app(config: Optional[dict[str, Any]] = None) -> Flask:
atexit.register(service.shutdown) atexit.register(service.shutdown)
global _access_log_filter_attached
if not _access_log_filter_attached:
logging.getLogger("werkzeug").addFilter(_SuppressSuccessfulAccessFilter())
_access_log_filter_attached = True
return app return app
-41
View File
@@ -10,12 +10,6 @@ const initDashboard = () => {
const formulaInput = scope.querySelector('[data-role="voice-formula"]'); const formulaInput = scope.querySelector('[data-role="voice-formula"]');
const languageSelect = uploadModal?.querySelector("#language") || document.getElementById("language"); const languageSelect = uploadModal?.querySelector("#language") || document.getElementById("language");
const sourceText = scope.querySelector('[data-role="source-text"]');
const previewEl = scope.querySelector('[data-role="text-preview"]');
const previewBody = scope.querySelector('[data-role="preview-body"]');
const charCountEl = scope.querySelector('[data-role="char-count"]');
const wordCountEl = scope.querySelector('[data-role="word-count"]');
let lastTrigger = null; let lastTrigger = null;
const openUploadModal = (trigger) => { const openUploadModal = (trigger) => {
@@ -156,36 +150,6 @@ const initDashboard = () => {
} }
}; };
const updatePreview = () => {
if (!sourceText || !previewBody || !charCountEl || !wordCountEl) {
return;
}
const raw = sourceText.value || "";
const trimmed = raw.trim();
const charCount = raw.length;
const wordCount = trimmed ? trimmed.split(/\s+/).length : 0;
const charLabel = `${charCount.toLocaleString()} ${charCount === 1 ? "character" : "characters"}`;
const wordLabel = `${wordCount.toLocaleString()} ${wordCount === 1 ? "word" : "words"}`;
charCountEl.textContent = charLabel;
wordCountEl.textContent = wordLabel;
if (!trimmed) {
previewBody.textContent = "Paste text to see a live preview and character count.";
if (previewEl) {
previewEl.setAttribute("data-state", "empty");
}
return;
}
const snippet = trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}` : trimmed;
previewBody.textContent = snippet;
if (previewEl) {
previewEl.setAttribute("data-state", "ready");
}
};
if (profileSelect) { if (profileSelect) {
const hasSaved = selectFirstProfileIfAvailable(); const hasSaved = selectFirstProfileIfAvailable();
profileSelect.addEventListener("change", updateVoiceControls); profileSelect.addEventListener("change", updateVoiceControls);
@@ -196,11 +160,6 @@ const initDashboard = () => {
} else { } else {
hydrateDefaultVoice(); hydrateDefaultVoice();
} }
if (sourceText) {
sourceText.addEventListener("input", updatePreview);
updatePreview();
}
}; };
if (document.readyState === "loading") { if (document.readyState === "loading") {
+3
View File
@@ -150,6 +150,7 @@ body {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 300; z-index: 300;
overflow-y: auto;
} }
.modal[data-open="true"] { .modal[data-open="true"] {
@@ -211,6 +212,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
flex: 1 1 auto;
} }
.modal__footer { .modal__footer {
@@ -246,6 +248,7 @@ body {
padding: 1.5rem 1.75rem 1.75rem; padding: 1.5rem 1.75rem 1.75rem;
overflow-y: auto; overflow-y: auto;
max-height: calc(90vh - 150px); max-height: calc(90vh - 150px);
flex: 1 1 auto;
} }
.voice-browser__filters, .voice-browser__filters,
+86 -105
View File
@@ -39,112 +39,93 @@
<form action="{{ url_for('web.enqueue_job') }}" method="post" enctype="multipart/form-data" class="upload-form"> <form action="{{ url_for('web.enqueue_job') }}" method="post" enctype="multipart/form-data" class="upload-form">
<div class="modal__body"> <div class="modal__body">
<div class="grid grid--two form-grid"> <div class="grid grid--two form-grid">
<div class="grid"> <div class="field field--file">
<div class="field field--file"> <label for="source_file">Source File</label>
<label for="source_file">Source File</label> <input type="file" id="source_file" name="source_file" accept=".txt,.pdf,.epub,.md,.markdown">
<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 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 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> </div>
<div class="grid"> <div class="field">
<div class="field field--full"> <label for="language">Language</label>
<label for="source_text">Or Paste Text Directly</label> <select id="language" name="language">
<textarea id="source_text" name="source_text" rows="12" placeholder="Drop some text here if you don't have a file handy..." data-role="source-text"></textarea> {% for key, label in options.languages.items() %}
</div> <option value="{{ key }}">{{ label }}</option>
<div class="text-preview" data-role="text-preview" aria-live="polite"> {% endfor %}
<div class="text-preview__header"> </select>
<h2>Preview</h2> </div>
<div class="text-preview__meta"> <div class="field">
<span data-role="char-count">0 characters</span> <label for="voice_profile">Voice Profile</label>
<span>·</span> <select id="voice_profile" name="voice_profile" data-role="voice-profile">
<span data-role="word-count">0 words</span> <option value="__standard" {% if not options.voice_profile_options %}selected{% endif %}>Standard Voice</option>
</div> <option value="__formula">Custom Voice Formula</option>
</div> {% if options.voice_profile_options %}
<pre class="text-preview__body" data-role="preview-body">Paste text to see a live preview and character count.</pre> <optgroup label="Saved mixes">
</div> {% 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 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>
</div> </div>
</div> </div>