Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Video Subtitle OCR - Extract Hard Subtitles</title> | |
| <link rel="stylesheet" href="style.css"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Header --> | |
| <header class="app-header"> | |
| <div class="header-content"> | |
| <div class="logo"> | |
| <svg width="32" height="32" viewBox="0 0 32 32" fill="none"> | |
| <rect width="32" height="32" rx="8" fill="#6366f1"/> | |
| <path d="M8 12h16M8 16h16M8 20h12" stroke="white" stroke-width="2" stroke-linecap="round"/> | |
| </svg> | |
| <h1>Video Subtitle OCR</h1> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-badge"> | |
| Built with anycoder | |
| </a> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="main-content"> | |
| <!-- Setup Page --> | |
| <div id="setup-page" class="page active"> | |
| <div class="setup-container"> | |
| <!-- Video Upload Section --> | |
| <section class="upload-section"> | |
| <h2>1. Upload Video</h2> | |
| <div class="upload-zone" id="upload-zone"> | |
| <input type="file" id="video-input" accept="video/*" hidden> | |
| <div class="upload-content"> | |
| <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> | |
| <polyline points="17,8 12,3 7,8"/> | |
| <line x1="12" y1="3" x2="12" y2="15"/> | |
| </svg> | |
| <p>Drag & drop video here or <span class="browse">browse</span></p> | |
| <p class="upload-hint">Supports MP4, WebM, MOV, AVI (max 2GB)</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Video Preview & Subtitle Area --> | |
| <section class="preview-section" id="preview-section" style="display: none;"> | |
| <h2>2. Adjust Subtitle Area</h2> | |
| <div class="video-wrapper"> | |
| <video id="video-preview" controls></video> | |
| <canvas id="subtitle-overlay" class="subtitle-overlay"></canvas> | |
| <div id="subtitle-box" class="subtitle-box"> | |
| <div class="resize-handle nw"></div> | |
| <div class="resize-handle ne"></div> | |
| <div class="resize-handle sw"></div> | |
| <div class="resize-handle se"></div> | |
| </div> | |
| </div> | |
| <!-- Subtitle Area Controls --> | |
| <div class="subtitle-controls"> | |
| <div class="control-group"> | |
| <label>Horizontal Position (X)</label> | |
| <input type="range" id="x-position" min="0" max="100" value="10"> | |
| <span class="value" id="x-value">10%</span> | |
| </div> | |
| <div class="control-group"> | |
| <label>Vertical Position (Y)</label> | |
| <input type="range" id="y-position" min="0" max="100" value="75"> | |
| <span class="value" id="y-value">75%</span> | |
| </div> | |
| <div class="control-group"> | |
| <label>Width</label> | |
| <input type="range" id="width" min="10" max="100" value="80"> | |
| <span class="value" id="width-value">80%</span> | |
| </div> | |
| <div class="control-group"> | |
| <label>Height</label> | |
| <input type="range" id="height" min="5" max="50" value="15"> | |
| <span class="value" id="height-value">15%</span> | |
| </div> | |
| <div class="control-group checkbox"> | |
| <label> | |
| <input type="checkbox" id="aspect-lock" checked> | |
| Lock Aspect Ratio | |
| </label> | |
| </div> | |
| </div> | |
| <div class="quick-actions"> | |
| <button id="reset-box" class="btn-secondary">Reset Box</button> | |
| <button id="auto-detect" class="btn-secondary">Auto-Detect Subtitles</button> | |
| </div> | |
| </section> | |
| <!-- Language Selection --> | |
| <section class="language-section" id="language-section" style="display: none;"> | |
| <h2>3. Select Language</h2> | |
| <div class="language-grid"> | |
| <button class="lang-btn active" data-lang="eng">English</button> | |
| <button class="lang-btn" data-lang="jpn">Japanese</button> | |
| <button class="lang-btn" data-lang="kor">Korean</button> | |
| <button class="lang-btn" data-lang="chi_sim">Chinese (Simplified)</button> | |
| <button class="lang-btn" data-lang="chi_tra">Chinese (Traditional)</button> | |
| <button class="lang-btn" data-lang="tha">Thai</button> | |
| </div> | |
| </section> | |
| <!-- Time Range Selection --> | |
| <section class="time-section" id="time-section" style="display: none;"> | |
| <h2>4. Select Time Range</h2> | |
| <div class="time-controls"> | |
| <div class="time-input-group"> | |
| <label>Start Time</label> | |
| <div class="time-input"> | |
| <input type="number" id="start-hours" min="0" max="99" value="0" placeholder="HH"> | |
| <span>:</span> | |
| <input type="number" id="start-minutes" min="0" max="59" value="0" placeholder="MM"> | |
| <span>:</span> | |
| <input type="number" id="start-seconds" min="0" max="59" value="0" placeholder="SS"> | |
| <span>.</span> | |
| <input type="number" id="start-ms" min="0" max="999" value="0" placeholder="mmm"> | |
| </div> | |
| <button id="set-start-current" class="btn-small">Set to Current</button> | |
| </div> | |
| <div class="time-input-group"> | |
| <label>End Time</label> | |
| <div class="time-input"> | |
| <input type="number" id="end-hours" min="0" max="99" value="0" placeholder="HH"> | |
| <span>:</span> | |
| <input type="number" id="end-minutes" min="0" max="59" value="0" placeholder="MM"> | |
| <span>:</span> | |
| <input type="number" id="end-seconds" min="0" max="59" value="0" placeholder="SS"> | |
| <span>.</span> | |
| <input type="number" id="end-ms" min="0" max="999" value="0" placeholder="mmm"> | |
| </div> | |
| <button id="set-end-current" class="btn-small">Set to Current</button> | |
| </div> | |
| </div> | |
| <div class="time-slider-container"> | |
| <div class="range-slider" id="time-range-slider"> | |
| <div class="range-track"></div> | |
| <div class="range-fill" id="range-fill"></div> | |
| <div class="range-thumb start" id="start-thumb"></div> | |
| <div class="range-thumb end" id="end-thumb"></div> | |
| </div> | |
| <div class="time-labels"> | |
| <span id="slider-start-time">00:00:00.000</span> | |
| <span id="slider-end-time">00:00:00.000</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Process Button --> | |
| <section class="process-section" id="process-section" style="display: none;"> | |
| <button id="process-btn" class="btn-primary btn-large"> | |
| <span class="btn-text">Process Video</span> | |
| <span class="btn-icon"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polygon points="5,3 19,12 5,21 5,3"/> | |
| </svg> | |
| </span> | |
| </button> | |
| <p class="process-hint">This may take a few minutes depending on video length</p> | |
| </section> | |
| </div> | |
| </div> | |
| <!-- Processing Page --> | |
| <div id="processing-page" class="page"> | |
| <div class="processing-container"> | |
| <div class="processing-animation"> | |
| <div class="spinner"></div> | |
| <div class="processing-video" id="processing-preview"></div> | |
| </div> | |
| <h2>Processing Video...</h2> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="progress-bar"> | |
| <div class="progress-fill" id="progress-fill"></div> | |
| </div> | |
| <div class="progress-info"> | |
| <span id="progress-text">Initializing OCR model...</span> | |
| <span id="progress-percent">0%</span> | |
| </div> | |
| </div> | |
| <div class="processing-stats"> | |
| <div class="stat"> | |
| <span class="stat-label">Frames Processed</span> | |
| <span class="stat-value" id="frames-processed">0</span> | |
| </div> | |
| <div class="stat"> | |
| <span class="stat-label">Subtitles Found</span> | |
| <span class="stat-value" id="subtitles-found">0</span> | |
| </div> | |
| <div class="stat"> | |
| <span class="stat-label">Time Elapsed</span> | |
| <span class="stat-value" id="time-elapsed">00:00</span> | |
| </div> | |
| </div> | |
| <button id="cancel-process" class="btn-secondary">Cancel</button> | |
| </div> | |
| </div> | |
| <!-- Results Page --> | |
| <div id="results-page" class="page"> | |
| <div class="results-container"> | |
| <!-- Video Player with Active Subtitle Highlighting --> | |
| <section class="result-video-section"> | |
| <h2>Preview with Subtitles</h2> | |
| <div class="video-wrapper result-video-wrapper"> | |
| <video id="result-video" controls></video> | |
| <div id="active-subtitle" class="active-subtitle-display"></div> | |
| </div> | |
| <div class="playback-controls"> | |
| <button id="prev-subtitle" class="btn-icon"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polygon points="19,20 9,12 19,4 19,20"/> | |
| <line x1="5" y1="19" x2="5" y2="5"/> | |
| </svg> | |
| </button> | |
| <button id="sync-subtitle" class="btn-icon" title="Sync to current subtitle"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"/> | |
| <polyline points="12,6 12,12 16,14"/> | |
| </svg> | |
| </button> | |
| <button id="next-subtitle" class="btn-icon"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polygon points="5,4 15,12 5,20 5,4"/> | |
| <line x1="19" y1="5" x2="19" y2="19"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </section> | |
| <!-- Subtitle List --> | |
| <section class="subtitle-list-section"> | |
| <div class="section-header"> | |
| <h2>Extracted Subtitles</h2> | |
| <div class="view-toggle"> | |
| <button class="toggle-btn active" data-view="list">List</button> | |
| <button class="toggle-btn" data-view="json">JSON</button> | |
| </div> | |
| </div> | |
| <!-- List View --> | |
| <div id="subtitle-list-view" class="subtitle-list"> | |
| <div class="list-header"> | |
| <span>#</span> | |
| <span>Time</span> | |
| <span>Subtitle Text</span> | |
| <span>Status</span> | |
| </div> | |
| <div id="subtitles-container" class="subtitles-container"> | |
| <!-- Subtitles will be inserted here --> | |
| </div> | |
| </div> | |
| <!-- JSON View --> | |
| <div id="subtitle-json-view" class="json-view" style="display: none;"> | |
| <pre id="json-output"></pre> | |
| <button id="copy-json" class="btn-secondary btn-small">Copy JSON</button> | |
| </div> | |
| </section> | |
| <!-- Export Section --> | |
| <section class="export-section"> | |
| <h2>Export Subtitles</h2> | |
| <div class="export-buttons"> | |
| <button id="export-srt" class="btn-primary"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> | |
| <polyline points="7,10 12,15 17,10"/> | |
| <line x1="12" y1="15" x2="12" y2="3"/> | |
| </svg> | |
| Download SRT | |
| </button> | |
| <button id="export-txt" class="btn-secondary"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> | |
| <polyline points="14,2 14,8 20,8"/> | |
| <line x1="16" y1="13" x2="8" y2="13"/> | |
| <line x1="16" y1="17" x2="8" y2="17"/> | |
| <polyline points="10,9 9,9 8,9"/> | |
| </svg> | |
| Download TXT | |
| </button> | |
| <button id="export-json" class="btn-secondary"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> | |
| <polyline points="14,2 14,8 20,8"/> | |
| <line x1="16" y1="13" x2="8" y2="13"/> | |
| <line x1="16" y1="17" x2="8" y2="17"/> | |
| <polyline points="10,9 9,9 8,9"/> | |
| </svg> | |
| Download JSON | |
| </button> | |
| </div> | |
| </section> | |
| <!-- Start Over --> | |
| <section class="start-over-section"> | |
| <button id="start-over" class="btn-secondary btn-large">Process Another Video</button> | |
| </section> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Loading Overlay --> | |
| <div id="loading-overlay" class="loading-overlay"> | |
| <div class="loading-content"> | |
| <div class="spinner"></div> | |
| <p>Loading OCR model...</p> | |
| <div class="model-progress"> | |
| <div class="model-progress-bar" id="model-progress-bar"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Error Toast --> | |
| <div id="error-toast" class="error-toast"> | |
| <span id="error-message"></span> | |
| <button id="error-close">×</button> | |
| </div> | |
| </div> | |
| <script type="module" src="index.js"></script> | |
| </body> | |
| </html> |