Skip to content

Commit 492a5ad

Browse files
author
Данил
committed
feat(export): include visual evidence assets
1 parent debedca commit 492a5ad

5 files changed

Lines changed: 796 additions & 12 deletions

File tree

docs/config-registry.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Current registry index:
88

99
```text
1010
https://raw.githubusercontent.com/nigdanil/AuditM-Field/main/public/config-registry/index.json
11-
````
11+
```
1212

1313
Repository path:
1414

docs/export-format.md

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,41 @@ photos/
1616
<photoId>_<fileName>
1717
annotations/
1818
annotations.json
19+
<photoId>.annotations.json
20+
rendered/
21+
<photoId>.overlay.png
22+
visual-evidence.warnings.json
23+
crops/
24+
<photoId>/
25+
<annotationId>.png
1926
```
2027

2128
---
2229

30+
## Source of truth
31+
32+
Original photos are exported without drawn boxes.
33+
34+
This is intentional.
35+
36+
```text
37+
Original photo
38+
+
39+
annotations.json
40+
+
41+
overlay previews
42+
+
43+
annotation crops
44+
```
45+
46+
Original photos remain clean for OCR, CV, LLM and repeated processing.
47+
48+
Overlay images are only visual evidence for humans and reports.
49+
50+
Crops are useful for AI pipelines because they isolate each annotated area.
51+
52+
---
53+
2354
## manifest.json
2455

2556
The manifest describes the exported package.
@@ -30,7 +61,7 @@ Example:
3061
{
3162
"app": {
3263
"name": "AuditM-Field",
33-
"packageFormatVersion": "1.0.0",
64+
"packageFormatVersion": "1.1.0",
3465
"exportedAt": "2026-05-10T10:00:00.000Z"
3566
},
3667
"inspection": {
@@ -48,15 +79,20 @@ Example:
4879
},
4980
"counts": {
5081
"photos": 1,
51-
"annotations": 2
82+
"annotations": 2,
83+
"renderedOverlays": 1,
84+
"annotationCrops": 2
5285
},
5386
"files": {
5487
"manifest": "manifest.json",
5588
"config": "config.json",
5689
"inspection": "inspections/inspection_<id>.json",
5790
"photosMetadata": "photos/photos.metadata.json",
5891
"annotations": "annotations/annotations.json",
59-
"photos": []
92+
"photos": [],
93+
"annotationsByPhoto": [],
94+
"renderedOverlays": [],
95+
"annotationCrops": []
6096
}
6197
}
6298
```
@@ -110,6 +146,8 @@ createdAt
110146

111147
Photo binary files are stored separately in the `photos/` folder.
112148

149+
Original photo files are not modified and do not contain rendered annotation boxes.
150+
113151
---
114152

115153
## annotations/annotations.json
@@ -140,6 +178,80 @@ imported
140178

141179
---
142180

181+
## annotations/<photoId>.annotations.json
182+
183+
Contains annotations grouped by photo.
184+
185+
This is useful for backend/n8n/AI workflows where a single photo and its annotations are processed together.
186+
187+
---
188+
189+
## rendered/<photoId>.overlay.png
190+
191+
Human-readable visual preview.
192+
193+
The overlay image contains:
194+
195+
```text
196+
original photo
197+
+ annotation boxes
198+
+ annotation label
199+
+ annotation source
200+
```
201+
202+
Use this for:
203+
204+
```text
205+
manual review
206+
reports
207+
emails
208+
n8n previews
209+
portfolio screenshots
210+
visual evidence
211+
```
212+
213+
Do not use this as the only input for OCR/CV/LLM processing.
214+
215+
---
216+
217+
## crops/<photoId>/<annotationId>.png
218+
219+
Cropped image regions generated from annotation geometry.
220+
221+
Use this for:
222+
223+
```text
224+
AI verification
225+
OCR on selected area
226+
classification
227+
brand/condition checks
228+
focused LLM vision prompts
229+
```
230+
231+
---
232+
233+
## rendered/visual-evidence.warnings.json
234+
235+
Optional file.
236+
237+
Created only when some annotations could not be rendered as overlay/crop because geometry could not be resolved.
238+
239+
Example:
240+
241+
```json
242+
{
243+
"warnings": [
244+
{
245+
"id": "annotation-id",
246+
"photoId": "photo-id",
247+
"reason": "Annotation geometry could not be resolved."
248+
}
249+
]
250+
}
251+
```
252+
253+
---
254+
143255
## Import behavior
144256

145257
ZIP import is idempotent:
@@ -152,14 +264,25 @@ annotations -> bulkPut
152264

153265
Re-importing the same ZIP updates existing records by id.
154266

267+
Visual evidence files are ignored during import because they can be generated again from original photos and annotations.
268+
155269
---
156270

157271
## Compatibility
158272

159273
Current package format version:
160274

161275
```text
162-
1.0.0
276+
1.1.0
277+
```
278+
279+
Version `1.0.0` packages remain compatible because import only requires:
280+
281+
```text
282+
manifest.json
283+
config.json
284+
inspections/
285+
photos/
286+
annotations/
163287
```
164288

165-
Future versions should preserve backward compatibility where possible.

docs/github-pages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Repository:
66

77
```text
88
https://github.com/nigdanil/AuditM-Field
9-
````
9+
```
1010

1111
Public URL:
1212

