150 lines
3.8 KiB
TypeScript
150 lines
3.8 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { cn } from "@renderer/lib/utils";
|
|
import {
|
|
Button,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverAnchor,
|
|
} from "@renderer/components/ui";
|
|
import { LookupResult } from "@renderer/components";
|
|
import { LanguagesIcon, PlayIcon } from "lucide-react";
|
|
|
|
export const AudioCaption = (props: {
|
|
audioId: string;
|
|
currentTime: number;
|
|
transcription: TranscriptionGroupType;
|
|
onSeek?: (time: number) => void;
|
|
className?: string;
|
|
isPlaying: boolean;
|
|
setIsPlaying: (isPlaying: boolean) => void;
|
|
}) => {
|
|
const {
|
|
transcription,
|
|
currentTime,
|
|
onSeek,
|
|
className,
|
|
isPlaying,
|
|
setIsPlaying,
|
|
} = props;
|
|
const [activeIndex, setActiveIndex] = useState<number>(0);
|
|
const [selected, setSelected] = useState<{
|
|
index: number;
|
|
word: string;
|
|
position?: {
|
|
top: number;
|
|
left: number;
|
|
};
|
|
}>();
|
|
|
|
useEffect(() => {
|
|
if (!transcription) return;
|
|
const time = Math.round(currentTime * 1000);
|
|
const index = transcription.segments.findIndex(
|
|
(w) => time >= w.offsets.from && time < w.offsets.to
|
|
);
|
|
|
|
if (index !== activeIndex) {
|
|
setActiveIndex(index);
|
|
}
|
|
}, [currentTime, transcription]);
|
|
|
|
if (!transcription) return null;
|
|
if (Math.round(currentTime * 1000) < transcription.offsets.from) return null;
|
|
|
|
return (
|
|
<div className={cn("relative px-4 py-2 text-lg", className)}>
|
|
<div className="flex flex-wrap">
|
|
{(transcription.segments || []).map((w, index) => (
|
|
<span
|
|
key={index}
|
|
className={`mr-1 cursor-pointer hover:bg-red-500/10 ${
|
|
index === activeIndex ? "text-red-500" : ""
|
|
}`}
|
|
onClick={(event) => {
|
|
setSelected({
|
|
index,
|
|
word: w.text,
|
|
position: {
|
|
top:
|
|
event.currentTarget.offsetTop +
|
|
event.currentTarget.offsetHeight,
|
|
left: event.currentTarget.offsetLeft,
|
|
},
|
|
});
|
|
|
|
setIsPlaying(false);
|
|
if (onSeek) onSeek(w.offsets.from / 1000);
|
|
}}
|
|
>
|
|
{w.text}
|
|
</span>
|
|
))}
|
|
|
|
<Popover
|
|
open={Boolean(selected) && !isPlaying}
|
|
onOpenChange={(value) => {
|
|
if (!value) setSelected(null);
|
|
}}
|
|
>
|
|
<PopoverAnchor
|
|
className="absolute w-0 h-0"
|
|
style={{
|
|
top: selected?.position?.top,
|
|
left: selected?.position?.left,
|
|
}}
|
|
></PopoverAnchor>
|
|
<PopoverContent
|
|
className="w-full max-w-md p-0"
|
|
updatePositionStrategy="always"
|
|
>
|
|
{selected?.word && (
|
|
<AudioCaptionSelectionMenu
|
|
word={selected.word}
|
|
context={transcription.segments.map((w) => w.text).join(" ").trim()}
|
|
audioId={props.audioId}
|
|
onPlay={() => {
|
|
setIsPlaying(true);
|
|
}}
|
|
/>
|
|
)}
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const AudioCaptionSelectionMenu = (props: {
|
|
word: string;
|
|
context: string;
|
|
audioId: string;
|
|
onPlay: () => void;
|
|
}) => {
|
|
const { word, context, audioId, onPlay } = props;
|
|
const [translating, setTranslating] = useState<boolean>(false);
|
|
|
|
if (!word) return null;
|
|
|
|
if (translating) {
|
|
return (
|
|
<LookupResult
|
|
word={word}
|
|
context={context}
|
|
sourceId={audioId}
|
|
sourceType={"Audio"}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex items-center p-1">
|
|
<Button onClick={onPlay} variant="ghost" size="icon">
|
|
<PlayIcon size={16} />
|
|
</Button>
|
|
<Button onClick={() => setTranslating(true)} variant="ghost" size="icon">
|
|
<LanguagesIcon size={16} />
|
|
</Button>
|
|
</div>
|
|
);
|
|
};
|