Better text extraction
This commit is contained in:
parent
f7fd360a1b
commit
b978e8c58a
4 changed files with 121 additions and 66 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import { usePDF } from '@/contexts/PDFContext';
|
import { usePDF } from '@/contexts/PDFContext';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Dialog } from '@headlessui/react';
|
import { Dialog } from '@headlessui/react';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition, TransitionChild, DialogPanel, DialogTitle } from '@headlessui/react';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
|
|
||||||
export function DocumentList() {
|
export function DocumentList() {
|
||||||
|
|
@ -102,7 +102,7 @@ export function DocumentList() {
|
||||||
className="relative z-50"
|
className="relative z-50"
|
||||||
onClose={() => setIsDeleteDialogOpen(false)}
|
onClose={() => setIsDeleteDialogOpen(false)}
|
||||||
>
|
>
|
||||||
<Transition.Child
|
<TransitionChild
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
|
|
@ -112,11 +112,11 @@ export function DocumentList() {
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||||
</Transition.Child>
|
</TransitionChild>
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<TransitionChild
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 scale-95"
|
enterFrom="opacity-0 scale-95"
|
||||||
|
|
@ -125,13 +125,13 @@ export function DocumentList() {
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-background p-6 text-left align-middle shadow-xl transition-all">
|
<DialogPanel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-background p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<DialogTitle
|
||||||
as="h3"
|
as="h3"
|
||||||
className="text-lg font-medium leading-6 text-foreground"
|
className="text-lg font-medium leading-6 text-foreground"
|
||||||
>
|
>
|
||||||
Delete Document
|
Delete Document
|
||||||
</Dialog.Title>
|
</DialogTitle>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-muted">
|
<p className="text-sm text-muted">
|
||||||
Are you sure you want to delete "{documentToDelete?.name}"? This action cannot be undone.
|
Are you sure you want to delete "{documentToDelete?.name}"? This action cannot be undone.
|
||||||
|
|
@ -154,8 +154,8 @@ export function DocumentList() {
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</DialogPanel>
|
||||||
</Transition.Child>
|
</TransitionChild>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { Dialog, Transition, Listbox } from '@headlessui/react';
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react';
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
|
|
@ -22,7 +22,7 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
|
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
|
||||||
<Transition.Child
|
<TransitionChild
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
|
|
@ -32,11 +32,11 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-black/25 backdrop-blur-sm" />
|
<div className="fixed inset-0 bg-black/25 backdrop-blur-sm" />
|
||||||
</Transition.Child>
|
</TransitionChild>
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<TransitionChild
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 scale-95"
|
enterFrom="opacity-0 scale-95"
|
||||||
|
|
@ -45,20 +45,20 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
|
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<DialogTitle
|
||||||
as="h3"
|
as="h3"
|
||||||
className="text-lg font-semibold leading-6 text-foreground"
|
className="text-lg font-semibold leading-6 text-foreground"
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</Dialog.Title>
|
</DialogTitle>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-foreground">Theme</label>
|
<label className="block text-sm font-medium text-foreground">Theme</label>
|
||||||
<Listbox value={selectedTheme} onChange={(newTheme) => setTheme(newTheme.id)}>
|
<Listbox value={selectedTheme} onChange={(newTheme) => setTheme(newTheme.id)}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button className="relative w-full cursor-pointer rounded-lg bg-background py-2 pl-3 pr-10 text-left text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent">
|
<ListboxButton className="relative w-full cursor-pointer rounded-lg bg-background py-2 pl-3 pr-10 text-left text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent">
|
||||||
<span className="block truncate">{selectedTheme.name}</span>
|
<span className="block truncate">{selectedTheme.name}</span>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -74,16 +74,16 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</Listbox.Button>
|
</ListboxButton>
|
||||||
<Transition
|
<Transition
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
leave="transition ease-in duration-100"
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-background py-1 shadow-lg ring-1 ring-black/5 focus:outline-none">
|
<ListboxOptions className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-background py-1 shadow-lg ring-1 ring-black/5 focus:outline-none">
|
||||||
{themes.map((theme) => (
|
{themes.map((theme) => (
|
||||||
<Listbox.Option
|
<ListboxOption
|
||||||
key={theme.id}
|
key={theme.id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
|
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
|
||||||
|
|
@ -115,9 +115,9 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox.Option>
|
</ListboxOption>
|
||||||
))}
|
))}
|
||||||
</Listbox.Options>
|
</ListboxOptions>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
|
|
@ -137,8 +137,8 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</DialogPanel>
|
||||||
</Transition.Child>
|
</TransitionChild>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,33 @@ export default function TTSPlayer() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 ${
|
className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 ${isVisible ? 'opacity-100' : 'opacity-0'
|
||||||
isVisible ? 'opacity-100' : 'opacity-0'
|
} transition-opacity duration-300`}
|
||||||
} transition-opacity duration-300`}
|
|
||||||
>
|
>
|
||||||
<div className="bg-base dark:bg-base rounded-full shadow-lg px-4 py-1 flex items-center space-x-1 relative">
|
<div className="bg-base dark:bg-base rounded-full shadow-lg px-4 py-1 flex items-center space-x-1 relative">
|
||||||
|
<div className="relative">
|
||||||
|
<Listbox value={speed} onChange={setSpeedAndRestart}>
|
||||||
|
<ListboxButton className="flex items-center space-x-1 bg-transparent text-foreground text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-2 pr-1 py-1">
|
||||||
|
<span>{speed}x</span>
|
||||||
|
<ChevronUpDownIcon className="h-3 w-3" />
|
||||||
|
</ListboxButton>
|
||||||
|
<ListboxOptions className="absolute bottom-full mb-1 w-24 overflow-auto rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
{speedOptions.map((option) => (
|
||||||
|
<ListboxOption
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
className={({ active, selected }) =>
|
||||||
|
`relative cursor-pointer select-none py-2 px-3 ${active ? 'bg-offbase' : ''
|
||||||
|
} ${selected ? 'font-medium' : ''}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</ListboxOption>
|
||||||
|
))}
|
||||||
|
</ListboxOptions>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={skipBackward}
|
onClick={skipBackward}
|
||||||
className="relative p-2 rounded-full text-foreground hover:bg-offbase data-[hover]:bg-offbase data-[active]:bg-offbase/80 transition-colors duration-200 focus:outline-none disabled:opacity-50"
|
className="relative p-2 rounded-full text-foreground hover:bg-offbase data-[hover]:bg-offbase data-[active]:bg-offbase/80 transition-colors duration-200 focus:outline-none disabled:opacity-50"
|
||||||
|
|
@ -82,29 +104,6 @@ export default function TTSPlayer() {
|
||||||
{isProcessing ? <LoadingSpinner /> : <SkipForwardIcon />}
|
{isProcessing ? <LoadingSpinner /> : <SkipForwardIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<Listbox value={speed} onChange={setSpeedAndRestart}>
|
|
||||||
<ListboxButton className="flex items-center space-x-1 bg-transparent text-foreground text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-2 pr-1 py-1">
|
|
||||||
<span>{speed}x</span>
|
|
||||||
<ChevronUpDownIcon className="h-3 w-3" />
|
|
||||||
</ListboxButton>
|
|
||||||
<ListboxOptions className="absolute bottom-full mb-1 w-24 overflow-auto rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
||||||
{speedOptions.map((option) => (
|
|
||||||
<ListboxOption
|
|
||||||
key={option.value}
|
|
||||||
value={option.value}
|
|
||||||
className={({ active, selected }) =>
|
|
||||||
`relative cursor-pointer select-none py-2 px-3 ${
|
|
||||||
active ? 'bg-offbase' : ''
|
|
||||||
} ${selected ? 'font-medium' : ''}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</ListboxOption>
|
|
||||||
))}
|
|
||||||
</ListboxOptions>
|
|
||||||
</Listbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox value={voice} onChange={setVoiceAndRestart}>
|
<Listbox value={voice} onChange={setVoiceAndRestart}>
|
||||||
|
|
@ -117,10 +116,8 @@ export default function TTSPlayer() {
|
||||||
<ListboxOption
|
<ListboxOption
|
||||||
key={voiceId}
|
key={voiceId}
|
||||||
value={voiceId}
|
value={voiceId}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`relative cursor-pointer select-none py-2 px-3 ${
|
`relative cursor-pointer select-none py-2 px-3 ${active ? 'bg-offbase' : ''} ${selected ? 'font-medium' : ''}`
|
||||||
active ? 'bg-offbase' : ''
|
|
||||||
} ${selected ? 'font-medium' : ''}`
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>{voiceId}</span>
|
<span>{voiceId}</span>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ import { pdfjs } from 'react-pdf';
|
||||||
import stringSimilarity from 'string-similarity';
|
import stringSimilarity from 'string-similarity';
|
||||||
import nlp from 'compromise';
|
import nlp from 'compromise';
|
||||||
|
|
||||||
|
// Add the correct type import
|
||||||
|
import type { TextContent, TextItem } from 'pdfjs-dist/types/src/display/api';
|
||||||
|
|
||||||
// Set worker from public directory
|
// Set worker from public directory
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
|
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
|
||||||
|
|
||||||
|
|
@ -135,11 +138,66 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const textContent = await page.getTextContent();
|
const textContent = await page.getTextContent();
|
||||||
const pageText = textContent.items.map((item: any) => item.str).join(' ');
|
|
||||||
fullText += pageText + ' ';
|
// Filter out non-text items and assert proper type
|
||||||
|
const textItems = textContent.items.filter((item): item is TextItem =>
|
||||||
|
'str' in item && 'transform' in item
|
||||||
|
);
|
||||||
|
|
||||||
|
// Group text items into lines based on their vertical position
|
||||||
|
const tolerance = 2;
|
||||||
|
const lines: TextItem[][] = [];
|
||||||
|
let currentLine: TextItem[] = [];
|
||||||
|
let currentY: number | null = null;
|
||||||
|
|
||||||
|
textItems.forEach((item) => {
|
||||||
|
const y = item.transform[5];
|
||||||
|
if (currentY === null) {
|
||||||
|
currentY = y;
|
||||||
|
currentLine.push(item);
|
||||||
|
} else if (Math.abs(y - currentY) < tolerance) {
|
||||||
|
currentLine.push(item);
|
||||||
|
} else {
|
||||||
|
lines.push(currentLine);
|
||||||
|
currentLine = [item];
|
||||||
|
currentY = y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lines.push(currentLine);
|
||||||
|
|
||||||
|
// Process each line to build text
|
||||||
|
let pageText = '';
|
||||||
|
for (const line of lines) {
|
||||||
|
// Sort items horizontally within the line
|
||||||
|
line.sort((a, b) => a.transform[4] - b.transform[4]);
|
||||||
|
|
||||||
|
let lineText = '';
|
||||||
|
let prevItem: TextItem | null = null;
|
||||||
|
|
||||||
|
for (const item of line) {
|
||||||
|
if (!prevItem) {
|
||||||
|
lineText = item.str;
|
||||||
|
} else {
|
||||||
|
const prevEndX = prevItem.transform[4] + (prevItem.width ?? 0);
|
||||||
|
const currentStartX = item.transform[4];
|
||||||
|
const space = currentStartX - prevEndX;
|
||||||
|
|
||||||
|
// Add space if gap is significant, otherwise concatenate directly
|
||||||
|
if (space > ((item.width ?? 0) * 0.3)) {
|
||||||
|
lineText += ' ' + item.str;
|
||||||
|
} else {
|
||||||
|
lineText += item.str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevItem = item;
|
||||||
|
}
|
||||||
|
pageText += lineText + ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
fullText += pageText + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullText;
|
return fullText.replace(/\s+/g, ' ').trim();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error extracting text from PDF:', error);
|
console.error('Error extracting text from PDF:', error);
|
||||||
throw new Error('Failed to extract text from PDF');
|
throw new Error('Failed to extract text from PDF');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue