ritesh-torinit commited on
Commit
1a91148
·
verified ·
1 Parent(s): 74bd7b9

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. DEPLOY.md +181 -0
  2. README.md +150 -0
  3. handler.py +74 -0
  4. inference.py +364 -0
  5. requirements.txt +2 -0
DEPLOY.md ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploying LearningStudio Wrapper to Hugging Face
2
+
3
+ This guide explains how to deploy the LearningStudio callout detection wrapper to a HuggingFace Inference Endpoint.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. **HuggingFace Account**: Create an account at [huggingface.co](https://huggingface.co)
8
+ 2. **HuggingFace CLI**: Install the CLI tool
9
+ 3. **AWS Infrastructure**: The callout detection Lambda stack must be deployed
10
+
11
+ ### Install HuggingFace CLI
12
+
13
+ ```bash
14
+ pip install huggingface_hub
15
+ ```
16
+
17
+ ### Login to HuggingFace
18
+
19
+ ```bash
20
+ huggingface-cli login
21
+ ```
22
+
23
+ Follow the prompts to enter your HuggingFace token.
24
+
25
+ ## Step 1: Get AWS API Gateway Info
26
+
27
+ After deploying the callout detection Lambda stack, get the API Gateway URL and key:
28
+
29
+ ```bash
30
+ cd callout-detection-lambda
31
+
32
+ # Get the API Gateway endpoint URL
33
+ aws cloudformation describe-stacks \
34
+ --stack-name callout-detection-dev \
35
+ --query "Stacks[0].Outputs[?OutputKey=='ServiceEndpoint'].OutputValue" \
36
+ --output text
37
+
38
+ # Get the API key
39
+ aws apigateway get-api-keys \
40
+ --name-query "learningstudio-key-dev" \
41
+ --include-values \
42
+ --query "items[0].value" \
43
+ --output text
44
+ ```
45
+
46
+ Save these values - you'll need them when configuring the HF endpoint.
47
+
48
+ ## Step 2: Create HuggingFace Model Repository
49
+
50
+ First time only - create the model repository:
51
+
52
+ ```bash
53
+ huggingface-cli repo create YOUR_USERNAME/learningstudio-callout-wrapper --type model
54
+ ```
55
+
56
+ Or create via the HuggingFace web interface at https://huggingface.co/new
57
+
58
+ ## Step 3: Upload Wrapper Files
59
+
60
+ Navigate to the wrapper directory and upload files:
61
+
62
+ ```bash
63
+ cd callout-detection-lambda/hf_inference/learningstudio_wrapper
64
+
65
+ # Upload all files to the repository
66
+ huggingface-cli upload YOUR_USERNAME/learningstudio-callout-wrapper \
67
+ handler.py inference.py requirements.txt README.md \
68
+ --repo-type model
69
+ ```
70
+
71
+ ## Step 4: Create Inference Endpoint
72
+
73
+ 1. Go to https://ui.endpoints.huggingface.co/
74
+ 2. Click "New endpoint"
75
+ 3. Select your model repository (`YOUR_USERNAME/learningstudio-callout-wrapper`)
76
+ 4. Configure the endpoint:
77
+ - **Instance type**: CPU (this wrapper doesn't need GPU)
78
+ - **Region**: Choose a region close to your API Gateway
79
+ - **Scaling**: Start with 1 replica
80
+
81
+ ## Step 5: Configure Secrets
82
+
83
+ In the HuggingFace Inference Endpoint settings, add environment variables:
84
+
85
+ 1. Go to your endpoint settings
86
+ 2. Click "Settings" or "Environment Variables"
87
+ 3. Add the following secrets:
88
+
89
+ | Name | Value |
90
+ |------|-------|
91
+ | `API_GATEWAY_URL` | `https://xxx.execute-api.us-east-1.amazonaws.com/dev` |
92
+ | `API_KEY` | Your API key from Step 1 |
93
+
94
+ ## Step 6: Test the Endpoint
95
+
96
+ Once the endpoint is running, test it:
97
+
98
+ ```bash
99
+ # Set your HuggingFace token
100
+ export HF_TOKEN="your-hf-token"
101
+
102
+ # Test with a URL
103
+ curl -X POST https://YOUR_ENDPOINT.endpoints.huggingface.cloud \
104
+ -H "Authorization: Bearer $HF_TOKEN" \
105
+ -H "Content-Type: application/json" \
106
+ -d '{"inputs": "https://example.com/test-drawing.png"}'
107
+ ```
108
+
109
+ Expected response:
110
+
111
+ ```json
112
+ {
113
+ "predictions": [
114
+ {
115
+ "id": 1,
116
+ "label": "callout",
117
+ "class_id": 0,
118
+ "confidence": 0.95,
119
+ "bbox": {"x1": 100, "y1": 200, "x2": 300, "y2": 400}
120
+ }
121
+ ],
122
+ "total_detections": 1,
123
+ "image": "...",
124
+ "image_width": 1920,
125
+ "image_height": 1080
126
+ }
127
+ ```
128
+
129
+ ## Updating the Wrapper
130
+
131
+ To update the wrapper code:
132
+
133
+ ```bash
134
+ cd callout-detection-lambda/hf_inference/learningstudio_wrapper
135
+
136
+ # Upload updated files
137
+ huggingface-cli upload YOUR_USERNAME/learningstudio-callout-wrapper \
138
+ handler.py inference.py requirements.txt README.md \
139
+ --repo-type model
140
+ ```
141
+
142
+ The endpoint will automatically pick up the changes on the next request (after a brief cold start).
143
+
144
+ ## Rotating API Keys
145
+
146
+ To rotate the API key without touching the HF endpoint:
147
+
148
+ 1. Create a new API key in AWS API Gateway
149
+ 2. Update the `API_KEY` secret in HF endpoint settings
150
+ 3. Delete the old API key in AWS
151
+
152
+ ## Troubleshooting
153
+
154
+ ### "API_GATEWAY_URL and API_KEY must be set"
155
+
156
+ The environment variables are not configured. Go to your endpoint settings and add the secrets.
157
+
158
+ ### Timeout errors
159
+
160
+ The callout detection pipeline takes 30-120 seconds typically. If you're getting timeouts:
161
+ - Check that the Lambda stack is deployed and working
162
+ - Verify the API Gateway URL is correct
163
+ - Check CloudWatch logs for the Lambda functions
164
+
165
+ ### Authentication errors
166
+
167
+ - Verify the API key is correct
168
+ - Check that the key hasn't been deleted or rotated
169
+ - Ensure the key is associated with the usage plan
170
+
171
+ ### Connection refused
172
+
173
+ - Verify the API Gateway URL is correct
174
+ - Check that the endpoint is in the right region
175
+ - Ensure the Lambda stack is deployed
176
+
177
+ ## Monitoring
178
+
179
+ - **HuggingFace**: Check endpoint logs in the HF dashboard
180
+ - **AWS CloudWatch**: Monitor Lambda function logs and metrics
181
+ - **API Gateway**: View API Gateway metrics for request counts and errors
README.md ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - object-detection
4
+ - callout-detection
5
+ - architectural-drawings
6
+ - wrapper
7
+ library_name: custom
8
+ task: object-detection
9
+ license: apache-2.0
10
+ ---
11
+
12
+ # LearningStudio Callout Detection Wrapper
13
+
14
+ Wrapper for the Lambda-based callout detection pipeline, providing EMCO-compatible API format for LearningStudio integration.
15
+
16
+ ## Overview
17
+
18
+ This wrapper:
19
+ 1. Accepts image input in multiple formats (URL, base64, data URL)
20
+ 2. Gets a presigned S3 URL from API Gateway
21
+ 3. Uploads the image directly to S3 (avoids API Gateway data transfer costs)
22
+ 4. Starts the detection job via API Gateway (small JSON payload)
23
+ 5. Polls for completion
24
+ 6. Transforms results to EMCO-compatible format
25
+
26
+ ## Architecture
27
+
28
+ ```
29
+ HF Wrapper
30
+
31
+ ├─1─▶ GET /upload-url (get presigned S3 URL)
32
+
33
+ ├─2─▶ PUT image directly to S3 (free, bypasses API Gateway)
34
+
35
+ ├─3─▶ POST /detect {"job_id", "s3_url"} (tiny payload)
36
+
37
+ └─4─▶ GET /status/{job_id} (poll until complete)
38
+ ```
39
+
40
+ ## API Format
41
+
42
+ ### Input
43
+
44
+ Accepts images in multiple formats:
45
+
46
+ ```json
47
+ // HTTP URL
48
+ {"inputs": "https://example.com/image.jpg"}
49
+
50
+ // Data URL (base64 encoded)
51
+ {"inputs": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..."}
52
+
53
+ // Raw base64
54
+ {"inputs": "iVBORw0KGgoAAAANSUhEUgAAAAUA..."}
55
+ ```
56
+
57
+ ### Output
58
+
59
+ Returns EMCO-compatible format:
60
+
61
+ ```json
62
+ {
63
+ "predictions": [
64
+ {
65
+ "id": 1,
66
+ "label": "callout",
67
+ "class_id": 0,
68
+ "confidence": 0.95,
69
+ "bbox": {
70
+ "x1": 100,
71
+ "y1": 200,
72
+ "x2": 300,
73
+ "y2": 400
74
+ }
75
+ }
76
+ ],
77
+ "total_detections": 1,
78
+ "image": "base64_encoded_image",
79
+ "image_width": 1920,
80
+ "image_height": 1080
81
+ }
82
+ ```
83
+
84
+ ### Bounding Box Format
85
+
86
+ - **Input from Lambda**: `[x, y, width, height]` (xywh format)
87
+ - **Output to LearningStudio**: `{"x1", "y1", "x2", "y2"}` (xyxy format)
88
+
89
+ The wrapper automatically converts between these formats.
90
+
91
+ ## Configuration
92
+
93
+ This endpoint requires the following secrets to be configured in HuggingFace Inference Endpoint settings:
94
+
95
+ | Secret | Description |
96
+ |--------|-------------|
97
+ | `API_GATEWAY_URL` | Full URL of the API Gateway endpoint (e.g., `https://xxx.execute-api.us-east-1.amazonaws.com/dev`) |
98
+ | `API_KEY` | API key for authentication |
99
+
100
+ ## Usage
101
+
102
+ ### Python
103
+
104
+ ```python
105
+ import requests
106
+
107
+ HF_ENDPOINT = "https://your-endpoint.endpoints.huggingface.cloud"
108
+ HF_TOKEN = "your-hf-token"
109
+
110
+ response = requests.post(
111
+ HF_ENDPOINT,
112
+ headers={"Authorization": f"Bearer {HF_TOKEN}"},
113
+ json={"inputs": "https://example.com/architectural-drawing.png"}
114
+ )
115
+
116
+ result = response.json()
117
+ print(f"Found {result['total_detections']} callouts")
118
+ for pred in result["predictions"]:
119
+ print(f" Callout {pred['id']}: {pred['bbox']}, confidence={pred['confidence']}")
120
+ ```
121
+
122
+ ### cURL
123
+
124
+ ```bash
125
+ curl -X POST https://your-endpoint.endpoints.huggingface.cloud \
126
+ -H "Authorization: Bearer $HF_TOKEN" \
127
+ -H "Content-Type: application/json" \
128
+ -d '{"inputs": "https://example.com/architectural-drawing.png"}'
129
+ ```
130
+
131
+ ## Processing Time
132
+
133
+ Typical processing time is 30-120 seconds depending on image size and complexity. The wrapper polls the backend every 5 seconds with a maximum timeout of 15 minutes.
134
+
135
+ ## Error Handling
136
+
137
+ Errors are returned in a consistent format:
138
+
139
+ ```json
140
+ {
141
+ "error": "Description of the error",
142
+ "predictions": [],
143
+ "total_detections": 0,
144
+ "image": ""
145
+ }
146
+ ```
147
+
148
+ ## License
149
+
150
+ Apache 2.0
handler.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HuggingFace Inference Endpoint Handler for LearningStudio Callout Detection.
3
+
4
+ This wrapper provides an EMCO-compatible API format for LearningStudio integration,
5
+ calling the AWS Lambda-based callout detection pipeline via API Gateway.
6
+ """
7
+
8
+ from typing import Dict, Any, List, Union
9
+ from inference import inference, normalize_to_base64
10
+
11
+
12
+ class EndpointHandler:
13
+ """
14
+ HuggingFace Inference Endpoint Handler.
15
+
16
+ This class provides the interface expected by HuggingFace Inference Endpoints.
17
+ It wraps the callout detection pipeline and transforms outputs to EMCO format.
18
+ """
19
+
20
+ def __init__(self, path: str = ""):
21
+ """
22
+ Initialize the endpoint handler.
23
+
24
+ Args:
25
+ path: Model path (unused for this wrapper, but required by HF interface)
26
+ """
27
+ # No model to load - this is a wrapper for an external API
28
+ pass
29
+
30
+ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
31
+ """
32
+ Process an inference request.
33
+
34
+ Args:
35
+ data: Request data with format:
36
+ {
37
+ "inputs": "image_url_or_base64",
38
+ "parameters": {...} # Optional parameters
39
+ }
40
+
41
+ Returns:
42
+ EMCO-compatible response:
43
+ {
44
+ "predictions": [
45
+ {
46
+ "id": 1,
47
+ "label": "callout",
48
+ "class_id": 0,
49
+ "confidence": 0.95,
50
+ "bbox": {"x1": 100, "y1": 200, "x2": 300, "y2": 400}
51
+ },
52
+ ...
53
+ ],
54
+ "total_detections": N,
55
+ "image": "base64_encoded_image"
56
+ }
57
+ """
58
+ # Extract input
59
+ inputs = data.get("inputs")
60
+ if inputs is None:
61
+ return {
62
+ "error": "Missing 'inputs' field",
63
+ "predictions": [],
64
+ "total_detections": 0,
65
+ "image": ""
66
+ }
67
+
68
+ # Extract optional parameters
69
+ parameters = data.get("parameters", {})
70
+
71
+ # Call the inference function
72
+ result = inference(inputs, parameters)
73
+
74
+ return result
inference.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inference module for LearningStudio Callout Detection wrapper.
3
+
4
+ This module:
5
+ 1. Normalizes input to bytes (handles URLs, data URLs, raw base64)
6
+ 2. Gets presigned S3 URL from API Gateway
7
+ 3. Uploads image directly to S3 (bypasses API Gateway for large payloads)
8
+ 4. Calls API Gateway to start detection job
9
+ 5. Polls for completion
10
+ 6. Transforms callouts to EMCO format
11
+ """
12
+
13
+ import os
14
+ import base64
15
+ import time
16
+ import logging
17
+ from typing import Dict, Any, List, Optional, Tuple
18
+
19
+ import requests
20
+
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Environment variables (set in HF Inference Endpoint secrets)
26
+ API_GATEWAY_URL = os.environ.get("API_GATEWAY_URL", "")
27
+ API_KEY = os.environ.get("API_KEY", "")
28
+
29
+ # Polling configuration
30
+ MAX_WAIT_SECONDS = 900 # 15 minutes
31
+ POLL_INTERVAL_SECONDS = 5
32
+
33
+
34
+ def normalize_to_bytes(image_input: str) -> Tuple[bytes, str]:
35
+ """
36
+ Normalize image input to bytes.
37
+
38
+ Handles:
39
+ - HTTP/HTTPS URLs: Downloads image
40
+ - Data URLs (data:image/png;base64,...): Decodes base64
41
+ - Raw base64: Decodes to bytes
42
+
43
+ Args:
44
+ image_input: Image URL, data URL, or base64 string
45
+
46
+ Returns:
47
+ Tuple of (image_bytes, filename)
48
+ """
49
+ # Check if it's a URL
50
+ if image_input.startswith(("http://", "https://")):
51
+ logger.info(f"Downloading image from URL: {image_input[:100]}...")
52
+ response = requests.get(image_input, timeout=60)
53
+ response.raise_for_status()
54
+
55
+ # Try to get filename from URL
56
+ from urllib.parse import urlparse
57
+ parsed = urlparse(image_input)
58
+ filename = os.path.basename(parsed.path) or "image.png"
59
+
60
+ return response.content, filename
61
+
62
+ # Check if it's a data URL
63
+ if image_input.startswith("data:"):
64
+ # Parse data URL: data:image/png;base64,<data>
65
+ try:
66
+ header, encoded = image_input.split(",", 1)
67
+ # Extract extension from mime type
68
+ mime_part = header.split(";")[0].replace("data:", "")
69
+ ext = mime_part.split("/")[-1] if "/" in mime_part else "png"
70
+ return base64.b64decode(encoded), f"image.{ext}"
71
+ except ValueError:
72
+ raise ValueError("Invalid data URL format")
73
+
74
+ # Assume it's already base64
75
+ try:
76
+ return base64.b64decode(image_input), "image.png"
77
+ except Exception as e:
78
+ raise ValueError(f"Invalid base64 string: {e}")
79
+
80
+
81
+ def get_upload_url(filename: str = "image.png") -> Dict[str, str]:
82
+ """
83
+ Get presigned S3 URL for image upload.
84
+
85
+ Args:
86
+ filename: Original filename for the image
87
+
88
+ Returns:
89
+ Dict with job_id, upload_url, s3_url
90
+ """
91
+ if not API_GATEWAY_URL or not API_KEY:
92
+ raise ValueError(
93
+ "API_GATEWAY_URL and API_KEY must be set in environment variables. "
94
+ "Configure these in your HF Inference Endpoint secrets."
95
+ )
96
+
97
+ url = f"{API_GATEWAY_URL.rstrip('/')}/upload-url"
98
+ headers = {"x-api-key": API_KEY}
99
+ params = {"filename": filename}
100
+
101
+ logger.info(f"Getting upload URL from {url}")
102
+ response = requests.get(url, headers=headers, params=params, timeout=30)
103
+ response.raise_for_status()
104
+
105
+ result = response.json()
106
+ logger.info(f"Got upload URL for job_id={result.get('job_id')}")
107
+ return result
108
+
109
+
110
+ def upload_to_s3(upload_url: str, image_bytes: bytes) -> None:
111
+ """
112
+ Upload image directly to S3 using presigned URL.
113
+
114
+ Args:
115
+ upload_url: Presigned PUT URL
116
+ image_bytes: Image data to upload
117
+ """
118
+ logger.info(f"Uploading {len(image_bytes)} bytes to S3...")
119
+ response = requests.put(
120
+ upload_url,
121
+ data=image_bytes,
122
+ headers={"Content-Type": "image/png"},
123
+ timeout=60
124
+ )
125
+ response.raise_for_status()
126
+ logger.info("Upload complete")
127
+
128
+
129
+ def start_detection_job(job_id: str, s3_url: str, params: Optional[Dict] = None) -> str:
130
+ """
131
+ Start a detection job via API Gateway.
132
+
133
+ Args:
134
+ job_id: Job ID from get_upload_url
135
+ s3_url: S3 URL from get_upload_url
136
+ params: Optional processing parameters
137
+
138
+ Returns:
139
+ Job ID for polling
140
+ """
141
+ url = f"{API_GATEWAY_URL.rstrip('/')}/detect"
142
+ headers = {
143
+ "x-api-key": API_KEY,
144
+ "Content-Type": "application/json"
145
+ }
146
+ payload = {
147
+ "job_id": job_id,
148
+ "s3_url": s3_url
149
+ }
150
+ if params:
151
+ payload["params"] = params
152
+
153
+ logger.info(f"Starting detection job {job_id}")
154
+ response = requests.post(url, headers=headers, json=payload, timeout=30)
155
+ response.raise_for_status()
156
+
157
+ result = response.json()
158
+ logger.info(f"Detection job started: {result.get('status')}")
159
+ return job_id
160
+
161
+
162
+ def poll_for_completion(job_id: str) -> Dict[str, Any]:
163
+ """
164
+ Poll API Gateway for job completion.
165
+
166
+ Args:
167
+ job_id: Job ID to poll
168
+
169
+ Returns:
170
+ Final result with callouts
171
+ """
172
+ url = f"{API_GATEWAY_URL.rstrip('/')}/status/{job_id}"
173
+ headers = {"x-api-key": API_KEY}
174
+
175
+ elapsed = 0
176
+ while elapsed < MAX_WAIT_SECONDS:
177
+ logger.info(f"Polling job {job_id} (elapsed: {elapsed}s)")
178
+
179
+ response = requests.get(url, headers=headers, timeout=30)
180
+ response.raise_for_status()
181
+
182
+ result = response.json()
183
+ status = result.get("status")
184
+
185
+ if status == "SUCCEEDED":
186
+ logger.info(f"Job {job_id} completed successfully")
187
+ return result
188
+
189
+ if status in ("FAILED", "TIMED_OUT", "ABORTED"):
190
+ error_msg = result.get("error", f"Job {status.lower()}")
191
+ logger.error(f"Job {job_id} failed: {error_msg}")
192
+ return {
193
+ "status": status,
194
+ "error": error_msg,
195
+ "callouts": []
196
+ }
197
+
198
+ # Still running, wait and retry
199
+ time.sleep(POLL_INTERVAL_SECONDS)
200
+ elapsed += POLL_INTERVAL_SECONDS
201
+
202
+ # Timeout
203
+ logger.error(f"Job {job_id} timed out after {MAX_WAIT_SECONDS}s")
204
+ return {
205
+ "status": "TIMEOUT",
206
+ "error": f"Timeout waiting for results after {MAX_WAIT_SECONDS}s",
207
+ "callouts": []
208
+ }
209
+
210
+
211
+ def transform_to_emco_format(
212
+ callouts: List[Dict],
213
+ image_base64: str,
214
+ image_width: int = 0,
215
+ image_height: int = 0
216
+ ) -> Dict[str, Any]:
217
+ """
218
+ Transform callouts from Lambda format to EMCO format.
219
+
220
+ Lambda format:
221
+ {"bbox": [x, y, w, h], "score": 0.95, ...} # xywh
222
+
223
+ EMCO format:
224
+ {"bbox": {"x1": x, "y1": y, "x2": x+w, "y2": y+h}, "confidence": 0.95, ...} # xyxy
225
+
226
+ Args:
227
+ callouts: List of callouts from Lambda
228
+ image_base64: Original image as base64
229
+ image_width: Image width
230
+ image_height: Image height
231
+
232
+ Returns:
233
+ EMCO-compatible response dict
234
+ """
235
+ predictions = []
236
+
237
+ for i, callout in enumerate(callouts):
238
+ bbox = callout.get("bbox", [0, 0, 0, 0])
239
+
240
+ # Convert from [x, y, w, h] to {x1, y1, x2, y2}
241
+ x, y, w, h = bbox[0], bbox[1], bbox[2], bbox[3]
242
+
243
+ prediction = {
244
+ "id": i + 1,
245
+ "label": "callout",
246
+ "class_id": 0,
247
+ "confidence": callout.get("score", callout.get("confidence", 1.0)),
248
+ "bbox": {
249
+ "x1": int(x),
250
+ "y1": int(y),
251
+ "x2": int(x + w),
252
+ "y2": int(y + h)
253
+ }
254
+ }
255
+
256
+ # Include optional fields if present
257
+ if "text" in callout:
258
+ prediction["text"] = callout["text"]
259
+
260
+ predictions.append(prediction)
261
+
262
+ return {
263
+ "predictions": predictions,
264
+ "total_detections": len(predictions),
265
+ "image": image_base64,
266
+ "image_width": image_width,
267
+ "image_height": image_height
268
+ }
269
+
270
+
271
+ def inference(image_input: str, parameters: Optional[Dict] = None) -> Dict[str, Any]:
272
+ """
273
+ Run inference on an image.
274
+
275
+ This is the main entry point for the HF wrapper.
276
+
277
+ Flow:
278
+ 1. Normalize input to bytes
279
+ 2. Get presigned S3 URL
280
+ 3. Upload image directly to S3
281
+ 4. Start detection job (small JSON payload)
282
+ 5. Poll for completion
283
+ 6. Transform results to EMCO format
284
+
285
+ Args:
286
+ image_input: Image URL, data URL, or base64 string
287
+ parameters: Optional processing parameters
288
+
289
+ Returns:
290
+ EMCO-compatible response with predictions
291
+ """
292
+ try:
293
+ # 1. Normalize input to bytes
294
+ logger.info("Normalizing input...")
295
+ image_bytes, filename = normalize_to_bytes(image_input)
296
+
297
+ # Keep base64 for response
298
+ image_base64 = base64.b64encode(image_bytes).decode("utf-8")
299
+
300
+ # 2. Get presigned upload URL
301
+ logger.info("Getting upload URL...")
302
+ upload_info = get_upload_url(filename)
303
+ job_id = upload_info["job_id"]
304
+ upload_url = upload_info["upload_url"]
305
+ s3_url = upload_info["s3_url"]
306
+
307
+ # 3. Upload image directly to S3
308
+ logger.info("Uploading to S3...")
309
+ upload_to_s3(upload_url, image_bytes)
310
+
311
+ # 4. Start detection job
312
+ logger.info("Starting detection job...")
313
+ start_detection_job(job_id, s3_url, parameters)
314
+
315
+ # 5. Poll for completion
316
+ logger.info("Polling for completion...")
317
+ result = poll_for_completion(job_id)
318
+
319
+ # 6. Check for errors
320
+ if result.get("status") in ("FAILED", "TIMED_OUT", "ABORTED", "TIMEOUT"):
321
+ return {
322
+ "error": result.get("error", "Unknown error"),
323
+ "predictions": [],
324
+ "total_detections": 0,
325
+ "image": image_base64
326
+ }
327
+
328
+ # 7. Transform to EMCO format
329
+ logger.info("Transforming results to EMCO format...")
330
+ callouts = result.get("callouts", [])
331
+ image_width = result.get("image_width", 0)
332
+ image_height = result.get("image_height", 0)
333
+
334
+ return transform_to_emco_format(
335
+ callouts,
336
+ image_base64,
337
+ image_width,
338
+ image_height
339
+ )
340
+
341
+ except requests.exceptions.RequestException as e:
342
+ logger.error(f"Request error: {e}")
343
+ return {
344
+ "error": f"Request error: {str(e)}",
345
+ "predictions": [],
346
+ "total_detections": 0,
347
+ "image": ""
348
+ }
349
+ except ValueError as e:
350
+ logger.error(f"Validation error: {e}")
351
+ return {
352
+ "error": str(e),
353
+ "predictions": [],
354
+ "total_detections": 0,
355
+ "image": ""
356
+ }
357
+ except Exception as e:
358
+ logger.error(f"Unexpected error: {e}", exc_info=True)
359
+ return {
360
+ "error": f"Unexpected error: {str(e)}",
361
+ "predictions": [],
362
+ "total_detections": 0,
363
+ "image": ""
364
+ }
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ requests>=2.31.0
2
+ Pillow>=10.0.0