feat(ocr-paymentpage) - frontend recieving content and displaying initial, not editable
This commit is contained in:
@@ -4,7 +4,7 @@ import appointmentsRoutes from "./appointments";
|
|||||||
import usersRoutes from "./users";
|
import usersRoutes from "./users";
|
||||||
import staffsRoutes from "./staffs";
|
import staffsRoutes from "./staffs";
|
||||||
import claimsRoutes from "./claims";
|
import claimsRoutes from "./claims";
|
||||||
import patientDataExtractionRoutes from "./patientdataExtraction";
|
import patientDataExtractionRoutes from "./patientDataExtraction";
|
||||||
import insuranceCredsRoutes from "./insuranceCreds";
|
import insuranceCredsRoutes from "./insuranceCreds";
|
||||||
import documentsRoutes from "./documents";
|
import documentsRoutes from "./documents";
|
||||||
import insuranceEligibilityRoutes from "./insuranceEligibility";
|
import insuranceEligibilityRoutes from "./insuranceEligibility";
|
||||||
|
|||||||
276
apps/Frontend/src/components/payments/payment-ocr-block.tsx
Normal file
276
apps/Frontend/src/components/payments/payment-ocr-block.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
// PaymentOCRBlock.tsx
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
// If you're using shadcn/ui and lucide-react, keep these.
|
||||||
|
// Otherwise swap for your own components/icons.
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Upload, Image as ImageIcon, X } from "lucide-react";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
type Row = Record<string, string | number | boolean | null | undefined>;
|
||||||
|
|
||||||
|
export default function PaymentOCRBlock() {
|
||||||
|
// UI state
|
||||||
|
const [uploadedImage, setUploadedImage] = React.useState<File | null>(null);
|
||||||
|
const [uploadedImages, setUploadedImages] = React.useState<File[]>([]);
|
||||||
|
const [isDragging, setIsDragging] = React.useState(false);
|
||||||
|
const [isExtracting, setIsExtracting] = React.useState(false);
|
||||||
|
const [rows, setRows] = React.useState<Row[]>([]);
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
//Mutation
|
||||||
|
const extractPaymentOCR = useMutation({
|
||||||
|
mutationFn: async (files: File[]) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
files.forEach((file) => formData.append("files", file, file.name));
|
||||||
|
|
||||||
|
const res = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/payment-ocr/extract",
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error("Failed to extract payment data");
|
||||||
|
const data = (await res.json()) as { rows: Row[] } | Row[];
|
||||||
|
return Array.isArray(data) ? data : data.rows;
|
||||||
|
},
|
||||||
|
onSuccess: (rows) => {
|
||||||
|
setRows(rows);
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: `Failed to extract payment data: ${error.message}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- handlers (all in this file) -----------------------------------------
|
||||||
|
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (!files || !files.length) return;
|
||||||
|
const list = Array.from(files);
|
||||||
|
setUploadedImage(list[0] as File); // preview first
|
||||||
|
setUploadedImages(list);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
const list = Array.from(e.dataTransfer.files || []).filter((f) =>
|
||||||
|
f.type.startsWith("image/")
|
||||||
|
);
|
||||||
|
if (!list.length) {
|
||||||
|
setError("Please drop image files (JPG/PNG/TIFF/BMP).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUploadedImage(list[0] as File);
|
||||||
|
|
||||||
|
setUploadedImages(list);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUploadedImage = () => {
|
||||||
|
setUploadedImage(null);
|
||||||
|
setUploadedImages([]);
|
||||||
|
setRows([]);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExtract = () => {
|
||||||
|
if (!uploadedImages.length) return;
|
||||||
|
extractPaymentOCR.mutate(uploadedImages);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- render ---------------------------------------------------------------
|
||||||
|
return (
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-medium text-gray-800">
|
||||||
|
Payment Document OCR
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div
|
||||||
|
className={`flex-1 border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||||
|
isDragging
|
||||||
|
? "border-blue-400 bg-blue-50"
|
||||||
|
: uploadedImage
|
||||||
|
? "border-green-400 bg-green-50"
|
||||||
|
: "border-gray-300 bg-gray-50 hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
onDrop={handleImageDrop}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setIsDragging(false)}
|
||||||
|
onClick={() =>
|
||||||
|
document.getElementById("image-upload-input")?.click()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{uploadedImage ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-center space-x-4">
|
||||||
|
<ImageIcon className="h-8 w-8 text-green-500" />
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="font-medium text-green-700">
|
||||||
|
{uploadedImage.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{(uploadedImage.size / 1024 / 1024).toFixed(2)} MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeUploadedImage();
|
||||||
|
}}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isExtracting && (
|
||||||
|
<div className="text-sm text-blue-600">
|
||||||
|
Extracting payment information...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Upload className="h-12 w-12 text-gray-400 mx-auto" />
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-medium text-gray-700 mb-2">
|
||||||
|
Upload Payment Document
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
|
Drag and drop image(s) or click to browse
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" type="button">
|
||||||
|
Choose Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Supported formats: JPG, PNG, TIFF, BMP • Max size: 10MB each
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="image-upload-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageSelect}
|
||||||
|
className="hidden"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleExtract}
|
||||||
|
disabled={!uploadedImages.length || isExtracting}
|
||||||
|
>
|
||||||
|
{extractPaymentOCR.isPending
|
||||||
|
? "Extracting..."
|
||||||
|
: "Extract Payment Data"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
{error && <p className="mt-4 text-sm text-red-600">{error}</p>}
|
||||||
|
<OCRTable rows={rows} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------- helpers (kept in same file) ----------------------------
|
||||||
|
|
||||||
|
function OCRTable({ rows }: { rows: Row[] }) {
|
||||||
|
if (!rows?.length) return null;
|
||||||
|
|
||||||
|
// prefer Excel-like order if present, then append any extra columns
|
||||||
|
const desired = [
|
||||||
|
"Patient Name",
|
||||||
|
"Patient ID",
|
||||||
|
"ICN",
|
||||||
|
"CDT Code",
|
||||||
|
"Tooth",
|
||||||
|
"Date SVC",
|
||||||
|
"Billed Amount",
|
||||||
|
"Allowed Amount",
|
||||||
|
"Paid Amount",
|
||||||
|
"Extraction Success",
|
||||||
|
"Source File",
|
||||||
|
];
|
||||||
|
const dynamicCols = Array.from(
|
||||||
|
rows.reduce<Set<string>>((acc, r) => {
|
||||||
|
Object.keys(r || {}).forEach((k) => acc.add(k));
|
||||||
|
return acc;
|
||||||
|
}, new Set())
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
...desired.filter((c) => dynamicCols.includes(c)),
|
||||||
|
...dynamicCols.filter((c) => !desired.includes(c)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 overflow-auto border rounded-lg">
|
||||||
|
<table className="min-w-full text-sm">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
{columns.map((c) => (
|
||||||
|
<th
|
||||||
|
key={c}
|
||||||
|
className="px-3 py-2 text-left font-semibold text-gray-700 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{c}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((r, i) => (
|
||||||
|
<tr key={i} className="odd:bg-white even:bg-gray-50">
|
||||||
|
{columns.map((c) => (
|
||||||
|
<td key={c} className="px-3 py-2 whitespace-nowrap">
|
||||||
|
{formatCell(r[c])}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCell(v: unknown) {
|
||||||
|
if (v === null || v === undefined) return "";
|
||||||
|
if (typeof v === "boolean") return v ? "Yes" : "No";
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeJson(resp: Response) {
|
||||||
|
try {
|
||||||
|
return await resp.json();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,7 @@ import {
|
|||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { DollarSign } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { DollarSign, Upload, Image, X } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -18,95 +16,10 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import PaymentsRecentTable from "@/components/payments/payments-recent-table";
|
import PaymentsRecentTable from "@/components/payments/payments-recent-table";
|
||||||
import PaymentsOfPatientModal from "@/components/payments/payments-of-patient-table";
|
import PaymentsOfPatientModal from "@/components/payments/payments-of-patient-table";
|
||||||
|
import PaymentOCRBlock from "@/components/payments/payment-ocr-block";
|
||||||
|
|
||||||
export default function PaymentsPage() {
|
export default function PaymentsPage() {
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
||||||
const [paymentPeriod, setPaymentPeriod] = useState<string>("all-time");
|
const [paymentPeriod, setPaymentPeriod] = useState<string>("all-time");
|
||||||
const [uploadedImage, setUploadedImage] = useState<File | null>(null);
|
|
||||||
const [isExtracting, setIsExtracting] = useState(false);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const [editableData, setEditableData] = useState<any[]>([]);
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const toggleMobileMenu = () => {
|
|
||||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Image upload handlers for OCR
|
|
||||||
const handleImageDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragging(false);
|
|
||||||
|
|
||||||
const files = e.dataTransfer.files;
|
|
||||||
if (files && files[0]) {
|
|
||||||
const file = files[0];
|
|
||||||
if (file.type.startsWith("image/")) {
|
|
||||||
setUploadedImage(file);
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: "Invalid file type",
|
|
||||||
description: "Please upload an image file (JPG, PNG, etc.)",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = e.target.files;
|
|
||||||
if (files && files[0]) {
|
|
||||||
const file = files[0];
|
|
||||||
setUploadedImage(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOCRExtraction = async (file: File) => {
|
|
||||||
setIsExtracting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create FormData for image upload
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("image", file);
|
|
||||||
|
|
||||||
// Simulate OCR extraction process
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
} finally {
|
|
||||||
setIsExtracting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeUploadedImage = () => {
|
|
||||||
setUploadedImage(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateEditableData = (
|
|
||||||
index: number,
|
|
||||||
field: string,
|
|
||||||
value: string | number
|
|
||||||
) => {
|
|
||||||
setEditableData((prev) => {
|
|
||||||
const updated = [...prev];
|
|
||||||
updated[index] = { ...updated[index], [field]: value };
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteRow = (index: number) => {
|
|
||||||
setEditableData((prev) => prev.filter((_, i) => i !== index));
|
|
||||||
toast({
|
|
||||||
title: "Row Deleted",
|
|
||||||
description: `Payment record has been removed`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveRow = (index: number) => {
|
|
||||||
toast({
|
|
||||||
title: "Row Saved",
|
|
||||||
description: `Changes to payment record ${index + 1} have been saved`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -192,118 +105,8 @@ export default function PaymentsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OCR Image Upload Section - not working rn*/}
|
{/* OCR Image Upload Section*/}
|
||||||
<div className="mb-8">
|
<PaymentOCRBlock />
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h2 className="text-xl font-medium text-gray-800">
|
|
||||||
Payment Document OCR
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<div
|
|
||||||
className={`flex-1 border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
|
||||||
isDragging
|
|
||||||
? "border-blue-400 bg-blue-50"
|
|
||||||
: uploadedImage
|
|
||||||
? "border-green-400 bg-green-50"
|
|
||||||
: "border-gray-300 bg-gray-50 hover:border-gray-400"
|
|
||||||
}`}
|
|
||||||
onDrop={handleImageDrop}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(true);
|
|
||||||
}}
|
|
||||||
onDragLeave={() => setIsDragging(false)}
|
|
||||||
onClick={() =>
|
|
||||||
document.getElementById("image-upload-input")?.click()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{uploadedImage ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-center space-x-4">
|
|
||||||
<Image className="h-8 w-8 text-green-500" />
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="font-medium text-green-700">
|
|
||||||
{uploadedImage.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
{(uploadedImage.size / 1024 / 1024).toFixed(2)} MB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
removeUploadedImage();
|
|
||||||
}}
|
|
||||||
className="ml-auto"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{isExtracting && (
|
|
||||||
<div className="text-sm text-blue-600">
|
|
||||||
Extracting payment information...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Upload className="h-12 w-12 text-gray-400 mx-auto" />
|
|
||||||
<div>
|
|
||||||
<p className="text-lg font-medium text-gray-700 mb-2">
|
|
||||||
Upload Payment Document
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 mb-4">
|
|
||||||
Drag and drop an image or click to browse
|
|
||||||
</p>
|
|
||||||
<Button variant="outline" type="button">
|
|
||||||
Choose Image
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Supported formats: JPG, PNG, GIF • Max size: 10MB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input
|
|
||||||
id="image-upload-input"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleImageSelect}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col justify-center">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (!uploadedImage) {
|
|
||||||
toast({
|
|
||||||
title: "No Image Selected",
|
|
||||||
description:
|
|
||||||
"Please upload an image first before extracting information",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleOCRExtraction(uploadedImage);
|
|
||||||
}}
|
|
||||||
disabled={!uploadedImage || isExtracting}
|
|
||||||
className="min-w-32"
|
|
||||||
>
|
|
||||||
{isExtracting ? "Extracting..." : "Extract Info"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recent Payments table */}
|
{/* Recent Payments table */}
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
GOOGLE_APPLICATION_CREDENTIALS=google-credentials.json
|
GOOGLE_APPLICATION_CREDENTIALS=google_credentials.json
|
||||||
HOST="0.0.0.0"
|
HOST="0.0.0.0"
|
||||||
PORT="5003"
|
PORT="5003"
|
||||||
@@ -7,14 +7,10 @@ import os
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
load_dotenv() # loads .env (GOOGLE_APPLICATION_CREDENTIALS, HOST, PORT, etc.)
|
load_dotenv()
|
||||||
|
|
||||||
# Your adapter that calls the pipeline
|
|
||||||
from complete_pipeline_adapter import process_images_to_rows,rows_to_csv_bytes
|
from complete_pipeline_adapter import process_images_to_rows,rows_to_csv_bytes
|
||||||
|
|
||||||
# -------------------------------------------------
|
|
||||||
# App + concurrency controls (similar to your other app)
|
|
||||||
# -------------------------------------------------
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Payment OCR Services API",
|
title="Payment OCR Services API",
|
||||||
description="FastAPI wrapper around the OCR pipeline (Google Vision + deskew + line grouping + extraction).",
|
description="FastAPI wrapper around the OCR pipeline (Google Vision + deskew + line grouping + extraction).",
|
||||||
@@ -92,7 +88,7 @@ async def extract_json(files: List[UploadFile] = File(...)):
|
|||||||
try:
|
try:
|
||||||
blobs = [await f.read() for f in files]
|
blobs = [await f.read() for f in files]
|
||||||
names = [f.filename or "upload.bin" for f in files]
|
names = [f.filename or "upload.bin" for f in files]
|
||||||
rows = process_images_to_rows(blobs, names) # calls your pipeline
|
rows = process_images_to_rows(blobs, names) # calls pipeline
|
||||||
return JSONResponse(content={"rows": rows})
|
return JSONResponse(content={"rows": rows})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Processing error: {e}")
|
raise HTTPException(status_code=500, detail=f"Processing error: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user