mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Move form detection off the AI endpoint prefix and into Automation
This commit is contained in:
@@ -443,7 +443,7 @@ public class ApplicationProperties {
|
||||
|
||||
/**
|
||||
* Auto Form Detection settings. The model itself is downloaded on demand by an admin (see
|
||||
* {@code /api/v1/ai/form-detection-model/*}); only lightweight pointers are persisted here.
|
||||
* {@code /api/v1/form/form-detection-model/*}); only lightweight pointers are persisted here.
|
||||
*/
|
||||
@Data
|
||||
public static class FormDetection {
|
||||
|
||||
+5
-1
@@ -41,10 +41,14 @@ import stirling.software.proprietary.formdetection.service.FormDetectionModelMan
|
||||
* 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.
|
||||
*
|
||||
* <p>Deliberately under {@code /api/v1/form}, not {@code /api/v1/ai}: anything on the AI prefix is
|
||||
* classified billable by {@code BillableOperationClassifier} and blocked with 402 when the instance
|
||||
* is unlinked - which would gate on-device detection that never touches the server.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ai/form-detection")
|
||||
@RequestMapping("/api/v1/form/form-detection")
|
||||
@ConditionalOnClass(name = "ai.onnxruntime.OrtEnvironment")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Auto Form Detection")
|
||||
|
||||
+5
-1
@@ -26,10 +26,14 @@ import stirling.software.proprietary.formdetection.service.FormDetectionModelMan
|
||||
* 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.
|
||||
*
|
||||
* <p>Deliberately under {@code /api/v1/form}, not {@code /api/v1/ai}: anything on the AI prefix is
|
||||
* classified billable by {@code BillableOperationClassifier} and blocked with 402 when the instance
|
||||
* is unlinked - which would gate on-device detection that never touches the server.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ai/form-detection-model")
|
||||
@RequestMapping("/api/v1/form/form-detection-model")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Auto Form Detection")
|
||||
public class FormDetectionModelController {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ import stirling.software.proprietary.formdetection.service.FormDetectionModelMan
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ai/form-detection-model")
|
||||
@RequestMapping("/api/v1/form/form-detection-model")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Auto Form Detection")
|
||||
public class FormDetectionModelServeController {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Snapshot returned by {@code GET /api/v1/ai/form-detection-model/status}. Includes the full
|
||||
* Snapshot returned by {@code GET /api/v1/form/form-detection-model/status}. Includes the full
|
||||
* catalog so the browser can read the active model's parity-critical pipeline spec.
|
||||
*/
|
||||
@Data
|
||||
|
||||
+4
-4
@@ -46,7 +46,7 @@ class FormDetectionControllerTest {
|
||||
Mockito.when(manager.isReady()).thenReturn(false);
|
||||
|
||||
mvc(manager, Mockito.mock(OnnxFormDetector.class), Mockito.mock(PageRasterizer.class))
|
||||
.perform(multipart("/api/v1/ai/form-detection/detect").file(pdf()))
|
||||
.perform(multipart("/api/v1/form/form-detection/detect").file(pdf()))
|
||||
.andExpect(status().isServiceUnavailable())
|
||||
.andExpect(jsonPath("$.reason").value("DEPENDENCY"));
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class FormDetectionControllerTest {
|
||||
.thenReturn(List.of()); // no pages -> no detections, detector never called
|
||||
|
||||
mvc(manager, Mockito.mock(OnnxFormDetector.class), rasterizer)
|
||||
.perform(multipart("/api/v1/ai/form-detection/detect").file(pdf()))
|
||||
.perform(multipart("/api/v1/form/form-detection/detect").file(pdf()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.detections").isArray())
|
||||
.andExpect(jsonPath("$.detections").isEmpty());
|
||||
@@ -83,7 +83,7 @@ class FormDetectionControllerTest {
|
||||
Mockito.when(rasterizer.rasterize(Mockito.any(), Mockito.anyInt())).thenReturn(tooMany);
|
||||
|
||||
mvc(manager, Mockito.mock(OnnxFormDetector.class), rasterizer)
|
||||
.perform(multipart("/api/v1/ai/form-detection/detect").file(pdf()))
|
||||
.perform(multipart("/api/v1/form/form-detection/detect").file(pdf()))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.reason").value("LIMIT"));
|
||||
}
|
||||
@@ -99,7 +99,7 @@ class FormDetectionControllerTest {
|
||||
|
||||
mvc(manager, Mockito.mock(OnnxFormDetector.class), rasterizer)
|
||||
.perform(
|
||||
multipart("/api/v1/ai/form-detection/detect")
|
||||
multipart("/api/v1/form/form-detection/detect")
|
||||
.file(pdf())
|
||||
.param("confThreshold", "-42"))
|
||||
.andExpect(status().isOk())
|
||||
|
||||
+5
-5
@@ -34,7 +34,7 @@ class FormDetectionModelControllerTest {
|
||||
Mockito.when(manager.status()).thenReturn(notInstalled());
|
||||
|
||||
mvc(manager)
|
||||
.perform(get("/api/v1/ai/form-detection-model/status"))
|
||||
.perform(get("/api/v1/form/form-detection-model/status"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("not_installed"))
|
||||
.andExpect(jsonPath("$.writable").value(true));
|
||||
@@ -47,7 +47,7 @@ class FormDetectionModelControllerTest {
|
||||
|
||||
mvc(manager)
|
||||
.perform(
|
||||
post("/api/v1/ai/form-detection-model/install")
|
||||
post("/api/v1/form/form-detection-model/install")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"modelId\":\"\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
@@ -60,7 +60,7 @@ class FormDetectionModelControllerTest {
|
||||
|
||||
mvc(manager)
|
||||
.perform(
|
||||
post("/api/v1/ai/form-detection-model/install")
|
||||
post("/api/v1/form/form-detection-model/install")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"modelId\":\"ffdnet-s\"}"))
|
||||
.andExpect(status().isAccepted());
|
||||
@@ -76,7 +76,7 @@ class FormDetectionModelControllerTest {
|
||||
|
||||
mvc(manager)
|
||||
.perform(
|
||||
post("/api/v1/ai/form-detection-model/install")
|
||||
post("/api/v1/form/form-detection-model/install")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"modelId\":\"ffdnet-s\"}"))
|
||||
.andExpect(status().isConflict());
|
||||
@@ -88,7 +88,7 @@ class FormDetectionModelControllerTest {
|
||||
Mockito.when(manager.status()).thenReturn(notInstalled());
|
||||
|
||||
mvc(manager)
|
||||
.perform(delete("/api/v1/ai/form-detection-model"))
|
||||
.perform(delete("/api/v1/form/form-detection-model"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("not_installed"));
|
||||
}
|
||||
|
||||
@@ -486,7 +486,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
||||
"Automatically detect form fields with AI and make your PDF fillable.",
|
||||
),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.GENERAL,
|
||||
subcategoryId: SubcategoryId.AUTOMATION,
|
||||
maxFiles: 1,
|
||||
endpoints: ["form-detection"],
|
||||
unavailableMessage: t(
|
||||
|
||||
+2
-2
@@ -22,8 +22,8 @@ import {
|
||||
resolveConfidence,
|
||||
} from "@app/hooks/tools/autoFormDetection/useAutoFormDetectionParameters";
|
||||
|
||||
const DETECT_ENDPOINT = "/api/v1/ai/form-detection/detect";
|
||||
const STATUS_URL = "/api/v1/ai/form-detection-model/status";
|
||||
const DETECT_ENDPOINT = "/api/v1/form/form-detection/detect";
|
||||
const STATUS_URL = "/api/v1/form/form-detection-model/status";
|
||||
|
||||
export const buildAutoFormDetectionFormData = (
|
||||
parameters: AutoFormDetectionParameters,
|
||||
|
||||
@@ -51,10 +51,10 @@ export interface FormDetectionModelStatus {
|
||||
downloadingModelId?: string | null;
|
||||
}
|
||||
|
||||
const STATUS_URL = "/api/v1/ai/form-detection-model/status";
|
||||
const INSTALL_URL = "/api/v1/ai/form-detection-model/install";
|
||||
const CONFIG_URL = "/api/v1/ai/form-detection-model/config";
|
||||
const MODEL_URL = "/api/v1/ai/form-detection-model";
|
||||
const STATUS_URL = "/api/v1/form/form-detection-model/status";
|
||||
const INSTALL_URL = "/api/v1/form/form-detection-model/install";
|
||||
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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Fetch the active .onnx from the backend serve endpoint, verify its SHA-256, and keep it in the
|
||||
// Cache API keyed by checksum so it is downloaded only once per device (then reused across reloads).
|
||||
|
||||
const MODEL_FILE_URL = "/api/v1/ai/form-detection-model/file";
|
||||
const MODEL_FILE_URL = "/api/v1/form/form-detection-model/file";
|
||||
const CACHE_NAME = "stirling-form-detection-models";
|
||||
|
||||
function toHex(buf: ArrayBuffer): string {
|
||||
|
||||
@@ -73,7 +73,7 @@ function modelStatus(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
async function stubStatus(page: Page, overrides: Record<string, unknown> = {}) {
|
||||
await page.route("**/api/v1/ai/form-detection-model/status", (r: Route) =>
|
||||
await page.route("**/api/v1/form/form-detection-model/status", (r: Route) =>
|
||||
r.fulfill({ json: modelStatus(overrides) }),
|
||||
);
|
||||
}
|
||||
@@ -111,7 +111,7 @@ async function stubDetect(
|
||||
) {
|
||||
const pdf = fs.readFileSync(SAMPLE_PDF);
|
||||
const detections = opts.detections ?? DETECTIONS;
|
||||
await page.route("**/api/v1/ai/form-detection/detect", async (r: Route) => {
|
||||
await page.route("**/api/v1/form/form-detection/detect", async (r: Route) => {
|
||||
if (opts.delayMs) await new Promise((res) => setTimeout(res, opts.delayMs));
|
||||
const body = r.request().postData() ?? "";
|
||||
if (body.includes("applyToPdf") && body.includes("true")) {
|
||||
|
||||
@@ -35,7 +35,7 @@ const MODEL_STATUS_NOT_INSTALLED = {
|
||||
};
|
||||
|
||||
async function stubModelStatus(page: Page) {
|
||||
await page.route("**/api/v1/ai/form-detection-model/status", (route) =>
|
||||
await page.route("**/api/v1/form/form-detection-model/status", (route) =>
|
||||
route.fulfill({ json: MODEL_STATUS_NOT_INSTALLED }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ function modelStatus(overrides: StatusOverrides = {}) {
|
||||
}
|
||||
|
||||
async function stubStatus(page: Page, overrides: StatusOverrides = {}) {
|
||||
await page.route("**/api/v1/ai/form-detection-model/status", (r: Route) =>
|
||||
await page.route("**/api/v1/form/form-detection-model/status", (r: Route) =>
|
||||
r.fulfill({ json: modelStatus(overrides) }),
|
||||
);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ async function stubDetect(page: Page, delayMs = 0) {
|
||||
confidence: 0.74,
|
||||
},
|
||||
];
|
||||
await page.route("**/api/v1/ai/form-detection/detect", async (r: Route) => {
|
||||
await page.route("**/api/v1/form/form-detection/detect", async (r: Route) => {
|
||||
if (delayMs) await new Promise((res) => setTimeout(res, delayMs));
|
||||
const body = r.request().postData() ?? "";
|
||||
if (body.includes("applyToPdf") && body.includes("true")) {
|
||||
|
||||
Reference in New Issue
Block a user