src/services/export/exportPackage.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import type { Inspection } from '../../entities/inspection/types';
66
import type { PhotoRecord } from '../../entities/photo/types';
77
import { db } from '../db/db';
88

9+
import { buildVisualEvidenceAssets } from './visualEvidence';
10+
911
type ExportPhotoMetadata = Omit<PhotoRecord, 'blob'>;
1012

1113
export type ExportManifest = {
1214
app: {
1315
name: 'AuditM-Field';
14-
packageFormatVersion: '1.0.0';
16+
packageFormatVersion: '1.1.0';
1517
exportedAt: string;
1618
};
1719
inspection: {
@@ -32,6 +34,8 @@ export type ExportManifest = {
3234
counts: {
3335
photos: number;
3436
annotations: number;
37+
renderedOverlays: number;
38+
annotationCrops: number;
3539
};
3640
files: {
3741
manifest: string;
@@ -41,6 +45,8 @@ export type ExportManifest = {
4145
annotations: string;
4246
photos: string[];
4347
annotationsByPhoto: string[];
48+
renderedOverlays: string[];
49+
annotationCrops: string[];
4450
};
4551
};
4652

@@ -150,12 +156,17 @@ function buildManifest(input: {
150156
annotations: ImageAnnotationRecord[];
151157
photoFilePaths: string[];
152158
annotationFilePaths: string[];
159+
overlayFilePaths?: string[];
160+
cropFilePaths?: string[];
153161
exportedAt: Date;
154162
}): ExportManifest {
163+
const overlayFilePaths = input.overlayFilePaths ?? [];
164+
const cropFilePaths = input.cropFilePaths ?? [];
165+
155166
return {
156167
app: {
157168
name: 'AuditM-Field',
158-
packageFormatVersion: '1.0.0',
169+
packageFormatVersion: '1.1.0',
159170
exportedAt: input.exportedAt.toISOString(),
160171
},
161172
inspection: {
@@ -176,6 +187,8 @@ function buildManifest(input: {
176187
counts: {
177188
photos: input.photos.length,
178189
annotations: input.annotations.length,
190+
renderedOverlays: overlayFilePaths.length,
191+
annotationCrops: cropFilePaths.length,
179192
},
180193
files: {
181194
manifest: 'manifest.json',
@@ -185,6 +198,8 @@ function buildManifest(input: {
185198
annotations: 'annotations/annotations.json',
186199
photos: input.photoFilePaths,
187200
annotationsByPhoto: input.annotationFilePaths,
201+
renderedOverlays: overlayFilePaths,
202+
annotationCrops: cropFilePaths,
188203
},
189204
};
190205
}
@@ -276,6 +291,26 @@ export async function buildInspectionExportPackage(
276291
throw new Error('Add at least one photo before export.');
277292
}
278293

294+
const visualEvidence = await buildVisualEvidenceAssets({
295+
photos: data.photos,
296+
annotations: data.annotations,
297+
config: activeConfig,
298+
});
299+
300+
const manifest = buildManifest({
301+
inspection: data.inspection,
302+
config: activeConfig,
303+
configSource: input.configSource,
304+
configLoadedAt: input.configLoadedAt,
305+
photos: data.photos,
306+
annotations: data.annotations,
307+
photoFilePaths: data.photoFilePaths,
308+
annotationFilePaths: data.annotationFilePaths,
309+
overlayFilePaths: visualEvidence.overlayFilePaths,
310+
cropFilePaths: visualEvidence.cropFilePaths,
311+
exportedAt: new Date(data.manifest.app.exportedAt),
312+
});
313+
279314
const zip = new JSZip();
280315
const inspectionJson = {
281316
...data.inspection,
@@ -287,8 +322,10 @@ export async function buildInspectionExportPackage(
287322
const inspectionsFolder = zip.folder('inspections');
288323
const photosFolder = zip.folder('photos');
289324
const annotationsFolder = zip.folder('annotations');
325+
const renderedFolder = zip.folder('rendered');
326+
const cropsFolder = zip.folder('crops');
290327

291-
if (!inspectionsFolder || !photosFolder || !annotationsFolder) {
328+
if (!inspectionsFolder || !photosFolder || !annotationsFolder || !renderedFolder || !cropsFolder) {
292329
throw new Error('Failed to create ZIP folder structure.');
293330
}
294331

@@ -311,7 +348,20 @@ export async function buildInspectionExportPackage(
311348
annotationsFolder.file(`${photoId}.annotations.json`, toJsonBlobContent(photoAnnotations));
312349
});
313350

314-
zip.file('manifest.json', toJsonBlobContent(data.manifest));
351+
visualEvidence.assets.forEach((asset) => {
352+
zip.file(asset.path, asset.blob);
353+
});
354+
355+
if (visualEvidence.skippedAnnotations.length > 0) {
356+
zip.file(
357+
'rendered/visual-evidence.warnings.json',
358+
toJsonBlobContent({
359+
warnings: visualEvidence.skippedAnnotations,
360+
}),
361+
);
362+
}
363+
364+
zip.file('manifest.json', toJsonBlobContent(manifest));
315365

316366
const blob = await zip.generateAsync({
317367
type: 'blob',
@@ -323,7 +373,7 @@ export async function buildInspectionExportPackage(
323373

324374
return {
325375
blob,
326-
manifest: data.manifest,
376+
manifest,
327377
fileName: data.fileName,
328378
};
329379
}

0 commit comments

Comments
 (0)