Skip to content

Commit 65c9c47

Browse files
Fix OCR gibberish output and add CLAUDE.md
- Fix UTF-8 encoding: Replace mojibake token string with correct fullwidth characters for end-of-sentence detection - Update NGram parameters: ngram_size=20, window_size=50 for more aggressive repeat prevention (matching custom_run_dpsk_ocr_pdf.py) - Fix default prompts: Use proper grounding prompt from config instead of bare <image> tag - Add CLAUDE.md with project documentation for Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3833957 commit 65c9c47

2 files changed

Lines changed: 110 additions & 14 deletions

File tree

CLAUDE.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
DeepSeek OCR Pipeline - A production-ready OCR solution using DeepSeek-OCR with AWS CDK, ECS on GPU instances (g4dn.xlarge), and A2I human review workflows. This is a hybrid architecture combining the Bogdanovich77 DeepSeek-OCR Docker implementation with AWS orchestration.
8+
9+
## Common Commands
10+
11+
```bash
12+
# Install dependencies
13+
npm install
14+
15+
# Build (compile + lint + synth)
16+
npm run build
17+
18+
# Run linting
19+
npm run eslint
20+
21+
# Synthesize CDK templates
22+
npm run synth
23+
24+
# Deploy to dev environment (requires STAGE=dev env var)
25+
STAGE=dev npm run deploy:dev
26+
27+
# Deploy with specific profile
28+
npm run deploy-dev # uses --profile dev
29+
npm run deploy-prod # uses --profile prod
30+
31+
# Build Docker image locally
32+
npm run build-docker
33+
34+
# View CDK diff
35+
npm run diff
36+
37+
# Watch mode (hotswap deploys)
38+
npm run watch
39+
40+
# Destroy dev stacks
41+
STAGE=dev npm run destroy:dev
42+
```
43+
44+
## Architecture
45+
46+
### CDK Stage Structure
47+
48+
The app uses a single stage (`DevStage`) defined in `src/lib/stages.ts` that orchestrates six stacks:
49+
50+
1. **KmsStack** - Encryption keys for all resources
51+
2. **NetworkingStack** - VPC with public/private/isolated subnets across 3 AZs
52+
3. **EcsStack** - GPU cluster with g4dn.xlarge instances, ALB, auto-scaling (1-10 instances)
53+
4. **S3Stack** - File storage bucket with KMS encryption
54+
5. **LambdasStack** - Processing Lambda functions (start processing handler)
55+
6. **ApiGatewayStack** - REST API with VPC integration to ECS
56+
57+
Stack dependencies: EcsStack depends on KmsStack and NetworkingStack.
58+
59+
### Docker Container (`docker/`)
60+
61+
Custom DeepSeek-OCR implementation based on vLLM OpenAI image. Key customizations:
62+
- `custom_config.py`, `custom_image_process.py`, `custom_deepseek_ocr.py` - Fixed prompt parameter bugs
63+
- `start_server.py` - FastAPI server with `/health`, `/ocr/pdf`, `/ocr/image`, `/ocr/batch` endpoints
64+
- Model downloads from HuggingFace at runtime (`deepseek-ai/DeepSeek-OCR`)
65+
66+
### Key Construct: `DeepSeekOcrEc2GpuConstruct`
67+
68+
Located in `src/constructs/deepseek-ocr-ecs.ts`, this is the core infrastructure construct managing:
69+
- ECS cluster with GPU-optimized AMI
70+
- Auto-scaling group with optional spot instance support
71+
- Application Load Balancer with health checks on port 8000
72+
- Task definition with 1 GPU, 14GB memory, 3.75 vCPU allocation
73+
- Model cache persistence via host volume mount at `/mnt/ecs-data/models`
74+
75+
## Environment Variables
76+
77+
Required:
78+
- `STAGE` - Must be set for deployment (e.g., `dev`)
79+
- `CDK_DEFAULT_REGION` - AWS region (defaults to `us-east-1`)
80+
- `CDK_DEFAULT_ACCOUNT` - AWS account ID
81+
82+
Container environment:
83+
- `MODEL_PATH=deepseek-ai/DeepSeek-OCR` - HuggingFace repository ID
84+
- `GPU_MEMORY_UTILIZATION=0.85`
85+
- `MAX_CONCURRENCY=5`
86+
87+
## Deployment Script
88+
89+
`scripts/deploy-ocr-update.sh` provides automated deployment:
90+
1. Zips and uploads Docker source to S3
91+
2. Triggers CodeBuild to build and push to ECR
92+
3. Forces new ECS deployment
93+
4. Monitors rollout and runs health check
94+
95+
## Projen
96+
97+
This project is managed by Projen. Configuration is in `.projenrc.ts`. After modifying `.projenrc.ts`, run `npx projen` to regenerate project files. Do not manually edit generated files (marked with `// ~~ Generated by projen`).

