File size: 10,926 Bytes
8387086
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
"""
Cloud Infrastructure - AWS (S3 + Rekognition)
Mirrors the Google Cloud setup (setup_gcs.py) but uses:
  - AWS S3       β†’ image storage (equivalent to GCS)
  - AWS Rekognition β†’ object detection (equivalent to GCP Vision API)

Run once to set up: python -m cloud_infra.setup_aws
"""

import os
import json
from pathlib import Path
from loguru import logger


# ─── Config from .env ─────────────────────────────────────────────
AWS_ACCESS_KEY_ID     = os.getenv("AWS_ACCESS_KEY_ID", "")
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "")
AWS_REGION            = os.getenv("AWS_REGION", "us-east-1")
S3_BUCKET_NAME        = os.getenv("S3_BUCKET_NAME", "warehouse-visual-intelligence")


def get_s3_client():
    """Return a boto3 S3 client using env credentials."""
    import boto3
    return boto3.client(
        "s3",
        region_name=AWS_REGION,
        aws_access_key_id=AWS_ACCESS_KEY_ID,
        aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
    )


def get_rekognition_client():
    """Return a boto3 Rekognition client."""
    import boto3
    return boto3.client(
        "rekognition",
        region_name=AWS_REGION,
        aws_access_key_id=AWS_ACCESS_KEY_ID,
        aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
    )


# ─── S3 Bucket Setup ──────────────────────────────────────────────

def create_bucket(bucket_name: str = S3_BUCKET_NAME, region: str = AWS_REGION) -> bool:
    """
    Create an S3 bucket if it does not already exist.

    Args:
        bucket_name: Name of the S3 bucket
        region: AWS region to create bucket in

    Returns:
        True on success, False on failure
    """
    try:
        s3 = get_s3_client()

        # Check if bucket already exists
        existing = [b["Name"] for b in s3.list_buckets().get("Buckets", [])]
        if bucket_name in existing:
            logger.info(f"S3 bucket already exists: s3://{bucket_name}")
            return True

        # us-east-1 does NOT accept LocationConstraint
        if region == "us-east-1":
            s3.create_bucket(Bucket=bucket_name)
        else:
            s3.create_bucket(
                Bucket=bucket_name,
                CreateBucketConfiguration={"LocationConstraint": region},
            )

        # Block all public access (security best practice)
        s3.put_public_access_block(
            Bucket=bucket_name,
            PublicAccessBlockConfiguration={
                "BlockPublicAcls": True,
                "IgnorePublicAcls": True,
                "BlockPublicPolicy": True,
                "RestrictPublicBuckets": True,
            },
        )

        logger.success(f"S3 bucket created: s3://{bucket_name} in {region}")
        return True

    except Exception as e:
        logger.error(f"Failed to create S3 bucket: {e}")
        return False


# ─── Upload / Download ────────────────────────────────────────────

def upload_image(local_path: Path, s3_folder: str = "input/") -> str | None:
    """
    Upload a local image to S3.

    Args:
        local_path: Path to the local image file
        s3_folder: Destination folder prefix in S3

    Returns:
        S3 URI (s3://bucket/key) or None on failure
    """
    try:
        s3 = get_s3_client()
        key = s3_folder + local_path.name
        s3.upload_file(str(local_path), S3_BUCKET_NAME, key)
        uri = f"s3://{S3_BUCKET_NAME}/{key}"
        logger.success(f"Uploaded: {local_path.name} β†’ {uri}")
        return uri
    except Exception as e:
        logger.error(f"S3 upload failed: {e}")
        return None


def upload_report(report_path: Path, s3_folder: str = "reports/") -> str | None:
    """Upload a JSON report to S3."""
    return upload_image(report_path, s3_folder=s3_folder)


def download_images(s3_folder: str = "input/", local_dir: Path = Path("data/downloaded/")) -> list[Path]:
    """
    Download all images from an S3 folder to a local directory.

    Args:
        s3_folder: S3 key prefix to list and download
        local_dir: Local directory to save files into

    Returns:
        List of downloaded local file paths
    """
    local_dir.mkdir(parents=True, exist_ok=True)
    downloaded = []
    try:
        s3 = get_s3_client()
        response = s3.list_objects_v2(Bucket=S3_BUCKET_NAME, Prefix=s3_folder)
        for obj in response.get("Contents", []):
            key = obj["Key"]
            local_path = local_dir / Path(key).name
            s3.download_file(S3_BUCKET_NAME, key, str(local_path))
            downloaded.append(local_path)
            logger.debug(f"Downloaded: {key} β†’ {local_path}")
        logger.info(f"Downloaded {len(downloaded)} file(s) to {local_dir}")
    except Exception as e:
        logger.error(f"S3 download failed: {e}")
    return downloaded


# ─── AWS Rekognition Detection ────────────────────────────────────

