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 Extractor</title> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-primary: #0a0a0f; | |
| --bg-secondary: #12121a; | |
| --bg-tertiary: #1a1a25; | |
| --bg-card: #15151f; | |
| --accent-primary: #6366f1; | |
| --accent-secondary: #8b5cf6; | |
| --accent-tertiary: #ec4899; | |
| --accent-success: #10b981; | |
| --accent-warning: #f59e0b; | |
| --accent-danger: #ef4444; | |
| --text-primary: #f8fafc; | |
| --text-secondary: #94a3b8; | |
| --text-muted: #64748b; | |
| --border-color: #2d2d3a; | |
| --glow-primary: rgba(99, 102, 241, 0.4); | |
| --glow-active: rgba(236, 72, 153, 0.6); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| radial-gradient(ellipse at 20% 20%, rgba(99, 102, 241, 0.08) 0%, transparent 50%), | |
| radial-gradient(ellipse at 80% 80%, rgba(236, 72, 153, 0.06) 0%, transparent 50%), | |
| radial-gradient(ellipse at 50% 50%, rgba(139, 92, 246, 0.04) 0%, transparent 70%); | |
| pointer-events: none; | |
| z-index: -1; | |
| } | |
| .container { | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 20px 0; | |
| border-bottom: 1px solid var(--border-color); | |
| margin-bottom: 30px; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo-icon { | |
| width: 40px; | |
| height: 40px; | |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 20px; | |
| } | |
| .logo h1 { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, var(--text-primary), var(--accent-primary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .logo a { | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| font-size: 0.85rem; | |
| transition: color 0.3s; | |
| } | |
| .logo a:hover { | |
| color: var(--accent-primary); | |
| } | |
| .main-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 400px; | |
| gap: 24px; | |
| } | |
| @media (max-width: 1200px) { | |
| .main-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| border-radius: 16px; | |
| padding: 24px; | |
| margin-bottom: 24px; | |
| } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .card-title { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .card-title-icon { | |
| width: 28px; | |
| height: 28px; | |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-tertiary)); | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 14px; | |
| } | |
| /* Upload Source Tabs */ | |
| .source-tabs { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 20px; | |
| } | |
| .source-tab { | |
| flex: 1; | |
| padding: 14px 20px; | |
| background: var(--bg-secondary); | |
| border: 2px solid var(--border-color); | |
| border-radius: 10px; | |
| color: var(--text-secondary); | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .source-tab:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--accent-primary); | |
| } | |
| .source-tab.active { | |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); | |
| border-color: var(--accent-primary); | |
| color: white; | |
| box-shadow: 0 4px 15px var(--glow-primary); | |
| } | |
| .source-tab .live-dot { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--accent-success); | |
| border-radius: 50%; | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.5; transform: scale(1.2); } | |
| } | |
| .upload-zone { | |
| border: 2px dashed var(--border-color); | |
| border-radius: 12px; | |
| padding: 40px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| background: var(--bg-secondary); | |
| } | |
| .upload-zone:hover, | |
| .upload-zone.dragover { | |
| border-color: var(--accent-primary); | |
| background: rgba(99, 102, 241, 0.05); | |
| } | |
| .upload-zone input { | |
| display: none; | |
| } | |
| .upload-icon { | |
| font-size: 48px; | |
| margin-bottom: 16px; | |
| } | |
| .upload-text { | |
| color: var(--text-secondary); | |
| margin-bottom: 8px; | |
| } | |
| .upload-hint { | |
| color: var(--text-muted); | |
| font-size: 0.85rem; | |
| } | |
| /* Live Camera Section */ | |
| .live-camera-section { | |
| display: none; | |
| } | |
| .live-camera-section.active { | |
| display: block; | |
| } | |
| .camera-preview-container { | |
| position: relative; | |
| width: 100%; | |
| background: #000; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| aspect-ratio: 16/9; | |
| } | |
| .camera-preview-container video { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| transform: scaleX(-1); | |
| } | |
| .camera-placeholder { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--bg-secondary); | |
| color: var(--text-muted); | |
| } | |
| .camera-controls { | |
| display: flex; | |
| justify-content: center; | |
| gap: 12px; | |
| padding: 16px; | |
| background: var(--bg-tertiary); | |
| border-radius: 0 0 12px 12px; | |
| } | |
| .camera-btn { | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .camera-btn.start { | |
| background: linear-gradient(135deg, var(--accent-success), #059669); | |
| color: white; | |
| } | |
| .camera-btn.stop { | |
| background: linear-gradient(135deg, var(--accent-danger), #dc2626); | |
| color: white; | |
| } | |
| .camera-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); | |
| } | |
| .recording-indicator { | |
| position: absolute; | |
| top: 16px; | |
| right: 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 16px; | |
| background: rgba(239, 68, 68, 0.9); | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .recording-indicator.active { | |
| opacity: 1; | |
| } | |
| .recording-dot { | |
| width: 10px; | |
| height: 10px; | |
| background: white; | |
| border-radius: 50%; | |
| animation: blink 1s infinite; | |
| } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| } | |
| .live-timestamp { | |
| position: absolute; | |
| top: 16px; | |
| left: 16px; | |
| padding: 8px 16px; | |
| background: rgba(0, 0, 0, 0.7); | |
| border-radius: 8px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.9rem; | |
| display: none; | |
| } | |
| .live-timestamp.active { | |
| display: block; | |
| } | |
| .video-container { | |
| position: relative; | |
| width: 100%; | |
| background: #000; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| aspect-ratio: 16/9; | |
| } | |
| .video-container video { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| } | |
| .video-placeholder { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--bg-secondary); | |
| color: var(--text-muted); | |
| } | |
| .subtitle-overlay { | |
| position: absolute; | |
| border: 2px solid var(--accent-primary); | |
| background: rgba(99, 102, 241, 0.1); | |
| cursor: move; | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| display: none; | |
| } | |
| .subtitle-overlay.active { | |
| display: block; | |
| } | |
| .subtitle-overlay:hover, | |
| .subtitle-overlay.dragging { | |
| border-color: var(--accent-tertiary); | |
| box-shadow: 0 0 20px var(--glow-primary); | |
| } | |
| .video-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px padding: 16px; | |
| background: var(--bg-tertiary); | |
| border-radius: 0 0 12px 12px; | |
| } | |
| .control-btn { | |
| width: 40px; | |
| height: 40px; | |
| border: none; | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 16px; | |
| transition: all 0.2s; | |
| } | |
| .control-btn:hover { | |
| background: var(--accent-primary); | |
| } | |
| .control-btn.active { | |
| background: var(--accent-primary); | |
| } | |
| .time-display { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| min-width: 100px; | |
| } | |
| .progress-bar { | |
| flex: 1; | |
| height: 6px; | |
| background: var(--bg-secondary); | |
| border-radius: 3px; | |
| cursor: pointer; | |
| position: relative; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); | |
| border-radius: 3px; | |
| width: 0%; | |
| transition: width 0.1s; | |
| } | |
| .progress-range { | |
| position: absolute; | |
| height: 100%; | |
| background: rgba(236, 72, 153, 0.3); | |
| border-radius: 3px; | |
| } | |
| .volume-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .volume-slider { | |
| width: 80px; | |
| height: 4px; | |
| -webkit-appearance: none; | |
| background: var(--bg-secondary); | |
| border-radius: 2px; | |
| cursor: pointer; | |
| } | |
| .volume-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 12px; | |
| height: 12px; | |
| background: var(--accent-primary); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| .adjustment-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 16px; | |
| } | |
| .adjustment-item { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .adjustment-label { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .adjustment-value { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.8rem; | |
| color: var(--accent-primary); | |
| background: var(--bg-secondary); | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| } | |
| input[type="range"] { | |
| width: 100%; | |
| height: 6px; | |
| -webkit-appearance: none; | |
| background: var(--bg-secondary); | |
| border-radius: 3px; | |
| cursor: pointer; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| box-shadow: 0 2px 8px var(--glow-primary); | |
| transition: transform 0.2s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| transform: scale(1.2); | |
| } | |
| .language-select { | |
| width: 100%; | |
| padding: 12px 16px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| color: var(--text-primary); | |
| font-size: 0.95rem; | |
| cursor: pointer; | |
| } | |
| .language-select:hover, | |
| .language-select:focus { | |
| border-color: var(--accent-primary); | |
| outline: none; | |
| } | |
| .language-select option { | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| } | |
| .time-inputs { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 16px; | |
| } | |
| .time-input-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .time-input-group label { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| } | |
| .time-input-wrapper { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .time-input { | |
| flex: 1; | |
| padding: 10px 12px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| color: var(--text-primary); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.9rem; | |
| } | |
| .time-input:focus { | |
| border-color: var(--accent-primary); | |
| outline: none; | |
| } | |
| .set-time-btn { | |
| padding: 10px 14px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| transition: all 0.2s; | |
| } | |
| .set-time-btn:hover { | |
| background: var(--accent-primary); | |
| color: var(--text-primary); | |
| border-color: var(--accent-primary); | |
| } | |
| .btn { | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| cursor: pointer all 0.3s; | |
| ; | |
| transition: display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); | |
| color: white; | |
| box-shadow: 0 4px 15px var(--glow-primary); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px var(--glow-primary); | |
| } | |
| .btn-primary:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .btn-secondary { | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| border: 1px solid var(--border-color); | |
| } | |
| .btn-secondary:hover { | |
| background: var(--bg-secondary); | |
| border-color: var(--accent-primary); | |
| } | |
| .btn-danger { | |
| background: rgba(239, 68, 68, 0.1); | |
| color: var(--accent-danger); | |
| border: 1px solid var(--accent-danger); | |
| } | |
| .btn-danger:hover { | |
| background: var(--accent-danger); | |
| color: white; | |
| } | |
| .options-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| } | |
| .option-toggle { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 12px; | |
| background: var(--bg-secondary); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .option-toggle:hover { | |
| background: var(--bg-tertiary); | |
| } | |
| .option-toggle input { | |
| display: none; | |
| } | |
| .toggle-switch { | |
| width: 40px; | |
| height: 22px; | |
| background: var(--bg-tertiary); | |
| border-radius: 11px; | |
| position: relative; | |
| transition: background 0.3s; | |
| flex-shrink: 0; | |
| } | |
| .option-toggle input:checked+.toggle-switch { | |
| background: var(--accent-primary); | |
| } | |
| .toggle-switch::after { | |
| content: ''; | |
| position: absolute; | |
| top: 3px; | |
| left: 3px; | |
| width: 16px; | |
| height: 16px; | |
| background: white; | |
| border-radius: 50%; | |
| transition: transform 0.3s; | |
| } | |
| .option-toggle input:checked+.toggle-switch::after { | |
| transform: translateX(18px); | |
| } | |
| .option-label { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| } | |
| .subtitles-panel { | |
| max-height: 500px; | |
| overflow-y: auto; | |
| } | |
| .subtitles-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .subtitle-item { | |
| padding: 14px; | |
| background: var(--bg-secondary); | |
| border-radius: 10px; | |
| border-left: 3px solid var(--border-color); | |
| transition: all 0.3s; | |
| cursor: pointer; | |
| } | |
| .subtitle-item:hover { | |
| background: var(--bg-tertiary); | |
| } | |
| .subtitle-item.active { | |
| border-left-color: var(--accent-tertiary); | |
| background: rgba(236, 72, 153, 0.1); | |
| } | |
| .subtitle-item.live { | |
| border-left-color: var(--accent-success); | |
| background: rgba(16, 185, 129, 0.1); | |
| } | |
| .subtitle-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .subtitle-index { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| .subtitle-time { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| margin-bottom: 6px; | |
| } | |
| .subtitle-text { | |
| font-size: 0.95rem; | |
| line-height: 1.5; | |
| color: var(--text-primary); | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 40px 20px; | |
| color: var(--text-muted); | |
| } | |
| .empty-state-icon { | |
| font-size: 48px; | |
| margin-bottom: 16px; | |
| opacity: 0.5; | |
| } | |
| .processing-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(10, 10, 15, 0.95); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| flex-direction: column; | |
| gap: 24px; | |
| } | |
| .processing-overlay.active { | |
| display: flex; | |
| } | |
| .spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 3px solid var(--border-color); | |
| border-top-color: var(--accent-primary); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .processing-text { | |
| font-size: 1.2rem; | |
| color: var(--text-primary); | |
| } | |
| .processing-progress { | |
| width: 300px; | |
| height: 8px; | |
| background: var(--bg-secondary); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| .processing-progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent-primary), var(--accent-tertiary)); | |
| width: 0%; | |
| transition: width 0.3s; | |
| } | |
| .export-panel { | |
| display: flex; | |
| gap: 12px; | |
| margin-top: 20px; | |
| padding-top: 20px; | |
| border-top: 1px solid var(--border-color); | |
| } | |
| .export-panel .btn { | |
| flex: 1; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-secondary); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border-color); | |
| border-radius: 4px; | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| } | |
| .stat-card { | |
| background: var(--bg-secondary); | |
| padding: 16px; | |
| border-radius: 10px; | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--accent-primary); | |
| } | |
| .stat-label { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| margin-top: 4px; | |
| } | |
| .toast { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| padding: 16px 24px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| transform: translateX(150%); | |
| transition: transform 0.3s; | |
| z-index: 1001; | |
| } | |
| .toast.show { | |
| transform: translateX(0); | |
| } | |
| .toast.success { | |
| border-color: var(--accent-success); | |
| } | |
| .toast.error { | |
| border-color: var(--accent-danger); | |
| } | |
| .fps-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| padding: 12px; | |
| background: var(--bg-secondary); | |
| border-radius: 8px; | |
| } | |
| .fps-control label { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| } | |
| .fps-control input[type="number"] { | |
| width: 80px; | |
| padding: 8px 12px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 6px; | |
| color: var(--text-primary); | |
| font-family: 'JetBrains Mono', mon-size: 0ospace; | |
| font.9rem; | |
| } | |
| .fps-control input[type="number"]:focus { | |
| border-color: var(--accent-primary); | |
| outline: none; | |
| } | |
| .btn-secondary:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .live-processing-info { | |
| background: rgba(16, 185, 129, 0.1); | |
| border: 1px solid var(--accent-success); | |
| border-radius: 8px; | |
| padding: 12px 16px; | |
| margin-bottom: 16px; | |
| display: none; | |
| } | |
| .live-processing-info.active { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .live-processing-info .pulse-dot { | |
| width: 10px; | |
| height: 10px; | |
| background: var(--accent-success); | |
| border-radius: 50%; | |
| animation: pulse 1.5s infinite; | |
| } | |
| .live-processing-text { | |
| color: var(--accent-success); | |
| font-size: 0.9rem; | |
| } | |
| @media (max-width: 768px) { | |
| .adjustment-grid, | |
| .options-grid, | |
| .time-inputs, | |
| .stats-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .export-panel { | |
| flex-direction: column; | |
| } | |
| .video-controls { | |
| flex-wrap: wrap; | |
| } | |
| .source-tabs { | |
| flex-direction: column; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div class="logo"> | |
| <div class="logo-icon">🎬</div> | |
| <div> | |
| <h1>Video Subtitle Extractor</h1> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">Built with anycoder</a> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="main-grid"> | |
| <div class="left-column"> | |
| <!-- Video Upload --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title"> | |
| <div class="card-title-icon">📁</div> | |
| Video Source | |
| </div> | |
| </div> | |
| <!-- Source Tabs --> | |
| <div class="source-tabs"> | |
| <button class="source-tab active" id="fileTab" onclick="switchSource('file')"> | |
| 📂 Upload File | |
| </button> | |
| <button class="source-tab" id="liveTab" onclick="switchSource('live')"> | |
| <span class="live-dot"></span> | |
| Live Camera | |
| </button> | |
| </div> | |
| <!-- File Upload Zone --> | |
| <div class="upload-zone" id="uploadZone"> | |
| <input type="file" id="videoInput" accept="video/*"> | |
| <div class="upload-icon">🎥</div> | |
| <div class="upload-text">Drop video here or click to browse</div> | |
| <div class="upload-hint">Supports MP4, WebM, MOV, AVI</div> | |
| </div> | |
| <!-- Live Camera Section --> | |
| <div class="live-camera-section" id="liveCameraSection"> | |
| <div class="camera-preview-container" id="cameraContainer"> | |
| <div class="camera-placeholder" id="cameraPlaceholder"> | |
| <div style="font-size: 48px; margin-bottom: 16px;">📷</div> | |
| <div>Click "Start Camera" to begin</div> | |
| <div style="font-size: 0.85rem; margin-top: 8px;">Allow camera access when prompted</div> | |
| </div> | |
| <video id="cameraVideo" autoplay playsinline muted></video> | |
| <div class="recording-indicator" id="recordingIndicator"> | |
| <span class="recording-dot"></span> | |
| LIVE | |
| </div> | |
| <div class="live-timestamp" id="liveTimestamp">00:00:00</div> | |
| <div class="subtitle-overlay" id="liveSubtitleOverlay"></div> | |
| </div> | |
| <div class="camera-controls"> | |
| <button class="camera-btn start" id="startCameraBtn"> | |
| 📷 Start Camera | |
| </button> | |
| <button class="camera-btn stop" id="stopCameraBtn" style="display: none;"> | |
| ⏹ Stop Camera | |
| </button> | |
| </div> | |
| <div class="live-processing-info" id="liveProcessingInfo"> | |
| <span class="pulse-dot"></span> | |
| <span class="live-processing-text">Processing live frames in real-time...</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Video Player --> | |
| <div class="card" id="videoPlayerCard"> | |
| <div class="card-header"> | |
| <div class="card-title"> | |
| <div class="card-title-icon">▶️</div> | |
| Video Preview | |
| </div> | |
| <span id="videoInfo" style="color: var(--text-muted); font-size: 0.85rem;"></span> | |
| </div> | |
| <div class="video-container" id="videoContainer"> | |
| <div class="video-placeholder" id="videoPlaceholder"> | |
| <div style="font-size: 48px; margin-bottom: 16px;">🎬</div> | |
| <div>No video loaded</div> | |
| </div> | |
| <video id="videoPlayer" style="display: none;" playsinline webkit-playsinline></video> | |
| <div class="subtitle-overlay" id="subtitleOverlay"></div> | |
| </div> | |
| <div class="video-controls" id="videoControls" style="display: none;"> | |
| <button class="control-btn" id="playPauseBtn" title="Play/Pause">▶</button> | |
| <button class="control-btn" id="skipBackBtn" title="Back 10s">⏪</button> | |
| <button class="control-btn" id="skipForwardBtn" title="Forward 10s">⏩</button> | |
| <div class="time-display" id="currentTime">00:00:00</div> | |
| <div class="progress-bar" id="progressBar"> | |
| <div class="progress-range"></div> | |
| <div class="progress-fill" id="progressFill"></div> | |
| </div> | |
| <div class="time-display" id="duration">00:00:00</div> | |
| <div class="volume-control"> | |
| <button class="control-btn" id="muteBtn" title="Mute">🔊</button> | |
| <input type="range" class="volume-slider" id="volumeSlider" min="0" max="100" value="100"> | |
| </div> | |
| <button class="control-btn" id="fullscreenBtn" title="Fullscreen">⛶</button> | |
| </div> | |
| </div> | |
| <!-- Adjust Subtitle Area --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title"> | |
| <div class="card-title-icon">📐</div> | |
| Subtitle Region Selection | |
| </div> | |
| <button class="btn btn-secondary" id="resetArea" style="padding: 8px 16px; font-size: 0.85rem;">↺ Reset</button> | |
| </div> | |
| <div class="adjustment-grid"> | |
| <div class="adjustment-item"> | |
| <div class="adjustment-label"> | |
| <span>Horizontal Position (X)</span> | |
| <span class="adjustment-value" id="xValue">10%</span> | |
| </div> | |
| <input type="range" id="xSlider" min="0" max="90" value="10"> | |
| </div> | |
| <div class="adjustment-item"> | |
| <div class="adjustment-label"> | |
| <span>Vertical Position (Y)</span> | |
| <span class="adjustment-value" id="yValue">70%</span> | |
| </div> | |
| <input type="range" id="ySlider" min="0" max="90" value="70"> | |
| </div> | |
| <div class="adjustment-item"> | |
| <div class="adjustment-label"> | |
| <span>Width</span> | |
| <span class="adjustment-value" id="widthValue">80%</span> | |
| </div> | |
| <input type="range" id="widthSlider" min="10" max="100" value="80"> | |
| </div> | |
| <div class="adjustment-item"> | |
| <div class="adjustment-label"> | |
| <span>Height</span> | |
| <span class="adjustment-value" id="heightValue">20%</span> | |
| </div> | |
| <input type="range" id="heightSlider" min="5" max="40" value="20"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Time & Language --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title"> | |
| <div class="card-title-icon">⏱️</div> | |
| Processing Settings | |
| </div> | |
| </div> | |
| <div class="fps-control"> | |
| <label>Processing interval:</label> | |
| <input type="number" id="fpsInput" value="1" min="0.1" max="5" step="0.1" title="Seconds between OCR captures"> | |
| <span style="color: var(--text-muted); font-size: 0.85rem;">seconds</span> | |
| </div> | |
| <div class="time-inputs" style="margin-bottom: 20px;" id="timeInputsSection"> | |
| <div class="time-input-group"> | |
| <label>Start Time (optional)</label> | |
| <div class="time-input-wrapper"> | |
| <input type="text" class="time-input" id="startTime" value="" placeholder="Leave empty for start"> | |
| <button class="set-time-btn" id="setStartBtn">Set</button> | |
| </div> | |
| </div> | |
| <div class="time-input-group"> | |
| <label>End Time (optional)</label> | |
| <div class="time-input-wrapper"> | |
| <input type="text" class="time-input" id="endTime" value="" placeholder="Leave empty for end"> | |
| <button class="set-time-btn" id="setEndBtn">Set</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="adjustment-item" style="margin-bottom: 20px;"> | |
| <label class="adjustment-label" style="margin-bottom: 8px;">OCR Language</label> | |
| <select class="language-select" id="languageSelect"> | |
| <option value="eng">English</option> | |
| <option value="jpn">Japanese</option> | |
| <option value="kor">Korean</option> | |
| <option value="chi_sim">Chinese (Simplified)</option> | |
| <option value="chi_tra">Chinese (Traditional)</option> | |
| <option value="tha">Thai</option> | |
| <option value="vie">Vietnamese</option> | |
| <option value="hin">Hindi</option> | |
| <option value="ara">Arabic</option> | |
| <option value="rus">Russian</option> | |
| <option value="spa">Spanish</option> | |
| <option value="fra">French</option> | |
| <option value="deu">German</option> | |
| <option value="por">Portuguese</option> | |
| <option value="ita">Italian</option> | |
| <option value="dut">Dutch</option> | |
| <option value="pol">Polish</option> | |
| <option value="tur">Turkish</option> | |
| </select> | |
| </div> | |
| <button class="btn btn-primary" id="processBtn" style="width: 100%;" disabled> | |
| <span>⚡</span> Start OCR Processing | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Right Column - Results --> | |
| <div class="right-column"> | |
| <!-- Options --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title"> | |
| <div class="card-title-icon">⚙️</div> | |
| Options | |
| </div> | |
| </div> | |
| <div class="options-grid"> | |
| <label class="option-toggle"> | |
| <input type="checkbox" id="showOverlay" checked> | |
| <div class="toggle-switch"></div> | |
| <span class="option-label">Show Selection Box</span> | |
| </label> | |
| <label class="option-toggle"> | |
| <input type="checkbox" id="autoPlay" checked> | |
| <div class="toggle-switch"></div> | |
| <span class="option-label">Auto Play Preview</span> | |
| </label> | |
| </div> | |
| </div> | |
| <!-- Results --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title"> | |
| <div class="card-title-icon">📝</div> | |
| Extracted Subtitles | |
| </div> | |
| <span id="subtitleCount" style="color: var(--text-muted); font-size: 0.85rem;">0 lines</span> | |
| </div> | |
| <div class="subtitles-panel"> | |
| <div class="subtitles-list" id="subtitlesList"> | |
| <div class="empty-state" id="emptyState"> | |
| <div class="empty-state-icon">📄</div> | |
| <div>No subtitles extracted yet</div> | |
| <div style="font-size: 0.85rem; margin-top: 8px;">Load a video or start live camera and click "Start OCR Processing"</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="export-panel" id="exportPanel" style="display: none;"> | |
| <button class="btn btn-secondary" id="exportSRT">SRT</button> | |
| <button class="btn btn-secondary" id="exportTXT">TXT</button> | |
| <button class="btn btn-secondary" id="exportJSON">JSON</button> | |
| <button class="btn btn-danger" id="clearAll">Clear</button> | |
| </div> | |
| </div> | |
| <!-- Stats --> | |
| <div class="card" id="statsCard" style="display: none;"> | |
| <div class="stats-grid"> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="statTotal">0</div> | |
| <div class="stat-label">Total Lines</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="statDuration">0:00</div> | |
| <div class="stat-label">Duration</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="statLanguage">-</div> | |
| <div class="stat-label">Language</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Processing Overlay --> | |
| <div class="processing-overlay" id="processingOverlay"> | |
| <div class="spinner"></div> | |
| <div class="processing-text" id="processingText">Initializing OCR Engine...</div> | |
| <div class="processing-progress"> | |
| <div class="processing-progress-fill" id="processingProgress"></div> | |
| </div> | |
| <div style="color: var(--text-muted); font-size: 0.9rem;" id="processingStatus">Loading Tesseract.js...</div> | |
| </div> | |
| <!-- Toast --> | |
| <div class="toast" id="toast"> | |
| <span id="toastIcon">✓</span> | |
| <span id="toastMessage">Success!</span> | |
| </div> | |
| <!-- Hidden Canvas --> | |
| <canvas id="processCanvas" style="display: none;"></canvas> | |
| <script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script> | |
| <script> | |
| // Application State | |
| const state = { | |
| videoLoaded: false, | |
| videoFile: null, | |
| subtitles: [], | |
| currentTime: 0, | |
| duration: 0, | |
| isPlaying: false, | |
| isMuted: false, | |
| showOverlay: true, | |
| autoPlay: true, | |
| subtitleBox: { x: 10, y: 70, width: 80, height: 20 }, | |
| volume: 1, | |
| processing: false, | |
| ocrWorker: null, | |
| currentSource: 'file', | |
| cameraActive: false, | |
| liveProcessing: false, | |
| liveStartTime: null, | |
| liveProcessingInterval: null | |
| }; | |
| // DOM Elements | |
| const elements = { | |
| uploadZone: document.getElementById('uploadZone'), | |
| videoInput: document.getElementById('videoInput'), | |
| videoPlayer: document.getElementById('videoPlayer'), | |
| videoPlaceholder: document.getElementById('videoPlaceholder'), | |
| videoContainer: document.getElementById('videoContainer'), | |
| videoControls: document.getElementById('videoControls'), | |
| videoInfo: document.getElementById('videoInfo'), | |
| subtitleOverlay: document.getElementById('subtitleOverlay'), | |
| playPauseBtn: document.getElementById('playPauseBtn'), | |
| skipBackBtn: document.getElementById('skipBackBtn'), | |
| skipForwardBtn: document.getElementById('skipForwardBtn'), | |
| muteBtn: document.getElementById('muteBtn'), | |
| fullscreenBtn: document.getElementById('fullscreenBtn'), | |
| progressBar: document.getElementById('progressBar'), | |
| progressFill: document.getElementById('progressFill'), | |
| currentTime: document.getElementById('currentTime'), | |
| duration: document.getElementById('duration'), | |
| volumeSlider: document |