# PRD — datafy_rekognition

Evidence date: 2026-10-17. Source: `git@bitbucket.org:overdrive/datafy_rekognition.git`, `main` at `1d03653` (2024-06-03, Dirk, "Adding function to detect images").

There is no prior PRD in Silicon Ark and no requirements document in the source repository. `readme.md` is a TensorFlow tutorial. `DatafyRek/README.md` is the AWS SAM hello-world sample and still names `hello_world` and API Gateway. Requirements below are taken from code, the training tree, and those two files. Each item is labelled.

## Product purpose

Give an operator a labelled set of Rhodes packaged-goods photos, notes for a TensorFlow detector, and a Lambda that runs Amazon Rekognition Custom Labels on a new S3 object and stores the first predicted name in DynamoDB.

Provenance: **Inferred** from `training/` label strings, `readme.md`, and `DatafyRek/aws_rekognition/app.py`. No file states a customer, a store process, or a success measure.

`datafy_web` documents a client call to `POST /analyze-image` on an API Gateway in `eu-west-1`. This repository does not implement that route. Whether this Lambda is that backend is **Unknown / Requires Confirmation**.

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Person who labelled images | Drew Pascal VOC boxes. One XML path is `C:/Users/User/OneDrive/Desktop/windows_v1.8.1 (1)/windows_v1.8.1/TRAIN/...` | Sample XML under `training/100% JUICE/MUSI 6X200ML RHODES JUICE/MUSI 6X200ML APPLE/` | Implemented path string. The person's identity is **Unknown** |
| Jaco Brits | Initial commit, Sonar files, training tree, readme | Commits `eaff27e` through `9673edc` | Implemented in git history |
| Dirk | Added the SAM function | Commit `1d03653` | Implemented in git history |
| S3 event | Invokes the Lambda. There is no human caller in the template | `template.yaml` event `s3:ObjectCreated:*` | Implemented |
| Reader of `readme.md` | Would create a venv and run TensorFlow commands on a workstation | `readme.md` | Documented commands. The reader is not named |

No login, role, or permission model exists in application code. Who may clone the Bitbucket repo is **Unknown / Requires Confirmation**.

Folder prefixes `MUSI`, `SHANNON`, and `YOLANDA` group juice images. What those prefixes mean (person, batch, or something else) is **Unknown / Requires Confirmation**.

## Main workflows

### W1 — Collect and label images

What is in git, not what a future user must do:

- Images sit beside XML under `training/`, grouped by category and pack.
- Every parsed XML file is Pascal VOC (`<annotation>`, `<object>`, `<bndbox>`) and `<source><database>Unknown</database>`.
- 1,530 XML files parse. None has zero objects. Box count across labels is 4,930.
- 1,564 images (1,520 `.jpg`, 44 `.jpeg`). 52 images have no same-stem XML. 18 XML files have no same-stem image. 16 of those 18 are the whole of `training/JAM TRAINING/` (XML only).
- `training/JAM/Photos/` is three WhatsApp JPEGs and no XML.
- One extra text file, `training/JAM/APRICOT 900G/WhatsApp Image 2024-04-16 at 11.15.01.txt`, has 12 lines of five numbers (`class cx cy w h` style). Provenance of the format: **Inferred** as YOLO. The file does not name the format.
- `training/JAM/APRICOT 900G/classes.txt` lists 15 stock-looking names (`dog` through `hamburger`, then `cavity`) and then `rhodes_mixed_fruit_jam_900g` and `rhodes_smooth_apricot_jam_900g`. The YOLO file's class id `16` is the last line if ids start at 0. That mapping is **Inferred**.

The readme instead tells the reader to build `dataset/images` and `dataset/annotations` and to save LabelImg as Pascal VOC. Provenance: **Documented**. That layout is not the layout in `training/`.

### W2 — Train with TensorFlow (documented only)

`readme.md` tells the reader to install TensorFlow, OpenCV, and matplotlib, clone `tensorflow/models`, paste `generate_tfrecord.py`, point a Model Zoo `pipeline.config` at the data, run `model_main_tf2.py` for `--num_train_steps=200000`, export with `exporter_main_v2.py`, and draw boxes when `score > 0.5`.

`class_text_to_int` in the pasted script returns `1` only when the label equals `object`. Every label in `training/` is a Rhodes string, not `object`. Provenance: **Documented** script text. The script is not a file in the repo, so this path is not **Implemented**.

### W3 — Detect a new S3 object with Rekognition Custom Labels

Trigger: `s3:ObjectCreated:*` on bucket `SrcBucket`. Provenance: **Implemented** in `template.yaml`.

`lambda_handler` then:

1. Reads `project_arn`, `model_arn`, and `version_name` from the environment.
2. Calls `start_project_version` with `MinInferenceUnits=1` and waits on waiter `project_version_running`.
3. For each record, reads `s3.bucket.name` and `s3.object.key` and calls `detect_custom_labels` with `MinConfidence=95`.
4. Appends the S3 key, the `CustomLabels` list, and the list length.
5. Writes DynamoDB (W4).
6. Returns HTTP-shaped `{statusCode: 200, body: ...}` even though the trigger is S3.
7. In `finally`, calls `stop_project_version`.

