From fc3ef6c8933231c731fad84e7477a476a6220a5e Mon Sep 17 00:00:00 2001 From: Igor Monadical Date: Fri, 23 Jan 2026 15:51:18 -0500 Subject: [PATCH] feat: mixdown optional (#834) * optional mixdown * optional mixdown --------- Co-authored-by: Igor Loskutov --- .../workflows/daily_multitrack_pipeline.py | 2 +- www/app/(app)/rooms/page.tsx | 4 +- .../[transcriptId]/correct/page.tsx | 4 +- .../(app)/transcripts/[transcriptId]/page.tsx | 5 +- .../[transcriptId]/processing/page.tsx | 3 +- .../[transcriptId]/record/page.tsx | 8 ++- .../[transcriptId]/upload/page.tsx | 8 ++- www/app/(app)/transcripts/transcriptTitle.tsx | 3 +- www/app/(app)/transcripts/useMp3.ts | 3 +- www/app/(app)/transcripts/useParticipants.ts | 3 +- .../(app)/transcripts/useTopicWithWords.ts | 3 +- www/app/(app)/transcripts/useTopics.ts | 7 ++- www/app/(app)/transcripts/useWaveform.ts | 3 +- www/app/(app)/transcripts/useWebSockets.ts | 43 ++++++--------- www/app/[roomName]/MeetingSelection.tsx | 10 +++- www/app/lib/apiHooks.ts | 55 ++++++++++++++++--- 16 files changed, 108 insertions(+), 56 deletions(-) diff --git a/server/reflector/hatchet/workflows/daily_multitrack_pipeline.py b/server/reflector/hatchet/workflows/daily_multitrack_pipeline.py index 2d1ab194..3fd64c2c 100644 --- a/server/reflector/hatchet/workflows/daily_multitrack_pipeline.py +++ b/server/reflector/hatchet/workflows/daily_multitrack_pipeline.py @@ -1095,7 +1095,7 @@ async def identify_action_items( @daily_multitrack_pipeline.task( - parents=[generate_waveform, generate_title, generate_recap, identify_action_items], + parents=[generate_title, generate_recap, identify_action_items], execution_timeout=timedelta(seconds=TIMEOUT_SHORT), retries=3, ) diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index f542e8e8..e5349bab 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -302,10 +302,10 @@ export default function RoomsList() { return; } - const platform: "whereby" | "daily" | null = + const platform: "whereby" | "daily" = room.platform === "whereby" || room.platform === "daily" ? room.platform - : null; + : "daily"; const roomData = { name: room.name, diff --git a/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx b/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx index c4d5a9fc..10ea2f82 100644 --- a/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx @@ -16,6 +16,7 @@ import { import { useError } from "../../../../(errors)/errorContext"; import { useRouter } from "next/navigation"; import { Box, Grid } from "@chakra-ui/react"; +import { parseNonEmptyString } from "../../../../lib/utils"; export type TranscriptCorrect = { params: Promise<{ @@ -25,8 +26,7 @@ export type TranscriptCorrect = { export default function TranscriptCorrect(props: TranscriptCorrect) { const params = use(props.params); - - const { transcriptId } = params; + const transcriptId = parseNonEmptyString(params.transcriptId); const updateTranscriptMutation = useTranscriptUpdate(); const transcript = useTranscriptGet(transcriptId); diff --git a/www/app/(app)/transcripts/[transcriptId]/page.tsx b/www/app/(app)/transcripts/[transcriptId]/page.tsx index ead2d259..523f8072 100644 --- a/www/app/(app)/transcripts/[transcriptId]/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/page.tsx @@ -9,7 +9,9 @@ import React, { useEffect, useState, use } from "react"; import FinalSummary from "./finalSummary"; import TranscriptTitle from "../transcriptTitle"; import Player from "../player"; +import { useWebSockets } from "../useWebSockets"; import { useRouter } from "next/navigation"; +import { parseNonEmptyString } from "../../../lib/utils"; import { Box, Flex, @@ -30,7 +32,7 @@ type TranscriptDetails = { export default function TranscriptDetails(details: TranscriptDetails) { const params = use(details.params); - const transcriptId = params.transcriptId; + const transcriptId = parseNonEmptyString(params.transcriptId); const router = useRouter(); const statusToRedirect = [ "idle", @@ -49,6 +51,7 @@ export default function TranscriptDetails(details: TranscriptDetails) { transcriptId, waiting || mp3.audioDeleted === true, ); + useWebSockets(transcriptId); const useActiveTopic = useState(null); const [finalSummaryElement, setFinalSummaryElement] = useState(null); diff --git a/www/app/(app)/transcripts/[transcriptId]/processing/page.tsx b/www/app/(app)/transcripts/[transcriptId]/processing/page.tsx index 4422e077..0b7affaf 100644 --- a/www/app/(app)/transcripts/[transcriptId]/processing/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/processing/page.tsx @@ -10,6 +10,7 @@ import { } from "@chakra-ui/react"; import { useRouter } from "next/navigation"; import { useTranscriptGet } from "../../../../lib/apiHooks"; +import { parseNonEmptyString } from "../../../../lib/utils"; type TranscriptProcessing = { params: Promise<{ @@ -19,7 +20,7 @@ type TranscriptProcessing = { export default function TranscriptProcessing(details: TranscriptProcessing) { const params = use(details.params); - const transcriptId = params.transcriptId; + const transcriptId = parseNonEmptyString(params.transcriptId); const router = useRouter(); const transcript = useTranscriptGet(transcriptId); diff --git a/www/app/(app)/transcripts/[transcriptId]/record/page.tsx b/www/app/(app)/transcripts/[transcriptId]/record/page.tsx index d93b34b6..cc6fbbc0 100644 --- a/www/app/(app)/transcripts/[transcriptId]/record/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/record/page.tsx @@ -12,6 +12,7 @@ import { Box, Text, Grid, Heading, VStack, Flex } from "@chakra-ui/react"; import LiveTrancription from "../../liveTranscription"; import { useTranscriptGet } from "../../../../lib/apiHooks"; import { TranscriptStatus } from "../../../../lib/transcript"; +import { parseNonEmptyString } from "../../../../lib/utils"; type TranscriptDetails = { params: Promise<{ @@ -21,13 +22,14 @@ type TranscriptDetails = { const TranscriptRecord = (details: TranscriptDetails) => { const params = use(details.params); - const transcript = useTranscriptGet(params.transcriptId); + const transcriptId = parseNonEmptyString(params.transcriptId); + const transcript = useTranscriptGet(transcriptId); const [transcriptStarted, setTranscriptStarted] = useState(false); const useActiveTopic = useState(null); - const webSockets = useWebSockets(params.transcriptId); + const webSockets = useWebSockets(transcriptId); - const mp3 = useMp3(params.transcriptId, true); + const mp3 = useMp3(transcriptId, true); const router = useRouter(); diff --git a/www/app/(app)/transcripts/[transcriptId]/upload/page.tsx b/www/app/(app)/transcripts/[transcriptId]/upload/page.tsx index 9fc6a687..76722d6f 100644 --- a/www/app/(app)/transcripts/[transcriptId]/upload/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/upload/page.tsx @@ -7,6 +7,7 @@ import useMp3 from "../../useMp3"; import { Center, VStack, Text, Heading } from "@chakra-ui/react"; import FileUploadButton from "../../fileUploadButton"; import { useTranscriptGet } from "../../../../lib/apiHooks"; +import { parseNonEmptyString } from "../../../../lib/utils"; type TranscriptUpload = { params: Promise<{ @@ -16,12 +17,13 @@ type TranscriptUpload = { const TranscriptUpload = (details: TranscriptUpload) => { const params = use(details.params); - const transcript = useTranscriptGet(params.transcriptId); + const transcriptId = parseNonEmptyString(params.transcriptId); + const transcript = useTranscriptGet(transcriptId); const [transcriptStarted, setTranscriptStarted] = useState(false); - const webSockets = useWebSockets(params.transcriptId); + const webSockets = useWebSockets(transcriptId); - const mp3 = useMp3(params.transcriptId, true); + const mp3 = useMp3(transcriptId, true); const router = useRouter(); diff --git a/www/app/(app)/transcripts/transcriptTitle.tsx b/www/app/(app)/transcripts/transcriptTitle.tsx index ea738673..9eb6f375 100644 --- a/www/app/(app)/transcripts/transcriptTitle.tsx +++ b/www/app/(app)/transcripts/transcriptTitle.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import type { components } from "../../reflector-api"; +import { parseMaybeNonEmptyString } from "../../lib/utils"; type UpdateTranscript = components["schemas"]["UpdateTranscript"]; type GetTranscriptWithParticipants = @@ -32,7 +33,7 @@ const TranscriptTitle = (props: TranscriptTitle) => { const [isEditing, setIsEditing] = useState(false); const updateTranscriptMutation = useTranscriptUpdate(); const participantsQuery = useTranscriptParticipants( - props.transcript?.id || null, + props.transcript?.id ? parseMaybeNonEmptyString(props.transcript.id) : null, ); const updateTitle = async (newTitle: string, transcriptId: string) => { diff --git a/www/app/(app)/transcripts/useMp3.ts b/www/app/(app)/transcripts/useMp3.ts index cc0635ec..cfeafb90 100644 --- a/www/app/(app)/transcripts/useMp3.ts +++ b/www/app/(app)/transcripts/useMp3.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { useTranscriptGet } from "../../lib/apiHooks"; +import { parseMaybeNonEmptyString } from "../../lib/utils"; import { useAuth } from "../../lib/AuthProvider"; import { API_URL } from "../../lib/apiClient"; @@ -27,7 +28,7 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => { data: transcript, isLoading: transcriptMetadataLoading, error: transcriptError, - } = useTranscriptGet(later ? null : transcriptId); + } = useTranscriptGet(later ? null : parseMaybeNonEmptyString(transcriptId)); const [serviceWorker, setServiceWorker] = useState(null); diff --git a/www/app/(app)/transcripts/useParticipants.ts b/www/app/(app)/transcripts/useParticipants.ts index a3674597..0230075b 100644 --- a/www/app/(app)/transcripts/useParticipants.ts +++ b/www/app/(app)/transcripts/useParticipants.ts @@ -1,6 +1,7 @@ import type { components } from "../../reflector-api"; type Participant = components["schemas"]["Participant"]; import { useTranscriptParticipants } from "../../lib/apiHooks"; +import { parseMaybeNonEmptyString } from "../../lib/utils"; type ErrorParticipants = { error: Error; @@ -32,7 +33,7 @@ const useParticipants = (transcriptId: string): UseParticipants => { isLoading: loading, error, refetch, - } = useTranscriptParticipants(transcriptId || null); + } = useTranscriptParticipants(parseMaybeNonEmptyString(transcriptId)); // Type-safe return based on state if (error) { diff --git a/www/app/(app)/transcripts/useTopicWithWords.ts b/www/app/(app)/transcripts/useTopicWithWords.ts index 31e184cc..dcf2dd60 100644 --- a/www/app/(app)/transcripts/useTopicWithWords.ts +++ b/www/app/(app)/transcripts/useTopicWithWords.ts @@ -1,5 +1,6 @@ import type { components } from "../../reflector-api"; import { useTranscriptTopicsWithWordsPerSpeaker } from "../../lib/apiHooks"; +import { parseMaybeNonEmptyString } from "../../lib/utils"; type GetTranscriptTopicWithWordsPerSpeaker = components["schemas"]["GetTranscriptTopicWithWordsPerSpeaker"]; @@ -38,7 +39,7 @@ const useTopicWithWords = ( error, refetch, } = useTranscriptTopicsWithWordsPerSpeaker( - transcriptId || null, + parseMaybeNonEmptyString(transcriptId), topicId || null, ); diff --git a/www/app/(app)/transcripts/useTopics.ts b/www/app/(app)/transcripts/useTopics.ts index 7f337582..faafcf9a 100644 --- a/www/app/(app)/transcripts/useTopics.ts +++ b/www/app/(app)/transcripts/useTopics.ts @@ -1,5 +1,6 @@ import { useTranscriptTopics } from "../../lib/apiHooks"; import type { components } from "../../reflector-api"; +import { parseMaybeNonEmptyString } from "../../lib/utils"; type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"]; @@ -10,7 +11,11 @@ type TranscriptTopics = { }; const useTopics = (id: string): TranscriptTopics => { - const { data: topics, isLoading: loading, error } = useTranscriptTopics(id); + const { + data: topics, + isLoading: loading, + error, + } = useTranscriptTopics(parseMaybeNonEmptyString(id)); return { topics: topics || null, diff --git a/www/app/(app)/transcripts/useWaveform.ts b/www/app/(app)/transcripts/useWaveform.ts index 8bb8c4c9..896aa002 100644 --- a/www/app/(app)/transcripts/useWaveform.ts +++ b/www/app/(app)/transcripts/useWaveform.ts @@ -1,5 +1,6 @@ import type { components } from "../../reflector-api"; import { useTranscriptWaveform } from "../../lib/apiHooks"; +import { parseMaybeNonEmptyString } from "../../lib/utils"; type AudioWaveform = components["schemas"]["AudioWaveform"]; @@ -14,7 +15,7 @@ const useWaveform = (id: string, skip: boolean): AudioWaveFormResponse => { data: waveform, isLoading: loading, error, - } = useTranscriptWaveform(skip ? null : id); + } = useTranscriptWaveform(skip ? null : parseMaybeNonEmptyString(id)); return { waveform: waveform || null, diff --git a/www/app/(app)/transcripts/useWebSockets.ts b/www/app/(app)/transcripts/useWebSockets.ts index ed44577e..47c036b8 100644 --- a/www/app/(app)/transcripts/useWebSockets.ts +++ b/www/app/(app)/transcripts/useWebSockets.ts @@ -7,6 +7,12 @@ type GetTranscriptSegmentTopic = components["schemas"]["GetTranscriptSegmentTopic"]; import { useQueryClient } from "@tanstack/react-query"; import { $api, WEBSOCKET_URL } from "../../lib/apiClient"; +import { + invalidateTranscript, + invalidateTranscriptTopics, + invalidateTranscriptWaveform, +} from "../../lib/apiHooks"; +import { NonEmptyString } from "../../lib/utils"; export type UseWebSockets = { transcriptTextLive: string; @@ -369,15 +375,10 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { }); console.debug("TOPIC event:", message.data); // Invalidate topics query to sync with WebSocket data - queryClient.invalidateQueries({ - queryKey: $api.queryOptions( - "get", - "/v1/transcripts/{transcript_id}/topics", - { - params: { path: { transcript_id: transcriptId } }, - }, - ).queryKey, - }); + invalidateTranscriptTopics( + queryClient, + transcriptId as NonEmptyString, + ); break; case "FINAL_SHORT_SUMMARY": @@ -388,15 +389,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { if (message.data) { setFinalSummary(message.data); // Invalidate transcript query to sync summary - queryClient.invalidateQueries({ - queryKey: $api.queryOptions( - "get", - "/v1/transcripts/{transcript_id}", - { - params: { path: { transcript_id: transcriptId } }, - }, - ).queryKey, - }); + invalidateTranscript(queryClient, transcriptId as NonEmptyString); } break; @@ -405,15 +398,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { if (message.data) { setTitle(message.data.title); // Invalidate transcript query to sync title - queryClient.invalidateQueries({ - queryKey: $api.queryOptions( - "get", - "/v1/transcripts/{transcript_id}", - { - params: { path: { transcript_id: transcriptId } }, - }, - ).queryKey, - }); + invalidateTranscript(queryClient, transcriptId as NonEmptyString); } break; @@ -424,6 +409,10 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { ); if (message.data) { setWaveForm(message.data.waveform); + invalidateTranscriptWaveform( + queryClient, + transcriptId as NonEmptyString, + ); } break; case "DURATION": diff --git a/www/app/[roomName]/MeetingSelection.tsx b/www/app/[roomName]/MeetingSelection.tsx index 63743668..e2356810 100644 --- a/www/app/[roomName]/MeetingSelection.tsx +++ b/www/app/[roomName]/MeetingSelection.tsx @@ -26,7 +26,7 @@ import { useRouter } from "next/navigation"; import { formatDateTime, formatStartedAgo } from "../lib/timeUtils"; import MeetingMinimalHeader from "../components/MeetingMinimalHeader"; import { NonEmptyString } from "../lib/utils"; -import { MeetingId } from "../lib/types"; +import { MeetingId, assertMeetingId } from "../lib/types"; type Meeting = components["schemas"]["Meeting"]; @@ -315,7 +315,9 @@ export default function MeetingSelection({ variant="outline" colorScheme="red" size="md" - onClick={() => handleEndMeeting(meeting.id)} + onClick={() => + handleEndMeeting(assertMeetingId(meeting.id)) + } loading={deactivateMeetingMutation.isPending} > @@ -460,7 +462,9 @@ export default function MeetingSelection({ variant="outline" colorScheme="red" size="md" - onClick={() => handleEndMeeting(meeting.id)} + onClick={() => + handleEndMeeting(assertMeetingId(meeting.id)) + } loading={deactivateMeetingMutation.isPending} > diff --git a/www/app/lib/apiHooks.ts b/www/app/lib/apiHooks.ts index a00eb552..788dfac6 100644 --- a/www/app/lib/apiHooks.ts +++ b/www/app/lib/apiHooks.ts @@ -6,6 +6,7 @@ import { QueryClient, useQueryClient } from "@tanstack/react-query"; import type { components } from "../reflector-api"; import { useAuth } from "./AuthProvider"; import { MeetingId } from "./types"; +import { NonEmptyString } from "./utils"; /* * XXX error types returned from the hooks are not always correct; declared types are ValidationError but real type could be string or any other @@ -103,7 +104,7 @@ export function useTranscriptProcess() { }); } -export function useTranscriptGet(transcriptId: string | null) { +export function useTranscriptGet(transcriptId: NonEmptyString | null) { return $api.useQuery( "get", "/v1/transcripts/{transcript_id}", @@ -120,6 +121,16 @@ export function useTranscriptGet(transcriptId: string | null) { ); } +export const invalidateTranscript = ( + queryClient: QueryClient, + transcriptId: NonEmptyString, +) => + queryClient.invalidateQueries({ + queryKey: $api.queryOptions("get", "/v1/transcripts/{transcript_id}", { + params: { path: { transcript_id: transcriptId } }, + }).queryKey, + }); + export function useRoomGet(roomId: string | null) { const { isAuthenticated } = useAuthReady(); @@ -297,7 +308,7 @@ export function useTranscriptUploadAudio() { ); } -export function useTranscriptWaveform(transcriptId: string | null) { +export function useTranscriptWaveform(transcriptId: NonEmptyString | null) { return $api.useQuery( "get", "/v1/transcripts/{transcript_id}/audio/waveform", @@ -312,7 +323,21 @@ export function useTranscriptWaveform(transcriptId: string | null) { ); } -export function useTranscriptMP3(transcriptId: string | null) { +export const invalidateTranscriptWaveform = ( + queryClient: QueryClient, + transcriptId: NonEmptyString, +) => + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/audio/waveform", + { + params: { path: { transcript_id: transcriptId } }, + }, + ).queryKey, + }); + +export function useTranscriptMP3(transcriptId: NonEmptyString | null) { const { isAuthenticated } = useAuthReady(); return $api.useQuery( @@ -329,7 +354,7 @@ export function useTranscriptMP3(transcriptId: string | null) { ); } -export function useTranscriptTopics(transcriptId: string | null) { +export function useTranscriptTopics(transcriptId: NonEmptyString | null) { return $api.useQuery( "get", "/v1/transcripts/{transcript_id}/topics", @@ -344,7 +369,23 @@ export function useTranscriptTopics(transcriptId: string | null) { ); } -export function useTranscriptTopicsWithWords(transcriptId: string | null) { +export const invalidateTranscriptTopics = ( + queryClient: QueryClient, + transcriptId: NonEmptyString, +) => + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/topics", + { + params: { path: { transcript_id: transcriptId } }, + }, + ).queryKey, + }); + +export function useTranscriptTopicsWithWords( + transcriptId: NonEmptyString | null, +) { const { isAuthenticated } = useAuthReady(); return $api.useQuery( @@ -362,7 +403,7 @@ export function useTranscriptTopicsWithWords(transcriptId: string | null) { } export function useTranscriptTopicsWithWordsPerSpeaker( - transcriptId: string | null, + transcriptId: NonEmptyString | null, topicId: string | null, ) { const { isAuthenticated } = useAuthReady(); @@ -384,7 +425,7 @@ export function useTranscriptTopicsWithWordsPerSpeaker( ); } -export function useTranscriptParticipants(transcriptId: string | null) { +export function useTranscriptParticipants(transcriptId: NonEmptyString | null) { const { isAuthenticated } = useAuthReady(); return $api.useQuery(