mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-25 06:35:18 +00:00
fix: live flow real-time updates during processing (#861)
* fix: live flow real-time updates during processing Three gaps caused transcript pages to require manual refresh after live recording/processing: 1. UserEventsProvider only invalidated list queries on TRANSCRIPT_STATUS, not individual transcript queries. Now parses data.id from the event and calls invalidateTranscript for the specific transcript. 2. useWebSockets had no reconnection logic — a dropped WS silently killed all real-time updates. Added exponential backoff reconnection (1s-30s, max 10 retries) with intentional close detection. 3. No polling fallback — WS was single point of failure. Added conditional refetchInterval to useTranscriptGet that polls every 5s when transcript status is processing/uploaded/recording. * feat: type-safe WebSocket events via OpenAPI stub Define Pydantic models with Literal discriminators for all WS events (9 transcript-level, 5 user-level). Expose via stub GET endpoints so pnpm openapi generates TS discriminated unions with exhaustive switch narrowing on the frontend. - New server/reflector/ws_events.py with TranscriptWsEvent and UserWsEvent - Tighten backend emit signatures with TranscriptEventName literal - Frontend uses generated types, removes Zod schema and manual casts - Fix pre-existing bugs: waveform mapping, FINAL_LONG_SUMMARY field name - STATUS value now typed as TranscriptStatus literal end-to-end - TOPIC handler simplified to query invalidation only (avoids shape mismatch) * fix: restore TOPIC WS handler with immediate state update The setTopics call provides instant topic rendering during live transcription. Query invalidation still follows for full data sync. * fix: align TOPIC WS event data with GetTranscriptTopic shape Convert TranscriptTopic → GetTranscriptTopic in pipeline before emitting, so WS sends segments instead of words. Removes the `as unknown as Topic` cast on the frontend. * fix: use NonEmptyString and TranscriptStatus in user WS event models --------- Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
This commit is contained in:
188
server/reflector/ws_events.py
Normal file
188
server/reflector/ws_events.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""Typed WebSocket event models.
|
||||
|
||||
Defines Pydantic models with Literal discriminators for all WS events.
|
||||
Exposed via stub GET endpoints so ``pnpm openapi`` generates TS discriminated unions.
|
||||
"""
|
||||
|
||||
from typing import Annotated, Literal, Union
|
||||
|
||||
from pydantic import BaseModel, Discriminator
|
||||
|
||||
from reflector.db.transcripts import (
|
||||
TranscriptActionItems,
|
||||
TranscriptDuration,
|
||||
TranscriptFinalLongSummary,
|
||||
TranscriptFinalShortSummary,
|
||||
TranscriptFinalTitle,
|
||||
TranscriptStatus,
|
||||
TranscriptText,
|
||||
TranscriptWaveform,
|
||||
)
|
||||
from reflector.utils.string import NonEmptyString
|
||||
from reflector.views.transcripts import GetTranscriptTopic
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transcript-level event name literal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TranscriptEventName = Literal[
|
||||
"TRANSCRIPT",
|
||||
"TOPIC",
|
||||
"STATUS",
|
||||
"FINAL_TITLE",
|
||||
"FINAL_LONG_SUMMARY",
|
||||
"FINAL_SHORT_SUMMARY",
|
||||
"ACTION_ITEMS",
|
||||
"DURATION",
|
||||
"WAVEFORM",
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transcript-level WS event wrappers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TranscriptWsTranscript(BaseModel):
|
||||
event: Literal["TRANSCRIPT"] = "TRANSCRIPT"
|
||||
data: TranscriptText
|
||||
|
||||
|
||||
class TranscriptWsTopic(BaseModel):
|
||||
event: Literal["TOPIC"] = "TOPIC"
|
||||
data: GetTranscriptTopic
|
||||
|
||||
|
||||
class TranscriptWsStatusData(BaseModel):
|
||||
value: TranscriptStatus
|
||||
|
||||
|
||||
class TranscriptWsStatus(BaseModel):
|
||||
event: Literal["STATUS"] = "STATUS"
|
||||
data: TranscriptWsStatusData
|
||||
|
||||
|
||||
class TranscriptWsFinalTitle(BaseModel):
|
||||
event: Literal["FINAL_TITLE"] = "FINAL_TITLE"
|
||||
data: TranscriptFinalTitle
|
||||
|
||||
|
||||
class TranscriptWsFinalLongSummary(BaseModel):
|
||||
event: Literal["FINAL_LONG_SUMMARY"] = "FINAL_LONG_SUMMARY"
|
||||
data: TranscriptFinalLongSummary
|
||||
|
||||
|
||||
class TranscriptWsFinalShortSummary(BaseModel):
|
||||
event: Literal["FINAL_SHORT_SUMMARY"] = "FINAL_SHORT_SUMMARY"
|
||||
data: TranscriptFinalShortSummary
|
||||
|
||||
|
||||
class TranscriptWsActionItems(BaseModel):
|
||||
event: Literal["ACTION_ITEMS"] = "ACTION_ITEMS"
|
||||
data: TranscriptActionItems
|
||||
|
||||
|
||||
class TranscriptWsDuration(BaseModel):
|
||||
event: Literal["DURATION"] = "DURATION"
|
||||
data: TranscriptDuration
|
||||
|
||||
|
||||
class TranscriptWsWaveform(BaseModel):
|
||||
event: Literal["WAVEFORM"] = "WAVEFORM"
|
||||
data: TranscriptWaveform
|
||||
|
||||
|
||||
TranscriptWsEvent = Annotated[
|
||||
Union[
|
||||
TranscriptWsTranscript,
|
||||
TranscriptWsTopic,
|
||||
TranscriptWsStatus,
|
||||
TranscriptWsFinalTitle,
|
||||
TranscriptWsFinalLongSummary,
|
||||
TranscriptWsFinalShortSummary,
|
||||
TranscriptWsActionItems,
|
||||
TranscriptWsDuration,
|
||||
TranscriptWsWaveform,
|
||||
],
|
||||
Discriminator("event"),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User-level event name literal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
UserEventName = Literal[
|
||||
"TRANSCRIPT_CREATED",
|
||||
"TRANSCRIPT_DELETED",
|
||||
"TRANSCRIPT_STATUS",
|
||||
"TRANSCRIPT_FINAL_TITLE",
|
||||
"TRANSCRIPT_DURATION",
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User-level WS event data models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class UserTranscriptCreatedData(BaseModel):
|
||||
id: NonEmptyString
|
||||
|
||||
|
||||
class UserTranscriptDeletedData(BaseModel):
|
||||
id: NonEmptyString
|
||||
|
||||
|
||||
class UserTranscriptStatusData(BaseModel):
|
||||
id: NonEmptyString
|
||||
value: TranscriptStatus
|
||||
|
||||
|
||||
class UserTranscriptFinalTitleData(BaseModel):
|
||||
id: NonEmptyString
|
||||
title: NonEmptyString
|
||||
|
||||
|
||||
class UserTranscriptDurationData(BaseModel):
|
||||
id: NonEmptyString
|
||||
duration: float
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User-level WS event wrappers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class UserWsTranscriptCreated(BaseModel):
|
||||
event: Literal["TRANSCRIPT_CREATED"] = "TRANSCRIPT_CREATED"
|
||||
data: UserTranscriptCreatedData
|
||||
|
||||
|
||||
class UserWsTranscriptDeleted(BaseModel):
|
||||
event: Literal["TRANSCRIPT_DELETED"] = "TRANSCRIPT_DELETED"
|
||||
data: UserTranscriptDeletedData
|
||||
|
||||
|
||||
class UserWsTranscriptStatus(BaseModel):
|
||||
event: Literal["TRANSCRIPT_STATUS"] = "TRANSCRIPT_STATUS"
|
||||
data: UserTranscriptStatusData
|
||||
|
||||
|
||||
class UserWsTranscriptFinalTitle(BaseModel):
|
||||
event: Literal["TRANSCRIPT_FINAL_TITLE"] = "TRANSCRIPT_FINAL_TITLE"
|
||||
data: UserTranscriptFinalTitleData
|
||||
|
||||
|
||||
class UserWsTranscriptDuration(BaseModel):
|
||||
event: Literal["TRANSCRIPT_DURATION"] = "TRANSCRIPT_DURATION"
|
||||
data: UserTranscriptDurationData
|
||||
|
||||
|
||||
UserWsEvent = Annotated[
|
||||
Union[
|
||||
UserWsTranscriptCreated,
|
||||
UserWsTranscriptDeleted,
|
||||
UserWsTranscriptStatus,
|
||||
UserWsTranscriptFinalTitle,
|
||||
UserWsTranscriptDuration,
|
||||
],
|
||||
Discriminator("event"),
|
||||
]
|
||||
Reference in New Issue
Block a user