Provenance: **Implemented** in `DatafyRek/aws_rekognition/app.py`. Lines that are constants, not configuration: inference units `1`, confidence `95`.

`events/event.json` is an API Gateway `/hello` event, not an S3 event. `sam local invoke` with that file would not find `Records`. Provenance: **Implemented** file content versus handler.

### W4 — Store the first label

`write_dynamodb` calls `PutItem` on `os.environ['dynamo_table_name']`.

| Attribute | Value written | Provenance |
|---|---|---|
| `key` (string) | S3 object key | Implemented |
| `predicted` (string) | `CustomLabels[0].Name` only | Implemented |
| `label_count` (number) | Length of `CustomLabels` | Implemented |

The full label list is commented out. If `CustomLabels` is empty, `response['Response'][0]` raises. The exception is re-raised after `finally` stops the model. Provenance: **Implemented**.

A comment directly above the function says `replace with postgress function`. Provenance: **Documented** comment. No PostgreSQL client, connection string, or schema is in the repo. Treat the replacement as **Planned** only if a human confirms the comment is still intended.

## Functional requirements

### FR-001 — Labelled training images are stored in git

The repository contains the image and XML counts in W1, under `training/100% JUICE`, `training/INFANT FOOD`, `training/JAM`, and `training/JAM TRAINING`, plus `training/RFG CATALOGUE 2024.pptx` and a saved OneDrive page.

Provenance: **Implemented**.

The `.pptx` bytes were not text-extracted for this PRD. What the catalogue says is **Unknown**. The `.mhtml` subject is `MUSI 6X200ML FRUIT MEDLEY - OneDrive`, saved Monday 6 May 2024. Provenance: **Documented** in the MHTML headers. The OneDrive account URL is omitted here.

### FR-002 — Annotation label strings

Object names found in XML, with box counts:

| Boxes | Label |
|---:|---|
| 774 | `rhodes_smooth_apricot_jam_900g` |
| 417 | `rhodes_juice_apple_cranberry_2l` |
| 385 | `Rhodes_cranberry_juice_1L` |
| 337 | `rhodes_juice_mango_orange_2l` |
| 328 | `rhodes_juice_red_grape_2l` |
| 285 | `rhodes_smooth_apricot_jam_450g` |
| 206 | `rhodes_juice_apple_2l` |
| 205 | `Rhodes_orange_juice_1L` |
| 193 | `rhodes_juice_orange_6x200ml` |
| 187 | `rhodes_juice_apple_6x200ml` |
| 180 | `rhodes_juice_fruit_medley_6x200ml` |
| 160 | `Rhodes_apple_juice_1L` |
| 147 | `rhodes_squish_pear_banana_strawberry_110ml` |
| 137 | `rhodes_juice_litchi_6x200ml` |
| 101 | `rhodes_squish_apple_guava_110ml` |
| 100 | `rhodes_squish_fruit_salad_110ml` |
| 99 | `rhodes_squish_summer_berries_110ml` |
| 94 | `rhodes_squish_apple_banana_yoghurt_110ml` |
| 77 | `rhodes_juice_apple_1l` |
| 77 | `rhodes_squish_apple_110ml` |
| 77 | `rhodes_squish_apple_guava_yoghurt_110ml` |
| 73 | `Rhodes_juice_orange_1L` |
| 73 | `rhodes_squish_pear_110ml` |
| 61 | `rhodes_squish_mixed_vegetables_110ml` |
| 59 | `Rhodes_juice_apple_1L` |
| 54 | `rhodes_mixed_fruit_jam_900g` |
| 42 | `rhodes_mixed_fruit_jam_450g` |
| 1 | `rhodes test pic` |
| 1 | `rhodes_juice_fruit_medley` |

Provenance: **Implemented** (parsed `<name>` text). Casing and spelling are not normalised. Whether `rhodes_juice_apple_1l` and `Rhodes_juice_apple_1L` are one SKU is **Unknown / Requires Confirmation**.

Juice boxes are only under `training/100% JUICE`. Squish boxes are only under `training/INFANT FOOD`. Jam boxes are under `training/JAM` and `training/JAM TRAINING`. `rhodes test pic` is one box under `JAM TRAINING`.

### FR-003 — S3 upload starts detection

The Lambda runs when an object is created in `SrcBucket`. It does not filter by suffix, content type, or prefix. Provenance: **Implemented**. The bucket name in the template is the placeholder `<Bucket-name>`, so a deploy of this file as written fails CloudFormation name rules. Provenance: **Implemented** template text. A real bucket name used in an account is **Unknown**.

### FR-004 — Custom Labels model is started and stopped per invocation

Each invocation starts the project version, waits until the waiter reports running, detects, then stops the version in `finally`. `start_model` prints exceptions and does not re-raise, so detection still runs after a failed start. Provenance: **Implemented**.

`MinInferenceUnits` is `1`. Provenance: **Implemented** constant. Whether that unit count is an approved cost rule is **Unknown / Requires Confirmation**.

