Signia is a Colombian Sign Language (LSC) translation web application built on Django 5.2 and deployed on Railway via Gunicorn. The project is split into focused Django apps that each own a clear domain — authentication, text-to-sign translation, webcam sign recognition, and history — plus a standalone grammar module that bridges Spanish text to the LSC gloss order through the Groq LLM API. Understanding how these pieces fit together is essential before modifying any part of the stack.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/jtapieromalambo-ctrl/Signia/llms.txt
Use this file to discover all available pages before exploring further.
Project Structure
The repository root doubles as the Django project root. The directory layout below matches the structure documented inAGENTS.md:
App Responsibilities
| App / Module | Responsibility |
|---|---|
Signia/ | Django project config — settings.py, root urls.py, wsgi.py, asgi.py |
usuarios/ | Custom user model (usuarios.Usuario), email-based auth, OTP verification, Google/Facebook OAuth via django-allauth, contact form |
traduccion/ | Text and audio input → LSC sign video sequence; calls Whisper STT for audio, then passes text to lsc_grammar.convertir_a_lsc(), then looks up matching video files in the DB |
reconocimientos/ | Webcam hand-sign recognition; MediaPipe HandLandmarker extracts landmarks per frame, a trained RandomForest classifies the sequence into a sign label |
historial/ | Stores per-user translation and recognition events in EntradaHistorial records |
lsc_grammar.py | Standalone module at the project root; converts Spanish text to LSC gloss order via the Groq API with a four-model fallback chain and a rule-based safety net |
Data Flows
Text or Audio → LSC Sign Videos
This path is handled bytraduccion/views.py:buscar_video.
Input arrives
The user submits either typed text (
POST field palabra) or a microphone recording (POST file audio). Audio is transcribed to Spanish text by faster-Whisper (base model, CPU, int8).Vocabulary cache lookup
_obtener_vocabulario_bd() fetches all sign names from the video table. Results are cached in Django’s cache backend under the key vocabulario_lsc for 10 minutes to avoid repeated DB queries.LSC grammar conversion
lsc_grammar.convertir_a_lsc(text, vocabulario_bd) sends the Spanish text to the Groq API, which returns a structured JSON payload containing an ordered list of LSC gloss tokens, sentence type, facial expression markers, and strategies for handling missing signs.Token extraction
lsc_grammar.tokens_para_busqueda(resultado_lsc) strips non-searchable tokens (facial expressions, aspect markers, topic markers) and returns an ordered list of plain gloss strings.Video DB lookup
For each token (attempting multi-token compound matches up to 3 tokens first),
_buscar_token_con_fallbacks() queries the video table by nombre__iexact. If a synonym strategy was returned by the AI, the synonym is tried next. Tokens with no match are collected in faltantes.Webcam Signs → Text
This path is handled byreconocimientos/views.py:predecir_landmarks (or the fallback predecir endpoint for raw frame data).
Client captures landmarks
The browser runs MediaPipe HandLandmarker in JavaScript. For each webcam frame it extracts 21 hand landmarks × 3 coordinates × up to 2 hands = 126 floats. The landmark sequence is accumulated client-side.
Landmarks POST to server
The client POSTs
{ "secuencia": [[126 floats], ...] } to /reconocimientos/predecir_landmarks/.Centroid normalization
Each frame’s landmark array is passed to
_normalizar_landmarks_centroide(), which subtracts the mean position of each hand’s 21 landmarks. This makes predictions position-invariant — the model does not care where on screen the signer’s hands appear.Sequence normalization
normalizar_secuencia() linearly interpolates the variable-length frame sequence to exactly 30 frames using numpy.interp.Feature construction
construir_features() concatenates the flattened normalized positions, per-frame deltas, and delta magnitudes into a single feature vector.Key Technical Constraints
CompressedStaticFilesStorage — not ManifestStaticFilesStorage. The STATICFILES_STORAGE backend is set to WhiteNoise’s CompressedStaticFilesStorage. Manifest mode appends content hashes to filenames, which breaks MediaPipe’s WASM loader because it resolves worker and model files by their exact, unhashed names.ML model loads at import time.
reconocimientos/views.py calls _cargar_modelo() at module load. If reconocimientos/modelo/model_seq.pkl is absent, modelo and encoder are set to None and every prediction endpoint returns HTTP 503 until a model is trained.ffmpeg/ is prepended to PATH. traduccion/views.py prepends the local ffmpeg/ directory to os.environ["PATH"] at module load so that faster-Whisper can locate the bundled ffmpeg binary. Do not move or rename this directory.Sessions expire after 20 minutes.
SESSION_COOKIE_AGE = 1200 and SESSION_EXPIRE_AT_BROWSER_CLOSE = True are both active. SESSION_SAVE_EVERY_REQUEST = True resets the 20-minute clock on every request.Database
| Environment | Backend | Notes |
|---|---|---|
| Local development | SQLite (db.sqlite3) | Default when DATABASE_URL is not set |
| Production (Railway) | PostgreSQL via Neon | DATABASE_URL env var; sslmode=require; DISABLE_SERVER_SIDE_CURSORS=True required for Neon’s connection pooler |
DISABLE_SERVER_SIDE_CURSORS = True is set unconditionally in settings.py because Neon uses PgBouncer connection pooling, which does not support named server-side cursors.
Static Files
In production, WhiteNoise serves static files directly from Gunicorn workers viawhitenoise.middleware.WhiteNoiseMiddleware — no separate static-file server or CDN is required. Files are compressed at collectstatic time. .wasm and .task files are excluded from compression (configured in WHITENOISE_SKIP_COMPRESS_EXTENSIONS) to avoid Range Not Satisfiable errors when Chromium makes byte-range requests to MediaPipe’s WASM binary and model file.