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:
@@ -3,298 +3,337 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ASR 전사 서비스</title>
|
||||
<link rel="stylesheet" href="/assets/styles.css">
|
||||
<title>ASR · TTS 콘솔</title>
|
||||
<link rel="stylesheet" href="/assets/css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<header class="page-header">
|
||||
<h1>ASR 전사 서비스</h1>
|
||||
<p class="muted">파일 전사 · 실시간 전사 · 처리 내역</p>
|
||||
<h1>ASR · TTS 콘솔</h1>
|
||||
<p class="muted">음성 인식(ASR)과 음성 합성(TTS)을 한 곳에서</p>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="file">파일 전사</button>
|
||||
<button class="tab-btn" data-tab="realtime">실시간 전사</button>
|
||||
<button class="tab-btn" data-tab="history">처리 내역</button>
|
||||
</div>
|
||||
<div class="tab-group tab-group--top">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-target="top-asr">ASR · 음성 인식</button>
|
||||
<button class="tab-btn" data-target="top-tts">TTS · 음성 합성</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════ TAB: FILE ═══════════════════ -->
|
||||
<div id="tab-file" class="tab-panel">
|
||||
<section class="card">
|
||||
<form id="file-form">
|
||||
|
||||
<label>오디오 / 동영상 파일
|
||||
<input type="file" id="file-input" name="file" accept="audio/*,video/*" required>
|
||||
</label>
|
||||
|
||||
<div class="grid2">
|
||||
<label>백엔드
|
||||
<select id="backend" name="backend">
|
||||
<option value="faster-whisper" selected>faster-whisper</option>
|
||||
<option value="qwen3">Qwen3-ASR</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>모델
|
||||
<select id="model" name="model">
|
||||
<!-- populated by JS based on backend -->
|
||||
</select>
|
||||
</label>
|
||||
<!-- ═══════════════════ TOP TAB: ASR ═══════════════════ -->
|
||||
<div id="top-asr" class="tab-panel">
|
||||
<div class="tab-group">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-target="tab-file">파일 전사</button>
|
||||
<button class="tab-btn" data-target="tab-realtime">실시간 전사</button>
|
||||
<button class="tab-btn" data-target="tab-history">처리 내역</button>
|
||||
</div>
|
||||
|
||||
<label>커스텀 모델 경로 (선택, faster-whisper 전용)
|
||||
<input type="text" id="custom_model_path" name="custom_model_path" placeholder="/srv/asr/custom-models/my-model">
|
||||
</label>
|
||||
<!-- TAB: FILE -->
|
||||
<div id="tab-file" class="tab-panel">
|
||||
<section class="card">
|
||||
<form id="file-form">
|
||||
|
||||
<div class="grid2">
|
||||
<label>언어
|
||||
<input type="text" id="language" name="language" value="ko" placeholder="ko">
|
||||
</label>
|
||||
<label>작업
|
||||
<select id="task" name="task">
|
||||
<option value="transcribe" selected>transcribe</option>
|
||||
<option value="translate">translate (영어로 번역)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>오디오 / 동영상 파일
|
||||
<input type="file" id="file-input" name="file" accept="audio/*,video/*" required>
|
||||
</label>
|
||||
|
||||
<div class="grid2">
|
||||
<label>백엔드
|
||||
<select id="backend" name="backend">
|
||||
<option value="faster-whisper" selected>faster-whisper</option>
|
||||
<option value="qwen3">Qwen3-ASR</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>모델
|
||||
<select id="model" name="model">
|
||||
<!-- populated by JS based on backend -->
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>커스텀 모델 경로 (선택, faster-whisper 전용)
|
||||
<input type="text" id="custom_model_path" name="custom_model_path" placeholder="/srv/asr/custom-models/my-model">
|
||||
</label>
|
||||
|
||||
<div class="grid2">
|
||||
<label>언어
|
||||
<input type="text" id="language" name="language" value="ko" placeholder="ko">
|
||||
</label>
|
||||
<label>작업
|
||||
<select id="task" name="task">
|
||||
<option value="transcribe" selected>transcribe</option>
|
||||
<option value="translate">translate (영어로 번역)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- faster-whisper only -->
|
||||
<div id="fw-options">
|
||||
<div class="grid2">
|
||||
<label>Beam size
|
||||
<input type="number" id="beam_size" name="beam_size" value="5" min="1" max="20">
|
||||
</label>
|
||||
<label>Temperature
|
||||
<input type="number" id="temperature" name="temperature" value="0" min="0" max="1" step="0.1">
|
||||
</label>
|
||||
</div>
|
||||
<label class="inline">
|
||||
<input type="checkbox" id="word_timestamps" name="word_timestamps">
|
||||
단어별 타임스탬프
|
||||
</label>
|
||||
|
||||
<div class="section-toggle">
|
||||
<button type="button" class="toggle-btn" id="antirep-toggle">
|
||||
반복 억제 옵션 <span class="chevron" id="antirep-chevron">▸</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="antirep-panel" class="collapsible" style="display:none">
|
||||
<p class="muted small">반복 토큰 문제 발생 시 조정하세요.</p>
|
||||
<div class="grid2">
|
||||
<label>no_repeat_ngram_size
|
||||
<input type="number" id="no_repeat_ngram_size" name="no_repeat_ngram_size" value="0" min="0" max="10">
|
||||
<span class="hint">0=비활성, 3~5 권장</span>
|
||||
</label>
|
||||
<label>repetition_penalty
|
||||
<input type="number" id="repetition_penalty" name="repetition_penalty" value="1.0" min="1.0" max="2.0" step="0.05">
|
||||
<span class="hint">1.0=없음, 1.1~1.3 권장</span>
|
||||
</label>
|
||||
<label>compression_ratio_threshold
|
||||
<input type="number" id="compression_ratio_threshold" name="compression_ratio_threshold" value="2.4" min="0.5" max="5.0" step="0.1">
|
||||
</label>
|
||||
<label>log_prob_threshold
|
||||
<input type="number" id="log_prob_threshold" name="log_prob_threshold" value="-1.0" min="-5.0" max="0.0" step="0.1">
|
||||
</label>
|
||||
<label>no_speech_threshold
|
||||
<input type="number" id="no_speech_threshold" name="no_speech_threshold" value="0.6" min="0.0" max="1.0" step="0.05">
|
||||
</label>
|
||||
<label class="inline" style="align-self:end; padding-top:8px">
|
||||
<input type="checkbox" id="condition_on_previous_text" checked>
|
||||
이전 텍스트 조건화
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<label class="inline">
|
||||
<input type="checkbox" id="diarize" name="diarize">
|
||||
화자 분리 (Speaker Diarization)
|
||||
</label>
|
||||
<div id="diarize-options" style="display:none">
|
||||
<div class="grid3">
|
||||
<label>정확한 화자 수
|
||||
<input type="number" id="num_speakers" name="num_speakers" min="1" max="30" placeholder="자동">
|
||||
</label>
|
||||
<label>최소 화자 수
|
||||
<input type="number" id="min_speakers" name="min_speakers" min="1" max="30" placeholder="자동">
|
||||
</label>
|
||||
<label>최대 화자 수
|
||||
<input type="number" id="max_speakers" name="max_speakers" min="1" max="30" placeholder="자동">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" id="file-submit-btn">전사 실행</button>
|
||||
<button type="button" id="file-copy-btn">텍스트 복사</button>
|
||||
<button type="button" id="file-dl-txt-btn">TXT 저장</button>
|
||||
<button type="button" id="file-dl-json-btn">JSON 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>상태</h2>
|
||||
<pre id="file-status">대기 중</pre>
|
||||
</section>
|
||||
|
||||
<section class="card" id="file-speaker-section" style="display:none">
|
||||
<h2>화자별 결과</h2>
|
||||
<div id="file-speaker-result"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>텍스트 결과</h2>
|
||||
<textarea id="file-result-text" rows="12" placeholder="전사 결과가 여기에 표시됩니다."></textarea>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>JSON 결과</h2>
|
||||
<pre id="file-result-json" class="result-json">{}</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- faster-whisper only -->
|
||||
<div id="fw-options">
|
||||
<div class="grid2">
|
||||
<label>Beam size
|
||||
<input type="number" id="beam_size" name="beam_size" value="5" min="1" max="20">
|
||||
</label>
|
||||
<label>Temperature
|
||||
<input type="number" id="temperature" name="temperature" value="0" min="0" max="1" step="0.1">
|
||||
</label>
|
||||
</div>
|
||||
<label class="inline">
|
||||
<input type="checkbox" id="word_timestamps" name="word_timestamps">
|
||||
단어별 타임스탬프
|
||||
</label>
|
||||
|
||||
<div class="section-toggle">
|
||||
<button type="button" class="toggle-btn" id="antirep-toggle">
|
||||
반복 억제 옵션 <span class="chevron" id="antirep-chevron">▸</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="antirep-panel" class="collapsible" style="display:none">
|
||||
<p class="muted small">반복 토큰 문제 발생 시 조정하세요.</p>
|
||||
<!-- TAB: REALTIME -->
|
||||
<div id="tab-realtime" class="tab-panel" style="display:none">
|
||||
<section class="card">
|
||||
<h2>실시간 전사 설정</h2>
|
||||
<div class="grid2">
|
||||
<label>no_repeat_ngram_size
|
||||
<input type="number" id="no_repeat_ngram_size" name="no_repeat_ngram_size" value="0" min="0" max="10">
|
||||
<span class="hint">0=비활성, 3~5 권장</span>
|
||||
<label>모델
|
||||
<select id="rt-model">
|
||||
<option value="tiny">tiny (가장 빠름)</option>
|
||||
<option value="base">base</option>
|
||||
<option value="small">small</option>
|
||||
<option value="medium">medium</option>
|
||||
<option value="large-v3" selected>large-v3</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>repetition_penalty
|
||||
<input type="number" id="repetition_penalty" name="repetition_penalty" value="1.0" min="1.0" max="2.0" step="0.05">
|
||||
<span class="hint">1.0=없음, 1.1~1.3 권장</span>
|
||||
<label>언어
|
||||
<input type="text" id="rt-language" value="ko" placeholder="ko">
|
||||
</label>
|
||||
<label>compression_ratio_threshold
|
||||
<input type="number" id="compression_ratio_threshold" name="compression_ratio_threshold" value="2.4" min="0.5" max="5.0" step="0.1">
|
||||
</div>
|
||||
<div class="grid2">
|
||||
<label>Beam size
|
||||
<input type="number" id="rt-beam_size" value="3" min="1" max="10">
|
||||
</label>
|
||||
<label>log_prob_threshold
|
||||
<input type="number" id="log_prob_threshold" name="log_prob_threshold" value="-1.0" min="-5.0" max="0.0" step="0.1">
|
||||
<label>청크 처리 간격
|
||||
<select id="rt-chunk_seconds">
|
||||
<option value="2">2초 (빠른 반응)</option>
|
||||
<option value="3" selected>3초 (균형)</option>
|
||||
<option value="5">5초 (더 정확)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>no_speech_threshold
|
||||
<input type="number" id="no_speech_threshold" name="no_speech_threshold" value="0.6" min="0.0" max="1.0" step="0.05">
|
||||
</label>
|
||||
<label class="inline" style="align-self:end; padding-top:8px">
|
||||
<input type="checkbox" id="condition_on_previous_text" name="condition_on_previous_text" checked>
|
||||
</div>
|
||||
|
||||
<div class="section-toggle">
|
||||
<button type="button" class="toggle-btn" id="rt-antirep-toggle">
|
||||
반복 억제 옵션 <span class="chevron" id="rt-antirep-chevron">▸</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="rt-antirep-panel" class="collapsible" style="display:none">
|
||||
<div class="grid2">
|
||||
<label>no_repeat_ngram_size
|
||||
<input type="number" id="rt-no_repeat_ngram_size" value="0" min="0" max="10">
|
||||
<span class="hint">0=비활성, 3~5 권장</span>
|
||||
</label>
|
||||
<label>repetition_penalty
|
||||
<input type="number" id="rt-repetition_penalty" value="1.0" min="1.0" max="2.0" step="0.05">
|
||||
</label>
|
||||
<label>compression_ratio_threshold
|
||||
<input type="number" id="rt-compression_ratio_threshold" value="2.4" min="0.5" max="5.0" step="0.1">
|
||||
</label>
|
||||
<label>no_speech_threshold
|
||||
<input type="number" id="rt-no_speech_threshold" value="0.6" min="0.0" max="1.0" step="0.05">
|
||||
</label>
|
||||
</div>
|
||||
<label class="inline">
|
||||
<input type="checkbox" id="rt-condition_on_previous_text" checked>
|
||||
이전 텍스트 조건화
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
<label class="inline">
|
||||
<input type="checkbox" id="diarize" name="diarize">
|
||||
화자 분리 (Speaker Diarization)
|
||||
</label>
|
||||
<div id="diarize-options" style="display:none">
|
||||
<div class="grid3">
|
||||
<label>정확한 화자 수
|
||||
<input type="number" id="num_speakers" name="num_speakers" min="1" max="30" placeholder="자동">
|
||||
</label>
|
||||
<label>최소 화자 수
|
||||
<input type="number" id="min_speakers" name="min_speakers" min="1" max="30" placeholder="자동">
|
||||
</label>
|
||||
<label>최대 화자 수
|
||||
<input type="number" id="max_speakers" name="max_speakers" min="1" max="30" placeholder="자동">
|
||||
</label>
|
||||
<section class="card">
|
||||
<div class="mic-row">
|
||||
<button id="rt-start-btn" class="mic-btn">● 녹음 시작</button>
|
||||
<button id="rt-stop-btn" class="mic-btn stop-btn" disabled>■ 녹음 중지</button>
|
||||
</div>
|
||||
<div id="rt-indicator" class="rt-indicator idle">대기 중</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>실시간 전사</h2>
|
||||
<div id="rt-live-box" class="live-box">
|
||||
<span class="muted">녹음을 시작하면 텍스트가 여기에 나타납니다.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>최종 텍스트</h2>
|
||||
<textarea id="rt-final-text" rows="10" placeholder="녹음 종료 후 최종 결과가 표시됩니다."></textarea>
|
||||
<div class="actions" style="margin-top:10px">
|
||||
<button type="button" id="rt-copy-btn">텍스트 복사</button>
|
||||
<button type="button" id="rt-dl-txt-btn">TXT 저장</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- TAB: HISTORY -->
|
||||
<div id="tab-history" class="tab-panel" style="display:none">
|
||||
<section class="card">
|
||||
<div class="history-toolbar">
|
||||
<h2 style="margin:0">처리 내역</h2>
|
||||
<button type="button" id="history-refresh-btn" class="btn-secondary">새로고침</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="history-list">
|
||||
<div class="muted" style="padding:16px">내역을 불러오는 중...</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 뷰 -->
|
||||
<div id="history-detail" style="display:none">
|
||||
<section class="card">
|
||||
<div class="detail-header">
|
||||
<button type="button" id="history-back-btn" class="btn-secondary">← 목록으로</button>
|
||||
<span id="detail-title" class="detail-title"></span>
|
||||
<button type="button" id="detail-delete-btn" class="btn-danger">삭제</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="detail-media-section" style="display:none">
|
||||
<h2>원본 파일</h2>
|
||||
<div id="detail-media-container"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>정보</h2>
|
||||
<div id="detail-meta" class="meta-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="detail-speaker-section" style="display:none">
|
||||
<h2>화자별 결과</h2>
|
||||
<div id="detail-speaker-result"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>텍스트 결과</h2>
|
||||
<textarea id="detail-text" rows="12" readonly></textarea>
|
||||
<div class="actions" style="margin-top:10px">
|
||||
<button type="button" id="detail-copy-btn">텍스트 복사</button>
|
||||
<button type="button" id="detail-dl-txt-btn">TXT 저장</button>
|
||||
<button type="button" id="detail-dl-json-btn">JSON 저장</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>JSON 결과</h2>
|
||||
<pre id="detail-json" class="result-json">{}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" id="file-submit-btn">전사 실행</button>
|
||||
<button type="button" id="file-copy-btn">텍스트 복사</button>
|
||||
<button type="button" id="file-dl-txt-btn">TXT 저장</button>
|
||||
<button type="button" id="file-dl-json-btn">JSON 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>상태</h2>
|
||||
<pre id="file-status">대기 중</pre>
|
||||
</section>
|
||||
|
||||
<section class="card" id="file-speaker-section" style="display:none">
|
||||
<h2>화자별 결과</h2>
|
||||
<div id="file-speaker-result"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>텍스트 결과</h2>
|
||||
<textarea id="file-result-text" rows="12" placeholder="전사 결과가 여기에 표시됩니다."></textarea>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>JSON 결과</h2>
|
||||
<pre id="file-result-json" class="result-json">{}</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════ TAB: REALTIME ═══════════════════ -->
|
||||
<div id="tab-realtime" class="tab-panel" style="display:none">
|
||||
<section class="card">
|
||||
<h2>실시간 전사 설정</h2>
|
||||
<div class="grid2">
|
||||
<label>모델
|
||||
<select id="rt-model">
|
||||
<option value="tiny">tiny (가장 빠름)</option>
|
||||
<option value="base">base</option>
|
||||
<option value="small">small</option>
|
||||
<option value="medium">medium</option>
|
||||
<option value="large-v3" selected>large-v3</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>언어
|
||||
<input type="text" id="rt-language" value="ko" placeholder="ko">
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid2">
|
||||
<label>Beam size
|
||||
<input type="number" id="rt-beam_size" value="3" min="1" max="10">
|
||||
</label>
|
||||
<label>청크 처리 간격
|
||||
<select id="rt-chunk_seconds">
|
||||
<option value="2">2초 (빠른 반응)</option>
|
||||
<option value="3" selected>3초 (균형)</option>
|
||||
<option value="5">5초 (더 정확)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="section-toggle">
|
||||
<button type="button" class="toggle-btn" id="rt-antirep-toggle">
|
||||
반복 억제 옵션 <span class="chevron" id="rt-antirep-chevron">▸</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="rt-antirep-panel" class="collapsible" style="display:none">
|
||||
<div class="grid2">
|
||||
<label>no_repeat_ngram_size
|
||||
<input type="number" id="rt-no_repeat_ngram_size" value="0" min="0" max="10">
|
||||
<span class="hint">0=비활성, 3~5 권장</span>
|
||||
</label>
|
||||
<label>repetition_penalty
|
||||
<input type="number" id="rt-repetition_penalty" value="1.0" min="1.0" max="2.0" step="0.05">
|
||||
</label>
|
||||
<label>compression_ratio_threshold
|
||||
<input type="number" id="rt-compression_ratio_threshold" value="2.4" min="0.5" max="5.0" step="0.1">
|
||||
</label>
|
||||
<label>no_speech_threshold
|
||||
<input type="number" id="rt-no_speech_threshold" value="0.6" min="0.0" max="1.0" step="0.05">
|
||||
</label>
|
||||
</div>
|
||||
<label class="inline">
|
||||
<input type="checkbox" id="rt-condition_on_previous_text" checked>
|
||||
이전 텍스트 조건화
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="mic-row">
|
||||
<button id="rt-start-btn" class="mic-btn">● 녹음 시작</button>
|
||||
<button id="rt-stop-btn" class="mic-btn stop-btn" disabled>■ 녹음 중지</button>
|
||||
</div>
|
||||
<div id="rt-indicator" class="rt-indicator idle">대기 중</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>실시간 전사</h2>
|
||||
<div id="rt-live-box" class="live-box">
|
||||
<span class="muted">녹음을 시작하면 텍스트가 여기에 나타납니다.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>최종 텍스트</h2>
|
||||
<textarea id="rt-final-text" rows="10" placeholder="녹음 종료 후 최종 결과가 표시됩니다."></textarea>
|
||||
<div class="actions" style="margin-top:10px">
|
||||
<button type="button" id="rt-copy-btn">텍스트 복사</button>
|
||||
<button type="button" id="rt-dl-txt-btn">TXT 저장</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════ TAB: HISTORY ═══════════════════ -->
|
||||
<div id="tab-history" class="tab-panel" style="display:none">
|
||||
<section class="card">
|
||||
<div class="history-toolbar">
|
||||
<h2 style="margin:0">처리 내역</h2>
|
||||
<button type="button" id="history-refresh-btn" class="btn-secondary">새로고침</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="history-list">
|
||||
<div class="muted" style="padding:16px">내역을 불러오는 중...</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 뷰 -->
|
||||
<div id="history-detail" style="display:none">
|
||||
<section class="card">
|
||||
<div class="detail-header">
|
||||
<button type="button" id="history-back-btn" class="btn-secondary">← 목록으로</button>
|
||||
<span id="detail-title" class="detail-title"></span>
|
||||
<button type="button" id="detail-delete-btn" class="btn-danger">삭제</button>
|
||||
<!-- ═══════════════════ TOP TAB: TTS ═══════════════════ -->
|
||||
<div id="top-tts" class="tab-panel" style="display:none">
|
||||
<div class="tab-group">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-target="tab-tts-synth">텍스트 → 음성</button>
|
||||
<button class="tab-btn" data-target="tab-tts-history">처리 내역</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="detail-media-section" style="display:none">
|
||||
<h2>원본 파일</h2>
|
||||
<div id="detail-media-container"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>정보</h2>
|
||||
<div id="detail-meta" class="meta-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="detail-speaker-section" style="display:none">
|
||||
<h2>화자별 결과</h2>
|
||||
<div id="detail-speaker-result"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>텍스트 결과</h2>
|
||||
<textarea id="detail-text" rows="12" readonly></textarea>
|
||||
<div class="actions" style="margin-top:10px">
|
||||
<button type="button" id="detail-copy-btn">텍스트 복사</button>
|
||||
<button type="button" id="detail-dl-txt-btn">TXT 저장</button>
|
||||
<button type="button" id="detail-dl-json-btn">JSON 저장</button>
|
||||
<div id="tab-tts-synth" class="tab-panel">
|
||||
<section class="card">
|
||||
<h2>TTS 기능 준비 중</h2>
|
||||
<p class="muted">음성 합성 백엔드는 아직 연동되지 않았습니다. API 구조(<code>/tts/*</code>)는
|
||||
준비되어 있으며, 엔진이 연결되면 이 화면에서 텍스트를 음성으로 변환할 수 있습니다.</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>JSON 결과</h2>
|
||||
<pre id="detail-json" class="result-json">{}</pre>
|
||||
</section>
|
||||
<div id="tab-tts-history" class="tab-panel" style="display:none">
|
||||
<section class="card">
|
||||
<h2>처리 내역</h2>
|
||||
<div class="muted" style="padding:16px">아직 처리된 내역이 없습니다.</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<script src="/assets/app.js"></script>
|
||||
<script src="/assets/js/common.js"></script>
|
||||
<script src="/assets/js/asr.js"></script>
|
||||
<script src="/assets/js/tts.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user