apoorvrajdev commited on
Commit
f30f737
Β·
1 Parent(s): c062f77

feat(ci): add deploy-backend workflow + Phase 2C runbook + Live Demo README section

Browse files
.github/workflows/deploy-backend.yml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy backend to HuggingFace Space
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: ["CI"]
6
+ types: [completed]
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ group: deploy-backend
12
+ cancel-in-progress: false
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ push-to-space:
19
+ name: Push main to HF Space
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 10
22
+ if: >-
23
+ github.event_name == 'workflow_dispatch' ||
24
+ (github.event.workflow_run.conclusion == 'success' &&
25
+ github.event.workflow_run.head_branch == 'main')
26
+ env:
27
+ HF_USERNAME: apoorvrajdev
28
+ HF_SPACE: image-captioning-api
29
+ steps:
30
+ - name: Checkout full history
31
+ uses: actions/checkout@v4
32
+ with:
33
+ fetch-depth: 0
34
+
35
+ - name: Configure git identity
36
+ run: |
37
+ git config user.name "apoorvrajdev"
38
+ git config user.email "apoorvrajmgr@gmail.com"
39
+
40
+ - name: Push to HuggingFace Space
41
+ env:
42
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
43
+ run: |
44
+ if [ -z "${HF_TOKEN}" ]; then
45
+ echo "::error::HF_TOKEN secret is not set. Add it under repo Settings β†’ Secrets and variables β†’ Actions."
46
+ exit 1
47
+ fi
48
+ git remote add space "https://${HF_USERNAME}:${HF_TOKEN}@huggingface.co/spaces/${HF_USERNAME}/${HF_SPACE}"
49
+ git push space HEAD:main
README.md CHANGED
@@ -28,7 +28,7 @@ short_description: InceptionV3 + Transformer image captioning inference API
28
  <p align="center">
29
  <img alt="Ruff" src="https://img.shields.io/badge/lint-ruff-261230?style=flat-square&logo=ruff&logoColor=white">
30
  <img alt="mypy strict" src="https://img.shields.io/badge/typed-mypy%20strict-1F5082?style=flat-square">
31
- <img alt="Tests" src="https://img.shields.io/badge/tests-90%20passing-brightgreen?style=flat-square">
32
  <img alt="Pre-commit" src="https://img.shields.io/badge/pre--commit-enabled-FAB040?style=flat-square&logo=pre-commit&logoColor=white">
33
  <img alt="IEEE Published" src="https://img.shields.io/badge/IEEE-published-00629B?style=flat-square&logo=ieee&logoColor=white">
34
  <img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue?style=flat-square">
@@ -48,6 +48,20 @@ short_description: InceptionV3 + Transformer image captioning inference API
48
 
49
  ---
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  ## πŸ“Œ What Is This Project?
52
 
53
  Image Captioning System is a research-to-production conversion of the IEEE paper *"AI Narratives: Bridging Visual Content and Linguistic Expression"*. The original work β€” a Kaggle notebook training an InceptionV3-encoder + multi-head Transformer-decoder on MS COCO β€” is preserved verbatim as the canonical research artefact. Around it sits a typed Python package, a FastAPI inference service, and a React SPA that together turn the published model into something a serving team could actually run, version, and reason about.
@@ -552,20 +566,20 @@ The backend test suite ([`backend/app/tests/`](backend/app/tests/)) introduced i
552
  - [x] **2B-6** β€” Single `ErrorBanner` surface mapping every `ApiError.kind` to actionable copy
553
  - [x] **2B-7** β€” CORS allow-list wired through backend YAML (`serve.cors_allowed_origins`), dev origins pre-allowed
554
 
555
- ### Phase 2C β€” Public deployment 🚧 (in progress)
556
 
557
  - [x] **WS-A** β€” Backend containerisation: `Dockerfile` (python:3.11-slim, non-root UID 1000, EXPOSE 7860, HEALTHCHECK on `/healthz`) + `.dockerignore` + corrected `.env.example` schema
558
  - [x] **WS-A4** β€” Lifespan integration with HuggingFace Hub: extended `BackendSettings` with `weights_hub_repo` / `weights_hub_revision` / `weights_hub_filename` / `weights_cache_dir`; new `app.services.weights_loader.resolve_weights` calls `huggingface_hub.snapshot_download` when configured, falls back to local paths otherwise (4 new unit tests, downloader injected for offline testing)
