Spaces:
Running on Zero
Running on Zero
Add Dev tab for browsing usage logs and disable verbose debug flags
Browse files- Add in-app Dev tab (local only, hidden on HF Space) to browse
hetchyy/quran-aligner-logs dataset with filtering, sorting, and
segment rendering using the same pipeline as the main app
- Disable ANCHOR_DEBUG and PHONEME_ALIGNMENT_DEBUG in production config
- Suppress HF Hub download progress bars on cold start
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- app.py +4 -0
- config.py +3 -2
- src/ui/dev_tools.py +512 -0
- src/ui/event_wiring.py +47 -0
- src/ui/interface.py +49 -30
app.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
| 1 |
"""Quran Aligner — Automatic Quran recitation segmentation and alignment."""
|
|
|
|
| 2 |
import sys
|
| 3 |
from pathlib import Path
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
# Add paths for imports BEFORE importing anything else
|
| 6 |
_app_path = Path(__file__).parent.resolve()
|
| 7 |
sys.path.insert(0, str(_app_path))
|
|
|
|
| 1 |
"""Quran Aligner — Automatic Quran recitation segmentation and alignment."""
|
| 2 |
+
import os
|
| 3 |
import sys
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
+
# Suppress HF model download progress bars (hundreds of lines on cold start)
|
| 7 |
+
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
|
| 8 |
+
|
| 9 |
# Add paths for imports BEFORE importing anything else
|
| 10 |
_app_path = Path(__file__).parent.resolve()
|
| 11 |
sys.path.insert(0, str(_app_path))
|
config.py
CHANGED
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
| 6 |
|
| 7 |
# HF Spaces detection
|
| 8 |
IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
|
|
|
|
| 9 |
|
| 10 |
# Get project root directory
|
| 11 |
PROJECT_ROOT = Path(__file__).parent.absolute()
|
|
@@ -145,8 +146,8 @@ MAX_EDIT_DISTANCE_RELAXED = 0.4 # Relaxed threshold for retry tier 2
|
|
| 145 |
MAX_CONSECUTIVE_FAILURES = 2 # Re-anchor within surah after this many DP failures
|
| 146 |
|
| 147 |
# Debug output
|
| 148 |
-
ANCHOR_DEBUG =
|
| 149 |
-
PHONEME_ALIGNMENT_DEBUG =
|
| 150 |
PHONEME_ALIGNMENT_PROFILING = True # Track and log timing breakdown (DP, window setup, etc.)
|
| 151 |
|
| 152 |
# =============================================================================
|
|
|
|
| 6 |
|
| 7 |
# HF Spaces detection
|
| 8 |
IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
|
| 9 |
+
DEV_TAB_VISIBLE = not IS_HF_SPACE
|
| 10 |
|
| 11 |
# Get project root directory
|
| 12 |
PROJECT_ROOT = Path(__file__).parent.absolute()
|
|
|
|
| 146 |
MAX_CONSECUTIVE_FAILURES = 2 # Re-anchor within surah after this many DP failures
|
| 147 |
|
| 148 |
# Debug output
|
| 149 |
+
ANCHOR_DEBUG = False # Show detailed n-gram voting info (votes, top candidates)
|
| 150 |
+
PHONEME_ALIGNMENT_DEBUG = False # Show detailed alignment info (R, P, edit costs)
|
| 151 |
PHONEME_ALIGNMENT_PROFILING = True # Track and log timing breakdown (DP, window setup, etc.)
|
| 152 |
|
| 153 |
# =============================================================================
|
src/ui/dev_tools.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dev tab — browse and inspect usage logs from HF dataset (local only)."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import uuid
|
| 6 |
+
from datetime import datetime, timezone
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
from config import SEGMENT_AUDIO_DIR, SURAH_INFO_PATH
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# ── Surah names cache ──────────────────────────────────────────────────
|
| 16 |
+
|
| 17 |
+
_surah_names: dict[int, str] | None = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _load_surah_names() -> dict[int, str]:
|
| 21 |
+
global _surah_names
|
| 22 |
+
if _surah_names is not None:
|
| 23 |
+
return _surah_names
|
| 24 |
+
if not SURAH_INFO_PATH.exists():
|
| 25 |
+
_surah_names = {}
|
| 26 |
+
return _surah_names
|
| 27 |
+
with open(SURAH_INFO_PATH) as f:
|
| 28 |
+
data = json.load(f)
|
| 29 |
+
_surah_names = {int(k): v["name_en"] for k, v in data.items()}
|
| 30 |
+
return _surah_names
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ── HF token loading (same pattern as scripts/analyze_logs.py) ─────────
|
| 34 |
+
|
| 35 |
+
def _load_token() -> str | None:
|
| 36 |
+
token = os.environ.get("HF_TOKEN")
|
| 37 |
+
if token:
|
| 38 |
+
return token
|
| 39 |
+
env_path = Path(__file__).parent.parent.parent / ".env"
|
| 40 |
+
if env_path.exists():
|
| 41 |
+
for line in env_path.read_text().splitlines():
|
| 42 |
+
line = line.strip()
|
| 43 |
+
if line.startswith("HF_TOKEN="):
|
| 44 |
+
return line.split("=", 1)[1]
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# ── Dataset helpers ────────────────────────────────────────────────────
|
| 49 |
+
|
| 50 |
+
def _has_valid_segments(segments_str) -> bool:
|
| 51 |
+
if not segments_str:
|
| 52 |
+
return False
|
| 53 |
+
try:
|
| 54 |
+
runs = json.loads(segments_str)
|
| 55 |
+
if isinstance(runs, list) and runs:
|
| 56 |
+
return any(isinstance(run, dict) and run.get("segments") for run in runs)
|
| 57 |
+
except (json.JSONDecodeError, TypeError):
|
| 58 |
+
pass
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _fmt_duration(seconds) -> str:
|
| 63 |
+
if seconds is None:
|
| 64 |
+
return "N/A"
|
| 65 |
+
m, s = divmod(int(seconds), 60)
|
| 66 |
+
h, m = divmod(m, 60)
|
| 67 |
+
if h > 0:
|
| 68 |
+
return f"{h}h {m}m"
|
| 69 |
+
return f"{m}m {int(s)}s"
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _fmt_pct(val) -> str:
|
| 73 |
+
if val is None:
|
| 74 |
+
return "N/A"
|
| 75 |
+
return f"{val * 100:.1f}%"
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _fmt_time(val) -> str:
|
| 79 |
+
if val is None:
|
| 80 |
+
return "N/A"
|
| 81 |
+
return f"{val:.1f}s"
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# ── UI builder ─────────────────────────────────────────────────────────
|
| 85 |
+
|
| 86 |
+
def build_dev_tab_ui(c):
|
| 87 |
+
"""Build the Dev tab UI components and attach them to the namespace."""
|
| 88 |
+
with gr.Row():
|
| 89 |
+
c.dev_load_btn = gr.Button("Load Logs", variant="primary", size="sm")
|
| 90 |
+
c.dev_refresh_btn = gr.Button("Refresh", size="sm")
|
| 91 |
+
c.dev_status = gr.Markdown("Click **Load Logs** to stream metadata from HF dataset.")
|
| 92 |
+
|
| 93 |
+
with gr.Row():
|
| 94 |
+
c.dev_filter_device = gr.Dropdown(
|
| 95 |
+
choices=["All", "GPU", "CPU"], value="All", label="Device", scale=1,
|
| 96 |
+
)
|
| 97 |
+
c.dev_filter_model = gr.Dropdown(
|
| 98 |
+
choices=["All", "Base", "Large"], value="All", label="Model", scale=1,
|
| 99 |
+
)
|
| 100 |
+
c.dev_filter_status = gr.Dropdown(
|
| 101 |
+
choices=["All", "All Passed", "Has Failures"], value="All", label="Status", scale=1,
|
| 102 |
+
)
|
| 103 |
+
c.dev_sort = gr.Dropdown(
|
| 104 |
+
choices=["Newest", "Duration", "Failures"], value="Newest", label="Sort", scale=1,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
c.dev_table = gr.Dataframe(
|
| 108 |
+
headers=["#", "Time", "Surah", "Duration", "Segs", "Model", "Device",
|
| 109 |
+
"Passed", "Failed", "Conf", "T1", "T2"],
|
| 110 |
+
datatype=["number", "str", "str", "str", "number", "str", "str",
|
| 111 |
+
"number", "number", "str", "number", "number"],
|
| 112 |
+
interactive=False,
|
| 113 |
+
label="Usage Logs",
|
| 114 |
+
wrap=True,
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
c.dev_detail_html = gr.HTML(value="", label="Log Detail")
|
| 118 |
+
|
| 119 |
+
# State
|
| 120 |
+
c.dev_all_rows = gr.State(value=[])
|
| 121 |
+
c.dev_filtered_indices = gr.State(value=[])
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# ── Row extraction ─────────────────────────────────────────────────────
|
| 125 |
+
|
| 126 |
+
def _row_to_dict(row) -> dict:
|
| 127 |
+
"""Extract the fields we care about from a dataset row."""
|
| 128 |
+
return {
|
| 129 |
+
"audio_id": row.get("audio_id", ""),
|
| 130 |
+
"timestamp": row.get("timestamp", ""),
|
| 131 |
+
"surah": row.get("surah"),
|
| 132 |
+
"audio_duration_s": row.get("audio_duration_s"),
|
| 133 |
+
"num_segments": row.get("num_segments"),
|
| 134 |
+
"asr_model": row.get("asr_model", ""),
|
| 135 |
+
"device": row.get("device", ""),
|
| 136 |
+
"segments_passed": row.get("segments_passed"),
|
| 137 |
+
"segments_failed": row.get("segments_failed"),
|
| 138 |
+
"mean_confidence": row.get("mean_confidence"),
|
| 139 |
+
"tier1_retries": row.get("tier1_retries", 0) or 0,
|
| 140 |
+
"tier1_passed": row.get("tier1_passed", 0) or 0,
|
| 141 |
+
"tier2_retries": row.get("tier2_retries", 0) or 0,
|
| 142 |
+
"tier2_passed": row.get("tier2_passed", 0) or 0,
|
| 143 |
+
"reanchors": row.get("reanchors", 0) or 0,
|
| 144 |
+
"special_merges": row.get("special_merges", 0) or 0,
|
| 145 |
+
"total_time": row.get("total_time"),
|
| 146 |
+
"vad_queue_time": row.get("vad_queue_time"),
|
| 147 |
+
"vad_gpu_time": row.get("vad_gpu_time"),
|
| 148 |
+
"asr_gpu_time": row.get("asr_gpu_time"),
|
| 149 |
+
"dp_total_time": row.get("dp_total_time"),
|
| 150 |
+
"min_silence_ms": row.get("min_silence_ms"),
|
| 151 |
+
"min_speech_ms": row.get("min_speech_ms"),
|
| 152 |
+
"pad_ms": row.get("pad_ms"),
|
| 153 |
+
"segments": row.get("segments"),
|
| 154 |
+
"resegmented": row.get("resegmented"),
|
| 155 |
+
"retranscribed": row.get("retranscribed"),
|
| 156 |
+
"error": row.get("error"),
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# ── Table building ─────────────────────────────────────────────────────
|
| 161 |
+
|
| 162 |
+
def _build_table_row(row_dict, index, surah_names):
|
| 163 |
+
"""Build a single table row list from a row dict."""
|
| 164 |
+
ts = row_dict.get("timestamp", "")
|
| 165 |
+
try:
|
| 166 |
+
dt = datetime.fromisoformat(ts)
|
| 167 |
+
time_display = dt.strftime("%m-%d %H:%M")
|
| 168 |
+
except (ValueError, TypeError):
|
| 169 |
+
time_display = str(ts)[:16] if ts else "N/A"
|
| 170 |
+
|
| 171 |
+
surah = row_dict.get("surah")
|
| 172 |
+
name = surah_names.get(surah, "") if surah else ""
|
| 173 |
+
surah_display = f"{surah} {name}" if name else str(surah or "?")
|
| 174 |
+
|
| 175 |
+
return [
|
| 176 |
+
index + 1,
|
| 177 |
+
time_display,
|
| 178 |
+
surah_display,
|
| 179 |
+
_fmt_duration(row_dict.get("audio_duration_s")),
|
| 180 |
+
row_dict.get("num_segments") or 0,
|
| 181 |
+
row_dict.get("asr_model", "?"),
|
| 182 |
+
row_dict.get("device", "?"),
|
| 183 |
+
row_dict.get("segments_passed") or 0,
|
| 184 |
+
row_dict.get("segments_failed") or 0,
|
| 185 |
+
_fmt_pct(row_dict.get("mean_confidence")),
|
| 186 |
+
row_dict.get("tier1_retries", 0) or 0,
|
| 187 |
+
row_dict.get("tier2_retries", 0) or 0,
|
| 188 |
+
]
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _build_table(rows, indices, surah_names):
|
| 192 |
+
"""Build table data from rows and their display indices."""
|
| 193 |
+
return [_build_table_row(rows[i], display_idx, surah_names)
|
| 194 |
+
for display_idx, i in enumerate(indices)]
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# ── Handlers ───────────────────────────────────────────────────────────
|
| 198 |
+
|
| 199 |
+
def load_logs_handler():
|
| 200 |
+
"""Stream dataset (no audio) and return rows + table."""
|
| 201 |
+
token = _load_token()
|
| 202 |
+
if not token:
|
| 203 |
+
gr.Warning("HF_TOKEN not found in .env or environment.")
|
| 204 |
+
return [], [], "HF_TOKEN not found.", gr.update()
|
| 205 |
+
|
| 206 |
+
try:
|
| 207 |
+
from datasets import load_dataset
|
| 208 |
+
except ImportError:
|
| 209 |
+
gr.Warning("'datasets' package not installed.")
|
| 210 |
+
return [], [], "'datasets' package not installed.", gr.update()
|
| 211 |
+
|
| 212 |
+
surah_names = _load_surah_names()
|
| 213 |
+
|
| 214 |
+
try:
|
| 215 |
+
ds = load_dataset("hetchyy/quran-aligner-logs", token=token,
|
| 216 |
+
split="train", streaming=True)
|
| 217 |
+
ds = ds.remove_columns("audio")
|
| 218 |
+
except Exception as e:
|
| 219 |
+
gr.Warning(f"Failed to load dataset: {e}")
|
| 220 |
+
return [], [], f"Error: {e}", gr.update()
|
| 221 |
+
|
| 222 |
+
rows = []
|
| 223 |
+
total = 0
|
| 224 |
+
for row in ds:
|
| 225 |
+
total += 1
|
| 226 |
+
if _has_valid_segments(row.get("segments")):
|
| 227 |
+
rows.append(_row_to_dict(row))
|
| 228 |
+
|
| 229 |
+
# Sort newest first
|
| 230 |
+
rows.sort(key=lambda r: r.get("timestamp") or "", reverse=True)
|
| 231 |
+
|
| 232 |
+
indices = list(range(len(rows)))
|
| 233 |
+
table_data = _build_table(rows, indices, surah_names)
|
| 234 |
+
status = f"Loaded {len(rows)} rows with segments (out of {total} total)."
|
| 235 |
+
|
| 236 |
+
return rows, indices, status, table_data
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def filter_and_sort_handler(all_rows, device, model, status_filter, sort_by):
|
| 240 |
+
"""Filter and sort cached rows, return new table + index mapping."""
|
| 241 |
+
if not all_rows:
|
| 242 |
+
return [], gr.update()
|
| 243 |
+
|
| 244 |
+
surah_names = _load_surah_names()
|
| 245 |
+
indices = []
|
| 246 |
+
|
| 247 |
+
for i, row in enumerate(all_rows):
|
| 248 |
+
# Device filter
|
| 249 |
+
if device != "All":
|
| 250 |
+
row_device = (row.get("device") or "").lower()
|
| 251 |
+
if device == "GPU" and row_device not in ("cuda", "gpu"):
|
| 252 |
+
continue
|
| 253 |
+
if device == "CPU" and row_device not in ("cpu",):
|
| 254 |
+
continue
|
| 255 |
+
|
| 256 |
+
# Model filter
|
| 257 |
+
if model != "All":
|
| 258 |
+
row_model = row.get("asr_model", "")
|
| 259 |
+
if model == "Base" and row_model != "Base":
|
| 260 |
+
continue
|
| 261 |
+
if model == "Large" and row_model != "Large":
|
| 262 |
+
continue
|
| 263 |
+
|
| 264 |
+
# Status filter
|
| 265 |
+
if status_filter == "All Passed":
|
| 266 |
+
if (row.get("segments_failed") or 0) > 0:
|
| 267 |
+
continue
|
| 268 |
+
elif status_filter == "Has Failures":
|
| 269 |
+
if (row.get("segments_failed") or 0) == 0:
|
| 270 |
+
continue
|
| 271 |
+
|
| 272 |
+
indices.append(i)
|
| 273 |
+
|
| 274 |
+
# Sort
|
| 275 |
+
if sort_by == "Duration":
|
| 276 |
+
indices.sort(key=lambda i: all_rows[i].get("audio_duration_s") or 0, reverse=True)
|
| 277 |
+
elif sort_by == "Failures":
|
| 278 |
+
indices.sort(key=lambda i: all_rows[i].get("segments_failed") or 0, reverse=True)
|
| 279 |
+
# else "Newest" — already sorted by timestamp from load
|
| 280 |
+
|
| 281 |
+
table_data = _build_table(all_rows, indices, surah_names)
|
| 282 |
+
return indices, table_data
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def select_log_row_handler(all_rows, filtered_indices, evt: gr.SelectData):
|
| 286 |
+
"""When a table row is clicked, download audio and render segments."""
|
| 287 |
+
if not all_rows or not filtered_indices:
|
| 288 |
+
return ""
|
| 289 |
+
|
| 290 |
+
display_idx = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
|
| 291 |
+
if display_idx < 0 or display_idx >= len(filtered_indices):
|
| 292 |
+
return ""
|
| 293 |
+
|
| 294 |
+
row_idx = filtered_indices[display_idx]
|
| 295 |
+
row = all_rows[row_idx]
|
| 296 |
+
|
| 297 |
+
audio_id = row.get("audio_id", "")
|
| 298 |
+
surah_names = _load_surah_names()
|
| 299 |
+
|
| 300 |
+
# Build summary HTML
|
| 301 |
+
summary_html = _build_summary_html(row, surah_names)
|
| 302 |
+
|
| 303 |
+
# Try to reconstruct and render segments
|
| 304 |
+
segments_html = _build_segments_from_log(row, audio_id)
|
| 305 |
+
|
| 306 |
+
return summary_html + segments_html
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
# ── Summary HTML builder ───────────────────────────────────────────────
|
| 310 |
+
|
| 311 |
+
def _build_summary_html(row, surah_names) -> str:
|
| 312 |
+
"""Build the 4-section summary HTML for a log row."""
|
| 313 |
+
surah = row.get("surah")
|
| 314 |
+
name = surah_names.get(surah, "") if surah else ""
|
| 315 |
+
surah_display = f"{surah} ({name})" if name else str(surah or "N/A")
|
| 316 |
+
|
| 317 |
+
sections = []
|
| 318 |
+
|
| 319 |
+
# 1. Summary
|
| 320 |
+
sections.append(f"""
|
| 321 |
+
<div style="margin-bottom: 12px; padding: 10px; background: #f8f9fa; border-radius: 6px; border-left: 3px solid #4a9eff;">
|
| 322 |
+
<strong>Summary</strong><br>
|
| 323 |
+
<span>Surah: {surah_display}</span> |
|
| 324 |
+
<span>Duration: {_fmt_duration(row.get('audio_duration_s'))}</span> |
|
| 325 |
+
<span>Segments: {row.get('num_segments', 'N/A')}</span> |
|
| 326 |
+
<span>Audio ID: <code style="font-size: 0.85em;">{row.get('audio_id', 'N/A')}</code></span>
|
| 327 |
+
</div>
|
| 328 |
+
""")
|
| 329 |
+
|
| 330 |
+
# 2. Settings
|
| 331 |
+
sections.append(f"""
|
| 332 |
+
<div style="margin-bottom: 12px; padding: 10px; background: #f8f9fa; border-radius: 6px; border-left: 3px solid #f0ad4e;">
|
| 333 |
+
<strong>Settings</strong><br>
|
| 334 |
+
<span>Min Silence: {row.get('min_silence_ms', 'N/A')} ms</span> |
|
| 335 |
+
<span>Min Speech: {row.get('min_speech_ms', 'N/A')} ms</span> |
|
| 336 |
+
<span>Pad: {row.get('pad_ms', 'N/A')} ms</span> |
|
| 337 |
+
<span>Model: {row.get('asr_model', 'N/A')}</span> |
|
| 338 |
+
<span>Device: {row.get('device', 'N/A')}</span>
|
| 339 |
+
</div>
|
| 340 |
+
""")
|
| 341 |
+
|
| 342 |
+
# 3. Profiling
|
| 343 |
+
sections.append(f"""
|
| 344 |
+
<div style="margin-bottom: 12px; padding: 10px; background: #f8f9fa; border-radius: 6px; border-left: 3px solid #5cb85c;">
|
| 345 |
+
<strong>Profiling</strong><br>
|
| 346 |
+
<span>Total: {_fmt_time(row.get('total_time'))}</span> |
|
| 347 |
+
<span>VAD Queue: {_fmt_time(row.get('vad_queue_time'))}</span> |
|
| 348 |
+
<span>VAD GPU: {_fmt_time(row.get('vad_gpu_time'))}</span> |
|
| 349 |
+
<span>ASR GPU: {_fmt_time(row.get('asr_gpu_time'))}</span> |
|
| 350 |
+
<span>DP: {_fmt_time(row.get('dp_total_time'))}</span>
|
| 351 |
+
</div>
|
| 352 |
+
""")
|
| 353 |
+
|
| 354 |
+
# 4. Quality
|
| 355 |
+
passed = row.get("segments_passed") or 0
|
| 356 |
+
failed = row.get("segments_failed") or 0
|
| 357 |
+
total_segs = passed + failed
|
| 358 |
+
pass_rate = f"{passed}/{total_segs}" if total_segs else "N/A"
|
| 359 |
+
t1 = f"{row.get('tier1_passed', 0) or 0}/{row.get('tier1_retries', 0) or 0}"
|
| 360 |
+
t2 = f"{row.get('tier2_passed', 0) or 0}/{row.get('tier2_retries', 0) or 0}"
|
| 361 |
+
|
| 362 |
+
flags = []
|
| 363 |
+
if row.get("resegmented"):
|
| 364 |
+
flags.append("Resegmented")
|
| 365 |
+
if row.get("retranscribed"):
|
| 366 |
+
flags.append("Retranscribed")
|
| 367 |
+
if row.get("error"):
|
| 368 |
+
flags.append(f"Error: {str(row['error'])[:60]}")
|
| 369 |
+
flags_html = f" | <span>Flags: {', '.join(flags)}</span>" if flags else ""
|
| 370 |
+
|
| 371 |
+
sections.append(f"""
|
| 372 |
+
<div style="margin-bottom: 12px; padding: 10px; background: #f8f9fa; border-radius: 6px; border-left: 3px solid #d9534f;">
|
| 373 |
+
<strong>Quality</strong><br>
|
| 374 |
+
<span>Passed: {pass_rate}</span> |
|
| 375 |
+
<span>Confidence: {_fmt_pct(row.get('mean_confidence'))}</span> |
|
| 376 |
+
<span>T1 retries: {t1}</span> |
|
| 377 |
+
<span>T2 retries: {t2}</span> |
|
| 378 |
+
<span>Reanchors: {row.get('reanchors', 0) or 0}</span>
|
| 379 |
+
{flags_html}
|
| 380 |
+
</div>
|
| 381 |
+
""")
|
| 382 |
+
|
| 383 |
+
return "\n".join(sections)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
# ── Segment reconstruction from log ───────────────────────────────────
|
| 387 |
+
|
| 388 |
+
def _build_segments_from_log(row, audio_id) -> str:
|
| 389 |
+
"""Build segment cards from the log's segments JSON, downloading audio on demand."""
|
| 390 |
+
segments_str = row.get("segments")
|
| 391 |
+
if not segments_str:
|
| 392 |
+
return '<div style="color: #999; padding: 20px;">No segment data in this log row.</div>'
|
| 393 |
+
|
| 394 |
+
try:
|
| 395 |
+
runs = json.loads(segments_str)
|
| 396 |
+
except (json.JSONDecodeError, TypeError):
|
| 397 |
+
return '<div style="color: #999; padding: 20px;">Could not parse segments JSON.</div>'
|
| 398 |
+
|
| 399 |
+
if not runs or not isinstance(runs, list):
|
| 400 |
+
return '<div style="color: #999; padding: 20px;">Empty segment runs.</div>'
|
| 401 |
+
|
| 402 |
+
# Use the last run (most recent alignment pass)
|
| 403 |
+
last_run = runs[-1]
|
| 404 |
+
seg_list = last_run.get("segments", [])
|
| 405 |
+
if not seg_list:
|
| 406 |
+
return '<div style="color: #999; padding: 20px;">No segments in last run.</div>'
|
| 407 |
+
|
| 408 |
+
# Try to download audio for this specific row
|
| 409 |
+
audio_int16 = None
|
| 410 |
+
sample_rate = 16000
|
| 411 |
+
segment_dir = None
|
| 412 |
+
|
| 413 |
+
try:
|
| 414 |
+
audio_int16, sample_rate, segment_dir = _download_audio_for_row(audio_id)
|
| 415 |
+
except Exception as e:
|
| 416 |
+
print(f"[dev_tools] Audio download failed: {e}")
|
| 417 |
+
|
| 418 |
+
# Build SegmentInfo objects and render
|
| 419 |
+
from src.core.segment_types import SegmentInfo
|
| 420 |
+
from src.alignment.special_segments import ALL_SPECIAL_REFS, SPECIAL_TEXT
|
| 421 |
+
from src.ui.segments import render_segments, get_text_with_markers, check_undersegmented
|
| 422 |
+
|
| 423 |
+
segments = []
|
| 424 |
+
for seg_data in seg_list:
|
| 425 |
+
ref = seg_data.get("ref", "")
|
| 426 |
+
confidence = seg_data.get("confidence", 0.0) or 0.0
|
| 427 |
+
start = seg_data.get("start", 0.0) or 0.0
|
| 428 |
+
end = seg_data.get("end", 0.0) or 0.0
|
| 429 |
+
error = seg_data.get("error")
|
| 430 |
+
special_type = seg_data.get("special_type", "")
|
| 431 |
+
duration = end - start
|
| 432 |
+
|
| 433 |
+
# Reconstruct matched_text
|
| 434 |
+
matched_text = ""
|
| 435 |
+
if ref in ALL_SPECIAL_REFS:
|
| 436 |
+
# For known specials, use the constant text
|
| 437 |
+
if ref in SPECIAL_TEXT:
|
| 438 |
+
matched_text = SPECIAL_TEXT[ref]
|
| 439 |
+
elif ref == "Isti'adha+Basmala":
|
| 440 |
+
matched_text = SPECIAL_TEXT["Isti'adha"] + " \u06dd " + SPECIAL_TEXT["Basmala"]
|
| 441 |
+
elif ref:
|
| 442 |
+
matched_text = get_text_with_markers(ref) or ""
|
| 443 |
+
|
| 444 |
+
# Check for undersegmentation
|
| 445 |
+
underseg = False
|
| 446 |
+
if ref and ref not in ALL_SPECIAL_REFS:
|
| 447 |
+
underseg = check_undersegmented(ref, duration)
|
| 448 |
+
|
| 449 |
+
# Check for missing words
|
| 450 |
+
has_missing = seg_data.get("missing_words", False) or False
|
| 451 |
+
|
| 452 |
+
seg_info = SegmentInfo(
|
| 453 |
+
start_time=start,
|
| 454 |
+
end_time=end,
|
| 455 |
+
transcribed_text="",
|
| 456 |
+
matched_text=matched_text,
|
| 457 |
+
matched_ref=ref,
|
| 458 |
+
match_score=confidence,
|
| 459 |
+
error=error,
|
| 460 |
+
has_missing_words=has_missing,
|
| 461 |
+
potentially_undersegmented=underseg,
|
| 462 |
+
)
|
| 463 |
+
segments.append(seg_info)
|
| 464 |
+
|
| 465 |
+
if not segments:
|
| 466 |
+
return '<div style="color: #999; padding: 20px;">No valid segments to display.</div>'
|
| 467 |
+
|
| 468 |
+
return render_segments(segments, audio_int16=audio_int16, sample_rate=sample_rate,
|
| 469 |
+
segment_dir=segment_dir)
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
def _download_audio_for_row(audio_id: str):
|
| 473 |
+
"""Download audio for a specific row by streaming until audio_id matches.
|
| 474 |
+
|
| 475 |
+
Returns (audio_int16, sample_rate, segment_dir) or raises on failure.
|
| 476 |
+
"""
|
| 477 |
+
token = _load_token()
|
| 478 |
+
if not token:
|
| 479 |
+
raise ValueError("No HF token")
|
| 480 |
+
|
| 481 |
+
from datasets import load_dataset
|
| 482 |
+
import librosa
|
| 483 |
+
|
| 484 |
+
ds = load_dataset("hetchyy/quran-aligner-logs", token=token,
|
| 485 |
+
split="train", streaming=True)
|
| 486 |
+
|
| 487 |
+
for row in ds:
|
| 488 |
+
if row.get("audio_id") == audio_id:
|
| 489 |
+
audio_data = row.get("audio")
|
| 490 |
+
if audio_data is None:
|
| 491 |
+
raise ValueError("Row found but audio is None")
|
| 492 |
+
|
| 493 |
+
# HF Audio column returns {"path": ..., "array": np.array, "sampling_rate": int}
|
| 494 |
+
audio_array = audio_data["array"]
|
| 495 |
+
sr = audio_data["sampling_rate"]
|
| 496 |
+
|
| 497 |
+
# Resample to 16kHz if needed
|
| 498 |
+
if sr != 16000:
|
| 499 |
+
audio_array = librosa.resample(audio_array, orig_sr=sr, target_sr=16000)
|
| 500 |
+
sr = 16000
|
| 501 |
+
|
| 502 |
+
# Convert to int16
|
| 503 |
+
audio_float = np.clip(audio_array, -1.0, 1.0)
|
| 504 |
+
audio_int16 = (audio_float * 32767).astype(np.int16)
|
| 505 |
+
|
| 506 |
+
# Create segment directory
|
| 507 |
+
segment_dir = SEGMENT_AUDIO_DIR / f"dev_{uuid.uuid4().hex[:8]}"
|
| 508 |
+
segment_dir.mkdir(parents=True, exist_ok=True)
|
| 509 |
+
|
| 510 |
+
return audio_int16, sr, segment_dir
|
| 511 |
+
|
| 512 |
+
raise ValueError(f"Audio ID '{audio_id}' not found in dataset")
|
src/ui/event_wiring.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""Event wiring — connects all Gradio component events."""
|
| 2 |
import gradio as gr
|
| 3 |
|
|
|
|
| 4 |
from src.core.zero_gpu import QuotaExhaustedError
|
| 5 |
from src.pipeline import (
|
| 6 |
process_audio, resegment_audio,
|
|
@@ -34,6 +35,8 @@ def wire_events(app, c):
|
|
| 34 |
_wire_animation_settings(c)
|
| 35 |
_wire_settings_restoration(app, c)
|
| 36 |
_wire_api_endpoint(c)
|
|
|
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
def _wire_preset_buttons(c):
|
|
@@ -496,3 +499,47 @@ def _wire_api_endpoint(c):
|
|
| 496 |
outputs=[c.api_result],
|
| 497 |
api_name="mfa_timestamps_direct",
|
| 498 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Event wiring — connects all Gradio component events."""
|
| 2 |
import gradio as gr
|
| 3 |
|
| 4 |
+
from config import DEV_TAB_VISIBLE
|
| 5 |
from src.core.zero_gpu import QuotaExhaustedError
|
| 6 |
from src.pipeline import (
|
| 7 |
process_audio, resegment_audio,
|
|
|
|
| 35 |
_wire_animation_settings(c)
|
| 36 |
_wire_settings_restoration(app, c)
|
| 37 |
_wire_api_endpoint(c)
|
| 38 |
+
if DEV_TAB_VISIBLE:
|
| 39 |
+
_wire_dev_tab(c)
|
| 40 |
|
| 41 |
|
| 42 |
def _wire_preset_buttons(c):
|
|
|
|
| 499 |
outputs=[c.api_result],
|
| 500 |
api_name="mfa_timestamps_direct",
|
| 501 |
)
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
def _wire_dev_tab(c):
|
| 505 |
+
"""Wire dev tab event handlers."""
|
| 506 |
+
from src.ui.dev_tools import (
|
| 507 |
+
load_logs_handler, filter_and_sort_handler, select_log_row_handler,
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
# Load / Refresh buttons
|
| 511 |
+
_load_outputs = [c.dev_all_rows, c.dev_filtered_indices, c.dev_status, c.dev_table]
|
| 512 |
+
|
| 513 |
+
c.dev_load_btn.click(
|
| 514 |
+
fn=load_logs_handler,
|
| 515 |
+
inputs=[],
|
| 516 |
+
outputs=_load_outputs,
|
| 517 |
+
api_name=False, show_progress="minimal",
|
| 518 |
+
)
|
| 519 |
+
c.dev_refresh_btn.click(
|
| 520 |
+
fn=load_logs_handler,
|
| 521 |
+
inputs=[],
|
| 522 |
+
outputs=_load_outputs,
|
| 523 |
+
api_name=False, show_progress="minimal",
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
# Filter / Sort changes
|
| 527 |
+
_filter_inputs = [c.dev_all_rows, c.dev_filter_device, c.dev_filter_model,
|
| 528 |
+
c.dev_filter_status, c.dev_sort]
|
| 529 |
+
_filter_outputs = [c.dev_filtered_indices, c.dev_table]
|
| 530 |
+
|
| 531 |
+
for component in [c.dev_filter_device, c.dev_filter_model, c.dev_filter_status, c.dev_sort]:
|
| 532 |
+
component.change(
|
| 533 |
+
fn=filter_and_sort_handler,
|
| 534 |
+
inputs=_filter_inputs,
|
| 535 |
+
outputs=_filter_outputs,
|
| 536 |
+
api_name=False, show_progress="hidden",
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
# Table row selection
|
| 540 |
+
c.dev_table.select(
|
| 541 |
+
fn=select_log_row_handler,
|
| 542 |
+
inputs=[c.dev_all_rows, c.dev_filtered_indices],
|
| 543 |
+
outputs=[c.dev_detail_html],
|
| 544 |
+
api_name=False, show_progress="minimal",
|
| 545 |
+
)
|
src/ui/interface.py
CHANGED
|
@@ -7,6 +7,7 @@ import gradio as gr
|
|
| 7 |
|
| 8 |
from config import (
|
| 9 |
DELETE_CACHE_FREQUENCY, DELETE_CACHE_AGE,
|
|
|
|
| 10 |
ANIM_WORD_COLOR, ANIM_STYLE_ROW_SCALES,
|
| 11 |
ANIM_DISPLAY_MODES, ANIM_DISPLAY_MODE_DEFAULT,
|
| 12 |
ANIM_OPACITY_PREV_DEFAULT, ANIM_OPACITY_AFTER_DEFAULT, ANIM_OPACITY_STEP,
|
|
@@ -205,36 +206,54 @@ def _build_animation_settings(c):
|
|
| 205 |
|
| 206 |
|
| 207 |
def _build_right_column(c):
|
| 208 |
-
"""Build the right output column."""
|
| 209 |
with gr.Column(scale=RIGHT_COLUMN_SCALE):
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
)
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
| 234 |
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
| 238 |
)
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from config import (
|
| 9 |
DELETE_CACHE_FREQUENCY, DELETE_CACHE_AGE,
|
| 10 |
+
DEV_TAB_VISIBLE,
|
| 11 |
ANIM_WORD_COLOR, ANIM_STYLE_ROW_SCALES,
|
| 12 |
ANIM_DISPLAY_MODES, ANIM_DISPLAY_MODE_DEFAULT,
|
| 13 |
ANIM_OPACITY_PREV_DEFAULT, ANIM_OPACITY_AFTER_DEFAULT, ANIM_OPACITY_STEP,
|
|
|
|
| 206 |
|
| 207 |
|
| 208 |
def _build_right_column(c):
|
| 209 |
+
"""Build the right output column, with optional Dev tab."""
|
| 210 |
with gr.Column(scale=RIGHT_COLUMN_SCALE):
|
| 211 |
+
if DEV_TAB_VISIBLE:
|
| 212 |
+
with gr.Tabs():
|
| 213 |
+
with gr.Tab("Results"):
|
| 214 |
+
_build_results_content(c)
|
| 215 |
+
with gr.Tab("Dev"):
|
| 216 |
+
_build_dev_tab(c)
|
| 217 |
+
else:
|
| 218 |
+
_build_results_content(c)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def _build_results_content(c):
|
| 222 |
+
"""Build the main results content (extract/resegment/output)."""
|
| 223 |
+
c.extract_btn = gr.Button("Extract Segments", variant="primary", size="lg")
|
| 224 |
+
with gr.Row(elem_id="action-btns-row"):
|
| 225 |
+
c.resegment_toggle_btn = gr.Button(
|
| 226 |
+
"Resegment with New Settings", variant="primary", size="lg", visible=False
|
| 227 |
+
)
|
| 228 |
+
c.retranscribe_btn = gr.Button(
|
| 229 |
+
"Retranscribe with Large Model", variant="primary", size="lg", visible=False
|
| 230 |
+
)
|
| 231 |
+
with gr.Row(elem_id="ts-row"):
|
| 232 |
+
c.compute_ts_btn = gr.Button(
|
| 233 |
+
"Compute Timestamps", variant="secondary", size="lg", interactive=False, visible=False
|
| 234 |
+
)
|
| 235 |
+
c.compute_ts_progress = gr.HTML(value="", visible=False)
|
| 236 |
+
c.animate_all_html = gr.HTML(value="", visible=False)
|
| 237 |
|
| 238 |
+
with gr.Column(visible=False) as c.resegment_panel:
|
| 239 |
+
gr.Markdown(
|
| 240 |
+
"Uses cached data, skipping the heavy computation, "
|
| 241 |
+
"so it's much faster. Useful if results are over-segmented "
|
| 242 |
+
"or under-segmented"
|
| 243 |
)
|
| 244 |
+
c.rs_silence, c.rs_speech, c.rs_pad, \
|
| 245 |
+
c.rs_btn_muj, c.rs_btn_mur, c.rs_btn_fast = create_segmentation_settings(id_suffix="-rs")
|
| 246 |
+
c.resegment_btn = gr.Button("Resegment", variant="primary", size="lg")
|
| 247 |
+
|
| 248 |
+
c.output_html = gr.HTML(
|
| 249 |
+
value='<div style="text-align: center; color: #666; padding: 60px;">Upload audio and click "Extract Segments" to begin</div>',
|
| 250 |
+
elem_classes=["output-html"]
|
| 251 |
+
)
|
| 252 |
+
# Hidden JSON output for API consumers
|
| 253 |
+
c.output_json = gr.JSON(visible=False, label="JSON Output")
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def _build_dev_tab(c):
|
| 257 |
+
"""Build the Dev tab UI (delegates to dev_tools module)."""
|
| 258 |
+
from src.ui.dev_tools import build_dev_tab_ui
|
| 259 |
+
build_dev_tab_ui(c)
|