bbannotate 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: bbannotate
3
+ Version: 1.0.0
4
+ Summary: A lightweight bounding box annotation tool for image datasets with YOLO/COCO/Pascal VOC export
5
+ Project-URL: Homepage, https://github.com/seba2390/bbannotate
6
+ Project-URL: Documentation, https://github.com/seba2390/bbannotate#readme
7
+ Project-URL: Repository, https://github.com/seba2390/bbannotate
8
+ Project-URL: Issues, https://github.com/seba2390/bbannotate/issues
9
+ Author: Sebastian Yde Madsen
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: annotation,bounding-box,coco,computer-vision,image-annotation,machine-learning,object-detection,pascal-voc,yolo
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.12
27
+ Requires-Dist: aiofiles>=23.2.1
28
+ Requires-Dist: fastapi>=0.109.0
29
+ Requires-Dist: pillow>=10.2.0
30
+ Requires-Dist: pydantic>=2.5.0
31
+ Requires-Dist: python-multipart>=0.0.6
32
+ Requires-Dist: rich>=13.0.0
33
+ Requires-Dist: typer>=0.9.0
34
+ Requires-Dist: uvicorn[standard]>=0.27.0
35
+ Provides-Extra: dev
36
+ Requires-Dist: httpx; extra == 'dev'
37
+ Requires-Dist: pyright; extra == 'dev'
38
+ Requires-Dist: pytest; extra == 'dev'
39
+ Requires-Dist: pytest-cov; extra == 'dev'
40
+ Requires-Dist: ruff; extra == 'dev'
41
+ Description-Content-Type: text/markdown
42
+
43
+ <p align="center">
44
+ <img src="frontend/public/logo.png" alt="Bbannotate Logo" width="400">
45
+ </p>
46
+
47
+ <p align="center">
48
+ <a href="https://pypi.org/project/bbannotate/"><img src="https://img.shields.io/pypi/v/bbannotate" alt="PyPI version"></a>
49
+ <a href="https://pypi.org/project/bbannotate/"><img src="https://img.shields.io/pypi/pyversions/bbannotate" alt="Python versions"></a>
50
+ <a href="https://github.com/sebastianydemadsen/bbannotate/blob/main/LICENSE"><img src="https://img.shields.io/github/license/sebastianydemadsen/bbannotate" alt="License"></a>
51
+ </p>
52
+
53
+ A lightweight bounding box annotation tool for image datasets. Built with React/TypeScript frontend and FastAPI backend. Export to YOLO, COCO, Pascal VOC, and more.
54
+
55
+ ## Features
56
+
57
+ - 🖼️ **Multi-format support** — PNG, JPEG, WebP, BMP
58
+ - 📁 **Project management** — Organize annotations by project
59
+ - 🏷️ **Custom labels** — Define your own class labels
60
+ - ⌨️ **Keyboard shortcuts** — Fast annotation workflow
61
+ - 📤 **Multiple export formats** — YOLO, COCO, Pascal VOC, CreateML, CSV
62
+ - 🔄 **Train/Val/Test split** — Automatic dataset splitting for YOLO export
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install bbannotate
68
+ ```
69
+
70
+ ### Requirements
71
+
72
+ - Python 3.12+
73
+ - Node.js (only for frontend development)
74
+
75
+ ## Quick Start
76
+
77
+ ```bash
78
+ # Start the annotation server
79
+ bbannotate start
80
+
81
+ # Opens http://127.0.0.1:8000 in your browser
82
+ ```
83
+
84
+ ### CLI Options
85
+
86
+ ```bash
87
+ bbannotate start [OPTIONS]
88
+
89
+ Options:
90
+ -h, --host TEXT Host to bind the server to [default: 127.0.0.1]
91
+ -p, --port INTEGER Port to bind the server to [default: 8000]
92
+ --no-browser Don't open browser automatically
93
+ -r, --reload Enable auto-reload for development
94
+ -d, --data-dir PATH Directory for storing data [default: ./data]
95
+ --projects-dir PATH Directory for storing projects [default: ./projects]
96
+ -v, --version Show version and exit
97
+ --help Show help and exit
98
+ ```
99
+
100
+ ### Other Commands
101
+
102
+ ```bash
103
+ bbannotate info # Show installation info
104
+ bbannotate build-frontend # Build frontend assets (development)
105
+ ```
106
+
107
+ ## Keyboard Shortcuts
108
+
109
+ | Key | Action |
110
+ |-----|--------|
111
+ | `D` | Draw mode |
112
+ | `S` | Select mode |
113
+ | `Space` | Pan mode |
114
+ | `←` `→` | Navigate images |
115
+ | `1-9` | Select label by index |
116
+ | `Del` / `Backspace` | Delete annotation |
117
+ | `Esc` | Deselect / Cancel |
118
+
119
+ ## Export Formats
120
+
121
+ | Format | Description |
122
+ |--------|-------------|
123
+ | **YOLO** | ZIP with train/val/test split, `data.yaml`, normalized coordinates |
124
+ | **COCO** | COCO JSON format with categories, images, and annotations |
125
+ | **Pascal VOC** | XML files per image with absolute coordinates |
126
+ | **CreateML** | Apple CreateML JSON format |
127
+ | **CSV** | Simple CSV with image, label, and bbox columns |
128
+
129
+ ## Configuration
130
+
131
+ ### Environment Variables
132
+
133
+ | Variable | Description |
134
+ |----------|-------------|
135
+ | `BBANNOTATE_DATA_DIR` | Override default data directory |
136
+ | `BBANNOTATE_PROJECTS_DIR` | Override default projects directory |
137
+
138
+ ## Development
139
+
140
+ ### Setup
141
+
142
+ ```bash
143
+ git clone https://github.com/sebastianydemadsen/bbannotate.git
144
+ cd bbannotate
145
+ make install # Install with dev dependencies
146
+ make frontend-install # Install frontend dependencies
147
+ ```
148
+
149
+ ### Development Commands
150
+
151
+ | Command | Description |
152
+ |---------|-------------|
153
+ | `make run` | Start full application (backend + frontend) |
154
+ | `make backend-dev` | Start backend only with auto-reload |
155
+ | `make frontend-dev` | Start frontend dev server |
156
+ | `make stop` | Stop all servers |
157
+ | `make test` | Run tests |
158
+ | `make test-cov` | Run tests with coverage report |
159
+ | `make type-check` | Run pyright type checking |
160
+ | `make format` | Format code with ruff |
161
+ | `make check-all` | Run all checks (lint, type, test) |
162
+ | `make build` | Build package for distribution |
163
+ | `make clean` | Remove build artifacts |
164
+
165
+ ### Project Structure
166
+
167
+ ```
168
+ src/ # Python package (FastAPI backend)
169
+ api/ # API routes
170
+ models/ # Pydantic models
171
+ services/ # Business logic
172
+ cli.py # CLI entry point
173
+ frontend/ # React/TypeScript frontend
174
+ src/
175
+ components/ # UI components
176
+ hooks/ # React hooks
177
+ tests/ # Test suite (145 tests)
178
+ ```
179
+
180
+ ## License
181
+
182
+ [MIT](LICENSE)
@@ -0,0 +1,21 @@
1
+ src/__init__.py,sha256=Eg75iuRICqJS9qBThEk9OQ4l7sDRagmKrjuc4tGCVns,117
2
+ src/cli.py,sha256=q2u1j1Q4A1F1yMAWPzNmnw053CKqxxnHVGzhUGSJKzI,7533
3
+ src/main.py,sha256=M2yuPRxkRDYCwNw7jPBwDm6R-Egk6EFbdUeh3W3YHss,2346
4
+ src/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ src/api/__init__.py,sha256=rin7NuwATcUVSpd-oncHr4VPxCj2je9kPpsjMdwQAJ0,99
6
+ src/api/routes.py,sha256=iLliNvNMXFFpm5SxCQ5fj-txaWurIGy-kuchx9v-mYM,11085
7
+ src/models/__init__.py,sha256=V3IlbnQ_PWn43HCKlOi-IRTmNj1GX7iA79ZbCns_yts,368
8
+ src/models/annotations.py,sha256=NiCBjmejQbgoMKkDiNqFZBKK6-QggcU1ikXwXFoQWgc,1951
9
+ src/services/__init__.py,sha256=jDAHHd3p_fYdT3Txzakd2I0fxwEQ6irMQ1nNELhuIEY,207
10
+ src/services/annotation_service.py,sha256=_OiUlm42LXIMrcbTVjGWZPOovhppUJMX3v8rZuq6WVU,10495
11
+ src/services/export_service.py,sha256=-NhO6jCQtOpIgFL3cVDk86KQSQBBiogN3jVpESJQVac,17180
12
+ src/services/project_service.py,sha256=6l2mih32z5_tqLj8orF5szNhuEJYkboNeUwNqDEs9iA,7125
13
+ src/frontend_dist/index.html,sha256=opKxjiCx3PXtHQsAsn7iQ3zkOUMr-fTgZ_l85E4X7cc,733
14
+ src/frontend_dist/logo.png,sha256=hiuri3y0lYBsavGECeXh8VPVJvhA8nOlHCDeTSfqk8s,198758
15
+ src/frontend_dist/assets/index-DFfgJzXo.css,sha256=v9Ez3rvpb4mWv3JtVFrke3ubOwyOfCBSl0vbIbekCC0,20719
16
+ src/frontend_dist/assets/index-DezlWFbC.js,sha256=S8frLZGZ7cm5ugjS7bLjPrJBRGeoJnx1lDyPQVBRQg4,518021
17
+ bbannotate-1.0.0.dist-info/METADATA,sha256=r7SrH-rMJ-TJKqHcWm11Hy-ee3QbeNevgzAA9Pj6zqM,6046
18
+ bbannotate-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
+ bbannotate-1.0.0.dist-info/entry_points.txt,sha256=nu0u4lG6-pWhDUmquxeusVDJzhsWQOUczupOWmKH01c,44
20
+ bbannotate-1.0.0.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
21
+ bbannotate-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bbannotate = src.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sebastian Yde Madsen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
src/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Bbannotate - Bounding box annotation tool for image datasets."""
2
+
3
+ __version__ = "1.0.0"
4
+ __all__ = ["__version__"]
src/api/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """API routes for the annotation tool."""
2
+
3
+ from src.api.routes import router
4
+
5
+ __all__ = ["router"]
src/api/routes.py ADDED
@@ -0,0 +1,339 @@
1
+ """FastAPI routes for the annotation API."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
8
+ from fastapi.responses import FileResponse
9
+
10
+ from src.models.annotations import (
11
+ Annotation,
12
+ AnnotationCreate,
13
+ AnnotationUpdate,
14
+ ImageInfo,
15
+ ProjectInfo,
16
+ )
17
+ from src.services.annotation_service import AnnotationService
18
+ from src.services.export_service import ExportService
19
+ from src.services.project_service import Project, ProjectCreate, ProjectService
20
+
21
+ router = APIRouter()
22
+
23
+
24
+ def get_projects_dir() -> Path:
25
+ """Get the projects directory from environment or default."""
26
+ env_path = os.environ.get("BBANNOTATE_PROJECTS_DIR")
27
+ if env_path:
28
+ return Path(env_path)
29
+ return Path.cwd() / "projects"
30
+
31
+
32
+ def get_data_dir() -> Path:
33
+ """Get the data directory from environment or default."""
34
+ env_path = os.environ.get("BBANNOTATE_DATA_DIR")
35
+ if env_path:
36
+ return Path(env_path)
37
+ return Path.cwd() / "data"
38
+
39
+
40
+ # Default directories (use functions for dynamic resolution)
41
+ PROJECTS_DIR = get_projects_dir()
42
+ DATA_DIR = get_data_dir() # Legacy fallback
43
+
44
+ # Global state for current project
45
+ _current_project_id: str | None = None
46
+
47
+
48
+ def get_project_service() -> ProjectService:
49
+ """Dependency for project service."""
50
+ return ProjectService(get_projects_dir())
51
+
52
+
53
+ def get_annotation_service() -> AnnotationService:
54
+ """Dependency for annotation service - uses current project's data directory."""
55
+ global _current_project_id
56
+ if _current_project_id:
57
+ project_service = ProjectService(get_projects_dir())
58
+ data_dir = project_service.get_project_data_dir(_current_project_id)
59
+ if data_dir:
60
+ return AnnotationService(data_dir)
61
+ # Fallback to legacy data directory
62
+ return AnnotationService(get_data_dir())
63
+
64
+
65
+ def get_export_service(
66
+ annotation_service: Annotated[AnnotationService, Depends(get_annotation_service)],
67
+ ) -> ExportService:
68
+ """Dependency for export service."""
69
+ return ExportService(annotation_service)
70
+
71
+
72
+ # Project management endpoints
73
+ @router.get("/projects", response_model=list[Project])
74
+ def list_projects(
75
+ service: Annotated[ProjectService, Depends(get_project_service)],
76
+ ) -> list[Project]:
77
+ """List all projects, sorted by last opened."""
78
+ return service.list_projects()
79
+
80
+
81
+ @router.post("/projects", response_model=Project)
82
+ def create_project(
83
+ create: ProjectCreate,
84
+ service: Annotated[ProjectService, Depends(get_project_service)],
85
+ ) -> Project:
86
+ """Create a new project."""
87
+ return service.create_project(create)
88
+
89
+
90
+ @router.get("/projects/current", response_model=Project | None)
91
+ def get_current_project(
92
+ service: Annotated[ProjectService, Depends(get_project_service)],
93
+ ) -> Project | None:
94
+ """Get the currently active project."""
95
+ global _current_project_id
96
+ if not _current_project_id:
97
+ return None
98
+ return service.get_project(_current_project_id)
99
+
100
+
101
+ @router.post("/projects/{project_id}/open", response_model=Project)
102
+ def open_project(
103
+ project_id: str,
104
+ service: Annotated[ProjectService, Depends(get_project_service)],
105
+ ) -> Project:
106
+ """Open a project (set as current and update last_opened)."""
107
+ global _current_project_id
108
+ project = service.open_project(project_id)
109
+ if not project:
110
+ raise HTTPException(status_code=404, detail="Project not found")
111
+ _current_project_id = project_id
112
+ return project
113
+
114
+
115
+ @router.post("/projects/close")
116
+ def close_project() -> dict[str, bool]:
117
+ """Close the current project."""
118
+ global _current_project_id
119
+ _current_project_id = None
120
+ return {"success": True}
121
+
122
+
123
+ @router.delete("/projects/{project_id}")
124
+ def delete_project(
125
+ project_id: str,
126
+ service: Annotated[ProjectService, Depends(get_project_service)],
127
+ ) -> dict[str, bool]:
128
+ """Delete a project and all its data."""
129
+ global _current_project_id
130
+ success = service.delete_project(project_id)
131
+ if not success:
132
+ raise HTTPException(status_code=404, detail="Project not found")
133
+ if _current_project_id == project_id:
134
+ _current_project_id = None
135
+ return {"success": True}
136
+
137
+
138
+ # Project endpoints (existing)
139
+ @router.get("/project", response_model=ProjectInfo)
140
+ def get_project_info(
141
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
142
+ ) -> ProjectInfo:
143
+ """Get project statistics and label information."""
144
+ return service.get_project_info()
145
+
146
+
147
+ # Image endpoints
148
+ @router.get("/images", response_model=list[str])
149
+ def list_images(
150
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
151
+ ) -> list[str]:
152
+ """List all images in the project."""
153
+ return service.list_images()
154
+
155
+
156
+ @router.post("/images", response_model=ImageInfo)
157
+ async def upload_image(
158
+ file: Annotated[UploadFile, File(...)],
159
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
160
+ ) -> ImageInfo:
161
+ """Upload a new image."""
162
+ if not file.filename:
163
+ raise HTTPException(status_code=400, detail="Filename is required")
164
+
165
+ content = await file.read()
166
+ try:
167
+ return service.upload_image(file.filename, content)
168
+ except ValueError as e:
169
+ raise HTTPException(status_code=400, detail=str(e)) from e
170
+
171
+
172
+ @router.get("/images/{filename}")
173
+ def get_image(
174
+ filename: str,
175
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
176
+ ) -> FileResponse:
177
+ """Get an image file."""
178
+ path = service.get_image_path(filename)
179
+ if path is None:
180
+ raise HTTPException(status_code=404, detail="Image not found")
181
+ return FileResponse(path)
182
+
183
+
184
+ @router.delete("/images/{filename}")
185
+ def delete_image(
186
+ filename: str,
187
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
188
+ ) -> dict[str, bool]:
189
+ """Delete an image and its annotations."""
190
+ success = service.delete_image(filename)
191
+ if not success:
192
+ raise HTTPException(status_code=404, detail="Image not found")
193
+ return {"success": True}
194
+
195
+
196
+ # Annotation endpoints
197
+ @router.get("/images/{filename}/annotations", response_model=list[Annotation])
198
+ def get_annotations(
199
+ filename: str,
200
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
201
+ ) -> list[Annotation]:
202
+ """Get all annotations for an image."""
203
+ return service.get_annotations(filename)
204
+
205
+
206
+ @router.post("/images/{filename}/annotations", response_model=Annotation)
207
+ def add_annotation(
208
+ filename: str,
209
+ annotation: AnnotationCreate,
210
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
211
+ ) -> Annotation:
212
+ """Add a new annotation to an image."""
213
+ try:
214
+ return service.add_annotation(filename, annotation)
215
+ except FileNotFoundError as e:
216
+ raise HTTPException(status_code=404, detail=str(e)) from e
217
+
218
+
219
+ @router.put("/images/{filename}/annotations/{annotation_id}", response_model=Annotation)
220
+ def update_annotation(
221
+ filename: str,
222
+ annotation_id: str,
223
+ update: AnnotationUpdate,
224
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
225
+ ) -> Annotation:
226
+ """Update an existing annotation."""
227
+ result = service.update_annotation(filename, annotation_id, update)
228
+ if result is None:
229
+ raise HTTPException(status_code=404, detail="Annotation not found")
230
+ return result
231
+
232
+
233
+ @router.delete("/images/{filename}/annotations/{annotation_id}")
234
+ def delete_annotation(
235
+ filename: str,
236
+ annotation_id: str,
237
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
238
+ ) -> dict[str, bool]:
239
+ """Delete an annotation."""
240
+ success = service.delete_annotation(filename, annotation_id)
241
+ if not success:
242
+ raise HTTPException(status_code=404, detail="Annotation not found")
243
+ return {"success": True}
244
+
245
+
246
+ @router.delete("/images/{filename}/annotations")
247
+ def clear_annotations(
248
+ filename: str,
249
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
250
+ ) -> dict[str, int]:
251
+ """Clear all annotations for an image."""
252
+ count = service.clear_annotations(filename)
253
+ return {"deleted": count}
254
+
255
+
256
+ @router.post("/images/{filename}/annotations/copy-from/{source_filename}")
257
+ def copy_annotations(
258
+ filename: str,
259
+ source_filename: str,
260
+ service: Annotated[AnnotationService, Depends(get_annotation_service)],
261
+ ) -> dict[str, int]:
262
+ """Copy annotations from another image."""
263
+ count = service.copy_annotations(source_filename, filename)
264
+ return {"copied": count}
265
+
266
+
267
+ # Export endpoints
268
+ @router.post("/export/yolo")
269
+ def export_yolo(
270
+ export_service: Annotated[ExportService, Depends(get_export_service)],
271
+ train_split: Annotated[float, Query(ge=0.1, le=0.98)] = 0.7,
272
+ val_split: Annotated[float, Query(ge=0.01, le=0.5)] = 0.2,
273
+ test_split: Annotated[float, Query(ge=0.0, le=0.5)] = 0.1,
274
+ ) -> FileResponse:
275
+ """Export annotations in YOLO format as a ZIP file."""
276
+ zip_path = export_service.export_yolo_zip(train_split, val_split, test_split)
277
+ return FileResponse(
278
+ zip_path,
279
+ media_type="application/zip",
280
+ filename="yolo_dataset.zip",
281
+ )
282
+
283
+
284
+ @router.post("/export/coco")
285
+ def export_coco(
286
+ export_service: Annotated[ExportService, Depends(get_export_service)],
287
+ annotation_service: Annotated[AnnotationService, Depends(get_annotation_service)],
288
+ ) -> FileResponse:
289
+ """Export annotations in COCO JSON format."""
290
+ output_path = annotation_service.data_dir / "coco_annotations.json"
291
+ export_service.export_coco(output_path)
292
+ return FileResponse(
293
+ output_path,
294
+ media_type="application/json",
295
+ filename="coco_annotations.json",
296
+ )
297
+
298
+
299
+ @router.post("/export/pascal-voc")
300
+ def export_pascal_voc(
301
+ export_service: Annotated[ExportService, Depends(get_export_service)],
302
+ ) -> FileResponse:
303
+ """Export annotations in Pascal VOC XML format as a ZIP file."""
304
+ zip_path = export_service.export_pascal_voc_zip()
305
+ return FileResponse(
306
+ zip_path,
307
+ media_type="application/zip",
308
+ filename="pascal_voc_dataset.zip",
309
+ )
310
+
311
+
312
+ @router.post("/export/createml")
313
+ def export_createml(
314
+ export_service: Annotated[ExportService, Depends(get_export_service)],
315
+ annotation_service: Annotated[AnnotationService, Depends(get_annotation_service)],
316
+ ) -> FileResponse:
317
+ """Export annotations in Apple CreateML JSON format."""
318
+ output_path = annotation_service.data_dir / "createml_annotations.json"
319
+ export_service.export_createml(output_path)
320
+ return FileResponse(
321
+ output_path,
322
+ media_type="application/json",
323
+ filename="createml_annotations.json",
324
+ )
325
+
326
+
327
+ @router.post("/export/csv")
328
+ def export_csv(
329
+ export_service: Annotated[ExportService, Depends(get_export_service)],
330
+ annotation_service: Annotated[AnnotationService, Depends(get_annotation_service)],
331
+ ) -> FileResponse:
332
+ """Export annotations in CSV format."""
333
+ output_path = annotation_service.data_dir / "annotations.csv"
334
+ export_service.export_csv(output_path)
335
+ return FileResponse(
336
+ output_path,
337
+ media_type="text/csv",
338
+ filename="annotations.csv",
339
+ )