diff --git a/abogen/web/routes/entities.py b/abogen/web/routes/entities.py index 88fd768..0bc62ef 100644 --- a/abogen/web/routes/entities.py +++ b/abogen/web/routes/entities.py @@ -1,5 +1,5 @@ from typing import Mapping -from flask import Blueprint, request, jsonify, abort, render_template +from flask import Blueprint, request, jsonify, abort, render_template, redirect, url_for from flask.typing import ResponseReturnValue from abogen.web.routes.utils.service import require_pending_job, get_service @@ -12,6 +12,10 @@ from abogen.web.routes.utils.entity import ( ) from abogen.web.routes.utils.settings import coerce_int, load_settings from abogen.web.routes.utils.voice import template_options +from abogen.pronunciation_store import ( + delete_override as delete_pronunciation_override, + save_override as save_pronunciation_override, +) entities_bp = Blueprint("entities", __name__) @@ -96,6 +100,29 @@ def search_candidates(pending_id: str) -> ResponseReturnValue: results = search_manual_override_candidates(pending, query, limit=limit_value) return jsonify({"query": query, "limit": limit_value, "results": results}) +@entities_bp.post("/overrides") +def upsert_global_override() -> ResponseReturnValue: + payload = request.form + action = payload.get("action", "save") + lang = payload.get("lang", "en") + token = payload.get("token", "").strip() + + if action == "delete": + if token: + delete_pronunciation_override(token=token, language=lang) + else: + pronunciation = payload.get("pronunciation", "").strip() + voice = payload.get("voice", "").strip() + if token: + save_pronunciation_override( + token=token, + pronunciation=pronunciation, + voice=voice or None, + language=lang + ) + + return redirect(url_for("entities.entities_page", lang=lang)) + @entities_bp.get("/") def entities_page() -> str: settings = load_settings() diff --git a/abogen/web/routes/jobs.py b/abogen/web/routes/jobs.py index 688fff8..6d91c1d 100644 --- a/abogen/web/routes/jobs.py +++ b/abogen/web/routes/jobs.py @@ -275,6 +275,13 @@ def stream_logs(job_id: str) -> ResponseReturnValue: return Response(generate(), mimetype="text/event-stream") +@jobs_bp.get("//reader") +def job_reader(job_id: str) -> ResponseReturnValue: + job = get_service().get_job(job_id) + if not job: + abort(404) + return render_template("reader_embed.html", job=job) + @jobs_bp.get("/queue") def queue_page() -> str: return render_template( diff --git a/abogen/web/routes/settings.py b/abogen/web/routes/settings.py index 701ac59..b18d328 100644 --- a/abogen/web/routes/settings.py +++ b/abogen/web/routes/settings.py @@ -14,12 +14,21 @@ from abogen.web.routes.utils.voice import template_options settings_bp = Blueprint("settings", __name__) +_NORMALIZATION_SAMPLES = { + "apostrophes": "It's a beautiful day, isn't it? 'Yes,' she said, 'it is.'", + "currency": "The price is $10.50, but it was £8.00 yesterday.", + "dates": "On 2023-01-01, we celebrated the new year.", + "numbers": "There are 123 apples and 456 oranges.", + "abbreviations": "Dr. Smith lives on Elm St. near the U.S. border.", +} + @settings_bp.get("/") def settings_page() -> str: return render_template( "settings.html", settings=load_settings(), options=template_options(), + normalization_samples=_NORMALIZATION_SAMPLES, ) @settings_bp.post("/update") diff --git a/abogen/web/routes/voices.py b/abogen/web/routes/voices.py index 47813d6..c66d1bc 100644 --- a/abogen/web/routes/voices.py +++ b/abogen/web/routes/voices.py @@ -23,7 +23,7 @@ voices_bp = Blueprint("voices", __name__) @voices_bp.get("/") def voice_profiles() -> ResponseReturnValue: - return render_template("voices.html") + return render_template("voices.html", options=template_options()) @voices_bp.post("/test") def test_voice() -> ResponseReturnValue: @@ -76,3 +76,65 @@ def delete_speaker_config() -> ResponseReturnValue: delete_config(name) return jsonify({"status": "deleted", "configs": list_configs()}) + +@voices_bp.route("/presets", methods=["GET", "POST"]) +def speaker_configs_page() -> ResponseReturnValue: + configs = load_configs() + editing_name = request.args.get("config") + message = None + error = None + + if request.method == "POST": + try: + name = request.form.get("config_name", "").strip() + if not name: + raise ValueError("Preset name is required") + + language = request.form.get("config_language", "en") + + speakers = [] + row_keys = request.form.getlist("speaker_rows") + for key in row_keys: + s_id = request.form.get(f"speaker-{key}-id", key) + label = request.form.get(f"speaker-{key}-label", "") + gender = request.form.get(f"speaker-{key}-gender", "unknown") + voice = request.form.get(f"speaker-{key}-voice", "") + + if label: + speakers.append({ + "id": s_id, + "label": label, + "gender": gender, + "voice": voice or None + }) + + config = { + "name": name, + "language": language, + "speakers": speakers, + "version": 1 + } + + configs[name] = config + save_configs(configs) + message = f"Preset '{name}' saved." + editing_name = name + except Exception as e: + error = str(e) + + editing = configs.get(editing_name, {}) if editing_name else {} + + return render_template( + "speakers.html", + options=template_options(), + configs=configs.values(), + editing_name=editing_name, + editing=editing, + message=message, + error=error + ) + +@voices_bp.post("/presets//delete") +def delete_speaker_config_named(name: str) -> ResponseReturnValue: + delete_config(name) + return redirect(url_for("voices.speaker_configs_page")) diff --git a/abogen/web/templates/entities.html b/abogen/web/templates/entities.html index 3a3315b..5b37e87 100644 --- a/abogen/web/templates/entities.html +++ b/abogen/web/templates/entities.html @@ -49,11 +49,11 @@
- Reset + Reset
-

Looking for saved speaker rosters instead? Manage speaker presets.

+

Looking for saved speaker rosters instead? Manage speaker presets.

@@ -85,7 +85,7 @@ id="override-create-form" class="form-section entity-create" method="post" - action="{{ url_for('web.entities_override_update') }}" + action="{{ url_for('entities.upsert_global_override') }}" >

Add manual override

Create pronunciations or assign default voices without preparing a job.

@@ -243,7 +243,7 @@ id="{{ form_id }}" class="overrides-table__form" method="post" - action="{{ url_for('web.entities_override_update') }}" + action="{{ url_for('entities.upsert_global_override') }}" > diff --git a/abogen/web/templates/index.html b/abogen/web/templates/index.html index 696b7aa..525eded 100644 --- a/abogen/web/templates/index.html +++ b/abogen/web/templates/index.html @@ -14,7 +14,10 @@ {% if opds_available %} - Browse Calibre library +
+

Upload an EPUB or text file to begin.

+ Browse Calibre library +
{% endif %} diff --git a/abogen/web/templates/job_detail.html b/abogen/web/templates/job_detail.html index 1ab4a54..a7bfefc 100644 --- a/abogen/web/templates/job_detail.html +++ b/abogen/web/templates/job_detail.html @@ -149,7 +149,7 @@
{% endif %} -
+
{% include "partials/logs.html" %}
{% endblock %} diff --git a/abogen/web/templates/partials/jobs.html b/abogen/web/templates/partials/jobs.html index 31699a1..3fe76a4 100644 --- a/abogen/web/templates/partials/jobs.html +++ b/abogen/web/templates/partials/jobs.html @@ -62,30 +62,30 @@
  • - {{ job.original_filename }} + {{ job.original_filename }}
    Finished {{ job.finished_at|datetimeformat }}
    {{ job.status.value|title }}
  • {% endfor %} diff --git a/abogen/web/templates/partials/new_job_step_entities.html b/abogen/web/templates/partials/new_job_step_entities.html index e67734b..9a78d73 100644 --- a/abogen/web/templates/partials/new_job_step_entities.html +++ b/abogen/web/templates/partials/new_job_step_entities.html @@ -1,19 +1,19 @@ {% set embed_scripts = embed_scripts if embed_scripts is defined else True %} {% set recognition_enabled = settings.enable_entity_recognition if settings is defined and settings.enable_entity_recognition is not none else True %}
    diff --git a/abogen/web/templates/prepare_speakers.html b/abogen/web/templates/prepare_speakers.html index 822c72e..bd6988f 100644 --- a/abogen/web/templates/prepare_speakers.html +++ b/abogen/web/templates/prepare_speakers.html @@ -92,17 +92,17 @@ {% endif %} -