fix(mini-utilities): Stirling PDF API key auth, Tesseract.js OCR, emoji removal in cleaner
This commit is contained in:
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const STIRLING_PDF_URL =
|
||||
process.env.STIRLING_PDF_URL ?? "http://10.10.10.166:8087";
|
||||
const STIRLING_PDF_API_KEY =
|
||||
process.env.STIRLING_PDF_API_KEY ?? "cd829f62-6eef-43eb-a64d-c91af727b53a";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -9,6 +11,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const res = await fetch(`${STIRLING_PDF_URL}/api/v1/misc/compress-pdf`, {
|
||||
method: "POST",
|
||||
headers: { "X-API-KEY": STIRLING_PDF_API_KEY },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
|
||||
@@ -375,6 +375,10 @@ function AiArtifactCleaner() {
|
||||
r = r.replace(/\u0163/g, "ț");
|
||||
// Remove zero-width and invisible chars
|
||||
r = r.replace(/[\u200b\u200c\u200d\ufeff]/g, "");
|
||||
// Remove emoji
|
||||
r = r.replace(/\p{Extended_Pictographic}/gu, "");
|
||||
r = r.replace(/[\u{1F1E0}-\u{1F1FF}]/gu, ""); // flag emoji
|
||||
r = r.replace(/[\u{FE00}-\u{FE0F}\u{20D0}-\u{20FF}]/gu, ""); // variation selectors
|
||||
// Normalize typography
|
||||
r = r.replace(/[""]/g, '"');
|
||||
r = r.replace(/['']/g, "'");
|
||||
@@ -413,9 +417,9 @@ function AiArtifactCleaner() {
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Operații: eliminare markdown (###, **, `, liste, citate), corectare
|
||||
encoding românesc (mojibake), curățare Unicode invizibil, normalizare
|
||||
ghilimele / cratime / spații multiple.
|
||||
Operații: eliminare markdown (###, **, `, liste, citate), emoji,
|
||||
corectare encoding românesc (mojibake), curățare Unicode invizibil,
|
||||
normalizare ghilimele / cratime / spații multiple.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -574,33 +578,139 @@ function PdfReducer() {
|
||||
// ─── Quick OCR ────────────────────────────────────────────────────────────────
|
||||
|
||||
function QuickOcr() {
|
||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||
const [text, setText] = useState("");
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [lang, setLang] = useState("ron+eng");
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const runOcr = async (src: string) => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setText("");
|
||||
setProgress(0);
|
||||
try {
|
||||
const { createWorker } = await import("tesseract.js");
|
||||
const worker = await createWorker(lang.split("+"), 1, {
|
||||
logger: (m: { status: string; progress: number }) => {
|
||||
if (m.status === "recognizing text")
|
||||
setProgress(Math.round(m.progress * 100));
|
||||
},
|
||||
});
|
||||
const { data } = await worker.recognize(src);
|
||||
setText(data.text.trim());
|
||||
await worker.terminate();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Eroare OCR necunoscută");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const src = e.target?.result as string;
|
||||
setImageSrc(src);
|
||||
runOcr(src);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
const file = Array.from(e.dataTransfer.files).find((f) =>
|
||||
f.type.startsWith("image/"),
|
||||
);
|
||||
if (file) handleFile(file);
|
||||
};
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
const item = Array.from(e.clipboardData.items).find((i) =>
|
||||
i.type.startsWith("image/"),
|
||||
);
|
||||
const file = item?.getAsFile();
|
||||
if (file) handleFile(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||
<a
|
||||
href="https://ocr.z.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline underline-offset-2"
|
||||
<div className="space-y-3" onPaste={handlePaste}>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<select
|
||||
value={lang}
|
||||
onChange={(e) => setLang(e.target.value)}
|
||||
className="rounded-md border bg-background px-3 py-1.5 text-sm"
|
||||
>
|
||||
Deschide ocr.z.ai ↗
|
||||
</a>
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
Extragere text din imagini și PDF-uri scanate
|
||||
<option value="ron+eng">Română + Engleză</option>
|
||||
<option value="ron">Română</option>
|
||||
<option value="eng">Engleză</option>
|
||||
</select>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
sau Ctrl+V pentru a lipi imaginea
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="overflow-hidden rounded-md border"
|
||||
style={{ height: "560px" }}
|
||||
className="flex min-h-[120px] cursor-pointer items-center justify-center rounded-md border-2 border-dashed p-4 text-sm text-muted-foreground transition-colors hover:border-primary/50"
|
||||
onClick={() => fileRef.current?.click()}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
<iframe
|
||||
src="https://ocr.z.ai"
|
||||
className="h-full w-full"
|
||||
title="OCR — extragere text din imagini"
|
||||
allow="fullscreen"
|
||||
/>
|
||||
{imageSrc ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt="preview"
|
||||
className="max-h-48 max-w-full rounded object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span>Trage o imagine aici, apasă pentru a selecta, sau Ctrl+V</span>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) handleFile(f);
|
||||
}}
|
||||
/>
|
||||
|
||||
{loading && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Se procesează... (primul rulaj descarcă modelul ~10 MB)</span>
|
||||
<span>{progress}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full bg-primary transition-all"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
|
||||
{text && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Text extras</Label>
|
||||
<CopyButton text={text} />
|
||||
</div>
|
||||
<Textarea
|
||||
value={text}
|
||||
readOnly
|
||||
className="mt-1 h-56 font-mono text-xs bg-muted/30"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user