Expose document text and OCR page provenance for review
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
document-mixed.pdf — okunan metin
|
||||
|
||||
2/2 sayfa okundu. OCR kullanılan sayfalar: 2. Metni resmi dokümanla karşılaştırın; okunmuş olması doğruluğunun onaylandığı anlamına gelmez.
|
||||
|
||||
SYNTHETIC MIXED PDF - QA ONLY - NOT AN OFFICIAL DOCUMENT
|
||||
Reference 2026/1582991. This page has a native text layer.
|
||||
The following page is an image and requires OCR processing.
|
||||
This fixture contains no actual procurement requirements or prices.
|
||||
|
||||
SENTETİK OCR TESTİ
|
||||
|
||||
İhale Kayıt Numarası: 2026/1582991
|
||||
Laboratuvar ürünleri iXlin örnek belge.
|
||||
Bu sayfa yalnız görüntü iDlerir.
|
||||
|
||||
Resmi ihale dokümanı değildir.
|
||||
Metin önizlemesini kapat
|
||||
@@ -335,3 +335,11 @@ Varsayılan 40 metin sayfası ve 8 OCR sayfası sınırı kaldırıldı; tüm sa
|
||||
76 test ve üretim derlemesi geçti. Testler 45 sayfalık gerçek PDF baytlarını pdfjs ile okuyarak son sayfa maddesinin korunduğunu; karma sayfa sırasını ve OCR hatasının başarısızlık olarak iletildiğini doğruluyor. Gerçek PDF testi kütüphanede DocumentProxy.destroy bulunmadığını ortaya çıkardı; kapatma LoadingTask.destroy üzerinden düzeltildi.
|
||||
|
||||
Sınırlar: gerçek tarayıcıda uzun/taranmış karma PDF performansı ve OCR doğruluğu kabulü açık. Sayfa okunmuş olması OCR metninin anlamsal doğruluğunu kanıtlamaz. OCR varsayılanı metni 100 karakterden kısa sayfalarda çalışır; küçük metinli sayfalarda ek işlem maliyeti oluşabilir. Resmi dosya ve beş ihale kabulü tamamlanmadı.
|
||||
|
||||
### Karma PDF tarayıcı OCR kabulü ve kaynak önizlemesi
|
||||
|
||||
Test dosyası document-mixed.pdf bir yerel metin sayfası ve Türkçe metin içeren görüntü sayfasından oluşuyor. Gerçek in-app tarayıcıda yükleme tamamlandı, kayıt korundu ve cetvel bulunmadığı gösterildi. Yeni “Metni incele” işlemi kaydedilmiş dosyayı okuyor; toplam/okunan sayfa sayısı, OCR uygulanan sayfalar ve tam çıkarılan metni AI çağrısı yapmadan gösteriyor. Önizleme ihale değişiminde temizleniyor; geç gelen başka ihale/oturum yanıtı uygulanmıyor.
|
||||
|
||||
Tarayıcı kanıtı: evidence/qa-mixed-pdf-ocr.txt. 2/2 sayfa okundu, yalnız sayfa 2 OCR olarak işaretlendi. İKN doğru okundu; “için” ve “içerir” sözcüklerinde OCR hataları gözlendi. Dolayısıyla bu test OCR çalışmasını ve sayfa kapsamını doğrular, kusursuz metin veya resmi kalem doğruluğunu doğrulamaz. Önizlemede resmi dokümanla karşılaştırma uyarısı var. OCR doğruluğunu iyileştirme, insan kontrolünün sonraki hazırlık adımlarına zorunlu aktarımı ve uzun tarama performansı açık.
|
||||
|
||||
76 test ve üretim derlemesi geçti. Test dosyası sentetiktir; beş resmi ihale kabulü yerine geçmez.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readableDocument, readStoredDocument, retryDocumentExtraction } from '../services/storedDocument';
|
||||
import { readableDocument, readStoredDocument, inspectStoredDocument, retryDocumentExtraction } from '../services/storedDocument';
|
||||
import { documentProcessingLabel } from '../services/documentProcessing';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
@@ -18,6 +18,7 @@ export const TenderDocuments = ({ tender }) => {
|
||||
const [error, setError] = useState(null);
|
||||
const [bilgi, setBilgi] = useState(null);
|
||||
const [ilerleme, setIlerleme] = useState('');
|
||||
const [metinOnizleme, setMetinOnizleme] = useState(null);
|
||||
// Son üretilen EKAP doküman adresi — yeni sekme engellenirse kullanıcı
|
||||
// gerçek bir bağlantıya tıklayarak açabilsin (bağlantı tıklaması engellenmez).
|
||||
const [ekapUrl, setEkapUrl] = useState(null);
|
||||
@@ -30,6 +31,7 @@ export const TenderDocuments = ({ tender }) => {
|
||||
const [listeHata, setListeHata] = useState(null);
|
||||
const [listeBekliyor, setListeBekliyor] = useState(false);
|
||||
const listeIstegi = useRef(0);
|
||||
const onizlemeIstegi = useRef(0);
|
||||
const dosyaRef = useRef(null);
|
||||
|
||||
const belgeleriYukle = useCallback(async () => {
|
||||
@@ -45,7 +47,7 @@ export const TenderDocuments = ({ tender }) => {
|
||||
if (request === listeIstegi.current) setListeBekliyor(false);
|
||||
}
|
||||
}, [ikn]);
|
||||
useEffect(() => { setBelgeler([]); belgeleriYukle(); return () => { listeIstegi.current++; }; }, [belgeleriYukle]);
|
||||
useEffect(() => { setBelgeler([]); setMetinOnizleme(null); belgeleriYukle(); return () => { listeIstegi.current++; onizlemeIstegi.current++; }; }, [belgeleriYukle]);
|
||||
|
||||
// EKAP dokümanı kullanıcının tarayıcısına iniyor; indirdiği dosyayı buraya
|
||||
// yükleyince ihaleye bağlanır ve metni AI analizine verilebilir.
|
||||
@@ -92,6 +94,18 @@ export const TenderDocuments = ({ tender }) => {
|
||||
const okunabilirMi = readableDocument;
|
||||
const belgeMetni = readStoredDocument;
|
||||
|
||||
const metniIncele = async (b) => {
|
||||
setBusy('metin'); setError(null); setMetinOnizleme(null);
|
||||
const token = getToken();
|
||||
const request = ++onizlemeIstegi.current;
|
||||
try {
|
||||
const result = await inspectStoredDocument(b);
|
||||
if (token !== getToken() || request !== onizlemeIstegi.current) return;
|
||||
setMetinOnizleme({...result,name:b.ad});
|
||||
} catch (e) { if (token === getToken() && request === onizlemeIstegi.current) setError(e.message); }
|
||||
finally { if (token === getToken() && request === onizlemeIstegi.current) setBusy(null); }
|
||||
};
|
||||
|
||||
const kalemleriYenidenCikar = async (b) => {
|
||||
setBusy('kalem'); setError(null); setBilgi(null);
|
||||
try {
|
||||
@@ -373,6 +387,8 @@ export const TenderDocuments = ({ tender }) => {
|
||||
>
|
||||
Derin
|
||||
</button>
|
||||
{okunabilirMi(b.ad) && <button onClick={() => metniIncele(b)} disabled={busy !== null || !b.erisilebilir}
|
||||
aria-label={`${b.ad} metnini incele`} className="text-[10px] text-blue-700 disabled:opacity-40">Metni incele</button>}
|
||||
{b.tur === 'ihale' && okunabilirMi(b.ad) && (
|
||||
<button onClick={() => kalemleriYenidenCikar(b)} disabled={busy !== null || yukleniyor || !b.erisilebilir}
|
||||
aria-label={`${b.ad} için kalem çıkarımını yeniden dene`}
|
||||
@@ -388,6 +404,17 @@ export const TenderDocuments = ({ tender }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{metinOnizleme && <section aria-label="Okunan belge metni" className="border rounded-xl p-3 space-y-2">
|
||||
<h5 className="font-bold text-xs">{metinOnizleme.name} — okunan metin</h5>
|
||||
<p className="text-xs text-slate-600">
|
||||
{metinOnizleme.pageCount != null ? `${metinOnizleme.usedPages}/${metinOnizleme.pageCount} sayfa okundu. ` : 'Sunucuda okunan belge metni. '}
|
||||
{metinOnizleme.ocr ? `OCR kullanılan sayfalar: ${metinOnizleme.pages.filter(p=>p.method === 'ocr').map(p=>p.page).join(', ')}. ` : ''}
|
||||
Metni resmi dokümanla karşılaştırın; okunmuş olması doğruluğunun onaylandığı anlamına gelmez.
|
||||
</p>
|
||||
<pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words text-xs">{metinOnizleme.text || 'Okunabilir metin bulunamadı.'}</pre>
|
||||
<button onClick={()=>setMetinOnizleme(null)} className="text-xs underline">Metin önizlemesini kapat</button>
|
||||
</section>}
|
||||
|
||||
{(ekapIhalesi || belgeler.length > 0) && (
|
||||
<div className="bg-linear-to-r from-slate-900 via-indigo-950 to-slate-900 rounded-xl p-4 space-y-2.5">
|
||||
<div className="flex items-start gap-2.5">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { reportDocumentReadError } from './account.js';
|
||||
|
||||
export const readableDocument = (name) => /\.(pdf|txt|md|docx|xlsx|zip)$/i.test(String(name || ''));
|
||||
|
||||
export async function readStoredDocument(document) {
|
||||
export async function inspectStoredDocument(document) {
|
||||
if (!readableDocument(document.ad)) throw new Error('Bu dosya biçiminden metin çıkarımı desteklenmiyor.');
|
||||
const office = /\.(docx|xlsx|zip)$/i.test(document.ad);
|
||||
const response = await fetch(`/api/me/documents/${office ? 'text' : 'download'}?id=${encodeURIComponent(document.id)}`, {
|
||||
@@ -12,14 +12,16 @@ export async function readStoredDocument(document) {
|
||||
if (office) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !result.success) throw new Error(result.error || 'Belge metni alınamadı.');
|
||||
return result.metin;
|
||||
return {text:result.metin,format:document.ad.split('.').pop()};
|
||||
}
|
||||
if (!response.ok) throw new Error('Belge indirilemedi.');
|
||||
const blob = await response.blob();
|
||||
const { extractText } = await import('./pdfText');
|
||||
return (await extractText(new File([blob],document.ad,{type:document.mime || 'application/octet-stream'}))).text;
|
||||
return extractText(new File([blob],document.ad,{type:document.mime || 'application/octet-stream'}));
|
||||
}
|
||||
|
||||
export async function readStoredDocument(document) { return (await inspectStoredDocument(document)).text; }
|
||||
|
||||
export async function retryDocumentExtraction(document) {
|
||||
if (document.tur !== 'ihale' || !document.ikn) throw new Error('İhaleye bağlı ihale dokümanı gerekli.');
|
||||
const token = getToken();
|
||||
|
||||
Vendored
+282
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user