From a3b300b76a19fd222cb104a065dd656fabbba42e Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Wed, 29 Apr 2026 14:22:51 +0100 Subject: [PATCH] docs: add canonical documentation + audit cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AGENTS.md: canonical project entry point (Quick Nav, pipeline, constraints) - docs/: complete docs tree β€” architecture, API spec, DB schema, infra, runbook, requirements, tech stack, principles, reference ADRs, guides, tasks backlog, testing strategy - tests/README.md: test commands, structure, known gaps - README.md / CLAUDE.md / DEPLOYMENT.md: updated with canonical doc links - .archive/: backup of pre-documentation-pipeline originals - backend/uv.lock: uv dependency lockfile - Delete committed __pycache__ .pyc files (should have been gitignored) Co-Authored-By: Claude Sonnet 4.6 --- .../source-docs-2026-04-29/README_cleanup.md | 25 + .../original/APACHE_DEPLOYMENT.md | 236 +++ .../original/DEPLOYMENT.md | Bin 0 -> 12156 bytes .../original/DEPLOYMENT_OPTIONS.md | 168 ++ .../source-docs-2026-04-29/original/README.md | 384 +++++ AGENTS.md | 97 ++ CLAUDE.md | 3 + DEPLOYMENT.md | Bin 12156 -> 12425 bytes README.md | 2 + .../__pycache__/celery_worker.cpython-313.pyc | Bin 2319 -> 0 bytes backend/app/__pycache__/main.cpython-313.pyc | Bin 13400 -> 0 bytes .../__pycache__/routes_admin.cpython-313.pyc | Bin 30262 -> 0 bytes .../__pycache__/routes_auth.cpython-313.pyc | Bin 7546 -> 0 bytes .../__pycache__/routes_files.cpython-313.pyc | Bin 2290 -> 0 bytes .../__pycache__/routes_jobs.cpython-313.pyc | Bin 45136 -> 0 bytes .../routes_websockets.cpython-313.pyc | Bin 9225 -> 0 bytes .../core/__pycache__/config.cpython-313.pyc | Bin 2706 -> 0 bytes .../core/__pycache__/database.cpython-313.pyc | Bin 4386 -> 0 bytes .../__pycache__/dependencies.cpython-313.pyc | Bin 3899 -> 0 bytes .../core/__pycache__/logging.cpython-313.pyc | Bin 4399 -> 0 bytes .../core/__pycache__/redis.cpython-313.pyc | Bin 2920 -> 0 bytes .../secrets_config.cpython-313.pyc | Bin 6474 -> 0 bytes .../core/__pycache__/security.cpython-313.pyc | Bin 3129 -> 0 bytes .../app/lib/__pycache__/vtt.cpython-313.pyc | Bin 10363 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 523 -> 0 bytes .../__pycache__/rate_limiting.cpython-313.pyc | Bin 10947 -> 0 bytes .../__pycache__/validation.cpython-313.pyc | Bin 13747 -> 0 bytes .../__pycache__/audit_log.cpython-313.pyc | Bin 6623 -> 0 bytes .../models/__pycache__/job.cpython-313.pyc | Bin 5396 -> 0 bytes .../models/__pycache__/user.cpython-313.pyc | Bin 2978 -> 0 bytes .../schemas/__pycache__/auth.cpython-313.pyc | Bin 3866 -> 0 bytes .../schemas/__pycache__/file.cpython-313.pyc | Bin 957 -> 0 bytes .../schemas/__pycache__/job.cpython-313.pyc | Bin 5045 -> 0 bytes .../__pycache__/audit_logger.cpython-313.pyc | Bin 14431 -> 0 bytes .../__pycache__/emailer.cpython-313.pyc | Bin 5392 -> 0 bytes .../services/__pycache__/gcs.cpython-313.pyc | Bin 9871 -> 0 bytes .../__pycache__/gemini.cpython-313.pyc | Bin 18948 -> 0 bytes .../__pycache__/translate.cpython-313.pyc | Bin 4411 -> 0 bytes .../services/__pycache__/tts.cpython-313.pyc | Bin 12244 -> 0 bytes .../__pycache__/validation.cpython-313.pyc | Bin 6282 -> 0 bytes .../__pycache__/websocket.cpython-313.pyc | Bin 22251 -> 0 bytes .../websocket_publisher.cpython-313.pyc | Bin 8649 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 5918 -> 0 bytes .../__pycache__/ingest_and_ai.cpython-313.pyc | Bin 11933 -> 0 bytes .../tasks/__pycache__/notify.cpython-313.pyc | Bin 10631 -> 0 bytes .../translate_and_synthesize.cpython-313.pyc | Bin 17197 -> 0 bytes .../__pycache__/watchers.cpython-313.pyc | Bin 6855 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 860 -> 0 bytes .../__pycache__/metrics.cpython-313.pyc | Bin 13840 -> 0 bytes .../__pycache__/tracing.cpython-313.pyc | Bin 12517 -> 0 bytes backend/uv.lock | 3 + docs/README.md | 56 + docs/documentation_standards.md | 61 + docs/principles.md | 70 + docs/project/api_spec.md | 198 +++ docs/project/architecture.md | 170 ++ docs/project/database_schema.md | 219 +++ docs/project/infrastructure.md | 146 ++ docs/project/requirements.md | 154 ++ docs/project/runbook.md | 213 +++ docs/project/tech_stack.md | 94 ++ docs/reference/README.md | 46 + .../adrs/2026-04-29-async-celery-bridge.md | 34 + .../2026-04-29-hybrid-glossary-retrieval.md | 43 + .../adrs/2026-04-29-jwt-memory-storage.md | 34 + docs/reference/guides/testing-strategy.md | 113 ++ docs/tasks/README.md | 78 + docs/video_accessibility_user_guide_v3.md | 1476 +++++++++++++++++ tests/README.md | 122 ++ 69 files changed, 4245 insertions(+) create mode 100644 .archive/source-docs-2026-04-29/README_cleanup.md create mode 100644 .archive/source-docs-2026-04-29/original/APACHE_DEPLOYMENT.md create mode 100644 .archive/source-docs-2026-04-29/original/DEPLOYMENT.md create mode 100644 .archive/source-docs-2026-04-29/original/DEPLOYMENT_OPTIONS.md create mode 100644 .archive/source-docs-2026-04-29/original/README.md create mode 100644 AGENTS.md delete mode 100644 backend/__pycache__/celery_worker.cpython-313.pyc delete mode 100644 backend/app/__pycache__/main.cpython-313.pyc delete mode 100644 backend/app/api/v1/__pycache__/routes_admin.cpython-313.pyc delete mode 100644 backend/app/api/v1/__pycache__/routes_auth.cpython-313.pyc delete mode 100644 backend/app/api/v1/__pycache__/routes_files.cpython-313.pyc delete mode 100644 backend/app/api/v1/__pycache__/routes_jobs.cpython-313.pyc delete mode 100644 backend/app/api/v1/__pycache__/routes_websockets.cpython-313.pyc delete mode 100644 backend/app/core/__pycache__/config.cpython-313.pyc delete mode 100644 backend/app/core/__pycache__/database.cpython-313.pyc delete mode 100644 backend/app/core/__pycache__/dependencies.cpython-313.pyc delete mode 100644 backend/app/core/__pycache__/logging.cpython-313.pyc delete mode 100644 backend/app/core/__pycache__/redis.cpython-313.pyc delete mode 100644 backend/app/core/__pycache__/secrets_config.cpython-313.pyc delete mode 100644 backend/app/core/__pycache__/security.cpython-313.pyc delete mode 100644 backend/app/lib/__pycache__/vtt.cpython-313.pyc delete mode 100644 backend/app/middleware/__pycache__/__init__.cpython-313.pyc delete mode 100644 backend/app/middleware/__pycache__/rate_limiting.cpython-313.pyc delete mode 100644 backend/app/middleware/__pycache__/validation.cpython-313.pyc delete mode 100644 backend/app/models/__pycache__/audit_log.cpython-313.pyc delete mode 100644 backend/app/models/__pycache__/job.cpython-313.pyc delete mode 100644 backend/app/models/__pycache__/user.cpython-313.pyc delete mode 100644 backend/app/schemas/__pycache__/auth.cpython-313.pyc delete mode 100644 backend/app/schemas/__pycache__/file.cpython-313.pyc delete mode 100644 backend/app/schemas/__pycache__/job.cpython-313.pyc delete mode 100644 backend/app/services/__pycache__/audit_logger.cpython-313.pyc delete mode 100644 backend/app/services/__pycache__/emailer.cpython-313.pyc delete mode 100644 backend/app/services/__pycache__/gcs.cpython-313.pyc delete mode 100644 backend/app/services/__pycache__/gemini.cpython-313.pyc delete mode 100644 backend/app/services/__pycache__/translate.cpython-313.pyc delete mode 100644 backend/app/services/__pycache__/tts.cpython-313.pyc delete mode 100644 backend/app/services/__pycache__/validation.cpython-313.pyc delete mode 100644 backend/app/services/__pycache__/websocket.cpython-313.pyc delete mode 100644 backend/app/services/__pycache__/websocket_publisher.cpython-313.pyc delete mode 100644 backend/app/tasks/__pycache__/__init__.cpython-313.pyc delete mode 100644 backend/app/tasks/__pycache__/ingest_and_ai.cpython-313.pyc delete mode 100644 backend/app/tasks/__pycache__/notify.cpython-313.pyc delete mode 100644 backend/app/tasks/__pycache__/translate_and_synthesize.cpython-313.pyc delete mode 100644 backend/app/tasks/__pycache__/watchers.cpython-313.pyc delete mode 100644 backend/app/telemetry/__pycache__/__init__.cpython-313.pyc delete mode 100644 backend/app/telemetry/__pycache__/metrics.cpython-313.pyc delete mode 100644 backend/app/telemetry/__pycache__/tracing.cpython-313.pyc create mode 100644 backend/uv.lock create mode 100644 docs/README.md create mode 100644 docs/documentation_standards.md create mode 100644 docs/principles.md create mode 100644 docs/project/api_spec.md create mode 100644 docs/project/architecture.md create mode 100644 docs/project/database_schema.md create mode 100644 docs/project/infrastructure.md create mode 100644 docs/project/requirements.md create mode 100644 docs/project/runbook.md create mode 100644 docs/project/tech_stack.md create mode 100644 docs/reference/README.md create mode 100644 docs/reference/adrs/2026-04-29-async-celery-bridge.md create mode 100644 docs/reference/adrs/2026-04-29-hybrid-glossary-retrieval.md create mode 100644 docs/reference/adrs/2026-04-29-jwt-memory-storage.md create mode 100644 docs/reference/guides/testing-strategy.md create mode 100644 docs/tasks/README.md create mode 100644 docs/video_accessibility_user_guide_v3.md create mode 100644 tests/README.md diff --git a/.archive/source-docs-2026-04-29/README_cleanup.md b/.archive/source-docs-2026-04-29/README_cleanup.md new file mode 100644 index 0000000..763b530 --- /dev/null +++ b/.archive/source-docs-2026-04-29/README_cleanup.md @@ -0,0 +1,25 @@ +# Source Documentation Archive β€” 2026-04-29 + +## What was archived + +Original non-canonical documentation files backed up before canonical structure was created. + +## Files archived + +| File | Migrated to | +|------|------------| +| `README.md` | Updated in place; canonical docs in `docs/` | +| `DEPLOYMENT.md` | `docs/project/runbook.md` + `docs/project/infrastructure.md` | +| `DEPLOYMENT_OPTIONS.md` | `docs/project/infrastructure.md` | +| `APACHE_DEPLOYMENT.md` | `docs/project/runbook.md` (Apache config section) | + +## Rollback + +To restore original files: copy from `original/` back to project root. + +``` +cp original/README.md ../../README.md +cp original/DEPLOYMENT.md ../../DEPLOYMENT.md +cp original/DEPLOYMENT_OPTIONS.md ../../DEPLOYMENT_OPTIONS.md +cp original/APACHE_DEPLOYMENT.md ../../APACHE_DEPLOYMENT.md +``` diff --git a/.archive/source-docs-2026-04-29/original/APACHE_DEPLOYMENT.md b/.archive/source-docs-2026-04-29/original/APACHE_DEPLOYMENT.md new file mode 100644 index 0000000..fc852a2 --- /dev/null +++ b/.archive/source-docs-2026-04-29/original/APACHE_DEPLOYMENT.md @@ -0,0 +1,236 @@ +# Apache Frontend + Docker Backend Deployment Guide + +## πŸ— Architecture Overview + +**Frontend**: Built React app served by your existing Apache webserver +**Backend**: Docker containers running FastAPI + workers + database + +``` +Apache Webserver (Frontend) β†’ Docker Backend Services + └── Built React App β”œβ”€β”€ FastAPI API (:8000) + β”œβ”€β”€ Celery Workers + β”œβ”€β”€ Change Stream Service + β”œβ”€β”€ MongoDB + └── Redis +``` + +## πŸš€ Deployment Steps + +### 1. **Deploy Backend Services** + +```bash +# 1. Create production environment file +cp .env.prod.example .env.prod +# Edit .env.prod with your production values + +# 2. Start backend services only +docker-compose -f docker-compose.prod.yml up -d + +# 3. Verify services are running +docker-compose -f docker-compose.prod.yml ps +``` + +**Running Services:** +- `accessible-video-api-prod` - FastAPI API (port 8000) +- `accessible-video-worker-prod` - Celery workers +- `accessible-video-mongo-prod` - MongoDB database +- `accessible-video-redis-prod` - Redis cache/queue + +### 2. **Build and Deploy Frontend to Apache** + +```bash +# 1. Configure frontend environment +cd frontend +cp .env.example .env.production.local + +# Edit .env.production.local: +# VITE_API_URL=https://your-api-domain.com:8000 +# VITE_SENTRY_DSN=your-sentry-dsn +# VITE_ENVIRONMENT=production + +# 2. Build production frontend +npm run build + +# 3. Deploy to Apache document root +sudo cp -r dist/* /var/www/html/your-app/ +# OR +sudo rsync -av --delete dist/ /var/www/html/your-app/ +``` + +### 3. **Configure Apache Virtual Host** + +Create `/etc/apache2/sites-available/your-app.conf`: + +```apache + + ServerName your-domain.com + ServerAlias www.your-domain.com + DocumentRoot /var/www/html/your-app + + # SSL Configuration + SSLEngine on + SSLCertificateFile /path/to/your/certificate.crt + SSLCertificateKeyFile /path/to/your/private.key + + # Security Headers + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-Content-Type-Options "nosniff" + Header always set X-XSS-Protection "1; mode=block" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" + + # Compression + + AddOutputFilterByType DEFLATE text/plain + AddOutputFilterByType DEFLATE text/html + AddOutputFilterByType DEFLATE text/xml + AddOutputFilterByType DEFLATE text/css + AddOutputFilterByType DEFLATE application/xml + AddOutputFilterByType DEFLATE application/xhtml+xml + AddOutputFilterByType DEFLATE application/rss+xml + AddOutputFilterByType DEFLATE application/javascript + AddOutputFilterByType DEFLATE application/x-javascript + + + # Caching for static assets + + ExpiresActive On + ExpiresDefault "access plus 1 year" + Header set Cache-Control "public, immutable" + + + # Don't cache HTML files + + ExpiresActive On + ExpiresDefault "access plus 0 seconds" + Header set Cache-Control "no-cache, no-store, must-revalidate" + + + # React Router support (handle client-side routing) + + Options -Indexes + AllowOverride All + Require all granted + + # Fallback to index.html for client-side routing + FallbackResource /index.html + + + # Optional: Proxy API requests (alternative to CORS) + # ProxyPreserveHost On + # ProxyPass /api/ http://your-docker-host:8000/api/ + # ProxyPassReverse /api/ http://your-docker-host:8000/api/ + + # Logs + ErrorLog ${APACHE_LOG_DIR}/your-app_error.log + CustomLog ${APACHE_LOG_DIR}/your-app_access.log combined + + +# HTTP to HTTPS redirect + + ServerName your-domain.com + ServerAlias www.your-domain.com + Redirect permanent / https://your-domain.com/ + +``` + +Enable the site: +```bash +sudo a2ensite your-app.conf +sudo systemctl reload apache2 +``` + +## βš™οΈ Configuration Files Updated + +### `docker-compose.prod.yml` +- βœ… Removed frontend and nginx services +- βœ… Added CORS_ORIGINS environment variable +- βœ… Backend services only (API, workers, database) + +### `.env.prod.example` +- βœ… Production environment template +- βœ… CORS configuration for Apache frontend +- βœ… All required variables documented + +## πŸ”§ CORS Configuration + +Since frontend and backend are on different domains, configure CORS in your backend: + +**In `.env.prod`:** +```bash +CORS_ORIGINS=https://your-domain.com,https://www.your-domain.com +``` + +**Backend automatically handles CORS** based on this environment variable. + +## πŸ“‹ Deployment Checklist + +### Backend Services +- [ ] Copy `.env.prod.example` to `.env.prod` +- [ ] Update all environment variables in `.env.prod` +- [ ] Run `docker-compose -f docker-compose.prod.yml up -d` +- [ ] Verify API accessible at `http://your-docker-host:8000/docs` +- [ ] Check logs: `docker-compose -f docker-compose.prod.yml logs -f` + +### Frontend Deployment +- [ ] Update `frontend/.env.production.local` with API URL +- [ ] Run `npm run build` in frontend directory +- [ ] Copy `dist/*` to Apache document root +- [ ] Configure Apache virtual host +- [ ] Enable site and reload Apache +- [ ] Test frontend loads and connects to API + +### Security & Performance +- [ ] SSL certificate configured +- [ ] Security headers enabled +- [ ] Gzip compression enabled +- [ ] Static file caching configured +- [ ] CORS origins properly set +- [ ] Firewall rules: only expose port 8000 for API + +## πŸ” Troubleshooting + +### Common Issues + +**CORS Errors:** +- Verify `CORS_ORIGINS` in `.env.prod` matches your domain +- Check browser dev tools for exact error + +**API Connection Failed:** +- Verify `VITE_API_URL` in frontend build +- Check backend API is accessible from frontend server +- Ensure port 8000 is open and reachable + +**React Router 404s:** +- Verify `FallbackResource /index.html` in Apache config +- Ensure `AllowOverride All` is set + +**File Upload Issues:** +- Check Apache `LimitRequestBody` directive +- Verify backend can write to GCS bucket + +### Monitoring Commands + +```bash +# Backend services status +docker-compose -f docker-compose.prod.yml ps + +# View logs +docker-compose -f docker-compose.prod.yml logs -f api +docker-compose -f docker-compose.prod.yml logs -f worker + +# Apache status +sudo systemctl status apache2 +sudo tail -f /var/log/apache2/your-app_error.log +``` + +## 🎯 Benefits of This Setup + +βœ… **Separation of Concerns** - Frontend and backend independently deployable +βœ… **Existing Infrastructure** - Uses your current Apache setup +βœ… **Scalability** - Backend can be moved to different hosts easily +βœ… **Caching** - Apache handles static file caching efficiently +βœ… **SSL Termination** - Apache handles HTTPS for frontend +βœ… **Monitoring** - Separate logs and monitoring for each tier + +Your backend services will run in Docker containers while the frontend integrates seamlessly with your existing Apache web server infrastructure. \ No newline at end of file diff --git a/.archive/source-docs-2026-04-29/original/DEPLOYMENT.md b/.archive/source-docs-2026-04-29/original/DEPLOYMENT.md new file mode 100644 index 0000000000000000000000000000000000000000..5d93f42cc6bb4d75a4e8b74abf4b1084bb9500e7 GIT binary patch literal 12156 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 +cd video_accessibility + +# 2. Copy and configure environment files +cp .env.prod.example .env.local +# Edit .env.local with your API keys and settings + +# 3. Set up frontend environment +cp frontend/.env.example frontend/.env.local +# The defaults should work for local development + +# 4. Ensure GCP credentials are in place +# Copy your GCP service account JSON to: ./secrets/gcp-credentials.json +``` + +#### Starting the Development Environment + +**Step 1: Start Backend Services (Docker)** +```bash +# Start API, Worker, MongoDB, and Redis in Docker +./scripts/run-local.sh + +# Services will be available at: +# - API: http://localhost:8003 +# - API Docs: http://localhost:8003/docs +# - MongoDB: mongodb://localhost:27017 +# - Redis: redis://localhost:6379 +``` + +**Step 2: Start Frontend (Vite Dev Server)** +```bash +# In a separate terminal +cd frontend +npm install # First time only +npm run dev + +# Frontend will be available at: +# - Application: http://localhost:6001/video-accessibility +``` + +#### Useful Commands +```bash +# View logs +docker compose logs -f api # API logs +docker compose logs -f worker # Worker logs +docker compose logs -f # All logs + +# Restart a service +docker compose restart api +docker compose restart worker + +# Rebuild and restart (after code changes) +./scripts/run-local.sh --rebuild + +# Stop all services +./scripts/run-local.sh --stop +# or +docker compose down +``` + +#### Test User Credentials (Local Development Only) + +For testing different user roles locally: + +``` +Admin: admin@example.com / admin +Production: production@example.com / production +Reviewer: reviewer@example.com / reviewer +Client: client@example.com / client123 +``` + +**Note**: These test users are only for local development. Production uses Microsoft authentication. + +### Alternative: Native Development (Without Docker) + +For development without Docker, you'll need to run each service manually: + +```bash +# Terminal 1: MongoDB +mongod --dbpath ./data/db + +# Terminal 2: Redis +redis-server + +# Terminal 3: Backend API +cd backend +poetry install +poetry run uvicorn app.main:app --reload --port 8000 + +# Terminal 4: Celery Worker +cd backend +poetry run celery -A app.tasks worker --loglevel=info + +# Terminal 5: Frontend +cd frontend +npm install +npm run dev +``` + +**Note**: The Docker approach is strongly recommended as it ensures consistency and simplifies setup. + +### Testing & Quality +```bash +# Backend tests + linting +cd backend +poetry run pytest +poetry run ruff check . +poetry run mypy . + +# Frontend tests + linting +cd frontend +npm run test +npm run test:e2e +npm run lint +npm run type-check +``` + +## πŸ“ Project Structure + +``` +video_accessibility/ # Root monorepo +β”œβ”€β”€ backend/ # FastAPI Python backend (12,198 LOC) +β”‚ β”œβ”€β”€ app/ +β”‚ β”‚ β”œβ”€β”€ api/v1/ # REST API endpoints +β”‚ β”‚ β”‚ β”œβ”€β”€ auth.py # JWT authentication +β”‚ β”‚ β”‚ β”œβ”€β”€ jobs.py # Job CRUD & workflow +β”‚ β”‚ β”‚ β”œβ”€β”€ admin.py # Admin operations +β”‚ β”‚ β”‚ └── files.py # File management +β”‚ β”‚ β”œβ”€β”€ core/ # Core configuration +β”‚ β”‚ β”œβ”€β”€ models/ # Database models +β”‚ β”‚ β”œβ”€β”€ schemas/ # Pydantic request/response schemas +β”‚ β”‚ β”œβ”€β”€ services/ # External service integrations +β”‚ β”‚ β”‚ β”œβ”€β”€ gemini.py # AI processing +β”‚ β”‚ β”‚ β”œβ”€β”€ gcs.py # Google Cloud Storage +β”‚ β”‚ β”‚ β”œβ”€β”€ translation.py # Multi-language support +β”‚ β”‚ β”‚ └── tts.py # Text-to-speech +β”‚ β”‚ β”œβ”€β”€ tasks/ # Celery background workers +β”‚ β”‚ β”œβ”€β”€ middleware/ # Request processing +β”‚ β”‚ └── telemetry/ # Observability +β”‚ β”œβ”€β”€ tests/ # Comprehensive test suite +β”‚ └── Dockerfile # Container configuration +β”œβ”€β”€ frontend/ # React TypeScript SPA (8,273 LOC) +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ routes/ # Page components +β”‚ β”‚ β”‚ β”œβ”€β”€ auth/ # Login system +β”‚ β”‚ β”‚ β”œβ”€β”€ jobs/ # Job management +β”‚ β”‚ β”‚ β”œβ”€β”€ qc/ # Quality control +β”‚ β”‚ β”‚ └── admin/ # Admin interface +β”‚ β”‚ β”œβ”€β”€ components/ # Reusable UI components +β”‚ β”‚ β”‚ β”œβ”€β”€ VideoWithCaptions.tsx # Advanced video player +β”‚ β”‚ β”‚ β”œβ”€β”€ VttEditor.tsx # Caption editing +β”‚ β”‚ β”‚ └── UploadDropzone.tsx # File upload +β”‚ β”‚ β”œβ”€β”€ lib/ # Utilities and API client +β”‚ β”‚ β”œβ”€β”€ hooks/ # Custom React hooks +β”‚ β”‚ └── types/ # TypeScript definitions +β”‚ β”œβ”€β”€ tests/ # Unit + E2E tests +β”‚ β”œβ”€β”€ .env.local # Local development config +β”‚ └── Dockerfile # Container configuration +β”œβ”€β”€ scripts/ +β”‚ β”œβ”€β”€ run-local.sh # Local development startup +β”‚ β”œβ”€β”€ deploy.sh # Production deployment +β”‚ β”œβ”€β”€ full-deploy.sh # Full production rebuild +β”‚ └── build-frontend.sh # Frontend build script +β”œβ”€β”€ docker-compose.yml # Base Docker configuration +β”œβ”€β”€ docker-compose.local.yml # Local development overrides +β”œβ”€β”€ docker-compose.prod.yml # Production overrides +β”œβ”€β”€ .env.local # Local environment variables +β”œβ”€β”€ .env.production # Production environment variables +β”œβ”€β”€ CLAUDE.md # Development guidelines +└── video_accessibility_development_plan.txt # Complete specification +``` + +## βš™οΈ Configuration + +### Environment Variables +**Backend** (`backend/.env`): +```bash +# Database +MONGODB_URL=mongodb://admin:password@localhost:27017/accessible_video +REDIS_URL=redis://localhost:6379/0 + +# Authentication +JWT_SECRET_KEY=your-jwt-secret +JWT_REFRESH_SECRET_KEY=your-refresh-secret + +# AI Services +GEMINI_API_KEY=your-gemini-key +ELEVENLABS_API_KEY=your-elevenlabs-key + +# Google Cloud +GCS_BUCKET_NAME=your-bucket-name +GOOGLE_CLOUD_PROJECT=your-project-id + +# Email +SENDGRID_API_KEY=your-sendgrid-key + +# Monitoring +SENTRY_DSN=your-sentry-dsn +``` + +**Frontend** (`frontend/.env`): +```bash +VITE_API_URL=http://localhost:8000 +VITE_SENTRY_DSN=your-sentry-dsn +VITE_ENVIRONMENT=development +``` + +### Google Cloud Setup +1. **Create GCP Project** with billing enabled +2. **Enable APIs**: + - Cloud Storage API + - Cloud Translation API + - Cloud Text-to-Speech API + - Vertex AI API (for Gemini) + - Secret Manager API +3. **Create Service Account** with roles: + - Storage Admin + - AI Platform Admin + - Secret Manager Admin +4. **Download JSON key** and set `GOOGLE_APPLICATION_CREDENTIALS` + +## 🚒 Deployment Options + +### Production Architecture (Google Cloud) +- **Frontend**: Cloud Storage + Cloud CDN (static hosting) +- **Backend API**: Cloud Run (serverless, auto-scaling) +- **Workers**: Cloud Run (Celery with Redis) +- **Database**: MongoDB Atlas (managed) +- **Queue**: Cloud Memorystore (Redis) +- **Storage**: Google Cloud Storage +- **Monitoring**: Cloud Monitoring + Sentry + +### Docker Production +```bash +# Build production images +docker-compose -f docker-compose.prod.yml up -d +``` + +## πŸ”’ Security Features + +### Implemented Security βœ… +- **JWT Authentication**: Access (15min) + refresh (7 days) token rotation +- **RBAC System**: CLIENT/REVIEWER/ADMIN roles with endpoint protection +- **Secure Storage**: HttpOnly cookies for refresh tokens +- **File Security**: Signed URLs with 24h expiry, no client access to raw files +- **Input Validation**: Comprehensive Pydantic validation on all endpoints +- **Audit Logging**: Complete trail of all reviewer actions and system events +- **CORS Protection**: Configured for production domains +- **Rate Limiting**: Request throttling and validation middleware + +## πŸ”§ API Documentation + +### Key Endpoints Implemented +``` +POST /api/v1/auth/login # Authentication +POST /api/v1/jobs # Create job with file upload +GET /api/v1/jobs # List jobs (filtered by role) +GET /api/v1/jobs/{id} # Job details with real-time status +POST /api/v1/jobs/{id}/actions/* # Workflow actions (approve/reject/complete) +GET /api/v1/jobs/{id}/vtt # VTT content retrieval +PATCH /api/v1/jobs/{id}/vtt # VTT editing and updates +GET /api/v1/jobs/{id}/downloads # Signed download URLs +WS /api/v1/ws/jobs/{id} # Real-time job status updates +``` + +**OpenAPI Documentation**: http://localhost:8000/docs + +## 🎯 Development Status + +### βœ… Completed (Production Ready) +- **User Management**: Full authentication, RBAC, password management +- **Job Pipeline**: Complete video processing workflow with state machine +- **Quality Control**: VTT editor, approval workflows, reviewer dashboards +- **Real-time Features**: WebSocket updates, live notifications +- **Multi-language**: Translation pipeline with cultural transcreation +- **File Management**: Secure uploads, downloads, asset validation +- **Admin Features**: User management, system monitoring, audit logs + +### ⚠️ Needs Attention (Minor) +- **Integration Tests**: Framework exists but needs completion +- **Email Templates**: Service implemented, templates may need customization +- **Performance Testing**: No load testing implemented yet +- **Documentation**: API docs complete, user guides could be enhanced + +### 🎯 Recommended Next Steps +1. **Complete integration test suite** for end-to-end validation +2. **Performance testing** with realistic video processing loads +3. **Production deployment** configuration and CI/CD pipeline +4. **User documentation** and training materials +5. **Monitoring dashboards** for production operations + +## πŸ“š Development Resources + +- **Complete Specification**: `video_accessibility_development_plan.txt` +- **Development Guidelines**: `CLAUDE.md` +- **API Documentation**: http://localhost:8000/docs (when running) +- **Test Coverage Reports**: `backend/htmlcov/` (after running tests) \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b7d9f69 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,97 @@ +# Accessible Video Processing Platform β€” Project Entry Point + + + +## What Is This Project + +AI-powered SaaS platform that generates legally-required accessibility assets from video files: closed captions, audio descriptions, SDH captions, and descriptive transcripts. Outputs are reviewed through a human QC workflow before client delivery. 50+ language translation and cultural transcreation are built in. + +**Client:** Oliver Internal +**Server:** optical-web-1 +**Status:** 85% production-ready + +--- + +## Quick Navigation + +| Need | Go to | +|------|-------| +| Architecture, data flow, state machine | [docs/project/architecture.md](docs/project/architecture.md) | +| Tech stack versions and config | [docs/project/tech_stack.md](docs/project/tech_stack.md) | +| API endpoint reference | [docs/project/api_spec.md](docs/project/api_spec.md) | +| Database collections and indexes | [docs/project/database_schema.md](docs/project/database_schema.md) | +| Infrastructure inventory | [docs/project/infrastructure.md](docs/project/infrastructure.md) | +| Runbook β€” deploy, restart, rollback | [docs/project/runbook.md](docs/project/runbook.md) | +| Functional requirements | [docs/project/requirements.md](docs/project/requirements.md) | +| Development principles | [docs/principles.md](docs/principles.md) | +| Reference β€” ADRs, guides, research | [docs/reference/README.md](docs/reference/README.md) | +| Task management | [docs/tasks/README.md](docs/tasks/README.md) | +| Test strategy and commands | [tests/README.md](tests/README.md) | +| Documentation hub | [docs/README.md](docs/README.md) | + +--- + +## Entry Points by Audience + +| Audience | Start here | +|----------|-----------| +| New developer | [docs/project/runbook.md](docs/project/runbook.md) β†’ local setup section | +| Reviewer / QC | [docs/project/requirements.md](docs/project/requirements.md) β†’ QC workflow section | +| DevOps | [docs/project/infrastructure.md](docs/project/infrastructure.md) + [docs/project/runbook.md](docs/project/runbook.md) | +| Security reviewer | [docs/project/architecture.md](docs/project/architecture.md) β†’ security section | +| AI agent | Read this file β†’ pick topic β†’ read `_index`-equivalent doc β†’ synthesize | + +--- + +## Core Pipeline (one-line summary per stage) + +| Stage | What happens | Key file | +|-------|-------------|---------| +| Upload | MP4 β†’ GCS + MongoDB job record | `routes_files.py` | +| Ingestion | Celery worker transcribes with Gemini 2.5 Pro | `tasks/ingest_and_ai.py` | +| AI Processing | VTT generated, validated, stored in GCS | `services/gemini.py` | +| QC Review | Reviewer edits VTT, approves or rejects | `services/language_qc.py` | +| Translation | Google Translate + transcreation per language | `tasks/translate_and_synthesize.py` | +| TTS | Per-cue audio synthesis (Google TTS / ElevenLabs) | `services/tts.py` | +| Final Review | PM approves deliverables | `routes_language_qc.py` | +| Delivery | Signed GCS URLs emailed to client | `services/emailer.py` | + +See full state machine (16 states) in [docs/project/architecture.md](docs/project/architecture.md#job-state-machine). + +--- + +## Development Commands + +| Action | Command | +|--------|---------| +| Start local (Docker + Vite) | `./scripts/run-local.sh` | +| Rebuild after code change | `./scripts/run-local.sh --rebuild` | +| Stop all local services | `./scripts/run-local.sh --stop` | +| Backend lint | `cd backend && ruff check .` | +| Backend type-check | `cd backend && mypy .` (run in Docker container) | +| Frontend lint | `cd frontend && npm run lint` | +| Frontend type-check | `cd frontend && npm run type-check` | +| Backend tests | `cd backend && poetry run pytest` | +| Frontend tests | `cd frontend && npm run test` | +| E2E tests | `cd frontend && npm run test:e2e` | + +--- + +## Key Constraints + +- **NO SSH to optical-web-1** without explicit user instruction β€” hard rule in CLAUDE.md +- **Access tokens in memory only** (not localStorage) β€” auth architecture constraint +- **Refresh tokens in HttpOnly cookies** β€” security requirement +- **Signed GCS URLs** expire in 24h β€” do not cache or store URLs +- **RBAC enforced server-side** β€” never trust client-supplied role claims +- **All reviewer actions emit audit log entries** β€” compliance requirement + +--- + +## Maintenance + +**Update triggers:** New route added, deployment target changes, key dependency version change, new team member onboarded. + +**Verification:** All links in Quick Navigation resolve. Entry commands are correct against current scripts/. + + diff --git a/CLAUDE.md b/CLAUDE.md index 9fb2153..3a0d087 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,8 @@ # Accessible Video Processing Platform - Development Guide + +@AGENTS.md + ## Project Overview This is a comprehensive video accessibility platform that automatically generates closed captions and audio descriptions using AI, with quality control workflows and multi-language support. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 5d93f42cc6bb4d75a4e8b74abf4b1084bb9500e7..96d60fd4929d6d6029d1e9ca1727aba8b3705b85 100644 GIT binary patch delta 280 zcmZ{fy$ZrG5XX1HceqhIw6h8>eFWV^Ato0Zo94nSahAm`SDI7T?g#}NMB|<|Ts&$sZ4iOHDq%T^g3(8P%bZ4SEZ`5(|2P{DoL#PmB zWJVW2VQZ;x#`XOV!l_Yt_>wkGv+#kV43Tl_FySAgUj)W=$&0wpVP6>!+E|1%Yiz?S VTskJ4D$gx2YD_65=Ck{%ES{n!Xq*55 delta 11 ScmeB7{1Z39db0U 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 diff --git a/backend/app/api/v1/__pycache__/routes_admin.cpython-313.pyc b/backend/app/api/v1/__pycache__/routes_admin.cpython-313.pyc deleted file mode 100644 index d5dbc3d9ea212773cc2548d6696959fd4ab7cfb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30262 zcmchA33OZ6dFFe-!@iINK!Q8Los`5y6tz%0DN+k5Q4*gZ%a&pqA|VPE39t{KWx;XE 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

