Vtranscript / server.py
Depo23's picture
Upload 9 files
6e9392f verified
Raw
History Blame Contribute Delete
9.66 kB
#!/usr/bin/env python3
"""Local web UI for transcribe_vod.sh — run with: python3 server.py"""
import http.server
import subprocess
import json
import os
import threading
import signal
SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "transcribe_vod.sh")
PORT = 8765
HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VOD Transcriber</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0f0f11;
color: #e2e2e5;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.card {
background: #1a1a1f;
border: 1px solid #2a2a32;
border-radius: 16px;
padding: 2.5rem;
width: 100%;
max-width: 640px;
box-shadow: 0 8px 40px rgba(0,0,0,0.4);
}
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 2rem;
}
h1 {
font-size: 1.4rem;
font-weight: 600;
color: #fff;
margin-bottom: 0.4rem;
letter-spacing: -0.02em;
}
.subtitle {
font-size: 0.85rem;
color: #666;
}
.btn-stop {
background: transparent;
color: #555;
border: 1px solid #2a2a32;
border-radius: 8px;
padding: 0.4rem 0.85rem;
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: color 0.15s, border-color 0.15s;
flex-shrink: 0;
}
.btn-stop:hover { color: #f66; border-color: #f66; background: transparent; }
label {
display: block;
font-size: 0.8rem;
font-weight: 500;
color: #888;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.input-row {
display: flex;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
input[type="text"] {
flex: 1;
background: #111115;
border: 1px solid #2a2a32;
border-radius: 10px;
padding: 0.7rem 1rem;
font-size: 0.9rem;
color: #e2e2e5;
outline: none;
transition: border-color 0.15s;
}
input[type="text"]:focus { border-color: #5b5bf6; }
input[type="text"]::placeholder { color: #444; }
button {
background: #5b5bf6;
color: #fff;
border: none;
border-radius: 10px;
padding: 0.7rem 1.4rem;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, opacity 0.15s;
}
button:hover:not(:disabled) { background: #4a4ae0; }
button:disabled { opacity: 0.45; cursor: not-allowed; }
.status {
display: none;
align-items: center;
gap: 0.6rem;
font-size: 0.85rem;
color: #888;
margin-bottom: 1.25rem;
}
.status.visible { display: flex; }
.spinner {
width: 16px; height: 16px;
border: 2px solid #2a2a32;
border-top-color: #5b5bf6;
border-radius: 50%;
animation: spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
.log-box {
display: none;
background: #111115;
border: 1px solid #2a2a32;
border-radius: 10px;
padding: 1rem;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.75rem;
line-height: 1.6;
color: #666;
max-height: 220px;
overflow-y: auto;
margin-bottom: 1.25rem;
white-space: pre-wrap;
word-break: break-all;
}
.log-box.visible { display: block; }
.result {
display: none;
background: #0d1f17;
border: 1px solid #1a3a28;
border-radius: 10px;
padding: 1rem 1.25rem;
}
.result.visible { display: block; }
.result-label {
font-size: 0.75rem;
font-weight: 600;
color: #3d9e6a;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.result-link {
color: #5bf6a0;
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
word-break: break-all;
}
.result-link:hover { text-decoration: underline; }
.error-box {
display: none;
background: #1f0d0d;
border: 1px solid #3a1a1a;
border-radius: 10px;
padding: 1rem 1.25rem;
font-size: 0.85rem;
color: #f66;
}
.error-box.visible { display: block; }
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<div>
<h1>VOD Transcriber</h1>
<p class="subtitle">whisper.cpp · local · no upload</p>
</div>
<button class="btn-stop" onclick="stopServer()">Stop Server</button>
</div>
<label for="path">Video file path</label>
<div class="input-row">
<input type="text" id="path" placeholder="/path/to/video.mp4" autocomplete="off" spellcheck="false">
<button id="btn" onclick="run()">Transcribe</button>
</div>
<div class="status" id="status">
<div class="spinner"></div>
<span id="status-text">Running transcription…</span>
</div>
<pre class="log-box" id="log"></pre>
<div class="result" id="result">
<div class="result-label">Transcript ready</div>
<a class="result-link" id="result-link" href="#" target="_blank"></a>
</div>
<div class="error-box" id="error"></div>
</div>
<script>
async function run() {
const path = document.getElementById('path').value.trim();
if (!path) return;
const btn = document.getElementById('btn');
btn.disabled = true;
document.getElementById('status').classList.add('visible');
document.getElementById('log').classList.remove('visible');
document.getElementById('result').classList.remove('visible');
document.getElementById('error').classList.remove('visible');
document.getElementById('log').textContent = '';
try {
const res = await fetch('/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path })
});
const data = await res.json();
document.getElementById('status').classList.remove('visible');
if (data.log) {
document.getElementById('log').textContent = data.log;
document.getElementById('log').classList.add('visible');
}
if (data.ok && data.transcript) {
const link = document.getElementById('result-link');
link.href = '/file?path=' + encodeURIComponent(data.transcript);
link.textContent = data.transcript;
document.getElementById('result').classList.add('visible');
} else if (!data.ok) {
document.getElementById('error').textContent = 'Transcription failed. See log above.';
document.getElementById('error').classList.add('visible');
}
} catch (e) {
document.getElementById('status').classList.remove('visible');
document.getElementById('error').textContent = 'Server error: ' + e.message;
document.getElementById('error').classList.add('visible');
}
btn.disabled = false;
}
document.getElementById('path').addEventListener('keydown', e => {
if (e.key === 'Enter') run();
});
async function stopServer() {
if (!confirm('Stop the server?')) return;
try { await fetch('/stop', { method: 'POST' }); } catch (_) {}
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;color:#555;font-family:sans-serif">Server stopped.</div>';
}
</script>
</body>
</html>"""
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *args):
pass # suppress request logs
def do_GET(self):
if self.path == "/":
self._send(200, "text/html; charset=utf-8", HTML.encode())
elif self.path.startswith("/file?path="):
from urllib.parse import unquote, urlparse, parse_qs
qs = parse_qs(urlparse(self.path).query)
path = unquote(qs.get("path", [""])[0])
if not path or not os.path.isfile(path):
self._send(404, "text/plain", b"Not found")
return
with open(path, "rb") as f:
content = f.read()
self._send(200, "text/plain; charset=utf-8", content)
else:
self._send(404, "text/plain", b"Not found")
def do_POST(self):
if self.path == "/stop":
self._send(200, "text/plain", b"Stopping.")
threading.Timer(0.3, lambda: os.kill(os.getpid(), signal.SIGTERM)).start()
return
n = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(n))
video = body.get("path", "").strip()
if not video:
self._json({"ok": False, "log": "No path provided.", "transcript": None})
return
result = subprocess.run(
["bash", SCRIPT, video],
capture_output=True,
text=True,
)
base = os.path.splitext(video)[0]
transcript = base + "_transcript.txt"
self._json({
"ok": result.returncode == 0,
"log": (result.stdout + result.stderr).strip(),
"transcript": transcript if os.path.isfile(transcript) else None,
})
def _send(self, code, ctype, body):
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", len(body))
self.end_headers()
self.wfile.write(body)
def _json(self, data):
body = json.dumps(data).encode()
self._send(200, "application/json", body)
if __name__ == "__main__":
server = http.server.HTTPServer(("localhost", PORT), Handler)
print(f"Open → http://localhost:{PORT}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopped.")