Fix ' bug

This commit is contained in:
Richard Roberson 2025-01-20 18:24:59 -07:00
parent d4b4ad8187
commit 0979f55aec
3 changed files with 86 additions and 4 deletions

View file

@ -2,12 +2,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTTS } from '@/contexts/TTSContext'; import { useTTS } from '@/contexts/TTSContext';
import { Button } from '@headlessui/react'; import { Button, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react';
import { import {
PlayIcon, PlayIcon,
PauseIcon, PauseIcon,
SkipForwardIcon, SkipForwardIcon,
SkipBackwardIcon, SkipBackwardIcon,
ChevronUpDownIcon,
} from './icons/Icons'; } from './icons/Icons';
// Loading spinner component // Loading spinner component
@ -19,9 +20,21 @@ function LoadingSpinner() {
); );
} }
const speedOptions = [
{ value: 0.5, label: '0.5x' },
{ value: 0.75, label: '0.75x' },
{ value: 1, label: '1x' },
{ value: 1.25, label: '1.25x' },
{ value: 1.5, label: '1.5x' },
{ value: 1.75, label: '1.75x' },
{ value: 2, label: '2x' },
{ value: 2.5, label: '2.5x' },
{ value: 3, label: '3x' },
];
export default function TTSPlayer() { export default function TTSPlayer() {
const [isVisible, setIsVisible] = useState(true); const [isVisible, setIsVisible] = useState(true);
const { isPlaying, togglePlay, skipForward, skipBackward, isProcessing } = useTTS(); const { isPlaying, togglePlay, skipForward, skipBackward, isProcessing, speed, setSpeedAndRestart } = useTTS();
return ( return (
<div <div
@ -29,7 +42,7 @@ export default function TTSPlayer() {
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-2 py-1 flex items-center space-x-2 relative"> <div className="bg-base dark:bg-base rounded-full shadow-lg px-4 py-1 flex items-center space-x-1 relative">
<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"
@ -55,6 +68,30 @@ 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> </div>
</div> </div>
); );

View file

@ -108,3 +108,23 @@ export const SkipBackwardIcon = () => (
/> />
</svg> </svg>
); );
export function ChevronUpDownIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className={props.className}
width={props.width || "1.5em"}
height={props.height || "1.5em"}
{...props}
>
<path
fillRule="evenodd"
d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
clipRule="evenodd"
/>
</svg>
);
}

View file

@ -38,6 +38,9 @@ interface TTSContextType {
stopAndPlayFromIndex: (index: number) => void; stopAndPlayFromIndex: (index: number) => void;
sentences: string[]; sentences: string[];
isProcessing: boolean; isProcessing: boolean;
speed: number;
setSpeed: (speed: number) => void;
setSpeedAndRestart: (speed: number) => void;
} }
const TTSContext = createContext<TTSContextType | undefined>(undefined); const TTSContext = createContext<TTSContextType | undefined>(undefined);
@ -56,6 +59,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
const skipTriggeredRef = useRef(false); const skipTriggeredRef = useRef(false);
const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null); const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isPausingRef = useRef(false); const isPausingRef = useRef(false);
const [speed, setSpeed] = useState(1);
// Create OpenAI instance // Create OpenAI instance
const [openai] = useState( const [openai] = useState(
@ -97,7 +101,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
// Replace URLs with descriptive text including domain // Replace URLs with descriptive text including domain
.replace(/\S*(?:https?:\/\/|www\.)([^\/\s]+)(?:\/\S*)?/gi, '- (link to $1) -') .replace(/\S*(?:https?:\/\/|www\.)([^\/\s]+)(?:\/\S*)?/gi, '- (link to $1) -')
// Remove special characters except basic punctuation // Remove special characters except basic punctuation
.replace(/[^\w\s.,!?;:'"()-]/g, ' ') //.replace(/[^\w\s.,!?;:'"()-]/g, ' ')
// Fix hyphenated words at line breaks (word- word -> wordword) // Fix hyphenated words at line breaks (word- word -> wordword)
.replace(/(\w+)-\s+(\w+)/g, '$1$2') .replace(/(\w+)-\s+(\w+)/g, '$1$2')
// Replace multiple spaces with single space // Replace multiple spaces with single space
@ -162,6 +166,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
model: 'tts-1', model: 'tts-1',
voice: 'alloy', voice: 'alloy',
input: cleanedSentence, input: cleanedSentence,
speed: speed,
}); });
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
@ -388,6 +393,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
model: 'tts-1', model: 'tts-1',
voice: 'alloy', voice: 'alloy',
input: sentence, input: sentence,
speed: speed,
}); });
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
@ -472,6 +478,22 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
skipTriggeredRef.current = false; skipTriggeredRef.current = false;
}, [activeSource]); }, [activeSource]);
const setSpeedAndRestart = useCallback((newSpeed: number) => {
setSpeed(newSpeed);
// Clear the audio cache since it contains audio at the old speed
audioCacheRef.current.clear();
if (isPlaying) {
const currentIdx = currentIndex;
stop();
// Small delay to ensure audio is fully stopped
setTimeout(() => {
setCurrentIndex(currentIdx);
setIsPlaying(true);
}, 50);
}
}, [isPlaying, currentIndex, stop]);
const value = { const value = {
isPlaying, isPlaying,
currentText, currentText,
@ -487,6 +509,9 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
stopAndPlayFromIndex, stopAndPlayFromIndex,
sentences, sentences,
isProcessing, isProcessing,
speed,
setSpeed,
setSpeedAndRestart,
}; };
return ( return (