#!/bin/bash # setup-docker-pii.sh # Definitive setup for google/gemma-4-E4B-it with fixed huggingface_hub imports IMAGE_NAME="pii-extractor-gemma4:latest" CONTAINER_NAME="pii-extractor-gemma4" PORT="7005" DEFAULT_MODEL="google/gemma-4-E4B-it" CYAN='\033[0;36m' GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' NC='\033[0m' echo "" echo -e "${CYAN}=== PII Extract + Mask (Gemma 4 Definitive Setup) ===${NC}" echo "" if ! command -v docker &>/dev/null; then echo -e "${RED}[FAIL] Docker is not installed.${NC}" exit 1 fi # ============================================================================== # 1. ENSURE HF_TOKEN IS PROVIDED # ============================================================================== if [ -z "$HF_TOKEN" ]; then echo -e "${YELLOW}⚠️ CRITICAL: HF_TOKEN is not set in your environment.${NC}" echo -e "${YELLOW} Gemma models on Hugging Face are gated and REQUIRE a token to download config.json.${NC}" read -p "👉 Enter your Hugging Face Read Token (starts with 'hf_'): " INPUT_TOKEN if [[ "$INPUT_TOKEN" == hf_* ]]; then export HF_TOKEN="$INPUT_TOKEN" echo -e "${GREEN}✅ Token accepted.${NC}" else echo -e "${RED}❌ Invalid token format. Exiting.${NC}" exit 1 fi else echo -e "${GREEN}✅ HF_TOKEN detected.${NC}" fi # ============================================================================== # 2. WRITE FILES # ============================================================================== echo -e "${YELLOW}[1/4] Writing requirements.txt...${NC}" cat << 'EOF' > requirements.txt fastapi uvicorn[standard] pydantic transformers>=4.50.0 torch>=2.4.0 accelerate>=0.34.0 huggingface-hub>=0.26.0 sentencepiece protobuf EOF echo -e "${GREEN} [OK] requirements.txt created${NC}" echo -e "${YELLOW}[2/4] Writing app.py (Fixed HfFolder import error)...${NC}" cat << 'PYEOF' > app.py import os, re, json, logging, asyncio, unicodedata from typing import Optional, List, Tuple, Dict, Any from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, JSONResponse from fastapi.exceptions import RequestValidationError from pydantic import BaseModel, Field, field_validator from transformers import pipeline from huggingface_hub import model_info, get_token import torch logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") logger = logging.getLogger("pii-masker") app = FastAPI(title="PII Extract + Mask API", version="18.0-Gemma4-Fixed-Import") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) DEFAULT_INSTRUCTION = os.environ.get( "HF_INSTRUCTION", "Extract all PII including: PERSON, DATE_OF_BIRTH, SSN, PHONE_NUMBER, EMAIL_ADDRESS, ADDRESS, ID_NUMBER, BANK_ACCOUNT, CREDIT_CARD. Return ONLY valid JSON with these exact keys and arrays of strings." ) def api_success(data: Any, message: str) -> JSONResponse: return JSONResponse(status_code=200, content={"success": True, "message": message, "data": data, "error": None}) def api_error(message: str, error: str, code: int = 400) -> JSONResponse: return JSONResponse(status_code=code, content={"success": False, "message": message, "data": None, "error": error}) @app.exception_handler(RequestValidationError) async def validation_handler(request: Request, exc: RequestValidationError): errors = [".".join(str(l) for l in err["loc"] if l != "body") + f": {err['msg']}" for err in exc.errors()] return api_error("Validation failed", "; ".join(errors), 400) class AppConfig(BaseModel): hf_model: str = os.environ.get("HF_MODEL", "google/gemma-4-E4B-it") hf_token: Optional[str] = os.environ.get("HF_TOKEN", None) device: str = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" CONFIG = AppConfig() generator = None @app.on_event("startup") async def startup_event(): global generator logger.info(f"🧠 Verifying access to model: {CONFIG.hf_model}...") # Use modern get_token() instead of deprecated HfFolder token = CONFIG.hf_token or get_token() try: info = model_info(CONFIG.hf_model, token=token) logger.info(f"✅ Successfully verified access to {CONFIG.hf_model} (Gated/Private: {info.gated or info.private})") except Exception as e: logger.error("=" * 80) logger.error("❌ CRITICAL: Cannot access the model repository!") logger.error(f"Error details: {e}") logger.error("") logger.error("💡 WHY THIS HAPPENS:") logger.error("Gemma models on Hugging Face are 'gated'. You MUST do the following:") logger.error("1. Go to: https://huggingface.co/google/gemma-4-E4B-it") logger.error("2. Log in and click 'Agree and access repository'.") logger.error("3. Generate a 'Read' token at: https://huggingface.co/settings/tokens") logger.error("4. Run this script and provide the token when prompted.") logger.error("=" * 80) return # Exit startup, app will run but model will be None logger.info(f"🧠 Loading model weights on {CONFIG.device.upper()}...") try: dtype = torch.float16 if CONFIG.device in ["cuda", "mps"] else torch.float32 generator = pipeline( "text-generation", model=CONFIG.hf_model, token=token, device=CONFIG.device, torch_dtype=dtype, trust_remote_code=True, revision="main", low_cpu_mem_usage=True ) logger.info("✅ Model loaded successfully!") except Exception as e: logger.error(f"❌ Failed to load model weights: {e}") class ExtractMaskRequest(BaseModel): text: str = Field(..., min_length=1) instruction: str = Field(default=DEFAULT_INSTRUCTION) @field_validator("text") @classmethod def _validate_text(cls, v: str) -> str: if not v or not v.strip(): raise ValueError("Input text cannot be empty") return v.strip() class UnmaskRequest(BaseModel): masked_text: str = Field(..., min_length=1) mask_mapping: Dict[str, str] = Field(..., description="Mapping of tokens to original values") async def llm_call(prompt: str, temp: float = 0.1, n: int = 2048) -> str: if generator is None: raise RuntimeError("Model not loaded. Check startup logs for token/gating errors.") def _generate(): return generator(prompt, max_new_tokens=n, temperature=temp, do_sample=True, return_full_text=False, pad_token_id=generator.tokenizer.eos_token_id)[0]["generated_text"].strip() return await asyncio.to_thread(_generate) def sanitize(s) -> str: if s is None: return "" s = re.sub(r'[\u200b\u200c\u200d\ufeff\u00a0\x00-\x08\x0b\x0c\x0e-\x1f]', '', str(s)) return unicodedata.normalize("NFC", s.strip().strip('"').strip("'").rstrip(",;")) def clean_label(s: str) -> str: return re.sub(r'_+', '_', re.sub(r'[^A-Za-z0-9_]', '_', sanitize(s))).strip("_").upper() SKIP_VALUES = {"null", "none", "empty", "n/a", "na", "", "nil"} LABEL_BLACKLIST = {"name", "full name", "date of birth", "dob", "ssn", "phone", "email", "address", "id", "credit card", "bank", "n/a", "none", "null", "empty"} def parse_pii_json(raw: str) -> List[dict]: entities = [] sanitized = sanitize(raw) fence = re.search(r'```(?:json)?\s*\n?([\s\S]*?)\n?\s*```', sanitized) if fence: sanitized = fence.group(1).strip() def add(label_raw, val_raw): label, value = clean_label(label_raw), sanitize(val_raw) if label and value and len(value) >= 2 and value.lower() not in SKIP_VALUES and value.lower() not in LABEL_BLACKLIST: entities.append({"original": value, "label": label}) for cand in [sanitized, raw]: try: data = json.loads(cand) if isinstance(data, dict): for k, v in data.items(): for item in (v if isinstance(v, list) else [v]): add(k, item) if entities: return entities except (json.JSONDecodeError, TypeError): continue return entities async def extract_chunked(text: str, instruction: str) -> Tuple[str, List[dict]]: system_prompt = "You are a strict PII extraction AI. Extract ONLY actual data values. Return ONLY valid JSON.\n\n" prompt = f"{system_prompt}USER INSTRUCTION: {instruction}\n\nSOURCE TEXT:\n{text}\n\nOUTPUT:" raw = await llm_call(prompt, 0.1, 2048) return raw, parse_pii_json(raw) def mask_by_swap(original_text: str, entities: List[dict]) -> Tuple[str, int, Dict[str, str]]: if not entities: return original_text, 0, {} seen = {e["original"].strip(): e["label"] for e in entities if e["original"].strip()} sorted_entities = sorted(seen.items(), key=lambda x: len(x[0]), reverse=True) spans, label_counters, text_to_token, mask_mapping = [], {}, {}, {} for orig, label in sorted_entities: search_start, text_lower, orig_lower = 0, original_text.lower(), orig.lower() while True: idx = text_lower.find(orig_lower, search_start) if idx == -1: break s, e = idx, idx + len(orig_lower) if not any(not (e <= us or s >= ue) for us, ue, _, _ in spans): spans.append((s, e, label, orig)) search_start = e spans.sort(key=lambda x: x[0]) span_tokens = [] for s, e, label, orig in spans: if orig not in text_to_token: count = label_counters.get(label, 0) + 1 label_counters[label] = count token = f"{label}_{count}" text_to_token[orig] = token mask_mapping[token] = orig span_tokens.append((s, e, text_to_token[orig])) parts, cursor = [], 0 for s, e, token in span_tokens: if cursor < s: parts.append(original_text[cursor:s]) parts.append(f"[{token}]") cursor = e if cursor < len(original_text): parts.append(original_text[cursor:]) return "".join(parts), len(span_tokens), mask_mapping @app.get("/health") async def health(): is_loaded = generator is not None return api_success({"hf_model_loaded": is_loaded, "model_name": CONFIG.hf_model, "device": CONFIG.device}, "Service healthy") if is_loaded else api_error("Service degraded: Model failed to load. Check logs for token instructions.", 200) @app.post("/extractor") async def extractor_post(req: ExtractMaskRequest): try: merged_json, entities = await extract_chunked(req.text, req.instruction) grouped = {} for e in entities: grouped.setdefault(e["label"], []) if e["original"] not in grouped[e["label"]]: grouped[e["label"]].append(e["original"]) return api_success({"extracted_json": grouped, "entities_found": len(entities), "model_used": CONFIG.hf_model}, f"Completed ({len(entities)} entities)") except Exception as e: logger.error("Failed: %s", e, exc_info=True) return api_error("Processing error", str(e), 500) @app.post("/extract-mask") async def extract_mask_post(req: ExtractMaskRequest): try: merged_json, entities = await extract_chunked(req.text, req.instruction) masked_text, replacements, mask_mapping = mask_by_swap(req.text, entities) grouped = {} for e in entities: grouped.setdefault(e["label"], []) if e["original"] not in grouped[e["label"]]: grouped[e["label"]].append(e["original"]) return api_success({"extracted_json": grouped, "masked_text": masked_text, "mask_mapping": mask_mapping, "entities_found": len(entities), "replacements_made": replacements, "model_used": CONFIG.hf_model}, f"Completed ({len(entities)} entities, {replacements} replacements)") except Exception as e: logger.error("Failed: %s", e, exc_info=True) return api_error("Processing error", str(e), 500) @app.post("/unmask") async def unmask_post(req: UnmaskRequest): try: text = req.masked_text for token, original in sorted(req.mask_mapping.items(), key=lambda x: len(x[0]), reverse=True): text = text.replace(f"[{token}]", original) return api_success({"unmasked_text": text}, "Text successfully unmasked") except Exception as e: return api_error("Unmasking error", str(e), 500) HTML_UI = r"""PII Extract + Mask

