diff --git a/abogen/web/routes/api.py b/abogen/web/routes/api.py index 652d255..bc7226d 100644 --- a/abogen/web/routes/api.py +++ b/abogen/web/routes/api.py @@ -1,4 +1,5 @@ from typing import Any, Dict, Mapping, List, Optional +import base64 from flask import Blueprint, request, jsonify, send_file from flask.typing import ResponseReturnValue @@ -6,13 +7,14 @@ from flask.typing import ResponseReturnValue from abogen.web.routes.utils.settings import ( load_settings, coerce_float, + coerce_bool, ) from abogen.voice_profiles import ( load_profiles, save_profiles, delete_profile, ) -from abogen.web.routes.utils.preview import synthesize_preview +from abogen.web.routes.utils.preview import synthesize_preview, generate_preview_audio from abogen.normalization_settings import ( build_llm_configuration, build_apostrophe_config, @@ -211,3 +213,37 @@ def api_normalization_preview() -> ResponseReturnValue: "text": sample_text, "normalized_text": normalized_text, }) + +@api_bp.post("/entity-pronunciation/preview") +def api_entity_pronunciation_preview() -> ResponseReturnValue: + payload = request.get_json(force=True, silent=True) or {} + token = payload.get("token", "").strip() + pronunciation = payload.get("pronunciation", "").strip() + voice = payload.get("voice", "").strip() + language = payload.get("language", "a").strip() + + if not token and not pronunciation: + return jsonify({"error": "Token or pronunciation required"}), 400 + + text_to_speak = pronunciation if pronunciation else token + + if not voice: + settings = load_settings() + voice = settings.get("default_voice", "af_heart") + + try: + # Check GPU setting + settings = load_settings() + use_gpu = coerce_bool(settings.get("use_gpu"), False) + + audio_bytes = generate_preview_audio( + text=text_to_speak, + voice_spec=voice, + language=language, + speed=1.0, + use_gpu=use_gpu, + ) + audio_base64 = base64.b64encode(audio_bytes).decode("utf-8") + return jsonify({"audio_base64": audio_base64}) + except Exception as e: + return jsonify({"error": str(e)}), 400 diff --git a/abogen/web/routes/entities.py b/abogen/web/routes/entities.py index c2bf278..88fd768 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 +from flask import Blueprint, request, jsonify, abort, render_template from flask.typing import ResponseReturnValue from abogen.web.routes.utils.service import require_pending_job, get_service @@ -10,7 +10,8 @@ from abogen.web.routes.utils.entity import ( delete_manual_override, search_manual_override_candidates, ) -from abogen.web.routes.utils.settings import coerce_int +from abogen.web.routes.utils.settings import coerce_int, load_settings +from abogen.web.routes.utils.voice import template_options entities_bp = Blueprint("entities", __name__) @@ -94,3 +95,14 @@ 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.get("/") +def entities_page() -> str: + settings = load_settings() + lang = request.args.get("lang") or settings.get("language", "en") + options = template_options() + return render_template( + "entities.html", + language=lang, + languages=options["languages"].items(), + ) diff --git a/abogen/web/routes/jobs.py b/abogen/web/routes/jobs.py index 4c6bca9..688fff8 100644 --- a/abogen/web/routes/jobs.py +++ b/abogen/web/routes/jobs.py @@ -274,3 +274,14 @@ def stream_logs(job_id: str) -> ResponseReturnValue: time.sleep(0.5) return Response(generate(), mimetype="text/event-stream") + +@jobs_bp.get("/queue") +def queue_page() -> str: + return render_template( + "queue.html", + jobs_panel=render_jobs_panel(), + ) + +@jobs_bp.get("/partial") +def jobs_partial() -> str: + return render_jobs_panel() diff --git a/abogen/web/routes/utils/preview.py b/abogen/web/routes/utils/preview.py index 9636cf7..decaa60 100644 --- a/abogen/web/routes/utils/preview.py +++ b/abogen/web/routes/utils/preview.py @@ -25,14 +25,14 @@ def get_preview_pipeline(language: str, device: str) -> Any: _preview_pipelines[key] = pipeline return pipeline -def synthesize_preview( +def generate_preview_audio( text: str, voice_spec: str, language: str, speed: float, use_gpu: bool, max_seconds: float = 8.0, -) -> ResponseReturnValue: +) -> bytes: if not text.strip(): raise ValueError("Preview text is required") @@ -92,8 +92,29 @@ def synthesize_preview( audio_data = np.concatenate(audio_chunks) buffer = io.BytesIO() sf.write(buffer, audio_data, SAMPLE_RATE, format="WAV") - buffer.seek(0) - + return buffer.getvalue() + +def synthesize_preview( + text: str, + voice_spec: str, + language: str, + speed: float, + use_gpu: bool, + max_seconds: float = 8.0, +) -> ResponseReturnValue: + try: + audio_bytes = generate_preview_audio( + text=text, + voice_spec=voice_spec, + language=language, + speed=speed, + use_gpu=use_gpu, + max_seconds=max_seconds, + ) + except Exception as e: + raise e + + buffer = io.BytesIO(audio_bytes) response = send_file( buffer, mimetype="audio/wav", diff --git a/abogen/web/routes/voices.py b/abogen/web/routes/voices.py index b7824fa..47813d6 100644 --- a/abogen/web/routes/voices.py +++ b/abogen/web/routes/voices.py @@ -22,11 +22,8 @@ from abogen.constants import VOICES_INTERNAL voices_bp = Blueprint("voices", __name__) @voices_bp.get("/") -def voices_list() -> ResponseReturnValue: - # This might not be a standalone page in the original app, but useful to have. - # Or maybe it redirects to settings or something. - # For now, I'll just redirect to settings as voices are managed there usually. - return redirect(url_for("settings.settings_page")) +def voice_profiles() -> ResponseReturnValue: + return render_template("voices.html") @voices_bp.post("/test") def test_voice() -> ResponseReturnValue: diff --git a/abogen/web/templates/base.html b/abogen/web/templates/base.html index 753e1d6..b04c60c 100644 --- a/abogen/web/templates/base.html +++ b/abogen/web/templates/base.html @@ -17,12 +17,12 @@
diff --git a/abogen/web/templates/job_detail.html b/abogen/web/templates/job_detail.html index 2674102..1ab4a54 100644 --- a/abogen/web/templates/job_detail.html +++ b/abogen/web/templates/job_detail.html @@ -44,19 +44,19 @@

