{"id":33760372,"url":"https://github.com/objones25/document-scanner-summarizer","last_synced_at":"2026-04-20T07:04:23.328Z","repository":{"id":326430426,"uuid":"1105519426","full_name":"objones25/document-scanner-summarizer","owner":"objones25","description":"📄 AI-powered document scanner and summarizer with OCR, supporting images, PDFs, DOCX, and web pages. Features interactive CLI with streaming responses from Claude, GPT, or Gemini.","archived":false,"fork":false,"pushed_at":"2026-03-17T20:39:38.000Z","size":173,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-18T09:22:15.623Z","etag":null,"topics":["ai","anthropic","cli","document-processing","nlp","ocr","openai","pdf","python","tesseract"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/objones25.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-27T18:24:15.000Z","updated_at":"2026-03-17T20:39:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/objones25/document-scanner-summarizer","commit_stats":null,"previous_names":["objones25/document-scanner-summarizer"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/objones25/document-scanner-summarizer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/objones25%2Fdocument-scanner-summarizer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/objones25%2Fdocument-scanner-summarizer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/objones25%2Fdocument-scanner-summarizer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/objones25%2Fdocument-scanner-summarizer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/objones25","download_url":"https://codeload.github.com/objones25/document-scanner-summarizer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/objones25%2Fdocument-scanner-summarizer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32036803,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-20T00:18:06.643Z","status":"online","status_checked_at":"2026-04-20T02:00:06.527Z","response_time":94,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","anthropic","cli","document-processing","nlp","ocr","openai","pdf","python","tesseract"],"created_at":"2025-12-05T10:01:45.414Z","updated_at":"2026-04-20T07:04:23.313Z","avatar_url":"https://github.com/objones25.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Document Scanner \u0026 Summarizer\n\nA Python tool that extracts text from documents (images, PDFs, web pages, DOCX) and provides AI-powered analysis through streaming conversations. Available as both a CLI and a production REST API deployed on Railway.\n\n**Live API:** `https://document-scanner-summarizer-production.up.railway.app`\n\n---\n\n## REST API\n\nThe API is the primary integration point for the website. All responses from AI endpoints are [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) streams.\n\n### Base URL\n\n```text\nhttps://document-scanner-summarizer-production.up.railway.app\n```\n\n### Authentication\n\nAll `/api/*` endpoints require a Bearer token when `API_TOKEN` is set on the server:\n\n```text\nAuthorization: Bearer \u003cyour-token\u003e\n```\n\n`/health` is always public (required for Railway health checks).\n\nIf `API_TOKEN` is not set the server accepts all requests — useful for local development.\n\n### CORS\n\nRequests are accepted from:\n\n- `https://owenbeckettjones.com`\n- `https://www.owenbeckettjones.com`\n- `http://localhost:3000`, `http://localhost:5173`, `http://localhost:8080`\n\nAll other origins are blocked.\n\n---\n\n### `GET /health`\n\nCheck that the service is up and see the number of active sessions.\n\n**Response `200`**\n\n```json\n{ \"status\": \"ok\", \"sessions\": 3 }\n```\n\n---\n\n### `POST /api/sessions`\n\nUpload a document or provide a URL to create a session. Returns a `session_id` used by all subsequent calls.\n\n**Content-Type:** `multipart/form-data`\n\n| Field | Type | Required | Default | Notes |\n| --- | --- | --- | --- | --- |\n| `file` | file | one of file/url | — | Any supported format (see below) |\n| `url` | string | one of file/url | — | Any `http(s)://` URL |\n| `provider` | string | no | `anthropic` | `anthropic` or `gemini` |\n| `ocr_engine` | string | no | `tesseract` | `tesseract` or `mistral` |\n\nProviding both `file` and `url`, or neither, returns `400`.\n\n**Response `201`**\n\n```json\n{\n  \"session_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"char_count\": 4821,\n  \"preview\": \"First 200 characters of extracted text...\"\n}\n```\n\n**Error responses**\n\n| Code | Reason |\n| --- | --- |\n| `400` | Neither file nor url provided, or both provided |\n| `400` | `ocr_engine=mistral` but `MISTRAL_API_KEY` not set on server |\n| `422` | Document was processed but no text could be extracted |\n| `500` | Extraction failed or provider API key missing on server |\n\n**Example — file upload**\n\n```js\nconst form = new FormData();\nform.append(\"file\", fileInput.files[0]);\nform.append(\"provider\", \"anthropic\");\n\nconst res = await fetch(`${BASE_URL}/api/sessions`, {\n  method: \"POST\",\n  headers: { Authorization: `Bearer ${API_TOKEN}` },\n  body: form,\n  credentials: \"include\",\n});\nconst { session_id, char_count, preview } = await res.json();\n```\n\n**Example — URL**\n\n```js\nconst form = new FormData();\nform.append(\"url\", \"https://example.com/article\");\nform.append(\"provider\", \"anthropic\");\n\nconst res = await fetch(`${BASE_URL}/api/sessions`, {\n  method: \"POST\",\n  headers: { Authorization: `Bearer ${API_TOKEN}` },\n  body: form,\n  credentials: \"include\",\n});\n```\n\n---\n\n### `POST /api/sessions/{session_id}/chat`\n\nSend a message and stream the AI response back token-by-token.\n\n**Content-Type:** `application/json`\n\n```json\n{ \"message\": \"What are the main conclusions?\" }\n```\n\n**Response `200` — SSE stream**\n\n```text\ndata: The\\n\\n\ndata: main\\n\\n\ndata: conclusions\\n\\n\ndata: are...\\n\\n\ndata: [DONE]\\n\\n\n```\n\nOn error:\n\n```text\nevent: error\ndata: {\"detail\": \"Provider error message\"}\n```\n\n**Parsing SSE in JavaScript**\n\n```js\nconst res = await fetch(`${BASE_URL}/api/sessions/${sessionId}/chat`, {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: `Bearer ${API_TOKEN}`,\n  },\n  body: JSON.stringify({ message: \"What are the main conclusions?\" }),\n  credentials: \"include\",\n});\n\nconst reader = res.body.getReader();\nconst decoder = new TextDecoder();\nlet buffer = \"\";\n\nwhile (true) {\n  const { done, value } = await reader.read();\n  if (done) break;\n\n  buffer += decoder.decode(value, { stream: true });\n  const lines = buffer.split(\"\\n\");\n  buffer = lines.pop();\n\n  for (const line of lines) {\n    if (line.startsWith(\"data: \")) {\n      const token = line.slice(6);\n      if (token === \"[DONE]\") break;\n      // tokens have literal \\n escaped — unescape if rendering as text\n      outputEl.textContent += token.replace(/\\\\n/g, \"\\n\");\n    }\n    if (line.startsWith(\"event: error\")) {\n      // next line will be: data: {\"detail\": \"...\"}\n    }\n  }\n}\n```\n\n**Error responses**\n\n| Code | Reason |\n| --- | --- |\n| `404` | Session not found or expired |\n| `422` | Empty message |\n\n---\n\n### `POST /api/sessions/{session_id}/summary`\n\nGenerate a one-shot summary of the document. Streams back the same SSE format as `/chat`.\n\n**Content-Type:** `application/json`\n\n```json\n{ \"style\": \"concise\" }\n```\n\n| `style` | Description |\n| --- | --- |\n| `concise` | 2–4 paragraph overview (default) |\n| `detailed` | Comprehensive summary with sections |\n| `bullet-points` | Structured bullet-point list |\n\n**Response `200` — SSE stream** (same format as `/chat`)\n\n**Example**\n\n```js\nconst res = await fetch(`${BASE_URL}/api/sessions/${sessionId}/summary`, {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: `Bearer ${API_TOKEN}`,\n  },\n  body: JSON.stringify({ style: \"bullet-points\" }),\n  credentials: \"include\",\n});\n// consume SSE stream same as /chat\n```\n\n**Error responses**\n\n| Code | Reason |\n| --- | --- |\n| `404` | Session not found or expired |\n| `422` | Invalid `style` value |\n\n---\n\n### `DELETE /api/sessions/{session_id}`\n\nExplicitly end a session and free server memory. Sessions also expire automatically after **30 minutes of inactivity**.\n\n**Response `204`** — no body\n\n**Response `404`** — session not found\n\n```js\nawait fetch(`${BASE_URL}/api/sessions/${sessionId}`, {\n  method: \"DELETE\",\n  headers: { Authorization: `Bearer ${API_TOKEN}` },\n  credentials: \"include\",\n});\n```\n\n---\n\n### Session lifecycle\n\n```text\nPOST /api/sessions                →  session_id (valid 30 min from last use)\nPOST /api/sessions/:id/summary    →  stream one-shot summary\nPOST /api/sessions/:id/chat       →  stream answer (repeatable, builds history)\nDELETE /api/sessions/:id          →  cleanup\n```\n\nEach chat message is appended to the conversation history, so follow-up questions have full context.\n\n---\n\n### Supported file formats\n\n| Category | Formats |\n| --- | --- |\n| Images | `.jpg`, `.jpeg`, `.png`, `.bmp`, `.tiff`, `.tif` |\n| PDF | `.pdf` (text extraction + OCR fallback for scanned docs) |\n| Word | `.docx` |\n| Text | `.txt`, `.md`, `.markdown` |\n| Web | any `http(s)://` URL |\n\n---\n\n## CLI\n\n### Installation\n\n```bash\ngit clone https://github.com/objones25/document-scanner-summarizer.git\ncd document_scanner_summarizer\nuv sync\ncp .env.example .env   # add API keys\n```\n\n### Quick start\n\n```bash\n# Interactive mode\npython main.py\n\n# Analyze a file directly\npython main.py document.pdf --provider anthropic\n\n# Quick bullet-point summary\npython main.py report.pdf --summary-only --summary-style bullet-points\n\n# OCR a handwritten image with Mistral\npython main.py notes.jpg --ocr mistral --provider anthropic\n\n# Claude with web search + code execution\npython main.py data.pdf --provider anthropic --web-search --code-execution\n\n# Gemini with thinking + grounding\npython main.py research.pdf --provider gemini --thinking --grounding\n```\n\n### API keys\n\n```bash\n# At least one AI provider required\nANTHROPIC_API_KEY=sk-ant-...\nGOOGLE_API_KEY=...\n\n# Optional — only needed for Mistral OCR\nMISTRAL_API_KEY=...\n```\n\n### Interactive commands\n\n| Command | Action |\n| --- | --- |\n| `/summary` | Concise summary |\n| `/summary detailed` | Detailed summary |\n| `/summary bullet-points` | Bullet-point summary |\n| `/clear` | Clear conversation history |\n| `/exit` | Quit |\n\n### All CLI flags\n\n```text\npositional arguments:\n  source                File path or URL\n\noptional arguments:\n  --ocr {tesseract,mistral}             OCR engine (default: tesseract)\n  --provider {anthropic,openai,gemini}  AI provider (default: anthropic)\n  --summary-only                        Print summary and exit\n  --summary-style {concise,detailed,bullet-points}\n  --thinking                            Extended reasoning (Gemini)\n  --grounding                           Google Search grounding (Gemini)\n  --code-execution                      Code execution sandbox (Gemini + Claude)\n  --web-search                          Web search with citations (Claude)\n  --web-fetch                           Fetch web pages/PDFs (Claude)\n```\n\n---\n\n## Development\n\n### Run tests\n\n```bash\nuv sync --all-groups\nuv run pytest --cov=src --cov-fail-under=98 -m \"not slow\" -v\n```\n\n326 tests, 99.90% coverage enforced in CI.\n\n### Project structure\n\n```text\ndocument_scanner_summarizer/\n├── src/\n│   ├── api.py              # FastAPI REST API (Railway deployment)\n│   ├── cli.py              # Interactive CLI\n│   ├── extractors.py       # Text extraction (PDF, DOCX, URL, images)\n│   ├── ocr.py              # Tesseract + Mistral OCR engines\n│   ├── preprocessing.py    # Image preprocessing pipeline\n│   └── summarizer.py       # AI providers + conversation history\n├── tests/                  # 326 tests, 99.90% coverage\n├── Dockerfile              # Railway container build\n├── railway.toml            # Railway deployment config\n├── .github/workflows/ci.yml\n├── pyproject.toml\n└── uv.lock\n```\n\n### Docker (local)\n\n```bash\ndocker build -t doc-scanner .\ndocker run -p 8000:8000 \\\n  -e ANTHROPIC_API_KEY=... \\\n  -e GOOGLE_API_KEY=... \\\n  doc-scanner\n```\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobjones25%2Fdocument-scanner-summarizer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fobjones25%2Fdocument-scanner-summarizer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobjones25%2Fdocument-scanner-summarizer/lists"}