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 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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue