gni commited on
Commit
1344296
·
1 Parent(s): dfff2a2

feat: unified build for Hugging Face Spaces

Browse files
Files changed (6) hide show
  1. Dockerfile +40 -0
  2. README.md +2 -2
  3. api/main.py +27 -3
  4. api/setup_models.py +2 -1
  5. ui/src/App.tsx +15 -7
  6. 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
- @app.get("/")
76
- async def root():
 
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
- subprocess.check_call([sys.executable, "-m", "spacy", "download", model])
 
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, Eye, Lock, CheckCircle2, Copy,
5
- Database, Languages, Fingerprint, Zap, Activity,
6
- Palette, ChevronDown, Check, Radio, Target, Terminal,
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 || 'http://localhost:8000';
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
  })