docker/start_server.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,9 @@ def initialize_model():
159159
)
160160

161161
# Set up sampling parameters
162+
# Use more aggressive repeat prevention (matching custom_run_dpsk_ocr_pdf.py)
162163
from process.ngram_norepeat import NoRepeatNGramLogitsProcessor
163-
logits_processors = [NoRepeatNGramLogitsProcessor(ngram_size=30, window_size=90, whitelist_token_ids={128821, 128822})]
164+
logits_processors = [NoRepeatNGramLogitsProcessor(ngram_size=20, window_size=50, whitelist_token_ids={128821, 128822})]
164165

165166
sampling_params = SamplingParams(
166167
temperature=0.1,
@@ -244,9 +245,10 @@ def process_single_image(
244245
print(f"[DEBUG] Model output (first 100 chars): {repr(result[:100])}")
245246
print(f"[DEBUG] Model output length: {len(result)} characters")
246247

247-
# Clean up result
248-
if '<|endâ–ofâ–sentence|>' in result:
249-
result = result.replace('<|endâ–ofâ–sentence|>', '')
248+
# Clean up result - remove end-of-sentence tokens
249+
# Note: The token uses fullwidth characters: | (U+FF5C) and ▁ (U+2581)
250+
if '<|end▁of▁sentence|>' in result:
251+
result = result.replace('<|end▁of▁sentence|>', '')
250252
print(f"[DEBUG] Removed end-of-sentence tokens")
251253

252254
return result
@@ -275,7 +277,7 @@ async def health_check():
275277
@app.post("/ocr/image", response_model=OCRResponse)
276278
async def process_image_endpoint(
277279
file: UploadFile = File(...),
278-
prompt: Optional[str] = Form('<image>'),
280+
prompt: Optional[str] = Form(None),
279281
temperature: Optional[float] = Form(None),
280282
top_p: Optional[float] = Form(None),
281283
max_tokens: Optional[int] = Form(None),
@@ -297,8 +299,8 @@ async def process_image_endpoint(
297299
print(f"[DEBUG] Default PROMPT from config: {repr(PROMPT)}")
298300
print(f"[API] Sampling overrides: temperature={temperature}, top_p={top_p}, max_tokens={max_tokens}")
299301

300-
# Use provided prompt or default
301-
use_prompt = prompt if prompt and prompt.strip() else '<image>'
302+
# Use provided prompt or default from config (includes grounding instruction)
303+
use_prompt = prompt if prompt and prompt.strip() else PROMPT
302304
print(f"[DEBUG] Image endpoint selected prompt: {repr(use_prompt)}")
303305
print(f"[DEBUG] Using custom prompt: {prompt is not None}")
304306

@@ -329,7 +331,7 @@ async def process_image_endpoint(
329331
@app.post("/ocr/pdf", response_model=BatchOCRResponse)
330332
async def process_pdf_endpoint(
331333
file: UploadFile = File(...),
332-
prompt: Optional[str] = Form('<image>'),
334+
prompt: Optional[str] = Form(None),
333335
temperature: Optional[float] = Form(None),
334336
top_p: Optional[float] = Form(None),
335337
max_tokens: Optional[int] = Form(None),
@@ -341,7 +343,7 @@ async def process_pdf_endpoint(
341343
print(f"[DEBUG] Default PROMPT from config: {repr(PROMPT)}")
342344
print(f"[API] Sampling overrides: temperature={temperature}, top_p={top_p}, max_tokens={max_tokens}")
343345

344-
# Read PDF data
346+
# Read PDF data
345347
pdf_data = await file.read()
346348
print(f"[DEBUG] Read {len(pdf_data)} bytes of PDF data")
347349

@@ -358,15 +360,12 @@ async def process_pdf_endpoint(
358360
filename=file.filename
359361
)
360362

361-
# Use provided prompt or default
362-
use_prompt = prompt if prompt else PROMPT
363+
# Use provided prompt or default from config (includes grounding instruction)
364+
use_prompt = prompt if prompt and prompt.strip() else PROMPT
363365
print(f"[DEBUG] PDF endpoint selected prompt: {repr(use_prompt)}")
364366
print(f"[DEBUG] Using custom prompt: {prompt is not None}")
365367

366368
# Process each page
367-
results = []
368-
use_prompt = prompt if prompt and prompt.strip() else '<image>'
369-
370369
results = []
371370
for page_num, image in enumerate(tqdm(images, desc="Processing pages")):
372371
try:

0 commit comments

Comments
 (0)