Bug fixes and readme update
This commit is contained in:
parent
5edb990324
commit
fc780746aa
4 changed files with 43 additions and 38 deletions
45
README.md
45
README.md
|
|
@ -6,45 +6,47 @@
|
||||||
[](../../releases)
|
[](../../releases)
|
||||||
|
|
||||||
[](../../discussions)
|
[](../../discussions)
|
||||||
[](https://bsky.app/profile/richardr.dev)
|
|
||||||
|
|
||||||
|
|
||||||
# OpenReader WebUI 📄🔊
|
# OpenReader WebUI 📄🔊
|
||||||
|
|
||||||
OpenReader WebUI is a document reader with Text-to-Speech capabilities, offering a TTS read along experience with narration for both PDF and EPUB documents. It can use any OpenAI compatible TTS endpoint, including [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI).
|
OpenReader WebUI is a document reader with Text-to-Speech capabilities, offering a TTS read along experience with narration for both PDF and EPUB documents. It can use any OpenAI compatible TTS endpoint, including [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) and [Orpheus-FastAPI](https://github.com/Lex-au/Orpheus-FastAPI)
|
||||||
|
|
||||||
|
> Highly available demo currently available at [https://openreader.richardr.dev/](https://openreader.richardr.dev/)
|
||||||
|
|
||||||
- 🎯 **TTS API Integration**:
|
- 🎯 **TTS API Integration**:
|
||||||
- Compatible with OpenAI text to speech API and GPT-4o Mini TTS, Kokoro-FastAPI TTS, or any other compatible service
|
- Compatible with OpenAI text to speech API and GPT-4o Mini TTS, Kokoro-FastAPI TTS, Orpheus FastAPI or any other compatible service
|
||||||
- Support for multiple TTS models (tts-1, tts-1-hd, gpt-4o-mini-tts, kokoro)
|
- Support for TTS models (tts-1, tts-1-hd, gpt-4o-mini-tts, kokoro, and custom)
|
||||||
- Custom model support for experimental or self-hosted models
|
|
||||||
- Model-specific instructions support (for gpt-4o-mini-tts)
|
|
||||||
- 💾 **Local-First Architecture**: Uses IndexedDB browser storage for documents
|
- 💾 **Local-First Architecture**: Uses IndexedDB browser storage for documents
|
||||||
- 🛜 **Optional Server-side documents**: Manually upload documents to the next backend for all users to download
|
- 🛜 **Optional Server-side documents**: Manually upload documents to the next backend for all users to download
|
||||||
- 📖 **Read Along Experience**: Follow along with highlighted text as the TTS narrates
|
- 📖 **Read Along Experience**: Follow along with highlighted text as the TTS narrates
|
||||||
- 📄 **Document formats**: EPUB, PDF, DOCX (with libreoffice installed)
|
- 📄 **Document formats**: EPUB, PDF, DOCX (with libreoffice installed)
|
||||||
- 🎧 **Audiobook Creation**: Create and export audiobooks from PDF and ePub files with m4b format
|
- 🎧 **Audiobook Creation**: Create and export audiobooks from PDF and ePub files **(in m4b format with ffmpeg and aac TTS output)**
|
||||||
- 📲 **Mobile Support**: Works on mobile devices, and can be added as a PWA web app
|
- 📲 **Mobile Support**: Works on mobile devices, and can be added as a PWA web app
|
||||||
- 🎨 **Customizable Experience**:
|
- 🎨 **Customizable Experience**:
|
||||||
- 🔑 Set TTS API base URL (and optional API key)
|
- 🔑 Set TTS API base URL (and optional API key)
|
||||||
- 🤖 Choose from multiple TTS models or use custom models
|
|
||||||
- 🎯 Set model-specific instructions for GPT-4o Mini TTS
|
- 🎯 Set model-specific instructions for GPT-4o Mini TTS
|
||||||
- 🏎️ Adjustable playback speed
|
- 🏎️ Adjustable playback speed
|
||||||
- 📐 Customize PDF text extraction margins
|
- 📐 Customize PDF text extraction margins
|
||||||
- 🗣️ Multiple voice options (checks `/v1/audio/voices` endpoint)
|
- 🗣️ Multiple voice options (checks `/v1/audio/voices` endpoint)
|
||||||
- 🎨 Multiple app theme options
|
- 🎨 Multiple app theme options
|
||||||
|
|
||||||
|
> Orpheus-FastAPI will only work through a [fork of Orpheus-FastAPI](https://github.com/richardr1126/LlamaCpp-Orpheus-FastAPI)
|
||||||
|
|
||||||
### 🛠️ Work in progress
|
### 🛠️ Work in progress
|
||||||
- [x] **Audiobook creation and download** (m4b format)
|
- [x] **Audiobook creation and download** (m4b format)
|
||||||
- [x] **Get PDFs on iOS 17 and below working 🤞**
|
|
||||||
- [x] **Support for GPT-4o Mini TTS with instructions**
|
- [x] **Support for GPT-4o Mini TTS with instructions**
|
||||||
- [ ] **End-to-end Testing**: More playwright tests (in progress)
|
- [x] **Intial e2e testing**: More playwright tests (in progress)
|
||||||
- [ ] **More document formats**: .txt, .md
|
- [x] **Orpheus-FastAPI support**: (in progress, submitted PR to Orpheus-FastAPI)
|
||||||
- [ ] **Support more TTS APIs**: ElevenLabs, etc.
|
- [ ] **More document formats**: .txt, .md, native .docx support
|
||||||
|
- [ ] **Support non-OpenAI TTS APIs**: ElevenLabs, etc.
|
||||||
- [ ] **Accessibility Improvements**
|
- [ ] **Accessibility Improvements**
|
||||||
|
|
||||||
## 🐳 Docker Quick Start
|
## 🐳 Docker Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Recent version of Docker installed on your machine
|
||||||
|
- A TTS API server (Kokoro-FastAPI, Orpheus-FastAPI, or OpenAI API)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --name openreader-webui \
|
docker run --name openreader-webui \
|
||||||
-p 3003:3003 \
|
-p 3003:3003 \
|
||||||
|
|
@ -69,11 +71,17 @@ Visit [http://localhost:3003](http://localhost:3003) to run the app and set your
|
||||||
|
|
||||||
### ⬆️ Update Docker Image
|
### ⬆️ Update Docker Image
|
||||||
```bash
|
```bash
|
||||||
docker stop openreader-webui && docker rm openreader-webui
|
docker stop openreader-webui && \
|
||||||
|
docker rm openreader-webui && \
|
||||||
docker pull ghcr.io/richardr1126/openreader-webui:latest
|
docker pull ghcr.io/richardr1126/openreader-webui:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Adding to a Docker Compose (i.e. with open-webui or Kokoro-FastAPI)
|
### Adding to a Docker Compose (i.e. with open-webui or Kokoro-FastAPI)
|
||||||
|
|
||||||
|
> Note: This is an example of how to add OpenReader WebUI to a docker-compose file. You can add it to your existing docker-compose file or create a new one in this directory. Then run `docker-compose up --build` to start the services.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
|
||||||
Create or add to a `docker-compose.yml`:
|
Create or add to a `docker-compose.yml`:
|
||||||
```yaml
|
```yaml
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -92,11 +100,6 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
## [**Demo**](https://openreader.richardr.dev/)
|
|
||||||
|
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/262b9a01-c608-4fee-893c-9461dd48c99b
|
|
||||||
|
|
||||||
## Dev Installation
|
## Dev Installation
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
@ -147,7 +150,7 @@ For feature requests or ideas you have for the project, please use the [Discussi
|
||||||
|
|
||||||
## 🙋♂️ Support and issues
|
## 🙋♂️ Support and issues
|
||||||
|
|
||||||
For general questions, you can reach out to me on [Bluesky](https://bsky.app/profile/richardr.dev). If you encounter issues, please open an issue on GitHub following the template (which is very simple).
|
If you encounter issues, please open an issue on GitHub following the template (which is very light).
|
||||||
|
|
||||||
## 👥 Contributing
|
## 👥 Contributing
|
||||||
|
|
||||||
|
|
@ -173,7 +176,7 @@ Contributions are welcome! Fork the repository and submit a pull request with yo
|
||||||
- [react-pdf](https://github.com/wojtekmaj/react-pdf)
|
- [react-pdf](https://github.com/wojtekmaj/react-pdf)
|
||||||
- [pdf.js](https://mozilla.github.io/pdf.js/)
|
- [pdf.js](https://mozilla.github.io/pdf.js/)
|
||||||
- **EPUB:**
|
- **EPUB:**
|
||||||
- [react-reader](https://github.com/happyr/react-reader)
|
- [react-reader](https://github.com/gerhardsletten/react-reader)
|
||||||
- [epubjs](https://github.com/futurepress/epub.js/)
|
- [epubjs](https://github.com/futurepress/epub.js/)
|
||||||
- **UI:**
|
- **UI:**
|
||||||
- [Tailwind CSS](https://tailwindcss.com)
|
- [Tailwind CSS](https://tailwindcss.com)
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import { Button } from '@headlessui/react';
|
||||||
export const Navigator = ({ currentPage, numPages, skipToLocation }: {
|
export const Navigator = ({ currentPage, numPages, skipToLocation }: {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
numPages: number | undefined;
|
numPages: number | undefined;
|
||||||
skipToLocation: (location: string | number) => void;
|
skipToLocation: (location: string | number, shouldPause?: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
{/* Page back */}
|
{/* Page back */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => skipToLocation(currentPage - 1)}
|
onClick={() => skipToLocation(currentPage - 1, true)}
|
||||||
disabled={currentPage <= 1}
|
disabled={currentPage <= 1}
|
||||||
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"
|
||||||
aria-label="Previous page"
|
aria-label="Previous page"
|
||||||
|
|
@ -30,7 +30,7 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
|
||||||
|
|
||||||
{/* Page forward */}
|
{/* Page forward */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => skipToLocation(currentPage + 1)}
|
onClick={() => skipToLocation(currentPage + 1, true)}
|
||||||
disabled={currentPage >= (numPages || 1)}
|
disabled={currentPage >= (numPages || 1)}
|
||||||
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"
|
||||||
aria-label="Next page"
|
aria-label="Next page"
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
// This prevents unnecessary resets of the sentence index
|
// This prevents unnecessary resets of the sentence index
|
||||||
if (text !== currDocText || text === '') {
|
if (text !== currDocText || text === '') {
|
||||||
setCurrDocText(text);
|
setCurrDocText(text);
|
||||||
setTTSText(text, true);
|
setTTSText(text);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF text:', error);
|
console.error('Error loading PDF text:', error);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
ReactElement
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Howl } from 'howler';
|
import { Howl } from 'howler';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
@ -73,7 +74,7 @@ interface TTSContextType {
|
||||||
setCurrDocPages: (num: number | undefined) => void;
|
setCurrDocPages: (num: number | undefined) => void;
|
||||||
setSpeedAndRestart: (speed: number) => void;
|
setSpeedAndRestart: (speed: number) => void;
|
||||||
setVoiceAndRestart: (voice: string) => void;
|
setVoiceAndRestart: (voice: string) => void;
|
||||||
skipToLocation: (location: string | number) => void;
|
skipToLocation: (location: string | number, shouldPause?: boolean) => void;
|
||||||
registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation
|
registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation
|
||||||
setIsEPUB: (isEPUB: boolean) => void;
|
setIsEPUB: (isEPUB: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +90,7 @@ const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
||||||
* @param {ReactNode} props.children - Child components to be wrapped by the provider
|
* @param {ReactNode} props.children - Child components to be wrapped by the provider
|
||||||
* @returns {JSX.Element} TTSProvider component
|
* @returns {JSX.Element} TTSProvider component
|
||||||
*/
|
*/
|
||||||
export function TTSProvider({ children }: { children: ReactNode }) {
|
export function TTSProvider({ children }: { children: ReactNode }): ReactElement {
|
||||||
// Configuration context consumption
|
// Configuration context consumption
|
||||||
const {
|
const {
|
||||||
apiKey: openApiKey,
|
apiKey: openApiKey,
|
||||||
|
|
@ -196,6 +197,15 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
}, [activeHowl]);
|
}, [activeHowl]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses the current audio playback
|
||||||
|
* Used for external control of playback state
|
||||||
|
*/
|
||||||
|
const pause = useCallback(() => {
|
||||||
|
abortAudio();
|
||||||
|
setIsPlaying(false);
|
||||||
|
}, [abortAudio]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to a specific location in the document
|
* Navigates to a specific location in the document
|
||||||
* Works for both PDF pages and EPUB locations
|
* Works for both PDF pages and EPUB locations
|
||||||
|
|
@ -203,9 +213,10 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
* @param {string | number} location - The target location to navigate to
|
* @param {string | number} location - The target location to navigate to
|
||||||
* @param {boolean} keepPlaying - Whether to maintain playback state
|
* @param {boolean} keepPlaying - Whether to maintain playback state
|
||||||
*/
|
*/
|
||||||
const skipToLocation = useCallback((location: string | number) => {
|
const skipToLocation = useCallback((location: string | number, shouldPause = false) => {
|
||||||
// Reset state for new content in correct order
|
// Reset state for new content in correct order
|
||||||
abortAudio();
|
abortAudio();
|
||||||
|
if (shouldPause) setIsPlaying(false);
|
||||||
setCurrentIndex(0);
|
setCurrentIndex(0);
|
||||||
setSentences([]);
|
setSentences([]);
|
||||||
setCurrDocPage(location);
|
setCurrDocPage(location);
|
||||||
|
|
@ -342,15 +353,6 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
}, [abortAudio]);
|
}, [abortAudio]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Pauses the current audio playback
|
|
||||||
* Used for external control of playback state
|
|
||||||
*/
|
|
||||||
const pause = useCallback(() => {
|
|
||||||
abortAudio();
|
|
||||||
setIsPlaying(false);
|
|
||||||
}, [abortAudio]);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves forward one sentence in the text
|
* Moves forward one sentence in the text
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue