Skip to content

Commit e7c40b6

Browse files
authored
Merge pull request #25 from richardr1126/server-tts
Server TTS
2 parents 86be935 + 67f3452 commit e7c40b6

File tree

12 files changed

+439
-305
lines changed

12 files changed

+439
-305
lines changed

.github/workflows/docker-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
with:
3939
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
4040
tags: |
41-
type=raw,value=latest
41+
type=raw,value=latest,enable=${{ !contains(github.ref, '-pre') }}
4242
type=semver,pattern={{version}}
4343
4444
- name: Build and push Docker image

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openreader-webui",
3-
"version": "0.1.6",
3+
"version": "0.2.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack",

src/app/api/tts/route.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import OpenAI from 'openai';
3+
4+
export async function POST(req: NextRequest) {
5+
try {
6+
// Get API credentials from headers
7+
const openApiKey = req.headers.get('x-openai-key');
8+
const openApiBaseUrl = req.headers.get('x-openai-base-url');
9+
const { text, voice, speed } = await req.json();
10+
console.log('Received TTS request:', text, voice, speed);
11+
12+
if (!openApiKey || !openApiBaseUrl) {
13+
return NextResponse.json({ error: 'Missing API credentials' }, { status: 401 });
14+
}
15+
16+
if (!text || !voice || !speed) {
17+
return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 });
18+
}
19+
20+
// Initialize OpenAI client
21+
const openai = new OpenAI({
22+
apiKey: openApiKey,
23+
baseURL: openApiBaseUrl,
24+
});
25+
26+
// Request audio from OpenAI
27+
const response = await openai.audio.speech.create({
28+
model: 'tts-1',
29+
voice: voice as "alloy",
30+
input: text,
31+
speed: speed,
32+
});
33+
34+
// Get the audio data as array buffer
35+
const arrayBuffer = await response.arrayBuffer();
36+
37+
// Return audio data with appropriate headers
38+
return new NextResponse(arrayBuffer);
39+
} catch (error) {
40+
console.error('Error generating TTS:', error);
41+
return NextResponse.json(
42+
{ error: 'Failed to generate audio' },
43+
{ status: 500 }
44+
);
45+
}
46+
}

src/app/api/tts/voices/route.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
const DEFAULT_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];
4+
5+
export async function GET(req: NextRequest) {
6+
try {
7+
// Get API credentials from headers
8+
const openApiKey = req.headers.get('x-openai-key');
9+
const openApiBaseUrl = req.headers.get('x-openai-base-url');
10+
11+
if (!openApiKey || !openApiBaseUrl) {
12+
return NextResponse.json({ error: 'Missing API credentials' }, { status: 401 });
13+
}
14+
15+
// Request voices from OpenAI
16+
const response = await fetch(`${openApiBaseUrl}/audio/voices`, {
17+
headers: {
18+
'Authorization': `Bearer ${openApiKey}`,
19+
'Content-Type': 'application/json',
20+
},
21+
});
22+
23+
if (!response.ok) {
24+
throw new Error('Failed to fetch voices');
25+
}
26+
27+
const data = await response.json();
28+
return NextResponse.json({ voices: data.voices || DEFAULT_VOICES });
29+
} catch (error) {
30+
console.error('Error fetching voices:', error);
31+
// Return default voices on error
32+
return NextResponse.json({ voices: DEFAULT_VOICES });
33+
}
34+
}

