Spaces:
Running
Running
remsky commited on
feat: version update, rate limit, etc
Browse files- .gitignore +1 -0
- Dockerfile +10 -15
- README.md +1 -0
- hf_app.py +47 -0
- hf_start.py +0 -12
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*.pyc
|
Dockerfile
CHANGED
|
@@ -1,19 +1,14 @@
|
|
| 1 |
-
FROM ghcr.io/remsky/kokoro-fastapi-cpu:
|
| 2 |
|
| 3 |
-
#
|
| 4 |
-
ENV
|
| 5 |
-
ENV
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
ENV
|
| 9 |
-
ENV ONNX_NUM_THREADS=2
|
| 10 |
-
ENV ONNX_INTER_OP_THREADS=1
|
| 11 |
-
ENV VOICE_CACHE_SIZE=1
|
| 12 |
|
| 13 |
-
#
|
| 14 |
-
COPY
|
| 15 |
|
| 16 |
-
EXPOSE
|
| 17 |
-
|
| 18 |
-
# Run FastAPI with root UI
|
| 19 |
-
CMD ["python", "/app/hf_start.py"]
|
|
|
|
| 1 |
+
FROM ghcr.io/remsky/kokoro-fastapi-cpu:v0.7.2
|
| 2 |
|
| 3 |
+
# 2 vCPU Space: keep torch from oversubscribing threads
|
| 4 |
+
ENV OMP_NUM_THREADS=2
|
| 5 |
+
ENV MKL_NUM_THREADS=2
|
| 6 |
|
| 7 |
+
# Default is DEBUG, which logs every caller's submitted text
|
| 8 |
+
ENV API_LOG_LEVEL=INFO
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
# Root redirect to the bundled web player
|
| 11 |
+
COPY hf_app.py /app/hf_app.py
|
| 12 |
|
| 13 |
+
EXPOSE 8880
|
| 14 |
+
CMD ["python", "-m", "uvicorn", "hf_app:app", "--host", "0.0.0.0", "--port", "8880"]
|
|
|
|
|
|
README.md
CHANGED
|
@@ -4,6 +4,7 @@ emoji: 📼
|
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: pink
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: true
|
| 8 |
short_description: Web Utility that's living on Kokoro-FastAPI
|
| 9 |
models:
|
|
|
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: pink
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 8880
|
| 8 |
pinned: true
|
| 9 |
short_description: Web Utility that's living on Kokoro-FastAPI
|
| 10 |
models:
|
hf_app.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HF Space shim: serve the web player at / instead of a bare 404.
|
| 2 |
+
import os
|
| 3 |
+
import time
|
| 4 |
+
from collections import deque
|
| 5 |
+
|
| 6 |
+
from fastapi import Request
|
| 7 |
+
from fastapi.responses import JSONResponse, RedirectResponse
|
| 8 |
+
|
| 9 |
+
from api.src.main import app
|
| 10 |
+
|
| 11 |
+
MAX_BODY_BYTES = int(os.getenv("MAX_BODY_BYTES", 8192))
|
| 12 |
+
RATE_LIMIT_PER_MIN = int(os.getenv("RATE_LIMIT_PER_MIN", 20))
|
| 13 |
+
|
| 14 |
+
_hits: dict[str, deque] = {}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _client_ip(request: Request) -> str:
|
| 18 |
+
forwarded = request.headers.get("x-forwarded-for", "").split(",")[-1].strip()
|
| 19 |
+
return forwarded or (request.client.host if request.client else "unknown")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@app.middleware("http")
|
| 23 |
+
async def throttle(request: Request, call_next):
|
| 24 |
+
if request.method != "POST":
|
| 25 |
+
return await call_next(request)
|
| 26 |
+
|
| 27 |
+
length = request.headers.get("content-length")
|
| 28 |
+
if length and int(length) > MAX_BODY_BYTES:
|
| 29 |
+
return JSONResponse({"detail": "Request body too large"}, status_code=413)
|
| 30 |
+
|
| 31 |
+
now = time.monotonic()
|
| 32 |
+
for stale in [k for k, v in _hits.items() if not v or v[-1] < now - 60]:
|
| 33 |
+
del _hits[stale]
|
| 34 |
+
hits = _hits.setdefault(_client_ip(request), deque())
|
| 35 |
+
while hits and hits[0] < now - 60:
|
| 36 |
+
hits.popleft()
|
| 37 |
+
if len(hits) >= RATE_LIMIT_PER_MIN:
|
| 38 |
+
return JSONResponse(
|
| 39 |
+
{"detail": "Rate limit exceeded"}, status_code=429, headers={"Retry-After": "60"}
|
| 40 |
+
)
|
| 41 |
+
hits.append(now)
|
| 42 |
+
return await call_next(request)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@app.get("/", include_in_schema=False)
|
| 46 |
+
async def root():
|
| 47 |
+
return RedirectResponse(url="/web/")
|
hf_start.py
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
from api.src.main import app
|
| 2 |
-
from fastapi.staticfiles import StaticFiles
|
| 3 |
-
from fastapi.responses import RedirectResponse
|
| 4 |
-
|
| 5 |
-
# Add root redirect to web UI
|
| 6 |
-
@app.get("/")
|
| 7 |
-
async def root():
|
| 8 |
-
return RedirectResponse(url="/web/")
|
| 9 |
-
|
| 10 |
-
if __name__ == "__main__":
|
| 11 |
-
import uvicorn
|
| 12 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|