gni commited on
Commit ·
1344296
1
Parent(s): dfff2a2
feat: unified build for Hugging Face Spaces
Browse files- Dockerfile +40 -0
- README.md +2 -2
- api/main.py +27 -3
- api/setup_models.py +2 -1
- ui/src/App.tsx +15 -7
- ui/vite.config.ts +8 -0
Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Unified Dockerfile for Hugging Face Spaces
|
| 2 |
+
# Stage 1: Build the frontend
|
| 3 |
+
FROM node:25-slim AS build-ui
|
| 4 |
+
WORKDIR /app/ui
|
| 5 |
+
COPY ui/package*.json ./
|
| 6 |
+
RUN npm install
|
| 7 |
+
COPY ui/ ./
|
| 8 |
+
RUN npm run build
|
| 9 |
+
|
| 10 |
+
# Stage 2: Final Image
|
| 11 |
+
FROM python:3.12-slim
|
| 12 |
+
WORKDIR /app
|
| 13 |
+
|
| 14 |
+
# Install system dependencies
|
| 15 |
+
RUN apt-get update && apt-get install -y \
|
| 16 |
+
build-essential \
|
| 17 |
+
curl \
|
| 18 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
# Create a non-root user for Hugging Face (UID 1000)
|
| 21 |
+
RUN useradd -m -u 1000 user
|
| 22 |
+
USER user
|
| 23 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 24 |
+
|
| 25 |
+
# Install Python dependencies
|
| 26 |
+
COPY --chown=user api/requirements.txt .
|
| 27 |
+
RUN pip install --no-cache-dir --user -r requirements.txt
|
| 28 |
+
|
| 29 |
+
# Copy backend code
|
| 30 |
+
COPY --chown=user api/ ./
|
| 31 |
+
|
| 32 |
+
# Copy built frontend from Stage 1 to the 'dist' folder
|
| 33 |
+
COPY --from=build-ui --chown=user /app/ui/dist ./dist
|
| 34 |
+
|
| 35 |
+
# Hugging Face Spaces uses port 7860
|
| 36 |
+
EXPOSE 7860
|
| 37 |
+
|
| 38 |
+
# Run setup and start the API
|
| 39 |
+
# We ensure models are installed in user space
|
| 40 |
+
CMD ["sh", "-c", "python setup_models.py && uvicorn main:app --host 0.0.0.0 --port 7860"]
|
README.md
CHANGED
|
@@ -49,7 +49,7 @@ make test # Run tests
|
|
| 49 |
docker compose up --build
|
| 50 |
```
|
| 51 |
|
| 52 |
-
- **API**: `http://localhost:8000`
|
| 53 |
- **UI Dashboard**: `http://localhost:5173`
|
| 54 |
|
| 55 |
---
|
|
@@ -61,7 +61,7 @@ You can interact directly with the Redac API using `curl`.
|
|
| 61 |
### Redact Text
|
| 62 |
**Request:**
|
| 63 |
```bash
|
| 64 |
-
curl -X POST http://localhost:8000/redact \
|
| 65 |
-H "Content-Type: application/json" \
|
| 66 |
-d '{"text": "My name is John Doe, call me at 06 12 34 56 78."}'
|
| 67 |
```
|
|
|
|
| 49 |
docker compose up --build
|
| 50 |
```
|
| 51 |
|
| 52 |
+
- **API**: `http://localhost:8000/api`
|
| 53 |
- **UI Dashboard**: `http://localhost:5173`
|
| 54 |
|
| 55 |
---
|
|
|
|
| 61 |
### Redact Text
|
| 62 |
**Request:**
|
| 63 |
```bash
|
| 64 |
+
curl -X POST http://localhost:8000/api/redact \
|
| 65 |
-H "Content-Type: application/json" \
|
| 66 |
-d '{"text": "My name is John Doe, call me at 06 12 34 56 78."}'
|
| 67 |
```
|
api/main.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
|
| 3 |
from pydantic import BaseModel
|
| 4 |
from typing import List, Dict, Optional
|
| 5 |
import logging
|
| 6 |
import re
|
|
|
|
| 7 |
|
| 8 |
from presidio_analyzer import AnalyzerEngine, RecognizerRegistry, PatternRecognizer, Pattern
|
| 9 |
from presidio_analyzer.predefined_recognizers import SpacyRecognizer
|
|
@@ -72,12 +75,14 @@ class RedactRequest(BaseModel):
|
|
| 72 |
text: str
|
| 73 |
language: Optional[str] = "auto"
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
|
|
|
| 77 |
return {"status": "online", "mode": "pro-visual"}
|
| 78 |
|
| 79 |
-
@app.post("/redact")
|
| 80 |
async def redact_text(request: RedactRequest):
|
|
|
|
| 81 |
try:
|
| 82 |
try:
|
| 83 |
target_lang = detect(request.text) if request.language == "auto" else request.language
|
|
@@ -118,5 +123,24 @@ async def redact_text(request: RedactRequest):
|
|
| 118 |
logger.error(f"Error: {str(e)}")
|
| 119 |
raise HTTPException(status_code=500, detail=str(e))
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
if __name__ == "__main__":
|
| 122 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.responses import FileResponse
|
| 5 |
from pydantic import BaseModel
|
| 6 |
from typing import List, Dict, Optional
|
| 7 |
import logging
|
| 8 |
import re
|
| 9 |
+
import os
|
| 10 |
|
| 11 |
from presidio_analyzer import AnalyzerEngine, RecognizerRegistry, PatternRecognizer, Pattern
|
| 12 |
from presidio_analyzer.predefined_recognizers import SpacyRecognizer
|
|
|
|
| 75 |
text: str
|
| 76 |
language: Optional[str] = "auto"
|
| 77 |
|
| 78 |
+
# API routes
|
| 79 |
+
@app.get("/api/status")
|
| 80 |
+
async def api_status():
|
| 81 |
return {"status": "online", "mode": "pro-visual"}
|
| 82 |
|
| 83 |
+
@app.post("/api/redact")
|
| 84 |
async def redact_text(request: RedactRequest):
|
| 85 |
+
# ... (rest of the logic)
|
| 86 |
try:
|
| 87 |
try:
|
| 88 |
target_lang = detect(request.text) if request.language == "auto" else request.language
|
|
|
|
| 123 |
logger.error(f"Error: {str(e)}")
|
| 124 |
raise HTTPException(status_code=500, detail=str(e))
|
| 125 |
|
| 126 |
+
# Mount static files for the UI
|
| 127 |
+
if os.path.exists("dist"):
|
| 128 |
+
# First, serve specific asset folders to avoid catching /api/
|
| 129 |
+
app.mount("/assets", StaticFiles(directory="dist/assets"), name="assets")
|
| 130 |
+
|
| 131 |
+
# Catch-all for the frontend SPA (must be last)
|
| 132 |
+
@app.get("/{full_path:path}")
|
| 133 |
+
async def serve_frontend(full_path: str):
|
| 134 |
+
# If the file exists in dist, serve it (e.g., favicon, icons.svg)
|
| 135 |
+
potential_file = os.path.join("dist", full_path)
|
| 136 |
+
if os.path.isfile(potential_file):
|
| 137 |
+
return FileResponse(potential_file)
|
| 138 |
+
# Otherwise serve index.html for SPA routing
|
| 139 |
+
return FileResponse("dist/index.html")
|
| 140 |
+
|
| 141 |
+
@app.get("/")
|
| 142 |
+
async def serve_index():
|
| 143 |
+
return FileResponse("dist/index.html")
|
| 144 |
+
|
| 145 |
if __name__ == "__main__":
|
| 146 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
api/setup_models.py
CHANGED
|
@@ -13,7 +13,8 @@ def check_and_download():
|
|
| 13 |
print(f"✅ {model} is already present.")
|
| 14 |
except OSError:
|
| 15 |
print(f"📥 {model} not found. Downloading (this may take a few minutes)...")
|
| 16 |
-
|
|
|
|
| 17 |
print(f"✨ {model} downloaded successfully.")
|
| 18 |
|
| 19 |
if __name__ == "__main__":
|
|
|
|
| 13 |
print(f"✅ {model} is already present.")
|
| 14 |
except OSError:
|
| 15 |
print(f"📥 {model} not found. Downloading (this may take a few minutes)...")
|
| 16 |
+
# Using --user to ensure it goes into a writable directory for non-root users
|
| 17 |
+
subprocess.check_call([sys.executable, "-m", "spacy", "download", model, "--user"])
|
| 18 |
print(f"✨ {model} downloaded successfully.")
|
| 19 |
|
| 20 |
if __name__ == "__main__":
|
ui/src/App.tsx
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
import { useState, useEffect, useRef } from 'react';
|
| 2 |
import axios from 'axios';
|
| 3 |
import {
|
| 4 |
-
Shield,
|
| 5 |
-
Database, Languages, Fingerprint, Zap,
|
| 6 |
-
Palette,
|
| 7 |
-
Upload, Trash2
|
| 8 |
} from 'lucide-react';
|
| 9 |
|
| 10 |
interface EntityMeta {
|
|
@@ -44,7 +43,7 @@ function App() {
|
|
| 44 |
const langRef = useRef<HTMLDivElement>(null);
|
| 45 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 46 |
|
| 47 |
-
const API_URL = import.meta.env.VITE_API_URL || '
|
| 48 |
|
| 49 |
useEffect(() => {
|
| 50 |
localStorage.setItem('pg-theme', theme);
|
|
@@ -58,7 +57,7 @@ function App() {
|
|
| 58 |
|
| 59 |
useEffect(() => {
|
| 60 |
const checkStatus = async () => {
|
| 61 |
-
try { await axios.get(`${API_URL}/`); setApiStatus('online'); }
|
| 62 |
catch (err) { setApiStatus('offline'); }
|
| 63 |
};
|
| 64 |
checkStatus();
|
|
@@ -68,7 +67,7 @@ function App() {
|
|
| 68 |
if (!text.trim()) return;
|
| 69 |
setLoading(true);
|
| 70 |
try {
|
| 71 |
-
const response = await axios.post(`${API_URL}/redact`, { text, language });
|
| 72 |
setResult(response.data);
|
| 73 |
} catch (err: any) { console.error(err); }
|
| 74 |
finally { setTimeout(() => setLoading(false), 400); }
|
|
@@ -141,6 +140,15 @@ function App() {
|
|
| 141 |
</div>
|
| 142 |
|
| 143 |
<div className="flex items-center gap-2 sm:gap-3">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
<div className="relative" ref={themeRef}>
|
| 145 |
<button onClick={() => setIsThemeOpen(!isThemeOpen)} className={`flex items-center gap-2 px-3 sm:px-4 py-2 border rounded-full text-[10px] sm:text-xs font-semibold transition-all ${themeClasses.panel} hover:border-blue-500`}>
|
| 146 |
<Palette className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> <span className="hidden xs:inline">{theme}</span>
|
|
|
|
| 1 |
import { useState, useEffect, useRef } from 'react';
|
| 2 |
import axios from 'axios';
|
| 3 |
import {
|
| 4 |
+
Shield, Lock, CheckCircle2,
|
| 5 |
+
Database, Languages, Fingerprint, Zap,
|
| 6 |
+
Palette, Upload, Trash2
|
|
|
|
| 7 |
} from 'lucide-react';
|
| 8 |
|
| 9 |
interface EntityMeta {
|
|
|
|
| 43 |
const langRef = useRef<HTMLDivElement>(null);
|
| 44 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 45 |
|
| 46 |
+
const API_URL = import.meta.env.VITE_API_URL || '';
|
| 47 |
|
| 48 |
useEffect(() => {
|
| 49 |
localStorage.setItem('pg-theme', theme);
|
|
|
|
| 57 |
|
| 58 |
useEffect(() => {
|
| 59 |
const checkStatus = async () => {
|
| 60 |
+
try { await axios.get(`${API_URL}/api/status`); setApiStatus('online'); }
|
| 61 |
catch (err) { setApiStatus('offline'); }
|
| 62 |
};
|
| 63 |
checkStatus();
|
|
|
|
| 67 |
if (!text.trim()) return;
|
| 68 |
setLoading(true);
|
| 69 |
try {
|
| 70 |
+
const response = await axios.post(`${API_URL}/api/redact`, { text, language });
|
| 71 |
setResult(response.data);
|
| 72 |
} catch (err: any) { console.error(err); }
|
| 73 |
finally { setTimeout(() => setLoading(false), 400); }
|
|
|
|
| 140 |
</div>
|
| 141 |
|
| 142 |
<div className="flex items-center gap-2 sm:gap-3">
|
| 143 |
+
<a
|
| 144 |
+
href="https://github.com/gni/redac"
|
| 145 |
+
target="_blank"
|
| 146 |
+
rel="noopener noreferrer"
|
| 147 |
+
className={`flex items-center gap-2 px-3 sm:px-4 py-2 border rounded-full text-[10px] sm:text-xs font-semibold transition-all ${themeClasses.panel} hover:border-blue-500 mr-2`}
|
| 148 |
+
>
|
| 149 |
+
<svg className="w-3.5 h-3.5 sm:w-4 sm:h-4 fill-current" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
| 150 |
+
<span className="hidden xs:inline">GitHub</span>
|
| 151 |
+
</a>
|
| 152 |
<div className="relative" ref={themeRef}>
|
| 153 |
<button onClick={() => setIsThemeOpen(!isThemeOpen)} className={`flex items-center gap-2 px-3 sm:px-4 py-2 border rounded-full text-[10px] sm:text-xs font-semibold transition-all ${themeClasses.panel} hover:border-blue-500`}>
|
| 154 |
<Palette className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> <span className="hidden xs:inline">{theme}</span>
|
ui/vite.config.ts
CHANGED
|
@@ -4,4 +4,12 @@ import react from '@vitejs/plugin-react'
|
|
| 4 |
// https://vite.dev/config/
|
| 5 |
export default defineConfig({
|
| 6 |
plugins: [react()],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
})
|
|
|
|
| 4 |
// https://vite.dev/config/
|
| 5 |
export default defineConfig({
|
| 6 |
plugins: [react()],
|
| 7 |
+
server: {
|
| 8 |
+
proxy: {
|
| 9 |
+
'/api': {
|
| 10 |
+
target: 'http://localhost:8000',
|
| 11 |
+
changeOrigin: true,
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
})
|