Metadata ve çeviri ile ilgili düzeltmeler. UI değişiklikleri.
This commit is contained in:
@@ -4,10 +4,11 @@ import cors from 'cors';
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
import Epub from 'epub-gen';
|
import Epub from 'epub-gen';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const requiredEnv = [
|
const requiredEnv = [
|
||||||
'SUPABASE_URL',
|
'SUPABASE_URL',
|
||||||
@@ -22,6 +23,7 @@ requiredEnv.forEach((key) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 4000;
|
const PORT = process.env.PORT || 4000;
|
||||||
const ORIGIN = process.env.CLIENT_ORIGIN || 'http://localhost:5173';
|
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ı.' });
|
return res.status(400).json({ message: 'Çevrilecek metin bulunamadı.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[Translate] İstek alındı', { length: text.length, snippet: text.slice(0, 60) });
|
||||||
try {
|
try {
|
||||||
const translated = await translateWithGlm(text);
|
const translated = await translateWithGlm(text);
|
||||||
|
console.log('[Translate] Çeviri başarıyla döndü');
|
||||||
return res.json({ text: translated });
|
return res.json({ text: translated });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('GLM çeviri hatası:', 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' });
|
return res.status(400).json({ message: 'text is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = meta?.title || 'imgPub OCR Export';
|
const title = meta?.title?.trim() || 'imgPub OCR Export';
|
||||||
const author = meta?.author || 'imgPub';
|
|
||||||
const filename = meta?.filename || `imgpub${Date.now()}.epub`;
|
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 = [
|
const content = [
|
||||||
{
|
{
|
||||||
@@ -288,6 +300,18 @@ app.post('/generate-epub', async (req, res) => {
|
|||||||
|
|
||||||
const outputPath = join(tmpdir(), `imgpub-${uuidV4()}.epub`);
|
const outputPath = join(tmpdir(), `imgpub-${uuidV4()}.epub`);
|
||||||
let coverPath;
|
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 {
|
try {
|
||||||
if (cover?.data) {
|
if (cover?.data) {
|
||||||
@@ -298,7 +322,16 @@ app.post('/generate-epub', async (req, res) => {
|
|||||||
await fs.writeFile(coverPath, coverBuffer);
|
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) {
|
if (coverPath) {
|
||||||
epubOptions.cover = coverPath;
|
epubOptions.cover = coverPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, {
|
const response = await fetch(GLM_API_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -125,7 +131,20 @@ export const translateWithGlm = async (text) => {
|
|||||||
body: JSON.stringify(body),
|
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) {
|
if (!response.ok) {
|
||||||
const message =
|
const message =
|
||||||
payload?.error?.message ||
|
payload?.error?.message ||
|
||||||
@@ -136,7 +155,9 @@ export const translateWithGlm = async (text) => {
|
|||||||
|
|
||||||
const translated = extractContent(payload);
|
const translated = extractContent(payload);
|
||||||
if (!translated) {
|
if (!translated) {
|
||||||
|
console.error('[GLM] Boş içerik döndü', payload);
|
||||||
throw new Error('GLM çıktısı boş döndü.');
|
throw new Error('GLM çıktısı boş döndü.');
|
||||||
}
|
}
|
||||||
|
console.log('[GLM] Çeviri tamamlandı');
|
||||||
return translated;
|
return translated;
|
||||||
};
|
};
|
||||||
|
|||||||
100
server/templates/content.opf.ejs
Normal file
100
server/templates/content.opf.ejs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<package xmlns="http://www.idpf.org/2007/opf"
|
||||||
|
version="3.0"
|
||||||
|
unique-identifier="BookId"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||||
|
xml:lang="en"
|
||||||
|
xmlns:media="http://www.idpf.org/epub/vocab/overlays/#"
|
||||||
|
prefix="ibooks: http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/">
|
||||||
|
|
||||||
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:opf="http://www.idpf.org/2007/opf">
|
||||||
|
|
||||||
|
<dc:identifier id="BookId"><%= id %></dc:identifier>
|
||||||
|
<meta refines="#BookId" property="identifier-type" scheme="onix:codelist5">22</meta>
|
||||||
|
<meta property="dcterms:identifier" id="meta-identifier">BookId</meta>
|
||||||
|
<dc:title><%= title %></dc:title>
|
||||||
|
<% if (bookMetadata && bookMetadata.subtitle) { %>
|
||||||
|
<meta property="dcterms:alternative"><%= bookMetadata.subtitle %></meta>
|
||||||
|
<% } %>
|
||||||
|
<meta property="dcterms:title" id="meta-title"><%= title %></meta>
|
||||||
|
<dc:language><%= lang || "en" %></dc:language>
|
||||||
|
<meta property="dcterms:language" id="meta-language"><%= lang || "en" %></meta>
|
||||||
|
<meta property="dcterms:modified"><%= (new Date()).toISOString().split(".")[0]+ "Z" %></meta>
|
||||||
|
<dc:creator id="creator"><%= author.length ? author.join(",") : author %></dc:creator>
|
||||||
|
<meta refines="#creator" property="file-as"><%= author.length ? author.join(",") : author %></meta>
|
||||||
|
<meta property="dcterms:publisher"><%= publisher || "anonymous" %></meta>
|
||||||
|
<dc:publisher><%= publisher || "anonymous" %></dc:publisher>
|
||||||
|
<% var date = new Date(); var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); var stringDate = "" + year + "-" + month + "-" + day; %>
|
||||||
|
<meta property="dcterms:date"><%= bookMetadata && bookMetadata.publishedDate ? bookMetadata.publishedDate : stringDate %></meta>
|
||||||
|
<dc:date><%= bookMetadata && bookMetadata.publishedDate ? bookMetadata.publishedDate : stringDate %></dc:date>
|
||||||
|
<% if (bookMetadata && bookMetadata.description) { %>
|
||||||
|
<dc:description><%= bookMetadata.description %></dc:description>
|
||||||
|
<meta property="dcterms:description"><%= bookMetadata.description %></meta>
|
||||||
|
<% } %>
|
||||||
|
<% if (bookMetadata && bookMetadata.categories && bookMetadata.categories.length) { bookMetadata.categories.forEach(function(category){ %>
|
||||||
|
<dc:subject><%= category %></dc:subject>
|
||||||
|
<% }); } %>
|
||||||
|
<% if (bookMetadata && bookMetadata.identifiers && bookMetadata.identifiers.length) { bookMetadata.identifiers.forEach(function(identifier, idx){ %>
|
||||||
|
<meta property="dcterms:identifier" id="extra-id-<%= idx %>"><%= identifier.identifier %></meta>
|
||||||
|
<% }); } %>
|
||||||
|
<% if (bookMetadata && bookMetadata.pageCount) { %>
|
||||||
|
<meta property="schema:pageCount"><%= bookMetadata.pageCount %></meta>
|
||||||
|
<% } %>
|
||||||
|
<% if (bookMetadata && bookMetadata.averageRating) { %>
|
||||||
|
<meta property="schema:ratingValue"><%= bookMetadata.averageRating %></meta>
|
||||||
|
<% } %>
|
||||||
|
<% if (bookMetadata && bookMetadata.ratingsCount) { %>
|
||||||
|
<meta property="schema:ratingCount"><%= bookMetadata.ratingsCount %></meta>
|
||||||
|
<% } %>
|
||||||
|
<% if (bookMetadata && bookMetadata.infoLink) { %>
|
||||||
|
<meta property="dcterms:source"><%= bookMetadata.infoLink %></meta>
|
||||||
|
<% } %>
|
||||||
|
<meta property="dcterms:rights">All rights reserved</meta>
|
||||||
|
<dc:rights>Copyright © <%= (new Date()).getFullYear() %> by <%= publisher || "anonymous" %></dc:rights>
|
||||||
|
<meta name="cover" content="image_cover"/>
|
||||||
|
<meta name="generator" content="epub-gen" />
|
||||||
|
<meta property="ibooks:specified-fonts">true</meta>
|
||||||
|
|
||||||
|
</metadata>
|
||||||
|
|
||||||
|
<manifest>
|
||||||
|
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml" />
|
||||||
|
<item id="toc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav"/>
|
||||||
|
<item id="css" href="style.css" media-type="text/css" />
|
||||||
|
|
||||||
|
<% if(locals.cover) { %>
|
||||||
|
<item id="image_cover" href="cover.<%= _coverExtension %>" media-type="<%= _coverMediaType %>" />
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% images.forEach(function(image, index){ %>
|
||||||
|
<item id="image_<%= index %>" href="images/<%= image.id %>.<%= image.extension %>" media-type="<%= image.mediaType %>" />
|
||||||
|
<% }) %>
|
||||||
|
|
||||||
|
<% content.forEach(function(content, index){ %>
|
||||||
|
<item id="content_<%= index %>_<%= content.id %>" href="<%= content.href %>" media-type="application/xhtml+xml" />
|
||||||
|
<% }) %>
|
||||||
|
|
||||||
|
<% fonts.forEach(function(font, index){%>
|
||||||
|
<item id="font_<%= index%>" href="fonts/<%= font %>" media-type="application/x-font-ttf" />
|
||||||
|
<%})%>
|
||||||
|
</manifest>
|
||||||
|
|
||||||
|
<spine toc="ncx">
|
||||||
|
<% content.forEach(function(content, index){ %>
|
||||||
|
<% if(content.beforeToc && !content.excludeFromToc){ %>
|
||||||
|
<itemref idref="content_<%= index %>_<%= content.id %>"/>
|
||||||
|
<% } %>
|
||||||
|
<% }) %>
|
||||||
|
<itemref idref="toc" />
|
||||||
|
<% content.forEach(function(content, index){ %>
|
||||||
|
<% if(!content.beforeToc && !content.excludeFromToc){ %>
|
||||||
|
<itemref idref="content_<%= index %>_<%= content.id %>"/>
|
||||||
|
<% } %>
|
||||||
|
<% }) %>
|
||||||
|
</spine>
|
||||||
|
<guide>
|
||||||
|
<reference type="text" title="Table of Content" href="toc.xhtml"/>
|
||||||
|
</guide>
|
||||||
|
</package>
|
||||||
@@ -25,6 +25,7 @@ export const wizardSteps = [
|
|||||||
{ label: 'Crop', path: '/crop' },
|
{ label: 'Crop', path: '/crop' },
|
||||||
{ label: 'Toplu Crop', path: '/bulk-crop' },
|
{ label: 'Toplu Crop', path: '/bulk-crop' },
|
||||||
{ label: 'OCR', path: '/ocr' },
|
{ label: 'OCR', path: '/ocr' },
|
||||||
|
{ label: 'Çeviri', path: '/translate' },
|
||||||
{ label: 'EPUB Oluştur', path: '/epub' },
|
{ label: 'EPUB Oluştur', path: '/epub' },
|
||||||
{ label: 'İndir', path: '/download' },
|
{ label: 'İndir', path: '/download' },
|
||||||
];
|
];
|
||||||
@@ -142,11 +143,16 @@ const App = () => {
|
|||||||
<Stack direction="row" alignItems="center" justifyContent="space-between" py={1.5}>
|
<Stack direction="row" alignItems="center" justifyContent="space-between" py={1.5}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h4"
|
variant="h4"
|
||||||
|
onClick={() => {
|
||||||
|
resetFromStep('upload');
|
||||||
|
navigate('/');
|
||||||
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: '"Caudex", serif',
|
fontFamily: '"Caudex", serif',
|
||||||
color: '#1C1815',
|
color: '#1C1815',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
imagepub
|
imagepub
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const BulkCropStep = () => {
|
|||||||
const setCroppedImages = useAppStore((state) => state.setCroppedImages);
|
const setCroppedImages = useAppStore((state) => state.setCroppedImages);
|
||||||
const setError = useAppStore((state) => state.setError);
|
const setError = useAppStore((state) => state.setError);
|
||||||
const croppedImages = useAppStore((state) => state.croppedImages);
|
const croppedImages = useAppStore((state) => state.croppedImages);
|
||||||
|
const bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
|
|
||||||
const targetImages = useMemo(
|
const targetImages = useMemo(
|
||||||
@@ -90,6 +91,12 @@ const BulkCropStep = () => {
|
|||||||
if (!targetImages.length) {
|
if (!targetImages.length) {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={2}>
|
<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>
|
<Alert severity="info">Kapak dışında crop uygulanacak görsel bulunmuyor. Bu adımı geçebilirsin.</Alert>
|
||||||
<Button variant="contained" onClick={() => navigate('/ocr')}>
|
<Button variant="contained" onClick={() => navigate('/ocr')}>
|
||||||
OCR'ye geç
|
OCR'ye geç
|
||||||
@@ -100,6 +107,12 @@ const BulkCropStep = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<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">
|
<Box textAlign="center">
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
Toplu crop işlemi
|
Toplu crop işlemi
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const CropStep = () => {
|
|||||||
const coverCropConfig = useAppStore((state) => state.coverCropConfig);
|
const coverCropConfig = useAppStore((state) => state.coverCropConfig);
|
||||||
const updateCropConfig = useAppStore((state) => state.updateCropConfig);
|
const updateCropConfig = useAppStore((state) => state.updateCropConfig);
|
||||||
const updateCoverCropConfig = useAppStore((state) => state.updateCoverCropConfig);
|
const updateCoverCropConfig = useAppStore((state) => state.updateCoverCropConfig);
|
||||||
|
const bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||||
const setCroppedCoverImage = useAppStore((state) => state.setCroppedCoverImage);
|
const setCroppedCoverImage = useAppStore((state) => state.setCroppedCoverImage);
|
||||||
const resetFromStep = useAppStore((state) => state.resetFromStep);
|
const resetFromStep = useAppStore((state) => state.resetFromStep);
|
||||||
const setError = useAppStore((state) => state.setError);
|
const setError = useAppStore((state) => state.setError);
|
||||||
@@ -290,6 +291,12 @@ const CropStep = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<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>
|
<Typography variant="h6">Referans görseli seç</Typography>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{uploadedImages.map((image) => (
|
{uploadedImages.map((image) => (
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
@@ -22,10 +23,34 @@ const EpubStep = () => {
|
|||||||
const setError = useAppStore((state) => state.setError);
|
const setError = useAppStore((state) => state.setError);
|
||||||
const coverImageId = useAppStore((state) => state.coverImageId);
|
const coverImageId = useAppStore((state) => state.coverImageId);
|
||||||
const croppedCoverImage = useAppStore((state) => state.croppedCoverImage);
|
const croppedCoverImage = useAppStore((state) => state.croppedCoverImage);
|
||||||
|
const bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||||
|
const bookTitle = useAppStore((state) => state.bookTitle);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const needsCoverCrop = Boolean(coverImageId && !croppedCoverImage);
|
const needsCoverCrop = Boolean(coverImageId && !croppedCoverImage);
|
||||||
const translationBlocking = translationStatus === 'running';
|
const translationBlocking = translationStatus === 'running';
|
||||||
const exportText = translatedText?.trim() || ocrText;
|
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(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -33,7 +58,7 @@ const EpubStep = () => {
|
|||||||
if (!exportText?.trim() || generatedEpub || needsCoverCrop || translationBlocking) return;
|
if (!exportText?.trim() || generatedEpub || needsCoverCrop || translationBlocking) return;
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
try {
|
try {
|
||||||
const epub = await createEpubFromOcr(exportText, croppedCoverImage);
|
const epub = await createEpubFromOcr(exportText, croppedCoverImage, metaForEpub);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setGeneratedEpub(epub);
|
setGeneratedEpub(epub);
|
||||||
}
|
}
|
||||||
@@ -52,6 +77,7 @@ const EpubStep = () => {
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
metaForEpub,
|
||||||
croppedCoverImage,
|
croppedCoverImage,
|
||||||
exportText,
|
exportText,
|
||||||
generatedEpub,
|
generatedEpub,
|
||||||
@@ -79,6 +105,73 @@ const EpubStep = () => {
|
|||||||
<Typography color="text.secondary">
|
<Typography color="text.secondary">
|
||||||
OCR sonucundaki tüm metinleri tek bir EPUB dosyasında topluyoruz.
|
OCR sonucundaki tüm metinleri tek bir EPUB dosyasında topluyoruz.
|
||||||
</Typography>
|
</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>
|
</Box>
|
||||||
{translationBlocking && (
|
{translationBlocking && (
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
@@ -108,29 +201,30 @@ const EpubStep = () => {
|
|||||||
EPUB hazır: {generatedEpub.filename}
|
EPUB hazır: {generatedEpub.filename}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{croppedCoverImage ? (
|
{!bookMetadata &&
|
||||||
<Box sx={{ textAlign: 'center' }}>
|
(croppedCoverImage ? (
|
||||||
<Typography variant="subtitle1">Kapak önizlemesi</Typography>
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
<Box
|
<Typography variant="subtitle1">Kapak önizlemesi</Typography>
|
||||||
component="img"
|
<Box
|
||||||
src={croppedCoverImage.url}
|
component="img"
|
||||||
alt="Epub kapak görseli"
|
src={croppedCoverImage.url}
|
||||||
sx={{
|
alt="Epub kapak görseli"
|
||||||
mt: 2,
|
sx={{
|
||||||
maxHeight: 260,
|
mt: 2,
|
||||||
width: 'auto',
|
maxHeight: 260,
|
||||||
borderRadius: 2,
|
width: 'auto',
|
||||||
boxShadow: 3,
|
borderRadius: 2,
|
||||||
}}
|
boxShadow: 3,
|
||||||
/>
|
}}
|
||||||
</Box>
|
/>
|
||||||
) : (
|
</Box>
|
||||||
coverImageId && (
|
) : (
|
||||||
<Alert severity="info">
|
coverImageId && (
|
||||||
Kapak seçili ancak crop işlemi tamamlanmadı. Crop adımına dönerek kapak kesimini belirle.
|
<Alert severity="info">
|
||||||
</Alert>
|
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">
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} justifyContent="space-between">
|
||||||
<Button variant="contained" onClick={() => navigate('/ocr')}>
|
<Button variant="contained" onClick={() => navigate('/ocr')}>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import Tesseract from 'tesseract.js';
|
import Tesseract from 'tesseract.js';
|
||||||
import { useAppStore } from '../store/useAppStore';
|
import { useAppStore } from '../store/useAppStore';
|
||||||
import { correctTurkishCharacters } from '../utils/ocrUtils';
|
import { correctTurkishCharacters } from '../utils/ocrUtils';
|
||||||
import { translateChunkToTurkish } from '../utils/translationUtils';
|
|
||||||
|
|
||||||
const OcrStep = () => {
|
const OcrStep = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -20,21 +19,12 @@ const OcrStep = () => {
|
|||||||
const ocrText = useAppStore((state) => state.ocrText);
|
const ocrText = useAppStore((state) => state.ocrText);
|
||||||
const setOcrText = useAppStore((state) => state.setOcrText);
|
const setOcrText = useAppStore((state) => state.setOcrText);
|
||||||
const setError = useAppStore((state) => state.setError);
|
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 clearTranslation = useAppStore((state) => state.clearTranslation);
|
||||||
|
const bookMetadata = useAppStore((state) => state.bookMetadata);
|
||||||
const [status, setStatus] = useState('idle');
|
const [status, setStatus] = useState('idle');
|
||||||
const [translationTrigger, setTranslationTrigger] = useState(0);
|
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [previewText, setPreviewText] = useState('');
|
const [previewText, setPreviewText] = useState('');
|
||||||
const total = croppedImages.length;
|
const total = croppedImages.length;
|
||||||
const hasResults = useMemo(() => Boolean(ocrText?.length), [ocrText]);
|
|
||||||
const abortRef = useRef(false);
|
const abortRef = useRef(false);
|
||||||
|
|
||||||
const assetBase = useMemo(() => {
|
const assetBase = useMemo(() => {
|
||||||
@@ -47,7 +37,7 @@ const OcrStep = () => {
|
|||||||
const workerRef = useRef(null);
|
const workerRef = useRef(null);
|
||||||
const [workerReady, setWorkerReady] = useState(false);
|
const [workerReady, setWorkerReady] = useState(false);
|
||||||
const previewRef = useRef(null);
|
const previewRef = useRef(null);
|
||||||
const translationPreviewRef = useRef(null);
|
// removed auto navigation to translation
|
||||||
|
|
||||||
const orderedImages = useMemo(
|
const orderedImages = useMemo(
|
||||||
() => [...croppedImages].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
() => [...croppedImages].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
||||||
@@ -140,12 +130,6 @@ const OcrStep = () => {
|
|||||||
previewRef.current.scrollTop = previewRef.current.scrollHeight;
|
previewRef.current.scrollTop = previewRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}, [previewText]);
|
}, [previewText]);
|
||||||
useEffect(() => {
|
|
||||||
if (translationPreviewRef.current) {
|
|
||||||
translationPreviewRef.current.scrollTop = translationPreviewRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [translatedText]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!total || status === 'done' || !workerReady) return;
|
if (!total || status === 'done' || !workerReady) return;
|
||||||
abortRef.current = false;
|
abortRef.current = false;
|
||||||
@@ -189,58 +173,6 @@ const OcrStep = () => {
|
|||||||
};
|
};
|
||||||
}, [orderedImages, setError, setOcrText, status, total, workerReady]);
|
}, [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) {
|
if (!orderedImages.length) {
|
||||||
return (
|
return (
|
||||||
@@ -264,6 +196,12 @@ const OcrStep = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<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">
|
<Box textAlign="center">
|
||||||
<Typography variant="h5">OCR işlemi</Typography>
|
<Typography variant="h5">OCR işlemi</Typography>
|
||||||
<Typography color="text.secondary">
|
<Typography color="text.secondary">
|
||||||
@@ -280,99 +218,34 @@ const OcrStep = () => {
|
|||||||
{progressText}
|
{progressText}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack spacing={1}>
|
<Box sx={{ p: 2, borderRadius: 2, bgcolor: 'background.default' }}>
|
||||||
<Box sx={{ p: 2, borderRadius: 2, bgcolor: 'background.default' }}>
|
<Typography variant="subtitle1">Ön izleme</Typography>
|
||||||
<Typography variant="subtitle1">Ön izleme</Typography>
|
<Box
|
||||||
<Box
|
ref={previewRef}
|
||||||
ref={previewRef}
|
sx={{
|
||||||
sx={{
|
mt: 1,
|
||||||
mt: 1,
|
maxHeight: '8.5em',
|
||||||
maxHeight: '8.5em',
|
overflowY: 'auto',
|
||||||
overflowY: 'auto',
|
whiteSpace: 'pre-wrap',
|
||||||
whiteSpace: 'pre-wrap',
|
lineHeight: 1.5,
|
||||||
lineHeight: 1.5,
|
fontSize: '0.95rem',
|
||||||
fontSize: '0.95rem',
|
color: 'text.secondary',
|
||||||
color: 'text.secondary',
|
pr: 1,
|
||||||
pr: 1,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{previewText || 'Metin bekleniyor'}
|
||||||
{previewText || 'Metin bekleniyor'}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ p: 2, borderRadius: 2, bgcolor: 'background.default' }}>
|
</Box>
|
||||||
<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">
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} justifyContent="space-between">
|
||||||
<Button variant="contained" onClick={() => navigate('/bulk-crop')}>
|
<Button variant="contained" onClick={() => navigate('/bulk-crop')}>
|
||||||
Geri dön
|
Geri dön
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => navigate('/epub')}
|
onClick={() => navigate('/translate')}
|
||||||
disabled={!hasResults || translationStatus === 'running'}
|
disabled={status !== 'done'}
|
||||||
>
|
>
|
||||||
EPUB oluştur
|
Çeviri adımına geç
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -380,37 +253,3 @@ const OcrStep = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default 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 { useDropzone } from 'react-dropzone';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -7,8 +7,12 @@ import {
|
|||||||
CardActionArea,
|
CardActionArea,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardMedia,
|
CardMedia,
|
||||||
|
Divider,
|
||||||
Grid,
|
Grid,
|
||||||
|
LinearProgress,
|
||||||
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@@ -30,6 +34,15 @@ const UploadStep = () => {
|
|||||||
const resetFromStep = useAppStore((state) => state.resetFromStep);
|
const resetFromStep = useAppStore((state) => state.resetFromStep);
|
||||||
const coverImageId = useAppStore((state) => state.coverImageId);
|
const coverImageId = useAppStore((state) => state.coverImageId);
|
||||||
const setCoverImageId = useAppStore((state) => state.setCoverImageId);
|
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(
|
const onDrop = useCallback(
|
||||||
(acceptedFiles) => {
|
(acceptedFiles) => {
|
||||||
@@ -52,6 +65,88 @@ const UploadStep = () => {
|
|||||||
setCoverImageId(nextId);
|
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({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
accept: {
|
accept: {
|
||||||
@@ -61,8 +156,176 @@ const UploadStep = () => {
|
|||||||
multiple: true,
|
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 (
|
return (
|
||||||
<Stack spacing={4}>
|
<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}>
|
<Box {...getRootProps()} sx={dropzoneStyle}>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import UploadStep from './components/UploadStep';
|
|||||||
import CropStep from './components/CropStep';
|
import CropStep from './components/CropStep';
|
||||||
import BulkCropStep from './components/BulkCropStep';
|
import BulkCropStep from './components/BulkCropStep';
|
||||||
import OcrStep from './components/OcrStep';
|
import OcrStep from './components/OcrStep';
|
||||||
|
import TranslationStep from './components/TranslationStep';
|
||||||
import EpubStep from './components/EpubStep';
|
import EpubStep from './components/EpubStep';
|
||||||
import DownloadStep from './components/DownloadStep';
|
import DownloadStep from './components/DownloadStep';
|
||||||
import Login from './pages/auth/Login';
|
import Login from './pages/auth/Login';
|
||||||
@@ -134,8 +135,9 @@ const router = createBrowserRouter([
|
|||||||
{ path: wizardSteps[1].path, element: <CropStep /> },
|
{ path: wizardSteps[1].path, element: <CropStep /> },
|
||||||
{ path: wizardSteps[2].path, element: <BulkCropStep /> },
|
{ path: wizardSteps[2].path, element: <BulkCropStep /> },
|
||||||
{ path: wizardSteps[3].path, element: <OcrStep /> },
|
{ path: wizardSteps[3].path, element: <OcrStep /> },
|
||||||
{ path: wizardSteps[4].path, element: <EpubStep /> },
|
{ path: wizardSteps[4].path, element: <TranslationStep /> },
|
||||||
{ path: wizardSteps[5].path, element: <DownloadStep /> },
|
{ path: wizardSteps[5].path, element: <EpubStep /> },
|
||||||
|
{ path: wizardSteps[6].path, element: <DownloadStep /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '/login', element: <Login /> },
|
{ path: '/login', element: <Login /> },
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export const useAppStore = create((set) => ({
|
|||||||
coverCropConfig: createEmptyCropConfig(),
|
coverCropConfig: createEmptyCropConfig(),
|
||||||
croppedCoverImage: null,
|
croppedCoverImage: null,
|
||||||
ocrText: '',
|
ocrText: '',
|
||||||
|
bookTitle: '',
|
||||||
|
bookMetadata: null,
|
||||||
translatedText: '',
|
translatedText: '',
|
||||||
translationStatus: 'idle',
|
translationStatus: 'idle',
|
||||||
translationError: null,
|
translationError: null,
|
||||||
@@ -72,6 +74,8 @@ export const useAppStore = create((set) => ({
|
|||||||
return { croppedCoverImage: image };
|
return { croppedCoverImage: image };
|
||||||
}),
|
}),
|
||||||
setOcrText: (text) => set({ ocrText: text }),
|
setOcrText: (text) => set({ ocrText: text }),
|
||||||
|
setBookTitle: (title) => set({ bookTitle: title }),
|
||||||
|
setBookMetadata: (metadata) => set({ bookMetadata: metadata }),
|
||||||
setTranslatedText: (text) => set({ translatedText: text }),
|
setTranslatedText: (text) => set({ translatedText: text }),
|
||||||
setTranslationStatus: (status) => set({ translationStatus: status }),
|
setTranslationStatus: (status) => set({ translationStatus: status }),
|
||||||
setTranslationError: (message) => set({ translationError: message }),
|
setTranslationError: (message) => set({ translationError: message }),
|
||||||
@@ -147,6 +151,8 @@ export const useAppStore = create((set) => ({
|
|||||||
draft.coverCropConfig = createEmptyCropConfig();
|
draft.coverCropConfig = createEmptyCropConfig();
|
||||||
draft.croppedCoverImage = null;
|
draft.croppedCoverImage = null;
|
||||||
draft.ocrText = '';
|
draft.ocrText = '';
|
||||||
|
draft.bookTitle = '';
|
||||||
|
draft.bookMetadata = null;
|
||||||
draft.translatedText = '';
|
draft.translatedText = '';
|
||||||
draft.translationStatus = 'idle';
|
draft.translationStatus = 'idle';
|
||||||
draft.translationError = null;
|
draft.translationError = null;
|
||||||
|
|||||||
@@ -26,7 +26,15 @@ const blobToBase64 = (blob) =>
|
|||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:4000';
|
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()) {
|
if (!text?.trim()) {
|
||||||
throw new Error('Önce OCR adımını tamamlamalısın.');
|
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`, {
|
const response = await fetch(`${API_BASE}/generate-epub`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
text,
|
text,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'imgPub OCR Export',
|
...metadataPayload,
|
||||||
author: 'imgPub',
|
|
||||||
filename: `imgpub${Date.now()}.epub`,
|
|
||||||
},
|
},
|
||||||
cover: coverPayload,
|
cover: coverPayload,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -23,3 +23,35 @@ export const translateChunkToTurkish = async (text) => {
|
|||||||
|
|
||||||
return payload.text.trim();
|
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