PII Extract + Mask (Gemma 4)

Powered by google/gemma-4-E4B-it. Extracts PII, masks with indexed tokens, and provides a mapping for easy unmasking.

Extracted PII JSON
Masked Text
Mask Mapping (Index)
""" @app.get("/PII") @app.get("/") def home(): return HTMLResponse(HTML_UI) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7005) PYEOF echo -e "${GREEN} [OK] app.py created${NC}" echo -e "${YELLOW}[3/4] Writing Dockerfile...${NC}" cat << 'EOF' > Dockerfile FROM python:3.11-slim WORKDIR /app RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 7005 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7005"] EOF echo -e "${GREEN} [OK] Dockerfile created${NC}" # ============================================================================== # 3. BUILD AND RUN DOCKER # ============================================================================== echo -e "${YELLOW}[4/4] Building and Running Docker Container...${NC}" echo " Stopping and removing old container..." docker rm -f $CONTAINER_NAME 2>/dev/null || true echo " Clearing any corrupted local Hugging Face cache..." rm -rf ~/.cache/huggingface/hub/models--google--gemma-4-E4B-it 2>/dev/null || true echo " Building image: $IMAGE_NAME ..." docker build --no-cache -t $IMAGE_NAME . echo " Starting container: $CONTAINER_NAME ..." docker run -d \ --gpus all \ --name $CONTAINER_NAME \ -p $PORT:7005 \ -e HF_MODEL="$DEFAULT_MODEL" \ -e HF_TOKEN="$HF_TOKEN" \ -e HF_INSTRUCTION="${HF_INSTRUCTION:-Extract all PII including: PERSON, DATE_OF_BIRTH, SSN, PHONE_NUMBER, EMAIL_ADDRESS, ADDRESS, ID_NUMBER, BANK_ACCOUNT, CREDIT_CARD. Return ONLY valid JSON with these exact keys and arrays of strings.}" \ $IMAGE_NAME echo "" echo -e "${GREEN}========================================================${NC}" echo -e "${GREEN}✅ SETUP COMPLETE!${NC}" echo -e "${GREEN}========================================================${NC}" echo -e "${CYAN}🌐 Open your browser: http://localhost:$PORT/PII${NC}" echo -e "${YELLOW}📋 View live logs (watch for the access check):${NC}" echo -e " docker logs -f $CONTAINER_NAME" echo -e "${GREEN}========================================================${NC}" https://buz.best/sitemap-misc.xml 2026-10-25T18:18:44+00:00 https://buz.best/post-sitemap.xml 2026-10-25T16:40:55+00:00