diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 3367f1c18d..ddc9e92095 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -457,10 +457,8 @@ public class ApplicationProperties { private String modelDir = ""; /** - * Read-only dir of models baked into the image (the air-gapped image bakes one here). On - * startup any {@code .onnx} found here is activated if no model is active, so - * the feature works out-of-the-box. The file is read in place rather than copied into the - * writable model dir, so it is not stored twice. Blank (default) disables seeding. + * Read-only dir of image-baked models, activated on startup when none is active and read in + * place rather than copied. Blank disables seeding. */ private String preinstalledModelDir = ""; } diff --git a/app/common/src/main/java/stirling/software/common/util/FormUtils.java b/app/common/src/main/java/stirling/software/common/util/FormUtils.java index 3face79257..1184a83b11 100644 --- a/app/common/src/main/java/stirling/software/common/util/FormUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/FormUtils.java @@ -940,9 +940,8 @@ public class FormUtils { } /** - * With {@code onlyFields} non-null, regenerates appearances for just those fields; {@code - * preserveNeedAppearances} then keeps the viewer-side generation flag set for the untouched - * pre-existing fields that still rely on it. + * With {@code onlyFields} set, regenerates only those. {@code preserveNeedAppearances} keeps + * the viewer-side flag on for untouched fields that still rely on it. */ private void ensureAppearances( PDAcroForm acroForm, List onlyFields, boolean preserveNeedAppearances) { @@ -1012,10 +1011,8 @@ public class FormUtils { } /** - * Create new AcroForm fields from a list of definitions (used by Auto Form Detection). Reuses - * the same field-creation and appearance logic as the rest of this class, and creates the - * AcroForm (with a Helvetica default resource) when the document has none. Field names are made - * unique against any existing fields. + * Create AcroForm fields from definitions, uniquifying names against existing fields. Creates + * the AcroForm with a Helvetica default resource when the document has none. */ public void addFields(PDDocument document, List definitions) throws IOException { @@ -3718,9 +3715,8 @@ public class FormUtils { } } - // Only link a SEPARATE widget into the field; the merged widget IS the - // field dictionary and is already its own widget. setWidgets is what persists - // the /Kids link - getWidgets() alone returns a detached copy. + // Only link a SEPARATE widget; a merged widget is already the field dictionary. + // setWidgets persists the /Kids link - getWidgets() returns a detached copy. if (!reuseFieldDict) { List widgets = new ArrayList<>(field.getWidgets()); if (!widgets.contains(widget)) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionController.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionController.java index f3f72d4bfa..72c5bf8809 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionController.java @@ -38,9 +38,8 @@ import stirling.software.proprietary.formdetection.render.PageRasterizer; import stirling.software.proprietary.formdetection.service.FormDetectionModelManager; /** - * Server-side detection endpoint. Gated behind the {@code form-detection} endpoint key, which is - * disabled until a model is installed (so the tool tile is greyed in the UI). Returns the shared - * detection schema, or - when {@code applyToPdf=true} - the AcroForm-applied PDF. + * Detection endpoint, behind the {@code form-detection} key that is disabled until a model is + * installed. Returns detected fields, or the applied PDF when {@code applyToPdf=true}. */ @Slf4j @RestController @@ -150,9 +149,8 @@ public class FormDetectionController { return ResponseEntity.badRequest() .body(Map.of("reason", "INVALID_PDF", "message", e.getMessage())); } catch (IllegalStateException e) { - // e.g. ONNX Runtime native unavailable for this OS/arch - report unavailable cleanly - // rather than a 500. Cannot happen on a normally-built jar (all platforms bundled), but - // keeps a slimmed/mis-targeted build from erroring. + // ONNX Runtime native missing for this OS/arch (a slimmed or mis-targeted build): + // report unavailable rather than 500. log.warn("Auto Form Detection inference unavailable: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(Map.of("reason", "DEPENDENCY", "message", e.getMessage())); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelController.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelController.java index 94e2882bf6..d2952c6a22 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelController.java @@ -23,9 +23,8 @@ import stirling.software.proprietary.formdetection.model.ModelStatusResponse; import stirling.software.proprietary.formdetection.service.FormDetectionModelManager; /** - * Admin-managed lifecycle for the Auto Form Detection model. Lives under the never-gated {@code - * form-detection-model} endpoint key so install/status stay reachable while the feature itself (the - * {@code form-detection} detect endpoint) is disabled until a model is ready. + * Admin lifecycle for the detection model. Uses the never-gated {@code form-detection-model} key so + * install and status stay reachable while {@code form-detection} itself is disabled. */ @Slf4j @RestController diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/FormDetectionEngine.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/FormDetectionEngine.java index 7cca78105e..cb71b45cb3 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/FormDetectionEngine.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/FormDetectionEngine.java @@ -1,11 +1,8 @@ package stirling.software.proprietary.formdetection.inference; /** - * Lets the model manager drop an engine's loaded model without knowing what the engine is. - * - *

Deliberately free of any ONNX type: the manager is an unconditional bean, and referencing - * {@code OnnxFormDetector} directly would make Spring introspect it on builds that ship no - * onnxruntime, failing startup for the whole app. + * Lets the model manager drop an engine's loaded model. Must stay ONNX-free: Spring introspecting + * {@code OnnxFormDetector} without onnxruntime would kill startup. */ public interface FormDetectionEngine { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/OnnxFormDetector.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/OnnxFormDetector.java index dac870ffd1..6c0d00bc6d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/OnnxFormDetector.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/OnnxFormDetector.java @@ -26,10 +26,8 @@ import ai.onnxruntime.OrtException; import ai.onnxruntime.OrtSession; /** - * Holds the ONNX Runtime session for the active model. Lazily (re)loads when the active model - * changes, guards session swaps with a read/write lock, and bounds concurrent inferences to limit - * memory. The session input is NCHW float32 {@code [1,3,N,N]}; the raw output is returned as-is for - * {@link Yolo#decode} to interpret per the model spec. + * Holds the ONNX Runtime session, reloading on model change under a read/write lock and capping + * concurrent inferences. Input is NCHW float32 {@code [1,3,N,N]}; output is raw. */ @Slf4j @Service @@ -48,12 +46,8 @@ public class OnnxFormDetector implements FormDetectionEngine { private volatile String inputName; /** - * Run the model and return every output tensor keyed by its graph name, in graph order. - * - *

Keyed by name rather than position because a query-based head emits two tensors of the - * SAME shape - RF-DETR's {@code dets} and {@code labels} are both [1, 300, 4] when there are - * three classes, since 4 box values and 3 classes + 1 no-object slot coincide. Picking by index - * would silently decode logits as boxes. + * Run the model, returning each output tensor keyed by graph name. RF-DETR's {@code dets} and + * {@code labels} share a shape, so an index would decode logits as boxes. */ public Map infer(float[] chw, int inputSize) { ensureLoaded(); @@ -147,10 +141,8 @@ public class OnnxFormDetector implements FormDetectionEngine { loadedModelId = activeId; log.info("Loaded ONNX session for Auto Form Detection model '{}'", activeId); } catch (OrtException | RuntimeException | LinkageError e) { - // Native library missing/incompatible for this OS+arch (e.g. a Linux-slimmed jar - // run on Windows), or a model load failure. Degrade gracefully instead of letting - // an UnsatisfiedLinkError escape - the detect endpoint reports unavailable and the - // server keeps running. + // Native missing for this OS+arch, or the model failed to load. + // Swallowed so no UnsatisfiedLinkError escapes; detect reports unavailable. throw new IllegalStateException( "ONNX Runtime is unavailable on this platform/build: " + e.getMessage(), e); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/RfDetr.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/RfDetr.java index 2eb106ce3a..bb4cd951f2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/RfDetr.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/RfDetr.java @@ -9,13 +9,8 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.proprietary.formdetection.model.ModelCatalogEntry; /** - * Decoder for RF-DETR style query-based heads, as used by the Apache-2.0 FFDetr checkpoint. - * - *

Differs from {@link Yolo} in every respect that matters downstream: two output tensors instead - * of one, a fixed set of queries instead of an anchor grid, boxes normalised to [0,1] instead of - * input pixels, and raw logits instead of activated scores. Preprocessing is shared - {@link - * Yolo#preprocess} already honours the spec's channel order and mean/std, which is all RF-DETR - * needs (RGB, ImageNet normalisation). + * Decoder for RF-DETR query-based heads: two output tensors, fixed queries, boxes normalised to + * [0,1] and raw logits. Preprocessing is shared with {@link Yolo#preprocess}. */ @Slf4j public final class RfDetr { @@ -29,11 +24,8 @@ public final class RfDetr { private static final String LOGITS = "labels"; /** - * Decode two named outputs into detections in original-bitmap pixels. - * - *

Outputs are looked up by name, never by position: with three classes both tensors are - * [300, 4] - four box values against three classes plus the no-object slot - so they cannot be - * told apart by shape. + * Decode two named outputs into detections in original-bitmap pixels. Looked up by name, never + * position: with three classes both tensors are [300, 4] and share a shape. */ public static List decode( Map outputs, diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/Yolo.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/Yolo.java index c360bbb070..f2afc7253a 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/Yolo.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/Yolo.java @@ -12,13 +12,8 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.proprietary.formdetection.model.ModelCatalogEntry; /** - * Pure pre/post-processing for a YOLO-style detector, driven entirely by the {@link - * ModelCatalogEntry} spec. - * - *

Coordinate spaces: {@code preprocess} maps the source bitmap into the model's NxN input; - * {@code decode} reads raw model output (boxes in input-pixel space), thresholds, runs NMS, and - * un-projects boxes back to the original bitmap-pixel space (top-left origin). Mapping to PDF - * points is done separately by {@code CoordinateMapper}. + * Pre/post-processing for a YOLO-style detector, driven by the {@link ModelCatalogEntry} spec. + * {@code decode} yields bitmap pixels, top-left origin; CoordinateMapper maps to PDF points. */ @Slf4j public final class Yolo { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/model/ModelCatalogEntry.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/model/ModelCatalogEntry.java index 70c92eb866..d63bf528b1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/model/ModelCatalogEntry.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/model/ModelCatalogEntry.java @@ -5,14 +5,8 @@ import java.util.List; import lombok.Data; /** - * One installable form-detection model plus the data-driven pre/post-processing spec the inference - * pipeline needs. - * - *

NOTE: the pipeline-spec defaults below follow common Ultralytics-YOLO conventions. The precise - * values for a given model (input size, resize mode, channel order, output layout, NMS, class - * indices) MUST be verified against the actual exported {@code .onnx} before that entry's {@code - * onnxUrl}/{@code sha256} are populated. An entry with a blank {@code onnxUrl} or {@code sha256} is - * not installable, which keeps the distribution shippable without any bundled model. + * One installable model plus the pre/post-processing spec its pipeline needs; verify every value + * against the exported {@code .onnx}. A blank {@code onnxUrl} means not installable. */ @Data public class ModelCatalogEntry { @@ -54,15 +48,8 @@ public class ModelCatalogEntry { // --- Post-processing (parity-critical) --------------------------------------- /** - * Which head shape the model emits, and so how its output is read. - * - *

    - *
  • {@code yolo} - one anchor-grid tensor, boxes already in input pixels, scores already - * through their activation. Described by {@link #outputLayout}/{@link #hasObjectness}. - *
  • {@code rfdetr} - two named tensors, {@code dets} (normalised cxcywh) and {@code labels} - * (raw logits, one column per class plus a trailing no-object slot). Query based, so - * there is no anchor grid and the two fields above do not apply. - *
+ * Head shape, so how the output is read. {@code yolo}: one anchor-grid tensor, input pixels, + * activated scores. {@code rfdetr}: {@code dets} (normalised cxcywh) plus {@code labels}. */ private String decoder = "yolo"; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/render/CoordinateMapper.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/render/CoordinateMapper.java index 32b2363d67..c7e688861e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/render/CoordinateMapper.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/render/CoordinateMapper.java @@ -6,10 +6,8 @@ import stirling.software.proprietary.formdetection.inference.Yolo; import stirling.software.proprietary.formdetection.model.DetectedField; /** - * Maps a detection (rendered bitmap pixels, top-left origin) to PDF points in unrotated user space - * (bottom-left origin, crop-box offset applied) - the space PDFBox and pdf-lib widget rectangles - * live in. The bitmap is display space: /Rotate baked in and the crop box anchored at (0,0), so the - * mapping is scale + Y-flip, then the inverse page rotation, then the crop-box translation. + * Maps bitmap pixels (top-left origin) to PDF points in unrotated user space, where widget rects + * live. The bitmap has /Rotate baked in: scale + Y-flip, inverse rotation, then crop. */ @UtilityClass public class CoordinateMapper { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/render/PageRasterizer.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/render/PageRasterizer.java index 2ccf033aef..d8cf4c549e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/render/PageRasterizer.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/render/PageRasterizer.java @@ -19,18 +19,8 @@ import stirling.software.jpdfium.model.PageSize; import stirling.software.jpdfium.model.RenderResult; /** - * Renders PDF pages to RGBA bitmaps via JPDFium. Each page is rendered at a DPI chosen so its long - * side is approximately the model input size, minimising any later resampling. The actual - * pixels-per-point scale is computed from the rendered dimensions so coordinate mapping does not - * depend on how {@code renderAt} interprets its argument. - * - *

PDFium renders the page as displayed: /Rotate baked in and the crop box anchored at (0,0). The - * per-page rotation and crop-box origin needed to map detections back into unrotated user space are - * not exposed by JPDFium, so they are read from PDFBox alongside the render. - * - *

Pages are handed to the caller one at a time rather than returned as a list: a rendered page - * is several megabytes of RGBA, so holding a whole document worth of them at once is enough to - * exhaust the heap on a large upload. + * Renders pages to RGBA via JPDFium at roughly the model input size, one at a time so a large + * document cannot exhaust the heap. Rotation and crop box come from PDFBox, not JPDFium. */ @Slf4j @Service diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManager.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManager.java index 7eaf58b911..73798e727a 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManager.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/service/FormDetectionModelManager.java @@ -45,10 +45,8 @@ import stirling.software.proprietary.formdetection.model.ModelCatalogEntry; import stirling.software.proprietary.formdetection.model.ModelStatusResponse; /** - * Downloads, verifies and tracks the on-demand Auto Form Detection model. Concurrency-safe - * (single-flight install), checksum-verified, and atomic-published to a mounted volume so the model - * survives container restarts/updates. Mirrors the OCR tessdata admin pattern but adds the lock, - * temp-file + atomic rename, and SHA-256 verification the spec requires. + * Downloads, verifies and tracks the detection model. Single-flight install, SHA-256 verified, + * published by atomic rename into a mounted volume so it survives container restarts. */ @Slf4j @Service @@ -70,9 +68,8 @@ public class FormDetectionModelManager { private static final int MAX_REDIRECTS = 5; /** - * Whether the server-side ONNX engine is bundled in this build (the onnxruntime jar is only - * included via {@code -PbundleOnnxRuntime=true}, e.g. the Docker server image). The frontend - * uses this to disable the "Server" execution mode when it isn't available. + * Whether the ONNX engine is bundled in this build; the jar only ships with {@code + * -PbundleOnnxRuntime=true}. Without it the tool cannot run at all. */ private static final boolean SERVER_ENGINE_AVAILABLE = isOnnxRuntimePresent(); @@ -355,10 +352,8 @@ public class FormDetectionModelManager { } /** - * Tombstone marking a model an admin explicitly uninstalled, so the pre-installed seeding on - * the next boot does not silently resurrect it. Cleared by an explicit (re)install. The id is - * SAFE_ID-validated by every caller; the containment check keeps the resolve provably inside - * the model dir regardless. + * Tombstone for an admin-uninstalled model, so the next boot's seeding cannot resurrect it; + * cleared by a reinstall. The containment check keeps the path inside the model dir. */ private Path tombstoneFor(String id) { Path base = modelDir().normalize(); @@ -378,10 +373,8 @@ public class FormDetectionModelManager { } /** - * Mark a verified, on-disk model as the active one and (re)enable the feature. - * - *

Synchronized because it runs on the install thread and writes settings, which the admin - * setters also do; without the shared monitor a concurrent toggle could drop one of the keys. + * Mark a verified on-disk model active and re-enable the feature. Synchronized because it + * writes settings from the install thread, where a concurrent toggle could drop a key. */ private synchronized void activate(String modelId, String expectedSha) { clearTombstone(modelId); @@ -401,8 +394,7 @@ public class FormDetectionModelManager { /** * Drop any model an engine still holds. Reinstalling the same id leaves the loaded id - * unchanged, so without this the engine would keep serving from the session it opened before - * the swap. + * unchanged, so without this the engine keeps serving the pre-swap session. */ private void invalidateEngine() { engineProvider.ifAvailable(FormDetectionEngine::unload); @@ -497,9 +489,8 @@ public class FormDetectionModelManager { } /** - * Locate an installed model by listing a directory, so the path handed to the file API comes - * from the directory itself and can never escape it via the supplied id. The writable model dir - * wins; an image-baked copy is read in place rather than duplicated into it. + * Locate a model by listing the directory, so the path can never escape it via the id. The + * writable model dir wins; an image-baked copy is read in place. */ private Optional installedModelFile(String id) { if (StringUtils.isBlank(id) || !SAFE_ID.matcher(id).matches()) { @@ -580,10 +571,8 @@ public class FormDetectionModelManager { } /** - * Activate an image-baked model (see {@code formDetection.preinstalledModelDir}) when nothing - * is active yet, so the air-gapped image works without an admin install. The file is read where - * the image put it - copying it into the writable model dir would store the same ~37MB twice on - * every running container. No-op when the dir is unset or missing (desktop/local). + * Activate an image-baked model when nothing is active, so the air-gapped image needs no admin + * install. Read in place; copying would store the same ~37MB twice per container. */ private void seedPreinstalledModels() { Path src = preinstalledDir(); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/catalog/ModelCatalogServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/catalog/ModelCatalogServiceTest.java index d7980f3de2..b54a506784 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/catalog/ModelCatalogServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/catalog/ModelCatalogServiceTest.java @@ -29,11 +29,8 @@ class ModelCatalogServiceTest { assertEquals(3, l.getClassFieldTypes().size()); assertTrue(l.getInputSize() > 0); - // Model-free distribution: the jar bundles no weights, they are fetched on demand. What - // must never happen is downloading without a checksum to verify against, so any entry - // that declares a URL must also declare a SHA-256. An entry may legitimately carry - // neither yet - the admin panel renders it as not-installable (see `installable` in - // AdminFormDetectionSection) - which is how a model we have not published lands here. + // Weights are fetched on demand, so a URL without a SHA-256 would download unverified. + // Neither field set is fine: the admin panel renders that entry as not-installable. for (ModelCatalogEntry e : all) { assertNotNull(e.getOnnxUrl(), e.getId() + " must declare a URL field, even if blank"); assertNotNull(e.getSha256(), e.getId() + " must declare a checksum"); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/inference/RfDetrTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/inference/RfDetrTest.java index db2c5016c9..5fac07fd0a 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/inference/RfDetrTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/inference/RfDetrTest.java @@ -18,13 +18,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import stirling.software.proprietary.formdetection.model.ModelCatalogEntry; /** - * Parity tests for the RF-DETR decode path. - * - *

The fixture holds real tensors captured from the exported FFDetr ONNX, together with the - * detections the reference Python decode produced from them. That makes this a genuine parity check - * against the model rather than a restatement of the Java code: if the Java decode forgets the - * sigmoid, reads the no-object column as a class, or treats the boxes as pixels rather than - * normalised, the expected values below stop matching. + * Parity tests for the RF-DETR decode path. The fixture holds real tensors from the exported ONNX + * plus the reference Python decode output, so a missing sigmoid breaks it. */ class RfDetrTest { @@ -159,9 +154,8 @@ class RfDetrTest { } /** - * Hand-built because the exported model never lets the no-object column win - its highest - * sigmoid across all 300 queries is 0.0014 - so real tensors cannot exercise this guard. - * Without it a dominant 4th column would yield classId 3 and index past classNames. + * Hand-built: the exported model never lets the no-object column win, so real tensors cannot + * exercise this guard. Without it a dominant 4th column indexes past classNames. */ @Test void neverClassifiesAQueryAsTheNoObjectColumn() { diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/render/PageRasterizerRotationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/render/PageRasterizerRotationTest.java index a76331d0aa..39a4818a57 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/render/PageRasterizerRotationTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/render/PageRasterizerRotationTest.java @@ -26,9 +26,8 @@ import stirling.software.proprietary.formdetection.inference.Yolo; import stirling.software.proprietary.formdetection.model.DetectedField; /** - * End-to-end geometry check: draw a black rectangle at known user-space coordinates on pages with - * /Rotate and a shifted crop box, render through the real PDFium rasterizer, locate the dark region - * in the bitmap, and assert the coordinate mapper recovers the original user-space rectangle. + * End-to-end geometry check: render a black rectangle on rotated, crop-shifted pages through real + * PDFium, find it in the bitmap, and assert the mapper recovers its user-space rect. */ class PageRasterizerRotationTest { diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index 493405f3b1..9b4c9e22f5 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -19,11 +19,8 @@ RUN apt-get update \ && rm /tmp/task.deb \ && rm -rf /var/lib/apt/lists/* -# Empty by default: this image can reach the internet, so an admin installs a detection model -# on demand rather than every user pulling ~27MB. Only bake in weights we may redistribute: -# --build-arg FORM_DETECTION_MODEL_URL=... \ -# --build-arg FORM_DETECTION_MODEL_SHA256=... \ -# --build-arg FORM_DETECTION_MODEL_ID=ffdetr +# Empty by default: this image has internet, so an admin installs a model rather than every user +# pulling ~27MB. Bake one in only if we may redistribute it, via the three FORM_DETECTION_MODEL_* args. ARG FORM_DETECTION_MODEL_URL="" ARG FORM_DETECTION_MODEL_SHA256="" ARG FORM_DETECTION_MODEL_ID="ffdetr" diff --git a/frontend/editor/src/core/hooks/useFormDetectionModelStatus.ts b/frontend/editor/src/core/hooks/useFormDetectionModelStatus.ts index c1233c203f..563d525045 100644 --- a/frontend/editor/src/core/hooks/useFormDetectionModelStatus.ts +++ b/frontend/editor/src/core/hooks/useFormDetectionModelStatus.ts @@ -39,9 +39,8 @@ const CONFIG_URL = "/api/v1/form/form-detection-model/config"; const MODEL_URL = "/api/v1/form/form-detection-model"; /** - * Polls the Auto Form Detection model status and exposes admin install/uninstall actions. - * Polling only runs while a download/verify is in flight. When readiness flips, the shared - * endpoint-availability cache is invalidated so the tool tile re-enables/disables. + * Polls model status while an install is in flight and exposes the admin actions. Readiness flips + * invalidate the endpoint-availability cache so the tool tile enables/disables. */ export function useFormDetectionModelStatus() { const queryClient = useQueryClient(); diff --git a/frontend/editor/src/core/tests/stubbed/auto-form-detection.spec.ts b/frontend/editor/src/core/tests/stubbed/auto-form-detection.spec.ts index b01dbe7db1..74fd7f23b2 100644 --- a/frontend/editor/src/core/tests/stubbed/auto-form-detection.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/auto-form-detection.spec.ts @@ -6,11 +6,8 @@ import { } from "@app/tests/helpers/api-stubs"; /** - * Stubbed coverage for the Auto Form Detection tool and its admin install panel. - * - the tool tile renders even when the `form-detection` endpoint is disabled - * (model not installed), so users can discover it, - * - the tile opens the tool once the endpoint reports enabled, - * - an admin sees the "AI Form Detection" settings section. + * Stubbed coverage for the tool tile (shown even when the endpoint is disabled, so the tool stays + * discoverable), opening it once enabled, and the admin install panel. */ const MODEL_STATUS_NOT_INSTALLED = { diff --git a/frontend/editor/src/core/tests/stubbed/ui-review-capture.spec.ts b/frontend/editor/src/core/tests/stubbed/ui-review-capture.spec.ts index 21fbb36dd7..586c776f10 100644 --- a/frontend/editor/src/core/tests/stubbed/ui-review-capture.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/ui-review-capture.spec.ts @@ -76,10 +76,8 @@ async function stubStatus(page: Page, overrides: StatusOverrides = {}) { } /** - * Stub the server detect endpoint. The tool now posts applyToPdf=false and expects - * JSON detections (client-side apply); return a realistic field set so the summary - * panel and the pdf-lib apply path exercise for real. Falls back to the PDF blob - * shape if the request asked for applyToPdf=true. + * Stub the detect endpoint: JSON detections for applyToPdf=false so the summary panel and the + * pdf-lib apply path run for real, a PDF blob otherwise. */ async function stubDetect(page: Page, delayMs = 0) { const pdf = fs.readFileSync(SAMPLE_PDF); diff --git a/scripts/export-ffdetr-onnx.py b/scripts/export-ffdetr-onnx.py index 4f1ab20751..19285e73d6 100644 --- a/scripts/export-ffdetr-onnx.py +++ b/scripts/export-ffdetr-onnx.py @@ -1,18 +1,10 @@ #!/usr/bin/env python3 -"""Export the Apache-2.0 FFDetr form-field detector to the ONNX we ship. - -Its publisher releases only a PyTorch `.pth`, so unlike the FFDNet checkpoints there is no -`.onnx` to point a catalogue entry at. This does the conversion they skipped. Run it once on a -workstation or a CI job; torch is needed HERE and nowhere else - the product loads the result -with onnxruntime alone, exactly as it loads any other model in the catalogue. +"""Export the Apache-2.0 FFDetr detector to the ONNX we ship: int8 (~37MB) plus its fp32 source, +printing the sha256 for model-catalog.json. torch is needed here and nowhere else. pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install rfdetr onnx onnxruntime onnxconverter-common python scripts/export-ffdetr-onnx.py --out build/ffdetr - -Emits `ffdetr-int8.onnx` (~37MB, the one to host) plus the fp32 graph it came from, and prints -the sha256 for `model-catalog.json` and docker/embedded/Dockerfile.fat. On a form page the int8 -graph returns detections indistinguishable from fp32. """ from __future__ import annotations @@ -24,10 +16,8 @@ import sys import warnings from pathlib import Path -# Pinned so a re-export uses the same checkpoint; bump deliberately, not incidentally. The OUTPUT -# is not byte-reproducible - onnxruntime writes its registered opset_import domains into the graph, -# so a different runtime version shifts the tail and the sha256. Check equivalence by running both -# graphs on one input, not by comparing hashes; update model-catalog.json when you republish. +# Pinned so a re-export uses the same checkpoint. The output is not byte-reproducible (opset_import +# domains shift with the runtime), so check equivalence by running both graphs, not by hash. REPO = "jbarrow/FFDetr" REVISION = "56f4e4235e28dcb2953513dc020bb191a2f54cfe" CHECKPOINT_SHA256 = "f852e1bac18c8f435b82270fc8ff8e2ca4a2cd8869c411fa8f473f16e69585ef" diff --git a/scripts/slim-onnxruntime.sh b/scripts/slim-onnxruntime.sh index f5223d6a27..455a318c89 100644 --- a/scripts/slim-onnxruntime.sh +++ b/scripts/slim-onnxruntime.sh @@ -1,17 +1,9 @@ #!/usr/bin/env sh -# Shrink the bundled ONNX Runtime native libraries down to a single Linux arch. +# Shrink the bundled ONNX Runtime natives to one Linux arch: the jar carries four platforms (~42MB) +# and a container loads exactly one, so keeping only the target takes it to ~8MB. Docker build only. # -# The onnxruntime jar carries natives for macOS arm64, Windows x64 and Linux x64/arm64 (~42MB). -# A container loads exactly one, so keeping only the target arch takes the jar to ~8MB. -# Docker-build-only, so local and desktop builds keep every platform for cross-platform work. -# -# Usage: slim-onnxruntime.sh [debian-arch] -# = a directory containing onnxruntime-*.jar (e.g. an extracted BOOT-INF/lib) -# | a path to an onnxruntime-*.jar -# | a Spring Boot fat jar that nests BOOT-INF/lib/onnxruntime-*.jar -# [arch] = amd64 | arm64 (defaults to the build host's dpkg arch) -# -# Safe no-op when onnxruntime is absent or `zip` is unavailable. +# Usage: slim-onnxruntime.sh [amd64|arm64], where is a dir holding +# onnxruntime-*.jar, that jar, or a Spring Boot fat jar. No-op if it or `zip` is missing. set -eu target="${1:?usage: slim-onnxruntime.sh [arch]}" diff --git a/testing/cucumber/features/form_detection_nightly.feature b/testing/cucumber/features/form_detection_nightly.feature index 93de905600..9f10e2cf2a 100644 --- a/testing/cucumber/features/form_detection_nightly.feature +++ b/testing/cucumber/features/form_detection_nightly.feature @@ -1,9 +1,8 @@ @nightly @proprietary @form-detection Feature: Auto Form Detection API Validation - # Needs the ONNX runtime bundled in and a detection model baked into the image, - # which only Dockerfile.fat does. behave.ini excludes @nightly; the nightly job - # opts back in with --tags=@nightly against that image. + # Needs the ONNX runtime and a baked-in model, which only Dockerfile.fat has. + # behave.ini excludes @nightly; the nightly job opts back in against that image. @model-status @positive Scenario: Model status reports a ready model and an available engine diff --git a/testing/cucumber/features/steps/form_detection_step_definitions.py b/testing/cucumber/features/steps/form_detection_step_definitions.py index c72365e4ff..b81dc338bb 100644 --- a/testing/cucumber/features/steps/form_detection_step_definitions.py +++ b/testing/cucumber/features/steps/form_detection_step_definitions.py @@ -10,9 +10,7 @@ FIELD_TYPES = {"text", "checkbox", "radio", "signature"} def _draw_form_page(c, width, height, title): """Draw a printed form: ruled entry boxes, checkbox squares and a signature line. - - The detector works off the rendered page, so the page has to *look* like a form - - an AcroForm-only PDF gives it nothing to see. + The detector works off the rendered page, so an AcroForm-only PDF gives it nothing to see. """ c.setFont("Helvetica-Bold", 16) c.drawString(60, height - 70, title)