Restructure into asr/tts packages and add TTS tab skeleton
Split the single ASR gateway into app/main.py (entrypoint) + app/core (shared env helpers) + app/asr (all existing ASR logic, unchanged behavior) + app/tts (placeholder router/config, no engine yet), so a real TTS backend can be dropped in later without reshuffling ASR code. API moved under /asr/* and /tts/* prefixes; UI gained a top-level ASR/TTS tab switcher built on a reusable nested .tab-group mechanism, with JS split into common.js/asr.js/tts.js. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
121
app/ui/assets/js/common.js
Normal file
121
app/ui/assets/js/common.js
Normal file
@@ -0,0 +1,121 @@
|
||||
// ─── Shared utilities (used by both ASR and TTS tabs) ─────────────────────────
|
||||
|
||||
const SPEAKER_COLORS = [
|
||||
'#4fa3e0','#e06b4f','#4fe09c','#e0c44f','#a04fe0',
|
||||
'#e04fa3','#4fd4e0','#e0a44f','#7fe04f','#9b59b6',
|
||||
];
|
||||
const colorMap = {};
|
||||
let colorIdx = 0;
|
||||
|
||||
function speakerColor(speaker) {
|
||||
if (!(speaker in colorMap)) {
|
||||
colorMap[speaker] = speaker === 'UNKNOWN'
|
||||
? '#7a85a8'
|
||||
: SPEAKER_COLORS[colorIdx++ % SPEAKER_COLORS.length];
|
||||
}
|
||||
return colorMap[speaker];
|
||||
}
|
||||
|
||||
function resetColors() {
|
||||
Object.keys(colorMap).forEach(k => delete colorMap[k]);
|
||||
colorIdx = 0;
|
||||
}
|
||||
|
||||
function esc(text) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(text));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function fmtTime(s) {
|
||||
const m = Math.floor(s / 60);
|
||||
const sec = (s % 60).toFixed(1).padStart(4, '0');
|
||||
return `${String(m).padStart(2,'0')}:${sec}`;
|
||||
}
|
||||
|
||||
function fmtTimestamp(id) {
|
||||
// id: YYYYMMDD_HHMMSS_ffffff
|
||||
if (!id || id.length < 15) return id;
|
||||
const y = id.slice(0,4), mo = id.slice(4,6), d = id.slice(6,8);
|
||||
const h = id.slice(9,11), mi = id.slice(11,13), s = id.slice(13,15);
|
||||
return `${y}-${mo}-${d} ${h}:${mi}:${s}`;
|
||||
}
|
||||
|
||||
function renderSpeakerBlocks(segments, container, sectionEl) {
|
||||
if (!segments || !segments.length || !segments.some(s => s.speaker)) {
|
||||
sectionEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
resetColors();
|
||||
const groups = [];
|
||||
let cur = null;
|
||||
for (const seg of segments) {
|
||||
const spk = seg.speaker || 'UNKNOWN';
|
||||
if (!cur || cur.speaker !== spk) { cur = {speaker: spk, segs: [seg]}; groups.push(cur); }
|
||||
else cur.segs.push(seg);
|
||||
}
|
||||
let html = '';
|
||||
for (const g of groups) {
|
||||
const c = speakerColor(g.speaker);
|
||||
const text = g.segs.map(s => (s.text||'').trim()).join(' ');
|
||||
const t0 = g.segs[0].start, t1 = g.segs[g.segs.length-1].end;
|
||||
html += `<div class="speaker-block">
|
||||
<div class="speaker-badge">
|
||||
<span class="speaker-dot" style="background:${c}"></span>
|
||||
<span style="color:${c}">${esc(g.speaker)}</span>
|
||||
<span class="speaker-time">${fmtTime(t0)} – ${fmtTime(t1)}</span>
|
||||
</div>
|
||||
<div class="speaker-text">${esc(text)}</div>
|
||||
</div>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
sectionEl.style.display = '';
|
||||
}
|
||||
|
||||
function downloadBlob(name, text, type) {
|
||||
const blob = new Blob([text], {type});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = name;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// ─── Collapsible toggles ──────────────────────────────────────────────────────
|
||||
|
||||
function makeToggle(btnId, panelId, chevronId) {
|
||||
const btn = document.getElementById(btnId);
|
||||
const panel = document.getElementById(panelId);
|
||||
const chevron = document.getElementById(chevronId);
|
||||
if (!btn || !panel) return;
|
||||
btn.addEventListener('click', () => {
|
||||
const open = panel.style.display !== 'none';
|
||||
panel.style.display = open ? 'none' : '';
|
||||
if (chevron) chevron.style.transform = open ? '' : 'rotate(90deg)';
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Nestable tab groups (top-level ASR/TTS switch + each domain's sub-tabs) ───
|
||||
//
|
||||
// Each `.tab-group` owns one `.tabs` row of `.tab-btn` and a set of sibling
|
||||
// `.tab-panel` elements. Scoping with `:scope >` means groups can nest freely
|
||||
// (e.g. the top ASR/TTS switch contains a `.tab-group` per domain) without
|
||||
// their click handlers interfering with each other.
|
||||
|
||||
function initTabGroups() {
|
||||
document.querySelectorAll('.tab-group').forEach(group => {
|
||||
const buttons = group.querySelectorAll(':scope > .tabs > .tab-btn');
|
||||
const panels = group.querySelectorAll(':scope > .tab-panel');
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
buttons.forEach(b => b.classList.remove('active'));
|
||||
panels.forEach(p => p.style.display = 'none');
|
||||
btn.classList.add('active');
|
||||
const target = document.getElementById(btn.dataset.target);
|
||||
if (target) target.style.display = '';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initTabGroups();
|
||||
Reference in New Issue
Block a user