Skip to content

Commit ec503ae

Browse files
committed
build(desktop): package exe artifact for Docker Hub
1 parent f19e781 commit ec503ae

8 files changed

Lines changed: 238 additions & 34 deletions

Dockerfile.desktop-artifact

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM alpine:3.20
2+
3+
ARG BUILD_COMMIT=unknown
4+
ARG BUILD_DATE=unknown
5+
6+
LABEL org.opencontainers.image.title="Trash Sorter Pro Desktop EXE Artifact" \
7+
org.opencontainers.image.description="Windows PySide6 desktop EXE bundle for Trash Sorter Pro; extract artifact, do not run GUI inside Docker." \
8+
org.opencontainers.image.source="https://github.com/JasonTM17/App_AI_powered_waste_sorting" \
9+
org.opencontainers.image.revision="${BUILD_COMMIT}" \
10+
org.opencontainers.image.created="${BUILD_DATE}"
11+
12+
WORKDIR /artifacts
13+
COPY dist/TrashSorterPro/ /artifacts/TrashSorterPro/
14+
15+
RUN test -f /artifacts/TrashSorterPro/TrashSorterPro.exe \
16+
&& test -f /artifacts/TrashSorterPro/desktop-exe.sha256 \
17+
&& test -f /artifacts/TrashSorterPro/desktop-release-manifest.json
18+
19+
CMD ["sh", "-c", "printf '%s\n' 'Trash Sorter Pro desktop EXE artifact' 'Extract /artifacts/TrashSorterPro from this image on Windows.' 'Verify /artifacts/TrashSorterPro/desktop-exe.sha256 before use.'"]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
**
2+
!dist/
3+
!dist/TrashSorterPro/
4+
!dist/TrashSorterPro/**

app/core/config.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ class ModelConfig(BaseModel):
172172
iou_threshold: float = Field(0.45, ge=0.0, le=1.0)
173173
input_size: int = 640
174174
half_precision: bool = True
175-
specialist: SpecialistModelConfig = Field(default_factory=SpecialistModelConfig)
175+
specialist: SpecialistModelConfig = Field(
176+
default_factory=lambda: SpecialistModelConfig.model_validate({})
177+
)
176178

177179
@field_validator("class_thresholds")
178180
@classmethod
@@ -420,8 +422,12 @@ class AppConfig(BaseModel):
420422
mappings: list[ClassMapping] = Field(default_factory=list)
421423
roi: RoiConfig = Field(default_factory=lambda: RoiConfig())
422424
capture: CaptureConfig = Field(default_factory=lambda: CaptureConfig(low_conf_threshold=0.6))
423-
auto_review_queue: AutoReviewQueueConfig = Field(default_factory=AutoReviewQueueConfig)
424-
hazardous_waste: HazardousWasteConfig = Field(default_factory=HazardousWasteConfig)
425+
auto_review_queue: AutoReviewQueueConfig = Field(
426+
default_factory=lambda: AutoReviewQueueConfig.model_validate({})
427+
)
428+
hazardous_waste: HazardousWasteConfig = Field(
429+
default_factory=lambda: HazardousWasteConfig.model_validate({})
430+
)
425431
speaker: SpeakerConfig = Field(
426432
default_factory=lambda: SpeakerConfig(
427433
enabled=False,

app/core/history.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
)
2424
from sqlalchemy.engine import Engine
2525

26+
from app.core.history_labels import infer_history_label, validate_review_label
2627
from app.core.waste_categories import (
2728
category_for_bin_index,
2829
category_for_class,
2930
category_for_command,
3031
category_for_known_class,
3132
)
32-
from app.core.history_labels import infer_history_label, validate_review_label
3333

3434
metadata = MetaData()
3535

@@ -165,6 +165,10 @@ class HistoryRow:
165165
route_label: str | None
166166
bin_index: int | None
167167
uart_command: str | None
168+
display_label: str | None
169+
label_status: str | None
170+
label_source: str | None
171+
label_confidence: float | None
168172

169173
def __init__(self, **kw):
170174
for k, v in kw.items():

app/core/history_labels.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def infer_history_label(
7070
if command:
7171
category = category_for_class(clean_class)
7272
return HistoryLabelDecision(
73-
f"Chưa xác định vật {category.name}",
73+
f"Chưa xác định vật - {category.name}",
7474
"needs_review" if image_available else "no_evidence",
7575
"three_bin_route_only",
7676
None,

app/core/three_bin_classifier.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ def three_bin_route(command: str):
239239

240240
def three_bin_display_name(cls_name: str) -> str:
241241
command = parse_three_bin_class_name(cls_name)
242+
if command is None:
243+
return cls_name
242244
return THREE_BIN_DISPLAY_NAMES.get(command, cls_name)
243245

244246

docs/desktop-dockerhub-keepalive-release-plan.md

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,24 @@ This is the tracked repo plan for one combined release: fix/build the Windows de
66

77
## 1. Scope and release boundary
88

9-
The release has four deliverables:
9+
The release has five deliverables:
1010

1111
1. Windows desktop EXE:
1212
- output: `dist/TrashSorterPro/TrashSorterPro.exe`;
1313
- includes PySide6 desktop GUI, local camera/UART/audio behavior, assets, config sample, OpenCV runtime, and runtime models as required by `scripts/build_exe.py`;
1414
- does not run inside Docker.
15-
2. Docker Hub runtime images:
16-
- `docker.io/<namespace>/trash-sorter-web:<git-sha>`;
17-
- `docker.io/<namespace>/trash-sorter-agent:<git-sha>`;
15+
2. Docker Hub desktop artifact image:
16+
- `docker.io/nguyenson1710/trash-sorter-desktop-exe:<git-sha>`;
17+
- contains `dist/TrashSorterPro/`, `desktop-exe.sha256`, and `desktop-release-manifest.json`;
18+
- default command prints restore instructions; it does not launch the Windows GUI inside Docker.
19+
3. Docker Hub runtime images:
20+
- `docker.io/nguyenson1710/trash-sorter-web:<git-sha>`;
21+
- `docker.io/nguyenson1710/trash-sorter-agent:<git-sha>`;
1822
- optional `latest` tags only after SHA tags pass verification.
19-
3. Optional Docker Hub artifact images:
20-
- `docker.io/<namespace>/trash-sorter-models:<manifest-sha>` if models need their own artifact;
21-
- `docker.io/<namespace>/trash-sorter-dataset-archive:<YYYYMMDD>` only if the owner explicitly wants a large non-runtime archive on Docker Hub after privacy review.
22-
4. Vercel/Supabase keepalive:
23+
4. Optional Docker Hub artifact images:
24+
- `docker.io/nguyenson1710/trash-sorter-models:<manifest-sha>` if models need their own artifact;
25+
- `docker.io/nguyenson1710/trash-sorter-dataset-archive:<YYYYMMDD>` only if the owner explicitly wants a large non-runtime archive on Docker Hub after privacy review.
26+
5. Vercel/Supabase keepalive:
2327
- Vercel Cron calls a protected Next.js route;
2428
- the route performs one tiny Supabase/Postgres read or single-row upsert;
2529
- this creates real backend/database activity instead of merely pinging a static page.
@@ -74,6 +78,7 @@ Steps:
7478
- `Test-Path web\node_modules`.
7579
7. Confirm Docker Hub namespace:
7680
- use `docker info | Select-String Username`;
81+
- target namespace is `nguyenson1710`;
7782
- if empty or push fails, owner must run `docker login` or create/provide the right repository namespace.
7883

7984
Exit criteria:
@@ -128,6 +133,7 @@ Default images:
128133

129134
| Image | Purpose | Included | Excluded |
130135
| --- | --- | --- | --- |
136+
| `trash-sorter-desktop-exe` | Windows EXE artifact | `dist/TrashSorterPro`, checksum, release manifest | `.env*`, local DB, logs, user config, caches |
131137
| `trash-sorter-web` | Next.js dashboard | standalone Next build, public assets | `.env*`, host `node_modules`, local caches |
132138
| `trash-sorter-agent` | FastAPI/YOLO runtime + bridge command | app, scripts, config sample, `best.pt`, `new-class-specialist.pt` | dataset, runs, local DB, secrets |
133139
| `trash-sorter-models` optional | model artifact | promoted model set + checksum manifest | app source, datasets, secrets |
@@ -136,17 +142,21 @@ Default images:
136142
Steps:
137143

138144
1. Audit `.dockerignore`.
139-
2. Rebuild clean images:
145+
2. Package the desktop EXE artifact:
146+
- `python -m uv run python scripts/build_exe.py`;
147+
- `python -m uv run python scripts/package_desktop_artifact.py`;
148+
- `docker build -f Dockerfile.desktop-artifact -t trash-sorter-desktop-exe:local .`.
149+
3. Rebuild clean runtime images:
140150
- `docker build -f Dockerfile.web -t trash-sorter-web:local .`;
141151
- `docker build -f Dockerfile.agent -t trash-sorter-agent:local .`.
142-
3. Verify model checksums inside agent:
152+
4. Verify model checksums inside agent:
143153
- `docker run --rm trash-sorter-agent:local sha256sum -c models/runtime-models.sha256`.
144-
4. Smoke local containers:
154+
5. Smoke local containers:
145155
- agent `/api/health`;
146156
- agent `/api/status` and `/api/model/classes` with temporary token;
147157
- web `/` on a temporary port.
148-
5. Decide whether optional artifact images are needed.
149-
6. Label images with git SHA, build date, source repo, and model manifest hash.
158+
6. Decide whether optional artifact images are needed.
159+
7. Label images with git SHA, build date, source repo, and model manifest hash.
150160

151161
Exit criteria:
152162

@@ -163,28 +173,31 @@ Push order:
163173

164174
1. `trash-sorter-web:<git-sha>`;
165175
2. `trash-sorter-agent:<git-sha>`;
166-
3. `latest` tags after SHA tags are verified;
167-
4. optional artifact images after explicit approval.
176+
3. `trash-sorter-desktop-exe:<git-sha>`;
177+
4. `latest` tags after SHA tags are verified;
178+
5. optional artifact images after explicit approval.
168179

169180
Steps:
170181

171182
1. Confirm Docker Hub login/namespace/repository.
172183
2. Tag images:
173-
- `docker tag trash-sorter-web:local <namespace>/trash-sorter-web:<git-sha>`;
174-
- `docker tag trash-sorter-agent:local <namespace>/trash-sorter-agent:<git-sha>`.
184+
- `docker tag trash-sorter-web:local nguyenson1710/trash-sorter-web:<git-sha>`;
185+
- `docker tag trash-sorter-agent:local nguyenson1710/trash-sorter-agent:<git-sha>`;
186+
- `docker tag trash-sorter-desktop-exe:local nguyenson1710/trash-sorter-desktop-exe:<git-sha>`.
175187
3. Push web first:
176188
- if `insufficient_scope`, stop and fix Docker Hub permissions.
177189
4. Push agent.
178-
5. Capture remote digests:
190+
5. Push desktop EXE artifact.
191+
6. Capture remote digests:
179192
- `docker buildx imagetools inspect <image>:<tag>`.
180-
6. Update docs:
193+
7. Update docs:
181194
- image names/tags/digests;
182195
- CPU/GPU compose commands;
183196
- environment variables;
184197
- model checksum verification;
185198
- rollback commands;
186199
- disk cleanup commands.
187-
7. Commit and push GitHub.
200+
8. Commit and push GitHub.
188201

189202
Exit criteria:
190203

@@ -222,7 +235,7 @@ Safer schedule recommendation:
222235
}
223236
```
224237

225-
Reason: Supabase Free pausing is based on low activity over a 7-day period, and Vercel Hobby cron timing can drift. Exactly weekly can be close to the edge. If the owner insists on exactly one request per week, add a stale-heartbeat warning when last success is older than 5.5 days.
238+
Decision: use the safer twice-weekly schedule (`0 3 * * 1,4`). Reason: Supabase Free pausing is based on low activity over a 7-day period, and Vercel Hobby cron timing can drift. Exactly weekly can be close to the edge.
226239

227240
Route contract:
228241

@@ -276,6 +289,9 @@ Verification checklist:
276289
- web root 200;
277290
- model checksum inside agent.
278291
3. Docker Hub:
292+
- pull desktop EXE SHA tag;
293+
- extract `/artifacts/TrashSorterPro`;
294+
- run `sha256sum -c desktop-exe.sha256`;
279295
- pull web SHA tag;
280296
- pull agent SHA tag;
281297
- inspect digests;
@@ -304,11 +320,9 @@ Exit criteria:
304320

305321
## 4. Known blockers before implementation
306322

307-
- Docker Hub push already failed once with `insufficient_scope`. Need confirmed Docker Hub login, namespace, and repositories.
308-
- Current `python` launcher reports 3.14, while the project requires `<3.13`. Need uv-managed Python 3.12 or another compatible interpreter for desktop build.
309-
- Exact keepalive cadence needs owner decision:
310-
- exact weekly per request;
311-
- or safer twice-weekly to avoid the Supabase 7-day inactivity edge.
323+
- Docker Hub push previously failed once with `insufficient_scope` under a different namespace. Current target namespace is `nguyenson1710`; push still requires Docker Desktop login with rights to that namespace.
324+
- Use uv-managed Python 3.12 because `pyproject.toml` requires `<3.13`.
325+
- Keepalive cadence is twice weekly to avoid the Supabase 7-day inactivity edge.
312326
- Manual hardware acceptance needs the Windows machine, camera, COM/UART, and laptop speaker available.
313327

314328
## 5. Source notes
@@ -319,6 +333,5 @@ Exit criteria:
319333

320334
## 6. Unresolved questions
321335

322-
- Docker Hub namespace/repositories: use `jasontm17` or another org?
323-
- Keepalive cadence: exactly weekly, or safer twice-weekly?
324-
- Should Docker Hub hold only runtime images, or also a large non-runtime dataset/archive image?
336+
- None for the desktop EXE artifact image.
337+
- Runtime web/agent image push, keepalive implementation, and large dataset/model archive images remain separate release slices unless explicitly included in the current implementation run.

0 commit comments

Comments
 (0)