diff --git a/server/index.js b/server/index.js index 5238788..d228a35 100644 --- a/server/index.js +++ b/server/index.js @@ -4,10 +4,11 @@ import cors from 'cors'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { tmpdir } from 'os'; -import { join } from 'path'; +import { dirname, join } from 'path'; import { promises as fs } from 'fs'; import { v4 as uuidV4 } from 'uuid'; import Epub from 'epub-gen'; +import { fileURLToPath } from 'url'; const requiredEnv = [ 'SUPABASE_URL', @@ -22,6 +23,7 @@ requiredEnv.forEach((key) => { } }); +const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); const PORT = process.env.PORT || 4000; const ORIGIN = process.env.CLIENT_ORIGIN || 'http://localhost:5173'; @@ -260,8 +262,10 @@ app.post('/translate', async (req, res) => { return res.status(400).json({ message: 'Çevrilecek metin bulunamadı.' }); } + console.log('[Translate] İstek alındı', { length: text.length, snippet: text.slice(0, 60) }); try { const translated = await translateWithGlm(text); + console.log('[Translate] Çeviri başarıyla döndü'); return res.json({ text: translated }); } catch (error) { console.error('GLM çeviri hatası:', error); @@ -275,9 +279,17 @@ app.post('/generate-epub', async (req, res) => { return res.status(400).json({ message: 'text is required' }); } - const title = meta?.title || 'imgPub OCR Export'; - const author = meta?.author || 'imgPub'; + const title = meta?.title?.trim() || 'imgPub OCR Export'; const filename = meta?.filename || `imgpub${Date.now()}.epub`; + const authors = + Array.isArray(meta?.authors) && meta.authors.length + ? meta.authors.filter(Boolean) + : meta?.author + ? [meta.author] + : ['imgPub']; + const publisher = meta?.publisher || 'imgPub'; + const language = meta?.language || 'tr'; + const description = meta?.description || title; const content = [ { @@ -288,6 +300,18 @@ app.post('/generate-epub', async (req, res) => { const outputPath = join(tmpdir(), `imgpub-${uuidV4()}.epub`); let coverPath; + const metadataPayload = { + subtitle: meta?.subtitle, + description: meta?.description, + categories: Array.isArray(meta?.categories) ? meta.categories : [], + publishedDate: meta?.publishedDate, + language: meta?.language, + pageCount: meta?.pageCount, + averageRating: meta?.averageRating, + ratingsCount: meta?.ratingsCount, + identifiers: Array.isArray(meta?.identifiers) ? meta.identifiers : [], + infoLink: meta?.infoLink, + }; try { if (cover?.data) { @@ -298,7 +322,16 @@ app.post('/generate-epub', async (req, res) => { await fs.writeFile(coverPath, coverBuffer); } - const epubOptions = { title, author, content }; + const epubOptions = { + title, + author: authors, + publisher, + description, + lang: language, + content, + bookMetadata: metadataPayload, + customOpfTemplatePath: join(__dirname, 'templates', 'content.opf.ejs'), + }; if (coverPath) { epubOptions.cover = coverPath; } diff --git a/server/src/services/glmClient.js b/server/src/services/glmClient.js index 8919a4a..b474ecf 100644 --- a/server/src/services/glmClient.js +++ b/server/src/services/glmClient.js @@ -112,6 +112,12 @@ export const translateWithGlm = async (text) => { }; } + console.log('[GLM] İstek hazırlanıyor', { + endpoint: GLM_API_URL, + model: GLM_MODEL, + snippet: text.slice(0, 80), + }); + const response = await fetch(GLM_API_URL, { method: 'POST', headers: { @@ -125,7 +131,20 @@ export const translateWithGlm = async (text) => { body: JSON.stringify(body), }); - const payload = await response.json().catch(() => ({})); + let payload = {}; + try { + payload = await response.json(); + } catch (error) { + console.error('[GLM] JSON parse başarısız', error); + } + + console.log('[GLM] Yanıt alındı', { + status: response.status, + ok: response.ok, + hasOutput: Boolean(payload?.output || payload?.choices || payload?.content), + error: payload?.error, + }); + if (!response.ok) { const message = payload?.error?.message || @@ -136,7 +155,9 @@ export const translateWithGlm = async (text) => { const translated = extractContent(payload); if (!translated) { + console.error('[GLM] Boş içerik döndü', payload); throw new Error('GLM çıktısı boş döndü.'); } + console.log('[GLM] Çeviri tamamlandı'); return translated; }; diff --git a/server/templates/content.opf.ejs b/server/templates/content.opf.ejs new file mode 100644 index 0000000..90a6d10 --- /dev/null +++ b/server/templates/content.opf.ejs @@ -0,0 +1,100 @@ + + + + + + <%= id %> + 22 + BookId + <%= title %> + <% if (bookMetadata && bookMetadata.subtitle) { %> + <%= bookMetadata.subtitle %> + <% } %> + <%= title %> + <%= lang || "en" %> + <%= lang || "en" %> + <%= (new Date()).toISOString().split(".")[0]+ "Z" %> + <%= author.length ? author.join(",") : author %> + <%= author.length ? author.join(",") : author %> + <%= publisher || "anonymous" %> + <%= publisher || "anonymous" %> + <% var date = new Date(); var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); var stringDate = "" + year + "-" + month + "-" + day; %> + <%= bookMetadata && bookMetadata.publishedDate ? bookMetadata.publishedDate : stringDate %> + <%= bookMetadata && bookMetadata.publishedDate ? bookMetadata.publishedDate : stringDate %> + <% if (bookMetadata && bookMetadata.description) { %> + <%= bookMetadata.description %> + <%= bookMetadata.description %> + <% } %> + <% if (bookMetadata && bookMetadata.categories && bookMetadata.categories.length) { bookMetadata.categories.forEach(function(category){ %> + <%= category %> + <% }); } %> + <% if (bookMetadata && bookMetadata.identifiers && bookMetadata.identifiers.length) { bookMetadata.identifiers.forEach(function(identifier, idx){ %> + <%= identifier.identifier %> + <% }); } %> + <% if (bookMetadata && bookMetadata.pageCount) { %> + <%= bookMetadata.pageCount %> + <% } %> + <% if (bookMetadata && bookMetadata.averageRating) { %> + <%= bookMetadata.averageRating %> + <% } %> + <% if (bookMetadata && bookMetadata.ratingsCount) { %> + <%= bookMetadata.ratingsCount %> + <% } %> + <% if (bookMetadata && bookMetadata.infoLink) { %> + <%= bookMetadata.infoLink %> + <% } %> + All rights reserved + Copyright © <%= (new Date()).getFullYear() %> by <%= publisher || "anonymous" %> + + + true + + + + + + + + + <% if(locals.cover) { %> + + <% } %> + + <% images.forEach(function(image, index){ %> + + <% }) %> + + <% content.forEach(function(content, index){ %> + + <% }) %> + + <% fonts.forEach(function(font, index){%> + + <%})%> + + + + <% content.forEach(function(content, index){ %> + <% if(content.beforeToc && !content.excludeFromToc){ %> + + <% } %> + <% }) %> + + <% content.forEach(function(content, index){ %> + <% if(!content.beforeToc && !content.excludeFromToc){ %> + + <% } %> + <% }) %> + + + + + diff --git a/src/App.jsx b/src/App.jsx index 3200d3a..c5c2dd9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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 = () => { { + resetFromStep('upload'); + navigate('/'); + }} sx={{ fontFamily: '"Caudex", serif', color: '#1C1815', fontWeight: 700, letterSpacing: 1, + cursor: 'pointer', }} > imagepub diff --git a/src/components/BulkCropStep.jsx b/src/components/BulkCropStep.jsx index 3540809..3385d08 100644 --- a/src/components/BulkCropStep.jsx +++ b/src/components/BulkCropStep.jsx @@ -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 ( + {bookMetadata && ( + + Seçilen kitap: {bookMetadata.title} + {bookMetadata.authors?.length ? ` • ${bookMetadata.authors.join(', ')}` : ''} + + )} Kapak dışında crop uygulanacak görsel bulunmuyor. Bu adımı geçebilirsin. - )} - - {translationStatus === 'running' && ( - - - - %{translationProgress} tamamlandı - - - )} - {translationStatus === 'done' && translatedText && ( - - Çeviri tamamlandı. EPUB üretiminde Türkçe içerik kullanılacak. - - )} - {translationStatus === 'error' && translationError && ( - - {translationError} - - )} - - {translatedText || 'Çeviri bekleniyor...'} - - - + @@ -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; -}; diff --git a/src/components/TranslationStep.jsx b/src/components/TranslationStep.jsx new file mode 100644 index 0000000..b643281 --- /dev/null +++ b/src/components/TranslationStep.jsx @@ -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 ( + + Çevrilecek metin bulunamadı. OCR adımını tamamla. + + + ); + } + + return ( + + {bookMetadata && ( + + Seçilen kitap: {bookMetadata.title} + {bookMetadata.authors?.length ? ` • ${bookMetadata.authors.join(', ')}` : ''} + + )} + + Çeviri + {summaryLine} + + + + + {translationStatus === 'running' && ( + + + + %{translationProgress} tamamlandı + + + )} + {translationStatus === 'error' && translationError && ( + + {translationError} + + + )} + {translationStatus === 'done' && ( + Çeviri tamamlandı. EPUB adımına geçebilirsin. + )} + + Çeviri önizlemesi + + {translationStatus === 'done' + ? translatedText + : translatedText + ? translatedText + : 'Çeviri bekleniyor...'} + + + + + + + + + ); +}; + +export default TranslationStep; diff --git a/src/components/UploadStep.jsx b/src/components/UploadStep.jsx index db9e9ee..0aecb1f 100644 --- a/src/components/UploadStep.jsx +++ b/src/components/UploadStep.jsx @@ -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 ( + {bookMetadata && ( + + Seçilen kitap: {bookMetadata.title} + {bookMetadata.authors?.length ? ` • ${bookMetadata.authors.join(', ')}` : ''} + + )} + + + Kitap adı + + + + 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. + + {searching && } + {searchError && bookTitle.trim() && !searching && ( + + {searchError} + + )} + {searchResults.length > 0 && bookTitle?.trim() && ( + + + Google Books sonuçları + + + } 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 ( + 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', + }} + > + + {book.thumbnail ? ( + + ) : ( + + + Kapak yok + + + )} + + + + {book.title} + + {book.subtitle && ( + + {book.subtitle} + + )} + + {book.authors?.length ? book.authors.join(', ') : 'Yazar bilgisi bulunamadı'} + + {detailLine && ( + + {detailLine} + + )} + {ratingLine && ( + + {ratingLine} + + )} + {book.categories?.length > 0 && ( + + {book.categories.join(', ')} + + )} + + + ); + })} + + + )} + diff --git a/src/main.jsx b/src/main.jsx index 1663218..4c5b1be 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -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: }, { path: wizardSteps[2].path, element: }, { path: wizardSteps[3].path, element: }, - { path: wizardSteps[4].path, element: }, - { path: wizardSteps[5].path, element: }, + { path: wizardSteps[4].path, element: }, + { path: wizardSteps[5].path, element: }, + { path: wizardSteps[6].path, element: }, ], }, { path: '/login', element: }, diff --git a/src/store/useAppStore.js b/src/store/useAppStore.js index 37cc295..38a10c2 100644 --- a/src/store/useAppStore.js +++ b/src/store/useAppStore.js @@ -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; diff --git a/src/utils/epubUtils.js b/src/utils/epubUtils.js index eb898eb..42c24e7 100644 --- a/src/utils/epubUtils.js +++ b/src/utils/epubUtils.js @@ -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, }), diff --git a/src/utils/translationUtils.js b/src/utils/translationUtils.js index 1f2c6c9..c484651 100644 --- a/src/utils/translationUtils.js +++ b/src/utils/translationUtils.js @@ -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; +};