fix: don't attempt to load waveform/mp3 if audio was deleted (#495)

This commit is contained in:
2025-07-17 10:04:59 -06:00
committed by GitHub
parent 4a340c797b
commit 0eb670ca19
4 changed files with 107 additions and 88 deletions

View File

@@ -12,7 +12,7 @@ import FinalSummary from "./finalSummary";
import TranscriptTitle from "../transcriptTitle";
import Player from "../player";
import { useRouter } from "next/navigation";
import { Flex, Grid, GridItem, Skeleton, Text } from "@chakra-ui/react";
import { Box, Flex, Grid, GridItem, Skeleton, Text } from "@chakra-ui/react";
type TranscriptDetails = {
params: {
@@ -29,10 +29,13 @@ export default function TranscriptDetails(details: TranscriptDetails) {
const transcriptStatus = transcript.response?.status;
const waiting = statusToRedirect.includes(transcriptStatus || "");
const topics = useTopics(transcriptId);
const waveform = useWaveform(transcriptId, waiting);
const useActiveTopic = useState<Topic | null>(null);
const mp3 = useMp3(transcriptId, waiting);
const topics = useTopics(transcriptId);
const waveform = useWaveform(
transcriptId,
waiting || mp3.loading || mp3.audioDeleted === true,
);
const useActiveTopic = useState<Topic | null>(null);
useEffect(() => {
if (waiting) {
@@ -76,10 +79,9 @@ export default function TranscriptDetails(details: TranscriptDetails) {
mt={4}
mb={4}
>
{waveform.waveform &&
mp3.media &&
!mp3.audioDeleted &&
topics.topics ? (
{!mp3.audioDeleted && (
<>
{waveform.waveform && mp3.media && topics.topics ? (
<Player
topics={topics?.topics}
useActiveTopic={useActiveTopic}
@@ -87,13 +89,15 @@ export default function TranscriptDetails(details: TranscriptDetails) {
media={mp3.media}
mediaDuration={transcript.response.duration}
/>
) : waveform.error ? (
<div>error loading this recording</div>
) : mp3.audioDeleted ? (
<div>Audio was deleted</div>
) : !mp3.loading && (waveform.error || mp3.error) ? (
<Box p={4} bg="red.100" borderRadius="md">
<Text>Error loading this recording</Text>
</Box>
) : (
<Skeleton h={14} />
)}
</>
)}
<Grid
templateColumns={{ base: "minmax(0, 1fr)", md: "repeat(2, 1fr)" }}
templateRows={{
@@ -108,12 +112,9 @@ export default function TranscriptDetails(details: TranscriptDetails) {
borderColor={"gray.bg"}
borderRadius={8}
>
<GridItem
display="flex"
flexDir="row"
alignItems={"center"}
colSpan={{ base: 1, md: 2 }}
>
<GridItem colSpan={{ base: 1, md: 2 }}>
<Flex direction="column" gap={0}>
<Flex alignItems="center" gap={2}>
<TranscriptTitle
title={transcript.response.title || "Unnamed Transcript"}
transcriptId={transcriptId}
@@ -121,6 +122,14 @@ export default function TranscriptDetails(details: TranscriptDetails) {
transcript.reload();
}}
/>
</Flex>
{mp3.audioDeleted && (
<Text fontSize="xs" color="gray.600" fontStyle="italic">
No audio is available because one or more participants didn't
consent to keep the audio
</Text>
)}
</Flex>
</GridItem>
<TopicList
topics={topics.topics || []}

View File

@@ -55,8 +55,8 @@ export function TopicList({
};
useEffect(() => {
if (activeTopic) scrollToTopic();
}, [activeTopic]);
if (activeTopic && autoscroll) scrollToTopic();
}, [activeTopic, autoscroll]);
// scroll top is not rounded, heights are, so exact match won't work.
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
@@ -105,8 +105,10 @@ export function TopicList({
const requireLogin = featureEnabled("requireLogin");
useEffect(() => {
if (autoscroll) {
setActiveTopic(topics[topics.length - 1]);
}, [topics]);
}
}, [topics, autoscroll]);
return (
<Flex

View File

@@ -54,62 +54,65 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
useEffect(() => {
if (!transcriptId || !api || later) return;
let deleted: boolean | null = null;
let stopped = false;
let audioElement: HTMLAudioElement | null = null;
let handleCanPlay: (() => void) | null = null;
let handleError: (() => void) | null = null;
setTranscriptMetadataLoading(true);
setAudioLoading(true);
const audioElement = document.createElement("audio");
// First fetch transcript info to check if audio is deleted
api
.v1TranscriptGet({ transcriptId })
.then((transcript) => {
if (stopped) {
return;
}
const deleted = transcript.audio_deleted || false;
setAudioDeleted(deleted);
setTranscriptMetadataLoadingError(null);
if (deleted) {
// Audio is deleted, don't attempt to load it
setMedia(null);
setAudioLoadingError(null);
setAudioLoading(false);
return;
}
// Audio is not deleted, proceed to load it
audioElement = document.createElement("audio");
audioElement.src = `${api_url}/v1/transcripts/${transcriptId}/audio/mp3`;
audioElement.crossOrigin = "anonymous";
audioElement.preload = "auto";
const handleCanPlay = () => {
if (deleted) {
console.error(
"Illegal state: audio supposed to be deleted, but was loaded",
);
return;
}
handleCanPlay = () => {
if (stopped) return;
setAudioLoading(false);
setAudioLoadingError(null);
};
const handleError = () => {
handleError = () => {
if (stopped) return;
setAudioLoading(false);
if (deleted) {
// we arrived here earlier, ignore
return;
}
setAudioLoadingError("Failed to load audio");
};
audioElement.addEventListener("canplay", handleCanPlay);
audioElement.addEventListener("error", handleError);
if (!stopped) {
setMedia(audioElement);
setAudioLoading(true);
let stopped = false;
// Fetch transcript info in parallel
api
.v1TranscriptGet({ transcriptId })
.then((transcript) => {
if (stopped) return;
deleted = transcript.audio_deleted || false;
setAudioDeleted(deleted);
setTranscriptMetadataLoadingError(null);
if (deleted) {
setMedia(null);
setAudioLoadingError(null);
}
// if deleted, media will or already returned error
})
.catch((error) => {
if (stopped) return;
console.error("Failed to fetch transcript:", error);
setAudioDeleted(null);
setTranscriptMetadataLoadingError(error.message);
setAudioLoading(false);
})
.finally(() => {
if (stopped) return;
@@ -118,10 +121,14 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
return () => {
stopped = true;
if (audioElement) {
audioElement.src = "";
if (handleCanPlay)
audioElement.removeEventListener("canplay", handleCanPlay);
audioElement.removeEventListener("error", handleError);
if (handleError) audioElement.removeEventListener("error", handleError);
}
};
}, [transcriptId, !api, later, api_url]);
}, [transcriptId, api, later, api_url]);
const getNow = () => {
setLater(false);

View File

@@ -10,16 +10,22 @@ type AudioWaveFormResponse = {
error: Error | null;
};
const useWaveform = (id: string, waiting: boolean): AudioWaveFormResponse => {
const useWaveform = (id: string, skip: boolean): AudioWaveFormResponse => {
const [waveform, setWaveform] = useState<AudioWaveform | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = useApi();
useEffect(() => {
if (!id || !api || waiting) return;
if (!id || !api || skip) {
setLoading(false);
setErrorState(null);
setWaveform(null);
return;
}
setLoading(true);
setErrorState(null);
api
.v1TranscriptGetAudioWaveform({ transcriptId: id })
.then((result) => {
@@ -29,14 +35,9 @@ const useWaveform = (id: string, waiting: boolean): AudioWaveFormResponse => {
})
.catch((err) => {
setErrorState(err);
const shouldShowHuman = shouldShowError(err);
if (shouldShowHuman) {
setError(err, "There was an error loading the waveform");
} else {
setError(err);
}
setLoading(false);
});
}, [id, !api, waiting]);
}, [id, api, skip]);
return { waveform, loading, error };
};