mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Condense Auto Form Detection code comments
This commit is contained in:
@@ -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 <catalogId>.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 = "";
|
||||
}
|
||||
|
||||
@@ -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<PDField> 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<NewFormFieldDefinition> 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<PDAnnotationWidget> widgets = new ArrayList<>(field.getWidgets());
|
||||
if (!widgets.contains(widget)) {
|
||||
|
||||
+4
-6
@@ -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()));
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
+2
-5
@@ -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.
|
||||
*
|
||||
* <p>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 {
|
||||
|
||||
|
||||
+6
-14
@@ -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.
|
||||
*
|
||||
* <p>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<String, Yolo.RawOutput> 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);
|
||||
}
|
||||
|
||||
+4
-12
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<Yolo.Detection> decode(
|
||||
Map<String, Yolo.RawOutput> outputs,
|
||||
|
||||
+2
-7
@@ -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.
|
||||
*
|
||||
* <p>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 {
|
||||
|
||||
+4
-17
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code yolo} - one anchor-grid tensor, boxes already in input pixels, scores already
|
||||
* through their activation. Described by {@link #outputLayout}/{@link #hasObjectness}.
|
||||
* <li>{@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.
|
||||
* </ul>
|
||||
* 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";
|
||||
|
||||
|
||||
+2
-4
@@ -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 {
|
||||
|
||||
+2
-12
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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
|
||||
|
||||
+13
-24
@@ -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.
|
||||
*
|
||||
* <p>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<Path> 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();
|
||||
|
||||
+2
-5
@@ -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");
|
||||
|
||||
+4
-10
@@ -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.
|
||||
*
|
||||
* <p>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() {
|
||||
|
||||
+2
-3
@@ -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 {
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 <target> [debian-arch]
|
||||
# <target> = 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 <target> [amd64|arm64], where <target> 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 <target> [arch]}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user