src/components/EPUBViewer.tsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface EPUBViewerProps {
2525
export function EPUBViewer({ className = '' }: EPUBViewerProps) {
2626
const { id } = useParams();
2727
const { currDocData, currDocName, currDocPage, extractPageText } = useEPUB();
28-
const { skipToLocation, registerLocationChangeHandler, setIsEPUB } = useTTS();
28+
const { skipToLocation, registerLocationChangeHandler, setIsEPUB, pause } = useTTS();
2929
const { epubTheme } = useConfig();
3030
const bookRef = useRef<Book | null>(null);
3131
const rendition = useRef<Rendition | undefined>(undefined);
@@ -35,9 +35,7 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
3535
const containerRef = useRef<HTMLDivElement>(null);
3636

3737
const isEPUBSetOnce = useRef(false);
38-
const isResizing = useRef(false);
39-
40-
useEPUBResize(containerRef, isResizing);
38+
const { isResizing, setIsResizing, dimensions } = useEPUBResize(containerRef);
4139

4240
const handleLocationChanged = useCallback((location: string | number) => {
4341
// Set the EPUB flag once the location changes
@@ -68,23 +66,45 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
6866
setLastDocumentLocation(id as string, location.toString());
6967
}
7068

71-
if (isResizing.current) {
72-
skipToLocation(location, false);
73-
isResizing.current = false;
74-
} else {
75-
skipToLocation(location, true);
76-
}
69+
skipToLocation(location);
7770

7871
locationRef.current = location;
7972
extractPageText(bookRef.current, rendition.current);
73+
8074
}, [id, skipToLocation, extractPageText, setIsEPUB]);
8175

82-
// Load the initial location
76+
const initialExtract = useCallback(() => {
77+
if (!bookRef.current || !rendition.current?.location || isEPUBSetOnce.current) return;
78+
extractPageText(bookRef.current, rendition.current, false);
79+
}, [extractPageText]);
80+
81+
const checkResize = useCallback(() => {
82+
if (isResizing && dimensions && bookRef.current?.isOpen && rendition.current && isEPUBSetOnce.current) {
83+
pause();
84+
// Only extract text when we have dimensions, ensuring the resize is complete
85+
extractPageText(bookRef.current, rendition.current, true);
86+
setIsResizing(false);
87+
88+
return true;
89+
} else {
90+
return false;
91+
}
92+
}, [isResizing, setIsResizing, dimensions, pause, extractPageText]);
93+
94+
// Check for isResizing to pause TTS and re-extract text
8395
useEffect(() => {
84-
if (!bookRef.current || !rendition.current || isEPUBSetOnce.current) return;
96+
if (checkResize()) return;
8597

86-
extractPageText(bookRef.current, rendition.current);
87-
}, [extractPageText]);
98+
// Load initial location when not resizing
99+
initialExtract();
100+
}, [checkResize, initialExtract]);
101+
102+
// Load the initial location
103+
// useEffect(() => {
104+
// if (!bookRef.current || !rendition.current || isEPUBSetOnce.current) return;
105+
106+
// extractPageText(bookRef.current, rendition.current, false);
107+
// }, [extractPageText]);
88108

89109
// Register the location change handler
90110
useEffect(() => {

src/contexts/EPUBContext.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ interface EPUBContextType {
2121
currDocText: string | undefined;
2222
setCurrentDocument: (id: string) => Promise<void>;
2323
clearCurrDoc: () => void;
24-
extractPageText: (book: Book, rendition: Rendition) => Promise<string>;
24+
extractPageText: (book: Book, rendition: Rendition, shouldPause?: boolean) => Promise<string>;
2525
}
2626

2727
const EPUBContext = createContext<EPUBContextType | undefined>(undefined);
@@ -83,9 +83,10 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
8383
* Extracts text content from the current EPUB page/location
8484
* @param {Book} book - The EPUB.js Book instance
8585
* @param {Rendition} rendition - The EPUB.js Rendition instance
86+
* @param {boolean} shouldPause - Whether to pause TTS
8687
* @returns {Promise<string>} The extracted text content
8788
*/
88-
const extractPageText = useCallback(async (book: Book, rendition: Rendition): Promise<string> => {
89+
const extractPageText = useCallback(async (book: Book, rendition: Rendition, shouldPause = false): Promise<string> => {
8990
try {
9091
const { start, end } = rendition?.location;
9192
if (!start?.cfi || !end?.cfi || !book || !book.isOpen || !rendition) return '';
@@ -95,7 +96,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
9596
const range = await book.getRange(rangeCfi);
9697
const textContent = range.toString().trim();
9798

98-
setTTSText(textContent);
99+
setTTSText(textContent, shouldPause);
99100
setCurrDocText(textContent);
100101

101102
return textContent;

src/contexts/PDFContext.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,16 @@ export function PDFProvider({ children }: { children: ReactNode }) {
105105
try {
106106
if (!pdfDocument) return;
107107
const text = await extractTextFromPDF(pdfDocument, currDocPage);
108-
setCurrDocText(text);
109-
setTTSText(text);
110-
108+
// Only update TTS text if the content has actually changed
109+
// This prevents unnecessary resets of the sentence index
110+
if (text !== currDocText || text === '') {
111+
setCurrDocText(text);
112+
setTTSText(text);
113+
}
111114
} catch (error) {
112115
console.error('Error loading PDF text:', error);
113116
}
114-
}, [pdfDocument, currDocPage, setTTSText]);
117+
}, [pdfDocument, currDocPage, setTTSText, currDocText]);
115118

116119
/**
117120
* Effect hook to update document text when the page changes

0 commit comments

Comments
 (0)