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 { useTTS } from '@/contexts/TTSContext';
import { Button } from '@headlessui/react';
import { Button, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react';
import {
PlayIcon,
PauseIcon,
SkipForwardIcon,
SkipBackwardIcon,
ChevronUpDownIcon,
} from './icons/Icons';
// 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() {
const [isVisible, setIsVisible] = useState(true);
const { isPlaying, togglePlay, skipForward, skipBackward, isProcessing } = useTTS();
const { isPlaying, togglePlay, skipForward, skipBackward, isProcessing, speed, setSpeedAndRestart } = useTTS();
return (
<div
@ -29,7 +42,7 @@ export default function TTSPlayer() {
isVisible ? 'opacity-100' : 'opacity-0'
} 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
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"
@ -55,6 +68,30 @@ export default function TTSPlayer() {
>
{isProcessing ? <LoadingSpinner /> : <SkipForwardIcon />}
</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>
);

View file

@ -108,3 +108,23 @@ export const SkipBackwardIcon = () => (
/>
</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;
sentences: string[];
isProcessing: boolean;
speed: number;
setSpeed: (speed: number) => void;
setSpeedAndRestart: (speed: number) => void;
}
const TTSContext = createContext<TTSContextType | undefined>(undefined);
@ -56,6 +59,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
const skipTriggeredRef = useRef(false);
const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isPausingRef = useRef(false);
const [speed, setSpeed] = useState(1);
// Create OpenAI instance
const [openai] = useState(
@ -97,7 +101,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
// Replace URLs with descriptive text including domain
.replace(/\S*(?:https?:\/\/|www\.)([^\/\s]+)(?:\/\S*)?/gi, '- (link to $1) -')
// Remove special characters except basic punctuation
.replace(/[^\w\s.,!?;:'"()-]/g, ' ')
//.replace(/[^\w\s.,!?;:'"()-]/g, ' ')
// Fix hyphenated words at line breaks (word- word -> wordword)
.replace(/(\w+)-\s+(\w+)/g, '$1$2')
// Replace multiple spaces with single space
@ -162,6 +166,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
model: 'tts-1',
voice: 'alloy',
input: cleanedSentence,
speed: speed,
});
const duration = Date.now() - startTime;
@ -388,6 +393,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
model: 'tts-1',
voice: 'alloy',
input: sentence,
speed: speed,
});
const duration = Date.now() - startTime;
@ -472,6 +478,22 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
skipTriggeredRef.current = false;
}, [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 = {
isPlaying,
currentText,
@ -487,6 +509,9 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
stopAndPlayFromIndex,
sentences,
isProcessing,
speed,
setSpeed,
setSpeedAndRestart,
};
return (