mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-19 22:00:28 +02:00
feat: Enhance job management UI and add apostrophe normalization
- Updated styles for job cards to include a paused state and improved title styling. - Modified job card template to display job status and progress more clearly, including pause/resume functionality. - Introduced a new script for managing chapter row states in the prepare job form, allowing for dynamic enabling/disabling of inputs. - Created a new template for preparing jobs, featuring a summary of metadata and chapter details. - Added a comprehensive apostrophe normalization module to handle various cases of apostrophe usage in text.
This commit is contained in:
@@ -7,23 +7,37 @@
|
||||
{% if active_jobs %}
|
||||
<ul class="job-cards">
|
||||
{% for job in active_jobs %}
|
||||
<li class="job-card">
|
||||
{% set progress_value = ((job.progress or 0) * 100)|round(1) %}
|
||||
<li class="job-card" data-status="{{ job.status.value }}">
|
||||
<div class="job-card__header">
|
||||
<div>
|
||||
<strong>{{ job.original_filename }}</strong>
|
||||
<div class="job-card__meta">{% if job.voice_profile %}Profile: {{ job.voice_profile }}{% else %}Voice: {{ job.voice }}{% endif %} · {{ job.language }}</div>
|
||||
<a class="job-card__title" href="{{ url_for('web.job_detail', job_id=job.id) }}">{{ job.original_filename }}</a>
|
||||
<div class="job-card__meta">
|
||||
{% if job.queue_position %}Position #{{ job.queue_position }} · {% endif %}
|
||||
{% if job.voice_profile %}Profile: {{ job.voice_profile }}{% else %}Voice: {{ job.voice }}{% endif %} · {{ job.language }}
|
||||
</div>
|
||||
{% if job.pause_requested and job.status == JobStatus.RUNNING %}
|
||||
<div class="job-card__meta job-card__meta--warning">Pause requested — waiting for a safe point…</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="badge badge--{{ job.status.value }}">{{ job.status.value|title }}</span>
|
||||
</div>
|
||||
<div class="job-card__progress">
|
||||
<div class="progress-bar" style="--progress: {{ ((job.progress or 0) * 100)|round(1) }}%">
|
||||
<div class="progress-bar" style="--progress: {{ progress_value }}%">
|
||||
<div class="progress-bar__fill"></div>
|
||||
</div>
|
||||
<small>{{ job.processed_characters }} / {{ job.total_characters or '—' }}</small>
|
||||
<small>{{ progress_value }}% · {{ job.processed_characters }} / {{ job.total_characters or '—' }}</small>
|
||||
</div>
|
||||
<div class="job-card__footer">
|
||||
<a class="button button--ghost" href="{{ url_for('web.job_detail', job_id=job.id) }}">Inspect</a>
|
||||
{% if job.status in [JobStatus.PENDING, JobStatus.RUNNING] %}
|
||||
<a class="button button--ghost" href="{{ url_for('web.job_detail', job_id=job.id) }}">Details</a>
|
||||
{% if job.status == JobStatus.RUNNING %}
|
||||
<button type="button" class="button button--ghost" hx-post="{{ url_for('web.pause_job', job_id=job.id) }}" hx-target="#jobs-panel" hx-swap="innerHTML">Pause</button>
|
||||
{% elif job.status == JobStatus.PAUSED %}
|
||||
<button type="button" class="button button--ghost" hx-post="{{ url_for('web.resume_job', job_id=job.id) }}" hx-target="#jobs-panel" hx-swap="innerHTML">Resume</button>
|
||||
{% elif job.status == JobStatus.PENDING %}
|
||||
<button type="button" class="button button--ghost" hx-post="{{ url_for('web.pause_job', job_id=job.id) }}" hx-target="#jobs-panel" hx-swap="innerHTML">Pause</button>
|
||||
{% endif %}
|
||||
{% if job.status in [JobStatus.PENDING, JobStatus.RUNNING, JobStatus.PAUSED] %}
|
||||
<button type="button" class="button button--ghost" hx-post="{{ url_for('web.cancel_job', job_id=job.id) }}" hx-target="#jobs-panel" hx-swap="innerHTML" hx-confirm="Cancel this conversion?">Cancel</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -48,7 +62,7 @@
|
||||
<li class="job-card">
|
||||
<div class="job-card__header">
|
||||
<div>
|
||||
<strong>{{ job.original_filename }}</strong>
|
||||
<a class="job-card__title" href="{{ url_for('web.job_detail', job_id=job.id) }}">{{ job.original_filename }}</a>
|
||||
<div class="job-card__meta">Finished {{ job.finished_at|datetimeformat }}</div>
|
||||
</div>
|
||||
<span class="badge badge--{{ job.status.value }}">{{ job.status.value|title }}</span>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Prepare · {{ pending.original_filename }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="card">
|
||||
<div class="card__title">Prepare audiobook</div>
|
||||
<p class="card__subtitle">Review the detected chapters, choose which ones to render, and optionally override the voice per chapter before sending the job to the queue.</p>
|
||||
|
||||
<div class="prepare-summary">
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Source</dt>
|
||||
<dd>{{ pending.original_filename }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Language</dt>
|
||||
<dd>{{ options.languages.get(pending.language, pending.language|upper) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Default voice</dt>
|
||||
<dd>{% if pending.voice_profile %}Profile · {{ pending.voice_profile }}{% else %}{{ pending.voice }}{% endif %}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Total chapters</dt>
|
||||
<dd>{{ pending.chapters|length }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Characters</dt>
|
||||
<dd>{{ pending.total_characters|default(0) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{% if pending.metadata_tags %}
|
||||
<div class="prepare-metadata">
|
||||
<h2>Metadata</h2>
|
||||
<ul>
|
||||
{% for key, value in pending.metadata_tags.items() %}
|
||||
<li><strong>{{ key|replace('_', ' ')|title }}:</strong> {{ value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert--error">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('web.finalize_job', pending_id=pending.id) }}" class="prepare-form" id="prepare-form">
|
||||
<div class="chapter-grid">
|
||||
{% for chapter in pending.chapters %}
|
||||
{% set is_enabled = chapter.enabled is not defined or chapter.enabled %}
|
||||
{% set selected_option = '__default' %}
|
||||
{% if chapter.voice_profile %}
|
||||
{% set selected_option = 'profile:' ~ chapter.voice_profile %}
|
||||
{% elif chapter.voice %}
|
||||
{% set selected_option = 'voice:' ~ chapter.voice %}
|
||||
{% elif chapter.voice_formula %}
|
||||
{% set selected_option = 'formula' %}
|
||||
{% endif %}
|
||||
<article class="chapter-card" data-role="chapter-row">
|
||||
<header class="chapter-card__header">
|
||||
<div class="chapter-card__title">
|
||||
<label class="chapter-card__checkbox">
|
||||
<input type="checkbox" name="chapter-{{ loop.index0 }}-enabled" data-role="chapter-enabled" {% if is_enabled %}checked{% endif %}>
|
||||
<span>Chapter {{ loop.index }} · {{ chapter.title }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="chapter-card__metrics">
|
||||
{{ chapter.characters }} characters
|
||||
</div>
|
||||
</header>
|
||||
<div class="chapter-card__body">
|
||||
<div class="chapter-card__field">
|
||||
<label for="chapter-{{ loop.index0 }}-title">Title</label>
|
||||
<input type="text" id="chapter-{{ loop.index0 }}-title" name="chapter-{{ loop.index0 }}-title" value="{{ chapter.title }}">
|
||||
</div>
|
||||
<div class="chapter-card__preview">
|
||||
<details>
|
||||
<summary>Preview text</summary>
|
||||
<pre>{{ chapter.text[:2000] }}{% if chapter.text|length > 2000 %}…{% endif %}</pre>
|
||||
</details>
|
||||
</div>
|
||||
<div class="chapter-card__field">
|
||||
<label for="chapter-{{ loop.index0 }}-voice">Voice override</label>
|
||||
<select id="chapter-{{ loop.index0 }}-voice" name="chapter-{{ loop.index0 }}-voice" data-role="voice-select">
|
||||
<option value="__default" {% if selected_option == '__default' %}selected{% endif %}>Use job default</option>
|
||||
<optgroup label="Voices">
|
||||
{% for voice in options.voices %}
|
||||
<option value="voice:{{ voice }}" {% if selected_option == 'voice:' ~ voice %}selected{% endif %}>{{ voice }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% if options.voice_profile_options %}
|
||||
<optgroup label="Profiles">
|
||||
{% for profile in options.voice_profile_options %}
|
||||
<option value="profile:{{ profile.name }}" {% if selected_option == 'profile:' ~ profile.name %}selected{% endif %}>{{ profile.name }}{% if profile.language %} · {{ profile.language|upper }}{% endif %}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endif %}
|
||||
<option value="formula" {% if selected_option == 'formula' %}selected{% endif %}>Custom formula…</option>
|
||||
</select>
|
||||
<input type="text" name="chapter-{{ loop.index0 }}-formula" class="chapter-card__formula" data-role="formula-input" placeholder="af_nova*0.4+am_liam*0.6" value="{{ chapter.voice_formula or '' }}" {% if selected_option != 'formula' %}hidden aria-hidden="true"{% else %}aria-hidden="false"{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="prepare-actions">
|
||||
<button type="submit" class="button">Queue conversion</button>
|
||||
<button type="submit" class="button button--ghost" form="cancel-form">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('web.cancel_pending_job', pending_id=pending.id) }}" id="cancel-form"></form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script type="module" src="{{ url_for('static', filename='prepare.js') }}"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user