def detect_labels_from_bytes(image_bytes: bytes, min_confidence: float = 40.0) -> list[dict]:
    """
    Run AWS Rekognition label detection on raw image bytes.
    Equivalent to Google Cloud Vision object localisation.

    Args:
        image_bytes: JPEG/PNG image as bytes
        min_confidence: Minimum confidence threshold (0-100)

    Returns:
        List of label dicts with name, confidence, and bounding box
    """
    try:
        rekognition = get_rekognition_client()
        response = rekognition.detect_labels(
            Image={"Bytes": image_bytes},
            MaxLabels=20,
            MinConfidence=min_confidence,
        )

        results = []
        for label in response.get("Labels", []):
            for instance in label.get("Instances", []):
                box = instance.get("BoundingBox", {})
                results.append({
                    "name": label["Name"],
                    "confidence": round(label["Confidence"], 2),
                    "bounding_box": {
                        "left":   box.get("Left", 0),
                        "top":    box.get("Top", 0),
                        "width":  box.get("Width", 0),
                        "height": box.get("Height", 0),
                    },
                })
            # Labels without instances (scene-level labels)
            if not label.get("Instances"):
                results.append({
                    "name": label["Name"],
                    "confidence": round(label["Confidence"], 2),
                    "bounding_box": None,
                })

        logger.debug(f"Rekognition: {len(results)} label(s) detected")
        return results

    except Exception as e:
        logger.error(f"Rekognition detection failed: {e}")
        return []


def detect_labels_from_s3(s3_key: str, min_confidence: float = 40.0) -> list[dict]:
    """
    Run Rekognition directly on an image already stored in S3.
    More efficient than downloading first β€” no data transfer cost.

    Args:
        s3_key: S3 object key (e.g. 'input/warehouse_01.jpg')
        min_confidence: Minimum confidence threshold

    Returns:
        List of label dicts
    """
    try:
        rekognition = get_rekognition_client()
        response = rekognition.detect_labels(
            Image={"S3Object": {"Bucket": S3_BUCKET_NAME, "Name": s3_key}},
            MaxLabels=20,
            MinConfidence=min_confidence,
        )
        labels = response.get("Labels", [])
        logger.debug(f"Rekognition (S3): {len(labels)} label(s) for {s3_key}")
        return labels

    except Exception as e:
        logger.error(f"Rekognition S3 detection failed: {e}")
        return []


def detect_ppe(image_bytes: bytes) -> list[dict]:
    """
    Detect PPE (hard hats, masks, hand covers) using Rekognition PPE API.
    Useful for warehouse safety compliance checks.

    Args:
        image_bytes: JPEG/PNG image as bytes

    Returns:
        List of person-level PPE detection results
    """
    try:
        rekognition = get_rekognition_client()
        response = rekognition.detect_protective_equipment(
            Image={"Bytes": image_bytes},
            SummarizationAttributes={
                "MinConfidence": 80.0,
                "RequiredEquipmentTypes": ["HEAD_COVER", "HAND_COVER", "FACE_COVER"],
            },
        )
        persons = response.get("Persons", [])
        logger.debug(f"PPE check: {len(persons)} person(s) analysed")
        return persons

    except Exception as e:
        logger.error(f"PPE detection failed: {e}")
        return []


# ─── Cost Estimation Helpers ──────────────────────────────────────

def estimate_rekognition_cost(num_images: int) -> dict:
    """
    Estimate AWS Rekognition API cost for a given number of images.
    Pricing as of 2024: $0.001 per image (first 1M images/month).

    Args:
        num_images: Number of images to process

    Returns:
        Cost breakdown dict
    """
    price_per_image = 0.001  # USD
    total = round(num_images * price_per_image, 4)
    return {
        "num_images": num_images,
        "price_per_image_usd": price_per_image,
        "estimated_total_usd": total,
        "note": "AWS Rekognition label detection pricing (first 1M images/month)",
    }


def estimate_s3_cost(storage_gb: float, requests: int = 1000) -> dict:
    """
    Estimate S3 storage and request costs.
    Pricing: ~$0.023/GB storage, $0.0004 per 1000 PUT requests.

    Args:
        storage_gb: Estimated data stored in GB
        requests: Number of PUT/GET requests

    Returns:
        Cost breakdown dict
    """
    storage_cost = round(storage_gb * 0.023, 4)
    request_cost = round((requests / 1000) * 0.0004, 4)
    return {
        "storage_gb": storage_gb,
        "storage_cost_usd": storage_cost,
        "requests": requests,
        "request_cost_usd": request_cost,
        "total_usd": round(storage_cost + request_cost, 4),
        "note": "AWS S3 Standard pricing (us-east-1)",
    }


# ─── CLI Entry Point ──────────────────────────────────────────────

if __name__ == "__main__":
    logger.info("Setting up AWS infrastructure...")
    logger.info(f"Region : {AWS_REGION}")
    logger.info(f"Bucket : s3://{S3_BUCKET_NAME}")

    success = create_bucket()

    if success:
        logger.success("AWS S3 bucket ready.")
        cost = estimate_rekognition_cost(100)
        logger.info(f"Cost estimate for 100 images: ${cost['estimated_total_usd']}")
    else:
        logger.error("Setup failed. Check your AWS credentials in .env")