@@ -742,6 +910,7 @@ function applyUserUI(){
const b=document.getElementById('user-badge');b.textContent=currentUser.role==='admin'?'ADMIN':'USER';b.className='user-badge '+currentUser.role;
document.getElementById('admin-tab').style.display=currentUser.role==='admin'?'flex':'none';
document.getElementById('btn-hist-clear').style.display=currentUser.role==='admin'?'block':'none';
+ if(appSettings.openrouter_api_key_masked)loadOrModels();
}
const showLogin=()=>{document.getElementById('login-overlay').style.display='flex';stopSysMonitor()};
const hideLogin=()=>document.getElementById('login-overlay').style.display='none';
@@ -801,7 +970,6 @@ function populateModelSelects(){
fill(document.getElementById('ocr-ollama-model'),appSettings.ocr_ollama_model,'설정 기본 모델 사용');
fill(document.getElementById('setting-stt-model'),appSettings.stt_ollama_model,'(없음)');
fill(document.getElementById('setting-ocr-model'),appSettings.ocr_ollama_model,'(없음)');
- populateOrSelects();
}
// ══ 설정 ══
@@ -810,6 +978,9 @@ async function loadSettings(){
const th=appSettings.cpu_threads||0;cpuSlider.value=th;cpuDisplay.textContent=th===0?'0 (자동)':th+' 스레드';
document.getElementById('stt-timeout').value=appSettings.stt_timeout||0;
document.getElementById('ollama-timeout').value=appSettings.ollama_timeout||600;
+ if(appSettings.openrouter_url)document.getElementById('or-url').value=appSettings.openrouter_url;
+ if(appSettings.openrouter_api_key_masked&&document.getElementById('or-api-key'))
+ document.getElementById('or-api-key').placeholder='저장된 키: '+appSettings.openrouter_api_key_masked;
populateModelSelects()}catch{}
}
document.getElementById('btn-save-settings').addEventListener('click',async()=>{
@@ -837,64 +1008,19 @@ document.querySelectorAll('.nav-tab').forEach(btn=>{
if(btn.dataset.page==='admin')loadUsers();
if(btn.dataset.page==='settings'){loadSettings();fetchSysInfo()}
if(btn.dataset.page==='history'){histPage=1;loadHistory()}
+ if(btn.dataset.page==='subtitle')fillSubTransModels();
});
});
-// ══ STT — 배치 + 자막 ══
+// ══ STT — 배치 ══
const sttDrop=document.getElementById('stt-drop'),sttInput=document.getElementById('stt-input');
-let sttQueue=[],sttSubFmt='srt',sttTransVia='ollama';
-let languages={};
+let sttQueue=[];
+const AUDIO_EXTS=['mp3','mp4','wav','m4a','ogg','flac','aac','wma','webm','mkv','avi','mov','ts','mts','h264','h265'];
-// 언어 목록 로드
-async function loadLanguages(){
- try{const r=await api('GET','/api/languages');const d=await r.json();languages=d.languages||{};
- const sel1=document.getElementById('force-language');
- const sel2=document.getElementById('translate-to');
- Object.entries(languages).forEach(([code,name])=>{
- sel1.appendChild(Object.assign(document.createElement('option'),{value:code,textContent:`${name} (${code})`}));
- sel2.appendChild(Object.assign(document.createElement('option'),{value:code,textContent:`${name} (${code})`}));
- });
- }catch{}
-}
-
-// 번역 모델 드롭다운 채우기
-function fillTranslateModels(){
- const sel=document.getElementById('translate-model');
- const cur=sel.value;sel.innerHTML='
';
- const models=sttTransVia==='openrouter'?orModels:ollamaModels;
- models.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;if(m===cur)o.selected=true;sel.appendChild(o)});
-}
-
-// 자막 모드 토글
-document.getElementById('subtitle-mode').addEventListener('change',function(){
- document.getElementById('sub-opts').style.display=this.checked?'block':'none';
-});
-
-// 포맷 버튼
-document.querySelectorAll('.fmt-btn[data-fmt]').forEach(btn=>{
- btn.addEventListener('click',()=>{document.querySelectorAll('.fmt-btn[data-fmt]').forEach(b=>b.classList.remove('active'));btn.classList.add('active');sttSubFmt=btn.dataset.fmt});
-});
-
-// 번역 언어 선택 → 모델 옵션 표시
-document.getElementById('translate-to').addEventListener('change',function(){
- document.getElementById('trans-model-wrap').style.display=this.value?'block':'none';
- if(this.value)fillTranslateModels();
-});
-
-// 번역 엔진 선택
-document.querySelectorAll('button[data-via]').forEach(btn=>{
- btn.addEventListener('click',()=>{
- document.querySelectorAll('button[data-via]').forEach(b=>b.classList.remove('active'));
- btn.classList.add('active');sttTransVia=btn.dataset.via;fillTranslateModels();
- });
-});
-
-// 파일 추가
function addSttFiles(fileList){
- const AUDIO=['mp3','mp4','wav','m4a','ogg','flac','aac','wma','webm','mkv','avi','mov','ts','mts','h264','h265'];
- const files=Array.from(fileList).filter(f=>AUDIO.includes(f.name.split('.').pop().toLowerCase()));
+ const files=Array.from(fileList).filter(f=>AUDIO_EXTS.includes(f.name.split('.').pop().toLowerCase()));
if(!files.length)return;
- files.forEach(f=>sttQueue.push({file:f,taskId:null,outputFile:null,srtFile:null,vttFile:null,srtOrigFile:null,status:'waiting',el:null}));
+ files.forEach(f=>sttQueue.push({file:f,taskId:null,outputFile:null,status:'waiting'}));
renderSttQueue();document.getElementById('stt-btn').disabled=false;
}
sttInput.addEventListener('change',()=>addSttFiles(sttInput.files));
@@ -909,26 +1035,19 @@ function renderSttQueue(){
qEl.style.display='block';list.innerHTML='';
sttQueue.forEach((item,i)=>{
const div=document.createElement('div');div.className='batch-item '+item.status;
- const dlBtns=item.status==='done'?[
- item.outputFile?`
`:'',
- item.srtFile?`
`:'',
- item.vttFile?`
`:'',
- item.srtOrigFile?`
`:'',
- ].filter(Boolean).join(''):''
- div.innerHTML=`
${{waiting:'대기',running:'변환중',done:'완료',failed:'실패'}[item.status]}${dlBtns}`;
- item.el=div;list.appendChild(div);
+ div.innerHTML=`
${{waiting:'대기',running:'변환중',done:'완료',failed:'실패'}[item.status]}${item.status==='done'&&item.outputFile?``:''}`;
+ list.appendChild(div);
});
const done=sttQueue.filter(i=>i.status==='done').length,failed=sttQueue.filter(i=>i.status==='failed').length,running=sttQueue.filter(i=>i.status==='running').length;
sum.innerHTML=`전체
${sttQueue.length}개 · 완료
${done} · 실패
${failed}${running?` · 진행중
${running}`:''}`;
}
-// 엔진 버튼
document.querySelectorAll('#page-stt .engine-btn').forEach(btn=>{
btn.addEventListener('click',()=>{
document.querySelectorAll('#page-stt .engine-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');sttEngine=btn.dataset.engine;
document.getElementById('stt-ollama-opts').classList.toggle('visible',sttEngine==='whisper+ollama');
- document.getElementById('stt-or-opts').classList.toggle('visible',sttEngine==='whisper+openrouter');
+ document.getElementById('stt-or-opts')?.classList.toggle('visible',sttEngine==='whisper+openrouter');
document.getElementById('stt-btn').className='btn-start '+(sttEngine!=='whisper'?'purple':'green');
});
});
@@ -939,22 +1058,13 @@ document.getElementById('stt-btn').addEventListener('click',async()=>{
document.getElementById('stt-err').style.display='none';
document.getElementById('stt-btn').disabled=true;
document.getElementById('stt-prog').style.display='block';
- setProg('stt',0,`${pending.length}개 파일 업로드 중...`);
-
- const subMode=document.getElementById('subtitle-mode').checked;
+ setProg('stt',0,`${pending.length}개 업로드 중...`);
const fd=new FormData();
pending.forEach(item=>fd.append('files',item.file));
fd.append('use_ollama',sttEngine==='whisper+ollama'?'true':'false');
fd.append('ollama_model',document.getElementById('stt-ollama-model')?.value||'');
fd.append('use_openrouter',sttEngine==='whisper+openrouter'?'true':'false');
fd.append('openrouter_model',document.getElementById('stt-or-model')?.value||'');
- fd.append('subtitle_mode',subMode?'true':'false');
- fd.append('subtitle_format',sttSubFmt);
- fd.append('force_language',document.getElementById('force-language').value||'');
- fd.append('translate_to',document.getElementById('translate-to').value||'');
- fd.append('translate_model',document.getElementById('translate-model').value||'');
- fd.append('translate_via',sttTransVia);
-
try{
const url=pending.length===1?'/api/transcribe':'/api/transcribe/batch';
const r=await api('POST',url,fd);const d=await r.json();
@@ -963,38 +1073,24 @@ document.getElementById('stt-btn').addEventListener('click',async()=>{
let pi=0;
sttQueue.forEach((qItem,qi)=>{
if(qItem.status!=='waiting')return;
- const taskItem=items[pi++];if(!taskItem)return;
- if(taskItem.error){qItem.status='failed';return}
- qItem.status='running';qItem.taskId=taskItem.task_id;renderSttQueue();
- pollSttItem(qi,taskItem.task_id);
+ const ti=items[pi++];if(!ti)return;
+ if(ti.error){qItem.status='failed';return}
+ qItem.status='running';qItem.taskId=ti.task_id;renderSttQueue();
+ const t=setInterval(async()=>{
+ try{
+ const r2=await api('GET','/api/status/'+ti.task_id);if(r2.status===401){clearInterval(t);showLogin();return}
+ const d2=await r2.json();
+ if(d2.state==='success'){clearInterval(t);qItem.outputFile=d2.output_file||null;qItem.status='done';renderSttQueue();
+ if(sttQueue.filter(i=>i.status==='done').length===1&&!sttQueue.some(i=>i.status==='running'))showSttResult(d2);
+ checkSttBatchDone();}
+ else if(d2.state==='failure'){clearInterval(t);qItem.status='failed';renderSttQueue();checkSttBatchDone();}
+ else{const done=sttQueue.filter(i=>i.status==='done').length;setProg('stt',20+Math.round((done/sttQueue.length)*75),d2.message||'처리 중...')}
+ }catch{}
+ },1800);
});
- setProg('stt',20,`${items.length}개 변환 중...`);
}catch(e){showErr('stt-err',e.message);document.getElementById('stt-btn').disabled=false;document.getElementById('stt-prog').style.display='none'}
});
-function pollSttItem(qi,taskId){
- const t=setInterval(async()=>{
- try{
- const r=await api('GET','/api/status/'+taskId);if(r.status===401){clearInterval(t);showLogin();return}
- const d=await r.json();
- if(d.state==='success'){
- clearInterval(t);
- const item=sttQueue[qi];
- item.outputFile=d.output_file||null;item.srtFile=d.srt_file||null;
- item.vttFile=d.vtt_file||null;item.srtOrigFile=d.srt_original_file||null;
- item.status='done';renderSttQueue();checkSttBatchDone();
- if(sttQueue.filter(i=>i.status!=='waiting'&&i.status!=='done'&&i.status!=='failed').length===0&&
- sttQueue.filter(i=>i.status==='done').length===1) showSttResult(d);
- } else if(d.state==='failure'){
- clearInterval(t);sttQueue[qi].status='failed';renderSttQueue();checkSttBatchDone();
- } else {
- const done=sttQueue.filter(i=>i.status==='done').length;
- setProg('stt',20+Math.round((done/sttQueue.length)*75),d.message||'처리 중...');
- }
- }catch{}
- },1800);
-}
-
function checkSttBatchDone(){
if(sttQueue.every(i=>['done','failed','waiting'].includes(i.status))){
const done=sttQueue.filter(i=>i.status==='done').length;
@@ -1003,48 +1099,32 @@ function checkSttBatchDone(){
document.getElementById('stt-btn').disabled=false;
}
}
-
function setSttLoading(on){document.getElementById('stt-btn').disabled=on;document.getElementById('stt-prog').style.display=on?'block':'none';if(on)setProg('stt',0,'준비 중...')}
-
function showSttResult(d){
sttOutputFile=d.output_file;
- document.getElementById('stt-mlang').textContent=((d.language||'')+( d.translated?' → '+d.translate_to:'')).toUpperCase();
+ document.getElementById('stt-mlang').textContent=(d.language||'').toUpperCase();
document.getElementById('stt-mdur').textContent=fmtDur(d.duration);
document.getElementById('stt-msegs').textContent=(d.segments||[]).length+'개';
- const chip=document.getElementById('stt-mollama-chip');
- if(d.ollama_used){chip.style.display='block';document.getElementById('stt-mollama').textContent=d.ollama_model}else chip.style.display='none';
+ const chip=document.getElementById('stt-mollama-chip');if(d.ollama_used){chip.style.display='block';document.getElementById('stt-mollama').textContent=d.ollama_model}else chip.style.display='none';
document.getElementById('stt-meta').style.display='flex';document.getElementById('stt-tabs').style.display='flex';
document.getElementById('stt-empty').style.display='none';document.getElementById('stt-result').style.display='block';document.getElementById('stt-result').value=d.text||'';
const sl=document.getElementById('stt-seglist');sl.innerHTML='';
(d.segments||[]).forEach(s=>{const row=document.createElement('div');row.className='seg-item';row.innerHTML=`
${fmtTime(s.start)}
→${fmtTime(s.end)}
${esc(s.text)}
`;sl.appendChild(row)});
- document.getElementById('stt-actions').style.display='flex';
- // 자막 다운로드 버튼
- const srtBtn=document.getElementById('stt-dl-srt'),vttBtn=document.getElementById('stt-dl-vtt'),origBtn=document.getElementById('stt-dl-srt-orig');
- srtBtn.style.display=d.srt_file?'flex':'none';if(d.srt_file)srtBtn.onclick=()=>dlFile(d.srt_file);
- vttBtn.style.display=d.vtt_file?'flex':'none';if(d.vtt_file)vttBtn.onclick=()=>dlFile(d.vtt_file);
- origBtn.style.display=d.srt_original_file?'flex':'none';if(d.srt_original_file)origBtn.onclick=()=>dlFile(d.srt_original_file);
- setSttLoading(false);
+ document.getElementById('stt-actions').style.display='flex';setSttLoading(false);
}
document.getElementById('stt-copy').addEventListener('click',()=>copyText(document.getElementById('stt-result').value,document.getElementById('stt-copy')));
document.getElementById('stt-dl').addEventListener('click',()=>dlFile(sttOutputFile));
-document.getElementById('stt-new').addEventListener('click',()=>{
- sttQueue=[];sttInput.value='';sttOutputFile=null;renderSttQueue();
- ['stt-prog','stt-err','stt-meta','stt-tabs','stt-actions'].forEach(id=>document.getElementById(id).style.display='none');
- document.getElementById('stt-empty').style.display='flex';
- document.getElementById('stt-result').style.display='none';document.getElementById('stt-result').value='';
- document.getElementById('stt-seglist').innerHTML='';document.getElementById('stt-btn').disabled=true;resetTabs('stt-tabs');
- ['stt-dl-srt','stt-dl-vtt','stt-dl-srt-orig'].forEach(id=>document.getElementById(id).style.display='none');
-});
+document.getElementById('stt-new').addEventListener('click',()=>{sttQueue=[];sttInput.value='';sttOutputFile=null;renderSttQueue();['stt-prog','stt-err','stt-meta','stt-tabs','stt-actions'].forEach(id=>document.getElementById(id).style.display='none');document.getElementById('stt-empty').style.display='flex';document.getElementById('stt-result').style.display='none';document.getElementById('stt-result').value='';document.getElementById('stt-seglist').innerHTML='';document.getElementById('stt-btn').disabled=true;resetTabs('stt-tabs')});
// ══ OCR — 배치 ══
const ocrDrop=document.getElementById('ocr-drop'),ocrInput=document.getElementById('ocr-input');
let ocrQueue=[];
+const IMG_EXTS=['jpg','jpeg','png','bmp','tiff','tif','webp','gif'];
function addOcrFiles(fileList){
- const IMG=['jpg','jpeg','png','bmp','tiff','tif','webp','gif'];
- const files=Array.from(fileList).filter(f=>IMG.includes(f.name.split('.').pop().toLowerCase()));
+ const files=Array.from(fileList).filter(f=>IMG_EXTS.includes(f.name.split('.').pop().toLowerCase()));
if(!files.length)return;
- files.forEach(f=>ocrQueue.push({file:f,taskId:null,txtFile:null,xlsxFile:null,status:'waiting',el:null}));
+ files.forEach(f=>ocrQueue.push({file:f,taskId:null,txtFile:null,xlsxFile:null,status:'waiting'}));
renderOcrQueue();document.getElementById('ocr-btn').disabled=false;
}
ocrInput.addEventListener('change',()=>addOcrFiles(ocrInput.files));
@@ -1059,8 +1139,8 @@ function renderOcrQueue(){
qEl.style.display='block';list.innerHTML='';
ocrQueue.forEach((item,i)=>{
const div=document.createElement('div');div.className='batch-item '+item.status;
- div.innerHTML=`
${{waiting:'대기',running:'인식중',done:'완료',failed:'실패'}[item.status]}${item.status==='done'?[item.txtFile?``:'',item.xlsxFile?``:''].filter(Boolean).join(''):''}`;
- item.el=div;list.appendChild(div);
+ div.innerHTML=`
${{waiting:'대기',running:'인식중',done:'완료',failed:'실패'}[item.status]}${item.status==='done'?[item.txtFile?``:'',item.xlsxFile?``:''].filter(Boolean).join(''):''}`;
+ list.appendChild(div);
});
const done=ocrQueue.filter(i=>i.status==='done').length,failed=ocrQueue.filter(i=>i.status==='failed').length,running=ocrQueue.filter(i=>i.status==='running').length;
sum.innerHTML=`전체
${ocrQueue.length}개 · 완료
${done} · 실패
${failed}${running?` · 진행중
${running}`:''}`;
@@ -1071,11 +1151,11 @@ document.querySelectorAll('#page-ocr .engine-btn').forEach(btn=>{
document.querySelectorAll('#page-ocr .engine-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');ocrEngine=btn.dataset.engine;
document.getElementById('ocr-ollama-opts').classList.toggle('visible',ocrEngine==='ollama');
- document.getElementById('ocr-or-opts').classList.toggle('visible',ocrEngine==='openrouter');
+ document.getElementById('ocr-or-opts')?.classList.toggle('visible',ocrEngine==='openrouter');
document.getElementById('ocr-btn').className='btn-start '+(ocrEngine!=='paddle'?'purple':'green');
});
});
-document.getElementById('cprompt-toggle').addEventListener('click',()=>{const ta=document.getElementById('custom-prompt');const open=ta.style.display!=='block';ta.style.display=open?'block':'none';document.getElementById('cprompt-toggle').textContent=(open?'▼':'▶')+' 커스텀 프롬프트'});
+document.getElementById('cprompt-toggle')?.addEventListener('click',()=>{const ta=document.getElementById('custom-prompt');const open=ta.style.display!=='block';ta.style.display=open?'block':'none';document.getElementById('cprompt-toggle').textContent=(open?'▼':'▶')+' 커스텀 프롬프트'});
document.querySelectorAll('.mode-btn').forEach(btn=>{btn.addEventListener('click',()=>{document.querySelectorAll('.mode-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');ocrMode=btn.dataset.mode;document.getElementById('mode-desc').textContent=ocrMode==='structure'?'표 구조를 감지하고 Excel로 저장합니다':'일반 텍스트와 글자를 인식합니다'})});
document.getElementById('ocr-btn').addEventListener('click',async()=>{
@@ -1088,9 +1168,9 @@ document.getElementById('ocr-btn').addEventListener('click',async()=>{
const fd=new FormData();
pending.forEach(item=>fd.append('files',item.file));
fd.append('mode',ocrMode);fd.append('backend',ocrEngine);
- fd.append('ollama_model',ocrEngine==='ollama'?(document.getElementById('ocr-ollama-model').value||''):'');
+ fd.append('ollama_model',ocrEngine==='ollama'?(document.getElementById('ocr-ollama-model')?.value||''):'');
fd.append('openrouter_model',ocrEngine==='openrouter'?(document.getElementById('ocr-or-model')?.value||''):'');
- const cp=ocrEngine==='openrouter'?(document.getElementById('custom-prompt-or')?.value||''):(document.getElementById('custom-prompt').value||'');
+ const cp=ocrEngine==='openrouter'?(document.getElementById('custom-prompt-or')?.value||''):(document.getElementById('custom-prompt')?.value||'');
fd.append('custom_prompt',cp);
try{
const url=pending.length===1?'/api/ocr':'/api/ocr/batch';
@@ -1099,23 +1179,24 @@ document.getElementById('ocr-btn').addEventListener('click',async()=>{
let pi=0;
ocrQueue.forEach((qItem,qi)=>{
if(qItem.status!=='waiting')return;
- const taskItem=items[pi++];if(!taskItem)return;
- if(taskItem.error){qItem.status='failed';return}
- qItem.status='running';qItem.taskId=taskItem.task_id;renderOcrQueue();
+ const ti=items[pi++];if(!ti)return;
+ if(ti.error){qItem.status='failed';return}
+ qItem.status='running';qItem.taskId=ti.task_id;renderOcrQueue();
const t=setInterval(async()=>{
try{
- const r2=await api('GET','/api/status/'+taskItem.task_id);if(r2.status===401){clearInterval(t);showLogin();return}
+ const r2=await api('GET','/api/status/'+ti.task_id);if(r2.status===401){clearInterval(t);showLogin();return}
const d2=await r2.json();
if(d2.state==='success'){clearInterval(t);qItem.txtFile=d2.txt_file||null;qItem.xlsxFile=d2.xlsx_file||null;qItem.status='done';renderOcrQueue();
- if(ocrQueue.filter(i=>i.status==='done').length===1&&ocrQueue.filter(i=>i.status==='running').length===0)showOcrResult(d2);
- if(ocrQueue.every(i=>['done','failed','waiting'].includes(i.status))){const done=ocrQueue.filter(i=>i.status==='done').length;setProg('ocr',100,`완료 ${done}/${ocrQueue.length}개`);setTimeout(()=>document.getElementById('ocr-prog').style.display='none',2000);document.getElementById('ocr-btn').disabled=false;}
- } else if(d2.state==='failure'){clearInterval(t);qItem.status='failed';renderOcrQueue();}
- else{const done=ocrQueue.filter(i=>i.status==='done').length;setProg('ocr',20+Math.round((done/ocrQueue.length)*75),d2.message||'처리중...')}
+ if(ocrQueue.filter(i=>i.status==='done').length===1&&!ocrQueue.some(i=>i.status==='running'))showOcrResult(d2);
+ if(ocrQueue.every(i=>['done','failed','waiting'].includes(i.status))){const done=ocrQueue.filter(i=>i.status==='done').length;setProg('ocr',100,`완료 ${done}/${ocrQueue.length}개`);setTimeout(()=>document.getElementById('ocr-prog').style.display='none',2000);document.getElementById('ocr-btn').disabled=false;}}
+ else if(d2.state==='failure'){clearInterval(t);qItem.status='failed';renderOcrQueue();}
+ else{const done=ocrQueue.filter(i=>i.status==='done').length;setProg('ocr',20+Math.round((done/ocrQueue.length)*75),d2.message||'처리 중...')}
}catch{}
},1800);
});
}catch(e){showErr('ocr-err',e.message);document.getElementById('ocr-btn').disabled=false;document.getElementById('ocr-prog').style.display='none'}
});
+
function setOcrLoading(on){const isAI=ocrEngine!=='paddle',c=isAI?'var(--purple)':'var(--accent)';document.getElementById('ocr-btn').disabled=on;document.getElementById('ocr-prog').style.display=on?'block':'none';document.getElementById('ocr-wave').style.display=on?'flex':'none';document.getElementById('ocr-pfill').style.background=c;document.getElementById('ocr-ppct').style.color=c;document.querySelectorAll('#ocr-wave .wave-bar').forEach(b=>b.style.background=c);if(on)setProg('ocr',0,'준비 중...')}
function showOcrResult(d){
ocrOutputTxt=d.txt_file||null;ocrOutputXlsx=d.xlsx_file||null;const io=d.backend!=='paddle';
@@ -1128,12 +1209,184 @@ function showOcrResult(d){
document.getElementById('ocr-copy').addEventListener('click',()=>copyText(document.getElementById('ocr-result').value,document.getElementById('ocr-copy')));
document.getElementById('ocr-dl-txt').addEventListener('click',()=>dlFile(ocrOutputTxt));
document.getElementById('ocr-dl-xlsx').addEventListener('click',()=>dlFile(ocrOutputXlsx));
-document.getElementById('ocr-new').addEventListener('click',()=>{
- ocrQueue=[];ocrInput.value='';ocrOutputTxt=null;ocrOutputXlsx=null;renderOcrQueue();
- ['ocr-prog','ocr-err','ocr-meta','ocr-tabs','ocr-actions'].forEach(id=>document.getElementById(id).style.display='none');
- document.getElementById('ocr-empty').style.display='flex';document.getElementById('ocr-result').style.display='none';document.getElementById('ocr-result').value='';document.getElementById('ocr-linelist').innerHTML='';document.getElementById('ocr-tablelist').innerHTML='';document.getElementById('ocr-btn').disabled=true;resetTabs('ocr-tabs');
+document.getElementById('ocr-new').addEventListener('click',()=>{ocrQueue=[];ocrInput.value='';ocrOutputTxt=null;ocrOutputXlsx=null;renderOcrQueue();['ocr-prog','ocr-err','ocr-meta','ocr-tabs','ocr-actions'].forEach(id=>document.getElementById(id).style.display='none');document.getElementById('ocr-empty').style.display='flex';document.getElementById('ocr-result').style.display='none';document.getElementById('ocr-result').value='';document.getElementById('ocr-linelist').innerHTML='';document.getElementById('ocr-tablelist').innerHTML='';document.getElementById('ocr-btn').disabled=true;resetTabs('ocr-tabs')});
+
+// ══ 자막 ══
+const subDrop=document.getElementById('sub-drop'),subInput=document.getElementById('sub-input');
+let subFile=null, subTransVia='ollama', subFmt='srt';
+
+subInput.addEventListener('change',()=>setSubFile(subInput.files[0]));
+subDrop.addEventListener('dragover',e=>{e.preventDefault();subDrop.classList.add('dragover')});
+subDrop.addEventListener('dragleave',()=>subDrop.classList.remove('dragover'));
+subDrop.addEventListener('drop',e=>{e.preventDefault();subDrop.classList.remove('dragover');setSubFile(e.dataTransfer.files[0])});
+
+function setSubFile(f){
+ if(!f)return;subFile=f;
+ document.getElementById('sub-info').style.display='block';
+ document.getElementById('sub-fname').textContent=f.name;
+ document.getElementById('sub-fsize').textContent=fmtBytes(f.size);
+ document.getElementById('sub-btn').disabled=false;
+ document.getElementById('sub-err').style.display='none';
+}
+
+// 자막 포맷 버튼
+document.querySelectorAll('#page-subtitle .fmt-btn[data-fmt]').forEach(btn=>{
+ btn.addEventListener('click',()=>{
+ document.querySelectorAll('#page-subtitle .fmt-btn[data-fmt]').forEach(b=>b.classList.remove('active'));
+ btn.classList.add('active');subFmt=btn.dataset.fmt;
+ });
});
+// 번역 언어 선택 → 번역 엔진/모델 표시
+document.getElementById('sub-trans-lang').addEventListener('change',function(){
+ document.getElementById('sub-trans-engine-wrap').style.display=this.value?'flex':'none';
+ if(this.value)fillSubTransModels();
+});
+
+// 번역 엔진 버튼
+document.querySelectorAll('button[data-via]').forEach(btn=>{
+ btn.addEventListener('click',()=>{
+ document.querySelectorAll('button[data-via]').forEach(b=>b.classList.remove('active'));
+ btn.classList.add('active');subTransVia=btn.dataset.via;fillSubTransModels();
+ });
+});
+
+function fillSubTransModels(){
+ const sel=document.getElementById('sub-trans-model');if(!sel)return;
+ const cur=sel.value;
+ sel.innerHTML='
';
+ const models=subTransVia==='openrouter'?orModels:ollamaModels;
+ models.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;if(m===cur)o.selected=true;sel.appendChild(o)});
+}
+
+// 스텝 표시기 업데이트
+function setSubStep(step, status){
+ // step: 1|2|3, status: waiting|running|done|failed
+ const icon=document.getElementById(`sub-sicon-${step}`);
+ if(!icon)return;
+ icon.className='sub-step-icon '+status;
+ icon.textContent=status==='done'?'✓':status==='failed'?'✗':status==='running'?'⚡':'⏳';
+ const dot=document.getElementById(`sdot-${step}`);
+ if(dot){dot.className='step-dot '+(status==='done'?'done':status==='running'?'active':'');}
+ if(step>1){const line=document.getElementById(`sline-${step-1}`);if(line)line.className='step-line '+(status!=='waiting'?'done':'');}
+ const lbl=document.getElementById(`slabel-${step}`);
+ if(lbl)lbl.className='step-label '+(status==='done'?'done':status==='running'?'active':'');
+}
+
+document.getElementById('sub-btn').addEventListener('click',async()=>{
+ if(!subFile)return;
+ const transLang=document.getElementById('sub-trans-lang').value;
+ const fd=new FormData();
+ fd.append('file',subFile);
+ fd.append('src_language',document.getElementById('sub-src-lang').value||'');
+ fd.append('subtitle_fmt',subFmt);
+ fd.append('translate_to',transLang);
+ fd.append('trans_model',transLang?(document.getElementById('sub-trans-model')?.value||''):'');
+ fd.append('trans_via',subTransVia);
+
+ document.getElementById('sub-btn').disabled=true;
+ document.getElementById('sub-err').style.display='none';
+ document.getElementById('sub-prog-box').style.display='block';
+ document.getElementById('sub-result-card').style.display='none';
+ document.getElementById('sub-prog-bar').style.width='0%';
+ [1,2,3].forEach(s=>setSubStep(s,'waiting'));
+ setSubStep(1,'running');
+
+ try{
+ const r=await api('POST','/api/subtitle',fd);
+ const d=await r.json();
+ if(!r.ok)throw new Error(d.detail||'업로드 실패');
+ pollSubtitle(d.task_id, transLang);
+ }catch(e){
+ showErr('sub-err',e.message);
+ document.getElementById('sub-btn').disabled=false;
+ document.getElementById('sub-prog-box').style.display='none';
+ }
+});
+
+function pollSubtitle(taskId, transLang){
+ let prevStep=0;
+ const t=setInterval(async()=>{
+ try{
+ const r=await api('GET','/api/status/'+taskId);
+ if(r.status===401){clearInterval(t);showLogin();return}
+ const d=await r.json();
+
+ if(d.state==='progress'||d.state==='success'){
+ const step=d.step||1;const prog=d.progress||0;
+ document.getElementById('sub-prog-bar').style.width=prog+'%';
+
+ // 스텝 전환
+ if(step!==prevStep){
+ if(prevStep>0&&prevStep
setSubStep(s,'done'));
+ document.getElementById('sub-prog-bar').style.width='100%';
+ setTimeout(()=>showSubResult(d),400);
+ } else if(d.state==='failure'){
+ clearInterval(t);
+ if(prevStep>0) setSubStep(prevStep,'failed');
+ showErr('sub-err',d.message||'자막 생성 실패');
+ document.getElementById('sub-btn').disabled=false;
+ }
+ }catch{}
+ },1800);
+}
+
+const LANG_NAMES={ko:'한국어',en:'English',ja:'日本語',zh:'中文(简体)',
+ 'zh-tw':'中文(繁體)',fr:'Français',de:'Deutsch',es:'Español',
+ it:'Italiano',pt:'Português',ru:'Русский',ar:'العربية',
+ vi:'Tiếng Việt',th:'ไทย',id:'Bahasa Indonesia',nl:'Nederlands',
+ pl:'Polski',tr:'Türkçe',sv:'Svenska',uk:'Українська',hi:'हिन्दी'};
+function langName(code){return LANG_NAMES[code]||code||'알 수 없음'}
+
+function showSubResult(d){
+ document.getElementById('sub-prog-box').style.display='none';
+ const rc=document.getElementById('sub-result-card');rc.style.display='block';
+ document.getElementById('sub-res-lang').textContent=langName(d.detected_language);
+ document.getElementById('sub-res-dur').textContent=fmtDur(d.duration);
+ document.getElementById('sub-res-segs').textContent=(d.segment_count||0)+'개';
+ document.getElementById('sub-res-trans').textContent=d.translated?langName(d.translate_to):'없음';
+
+ const grid=document.getElementById('sub-dl-grid');grid.innerHTML='';
+ const addBtn=(label,lang,file,cls='')=>{
+ if(!file)return;
+ const ext=file.split('.').pop().toUpperCase();
+ const btn=document.createElement('button');
+ btn.className='sub-dl-btn '+(cls);
+ btn.innerHTML=`📄${ext} ${label}${langName(lang)}`;
+ btn.onclick=()=>dlFile(file);
+ grid.appendChild(btn);
+ };
+ addBtn('원어',d.detected_language,d.srt_orig);
+ addBtn('원어',d.detected_language,d.vtt_orig);
+ addBtn('번역',d.translate_to,d.srt_trans,'trans');
+ addBtn('번역',d.translate_to,d.vtt_trans,'trans');
+
+ document.getElementById('sub-btn').disabled=false;
+}
+
+document.getElementById('sub-new').addEventListener('click',()=>{
+ subFile=null;subInput.value='';
+ document.getElementById('sub-info').style.display='none';
+ document.getElementById('sub-prog-box').style.display='none';
+ document.getElementById('sub-result-card').style.display='none';
+ document.getElementById('sub-err').style.display='none';
+ document.getElementById('sub-btn').disabled=true;
+ document.getElementById('sub-prog-bar').style.width='0%';
+ [1,2,3].forEach(s=>setSubStep(s,'waiting'));
+});
// ══ HISTORY ══
document.querySelectorAll('.hist-filter-btn').forEach(btn=>{btn.addEventListener('click',()=>{document.querySelectorAll('.hist-filter-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');histType=btn.dataset.type;histPage=1;loadHistory()})});
@@ -1387,24 +1640,19 @@ function esc(s){return String(s||'').replace(/&/g,'&').replace(/btn.textContent=o,1500)}catch{}}
// ══ OPENROUTER ══
+let orModels=[],orVisionModels=[];
async function loadOrModels(){
try{const r=await api('GET','/api/openrouter/models');const d=await r.json();
- const wrap=document.getElementById('or-models-wrap');
- if(d.connected){orModels=d.models||[];orVisionModels=d.vision_models||[];
- wrap.style.display='block';document.getElementById('or-connected-badge').textContent=`✓ 연결됨 — Vision ${orVisionModels.length}개 / 전체 ${orModels.length}개`;populateOrSelects('vision');}
- else wrap.style.display='none';
+ if(d.connected){orModels=d.models||[];orVisionModels=d.vision_models||[];populateOrSelects();}
}catch{}
}
-let orFilter='vision';
-document.querySelectorAll('.or-model-tab').forEach(btn=>{btn.addEventListener('click',()=>{document.querySelectorAll('.or-model-tab').forEach(b=>b.classList.remove('active'));btn.classList.add('active');orFilter=btn.dataset.filter;populateOrSelects(orFilter)})});
-function populateOrSelects(filter){
- filter=filter||orFilter;const list=filter==='vision'?orVisionModels:filter==='text'?orTextModels:orModels;
- const fillOr=(sel,def)=>{if(!sel)return;const cur=sel.value||def||'';sel.innerHTML='';list.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;if(m===cur)o.selected=true;sel.appendChild(o)})};
- fillOr(document.getElementById('setting-or-stt-model'),appSettings.openrouter_stt_model);
- const ocrSel=document.getElementById('setting-or-ocr-model');
- if(ocrSel){const cur=ocrSel.value||appSettings.openrouter_ocr_model||'';ocrSel.innerHTML='';orVisionModels.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;if(m===cur)o.selected=true;ocrSel.appendChild(o)})}
- fillOr(document.getElementById('stt-or-model'),appSettings.openrouter_stt_model);
- const ocrPage=document.getElementById('ocr-or-model');if(ocrPage){const cur=ocrPage.value||appSettings.openrouter_ocr_model||'';ocrPage.innerHTML='';orVisionModels.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;if(m===cur)o.selected=true;ocrPage.appendChild(o)})}
+function populateOrSelects(){
+ const fill=(sel,def,list)=>{if(!sel)return;const cur=sel.value||def||'';sel.innerHTML='';list.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;if(m===cur)o.selected=true;sel.appendChild(o)})};
+ fill(document.getElementById('setting-or-stt-model'),appSettings.openrouter_stt_model,orModels);
+ fill(document.getElementById('setting-or-ocr-model'),appSettings.openrouter_ocr_model,orVisionModels);
+ fill(document.getElementById('stt-or-model'),appSettings.openrouter_stt_model,orModels);
+ fill(document.getElementById('ocr-or-model'),appSettings.openrouter_ocr_model,orVisionModels);
+ fillSubTransModels();
}
document.getElementById('btn-or-test')?.addEventListener('click',async()=>{
const key=document.getElementById('or-api-key').value.trim(),url=document.getElementById('or-url').value.trim()||'https://openrouter.ai/api/v1';
@@ -1416,8 +1664,8 @@ document.getElementById('btn-or-test')?.addEventListener('click',async()=>{
result.style.color=d.ok?'var(--accent)':'var(--warn)';result.textContent=d.message;if(d.ok)loadOrModels();}
catch{result.style.color='var(--warn)';result.textContent='요청 실패'}
});
+document.getElementById('btn-refresh-models')?.addEventListener('click',()=>{loadOllamaModels();loadOrModels()});
-loadLanguages();
checkAuth();