Files
speech/app/ui/assets/js/common.js
du5t cabbdac39b 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>
2026-06-18 11:28:43 +09:00

122 lines
4.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ─── 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();