Spaces:
Configuration error
Configuration error
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 +49 -0
- README.md +22 -8
- docs/CI.md +61 -0
- docs/PHASE_2C_DEPLOYMENT_RUNBOOK.md +233 -0
.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-
|
| 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
|
| 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 |
-
- [
|
| 563 |
-
- [
|
| 564 |
-
- [
|
| 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 |
-
- [
|
| 567 |
-
- [
|
| 568 |
-
- [
|
| 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).
|