Characters: {{ job.processed_characters }} / {{ job.total_characters or '—' }}

{% set flags = downloads or {} %} {% if flags.get('m4b') %} -

Download M4B

+

Download M4B

{% elif flags.get('audio') %} -

Download audio

+

Download audio

{% endif %} {% if flags.get('epub3') %} -

Download EPUB 3

+

Download EPUB 3

{% endif %} {% if job.status in [JobStatus.PENDING, JobStatus.RUNNING, JobStatus.PAUSED] %} -
+
{% elif job.status in [JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED] %} -
+
{% endif %} diff --git a/abogen/web/templates/partials/jobs.html b/abogen/web/templates/partials/jobs.html index eb400f5..31699a1 100644 --- a/abogen/web/templates/partials/jobs.html +++ b/abogen/web/templates/partials/jobs.html @@ -11,7 +11,7 @@
  • - {{ job.original_filename }} + {{ job.original_filename }}
    {% if job.queue_position %}Position #{{ job.queue_position }} · {% endif %} {% if job.voice_profile %}Profile: {{ job.voice_profile }}{% else %}Voice: {{ job.voice }}{% endif %} · {{ job.language }} @@ -29,16 +29,16 @@ {{ progress_value }}% · {{ job.processed_characters }} / {{ job.total_characters or '—' }}
  • @@ -53,7 +53,7 @@

    Recent results

    {% if total_finished > 0 %} - + {% endif %}
    {% if finished_jobs %} diff --git a/abogen/web/templates/partials/logs.html b/abogen/web/templates/partials/logs.html index e5221f1..cd8f983 100644 --- a/abogen/web/templates/partials/logs.html +++ b/abogen/web/templates/partials/logs.html @@ -2,9 +2,9 @@
    Live log
    {% if not is_static %} - Open static view + Open static view {% else %} - Back to job + Back to job {% endif %}
    {% if job.logs %} diff --git a/abogen/web/templates/partials/new_job_step_book.html b/abogen/web/templates/partials/new_job_step_book.html index 7b01886..ba7b1bb 100644 --- a/abogen/web/templates/partials/new_job_step_book.html +++ b/abogen/web/templates/partials/new_job_step_book.html @@ -249,7 +249,7 @@ {% if notice %}
    {{ notice }}
    {% endif %} -
    + data-analyze-url="{{ url_for('main.wizard_update', pending_id=pending.id) }}">
    diff --git a/abogen/web/templates/settings.html b/abogen/web/templates/settings.html index 6b26649..24716b0 100644 --- a/abogen/web/templates/settings.html +++ b/abogen/web/templates/settings.html @@ -21,7 +21,7 @@ -
    +