mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-03-21 22:56:47 +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:
@@ -12,3 +12,5 @@ AccessTokenInfo = auth_module.AccessTokenInfo
|
|||||||
authenticated = auth_module.authenticated
|
authenticated = auth_module.authenticated
|
||||||
current_user = auth_module.current_user
|
current_user = auth_module.current_user
|
||||||
current_user_optional = auth_module.current_user_optional
|
current_user_optional = auth_module.current_user_optional
|
||||||
|
parse_ws_bearer_token = auth_module.parse_ws_bearer_token
|
||||||
|
current_user_ws_optional = auth_module.current_user_ws_optional
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
from typing import Annotated, List, Optional
|
from typing import TYPE_CHECKING, Annotated, List, Optional
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException
|
from fastapi import Depends, HTTPException
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from fastapi import WebSocket
|
||||||
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
|
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -124,3 +127,20 @@ async def current_user_optional(
|
|||||||
jwtauth: JWTAuth = Depends(),
|
jwtauth: JWTAuth = Depends(),
|
||||||
):
|
):
|
||||||
return await _authenticate_user(jwt_token, api_key, jwtauth)
|
return await _authenticate_user(jwt_token, api_key, jwtauth)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ws_bearer_token(
|
||||||
|
websocket: "WebSocket",
|
||||||
|
) -> tuple[Optional[str], Optional[str]]:
|
||||||
|
raw = websocket.headers.get("sec-websocket-protocol") or ""
|
||||||
|
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
||||||
|
if len(parts) >= 2 and parts[0].lower() == "bearer":
|
||||||
|
return parts[1], "bearer"
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
async def current_user_ws_optional(websocket: "WebSocket") -> Optional[UserInfo]:
|
||||||
|
token, _ = parse_ws_bearer_token(websocket)
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
return await _authenticate_user(token, None, JWTAuth())
|
||||||
|
|||||||
@@ -19,3 +19,11 @@ def current_user():
|
|||||||
|
|
||||||
def current_user_optional():
|
def current_user_optional():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ws_bearer_token(websocket):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
async def current_user_ws_optional(websocket):
|
||||||
|
return None
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import shutil
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal, Sequence
|
from typing import TYPE_CHECKING, Any, Literal, Sequence
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from reflector.ws_events import TranscriptEventName
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -184,7 +187,7 @@ class TranscriptWaveform(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class TranscriptEvent(BaseModel):
|
class TranscriptEvent(BaseModel):
|
||||||
event: str
|
event: str # Typed at call sites via ws_events.TranscriptEventName; str here for DB compat
|
||||||
data: dict
|
data: dict
|
||||||
|
|
||||||
|
|
||||||
@@ -233,7 +236,9 @@ class Transcript(BaseModel):
|
|||||||
dt = dt.replace(tzinfo=timezone.utc)
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
return dt.isoformat()
|
return dt.isoformat()
|
||||||
|
|
||||||
def add_event(self, event: str, data: BaseModel) -> TranscriptEvent:
|
def add_event(
|
||||||
|
self, event: "TranscriptEventName", data: BaseModel
|
||||||
|
) -> TranscriptEvent:
|
||||||
ev = TranscriptEvent(event=event, data=data.model_dump())
|
ev = TranscriptEvent(event=event, data=data.model_dump())
|
||||||
self.events.append(ev)
|
self.events.append(ev)
|
||||||
return ev
|
return ev
|
||||||
@@ -688,7 +693,7 @@ class TranscriptController:
|
|||||||
async def append_event(
|
async def append_event(
|
||||||
self,
|
self,
|
||||||
transcript: Transcript,
|
transcript: Transcript,
|
||||||
event: str,
|
event: "TranscriptEventName",
|
||||||
data: Any,
|
data: Any,
|
||||||
) -> TranscriptEvent:
|
) -> TranscriptEvent:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import structlog
|
|||||||
|
|
||||||
from reflector.db.transcripts import Transcript, TranscriptEvent, transcripts_controller
|
from reflector.db.transcripts import Transcript, TranscriptEvent, transcripts_controller
|
||||||
from reflector.utils.string import NonEmptyString
|
from reflector.utils.string import NonEmptyString
|
||||||
|
from reflector.ws_events import TranscriptEventName
|
||||||
from reflector.ws_manager import get_ws_manager
|
from reflector.ws_manager import get_ws_manager
|
||||||
|
|
||||||
# Events that should also be sent to user room (matches Celery behavior)
|
# Events that should also be sent to user room (matches Celery behavior)
|
||||||
USER_ROOM_EVENTS = {"STATUS", "FINAL_TITLE", "DURATION"}
|
USER_ROOM_EVENTS: set[TranscriptEventName] = {"STATUS", "FINAL_TITLE", "DURATION"}
|
||||||
|
|
||||||
|
|
||||||
async def broadcast_event(
|
async def broadcast_event(
|
||||||
@@ -81,8 +82,7 @@ async def set_status_and_broadcast(
|
|||||||
async def append_event_and_broadcast(
|
async def append_event_and_broadcast(
|
||||||
transcript_id: NonEmptyString,
|
transcript_id: NonEmptyString,
|
||||||
transcript: Transcript,
|
transcript: Transcript,
|
||||||
event_name: NonEmptyString,
|
event_name: TranscriptEventName,
|
||||||
# TODO proper dictionary event => type
|
|
||||||
data: Any,
|
data: Any,
|
||||||
logger: structlog.BoundLogger,
|
logger: structlog.BoundLogger,
|
||||||
) -> TranscriptEvent:
|
) -> TranscriptEvent:
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ from reflector.processors.types import (
|
|||||||
from reflector.processors.types import Transcript as TranscriptProcessorType
|
from reflector.processors.types import Transcript as TranscriptProcessorType
|
||||||
from reflector.settings import settings
|
from reflector.settings import settings
|
||||||
from reflector.storage import get_transcripts_storage
|
from reflector.storage import get_transcripts_storage
|
||||||
|
from reflector.views.transcripts import GetTranscriptTopic
|
||||||
|
from reflector.ws_events import TranscriptEventName
|
||||||
from reflector.ws_manager import WebsocketManager, get_ws_manager
|
from reflector.ws_manager import WebsocketManager, get_ws_manager
|
||||||
from reflector.zulip import (
|
from reflector.zulip import (
|
||||||
get_zulip_message,
|
get_zulip_message,
|
||||||
@@ -89,7 +91,11 @@ def broadcast_to_sockets(func):
|
|||||||
if transcript and transcript.user_id:
|
if transcript and transcript.user_id:
|
||||||
# Emit only relevant events to the user room to avoid noisy updates.
|
# Emit only relevant events to the user room to avoid noisy updates.
|
||||||
# Allowed: STATUS, FINAL_TITLE, DURATION. All are prefixed with TRANSCRIPT_
|
# Allowed: STATUS, FINAL_TITLE, DURATION. All are prefixed with TRANSCRIPT_
|
||||||
allowed_user_events = {"STATUS", "FINAL_TITLE", "DURATION"}
|
allowed_user_events: set[TranscriptEventName] = {
|
||||||
|
"STATUS",
|
||||||
|
"FINAL_TITLE",
|
||||||
|
"DURATION",
|
||||||
|
}
|
||||||
if resp.event in allowed_user_events:
|
if resp.event in allowed_user_events:
|
||||||
await self.ws_manager.send_json(
|
await self.ws_manager.send_json(
|
||||||
room_id=f"user:{transcript.user_id}",
|
room_id=f"user:{transcript.user_id}",
|
||||||
@@ -244,13 +250,14 @@ class PipelineMainBase(PipelineRunner[PipelineMessage], Generic[PipelineMessage]
|
|||||||
)
|
)
|
||||||
if isinstance(data, TitleSummaryWithIdProcessorType):
|
if isinstance(data, TitleSummaryWithIdProcessorType):
|
||||||
topic.id = data.id
|
topic.id = data.id
|
||||||
|
get_topic = GetTranscriptTopic.from_transcript_topic(topic)
|
||||||
async with self.transaction():
|
async with self.transaction():
|
||||||
transcript = await self.get_transcript()
|
transcript = await self.get_transcript()
|
||||||
await transcripts_controller.upsert_topic(transcript, topic)
|
await transcripts_controller.upsert_topic(transcript, topic)
|
||||||
return await transcripts_controller.append_event(
|
return await transcripts_controller.append_event(
|
||||||
transcript=transcript,
|
transcript=transcript,
|
||||||
event="TOPIC",
|
event="TOPIC",
|
||||||
data=topic,
|
data=get_topic,
|
||||||
)
|
)
|
||||||
|
|
||||||
@broadcast_to_sockets
|
@broadcast_to_sockets
|
||||||
|
|||||||
@@ -4,18 +4,22 @@ Transcripts websocket API
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
|
||||||
|
|
||||||
import reflector.auth as auth
|
import reflector.auth as auth
|
||||||
from reflector.db.transcripts import transcripts_controller
|
from reflector.db.transcripts import transcripts_controller
|
||||||
|
from reflector.ws_events import TranscriptWsEvent
|
||||||
from reflector.ws_manager import get_ws_manager
|
from reflector.ws_manager import get_ws_manager
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/transcripts/{transcript_id}/events")
|
@router.get(
|
||||||
|
"/transcripts/{transcript_id}/events",
|
||||||
|
response_model=TranscriptWsEvent,
|
||||||
|
summary="Transcript WebSocket event schema",
|
||||||
|
description="Stub exposing the discriminated union of all transcript-level WS events for OpenAPI type generation. Real events are delivered over the WebSocket at the same path.",
|
||||||
|
)
|
||||||
async def transcript_get_websocket_events(transcript_id: str):
|
async def transcript_get_websocket_events(transcript_id: str):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -24,8 +28,9 @@ async def transcript_get_websocket_events(transcript_id: str):
|
|||||||
async def transcript_events_websocket(
|
async def transcript_events_websocket(
|
||||||
transcript_id: str,
|
transcript_id: str,
|
||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
user: Optional[auth.UserInfo] = Depends(auth.current_user_optional),
|
|
||||||
):
|
):
|
||||||
|
_, negotiated_subprotocol = auth.parse_ws_bearer_token(websocket)
|
||||||
|
user = await auth.current_user_ws_optional(websocket)
|
||||||
user_id = user["sub"] if user else None
|
user_id = user["sub"] if user else None
|
||||||
transcript = await transcripts_controller.get_by_id_for_http(
|
transcript = await transcripts_controller.get_by_id_for_http(
|
||||||
transcript_id, user_id=user_id
|
transcript_id, user_id=user_id
|
||||||
@@ -37,7 +42,9 @@ async def transcript_events_websocket(
|
|||||||
# use ts:transcript_id as room id
|
# use ts:transcript_id as room id
|
||||||
room_id = f"ts:{transcript_id}"
|
room_id = f"ts:{transcript_id}"
|
||||||
ws_manager = get_ws_manager()
|
ws_manager = get_ws_manager()
|
||||||
await ws_manager.add_user_to_room(room_id, websocket)
|
await ws_manager.add_user_to_room(
|
||||||
|
room_id, websocket, subprotocol=negotiated_subprotocol
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# on first connection, send all events only to the current user
|
# on first connection, send all events only to the current user
|
||||||
|
|||||||
@@ -4,10 +4,22 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|||||||
|
|
||||||
from reflector.auth.auth_jwt import JWTAuth # type: ignore
|
from reflector.auth.auth_jwt import JWTAuth # type: ignore
|
||||||
from reflector.db.users import user_controller
|
from reflector.db.users import user_controller
|
||||||
|
from reflector.ws_events import UserWsEvent
|
||||||
from reflector.ws_manager import get_ws_manager
|
from reflector.ws_manager import get_ws_manager
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/events",
|
||||||
|
response_model=UserWsEvent,
|
||||||
|
summary="User WebSocket event schema",
|
||||||
|
description="Stub exposing the discriminated union of all user-level WS events for OpenAPI type generation. Real events are delivered over the WebSocket at the same path.",
|
||||||
|
)
|
||||||
|
async def user_get_websocket_events():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Close code for unauthorized WebSocket connections
|
# Close code for unauthorized WebSocket connections
|
||||||
UNAUTHORISED = 4401
|
UNAUTHORISED = 4401
|
||||||
|
|
||||||
|
|||||||
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"),
|
||||||
|
]
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Topic, FinalSummary, Status } from "./webSocketTypes";
|
import { Topic, FinalSummary, Status } from "./webSocketTypes";
|
||||||
import { useError } from "../../(errors)/errorContext";
|
import { useError } from "../../(errors)/errorContext";
|
||||||
import type { components } from "../../reflector-api";
|
import type { components, operations } from "../../reflector-api";
|
||||||
type AudioWaveform = components["schemas"]["AudioWaveform"];
|
type AudioWaveform = components["schemas"]["AudioWaveform"];
|
||||||
type GetTranscriptSegmentTopic =
|
type GetTranscriptSegmentTopic =
|
||||||
components["schemas"]["GetTranscriptSegmentTopic"];
|
components["schemas"]["GetTranscriptSegmentTopic"];
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { $api, WEBSOCKET_URL } from "../../lib/apiClient";
|
import { WEBSOCKET_URL } from "../../lib/apiClient";
|
||||||
import {
|
import {
|
||||||
invalidateTranscript,
|
invalidateTranscript,
|
||||||
invalidateTranscriptTopics,
|
invalidateTranscriptTopics,
|
||||||
invalidateTranscriptWaveform,
|
invalidateTranscriptWaveform,
|
||||||
} from "../../lib/apiHooks";
|
} from "../../lib/apiHooks";
|
||||||
import { NonEmptyString } from "../../lib/utils";
|
import { useAuth } from "../../lib/AuthProvider";
|
||||||
|
import { parseNonEmptyString } from "../../lib/utils";
|
||||||
|
|
||||||
|
type TranscriptWsEvent =
|
||||||
|
operations["v1_transcript_get_websocket_events"]["responses"][200]["content"]["application/json"];
|
||||||
|
|
||||||
export type UseWebSockets = {
|
export type UseWebSockets = {
|
||||||
transcriptTextLive: string;
|
transcriptTextLive: string;
|
||||||
@@ -27,6 +31,7 @@ export type UseWebSockets = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
|
export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
|
||||||
|
const auth = useAuth();
|
||||||
const [transcriptTextLive, setTranscriptTextLive] = useState<string>("");
|
const [transcriptTextLive, setTranscriptTextLive] = useState<string>("");
|
||||||
const [translateText, setTranslateText] = useState<string>("");
|
const [translateText, setTranslateText] = useState<string>("");
|
||||||
const [title, setTitle] = useState<string>("");
|
const [title, setTitle] = useState<string>("");
|
||||||
@@ -331,156 +336,168 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!transcriptId) return;
|
if (!transcriptId) return;
|
||||||
|
const tsId = parseNonEmptyString(transcriptId);
|
||||||
|
|
||||||
|
const MAX_RETRIES = 10;
|
||||||
const url = `${WEBSOCKET_URL}/v1/transcripts/${transcriptId}/events`;
|
const url = `${WEBSOCKET_URL}/v1/transcripts/${transcriptId}/events`;
|
||||||
let ws = new WebSocket(url);
|
let ws: WebSocket | null = null;
|
||||||
|
let retryCount = 0;
|
||||||
|
let retryTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let intentionalClose = false;
|
||||||
|
|
||||||
ws.onopen = () => {
|
const connect = () => {
|
||||||
console.debug("WebSocket connection opened");
|
const subprotocols = auth.accessToken
|
||||||
};
|
? ["bearer", auth.accessToken]
|
||||||
|
: undefined;
|
||||||
|
ws = new WebSocket(url, subprotocols);
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onopen = () => {
|
||||||
const message = JSON.parse(event.data);
|
console.debug("WebSocket connection opened");
|
||||||
|
retryCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
ws.onmessage = (event) => {
|
||||||
switch (message.event) {
|
const message: TranscriptWsEvent = JSON.parse(event.data);
|
||||||
case "TRANSCRIPT":
|
|
||||||
const newText = (message.data.text ?? "").trim();
|
|
||||||
const newTranslation = (message.data.translation ?? "").trim();
|
|
||||||
|
|
||||||
if (!newText) break;
|
try {
|
||||||
|
switch (message.event) {
|
||||||
|
case "TRANSCRIPT": {
|
||||||
|
const newText = (message.data.text ?? "").trim();
|
||||||
|
const newTranslation = (message.data.translation ?? "").trim();
|
||||||
|
|
||||||
console.debug("TRANSCRIPT event:", newText);
|
if (!newText) break;
|
||||||
setTextQueue((prevQueue) => [...prevQueue, newText]);
|
|
||||||
setTranslationQueue((prevQueue) => [...prevQueue, newTranslation]);
|
|
||||||
|
|
||||||
setAccumulatedText((prevText) => prevText + " " + newText);
|
console.debug("TRANSCRIPT event:", newText);
|
||||||
break;
|
setTextQueue((prevQueue) => [...prevQueue, newText]);
|
||||||
|
setTranslationQueue((prevQueue) => [
|
||||||
|
...prevQueue,
|
||||||
|
newTranslation,
|
||||||
|
]);
|
||||||
|
|
||||||
case "TOPIC":
|
setAccumulatedText((prevText) => prevText + " " + newText);
|
||||||
setTopics((prevTopics) => {
|
break;
|
||||||
const topic = message.data as Topic;
|
|
||||||
const index = prevTopics.findIndex(
|
|
||||||
(prevTopic) => prevTopic.id === topic.id,
|
|
||||||
);
|
|
||||||
if (index >= 0) {
|
|
||||||
prevTopics[index] = topic;
|
|
||||||
return prevTopics;
|
|
||||||
}
|
|
||||||
setAccumulatedText((prevText) =>
|
|
||||||
prevText.slice(topic.transcript.length),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [...prevTopics, topic];
|
|
||||||
});
|
|
||||||
console.debug("TOPIC event:", message.data);
|
|
||||||
// Invalidate topics query to sync with WebSocket data
|
|
||||||
invalidateTranscriptTopics(
|
|
||||||
queryClient,
|
|
||||||
transcriptId as NonEmptyString,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "FINAL_SHORT_SUMMARY":
|
|
||||||
console.debug("FINAL_SHORT_SUMMARY event:", message.data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "FINAL_LONG_SUMMARY":
|
|
||||||
if (message.data) {
|
|
||||||
setFinalSummary(message.data);
|
|
||||||
// Invalidate transcript query to sync summary
|
|
||||||
invalidateTranscript(queryClient, transcriptId as NonEmptyString);
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
case "FINAL_TITLE":
|
case "TOPIC":
|
||||||
console.debug("FINAL_TITLE event:", message.data);
|
setTopics((prevTopics) => {
|
||||||
if (message.data) {
|
const topic = message.data;
|
||||||
|
const index = prevTopics.findIndex(
|
||||||
|
(prevTopic) => prevTopic.id === topic.id,
|
||||||
|
);
|
||||||
|
if (index >= 0) {
|
||||||
|
prevTopics[index] = topic;
|
||||||
|
return prevTopics;
|
||||||
|
}
|
||||||
|
setAccumulatedText((prevText) =>
|
||||||
|
prevText.slice(topic.transcript?.length ?? 0),
|
||||||
|
);
|
||||||
|
return [...prevTopics, topic];
|
||||||
|
});
|
||||||
|
console.debug("TOPIC event:", message.data);
|
||||||
|
invalidateTranscriptTopics(queryClient, tsId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "FINAL_SHORT_SUMMARY":
|
||||||
|
console.debug("FINAL_SHORT_SUMMARY event:", message.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "FINAL_LONG_SUMMARY":
|
||||||
|
setFinalSummary({ summary: message.data.long_summary });
|
||||||
|
invalidateTranscript(queryClient, tsId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "FINAL_TITLE":
|
||||||
|
console.debug("FINAL_TITLE event:", message.data);
|
||||||
setTitle(message.data.title);
|
setTitle(message.data.title);
|
||||||
// Invalidate transcript query to sync title
|
invalidateTranscript(queryClient, tsId);
|
||||||
invalidateTranscript(queryClient, transcriptId as NonEmptyString);
|
break;
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "WAVEFORM":
|
case "WAVEFORM":
|
||||||
console.debug(
|
console.debug(
|
||||||
"WAVEFORM event length:",
|
"WAVEFORM event length:",
|
||||||
message.data.waveform.length,
|
message.data.waveform.length,
|
||||||
);
|
|
||||||
if (message.data) {
|
|
||||||
setWaveForm(message.data.waveform);
|
|
||||||
invalidateTranscriptWaveform(
|
|
||||||
queryClient,
|
|
||||||
transcriptId as NonEmptyString,
|
|
||||||
);
|
);
|
||||||
}
|
setWaveForm({ data: message.data.waveform });
|
||||||
break;
|
invalidateTranscriptWaveform(queryClient, tsId);
|
||||||
case "DURATION":
|
break;
|
||||||
console.debug("DURATION event:", message.data);
|
|
||||||
if (message.data) {
|
case "DURATION":
|
||||||
|
console.debug("DURATION event:", message.data);
|
||||||
setDuration(message.data.duration);
|
setDuration(message.data.duration);
|
||||||
}
|
break;
|
||||||
break;
|
|
||||||
|
|
||||||
case "STATUS":
|
case "STATUS":
|
||||||
console.log("STATUS event:", message.data);
|
console.log("STATUS event:", message.data);
|
||||||
if (message.data.value === "error") {
|
if (message.data.value === "error") {
|
||||||
setError(
|
setError(
|
||||||
Error("Websocket error status"),
|
Error("Websocket error status"),
|
||||||
"There was an error processing this meeting.",
|
"There was an error processing this meeting.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setStatus(message.data);
|
||||||
|
invalidateTranscript(queryClient, tsId);
|
||||||
|
if (message.data.value === "ended") {
|
||||||
|
intentionalClose = true;
|
||||||
|
ws?.close();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ACTION_ITEMS":
|
||||||
|
console.debug("ACTION_ITEMS event:", message.data);
|
||||||
|
invalidateTranscript(queryClient, tsId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
const _exhaustive: never = message;
|
||||||
|
console.warn(
|
||||||
|
`Received unknown WebSocket event: ${(_exhaustive as TranscriptWsEvent).event}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setStatus(message.data);
|
}
|
||||||
invalidateTranscript(queryClient, transcriptId as NonEmptyString);
|
} catch (error) {
|
||||||
if (message.data.value === "ended") {
|
setError(error);
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
setError(
|
|
||||||
new Error(`Received unknown WebSocket event: ${message.event}`),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
setError(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error("WebSocket error:", error);
|
console.error("WebSocket error:", error);
|
||||||
setError(new Error("A WebSocket error occurred."));
|
};
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
ws.onclose = (event) => {
|
||||||
console.debug("WebSocket connection closed");
|
console.debug("WebSocket connection closed, code:", event.code);
|
||||||
switch (event.code) {
|
if (intentionalClose) return;
|
||||||
case 1000: // Normal Closure:
|
|
||||||
break;
|
const normalCodes = [1000, 1001, 1005];
|
||||||
case 1005: // Closure by client FF
|
if (normalCodes.includes(event.code)) return;
|
||||||
break;
|
|
||||||
case 1001: // Navigate away
|
if (retryCount < MAX_RETRIES) {
|
||||||
break;
|
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
|
||||||
case 1006: // Closed by client Chrome
|
console.log(
|
||||||
console.warn(
|
`WebSocket reconnecting in ${delay}ms (attempt ${retryCount + 1}/${MAX_RETRIES})`,
|
||||||
"WebSocket closed by client, likely duplicated connection in react dev mode",
|
|
||||||
);
|
);
|
||||||
break;
|
if (retryCount === 0) {
|
||||||
default:
|
setError(
|
||||||
|
new Error("WebSocket connection lost"),
|
||||||
|
"Connection lost. Reconnecting...",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
retryCount++;
|
||||||
|
retryTimeout = setTimeout(connect, delay);
|
||||||
|
} else {
|
||||||
setError(
|
setError(
|
||||||
new Error(`WebSocket closed unexpectedly with code: ${event.code}`),
|
new Error(`WebSocket closed unexpectedly with code: ${event.code}`),
|
||||||
"Disconnected from the server. Please refresh the page.",
|
"Disconnected from the server. Please refresh the page.",
|
||||||
);
|
);
|
||||||
console.log(
|
}
|
||||||
"Socket is closed. Reconnect will be attempted in 1 second.",
|
};
|
||||||
event.reason,
|
|
||||||
);
|
|
||||||
// todo handle reconnect with socket.io
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
connect();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ws.close();
|
intentionalClose = true;
|
||||||
|
if (retryTimeout) clearTimeout(retryTimeout);
|
||||||
|
ws?.close();
|
||||||
};
|
};
|
||||||
}, [transcriptId]);
|
}, [transcriptId]);
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import React, { useEffect, useRef } from "react";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { WEBSOCKET_URL } from "./apiClient";
|
import { WEBSOCKET_URL } from "./apiClient";
|
||||||
import { useAuth } from "./AuthProvider";
|
import { useAuth } from "./AuthProvider";
|
||||||
import { z } from "zod";
|
import { invalidateTranscript, invalidateTranscriptLists } from "./apiHooks";
|
||||||
import { invalidateTranscriptLists, TRANSCRIPT_SEARCH_URL } from "./apiHooks";
|
import { parseNonEmptyString } from "./utils";
|
||||||
|
import type { operations } from "../reflector-api";
|
||||||
|
|
||||||
const UserEvent = z.object({
|
type UserWsEvent =
|
||||||
event: z.string(),
|
operations["v1_user_get_websocket_events"]["responses"][200]["content"]["application/json"];
|
||||||
});
|
|
||||||
|
|
||||||
type UserEvent = z.TypeOf<typeof UserEvent>;
|
|
||||||
|
|
||||||
class UserEventsStore {
|
class UserEventsStore {
|
||||||
private socket: WebSocket | null = null;
|
private socket: WebSocket | null = null;
|
||||||
@@ -133,23 +131,26 @@ export function UserEventsProvider({
|
|||||||
if (!detachRef.current) {
|
if (!detachRef.current) {
|
||||||
const onMessage = (event: MessageEvent) => {
|
const onMessage = (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
const msg = UserEvent.parse(JSON.parse(event.data));
|
const msg: UserWsEvent = JSON.parse(event.data);
|
||||||
const eventName = msg.event;
|
|
||||||
|
|
||||||
const invalidateList = () => invalidateTranscriptLists(queryClient);
|
switch (msg.event) {
|
||||||
|
|
||||||
switch (eventName) {
|
|
||||||
case "TRANSCRIPT_CREATED":
|
case "TRANSCRIPT_CREATED":
|
||||||
case "TRANSCRIPT_DELETED":
|
case "TRANSCRIPT_DELETED":
|
||||||
case "TRANSCRIPT_STATUS":
|
case "TRANSCRIPT_STATUS":
|
||||||
case "TRANSCRIPT_FINAL_TITLE":
|
case "TRANSCRIPT_FINAL_TITLE":
|
||||||
case "TRANSCRIPT_DURATION":
|
case "TRANSCRIPT_DURATION":
|
||||||
invalidateList().then(() => {});
|
invalidateTranscriptLists(queryClient).then(() => {});
|
||||||
break;
|
invalidateTranscript(
|
||||||
|
queryClient,
|
||||||
default:
|
parseNonEmptyString(msg.data.id),
|
||||||
// Ignore other content events for list updates
|
).then(() => {});
|
||||||
break;
|
break;
|
||||||
|
default: {
|
||||||
|
const _exhaustive: never = msg;
|
||||||
|
console.warn(
|
||||||
|
`Unknown user event: ${(_exhaustive as UserWsEvent).event}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Invalid user event message", event.data);
|
console.warn("Invalid user event message", event.data);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { components } from "../reflector-api";
|
|||||||
import { useAuth } from "./AuthProvider";
|
import { useAuth } from "./AuthProvider";
|
||||||
import { MeetingId } from "./types";
|
import { MeetingId } from "./types";
|
||||||
import { NonEmptyString } from "./utils";
|
import { NonEmptyString } from "./utils";
|
||||||
|
import type { TranscriptStatus } from "./transcript";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* XXX error types returned from the hooks are not always correct; declared types are ValidationError but real type could be string or any other
|
* XXX error types returned from the hooks are not always correct; declared types are ValidationError but real type could be string or any other
|
||||||
@@ -104,6 +105,12 @@ export function useTranscriptProcess() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ACTIVE_TRANSCRIPT_STATUSES = new Set<TranscriptStatus>([
|
||||||
|
"processing",
|
||||||
|
"uploaded",
|
||||||
|
"recording",
|
||||||
|
]);
|
||||||
|
|
||||||
export function useTranscriptGet(transcriptId: NonEmptyString | null) {
|
export function useTranscriptGet(transcriptId: NonEmptyString | null) {
|
||||||
return $api.useQuery(
|
return $api.useQuery(
|
||||||
"get",
|
"get",
|
||||||
@@ -117,6 +124,10 @@ export function useTranscriptGet(transcriptId: NonEmptyString | null) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!transcriptId,
|
enabled: !!transcriptId,
|
||||||
|
refetchInterval: (query) => {
|
||||||
|
const status = query.state.data?.status;
|
||||||
|
return status && ACTIVE_TRANSCRIPT_STATUSES.has(status) ? 5000 : false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
271
www/app/reflector-api.d.ts
vendored
271
www/app/reflector-api.d.ts
vendored
@@ -568,7 +568,10 @@ export interface paths {
|
|||||||
path?: never;
|
path?: never;
|
||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
/** Transcript Get Websocket Events */
|
/**
|
||||||
|
* Transcript WebSocket event schema
|
||||||
|
* @description Stub exposing the discriminated union of all transcript-level WS events for OpenAPI type generation. Real events are delivered over the WebSocket at the same path.
|
||||||
|
*/
|
||||||
get: operations["v1_transcript_get_websocket_events"];
|
get: operations["v1_transcript_get_websocket_events"];
|
||||||
put?: never;
|
put?: never;
|
||||||
post?: never;
|
post?: never;
|
||||||
@@ -664,6 +667,26 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/v1/events": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* User WebSocket event schema
|
||||||
|
* @description Stub exposing the discriminated union of all user-level WS events for OpenAPI type generation. Real events are delivered over the WebSocket at the same path.
|
||||||
|
*/
|
||||||
|
get: operations["v1_user_get_websocket_events"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/v1/zulip/streams": {
|
"/v1/zulip/streams": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -1877,6 +1900,33 @@ export interface components {
|
|||||||
/** Name */
|
/** Name */
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
/** TranscriptActionItems */
|
||||||
|
TranscriptActionItems: {
|
||||||
|
/** Action Items */
|
||||||
|
action_items: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** TranscriptDuration */
|
||||||
|
TranscriptDuration: {
|
||||||
|
/** Duration */
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
/** TranscriptFinalLongSummary */
|
||||||
|
TranscriptFinalLongSummary: {
|
||||||
|
/** Long Summary */
|
||||||
|
long_summary: string;
|
||||||
|
};
|
||||||
|
/** TranscriptFinalShortSummary */
|
||||||
|
TranscriptFinalShortSummary: {
|
||||||
|
/** Short Summary */
|
||||||
|
short_summary: string;
|
||||||
|
};
|
||||||
|
/** TranscriptFinalTitle */
|
||||||
|
TranscriptFinalTitle: {
|
||||||
|
/** Title */
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
/** TranscriptParticipant */
|
/** TranscriptParticipant */
|
||||||
TranscriptParticipant: {
|
TranscriptParticipant: {
|
||||||
/** Id */
|
/** Id */
|
||||||
@@ -1917,6 +1967,113 @@ export interface components {
|
|||||||
/** End */
|
/** End */
|
||||||
end: number;
|
end: number;
|
||||||
};
|
};
|
||||||
|
/** TranscriptText */
|
||||||
|
TranscriptText: {
|
||||||
|
/** Text */
|
||||||
|
text: string;
|
||||||
|
/** Translation */
|
||||||
|
translation: string | null;
|
||||||
|
};
|
||||||
|
/** TranscriptWaveform */
|
||||||
|
TranscriptWaveform: {
|
||||||
|
/** Waveform */
|
||||||
|
waveform: number[];
|
||||||
|
};
|
||||||
|
/** TranscriptWsActionItems */
|
||||||
|
TranscriptWsActionItems: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "ACTION_ITEMS";
|
||||||
|
data: components["schemas"]["TranscriptActionItems"];
|
||||||
|
};
|
||||||
|
/** TranscriptWsDuration */
|
||||||
|
TranscriptWsDuration: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "DURATION";
|
||||||
|
data: components["schemas"]["TranscriptDuration"];
|
||||||
|
};
|
||||||
|
/** TranscriptWsFinalLongSummary */
|
||||||
|
TranscriptWsFinalLongSummary: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "FINAL_LONG_SUMMARY";
|
||||||
|
data: components["schemas"]["TranscriptFinalLongSummary"];
|
||||||
|
};
|
||||||
|
/** TranscriptWsFinalShortSummary */
|
||||||
|
TranscriptWsFinalShortSummary: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "FINAL_SHORT_SUMMARY";
|
||||||
|
data: components["schemas"]["TranscriptFinalShortSummary"];
|
||||||
|
};
|
||||||
|
/** TranscriptWsFinalTitle */
|
||||||
|
TranscriptWsFinalTitle: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "FINAL_TITLE";
|
||||||
|
data: components["schemas"]["TranscriptFinalTitle"];
|
||||||
|
};
|
||||||
|
/** TranscriptWsStatus */
|
||||||
|
TranscriptWsStatus: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "STATUS";
|
||||||
|
data: components["schemas"]["TranscriptWsStatusData"];
|
||||||
|
};
|
||||||
|
/** TranscriptWsStatusData */
|
||||||
|
TranscriptWsStatusData: {
|
||||||
|
/**
|
||||||
|
* Value
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
value:
|
||||||
|
| "idle"
|
||||||
|
| "uploaded"
|
||||||
|
| "recording"
|
||||||
|
| "processing"
|
||||||
|
| "error"
|
||||||
|
| "ended";
|
||||||
|
};
|
||||||
|
/** TranscriptWsTopic */
|
||||||
|
TranscriptWsTopic: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "TOPIC";
|
||||||
|
data: components["schemas"]["GetTranscriptTopic"];
|
||||||
|
};
|
||||||
|
/** TranscriptWsTranscript */
|
||||||
|
TranscriptWsTranscript: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "TRANSCRIPT";
|
||||||
|
data: components["schemas"]["TranscriptText"];
|
||||||
|
};
|
||||||
|
/** TranscriptWsWaveform */
|
||||||
|
TranscriptWsWaveform: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "WAVEFORM";
|
||||||
|
data: components["schemas"]["TranscriptWaveform"];
|
||||||
|
};
|
||||||
/** UpdateParticipant */
|
/** UpdateParticipant */
|
||||||
UpdateParticipant: {
|
UpdateParticipant: {
|
||||||
/** Speaker */
|
/** Speaker */
|
||||||
@@ -1987,6 +2144,82 @@ export interface components {
|
|||||||
/** Email */
|
/** Email */
|
||||||
email: string | null;
|
email: string | null;
|
||||||
};
|
};
|
||||||
|
/** UserTranscriptCreatedData */
|
||||||
|
UserTranscriptCreatedData: {
|
||||||
|
/** Id */
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
/** UserTranscriptDeletedData */
|
||||||
|
UserTranscriptDeletedData: {
|
||||||
|
/** Id */
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
/** UserTranscriptDurationData */
|
||||||
|
UserTranscriptDurationData: {
|
||||||
|
/** Id */
|
||||||
|
id: string;
|
||||||
|
/** Duration */
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
/** UserTranscriptFinalTitleData */
|
||||||
|
UserTranscriptFinalTitleData: {
|
||||||
|
/** Id */
|
||||||
|
id: string;
|
||||||
|
/** Title */
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
/** UserTranscriptStatusData */
|
||||||
|
UserTranscriptStatusData: {
|
||||||
|
/** Id */
|
||||||
|
id: string;
|
||||||
|
/** Value */
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
/** UserWsTranscriptCreated */
|
||||||
|
UserWsTranscriptCreated: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "TRANSCRIPT_CREATED";
|
||||||
|
data: components["schemas"]["UserTranscriptCreatedData"];
|
||||||
|
};
|
||||||
|
/** UserWsTranscriptDeleted */
|
||||||
|
UserWsTranscriptDeleted: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "TRANSCRIPT_DELETED";
|
||||||
|
data: components["schemas"]["UserTranscriptDeletedData"];
|
||||||
|
};
|
||||||
|
/** UserWsTranscriptDuration */
|
||||||
|
UserWsTranscriptDuration: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "TRANSCRIPT_DURATION";
|
||||||
|
data: components["schemas"]["UserTranscriptDurationData"];
|
||||||
|
};
|
||||||
|
/** UserWsTranscriptFinalTitle */
|
||||||
|
UserWsTranscriptFinalTitle: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "TRANSCRIPT_FINAL_TITLE";
|
||||||
|
data: components["schemas"]["UserTranscriptFinalTitleData"];
|
||||||
|
};
|
||||||
|
/** UserWsTranscriptStatus */
|
||||||
|
UserWsTranscriptStatus: {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
event: "TRANSCRIPT_STATUS";
|
||||||
|
data: components["schemas"]["UserTranscriptStatusData"];
|
||||||
|
};
|
||||||
/** ValidationError */
|
/** ValidationError */
|
||||||
ValidationError: {
|
ValidationError: {
|
||||||
/** Location */
|
/** Location */
|
||||||
@@ -3423,7 +3656,16 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": unknown;
|
"application/json":
|
||||||
|
| components["schemas"]["TranscriptWsTranscript"]
|
||||||
|
| components["schemas"]["TranscriptWsTopic"]
|
||||||
|
| components["schemas"]["TranscriptWsStatus"]
|
||||||
|
| components["schemas"]["TranscriptWsFinalTitle"]
|
||||||
|
| components["schemas"]["TranscriptWsFinalLongSummary"]
|
||||||
|
| components["schemas"]["TranscriptWsFinalShortSummary"]
|
||||||
|
| components["schemas"]["TranscriptWsActionItems"]
|
||||||
|
| components["schemas"]["TranscriptWsDuration"]
|
||||||
|
| components["schemas"]["TranscriptWsWaveform"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
@@ -3607,6 +3849,31 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
v1_user_get_websocket_events: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json":
|
||||||
|
| components["schemas"]["UserWsTranscriptCreated"]
|
||||||
|
| components["schemas"]["UserWsTranscriptDeleted"]
|
||||||
|
| components["schemas"]["UserWsTranscriptStatus"]
|
||||||
|
| components["schemas"]["UserWsTranscriptFinalTitle"]
|
||||||
|
| components["schemas"]["UserWsTranscriptDuration"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
v1_zulip_get_streams: {
|
v1_zulip_get_streams: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
Reference in New Issue
Block a user