feat(rework): '전사' → '자막 추출(음성)' 용어 변경 + 왼쪽 영상 패널 sticky
- 이해하기 쉽게 모든 '전사' 문구를 '자막 추출'로 변경(메인 버튼은 '자막 추출(음성)') - 좌측 영상 카드 position:sticky(top:1rem) → 스크롤해도 상단에 고정되어 항상 보임 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7b94515180
commit
31b7720af9
@ -26,11 +26,11 @@
|
|||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<div class="modal-head"><h3>📖 재가공 사용법</h3><button class="modal-close" onclick="closeHelp()">×</button></div>
|
<div class="modal-head"><h3>📖 재가공 사용법</h3><button class="modal-close" onclick="closeHelp()">×</button></div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<!-- 이미지 중심: 원본 다운로드 → 전사 → 내보내기 한 번에 (NEW) -->
|
<!-- 이미지 중심: 원본 다운로드 → 자막 추출 → 내보내기 한 번에 (NEW) -->
|
||||||
<div class="usage-guide">
|
<div class="usage-guide">
|
||||||
<div class="ug-title">🎬 원본 다운로드 한 번에 <span class="ug-new">NEW</span></div>
|
<div class="ug-title">🎬 원본 다운로드 한 번에 <span class="ug-new">NEW</span></div>
|
||||||
<p class="ug-lead">유튜브에서 직접 받아 올릴 필요 없이, <b>버튼 한 번</b>으로 원본을 받아
|
<p class="ug-lead">유튜브에서 직접 받아 올릴 필요 없이, <b>버튼 한 번</b>으로 원본을 받아
|
||||||
<b>자동 전사</b>하고 그대로 <b>컷편집·내보내기</b>까지 이어집니다.</p>
|
<b>자동 자막 추출</b>하고 그대로 <b>컷편집·내보내기</b>까지 이어집니다.</p>
|
||||||
|
|
||||||
<div class="ug-step">
|
<div class="ug-step">
|
||||||
<span class="ug-badge">1</span>
|
<span class="ug-badge">1</span>
|
||||||
@ -45,8 +45,8 @@
|
|||||||
<div class="ug-step">
|
<div class="ug-step">
|
||||||
<span class="ug-badge">2</span>
|
<span class="ug-badge">2</span>
|
||||||
<div class="ug-step-body">
|
<div class="ug-step-body">
|
||||||
<div class="ug-step-t">자동 전사 완료 → 시간 싱크 자막 생성</div>
|
<div class="ug-step-t">자동 자막 추출 완료 → 시간 싱크 자막 생성</div>
|
||||||
<div class="ug-step-d">다운로드가 끝나면 <b>Whisper</b>가 바로 전사해 <b>[00:00] 자막</b> 세그먼트가 만들어집니다.
|
<div class="ug-step-d">다운로드가 끝나면 <b>Whisper</b>가 바로 자막 추출해 <b>[00:00] 자막</b> 세그먼트가 만들어집니다.
|
||||||
줄을 클릭하면 영상이 그 지점으로 이동해요.</div>
|
줄을 클릭하면 영상이 그 지점으로 이동해요.</div>
|
||||||
<img class="ug-img" th:src="@{/img/usage/dl-2-segments.png}" alt="자동 생성된 시간 싱크 자막" loading="lazy">
|
<img class="ug-img" th:src="@{/img/usage/dl-2-segments.png}" alt="자동 생성된 시간 싱크 자막" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
@ -62,14 +62,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="ug-tip">💡 직접 받은 파일을 쓰고 싶으면 옆의 <b>‘영상 업로드·전사’</b>로 올리면 됩니다(기존 방식 그대로).</p>
|
<p class="ug-tip">💡 직접 받은 파일을 쓰고 싶으면 옆의 <b>‘영상 업로드·자막 추출’</b>로 올리면 됩니다(기존 방식 그대로).</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ug-divider"></div>
|
<div class="ug-divider"></div>
|
||||||
|
|
||||||
<div class="help-item">
|
<div class="help-item">
|
||||||
<div class="hi-ic"><i data-lucide="upload"></i></div>
|
<div class="hi-ic"><i data-lucide="upload"></i></div>
|
||||||
<div><div class="hi-t">영상 업로드 · 전사</div><div class="hi-d">영상 파일을 올리면 <b>Whisper</b>가 말소리를 <b>시간 싱크 자막</b>으로 추출합니다. 언어는 자동감지(틀리면 옆에서 ko/en/zh/ja 선택). ‘URL자막’은 YouTube 자막을 평문으로(시간 없음) 가져옵니다.</div></div>
|
<div><div class="hi-t">영상 업로드 · 자막 추출</div><div class="hi-d">영상 파일을 올리면 <b>Whisper</b>가 말소리를 <b>시간 싱크 자막</b>으로 추출합니다. 언어는 자동감지(틀리면 옆에서 ko/en/zh/ja 선택). ‘URL자막’은 YouTube 자막을 평문으로(시간 없음) 가져옵니다.</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="help-item">
|
<div class="help-item">
|
||||||
<div class="hi-ic"><i data-lucide="list-video"></i></div>
|
<div class="hi-ic"><i data-lucide="list-video"></i></div>
|
||||||
@ -112,8 +112,8 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div style="display:grid; grid-template-columns: 360px 1fr; gap:1.5rem; align-items:start;">
|
<div style="display:grid; grid-template-columns: 360px 1fr; gap:1.5rem; align-items:start;">
|
||||||
<!-- 좌: 영상 + 정보 -->
|
<!-- 좌: 영상 + 정보 (스크롤해도 상단에 따라다님) -->
|
||||||
<div class="card">
|
<div class="card" style="position:sticky; top:1rem; align-self:start;">
|
||||||
<div id="player" style="width:100%; aspect-ratio:9/16; max-height:480px; border-radius:8px; overflow:hidden; background:#000; margin-bottom:12px;"></div>
|
<div id="player" style="width:100%; aspect-ratio:9/16; max-height:480px; border-radius:8px; overflow:hidden; background:#000; margin-bottom:12px;"></div>
|
||||||
<video id="localVideo" controls playsinline
|
<video id="localVideo" controls playsinline
|
||||||
style="display:none; width:100%; aspect-ratio:9/16; max-height:480px; border-radius:8px; background:#000; margin-bottom:12px;"></video>
|
style="display:none; width:100%; aspect-ratio:9/16; max-height:480px; border-radius:8px; background:#000; margin-bottom:12px;"></video>
|
||||||
@ -132,21 +132,21 @@
|
|||||||
<div class="flex items-center justify-between mb-3" style="flex-wrap:wrap; gap:8px;">
|
<div class="flex items-center justify-between mb-3" style="flex-wrap:wrap; gap:8px;">
|
||||||
<h3 class="text-lg font-bold">원본 스크립트</h3>
|
<h3 class="text-lg font-bold">원본 스크립트</h3>
|
||||||
<div class="flex gap-2" style="flex-wrap:wrap; align-items:center;">
|
<div class="flex gap-2" style="flex-wrap:wrap; align-items:center;">
|
||||||
<select id="langSel" title="전사 언어(자동감지가 틀릴 때 강제)" class="pub-in" style="width:auto; padding:7px 9px;">
|
<select id="langSel" title="자막 추출 언어(자동감지가 틀릴 때 강제)" class="pub-in" style="width:auto; padding:7px 9px;">
|
||||||
<option value="">자동감지</option>
|
<option value="">자동감지</option>
|
||||||
<option value="ko">한국어</option>
|
<option value="ko">한국어</option>
|
||||||
<option value="en">English</option>
|
<option value="en">English</option>
|
||||||
<option value="zh">中文</option>
|
<option value="zh">中文</option>
|
||||||
<option value="ja">日本語</option>
|
<option value="ja">日本語</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn-primary px-3 py-2 flex items-center gap-1" id="downloadBtn" onclick="downloadOriginal()" title="저장된 URL로 원본을 yt-dlp로 받아 자동 전사(수동 다운로드+업로드 불필요)">
|
<button class="btn btn-primary px-3 py-2 flex items-center gap-1" id="downloadBtn" onclick="downloadOriginal()" title="저장된 URL로 원본을 yt-dlp로 받아 자동 자막 추출(수동 다운로드+업로드 불필요)">
|
||||||
<i data-lucide="download" style="width:15px;"></i> 원본 다운로드
|
<i data-lucide="download" style="width:15px;"></i> 원본 다운로드
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary px-3 py-2 flex items-center gap-1" id="transcribeCachedBtn" onclick="transcribeCached()" style="display:none;" title="받아둔 원본을 Whisper로 전사(전사만 다시 하기)">
|
<button class="btn btn-primary px-3 py-2 flex items-center gap-1" id="transcribeCachedBtn" onclick="transcribeCached()" style="display:none;" title="받아둔 원본을 Whisper로 자막 추출(자막 추출만 다시 하기)">
|
||||||
<i data-lucide="captions" style="width:15px;"></i> 전사
|
<i data-lucide="captions" style="width:15px;"></i> 자막 추출(음성)
|
||||||
</button>
|
</button>
|
||||||
<label class="btn btn-secondary px-3 py-2 flex items-center gap-1" id="uploadBtn" style="cursor:pointer;" title="직접 받은 영상 파일을 올려 전사">
|
<label class="btn btn-secondary px-3 py-2 flex items-center gap-1" id="uploadBtn" style="cursor:pointer;" title="직접 받은 영상 파일을 올려 자막 추출">
|
||||||
<i data-lucide="upload" style="width:15px;"></i> 영상 업로드·전사
|
<i data-lucide="upload" style="width:15px;"></i> 영상 업로드·자막 추출
|
||||||
<input type="file" id="videoFile" accept="video/*" style="display:none;" onchange="onVideoSelected(event)">
|
<input type="file" id="videoFile" accept="video/*" style="display:none;" onchange="onVideoSelected(event)">
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn-secondary px-3 py-2 flex items-center gap-1" id="extractBtn" onclick="extractScript()" title="YouTube 자막에서 평문 추출(타임스탬프 없음)">
|
<button class="btn btn-secondary px-3 py-2 flex items-center gap-1" id="extractBtn" onclick="extractScript()" title="YouTube 자막에서 평문 추출(타임스탬프 없음)">
|
||||||
@ -173,7 +173,7 @@
|
|||||||
<div id="segmentList" style="display:none; max-height:340px; overflow:auto; border:1px solid var(--glass-border); border-radius:var(--radius-md); background:var(--surface-2);"></div>
|
<div id="segmentList" style="display:none; max-height:340px; overflow:auto; border:1px solid var(--glass-border); border-radius:var(--radius-md); background:var(--surface-2);"></div>
|
||||||
|
|
||||||
<!-- 평문 폴백 (URL 자막 추출 시) -->
|
<!-- 평문 폴백 (URL 자막 추출 시) -->
|
||||||
<textarea id="transcript" readonly placeholder="‘영상 업로드·전사’로 영상에서 싱크된 자막을 추출하거나, ‘URL자막’으로 YouTube 자막을 가져오세요."
|
<textarea id="transcript" readonly placeholder="‘영상 업로드·자막 추출’로 영상에서 싱크된 자막을 추출하거나, ‘URL자막’으로 YouTube 자막을 가져오세요."
|
||||||
style="width:100%; min-height:140px; resize:vertical; padding:12px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:var(--text-2); outline:none; font-size:0.9rem; line-height:1.6;"></textarea>
|
style="width:100%; min-height:140px; resize:vertical; padding:12px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:var(--text-2); outline:none; font-size:0.9rem; line-height:1.6;"></textarea>
|
||||||
|
|
||||||
<!-- 내보내기 (배속 + 말 없는 구간 제거 + SRT/영상) -->
|
<!-- 내보내기 (배속 + 말 없는 구간 제거 + SRT/영상) -->
|
||||||
@ -228,7 +228,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex items-center justify-between mb-3" style="flex-wrap:wrap; gap:8px;">
|
<div class="flex items-center justify-between mb-3" style="flex-wrap:wrap; gap:8px;">
|
||||||
<h3 class="text-lg font-bold">🤖 AI 자막 (음성 / 화면)</h3>
|
<h3 class="text-lg font-bold">🤖 AI 자막 (음성 / 화면)</h3>
|
||||||
<button class="btn btn-primary px-3 py-2 flex items-center gap-1" id="aiGenBtn" onclick="generateAiSubtitles()" title="왼쪽=음성(Whisper 정밀+한국어), 오른쪽=화면(Gemini 한국어). 음성은 먼저 '전사' 필요">
|
<button class="btn btn-primary px-3 py-2 flex items-center gap-1" id="aiGenBtn" onclick="generateAiSubtitles()" title="왼쪽=음성(Whisper 정밀+한국어), 오른쪽=화면(Gemini 한국어). 음성은 먼저 '자막 추출' 필요">
|
||||||
<i data-lucide="sparkles" style="width:15px;"></i> 생성
|
<i data-lucide="sparkles" style="width:15px;"></i> 생성
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -348,7 +348,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 세그먼트(영상 싱크) =====
|
// ===== 세그먼트(영상 싱크) =====
|
||||||
let SEGMENTS = []; // 전사 원본 세그먼트(원본 영상 재생에 싱크)
|
let SEGMENTS = []; // 자막 추출 원본 세그먼트(원본 영상 재생에 싱크)
|
||||||
let UPLOADED_FILE = null; // 업로드한 영상 File (렌더 재전송용)
|
let UPLOADED_FILE = null; // 업로드한 영상 File (렌더 재전송용)
|
||||||
let HAS_SERVER_FILE = false; // '원본 다운로드'로 서버에 캐시된 영상 보유 여부(렌더가 업로드 없이 재사용)
|
let HAS_SERVER_FILE = false; // '원본 다운로드'로 서버에 캐시된 영상 보유 여부(렌더가 업로드 없이 재사용)
|
||||||
let AI_AUDIO = []; // AI 자막 - 음성(Whisper+한국어) 세그먼트
|
let AI_AUDIO = []; // AI 자막 - 음성(Whisper+한국어) 세그먼트
|
||||||
@ -457,7 +457,7 @@
|
|||||||
try {
|
try {
|
||||||
const r = await api(API + '/' + VIDEO_ID + '/translate?target=ko&format=segments', { method:'POST' });
|
const r = await api(API + '/' + VIDEO_ID + '/translate?target=ko&format=segments', { method:'POST' });
|
||||||
const ko = (r && r.texts) || [];
|
const ko = (r && r.texts) || [];
|
||||||
if(!ko.length){ alert('번역할 세그먼트가 없습니다. 먼저 전사하세요.'); return; }
|
if(!ko.length){ alert('번역할 세그먼트가 없습니다. 먼저 자막 추출하세요.'); return; }
|
||||||
// 현재 작업 세그먼트(원본/무음제거 후)와 1:1 정렬 → 텍스트만 한국어로 교체(타임코드·배속 그대로)
|
// 현재 작업 세그먼트(원본/무음제거 후)와 1:1 정렬 → 텍스트만 한국어로 교체(타임코드·배속 그대로)
|
||||||
const segs = workingSegments();
|
const segs = workingSegments();
|
||||||
const koSegs = segs.map((sg, i) => ({ start: sg.start, end: sg.end, text: (ko[i] != null ? ko[i] : (sg.text || '')) }));
|
const koSegs = segs.map((sg, i) => ({ start: sg.start, end: sg.end, text: (ko[i] != null ? ko[i] : (sg.text || '')) }));
|
||||||
@ -495,7 +495,7 @@
|
|||||||
// 왼쪽: 저장된 Whisper 세그먼트 → 한국어 번역(LibreTranslate)
|
// 왼쪽: 저장된 Whisper 세그먼트 → 한국어 번역(LibreTranslate)
|
||||||
const audioP = (async () => {
|
const audioP = (async () => {
|
||||||
// 말소리가 없는(음악·화면자막만) 영상은 오류가 아니라 '음성 없음' 안내.
|
// 말소리가 없는(음악·화면자막만) 영상은 오류가 아니라 '음성 없음' 안내.
|
||||||
if(!SEGMENTS || !SEGMENTS.length) return { note: '음성 없음 — 말소리가 없거나(음악·화면자막만) ‘전사’ 미실행' };
|
if(!SEGMENTS || !SEGMENTS.length) return { note: '음성 없음 — 말소리가 없거나(음악·화면자막만) ‘자막 추출’ 미실행' };
|
||||||
const r = await api(API + '/' + VIDEO_ID + '/translate?target=ko&format=segments', { method:'POST' });
|
const r = await api(API + '/' + VIDEO_ID + '/translate?target=ko&format=segments', { method:'POST' });
|
||||||
const ko = (r && r.texts) || [];
|
const ko = (r && r.texts) || [];
|
||||||
return { segments: SEGMENTS.map((sg, i) => ({ start: sg.start, end: sg.end, text: (ko[i] != null ? ko[i] : (sg.text || '')) })) };
|
return { segments: SEGMENTS.map((sg, i) => ({ start: sg.start, end: sg.end, text: (ko[i] != null ? ko[i] : (sg.text || '')) })) };
|
||||||
@ -560,7 +560,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderVideo(){
|
async function renderVideo(){
|
||||||
if(!UPLOADED_FILE && !HAS_SERVER_FILE){ alert('먼저 "원본 다운로드" 또는 "영상 업로드·전사"를 실행하세요. (렌더에는 원본 영상이 필요합니다)'); return; }
|
if(!UPLOADED_FILE && !HAS_SERVER_FILE){ alert('먼저 "원본 다운로드" 또는 "영상 업로드·자막 추출"를 실행하세요. (렌더에는 원본 영상이 필요합니다)'); return; }
|
||||||
const pad = parseFloat(document.getElementById('trimPad').value) || 0;
|
const pad = parseFloat(document.getElementById('trimPad').value) || 0;
|
||||||
const gap = parseFloat(document.getElementById('trimGap').value) || 0;
|
const gap = parseFloat(document.getElementById('trimGap').value) || 0;
|
||||||
const speed = curSpeed();
|
const speed = curSpeed();
|
||||||
@ -593,7 +593,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 저장된 URL로 원본을 서버에서 yt-dlp로 받아 자동 전사. 성공 시 렌더도 업로드 없이 가능.
|
// 저장된 URL로 원본을 서버에서 yt-dlp로 받아 자동 자막 추출. 성공 시 렌더도 업로드 없이 가능.
|
||||||
async function downloadOriginal(){
|
async function downloadOriginal(){
|
||||||
const status = document.getElementById('transcribeStatus');
|
const status = document.getElementById('transcribeStatus');
|
||||||
const btn = document.getElementById('downloadBtn');
|
const btn = document.getElementById('downloadBtn');
|
||||||
@ -608,7 +608,7 @@
|
|||||||
updateCacheUI(true, d.sizeBytes || 0); // 캐시 보유 → 삭제버튼 노출 + 왼쪽 플레이어를 받은 원본으로
|
updateCacheUI(true, d.sizeBytes || 0); // 캐시 보유 → 삭제버튼 노출 + 왼쪽 플레이어를 받은 원본으로
|
||||||
const mb = d.sizeBytes ? ((d.sizeBytes/1048576).toFixed(1) + 'MB') : '';
|
const mb = d.sizeBytes ? ((d.sizeBytes/1048576).toFixed(1) + 'MB') : '';
|
||||||
status.style.color = '#4ade80';
|
status.style.color = '#4ade80';
|
||||||
status.textContent = '다운로드 완료 ✓ ' + mb + ' · 전사 중…';
|
status.textContent = '다운로드 완료 ✓ ' + mb + ' · 자막 추출 중…';
|
||||||
} catch(e){
|
} catch(e){
|
||||||
status.style.color = '#f87171';
|
status.style.color = '#f87171';
|
||||||
status.textContent = '다운로드 실패: ' + e.message;
|
status.textContent = '다운로드 실패: ' + e.message;
|
||||||
@ -616,24 +616,24 @@
|
|||||||
if(window.lucide) lucide.createIcons();
|
if(window.lucide) lucide.createIcons();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 2단계: 전사(분리). 실패해도 다운로드(영상)는 유지 → '전사' 버튼으로 재시도 가능.
|
// 2단계: 자막 추출(분리). 실패해도 다운로드(영상)는 유지 → '자막 추출' 버튼으로 재시도 가능.
|
||||||
try {
|
try {
|
||||||
const s = await runTranscribeCached();
|
const s = await runTranscribeCached();
|
||||||
status.style.color = '#4ade80';
|
status.style.color = '#4ade80';
|
||||||
status.textContent = '다운로드+전사 완료 · ' + (s.segments ? s.segments.length : 0) + '개 세그먼트'
|
status.textContent = '다운로드+자막 추출 완료 · ' + (s.segments ? s.segments.length : 0) + '개 세그먼트'
|
||||||
+ (s.language ? (' · ' + s.language) : '')
|
+ (s.language ? (' · ' + s.language) : '')
|
||||||
+ (s.duration ? (' · ' + mmss(s.duration)) : '');
|
+ (s.duration ? (' · ' + mmss(s.duration)) : '');
|
||||||
} catch(e){
|
} catch(e){
|
||||||
status.style.color = '#fbbf24';
|
status.style.color = '#fbbf24';
|
||||||
status.textContent = '다운로드는 완료 ✓ · 전사 실패(서버 혼잡일 수 있음): ' + e.message
|
status.textContent = '다운로드는 완료 ✓ · 자막 추출 실패(서버 혼잡일 수 있음): ' + e.message
|
||||||
+ ' — ‘전사’ 버튼으로 다시 시도하세요';
|
+ ' — ‘자막 추출’ 버튼으로 다시 시도하세요';
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false; btn.style.opacity = ''; btn.innerHTML = orig;
|
btn.disabled = false; btn.style.opacity = ''; btn.innerHTML = orig;
|
||||||
if(window.lucide) lucide.createIcons();
|
if(window.lucide) lucide.createIcons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 받아둔 원본을 Whisper 로 전사(다운로드와 분리). 세그먼트를 렌더하고 응답을 반환.
|
// 받아둔 원본을 Whisper 로 자막 추출(다운로드와 분리). 세그먼트를 렌더하고 응답을 반환.
|
||||||
async function runTranscribeCached(){
|
async function runTranscribeCached(){
|
||||||
const lang = document.getElementById('langSel').value;
|
const lang = document.getElementById('langSel').value;
|
||||||
const url = API + '/' + VIDEO_ID + '/transcribe-cached' + (lang ? ('?language=' + lang) : '');
|
const url = API + '/' + VIDEO_ID + '/transcribe-cached' + (lang ? ('?language=' + lang) : '');
|
||||||
@ -643,22 +643,22 @@
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// '전사' 버튼: 받아둔 원본만 다시 전사(전사만 실패했을 때 재시도용).
|
// '자막 추출' 버튼: 받아둔 원본만 다시 자막 추출(자막 추출만 실패했을 때 재시도용).
|
||||||
async function transcribeCached(){
|
async function transcribeCached(){
|
||||||
const status = document.getElementById('transcribeStatus');
|
const status = document.getElementById('transcribeStatus');
|
||||||
const btn = document.getElementById('transcribeCachedBtn');
|
const btn = document.getElementById('transcribeCachedBtn');
|
||||||
const orig = btn.innerHTML; btn.disabled = true; btn.style.opacity = '0.6'; btn.innerHTML = '전사 중…';
|
const orig = btn.innerHTML; btn.disabled = true; btn.style.opacity = '0.6'; btn.innerHTML = '자막 추출 중…';
|
||||||
status.style.display = 'block'; status.style.color = '#facc15';
|
status.style.display = 'block'; status.style.color = '#facc15';
|
||||||
status.textContent = '전사 중… (Shorts 기준 보통 20~60초, 서버 혼잡 시 더 걸림)';
|
status.textContent = '자막 추출 중… (Shorts 기준 보통 20~60초, 서버 혼잡 시 더 걸림)';
|
||||||
try {
|
try {
|
||||||
const s = await runTranscribeCached();
|
const s = await runTranscribeCached();
|
||||||
status.style.color = '#4ade80';
|
status.style.color = '#4ade80';
|
||||||
status.textContent = '전사 완료 · ' + (s.segments ? s.segments.length : 0) + '개 세그먼트'
|
status.textContent = '자막 추출 완료 · ' + (s.segments ? s.segments.length : 0) + '개 세그먼트'
|
||||||
+ (s.language ? (' · ' + s.language) : '')
|
+ (s.language ? (' · ' + s.language) : '')
|
||||||
+ (s.duration ? (' · ' + mmss(s.duration)) : '');
|
+ (s.duration ? (' · ' + mmss(s.duration)) : '');
|
||||||
} catch(e){
|
} catch(e){
|
||||||
status.style.color = '#f87171';
|
status.style.color = '#f87171';
|
||||||
status.textContent = '전사 실패: ' + e.message;
|
status.textContent = '자막 추출 실패: ' + e.message;
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false; btn.style.opacity = ''; btn.innerHTML = orig;
|
btn.disabled = false; btn.style.opacity = ''; btn.innerHTML = orig;
|
||||||
if(window.lucide) lucide.createIcons();
|
if(window.lucide) lucide.createIcons();
|
||||||
@ -668,7 +668,7 @@
|
|||||||
// 서버 캐시 보유 상태 → '받은 원본 삭제' 버튼 표시 + 렌더 캐시 사용 여부(HAS_SERVER_FILE) 갱신
|
// 서버 캐시 보유 상태 → '받은 원본 삭제' 버튼 표시 + 렌더 캐시 사용 여부(HAS_SERVER_FILE) 갱신
|
||||||
function updateCacheUI(cached, sizeBytes){
|
function updateCacheUI(cached, sizeBytes){
|
||||||
HAS_SERVER_FILE = !!cached;
|
HAS_SERVER_FILE = !!cached;
|
||||||
// 받아진 상태면 '원본 다운로드' 숨기고 '전사'·'받은 원본 삭제' 노출(없으면 반대)
|
// 받아진 상태면 '원본 다운로드' 숨기고 '자막 추출'·'받은 원본 삭제' 노출(없으면 반대)
|
||||||
const dl = document.getElementById('downloadBtn');
|
const dl = document.getElementById('downloadBtn');
|
||||||
const tr = document.getElementById('transcribeCachedBtn');
|
const tr = document.getElementById('transcribeCachedBtn');
|
||||||
const del = document.getElementById('deleteDlBtn');
|
const del = document.getElementById('deleteDlBtn');
|
||||||
@ -717,11 +717,11 @@
|
|||||||
v.style.display = 'block';
|
v.style.display = 'block';
|
||||||
document.getElementById('player').style.display = 'none';
|
document.getElementById('player').style.display = 'none';
|
||||||
|
|
||||||
// 2) 서버로 보내 Whisper 전사
|
// 2) 서버로 보내 Whisper 자막 추출
|
||||||
const status = document.getElementById('transcribeStatus');
|
const status = document.getElementById('transcribeStatus');
|
||||||
const btn = document.getElementById('uploadBtn');
|
const btn = document.getElementById('uploadBtn');
|
||||||
status.style.display = 'block'; status.style.color = '#facc15';
|
status.style.display = 'block'; status.style.color = '#facc15';
|
||||||
status.textContent = '전사 중… (Shorts 기준 보통 20~60초)';
|
status.textContent = '자막 추출 중… (Shorts 기준 보통 20~60초)';
|
||||||
btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6';
|
btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6';
|
||||||
try {
|
try {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
@ -732,12 +732,12 @@
|
|||||||
renderSegments(s.segments);
|
renderSegments(s.segments);
|
||||||
document.getElementById('transcript').value = s.transcript || '';
|
document.getElementById('transcript').value = s.transcript || '';
|
||||||
status.style.color = '#4ade80';
|
status.style.color = '#4ade80';
|
||||||
status.textContent = '전사 완료 · ' + (s.segments ? s.segments.length : 0) + '개 세그먼트'
|
status.textContent = '자막 추출 완료 · ' + (s.segments ? s.segments.length : 0) + '개 세그먼트'
|
||||||
+ (s.language ? (' · ' + s.language) : '')
|
+ (s.language ? (' · ' + s.language) : '')
|
||||||
+ (s.duration ? (' · ' + mmss(s.duration)) : '');
|
+ (s.duration ? (' · ' + mmss(s.duration)) : '');
|
||||||
} catch(e){
|
} catch(e){
|
||||||
status.style.color = '#f87171';
|
status.style.color = '#f87171';
|
||||||
status.textContent = '전사 실패: ' + e.message;
|
status.textContent = '자막 추출 실패: ' + e.message;
|
||||||
} finally {
|
} finally {
|
||||||
btn.style.pointerEvents = ''; btn.style.opacity = '';
|
btn.style.pointerEvents = ''; btn.style.opacity = '';
|
||||||
if(window.lucide) lucide.createIcons();
|
if(window.lucide) lucide.createIcons();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user