y&F<0qk5z`Ie$$x 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 diff --git a/backend/app/api/v1/__pycache__/routes_websockets.cpython-313.pyc b/backend/app/api/v1/__pycache__/routes_websockets.cpython-313.pyc deleted file mode 100644 index acdb4bb951bb1622589bac97062f6e5642f50b75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9225 zcmdT}TWlLwdOn9ULk=n4B$2dDS<*~$iGx;RcEhPhc?f+9yUV~SMI zQ1)6u3U~LRHu?~!#bUka!@5@}z%HuCY#&-*qKorHEe+5aD1qAsE%MSzcG^vyKJ@>G z8j`wsz1#Gm1I(F!&iQZWpEGm*@1G|&n}xv9@Gn2V;Mh;dKjV#lXd`g*|l;zCvy&$vvRFp9H|(tWR)6i9&ru3nOmbRBc9!07=;$%swc@p&jP2_H z|L|bmF`=FW%UWpA+Cy2@n6;jCP8-n&R zW6&{H6LgMoV>JS32$qkPw~N6FFL{@Lmlo{Q?qa7>*YzBBHrp@;U0ZBOVB@8dNoM3)EOjX!1Jolo?zglyOGC+69Jpp8i5+zKrIyP|3+Pr_-h(zA zkG*ecYnFz=SR$^ZbyGz)sVS1s6y^Pk%%m^K$#gsl4#lJyMP@$BS2*8uKB|hZOsC_i zWF+A;s^-(L4vpfxm})L~t5z1s{?r(sr#D!eu$ zvn!z2f($Rp5hk;*II!j8)rt*aFc+SRBqNhx$5ZND__92%XyH{olX5z|1AqBTc?Uqp z%Xt-Gi-STE{>6l>*CLhLWGhA~pz^5aV%53?zsH*0o1IB!$RUw_a zC?{3ZhQm=F1GjM);rb&upIidP4Mc)B{QSmyM95o)4nv-A2;T1t5Q9tf$J{x=N|Jnt zZ`x211O7=`L(XH2v_pRjwn+&gA!xuEB#j~CC=y$aAQz&?sJ?!;$(ur^g4N@Mgvh9A zivqX}@@jSWmPic=aiiw#5^wRl%~OJzfzKv%br*m6XZS0I#Dc$v3D^&cLGuWF zA(Li_{06=7Iu@v)shLS@tH(&?%(13J2?LW&g*dgKAb%u4>>{p0C|WW%D-}9 zlh%58r^`xWQL9T7uR(XH45CojTTx4pCd*Ejz4BuIIze@hcCp#~Tif*PYE#^5^SQna z5WW02P$X7}!z8VHzHiDFIeng8+ywRQiTn+GLQ6JgeRF+|tvRH?9^|(il{PjNzjRsg zx3nZu(MaM#N=f^>j~zSK5}A&-Tx#96S)9@~hQ9aSy_^nf2(l zCj^>Xupdjxv>c63#G|<3TP!cj3Oa+MQ<1da@}5P4qdt9QTCVp?^|%@3^@Q_@Mg5UH z18#S~kZ0FK^7|GFH8(eJuxSH%c^sH#sVLZsCnrnC^NM5PNE&(Cj

