Fix ' bug
This commit is contained in:
parent
d4b4ad8187
commit
0979f55aec
3 changed files with 86 additions and 4 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
Loading…
Reference in a new issue