Metadata ve çeviri ile ilgili düzeltmeler. UI değişiklikleri.
This commit is contained in:
@@ -25,6 +25,7 @@ export const wizardSteps = [
|
||||
{ label: 'Crop', path: '/crop' },
|
||||
{ label: 'Toplu Crop', path: '/bulk-crop' },
|
||||
{ label: 'OCR', path: '/ocr' },
|
||||
{ label: 'Çeviri', path: '/translate' },
|
||||
{ label: 'EPUB Oluştur', path: '/epub' },
|
||||
{ label: 'İndir', path: '/download' },
|
||||
];
|
||||
@@ -142,11 +143,16 @@ const App = () => {
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" py={1.5}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
onClick={() => {
|
||||
resetFromStep('upload');
|
||||
navigate('/');
|
||||
}}
|
||||
sx={{
|
||||
fontFamily: '"Caudex", serif',
|
||||
color: '#1C1815',
|
||||
fontWeight: 700,
|
||||
letterSpacing: 1,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
imagepub
|
||||
|
||||
@@ -19,6 +19,7 @@ const BulkCropStep = () => {
|
||||
const setCroppedImages = useAppStore((state) => state.setCroppedImages);
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
const croppedImages = useAppStore((state) => state.croppedImages);
|
||||
const bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
const targetImages = useMemo(
|
||||
@@ -90,6 +91,12 @@ const BulkCropStep = () => {
|
||||
if (!targetImages.length) {
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
{bookMetadata && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
Seçilen kitap: <strong>{bookMetadata.title}</strong>
|
||||
{bookMetadata.authors?.length ? ` • ${bookMetadata.authors.join(', ')}` : ''}
|
||||
</Typography>
|
||||
)}
|
||||
<Alert severity="info">Kapak dışında crop uygulanacak görsel bulunmuyor. Bu adımı geçebilirsin.</Alert>
|
||||
<Button variant="contained" onClick={() => navigate('/ocr')}>
|
||||
OCR'ye geç
|
||||
@@ -100,6 +107,12 @@ const BulkCropStep = () => {
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
{bookMetadata && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
Seçilen kitap: <strong>{bookMetadata.title}</strong>
|
||||
{bookMetadata.authors?.length ? ` • ${bookMetadata.authors.join(', ')}` : ''}
|
||||
</Typography>
|
||||
)}
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Toplu crop işlemi
|
||||
|
||||
@@ -41,6 +41,7 @@ const CropStep = () => {
|
||||
const coverCropConfig = useAppStore((state) => state.coverCropConfig);
|
||||
const updateCropConfig = useAppStore((state) => state.updateCropConfig);
|
||||
const updateCoverCropConfig = useAppStore((state) => state.updateCoverCropConfig);
|
||||
const bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||
const setCroppedCoverImage = useAppStore((state) => state.setCroppedCoverImage);
|
||||
const resetFromStep = useAppStore((state) => state.resetFromStep);
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
@@ -290,6 +291,12 @@ const CropStep = () => {
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
{bookMetadata && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
Seçilen kitap: <strong>{bookMetadata.title}</strong>
|
||||
{bookMetadata.authors?.length ? ` • ${bookMetadata.authors.join(', ')}` : ''}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="h6">Referans görseli seç</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{uploadedImages.map((image) => (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
@@ -22,10 +23,34 @@ const EpubStep = () => {
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
const coverImageId = useAppStore((state) => state.coverImageId);
|
||||
const croppedCoverImage = useAppStore((state) => state.croppedCoverImage);
|
||||
const bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||
const bookTitle = useAppStore((state) => state.bookTitle);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const needsCoverCrop = Boolean(coverImageId && !croppedCoverImage);
|
||||
const translationBlocking = translationStatus === 'running';
|
||||
const exportText = translatedText?.trim() || ocrText;
|
||||
const metaForEpub = useMemo(() => {
|
||||
const fallbackTitle = bookTitle?.trim() || 'imgPub OCR Export';
|
||||
if (!bookMetadata) {
|
||||
return { title: fallbackTitle };
|
||||
}
|
||||
return {
|
||||
title: bookMetadata.title || fallbackTitle,
|
||||
subtitle: bookMetadata.subtitle,
|
||||
authors: bookMetadata.authors,
|
||||
publisher: bookMetadata.publisher,
|
||||
language: bookMetadata.language,
|
||||
description: bookMetadata.description,
|
||||
categories: bookMetadata.categories,
|
||||
publishedDate: bookMetadata.publishedDate,
|
||||
pageCount: bookMetadata.pageCount,
|
||||
infoLink: bookMetadata.infoLink,
|
||||
averageRating: bookMetadata.averageRating,
|
||||
ratingsCount: bookMetadata.ratingsCount,
|
||||
identifiers: bookMetadata.identifiers,
|
||||
filename: bookMetadata.filename,
|
||||
};
|
||||
}, [bookMetadata, bookTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -33,7 +58,7 @@ const EpubStep = () => {
|
||||
if (!exportText?.trim() || generatedEpub || needsCoverCrop || translationBlocking) return;
|
||||
setProcessing(true);
|
||||
try {
|
||||
const epub = await createEpubFromOcr(exportText, croppedCoverImage);
|
||||
const epub = await createEpubFromOcr(exportText, croppedCoverImage, metaForEpub);
|
||||
if (!cancelled) {
|
||||
setGeneratedEpub(epub);
|
||||
}
|
||||
@@ -52,6 +77,7 @@ const EpubStep = () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
metaForEpub,
|
||||
croppedCoverImage,
|
||||
exportText,
|
||||
generatedEpub,
|
||||
@@ -79,6 +105,73 @@ const EpubStep = () => {
|
||||
<Typography color="text.secondary">
|
||||
OCR sonucundaki tüm metinleri tek bir EPUB dosyasında topluyoruz.
|
||||
</Typography>
|
||||
{bookMetadata && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 3,
|
||||
mx: 'auto',
|
||||
maxWidth: 480,
|
||||
p: 2,
|
||||
borderRadius: 1.2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 72,
|
||||
height: 108,
|
||||
borderRadius: 0.3,
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
bgcolor: '#f0ece4',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{bookMetadata.thumbnail ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={bookMetadata.thumbnail}
|
||||
alt={`${bookMetadata.title} kapak görseli`}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
) : croppedCoverImage ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={croppedCoverImage.url}
|
||||
alt={`${bookMetadata.title} kapak görseli`}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
<Stack alignItems="center" justifyContent="center" sx={{ height: '100%' }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Kapak yok
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
{bookMetadata.title}
|
||||
</Typography>
|
||||
{bookMetadata.authors?.length > 0 && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
|
||||
{bookMetadata.authors.join(', ')}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="caption" color="text.secondary" display="block" mt={1}>
|
||||
{[bookMetadata.publisher, bookMetadata.publishedDate, bookMetadata.pageCount ? `${bookMetadata.pageCount} sayfa` : null]
|
||||
.filter(Boolean)
|
||||
.join(' • ')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{translationBlocking && (
|
||||
<Alert severity="info">
|
||||
@@ -108,29 +201,30 @@ const EpubStep = () => {
|
||||
EPUB hazır: {generatedEpub.filename}
|
||||
</Alert>
|
||||
)}
|
||||
{croppedCoverImage ? (
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="subtitle1">Kapak önizlemesi</Typography>
|
||||
<Box
|
||||
component="img"
|
||||
src={croppedCoverImage.url}
|
||||
alt="Epub kapak görseli"
|
||||
sx={{
|
||||
mt: 2,
|
||||
maxHeight: 260,
|
||||
width: 'auto',
|
||||
borderRadius: 2,
|
||||
boxShadow: 3,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
coverImageId && (
|
||||
<Alert severity="info">
|
||||
Kapak seçili ancak crop işlemi tamamlanmadı. Crop adımına dönerek kapak kesimini belirle.
|
||||
</Alert>
|
||||
)
|
||||
)}
|
||||
{!bookMetadata &&
|
||||
(croppedCoverImage ? (
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="subtitle1">Kapak önizlemesi</Typography>
|
||||
<Box
|
||||
component="img"
|
||||
src={croppedCoverImage.url}
|
||||
alt="Epub kapak görseli"
|
||||
sx={{
|
||||
mt: 2,
|
||||
maxHeight: 260,
|
||||
width: 'auto',
|
||||
borderRadius: 2,
|
||||
boxShadow: 3,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
coverImageId && (
|
||||
<Alert severity="info">
|
||||
Kapak seçili ancak crop işlemi tamamlanmadı. Crop adımına dönerek kapak kesimini belirle.
|
||||
</Alert>
|
||||
)
|
||||
))}
|
||||
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} justifyContent="space-between">
|
||||
<Button variant="contained" onClick={() => navigate('/ocr')}>
|
||||
|
||||
@@ -11,7 +11,6 @@ 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();
|
||||
@@ -20,21 +19,12 @@ 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 bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||
const [status, setStatus] = useState('idle');
|
||||
const [translationTrigger, setTranslationTrigger] = useState(0);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [previewText, setPreviewText] = useState('');
|
||||
const total = croppedImages.length;
|
||||
const hasResults = useMemo(() => Boolean(ocrText?.length), [ocrText]);
|
||||
const abortRef = useRef(false);
|
||||
|
||||
const assetBase = useMemo(() => {
|
||||
@@ -47,7 +37,7 @@ const OcrStep = () => {
|
||||
const workerRef = useRef(null);
|
||||
const [workerReady, setWorkerReady] = useState(false);
|
||||
const previewRef = useRef(null);
|
||||
const translationPreviewRef = useRef(null);
|
||||
// removed auto navigation to translation
|
||||
|
||||
const orderedImages = useMemo(
|
||||
() => [...croppedImages].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
||||
@@ -140,12 +130,6 @@ const OcrStep = () => {
|
||||
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;
|
||||
abortRef.current = false;
|
||||
@@ -189,58 +173,6 @@ 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 (
|
||||
@@ -264,6 +196,12 @@ const OcrStep = () => {
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
{bookMetadata && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
Seçilen kitap: <strong>{bookMetadata.title}</strong>
|
||||
{bookMetadata.authors?.length ? ` • ${bookMetadata.authors.join(', ')}` : ''}
|
||||
</Typography>
|
||||
)}
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5">OCR işlemi</Typography>
|
||||
<Typography color="text.secondary">
|
||||
@@ -280,99 +218,34 @@ const OcrStep = () => {
|
||||
{progressText}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack spacing={1}>
|
||||
<Box sx={{ p: 2, borderRadius: 2, bgcolor: 'background.default' }}>
|
||||
<Typography variant="subtitle1">Ön izleme</Typography>
|
||||
<Box
|
||||
ref={previewRef}
|
||||
sx={{
|
||||
mt: 1,
|
||||
maxHeight: '8.5em',
|
||||
overflowY: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: 1.5,
|
||||
fontSize: '0.95rem',
|
||||
color: 'text.secondary',
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
{previewText || 'Metin bekleniyor'}
|
||||
</Box>
|
||||
<Box sx={{ p: 2, borderRadius: 2, bgcolor: 'background.default' }}>
|
||||
<Typography variant="subtitle1">Ön izleme</Typography>
|
||||
<Box
|
||||
ref={previewRef}
|
||||
sx={{
|
||||
mt: 1,
|
||||
maxHeight: '8.5em',
|
||||
overflowY: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: 1.5,
|
||||
fontSize: '0.95rem',
|
||||
color: 'text.secondary',
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
{previewText || 'Metin bekleniyor'}
|
||||
</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>
|
||||
</Box>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} justifyContent="space-between">
|
||||
<Button variant="contained" onClick={() => navigate('/bulk-crop')}>
|
||||
Geri dön
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate('/epub')}
|
||||
disabled={!hasResults || translationStatus === 'running'}
|
||||
onClick={() => navigate('/translate')}
|
||||
disabled={status !== 'done'}
|
||||
>
|
||||
EPUB oluştur
|
||||
Çeviri adımına geç
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -380,37 +253,3 @@ 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;
|
||||
};
|
||||
|
||||
196
src/components/TranslationStep.jsx
Normal file
196
src/components/TranslationStep.jsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, Box, Button, LinearProgress, Stack, Typography } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { segmentOcrText, translateChunkToTurkish } from '../utils/translationUtils';
|
||||
|
||||
const TranslationStep = () => {
|
||||
const navigate = useNavigate();
|
||||
const ocrText = useAppStore((state) => state.ocrText);
|
||||
const bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||
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 [trigger, setTrigger] = useState(0);
|
||||
const previewRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewRef.current) {
|
||||
previewRef.current.scrollTop = previewRef.current.scrollHeight;
|
||||
}
|
||||
}, [translatedText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ocrText?.trim()) return;
|
||||
if (!trigger) return;
|
||||
let cancelled = false;
|
||||
const sections = segmentOcrText(ocrText);
|
||||
if (!sections.length) {
|
||||
setTranslationStatus('error');
|
||||
setTranslationError('Çevrilecek metin bulunamadı.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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, trigger]);
|
||||
|
||||
const handleStart = () => {
|
||||
if (!ocrText?.trim()) return;
|
||||
clearTranslation();
|
||||
setTrigger((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
handleStart();
|
||||
};
|
||||
|
||||
const summaryLine = useMemo(() => {
|
||||
if (!translationStatus || translationStatus === 'idle') {
|
||||
return 'OCR çıktısı Türkçe\'ye çevrilmek üzere parçalanıyor.';
|
||||
}
|
||||
if (translationStatus === 'running') {
|
||||
return 'GLM 4.6 API ile çeviri devam ediyor.';
|
||||
}
|
||||
if (translationStatus === 'done') {
|
||||
return 'Çeviri tamamlandı. EPUB adımına geçebilirsin.';
|
||||
}
|
||||
if (translationStatus === 'error') {
|
||||
return 'Çeviri sırasında bir sorun oluştu.';
|
||||
}
|
||||
return '';
|
||||
}, [translationStatus]);
|
||||
|
||||
if (!ocrText?.trim()) {
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Alert severity="info">Çevrilecek metin bulunamadı. OCR adımını tamamla.</Alert>
|
||||
<Button variant="contained" onClick={() => navigate('/ocr')}>
|
||||
OCR adımına dön
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
{bookMetadata && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
Seçilen kitap: <strong>{bookMetadata.title}</strong>
|
||||
{bookMetadata.authors?.length ? ` • ${bookMetadata.authors.join(', ')}` : ''}
|
||||
</Typography>
|
||||
)}
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5">Çeviri</Typography>
|
||||
<Typography color="text.secondary">{summaryLine}</Typography>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} mt={2} justifyContent="center">
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleStart}
|
||||
disabled={!ocrText?.trim() || translationStatus === 'running'}
|
||||
>
|
||||
{translationStatus === 'done' ? 'Çeviriyi Yenile' : 'Çeviriyi Başlat'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
{translationStatus === 'running' && (
|
||||
<Box>
|
||||
<LinearProgress variant="determinate" value={translationProgress} sx={{ height: 10, borderRadius: 5 }} />
|
||||
<Typography mt={1} align="center" variant="caption" color="text.secondary">
|
||||
%{translationProgress} tamamlandı
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{translationStatus === 'error' && translationError && (
|
||||
<Alert severity="error">
|
||||
{translationError}
|
||||
<Button size="small" sx={{ ml: 2 }} variant="outlined" onClick={handleRetry}>
|
||||
Tekrar dene
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
{translationStatus === 'done' && (
|
||||
<Alert severity="success">Çeviri tamamlandı. EPUB adımına geçebilirsin.</Alert>
|
||||
)}
|
||||
<Box sx={{ p: 2, borderRadius: 2, bgcolor: 'background.default' }}>
|
||||
<Typography variant="subtitle1">Çeviri önizlemesi</Typography>
|
||||
<Box
|
||||
ref={previewRef}
|
||||
sx={{
|
||||
mt: 1,
|
||||
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,
|
||||
}}
|
||||
>
|
||||
{translationStatus === 'done'
|
||||
? translatedText
|
||||
: translatedText
|
||||
? translatedText
|
||||
: 'Çeviri bekleniyor...'}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} justifyContent="space-between">
|
||||
<Button variant="contained" onClick={() => navigate('/ocr')}>
|
||||
OCR'ye dön
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate('/epub')}
|
||||
disabled={!ocrText?.trim() || translationStatus === 'running'}
|
||||
>
|
||||
EPUB adımına geç
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default TranslationStep;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import {
|
||||
Box,
|
||||
@@ -7,8 +7,12 @@ import {
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Divider,
|
||||
Grid,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -30,6 +34,15 @@ const UploadStep = () => {
|
||||
const resetFromStep = useAppStore((state) => state.resetFromStep);
|
||||
const coverImageId = useAppStore((state) => state.coverImageId);
|
||||
const setCoverImageId = useAppStore((state) => state.setCoverImageId);
|
||||
const bookTitle = useAppStore((state) => state.bookTitle);
|
||||
const setBookTitle = useAppStore((state) => state.setBookTitle);
|
||||
const bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||
const setBookMetadata = useAppStore((state) => state.setBookMetadata);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchError, setSearchError] = useState(null);
|
||||
const [selectedBookId, setSelectedBookId] = useState(bookMetadata?.id || null);
|
||||
const skipSearchRef = useRef(false);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles) => {
|
||||
@@ -52,6 +65,88 @@ const UploadStep = () => {
|
||||
setCoverImageId(nextId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedBookId(bookMetadata?.id || null);
|
||||
}, [bookMetadata]);
|
||||
|
||||
const normalizeVolume = useCallback((volume) => {
|
||||
const info = volume?.volumeInfo || {};
|
||||
const identifiers = Array.isArray(info.industryIdentifiers)
|
||||
? info.industryIdentifiers.map((identifier) => ({
|
||||
type: identifier?.type,
|
||||
identifier: identifier?.identifier,
|
||||
}))
|
||||
: [];
|
||||
const thumbnail =
|
||||
info.imageLinks?.thumbnail?.replace('http://', 'https://') ||
|
||||
info.imageLinks?.smallThumbnail?.replace('http://', 'https://') ||
|
||||
null;
|
||||
return {
|
||||
id: volume.id,
|
||||
title: info.title || 'İsimsiz kitap',
|
||||
subtitle: info.subtitle || '',
|
||||
authors: info.authors || [],
|
||||
publisher: info.publisher || '',
|
||||
publishedDate: info.publishedDate || '',
|
||||
description: info.description || '',
|
||||
pageCount: info.pageCount || null,
|
||||
categories: info.categories || [],
|
||||
averageRating: info.averageRating || null,
|
||||
ratingsCount: info.ratingsCount || null,
|
||||
language: info.language || '',
|
||||
infoLink: info.infoLink || info.previewLink || '',
|
||||
identifiers,
|
||||
thumbnail,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (skipSearchRef.current) {
|
||||
skipSearchRef.current = false;
|
||||
return;
|
||||
}
|
||||
const query = bookTitle?.trim();
|
||||
if (!query) {
|
||||
setSearchResults([]);
|
||||
setSearchError(null);
|
||||
setSearching(false);
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(async () => {
|
||||
setSearching(true);
|
||||
setSearchError(null);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/books/v1/volumes?q=intitle:${encodeURIComponent(query)}&maxResults=5&printType=books`,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error('Google Books sonuçları alınamadı.');
|
||||
}
|
||||
const payload = await response.json();
|
||||
const items = Array.isArray(payload.items) ? payload.items : [];
|
||||
const normalized = items.map((item) => normalizeVolume(item));
|
||||
setSearchResults(normalized);
|
||||
if (!normalized.length) {
|
||||
setSearchError('Bu başlıkla eşleşen bir kayıt bulunamadı.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return;
|
||||
setSearchResults([]);
|
||||
setSearchError(error.message || 'Google Books araması başarısız oldu.');
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
setSearching(false);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
controller.abort();
|
||||
};
|
||||
}, [bookTitle, normalizeVolume]);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
@@ -61,8 +156,176 @@ const UploadStep = () => {
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
const handleTitleChange = (event) => {
|
||||
const value = event.target.value;
|
||||
setBookTitle(value);
|
||||
if (!value?.trim()) {
|
||||
setBookMetadata(null);
|
||||
setSelectedBookId(null);
|
||||
setSearchResults([]);
|
||||
setSearchError(null);
|
||||
} else if (bookMetadata && bookMetadata.title !== value) {
|
||||
setBookMetadata(null);
|
||||
setSelectedBookId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectBook = (book) => {
|
||||
skipSearchRef.current = true;
|
||||
setSelectedBookId(book.id);
|
||||
setBookMetadata(book);
|
||||
setBookTitle(book.title || '');
|
||||
setSearchResults([]);
|
||||
setSearchError(null);
|
||||
};
|
||||
|
||||
const selectedBookSummary = useMemo(() => {
|
||||
if (!bookMetadata) return null;
|
||||
const authorsLine = bookMetadata.authors?.length ? bookMetadata.authors.join(', ') : null;
|
||||
const details = [
|
||||
bookMetadata.publisher,
|
||||
bookMetadata.publishedDate,
|
||||
bookMetadata.pageCount ? `${bookMetadata.pageCount} sayfa` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' • ');
|
||||
return { authorsLine, details };
|
||||
}, [bookMetadata]);
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
{bookMetadata && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
Seçilen kitap: <strong>{bookMetadata.title}</strong>
|
||||
{bookMetadata.authors?.length ? ` • ${bookMetadata.authors.join(', ')}` : ''}
|
||||
</Typography>
|
||||
)}
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Kitap adı
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="Örn. Yapay Zeka İmparatorluğu"
|
||||
value={bookTitle}
|
||||
onChange={handleTitleChange}
|
||||
InputProps={{ sx: { borderRadius: 2 } }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" mt={1}>
|
||||
Google Books veritabanında arama yapmak için kitap adını yaz. Seçtiğin kaydın tüm meta bilgileri EPUB'a işlenecek.
|
||||
</Typography>
|
||||
{searching && <LinearProgress sx={{ mt: 2, borderRadius: 999 }} />}
|
||||
{searchError && bookTitle.trim() && !searching && (
|
||||
<Typography variant="body2" color="error.main" mt={1}>
|
||||
{searchError}
|
||||
</Typography>
|
||||
)}
|
||||
{searchResults.length > 0 && bookTitle?.trim() && (
|
||||
<Paper variant="outlined" sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ px: 2, pt: 2, pb: 1, color: 'text.secondary' }}>
|
||||
Google Books sonuçları
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Stack divider={<Divider />} spacing={0}>
|
||||
{searchResults.map((book) => {
|
||||
const detailLine = [
|
||||
book.publisher,
|
||||
book.publishedDate,
|
||||
book.pageCount ? `${book.pageCount} sayfa` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' • ');
|
||||
const ratingLine = [
|
||||
book.averageRating ? `Puan ${book.averageRating}/5` : null,
|
||||
book.ratingsCount ? `${book.ratingsCount} oy` : null,
|
||||
book.language ? book.language.toUpperCase() : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' • ');
|
||||
const isSelected = selectedBookId === book.id;
|
||||
return (
|
||||
<Box
|
||||
key={book.id}
|
||||
role="button"
|
||||
onClick={() => handleSelectBook(book)}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
alignItems: 'flex-start',
|
||||
cursor: 'pointer',
|
||||
bgcolor: isSelected ? 'rgba(231,193,121,0.15)' : 'transparent',
|
||||
transition: 'background-color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 64,
|
||||
height: 96,
|
||||
borderRadius: 1,
|
||||
overflow: 'hidden',
|
||||
bgcolor: '#f0ece4',
|
||||
flexShrink: 0,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{book.thumbnail ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={book.thumbnail}
|
||||
alt={`${book.title} kapak görseli`}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Stack alignItems="center" justifyContent="center" sx={{ height: '100%' }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Kapak yok
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
{book.title}
|
||||
</Typography>
|
||||
{book.subtitle && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{book.subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontStyle: 'italic', mt: 0.5 }}
|
||||
>
|
||||
{book.authors?.length ? book.authors.join(', ') : 'Yazar bilgisi bulunamadı'}
|
||||
</Typography>
|
||||
{detailLine && (
|
||||
<Typography variant="caption" color="text.secondary" display="block" mt={0.5}>
|
||||
{detailLine}
|
||||
</Typography>
|
||||
)}
|
||||
{ratingLine && (
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
{ratingLine}
|
||||
</Typography>
|
||||
)}
|
||||
{book.categories?.length > 0 && (
|
||||
<Typography variant="caption" color="text.secondary" display="block" mt={0.5}>
|
||||
{book.categories.join(', ')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
<Box {...getRootProps()} sx={dropzoneStyle}>
|
||||
<input {...getInputProps()} />
|
||||
<Typography variant="h5" gutterBottom>
|
||||
|
||||
@@ -7,6 +7,7 @@ import UploadStep from './components/UploadStep';
|
||||
import CropStep from './components/CropStep';
|
||||
import BulkCropStep from './components/BulkCropStep';
|
||||
import OcrStep from './components/OcrStep';
|
||||
import TranslationStep from './components/TranslationStep';
|
||||
import EpubStep from './components/EpubStep';
|
||||
import DownloadStep from './components/DownloadStep';
|
||||
import Login from './pages/auth/Login';
|
||||
@@ -134,8 +135,9 @@ const router = createBrowserRouter([
|
||||
{ path: wizardSteps[1].path, element: <CropStep /> },
|
||||
{ path: wizardSteps[2].path, element: <BulkCropStep /> },
|
||||
{ path: wizardSteps[3].path, element: <OcrStep /> },
|
||||
{ path: wizardSteps[4].path, element: <EpubStep /> },
|
||||
{ path: wizardSteps[5].path, element: <DownloadStep /> },
|
||||
{ path: wizardSteps[4].path, element: <TranslationStep /> },
|
||||
{ path: wizardSteps[5].path, element: <EpubStep /> },
|
||||
{ path: wizardSteps[6].path, element: <DownloadStep /> },
|
||||
],
|
||||
},
|
||||
{ path: '/login', element: <Login /> },
|
||||
|
||||
@@ -44,6 +44,8 @@ export const useAppStore = create((set) => ({
|
||||
coverCropConfig: createEmptyCropConfig(),
|
||||
croppedCoverImage: null,
|
||||
ocrText: '',
|
||||
bookTitle: '',
|
||||
bookMetadata: null,
|
||||
translatedText: '',
|
||||
translationStatus: 'idle',
|
||||
translationError: null,
|
||||
@@ -72,6 +74,8 @@ export const useAppStore = create((set) => ({
|
||||
return { croppedCoverImage: image };
|
||||
}),
|
||||
setOcrText: (text) => set({ ocrText: text }),
|
||||
setBookTitle: (title) => set({ bookTitle: title }),
|
||||
setBookMetadata: (metadata) => set({ bookMetadata: metadata }),
|
||||
setTranslatedText: (text) => set({ translatedText: text }),
|
||||
setTranslationStatus: (status) => set({ translationStatus: status }),
|
||||
setTranslationError: (message) => set({ translationError: message }),
|
||||
@@ -147,6 +151,8 @@ export const useAppStore = create((set) => ({
|
||||
draft.coverCropConfig = createEmptyCropConfig();
|
||||
draft.croppedCoverImage = null;
|
||||
draft.ocrText = '';
|
||||
draft.bookTitle = '';
|
||||
draft.bookMetadata = null;
|
||||
draft.translatedText = '';
|
||||
draft.translationStatus = 'idle';
|
||||
draft.translationError = null;
|
||||
|
||||
@@ -26,7 +26,15 @@ const blobToBase64 = (blob) =>
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:4000';
|
||||
|
||||
export const createEpubFromOcr = async (text, coverImage) => {
|
||||
const slugify = (value = '') =>
|
||||
value
|
||||
.toLowerCase()
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
export const createEpubFromOcr = async (text, coverImage, meta = {}) => {
|
||||
if (!text?.trim()) {
|
||||
throw new Error('Önce OCR adımını tamamlamalısın.');
|
||||
}
|
||||
@@ -40,15 +48,28 @@ export const createEpubFromOcr = async (text, coverImage) => {
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedTitle = meta?.title?.trim() || 'imgPub OCR Export';
|
||||
const authors = Array.isArray(meta?.authors) && meta.authors.length
|
||||
? meta.authors
|
||||
: meta?.author
|
||||
? [meta.author]
|
||||
: ['imgPub'];
|
||||
const resolvedSlug = slugify(resolvedTitle) || 'imgpub';
|
||||
const resolvedFilename = meta?.filename || `${resolvedSlug}-${Date.now()}.epub`;
|
||||
const metadataPayload = {
|
||||
...meta,
|
||||
title: resolvedTitle,
|
||||
authors,
|
||||
filename: resolvedFilename,
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE}/generate-epub`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
meta: {
|
||||
title: 'imgPub OCR Export',
|
||||
author: 'imgPub',
|
||||
filename: `imgpub${Date.now()}.epub`,
|
||||
...metadataPayload,
|
||||
},
|
||||
cover: coverPayload,
|
||||
}),
|
||||
|
||||
@@ -23,3 +23,35 @@ export const translateChunkToTurkish = async (text) => {
|
||||
|
||||
return payload.text.trim();
|
||||
};
|
||||
|
||||
export const segmentOcrText = (text, maxChunkLength = 800) => {
|
||||
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 <= maxChunkLength) {
|
||||
chunks.push(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
let remaining = paragraph;
|
||||
while (remaining.length > maxChunkLength) {
|
||||
let sliceIndex = remaining.lastIndexOf(' ', maxChunkLength);
|
||||
if (sliceIndex === -1 || sliceIndex < maxChunkLength * 0.6) {
|
||||
sliceIndex = maxChunkLength;
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user