>x<$iO-boyW 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 OX*mviHHt+bUjGBnn;+c( diff --git a/backend/app/core/__pycache__/config.cpython-313.pyc b/backend/app/core/__pycache__/config.cpython-313.pyc deleted file mode 100644 index 575521be4ba32a5d8f140807866c62e8d9f5760a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2706 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 diff --git a/backend/app/core/__pycache__/logging.cpython-313.pyc b/backend/app/core/__pycache__/logging.cpython-313.pyc deleted file mode 100644 index 3a514bfe3061b8d4f1ca65a32bf383d8acfe1f63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4399 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 Gz`p^+sx$)t diff --git a/backend/app/core/__pycache__/redis.cpython-313.pyc b/backend/app/core/__pycache__/redis.cpython-313.pyc deleted file mode 100644 index ac3564d8c5b8d9b55dd5885a0ae7d3ebd5a3ef3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2920 zcmb_eO>7%Q6rT0&+UuWeoHoDCpOfa#3AK^ZG)XB)N!kL5lp-(ffds8wd+lu5?3&qi zX>w_3E2TN0a;ZeQRN_`7q#_}4Luz{E5Gw&ktMpJ&4!OBa5ma#C&92vWqEI+6+L?JX z^WMDq`M$TaS5xCdkUksxYT*Y4p`w}88l5{R~QK{fu5nK8O&HR zGs8~1u{(_B-Q8&3GeX&hm}^GeNMJ)qaF4i3ecpL*HyW`U2>S$22w}g#K^YLd^R9TP zG*nfca0$K;y3Any6&ij^qc%fD0=vX^c9*OoLYbT{8HTK8bdY@+$q*GSlaX+;$^oKF zjlB;*xP#^qrUZmt0)^>b*eqt|*?6GZz?SDd0^JWHXmFr$MjRHvnG?(G{tl$6Mi&> z^1hBurguBkv2yO#+1oQ~Ge0vOxWO^mcZFo48<6*YgnK`R!f=gq(6m$BxnQRQYJqZa z8b)&t)=}JDrBV{(^Dtl#eh7@fjx%$-W2lfFTB*w0A&}`t6`Od_v8mL8 zdz`9hDCKo@RNK{8^c^YQvIUYtC#G%6vYOT~xjbA4g~_F{wkYPX zf(g(}uLXb7Hhs28X0zCfj}jw`noO4L#C!H?Qpys}nyiGehGF59q?6zl#*JjQU}-6$ zp}~HE`nV6eiF5rMD0a~b+F^XFGoQ|E1P8X5$WCKxp>Z(ZI9O;L&NmM4aKQo>%5$OZ zy5{Yc_U-0S(Tkc+6cO9xzBzsK{EGl;>A2_r(*ND`_vgPopKm^Mb9#$uD7sme-{G4J zd}p5TEbu*fzGsu~D{y@qT;C4I-}bNhH|j@z;!ZxJk+=Vkr~djqY*OK(KR`a*4wDbQD(ipg~%r<=wBbDh&=9_ z>Y^XfEmLjuqc)bvT{MjDiwT7FHz4o5_P@u30hpGg1e|phpZ{a@cm?k&k*LgU+Y>3v zUg4io<@FHFcumF1k^b3h#%A#ZFqyMB=bBw^ol-O%5NNZ_VFTb0h!^qTc9<0d%@OV* zg@sr)2_};L7xDoROTe^#BX-!f0NZG(*;uWjBp!r`jzQKxgkm3Tk8d&29Zy}s(+pn~ z=z8;f?fgi~81DpI{fg8+ogGCx~O$3PpPq@LCdJlNG zJ`tF#rylauWRQ7SdkW-rFEx3DS@(H}e1wA1bYscLVRav@$Wz9GmA&8tznaoR2XxA* z2U&+o&E9uaPV3;Qc*)zu=5G`%hal@DcvuJFZz%ZN^8U6BrtPIpAacF#=zgq|fIvgu z>xN?AzwgimMX2RdZ%anlSdg3xw%#6LeMwC&U`^F>I>f*M2xH+zIh9hRPZC%P!&@qs zjYRQraB^@1!SV(~`yGQ{2dx-no%{q>(9^12Sa660%^GXR(FAcTYo@0pSg{k{LzX5ZOVLoT9aagA3(O+wZBjFku8aIQ=3NMVg|h z$Ef{R)b<#4KJgy?{GCm2`-=N_#zl2LL!|H_zJ0T%W5xH}9i;f@eJnLp3{q747Y+AX Z@3y``AQm|nw0rLj-5vT3Y}&1j{td>SMtlGO diff --git a/backend/app/core/__pycache__/secrets_config.cpython-313.pyc b/backend/app/core/__pycache__/secrets_config.cpython-313.pyc deleted file mode 100644 index 804a1fd09876071c80ac08e8cfe5efb085d49b4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6474 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 Pqgyxrn)(5k;>`a8M&_AZ diff --git a/backend/app/lib/__pycache__/vtt.cpython-313.pyc b/backend/app/lib/__pycache__/vtt.cpython-313.pyc deleted file mode 100644 index 767c20829777e930f90c89f275c768d35a74e0cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10363 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 BVHp4b diff --git a/backend/app/middleware/__pycache__/__init__.cpython-313.pyc b/backend/app/middleware/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index b547000f3c69538dd4f895106d7284ff8108bb1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 523 zcmZ8dyGjE=6rFv?gkYhCg~hf($Ra3Mh#-iNjY>4Aq#7o(V=_AX;O-_GJ3qo7u=8^) zoyNjK?8N+lGh(vATO974d(Sz;nbzx7q-uNTX?SNK^sa|lnSaf6OEr(EivV?vuGuz1 z6U@*8E40D(^_$gpLKoc71J6eR>e{CUR0dANz27+IQ6$(c0p@3f-jD(FuTt<2NnY%C zT0Y4#!6_+tnlwMv2T;S3D`%u&M?B_5NtU%%=W@skCU{=RbqY+~V9+4+C64Dl4O6aO z5W%Bg)dh(eYdl{rvK+9KJ=hu?4Xy@H!AWCDdKgD3#aPx$Po50^ab&gBF8P{FguwV2 zO_dyyI5^K4QcNgidCvP>@M045Rg5f& z0u`N7n57$F%oCLZZe|l%*^Sd^B-kD-D?c4;zNX;IG7RGtZGNEM3+lb0@Y{B4=7R@y Gwfq5hs+)BH diff --git a/backend/app/middleware/__pycache__/rate_limiting.cpython-313.pyc b/backend/app/middleware/__pycache__/rate_limiting.cpython-313.pyc deleted file mode 100644 index 42e59135d14108793c24f1645c0a28d0006773ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10947 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 A2><{9 diff --git a/backend/app/models/__pycache__/audit_log.cpython-313.pyc b/backend/app/models/__pycache__/audit_log.cpython-313.pyc deleted file mode 100644 index b36f052b9fea66d5ef20766df32224bea2c18547..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6623 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 diff --git a/backend/app/models/__pycache__/user.cpython-313.pyc b/backend/app/models/__pycache__/user.cpython-313.pyc deleted file mode 100644 index fa0ffeb02c88c0b588e79a14b6d039a195c6fd37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2978 zcmZuz-D?|H7QZu^FO4Ki{!nAPiR~u#HnX(R?ow|Owxp?Jg;;Ks#S^Cs5e#E#Y)?9# z(cT%UbzzGjkgD`W3mZ0lEA**v{R{dx*a#Z+lF)~?P`?GNvafs2mE=aQF6cLB&OP_z z{LY8wIF-@~j8E!c?fxne@*}~e*xh!F^MUxlLDb+0cCMrD2P<#a%??Tkf>CU zsm$M{b)}$EmCN$FR*2JhL8p2lK@)`}O@dx2rQQ-vb(TDk=vYZErAo>3q%^iH^v--s zltG~BX>y*J+AJ~SU?DEU4RO#6*Xy89l;X>=zIL2zlb}tN#!s{pTssEZ^h>srT$=&y z_)FTUoe5*|2)8o?rd907iQGL0WGV00_6(6}cdC&a__oL5@A`fafkg#P?`%DA%F$ZI zkXU@h4xQUU#qpSy-?LrMj3|?Dx{g<2=@n-?pw2zpb7Av;!lWVsbO2}j1^7ycEXL4H!lTd)1+ zA5Yb}Lv`+1dZP8`&+E<9Gp;qCsxyb`OedXbYR~kkgY4^V{o;Xi@fcQ;GQwaCf^@t9 zn#ZI>s9+K*ngWfPB9+V-l}(8%rcBix#ZZ}s4sQgWbCiVzOeuRVILb8Y)LrL82QtR4 z?79Beq-bbNvn=1rtmhxwj08V*r0$im}qVP=?$B~p^0phYugN>@cHmz0u<<|WOPU&m)0B!x>l zNa~5CaVfDe4MwL1O_dU08%GOMhh~z7w*-F^X$DRh@1KzD>n2{(rBL@pcNM1u6Z608j~nQ z`7VpQA*5~O)*Ysose_FJ_-A^pifsnvArepW=`5T)Od`)GAP;g%*^vyHCGd)upgyWKQabf!3_Xiz?K@Qu!t_nRC za}vvxrV)5LE}--#!X*GEZ3TfxbNC)Z_!9y@e#HGL-kAgX748CjO1__8{PONM*~9tE zjg_X>nmyFcwx!FR#N@*}o$Pd5n(m}09u_;LfU85~`w`Hl^)7_^p$o-8f#did!0?5za7(f&V!1&1K<=GL5|`AC3@#lWG+t7{ zmY24vP<=;*W~O7d-ryB*gkhw3H>M3rhrWS29%Kwu(MXK+mS@HlG#!;5qqw++#t3+5 zh?Vdu=|y^M{Ne4>f&NZf?6HI^MzgdHGpAX?+Jvte1*tBnJ(fVHfiB+HalSyOgD%n6 z^%xRq62FA{I^G6Nm5xCh>a(`Vd&SW*Y+mHFy+jtL5*ahnBMIaAgIo8Cic`m_=KOKK zd5!UXuA<#25?62n0q+pL4FDuAEH?7ZE3I3HiE~&V^B15#K6(=S>|br^0=_}rw5}ZL zP+Awz7AK|8J~?3+w<|)k!1QAhwjqk2DNKp>tMGNf+XTgLQ7HH@@OxhqT6Mo-`|u}5 zHJIu7q@x7iLH(S-|9N^DfjhQ@66QQl0`A1=7N@=4`*XcUZ-E`Q;P4}W7coH)o|ALW zN%jZwho8yJ59E5ATz{b`g7L)c5_r6rAY*4*;TLxf-niC2`_93ccMen68+uoX2~*9m swb~^hbdyP8zInAv;L)8Egm^RR5_ohqNmy#lbO}7VV_g0fWuAcl2gEISkpKVy diff --git a/backend/app/schemas/__pycache__/auth.cpython-313.pyc b/backend/app/schemas/__pycache__/auth.cpython-313.pyc deleted file mode 100644 index aba36ac574c41ae20182b15d912f69152711c97c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3866 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 Qo2F^BFQ5GZ-Yz!(3;&hrD*ylh diff --git a/backend/app/schemas/__pycache__/job.cpython-313.pyc b/backend/app/schemas/__pycache__/job.cpython-313.pyc deleted file mode 100644 index f1dfa21a9f824a9de725f97417bda33354b14e1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5045 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 Z$Fa{G&bchx%Y7}ZF10iNfj-hR{|gqu0(k%c diff --git a/backend/app/services/__pycache__/audit_logger.cpython-313.pyc b/backend/app/services/__pycache__/audit_logger.cpython-313.pyc deleted file mode 100644 index ce004b299e7c5da915de15cb4e8e556e3f54a603..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14431 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 diff --git a/backend/app/services/__pycache__/tts.cpython-313.pyc b/backend/app/services/__pycache__/tts.cpython-313.pyc deleted file mode 100644 index 9d38586bd5cd409fbbd3e5c85b231f43da104742..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12244 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 diff --git a/backend/app/services/__pycache__/websocket.cpython-313.pyc b/backend/app/services/__pycache__/websocket.cpython-313.pyc deleted file mode 100644 index 9507a99436cded8f63322fff76cf1004f7953fdf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22251 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 f71$BNTSBMc^DC9EJ@CT$1=mJ}oqpzi{b z$fgs=dC8romfd)yw8>bLbjEU~ov8WJ^h@^Ct;SAf76LV3)>P{zX=eOKiOR%}eDs_P zEC5oBC8yKrC2{udeVlvlx#!+{9=lXoX<;B${p&xT&2})%Z}3G+6@|>wNtR*WWJE?} zhZ&N8vV^7YI--N8ZkQY52q)z0^h6J3+^}KDNQ|^hKWrM}2@iRLXdE^VS%`(^O~VyK zR$`@jez*angLnuxqG>)Ii=URu0z=xrtj~#;jgu ztgavE#6;@FX3;j*All(q1;1+eImXuXSwv?8b54JbC5?5Aml0hJj94>f?B@#onrLY) zl)A^7ilv@0cVJh(MLC52bz>f}ewz-*>eGo0h`Ybggb0PwHCReo#73Z@Y0TTF7d^CO z?Ii0HKEc)_@31N#4X313Y)bO6s{UkHJ`YdFpgfa^9C_wgGL=v}TtmS1c$W%V{%NE+@^5XiQWNR!$o8&k1&d*~v9C6RdB| z?6l>u6rYxeFp*BsEiI&y!Z?9rAQF~S0_<5Kzkh}FG@d}Ra3K~Jo|VSMWaPY*vIJ!) z!c#~HNYR)qMCd6bOeBf0kDdj>#Uwco_~FnueZCi_5A58JS*tkSwx+ojuX(Z{L;bN5i7o~BT z$~&op3ZlnaV>%UqwN=fudnirfs(~(%%1@Bw6fH0nRz|f?!J@`P(e%`G=$xEPsD?;9 zDNCyPz@>;ZjaR8^kO)Z<)m$W1wZJZ#3a3KSrHId762=t*s9JFQfxy5dDBa6wq&q;h z=4s4tPSpg$m5!%2YMD(cE0OguC)o}_Sf12SH|%jDcTdG4XTwt5AAuYGLacj#GLoJG zg+uO!I~*ll?!FLEnBH9faL3awY7P%XwRFYGnZ~`_{%s)M& zx{K?%Tv!S2EZk@E&q2L-nR#Sk3>DuU{>HH4*mBEw^U|%EJGFOs<@lKL!h|w5sZ744 zkQwErS*7YP=Ja1%)U7iNvmf!^`+V1Ze(Qa{?YF$`wTf3Ol-hl_58mFYxQ@>8#~v6N zN6Wmk9{=6VSHJ$o*R$@<`P$}r*P8j7rU#WKhvl;Ik%KYY^IeDU9L^nmF>~}qWjLg` z&dl-QMI%$^&AB@=?v9+hC*$s!x4LpxPsZw*uW6ibYFRWfbsHBMeVyg9^^vuN2nL6) z&gPt>J>zIsI=0_BmD?$1c8b}Z!K~xdWz)RFQ>gRpxOM8*VWs21?F+d>FJunAkUbR2 zI?h}+6=$^L*0Z;cCo~OE7@BCdW5q_=Y*YvDkX#&V@T?l~d0t zr$fqfXB1cX9zXu+Z!26&3_*(tx)IivH5?04!5D z3yKRA7gI^0OK@e^G@c175~zh-Nloy;TL4E|k%CMBFRm2{`m#MtP`97m%xXBGzh-fj zwZ>h7Nnl!;798VC+h|w+JM4(hsG2BOVoBAOkS>O#3$UNTP)JUzHZUK`-yI~K2&EIL zSUeO-PEE(96r5dHMiCGnjPKK{dRdB3sQNIOlvU&Ti}a~Sd@^ze&lS=K8I=!(BH?&E z6e?ISxQKqO>?pL50{~uT7VNb-d)r-mTh`ujS--&BbG+v+@41$m<69SXoEf&QJLl}m zIJ<82{cP7ycirk!)(zd>oLM)Nbq;@Qt$JzF5m$uMGxS??c>NEs3n64_5zSm z?a-!7cEgM6EQ+kZ$h(u+6zTc|Cv0m`_?FKoyIwJw*>{)`ACK%xgr_86mn9T}izFS# zyfqYhDIJa%YV4uVM2yI(cq}0$l2C05g`&wwC`9%H?)>mIA$q{ZBA~^j;08~FItLAo zK!YXtF?4ar7R({QT}5w%?=Zj8Z+WOQ8tQ*v#TvZixYlCX9@e(Xk{7C1?%^>#n7^=# z!O(?$OpKETkUskndKC;NqJu~1K7I}nqtX@8kU^fBg5QMa63R|tVT!vqSlk^)z_)M~ zcz%I>lN&KuVK?y1h`xN;+{><>5FQnw7%byEy8P1N$TE&xkkdTZ>Q_)%TDsHN9;JmY zUCsI%r?NGDIeVYN_hsyTpUGCJdc|JHzRuR_-!Rm%l+zzlT(Ba zNWgqxXwy8San=O)0|m$;*#_V{%%YAp^k0wsfq_T9DYim&Qa>I`L;=EqxQvs?HI&LF z+>UQD;9t^`VdH?1pDUb?Lc1_{L^w{8sI&p!EVw|zMFRIYDD(7a1%*bM zq$UCAwE5!|xk)!g7|&SCNPZfpV6<|+s*cAOf{a0<7qB57kuNigHpXD4vs$)EEl^2i z;Zw-1+@ycnJy340j}~1Gqej3+(`qNMcIqbBAY0ZFDOiyZVL}JzyDorBz9I$iW?7NI zUBUE}ODV|iRu-I;1%2*=m0VFF28Ho02(eUQt7UkgfRlBT&+l= zQ8X3Jb$!|WhH|qaiF{DM+|>LN99YFAk>|iDs?#(D8nj&L8E7o!wOq;24hULV=~=p) zMYCTgTAsG`SEoa)5UqZ$hZQURI*ng&0xerd;r~V=K%Pd|YI82EzOA(S5*y@1JIbV3 zg>W@TfmmH8(P;gxNFYy+UQTpsGfNmoHGyOTT`L;HD|$-B8nJepPRAsS;BH#~c$l4q- zH9>T~l{s?7hG6A(ATkVRk=Pithz%X^_JlLAsU&}+sN~TEed}@W(UaK&WBds|fupay zPeZX!^Oy9of7SIWJKgr==_`k;NqNu$GY}{>wxC^X@#~)UBo)1W!=H8TJ|$&ODfj!d z83vg@WA=Ky*Tl8GI5^k+Hl>8Soy~<_fOxzX8pts*HcD=`ZbcmvjWHfQw~r^{*=B za4L0y(rV zgQq_%bVk#J>hbHre+_PFpQV?!MvFkW7)zZM4$;t{u*u(#SS)>hC_zUtc-2pxJSj|o z!&bKRuSUW&_ymD~&`XgH%j^a*6($(06bh7q`2p@hG(e_{21wB}ta1<>7@5_B-(Hxl z1^0Ris?yLrmJpC`w0cwnjTFpUgW$4&h+`DJ{IgXha|WXc;0o4J?><3C)vS$37h=*y z|Jj%f(bO4;Th7QSY0Af`I&k`nKiUdr{%M=mJ$KbBbg=;M>Szk zHBDl(hDmv#yIWAz8IX!qhP= zV?0)^38$m6WGE`3@12%o@$80UF>0O(M0zP511~QyU5ZD+-8BKImva#O^GBXNuoqUw zV(IA_p#T1!o*rSthFwCTpdSk?r>HQ{h)p7uU+N1n8JrCQkW8Jv{@Hdd+%W;39Q5)E zt5aCjEhW0OsrZp*3zmeVn1_Hd`rD%r{*Zt*Ra!e5f(92(#7TH7?2t zb*9f&LQx6PtOWu*)9IA#mlFA<-6EU~LujhtxLPS#C~6S7g6K_jhDzCqeei;^0P^92 z)uF+j+3m|B7$syyTE0YPXV)E}t`0P;SCQ;|WCLSnAeI8b7^PZW#2cD``ZSS167{JW zFu&1kgE#?v3jpztscE1Rj70dR0f4WAU?h?>BS7;uAIYegN+u?g(Q%YY)uO#b$5lNH zC_}&k`~{&TY+Z^7y?H7J9}Q?>FIrWBrDllM9S|{Qry4GV<7tV;LdaUIvz)>L>I}ok z!gL%$F7WLE?1k|(#tKy)w$n5o3aVoS{8`bH@R}rHGIJ2i^ytYVXh5sRLI?eLJUOnq zqRER1`e6cmlz`o_p;|e3By>FR%)SGnh}1Yk_>n_C0-d0>K6 zQj(7!7}*cqLnrppXoIRtg6A@w0C&iFf_4r;e~W64$;o^y##c@KQ>w8LO3?uiO~6C+ zwNY77tMWTJgrtY2!>O~dQZP?Scp;0W~|a4)L{`c!RG z`Ms_cmv&3xyrH|LkSAzO$#~s@3vzkNyZM4pMG#D4!%ook(2E734lTyA`?m4cC8g@{ zoc_ooBV!Z(fzJtj8KEy*+5hsvd9(fde6DJ32L8>hFYjH{kLnEWkLw!VGX21mtLx0v zb>3*ZS$m^7TemIe*ml|UANJY>tK+q!uO7YDcinjHlG3qLsoync-92x0EXMU17lEL| z_iT$XJyX+ob@=t+>!ur?tZVCKGX@-J+@}5d%=Pn%YttOx3y}?b^=p%_PUdVv#wN^H zI<6eN{0z|9+?n%i%y>5DJX8p^a;=Z_TV(f8%s!&6eDn-I+DJZ@2w|&mA7k93E8;jVWU< zD0^O1&WvZ*L=?X6x0N-y%Fer$oi~lQ(swHl+^L_hYkAA|1KVX>23K>=*_Ls(<-Oj@ zy6su#j?4S!tyMW|TgKXUJvL|US=66qE89MpGO_ zIyB$z2mW+>fIr>_;7?60@W<@{{!}}FKh=&$t&F$-=ILz9ZkXm39;WWDe~gCtnl*}N zB?Ef<5yhW zKwTG7*XmHJ1PDL2_Cq7Z)1Pr|rme1>Q(Qi1)QXKfjw_Z$>uOUjuzeuYK9FnQlWE^G zzj0uG!{(b`%WT;Hao_%V|5o7sI?p4XS=XyAN3|K2qxSLT*p=zpmFwD{>Ds@1Iou9h zjv8cU4KfqJqaLQZbKcdk;A+abIy0`$oXeka`R6?y^Ua;t&MUq%N@Mt;*#IqVOtt%J z#p@M{=d|+t8Rc|XX&j%ciU38`&a37(%-4MPs#^ar$T7Bq>|%l|?$(D8sc;TFbifXI z*orv;!~k-Qd)S9YSkH%6rYXRF_K;`1XV`}>VAMkkFzSI7$oursLAD(I=LP)tDi3_| zXg|kTYnB*?!Bz}6?!CSFcIO@MofCKVDK*FE_!Fz20UuY@ek}AZtZl#k(oZ%ov~74h z^y5&rZF8<|Po`~8wrwwnu%{N*a_u_pWT-U3$-tR$yIXO)Tg{dS2o@U|v+aw;HX!1Q z#jfI<|0qApf?t2o5FB9Nuh<#b#@#XRC{?b#A`7Laq-uTO{d#m6@;n_f=Q85qV zKpAT-jG9@?0|l9NNM~jTi}~FM@9HQtu+Z`@bOTsHp^d_7BbL94+lcul1617ea?tc%ncZX3a1yRkw~dDSstoIpTsxexAY!>d;q{xZlD(&El^oZU~n^UW?*AYw zvWQO9QOF@Iw$lZ57)~v@Nx_))f-%dD@*3jR07;+INCcyj|2YUiSLsEE3rr$b|D=_|S>%L?<9;LQN6paY@L{qcj zG$aGU2wv?pL9op@F)=kQO{#`GPQwIfR4GDr9Ml+96NGCMFse^auEIO{Ap#sLuStLj z%b*&OS028F{v3dIk#X3`XP{&ej0=y4FQ7)_#Hvnr62Uje$f30 z)r_shkA*-3qiSqk#k7~oskKa%QUapH5DW|vTqA;dT0SaBwRfs?a=6SGP%(7Y?>4{Ed?k9lIm2#T x)EQaxb#aM-#}dcrHqnAb$2lGAzOnWZgU`k14Ls|-v38Mx$IZ6i34)(Eeu5yy4_TBgN;Vaek|==`N#O{IB4t5G;6OnH5A@xE z5|O6kiDweIGpVJSDz@7nVP-m0I!$W*tNwI`&PU56p2>lr1NuUx>NLrWew7k6v21sm zzCGXokY*w^Z6`Ot?%UnByKmpV_xA0(+bJ$KB6v=I^Y5>Iy%C{*#|7mn$^tu20JwuF zL=nS?!?|jh@DdzBVpu(_@~Szt3~PopUM;7UVePQaTf`N~ux?oIHE;%iizxlD(QD#N zGHe(&dyBbZ88!}EyjITYwQ)ANY#J`{mU5*sY#z3I%eXRtiz&-+x!1utNOY;V0bQy% zOk}9!oKDn$C~Fm>Y?ri$)!Ew0OAcR81};niv*c1GRZ7{ZvR(~UeyO@gMLDX_HO=8H z8doz*cq$&^(x``!^dVYcgvdOD;jx)(Oi(xz0$4La^K+1EeT={@0Z`h{FR{ThV`pPR zj2nzb7*=?wf5qpZ!bog(kYivT$t0KigF+<6 z0&+1kPe-Bwp5YcFK}S#;uRXOXrQ`IoMIOCm8;uE(@KR<; zc-0T>Fi4_NUN0#?1sRTUWKdo*$ztS4kj5+EgV{a8QzI-@R`~Dy29i5y8q&<;Q>ara zAa6j*ez58SOickw7HmL%wIT~7s)LeGk>3JjKl14diut}oH}b1a5HAwS7|`GJMNEYn z5;dWEJjWlz38PokE)*u-BSt+ssffms5s66_mbo5a7C|yV0%8l21>~ZzT*}4SK%5m| z%Y(7`g(w3OVZe3r0FpKsjq!{}BWZXh8kRIPH_J=9x$APS!6WjRKO{WTWS1YLhVPn> z1h3Lev@-}|u^8z(5evrWVSc`Afs0{T=DQXnAtn}}gF%MpBQud`L|E#Yp@VY_8|nfv z>cSh$cgfqj)VZ)E=>vfv9gPM8QsrL3=v11@fehXU$?Is#>PTCg*R9PdtNW&AOK(l< z>(=#k?+6?ErfrqlP`PQZNZZ?z_O_Lt_xrxncdtim_pZK}Z1<+@!w<~1x4dt9zcKRk z=7|jO8IKwNJ+i+JeNb20->sH(*J3k)NGMn+M8$yf2K;x%0lMsHMC5LP%Nhy5$UPL{ zCoJfYN)ZDRO>33LX`KS?CqzmQR^?X}V9MrI>6gjzfUH^;QSxBmeN=&bFDpjQ;MAn5*`gb2aTd*P}gi-2fhzOv4$Q zdHf~H&d)mvIJ@6T*evUA#e`!wXQ`G6$1xZzR0S5^orTK)Zk=%Wz>C@YC=!SJ(7Wx; ze*5W>XKgFA81i10^p^|~qd%TLq>N9s%arBp1VaEn0BhGi6QkF5^J9phffpcEd5&0~ znozE*)0u2M%k)z3Q)b*xhb zep1#Cku@JKHW{y1kIbNvV=wIH7TpZIvOhykUY5D?ir>ClPUKy)_uI$xurHA3_oWL_ zedh?aY#JYS^5b{(5Y%zFAnW~QU$n|>xxZX73v~B0PpmJ=^z`qQTg4(g+m9<)?|$da zfIR---tn2N#mTP|F0{|^85N84k~&OyP9%KB@yG%bjj#-P=qQMH1u)10Fo&zL5 z7g<=qRLD2p>~dfvIl2LJCrU?Ru{n$oUr;Db7;^L_in82Dxb`P5hba}s;=)2);3Zp- zUXa5jfki{c9uWd)g=*K5_;$a!IR8|D2et{5ETN}0t;sumf;|BL}r3Qq~A9p z&!FIw3Dr?jGD4InJGN9#L&zx>2r+z+i^%ihriAC&WA>jQ&reKbMw?$agel!`G-!Ix z(aw$^B2P(LImjhxg@_PkJZj0xF~P_Jj-LfGEe3qUms=%EF6afqH^(PhsJI-s2*;z* zC6b3e$_7LDiIlrzE+yP2aUcy4+pI)%^e3ssc3-~1cqZJgS?!;JU`20cY`Efm$fc0QU3(B zJdPMWl|`x{ygtUP4y{DqkG&U5bsqnD<8i3?R1Q+V;c6ff6_j$=$|g3yMYUuo&S0MI zGQVV&or#M6C{T!Zeowzr4i4a~$L*e-Au+|O`QIZ`-?TqCI* zOM>z;03;!MBC2_T9UPTe~qmYvwppM0W2rA>c(I^A?6*>ogT(tA_4S#1BS z)9EsI5`M+4H&xr3D=I_vgPOW;4ZS;*t~rpbIk3XqJ--r2)x45+ymE8sm-ebHo8xWs zZSyJFsCJ+_X8i7d5EZ{?^EwBi|U^2353G+`5splFP#>YZvZhxouf4 zTMjQziXG$Pxk>RNojf-qjt9lymqpi~ZP>no$6yP_h-gqHxok>058OPnWiEYd_|0Ll z>cGmymEpTftBzH@=sdGw_HOGC+3>9!@7_qex|6Q%yUq71#e;(>*NJrHiJRVE+H1Ep z>XOP$x99I}eEUY)eI)5VaeK+PCL!&n zEM@P65m!c%WN(`6Pm=wsCqHaWdtXg@Ulq?>7B2_Hfh*!nC`B@v@j_o1Z(u}p_a&Xj z|IY{f1lZeft8cUGmE|KFWd|OeBv4!L_6UJ$8*ZNcjkRW5RbDbiY&9KBHw~>f4Q;l* zaQCHT>#@xS&x$+Q@WQ71@S`G4v*Qz_aXD@c{nm_}t}SQXw@mMv#Fmk@DRDB8JbOhP zp+)+dIQN=(jT4*rkDS7<0LSvKMf50JIVp(l_(#sgCsnAV;%(DylUO$+1|#CkHL+%H z!xqI7AlDA89$lMF4xSeWE{GRi-MCCY%J^>J!hM-IqxuAA<0`1c8GOxv9lD(i3S6u#XOb=X04qVt6xR~-xrCO#o8@e{@JdX_8%F;*0=1R-WGuu_j zUjBB+?T&P5Te7rmr8nJvJlTFc)!v^f9Y~u8MDxI}6iqrO`X}XF%=y{lHq_!Hp8l%3X$KKlAQc8ri2hf_2@tX? zB0VEs35xDe+R3atna{V?81b{mmcpJOdP+~9NKK2rOX8)=V&IB+nHJqMX=iZV8I*g5 z{#QMnATCdzIFPe`2N|${k&R%F$nINbw#usB9=SatHV&?yT0OG%!dhf)TCBggQ8oqY z>8xEgq$)fsm+vj5I!=otm&7ZP^p#lhN-TBdwbaOKIoi*zh1VuwYtIv>@D8^QVZo?8 zjXiuwVV_+dFp;aC)oRgmHdQw&I!8a>wqr^@dwc{H4-!wG=#j1R333}8Th8it`u|3k zv2|cY#p=F$i}$Xs&a73gnZ(M84YPk+XDyy0HXY8lXK&A@9j!@6>t=b?JEv}qJ#f}N zXzkl<@7d}&{C?m&fmBCdy5n@R<8-RS3!2kX2S-Btq4&GK)0JvF_Nch1zG_=-D0l2= z%@w7eAahCSW1Kt%wJ58ForPJlwor8S+&zEy^u4od2iEGu+R3!-V$yaIZw=sl_E@iL zv54k|CpJVjZZw3GV@#?coN|RlbIot8uADXYpOg_kyY46DdJNZUaeYfEPJ3OH8U55rPzKFUD?I>zs3M^LLvK32=C_C$!dzwt{=lVDMAZ1 zDILa183-UWWj6XP>Xf73#w`e zu1Yr%7u>pZvlhc`1T>`EjgwW{^x^8sa_y!~3pJbNTBzBq>I3*=r)H`IeO!Zi@Nq{C z)JO&`3@TaFP%o8~PBp5fMm4m)2Om}$@OLEm)(78_7y|+DJL6Fdn*)K@;&e3IVhses z5snvdq>+t5b5Q_$lz{+O51tek!HMj1l?MVi<`)UVolM{&GjW080qg640Cpa*NSu@^ zi5&p>>iaeHv8Lm(N~f*dw#})u)yu(85YD;3R8S`#V(0fEzDwgTZ)V^E04a_#n&#;n zvUeC^c*!1O!gM^Ebr&JJ7NwVDmw4BADfmu?hm(T`cMyDNrb@IA%AagyKBT(N!6!Ei zC*Oqj2^@^dEApuF9(Uo@UMiLEv`c}ktBt?h;l2seeF%RZUveO}p+{PSygE-Uhd*TC zWZzyh!^han)kT;2gp9*4W~Gu$p)(ih;yLhMkz}98U%7S4Y3A#sNah+PnSwEn>CD8Z zGIuJoT!SxMa7*IwXEBmZzGv{AIlsK%x?;-)>G)3Bg$GAnz6qIg)o^E$FH0PLx6Ija zauX*Pae}uP?(lf!vUK3(%S_AcD2bc(FK5K9kNY+h@t*K8NWfSU#4nKLKC1mSs=SXX z?xV*0=!IXShWn`bU(wO~=l~S&Bj>*v?6*#*47IQ8p6KE#qWYHb1U`cxs<(k>D)ZM} nUv=FIiS-BXy(XTYOcEF2D<{IROzj}ZcGO5UMLfbaGC}_hUHju2 diff --git a/backend/app/tasks/__pycache__/translate_and_synthesize.cpython-313.pyc b/backend/app/tasks/__pycache__/translate_and_synthesize.cpython-313.pyc deleted file mode 100644 index b1f447ea4bbf5f89ef59e7baeecc127bd2513b70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17197 zcmch8Yjhjenb-`TgBKqFNP_PX#P?GaMTw#wCPRZo;yNOe~&jxGNlTSW@(E0 zHpNjKJw%CkRSnUDv`Dj9Ru8EL)uNh|=^>4%A@#JP7Ot8h-Jo97lXu!7X3!uSpseHc zL&iaqXd-21$UJBfEu?H1${EZRb4l4alsA|!<_}s$E4eof*#_;Ros`W(1%riRAt_si ziUu8`gOqcIiU&)?5-8_#c|)axWnvjiU9mW+E9D(9225fFw~x!eQps82Z-c)b{smXq zLq@K!gu1S|PK*0ECszbzT6A71bN46PW#>+cb8y8+X@qk~#g$;Yj#LG)D)qD!pNiF_ zud*vOS1PZl52>_J>yU;kC+}*>y9(&P@=DDi9mkTVb<=eH{tpq_@T;`UOnSpY*gq%K z)3VwXxC0l>h&OZtu2xs*PQZ8O{JCH_DE7|!g+Ta26*R+EAt4<02c|<%%{VQDJ+r~- zX+eZL!(ecN3wy(nP`yevP5VNgkRaal`vf^ZGCv#iPI_*J!=7-^1FxWZ&a^P+5BQZz z@FYJhdIO*YN5bs-02R1WQ#=oR-rKsRCL~&oM*lK}|pht4PlohO~pa0Ipyy zXB^TG>H`eBSAQn#uU(aa&g-2NPw_O*xbt=(2e(d2Sv$OnSMwUqd{h@O>@g+_uX9_U zgtenzt=s-&9e2@_&vu8#<={R#sty?Uz$QOytVVb7lkhn!Z*Z4B`CJ)!2QFMl0TaJ_ zds~vGPYK1m2flXR1goHcH$z_7uHA-xc@mTCT)}eBnJ*$M&4DYeU6X-2t=mV9S11tm zl=nQ0)RKkcKIL8J$`z+zS+>I>>oh-*vq$R^KF3|1fu@v{k!)WlmB5n&7MEpOm%L8V zQ`8A{B{fCYmqzD}r-j*hL1d>Q0kWmpaFCr4fmQgtp)d=3mQC(oHZqUQD#YIOd)bS^ z1Q+z(5W+@21dotYv2J0~A7XukQ?XM)k?keCfV~wIZvcE?8e!pf*cd^)w_f?yU$Cx` zk-l>mM$QZmup{T$9`||Isa_X1(uJJ@<2VsDG^P4J5j8iFr-Xf+h#H&N6bCsW;?f!x z8OTKgWWa=S2NfpB6O={d8j)tmlHE=OBVin+PZVH4?2g(5bsToRPAovEg^SCkXNO-Eba|Bp5 zBLsZGNnq)6-lPB}!2?WjJ{SPvX5eola1!~+B>4Hr#H>Fw1MSJUX1vhbY)G~uA(OM` zNzGo=_T;m!NR2Xt3rRr2WHubOnVl8_g6N$EoXglyWFq7f{S$&1B9qb_niqWjDZdZi znzP?VIu)?L{;NM@xromPjD9LIJ9{Uyz@VpX%PUR&PgGrOq&*GbyMM{{d;POX#4^is z_qGw77cwQk#&*yn3&)yInDF{;bg}g|S(iL8WsN^D6_m9iu3XvV3yMMu%+!>BTErtr zwx$lxP|Gc0B1G1^h+Iz8;9FfJ?1QwG6 z!GeK75WKhHNOpj1PU4u{oU$GkS7bKarW6j!v>>*^m_oUbvD=e;y%qMj7;2sK`)0ht zY>N+0_?!OLQ$b&34uq0WE1coD(nGB`{gXn_<0bR#pYYH6!*^Pd=0aey6(oYzFi19` z){KPV4NQ6f_wbAm@<)Z1`8#q^dTO_?TM?HM?z_-mgQW31_1H*hO@BW0&xa)IkrmzY z?Ug%gg=>s-;fnO^lyqfUnto0a??}%@rMxe$YyMz_alIPuzNKL7yJc@s*1!XQV!_G~t)L*QM(>CHt*)<~DR-C`d3R zF{WglDNENLUc0$ABaL2?#yrxcYm(jj4m0ub2PXR?N)0621TvXlG)qN;YlqkBB=?BK zk4hsKrGiWA%-H*kBmMrsa&e-qFV@xP@S$zft{WnM>PWqy9yW7H)jmVjb?h7vTRW05iRxVo^1r1L|?OOD3LHj$Rve+6G6h zrUJyTjF@!>>dyegoAkHU!&);HQn6GOWdcaNTA&$f*a*jUN+6*qIIjO0z|ivAfclb) zQ#C{XU6&}ztEVV9IO%}arA@ZvRUlq!n&G5r24Qs^c>qanol?qNL50w8Dn;zUqbHyn z$1{o~x1xF}58%paK{QlhO%PgiJSb7Q6vyydILdX(ctJohD^1)vN-1*%F*IG5PyujE zQh}k&Fn(n`=e`U5k&$IYfg@@V2Mp1Aw-6TZu-=R~aLXT_VTIdp7{O-uduLhP(ZQ*y zVG(FgB@bStW|VP-!@}Hrn2i>&Q-oJHu_8jlW62+2qw1D}Q&DvYI4}b|*Bb!60mm!r zpgb9zll75+A32`551;C9dBp&ph>(6TbbIV`1 ztmdz};s^U<_Wp&wO|$iD!!HisyR-#Z}p3ko0mo}O8h0MbZp&x zdDB|_Qu8;O7qxL~O~Q=7Pq*@+>)(fl0sAlK_jJ(TGB&EdN7w6KQ8%i7poa&q(v7O0 z7<&$C|C6z&S^IN*^qS693gy@L(LGhn>rO3}YjyDA^=2(peEpyr$|E3VX(s%ENnd&+ zK_4i8!N#K=9fYS(p|_zNt;#I!ioG;1qT^+^*lQDxBSs`;n1<3{K$Rt9Bi1_-Z1x@a z@v15cSO+pFl7Mx5A2zl+3ywv3m$`yAikhi*9M4vgc(sDVcG-UqIJ)utCn4;uCGG)L z&#QOSBDc>B$AG463TXC#W#l!xX@|ROB?8460%{kqowPP$icT0W_~(ULe?VXlcCjNV zTLef)Yz=$vLOVM^ioFpVN| ztufLKRNQ*y<>%SnzO#MqF?Pho4YAya%RSO}stX0hl>KA7bmBqbB`uvsY6*~UewTHn zwFxhXnJHG)B1Qt4x+qzp1f1Pp@A-2V&h|l%ATVdi#i%x+?h&cPHXyuax9NACISUPx zIwA{oB4`1?ifK8L4d0m;Kz4reg;$&g=9^TQ(yyP8jxB`!icWwlo`g(R!(ik}4;h;0 zv&cg@x!6MLhr*()K`dm(;|X}@1dm77VCO`}6c0e9dabMp3A0l~sD%T7$fJZmC*@GI z1`#nd$(zeRt6MTF;uoN{2mYZCAp_yioE8q<5;rEfFH3pj>zXTD8k05$MHV9YomiP# zxg^>9*O`IMe8)?r-zZ%#2jMDSekz{dx1dYc>sy^#g?hv5%=wMnf(6URnRkeJrCFaF&w!d&*QF{?LLrOdX?C6kOHa%~Ondr>0GSWvERr|_ zd27tP6=-1JXr7dY zZZwVTNbZsy58$@z7D(A`+bFV&aSlw2C=uI~exPj6HtB)vPrH#@?nug^!&1|_ESm2& z{sPiNVTP(8@9#eQD46a@$=S~uJu=Q6X}8gjS1Fa;Xnom{GTIDR)S}`XPh0zs$Ac^8 zN?OzhXs)y+t*`7{!>IpoWjM=RIoW@pEpZiCa@X%b9P3nO?cjzd-@^>D?P_kHvYrAu zfad%sgQg#EQu=h`d9)+BVa4rC8LcV6e2&)oH1{%q3!r@`bl`5OM{^D^aQ~tAML_5M zgw9oj&c^h8HMuu^#_lXTK&7c^=8d2i%M>+NJW|Y+6I~X>@pEtn?6D7OMvAwKeHGM5 zF>g8s*n2_V;fr_&UwlZ-)q*370Ry)(EQ+^qbzJ>X)y{Pj$l-Ii27-Amlp0AXk829# zpFc!kskmkW%L=fI!PVA6?rd3*t)BroH>-~AGwS4Vt%3Y;RPIRPbICquag^KvFyI4K zFg<~_J*!P#R-3jz(58U5^96i9cd$jxn@5UKJluoY?K`P`Zj(w{DLy~%IZg(&E!&Ov z@aCuGCtPQX`g3ugr^Mu`xleLe9K{u+oTgroaL1of27AfOt2$ELedmn+Kk@T!rH9OQ zH<~^*ML#b;e@Z-_j-une%4qoPllv+A;59^6=xSGS$Bt^)y;k~h+(X$?~!K_D|8hOsLuTtXsHW8IiuDREGJ3c z57x)k9r%9rDeyf(xX=^${;^UDcanQ%xA8pf+I9CSw8&hQLxAgUQ9msl&efs$?S=?k@yMLKQ})spr68vRs%LTmDcd_7Ib`cUJ?&PD1yTS zx6z-nE^chNm+j|=dq>WkAI=yD_E3V;s(EBTf%MJqv%T9Kf9HaM>ENlJOg$7;wqAjH z${WQ|2)GI0+#+s0FmWh4zv4Xo3VSvfyn%+*$UHF^Os2ds>6)?{T&Tkl)V0t&FbGD4 zXc71#!%!mvqXvU4xO2T|Hr*H1kk`h{YgtP?tx+=%HeZO?m_z`PQUnHujD7+E?G)R( zjjGtXsHt9Y+JdP8bplz>4-XBWzc?I0E%etfFaG1o*OKTokp2LXokkkPC0OT+h$2iK zI2H-m8~*uu7;|z^fCs_)APZh+00Q1S5xh{TwoNS9eS8A=#DGS`gsqqmVA|_d1T(P= z5Q^rA!cD($t7XO?fIR<6`J(V#1SS>?QVIaWELf`| z5DXKNEd)p6*##`41s8A#p%4y1sno^#0O@o>EA}_x{|HUIC)H0V^{FkEt`7w#HA0V2 zBPx#OR7W*%R8raMY83@6N<+4gdN^9ag<*$y#YuLXQ!nYu4}`%8tepV!`xFoq6=T_! z8Gzt{r5uLI140^vfG0@=ITwoA)RGxxWFa*`c>ek4lNKLS79{!59{aB4_-xSUoeiCA z$!Z0hmr+$gKwD686V_B=FbYf64gY1xu3N?Tm=5KF&% zglhi_rPcKJ=>3{kwAJ*7h}f&N=z@!E-iCaEH!|rDg4+Qd;$#Wn3J|YD&27^J+%0RG zR8KigFi}}N@-wFCReJbUm56aispUgrw*U*6lNop_`XLM9I#5V!B}5|Cl)=cy{x}M6 zYy3;>fQ2kY=);NXQ*T$FTJ=bySL4;5M5RY^cw&{F--)wO>Him9L=id@Ly!R>rm_44 zA9JRD6)Ng^=it1av zlUS%%i&qfaYmmvhAQ-v*foSWt;0eOnLLp$vqa!2i6huXY2-{b760pnCOt3_%cAA5* zA`76S_K_^(deU77jyaY1MeLV(&g+4rLI?pi$N|O6m?68AcUkgt^X<_AcE$@>Krmrw z1`tyytDLc9pQNHl$T{2$u0ikQ9fEhiKLBn;H(|qoK@s+aHl*m$ZNv5j@hAt@`2_Uz z37$UvqV?Is+jh#Qe3R*_;usDq$+=)%1I8XCM?@50@MjfVVCEo76Hes0c|Z~E3WOZ> z`?IX&$c&SLBc?e#=qD#22E;ELm7B>4Sp$b=2;xJ)bLIhuYSLhC9QDpdggz4ep@wiy z5jl{Y=elsgeSWm>l&5ca;OrUhw5+{}^~4j{O%qm01tlRQ*)-yI4RdE*7*0jvQ$#c? zVT8y0EQG#5oG1i?PDC(fOGGb~tV`a28*er^0bxhMTLBUv>J51?PK)?H#T@JlowOpl zLFJs05zaHvH{9nY{m7-BWMB|Pi(-fb;=B+bHc9r77(uGt z#wHo@0Ox=#2|z1VPC7jZgUYaLw3ZV0$CFuicQ-j9#X*FR8Fm!92Iloo@SyuZV@$@y zT$CRpbz&g2db7e-#8(7zz-qL(gr7cRwj~rX+UxD};;+b%iyrTb(KH;c~IqH_G zmkXCFV>(K@Mf6pyMH}=^V&)(}?>|gAZ8hfPT-gWb-O>5b`Fg{u8lFARqtw$vD zkq7qDmxjJRw5VUIh}%0C3~5hH+j7maW5v31X64xG4aq&0a6cP!KPz3Gl4hQZyG7{> zA;}(IXCe9bMNN88L0-GL1XL8WeBLr&Hvi;i_?i5 zHkQL~Sgd#Z7tRBrE9(*!?XimXM8(lq#nDY?!;f2k&>DBPKh&y=ty@N_pn9{S=9S>f z!FWaMrn52OJQ8yrNjOi$oF_IaY98t}wS`-1U5WLf$!y7aq|MDW{+7x$7`I4PMHQCc z8;cdxE@@%~%?n1rqM}Z!>xz{hOO*G<%6nIZw;(LZ6FcjXhOSB1ywa%&X>uxFJ}ogN z8%6sTn_@++iJ~L1q9ZHSZ!n2&F4oOS&x}YTywo`=UAi1E8ecGf;MkvVG{hVY2}f(p z(JCG2i9321ESuKygtaPWtx8zyW7hhmCYZ1V{ezO)L`i3?q%&U9wU7(ZQVBamGBm~Q zEei%fud+s}0YY@G9E(*9NKEO5vtDX|!Jb_CQp|Z?Vk#e$?0cp0JB^F}Wox|T&_eFt zI~o^GZ{!x;I~32Yd@s-O0j!*%7l)*h)@5cXDml8=&BwMh9Tw|`tyHQ!9Jd|*twvW+ z@;HyGI=p;i?YY?gFC@ypaJPS>;lOf5tf5=7*KSlbz>u)J_l~u}PAu_G1W5d2LyHb&4Z-YuzSgFE#*9f5>^+z|HO&b+e zFx}18ySa-@EUzB?mia|5nZ9n4D$cFVtX-DMF1?#K_RvpL#Z6m(LjQr~b~sjXI8kvt zR&jizvL$Q9={2z8%ulYku9&kc;p~n%ySJ}6iv?F)9@Z_L(JTeFdvAS4`8%j5qtNBv~NbT`#&;h4aUbg={URd(uL8r+mij#JIvTtOL~WY z^qiKmb$wI;d+ej@G*#iIKPsUbr)arq>>~yy?IR;h+Q*N}D0}%MN^7xh<`uqV{<>MJ z>{{tuX;|%9eQwn!l@G4x4LxY?kWP%mn@7HLL%KY@;XM4a&`R5X1n02CJ_EZyInjx{ zdMU5|(=9E4`sAZLAn`|alx2v1r~?E(eteiR7d)b@S?=ZTRd6b=sn*)o_DcomhTfgx z5Aq5hupOKGYZjmT;o;5d<{x{0;E7j*Fy0-j?v7XYz)Gtqgw?sfQQ=1xJ**D30jJ!I zQ*JgGA7ZvuMj3KG-Kqv8KHX|a)9Lr2X0VO@nX6;$0R7sr&WjZFdOOYAG_N1(X@{FP z>+<2nn@4-f;AX9W<_k4zg}u+f&0B?~e4Xm8HkvQiy>+k&%D=3j`7+%vEAz2@faWW8 zzih)gzcSOjQ}-)NF_vp+zFPOI+G;5OH**gIfMRxfv=jIqi{%b_^q3}gxJM5+@e}mu zNlpBuOAR;gs1T8NY&4b&NnT6?kax;RUP*E%$*W0TXB_R&zH{)%=mG7!^$70W16p|U zZifcT#}LB1CoxY@#xaLFVeC9Jhhdlns*ZCZHoov>iO&*&ow6f6w@j& z74*R0M9c-6XlE)qM_@rrWRoI48m^qsrmdAf_<|ie%cf;xlGe0j$AW79>M{OqgC5DAo4yht9%`k=75CHiO&ME z;#JHra*c4nsJfZwWV+d>j03N@bu$*;e^S#SF_D2a13hGNnPNnV3Z5sdn!uHFk_KGG zsw-R4ycs`N+Gb$H?@?0UbjS_6GxLnaHdB{J?#u!%NjqY3RifDScCl-=^w{}MA~o#J0&_7%*&3Yn~p%mdnDKR)<(_`sU_gqM6>1VNVI7ZAUUZx=Abg+gpT zSr(~geAa>)ns4fJl1C>RLdhv3lJoN(-oA|4Pa%^TqzAf&WDAxurWNvn7>i6PlA$dl zX}|cl*!(Aug>oRvwpu9WiXUPP%m#6q{}*JxBd4&YRcm}u}hMlqWxZi^MS#f#e)a^To*?pV>pn~vY>yw|YU z3&x1mORIxxUa-g}E?kXWxEjB3O*-$DdL|YuztPYJFRFkgO-qBTu2^w@!q&gQ0E{x{ zy*h}>KfTzsbWw72fbwgrNZ6`lw(5kfA!ch>IuW-W1)+d#T42_h@(m_0!R(JQ`xj5I z9G9?k`hlbTuVxYzZLx|rsr=ybzU6bv&%g&q3nqZRi{$|1_l?4ed$;dhUouJdL(4Zd z?Cg)~QnLCM(V5g*l+_uO)frUJAbV_2t2Z6XGs~AHd-pqx>tQ2hAD|x=LWPeXJBUVM z)*90>r)|r1%OxuZSG+5nRNz`?diKP~AlVN}htIv`lib{Yom#sj9U9#Wz26{upKckT z?-(_d>(#@KUihjc)B=Xfr193A{&EmG3)$VcG34CLbMobV(ruRc&QP(N3rUYo1`G(mginij zoQhV=$hm=0-r_mT_F;zGTvq!7VPa<$k$1pYA#$C>NE36RteN!t!bBb>`|(xk5!9lR zZa3yQj3hEEoD|=HXRt}B&;!V})HF@MPvzXF%I{N!?^CAtDa$`lMfa(L|3EqKQ`Pq= z=l`IN-lv-2$$hHkKGlAoa{Nz&?e6Kgq5OH>LtPs^L@)MkQFvXpC9kWgt28>6d?3vR z|2L`ec!tiWi?`}1mHE#rzg&5DGDeqfsjPI)vKmxlI_Hs^Qk9VhTMd0`x@dX-V+yZZ y7j#DYEWPC0qVT$MYSp&tTXp=+@MChnRnkTqmq@S16*cy0T&uz!jpEHp diff --git a/backend/app/tasks/__pycache__/watchers.cpython-313.pyc b/backend/app/tasks/__pycache__/watchers.cpython-313.pyc deleted file mode 100644 index f32320b1f20e82a71e9632d3060acbe4c18c89be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6855 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 diff --git a/backend/app/telemetry/__pycache__/__init__.cpython-313.pyc b/backend/app/telemetry/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 2f62cdb262c659e7824d24746bf3c78a79d23232..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 860 zcmb7?y>1jS5XZfHAA5J3y^lo7392nl6f!>j{_QFNL$#VZEVvHc4!y7sn4~JBih3r?PH$~a6n^>=^pOU1QR;MAsyj} zj&V#UI7vxH=Is{&?k_vD?z@L4P{0ZdZqka23%-Q(RAKtOg7PL~kPBHZQ(oq&KP!b! zg(?aVMym2h;B3x1Hs)2unzS7mxdO%|LwK_WZR|lEI9Cf+p%S3AD`*evpf(DfFjEyE z_Z989jtgfEH_UK-F&o&y5)AWB0PR$k+TeNxrC~WZuQ`-Lg0_dZ5~o}nUPGiq44GvGPpX8A$4ZJmW(iC)Zy9+m=+m{%jYc;{P zN5`Y<5qb1H`W}8RF!tEM6LE@cjiaZ$H>Aq#0X*}_ek z>sRKwl9}D?WsJ#E8pe*QjqTlEseE0)1H9wp{BY@e4nJE#5PT(1zLV+;sjk8}2(QQv OzwVCX@O^|+ckmC8o&bUX diff --git a/backend/app/telemetry/__pycache__/metrics.cpython-313.pyc b/backend/app/telemetry/__pycache__/metrics.cpython-313.pyc deleted file mode 100644 index 433fbd85f90dc49d0f9133df1be4d561e24c32d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13840 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 N7H7# diff --git a/backend/app/telemetry/__pycache__/tracing.cpython-313.pyc b/backend/app/telemetry/__pycache__/tracing.cpython-313.pyc deleted file mode 100644 index c463124d67016823b447bf059514df9c045e620b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12517 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 + +This is the central index for all project documentation. Each section links to a specific domain doc. + +## Project Documentation + +| Document | Contents | +|----------|---------| +| [requirements.md](project/requirements.md) | Functional requirements, QC workflow, RBAC matrix | +| [architecture.md](project/architecture.md) | System design, job state machine, data flow, security model | +| [tech_stack.md](project/tech_stack.md) | All dependency versions, configuration anchors | +| [api_spec.md](project/api_spec.md) | REST endpoints, WebSocket, auth flows | +| [database_schema.md](project/database_schema.md) | MongoDB collections, indexes, data shapes | +| [infrastructure.md](project/infrastructure.md) | Server inventory, ports, GCS layout, external services | +| [runbook.md](project/runbook.md) | Local setup, deploy, restart, backup, rollback | + +## Reference Documentation + +| Directory | Contains | +|-----------|---------| +| [reference/README.md](reference/README.md) | Index of ADRs, guides, manuals, research | +| [reference/adrs/](reference/adrs/) | Architecture Decision Records | +| [reference/guides/](reference/guides/) | Developer how-to guides | +| [reference/manuals/](reference/manuals/) | Operator manuals | +| [reference/research/](reference/research/) | Technology research notes | + +## Standards and Principles + +| Document | Contents | +|----------|---------| +| [documentation_standards.md](documentation_standards.md) | 60 universal documentation requirements | +| [principles.md](principles.md) | 11 development principles (Standards First, YAGNI, KISS, DRY…) | + +## Task and Test Management + +| Document | Contents | +|----------|---------| +| [tasks/README.md](tasks/README.md) | Task management rules and conventions | +| [tests/README.md](../tests/README.md) | Test strategy, commands, coverage targets | + +--- + +## Canonical Entry Point + +All documentation is reachable from [../AGENTS.md](../AGENTS.md). Agents and humans should start there. + +--- + +## Maintenance + +**Update triggers:** New doc added, section moved, external URL changes. +**Verification:** All links in this file resolve. AGENTS.md Quick Navigation matches this hub. + + diff --git a/docs/documentation_standards.md b/docs/documentation_standards.md new file mode 100644 index 0000000..90d374c --- /dev/null +++ b/docs/documentation_standards.md @@ -0,0 +1,61 @@ +# Documentation Standards β€” Accessible Video Processing Platform + + + +## Core Rules + +| Rule | Requirement | +|------|-------------| +| NO_CODE | No code block longer than 5 lines. Use tables, ASCII diagrams, or links to source files instead. | +| SCOPE tag | Every document must open with `` and close with ``. | +| Maintenance section | Every document must have a `## Maintenance` section at the end with: **Update triggers** (when to update) and **Verification** (how to confirm accuracy). | +| Canonical entry | All documentation is reachable from `AGENTS.md`. Never create a document that is an island. | +| No placeholder text | No `TODO`, `TBD`, `PLACEHOLDER`, `[describe here]`, or template metadata in committed documents. | +| No stale dates | Inline dates must be accurate at time of writing. Use `generated: {date}` in SCOPE tags rather than inline prose dates that rot. | +| Official links only | External links must point to official documentation (docs.python.org, fastapi.tiangolo.com, reactjs.org, MDN). No Stack Overflow, Medium, or blog links in reference docs. | + +## Structure Rules + +| Rule | Requirement | +|------|-------------| +| Tables over lists | Use markdown tables for: parameters, config values, comparison of alternatives, inventory lists. Use bullet lists only for sequential steps or genuinely unordered enumerations. | +| DAG navigation | Documents form a Directed Acyclic Graph. `AGENTS.md` β†’ `docs/README.md` β†’ domain docs. No circular links. | +| One canonical source | Every fact has exactly one home. Other documents link to it rather than repeating it. | +| File naming | Snake_case for all doc files. No spaces in filenames. | + +## Document Types and Templates + +| Type | Location | Contains | +|------|----------|---------| +| Root entry | `AGENTS.md` | Quick navigation, pipeline summary, constraints | +| Documentation hub | `docs/README.md` | Index of all docs | +| Requirements | `docs/project/requirements.md` | Functional requirements only (no implementation) | +| Architecture | `docs/project/architecture.md` | System design, state machines, data flow | +| Tech stack | `docs/project/tech_stack.md` | Dependency versions | +| API spec | `docs/project/api_spec.md` | Endpoints, auth, request/response shapes | +| Database schema | `docs/project/database_schema.md` | Collections, indexes, field definitions | +| Infrastructure | `docs/project/infrastructure.md` | Servers, ports, external services | +| Runbook | `docs/project/runbook.md` | Operational procedures | +| Principles | `docs/principles.md` | Engineering principles | +| ADR | `docs/reference/adrs/{date}-{slug}.md` | Single architectural decision | +| Guide | `docs/reference/guides/{slug}.md` | Developer how-to | +| Manual | `docs/reference/manuals/{slug}.md` | Operator procedures | +| Research | `docs/reference/research/{slug}.md` | Technology evaluation notes | +| Test docs | `tests/README.md` | Test commands, strategy, coverage | + +## ADR Format (Michael Nygard) + +| Section | Contents | +|---------|---------| +| Title | Short imperative verb phrase | +| Status | Proposed / Accepted / Deprecated / Superseded | +| Context | The problem or constraint that forced a decision | +| Decision | What was decided | +| Consequences | Trade-offs and impact | + +## Maintenance + +**Update triggers:** New document type added to the project, naming convention changes. +**Verification:** All committed docs have SCOPE tags. `grep -r "TODO\|PLACEHOLDER\|TBD" docs/` returns no results. + + diff --git a/docs/principles.md b/docs/principles.md new file mode 100644 index 0000000..85982bf --- /dev/null +++ b/docs/principles.md @@ -0,0 +1,70 @@ +# Development Principles β€” Accessible Video Processing Platform + + + +These 11 principles govern all engineering decisions on this project. They are ordered by priority β€” earlier principles override later ones when they conflict. + +--- + +## P-01: Standards First + +Implement to agreed specifications. The `video_accessibility_development_plan.txt` is the authoritative source for API contracts, schemas, state machine transitions, and worker pipeline behaviour. Read it before implementing a new feature. When the spec and the code diverge, fix the code. + +## P-02: Security by Design + +Security controls are non-negotiable: +- Access tokens in JS memory only β€” never localStorage +- Refresh tokens in HttpOnly cookies only +- RBAC enforced on every endpoint server-side +- Signed GCS URLs with 24h expiry β€” never store URLs +- All reviewer actions must emit audit log entries +- Generic error messages β€” never return internal exception details to clients + +## P-03: Async Correctness + +The backend is async (FastAPI + asyncio). Celery workers run in a separate sync process. Rules: +- Never call synchronous blocking I/O (`requests.get`, `time.sleep`) in async FastAPI routes +- Never share asyncio connections across Celery task boundaries β€” create connections per task +- Use `httpx.AsyncClient` for HTTP in async routes +- Use `asyncio.get_running_loop().run_in_executor()` only as a last resort for unavoidable sync calls + +## P-04: Fail Loudly on Configuration + +Missing required secrets must crash startup, not fall back to insecure defaults. Use `os.environ["KEY"]` (raises `KeyError`) instead of `os.environ.get("KEY", "weak_default")`. The `DEFAULT_ADMIN_PASSWORD` fallback is a known violation that must be fixed. + +## P-05: YAGNI + +Build only what is specified and currently needed. No speculative abstractions, no helper utilities for hypothetical future use cases. Three similar lines is better than a premature abstraction. If a feature is out of scope, defer it β€” don't build a "foundation" for it. + +## P-06: KISS + +Simple code is correct code. Prefer flat, readable functions over clever abstractions. A 20-line function that does one thing clearly is better than a 5-line function using three layers of metaprogramming. + +## P-07: DRY β€” but only after the second time + +Do not abstract on first encounter. Abstract when the same logic appears in a second place. The `broadcast_status_update()` function is copy-pasted in two task files β€” this is the known violation to fix. + +## P-08: No Comments for What, Only for Why + +Code identifiers describe what the code does. Comments explain non-obvious constraints, hidden invariants, or bug workarounds. `# logger undefined here` is a good comment. `# increment counter` is not. + +## P-09: Validate at System Boundaries Only + +Validate untrusted input (HTTP request bodies, AI model output, file uploads) at the boundary. Trust internal code, framework guarantees, and typed function signatures. Do not add defensive null-checks on values that the framework guarantees are non-null. + +## P-10: No Silent Failures + +Every error has two acceptable outcomes: it is handled with a logged warning, or it propagates up as an exception. Swallowed exceptions that log nothing (`except Exception: pass`) are forbidden. The `authz.py` `cache_key` NameError swallowed silently is a known violation. + +## P-11: Test What Matters + +Follow risk-based testing (Priority = Business Impact Γ— Probability). Tests with Priority β‰₯15 must exist before a feature is considered production-ready. Current critical gaps: RBAC (`authz.py`), job state machine (`ingest_and_ai.py`), audit logger, glossary retrieval β€” all Priority β‰₯20. + +--- + +## Maintenance + +**Update triggers:** New architectural decision that changes how engineers should approach a class of problems. +**Verification:** Each principle has a measurable compliance check β€” run a brief audit against recent commits before each production deploy. + + diff --git a/docs/project/api_spec.md b/docs/project/api_spec.md new file mode 100644 index 0000000..1899ed5 --- /dev/null +++ b/docs/project/api_spec.md @@ -0,0 +1,198 @@ +# API Specification β€” Accessible Video Processing Platform + + + +**Base URL (production):** `https://ai-sandbox.oliver.solutions/video-accessibility-back` +**Base URL (local):** `http://localhost:8003` +**OpenAPI docs:** `{base_url}/docs` (Swagger UI) + +All endpoints require `Authorization: Bearer ` except `/auth/login`, `/auth/refresh`, `/auth/microsoft/*`, and `/health`. + +--- + +## Authentication + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| POST | `/api/v1/auth/login` | None | Email/password login; returns access token + sets refresh cookie | +| POST | `/api/v1/auth/refresh` | Cookie | Exchange refresh cookie for new access token | +| POST | `/api/v1/auth/logout` | Bearer | Revoke refresh token, clear cookie | +| POST | `/api/v1/auth/microsoft/callback` | None | Microsoft SSO callback; validates OIDC token | +| GET | `/api/v1/auth/microsoft/login` | None | Redirect to Microsoft login | +| POST | `/api/v1/auth/change-password` | Bearer | Change own password | + +**Login response fields:** + +| Field | Type | Notes | +|-------|------|-------| +| access_token | string | JWT, 15-minute TTL | +| token_type | string | Always "bearer" | +| user | object | User profile (id, email, role, org_id) | + +--- + +## Jobs + +| Method | Path | Roles | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/jobs` | ALL | List jobs (role-filtered: client sees own, reviewer/admin see all) | +| POST | `/api/v1/jobs` | CLIENT, ADMIN | Create job with MP4 upload | +| GET | `/api/v1/jobs/{id}` | ALL | Job detail with current status + outputs | +| DELETE | `/api/v1/jobs/{id}` | ADMIN | Delete job and GCS files | +| GET | `/api/v1/jobs/{id}/downloads` | ALL | Signed download URLs for deliverables (24h expiry) | +| POST | `/api/v1/jobs/{id}/actions/approve` | REVIEWER, ADMIN | Approve job at current QC stage | +| POST | `/api/v1/jobs/{id}/actions/reject` | REVIEWER, ADMIN | Reject job with reason | +| POST | `/api/v1/jobs/{id}/actions/feedback` | REVIEWER, ADMIN | Send QC feedback without rejection | +| POST | `/api/v1/jobs/{id}/actions/retry` | ADMIN | Retry failed task (TTS_FAILED, RENDER_FAILED) | + +**Job object key fields:** + +| Field | Type | Notes | +|-------|------|-------| +| _id | string | MongoDB ObjectId | +| status | string | JobStatus enum β€” see architecture.md | +| org_id | string | Organisation that owns the job | +| source_language | string | BCP-47 language code | +| requested_outputs | array | Output language codes requested | +| outputs | object | Per-language GCS paths | +| language_qc | object | Per-language QC state | +| created_at | datetime | ISO 8601 | +| updated_at | datetime | ISO 8601 | +| error | string | Last error message if failed | + +--- + +## VTT Management + +| Method | Path | Roles | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/jobs/{id}/vtt/{lang}` | REVIEWER, ADMIN | Get VTT content for language | +| PATCH | `/api/v1/jobs/{id}/vtt/{lang}` | REVIEWER, LINGUIST, ADMIN | Update VTT content (auto-snapshots before save) | +| POST | `/api/v1/vtt/adjust-timing` | REVIEWER, ADMIN | Bulk shift all cue timings | + +--- + +## VTT Version Control + +| Method | Path | Roles | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/jobs/{id}/vtt-versions/{lang}` | REVIEWER, ADMIN | List version history | +| GET | `/api/v1/jobs/{id}/vtt-versions/{lang}/{version_id}` | REVIEWER, ADMIN | Get specific version content | +| POST | `/api/v1/jobs/{id}/vtt-versions/{lang}/{version_id}/restore` | REVIEWER, ADMIN | Restore a previous version (creates new snapshot) | +| GET | `/api/v1/jobs/{id}/vtt-versions/{lang}/diff` | REVIEWER, ADMIN | Diff two versions (`?from=v1_id&to=v2_id`) | + +--- + +## Language QC + +| Method | Path | Roles | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/jobs/{id}/language-qc` | REVIEWER, PM, ADMIN | Get per-language QC status for all languages | +| POST | `/api/v1/jobs/{id}/language-qc/{lang}/assign` | PM, ADMIN | Assign linguist to language | +| POST | `/api/v1/jobs/{id}/language-qc/{lang}/approve` | LINGUIST (assigned), PM, ADMIN | Approve language | +| POST | `/api/v1/jobs/{id}/language-qc/{lang}/reject` | LINGUIST (assigned), PM, ADMIN | Reject language with reason | +| POST | `/api/v1/jobs/{id}/language-qc/{lang}/feedback` | LINGUIST (assigned), PM, ADMIN | Send feedback without rejection | + +--- + +## Glossaries + +| Method | Path | Roles | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/glossaries` | ALL | List glossaries for current org | +| POST | `/api/v1/glossaries` | ADMIN | Create glossary | +| GET | `/api/v1/glossaries/{id}` | ALL | Get glossary with terms | +| PUT | `/api/v1/glossaries/{id}` | ADMIN | Update glossary metadata | +| DELETE | `/api/v1/glossaries/{id}` | ADMIN | Delete glossary | +| POST | `/api/v1/glossaries/{id}/terms` | ADMIN | Add term | +| DELETE | `/api/v1/glossaries/{id}/terms/{term_id}` | ADMIN | Delete term | + +--- + +## Files + +| Method | Path | Roles | Description | +|--------|------|-------|-------------| +| POST | `/api/v1/files/upload-url` | CLIENT, ADMIN | Get signed GCS upload URL | +| GET | `/api/v1/files/{job_id}/{path}` | ALL | Get signed download URL | + +--- + +## Users and Organisations + +| Method | Path | Roles | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/users/me` | ALL | Current user profile | +| GET | `/api/v1/organizations` | ADMIN | List organisations | +| POST | `/api/v1/organizations` | ADMIN | Create organisation | +| GET | `/api/v1/organizations/{id}/members` | PM, ADMIN | List org members | +| POST | `/api/v1/organizations/{id}/invite` | PM, ADMIN | Invite member | +| DELETE | `/api/v1/organizations/{id}/members/{user_id}` | PM, ADMIN | Remove member | + +--- + +## Admin + +| Method | Path | Roles | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/admin/users` | ADMIN | List all users | +| PATCH | `/api/v1/admin/users/{id}` | ADMIN | Update user role or status | +| GET | `/api/v1/admin/audit-log` | ADMIN, PM | Query audit log | + +--- + +## WebSocket + +| Path | Auth | Description | +|------|------|-------------| +| `WS /api/v1/ws/jobs/{id}` | Query param `token=` | Real-time job status updates | +| `WS /api/v1/ws/org/{org_id}` | Query param `token=` | Org-scoped event stream | + +**Message format:** + +| Field | Type | Notes | +|-------|------|-------| +| type | string | `job_status_update`, `notification`, `ping` | +| job_id | string | Job ObjectId | +| status | string | New JobStatus value | +| updated_at | datetime | ISO 8601 | + +--- + +## Health + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/health` | None | Returns `{"status":"healthy","version":"1.0.0"}` | +| GET | `/metrics` | None (internal) | Prometheus metrics | + +--- + +## Error Response Format + +All errors return: + +| Field | Type | Notes | +|-------|------|-------| +| detail | string | Human-readable error message (never internal exception text) | + +Common status codes: + +| Code | Meaning | +|------|---------| +| 400 | Bad request / validation error | +| 401 | Unauthenticated or invalid token | +| 403 | Forbidden β€” insufficient role | +| 404 | Resource not found | +| 422 | Pydantic validation error | +| 429 | Rate limit exceeded | +| 500 | Internal server error (details logged, not returned) | + +--- + +## Maintenance + +**Update triggers:** New endpoint added, request/response schema changed, auth flow change. +**Verification:** All endpoints listed here exist in `backend/app/api/v1/routes_*.py`. OpenAPI schema at `/docs` matches this table. + + diff --git a/docs/project/architecture.md b/docs/project/architecture.md new file mode 100644 index 0000000..f9f3425 --- /dev/null +++ b/docs/project/architecture.md @@ -0,0 +1,170 @@ +# Architecture β€” Accessible Video Processing Platform + + + +## System Overview + +Three-tier monorepo: React SPA frontend β†’ FastAPI backend β†’ Celery worker pool. Persistent stores are MongoDB Atlas (documents) and Redis (queue + cache). All AI processing happens asynchronously in Celery tasks. All file I/O is via GCS signed URLs. + +``` +Browser β†’ Apache β†’ FastAPI (sync surface) + β†’ Celery Workers (async AI pipeline) + β†’ MongoDB Atlas (job state) + β†’ Redis (task queue + rate limit state) + β†’ GCS (video + VTT files) +``` + +--- + +## Job State Machine + +16 states. Transitions are one-directional except for the QC feedback loop. + +| State | Description | Triggered by | +|-------|-------------|-------------| +| CREATED | Job record created | Upload complete | +| INGESTING | Worker has picked up the job | Celery task start | +| AI_PROCESSING | Gemini 2.5 Pro generating VTT | Ingestion complete | +| PENDING_QC | VTT ready for reviewer | AI processing done | +| QC_FEEDBACK | Reviewer sent feedback, not rejected | Reviewer action | +| APPROVED_ENGLISH | English content approved | QC approve (EN) | +| APPROVED_SOURCE | Source language approved | QC approve (source) | +| TRANSLATING | Google Translate + transcreation running | Approval triggers | +| TTS_GENERATING | Per-cue audio synthesis in progress | Translation done | +| TTS_FAILED | TTS service error β€” manual retry required | ElevenLabs/Google error | +| RENDERING_VIDEO | FFmpeg compositing accessible video | TTS done | +| RENDER_FAILED | FFmpeg error β€” manual retry required | FFmpeg error | +| RENDERING_QC | Rendering complete, awaiting final QC | Render done | +| PENDING_FINAL_REVIEW | PM reviewing final deliverables | QC approved | +| REJECTED | Job permanently rejected | Reviewer action | +| COMPLETED | Client notified, signed URLs delivered | PM final approval | + +**Terminal states:** COMPLETED, REJECTED. +**Manual-retry states:** TTS_FAILED, RENDER_FAILED. +**Feedback loop:** QC_FEEDBACK β†’ (fix) β†’ PENDING_QC. + +--- + +## Component Map + +### Backend (`backend/app/`) + +| Layer | Path | Responsibility | +|-------|------|---------------| +| API routes | `api/v1/routes_*.py` | HTTP + WebSocket endpoints, RBAC enforcement | +| Core | `core/security.py` | JWT encode/decode, password hashing | +| Core | `core/authz.py` | RBAC permission checks, `MembershipContext` | +| Core | `core/dependencies.py` | FastAPI DI β€” `get_current_user`, `get_database` | +| Core | `core/config.py` | Pydantic settings from env vars | +| Models | `models/job.py` | Job document schema + `JobStatus` enum (16 states) | +| Models | `models/user.py` | User document with roles | +| Services | `services/gemini.py` | Gemini 2.5 Pro API wrapper | +| Services | `services/gcs.py` | GCS V4 signed URLs, upload/download | +| Services | `services/language_qc.py` | Per-language QC state machine | +| Services | `services/glossary_service.py` | Hybrid exact + vector glossary retrieval | +| Services | `services/audit_logger.py` | Audit trail β€” all state-changing actions | +| Services | `services/microsoft_auth.py` | Microsoft SSO JWKS validation | +| Services | `services/websocket.py` | WebSocket connection manager | +| Tasks | `tasks/ingest_and_ai.py` | Main ingestion Celery task | +| Tasks | `tasks/translate_and_synthesize.py` | Translation + TTS pipeline | +| Tasks | `tasks/ffmpeg_operations.py` | Video rendering | +| Middleware | `middleware/rate_limiting.py` | Redis-backed request throttling | +| Middleware | `middleware/validation.py` | MongoDB injection protection | + +### Frontend (`frontend/src/`) + +| Layer | Path | Responsibility | +|-------|------|---------------| +| Routes | `routes/auth/` | Login, refresh, Microsoft SSO | +| Routes | `routes/jobs/` | Job list, job detail, VTT editor | +| Routes | `routes/admin/` | QC dashboard, audit log, user management | +| Routes | `routes/org/` | Organisation settings, invite members | +| Hooks | `hooks/useJob.tsx` | Job state + API calls | +| Hooks | `hooks/useJobStatusWebSocket.ts` | WS connection with backoff reconnect | +| Contexts | `contexts/GlobalWebSocketContext.tsx` | WS singleton per session | +| Contexts | `contexts/NotificationContext.tsx` | Toast notifications | +| Lib | `lib/auth.ts` | JWT in-memory store, refresh flow | +| Lib | `lib/api.ts` | Axios instance with auth interceptor | +| Components | `components/VttEditor.tsx` | Inline VTT editing with preview | +| Components | `components/VideoWithCaptions.tsx` | Multi-language video player | +| Components | `components/Layout/Sidebar.tsx` | Role-aware navigation | + +--- + +## Auth Architecture + +| Token | Storage | TTL | Purpose | +|-------|---------|-----|---------| +| Access token | JS memory (React context) | 15 min | Bearer for all API calls | +| Refresh token | HttpOnly cookie | 7 days | Obtain new access tokens | + +**Token flow:** Login β†’ both tokens issued β†’ access token in memory β†’ on expiry, silent refresh via cookie β†’ new access token in memory. On logout, both tokens revoked. + +**Critical:** `get_current_user()` in `dependencies.py` must reject refresh tokens used as Bearer tokens (type check on payload). + +--- + +## RBAC Matrix + +| Resource | CLIENT | REVIEWER | LINGUIST | PM | ADMIN | +|----------|--------|---------|---------|-----|-------| +| Upload video | βœ“ | β€” | β€” | β€” | βœ“ | +| View own jobs | βœ“ | βœ“ | β€” | βœ“ | βœ“ | +| View all jobs | β€” | βœ“ | β€” | βœ“ | βœ“ | +| Edit VTT | β€” | βœ“ | βœ“ | β€” | βœ“ | +| QC approve/reject | β€” | βœ“ | β€” | β€” | βœ“ | +| Assign linguist | β€” | β€” | β€” | βœ“ | βœ“ | +| Final review | β€” | β€” | β€” | βœ“ | βœ“ | +| User management | β€” | β€” | β€” | β€” | βœ“ | +| Audit log | β€” | β€” | β€” | βœ“ | βœ“ | + +Implementation: `authz.py` β†’ `MembershipContext`, `require_org_role(role)`, `require_platform_admin()`. + +--- + +## Security Model + +| Control | Implementation | File | +|---------|---------------|------| +| Rate limiting | Redis-backed, 5 req/5 min on login | `middleware/rate_limiting.py` | +| Input validation | MongoDB operator blocklist + Pydantic | `middleware/validation.py` | +| File access | GCS V4 signed URLs, 24h expiry | `services/gcs.py` | +| Audit trail | Every state-changing action logged | `services/audit_logger.py` | +| Secrets | GCP Secret Manager in production | `core/secrets_config.py` | +| Error messages | Generic HTTP errors β€” no internal detail | `routes_auth.py` | +| Token type check | Reject refresh tokens as Bearer | `core/dependencies.py` | + +**Known gaps (from security audit 2026-04-29):** Login endpoint currently bypasses rate limiting (debugging artifact β€” must be fixed before launch). Microsoft SSO uses synchronous `requests.get()` in async context. + +--- + +## Glossary Retrieval (Hybrid) + +Two-pass retrieval for translation prompt injection: + +| Pass | Method | Threshold | Limit | +|------|--------|-----------|-------| +| 1 β€” Exact | String match on source term | β€” | All matches | +| 2 β€” Vector | Atlas Vector Search on embedding | β‰₯ 0.75 similarity | Top 20 | + +Merged result: exact matches first, then vector matches, deduplicated, truncated to 50 terms. Injected as a block in the Gemini translation prompt. + +**Index:** `glossary_embedding_index` in MongoDB Atlas. + +--- + +## WebSocket Architecture + +- Server: `services/websocket.py` β€” `ConnectionManager` class, org-scoped broadcasts +- Client: `hooks/useJobStatusWebSocket.ts` β€” exponential backoff reconnect +- Auth: WS upgrade requires valid access token +- Events: `broadcast_to_org(org_id, event)` β€” no cross-tenant leakage + +--- + +## Maintenance + +**Update triggers:** New job state added, auth flow change, new service integrated, RBAC change. +**Verification:** State machine table matches `JobStatus` enum in `models/job.py`. RBAC matrix matches `authz.py` role checks. + + diff --git a/docs/project/database_schema.md b/docs/project/database_schema.md new file mode 100644 index 0000000..1f4ca34 --- /dev/null +++ b/docs/project/database_schema.md @@ -0,0 +1,219 @@ +# Database Schema β€” Accessible Video Processing Platform + + + +**Database:** MongoDB Atlas +**Database name:** configured via `MONGODB_DB` env var (default: `accessible_video`) + +--- + +## Collections + +### `jobs` + +Central document for each video accessibility job. + +| Field | Type | Description | +|-------|------|-------------| +| _id | ObjectId | Primary key | +| org_id | ObjectId | Owning organisation | +| client_user_id | ObjectId | User who uploaded the video | +| status | string | JobStatus enum (16 values β€” see architecture.md) | +| source_language | string | BCP-47 code (e.g., `en-US`) | +| requested_outputs | array[string] | Output language codes | +| source | object | `{ gcs_path, filename, duration_seconds }` | +| outputs | object | Per-language `{ captions_vtt, ad_vtt, ad_mp3, accessible_mp4 }` GCS paths | +| review | object | QC state `{ reviewer_id, approved_at, rejected_at, reason }` | +| language_qc | object | Per-language QC state (see LanguageQCState below) | +| vtt_versions | array | Version snapshot references (see `vtt_versions` collection) | +| glossary_id | ObjectId | Client glossary to use for translation | +| retry_count | int | Number of task retries | +| error | string | Last error message | +| created_at | datetime | ISO 8601 | +| updated_at | datetime | ISO 8601 | +| completed_at | datetime | ISO 8601 | + +**LanguageQCState (per-language, nested in `language_qc`):** + +| Field | Type | Description | +|-------|------|-------------| +| status | string | `pending`, `assigned`, `approved`, `rejected`, `feedback_requested` | +| linguist_id | ObjectId | Assigned linguist (nullable) | +| assigned_at | datetime | When linguist was assigned | +| reviewed_at | datetime | When approved/rejected | +| reason | string | Rejection or feedback reason | + +**Indexes:** + +| Index | Fields | Purpose | +|-------|--------|---------| +| Primary | `_id` | Document lookup | +| org_status | `org_id` + `status` | List jobs by org and status | +| client | `client_user_id` | Client's own jobs | +| created | `created_at` (desc) | Time-sorted listing | +| status | `status` | Status-filtered queries | + +--- + +### `users` + +| Field | Type | Description | +|-------|------|-------------| +| _id | ObjectId | Primary key | +| email | string | Unique, lowercase | +| hashed_password | string | bcrypt hash (null for SSO-only users) | +| role | string | `client`, `reviewer`, `linguist`, `pm`, `admin` | +| org_id | ObjectId | Primary organisation | +| is_active | boolean | Account enabled flag | +| microsoft_id | string | Entra ID subject claim (nullable) | +| created_at | datetime | | +| updated_at | datetime | | + +**Indexes:** + +| Index | Fields | Purpose | +|-------|--------|---------| +| email_unique | `email` (unique) | Login lookup | +| org | `org_id` | Members-of-org query | +| microsoft | `microsoft_id` (sparse) | SSO user lookup | + +--- + +### `organizations` + +| Field | Type | Description | +|-------|------|-------------| +| _id | ObjectId | Primary key | +| name | string | Organisation display name | +| slug | string | URL-safe identifier | +| member_ids | array[ObjectId] | User IDs in this org | +| created_at | datetime | | + +**Indexes:** + +| Index | Fields | Purpose | +|-------|--------|---------| +| slug_unique | `slug` (unique) | Org lookup by slug | + +--- + +### `glossaries` + +| Field | Type | Description | +|-------|------|-------------| +| _id | ObjectId | Primary key | +| org_id | ObjectId | Owning organisation | +| name | string | Glossary display name | +| terms | array | Array of GlossaryTerm documents | +| created_at | datetime | | +| updated_at | datetime | | + +**GlossaryTerm (embedded in `terms`):** + +| Field | Type | Description | +|-------|------|-------------| +| _id | ObjectId | Term ID | +| source_term | string | Term in source language | +| target_language | string | BCP-47 code | +| preferred_translation | string | Required translation | +| context | string | Usage notes (optional) | +| embedding | array[float] | Vector embedding for similarity search | + +**Indexes:** + +| Index | Fields | Purpose | +|-------|--------|---------| +| org | `org_id` | List org glossaries | +| vector | `terms.embedding` (Atlas Vector Search) | Similarity retrieval | + +**Atlas Vector Search index name:** `glossary_embedding_index` + +--- + +### `vtt_versions` + +Immutable version snapshots created before each VTT save. + +| Field | Type | Description | +|-------|------|-------------| +| _id | ObjectId | Primary key | +| job_id | ObjectId | Parent job | +| language | string | Language code | +| version_number | int | Sequential version number | +| content | string | Full VTT file content at time of snapshot | +| author_id | ObjectId | User who made the change | +| created_at | datetime | Snapshot timestamp | +| diff_from_prev | string | Diff against previous version (optional) | + +**Indexes:** + +| Index | Fields | Purpose | +|-------|--------|---------| +| job_lang | `job_id` + `language` + `version_number` | Version history listing | +| job_lang_created | `job_id` + `language` + `created_at` (desc) | Time-sorted history | + +--- + +### `audit_logs` + +Immutable audit trail for all reviewer, linguist, and PM actions. + +| Field | Type | Description | +|-------|------|-------------| +| _id | ObjectId | Primary key | +| actor_id | ObjectId | User performing the action | +| actor_email | string | Denormalised for readability | +| action | string | Action type enum (see below) | +| job_id | ObjectId | Affected job (nullable) | +| org_id | ObjectId | Organisation context | +| before_state | string | Job status before action | +| after_state | string | Job status after action | +| metadata | object | Action-specific context (reason, language, etc.) | +| created_at | datetime | Event timestamp | + +**Action types:** + +| Action | Trigger | +|--------|---------| +| `job_approved` | QC approve | +| `job_rejected` | QC reject | +| `qc_feedback_sent` | QC feedback | +| `language_approved` | Language-level QC approve | +| `language_rejected` | Language-level QC reject | +| `linguist_assigned` | PM assigns linguist | +| `vtt_edited` | VTT content saved | +| `vtt_restored` | Version restore | +| `job_retry` | Admin manual retry | +| `user_invited` | PM/Admin invites member | + +**Indexes:** + +| Index | Fields | Purpose | +|-------|--------|---------| +| job | `job_id` + `created_at` | Per-job audit trail | +| org_created | `org_id` + `created_at` (desc) | Org-level audit log | +| actor | `actor_id` + `created_at` | Per-user action history | + +--- + +### `invitations` + +| Field | Type | Description | +|-------|------|-------------| +| _id | ObjectId | Primary key | +| email | string | Invitee email | +| org_id | ObjectId | Org being joined | +| role | string | Role to assign on accept | +| token | string | Unique invite token (hashed) | +| expires_at | datetime | 7-day expiry | +| accepted_at | datetime | Nullable β€” set on accept | +| created_by | ObjectId | User who sent invite | + +--- + +## Maintenance + +**Update triggers:** New collection added, index added or removed, field added to model. +**Verification:** All collections listed here exist in production Atlas. Index names match `backend/app/core/database.py` `create_indexes()` function (currently commented out β€” indexes were created manually). + + diff --git a/docs/project/infrastructure.md b/docs/project/infrastructure.md new file mode 100644 index 0000000..8c48f4c --- /dev/null +++ b/docs/project/infrastructure.md @@ -0,0 +1,146 @@ +# Infrastructure β€” Accessible Video Processing Platform + + + +## Server Inventory + +| Server | Role | Resources | Location | +|--------|------|-----------|---------| +| optical-web-1 | Production host | 32GB RAM, 8 CPU | GCP VM | + +**Domain:** ai-sandbox.oliver.solutions +**SSL:** Wildcard certificate covering *.ai-sandbox.oliver.solutions + +--- + +## URL Map + +| Endpoint | URL | Served by | +|----------|-----|---------| +| Frontend SPA | `https://ai-sandbox.oliver.solutions/video-accessibility/` | Apache β†’ /var/www/html/video-accessibility | +| Backend API | `https://ai-sandbox.oliver.solutions/video-accessibility-back/` | Apache β†’ localhost:8000 | +| Backend health | `https://ai-sandbox.oliver.solutions/video-accessibility-back/health` | FastAPI | +| Backend docs | `https://ai-sandbox.oliver.solutions/video-accessibility-back/docs` | FastAPI (Swagger) | +| Prometheus metrics | localhost:8001 | Prometheus client (internal only) | +| WebSocket | `wss://ai-sandbox.oliver.solutions/video-accessibility-back/api/v1/ws/` | Apache mod_proxy_wstunnel | + +--- + +## Docker Compose Services + +| Service | Image | Port (internal) | Port (host) | Depends on | +|---------|-------|----------------|------------|-----------| +| api | backend/Dockerfile | 8000 | 8000 | mongodb, redis | +| worker | backend/Dockerfile (celery cmd) | β€” | β€” | mongodb, redis | +| mongodb | mongo:7.0 | 27017 | 27017 | β€” | +| redis | redis:7.2 | 6379 | 6379 | β€” | + +**Deploy path:** `/opt/video-accessibility/` + +--- + +## Apache Configuration Requirements + +| Module | Required for | +|--------|-------------| +| mod_rewrite | SPA routing (all paths β†’ index.html) | +| mod_proxy | API reverse proxy | +| mod_proxy_http | HTTP proxying | +| mod_proxy_wstunnel | WebSocket proxying | +| mod_headers | CORS + security headers | + +Config snippet location: `APACHE_DEPLOYMENT.md` (archived) and `/etc/apache2/sites-available/ai-sandbox.oliver.solutions-ssl.conf` on server. + +--- + +## GCS Layout + +**Bucket:** `accessible-video` (GCP project: `optical-414516`) + +| Path pattern | Contents | +|-------------|---------| +| `{jobId}/source.mp4` | Original uploaded video | +| `{jobId}/en/captions.vtt` | English closed captions | +| `{jobId}/en/ad.vtt` | English audio description VTT | +| `{jobId}/en/ad.mp3` | English audio description audio | +| `{jobId}/{lang}/captions.vtt` | Translated captions (e.g., `fr/`, `de/`) | +| `{jobId}/{lang}/ad.vtt` | Translated audio description VTT | +| `{jobId}/{lang}/ad.mp3` | Translated audio description audio | +| `{jobId}/accessible.mp4` | Final accessible video (burned-in captions + AD audio) | + +**Signed URL expiry:** 24h (V4 signing). URLs must not be cached or stored in the database. + +--- + +## External Service Dependencies + +| Service | Region / Endpoint | Rate limits / Quotas | +|---------|-----------------|-------------------| +| MongoDB Atlas | Cloud (Atlas cluster) | M10+ tier recommended | +| GCS | us-central1 | Standard storage class | +| Gemini 2.5 Pro | `generativelanguage.googleapis.com` | Per project quota | +| Google Cloud TTS | `texttospeech.googleapis.com` | 1M chars/month free tier | +| Google Cloud Translate | `translate.googleapis.com` | 500k chars/month free tier | +| ElevenLabs | `api.elevenlabs.io` | Subscription-dependent | +| SendGrid | `api.sendgrid.com` | 100 emails/day free tier | +| Microsoft Entra ID | `login.microsoftonline.com` | Tenant-configured | +| GCP Secret Manager | `secretmanager.googleapis.com` | 10k ops/month free | +| Sentry | `sentry.io` | Project DSN | + +--- + +## Network Ports + +| Port | Service | Exposed to | +|------|---------|-----------| +| 443 | Apache HTTPS | Public | +| 80 | Apache HTTP (β†’ 443 redirect) | Public | +| 8000 | FastAPI | localhost only | +| 8001 | Prometheus metrics | localhost only | +| 27017 | MongoDB | Docker network only | +| 6379 | Redis | Docker network only | + +--- + +## Secret Management + +**Production:** GCP Secret Manager. Secrets fetched at startup via `core/secrets_config.py`. +**Local:** `.env.local` (gitignored). +**Template:** `.env.prod.example` (checked in, no real values). + +| Secret | Where used | +|--------|-----------| +| `JWT_SECRET_KEY` | Access token signing | +| `JWT_REFRESH_SECRET_KEY` | Refresh token signing | +| `GEMINI_API_KEY` | Gemini API | +| `ELEVENLABS_API_KEY` | ElevenLabs TTS | +| `SENDGRID_API_KEY` | Email delivery | +| `GCS_BUCKET_NAME` | File storage | +| `GOOGLE_CLOUD_PROJECT` | GCP project ID | +| `MONGODB_URI` | Atlas connection string | +| `REDIS_URL` | Redis connection | +| `SENTRY_DSN` | Error tracking | +| `DEFAULT_ADMIN_PASSWORD` | Seed script (must not have fallback value) | + +--- + +## GCP Service Account IAM Roles + +| Role | Purpose | +|------|---------| +| Storage Admin | GCS read/write + signed URL generation | +| AI Platform User | Gemini API access | +| Cloud Translation User | Translate API access | +| Cloud Text-to-Speech User | TTS API access | +| Secret Manager Secret Accessor | Read secrets at runtime | + +**Credentials file:** `./secrets/gcp-credentials.json` (mounted into Docker containers, permissions 600). + +--- + +## Maintenance + +**Update triggers:** Server migration, new external service, GCS bucket rename, secret rotation. +**Verification:** All URLs in URL Map resolve. Docker service ports match `docker-compose.prod.yml`. GCS bucket name matches `GCS_BUCKET_NAME` env var. + + diff --git a/docs/project/requirements.md b/docs/project/requirements.md new file mode 100644 index 0000000..c61dec2 --- /dev/null +++ b/docs/project/requirements.md @@ -0,0 +1,154 @@ +# Functional Requirements β€” Accessible Video Processing Platform + + + +## Purpose + +This document specifies what the system must do from a user perspective. Implementation details belong in [architecture.md](architecture.md). Non-functional requirements (performance, security) belong in [architecture.md](architecture.md#security-model). + +--- + +## User Roles + +| Role | Who | Primary action | +|------|-----|---------------| +| CLIENT | Paying customer | Upload videos, download deliverables | +| REVIEWER | Oliver internal | QC approve/reject captions + audio description | +| LINGUIST | Language specialist | Review and approve translated content per language | +| PM | Project manager | Assign linguists, give final approval, monitor all jobs | +| ADMIN | Platform operator | Manage users, view audit log, configure platform | + +--- + +## Core Features + +### R-01: Video Upload + +| Requirement | Detail | +|-------------|--------| +| R-01.1 | Client can upload an MP4 video file | +| R-01.2 | File is stored in GCS; client receives a job ID | +| R-01.3 | Upload progress is displayed in real time | +| R-01.4 | System validates file type (MP4 only) and size on upload | +| R-01.5 | Upload creates a job record in CREATED state | + +### R-02: AI Processing Pipeline + +| Requirement | Detail | +|-------------|--------| +| R-02.1 | System automatically generates closed captions in VTT format using Gemini 2.5 Pro | +| R-02.2 | System generates audio description VTT (scene descriptions for blind/low-vision viewers) | +| R-02.3 | System generates SDH captions (includes sound effects and speaker IDs) | +| R-02.4 | System generates a descriptive transcript | +| R-02.5 | All generated VTT files are validated for correct format before advancing to QC | +| R-02.6 | Job status is updated in real time via WebSocket during processing | + +### R-03: Quality Control Workflow + +| Requirement | Detail | +|-------------|--------| +| R-03.1 | Reviewer can view video with captions side-by-side | +| R-03.2 | Reviewer can edit individual VTT cues (text + timing) inline | +| R-03.3 | Reviewer can approve English content (advances to APPROVED_ENGLISH) | +| R-03.4 | Reviewer can reject a job with a reason (advances to REJECTED) | +| R-03.5 | Reviewer can send QC feedback without full rejection (advances to QC_FEEDBACK) | +| R-03.6 | All QC actions are recorded in the audit log with timestamp and user ID | +| R-03.7 | VTT edits create a version snapshot before overwriting (version history maintained) | + +### R-04: Per-Language QC + +| Requirement | Detail | +|-------------|--------| +| R-04.1 | PM can assign a specific linguist to each output language | +| R-04.2 | Linguist can approve or reject their assigned language | +| R-04.3 | Language statuses are independent β€” approving French does not affect German | +| R-04.4 | Linguist cannot approve a language not assigned to them | +| R-04.5 | Job advances to PENDING_FINAL_REVIEW only when all languages are approved | +| R-04.6 | Per-language QC actions are recorded in the audit log | + +### R-05: Translation and TTS + +| Requirement | Detail | +|-------------|--------| +| R-05.1 | System translates captions and AD into all requested output languages | +| R-05.2 | System applies cultural transcreation (Gemini-assisted) where configured | +| R-05.3 | System uses client-specific glossary terms in translation prompts | +| R-05.4 | System synthesises audio description audio via Google TTS or ElevenLabs | +| R-05.5 | TTS is performed per cue to preserve timing | +| R-05.6 | TTS failures result in TTS_FAILED state; manual retry is supported | + +### R-06: Glossary Management + +| Requirement | Detail | +|-------------|--------| +| R-06.1 | Admin can create, read, update, and delete glossary terms per client organisation | +| R-06.2 | Glossary terms specify source term, target language, preferred translation | +| R-06.3 | System uses exact match first, then vector similarity (β‰₯0.75) for retrieval | +| R-06.4 | Up to 50 terms are injected per translation prompt | +| R-06.5 | Glossary embeddings are indexed in Atlas Vector Search | + +### R-07: VTT Version Control + +| Requirement | Detail | +|-------------|--------| +| R-07.1 | System creates a snapshot before each VTT edit save | +| R-07.2 | Reviewer can view version history with author, timestamp, and diff | +| R-07.3 | Reviewer can restore any previous version | +| R-07.4 | Concurrent edit conflict is detected and reported to the later editor | + +### R-08: Final Review and Delivery + +| Requirement | Detail | +|-------------|--------| +| R-08.1 | PM can view all final deliverable files before approving | +| R-08.2 | PM approval triggers client notification email | +| R-08.3 | Email contains signed GCS download URLs (24h expiry) | +| R-08.4 | Client can download captions, audio descriptions, and accessible video | +| R-08.5 | Job status advances to COMPLETED after PM approval | + +### R-09: Authentication and Access Control + +| Requirement | Detail | +|-------------|--------| +| R-09.1 | Users authenticate via email/password (local) or Microsoft SSO (enterprise) | +| R-09.2 | Access tokens are valid for 15 minutes; refresh tokens for 7 days | +| R-09.3 | Refresh tokens are stored in HttpOnly cookies only | +| R-09.4 | All API endpoints enforce RBAC β€” role checked server-side on every request | +| R-09.5 | Login is rate-limited to 5 attempts per 5-minute window | + +### R-10: Audit Logging + +| Requirement | Detail | +|-------------|--------| +| R-10.1 | Every state-changing action by a reviewer, linguist, or PM creates an audit log entry | +| R-10.2 | Audit log entries contain: actor user ID, action type, job ID, timestamp, before/after state | +| R-10.3 | Admin can view the full audit log filtered by user, job, or date range | +| R-10.4 | Audit log entries are immutable once written | + +### R-11: Real-time Notifications + +| Requirement | Detail | +|-------------|--------| +| R-11.1 | Job status changes are pushed to connected clients via WebSocket | +| R-11.2 | WebSocket events are org-scoped β€” users only receive events for their organisation | +| R-11.3 | WebSocket connection recovers automatically after disconnect (exponential backoff) | + +--- + +## Out of Scope (Current Version) + +| Feature | Reason | +|---------|--------| +| Automated transcription (Whisper) | Gemini handles transcription; Whisper worker exists but not active | +| CI/CD pipeline | Manual deploy via scripts; CI exists but does not run full test suite | +| Load testing | Not implemented; deferred to Phase 7 | +| Multi-tenant billing | Cost tracking via oliver-cost-tracker SDK (read-only dashboard) | + +--- + +## Maintenance + +**Update triggers:** New feature scope confirmed, requirement changed by stakeholder, QC workflow changes. +**Verification:** Every R-XX.X requirement maps to at least one test in [tests/README.md](../../tests/README.md) or [/tmp/audit/test-plan.md](/tmp/audit/test-plan.md). + + diff --git a/docs/project/runbook.md b/docs/project/runbook.md new file mode 100644 index 0000000..1e1f770 --- /dev/null +++ b/docs/project/runbook.md @@ -0,0 +1,213 @@ +# Runbook β€” Accessible Video Processing Platform + + + +## Local Development Setup + +### Prerequisites + +| Requirement | Version | +|-------------|---------| +| Docker | 20.10+ | +| Docker Compose | V2 (bundled with Docker Desktop) | +| Node.js | 20+ | +| Python | 3.11+ (for local scripts only; app runs in Docker) | +| GCP credentials file | `./secrets/gcp-credentials.json` | + +### First-Time Setup + +| Step | Command / Action | +|------|-----------------| +| 1. Copy env template | `cp .env.prod.example .env.local` β€” fill in all values | +| 2. Copy frontend env | `cp frontend/.env.example frontend/.env.local` | +| 3. Place GCP credentials | Copy service account JSON to `./secrets/gcp-credentials.json` | +| 4. Set permissions | `chmod 600 ./secrets/gcp-credentials.json` | + +### Starting the Local Environment + +**Step 1 β€” Backend (Docker):** + +`./scripts/run-local.sh` + +Services after start: + +| Service | URL | +|---------|-----| +| API | http://localhost:8003 | +| API docs (Swagger) | http://localhost:8003/docs | +| MongoDB | mongodb://localhost:27017 | +| Redis | redis://localhost:6379 | + +**Step 2 β€” Frontend (Vite dev server, separate terminal):** + +`cd frontend && npm install && npm run dev` + +Frontend URL: http://localhost:6001/video-accessibility + +### Common Local Commands + +| Action | Command | +|--------|---------| +| Rebuild containers after code change | `./scripts/run-local.sh --rebuild` | +| Stop all services | `./scripts/run-local.sh --stop` | +| Tail all logs | `docker compose logs -f` | +| Tail API logs | `docker compose logs -f api` | +| Tail worker logs | `docker compose logs -f worker` | +| Restart a service | `docker compose restart api` | + +### Test Credentials (Local Only) + +| Role | Email | Password | +|------|-------|---------| +| Admin | admin@example.com | admin | +| Reviewer | reviewer@example.com | reviewer | +| Client | client@example.com | client123 | + +Production uses Microsoft SSO β€” these credentials do not work in production. + +--- + +## Production Deployment + +**Server:** optical-web-1 +**Deploy path:** `/opt/video-accessibility/` +**URL:** https://ai-sandbox.oliver.solutions/video-accessibility/ + +### Full Deployment (code + frontend) + +Run on server (requires explicit user instruction β€” NEVER run via SSH without user approval): + +`./scripts/full-deploy.sh` + +This script: + +| Step | Action | +|------|--------| +| 1 | Pull latest code from git | +| 2 | Build Docker images | +| 3 | Restart containers | +| 4 | Build frontend bundle | +| 5 | Copy bundle to Apache webroot | +| 6 | Run DB seed if needed | + +### Frontend-Only Deployment + +`./scripts/build-frontend.sh` + +Builds the React bundle and copies to `/var/www/html/video-accessibility/`. + +### Verification After Deploy + +| Check | Command / URL | +|-------|--------------| +| API health | `curl https://ai-sandbox.oliver.solutions/video-accessibility-back/health` | +| Container status | `docker compose ps` | +| Frontend loads | Visit https://ai-sandbox.oliver.solutions/video-accessibility | +| Worker running | `docker compose logs --tail=20 worker` | + +--- + +## Database Operations + +### Backup MongoDB + +| Step | Command | +|------|---------| +| Dump to container | `docker compose exec mongodb mongodump --out=/data/backup` | +| Copy to host | `docker cp accessible-video-mongodb:/data/backup ./mongodb-backup-$(date +%Y%m%d)` | + +### Restore MongoDB + +| Step | Command | +|------|---------| +| Copy to container | `docker cp ./mongodb-backup accessible-video-mongodb:/data/restore` | +| Restore | `docker compose exec mongodb mongorestore /data/restore` | + +### MongoDB Shell + +`docker compose exec mongodb mongosh` + +--- + +## Restarting Services + +| Action | Command | +|--------|---------| +| Restart all | `docker compose restart` | +| Restart API only | `docker compose restart api` | +| Restart worker only | `docker compose restart worker` | +| Rebuild + restart one service | `docker compose up -d --build api` | + +--- + +## Updating Application + +| Step | Command | +|------|---------| +| Pull code | `git pull origin main` | +| Full redeploy | `./scripts/full-deploy.sh` | +| Frontend only | `./scripts/build-frontend.sh` | + +--- + +## Linting and Type Checking + +| Check | Command | Must pass before deploy | +|-------|---------|------------------------| +| Backend lint | `cd backend && ruff check .` | Yes | +| Backend type check | `docker compose exec api python -m mypy app/` | Yes | +| Frontend lint | `cd frontend && npm run lint` | Yes | +| Frontend type check | `cd frontend && npm run type-check` | Yes (currently 0 errors) | + +--- + +## Monitoring + +| Tool | Access | Purpose | +|------|--------|---------| +| Docker stats | `docker stats` | Container CPU/memory usage | +| API logs | `docker compose logs -f api` | Request errors | +| Worker logs | `docker compose logs -f worker` | Task errors | +| Sentry | sentry.io | Exception capture + stack traces | +| Prometheus | localhost:8001/metrics | Metrics (internal only) | + +--- + +## Troubleshooting + +| Symptom | Check | Fix | +|---------|-------|-----| +| 502 Bad Gateway on API | `docker compose ps api` + logs | Restart: `docker compose restart api` | +| Frontend 404 | `ls /var/www/html/video-accessibility/` | Rebuild: `./scripts/build-frontend.sh` | +| WebSocket fails | `apache2ctl -M | grep proxy_wstunnel` | `sudo a2enmod proxy_wstunnel && sudo systemctl restart apache2` | +| Worker not processing | `docker compose logs -f worker` | Check Redis URL + GCP credentials mount | +| Upload fails (GCS) | Test credentials in container | Check `./secrets/gcp-credentials.json` exists + permissions | +| MongoDB auth fails | Check `MONGODB_URI` env var | Verify Atlas connection string | + +--- + +## Apache Configuration + +Required modules: + +`sudo a2enmod rewrite proxy proxy_http proxy_wstunnel headers && sudo systemctl restart apache2` + +Config file: `/etc/apache2/sites-available/ai-sandbox.oliver.solutions-ssl.conf` + +Key directives needed: + +| Directive | Purpose | +|-----------|---------| +| `Alias /video-accessibility /var/www/html/video-accessibility` | Serve frontend | +| `ProxyPass /video-accessibility-back http://localhost:8000` | Proxy API | +| `RewriteRule ^ /video-accessibility/index.html [L]` | SPA routing | +| `RewriteEngine On` with WebSocket rules | WS proxy | + +--- + +## Maintenance + +**Update triggers:** New deploy script, new service port, new server. +**Verification:** All commands in this runbook execute without error on a clean checkout. Test credentials are not committed to production env files. + + diff --git a/docs/project/tech_stack.md b/docs/project/tech_stack.md new file mode 100644 index 0000000..b2f3c10 --- /dev/null +++ b/docs/project/tech_stack.md @@ -0,0 +1,94 @@ +# Tech Stack β€” Accessible Video Processing Platform + + + +## Backend + +| Component | Package | Version | Role | +|-----------|---------|---------|------| +| Web framework | FastAPI | 0.115.0 | Async REST + WebSocket | +| Task queue | Celery | 5.3.4 | Background AI processing | +| Database driver | Motor (AsyncIOMotor) | latest | MongoDB async driver | +| Cache / broker | Redis | 7.2 | Celery broker + rate limit state | +| Data validation | Pydantic | 2.5 | Request/response schemas | +| Runtime | Python | 3.11 | Language version | +| Package manager | Poetry | latest | Dependency management | +| ASGI server | Uvicorn | latest | HTTP server | +| JWT | python-jose | ^3.3.0 | Token encode/decode | +| Password hash | passlib + bcrypt | latest | Password hashing | +| Observability | OpenTelemetry | latest | Tracing (currently disabled) | +| Error tracking | Sentry SDK | latest | Exception capture | +| Metrics | Prometheus client | latest | `/metrics` endpoint | + +## Frontend + +| Component | Package | Version | Role | +|-----------|---------|---------|------| +| UI framework | React | 19.1.1 | Component model | +| Build tool | Vite | 7.1.2 | Dev server + bundler | +| Language | TypeScript | 5.8 | Type safety | +| Server state | TanStack Query | 5.85 | API caching + invalidation | +| Routing | React Router | 7.8 | Client-side routing | +| Styling | Tailwind CSS | 4.1 | Utility-first CSS | +| Client state | Zustand | 5.0 | Auth token + UI state | +| Forms | React Hook Form + Zod | latest | Form handling + validation | +| HTTP client | Axios | latest | API calls with interceptors | +| E2E testing | Playwright | latest | Browser automation | +| Unit testing | Vitest + RTL | latest | Component tests | + +## External Services + +| Service | Provider | Purpose | Auth | +|---------|---------|---------|------| +| Caption / AD generation | Gemini 2.5 Pro | Core AI processing | API key | +| Translation | Google Cloud Translate | 50+ language translation | Service account | +| Transcreation | Gemini 2.5 Pro | Cultural adaptation | API key | +| TTS (primary) | Google Cloud TTS | Audio description synthesis | Service account | +| TTS (premium voices) | ElevenLabs | High-quality voice synthesis | API key | +| File storage | Google Cloud Storage | Video + VTT files | Service account | +| Email | SendGrid | Client delivery notifications | API key | +| SSO | Microsoft Entra ID | Enterprise login | OAuth2/OIDC | +| Error tracking | Sentry | Exception capture + performance | DSN | +| Secrets | GCP Secret Manager | Production credentials | Service account | + +## Database + +| Store | Technology | Host | Purpose | +|-------|-----------|------|---------| +| Primary DB | MongoDB Atlas | Cloud (M10+) | Job documents, users, glossaries | +| Cache / broker | Redis | Docker (local) / Cloud (prod) | Celery tasks, rate limit counters | +| Vector index | Atlas Vector Search | In MongoDB Atlas | Glossary embedding retrieval | + +## Infrastructure + +| Layer | Technology | Notes | +|-------|-----------|-------| +| Container runtime | Docker + Docker Compose | All backend services containerized | +| Reverse proxy | Apache 2.4 | Modules: mod_proxy, mod_proxy_wstunnel, mod_rewrite | +| Server OS | Linux | optical-web-1 (GCP VM, 32GB RAM, 8 CPU) | +| Python env | Poetry virtualenv | Inside Docker containers | +| Node env | Node 20+ / npm | Frontend build | + +## Configuration Files + +| File | Purpose | +|------|---------| +| `backend/pyproject.toml` | Python deps + ruff config | +| `backend/mypy.ini` | mypy type-check config | +| `frontend/package.json` | Node deps | +| `frontend/eslint.config.js` | ESLint rules | +| `frontend/tsconfig.json` | TypeScript config | +| `frontend/playwright.config.ts` | E2E test config | +| `docker-compose.yml` | Base services | +| `docker-compose.local.yml` | Local dev overrides | +| `docker-compose.prod.yml` | Production overrides | +| `.env.local` | Local secrets (gitignored) | +| `.env.production` | Production secrets (gitignored) | +| `.env.prod.example` | Template for production env | + +## Maintenance + +**Update triggers:** Dependency version bump, new external service added, Python or Node runtime version change. +**Verification:** Versions match `backend/pyproject.toml` and `frontend/package.json`. + + diff --git a/docs/reference/README.md b/docs/reference/README.md new file mode 100644 index 0000000..ebbb681 --- /dev/null +++ b/docs/reference/README.md @@ -0,0 +1,46 @@ +# Reference Documentation β€” Accessible Video Processing Platform + + + +This hub indexes all reference documentation: architectural decisions, how-to guides, operator manuals, and research notes. + +--- + +## Architecture Decision Records (ADRs) + +ADRs capture significant architectural decisions, their context, and their trade-offs. + +| ADR | Title | Status | +|-----|-------|--------| +| [ADR-001](adrs/2026-04-29-async-celery-bridge.md) | Async Celery bridge via new event loop per task | Accepted | +| [ADR-002](adrs/2026-04-29-jwt-memory-storage.md) | Access tokens stored in JS memory, not localStorage | Accepted | +| [ADR-003](adrs/2026-04-29-hybrid-glossary-retrieval.md) | Hybrid exact + vector glossary retrieval | Accepted | + +--- + +## Guides (Developer How-Tos) + +| Guide | Description | +|-------|-------------| +| [testing-strategy.md](guides/testing-strategy.md) | Risk-based testing approach, what to test and how | + +--- + +## Manuals (Operator Procedures) + +_No operator manuals yet. Add as operational procedures are formalised._ + +--- + +## Research + +_No research notes yet. Add technology evaluations as decisions are made._ + +--- + +## Maintenance + +**Update triggers:** New ADR created, new guide written, research note added. +**Verification:** Every ADR in this index has a corresponding file. No files in subdirectories are missing from the index. + + diff --git a/docs/reference/adrs/2026-04-29-async-celery-bridge.md b/docs/reference/adrs/2026-04-29-async-celery-bridge.md new file mode 100644 index 0000000..993bcaa --- /dev/null +++ b/docs/reference/adrs/2026-04-29-async-celery-bridge.md @@ -0,0 +1,34 @@ +# ADR-001: Async Celery Bridge via New Event Loop Per Task + + + +**Status:** Accepted +**Date:** 2026-04-29 + +## Context + +FastAPI routes are async (asyncio). Celery workers run in synchronous Python processes. MongoDB (Motor) and other async clients cannot be shared across asyncio event loop boundaries. Tasks need to call async services (Gemini, GCS, MongoDB, TTS) that only have async APIs. + +## Decision + +Each Celery task creates a new `asyncio.EventLoop` via `asyncio.new_event_loop()` and runs its async implementation with `loop.run_until_complete(task_impl())`. The async implementation can freely use `await` with Motor, httpx, and other async clients. The event loop is closed in a `finally` block when the task completes. + +## Consequences + +**Benefits:** +- Async services work correctly inside Celery tasks +- No shared mutable state between tasks +- Each task is isolated β€” a failure does not corrupt another task's loop + +**Trade-offs:** +- Every task creates a new MongoDB connection (no connection pool reuse across tasks) +- This is a known performance limitation; mitigation is connection pooling within the task's event loop lifetime +- The pattern requires discipline: never `await` outside the task's own loop + +**Known violations:** `ingest_and_ai.py` creates `AsyncIOMotorClient(settings.mongodb_uri)` directly instead of using a shared factory β€” should be extracted to a `get_task_db()` context manager. + +## Maintenance + +**Update triggers:** If Celery adds native asyncio worker support, this ADR should be marked Deprecated and replaced. + + diff --git a/docs/reference/adrs/2026-04-29-hybrid-glossary-retrieval.md b/docs/reference/adrs/2026-04-29-hybrid-glossary-retrieval.md new file mode 100644 index 0000000..34bbebf --- /dev/null +++ b/docs/reference/adrs/2026-04-29-hybrid-glossary-retrieval.md @@ -0,0 +1,43 @@ +# ADR-003: Hybrid Exact + Vector Glossary Retrieval + + + +**Status:** Accepted +**Date:** 2026-04-29 + +## Context + +Client glossaries contain brand-specific terminology that must appear verbatim in translations. Simple string matching works for exact terms but fails for morphological variants, synonyms, and related phrases. Vector similarity alone would miss exact mandatory terms and potentially return unrelated results above the similarity threshold. + +## Decision + +Two-pass retrieval in `services/glossary_service.py`: + +| Pass | Method | Threshold | Limit | +|------|--------|-----------|-------| +| 1 | Exact string match on `source_term` | β€” | All | +| 2 | Atlas Vector Search on `terms.embedding` | β‰₯ 0.75 cosine similarity | Top 20 | + +Results are merged with exact matches first. Duplicates (same source term in both passes) are deduplicated. Total injected into the Gemini prompt is capped at 50 terms (`_MAX_TERMS_IN_PROMPT`). If the cap is reached, exact matches are kept over vector matches. + +Embeddings are generated by a separate Celery task (`tasks/embed_glossary.py`) when a term is created or updated and stored in `terms.embedding` (float array). The Atlas Vector Search index is `glossary_embedding_index`. + +## Consequences + +**Benefits:** +- Mandatory brand terms always appear (exact match guarantees inclusion) +- Related terms are surfaced without requiring exact phrasing (vector match) +- 50-term cap prevents context window bloat in the Gemini prompt + +**Trade-offs:** +- Embeddings must be pre-generated β€” new terms are not searchable by vector until the embed task runs +- Atlas Vector Search requires M10+ Atlas tier (not available on free tier) +- Similarity threshold (0.75) is a tunable parameter; too low = noisy matches, too high = missed variants + +**Known gaps:** No automated tests for exact-before-vector ordering, similarity threshold enforcement, or 50-term truncation. See test plan T-04 in `/tmp/audit/test-plan.md`. + +## Maintenance + +**Update triggers:** Threshold tuned, embedding model changed, Atlas index rebuilt. + + diff --git a/docs/reference/adrs/2026-04-29-jwt-memory-storage.md b/docs/reference/adrs/2026-04-29-jwt-memory-storage.md new file mode 100644 index 0000000..00e6004 --- /dev/null +++ b/docs/reference/adrs/2026-04-29-jwt-memory-storage.md @@ -0,0 +1,34 @@ +# ADR-002: Access Tokens Stored in JS Memory, Not localStorage + + + +**Status:** Accepted +**Date:** 2026-04-29 + +## Context + +SPAs need to persist access tokens for authenticated API calls. The traditional approach is `localStorage`, but this is vulnerable to XSS attacks β€” any injected script can read `localStorage` and exfiltrate tokens. The platform handles sensitive client video content, so the threat model warrants stronger protection. + +## Decision + +Access tokens (15-minute JWT) are stored in React context / Zustand in-memory state only. They are lost on page refresh. Refresh tokens (7-day JWT) are stored in HttpOnly cookies β€” inaccessible to JavaScript. On page load, the app silently calls `/auth/refresh` to obtain a new access token using the cookie. This exchange is transparent to the user. + +## Consequences + +**Benefits:** +- XSS cannot steal the access token β€” it is not in any DOM-accessible storage +- Refresh tokens are protected by the browser's HttpOnly cookie isolation +- Complies with OWASP token storage guidance + +**Trade-offs:** +- Page refresh requires a round-trip to `/auth/refresh` before the first authenticated API call +- `Authorization` header must be set on every request (Axios interceptor handles this) +- If the refresh endpoint is unavailable, the user must log in again + +**Implementation:** `frontend/src/lib/auth.ts` β€” in-memory store. Axios interceptor in `frontend/src/lib/api.ts` attaches the token on every request and calls refresh on 401. + +## Maintenance + +**Update triggers:** Token TTL changes, new auth provider added. + + diff --git a/docs/reference/guides/testing-strategy.md b/docs/reference/guides/testing-strategy.md new file mode 100644 index 0000000..6a21d9b --- /dev/null +++ b/docs/reference/guides/testing-strategy.md @@ -0,0 +1,113 @@ +# Testing Strategy Guide + + + +## Philosophy + +Risk-Based Testing: **Priority = Business Impact (1–5) Γ— Probability of failure (1–5)**. + +| Priority | Decision | Example | +|----------|----------|---------| +| β‰₯ 15 | MUST test | RBAC logic, job state machine, audit logger | +| 9–14 | SHOULD test | Translation pipeline, TTS routing | +| ≀ 8 | SKIP (manual sufficient) | Email template rendering, UI cosmetics | + +Write tests for business logic, not for framework behaviour. Never test that FastAPI routes a request β€” test that YOUR business logic in the handler produces the correct outcome. + +--- + +## Current Coverage (as of 2026-04-29 audit) + +| Layer | Files | Files with tests | Risk-weighted coverage | +|-------|-------|-----------------|----------------------| +| Backend | 118 | 8 (7%) | ~3% | +| Frontend | 98 | ~12 (12%) | β€” | +| E2E | β€” | 3 spec files | Effectively 0 (most tests skipped) | + +**Critical gap:** All Celery tasks (10 files) and 19 service files have zero test coverage. See full audit at `/tmp/audit/test-audit.md`. + +--- + +## Test Pyramid + +| Level | Framework | Location | Current count | +|-------|-----------|----------|--------------| +| Unit (backend) | pytest + AsyncMock | `backend/tests/unit/` | 8 files, ~338 assertions | +| Unit (frontend) | Vitest + RTL | `frontend/src/**/__tests__/` | 9 files, ~218 assertions | +| Integration (backend) | pytest + FastAPI TestClient | `backend/tests/integration/` | Does not exist yet | +| E2E | Playwright | `frontend/tests/e2e/` | 3 files, mostly skipped | + +--- + +## Test Commands + +| Command | What it runs | +|---------|-------------| +| `cd backend && poetry run pytest` | All backend unit tests | +| `cd backend && poetry run pytest -v tests/unit/test_security.py` | Single test file | +| `cd frontend && npm run test` | All frontend unit tests (Vitest) | +| `cd frontend && npm run test:e2e` | Playwright E2E tests | +| `cd frontend && npm run test:coverage` | Unit tests with coverage report | +| `docker compose exec backend python -m pytest` | Tests inside Docker (for integration tests) | + +--- + +## What Exists and Is High-Value + +| Test file | Value | What it tests | +|-----------|-------|--------------| +| `backend/tests/unit/test_security.py` | HIGH | JWT encode/decode, expiry, type fields, password hashing | +| `backend/tests/unit/test_vtt.py` | HIGH | VTT parsing (26 tests) | +| `backend/tests/unit/test_vtt_retimer.py` | HIGH | VTT timing logic (27 tests) | +| `frontend/src/lib/__tests__/auth.test.ts` | HIGH | JWT in-memory store, refresh flow | +| `frontend/src/components/Auth/__tests__/RequireAuth.test.tsx` | HIGH | Auth guard redirect | + +--- + +## Priority Gaps to Fill + +The following are MUST-fill based on Priority β‰₯15: + +| Priority | Module | Gap | +|----------|--------|-----| +| 25 | `tasks/ingest_and_ai.py` | Job state machine β€” zero tests | +| 20 | `core/authz.py` | RBAC permission checks β€” zero tests | +| 20 | `services/audit_logger.py` | Audit trail correctness β€” zero tests | +| 20 | `services/glossary_service.py` | Hybrid retrieval β€” zero tests | +| 16 | `services/language_qc.py` | QC state transitions β€” zero tests | +| 16 | `tasks/translate_and_synthesize.py` | Translation pipeline β€” zero tests | + +Full test plan at `/tmp/audit/test-plan.md`. + +--- + +## Anti-Patterns to Avoid + +| Anti-pattern | Why | Fix | +|-------------|-----|-----| +| Hardcoded job IDs like `test-job-123` | Non-existent in test DB | Use factories to create real test data | +| `with patch(...) as mock:` in every test method | Setup duplication | Move to `@pytest.fixture(autouse=True)` | +| `MagicMock()` on async functions | Silently returns a mock, not a coroutine | Use `AsyncMock()` | +| Testing that a library function was called | Tests library, not our logic | Test the business outcome | +| E2E tests that are `.skip` | They provide no coverage | Implement auth fixture and un-skip | + +--- + +## Infrastructure Required Before Writing Integration/E2E Tests + +| Blocker | What's needed | +|---------|--------------| +| Backend `conftest.py` | Shared `MockSettings`, `mock_db`, `test_user_factory`, `test_job_factory` | +| Celery test mode | `task_always_eager=True` fixture for synchronous task execution | +| Playwright auth fixture | Wire `tests/helpers/auth.ts` into `beforeEach` in all spec files | +| Playwright seed fixture | `tests/fixtures/seed.ts` to create test jobs, glossary, linguists | +| Mock AI responses | `tests/mocks/gemini-responses/*.json` fixtures | + +--- + +## Maintenance + +**Update triggers:** New test file added, coverage target changes, new testing tool added. +**Verification:** All commands in the Commands table execute without error. Priority gap table matches the current test-audit report. + + diff --git a/docs/tasks/README.md b/docs/tasks/README.md new file mode 100644 index 0000000..5b1c66b --- /dev/null +++ b/docs/tasks/README.md @@ -0,0 +1,78 @@ +# Task Management β€” Accessible Video Processing Platform + + + +## Task Tracking + +Tasks are tracked in conversation context and in the plan file at `~/.claude/plans/`. No external task tracker (Linear, Jira) is configured for this project. + +--- + +## Task Conventions + +| Convention | Rule | +|-----------|------| +| Status | `pending` β†’ `in_progress` β†’ `completed` | +| Naming | Imperative verb phrase: "Fix login rate-limit bypass" | +| Owner | Assigned agent or person | +| Blocking | Security/data-loss tasks block all others | + +--- + +## Active Work (as of 2026-04-29) + +### Immediate Priority (Security Blockers) + +| # | Task | File | Effort | +|---|------|------|--------| +| S-01 | Remove login endpoint from rate-limit bypass | `rate_limiting.py:165` | S | +| S-02 | Add refresh token type check in `get_current_user` | `dependencies.py:23` | S | +| S-03 | Generic exception message in refresh endpoint | `routes_auth.py:319` | S | +| S-04 | Replace `requests` with `httpx.AsyncClient` in Microsoft SSO | `microsoft_auth.py:59,91` | M | +| S-04b | Remove default admin password fallback | `seed.py:37` | S | + +### Quality / Tech Debt + +| # | Task | File | Effort | +|---|------|------|--------| +| Q-01 | Extract `broadcast_status_update()` to `tasks/utils.py` | `ingest_and_ai.py`, `translate_and_synthesize.py` | S | +| Q-02 | Fix `cache_key` scope bug in `authz.py:71` | `authz.py` | S | +| Q-03 | Replace all `print()` with `logger.debug()` in auth routes | `routes_auth.py` | S | +| Q-04 | Replace `asyncio.get_event_loop()` with `asyncio.get_running_loop()` in `gcs.py` | `services/gcs.py` | S | +| Q-05 | Fix MongoDB connection-per-login in auth routes | `routes_auth.py:44` | M | + +### Test Coverage (Priority β‰₯15) + +| # | Task | Target | Effort | +|---|------|--------|--------| +| T-01 | Create `backend/tests/conftest.py` with shared fixtures | All backend tests | M | +| T-02 | Write RBAC unit tests for `authz.py` | `core/authz.py` | M | +| T-03 | Write job state machine unit + integration tests | `tasks/ingest_and_ai.py` | L | +| T-04 | Write audit logger unit tests | `services/audit_logger.py` | M | +| T-05 | Write glossary hybrid retrieval unit tests | `services/glossary_service.py` | M | +| T-06 | Implement Playwright auth fixture, un-skip E2E tests | `tests/helpers/auth.ts` | L | + +--- + +## Backlog (Deferred) + +| # | Task | Priority | Notes | +|---|------|---------|-------| +| B-01 | Add `pip-audit` + `npm audit` to CI | LOW | CI exists, no security scan step | +| B-02 | Fix 53 B904 exception chain warnings (ruff) | LOW | `raise X from err` pattern | +| B-03 | Fix 33 ESLint errors (mostly `no-explicit-any`) | LOW | No security impact | +| B-04 | Fix B023 loop closure bug in translate_and_synthesize | MEDIUM | Safe in practice but violates best practices | +| B-05 | Add nonce validation in Microsoft SSO | INFO | Replay protection | +| B-06 | Validate `X-Forwarded-For` against trusted proxy list | MEDIUM | Rate limit bypass risk | +| B-07 | Enable mypy in CI (run in Docker) | MEDIUM | Currently not in CI pipeline | +| B-08 | VTT version control E2E tests | MEDIUM | Playwright spec needed | +| B-09 | WebSocket reconnect unit tests | MEDIUM | `useJobStatusWebSocket.ts` stale closure | + +--- + +## Maintenance + +**Update triggers:** Task completed, new task identified, priority changed. +**Verification:** Security blockers (S-01 through S-04b) are resolved before next production deploy. + + diff --git a/docs/video_accessibility_user_guide_v3.md b/docs/video_accessibility_user_guide_v3.md new file mode 100644 index 0000000..7164510 --- /dev/null +++ b/docs/video_accessibility_user_guide_v3.md @@ -0,0 +1,1476 @@ +# Video Accessibility Platform β€” Complete User Guide + +**Version:** 3.0 +**Date:** April 2026 +**Platform URL:** https://ai-sandbox.oliver.solutions/video-accessibility +**Support:** Contact your platform administrator + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [User Roles β€” Who Can Do What](#2-user-roles) +3. [Logging In](#3-logging-in) +4. [Dashboard β€” Your Home Screen](#4-dashboard) +5. [Client Guide β€” Upload & Download](#5-client-guide) + - 5.1 [Uploading a Single Video](#51-uploading-a-single-video) + - 5.2 [Uploading Multiple Videos at Once](#52-uploading-multiple-videos-at-once) + - 5.3 [Understanding Voice & TTS Settings](#53-understanding-voice--tts-settings) + - 5.4 [Tracking Your Job](#54-tracking-your-job) + - 5.5 [Downloading Your Completed Files](#55-downloading-your-completed-files) +6. [Reviewer & Linguist Guide](#6-reviewer--linguist-guide) + - 6.1 [QC Review Queue](#61-qc-review-queue) + - 6.2 [QC Review β€” Detailed Walkthrough](#62-qc-review--detailed-walkthrough) + - 6.3 [Final Review Queue](#63-final-review-queue) + - 6.4 [Final Review β€” Detailed Walkthrough](#64-final-review--detailed-walkthrough) +7. [Production User Guide](#7-production-user-guide) +8. [Admin Guide](#8-admin-guide) + - 8.1 [User Management β€” List View](#81-user-management--list-view) + - 8.2 [Creating a New User](#82-creating-a-new-user) + - 8.3 [Editing a User](#83-editing-a-user) + - 8.4 [Resetting a User Password](#84-resetting-a-user-password) + - 8.5 [Deactivating a User](#85-deactivating-a-user) + - 8.6 [Bulk Job Operations](#86-bulk-job-operations) +9. [All Jobs List](#9-all-jobs-list) +10. [Job Detail Page](#10-job-detail-page) +11. [Notifications](#11-notifications) +12. [Connection Status Indicator](#12-connection-status-indicator) +13. [Job Status Reference](#13-job-status-reference) +14. [Tips & Best Practices](#14-tips--best-practices) +15. [Troubleshooting & FAQ](#15-troubleshooting--faq) +16. [Glossary](#16-glossary) + +--- + +## 1. Overview + +The **Video Accessibility Platform** automatically creates closed captions and audio descriptions for your videos using AI, then translates them into multiple languages. Human reviewers check and approve the results before you receive the final files. + +### What the platform produces + +| File Type | Description | +|-----------|-------------| +| **Captions (VTT)** | Closed captions showing dialogue and sound effects on screen | +| **Audio Description Script (VTT)** | Text file describing on-screen visuals, for use with screen readers | +| **Audio Description Voiceover (MP3)** | Spoken audio description β€” a narrator describes what is happening visually | +| **Accessible Video (MP4)** | Your original video with audio descriptions embedded (spoken narration added automatically) | +| **SDH Captions (VTT)** | Captions for the Deaf and Hard of Hearing, including speaker labels, sound effects, and music notation | +| **Re-timed Captions (VTT)** | Captions re-synchronised to match an accessible video with added pauses | +| **Descriptive Transcript (TXT)** | A plain-text document combining all dialogue and visual descriptions | + +### How the process works + +``` +You upload a video + ↓ +AI generates captions + audio descriptions (1–3 minutes) + ↓ +A reviewer checks and approves the English content (QC Review) + ↓ +System translates to all requested languages (automatic, 2–5 min per language) + ↓ +System generates audio voiceovers (automatic) + ↓ +A reviewer validates all language versions (Final Review) + ↓ +You receive a notification and download your files +``` + +**Total time:** Typically 15–45 minutes, depending on video length and reviewer availability. + +--- + +## 2. User Roles + +The platform has five user roles. Each role can see and do different things. + +| Role | What they can do | +|------|-----------------| +| **Client** | Upload videos, track their own jobs, download completed files | +| **Reviewer** | Everything a client can do, plus perform QC Review and Final Review | +| **Linguist** | Same as Reviewer β€” primarily for language specialists who check translated content | +| **Production** | Everything a Reviewer can do, plus upload videos for clients, bulk-manage jobs | +| **Admin** | Full access β€” everything above, plus manage users, delete any job, reprocess jobs | + +> **Not sure what role you have?** Look at your left sidebar. If you see "QC Review" and "Final Review" links, you are a Reviewer, Linguist, or above. If you see "User Management," you are an Admin. + +--- + +## 3. Logging In + +### Step 1 β€” Go to the platform + +Open your browser and go to: +**https://ai-sandbox.oliver.solutions/video-accessibility** + +You will see the Login page with the Oliver logo on the left and a sign-in form on the right. + +### Step 2 β€” Choose how to log in + +**Option A β€” Microsoft Account (for Oliver staff)** + +Click the large **"Sign in with Microsoft"** button. +A Microsoft window will open. Enter your Oliver email and password there. +The window will close and you will be taken to your dashboard automatically. + +**Option B β€” Email & Password (for external users and linguists)** + +The email/password form is **hidden by default**. To reveal it: + +1. On the login page, look for a small **"Local login"** link or toggle (usually below the Microsoft button). Click it. +2. An Email and Password form will slide into view. +3. In the **Email** field, type your email address (e.g., `john.smith@example.com`). +4. In the **Password** field, type your password. +5. Click the **"Sign In"** button. +6. If you see a red error message, check that you typed the correct email and password. Passwords are case-sensitive. + +> **Forgot your password?** Contact your administrator. They can reset your password and give you a new temporary one. + +### Step 3 β€” You are in + +After logging in, you will see your **Dashboard** β€” the main home screen. + +--- + +## 4. Dashboard + +The Dashboard is the first screen you see after logging in. It gives you a quick overview of what is happening. + +### What you see on the Dashboard + +**Top statistics row** β€” Four large numbers: + +| Number | What it means | +|--------|---------------| +| **Total Jobs** | All jobs you have access to (your own jobs if you are a Client; all jobs if you are Reviewer or above) | +| **Processing** | Jobs that are currently being worked on by the AI system | +| **In QC** | Jobs waiting for a human reviewer to check them | +| **Completed** | Jobs that are finished and ready to download | + +**For Clients:** Below the statistics you will see: +- A **"Upload New Video"** button β€” click this to start a new job +- A list of feature highlights explaining what the platform does + +**For Reviewers, Linguists, Production, and Admins:** Below the statistics you will see: +- A **"Quality Control"** card showing how many jobs are waiting for QC review, with a **"Review Now"** button +- A **"Final Approval"** card showing how many jobs are waiting for final approval, with a **"Review Now"** button +- A **"Recent Activity"** list showing the last 5 jobs and their current status + +### Navigation sidebar (left side) + +The sidebar is always visible. Click any item to navigate: + +| Sidebar Item | Who sees it | Where it goes | +|-------------|-------------|---------------| +| **Dashboard** | Everyone | Home screen | +| **All Jobs** | Everyone | Full list of all your accessible jobs | +| **Upload Video** | Client, Production, Admin | New job upload form | +| **QC Review** | Reviewer, Linguist, Production, Admin | QC queue | +| **Final Review** | Reviewer, Linguist, Production, Admin | Final review queue | +| **User Management** | Admin only | User administration | + +### Top navigation bar + +The top bar has: +- **Bell icon (πŸ””)** β€” Click to see your notifications (see [Section 11](#11-notifications)) +- **"New Upload" button** β€” Quick access to upload a video (Client, Production, Admin) +- **Your name / avatar** β€” Click to open a dropdown with Profile, User Management (Admin), and Sign Out + +--- + +## 5. Client Guide + +This section explains everything a **Client** needs to do: upload a video, track its progress, and download the final files. + +--- + +### 5.1 Uploading a Single Video + +#### How to get to the upload form + +Option A: Click **"Upload Video"** in the left sidebar. +Option B: Click the **"New Upload"** button in the top right. +Option C: Click **"Upload New Video"** on the Dashboard. + +All three take you to the same page: **New Job**. + +#### Step 1 β€” Choose upload mode + +At the top of the form, you will see two tabs: + +- **"Single Upload"** β€” Upload one video with full customisation options +- **"Multi Upload"** β€” Upload several videos at the same time (see [Section 5.2](#52-uploading-multiple-videos-at-once)) + +Make sure **"Single Upload"** is selected (it is the default). + +#### Step 2 β€” Give your job a title + +In the **"Video Title"** field, type a name for this job. This is just for your reference β€” it helps you find the job later. +Example: `Q4 Training Video - Final Cut` + +#### Step 3 β€” Select what outputs you need + +Under **"Requested Outputs"**, tick the boxes for the files you need: + +| Checkbox | What it creates | +|----------|----------------| +| **Captions (VTT)** | A subtitle file showing spoken dialogue | +| **Audio Description Script (VTT)** | A text file describing visual content | +| **Audio Description Voiceover (MP3)** | A spoken audio track describing visual content | +| **Accessible Video (MP4)** | Your original video with narration embedded | +| **SDH Captions (VTT)** | Captions with speaker labels and sound effect notation | + +> **Tip:** If you tick "Accessible Video," a sub-option appears asking for the **Accessible Video Method**: +> - **"Pause & Insert"** β€” The video pauses briefly while narration is spoken, then resumes. Best for compliance. +> - **"Audio Overlay"** β€” Narration plays over the video simultaneously. Best for continuous playback. + +#### Step 4 β€” Choose your languages + +Under **"Target Languages"**, click on the languages you need. Each language you select will be shown as a coloured tag. Click a language again to deselect it. + +Examples: `Spanish`, `French`, `German`, `Japanese` + +> **Translation Mode** β€” After selecting languages, you will see a **"Translation Mode"** option: +> - **"Video Native Mode (Recommended)"** β€” The AI re-analyses the video in each target language directly. This gives more natural results. +> - **"Traditional VTT Translation"** β€” Translates the English caption text word-for-word. Faster but less natural. +> +> We recommend **Video Native Mode** for marketing and branded content. + +#### Step 5 β€” (Optional) Configure Voice Settings + +If you selected "Audio Description Voiceover (MP3)" or "Accessible Video (MP4)", a **"Voice Settings"** section will appear. Click on it to expand. + +Inside Voice Settings you can customise: + +| Setting | What it does | +|---------|-------------| +| **TTS Provider** | Choose between **Gemini** (Google) or **ElevenLabs** | +| **Model** | **Flash** β€” faster, lighter voice; **Pro** β€” higher quality, more natural | +| **Speed** | How fast the narrator speaks (0.5 = slow, 1.0 = normal, 2.0 = fast) | +| **Style Preset** | The overall tone: Neutral, Expressive, Professional, Calm, Energetic | +| **Voice per Language** | For each language, choose a specific voice from the dropdown | +| **Stability** | (ElevenLabs only) How consistent the voice sounds. Higher = more consistent | +| **Similarity** | (ElevenLabs only) How closely the voice matches the reference | + +> **Not sure which settings to use?** Leave everything as default. The platform will use the best settings automatically. + +#### Step 6 β€” (Optional) Add brand context + +In the **"Brand Names (optional)"** text area, you can type any important names or terms the AI should know about. For example: +- Product names that might be mispronounced +- Character names +- Technical terminology specific to your company + +This is optional but improves accuracy. + +#### Step 7 β€” Upload your video file + +**Option A β€” Drag and drop:** +Drag your video file from your computer and drop it onto the dashed upload area that says "Drop your video here." + +**Option B β€” Click to browse:** +Click anywhere inside the dashed upload area, then find and select your video file using the file picker. + +**Requirements:** +- Format: MP4 (`.mp4`) only +- Maximum size: 2 GB +- Duration: No strict limit, but very long videos take longer to process + +A progress bar will appear showing the upload percentage: **0% β†’ 100%** + +Do not close the browser tab while the upload is in progress. + +#### Step 8 β€” Submit the job + +Once the file finishes uploading, click the **"Create Job"** button. + +A **success screen** appears with the message **"Job Created Successfully!"** and two buttons: + +- **"View Job Details"** β€” click to go directly to your new job's detail page +- **"Create Another Job"** β€” click to start a fresh upload form + +The page will **automatically redirect** to the Job Detail page after **3 seconds** if you do not click either button. + +--- + +### 5.2 Uploading Multiple Videos at Once + +If you need to upload many videos with the same settings, use the **Multi Upload** mode. + +#### How to access Multi Upload + +On the New Job page, click the **"Multi Upload"** tab at the top. + +#### Step 1 β€” Select all your video files + +Drag multiple `.mp4` files onto the upload area, or click to browse and select several files at once (hold Ctrl or Cmd while clicking to select multiple). + +Each file will appear as a row in the **"Files to Upload"** list. You can see: +- File name +- File size +- A remove button (βœ•) to remove that file from the list + +#### Step 2 β€” Configure shared settings + +All files in a multi-upload share the same settings: +- Requested Outputs (Captions, AD, etc.) +- Target Languages +- Translation Mode +- Voice Settings (if applicable) +- Brand Context + +Configure these the same way as for a single upload (Steps 3–6 above). The **Video Title** field is not shown β€” each job will use the original filename as its title. + +#### Step 3 β€” Upload all files + +Click **"Upload All"**. + +An **Upload Progress** panel appears showing the status of each file: +- **Uploading...** β€” currently being transferred +- **βœ“ Success** β€” job created +- **βœ— Failed** β€” an error occurred (usually file type or size issue) + +The system uploads up to **3 files simultaneously**. Remaining files wait in the queue and start automatically when a slot becomes free. You can watch all uploads complete in real time. + +**If some uploads fail:** +A **"Retry Failed Uploads"** button appears. Click it to automatically re-attempt any files that failed. This is useful if you had a temporary network issue. + +#### Step 4 β€” Go to All Jobs + +Once all uploads finish, click **"Go to All Jobs"** to see all your newly created jobs in the list. + +--- + +### 5.3 Understanding Voice & TTS Settings + +TTS stands for **Text-to-Speech** β€” the technology that converts the written audio description text into a spoken voice recording. + +#### Providers explained + +**Gemini (Google TTS):** +- Free tier available +- Very natural, neural voices +- Good for most use cases +- Available in 40+ languages + +**ElevenLabs:** +- Premium quality +- More human-sounding +- Best for high-end marketing content +- Additional cost per character + +#### Model options + +**Flash** β€” Faster generation, slightly simpler voice. Good for internal content or drafts. +**Pro** β€” Higher quality, more natural intonation. Recommended for client-facing content. + +#### Style Presets + +| Preset | Best for | +|--------|---------| +| **Neutral** | Informational content, training videos | +| **Professional** | Corporate presentations, e-learning | +| **Expressive** | Marketing videos, brand storytelling | +| **Calm** | Healthcare, wellness, instructional | +| **Energetic** | Sports, advertising, dynamic content | +| **Custom** | Full manual control β€” a free-text field appears where you type your own style instructions | + +#### Voice Preview + +Next to each voice selection dropdown, a **"Preview"** button (speaker icon) lets you listen to a short audio sample of that voice before committing to it. Click it and wait a moment β€” a short clip will play through your speakers or headphones. + +--- + +### 5.4 Tracking Your Job + +After uploading, you can follow the progress of your job. + +#### Where to find your jobs + +Click **"All Jobs"** in the left sidebar to see all your jobs. + +#### Job status colours and meanings + +Each job shows a coloured **status badge**: + +| Colour | Status | What is happening | +|--------|--------|------------------| +| βšͺ Grey | **Created** | Job queued, waiting to start | +| πŸ”΅ Blue | **Ingesting** | Downloading and analysing your video | +| πŸ”΅ Blue | **AI Processing** | AI is generating captions and audio descriptions | +| 🟑 Yellow | **Pending QC** | Waiting for a human reviewer | +| 🟒 Green | **Approved (English)** | English content approved, translations starting | +| 🟣 Purple | **Translating** | Converting to other languages | +| 🟣 Purple | **TTS Generating** | Creating audio voiceovers | +| 🟠 Orange | **Pending Final Review** | Waiting for final human approval | +| 🟒 Green | **Completed** | Ready to download! | +| πŸ”΄ Red | **Rejected** | An issue was found β€” see review notes | +| πŸ”΄ Red | **TTS Failed** | Audio generation encountered an error β€” contact your reviewer | + +#### Getting real-time updates + +The platform automatically updates job statuses in real time. You do not need to refresh the page. When your job reaches an important status (like "Pending QC" or "Completed"), a notification will pop up in the top right corner of your screen. + +#### Clicking on a job + +Click on any job title or row to open the **Job Detail** page. From there you can see: +- Full job information +- Processing history +- Download button (when ready) + +--- + +### 5.5 Downloading Your Completed Files + +When your job status changes to **Completed** (green), your files are ready. + +#### How to reach the download page + +**Option A:** Go to **All Jobs**, find your completed job, and click the **"Download"** button in the Actions column. + +**Option B:** Click on the job title to open Job Detail, then click the **"Download Files"** button (green button). + +**Option C:** Click the direct link in the email notification you received when the job completed. + +#### What you will see on the Download page + +The download page is organised by language. For each language, you will see a card with all the files available for that language. + +**Source Video section:** +- **Source Video (MP4)** β€” Your original uploaded video + Click **"Download"** to save it. + +**English section (and each additional language):** + +| Button | File | When available | +|--------|------|---------------| +| **Captions (VTT)** | Subtitle file for closed captions | Always | +| **Audio Descriptions (VTT)** | Text script of visual descriptions | If requested | +| **Audio Descriptions (MP3)** | Spoken audio narration | If requested | +| **Accessible Video (MP4)** | Video with narration embedded | If requested | +| **Re-timed Captions (VTT)** | Captions re-synchronised to the accessible video | If accessible video was created | +| **Descriptive Transcript (TXT)** | Full text transcript | If requested | + +Click any **"Download"** button to save that file to your computer. + +#### Important: Download links expire + +> ⚠️ **Download links are only valid for 24 hours.** After 24 hours, the buttons will stop working. If you need to download again after the links expire, contact your administrator to regenerate the links. + +#### How to use the files you downloaded + +**VTT caption files:** Upload to your video player, website, or video hosting platform (YouTube, Vimeo, etc.) as a caption/subtitle track. + +**MP3 audio description:** Play this audio alongside the video for visually impaired viewers using a screen reader or audio player. + +**Accessible Video (MP4):** Use this as a replacement for your original video. It already includes the narration β€” no separate audio track needed. + +--- + +## 6. Reviewer & Linguist Guide + +This section explains the QC Review and Final Review workflows for **Reviewers**, **Linguists**, and **Production** users. + +--- + +### 6.1 QC Review Queue + +The **QC Review** queue shows all jobs that have been processed by AI and are waiting for a human to check the English captions and audio descriptions. + +#### How to access the QC Queue + +Click **"QC Review"** in the left sidebar. + +#### What you see in the QC Queue + +Each job in the queue shows: +- **Job title** β€” the name given by the client +- **Status badge** β€” should be "Pending QC" (yellow) +- **Source filename** β€” the original video filename +- **Duration** β€” length of the video +- **Creation date** β€” when the job was submitted +- **Output badges** β€” which outputs were requested: `Captions`, `AD Script`, `AD Audio`, `Languages` + +#### Selecting jobs for bulk actions + +Tick the **checkbox** on the left of each job row to select it. + +Once jobs are selected, two bulk action buttons appear at the top: +- **"Approve Selected"** β€” Approve multiple jobs at once. A dialog box will ask for optional notes. +- **"Reject Selected"** β€” Reject multiple jobs. A dialog box will ask for required notes explaining the issues. + +> ⚠️ Use bulk approval carefully. It should only be used for jobs you have already individually reviewed. + +#### Opening a job for review + +Click on a job row (anywhere except the checkbox) to open the full **QC Detail** review page. + +--- + +### 6.2 QC Review β€” Detailed Walkthrough + +The **QC Detail** page is your main tool for reviewing AI-generated content. Here is a complete walkthrough of every element on the page. + +#### Top of the page + +**Back button (← QC Queue):** Click to return to the QC queue without making any changes. + +**Job title:** Shows the name of the job you are reviewing. + +**Language selector:** If the job has multiple languages, a row of language tabs appears at the top. Click a language code (e.g., `EN`, `ES`, `FR`) to switch to that language's content. + +> Currently, QC Review focuses on English content. You will typically work in English here, and then Final Review covers the other languages. + +--- + +#### View Mode buttons + +Three buttons let you change how you view the content: + +| Button | What it shows | +|--------|--------------| +| **Side-by-side** | Video on the left, editors on the right β€” this is the default and recommended mode | +| **Video only** | Full-width video player, editors hidden | +| **Editor only** | Full-width editors, video hidden | + +Use **Side-by-side** most of the time. Switch to **Video only** when you want to watch the full video without distraction. Switch to **Editor only** when you are doing intensive text editing. + +--- + +#### Video toggle + +Below the view mode buttons, a toggle lets you switch between: +- **Original Video** β€” the raw video uploaded by the client +- **Accessible Video** β€” the processed version with audio descriptions (only available if the job has been rendered) + +Click the toggle to switch between them. + +--- + +#### Video Player + +The integrated video player works like a standard web video player: + +| Control | What it does | +|---------|-------------| +| **Play/Pause button (β–Ά / ⏸)** | Start or stop playback | +| **Progress bar** | Click anywhere on the bar to jump to that point in the video. Drag the handle to scrub. | +| **Volume icon** | Click to mute/unmute. Drag the volume slider to adjust | +| **Fullscreen button** | Click to expand to full screen. Press Esc to exit | +| **Caption overlay** | Captions appear automatically over the video as it plays | + +When you click on a **cue** (a line) in the VTT editor on the right, the video will jump to that exact moment, so you can watch and verify. + +--- + +#### Timeline Preview + +Below the video player, a visual timeline shows coloured segments representing the video: +- **Blue segments** β€” sections with captions +- **Orange segments** β€” sections with audio descriptions +- **Grey segments** β€” silent sections with no captions or descriptions + +Click on any segment in the timeline to jump the video to that position. + +--- + +#### Captions VTT Editor + +The left editor (or top editor in some layouts) shows the **Closed Captions** VTT content. + +Each line of text in the editor represents one **cue** β€” a single caption that appears on screen at a specific time. + +**What you see in the editor:** +- **Timestamp** on the left (e.g., `00:00:05.200 β†’ 00:00:08.400`) β€” the time the caption appears and disappears +- **Text content** β€” what the AI transcribed + +**Editing caption text:** +1. Click on the text of any cue to place your cursor there +2. Type to correct mistakes, fix spellings, change punctuation, etc. +3. Changes are saved automatically β€” you do not need to click Save after every edit + +**Editing timestamps directly:** +1. Click on the timestamp of a cue (e.g., `00:00:05.200`) +2. The timestamp becomes an editable field β€” type the new time value +3. Press **Enter** or click elsewhere to confirm the change + +**Adding a new cue:** +Each cue has two small buttons: +- **"Insert Before"** β€” click to add a new blank cue before this one +- **"Insert After"** β€” click to add a new blank cue after this one + +The new cue appears with placeholder timestamps. Click on the text area and type the caption text. + +**Deleting a cue:** +Each cue has a **delete button** (trash icon). Click it. A **confirmation dialog** will ask "Are you sure you want to delete this cue?" Click **"Delete"** to confirm. This cannot be undone. + +**What to look for:** +- Incorrect words (mishearing by AI) +- Missing punctuation +- Wrong speaker attribution +- Technical terms spelled incorrectly +- Timing that does not match what is said on screen + +--- + +#### Audio Description VTT Editor + +The right editor (or bottom editor) shows the **Audio Description Script** β€” the text describing what is happening visually on screen. + +Editing works the same way as the Captions editor. + +**What to look for:** +- Missing descriptions for key visual elements (on-screen text, character actions, scene changes) +- Inaccurate descriptions +- Descriptions that overlap with dialogue (a description should not play over important speech) +- Descriptions that are too long for the time gap available + +--- + +#### Timing Adjustment Tool + +Found below the editors, this tool lets you shift all caption or audio description timings forward or backward. + +**When to use it:** If all captions are consistently 2 seconds late or 1 second early, use this to fix them all at once rather than editing each cue individually. + +**How to use it:** +1. Click on the **"Captions"** or **"Audio Descriptions"** tab to select which VTT to adjust +2. Use the quick-adjust buttons for common shifts: + - **"βˆ’1s"** β€” move all timestamps 1 second earlier + - **"βˆ’0.5s"** β€” move all timestamps 0.5 seconds earlier + - **"+0.5s"** β€” move all timestamps 0.5 seconds later + - **"+1s"** β€” move all timestamps 1 second later +3. For a custom amount, type a value (in **seconds**) in the number field β€” positive = later, negative = earlier. For example, `2` = 2 seconds later, `-1.5` = 1.5 seconds earlier. The allowed range is βˆ’30 to +30 seconds. +4. Click **"Apply Timing Adjustment"** + +The editor will update all timestamps immediately. + +--- + +#### Voice Settings (collapsible section) + +Click **"Voice Settings"** to expand this panel. Here you can change the TTS configuration for this specific job, overriding the defaults. + +Settings are the same as described in [Section 5.3](#53-understanding-voice--tts-settings). + +Changes here will be applied if TTS is regenerated. + +--- + +#### Accessible Video Controls (Pause Points) + +If the job requested an Accessible Video with the **"Pause & Insert"** method, a **"Accessible Video Controls"** section appears. + +This shows a list of **pause points** β€” specific timestamps where the video will pause to allow the audio description to play. + +**Adjusting pause points:** + +Each pause point shows a timestamp. Click on the timestamp field to edit it directly β€” type the new value and press **Enter** or click away to save. Changes take effect immediately (there is no separate Save button for individual pause points). + +The pause duration is automatically calculated based on how long the description takes to speak at the selected TTS speed. + +**Whisper Refinement:** + +A checkbox labelled **"Run Whisper pause refinement"** appears near the pause points controls. + +When ticked, the system uses a secondary AI pass (Whisper) to more precisely align pause points with natural speech pauses in the original audio. This can improve the smoothness of the paused accessible video. + +Tick this checkbox **before** clicking "Render Changes" if you want this refinement applied. + +**Applying changes to the accessible video:** + +After editing captions, audio descriptions, voice settings, or pause points, click the **"Render Changes"** button to generate an updated accessible video with your changes applied. + +A status message will show while rendering: "Rendering in progress…" This can take 1–5 minutes. + +--- + +#### Pending TTS Regenerations + +This panel (if visible) shows a list of audio description cues that have been edited but not yet synthesised into new audio. + +This is informational β€” it shows you that when you approve the job, the system will automatically regenerate audio for the changed cues. + +--- + +#### Manual VTT Upload + +If you have an externally-edited VTT file that you want to use instead of the AI-generated one, click **"Upload VTT"**. + +A file picker will open. Select your `.vtt` file. The editor will be replaced with the content from your uploaded file. + +--- + +#### Cost Tracker Project ID + +Near the bottom of the page, there is a small field labelled **"Cost Tracker Project ID"**. + +This is an optional field. If your organisation uses the Oliver AI Cost Tracker system, you can enter the project ID here to associate the AI processing costs of this job with a specific project. + +**How to use it:** +1. Type the project ID in the field (e.g., `PROJ-2026-001`) +2. Click the **"Save"** button next to the field +3. A green **"Saved"** indicator will confirm the value was stored + +If you do not use Cost Tracker, leave this field empty. + +--- + +#### Review Notes + +At the bottom of the page, a **"Review Notes"** text area lets you type notes about your review. + +- For **Approve**: Notes are optional but recommended β€” document what you checked and any changes you made +- For **Reject**: Notes are **required** β€” you must explain why you are rejecting the job so the client understands the issue + +--- + +#### Approve or Reject buttons + +Two large buttons at the bottom: + +**"Approve [LANG] Version" (green button)** + +The exact button label shows the language being approved, for example **"Approve EN Version"**. + +- Click when you are satisfied with the content +- **No confirmation dialog appears** β€” the approval is immediate +- The job status changes and the translation/TTS pipeline starts automatically +- You are redirected back to the QC queue + +**"Reject" (red button)** +- Click when the content has significant issues that cannot be fixed by editing +- An **inline rejection form** expands directly on the page (no pop-up dialog) +- A **required** notes field appears β€” type a clear explanation of the problem (e.g., "Audio quality too poor for accurate transcription β€” please re-record") +- Click **"Confirm Rejection"** to submit +- The job returns to AI processing for a retry +- You are redirected back to the QC queue + +--- + +### 6.3 Final Review Queue + +The **Final Review** queue shows jobs that have completed translation and TTS generation and are ready for a final check before delivery to the client. + +#### How to access the Final Review queue + +Click **"Final Review"** in the left sidebar. + +#### What you see in the Final Review queue + +The queue is divided into two sections: + +**Pending Final Reviews** β€” jobs waiting for approval: +- Job title +- Status badge (orange "Pending Final Review") +- Language count +- Creation date +- Checkbox for bulk actions + +**Recently Completed** β€” jobs you have recently approved (shown for reference). + +#### Bulk actions + +Select jobs using checkboxes, then use the bulk action buttons: +- **"Complete Selected"** β€” Approve multiple jobs for client delivery +- **"Return to QC"** β€” Send multiple jobs back to QC (requires notes in dialog) + +#### Opening a job for final review + +Click on any job row to open the **Final Detail** page. + +--- + +### 6.4 Final Review β€” Detailed Walkthrough + +The **Final Detail** page lets you verify all translated language versions of the job before approving delivery. + +#### Language cards + +The main content area shows a row of **Language Cards** β€” one card per language (including English). + +Each card shows: +- **Language name** (e.g., Spanish, French, German) +- **Origin badge:** + - `Original` β€” English source content + - `Translated` β€” standard machine translation + - `Transcreated` β€” AI-adapted cultural translation +- **Quality Issues badge** (yellow warning) β€” if any QA notes are present from the TTS system, a warning badge will appear. Click the badge to read the issue details. +- **Asset status row:** + - βœ“ or βœ— for **Captions** + - βœ“ or βœ— for **Audio Descriptions** + - βœ“ or βœ— for **MP3 Audio** + +#### Reviewing a language + +Click on a language card to expand it and review its content. + +Inside the card you will see: + +**Tab toggle: "Captions" / "Audio Descriptions"** +Click to switch between viewing the caption VTT and the audio description VTT. + +**Read-only VTT preview:** +The VTT content is shown in a read-only viewer (cannot be edited here). You can read through the translated content to check for obvious errors. + +> **Note:** The Final Review page does not allow editing. If you find issues that need fixing, use the **"Return to QC"** option and describe the required changes in the notes. + +**Audio player:** +If an MP3 audio file is available, an audio player will appear. Click **Play (β–Ά)** to listen to the TTS narration. Check for: +- Natural pronunciation +- Correct language +- Audio quality issues +- Mispronounced brand names or technical terms + +#### Cost Tracker Project ID + +Same as in QC Review β€” optional field to associate costs with a project. Type the ID and click Save. + +#### Final Review Notes + +Type any observations about your review in the notes field. + +- For **Approve**: Notes are optional +- For **Return to QC**: Notes are **required** β€” describe exactly what needs to be fixed and in which language + +#### "Approve for Client Delivery" (green button) + +Click when all language versions are acceptable. + +- **No confirmation dialog appears** β€” the approval is immediate +- Job status changes to **Completed** (green) +- Client receives an email notification with download links +- The job moves to the "Recently Completed" section of the Final Review queue + +#### "Return for QC" (button) + +Click when issues need to be fixed before delivery. + +An **inline form** expands on the page (no pop-up dialog). A **required** notes field appears β€” describe exactly what needs to be fixed and in which language. Click **"Confirm Return"** to submit. + +What happens: +- Job status changes back to **Pending QC** +- The job reappears in the QC Review queue +- A WebSocket notification is sent to the QC team + +--- + +## 7. Production User Guide + +**Production** users have the same capabilities as Reviewers, plus additional job management tools. + +Production users can: +- Upload videos (same as Clients) +- Perform QC Review and Final Review (same as Reviewers) +- Use all bulk job actions on the All Jobs page + +See [Section 5](#5-client-guide) for uploading and [Section 6](#6-reviewer--linguist-guide) for reviewing. + +For bulk job actions, see [Section 8.6](#86-bulk-job-operations). + +--- + +## 8. Admin Guide + +**Admins** have full access to all features, including User Management. + +--- + +### 8.1 User Management β€” List View + +#### How to access User Management + +Click **"User Management"** in the left sidebar (Admin only). + +#### What you see on the User List page + +At the top right, a blue **"AI Cost Dashboard"** button links to the external cost tracking system (opens in a new tab). + +**Filter options:** + +| Filter | How to use it | +|--------|--------------| +| **Role dropdown** | Click to filter by: All / Admin / Production / Reviewer / Linguist / Client | +| **"Active users only" checkbox** | Tick to hide deactivated accounts | + +**The user table columns:** + +| Column | What it shows | +|--------|--------------| +| **User** | Full name and email address | +| **Role** | Their role badge (colour coded) | +| **Auth Method** | "Microsoft" (SSO) or "Local" (email/password) | +| **Status** | Active (green) or Inactive (grey) | +| **Created** | When the account was created | +| **Actions** | Edit button (pencil icon) and additional action buttons | + +**Action buttons per row:** +- **Edit (pencil icon)** β€” Opens the User Detail page to edit this user +- **Reset Password** β€” Only shown for Local auth users. Generates a new temporary password. +- **Deactivate** β€” Only shown for active users. Deactivates the account. + +**Pagination:** +At the bottom of the table, you will see page numbers. Click them to navigate through users. Each page shows 20 users. + +#### Creating a new user + +Click the **"Create User"** button (top right of the page). + +A modal dialog box slides open. See [Section 8.2](#82-creating-a-new-user) for full instructions. + +--- + +### 8.2 Creating a New User + +Click **"Create User"** on the User Management page. A modal dialog appears with these fields: + +**Email address** (required) +Type the user's email address. For Oliver staff who will use Microsoft SSO, this must match their Microsoft account email exactly. + +**Full Name** (required) +Type the user's full name as it should appear in the system. + +**Password** (required) +Type a temporary password for the user. They should change it after first login. +Password rules: Minimum 8 characters. Use a mix of letters, numbers, and symbols. + +**Role** (required) +Select from the dropdown: + +| Role | Select for... | +|------|--------------| +| **Admin** | Platform administrators who need full access | +| **Production** | Internal production team members who upload and review | +| **Reviewer** | Quality control reviewers checking English content | +| **Linguist** | Language specialists reviewing translations | +| **Client** | Clients who upload videos and download results | + +> **Note about Microsoft SSO:** If the user will log in with their Microsoft account (oliver.agency email), you still need to create their account here first. The system will recognise their Microsoft email and use SSO automatically when they log in. The password you enter here will not be needed for Microsoft users. + +Click **"Create User"** to save. The dialog closes and the new user appears in the list. + +--- + +### 8.3 Editing a User + +Click the **Edit** (pencil) icon next to any user, or click on their name. + +You are taken to the **User Detail** page with these sections: + +#### Edit User Form + +| Field | What you can change | +|-------|-------------------| +| **Email** | Change the email address. For Microsoft users, this must match their Microsoft account. | +| **Full Name** | Update the display name | +| **Role** | Change their permission level | +| **Active User** | Toggle off to deactivate, toggle on to reactivate | + +The **"Save Changes"** button only becomes clickable when you have actually changed something. This prevents accidental saves. + +Click **"Save Changes"** to apply. + +#### User Information card (right side) + +Shows read-only information: +- **User ID** β€” internal system identifier +- **Auth Method** β€” Microsoft or Local +- **Created date** β€” when the account was first made +- **Current status** β€” Active or Inactive + +#### Actions card (right side) + +**If the user uses Local (email/password) authentication:** +A **"Reset Password"** button appears. Click it to generate a new temporary password. A dialog box will show the temporary password β€” copy it and send it to the user securely. They must change it on their next login. + +**If the user uses Microsoft authentication:** +An informational banner explains that their password is managed by Microsoft and cannot be reset here. + +--- + +### 8.4 Resetting a User Password + +For **Local authentication users only** (not Microsoft SSO users). + +**Option A β€” From the User List:** +Find the user in the list, then click the **"Reset Password"** button in their row (only visible for Local auth users). + +**Option B β€” From User Detail:** +Open the user's detail page. In the **Actions** card, click **"Reset Password"**. + +In both cases, a **toast notification** appears in the corner of the screen with the message: +**"Password reset. Temporary password: [new-password]"** + +Copy the temporary password from the toast before it disappears. Send it to the user through a secure channel (e.g., direct message or secure email). + +Tell the user to log in with this temporary password and change it immediately. + +--- + +### 8.5 Deactivating a User + +Deactivating a user prevents them from logging in but preserves all their historical data and jobs. + +**Option A β€” From the User List:** +Find the user, then click the **"Deactivate"** button in their row. + +**Option B β€” From User Detail:** +Open the user's detail page. Untick the **"Active User"** checkbox. Click **"Save Changes"**. + +To **reactivate** a deactivated user: +- From the list: Remove the "Active users only" filter tick, find the user, click **"Activate"** +- From detail page: Tick the **"Active User"** checkbox and save + +--- + +### 8.6 Bulk Job Operations + +Admins and Production users can perform bulk operations from the **All Jobs** page. + +#### How to select jobs for bulk operations + +On the **All Jobs** page, tick the **checkbox** in the first column of each job row you want to include. + +A **bulk actions toolbar** appears at the top of the table showing how many jobs are selected and offering action buttons. + +#### Available bulk operations + +**Delete** (Admin only) +- Permanently deletes selected jobs and all associated files (video, VTT, MP3, etc.) +- A confirmation dialog shows you exactly what will be deleted and asks you to type "DELETE" to confirm +- ⚠️ This is irreversible. All files in Google Cloud Storage are also deleted. + +**Reprocess** (Admin only) +- Resets selected jobs back to "Created" status and re-triggers the full AI processing pipeline +- Use this when: AI generated incorrect content, processing got stuck, or AI prompts have been updated +- A confirmation dialog will explain what will happen +- Existing VTT files will be overwritten + +**Download All Files** +- Generates signed download URLs for all assets across the selected jobs +- Useful for bulk exporting completed jobs + +**Return to QC** +- Moves selected jobs back to "Pending QC" status +- A dialog appears asking for required notes explaining why the jobs are being returned +- These notes will be visible to reviewers in the QC queue + +--- + +## 9. All Jobs List + +The **All Jobs** page is accessible to all users via **"All Jobs"** in the sidebar. + +**What you see:** + +- A table of jobs you have access to (your own jobs for Clients; all jobs for Reviewers and above) +- Pagination at the bottom (20 jobs per page) +- A WebSocket connection status indicator (top right) + +**Filters (top of page):** + +| Filter | How to use | +|--------|-----------| +| **Search box** | Type any part of the job name, filename, or submitting user's name. Results update as you type. | +| **Created By** | Click the dropdown to filter by a specific user (shows all users if you are Admin or Reviewer) | +| **Status** | Click to filter by a specific job status (e.g., show only Completed jobs) | +| **Date From / Date To** | Pick a date range to see only jobs created within those dates | + +**Table columns:** + +| Column | What it shows | +|--------|--------------| +| **Job Name** | The title given by the client. A small TTS icon appears if TTS was generated. | +| **Created By** | The user who submitted the job | +| **Date Created** | When the job was submitted | +| **Languages** | Number of target languages requested | +| **Status** | Current status badge | +| **Actions** | Context-sensitive buttons based on current status | + +**Clicking on a job row** (anywhere except the checkbox) opens the **Job Detail** page. + +**Action buttons that may appear in the Actions column:** + +| Button | When it appears | What it does | +|--------|----------------|-------------| +| **Review** | When status = Pending QC | Opens QC Review for this job | +| **Final Review** | When status = Pending Final Review | Opens Final Review for this job | +| **Download** | When status = Completed | Opens the Download page | +| **Delete** | Always (for job owner or Admin) | Deletes this job (requires confirmation) | + +--- + +## 10. Job Detail Page + +Click on any job title to open the **Job Detail** page. This page gives you a complete picture of one job. + +### Top section + +**Job title** β€” the name of the job. + +**Status badge** β€” current status with colour coding. + +**Progress Indicator** (right side panel) β€” A vertical timeline showing each step in the processing workflow. Completed steps show a green tick, the current step shows a spinning indicator, and future steps are grey. + +Steps shown: +1. Created +2. Ingesting +3. AI Processing +4. Pending QC +5. Translation +6. TTS Generation +7. Rendering Video +8. Pending Final Review +9. Completed + +### Tabs + +**Overview tab:** +- **Job Information card:** Original filename, source language, requested output languages, output types requested +- **Review Notes card:** A timeline of all status changes with notes from reviewers at each step +- **Download Section:** (if Completed) Download buttons for all files + +**Video Preview tab:** +- Full video player showing the source video +- Caption overlay enabled +- Click play to watch with captions + +**Assets tab:** +- A grid of language cards +- Each card shows which assets are available: Captions Ready (green), AD Ready (green), MP3 Ready (green), or pending (grey) + +**History tab:** +- A full processing timeline with timestamps +- Shows every status change, who made it, and any notes + +### Error display + +If a job encountered an error, a red **Error** panel appears on the Overview tab showing: +- Error type and message +- Which processing step failed +- For TTS failures: which specific cues failed and which languages were affected + +**"Retry TTS Generation" button** β€” If the job status is **TTS Failed**, this button appears. Click it to trigger a new TTS generation attempt. This is useful if TTS failed due to a temporary API error. + +### TTS Rewrites Caution + +If the AI rewrote any audio description text for TTS purposes (e.g., expanding abbreviations or adjusting pronunciation), a yellow **"TTS Rewrites"** panel appears. + +This panel lists each rewrite, showing: +- **Original text** β€” what was in the VTT +- **Rewritten text** β€” what was sent to the TTS engine + +These rewrites are intentional improvements for audio quality and do not affect the VTT files. + +### Return to QC button + +If you have Reviewer permissions and the job is in a state where returning to QC is appropriate, a **"Return to QC"** button appears. Clicking it opens a dialog requiring you to enter notes explaining why you are returning it. + +### Action buttons + +Depending on the job status and your role: +- **"Go to QC Review"** β€” when status is Pending QC (Reviewer role) +- **"Go to Final Review"** β€” when status is Pending Final Review (Reviewer role) +- **"Download Files"** β€” when status is Completed (all users) + +--- + +## 11. Notifications + +The notification system keeps you informed about job progress without needing to refresh any pages. + +### The bell icon + +The **πŸ”” bell icon** is in the top right of every page (in the navigation bar). + +A **red badge** on the bell shows the number of unread notifications. When the number reaches 99, it shows **"99+"**. + +### Opening the notification panel + +Click the bell icon to open the notification dropdown. + +### What you see in the notification panel + +**Header row:** +- **"Mark all as read"** button β€” clears all unread indicators at once +- **"Clear all"** button β€” removes all notifications from your list + +**Notification list:** + +Each notification shows: +- A **colour-coded dot** on the left: + - 🟒 Green = success (job completed, approval) + - πŸ”΅ Blue = information (status update) + - 🟑 Yellow = warning (job rejected, issues found) + - πŸ”΄ Red = error (processing failure) +- **Message text** β€” what happened +- **Job title** (if applicable) β€” which job this is about +- **Timestamp** β€” how long ago (e.g., "5 minutes ago") +- **"View job" link** β€” click to go directly to that job's detail page +- **βœ• close button** (appears on hover) β€” click to remove this notification + +**Blue dot indicator:** A small blue dot on the left of a notification means it is unread. Clicking the notification or "View job" marks it as read. + +**Empty state:** If you have no notifications, you will see an icon and the message "No notifications yet." + +### Closing the notification panel + +Click anywhere outside the panel, or click the bell icon again. + +### What triggers a notification + +You receive a notification when: +- Your job (or any job you have access to) changes to an important status: Pending QC, Completed, Rejected +- An error occurs during processing +- A bulk operation you triggered completes + +--- + +## 12. Connection Status Indicator + +On the **All Jobs** page and **Job Detail** page, you will see a small coloured dot in the corner labelled with a connection status: + +| Colour & text | What it means | +|--------------|--------------| +| 🟒 **Connected** | Live updates are working. Job statuses will update automatically. | +| 🟑 **Connecting…** | The system is trying to establish a live connection. Wait a moment. | +| ⚫ **Disconnected** | No live connection. Refresh the page to reconnect. | +| πŸ”΄ **Error** | Connection failed. Check your internet. Refresh the page. | + +If you see anything other than **Connected**, job statuses will not update automatically. You can still use the page and refresh it manually to see the latest status. + +--- + +## 13. Job Status Reference + +This is a complete list of every possible job status and what it means. + +| Status | Colour | Meaning | Who is responsible | +|--------|--------|---------|-------------------| +| **Created** | βšͺ Grey | Job has been submitted and is waiting to start processing | System (automated) | +| **Ingesting** | πŸ”΅ Blue | System is downloading your video and reading its properties | System (automated) | +| **AI Processing** | πŸ”΅ Blue | Gemini AI is analysing the video and generating captions + audio descriptions | AI (automated) | +| **Pending QC** | 🟑 Yellow | AI processing is complete. A human reviewer needs to check the content | Reviewer / Linguist | +| **QC Feedback** | 🟑 Yellow | Job has been returned to QC with feedback notes after a Final Review check | Reviewer / Linguist | +| **Approved (English)** | 🟒 Green | English content approved. Translations are starting automatically. | System (automated) | +| **Approved (Source)** | 🟒 Green | Source-language content approved (used when video native mode is active). Translations are starting. | System (automated) | +| **Translating** | 🟣 Purple | System is translating captions and audio descriptions to other languages | System (automated) | +| **TTS Generating** | 🟣 Purple | System is creating MP3 audio voiceovers for all languages | System (automated) | +| **Rendering Video** | πŸ”΅ Blue | System is creating the Accessible Video file (if requested) | System (automated) | +| **Pending Final Review** | 🟠 Orange | All processing done. A reviewer must check all language versions before delivery | Reviewer / Linguist | +| **Completed** | 🟒 Green | Job fully approved and ready to download | Client | +| **Rejected** | πŸ”΄ Red | Content was rejected at QC. The system will retry AI processing. | System then Reviewer | +| **TTS Failed** | πŸ”΄ Red | Audio generation encountered an error. A reviewer can trigger a retry from the Job Detail page. | Admin / Reviewer | +| **Render Failed** | πŸ”΄ Red | Accessible Video rendering failed. Contact your administrator to investigate. | Admin | + +--- + +## 14. Tips & Best Practices + +### For Clients β€” Getting the Best Results + +**Video quality matters most.** +The AI transcription is only as good as the audio in your video. Clear dialogue with minimal background noise will produce far more accurate captions than a noisy recording. If your video has very poor audio quality, expect more reviewer correction time. + +**Use brand names field for technical terms.** +The "Brand Names (optional)" field is especially helpful for product names with unusual spellings or pronunciations (e.g., brand names that don't follow standard pronunciation rules), acronyms, and proper nouns. The more context you provide, the fewer corrections will be needed. + +**Choose Audio Overlay when timing is critical.** +If your video has very tight pacing and you cannot afford any inserted pauses, choose the "Audio Overlay" accessible video method. The narration will speak over the video. If compliance requires that descriptions play when no speech is happening, use "Pause & Insert" instead. + +**Video Native Mode gives better translations.** +For marketing or brand content where cultural nuance matters, "Video Native Mode" re-analyses the video in each target language directly instead of translating English word-for-word. The results feel more natural and are worth the slightly longer processing time. + +**Download your files promptly.** +Download links expire after 24 hours. If you need files again later, contact your administrator. + +--- + +### For Reviewers β€” Efficient Review Workflow + +**Always watch the video in Side-by-side mode.** +This mode lets you click on any caption cue and the video immediately jumps to that point. Use it to verify that the timing of each caption matches the spoken audio. + +**Fix timing before text.** +If the captions are consistently offset (all late or all early), use the Timing Adjustment tool first with the Β±0.5s/Β±1s buttons. This is much faster than editing each timestamp individually. + +**Use the audio player in Final Review.** +In the Final Review page, each language has an audio player for the MP3 voiceover. Always listen to at least a sample of each language's audio β€” mispronounced brand names are the most common quality issue and are easy to catch by ear. + +**Reject with specific instructions.** +When rejecting a job, write notes that tell the next reviewer (or the client) exactly what the problem is and how to fix it. Vague notes like "bad quality" are not actionable. Good notes: "Spanish translation of cue at 00:02:15 is grammatically incorrect β€” please re-check." + +**Bulk approve only jobs you've verified.** +The "Approve Selected" bulk button in the QC queue does not warn you to review each job. Only select jobs that you have individually reviewed in their detail pages. + +--- + +### For Admins β€” User & Job Management + +**Use Linguist role for external language reviewers.** +The Linguist role has the same capabilities as Reviewer. Use it for external language specialists or contractors so you can distinguish them from internal QC reviewers in the user list. + +**Reprocess sparingly.** +The Reprocess bulk action re-runs AI processing from scratch, which overwrites all existing VTT content. Only use it when you know the AI output was fundamentally wrong (e.g., wrong language detected, corrupt output). For minor errors, it is faster for a reviewer to edit the VTT. + +**Deactivate, don't delete users.** +When a team member leaves, deactivate their account (do not create a fresh account later). Deactivating preserves all historical audit data and job records. If they return, you can reactivate the same account. + +--- + +## 15. Troubleshooting & FAQ + +### "My video has been 'AI Processing' for more than 30 minutes. What's wrong?" + +Very long videos (45+ minutes) or videos with very complex audio can take up to 1 hour to process. If it has been more than 1 hour: +1. Refresh the page and check if the status has updated +2. Check the Job Detail page β†’ History tab for any error messages +3. Contact your administrator with the Job ID (visible in the URL bar) + +--- + +### "The captions are in the wrong language." + +This happens when the AI detects the wrong source language. The reviewer should: +1. Open QC Review for the job +2. Edit the VTT content to correct language +3. If the entire content is wrong, an Admin should use "Reprocess" from the All Jobs bulk actions + +--- + +### "I approved a job but the client says their download links don't work." + +Download links expire after 24 hours. Ask the client to check when the email was received. If the links are expired: +- An Admin needs to regenerate the download URLs (contact the platform administrator to trigger this manually) + +--- + +### "TTS audio sounds wrong β€” mispronounced words." + +1. In QC Review, open Voice Settings +2. Check the selected voice model β€” try switching from Flash to Pro for better quality +3. For brand names, verify they are listed in the "Brand Names" field +4. Approve the job and trigger a TTS regeneration by using the voice settings change + +If a specific language's audio quality is consistently poor, contact your administrator to try a different voice for that language. + +--- + +### "I can't find a job in the QC queue." + +Possible reasons: +- The job may still be in "AI Processing" β€” it hasn't reached QC yet. Check All Jobs. +- The job may have been approved and moved forward already. Use the Status filter on All Jobs to check "Approved (English)" or later statuses. +- You may not have the Reviewer role required to see the QC queue. Check with your administrator. + +--- + +### "The video player won't load." + +Try these steps in order: +1. Refresh the page (F5 or Cmd+R) +2. Check your internet connection +3. Try a different browser (Chrome or Firefox recommended) +4. Check that you are not on a VPN that blocks Google Cloud Storage domains + +If the issue persists, the signed URL may have expired β€” contact your administrator. + +--- + +### "I accidentally approved a job β€” can I undo it?" + +There is no automatic undo for approvals. However: +- An Admin or Production user can use the **"Return to QC"** bulk action on the All Jobs page to move the job back to Pending QC +- From Job Detail, click "Return to QC" if that button is visible for your role + +--- + +### "The WebSocket shows 'Disconnected' β€” is the platform down?" + +The platform itself may still be fully functional. WebSocket disconnections are usually caused by: +- Brief network interruptions +- Browser tab going to sleep (on mobile or energy-saving mode) +- Firewall or proxy settings blocking WebSocket connections + +Try refreshing the page. If the issue persists consistently, contact your administrator. You can still use all features β€” the page simply won't auto-update statuses until reconnected. + +--- + +## 16. Glossary + +| Term | Definition | +|------|-----------| +| **AD** | Audio Description β€” narrated text describing visual content for blind/low-vision viewers | +| **Accessible Video** | A video file with audio descriptions embedded as a narration track | +| **AI Processing** | Automatic content generation by the Gemini 2.5 Pro AI model | +| **Cue** | A single subtitle entry in a VTT file with a start time, end time, and text | +| **ElevenLabs** | A premium AI text-to-speech service used for high-quality voice generation | +| **Final Review** | The second human review stage, where all translated content is checked before delivery | +| **GCS** | Google Cloud Storage β€” where all video and output files are stored securely | +| **Gemini** | Google's AI model used for video analysis and content generation | +| **Job** | A single video upload with all its associated processing, outputs, and history | +| **Linguist** | A user role for language specialists who review translated content | +| **MP3** | Audio file format used for the audio description voiceover | +| **MP4** | Video file format used for uploaded source videos and accessible video outputs | +| **QC** | Quality Control β€” human review of AI-generated content | +| **SDH Captions** | Subtitles for the Deaf and Hard of Hearing β€” includes speaker names, sound effects, and music notation | +| **Signed URL** | A time-limited secure link to download a file from cloud storage (valid for 24 hours) | +| **Status Badge** | Coloured label showing the current stage of a job | +| **TTS** | Text-to-Speech β€” technology that converts written text into spoken audio | +| **Transcreation** | AI-assisted cultural adaptation of translated content (more natural than direct translation) | +| **VTT** | WebVTT β€” the standard file format for video captions and subtitles | +| **WebSocket** | The technology that enables real-time status updates without page refreshes | +| **WCAG** | Web Content Accessibility Guidelines β€” international standards for digital accessibility | + +--- + +## Appendix A β€” Keyboard Shortcuts (QC Review Page) + +| Shortcut | Action | +|---------|--------| +| **Ctrl + S** | Save current VTT changes | +| **Ctrl + Enter** | Confirm current action | +| **1** | Switch to Side-by-side view | +| **2** | Switch to Video-only view | +| **3** | Switch to Editor-only view | +| **Spacebar** | Play/Pause video (when video is focused) | + +--- + +## Appendix B β€” Supported Languages + +The platform supports translation and TTS generation for the following languages (and many more via Google Translate): + +**European:** English, Spanish, French, German, Italian, Portuguese, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Greek, Turkish + +**Asian:** Japanese, Korean, Simplified Chinese, Traditional Chinese, Thai, Vietnamese, Indonesian, Malay + +**Middle East / Other:** Arabic, Hebrew, Hindi, Ukrainian, Russian + +> TTS (audio generation) is supported for most major languages. Contact your administrator if you need a language that is not generating audio. + +--- + +## Appendix C β€” File Naming Convention + +When you download files, they are named using this pattern: + +``` +{JobTitle}_{language}_{type}.{extension} +``` + +**Examples:** +- `Q4_Training_en_captions.vtt` β€” English closed captions +- `Q4_Training_es_captions.vtt` β€” Spanish closed captions +- `Q4_Training_en_ad.vtt` β€” English audio description script +- `Q4_Training_en_ad.mp3` β€” English audio description audio +- `Q4_Training_fr_accessible.mp4` β€” French accessible video +- `Q4_Training_source.mp4` β€” Original uploaded video + +--- + +*End of User Guide* + +**Video Accessibility Platform v3.0 | April 2026** +*For technical support or questions, contact your platform administrator.* diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..b8bf76f --- /dev/null +++ b/tests/README.md @@ -0,0 +1,122 @@ +# Tests β€” Accessible Video Processing Platform + + + +## Test Commands + +### Backend + +| Command | Description | +|---------|-------------| +| `cd backend && poetry run pytest` | Run all unit tests | +| `cd backend && poetry run pytest -v` | Verbose output | +| `cd backend && poetry run pytest tests/unit/test_security.py` | Single file | +| `cd backend && poetry run pytest -k "test_jwt"` | Keyword filter | +| `docker compose exec api python -m pytest` | Tests inside Docker container | +| `docker compose exec api python -m pytest --cov=app` | With coverage report | + +### Frontend + +| Command | Description | +|---------|-------------| +| `cd frontend && npm run test` | Vitest unit tests (watch mode) | +| `cd frontend && npm run test:run` | Vitest single run | +| `cd frontend && npm run test:coverage` | Coverage report | +| `cd frontend && npm run test:e2e` | Playwright E2E tests | +| `cd frontend && npx playwright test --ui` | Playwright UI mode | + +### Lint and Type Check (must pass before commit) + +| Command | Description | +|---------|-------------| +| `cd backend && ruff check .` | Python linting | +| `cd backend && poetry run mypy app/` | Python type checking | +| `cd frontend && npm run lint` | ESLint | +| `cd frontend && npm run type-check` | TypeScript compile check | + +--- + +## Test Structure + +### Backend (`backend/tests/`) + +| Directory | Purpose | Framework | +|-----------|---------|-----------| +| `tests/unit/` | Business logic unit tests | pytest | +| `tests/fixtures/` | VTT and JSON test fixtures | β€” | +| `tests/integration/` | FastAPI TestClient route tests | pytest (does not exist yet) | +| `conftest.py` | Shared fixtures | pytest (does not exist yet) | + +### Frontend (`frontend/src/` and `frontend/tests/`) + +| Directory | Purpose | Framework | +|-----------|---------|-----------| +| `src/**/__tests__/` | Component unit tests | Vitest + RTL | +| `src/hooks/__tests__/` | Hook tests | Vitest + RTL | +| `src/lib/__tests__/` | Utility tests | Vitest | +| `src/test/utils.tsx` | Shared test utilities | RTL | +| `tests/e2e/` | End-to-end specs | Playwright | +| `tests/helpers/auth.ts` | Auth fixture (exists, not yet wired) | Playwright | + +--- + +## Coverage Targets + +| Layer | Target | Current | +|-------|--------|---------| +| Backend unit | 80% line coverage on services/ | ~3% (critically low) | +| Frontend unit | 70% branch coverage on hooks/ | ~12% | +| E2E | All happy paths for QC workflow | 0% (tests skipped) | + +--- + +## Writing New Tests + +### Backend Unit Test Pattern + +```python +# Use AsyncMock for async service methods +from unittest.mock import AsyncMock, patch + +async def test_something(): + with patch('app.services.gemini.settings') as mock_settings: + mock_settings.gemini_api_key = "test-key" + # test body +``` + +Wait β€” **move the settings mock to a shared fixture in conftest.py**. See anti-patterns in [testing-strategy.md](../docs/reference/guides/testing-strategy.md). + +### Frontend Hook Test Pattern + +Use `renderHook` from `@testing-library/react` wrapped with `test/utils.tsx` providers. + +### E2E Auth Pattern + +Use `tests/helpers/auth.ts` in `beforeEach`: + +```typescript +// Wire auth fixture before un-skipping any test +test.beforeEach(async ({ page }) => { + await loginAs(page, 'reviewer'); +}); +``` + +--- + +## Known Issues + +| Issue | Impact | Fix needed | +|-------|--------|-----------| +| `conftest.py` missing | All tests define fixtures inline | Create shared conftest with MockSettings, mock_db | +| E2E tests mostly skipped | Zero E2E coverage | Implement auth + seed fixtures | +| `MagicMock` used for async services | May silently pass on sync mocks | Replace with `AsyncMock` | +| Hardcoded `test-job-123` in E2E | Tests would fail if un-skipped | Use seed fixtures | + +--- + +## Maintenance + +**Update triggers:** New test file added, test framework version changed, new command available. +**Verification:** All commands in the Commands section execute without error on a clean checkout. + +