If the waiter runs longer than the remaining Lambda time, the 900 second timeout can end the process before `finally`. That timeout behaviour is **Inferred** from the template timeout plus the waiter. It was not executed here.

### FR-005 — Minimum confidence is 95

`detect_custom_labels` is called with `MinConfidence=95`. Provenance: **Implemented** constant. The readme's OpenCV sample uses `score > 0.5`. Those are different thresholds in different documents. Which one an operator should use is **Unknown / Requires Confirmation**.

### FR-006 — Only the first label is stored

DynamoDB receives the first `Name` and the count, not bounding boxes, confidence, or later names. Provenance: **Implemented**. The Custom Labels response can include geometry. The code has a comment about displaying an image for object detection, and the `display_image` call is commented. `display_image` is not defined. Pillow imports are commented. Provenance: **Implemented**.

### FR-007 — Required environment

The handler requires `project_arn`, `model_arn`, `version_name`, and `dynamo_table_name`. Provenance: **Implemented** (`os.environ[...]`). `template.yaml` has no `Environment` block and no DynamoDB table or Rekognition project. A missing variable raises `KeyError` before the `try` that stops the model. Provenance: **Implemented**.

### FR-008 — IAM as declared

The function policy list is `AmazonS3FullAccess` and `AmazonRekognitionFullAccess`. Provenance: **Implemented**. There is no DynamoDB policy in the template. `PutItem` would be denied unless a role outside this file grants it. That denial is **Inferred** from the policy list. It was not deployed here.

`DatafyRek/README.md` says SAM example roles are scoped to minimum permissions. That sentence is the stock sample. It does not match these two managed policies. Provenance: **Documented** versus **Implemented**.

### FR-009 — Tests still expect hello world

`tests/unit/test_handler.py` imports `hello_world.app` and expects body message `hello world`. `tests/integration/test_api_gateway.py` looks for stack output `HelloWorldApi` and GETs it. The template outputs `AWSRekognitionFunction` and `AWSRekognitionFunctionIamRole` only. There is no `hello_world` package and no API event on the function. Provenance: **Implemented**. The tests do not describe the Rekognition behaviour.

### FR-010 — SonarQube on main

Pushes to `main` run `sonarsource/sonarqube-scan:2.0.1` with `SONAR_HOST_URL` and `SONAR_TOKEN` from repository or workspace variables. Clone depth is full. Provenance: **Documented** in `bitbucket-pipelines.yml`. The host URL and whether the pipeline is green are **Unknown**. The image is `maven:3.3.9`. There is no Maven project in the repo.

## Integrations

| System | Purpose | Direction | Auth in repo | Failure behaviour |
|---|---|---|---|---|
| Amazon S3 | Event source and image bytes for `DetectCustomLabels` (`S3Object` bucket + key) | Inbound event; Rekognition reads the object | Lambda role, `AmazonS3FullAccess` | Not handled specifically. Handler `except` re-raises |
| Amazon Rekognition Custom Labels | `start_project_version`, waiter, `describe_project_versions`, `detect_custom_labels`, `stop_project_version` | Outbound | `AmazonRekognitionFullAccess`. Region is the boto3 default | Start errors are printed. Other errors propagate. Stop errors are printed |
| Amazon DynamoDB | `PutItem` of key, first label, count | Outbound | Environment table name. No policy in the template | Exception propagates; `finally` still stops the model |
| SonarQube | Static scan of `main` | Outbound from Bitbucket Pipelines | `SONAR_TOKEN` variable, value not in git | Not described |
| TensorFlow Models (GitHub) | Readme says `git clone https://github.com/tensorflow/models.git` | Documented command only | None | Not a runtime of this repo |
| OneDrive | An MHTML snapshot of a folder page is committed | File in git, not an API client | None in code | Not applicable |

No queue, no second Lambda, no API Gateway resource, no PostgreSQL connection, and no Rekognition `CreateProject` or `CreateDataset` call is in the tree.

## Important constraints

- Lambda timeout 900 seconds, memory 1028 MB, architecture `x86_64`, tracing active, JSON logs. Provenance: **Implemented** in `template.yaml` globals.
- Python 3.9. Provenance: **Implemented** runtime.
- Confidence 95 and one inference unit. Provenance: **Implemented** constants.
- DynamoDB item identity is the S3 key alone. A later object with the same key overwrites the item if the table key is that attribute. The table key schema is **Unknown** because the table is not in the repo. Overwrite behaviour of `PutItem` is **Inferred** from the API call that is written.
- Two invocations can start and stop the same project version. Provenance: **Inferred** from a shared `model_arn` and no lock in code.
- Training labels are case-sensitive strings. The Lambda does not map them. It stores whatever Rekognition returns.

## Behaviour the SAM readme claims that the template does not implement

`DatafyRek/README.md` describes `sam local start-api`, `curl http://localhost:3000/`, route `GET /hello`, `HelloWorldFunction`, and stack output of an API URL. `template.yaml` has an S3 event and no `AWS::Serverless::Api` event. Provenance: **Documented** sample versus **Implemented** template. The sample was not updated when the function was added (`1d03653` added both files together).
