Spaces:
Sleeping
Sleeping
EdiBianco commited on
Commit ·
4efa1cd
1
Parent(s): 6e9392f
Replace Basic Auth with cookie login form (HF proxy fix)
Browse files
app.py
CHANGED
|
@@ -4,6 +4,8 @@ FastAPI + faster-whisper + yt-dlp
|
|
| 4 |
"""
|
| 5 |
import asyncio
|
| 6 |
import glob
|
|
|
|
|
|
|
| 7 |
import json
|
| 8 |
import os
|
| 9 |
import secrets
|
|
@@ -15,9 +17,9 @@ from typing import Optional
|
|
| 15 |
|
| 16 |
import yt_dlp
|
| 17 |
from faster_whisper import WhisperModel
|
| 18 |
-
from fastapi import
|
| 19 |
-
from fastapi.responses import StreamingResponse
|
| 20 |
-
from
|
| 21 |
|
| 22 |
# ---------------------------------------------------------------------------
|
| 23 |
# Config
|
|
@@ -25,12 +27,34 @@ from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
|
| 25 |
PASSWORD = os.getenv("APP_PASSWORD", "changeme")
|
| 26 |
MAX_UPLOAD_BYTES = 500 * 1024 * 1024 # 500 MB
|
| 27 |
WHISPER_MODEL = "base.en"
|
|
|
|
|
|
|
| 28 |
|
| 29 |
# ---------------------------------------------------------------------------
|
| 30 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
# ---------------------------------------------------------------------------
|
| 32 |
app = FastAPI()
|
| 33 |
-
|
| 34 |
executor = ThreadPoolExecutor(max_workers=2)
|
| 35 |
|
| 36 |
_model: Optional[WhisperModel] = None
|
|
@@ -46,15 +70,74 @@ def get_model() -> WhisperModel:
|
|
| 46 |
return _model
|
| 47 |
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
# ---------------------------------------------------------------------------
|
|
@@ -107,17 +190,13 @@ def transcribe_file(jid: str, path: str, cleanup_paths: list[str]):
|
|
| 107 |
# ---------------------------------------------------------------------------
|
| 108 |
# Routes
|
| 109 |
# ---------------------------------------------------------------------------
|
| 110 |
-
@app.get("/",
|
| 111 |
async def index():
|
| 112 |
-
|
| 113 |
-
return HTMLResponse(HTML)
|
| 114 |
|
| 115 |
|
| 116 |
@app.post("/transcribe/file")
|
| 117 |
-
async def transcribe_upload(
|
| 118 |
-
file: UploadFile = File(...),
|
| 119 |
-
_: str = Depends(check_auth),
|
| 120 |
-
):
|
| 121 |
# Size check via content-length header
|
| 122 |
size = int(file.headers.get("content-length", 0))
|
| 123 |
if size > MAX_UPLOAD_BYTES:
|
|
@@ -142,10 +221,7 @@ async def transcribe_upload(
|
|
| 142 |
|
| 143 |
|
| 144 |
@app.post("/transcribe/url")
|
| 145 |
-
async def transcribe_url(
|
| 146 |
-
url: str = Form(...),
|
| 147 |
-
_: str = Depends(check_auth),
|
| 148 |
-
):
|
| 149 |
jid = new_job()
|
| 150 |
push(jid, f"Fetching URL: {url}")
|
| 151 |
|
|
@@ -199,7 +275,7 @@ async def transcribe_url(
|
|
| 199 |
|
| 200 |
|
| 201 |
@app.get("/progress/{jid}")
|
| 202 |
-
async def progress(jid: str
|
| 203 |
if jid not in jobs:
|
| 204 |
raise HTTPException(404, "Job not found.")
|
| 205 |
|
|
|
|
| 4 |
"""
|
| 5 |
import asyncio
|
| 6 |
import glob
|
| 7 |
+
import hashlib
|
| 8 |
+
import hmac
|
| 9 |
import json
|
| 10 |
import os
|
| 11 |
import secrets
|
|
|
|
| 17 |
|
| 18 |
import yt_dlp
|
| 19 |
from faster_whisper import WhisperModel
|
| 20 |
+
from fastapi import FastAPI, File, Form, Request, UploadFile
|
| 21 |
+
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
|
| 22 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
| 23 |
|
| 24 |
# ---------------------------------------------------------------------------
|
| 25 |
# Config
|
|
|
|
| 27 |
PASSWORD = os.getenv("APP_PASSWORD", "changeme")
|
| 28 |
MAX_UPLOAD_BYTES = 500 * 1024 * 1024 # 500 MB
|
| 29 |
WHISPER_MODEL = "base.en"
|
| 30 |
+
COOKIE_NAME = "auth"
|
| 31 |
+
_SECRET = os.urandom(32) # ephemeral signing key
|
| 32 |
|
| 33 |
# ---------------------------------------------------------------------------
|
| 34 |
+
# Auth helpers
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
|
| 37 |
+
def _make_token(pw: str) -> str:
|
| 38 |
+
return hmac.new(_SECRET, pw.encode(), hashlib.sha256).hexdigest()
|
| 39 |
+
|
| 40 |
+
def _is_authed(request: Request) -> bool:
|
| 41 |
+
token = request.cookies.get(COOKIE_NAME, "")
|
| 42 |
+
return secrets.compare_digest(token, _make_token(PASSWORD))
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class AuthMiddleware(BaseHTTPMiddleware):
|
| 46 |
+
async def dispatch(self, request: Request, call_next):
|
| 47 |
+
if request.url.path in ("/login",):
|
| 48 |
+
return await call_next(request)
|
| 49 |
+
if not _is_authed(request):
|
| 50 |
+
return RedirectResponse("/login", status_code=302)
|
| 51 |
+
return await call_next(request)
|
| 52 |
+
|
| 53 |
+
# ---------------------------------------------------------------------------
|
| 54 |
+
# App
|
| 55 |
# ---------------------------------------------------------------------------
|
| 56 |
app = FastAPI()
|
| 57 |
+
app.add_middleware(AuthMiddleware)
|
| 58 |
executor = ThreadPoolExecutor(max_workers=2)
|
| 59 |
|
| 60 |
_model: Optional[WhisperModel] = None
|
|
|
|
| 70 |
return _model
|
| 71 |
|
| 72 |
|
| 73 |
+
# ---------------------------------------------------------------------------
|
| 74 |
+
# Login routes
|
| 75 |
+
# ---------------------------------------------------------------------------
|
| 76 |
+
LOGIN_HTML = """<!DOCTYPE html>
|
| 77 |
+
<html lang="en">
|
| 78 |
+
<head>
|
| 79 |
+
<meta charset="UTF-8">
|
| 80 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 81 |
+
<title>VOD Transcriber — Login</title>
|
| 82 |
+
<style>
|
| 83 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 84 |
+
body {
|
| 85 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 86 |
+
background: #0f0f11; color: #e2e2e5;
|
| 87 |
+
min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
| 88 |
+
}
|
| 89 |
+
.card {
|
| 90 |
+
background: #1a1a1f; border: 1px solid #2a2a32; border-radius: 16px;
|
| 91 |
+
padding: 2.5rem; width: 100%; max-width: 360px;
|
| 92 |
+
box-shadow: 0 8px 40px rgba(0,0,0,0.4);
|
| 93 |
+
}
|
| 94 |
+
h1 { font-size: 1.2rem; font-weight: 600; color: #fff; margin-bottom: 0.3rem; }
|
| 95 |
+
.sub { font-size: 0.82rem; color: #555; margin-bottom: 2rem; }
|
| 96 |
+
label { display: block; font-size: 0.78rem; font-weight: 500; color: #888; margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
| 97 |
+
input[type="password"] {
|
| 98 |
+
width: 100%; background: #111115; border: 1px solid #2a2a32; border-radius: 10px;
|
| 99 |
+
padding: 0.7rem 1rem; font-size: 0.9rem; color: #e2e2e5; outline: none;
|
| 100 |
+
margin-bottom: 1rem; transition: border-color 0.15s;
|
| 101 |
+
}
|
| 102 |
+
input[type="password"]:focus { border-color: #5b5bf6; }
|
| 103 |
+
button {
|
| 104 |
+
width: 100%; background: #5b5bf6; color: #fff; border: none; border-radius: 10px;
|
| 105 |
+
padding: 0.75rem; font-size: 0.95rem; font-weight: 600; cursor: pointer;
|
| 106 |
+
transition: background 0.15s;
|
| 107 |
+
}
|
| 108 |
+
button:hover { background: #4a4ae0; }
|
| 109 |
+
.err { color: #f66; font-size: 0.82rem; margin-top: 0.75rem; display: none; }
|
| 110 |
+
.err.visible { display: block; }
|
| 111 |
+
</style>
|
| 112 |
+
</head>
|
| 113 |
+
<body>
|
| 114 |
+
<div class="card">
|
| 115 |
+
<h1>VOD Transcriber</h1>
|
| 116 |
+
<p class="sub">Enter password to continue</p>
|
| 117 |
+
<form method="POST" action="/login">
|
| 118 |
+
<label for="pw">Password</label>
|
| 119 |
+
<input type="password" id="pw" name="password" autofocus autocomplete="current-password">
|
| 120 |
+
<button type="submit">Sign in</button>
|
| 121 |
+
</form>
|
| 122 |
+
<p class="err {err_class}">{err_msg}</p>
|
| 123 |
+
</div>
|
| 124 |
+
</body>
|
| 125 |
+
</html>"""
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@app.get("/login", response_class=HTMLResponse)
|
| 129 |
+
async def login_page():
|
| 130 |
+
return LOGIN_HTML.replace("{err_class}", "").replace("{err_msg}", "")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
@app.post("/login")
|
| 134 |
+
async def login(password: str = Form(...)):
|
| 135 |
+
if secrets.compare_digest(password.encode(), PASSWORD.encode()):
|
| 136 |
+
resp = RedirectResponse("/", status_code=302)
|
| 137 |
+
resp.set_cookie(COOKIE_NAME, _make_token(PASSWORD), httponly=True, samesite="lax")
|
| 138 |
+
return resp
|
| 139 |
+
html = LOGIN_HTML.replace("{err_class}", "visible").replace("{err_msg}", "Incorrect password.")
|
| 140 |
+
return HTMLResponse(html, status_code=401)
|
| 141 |
|
| 142 |
|
| 143 |
# ---------------------------------------------------------------------------
|
|
|
|
| 190 |
# ---------------------------------------------------------------------------
|
| 191 |
# Routes
|
| 192 |
# ---------------------------------------------------------------------------
|
| 193 |
+
@app.get("/", response_class=HTMLResponse)
|
| 194 |
async def index():
|
| 195 |
+
return HTML
|
|
|
|
| 196 |
|
| 197 |
|
| 198 |
@app.post("/transcribe/file")
|
| 199 |
+
async def transcribe_upload(file: UploadFile = File(...)):
|
|
|
|
|
|
|
|
|
|
| 200 |
# Size check via content-length header
|
| 201 |
size = int(file.headers.get("content-length", 0))
|
| 202 |
if size > MAX_UPLOAD_BYTES:
|
|
|
|
| 221 |
|
| 222 |
|
| 223 |
@app.post("/transcribe/url")
|
| 224 |
+
async def transcribe_url(url: str = Form(...)):
|
|
|
|
|
|
|
|
|
|
| 225 |
jid = new_job()
|
| 226 |
push(jid, f"Fetching URL: {url}")
|
| 227 |
|
|
|
|
| 275 |
|
| 276 |
|
| 277 |
@app.get("/progress/{jid}")
|
| 278 |
+
async def progress(jid: str):
|
| 279 |
if jid not in jobs:
|
| 280 |
raise HTTPException(404, "Job not found.")
|
| 281 |
|