1 line
34 KiB
JSON
1 line
34 KiB
JSON
{"repo": "baocoder288203/learn-now-nodejs", "files": {"README.md": {"7fe4b149c501e8a76a93c28cec2404e0a65af23c": {"t": 1785746715.670407, "n": 7633, "h": "6746a5038270e0eb", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `ALIBABA_API_KEY` (Alibaba Model Studio / Qwen) and/or `OPENAI_API_KEY` / `GEMINI_API_KEY` for admin AI import\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\n# Chỉnh learn-now-nodejs/.env — set ALIBABA_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY cho admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `AI_PROVIDER` | `auto` (default), `alibaba`, `deepseek`, `openai`, or `gemini` |\n| `AI_PROVIDER_ORDER` | Comma-separated fallback order; default `alibaba,deepseek,openai,gemini` |\n| `DEEPSEEK_API_KEY` | [DeepSeek API](https://api-docs.deepseek.com/) (OpenAI-compatible) |\n| `DEEPSEEK_BASE_URL` | Default `https://api.deepseek.com` |\n| `DEEPSEEK_MODEL` | Default `deepseek-v4-flash`; fallback `deepseek-v4-pro` via `DEEPSEEK_MODEL_FALLBACKS` |\n| `DEEPSEEK_MAX_OUTPUT_TOKENS` | Default `8192` |\n| `ALIBABA_API_KEY` | Alibaba Cloud Model Studio (Qwen), OpenAI-compatible endpoint |\n| `ALIBABA_BASE_URL` | Workspace compatible-mode URL (from Model Studio console) |\n| `ALIBABA_MODEL` | Default `qwen-plus`; vision: `ALIBABA_VISION_MODEL` (`qwen-vl-plus`) |\n| `OPENAI_API_KEY` | OpenAI for TOEIC import (fallback in `auto`) |\n| `OPENAI_MODEL` | Optional; default `gpt-4o-mini` |\n| `OPENAI_MODEL_FALLBACKS` | Comma-separated; default `gpt-4o,gpt-4.1` |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `GEMINI_API_KEY` | Google Gemini (fallback in `auto`) |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n| `PYMUPDF_URL` | PyMuPDF sidecar (`http://pymupdf:8081` in Docker Compose) |\n| `USE_PYMUPDF_PIPELINE` | `true` (default) — rule-based TOEIC import qua PyMuPDF |\n| `PYMUPDF_TIMEOUT_MS` | Optional; timeout gọi sidecar (default `120000`) |\n| ~~`MARKITDOWN_URL`~~ | *(deprecated)* MarkItDown sidecar — không dùng nữa |\n| ~~`MARKITDOWN_TIMEOUT_MS`~~ | *(deprecated)* |\n| `AI_ENABLE_STREAMING` | `true` (default) — stream JSON; on truncate, handoff partial sang provider kế |\n| `HEADROOM_BASE_URL` | Headroom proxy (`http://headroom:8787` trong Compose; `http://127.0.0.1:8787` nếu API dev + proxy Docker). Để trống hoặc `HEADROOM_ENABLED=false` = tắt nén |\n| `HEADROOM_MIN_CHARS` | Chỉ nén prompt text ≥ N ký tự (default `2000`). Vision (KEY RC ảnh) không nén |\n| `ALIBABA_MAX_OUTPUT_TOKENS` | Default `8192` (tránh lỗi Qwen max_tokens) |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Default `16384` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Default `8192` |\n\n### Import pipeline checkpoint + resume\n\nTOEIC import lưu tiến độ từng bước trong `IngestionDraft.pipelineState` (extract → RC key → Listening 1–4 → Reading 5–7 → save). Khi job `FAILED`, gọi lại chỉ các bước chưa `done` — không parse lại Part đã xong.\n\n```bash\n# Tiếp tục job sau khi hết quota AI\nPOST /api/admin/import-jobs/:jobId/resume\n```\n\nLog: `[Pipeline] step=parse_listening_2 status=skip` (đã checkpoint) hoặc `status=run`.\n\n### Docker Compose (API + Postgres + Redis + PyMuPDF + Headroom)\n\n```bash\ndocker compose up -d --build\n```\n\n| Service | Port | Mô tả |\n|---------|------|--------|\n| `api` | 4000 | Node.js API |\n| `pymupdf` | 8081 | Python sidecar — layout/text/clip PDF |\n| `headroom` | 8787 | Context compression proxy — nén prompt text trước Alibaba/OpenAI |\n| `db` | 5432 | PostgreSQL |\n| `redis` | 6379 | Redis |\n| ~~`markitdown`~~ | ~~8080~~ | *(deprecated)* Python MarkItDown sidecar |\n\nChỉ chạy sidecar (dev local, API bằng `npm run dev`):\n\n```bash\ndocker compose up -d pymupdf headroom\n# .env: PYMUPDF_URL=http://localhost:8081\n# .env: HEADROOM_BASE_URL=http://127.0.0.1:8787\n```\n\n<!-- MarkItDown deprecated:\ndocker compose up -d markitdown headroom\n# .env: MARKITDOWN_URL=http://localhost:8080\n-->\n\nHoặc proxy Headroom riêng: `docker run -d --name headroom -p 8787:8787 ghcr.io/chopratejas/headroom:latest`\n\n<!-- MarkItDown deprecated: set MARKITDOWN_URL= và bỏ depends_on markitdown -->\n\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n.\n"}, "a01689c5c72bc1cb79e2cc2b7fce8ab459ead732": {"t": 1785767224.6718392, "n": 7633, "h": "6746a5038270e0eb", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `ALIBABA_API_KEY` (Alibaba Model Studio / Qwen) and/or `OPENAI_API_KEY` / `GEMINI_API_KEY` for admin AI import\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\n# Chỉnh learn-now-nodejs/.env — set ALIBABA_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY cho admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `AI_PROVIDER` | `auto` (default), `alibaba`, `deepseek`, `openai`, or `gemini` |\n| `AI_PROVIDER_ORDER` | Comma-separated fallback order; default `alibaba,deepseek,openai,gemini` |\n| `DEEPSEEK_API_KEY` | [DeepSeek API](https://api-docs.deepseek.com/) (OpenAI-compatible) |\n| `DEEPSEEK_BASE_URL` | Default `https://api.deepseek.com` |\n| `DEEPSEEK_MODEL` | Default `deepseek-v4-flash`; fallback `deepseek-v4-pro` via `DEEPSEEK_MODEL_FALLBACKS` |\n| `DEEPSEEK_MAX_OUTPUT_TOKENS` | Default `8192` |\n| `ALIBABA_API_KEY` | Alibaba Cloud Model Studio (Qwen), OpenAI-compatible endpoint |\n| `ALIBABA_BASE_URL` | Workspace compatible-mode URL (from Model Studio console) |\n| `ALIBABA_MODEL` | Default `qwen-plus`; vision: `ALIBABA_VISION_MODEL` (`qwen-vl-plus`) |\n| `OPENAI_API_KEY` | OpenAI for TOEIC import (fallback in `auto`) |\n| `OPENAI_MODEL` | Optional; default `gpt-4o-mini` |\n| `OPENAI_MODEL_FALLBACKS` | Comma-separated; default `gpt-4o,gpt-4.1` |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `GEMINI_API_KEY` | Google Gemini (fallback in `auto`) |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n| `PYMUPDF_URL` | PyMuPDF sidecar (`http://pymupdf:8081` in Docker Compose) |\n| `USE_PYMUPDF_PIPELINE` | `true` (default) — rule-based TOEIC import qua PyMuPDF |\n| `PYMUPDF_TIMEOUT_MS` | Optional; timeout gọi sidecar (default `120000`) |\n| ~~`MARKITDOWN_URL`~~ | *(deprecated)* MarkItDown sidecar — không dùng nữa |\n| ~~`MARKITDOWN_TIMEOUT_MS`~~ | *(deprecated)* |\n| `AI_ENABLE_STREAMING` | `true` (default) — stream JSON; on truncate, handoff partial sang provider kế |\n| `HEADROOM_BASE_URL` | Headroom proxy (`http://headroom:8787` trong Compose; `http://127.0.0.1:8787` nếu API dev + proxy Docker). Để trống hoặc `HEADROOM_ENABLED=false` = tắt nén |\n| `HEADROOM_MIN_CHARS` | Chỉ nén prompt text ≥ N ký tự (default `2000`). Vision (KEY RC ảnh) không nén |\n| `ALIBABA_MAX_OUTPUT_TOKENS` | Default `8192` (tránh lỗi Qwen max_tokens) |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Default `16384` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Default `8192` |\n\n### Import pipeline checkpoint + resume\n\nTOEIC import lưu tiến độ từng bước trong `IngestionDraft.pipelineState` (extract → RC key → Listening 1–4 → Reading 5–7 → save). Khi job `FAILED`, gọi lại chỉ các bước chưa `done` — không parse lại Part đã xong.\n\n```bash\n# Tiếp tục job sau khi hết quota AI\nPOST /api/admin/import-jobs/:jobId/resume\n```\n\nLog: `[Pipeline] step=parse_listening_2 status=skip` (đã checkpoint) hoặc `status=run`.\n\n### Docker Compose (API + Postgres + Redis + PyMuPDF + Headroom)\n\n```bash\ndocker compose up -d --build\n```\n\n| Service | Port | Mô tả |\n|---------|------|--------|\n| `api` | 4000 | Node.js API |\n| `pymupdf` | 8081 | Python sidecar — layout/text/clip PDF |\n| `headroom` | 8787 | Context compression proxy — nén prompt text trước Alibaba/OpenAI |\n| `db` | 5432 | PostgreSQL |\n| `redis` | 6379 | Redis |\n| ~~`markitdown`~~ | ~~8080~~ | *(deprecated)* Python MarkItDown sidecar |\n\nChỉ chạy sidecar (dev local, API bằng `npm run dev`):\n\n```bash\ndocker compose up -d pymupdf headroom\n# .env: PYMUPDF_URL=http://localhost:8081\n# .env: HEADROOM_BASE_URL=http://127.0.0.1:8787\n```\n\n<!-- MarkItDown deprecated:\ndocker compose up -d markitdown headroom\n# .env: MARKITDOWN_URL=http://localhost:8080\n-->\n\nHoặc proxy Headroom riêng: `docker run -d --name headroom -p 8787:8787 ghcr.io/chopratejas/headroom:latest`\n\n<!-- MarkItDown deprecated: set MARKITDOWN_URL= và bỏ depends_on markitdown -->\n\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n.\n"}, "83904fc9b489d41e34acb628cff66f605dd012a1": {"t": 1785767226.9813328, "n": 7393, "h": "8006432094735a86", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `ALIBABA_API_KEY` (Alibaba Model Studio / Qwen) and/or `OPENAI_API_KEY` / `GEMINI_API_KEY` for admin AI import\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\n# Chỉnh learn-now-nodejs/.env — set ALIBABA_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY cho admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `AI_PROVIDER` | `auto` (default), `alibaba`, `deepseek`, `openai`, or `gemini` |\n| `AI_PROVIDER_ORDER` | Comma-separated fallback order; default `alibaba,deepseek,openai,gemini` |\n| `DEEPSEEK_API_KEY` | [DeepSeek API](https://api-docs.deepseek.com/) (OpenAI-compatible) |\n| `DEEPSEEK_BASE_URL` | Default `https://api.deepseek.com` |\n| `DEEPSEEK_MODEL` | Default `deepseek-v4-flash`; fallback `deepseek-v4-pro` via `DEEPSEEK_MODEL_FALLBACKS` |\n| `DEEPSEEK_MAX_OUTPUT_TOKENS` | Default `8192` |\n| `ALIBABA_API_KEY` | Alibaba Cloud Model Studio (Qwen), OpenAI-compatible endpoint |\n| `ALIBABA_BASE_URL` | Workspace compatible-mode URL (from Model Studio console) |\n| `ALIBABA_MODEL` | Default `qwen-plus`; vision: `ALIBABA_VISION_MODEL` (`qwen-vl-plus`) |\n| `OPENAI_API_KEY` | OpenAI for TOEIC import (fallback in `auto`) |\n| `OPENAI_MODEL` | Optional; default `gpt-4o-mini` |\n| `OPENAI_MODEL_FALLBACKS` | Comma-separated; default `gpt-4o,gpt-4.1` |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `GEMINI_API_KEY` | Google Gemini (fallback in `auto`) |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n| `MARKITDOWN_URL` | MarkItDown sidecar URL (`http://markitdown:8080` in Docker Compose; `http://localhost:8080` if API chạy ngoài Docker). Để trống = chỉ dùng pdf-parse |\n| `MARKITDOWN_TIMEOUT_MS` | Optional; timeout gọi sidecar (default `120000`) |\n| `AI_ENABLE_STREAMING` | `true` (default) — stream JSON; on truncate, handoff partial sang provider kế |\n| `HEADROOM_BASE_URL` | Headroom proxy (`http://headroom:8787` trong Compose; `http://127.0.0.1:8787` nếu API dev + proxy Docker). Để trống hoặc `HEADROOM_ENABLED=false` = tắt nén |\n| `HEADROOM_MIN_CHARS` | Chỉ nén prompt text ≥ N ký tự (default `2000`). Vision (KEY RC ảnh) không nén |\n| `ALIBABA_MAX_OUTPUT_TOKENS` | Default `8192` (tránh lỗi Qwen max_tokens) |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Default `16384` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Default `8192` |\n\n### Import pipeline checkpoint + resume\n\nTOEIC import lưu tiến độ từng bước trong `IngestionDraft.pipelineState` (extract → RC key → Listening 1–4 → Reading 5–7 → save). Khi job `FAILED`, gọi lại chỉ các bước chưa `done` — không parse lại Part đã xong.\n\n```bash\n# Tiếp tục job sau khi hết quota AI\nPOST /api/admin/import-jobs/:jobId/resume\n```\n\nLog: `[Pipeline] step=parse_listening_2 status=skip` (đã checkpoint) hoặc `status=run`.\n\n### Docker Compose (API + Postgres + Redis + MarkItDown + Headroom)\n\n```bash\ndocker compose up -d --build\n```\n\n| Service | Port | Mô tả |\n|---------|------|--------|\n| `api` | 4000 | Node.js API |\n| `markitdown` | 8080 | Python sidecar — `POST /convert`, `GET /health` |\n| `headroom` | 8787 | Context compression proxy — nén prompt text trước Alibaba/OpenAI |\n| `db` | 5432 | PostgreSQL |\n| `redis` | 6379 | Redis |\n\nChỉ chạy sidecar (dev local, API bằng `npm run dev`):\n\n```bash\ndocker compose up -d markitdown headroom\n# .env: MARKITDOWN_URL=http://localhost:8080\n# .env: HEADROOM_BASE_URL=http://127.0.0.1:8787\n```\n\nHoặc proxy Headroom riêng: `docker run -d --name headroom -p 8787:8787 ghcr.io/chopratejas/headroom:latest`\n\nTắt MarkItDown (chỉ pdf-parse): trong `.env` set `MARKITDOWN_URL=` (rỗng) và bỏ `depends_on` markitdown hoặc không start service `markitdown`.\n\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n.\n"}, "aadd4f598af7552b775adbf74189dae73126f440": {"t": 1785767228.4310558, "n": 4106, "h": "26ac6844285a6523", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `GEMINI_API_KEY` (required for admin AI import)\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\ncp .env.example .env\n# Edit .env — set GEMINI_API_KEY if using admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `GEMINI_API_KEY` | Google Gemini for OCR import |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n\n### S3 object key layout\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n.\n"}, "c3377474662f4606fab6c8a2b51b34582f6883d0": {"t": 1785767230.2031598, "n": 4104, "h": "ec5b88b499485964", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `GEMINI_API_KEY` (required for admin AI import)\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\ncp .env.example .env\n# Edit .env — set GEMINI_API_KEY if using admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `GEMINI_API_KEY` | Google Gemini for OCR import |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n\n### S3 object key layout\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n"}, "d5b4f182538891dea0d9fc4e563160c9d2fdc020": {"t": 1785767231.4823887, "n": 1680, "h": "efaf5d2e6665307c", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `GEMINI_API_KEY` (required only for admin AI import)\n\n## Setup\n\n```bash\nnpm install\ncp .env.example .env\n# Edit .env — set GEMINI_API_KEY if using admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `GEMINI_API_KEY` | Google Gemini for OCR import |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n"}}}} |