559
  - [x] **WS-B** β€” Uploaded dev-scaffold weights + tokenizer to [`apoorvrajdev/captioning-inceptionv3-transformer`](https://huggingface.co/apoorvrajdev/captioning-inceptionv3-transformer) on HuggingFace Hub, tagged `v1.0.0`, verified via `snapshot_download` (SHA-256 hashes match local artefacts byte-for-byte)
560
  - [x] **WS-C** β€” First manual deploy to [`apoorvrajdev/image-captioning-api`](https://huggingface.co/spaces/apoorvrajdev/image-captioning-api) on HuggingFace Spaces (Docker SDK, cpu-basic, port 7860, single worker) β€” Space variables wire `BACKEND_WEIGHTS_HUB_REPO` / `_REVISION` / `_FILENAME` + `BACKEND_WARMUP=true`; lifespan pulls weights from the Hub on cold start; `/healthz` returns `model_loaded: true` and `/v1/captions` verified end-to-end via Swagger UI
561
  - [x] **WS-D** β€” **Backend test suite** ([`backend/app/tests/`](backend/app/tests/)): 12 route tests covering the full `/healthz` + `/v1/captions` contract (200 / 400 / 413 / 415 / 422 / 503) with a duck-typed `FakePredictorService` β€” no TF loaded, full slice runs in 0.3 s
562
- - [ ] **WS-E** β€” Frontend deploy to Vercel (static SPA, `VITE_API_BASE` baked at build time, SPA rewrites)
563
- - [ ] **WS-F** β€” Production CORS: add the deployed Vercel origin to `serve.cors_allowed_origins`
564
- - [ ] **WS-G** β€” GitHub Actions CI/CD:
565
  - [x] `ci.yml` β€” Python quality (ruff lint + format check, mypy), pytest matrix on 3.10/3.11/3.12, notebook SHA-256 freeze check, frontend lint + build, concurrency cancel-in-progress, pip + npm caching
566
- - [ ] `deploy-backend.yml` β€” gated on `needs: ci`, pushes to the HF Space
567
- - [ ] `deploy-frontend.yml` *(optional)* β€” Vercel-native GitHub integration is the recommended path
568
- - [ ] **WS-H** β€” README "Live Demo" section (badges swapped to live HF Space + Vercel URLs) + `docs/PHASE_2C_DEPLOYMENT_RUNBOOK.md` + `docs/CI.md`
569
 
570
  ### Phase 3 β€” Multimodal baselines ⏳ (planned)
571
 
 
28
  <p align="center">
29
  <img alt="Ruff" src="https://img.shields.io/badge/lint-ruff-261230?style=flat-square&logo=ruff&logoColor=white">
30
  <img alt="mypy strict" src="https://img.shields.io/badge/typed-mypy%20strict-1F5082?style=flat-square">
31
+ <img alt="Tests" src="https://img.shields.io/badge/tests-94%20passing-brightgreen?style=flat-square">
32
  <img alt="Pre-commit" src="https://img.shields.io/badge/pre--commit-enabled-FAB040?style=flat-square&logo=pre-commit&logoColor=white">
33
  <img alt="IEEE Published" src="https://img.shields.io/badge/IEEE-published-00629B?style=flat-square&logo=ieee&logoColor=white">
34
  <img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue?style=flat-square">
 
48
 
49
  ---
50
 
51
+ ## 🌐 Live Demo
52
+
53
+ | Component | URL | What you can do |
54
+ |---|---|---|
55
+ | **Frontend SPA** | https://image-captioning-system.vercel.app | Drag-and-drop an image, hit **Generate caption**, see the typed `CaptionResponse` rendered with model version, decode strategy, and latency |
56
+ | **Backend API** | https://apoorvrajdev-image-captioning-api.hf.space | Interactive Swagger at [`/docs`](https://apoorvrajdev-image-captioning-api.hf.space/docs); liveness + readiness at [`/healthz`](https://apoorvrajdev-image-captioning-api.hf.space/healthz); inference at `POST /v1/captions` |
57
+ | **Weights (HF Hub)** | https://huggingface.co/apoorvrajdev/captioning-inceptionv3-transformer | Pinned to tag `v1.0.0`; the backend pulls these at lifespan startup via `snapshot_download` so the Space's git tree never contains the `.h5` |
58
+
59
+ Deployment topology: GitHub `main` β†’ CI on every push β†’ on green, `deploy-backend.yml` pushes to a HuggingFace Space (Docker SDK, cpu-basic, port 7860, single uvicorn worker); Vercel's Git integration builds and promotes the SPA in parallel. Production CORS is wired through the Space's `CAPTIONING__SERVE__CORS_ALLOWED_ORIGINS` variable, not a hardcoded config. Full topology + rollback procedure: [`docs/PHASE_2C_DEPLOYMENT_RUNBOOK.md`](docs/PHASE_2C_DEPLOYMENT_RUNBOOK.md). CI/CD workflows: [`docs/CI.md`](docs/CI.md).
60
+
61
+ > ⚠️ The live caption you get will look like noise (e.g. `"plate city mountain [UNK]"`). That is the dev-scaffold weight story above β€” the infrastructure is what's being demonstrated end-to-end; the trained checkpoint is a future `v2.0.0` swap with **zero code changes** (just a Space variable bump from `v1.0.0` β†’ `v2.0.0`).
62
+
63
+ ---
64
+
65
  ## πŸ“Œ What Is This Project?
66
 
67
  Image Captioning System is a research-to-production conversion of the IEEE paper *"AI Narratives: Bridging Visual Content and Linguistic Expression"*. The original work β€” a Kaggle notebook training an InceptionV3-encoder + multi-head Transformer-decoder on MS COCO β€” is preserved verbatim as the canonical research artefact. Around it sits a typed Python package, a FastAPI inference service, and a React SPA that together turn the published model into something a serving team could actually run, version, and reason about.
 
566
  - [x] **2B-6** β€” Single `ErrorBanner` surface mapping every `ApiError.kind` to actionable copy
567
  - [x] **2B-7** β€” CORS allow-list wired through backend YAML (`serve.cors_allowed_origins`), dev origins pre-allowed
568
 
569
+ ### Phase 2C β€” Public deployment βœ… (complete)
570
 
571
  - [x] **WS-A** β€” Backend containerisation: `Dockerfile` (python:3.11-slim, non-root UID 1000, EXPOSE 7860, HEALTHCHECK on `/healthz`) + `.dockerignore` + corrected `.env.example` schema
572
  - [x] **WS-A4** β€” Lifespan integration with HuggingFace Hub: extended `BackendSettings` with `weights_hub_repo` / `weights_hub_revision` / `weights_hub_filename` / `weights_cache_dir`; new `app.services.weights_loader.resolve_weights` calls `huggingface_hub.snapshot_download` when configured, falls back to local paths otherwise (4 new unit tests, downloader injected for offline testing)
573
  - [x] **WS-B** β€” Uploaded dev-scaffold weights + tokenizer to [`apoorvrajdev/captioning-inceptionv3-transformer`](https://huggingface.co/apoorvrajdev/captioning-inceptionv3-transformer) on HuggingFace Hub, tagged `v1.0.0`, verified via `snapshot_download` (SHA-256 hashes match local artefacts byte-for-byte)
574
  - [x] **WS-C** β€” First manual deploy to [`apoorvrajdev/image-captioning-api`](https://huggingface.co/spaces/apoorvrajdev/image-captioning-api) on HuggingFace Spaces (Docker SDK, cpu-basic, port 7860, single worker) β€” Space variables wire `BACKEND_WEIGHTS_HUB_REPO` / `_REVISION` / `_FILENAME` + `BACKEND_WARMUP=true`; lifespan pulls weights from the Hub on cold start; `/healthz` returns `model_loaded: true` and `/v1/captions` verified end-to-end via Swagger UI
575
  - [x] **WS-D** β€” **Backend test suite** ([`backend/app/tests/`](backend/app/tests/)): 12 route tests covering the full `/healthz` + `/v1/captions` contract (200 / 400 / 413 / 415 / 422 / 503) with a duck-typed `FakePredictorService` β€” no TF loaded, full slice runs in 0.3 s
576
+ - [x] **WS-E** β€” Frontend deploy to Vercel: `frontend/` imported as a Vite project, `VITE_API_BASE` env var baked at build time, production alias [`image-captioning-system.vercel.app`](https://image-captioning-system.vercel.app) auto-redeployed on every push to `main` via Vercel's GitHub integration
577
+ - [x] **WS-F** β€” Production CORS: deployed Vercel origin added to `serve.cors_allowed_origins` via the Space's `CAPTIONING__SERVE__CORS_ALLOWED_ORIGINS` variable (JSON array, pydantic-settings parsed), so the policy is explicit in app config rather than relying on the HF reverse-proxy default
578
+ - [x] **WS-G** β€” GitHub Actions CI/CD:
579
  - [x] `ci.yml` β€” Python quality (ruff lint + format check, mypy), pytest matrix on 3.10/3.11/3.12, notebook SHA-256 freeze check, frontend lint + build, concurrency cancel-in-progress, pip + npm caching
580
+ - [x] [`deploy-backend.yml`](.github/workflows/deploy-backend.yml) β€” chained via `workflow_run` after CI, pushes `HEAD:main` to the HF Space remote using the `HF_TOKEN` repo secret; also supports `workflow_dispatch` for manual redeploys
581
+ - [x] `deploy-frontend.yml` *(skipped β€” Vercel-native GitHub integration deploys on every push, no separate workflow needed)*
582
+ - [x] **WS-H** β€” "[Live Demo](#-live-demo)" section above + [`docs/PHASE_2C_DEPLOYMENT_RUNBOOK.md`](docs/PHASE_2C_DEPLOYMENT_RUNBOOK.md) (full topology, prerequisites, weights upload, Space setup, Vercel setup, CORS, CI/CD, smoke tests, known quirks, rollback) + [`docs/CI.md`](docs/CI.md) (workflow reference)
583
 
584
  ### Phase 3 β€” Multimodal baselines ⏳ (planned)
585
 
docs/CI.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CI / CD
2
+
3
+ GitHub Actions runs two workflows out of [`.github/workflows/`](../.github/workflows/).
4
+
5
+ ## `ci.yml` β€” quality + tests
6
+
7
+ Triggered on every push and pull request to `main`. Four parallel jobs:
8
+
9
+ | Job | What it runs | Why |
10
+ |---|---|---|
11
+ | `python-quality` | `ruff check`, `ruff format --check`, `mypy --strict` on `src/` and `backend/` | Catch style + typing regressions before they land |
12
+ | `python-tests` | `pytest` matrix on Python **3.10 / 3.11 / 3.12** | Confirm the package keeps working on every supported interpreter |
13
+ | `notebook-freeze` | `make freeze-paper-notebook` (SHA-256 check) | Fail if the IEEE notebook is mutated β€” it is the canonical research artefact |
14
+ | `frontend` | `npm ci`, `npm run lint`, `npm run build` on Node 20 | Catch ESLint + Vite build regressions in the SPA |
15
+
16
+ Caching:
17
+ - pip via `actions/setup-python` (key derived from `requirements*.txt` + `pyproject.toml`)
18
+ - npm via `actions/setup-node` (key derived from `frontend/package-lock.json`)
19
+
20
+ Concurrency: stacked runs on the same ref cancel each other so only the
21
+ newest commit's CI completes.
22
+
23
+ ## `deploy-backend.yml` β€” push main to the HF Space
24
+
25
+ Triggered by:
26
+ - `workflow_run` on `CI` completion, only when conclusion is `success` and
27
+ branch is `main` (so a failing CI never deploys)
28
+ - `workflow_dispatch` for manual redeploys from the Actions tab
29
+
30
+ The job:
31
+ 1. Checks out the full git history (HF Space remote needs the parent
32
+ commits to fast-forward)
33
+ 2. Sets a fixed git identity (`apoorvrajdev <apoorvrajmgr@gmail.com>`)
34
+ 3. Adds a `space` remote authenticated with the `HF_TOKEN` repository secret
35
+ 4. Pushes `HEAD:main` to the Space
36
+
37
+ The Space then rebuilds its Docker image. See
38
+ [`PHASE_2C_DEPLOYMENT_RUNBOOK.md`](PHASE_2C_DEPLOYMENT_RUNBOOK.md) for the
39
+ end-to-end deployment topology and smoke tests.
40
+
41
+ ## Required secrets
42
+
43
+ - `HF_TOKEN` β€” HuggingFace personal access token, **Write** scope. Used only
44
+ by `deploy-backend.yml` to push to the Space remote.
45
+
46
+ Set under repo Settings β†’ Secrets and variables β†’ Actions β†’ New repository
47
+ secret.
48
+
49
+ ## Local equivalents
50
+
51
+ Everything CI does is reproducible locally:
52
+
53
+ ```bash
54
+ make lint # ruff check + format --check
55
+ make typecheck # mypy strict
56
+ make test # pytest (single Python version)
57
+ make freeze-paper-notebook # SHA-256 freeze check
58
+
59
+ cd frontend
60
+ npm ci && npm run lint && npm run build
61
+ ```
docs/PHASE_2C_DEPLOYMENT_RUNBOOK.md ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 2C β€” Public Deployment Runbook
2
+
3
+ This runbook captures every step needed to (re)deploy the Image Captioning System
4
+ to its public hosts: weights to the HuggingFace Hub, backend to a HuggingFace
5
+ Space, frontend to Vercel, and the CI/CD chain wiring it all together. It is
6
+ written so a future maintainer (or the author six months from now) can rebuild
7
+ the public deployment from a cold start without reading commit history.
8
+
9
+ ## 0. Topology
10
+
11
+ ```
12
+ GitHub (apoorvrajdev/image-captioning-system, main)
13
+ β”œβ”€β”€ Actions: CI β†’ Deploy backend to HuggingFace Space (workflow_run chained)
14
+ └── Vercel Git Integration β†’ image-captioning-system.vercel.app
15
+
16
+ HuggingFace Hub
17
+ β”œβ”€β”€ Model repo: apoorvrajdev/captioning-inceptionv3-transformer (weights + vocab, tag v1.0.0)
18
+ └── Space: apoorvrajdev/image-captioning-api (Docker SDK, cpu-basic, port 7860)
19
+ ```
20
+
21
+ The Space pulls weights from the model repo at lifespan startup via
22
+ `huggingface_hub.snapshot_download`, so the Space's git tree never contains
23
+ `model.h5` β€” only the code that knows how to fetch it.
24
+
25
+ ---
26
+
27
+ ## 1. Live URLs
28
+
29
+ | Component | URL |
30
+ |---|---|
31
+ | Frontend SPA | `https://image-captioning-system.vercel.app` |
32
+ | Backend API | `https://apoorvrajdev-image-captioning-api.hf.space` |
33
+ | Backend health | `https://apoorvrajdev-image-captioning-api.hf.space/healthz` |
34
+ | Backend docs (Swagger) | `https://apoorvrajdev-image-captioning-api.hf.space/docs` |
35
+ | Weights repo | `https://huggingface.co/apoorvrajdev/captioning-inceptionv3-transformer` |
36
+ | Space console | `https://huggingface.co/spaces/apoorvrajdev/image-captioning-api` |
37
+
38
+ ---
39
+
40
+ ## 2. Prerequisites
41
+
42
+ - Local git working tree on `main`, clean
43
+ - Python 3.11 venv with `requirements.txt` + `requirements-dev.txt` installed
44
+ - A HuggingFace account and a personal access token with **Write** scope
45
+ (Settings β†’ Access Tokens). Used both in the local shell (`huggingface-cli login`)
46
+ and as a GitHub Actions secret named `HF_TOKEN`
47
+ - A Vercel account connected to the GitHub repo
48
+
49
+ ---
50
+
51
+ ## 3. Weights upload (WS-B) β€” only when shipping a new checkpoint
52
+
53
+ The Space's `BACKEND_WEIGHTS_HUB_REVISION` variable pins which Hub revision
54
+ the backend pulls at startup, so weights and code can be versioned
55
+ independently.
56
+
57
+ ```bash
58
+ # 1. Login (token cached at ~/.cache/huggingface/token)
59
+ huggingface-cli login
60
+
61
+ # 2. Upload the contents of models/vX.Y.Z/ to the Hub repo
62
+ python - <<'PY'
63
+ from huggingface_hub import HfApi
64
+ api = HfApi()
65
+ api.upload_folder(
66
+ repo_id="apoorvrajdev/captioning-inceptionv3-transformer",
67
+ folder_path="models/v1.0.0",
68
+ path_in_repo=".",
69
+ commit_message="upload v1.0.0 weights + vocab",
70
+ )
71
+ api.create_tag(
72
+ repo_id="apoorvrajdev/captioning-inceptionv3-transformer",
73
+ tag="v1.0.0",
74
+ tag_message="v1.0.0 dev-scaffold weights",
75
+ )
76
+ PY
77
+
78
+ # 3. Verify the snapshot round-trips byte-for-byte
79
+ HF_HUB_DISABLE_SYMLINKS=1 python - <<'PY'
80
+ import hashlib, pathlib
81
+ from huggingface_hub import snapshot_download
82
+ local = snapshot_download(
83
+ repo_id="apoorvrajdev/captioning-inceptionv3-transformer",
84
+ revision="v1.0.0",
85
+ )
86
+ for f in ("model.h5", "vocab.json"):
87
+ src = hashlib.sha256(pathlib.Path("models/v1.0.0", f).read_bytes()).hexdigest()
88
+ dst = hashlib.sha256(pathlib.Path(local, f).read_bytes()).hexdigest()
89
+ assert src == dst, f
90
+ print(f, "OK", src)
91
+ PY
92
+ ```
93
+
94
+ To promote a new checkpoint after this: bump the Space variable
95
+ `BACKEND_WEIGHTS_HUB_REVISION` from `v1.0.0` to the new tag (e.g. `v2.0.0`)
96
+ and the Space restarts with the new weights. No code change required.
97
+
98
+ ---
99
+
100
+ ## 4. Backend Space (WS-C) β€” one-time setup
101
+
102
+ 1. Create the Space at https://huggingface.co/new-space
103
+ - Owner: `apoorvrajdev` Β· Name: `image-captioning-api`
104
+ - SDK: **Docker** (blank template) Β· Hardware: **cpu-basic (free)** Β· Public
105
+ 2. In the Space's **Settings β†’ Variables and secrets**, add **Variables**
106
+ (not secrets β€” these are non-sensitive):
107
+
108
+ | Name | Value |
109
+ |---|---|
110
+ | `BACKEND_WEIGHTS_HUB_REPO` | `apoorvrajdev/captioning-inceptionv3-transformer` |
111
+ | `BACKEND_WEIGHTS_HUB_REVISION` | `v1.0.0` |
112
+ | `BACKEND_WEIGHTS_HUB_FILENAME` | `model.h5` |
113
+ | `BACKEND_WARMUP` | `true` |
114
+ | `CAPTIONING__SERVE__CORS_ALLOWED_ORIGINS` | `["https://image-captioning-system.vercel.app","http://localhost:5173","http://localhost:5174","http://127.0.0.1:5173","http://127.0.0.1:5174"]` |
115
+
116
+ 3. Add a `space` git remote and push `main`:
117
+ ```bash
118
+ git remote add space https://huggingface.co/spaces/apoorvrajdev/image-captioning-api
119
+ git push space main
120
+ ```
121
+ 4. Watch the Space's **Logs** tab. First build takes ~8–12 min (Docker base
122
+ pull, `apt-get`, `pip install -r requirements.txt` with TensorFlow,
123
+ weight download via `snapshot_download`, predictor warmup).
124
+ 5. When the badge in the Space header turns **Running**, verify:
125
+ ```bash
126
+ curl https://apoorvrajdev-image-captioning-api.hf.space/healthz
127
+ # {"status":"ok","model_loaded":true,"model_version":"v1.0.0",...}
128
+ ```
129
+
130
+ The README YAML frontmatter (`title`, `emoji`, `sdk: docker`, `app_port: 7860`,
131
+ etc.) is what tells the Space how to build. It must remain at the literal top
132
+ of `README.md`. GitHub auto-hides the frontmatter when rendering the README, so
133
+ the same file serves both audiences.
134
+
135
+ ---
136
+
137
+ ## 5. Frontend (WS-E) β€” Vercel one-time setup
138
+
139
+ 1. https://vercel.com/new β†’ import `apoorvrajdev/image-captioning-system`
140
+ 2. Configure:
141
+ - Framework Preset: **Vite** (auto-detected from `frontend/package.json`)
142
+ - Root Directory: `frontend`
143
+ - Build / Output / Install commands: leave on defaults
144
+ 3. Environment variable (Production + Preview):
145
+ - `VITE_API_BASE` = `https://apoorvrajdev-image-captioning-api.hf.space`
146
+ 4. Deploy. First build is ~90 sec. Production alias becomes
147
+ `https://image-captioning-system.vercel.app`.
148
+
149
+ After the initial import every push to `main` triggers an automatic Vercel
150
+ build via the GitHub integration β€” no separate GitHub Action required.
151
+
152
+ ---
153
+
154
+ ## 6. CORS (WS-F)
155
+
156
+ `backend/app/main.py` registers `CORSMiddleware` with
157
+ `config.serve.cors_allowed_origins`. The defaults in
158
+ [`configs/base.yaml`](../configs/base.yaml) cover localhost dev. Production
159
+ origins are added via the Space's `CAPTIONING__SERVE__CORS_ALLOWED_ORIGINS`
160
+ variable (JSON array, see Β§4). To add a new origin (e.g. a custom domain):
161
+ edit that variable, save, and the Space restarts (~30 sec, no rebuild).
162
+
163
+ ---
164
+
165
+ ## 7. CI/CD (WS-G)
166
+
167
+ Two workflows under [`.github/workflows/`](../.github/workflows/):
168
+
169
+ - **`ci.yml`** β€” runs on every push and PR to `main`:
170
+ - `python-quality`: ruff lint + format, mypy strict
171
+ - `python-tests`: pytest matrix on 3.10 / 3.11 / 3.12
172
+ - `notebook-freeze`: SHA-256 freeze check on the IEEE notebook
173
+ - `frontend`: `npm ci && npm run lint && npm run build`
174
+ - **`deploy-backend.yml`** β€” chained via `workflow_run`, runs only after a
175
+ successful `CI` run on `main`. Pushes `HEAD:main` to the Space remote using
176
+ the `HF_TOKEN` repository secret. Also supports `workflow_dispatch` for
177
+ manual redeploys.
178
+
179
+ ### Required GitHub secret
180
+
181
+ `HF_TOKEN` (repo Settings β†’ Secrets and variables β†’ Actions β†’ New repository
182
+ secret). Scope: **Write**. Used only for `git push` to the Space remote.
183
+
184
+ ---
185
+
186
+ ## 8. End-to-end smoke test
187
+
188
+ After any redeploy, verify in this order:
189
+
190
+ ```bash
191
+ # 1. Backend liveness + readiness
192
+ curl https://apoorvrajdev-image-captioning-api.hf.space/healthz
193
+
194
+ # 2. Backend caption round-trip (replace path with any local JPG/PNG)
195
+ curl -X POST https://apoorvrajdev-image-captioning-api.hf.space/v1/captions \
196
+ -F "image=@assets/sample.jpg"
197
+
198
+ # 3. Frontend loads + status badge flips to green
199
+ open https://image-captioning-system.vercel.app # macOS
200
+ # start https://image-captioning-system.vercel.app # Windows
201
+
202
+ # 4. Frontend ↔ backend integration (in the browser)
203
+ # Upload an image β†’ expect a 200 caption response from /v1/captions
204
+ # DevTools β†’ Network β†’ check no CORS errors
205
+ ```
206
+
207
+ ---
208
+
209
+ ## 9. Known operational quirks
210
+
211
+ - **Status badge briefly flips to "offline"** while a `/v1/captions` request is
212
+ in flight on the single uvicorn worker. The `/healthz` poll queues behind
213
+ inference and the frontend's 3 s timeout expires. The next 10 s poll
214
+ recovers. Cosmetic only β€” backend never actually goes down.
215
+ - **First request after Space idle is slow** (~5–10 s extra). HF Spaces
216
+ sleep idle containers; the next call wakes the container, which then runs
217
+ the lifespan startup (snapshot_download cache hit + predictor rewarmup).
218
+ - **Caption quality is gibberish** by design at `v1.0.0`. The shipped weights
219
+ are dev scaffolds from `scripts/bootstrap_dev_artifacts.py`. A real trained
220
+ checkpoint will be uploaded as `v2.0.0` and promoted via the Space variable
221
+ bump described in Β§3.
222
+
223
+ ---
224
+
225
+ ## 10. Rollback
226
+
227
+ - **Bad code on the Space**: `git push space <known-good-sha>:main --force`
228
+ (from a local checkout). Space rebuilds from that SHA.
229
+ - **Bad weights on the Hub**: bump the Space's
230
+ `BACKEND_WEIGHTS_HUB_REVISION` back to the previous tag (e.g. `v1.0.0`)
231
+ and save. Space restarts in ~30 s with the previous weights.
232
+ - **Bad frontend on Vercel**: dashboard β†’ Deployments β†’ previous green
233
+ deployment β†’ "Promote to Production" (one click, no rebuild).