Spaces:
Sleeping
Sleeping
Heebin Moon
Claude Opus 4.6
Switch default LLM to Gemini Flash-Lite and fix SSE for Cloudflare
97fa2d2 Download templates/index.html from heebin/YoutubeScript: direct link, hf CLI and curl.
- Browser
- Download file 60.1 kB
-
https://huggingface.co/spaces/heebin/YoutubeScript/resolve/main/templates/index.html
- Command line
-
hf download hf://spaces/heebin/YoutubeScript/templates/index.html
-
curl -L -o index.html https://huggingface.co/spaces/heebin/YoutubeScript/resolve/main/templates/index.html
60.1 kB
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>YouTube Script Extractor</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: #0f0f0f; | |
| color: #e1e1e1; | |
| min-height: 100vh; | |
| } | |
| .container { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 40px 20px; | |
| } | |
| h1 { | |
| text-align: center; | |
| font-size: 28px; | |
| margin-bottom: 8px; | |
| color: #fff; | |
| } | |
| .subtitle { | |
| text-align: center; | |
| color: #888; | |
| margin-bottom: 40px; | |
| font-size: 14px; | |
| } | |
| .card { | |
| background: #1a1a1a; | |
| border: 1px solid #333; | |
| border-radius: 12px; | |
| padding: 24px; | |
| margin-bottom: 20px; | |
| } | |
| .form-group { margin-bottom: 16px; } | |
| label { | |
| display: block; | |
| font-size: 13px; | |
| color: #aaa; | |
| margin-bottom: 6px; | |
| font-weight: 500; | |
| } | |
| input[type="text"], select { | |
| width: 100%; | |
| padding: 10px 14px; | |
| background: #0f0f0f; | |
| border: 1px solid #333; | |
| border-radius: 8px; | |
| color: #fff; | |
| font-size: 14px; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| } | |
| input[type="text"]:focus, select:focus { border-color: #ff4444; } | |
| .row { display: flex; gap: 12px; } | |
| .row .form-group { flex: 1; } | |
| .checkbox-group { | |
| display: flex; | |
| gap: 16px; | |
| align-items: center; | |
| } | |
| .checkbox-group label { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| cursor: pointer; | |
| color: #e1e1e1; | |
| font-size: 14px; | |
| } | |
| input[type="checkbox"] { | |
| accent-color: #ff4444; | |
| width: 16px; | |
| height: 16px; | |
| } | |
| .btn { | |
| width: 100%; | |
| padding: 12px; | |
| background: #ff4444; | |
| color: #fff; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| .btn:hover { background: #e03030; } | |
| .btn:disabled { | |
| background: #333; | |
| color: #666; | |
| cursor: not-allowed; | |
| } | |
| .mode-note { | |
| font-size: 12px; | |
| color: #666; | |
| margin-top: 4px; | |
| } | |
| /* ========= ์งํ ์ํ ========= */ | |
| #progress-section { display: none; } | |
| .steps { display: flex; flex-direction: column; gap: 0; } | |
| .step { | |
| padding: 16px 0; | |
| border-bottom: 1px solid #222; | |
| color: #555; | |
| transition: color 0.3s; | |
| } | |
| .step:last-child { border-bottom: none; } | |
| .step.active { color: #fff; } | |
| .step.done { color: #4caf50; } | |
| .step.error { color: #ff4444; } | |
| .step-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .step-icon { | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 14px; | |
| flex-shrink: 0; | |
| background: #222; | |
| color: #555; | |
| transition: all 0.3s; | |
| } | |
| .step.active .step-icon { | |
| background: #ff4444; | |
| color: #fff; | |
| } | |
| .step.done .step-icon { | |
| background: #4caf50; | |
| color: #fff; | |
| } | |
| .step-title { font-size: 14px; font-weight: 500; } | |
| .step-percent { | |
| margin-left: auto; | |
| font-size: 13px; | |
| font-weight: 600; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| color: #555; | |
| min-width: 40px; | |
| text-align: right; | |
| } | |
| .step.active .step-percent { color: #ff4444; } | |
| .step.done .step-percent { color: #4caf50; } | |
| /* ํ๋ก๊ทธ๋ ์ค ๋ฐ */ | |
| .step-progress { | |
| height: 4px; | |
| background: #222; | |
| border-radius: 2px; | |
| margin: 10px 0 0 40px; | |
| overflow: hidden; | |
| } | |
| .step-progress-bar { | |
| height: 100%; | |
| background: #ff4444; | |
| border-radius: 2px; | |
| width: 0%; | |
| transition: width 0.4s ease; | |
| } | |
| .step.done .step-progress-bar { | |
| background: #4caf50; | |
| width: 100%; | |
| } | |
| .step-progress-bar.indeterminate { | |
| width: 30%; | |
| animation: indeterminate 1.5s ease-in-out infinite; | |
| } | |
| @keyframes indeterminate { | |
| 0% { margin-left: 0%; } | |
| 50% { margin-left: 70%; } | |
| 100% { margin-left: 0%; } | |
| } | |
| /* ์์ธ ์ ๋ณด */ | |
| .step-detail { | |
| font-size: 12px; | |
| color: #555; | |
| margin: 6px 0 0 40px; | |
| min-height: 16px; | |
| transition: color 0.3s; | |
| } | |
| .step.active .step-detail { color: #999; } | |
| .step.done .step-detail { color: #4caf5099; } | |
| /* ํ์ด๋จธ + ํ */ | |
| .progress-footer { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-top: 20px; | |
| padding-top: 16px; | |
| border-top: 1px solid #222; | |
| } | |
| .elapsed { | |
| font-size: 13px; | |
| color: #888; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| } | |
| .tip { | |
| font-size: 12px; | |
| color: #666; | |
| font-style: italic; | |
| max-width: 60%; | |
| text-align: right; | |
| animation: fadeInOut 6s ease-in-out infinite; | |
| } | |
| @keyframes fadeInOut { | |
| 0%, 100% { opacity: 0.4; } | |
| 50% { opacity: 1; } | |
| } | |
| /* ========= ๊ฒฐ๊ณผ ========= */ | |
| #result-section { display: none; } | |
| .result-banner { | |
| background: #1a2e1a; | |
| border: 1px solid #2d5a2d; | |
| border-radius: 8px; | |
| padding: 16px; | |
| margin-bottom: 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .result-banner-icon { font-size: 24px; flex-shrink: 0; } | |
| .result-banner-text { flex: 1; } | |
| .result-banner-title { | |
| font-size: 15px; | |
| font-weight: 600; | |
| color: #4caf50; | |
| margin-bottom: 4px; | |
| } | |
| .result-banner-info { font-size: 13px; color: #888; } | |
| .result-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| } | |
| .result-title { font-size: 16px; font-weight: 600; color: #fff; } | |
| .result-lang { | |
| font-size: 12px; | |
| background: #333; | |
| color: #aaa; | |
| padding: 4px 10px; | |
| border-radius: 12px; | |
| } | |
| .result-text { | |
| background: #0f0f0f; | |
| border-radius: 8px; | |
| padding: 16px; | |
| font-size: 14px; | |
| line-height: 1.7; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| color: #ccc; | |
| } | |
| .download-buttons { display: flex; gap: 10px; margin-top: 16px; } | |
| .btn-download { | |
| flex: 1; | |
| padding: 12px; | |
| background: #1a3a1a; | |
| border: 1px solid #2d5a2d; | |
| border-radius: 8px; | |
| color: #4caf50; | |
| font-size: 14px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| text-align: center; | |
| text-decoration: none; | |
| transition: background 0.2s; | |
| } | |
| .btn-download:hover { background: #2d5a2d; color: #fff; } | |
| .save-path { | |
| font-size: 12px; | |
| color: #666; | |
| margin-top: 12px; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| padding: 8px 12px; | |
| background: #111; | |
| border-radius: 6px; | |
| } | |
| .error-msg { | |
| color: #ff4444; | |
| padding: 12px; | |
| font-size: 14px; | |
| background: #2a1515; | |
| border-radius: 8px; | |
| border: 1px solid #4a2020; | |
| } | |
| .btn-new { | |
| width: 100%; | |
| padding: 10px; | |
| background: transparent; | |
| border: 1px solid #444; | |
| border-radius: 8px; | |
| color: #aaa; | |
| font-size: 14px; | |
| cursor: pointer; | |
| margin-top: 12px; | |
| transition: all 0.2s; | |
| } | |
| .btn-new:hover { border-color: #888; color: #fff; } | |
| /* ========= LLM ์ต์ (MD/๋ฒ์ญ ๊ณต์ฉ) ========= */ | |
| .llm-options { | |
| display: none; | |
| margin-top: 12px; | |
| padding: 16px; | |
| background: #111; | |
| border-radius: 8px; | |
| border: 1px solid #2a2a2a; | |
| } | |
| .llm-options.visible { display: block; } | |
| .llm-options .form-group { margin-bottom: 12px; } | |
| .llm-options .form-group:last-child { margin-bottom: 0; } | |
| .llm-select-desc { | |
| font-size: 12px; | |
| color: #888; | |
| margin-top: 4px; | |
| } | |
| /* ========= ์ค์ ๋ฒํผ ========= */ | |
| .llm-header { display: flex; gap: 8px; align-items: stretch; } | |
| .llm-header > div { flex: 1; } | |
| .btn-settings { | |
| padding: 8px 14px; | |
| background: #222; | |
| border: 1px solid #333; | |
| border-radius: 8px; | |
| color: #aaa; | |
| font-size: 16px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| flex-shrink: 0; | |
| position: relative; | |
| } | |
| .btn-settings:hover { background: #333; color: #fff; border-color: #555; } | |
| .settings-dot { | |
| display: none; | |
| position: absolute; | |
| top: 4px; right: 4px; | |
| width: 8px; height: 8px; | |
| border-radius: 50%; | |
| background: #4caf50; | |
| } | |
| .settings-dot.active { display: block; } | |
| .workflow-summary { | |
| font-size: 12px; | |
| color: #4caf50; | |
| margin-top: 6px; | |
| line-height: 1.5; | |
| } | |
| /* ========= ์ํฌํ๋ก์ฐ ์ค์ ๋ชจ๋ฌ ========= */ | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| background: rgba(0,0,0,0.7); | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| backdrop-filter: blur(4px); | |
| } | |
| .modal { | |
| background: #1a1a1a; | |
| border: 1px solid #333; | |
| border-radius: 16px; | |
| padding: 28px; | |
| width: 540px; | |
| max-width: 90vw; | |
| max-height: 85vh; | |
| overflow-y: auto; | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .modal-header h3 { color: #fff; font-size: 18px; margin: 0; } | |
| .modal-close { | |
| background: none; | |
| border: none; | |
| color: #666; | |
| font-size: 24px; | |
| cursor: pointer; | |
| padding: 4px 8px; | |
| border-radius: 6px; | |
| transition: all 0.2s; | |
| line-height: 1; | |
| } | |
| .modal-close:hover { background: #333; color: #fff; } | |
| .modal-desc { | |
| color: #888; | |
| font-size: 13px; | |
| margin-bottom: 20px; | |
| line-height: 1.5; | |
| } | |
| .modal-step-row { | |
| border: 1px solid #2a2a2a; | |
| border-radius: 10px; | |
| padding: 16px; | |
| margin-bottom: 12px; | |
| background: #111; | |
| } | |
| .modal-step-label { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .modal-step-name { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #e1e1e1; | |
| } | |
| .modal-step-badge { | |
| font-size: 11px; | |
| padding: 2px 8px; | |
| border-radius: 10px; | |
| background: #222; | |
| color: #666; | |
| } | |
| .modal-step-badge.custom { | |
| background: #1a3a1a; | |
| color: #4caf50; | |
| } | |
| .modal-step-fields { display: flex; flex-direction: column; gap: 8px; } | |
| .modal-step-fields select, | |
| .modal-step-fields input[type="text"] { | |
| width: 100%; | |
| padding: 8px 12px; | |
| background: #0f0f0f; | |
| border: 1px solid #333; | |
| border-radius: 6px; | |
| color: #fff; | |
| font-size: 13px; | |
| outline: none; | |
| } | |
| .modal-step-fields select:focus, | |
| .modal-step-fields input[type="text"]:focus { border-color: #ff4444; } | |
| .modal-step-sub { | |
| margin-top: 10px; | |
| padding-top: 10px; | |
| border-top: 1px solid #222; | |
| } | |
| .modal-step-sub label { | |
| font-size: 12px; | |
| color: #888; | |
| margin-bottom: 4px; | |
| } | |
| .modal-step-sub select, | |
| .modal-step-sub input[type="text"] { | |
| width: 100%; | |
| padding: 6px 10px; | |
| background: #0f0f0f; | |
| border: 1px solid #333; | |
| border-radius: 6px; | |
| color: #fff; | |
| font-size: 12px; | |
| outline: none; | |
| margin-bottom: 6px; | |
| } | |
| .modal-save-btn { | |
| margin-top: 8px; | |
| padding: 10px; | |
| font-size: 14px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>YouTube Script Extractor</h1> | |
| <p class="subtitle">YouTube ์์์ ์์ฑ์ ํ ์คํธ๋ก ๋ณํํฉ๋๋ค</p> | |
| <div class="card" id="form-section"> | |
| <div class="form-group"> | |
| <label>YouTube URL</label> | |
| <input type="text" id="url" placeholder="https://www.youtube.com/watch?v=..." onkeydown="if(event.key==='Enter') startTranscription()"> | |
| </div> | |
| <div class="row"> | |
| <div class="form-group"> | |
| <label>๋ณํ ๋ชจ๋</label> | |
| <select id="mode" onchange="toggleApiKey()"> | |
| <option value="api">Whisper API (๋น ๋ฅด๊ณ ์ ํ)</option> | |
| <option value="local">๋ก์ปฌ Whisper (๋ฌด๋ฃ)</option> | |
| </select> | |
| </div> | |
| <div class="form-group" id="model-group" style="display:none;"> | |
| <label>๋ชจ๋ธ ํฌ๊ธฐ</label> | |
| <select id="model_size"> | |
| <option value="tiny">tiny (39MB, ๊ฐ์ฅ ๋น ๋ฆ)</option> | |
| <option value="base" selected>base (74MB, ๊ท ํ)</option> | |
| <option value="small">small (244MB, ์ข์)</option> | |
| <option value="medium">medium (769MB, ๋์)</option> | |
| <option value="large">large (1.5GB, ์ต๊ณ )</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-group" id="api-key-group"> | |
| <label>OpenAI API Key</label> | |
| <input type="text" id="api_key" placeholder="sk-..."> | |
| <p class="mode-note">์๋ฒ์ OPENAI_API_KEY ํ๊ฒฝ๋ณ์๊ฐ ์ค์ ๋์ด ์์ผ๋ฉด ๋น์๋ฌ๋ ๋ฉ๋๋ค.</p> | |
| </div> | |
| <div class="form-group"> | |
| <label>์ถ๋ ฅ ํ์</label> | |
| <div class="checkbox-group"> | |
| <label><input type="checkbox" id="fmt_txt" checked> TXT (ํ ์คํธ)</label> | |
| <label><input type="checkbox" id="fmt_srt" checked> SRT (์๋ง)</label> | |
| <label><input type="checkbox" id="fmt_md" onchange="toggleLlmOptions()"> MD (AI ์ ๋ฆฌ)</label> | |
| <label><input type="checkbox" id="fmt_translate" onchange="toggleLlmOptions()"> ๋ฒ์ญ</label> | |
| <label><input type="checkbox" id="fmt_keyframes" onchange="toggleLlmOptions()"> ํคํ๋ ์ ๋ถ์</label> | |
| </div> | |
| </div> | |
| <!-- LLM ์ต์ (MD/๋ฒ์ญ ๊ณต์ฉ) --> | |
| <div class="llm-options" id="llm-options"> | |
| <div class="form-group"> | |
| <label>๊ธฐ๋ณธ LLM <span style="color:#666; font-weight:400;">(๋ชจ๋ AI ๋จ๊ณ์ ์ ์ฉ)</span></label> | |
| <div class="llm-header"> | |
| <div> | |
| <select id="llm-select" onchange="onLLMChange()"> | |
| <option value="">๋ก๋ฉ ์ค...</option> | |
| </select> | |
| </div> | |
| <button type="button" class="btn-settings" id="btn-settings" onclick="openSettingsModal()" title="์ํฌํ๋ก์ฐ ์ค์ ">โ<span class="settings-dot" id="settings-dot"></span></button> | |
| </div> | |
| <p class="llm-select-desc" id="llm-select-desc"></p> | |
| <p class="workflow-summary" id="workflow-summary" style="display:none;"></p> | |
| </div> | |
| <div class="form-group" id="md-api-key-group" style="display:none;"> | |
| <label id="md-api-key-label">LLM API Key</label> | |
| <input type="text" id="md_api_key" placeholder="API ํค ์ ๋ ฅ..."> | |
| <p class="mode-note" id="md-api-key-note"></p> | |
| </div> | |
| <div class="form-group" id="md-ollama-group" style="display:none;"> | |
| <label>Ollama ๋ชจ๋ธ</label> | |
| <input type="text" id="md_ollama_model" value="llama3.2" placeholder="llama3.2"> | |
| <p class="mode-note">ollama pull <๋ชจ๋ธ๋ช > ์ผ๋ก ๋จผ์ ๋ค์ด๋ก๋ ํ์</p> | |
| </div> | |
| <div class="form-group" id="translate-lang-group" style="display:none;"> | |
| <label>๋ฒ์ญ ์ธ์ด</label> | |
| <select id="translate-lang"> | |
| <option value="">๋ก๋ฉ ์ค...</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>์ ์ฅ ๊ฒฝ๋ก</label> | |
| <input type="text" id="output_dir" placeholder="./output" value="./output"> | |
| <p class="mode-note">๋น์๋๋ฉด ๊ธฐ๋ณธ ๊ฒฝ๋ก(./output)์ ์ ์ฅ๋ฉ๋๋ค. ์ ๋ ๊ฒฝ๋ก๋ ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค (์: /Users/์ด๋ฆ/Desktop)</p> | |
| </div> | |
| <button class="btn" id="submit-btn" onclick="startTranscription()">์คํฌ๋ฆฝํธ ์ถ์ถ ์์</button> | |
| </div> | |
| <!-- ์งํ ์ํ --> | |
| <div class="card" id="progress-section"> | |
| <div class="steps"> | |
| <div class="step" id="step-download" data-step="download"> | |
| <div class="step-header"> | |
| <div class="step-icon">1</div> | |
| <div class="step-title">์ค๋์ค ๋ค์ด๋ก๋</div> | |
| <div class="step-percent" id="pct-download"></div> | |
| </div> | |
| <div class="step-progress"><div class="step-progress-bar" id="bar-download"></div></div> | |
| <div class="step-detail" id="detail-download">YouTube์์ ์ค๋์ค๋ฅผ ์ถ์ถํฉ๋๋ค</div> | |
| </div> | |
| <div class="step" id="step-keyframe_extract" data-step="keyframe_extract" style="display:none;"> | |
| <div class="step-header"> | |
| <div class="step-icon">2</div> | |
| <div class="step-title">ํคํ๋ ์ ์ถ์ถ</div> | |
| <div class="step-percent" id="pct-keyframe_extract"></div> | |
| </div> | |
| <div class="step-progress"><div class="step-progress-bar" id="bar-keyframe_extract"></div></div> | |
| <div class="step-detail" id="detail-keyframe_extract">์์์์ ์ฃผ์ ์ฅ๋ฉด์ ์ถ์ถํฉ๋๋ค</div> | |
| </div> | |
| <div class="step" id="step-keyframe_analysis" data-step="keyframe_analysis" style="display:none;"> | |
| <div class="step-header"> | |
| <div class="step-icon">3</div> | |
| <div class="step-title">ํคํ๋ ์ ๋ถ์</div> | |
| <div class="step-percent" id="pct-keyframe_analysis"></div> | |
| </div> | |
| <div class="step-progress"><div class="step-progress-bar" id="bar-keyframe_analysis"></div></div> | |
| <div class="step-detail" id="detail-keyframe_analysis">Vision AI๋ก ํ๋ฉด์ ๋ถ์ํฉ๋๋ค</div> | |
| </div> | |
| <div class="step" id="step-transcribe" data-step="transcribe"> | |
| <div class="step-header"> | |
| <div class="step-icon">2</div> | |
| <div class="step-title">์์ฑ ์ธ์</div> | |
| <div class="step-percent" id="pct-transcribe"></div> | |
| </div> | |
| <div class="step-progress"><div class="step-progress-bar" id="bar-transcribe"></div></div> | |
| <div class="step-detail" id="detail-transcribe">Whisper๋ก ์์ฑ์ ํ ์คํธ๋ก ๋ณํํฉ๋๋ค</div> | |
| </div> | |
| <div class="step" id="step-format" data-step="format" style="display:none;"> | |
| <div class="step-header"> | |
| <div class="step-icon">3</div> | |
| <div class="step-title">AI ๋งํฌ๋ค์ด ์ ๋ฆฌ</div> | |
| <div class="step-percent" id="pct-format"></div> | |
| </div> | |
| <div class="step-progress"><div class="step-progress-bar" id="bar-format"></div></div> | |
| <div class="step-detail" id="detail-format">LLM์ผ๋ก ํ ์คํธ๋ฅผ ๊ตฌ์กฐํํฉ๋๋ค</div> | |
| </div> | |
| <div class="step" id="step-translate" data-step="translate" style="display:none;"> | |
| <div class="step-header"> | |
| <div class="step-icon">4</div> | |
| <div class="step-title">๋ฒ์ญ</div> | |
| <div class="step-percent" id="pct-translate"></div> | |
| </div> | |
| <div class="step-progress"><div class="step-progress-bar" id="bar-translate"></div></div> | |
| <div class="step-detail" id="detail-translate">์ ํํ ์ธ์ด๋ก ๋ฒ์ญํฉ๋๋ค</div> | |
| </div> | |
| <div class="step" id="step-save" data-step="save"> | |
| <div class="step-header"> | |
| <div class="step-icon" id="icon-save">3</div> | |
| <div class="step-title">ํ์ผ ์ ์ฅ</div> | |
| <div class="step-percent" id="pct-save"></div> | |
| </div> | |
| <div class="step-progress"><div class="step-progress-bar" id="bar-save"></div></div> | |
| <div class="step-detail" id="detail-save">๊ฒฐ๊ณผ ํ์ผ์ ์์ฑํฉ๋๋ค</div> | |
| </div> | |
| </div> | |
| <div class="progress-footer"> | |
| <div class="elapsed" id="elapsed"></div> | |
| <div class="tip" id="tip"></div> | |
| </div> | |
| </div> | |
| <!-- ๊ฒฐ๊ณผ --> | |
| <div class="card" id="result-section"> | |
| <div class="result-banner"> | |
| <div class="result-banner-icon">✓</div> | |
| <div class="result-banner-text"> | |
| <div class="result-banner-title">์คํฌ๋ฆฝํธ ์ถ์ถ ์๋ฃ</div> | |
| <div class="result-banner-info" id="result-info"></div> | |
| </div> | |
| </div> | |
| <div class="result-header"> | |
| <span class="result-title" id="result-title"></span> | |
| <span class="result-lang" id="result-lang"></span> | |
| </div> | |
| <div class="result-text" id="result-text"></div> | |
| <div class="download-buttons" id="download-buttons"></div> | |
| <div class="save-path" id="save-path"></div> | |
| <button class="btn-new" onclick="resetForm()">์ ์์ ์ถ์ถํ๊ธฐ</button> | |
| </div> | |
| <!-- ์๋ฌ --> | |
| <div class="card" id="error-section" style="display:none;"> | |
| <div class="error-msg" id="error-msg"></div> | |
| <button class="btn-new" onclick="resetForm()">๋ค์ ์๋ํ๊ธฐ</button> | |
| </div> | |
| </div> | |
| <!-- ์ํฌํ๋ก์ฐ ์ค์ ๋ชจ๋ฌ --> | |
| <div class="modal-overlay" id="settings-modal" style="display:none;" onclick="if(event.target===this)closeSettingsModal()"> | |
| <div class="modal"> | |
| <div class="modal-header"> | |
| <h3>์ํฌํ๋ก์ฐ ์ค์ </h3> | |
| <button class="modal-close" onclick="closeSettingsModal()">×</button> | |
| </div> | |
| <p class="modal-desc">๊ฐ ๋จ๊ณ๋ณ๋ก ๋ค๋ฅธ LLM์ ์ง์ ํ ์ ์์ต๋๋ค.<br>๋น์๋๋ฉด ๊ธฐ๋ณธ LLM(<strong id="modal-global-llm-name" style="color:#e1e1e1;">โ</strong>)์ ์ฌ์ฉํฉ๋๋ค.</p> | |
| <!-- MD ์ ๋ฆฌ --> | |
| <div class="modal-step-row"> | |
| <div class="modal-step-label"> | |
| <span class="modal-step-name">MD ์ ๋ฆฌ</span> | |
| <span class="modal-step-badge" id="badge-format">๊ธฐ๋ณธ LLM</span> | |
| </div> | |
| <div class="modal-step-fields"> | |
| <select id="modal-format-llm" onchange="onModalLLMChange('format')"> | |
| <option value="">๊ธฐ๋ณธ LLM ์ฌ์ฉ</option> | |
| </select> | |
| <input type="text" id="modal-format-api-key" placeholder="API Key (๋น์๋๋ฉด ํ๊ฒฝ๋ณ์ ์ฌ์ฉ)" style="display:none;"> | |
| </div> | |
| </div> | |
| <!-- ๋ฒ์ญ --> | |
| <div class="modal-step-row"> | |
| <div class="modal-step-label"> | |
| <span class="modal-step-name">๋ฒ์ญ</span> | |
| <span class="modal-step-badge" id="badge-translate">๊ธฐ๋ณธ LLM</span> | |
| </div> | |
| <div class="modal-step-fields"> | |
| <select id="modal-translate-llm" onchange="onModalLLMChange('translate')"> | |
| <option value="">๊ธฐ๋ณธ LLM ์ฌ์ฉ</option> | |
| </select> | |
| <input type="text" id="modal-translate-api-key" placeholder="API Key (๋น์๋๋ฉด ํ๊ฒฝ๋ณ์ ์ฌ์ฉ)" style="display:none;"> | |
| </div> | |
| </div> | |
| <!-- ํคํ๋ ์ ๋ถ์ --> | |
| <div class="modal-step-row"> | |
| <div class="modal-step-label"> | |
| <span class="modal-step-name">ํคํ๋ ์ ๋ถ์</span> | |
| <span class="modal-step-badge" id="badge-keyframe">๊ธฐ๋ณธ LLM</span> | |
| </div> | |
| <div class="modal-step-fields"> | |
| <select id="modal-keyframe-llm" onchange="onModalLLMChange('keyframe')"> | |
| <option value="">๊ธฐ๋ณธ LLM ์ฌ์ฉ (Vision ์ง์ ํ์)</option> | |
| </select> | |
| <input type="text" id="modal-keyframe-api-key" placeholder="API Key (๋น์๋๋ฉด ํ๊ฒฝ๋ณ์ ์ฌ์ฉ)" style="display:none;"> | |
| </div> | |
| <div class="modal-step-sub"> | |
| <label>์ถ์ถ ๋ฐฉ์</label> | |
| <select id="keyframe-method" onchange="onKeyframeMethodChange()"> | |
| <option value="scene">์ฅ๋ฉด ์ ํ ๊ฐ์ง (์ถ์ฒ)</option> | |
| <option value="interval">๊ณ ์ ๊ฐ๊ฒฉ</option> | |
| </select> | |
| <div id="keyframe-interval-group" style="display:none;"> | |
| <label>๊ฐ๊ฒฉ (์ด)</label> | |
| <input type="text" id="keyframe-interval" value="30" placeholder="30"> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="btn modal-save-btn" onclick="saveModalSettings()">์ค์ ์ ์ฅ</button> | |
| </div> | |
| </div> | |
| <script> | |
| let timerInterval = null; | |
| let startTime = null; | |
| let tipInterval = null; | |
| let selectedLLM = 'gemini-flash-lite'; | |
| let llmModels = []; | |
| let languages = []; | |
| let llmLoaded = false; | |
| let langLoaded = false; | |
| let visionModels = []; | |
| let visionLoaded = false; | |
| // ๋จ๊ณ๋ณ LLM ์ค์ (๋ชจ๋ฌ์์ ๊ด๋ฆฌ) | |
| let perStepConfig = { | |
| format: { llm: '', apiKey: '' }, | |
| translate: { llm: '', apiKey: '' }, | |
| keyframe: { llm: '', apiKey: '', method: 'scene', interval: 30 }, | |
| }; | |
| const TIPS = [ | |
| "Whisper๋ 90๊ฐ ์ด์์ ์ธ์ด๋ฅผ ์๋์ผ๋ก ๊ฐ์งํฉ๋๋ค", | |
| "SRT ํ์ผ์ ๋๋ถ๋ถ์ ๋์์ ํ๋ ์ด์ด์์ ์๋ง์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค", | |
| "์์์ด ๊ธธ์๋ก ๋ณํ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฝ๋๋ค", | |
| "API ๋ชจ๋๋ ๋ก์ปฌ ๋ชจ๋๋ณด๋ค ๋ณดํต 5~10๋ฐฐ ๋น ๋ฆ ๋๋ค", | |
| "TXT ํ์ผ์ ํ ์คํธ ํธ์ง๊ธฐ์์ ๋ฐ๋ก ์ด ์ ์์ต๋๋ค", | |
| "์ถ์ถ๋ ์คํฌ๋ฆฝํธ๋ ./output ํด๋์ ์ ์ฅ๋ฉ๋๋ค", | |
| "๋ก์ปฌ ๋ชจ๋๋ ์ธํฐ๋ท ์์ด๋ ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค", | |
| "Whisper API ๋น์ฉ์ 1๋ถ๋น ์ฝ $0.006์ ๋๋ค", | |
| "MD ์ถ๋ ฅ์ LLM์ด ํ ์คํธ๋ฅผ ๊น๋ํ๊ฒ ๊ตฌ์กฐํํฉ๋๋ค", | |
| "Ollama๋ ๋ก์ปฌ์์ ๋ฌด๋ฃ๋ก LLM์ ์คํํ ์ ์์ต๋๋ค", | |
| "๋ฒ์ญ ๊ธฐ๋ฅ์ผ๋ก ์๋ณธ๊ณผ ๋ฒ์ญ๋ณธ์ ๋์์ ์์ฑํ ์ ์์ต๋๋ค", | |
| "ํคํ๋ ์ ๋ถ์์ผ๋ก ๋ฐํ ์ฌ๋ผ์ด๋๋ ํ๋ฉด ๋ด์ฉ์ MD์ ํฌํจํ ์ ์์ต๋๋ค", | |
| "์ํฌํ๋ก์ฐ ์ค์ ์์ ๋จ๊ณ๋ณ๋ก ๋ค๋ฅธ LLM์ ์ง์ ํ ์ ์์ต๋๋ค", | |
| "ํคํ๋ ์ ๋ถ์์ Gemini Flash๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฌด๋ฃ์ ๋๋ค", | |
| ]; | |
| // ๋ค์ด๋ก๋ ๋ฒํผ ๋ผ๋ฒจ ๋งคํ | |
| const FMT_LABELS = { | |
| 'txt': 'TXT', | |
| 'srt': 'SRT', | |
| 'md': 'MD', | |
| }; | |
| let STEP_ORDER = ['download', 'transcribe', 'save']; | |
| function getStepOrder() { | |
| const useMd = document.getElementById('fmt_md').checked; | |
| const useTranslate = document.getElementById('fmt_translate').checked; | |
| const useKeyframes = document.getElementById('fmt_keyframes').checked; | |
| const order = ['download']; | |
| if (useKeyframes) { | |
| order.push('keyframe_extract'); | |
| order.push('keyframe_analysis'); | |
| } | |
| order.push('transcribe'); | |
| if (useMd) order.push('format'); | |
| if (useTranslate) order.push('translate'); | |
| order.push('save'); | |
| return order; | |
| } | |
| function needsLLM() { | |
| return document.getElementById('fmt_md').checked || | |
| document.getElementById('fmt_translate').checked || | |
| document.getElementById('fmt_keyframes').checked; | |
| } | |
| function toggleApiKey() { | |
| const mode = document.getElementById('mode').value; | |
| document.getElementById('api-key-group').style.display = mode === 'api' ? '' : 'none'; | |
| document.getElementById('model-group').style.display = mode === 'local' ? '' : 'none'; | |
| } | |
| function toggleLlmOptions() { | |
| const opts = document.getElementById('llm-options'); | |
| const translateChecked = document.getElementById('fmt_translate').checked; | |
| const keyframesChecked = document.getElementById('fmt_keyframes').checked; | |
| if (needsLLM()) { | |
| opts.classList.add('visible'); | |
| if (!llmLoaded) loadLLMModels(); | |
| if (translateChecked && !langLoaded) loadLanguages(); | |
| if (keyframesChecked && !visionLoaded) loadVisionModels(); | |
| } else { | |
| opts.classList.remove('visible'); | |
| } | |
| // ๋ฒ์ญ ์ธ์ด ์ ํ ํ์/์จ๊ธฐ๊ธฐ | |
| document.getElementById('translate-lang-group').style.display = | |
| translateChecked ? '' : 'none'; | |
| } | |
| async function loadLLMModels() { | |
| try { | |
| const res = await fetch('/api/llm-models?sort=price'); | |
| const data = await res.json(); | |
| llmModels = data.models; | |
| llmLoaded = true; | |
| renderLLMDropdown(); | |
| } catch (e) { | |
| console.error('LLM ๋ชจ๋ธ ๋ก๋ฉ ์คํจ:', e); | |
| } | |
| } | |
| async function loadLanguages() { | |
| try { | |
| const res = await fetch('/api/languages'); | |
| const data = await res.json(); | |
| languages = data.languages; | |
| langLoaded = true; | |
| renderLanguageDropdown(); | |
| } catch (e) { | |
| console.error('์ธ์ด ๋ชฉ๋ก ๋ก๋ฉ ์คํจ:', e); | |
| } | |
| } | |
| function renderLLMDropdown() { | |
| const select = document.getElementById('llm-select'); | |
| select.innerHTML = ''; | |
| llmModels.forEach(model => { | |
| const opt = document.createElement('option'); | |
| opt.value = model.id; | |
| opt.textContent = model.version | |
| ? `${model.name} [${model.version}]` | |
| : model.name; | |
| if (model.id === selectedLLM) opt.selected = true; | |
| select.appendChild(opt); | |
| }); | |
| onLLMChange(); | |
| } | |
| function renderLanguageDropdown() { | |
| const select = document.getElementById('translate-lang'); | |
| select.innerHTML = ''; | |
| languages.forEach(lang => { | |
| const opt = document.createElement('option'); | |
| opt.value = lang.code; | |
| opt.textContent = `${lang.name} (${lang.code})`; | |
| select.appendChild(opt); | |
| }); | |
| } | |
| function onLLMChange() { | |
| const select = document.getElementById('llm-select'); | |
| selectedLLM = select.value; | |
| const model = llmModels.find(m => m.id === selectedLLM); | |
| if (!model) return; | |
| // ์ค๋ช ํ ์คํธ ์ ๋ฐ์ดํธ | |
| document.getElementById('llm-select-desc').textContent = model.description; | |
| // API ํค / Ollama ๋ชจ๋ธ UI ์ ๋ฐ์ดํธ | |
| const keyGroup = document.getElementById('md-api-key-group'); | |
| const ollamaGroup = document.getElementById('md-ollama-group'); | |
| if (selectedLLM === 'ollama') { | |
| keyGroup.style.display = 'none'; | |
| ollamaGroup.style.display = ''; | |
| } else if (model.needs_key) { | |
| keyGroup.style.display = ''; | |
| ollamaGroup.style.display = 'none'; | |
| const envKey = model.env_key || ''; | |
| document.getElementById('md-api-key-label').textContent = model.name + ' API Key'; | |
| document.getElementById('md-api-key-note').textContent = | |
| envKey ? `์๋ฒ์ ${envKey} ํ๊ฒฝ๋ณ์๊ฐ ์ค์ ๋์ด ์์ผ๋ฉด ๋น์๋ฌ๋ ๋ฉ๋๋ค.` : ''; | |
| } else { | |
| keyGroup.style.display = 'none'; | |
| ollamaGroup.style.display = 'none'; | |
| } | |
| // ์ํฌํ๋ก์ฐ ์์ฝ ์ ๋ฐ์ดํธ (๊ธฐ๋ณธ LLM ๋ณ๊ฒฝ ์ "๊ธฐ๋ณธ LLM ์ฌ์ฉ" ๋ฑ์ง๋ ๊ฐฑ์ ) | |
| updateWorkflowSummary(); | |
| } | |
| function startTimer() { | |
| startTime = Date.now(); | |
| timerInterval = setInterval(() => { | |
| const sec = Math.floor((Date.now() - startTime) / 1000); | |
| const min = Math.floor(sec / 60); | |
| const s = sec % 60; | |
| document.getElementById('elapsed').textContent = | |
| min > 0 ? `${min}๋ถ ${s}์ด ๊ฒฝ๊ณผ` : `${s}์ด ๊ฒฝ๊ณผ`; | |
| }, 1000); | |
| } | |
| function stopTimer() { | |
| if (timerInterval) clearInterval(timerInterval); | |
| if (tipInterval) clearInterval(tipInterval); | |
| } | |
| function startTips() { | |
| let tipIdx = Math.floor(Math.random() * TIPS.length); | |
| const tipEl = document.getElementById('tip'); | |
| tipEl.textContent = TIPS[tipIdx]; | |
| tipInterval = setInterval(() => { | |
| tipIdx = (tipIdx + 1) % TIPS.length; | |
| tipEl.style.opacity = '0'; | |
| setTimeout(() => { | |
| tipEl.textContent = TIPS[tipIdx]; | |
| tipEl.style.opacity = ''; | |
| }, 300); | |
| }, 6000); | |
| } | |
| function updateStep(step, percent, detail) { | |
| const stepEl = document.getElementById('step-' + step); | |
| const barEl = document.getElementById('bar-' + step); | |
| const pctEl = document.getElementById('pct-' + step); | |
| const detailEl = document.getElementById('detail-' + step); | |
| if (!stepEl) return; | |
| // ํ์ฌ ์คํ ์ active๋ก | |
| const stepIdx = STEP_ORDER.indexOf(step); | |
| STEP_ORDER.forEach((s, i) => { | |
| const el = document.getElementById('step-' + s); | |
| if (!el) return; | |
| const icon = el.querySelector('.step-icon'); | |
| if (i < stepIdx) { | |
| el.className = 'step done'; | |
| icon.innerHTML = '✓'; | |
| } else if (i === stepIdx) { | |
| el.className = 'step active'; | |
| icon.textContent = String(i + 1); | |
| } | |
| }); | |
| // ํ๋ก๊ทธ๋ ์ค ๋ฐ | |
| if (percent < 0) { | |
| barEl.className = 'step-progress-bar indeterminate'; | |
| barEl.style.width = ''; | |
| pctEl.textContent = ''; | |
| } else { | |
| barEl.className = 'step-progress-bar'; | |
| barEl.style.width = percent + '%'; | |
| pctEl.textContent = percent + '%'; | |
| } | |
| if (detail) { | |
| detailEl.textContent = detail; | |
| } | |
| if (percent >= 100) { | |
| stepEl.className = 'step done'; | |
| stepEl.querySelector('.step-icon').innerHTML = '✓'; | |
| pctEl.textContent = '100%'; | |
| } | |
| } | |
| function setAllStepsDone() { | |
| STEP_ORDER.forEach(step => { | |
| const el = document.getElementById('step-' + step); | |
| if (!el) return; | |
| el.className = 'step done'; | |
| el.querySelector('.step-icon').innerHTML = '✓'; | |
| document.getElementById('bar-' + step).style.width = '100%'; | |
| document.getElementById('bar-' + step).className = 'step-progress-bar'; | |
| document.getElementById('pct-' + step).textContent = '100%'; | |
| }); | |
| } | |
| function resetForm() { | |
| document.getElementById('form-section').style.display = 'block'; | |
| document.getElementById('progress-section').style.display = 'none'; | |
| document.getElementById('result-section').style.display = 'none'; | |
| document.getElementById('error-section').style.display = 'none'; | |
| document.getElementById('submit-btn').disabled = false; | |
| document.getElementById('submit-btn').textContent = '์คํฌ๋ฆฝํธ ์ถ์ถ ์์'; | |
| stopTimer(); | |
| document.getElementById('elapsed').textContent = ''; | |
| document.getElementById('tip').textContent = ''; | |
| // ์ ํ์ ์คํ ์จ๊ธฐ๊ธฐ | |
| document.getElementById('step-format').style.display = 'none'; | |
| document.getElementById('step-translate').style.display = 'none'; | |
| document.getElementById('step-keyframe_extract').style.display = 'none'; | |
| document.getElementById('step-keyframe_analysis').style.display = 'none'; | |
| const allSteps = ['download', 'keyframe_extract', 'keyframe_analysis', 'transcribe', 'format', 'translate', 'save']; | |
| allSteps.forEach((step, i) => { | |
| const el = document.getElementById('step-' + step); | |
| if (!el) return; | |
| el.className = 'step'; | |
| el.querySelector('.step-icon').textContent = String(i + 1); | |
| document.getElementById('bar-' + step).style.width = '0%'; | |
| document.getElementById('bar-' + step).className = 'step-progress-bar'; | |
| document.getElementById('pct-' + step).textContent = ''; | |
| }); | |
| document.getElementById('detail-download').textContent = 'YouTube์์ ์ค๋์ค๋ฅผ ์ถ์ถํฉ๋๋ค'; | |
| document.getElementById('detail-keyframe_extract').textContent = '์์์์ ์ฃผ์ ์ฅ๋ฉด์ ์ถ์ถํฉ๋๋ค'; | |
| document.getElementById('detail-keyframe_analysis').textContent = 'Vision AI๋ก ํ๋ฉด์ ๋ถ์ํฉ๋๋ค'; | |
| document.getElementById('detail-transcribe').textContent = 'Whisper๋ก ์์ฑ์ ํ ์คํธ๋ก ๋ณํํฉ๋๋ค'; | |
| document.getElementById('detail-format').textContent = 'LLM์ผ๋ก ํ ์คํธ๋ฅผ ๊ตฌ์กฐํํฉ๋๋ค'; | |
| document.getElementById('detail-translate').textContent = '์ ํํ ์ธ์ด๋ก ๋ฒ์ญํฉ๋๋ค'; | |
| document.getElementById('detail-save').textContent = '๊ฒฐ๊ณผ ํ์ผ์ ์์ฑํฉ๋๋ค'; | |
| } | |
| function getDownloadLabel(fmt) { | |
| // txt_ko โ "TXT (ํ๊ตญ์ด)", md_zhcn โ "MD (ไธญๆ(็ฎไฝ))" | |
| if (fmt.includes('_')) { | |
| const parts = fmt.split('_'); | |
| const baseFmt = parts[0].toUpperCase(); | |
| const langCode = parts.slice(1).join('_'); | |
| // ์ธ์ด ์ฝ๋๋ฅผ ์๋ ํํ๋ก ๋ณต์ ์๋ (zhcn โ zh-CN ๋ฑ) | |
| const langEntry = languages.find(l => | |
| l.code.replace('-', '').toLowerCase() === langCode.toLowerCase() | |
| ); | |
| const langName = langEntry ? langEntry.name : langCode.toUpperCase(); | |
| return `${baseFmt} (${langName})`; | |
| } | |
| return (FMT_LABELS[fmt] || fmt.toUpperCase()); | |
| } | |
| async function startTranscription() { | |
| const url = document.getElementById('url').value.trim(); | |
| if (!url) { alert('YouTube URL์ ์ ๋ ฅํด์ฃผ์ธ์.'); return; } | |
| const mode = document.getElementById('mode').value; | |
| const apiKey = document.getElementById('api_key').value.trim(); | |
| const modelSize = document.getElementById('model_size').value; | |
| const outputDir = document.getElementById('output_dir').value.trim() || './output'; | |
| const formats = []; | |
| if (document.getElementById('fmt_txt').checked) formats.push('txt'); | |
| if (document.getElementById('fmt_srt').checked) formats.push('srt'); | |
| if (document.getElementById('fmt_md').checked) formats.push('md'); | |
| const useTranslate = document.getElementById('fmt_translate').checked; | |
| const translateLang = useTranslate ? document.getElementById('translate-lang').value : ''; | |
| const enableKeyframes = document.getElementById('fmt_keyframes').checked; | |
| if (formats.length === 0 && !useTranslate) { | |
| alert('์ถ๋ ฅ ํ์์ ํ๋ ์ด์ ์ ํํด์ฃผ์ธ์.'); | |
| return; | |
| } | |
| // LLM ํ๋ผ๋ฏธํฐ (MD ๋๋ ๋ฒ์ญ ์ฌ์ฉ ์) | |
| const useLlm = needsLLM(); | |
| const mdLlm = useLlm ? selectedLLM : ''; | |
| const mdApiKey = useLlm ? document.getElementById('md_api_key').value.trim() : ''; | |
| const mdOllamaModel = useLlm ? document.getElementById('md_ollama_model').value.trim() || 'llama3.2' : 'llama3.2'; | |
| // STEP_ORDER๋ฅผ ๋์ ์ผ๋ก ์ค์ | |
| STEP_ORDER = getStepOrder(); | |
| // ์ ํ์ ์คํ ํ์/์จ๊ธฐ๊ธฐ + ์์ด์ฝ ๋ฒํธ ์กฐ์ | |
| const formatStep = document.getElementById('step-format'); | |
| const translateStep = document.getElementById('step-translate'); | |
| const keyExtStep = document.getElementById('step-keyframe_extract'); | |
| const keyAnalStep = document.getElementById('step-keyframe_analysis'); | |
| const useMd = formats.includes('md'); | |
| formatStep.style.display = useMd ? '' : 'none'; | |
| translateStep.style.display = useTranslate ? '' : 'none'; | |
| keyExtStep.style.display = enableKeyframes ? '' : 'none'; | |
| keyAnalStep.style.display = enableKeyframes ? '' : 'none'; | |
| // ์คํ ๋ฒํธ ์ฌ๋ฐฐ์ | |
| STEP_ORDER.forEach((s, i) => { | |
| const el = document.getElementById('step-' + s); | |
| if (el) el.querySelector('.step-icon').textContent = String(i + 1); | |
| }); | |
| const btn = document.getElementById('submit-btn'); | |
| btn.disabled = true; | |
| btn.textContent = '์ฒ๋ฆฌ ์ค...'; | |
| document.getElementById('form-section').style.display = 'none'; | |
| document.getElementById('progress-section').style.display = 'block'; | |
| document.getElementById('result-section').style.display = 'none'; | |
| document.getElementById('error-section').style.display = 'none'; | |
| startTimer(); | |
| startTips(); | |
| try { | |
| const res = await fetch('/api/transcribe', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| url, mode, api_key: apiKey, model_size: modelSize, | |
| formats, output_dir: outputDir, | |
| md_llm: mdLlm, md_api_key: mdApiKey, md_ollama_model: mdOllamaModel, | |
| translate_lang: translateLang, | |
| // Per-step LLM overrides (๋ชจ๋ฌ ์ค์ ) | |
| format_llm: perStepConfig.format.llm, | |
| format_api_key: perStepConfig.format.apiKey, | |
| translate_llm: perStepConfig.translate.llm, | |
| translate_api_key: perStepConfig.translate.apiKey, | |
| keyframe_llm: perStepConfig.keyframe.llm, | |
| keyframe_api_key: perStepConfig.keyframe.apiKey, | |
| // ํคํ๋ ์ ์ต์ | |
| enable_keyframes: enableKeyframes, | |
| keyframe_method: perStepConfig.keyframe.method, | |
| keyframe_interval: parseInt(perStepConfig.keyframe.interval) || 30, | |
| }), | |
| }); | |
| const data = await res.json(); | |
| if (data.error) { | |
| showError(data.error); | |
| return; | |
| } | |
| const jobId = data.job_id; | |
| let pollSeen = 0; | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const res = await fetch(`/api/status/${jobId}`); | |
| const job = await res.json(); | |
| if (job.error && !job.progress) { | |
| clearInterval(pollInterval); | |
| stopTimer(); | |
| showError(job.error); | |
| return; | |
| } | |
| while (pollSeen < job.progress.length) { | |
| const msg = job.progress[pollSeen]; | |
| if (msg.step && msg.percent !== undefined) { | |
| updateStep(msg.step, msg.percent, msg.detail || ''); | |
| } | |
| pollSeen++; | |
| } | |
| if (job.status === 'completed') { | |
| clearInterval(pollInterval); | |
| stopTimer(); | |
| setAllStepsDone(); | |
| setTimeout(() => showResult(job.result), 500); | |
| } else if (job.status === 'error') { | |
| clearInterval(pollInterval); | |
| stopTimer(); | |
| showError(job.error); | |
| } | |
| } catch (err) { | |
| clearInterval(pollInterval); | |
| stopTimer(); | |
| showError('์๋ฒ ์ฐ๊ฒฐ์ด ๋์ด์ก์ต๋๋ค. ์๋ฒ๊ฐ ์คํ ์ค์ธ์ง ํ์ธํด์ฃผ์ธ์.'); | |
| } | |
| }, 1000); | |
| } catch (err) { | |
| stopTimer(); | |
| showError(`์์ฒญ ์คํจ: ${err.message}`); | |
| } | |
| } | |
| function showError(message) { | |
| document.getElementById('progress-section').style.display = 'none'; | |
| document.getElementById('error-section').style.display = 'block'; | |
| document.getElementById('error-msg').textContent = message; | |
| } | |
| function showResult(result) { | |
| document.getElementById('progress-section').style.display = 'none'; | |
| document.getElementById('result-section').style.display = 'block'; | |
| const elapsed = Math.floor((Date.now() - startTime) / 1000); | |
| const min = Math.floor(elapsed / 60); | |
| const sec = elapsed % 60; | |
| const timeStr = min > 0 ? `${min}๋ถ ${sec}์ด` : `${sec}์ด`; | |
| document.getElementById('result-info').textContent = | |
| `๊ฐ์ง ์ธ์ด: ${result.language} | ์์ ์๊ฐ: ${timeStr}`; | |
| document.getElementById('result-title').textContent = result.title; | |
| document.getElementById('result-lang').textContent = result.language; | |
| document.getElementById('result-text').textContent = result.text; | |
| const downloadBtns = document.getElementById('download-buttons'); | |
| downloadBtns.innerHTML = ''; | |
| const filenames = []; | |
| const dirParam = result.output_dir_raw ? `?dir=${encodeURIComponent(result.output_dir_raw)}` : ''; | |
| for (const [fmt, filename] of Object.entries(result.files)) { | |
| filenames.push(filename); | |
| const a = document.createElement('a'); | |
| a.href = `/api/download/${encodeURIComponent(filename)}${dirParam}`; | |
| a.className = 'btn-download'; | |
| a.download = filename; | |
| a.textContent = `${getDownloadLabel(fmt)} ๋ค์ด๋ก๋`; | |
| downloadBtns.appendChild(a); | |
| } | |
| const dir = result.output_dir || './output'; | |
| document.getElementById('save-path').textContent = | |
| '์ ์ฅ ์์น: ' + filenames.map(f => dir + '/' + f).join(', '); | |
| } | |
| // ========= Vision ๋ชจ๋ธ ๋ก๋ฉ ========= | |
| async function loadVisionModels() { | |
| try { | |
| const res = await fetch('/api/vision-models?sort=price'); | |
| const data = await res.json(); | |
| visionModels = data.models; | |
| visionLoaded = true; | |
| } catch (e) { | |
| console.error('Vision ๋ชจ๋ธ ๋ก๋ฉ ์คํจ:', e); | |
| } | |
| } | |
| // ========= ์ํฌํ๋ก์ฐ ์ค์ ๋ชจ๋ฌ ========= | |
| function openSettingsModal() { | |
| // ๋ชจ๋ฌ ์ด๊ธฐ ์ ๋ชจ๋ธ ๋ฐ์ดํฐ ๋ก๋ฉ ํ์ธ | |
| if (!llmLoaded) loadLLMModels().then(() => populateModalDropdowns()); | |
| if (!visionLoaded) loadVisionModels().then(() => populateModalDropdowns()); | |
| populateModalDropdowns(); | |
| restoreModalFromConfig(); | |
| // ๊ธฐ๋ณธ LLM ์ด๋ฆ ํ์ | |
| const globalModel = llmModels.find(m => m.id === selectedLLM); | |
| document.getElementById('modal-global-llm-name').textContent = | |
| globalModel ? globalModel.name : selectedLLM; | |
| document.getElementById('settings-modal').style.display = 'flex'; | |
| } | |
| function closeSettingsModal() { | |
| document.getElementById('settings-modal').style.display = 'none'; | |
| } | |
| function populateModalDropdowns() { | |
| const globalModel = llmModels.find(m => m.id === selectedLLM); | |
| const globalName = globalModel ? globalModel.name : '๊ธฐ๋ณธ LLM'; | |
| // MD ์ ๋ฆฌ / ๋ฒ์ญ ๋๋กญ๋ค์ด (์ ์ฒด LLM) | |
| ['format', 'translate'].forEach(step => { | |
| const select = document.getElementById(`modal-${step}-llm`); | |
| const currentVal = select.value; | |
| select.innerHTML = `<option value="">๊ธฐ๋ณธ LLM ์ฌ์ฉ (${globalName})</option>`; | |
| llmModels.forEach(model => { | |
| const opt = document.createElement('option'); | |
| opt.value = model.id; | |
| opt.textContent = model.version | |
| ? `${model.name} [${model.version}]` | |
| : model.name; | |
| select.appendChild(opt); | |
| }); | |
| select.value = currentVal || ''; | |
| }); | |
| // ํคํ๋ ์ ๋๋กญ๋ค์ด (Vision ์ง์ ๋ชจ๋ธ๋ง) | |
| const kfSelect = document.getElementById('modal-keyframe-llm'); | |
| const kfVal = kfSelect.value; | |
| const isGlobalVision = globalModel && globalModel.supports_vision !== false; | |
| kfSelect.innerHTML = isGlobalVision | |
| ? `<option value="">๊ธฐ๋ณธ LLM ์ฌ์ฉ (${globalName})</option>` | |
| : `<option value="">์ ํ ํ์ (๊ธฐ๋ณธ LLM์ด Vision ๋ฏธ์ง์)</option>`; | |
| visionModels.forEach(model => { | |
| const opt = document.createElement('option'); | |
| opt.value = model.id; | |
| opt.textContent = model.version | |
| ? `${model.name} [${model.version}]` | |
| : model.name; | |
| kfSelect.appendChild(opt); | |
| }); | |
| kfSelect.value = kfVal || ''; | |
| updateModalBadges(); | |
| } | |
| function restoreModalFromConfig() { | |
| document.getElementById('modal-format-llm').value = perStepConfig.format.llm; | |
| document.getElementById('modal-format-api-key').value = perStepConfig.format.apiKey; | |
| document.getElementById('modal-translate-llm').value = perStepConfig.translate.llm; | |
| document.getElementById('modal-translate-api-key').value = perStepConfig.translate.apiKey; | |
| document.getElementById('modal-keyframe-llm').value = perStepConfig.keyframe.llm; | |
| document.getElementById('modal-keyframe-api-key').value = perStepConfig.keyframe.apiKey; | |
| document.getElementById('keyframe-method').value = perStepConfig.keyframe.method; | |
| document.getElementById('keyframe-interval').value = perStepConfig.keyframe.interval; | |
| onKeyframeMethodChange(); | |
| ['format', 'translate', 'keyframe'].forEach(s => onModalLLMChange(s)); | |
| updateModalBadges(); | |
| } | |
| function onModalLLMChange(step) { | |
| const select = document.getElementById(`modal-${step}-llm`); | |
| const apiKeyInput = document.getElementById(`modal-${step}-api-key`); | |
| const val = select.value; | |
| if (val && val !== 'ollama') { | |
| // ์ ํํ ๋ชจ๋ธ์ needs_key ํ์ธ | |
| const allModels = step === 'keyframe' ? visionModels : llmModels; | |
| const model = allModels.find(m => m.id === val); | |
| if (model && model.needs_key) { | |
| apiKeyInput.style.display = ''; | |
| apiKeyInput.placeholder = model.env_key | |
| ? `API Key (${model.env_key} ํ๊ฒฝ๋ณ์๊ฐ ์์ผ๋ฉด ์๋ต ๊ฐ๋ฅ)` | |
| : 'API Key ์ ๋ ฅ...'; | |
| } else { | |
| apiKeyInput.style.display = 'none'; | |
| } | |
| } else { | |
| apiKeyInput.style.display = 'none'; | |
| } | |
| updateModalBadges(); | |
| } | |
| function updateModalBadges() { | |
| const globalModel = llmModels.find(m => m.id === selectedLLM); | |
| const globalName = globalModel ? globalModel.name : '๊ธฐ๋ณธ'; | |
| ['format', 'translate', 'keyframe'].forEach(step => { | |
| const select = document.getElementById(`modal-${step}-llm`); | |
| const badge = document.getElementById(`badge-${step}`); | |
| if (select.value) { | |
| const allModels = step === 'keyframe' ? visionModels : llmModels; | |
| const model = allModels.find(m => m.id === select.value); | |
| badge.textContent = model ? model.name : select.value; | |
| badge.className = 'modal-step-badge custom'; | |
| } else { | |
| badge.textContent = globalName; | |
| badge.className = 'modal-step-badge'; | |
| } | |
| }); | |
| } | |
| function onKeyframeMethodChange() { | |
| const method = document.getElementById('keyframe-method').value; | |
| document.getElementById('keyframe-interval-group').style.display = | |
| method === 'interval' ? '' : 'none'; | |
| } | |
| function saveModalSettings() { | |
| perStepConfig.format.llm = document.getElementById('modal-format-llm').value; | |
| perStepConfig.format.apiKey = document.getElementById('modal-format-api-key').value.trim(); | |
| perStepConfig.translate.llm = document.getElementById('modal-translate-llm').value; | |
| perStepConfig.translate.apiKey = document.getElementById('modal-translate-api-key').value.trim(); | |
| perStepConfig.keyframe.llm = document.getElementById('modal-keyframe-llm').value; | |
| perStepConfig.keyframe.apiKey = document.getElementById('modal-keyframe-api-key').value.trim(); | |
| perStepConfig.keyframe.method = document.getElementById('keyframe-method').value; | |
| perStepConfig.keyframe.interval = parseInt(document.getElementById('keyframe-interval').value) || 30; | |
| updateWorkflowSummary(); | |
| closeSettingsModal(); | |
| } | |
| function updateWorkflowSummary() { | |
| const dot = document.getElementById('settings-dot'); | |
| const summary = document.getElementById('workflow-summary'); | |
| // ์ค๋ฒ๋ผ์ด๋๊ฐ ์๋์ง ํ์ธ | |
| const overrides = []; | |
| const getModelName = (id, models) => { | |
| const m = models.find(x => x.id === id); | |
| return m ? m.name : id; | |
| }; | |
| if (perStepConfig.format.llm) { | |
| overrides.push(`MD์ ๋ฆฌ โ ${getModelName(perStepConfig.format.llm, llmModels)}`); | |
| } | |
| if (perStepConfig.translate.llm) { | |
| overrides.push(`๋ฒ์ญ โ ${getModelName(perStepConfig.translate.llm, llmModels)}`); | |
| } | |
| if (perStepConfig.keyframe.llm) { | |
| overrides.push(`ํคํ๋ ์ โ ${getModelName(perStepConfig.keyframe.llm, visionModels)}`); | |
| } | |
| if (overrides.length > 0) { | |
| dot.className = 'settings-dot active'; | |
| summary.style.display = ''; | |
| summary.textContent = 'โ ๋จ๊ณ๋ณ ์ค์ : ' + overrides.join(' | '); | |
| } else { | |
| dot.className = 'settings-dot'; | |
| summary.style.display = 'none'; | |
| summary.textContent = ''; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |