Türkçe çeviri özelliği eklendi (GLM 4.6 ile çeviri yapılıyor)
This commit is contained in:
@@ -14,6 +14,9 @@ import { createEpubFromOcr } from '../utils/epubUtils';
|
||||
const EpubStep = () => {
|
||||
const navigate = useNavigate();
|
||||
const ocrText = useAppStore((state) => state.ocrText);
|
||||
const translatedText = useAppStore((state) => state.translatedText);
|
||||
const translationStatus = useAppStore((state) => state.translationStatus);
|
||||
const translationError = useAppStore((state) => state.translationError);
|
||||
const generatedEpub = useAppStore((state) => state.generatedEpub);
|
||||
const setGeneratedEpub = useAppStore((state) => state.setGeneratedEpub);
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
@@ -21,14 +24,16 @@ const EpubStep = () => {
|
||||
const croppedCoverImage = useAppStore((state) => state.croppedCoverImage);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const needsCoverCrop = Boolean(coverImageId && !croppedCoverImage);
|
||||
const translationBlocking = translationStatus === 'running';
|
||||
const exportText = translatedText?.trim() || ocrText;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const run = async () => {
|
||||
if (!ocrText?.trim() || generatedEpub || needsCoverCrop) return;
|
||||
if (!exportText?.trim() || generatedEpub || needsCoverCrop || translationBlocking) return;
|
||||
setProcessing(true);
|
||||
try {
|
||||
const epub = await createEpubFromOcr(ocrText, croppedCoverImage);
|
||||
const epub = await createEpubFromOcr(exportText, croppedCoverImage);
|
||||
if (!cancelled) {
|
||||
setGeneratedEpub(epub);
|
||||
}
|
||||
@@ -46,7 +51,15 @@ const EpubStep = () => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [croppedCoverImage, generatedEpub, needsCoverCrop, ocrText, setError, setGeneratedEpub]);
|
||||
}, [
|
||||
croppedCoverImage,
|
||||
exportText,
|
||||
generatedEpub,
|
||||
needsCoverCrop,
|
||||
setError,
|
||||
setGeneratedEpub,
|
||||
translationBlocking,
|
||||
]);
|
||||
|
||||
if (!ocrText?.trim()) {
|
||||
return (
|
||||
@@ -67,6 +80,23 @@ const EpubStep = () => {
|
||||
OCR sonucundaki tüm metinleri tek bir EPUB dosyasında topluyoruz.
|
||||
</Typography>
|
||||
</Box>
|
||||
{translationBlocking && (
|
||||
<Alert severity="info">
|
||||
Türkçe çeviri tamamlanana kadar EPUB üretimi bekletiliyor. Lütfen çeviri ekranındaki işlemin bitmesini
|
||||
bekle.
|
||||
</Alert>
|
||||
)}
|
||||
{translationStatus === 'done' && translatedText && (
|
||||
<Alert severity="success">
|
||||
Çeviri tamamlandı. EPUB, Türkçe metinle oluşturuluyor.
|
||||
</Alert>
|
||||
)}
|
||||
{translationStatus === 'error' && (
|
||||
<Alert severity="warning">
|
||||
Çeviri sırasında bir sorun oluştu. EPUB üretiminde orijinal OCR metni kullanılacak.
|
||||
{translationError ? ` (${translationError})` : ''}
|
||||
</Alert>
|
||||
)}
|
||||
{needsCoverCrop && (
|
||||
<Alert severity="warning">
|
||||
Kapak olarak işaretlediğin görseli croplamalısın. Crop adımında kapak görselini kaydet ve tekrar dene.
|
||||
@@ -108,7 +138,7 @@ const EpubStep = () => {
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!generatedEpub || processing || needsCoverCrop}
|
||||
disabled={!generatedEpub || processing || needsCoverCrop || translationBlocking}
|
||||
onClick={() => navigate('/download')}
|
||||
>
|
||||
EPUB'i indir
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import Tesseract from 'tesseract.js';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { correctTurkishCharacters } from '../utils/ocrUtils';
|
||||
import { translateChunkToTurkish } from '../utils/translationUtils';
|
||||
|
||||
const OcrStep = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -19,7 +20,17 @@ const OcrStep = () => {
|
||||
const ocrText = useAppStore((state) => state.ocrText);
|
||||
const setOcrText = useAppStore((state) => state.setOcrText);
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
const translatedText = useAppStore((state) => state.translatedText);
|
||||
const translationStatus = useAppStore((state) => state.translationStatus);
|
||||
const translationError = useAppStore((state) => state.translationError);
|
||||
const translationProgress = useAppStore((state) => state.translationProgress);
|
||||
const setTranslatedText = useAppStore((state) => state.setTranslatedText);
|
||||
const setTranslationStatus = useAppStore((state) => state.setTranslationStatus);
|
||||
const setTranslationError = useAppStore((state) => state.setTranslationError);
|
||||
const setTranslationProgress = useAppStore((state) => state.setTranslationProgress);
|
||||
const clearTranslation = useAppStore((state) => state.clearTranslation);
|
||||
const [status, setStatus] = useState('idle');
|
||||
const [translationTrigger, setTranslationTrigger] = useState(0);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [previewText, setPreviewText] = useState('');
|
||||
const total = croppedImages.length;
|
||||
@@ -36,6 +47,7 @@ const OcrStep = () => {
|
||||
const workerRef = useRef(null);
|
||||
const [workerReady, setWorkerReady] = useState(false);
|
||||
const previewRef = useRef(null);
|
||||
const translationPreviewRef = useRef(null);
|
||||
|
||||
const orderedImages = useMemo(
|
||||
() => [...croppedImages].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
||||
@@ -120,13 +132,19 @@ const OcrStep = () => {
|
||||
setCurrentIndex(0);
|
||||
setPreviewText('');
|
||||
setOcrText('');
|
||||
}, [orderedImages, setOcrText]);
|
||||
clearTranslation();
|
||||
}, [clearTranslation, orderedImages, setOcrText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewRef.current) {
|
||||
previewRef.current.scrollTop = previewRef.current.scrollHeight;
|
||||
}
|
||||
}, [previewText]);
|
||||
useEffect(() => {
|
||||
if (translationPreviewRef.current) {
|
||||
translationPreviewRef.current.scrollTop = translationPreviewRef.current.scrollHeight;
|
||||
}
|
||||
}, [translatedText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!total || status === 'done' || !workerReady) return;
|
||||
@@ -171,6 +189,59 @@ const OcrStep = () => {
|
||||
};
|
||||
}, [orderedImages, setError, setOcrText, status, total, workerReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'done') return;
|
||||
if (!ocrText?.trim()) return;
|
||||
if (translationStatus === 'running' || translationStatus === 'done') return;
|
||||
|
||||
let cancelled = false;
|
||||
const sections = segmentOcrText(ocrText);
|
||||
if (!sections.length) return;
|
||||
|
||||
const runTranslation = async () => {
|
||||
setTranslationStatus('running');
|
||||
setTranslationError(null);
|
||||
setTranslationProgress(0);
|
||||
setTranslatedText('');
|
||||
try {
|
||||
const translatedChunks = [];
|
||||
for (let index = 0; index < sections.length; index += 1) {
|
||||
if (cancelled) return;
|
||||
const chunk = sections[index];
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const translated = await translateChunkToTurkish(chunk);
|
||||
if (cancelled) return;
|
||||
translatedChunks[index] = translated;
|
||||
const combined = translatedChunks.filter(Boolean).join('\n\n');
|
||||
setTranslatedText(combined);
|
||||
setTranslationProgress(Math.round(((index + 1) / sections.length) * 100));
|
||||
}
|
||||
if (!cancelled) {
|
||||
setTranslationStatus('done');
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setTranslationStatus('error');
|
||||
setTranslationError(error.message || 'Çeviri tamamlanamadı.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
runTranslation();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
ocrText,
|
||||
setTranslatedText,
|
||||
setTranslationError,
|
||||
setTranslationProgress,
|
||||
setTranslationStatus,
|
||||
status,
|
||||
translationTrigger,
|
||||
]);
|
||||
|
||||
if (!orderedImages.length) {
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
@@ -228,6 +299,69 @@ const OcrStep = () => {
|
||||
{previewText || 'Metin bekleniyor'}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, borderRadius: 2, bgcolor: 'background.default' }}>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} alignItems="flex-start" justifyContent="space-between">
|
||||
<Box>
|
||||
<Typography variant="subtitle1">Türkçe çeviriler</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
OCR metni parçalara ayrılıp GLM 4.6 ile çevriliyor.
|
||||
</Typography>
|
||||
</Box>
|
||||
{translationStatus === 'error' && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
clearTranslation();
|
||||
setTranslationTrigger((prev) => prev + 1);
|
||||
}}
|
||||
>
|
||||
Tekrar dene
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
{translationStatus === 'running' && (
|
||||
<Box mt={2}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={translationProgress}
|
||||
sx={{ height: 8, borderRadius: 3 }}
|
||||
/>
|
||||
<Typography mt={1} color="text.secondary" variant="caption">
|
||||
%{translationProgress} tamamlandı
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{translationStatus === 'done' && translatedText && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
Çeviri tamamlandı. EPUB üretiminde Türkçe içerik kullanılacak.
|
||||
</Alert>
|
||||
)}
|
||||
{translationStatus === 'error' && translationError && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{translationError}
|
||||
</Alert>
|
||||
)}
|
||||
<Box
|
||||
ref={translationPreviewRef}
|
||||
sx={{
|
||||
mt: 2,
|
||||
maxHeight: '8.5em',
|
||||
overflowY: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: 1.5,
|
||||
fontSize: '0.95rem',
|
||||
color: 'text.secondary',
|
||||
pr: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1.5,
|
||||
p: 1.5,
|
||||
}}
|
||||
>
|
||||
{translatedText || 'Çeviri bekleniyor...'}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} justifyContent="space-between">
|
||||
<Button variant="contained" onClick={() => navigate('/bulk-crop')}>
|
||||
@@ -236,7 +370,7 @@ const OcrStep = () => {
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate('/epub')}
|
||||
disabled={!hasResults}
|
||||
disabled={!hasResults || translationStatus === 'running'}
|
||||
>
|
||||
EPUB oluştur
|
||||
</Button>
|
||||
@@ -246,3 +380,37 @@ const OcrStep = () => {
|
||||
};
|
||||
|
||||
export default OcrStep;
|
||||
|
||||
const MAX_CHUNK_LENGTH = 800;
|
||||
|
||||
const segmentOcrText = (text) => {
|
||||
if (!text) return [];
|
||||
const normalized = text.replace(/\r\n/g, '\n');
|
||||
const paragraphs = normalized.split(/\n{2,}/).map((part) => part.trim()).filter(Boolean);
|
||||
const chunks = [];
|
||||
|
||||
paragraphs.forEach((paragraph) => {
|
||||
if (paragraph.length <= MAX_CHUNK_LENGTH) {
|
||||
chunks.push(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
let remaining = paragraph;
|
||||
while (remaining.length > MAX_CHUNK_LENGTH) {
|
||||
let sliceIndex = remaining.lastIndexOf(' ', MAX_CHUNK_LENGTH);
|
||||
if (sliceIndex === -1 || sliceIndex < MAX_CHUNK_LENGTH * 0.6) {
|
||||
sliceIndex = MAX_CHUNK_LENGTH;
|
||||
}
|
||||
const chunk = remaining.slice(0, sliceIndex).trim();
|
||||
if (chunk) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
remaining = remaining.slice(sliceIndex).trim();
|
||||
}
|
||||
if (remaining.length) {
|
||||
chunks.push(remaining);
|
||||
}
|
||||
});
|
||||
|
||||
return chunks;
|
||||
};
|
||||
|
||||
@@ -94,7 +94,7 @@ const UploadStep = () => {
|
||||
{uploadedImages.map((image) => (
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={image.id}>
|
||||
<Card>
|
||||
<CardActionArea>
|
||||
<CardActionArea component="div">
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="160"
|
||||
|
||||
@@ -44,6 +44,10 @@ export const useAppStore = create((set) => ({
|
||||
coverCropConfig: createEmptyCropConfig(),
|
||||
croppedCoverImage: null,
|
||||
ocrText: '',
|
||||
translatedText: '',
|
||||
translationStatus: 'idle',
|
||||
translationError: null,
|
||||
translationProgress: 0,
|
||||
generatedEpub: null,
|
||||
authToken: null,
|
||||
currentUser: null,
|
||||
@@ -68,6 +72,17 @@ export const useAppStore = create((set) => ({
|
||||
return { croppedCoverImage: image };
|
||||
}),
|
||||
setOcrText: (text) => set({ ocrText: text }),
|
||||
setTranslatedText: (text) => set({ translatedText: text }),
|
||||
setTranslationStatus: (status) => set({ translationStatus: status }),
|
||||
setTranslationError: (message) => set({ translationError: message }),
|
||||
setTranslationProgress: (value) => set({ translationProgress: value }),
|
||||
clearTranslation: () =>
|
||||
set({
|
||||
translatedText: '',
|
||||
translationStatus: 'idle',
|
||||
translationError: null,
|
||||
translationProgress: 0,
|
||||
}),
|
||||
setGeneratedEpub: (epub) =>
|
||||
set((state) => {
|
||||
if (state.generatedEpub?.url) {
|
||||
@@ -132,6 +147,10 @@ export const useAppStore = create((set) => ({
|
||||
draft.coverCropConfig = createEmptyCropConfig();
|
||||
draft.croppedCoverImage = null;
|
||||
draft.ocrText = '';
|
||||
draft.translatedText = '';
|
||||
draft.translationStatus = 'idle';
|
||||
draft.translationError = null;
|
||||
draft.translationProgress = 0;
|
||||
if (state.generatedEpub?.url) {
|
||||
URL.revokeObjectURL(state.generatedEpub.url);
|
||||
}
|
||||
@@ -141,6 +160,10 @@ export const useAppStore = create((set) => ({
|
||||
state.croppedImages.forEach((img) => img.url && URL.revokeObjectURL(img.url));
|
||||
draft.croppedImages = [];
|
||||
draft.ocrText = '';
|
||||
draft.translatedText = '';
|
||||
draft.translationStatus = 'idle';
|
||||
draft.translationError = null;
|
||||
draft.translationProgress = 0;
|
||||
if (state.generatedEpub?.url) {
|
||||
URL.revokeObjectURL(state.generatedEpub.url);
|
||||
}
|
||||
@@ -148,6 +171,10 @@ export const useAppStore = create((set) => ({
|
||||
}
|
||||
if (step === 'ocr') {
|
||||
draft.ocrText = '';
|
||||
draft.translatedText = '';
|
||||
draft.translationStatus = 'idle';
|
||||
draft.translationError = null;
|
||||
draft.translationProgress = 0;
|
||||
if (state.generatedEpub?.url) {
|
||||
URL.revokeObjectURL(state.generatedEpub.url);
|
||||
}
|
||||
|
||||
25
src/utils/translationUtils.js
Normal file
25
src/utils/translationUtils.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:4000';
|
||||
|
||||
export const translateChunkToTurkish = async (text) => {
|
||||
if (!text?.trim()) {
|
||||
throw new Error('Çevrilecek metin bulunamadı.');
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}/translate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const message = await response.text();
|
||||
throw new Error(message || 'Çeviri isteği başarısız oldu.');
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!payload?.text) {
|
||||
throw new Error('Çeviri yanıtı boş döndü.');
|
||||
}
|
||||
|
||||
return payload.text.trim();
|
||||
};
|
||||
Reference in New Issue
Block a user