Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*
!app/
!app/**
!scripts/
!scripts/serve.sh
!pyproject.toml
!uv.lock
**/__pycache__/
**/*.pyc
50 changes: 50 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Checks
on: [push, pull_request]
permissions:
contents: read
jobs:
python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: "3.13"
version: "0.12.19"
- run: uv sync --frozen --dev
- run: bash scripts/check.sh
docs:
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: docs/package-lock.json
- run: npm ci
- run: npm run build
container:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t scamshield:test .
- name: Check container startup with Redis
run: |
cp example.env .env
echo 'SCAMSHIELD_API_KEYS=ci-only-token-00000000000000000000000000000000' >> .env
docker network create smoke
docker run -d --name redis --network smoke redis:7.4-alpine
docker run -d --name api --network smoke -p 127.0.0.1:8000:8000 \
--env-file .env -e REDIS_URL=redis://redis:6379/0 scamshield:test
for attempt in $(seq 1 30); do
if curl -fsS http://127.0.0.1:8000/ready; then break; fi
sleep 2
done
curl -fsS http://127.0.0.1:8000/ready
docker exec -i api python < scripts/smoke.py
- if: failure()
run: docker logs api
15 changes: 0 additions & 15 deletions .github/workflows/pylint.yml

This file was deleted.

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,8 @@ __marimo__/

# Streamlit
.streamlit/secrets.toml

# Astro documentation build outputs
docs/node_modules/
docs/.astro/
docs/dist/
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Repository notes

- Python 3.13+ with uv: `uv sync --dev --frozen`, then `bash scripts/check.sh` (tests, then pylint; fails fast). Run from the root because audio tests use `tests/test_audio.m4a`. Focused test: `uv run pytest tests/test_security.py::test_text_contract_and_legacy_email`.
- `tests/conftest.py` supplies fake credentials, authenticated TestClient headers, Whisper stubbing, and fresh fakeredis per test. No model downloads or paid API calls are needed. Remove the client's Authorization header explicitly when testing unauthenticated requests.
- `app/main.py` mounts `app/api/analyze.py`: `/text` is primary, `/email` is a deprecated alias, and `/ws` accepts complete clips rather than partial audio streams. HTTP bearer dependencies document auth; `app/middleware.py` also rejects invalid tokens before multipart parsing.
- Live startup requires `DEEPSEEK_API_KEY` and `SCAMSHIELD_API_KEYS` (comma-separated random backend tokens, each >=32 characters). Redis 7+ is required for `EXPIRE NX`; quotas are per token, shared across transports. Browser extensions need a user-authenticated gateway, not an embedded shared token.
- Audio requires the FFmpeg executable, not a Python FFmpeg wrapper. Whisper loads lazily unless `PRELOAD_WHISPER=true`; Compose enables preloading and persists its cache. Linux uses the explicit CPU PyTorch index in `pyproject.toml`; preserve it when updating `uv.lock`.
- Deployment uses `docker compose up --build -d` after configuring `.env`; see `docs/src/content/docs/guides/deployment.md`. Keep `scripts/serve.sh` WebSocket size/queue limits aligned with upload settings. `/ready` checks Redis, not DeepSeek or model accuracy.
- `scripts/smoke.py` runs via stdin inside a running test container (`docker exec -i <container> python < scripts/smoke.py`); it exercises real Redis/FFmpeg with mocked inference and consumes the test token's quota. Do not run against a production instance.
- `docs/` is an independent Astro/Starlight site: run `npm ci` and `npm run build` there. CI checks Python, docs, and the container smoke path; live model/provider acceptance checks remain separate.
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM ghcr.io/astral-sh/uv:0.12.19 AS uv
FROM python:3.13-slim-bookworm

COPY --from=uv /uv /usr/local/bin/uv
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --create-home --uid 10001 app

WORKDIR /app
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH" HOME=/home/app XDG_CACHE_HOME=/home/app/.cache \
OMP_NUM_THREADS=2 MKL_NUM_THREADS=2
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
UV_CACHE_DIR=/root/.cache/uv uv sync --frozen --no-dev --no-install-project
COPY app ./app
COPY scripts/serve.sh /app/serve.sh
RUN mkdir -p /home/app/.cache && chown -R app:app /home/app/.cache \
&& chmod +x /app/serve.sh
USER app
EXPOSE 8000
CMD ["/app/serve.sh"]
49 changes: 42 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# ScamShield API

A FastAPI-powered API for analyzing scam emails and scam audio using local transcription and AI-based risk assessment.
A FastAPI API for assessing scam risk in text and audio. Whisper transcribes audio locally;
DeepSeek analyzes the text. Results are advisory, not a guarantee of safety.

## Features
Risk Assessment for:
- Email scam analysis
- Audio file scam analysis (e.g. recorded call)
- Real-time audio analysis over WebSockets (e.g. live call)
Authenticated endpoints:
- `POST /text` — messages, emails, or selected browser text (`{"body":"..."}`).
- `POST /audio` — a multipart upload named `file`.
- `WS /ws` — one complete, decodable audio clip per binary message.
- `POST /email` — deprecated compatibility alias; optional `sender` is ignored.

All analysis requires `Authorization: Bearer <token>` from `SCAMSHIELD_API_KEYS`.
`GET /health` is liveness; `GET /ready` checks Redis. Neither probes DeepSeek.

## Project Structure

Expand All @@ -22,12 +27,42 @@ tests/
```

## Getting Started
* Get your .env configured (look at example.env)
Requires Python 3.13+, uv, Redis 7+, and the **FFmpeg executable** for audio.
Copy `example.env` to `.env`, set `DEEPSEEK_API_KEY`, and generate a backend token with
`openssl rand -hex 32` for `SCAMSHIELD_API_KEYS`. Keep `.env` out of version control.

```bash
uv sync
uv sync --dev --frozen
uv run --env-file .env fastapi dev app/main.py
```

```bash
curl http://localhost:8000/text \
-H "Authorization: Bearer $SCAMSHIELD_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"body":"Pay this fee now to claim your prize"}'
```

Export `SCAMSHIELD_API_KEY` in your client shell to one of the server's configured tokens.
Run `bash scripts/check.sh` for tests followed by lint. Tests use fake credentials,
Redis, and Whisper; they make no paid provider calls.

## Deployment and clients

See [the deployment guide](docs/src/content/docs/guides/deployment.md) for Docker Compose,
TLS proxying, resource limits, credential rotation, and rollout checks. The Linux image
uses CPU-only PyTorch; benchmark Whisper on the target host before accepting public traffic.

- **Discord:** keep the API token on the bot server; defer interactions before audio analysis.
The bot must enforce per-user quotas; the API quota is shared by all users of that token.
- **Chrome extension:** use your own user-authenticated backend as a gateway. Do not embed
a shared ScamShield or DeepSeek key in extension code. Browser WebSockets cannot set an
Authorization header; proxy those connections through the backend, or use HTTP uploads.

Text and transcripts are sent to DeepSeek after best-effort regex redaction. Redaction
does **not** remove every kind of personal data. Ask users before submitting content;
see the [privacy and integration notes](docs/src/content/docs/guides/privacy.md).

## Contributing

Contributions are welcome.
Expand Down
49 changes: 49 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
services:
api:
build: .
init: true
restart: unless-stopped
env_file: ${SCAMSHIELD_ENV_FILE:-.env}
environment:
REDIS_URL: redis://redis:6379/0
PRELOAD_WHISPER: "true"
ports:
- "127.0.0.1:8000:8000"
volumes:
- whisper-cache:/home/app/.cache
tmpfs:
- /tmp:size=256m,mode=1777
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
mem_limit: 6g
cpus: 2
pids_limit: 128
depends_on:
redis:
condition: service_healthy
healthcheck:
test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/ready', timeout=3)"]
interval: 30s
timeout: 5s
start_period: 10m
retries: 3

redis:
image: redis:7.4-alpine
restart: unless-stopped
command: [redis-server, --save, "", --appendonly, "no", --maxmemory, 128mb, --maxmemory-policy, noeviction]
read_only: true
tmpfs: [/data]
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
user: redis
mem_limit: 256m
healthcheck:
test: [CMD, redis-cli, ping]
interval: 5s
timeout: 3s
retries: 5

volumes:
whisper-cache:
4 changes: 3 additions & 1 deletion docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default defineConfig({
{
icon: 'github',
label: 'GitHub',
href: 'https://github.com/<your-github-username>/<your-repo>',
href: 'https://github.com/ashrafee-dev/scamshield-api',
},
],

Expand All @@ -23,6 +23,8 @@ export default defineConfig({
label: 'Quick Start',
slug: 'guides/quickstart',
},
{ label: 'Deployment', slug: 'guides/deployment' },
{ label: 'Privacy and clients', slug: 'guides/privacy' },
],
},
{
Expand Down
Loading
Loading