mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-17 02:36:54 +00:00
Compare commits
30 Commits
fix/standa
...
local-llm-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d4c5c463c | ||
|
|
f6a23cfddd | ||
|
|
b1405af8c7 | ||
|
|
71ad8a294f | ||
|
|
bba272505f | ||
|
|
67aea78243 | ||
|
|
2d81321733 | ||
|
|
8c2b720564 | ||
|
|
88e945ec00 | ||
|
|
f6201dd378 | ||
|
|
9f62959069 | ||
|
|
0353c23a94 | ||
|
|
7372f80530 | ||
|
|
208361c8cc | ||
|
|
70d17997ef | ||
| adc4c20bf4 | |||
|
|
ec4f356b4c | ||
|
|
39573626e9 | ||
|
|
d9aa6d6eb0 | ||
|
|
e1ea914675 | ||
|
|
7200f3c65f | ||
|
|
2f669dfd89 | ||
|
|
d25d77333c | ||
|
|
427254fe33 | ||
|
|
46750abad9 | ||
|
|
f36b95b09f | ||
|
|
608a3805c5 | ||
|
|
d0af8ffdb7 | ||
|
|
33a93db802 | ||
|
|
663345ece6 |
10
COMPOSE_STANDALONE_TODO.md
Normal file
10
COMPOSE_STANDALONE_TODO.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Standalone Compose: Remaining Production Work
|
||||||
|
|
||||||
|
## Server/worker/beat: remove host network mode + bind mounts
|
||||||
|
|
||||||
|
Currently `server` uses `network_mode: host` and all three services bind-mount `./server/:/app/`. For full standalone prod:
|
||||||
|
|
||||||
|
- Remove `network_mode: host` from server
|
||||||
|
- Remove bind-mount volumes from server, worker, beat (use built image only)
|
||||||
|
- Update `compose_cmd` in `setup-standalone.sh` to not rely on host network
|
||||||
|
- Change `SERVER_API_URL` from `http://host.docker.internal:1250` to `http://server:1250` (server reachable via Docker network once off host mode)
|
||||||
@@ -1,128 +1,11 @@
|
|||||||
# Self-contained standalone compose for fully local deployment (no external dependencies).
|
# Standalone services for fully local deployment (no external dependencies).
|
||||||
# Usage: docker compose -f docker-compose.standalone.yml up -d
|
# Usage: docker compose -f docker-compose.yml -f docker-compose.standalone.yml up -d
|
||||||
#
|
#
|
||||||
# On Linux with NVIDIA GPU, also pass: --profile ollama-gpu
|
# On Linux with NVIDIA GPU, also pass: --profile ollama-gpu
|
||||||
# On Linux without GPU: --profile ollama-cpu
|
# On Linux without GPU: --profile ollama-cpu
|
||||||
# On Mac: Ollama runs natively (Metal GPU) — no profile needed, services here unused.
|
# On Mac: Ollama runs natively (Metal GPU) — no profile needed, services here unused.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
server:
|
|
||||||
build:
|
|
||||||
context: server
|
|
||||||
ports:
|
|
||||||
- "1250:1250"
|
|
||||||
extra_hosts:
|
|
||||||
- "host.docker.internal:host-gateway"
|
|
||||||
volumes:
|
|
||||||
- ./server/:/app/
|
|
||||||
- /app/.venv
|
|
||||||
env_file:
|
|
||||||
- ./server/.env
|
|
||||||
environment:
|
|
||||||
ENTRYPOINT: server
|
|
||||||
# Docker DNS names instead of localhost
|
|
||||||
DATABASE_URL: postgresql+asyncpg://reflector:reflector@postgres:5432/reflector
|
|
||||||
REDIS_HOST: redis
|
|
||||||
CELERY_BROKER_URL: redis://redis:6379/1
|
|
||||||
CELERY_RESULT_BACKEND: redis://redis:6379/1
|
|
||||||
# Standalone doesn't run Hatchet
|
|
||||||
HATCHET_CLIENT_SERVER_URL: ""
|
|
||||||
HATCHET_CLIENT_HOST_PORT: ""
|
|
||||||
# Self-hosted transcription/diarization via CPU service
|
|
||||||
TRANSCRIPT_BACKEND: modal
|
|
||||||
TRANSCRIPT_URL: http://cpu:8000
|
|
||||||
TRANSCRIPT_MODAL_API_KEY: local
|
|
||||||
DIARIZATION_BACKEND: modal
|
|
||||||
DIARIZATION_URL: http://cpu:8000
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_started
|
|
||||||
|
|
||||||
worker:
|
|
||||||
build:
|
|
||||||
context: server
|
|
||||||
volumes:
|
|
||||||
- ./server/:/app/
|
|
||||||
- /app/.venv
|
|
||||||
env_file:
|
|
||||||
- ./server/.env
|
|
||||||
environment:
|
|
||||||
ENTRYPOINT: worker
|
|
||||||
HATCHET_CLIENT_SERVER_URL: ""
|
|
||||||
HATCHET_CLIENT_HOST_PORT: ""
|
|
||||||
TRANSCRIPT_BACKEND: modal
|
|
||||||
TRANSCRIPT_URL: http://cpu:8000
|
|
||||||
TRANSCRIPT_MODAL_API_KEY: local
|
|
||||||
DIARIZATION_BACKEND: modal
|
|
||||||
DIARIZATION_URL: http://cpu:8000
|
|
||||||
depends_on:
|
|
||||||
redis:
|
|
||||||
condition: service_started
|
|
||||||
|
|
||||||
beat:
|
|
||||||
build:
|
|
||||||
context: server
|
|
||||||
volumes:
|
|
||||||
- ./server/:/app/
|
|
||||||
- /app/.venv
|
|
||||||
env_file:
|
|
||||||
- ./server/.env
|
|
||||||
environment:
|
|
||||||
ENTRYPOINT: beat
|
|
||||||
depends_on:
|
|
||||||
redis:
|
|
||||||
condition: service_started
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7.2
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:17
|
|
||||||
command: postgres -c 'max_connections=200'
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: reflector
|
|
||||||
POSTGRES_PASSWORD: reflector
|
|
||||||
POSTGRES_DB: reflector
|
|
||||||
volumes:
|
|
||||||
- ./data/postgres:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -d reflector -U reflector"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
start_period: 15s
|
|
||||||
|
|
||||||
web:
|
|
||||||
image: reflector-frontend-standalone
|
|
||||||
build:
|
|
||||||
context: ./www
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
command: ["node", "server.js"]
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
# Browser-facing URLs (host-accessible ports)
|
|
||||||
API_URL: http://localhost:1250
|
|
||||||
WEBSOCKET_URL: ws://localhost:1250
|
|
||||||
SITE_URL: http://localhost:3000
|
|
||||||
# Server-side URLs (docker-network internal)
|
|
||||||
SERVER_API_URL: http://server:1250
|
|
||||||
KV_URL: redis://redis:6379
|
|
||||||
KV_USE_TLS: "false"
|
|
||||||
# Standalone: no external auth provider
|
|
||||||
FEATURE_REQUIRE_LOGIN: "false"
|
|
||||||
NEXTAUTH_URL: http://localhost:3000
|
|
||||||
NEXTAUTH_SECRET: standalone-local-secret
|
|
||||||
# Nullify partial auth vars inherited from base env_file
|
|
||||||
AUTHENTIK_ISSUER: ""
|
|
||||||
AUTHENTIK_REFRESH_TOKEN_URL: ""
|
|
||||||
|
|
||||||
garage:
|
garage:
|
||||||
image: dxflrs/garage:v1.1.0
|
image: dxflrs/garage:v1.1.0
|
||||||
ports:
|
ports:
|
||||||
@@ -140,6 +23,68 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
start_period: 5s
|
start_period: 5s
|
||||||
|
|
||||||
|
ollama:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
profiles: ["ollama-gpu"]
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama_data:/root/.ollama
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
ollama-cpu:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
profiles: ["ollama-cpu"]
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama_data:/root/.ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Override server/worker/beat to use self-hosted GPU service for transcription+diarization.
|
||||||
|
# compose `environment:` overrides values from `env_file:` — no need to edit server/.env.
|
||||||
|
server:
|
||||||
|
environment:
|
||||||
|
TRANSCRIPT_BACKEND: modal
|
||||||
|
TRANSCRIPT_URL: http://localhost:8100
|
||||||
|
TRANSCRIPT_MODAL_API_KEY: local
|
||||||
|
DIARIZATION_BACKEND: modal
|
||||||
|
DIARIZATION_URL: http://localhost:8100
|
||||||
|
|
||||||
|
worker:
|
||||||
|
environment:
|
||||||
|
TRANSCRIPT_BACKEND: modal
|
||||||
|
TRANSCRIPT_URL: http://cpu:8000
|
||||||
|
TRANSCRIPT_MODAL_API_KEY: local
|
||||||
|
DIARIZATION_BACKEND: modal
|
||||||
|
DIARIZATION_URL: http://cpu:8000
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: reflector-frontend-standalone
|
||||||
|
build:
|
||||||
|
context: ./www
|
||||||
|
command: ["node", "server.js"]
|
||||||
|
volumes: !reset []
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
|
||||||
cpu:
|
cpu:
|
||||||
build:
|
build:
|
||||||
context: ./gpu/self_hosted
|
context: ./gpu/self_hosted
|
||||||
@@ -177,41 +122,6 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
start_period: 120s
|
start_period: 120s
|
||||||
|
|
||||||
ollama:
|
|
||||||
image: ollama/ollama:latest
|
|
||||||
profiles: ["ollama-gpu"]
|
|
||||||
ports:
|
|
||||||
- "11434:11434"
|
|
||||||
volumes:
|
|
||||||
- ollama_data:/root/.ollama
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: all
|
|
||||||
capabilities: [gpu]
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
ollama-cpu:
|
|
||||||
image: ollama/ollama:latest
|
|
||||||
profiles: ["ollama-cpu"]
|
|
||||||
ports:
|
|
||||||
- "11434:11434"
|
|
||||||
volumes:
|
|
||||||
- ollama_data:/root/.ollama
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
garage_data:
|
garage_data:
|
||||||
garage_meta:
|
garage_meta:
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ Standalone runs without authentication (`FEATURE_REQUIRE_LOGIN=false`, `AUTH_BAC
|
|||||||
|
|
||||||
1. In `www/.env.local`: set `FEATURE_REQUIRE_LOGIN=true`, uncomment `AUTHENTIK_ISSUER` and `AUTHENTIK_REFRESH_TOKEN_URL`
|
1. In `www/.env.local`: set `FEATURE_REQUIRE_LOGIN=true`, uncomment `AUTHENTIK_ISSUER` and `AUTHENTIK_REFRESH_TOKEN_URL`
|
||||||
2. In `server/.env`: set `AUTH_BACKEND=authentik` (or your backend), configure `AUTH_JWT_AUDIENCE`
|
2. In `server/.env`: set `AUTH_BACKEND=authentik` (or your backend), configure `AUTH_JWT_AUDIENCE`
|
||||||
3. Restart: `docker compose -f docker-compose.standalone.yml up -d --force-recreate web server`
|
3. Restart: `docker compose -f docker-compose.yml -f docker-compose.standalone.yml up -d --force-recreate web server`
|
||||||
|
|
||||||
## What's NOT covered
|
## What's NOT covered
|
||||||
|
|
||||||
|
|||||||
@@ -35,75 +35,6 @@ err() { echo -e "${RED} ✗${NC} $*" >&2; }
|
|||||||
|
|
||||||
# --- Helpers ---
|
# --- Helpers ---
|
||||||
|
|
||||||
dump_diagnostics() {
|
|
||||||
local failed_svc="${1:-}"
|
|
||||||
echo ""
|
|
||||||
err "========== DIAGNOSTICS =========="
|
|
||||||
|
|
||||||
err "Container status:"
|
|
||||||
compose_cmd ps -a --format "table {{.Name}}\t{{.Status}}" 2>/dev/null || true
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show logs for any container that exited
|
|
||||||
local stopped
|
|
||||||
stopped=$(compose_cmd ps -a --format '{{.Name}}\t{{.Status}}' 2>/dev/null \
|
|
||||||
| grep -iv 'up\|running' | awk -F'\t' '{print $1}' || true)
|
|
||||||
for c in $stopped; do
|
|
||||||
err "--- Logs for $c (exited/unhealthy) ---"
|
|
||||||
docker logs --tail 30 "$c" 2>&1 || true
|
|
||||||
echo ""
|
|
||||||
done
|
|
||||||
|
|
||||||
# If a specific service failed, always show its logs
|
|
||||||
if [[ -n "$failed_svc" ]]; then
|
|
||||||
err "--- Logs for $failed_svc (last 40) ---"
|
|
||||||
compose_cmd logs "$failed_svc" --tail 40 2>&1 || true
|
|
||||||
echo ""
|
|
||||||
# Try health check from inside the container as extra signal
|
|
||||||
err "--- Internal health check ($failed_svc) ---"
|
|
||||||
compose_cmd exec -T "$failed_svc" \
|
|
||||||
curl -sf http://localhost:1250/health 2>&1 || echo "(not reachable internally either)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
err "================================="
|
|
||||||
}
|
|
||||||
|
|
||||||
trap 'dump_diagnostics' ERR
|
|
||||||
|
|
||||||
# Get the image ID for a compose service (works even when containers are not running).
|
|
||||||
svc_image_id() {
|
|
||||||
local svc="$1"
|
|
||||||
# Extract image name from compose config YAML, fall back to <project>-<service>
|
|
||||||
local img_name
|
|
||||||
img_name=$(compose_cmd config 2>/dev/null \
|
|
||||||
| sed -n "/^ ${svc}:/,/^ [a-z]/p" | grep '^\s*image:' | awk '{print $2}')
|
|
||||||
img_name="${img_name:-reflector-$svc}"
|
|
||||||
docker images -q "$img_name" 2>/dev/null | head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensure images with build contexts are up-to-date.
|
|
||||||
# Docker layer cache makes this fast (~seconds) when source hasn't changed.
|
|
||||||
rebuild_images() {
|
|
||||||
local svc
|
|
||||||
for svc in web cpu; do
|
|
||||||
local old_id
|
|
||||||
old_id=$(svc_image_id "$svc")
|
|
||||||
old_id="${old_id:-<none>}"
|
|
||||||
|
|
||||||
info "Building $svc..."
|
|
||||||
compose_cmd build "$svc"
|
|
||||||
|
|
||||||
local new_id
|
|
||||||
new_id=$(svc_image_id "$svc")
|
|
||||||
|
|
||||||
if [[ "$old_id" == "$new_id" ]]; then
|
|
||||||
ok "$svc unchanged (${new_id:0:12})"
|
|
||||||
else
|
|
||||||
ok "$svc rebuilt (${old_id:0:12} -> ${new_id:0:12})"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_url() {
|
wait_for_url() {
|
||||||
local url="$1" label="$2" retries="${3:-30}" interval="${4:-2}"
|
local url="$1" label="$2" retries="${3:-30}" interval="${4:-2}"
|
||||||
for i in $(seq 1 "$retries"); do
|
for i in $(seq 1 "$retries"); do
|
||||||
@@ -148,7 +79,7 @@ resolve_symlink() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compose_cmd() {
|
compose_cmd() {
|
||||||
local compose_files="-f $ROOT_DIR/docker-compose.standalone.yml"
|
local compose_files="-f $ROOT_DIR/docker-compose.yml -f $ROOT_DIR/docker-compose.standalone.yml"
|
||||||
if [[ "$OS" == "Linux" ]] && [[ -n "${OLLAMA_PROFILE:-}" ]]; then
|
if [[ "$OS" == "Linux" ]] && [[ -n "${OLLAMA_PROFILE:-}" ]]; then
|
||||||
docker compose $compose_files --profile "$OLLAMA_PROFILE" "$@"
|
docker compose $compose_files --profile "$OLLAMA_PROFILE" "$@"
|
||||||
else
|
else
|
||||||
@@ -182,7 +113,7 @@ step_llm() {
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Pull model if not already present
|
# Pull model if not already present
|
||||||
if ollama list 2>/dev/null | awk '{print $1}' | grep -qxF "$MODEL"; then
|
if ollama list 2>/dev/null | awk '{print $1}' | grep -qx "$MODEL"; then
|
||||||
ok "Model $MODEL already pulled"
|
ok "Model $MODEL already pulled"
|
||||||
else
|
else
|
||||||
info "Pulling model $MODEL (this may take a while)..."
|
info "Pulling model $MODEL (this may take a while)..."
|
||||||
@@ -212,7 +143,7 @@ step_llm() {
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Pull model inside container
|
# Pull model inside container
|
||||||
if compose_cmd exec "$OLLAMA_SVC" ollama list 2>/dev/null | awk '{print $1}' | grep -qxF "$MODEL"; then
|
if compose_cmd exec "$OLLAMA_SVC" ollama list 2>/dev/null | awk '{print $1}' | grep -qx "$MODEL"; then
|
||||||
ok "Model $MODEL already pulled"
|
ok "Model $MODEL already pulled"
|
||||||
else
|
else
|
||||||
info "Pulling model $MODEL inside container (this may take a while)..."
|
info "Pulling model $MODEL inside container (this may take a while)..."
|
||||||
@@ -382,24 +313,9 @@ step_services() {
|
|||||||
warn "Continuing anyway (services will start but may be shadowed)"
|
warn "Continuing anyway (services will start but may be shadowed)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Rebuild images if source has changed (Docker layer cache makes this fast when unchanged)
|
|
||||||
rebuild_images
|
|
||||||
|
|
||||||
# server runs alembic migrations on startup automatically (see runserver.sh)
|
# server runs alembic migrations on startup automatically (see runserver.sh)
|
||||||
compose_cmd up -d postgres redis garage cpu server worker beat web
|
compose_cmd up -d postgres redis garage cpu server worker beat web
|
||||||
ok "Containers started"
|
ok "Containers started"
|
||||||
|
|
||||||
# Quick sanity check — catch containers that exit immediately (bad image, missing file, etc.)
|
|
||||||
sleep 3
|
|
||||||
local exited
|
|
||||||
exited=$(compose_cmd ps -a --format '{{.Name}} {{.Status}}' 2>/dev/null \
|
|
||||||
| grep -i 'exit' || true)
|
|
||||||
if [[ -n "$exited" ]]; then
|
|
||||||
warn "Some containers exited immediately:"
|
|
||||||
echo "$exited" | while read -r line; do warn " $line"; done
|
|
||||||
dump_diagnostics
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Server is running migrations (alembic upgrade head)..."
|
info "Server is running migrations (alembic upgrade head)..."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,36 +345,9 @@ step_health() {
|
|||||||
warn "Check with: docker compose logs cpu"
|
warn "Check with: docker compose logs cpu"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Server may take a long time on first run — alembic migrations run before uvicorn starts.
|
wait_for_url "http://localhost:1250/health" "Server API" 60 3
|
||||||
# Use docker exec so this works regardless of network_mode or port mapping.
|
|
||||||
info "Waiting for Server API (first run includes database migrations)..."
|
|
||||||
local server_ok=false
|
|
||||||
for i in $(seq 1 90); do
|
|
||||||
# Check if container is still running
|
|
||||||
local svc_status
|
|
||||||
svc_status=$(compose_cmd ps server --format '{{.Status}}' 2>/dev/null || true)
|
|
||||||
if [[ -z "$svc_status" ]] || echo "$svc_status" | grep -qi 'exit'; then
|
|
||||||
echo ""
|
|
||||||
err "Server container exited unexpectedly"
|
|
||||||
dump_diagnostics server
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Health check from inside container (avoids host networking issues)
|
|
||||||
if compose_cmd exec -T server curl -sf http://localhost:1250/health > /dev/null 2>&1; then
|
|
||||||
server_ok=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo -ne "\r Waiting for Server API... ($i/90)"
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
echo ""
|
echo ""
|
||||||
if [[ "$server_ok" == "true" ]]; then
|
ok "Server API healthy"
|
||||||
ok "Server API healthy"
|
|
||||||
else
|
|
||||||
err "Server API not ready after ~7 minutes"
|
|
||||||
dump_diagnostics server
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
wait_for_url "http://localhost:3000" "Frontend" 90 3
|
wait_for_url "http://localhost:3000" "Frontend" 90 3
|
||||||
echo ""
|
echo ""
|
||||||
@@ -491,22 +380,6 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure Docker Compose V2 plugin is available.
|
|
||||||
# Check output for "Compose" — without the plugin, `docker compose version`
|
|
||||||
# may still exit 0 (falling through to `docker version`).
|
|
||||||
if ! docker compose version 2>/dev/null | grep -qi compose; then
|
|
||||||
err "Docker Compose plugin not found."
|
|
||||||
err "Install Docker Desktop, OrbStack, or: brew install docker-compose"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Dockerfiles use RUN --mount which requires BuildKit.
|
|
||||||
# Docker Desktop/OrbStack bundle it; Colima/bare engine need docker-buildx.
|
|
||||||
if ! docker buildx version &>/dev/null; then
|
|
||||||
err "Docker BuildKit (buildx) not found."
|
|
||||||
err "Install Docker Desktop, OrbStack, or: brew install docker-buildx"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# LLM_URL_VALUE is set by step_llm, used by later steps
|
# LLM_URL_VALUE is set by step_llm, used by later steps
|
||||||
LLM_URL_VALUE=""
|
LLM_URL_VALUE=""
|
||||||
|
|||||||
@@ -12,5 +12,3 @@ 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,9 +1,6 @@
|
|||||||
from typing import TYPE_CHECKING, Annotated, List, Optional
|
from typing import 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
|
||||||
@@ -127,20 +124,3 @@ 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,11 +19,3 @@ 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,10 +5,7 @@ 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 TYPE_CHECKING, Any, Literal, Sequence
|
from typing import Any, Literal, Sequence
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from reflector.ws_events import TranscriptEventName
|
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -187,7 +184,7 @@ class TranscriptWaveform(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class TranscriptEvent(BaseModel):
|
class TranscriptEvent(BaseModel):
|
||||||
event: str # Typed at call sites via ws_events.TranscriptEventName; str here for DB compat
|
event: str
|
||||||
data: dict
|
data: dict
|
||||||
|
|
||||||
|
|
||||||
@@ -236,9 +233,7 @@ class Transcript(BaseModel):
|
|||||||
dt = dt.replace(tzinfo=timezone.utc)
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
return dt.isoformat()
|
return dt.isoformat()
|
||||||
|
|
||||||
def add_event(
|
def add_event(self, event: str, data: BaseModel) -> TranscriptEvent:
|
||||||
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
|
||||||
@@ -693,7 +688,7 @@ class TranscriptController:
|
|||||||
async def append_event(
|
async def append_event(
|
||||||
self,
|
self,
|
||||||
transcript: Transcript,
|
transcript: Transcript,
|
||||||
event: "TranscriptEventName",
|
event: str,
|
||||||
data: Any,
|
data: Any,
|
||||||
) -> TranscriptEvent:
|
) -> TranscriptEvent:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ 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: set[TranscriptEventName] = {"STATUS", "FINAL_TITLE", "DURATION"}
|
USER_ROOM_EVENTS = {"STATUS", "FINAL_TITLE", "DURATION"}
|
||||||
|
|
||||||
|
|
||||||
async def broadcast_event(
|
async def broadcast_event(
|
||||||
@@ -82,7 +81,8 @@ 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: TranscriptEventName,
|
event_name: NonEmptyString,
|
||||||
|
# TODO proper dictionary event => type
|
||||||
data: Any,
|
data: Any,
|
||||||
logger: structlog.BoundLogger,
|
logger: structlog.BoundLogger,
|
||||||
) -> TranscriptEvent:
|
) -> TranscriptEvent:
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ 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,
|
||||||
@@ -91,11 +89,7 @@ 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: set[TranscriptEventName] = {
|
allowed_user_events = {"STATUS", "FINAL_TITLE", "DURATION"}
|
||||||
"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}",
|
||||||
@@ -250,14 +244,13 @@ 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=get_topic,
|
data=topic,
|
||||||
)
|
)
|
||||||
|
|
||||||
@broadcast_to_sockets
|
@broadcast_to_sockets
|
||||||
|
|||||||
@@ -4,22 +4,18 @@ Transcripts websocket API
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
|
from typing import Optional
|
||||||
|
|
||||||
|
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(
|
@router.get("/transcripts/{transcript_id}/events")
|
||||||
"/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
|
||||||
|
|
||||||
@@ -28,9 +24,8 @@ 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
|
||||||
@@ -42,9 +37,7 @@ 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(
|
await ws_manager.add_user_to_room(room_id, websocket)
|
||||||
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,22 +4,10 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
"""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,22 +1,18 @@
|
|||||||
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, operations } from "../../reflector-api";
|
import type { components } 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 { WEBSOCKET_URL } from "../../lib/apiClient";
|
import { $api, WEBSOCKET_URL } from "../../lib/apiClient";
|
||||||
import {
|
import {
|
||||||
invalidateTranscript,
|
invalidateTranscript,
|
||||||
invalidateTranscriptTopics,
|
invalidateTranscriptTopics,
|
||||||
invalidateTranscriptWaveform,
|
invalidateTranscriptWaveform,
|
||||||
} from "../../lib/apiHooks";
|
} from "../../lib/apiHooks";
|
||||||
import { useAuth } from "../../lib/AuthProvider";
|
import { NonEmptyString } from "../../lib/utils";
|
||||||
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;
|
||||||
@@ -31,7 +27,6 @@ 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>("");
|
||||||
@@ -336,168 +331,156 @@ 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: WebSocket | null = null;
|
let ws = new WebSocket(url);
|
||||||
let retryCount = 0;
|
|
||||||
let retryTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let intentionalClose = false;
|
|
||||||
|
|
||||||
const connect = () => {
|
ws.onopen = () => {
|
||||||
const subprotocols = auth.accessToken
|
console.debug("WebSocket connection opened");
|
||||||
? ["bearer", auth.accessToken]
|
};
|
||||||
: undefined;
|
|
||||||
ws = new WebSocket(url, subprotocols);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onmessage = (event) => {
|
||||||
console.debug("WebSocket connection opened");
|
const message = JSON.parse(event.data);
|
||||||
retryCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
try {
|
||||||
const message: TranscriptWsEvent = JSON.parse(event.data);
|
switch (message.event) {
|
||||||
|
case "TRANSCRIPT":
|
||||||
|
const newText = (message.data.text ?? "").trim();
|
||||||
|
const newTranslation = (message.data.translation ?? "").trim();
|
||||||
|
|
||||||
try {
|
if (!newText) break;
|
||||||
switch (message.event) {
|
|
||||||
case "TRANSCRIPT": {
|
|
||||||
const newText = (message.data.text ?? "").trim();
|
|
||||||
const newTranslation = (message.data.translation ?? "").trim();
|
|
||||||
|
|
||||||
if (!newText) break;
|
console.debug("TRANSCRIPT event:", newText);
|
||||||
|
setTextQueue((prevQueue) => [...prevQueue, newText]);
|
||||||
|
setTranslationQueue((prevQueue) => [...prevQueue, newTranslation]);
|
||||||
|
|
||||||
console.debug("TRANSCRIPT event:", newText);
|
setAccumulatedText((prevText) => prevText + " " + newText);
|
||||||
setTextQueue((prevQueue) => [...prevQueue, newText]);
|
break;
|
||||||
setTranslationQueue((prevQueue) => [
|
|
||||||
...prevQueue,
|
|
||||||
newTranslation,
|
|
||||||
]);
|
|
||||||
|
|
||||||
setAccumulatedText((prevText) => prevText + " " + newText);
|
case "TOPIC":
|
||||||
break;
|
setTopics((prevTopics) => {
|
||||||
}
|
const topic = message.data as Topic;
|
||||||
|
const index = prevTopics.findIndex(
|
||||||
case "TOPIC":
|
(prevTopic) => prevTopic.id === topic.id,
|
||||||
setTopics((prevTopics) => {
|
|
||||||
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);
|
|
||||||
invalidateTranscript(queryClient, tsId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "WAVEFORM":
|
|
||||||
console.debug(
|
|
||||||
"WAVEFORM event length:",
|
|
||||||
message.data.waveform.length,
|
|
||||||
);
|
);
|
||||||
setWaveForm({ data: message.data.waveform });
|
if (index >= 0) {
|
||||||
invalidateTranscriptWaveform(queryClient, tsId);
|
prevTopics[index] = topic;
|
||||||
break;
|
return prevTopics;
|
||||||
|
|
||||||
case "DURATION":
|
|
||||||
console.debug("DURATION event:", message.data);
|
|
||||||
setDuration(message.data.duration);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "STATUS":
|
|
||||||
console.log("STATUS event:", message.data);
|
|
||||||
if (message.data.value === "error") {
|
|
||||||
setError(
|
|
||||||
Error("Websocket error status"),
|
|
||||||
"There was an error processing this meeting.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
setStatus(message.data);
|
setAccumulatedText((prevText) =>
|
||||||
invalidateTranscript(queryClient, tsId);
|
prevText.slice(topic.transcript.length),
|
||||||
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}`,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setError(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
return [...prevTopics, topic];
|
||||||
console.error("WebSocket error:", error);
|
});
|
||||||
};
|
console.debug("TOPIC event:", message.data);
|
||||||
|
// Invalidate topics query to sync with WebSocket data
|
||||||
ws.onclose = (event) => {
|
invalidateTranscriptTopics(
|
||||||
console.debug("WebSocket connection closed, code:", event.code);
|
queryClient,
|
||||||
if (intentionalClose) return;
|
transcriptId as NonEmptyString,
|
||||||
|
|
||||||
const normalCodes = [1000, 1001, 1005];
|
|
||||||
if (normalCodes.includes(event.code)) return;
|
|
||||||
|
|
||||||
if (retryCount < MAX_RETRIES) {
|
|
||||||
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
|
|
||||||
console.log(
|
|
||||||
`WebSocket reconnecting in ${delay}ms (attempt ${retryCount + 1}/${MAX_RETRIES})`,
|
|
||||||
);
|
|
||||||
if (retryCount === 0) {
|
|
||||||
setError(
|
|
||||||
new Error("WebSocket connection lost"),
|
|
||||||
"Connection lost. Reconnecting...",
|
|
||||||
);
|
);
|
||||||
}
|
break;
|
||||||
retryCount++;
|
|
||||||
retryTimeout = setTimeout(connect, delay);
|
case "FINAL_SHORT_SUMMARY":
|
||||||
} else {
|
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":
|
||||||
|
console.debug("FINAL_TITLE event:", message.data);
|
||||||
|
if (message.data) {
|
||||||
|
setTitle(message.data.title);
|
||||||
|
// Invalidate transcript query to sync title
|
||||||
|
invalidateTranscript(queryClient, transcriptId as NonEmptyString);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "WAVEFORM":
|
||||||
|
console.debug(
|
||||||
|
"WAVEFORM event length:",
|
||||||
|
message.data.waveform.length,
|
||||||
|
);
|
||||||
|
if (message.data) {
|
||||||
|
setWaveForm(message.data.waveform);
|
||||||
|
invalidateTranscriptWaveform(
|
||||||
|
queryClient,
|
||||||
|
transcriptId as NonEmptyString,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "DURATION":
|
||||||
|
console.debug("DURATION event:", message.data);
|
||||||
|
if (message.data) {
|
||||||
|
setDuration(message.data.duration);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "STATUS":
|
||||||
|
console.log("STATUS event:", message.data);
|
||||||
|
if (message.data.value === "error") {
|
||||||
|
setError(
|
||||||
|
Error("Websocket error status"),
|
||||||
|
"There was an error processing this meeting.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setStatus(message.data);
|
||||||
|
invalidateTranscript(queryClient, transcriptId as NonEmptyString);
|
||||||
|
if (message.data.value === "ended") {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
setError(
|
||||||
|
new Error(`Received unknown WebSocket event: ${message.event}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error("WebSocket error:", error);
|
||||||
|
setError(new Error("A WebSocket error occurred."));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
console.debug("WebSocket connection closed");
|
||||||
|
switch (event.code) {
|
||||||
|
case 1000: // Normal Closure:
|
||||||
|
break;
|
||||||
|
case 1005: // Closure by client FF
|
||||||
|
break;
|
||||||
|
case 1001: // Navigate away
|
||||||
|
break;
|
||||||
|
case 1006: // Closed by client Chrome
|
||||||
|
console.warn(
|
||||||
|
"WebSocket closed by client, likely duplicated connection in react dev mode",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
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 () => {
|
||||||
intentionalClose = true;
|
ws.close();
|
||||||
if (retryTimeout) clearTimeout(retryTimeout);
|
|
||||||
ws?.close();
|
|
||||||
};
|
};
|
||||||
}, [transcriptId]);
|
}, [transcriptId]);
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ 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 { invalidateTranscript, invalidateTranscriptLists } from "./apiHooks";
|
import { z } from "zod";
|
||||||
import { parseNonEmptyString } from "./utils";
|
import { invalidateTranscriptLists, TRANSCRIPT_SEARCH_URL } from "./apiHooks";
|
||||||
import type { operations } from "../reflector-api";
|
|
||||||
|
|
||||||
type UserWsEvent =
|
const UserEvent = z.object({
|
||||||
operations["v1_user_get_websocket_events"]["responses"][200]["content"]["application/json"];
|
event: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type UserEvent = z.TypeOf<typeof UserEvent>;
|
||||||
|
|
||||||
class UserEventsStore {
|
class UserEventsStore {
|
||||||
private socket: WebSocket | null = null;
|
private socket: WebSocket | null = null;
|
||||||
@@ -131,26 +133,23 @@ export function UserEventsProvider({
|
|||||||
if (!detachRef.current) {
|
if (!detachRef.current) {
|
||||||
const onMessage = (event: MessageEvent) => {
|
const onMessage = (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
const msg: UserWsEvent = JSON.parse(event.data);
|
const msg = UserEvent.parse(JSON.parse(event.data));
|
||||||
|
const eventName = msg.event;
|
||||||
|
|
||||||
switch (msg.event) {
|
const invalidateList = () => invalidateTranscriptLists(queryClient);
|
||||||
|
|
||||||
|
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":
|
||||||
invalidateTranscriptLists(queryClient).then(() => {});
|
invalidateList().then(() => {});
|
||||||
invalidateTranscript(
|
break;
|
||||||
queryClient,
|
|
||||||
parseNonEmptyString(msg.data.id),
|
default:
|
||||||
).then(() => {});
|
// Ignore other content events for list updates
|
||||||
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,7 +7,6 @@ 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
|
||||||
@@ -105,12 +104,6 @@ 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",
|
||||||
@@ -124,10 +117,6 @@ 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,10 +568,7 @@ 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;
|
||||||
@@ -667,26 +664,6 @@ 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;
|
||||||
@@ -1900,33 +1877,6 @@ 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 */
|
||||||
@@ -1967,113 +1917,6 @@ 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 */
|
||||||
@@ -2144,82 +1987,6 @@ 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 */
|
||||||
@@ -3656,16 +3423,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json":
|
"application/json": unknown;
|
||||||
| 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 */
|
||||||
@@ -3849,31 +3607,6 @@ 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