IV.4
Turn paper, PDF and images into responses
You need this: When forms still arrive on paper, as scans or as filled PDFs.
The AI Form Response Extractor (MIT) takes the definition and a file, and returns answers in the form's own shape, ready to open in the form for a person to review. It calls the AI provider you configure with your own key; nothing is sent to SurveyJS. Here the SurveyJS service runs it next to this app; the route sends the scan as base64 JSON and saves nothing.
The SurveyJS service is called at http://surveyjs:3000. If it isn't running, the endpoint answers
503 "SurveyJS service is not running": start it with docker compose up -d surveyjs.
Try this
No scan yet.
Code that just ran
These regions are read from the files that served this page, the same lines the Server Integration page shows.
shared/client/extract-from-paper.js · sjs:IV.4.client
View on GitHub
// Upload the scan; open the draft in the form for review
async function extract(file) {
const body = new FormData();
body.append("scan", file);
const res = await fetch("/api/work-orders/extract", { method: "POST", body });
const result = await res.json();
if (!res.ok) throw new Error(result.error);
survey.data = result.answers; // a person checks and corrects, then saves as usual
return result; // { answers, confidence, uniqueId }
}
routes/examples/extract-from-paper.php · sjs:IV.4.server
View on GitHub
// POST /api/work-orders/extract — send the uploaded scan to the SurveyJS service; return a draft response
Route::post('/api/work-orders/extract', function (Request $request) {
$scan = $request->file('scan') ?? abort(400, 'Upload the scan in a field named "scan"');
abort_if(! $scan->isValid() || $scan->getSize() > 5 * 1024 * 1024, 413, 'The scan must be a file of 5 MB or less');
try {
$out = Http::baseUrl(config('surveyjs.service_url'))->timeout(120)->post('/extract', [ // JSON with base64, not multipart
'schema' => json_decode(DB::table('forms')->where('key', 'work-order')->value('json')), 'document' => base64_encode($scan->get())]);
} catch (ConnectionException $e) { // cURL error 28 is a timeout
return str_contains($e->getMessage(), 'cURL error 28')
? response()->json(['error' => 'SurveyJS service timed out'], 504)
: response()->json(['error' => 'SurveyJS service is not running at '.config('surveyjs.service_url')], 503);
}
$result = $out->object();
return match (true) { // nothing is saved: a person reviews the draft
$out->status() === 200 && is_object($result) && is_object($result->data ?? null) => response()->json(
['answers' => $result->data, 'confidence' => $result->confidence ?? [], 'uniqueId' => $result->uniqueId ?? null]),
$out->status() === 400 && $out->json('error') === 'INVALID_DOCUMENT' => response()->json(['error' => 'The file is not a PDF, PNG, JPEG, WebP or GIF document'], 400),
$out->status() === 503 && $out->json('error') === 'AI_NOT_CONFIGURED' => response()->json(['error' => 'The SurveyJS service has no AI provider configured'], 503),
$out->status() === 502 && $out->json('error') === 'EXTRACTION_FAILED' => response()->json(['error' => 'Could not extract the response from the document'], 502),
default => tap(response()->json(['error' => 'SurveyJS service error'], 502), fn () => Log::error('SurveyJS service: unexpected answer from /extract', ['status' => $out->status(), 'body' => substr($out->body(), 0, 2000)])),
};
});
Definition: shared/definitions/extract-from-paper.json