diff --git a/.dockerignore b/.dockerignore index 19aaf83..8439270 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ -api/.env \ No newline at end of file +api/.env +evals/ diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 20d0de3..418b67f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -4,7 +4,7 @@ on: release: types: [published] -# Ensure only one workflow run per branch at a time; cancel any in-progress runs on new push +# Ensure only one workflow run per branch at a time; cancel any in-progress runs on new push concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -13,11 +13,11 @@ jobs: build: runs-on: ubuntu-latest env: - COMMIT_SHA: ${{ github.sha }} # Used to tag images with short commit SHA + COMMIT_SHA: ${{ github.sha }} strategy: matrix: - service: + service: - "dograh-api|api/Dockerfile|." - "dograh-ui|ui/Dockerfile|." @@ -25,14 +25,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - submodules: true # Only for version check, not used in build + submodules: true - # Pipecat version check removed - now using local submodule - - - name: Set up QEMU # Enables cross-platform builds (e.g., arm64) + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx # Enables multi-arch and advanced Docker builds + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to DockerHub @@ -51,48 +49,50 @@ jobs: - name: Set build variables id: build-vars run: | - # Parse matrix entry and set variables early (before build) SERVICE="${{ matrix.service }}" IMAGE_NAME=$(echo "$SERVICE" | cut -d '|' -f1) SHORT_SHA=${COMMIT_SHA::8} - - # Export for use in subsequent steps + + # Get version from release tag (removes 'dograh-' and 'v' prefixes if present) + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#dograh-}" + VERSION="${VERSION#v}" + echo "image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT - echo "service=${SERVICE}" >> $GITHUB_OUTPUT - + echo "version=${VERSION}" >> $GITHUB_OUTPUT + - name: Build and Push ${{ matrix.service }} id: docker-build run: | - # Parse matrix entry into individual variables SERVICE="${{ matrix.service }}" IMAGE_NAME=$(echo "$SERVICE" | cut -d '|' -f1) DOCKERFILE=$(echo "$SERVICE" | cut -d '|' -f2) CONTEXT=$(echo "$SERVICE" | cut -d '|' -f3) SHORT_SHA=${COMMIT_SHA::8} + VERSION="${{ steps.build-vars.outputs.version }}" echo "Building and pushing image: $IMAGE_NAME" echo "Dockerfile: $DOCKERFILE" echo "Context: $CONTEXT" - echo "Commit SHA: $SHORT_SHA" - - # Export tags for Slack notification + echo "Version: $VERSION" + echo "image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT echo "dockerhub_tag=${{ secrets.DOCKERHUB_USERNAME }}/${IMAGE_NAME}:${SHORT_SHA}" >> $GITHUB_OUTPUT echo "ghcr_tag=ghcr.io/${{ secrets.GHCR_USERNAME }}/${IMAGE_NAME}:${SHORT_SHA}" >> $GITHUB_OUTPUT echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT - # Build and push multi-arch Docker image to DockerHub and GHCR docker buildx build \ -f "$DOCKERFILE" \ --platform linux/amd64,linux/arm64 \ + --tag ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:$VERSION \ --tag ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:$SHORT_SHA \ --tag ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest \ + --tag ghcr.io/${{ secrets.GHCR_USERNAME }}/$IMAGE_NAME:$VERSION \ --tag ghcr.io/${{ secrets.GHCR_USERNAME }}/$IMAGE_NAME:$SHORT_SHA \ --tag ghcr.io/${{ secrets.GHCR_USERNAME }}/$IMAGE_NAME:latest \ --push "$CONTEXT" - - # Success notification + - name: Send Slack notification - Success if: success() uses: slackapi/slack-github-action@v1.26.0 @@ -101,10 +101,9 @@ jobs: with: payload: | { - "text": "✅ Docker Build Successful - ${{ steps.build-vars.outputs.image_name }} (${{ steps.build-vars.outputs.short_sha }}) on ${{ github.ref_name }} by ${{ github.actor }}" + "text": "✅ Docker Build Successful - ${{ steps.build-vars.outputs.image_name }} (${{ steps.build-vars.outputs.version }}) on ${{ github.ref_name }} by ${{ github.actor }}" } - - # Failure notification + - name: Send Slack notification - Failure if: failure() uses: slackapi/slack-github-action@v1.26.0 @@ -113,5 +112,5 @@ jobs: with: payload: | { - "text": "❌ Docker Build Failed - ${{ steps.build-vars.outputs.image_name }} (${{ steps.build-vars.outputs.short_sha }}) on ${{ github.ref_name }} by ${{ github.actor }} - <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Logs>" + "text": "❌ Docker Build Failed - ${{ steps.build-vars.outputs.image_name }} (${{ steps.build-vars.outputs.version }}) on ${{ github.ref_name }} by ${{ github.actor }} - <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Logs>" } diff --git a/.gitignore b/.gitignore index c8acf44..7f268eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __pycache__ .DS_Store .env +.env.prod .env.test # logs and run directory on production diff --git a/api/alembic/versions/181475b2a1a1_add_public_access_token.py b/api/alembic/versions/181475b2a1a1_add_public_access_token.py new file mode 100644 index 0000000..a796285 --- /dev/null +++ b/api/alembic/versions/181475b2a1a1_add_public_access_token.py @@ -0,0 +1,72 @@ +"""add public_access_token + +Revision ID: 181475b2a1a1 +Revises: dc33eef8dabe +Create Date: 2026-01-23 17:37:54.449308 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "181475b2a1a1" +down_revision: Union[str, None] = "dc33eef8dabe" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_api_keys_key_hash"), table_name="api_keys") + op.create_index("ix_api_keys_key_hash", "api_keys", ["key_hash"], unique=False) + op.create_index( + "ix_kb_chunks_embedding_ivfflat", + "knowledge_base_chunks", + ["embedding"], + unique=False, + postgresql_using="ivfflat", + postgresql_with={"lists": 100}, + postgresql_ops={"embedding": "vector_cosine_ops"}, + ) + op.create_index( + "ix_kb_chunks_embedding_model", + "knowledge_base_chunks", + ["embedding_model"], + unique=False, + ) + op.add_column( + "workflow_runs", + sa.Column("public_access_token", sa.String(length=36), nullable=True), + ) + op.create_index( + "idx_workflow_runs_public_access_token", + "workflow_runs", + ["public_access_token"], + unique=True, + postgresql_where=sa.text("public_access_token IS NOT NULL"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index( + "idx_workflow_runs_public_access_token", + table_name="workflow_runs", + postgresql_where=sa.text("public_access_token IS NOT NULL"), + ) + op.drop_column("workflow_runs", "public_access_token") + op.drop_index("ix_kb_chunks_embedding_model", table_name="knowledge_base_chunks") + op.drop_index( + "ix_kb_chunks_embedding_ivfflat", + table_name="knowledge_base_chunks", + postgresql_using="ivfflat", + postgresql_with={"lists": 100}, + postgresql_ops={"embedding": "vector_cosine_ops"}, + ) + op.drop_index("ix_api_keys_key_hash", table_name="api_keys") + op.create_index(op.f("ix_api_keys_key_hash"), "api_keys", ["key_hash"], unique=True) + # ### end Alembic commands ### diff --git a/api/constants.py b/api/constants.py index 366a256..b92596a 100644 --- a/api/constants.py +++ b/api/constants.py @@ -14,7 +14,6 @@ FILLER_SOUND_PROBABILITY = 0.0 VOICEMAIL_RECORDING_DURATION = 5.0 # Configuration constants -ENABLE_SMART_TURN = os.getenv("ENABLE_SMART_TURN", "false").lower() == "true" ENABLE_TRACING = os.getenv("ENABLE_TRACING", "false").lower() == "true" ENABLE_RNNOISE = os.getenv("ENABLE_RNNOISE", "false").lower() == "true" @@ -52,6 +51,23 @@ ENABLE_ARI_STASIS = os.getenv("ENABLE_ARI_STASIS", "false").lower() == "true" SERIALIZE_LOG_OUTPUT = os.getenv("SERIALIZE_LOG_OUTPUT", "false").lower() == "true" ENABLE_TELEMETRY = os.getenv("ENABLE_TELEMETRY", "false").lower() == "true" + +def _get_version() -> str: + """Read version from pyproject.toml.""" + try: + import tomllib + + pyproject_path = APP_ROOT_DIR / "pyproject.toml" + with open(pyproject_path, "rb") as f: + pyproject = tomllib.load(f) + return pyproject.get("project", {}).get("version", "dev") + except Exception: + return "dev" + + +# Application version (read from pyproject.toml) +APP_VERSION = _get_version() + # Country code mapping: ISO country code -> international dialing prefix COUNTRY_CODES = { "US": "1", # United States diff --git a/api/db/models.py b/api/db/models.py index 55d0305..4f44636 100644 --- a/api/db/models.py +++ b/api/db/models.py @@ -360,6 +360,17 @@ class WorkflowRunModel(Base): campaign = relationship("CampaignModel") queued_run_id = Column(Integer, ForeignKey("queued_runs.id"), nullable=True) queued_run = relationship("QueuedRunModel", foreign_keys=[queued_run_id]) + public_access_token = Column(String(36), nullable=True) + + # Indexes + __table_args__ = ( + Index( + "idx_workflow_runs_public_access_token", + "public_access_token", + unique=True, + postgresql_where=text("public_access_token IS NOT NULL"), + ), + ) # LoopTalk Testing Models diff --git a/api/db/workflow_client.py b/api/db/workflow_client.py index 7ce4078..53eb00c 100644 --- a/api/db/workflow_client.py +++ b/api/db/workflow_client.py @@ -4,7 +4,7 @@ from typing import Optional from sqlalchemy import func from sqlalchemy.future import select -from sqlalchemy.orm import selectinload +from sqlalchemy.orm import load_only, selectinload from api.db.base_client import BaseDBClient from api.db.models import WorkflowDefinitionModel, WorkflowModel, WorkflowRunModel @@ -111,6 +111,70 @@ class WorkflowClient(BaseDBClient): result = await session.execute(query) return result.scalars().all() + async def get_all_workflows_for_listing( + self, organization_id: int = None, status: str = None + ) -> list[WorkflowModel]: + """Get workflows with only the columns needed for listing. + + This is an optimized version that excludes large JSON columns like + workflow_definition, template_context_variables, etc. + + Args: + organization_id: Filter by organization ID + status: Filter by status (active/archived) + + Returns: + List of WorkflowModel with only id, name, status, created_at loaded + """ + async with self.async_session() as session: + query = select(WorkflowModel).options( + load_only( + WorkflowModel.id, + WorkflowModel.name, + WorkflowModel.status, + WorkflowModel.created_at, + ) + ) + + if organization_id: + query = query.where(WorkflowModel.organization_id == organization_id) + + if status: + query = query.where(WorkflowModel.status == status) + + result = await session.execute(query) + return result.scalars().all() + + async def get_workflow_counts(self, organization_id: int = None) -> dict[str, int]: + """Get workflow counts by status. + + Args: + organization_id: Filter by organization ID + + Returns: + Dict with 'total', 'active', 'archived' counts + """ + async with self.async_session() as session: + query = select( + WorkflowModel.status, + func.count(WorkflowModel.id).label("count"), + ) + + if organization_id: + query = query.where(WorkflowModel.organization_id == organization_id) + + query = query.group_by(WorkflowModel.status) + + result = await session.execute(query) + rows = result.all() + + counts = {"total": 0, "active": 0, "archived": 0} + for status, count in rows: + counts[status] = count + counts["total"] += count + + return counts + async def get_workflow( self, workflow_id: int, user_id: int = None, organization_id: int = None ) -> WorkflowModel | None: @@ -310,3 +374,33 @@ class WorkflowClient(BaseDBClient): ) ) return result.scalar() or 0 + + async def get_workflow_run_counts(self, workflow_ids: list[int]) -> dict[int, int]: + """Get run counts for multiple workflows in a single query. + + Args: + workflow_ids: List of workflow IDs to get counts for + + Returns: + Dict mapping workflow_id to run count + """ + if not workflow_ids: + return {} + + async with self.async_session() as session: + result = await session.execute( + select( + WorkflowRunModel.workflow_id, + func.count(WorkflowRunModel.id).label("run_count"), + ) + .where(WorkflowRunModel.workflow_id.in_(workflow_ids)) + .group_by(WorkflowRunModel.workflow_id) + ) + rows = result.all() + + # Build dict with counts, defaulting to 0 for workflows with no runs + counts = {workflow_id: 0 for workflow_id in workflow_ids} + for workflow_id, run_count in rows: + counts[workflow_id] = run_count + + return counts diff --git a/api/db/workflow_run_client.py b/api/db/workflow_run_client.py index 27cffac..604a431 100644 --- a/api/db/workflow_run_client.py +++ b/api/db/workflow_run_client.py @@ -1,3 +1,4 @@ +import uuid from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple @@ -414,3 +415,56 @@ class WorkflowRunClient(BaseDBClient): organization_id = workflow_run.workflow.user.selected_organization_id return workflow_run, organization_id + + async def ensure_public_access_token(self, workflow_run_id: int) -> Optional[str]: + """Generate a public access token if not exists, return existing if present (idempotent). + + Args: + workflow_run_id: The ID of the workflow run + + Returns: + The public access token string, or None if workflow run not found + """ + async with self.async_session() as session: + result = await session.execute( + select(WorkflowRunModel).where(WorkflowRunModel.id == workflow_run_id) + ) + run = result.scalars().first() + if not run: + return None + + # Return existing token if present + if run.public_access_token: + return run.public_access_token + + # Generate and persist new token + token = str(uuid.uuid4()) + run.public_access_token = token + + try: + await session.commit() + except Exception as e: + await session.rollback() + raise e + await session.refresh(run) + + return run.public_access_token + + async def get_workflow_run_by_public_token( + self, token: str + ) -> Optional[WorkflowRunModel]: + """Lookup workflow run by public access token. + + Args: + token: The public access token + + Returns: + The WorkflowRunModel if found, None otherwise + """ + async with self.async_session() as session: + result = await session.execute( + select(WorkflowRunModel).where( + WorkflowRunModel.public_access_token == token + ) + ) + return result.scalars().first() diff --git a/api/pyproject.toml b/api/pyproject.toml new file mode 100644 index 0000000..7825d5a --- /dev/null +++ b/api/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "dograh-api" +version = "1.10.0" +description = "Backend API for Dograh voice AI platform" +requires-python = ">=3.12" diff --git a/api/routes/main.py b/api/routes/main.py index 2141172..8086114 100644 --- a/api/routes/main.py +++ b/api/routes/main.py @@ -1,5 +1,6 @@ from fastapi import APIRouter from loguru import logger +from pydantic import BaseModel from api.routes.campaign import router as campaign_router from api.routes.credentials import router as credentials_router @@ -9,6 +10,7 @@ from api.routes.looptalk import router as looptalk_router from api.routes.organization import router as organization_router from api.routes.organization_usage import router as organization_usage_router from api.routes.public_agent import router as public_agent_router +from api.routes.public_download import router as public_download_router from api.routes.public_embed import router as public_embed_router from api.routes.reports import router as reports_router from api.routes.s3_signed_url import router as s3_router @@ -43,11 +45,24 @@ router.include_router(reports_router) router.include_router(webrtc_signaling_router) router.include_router(public_embed_router) router.include_router(public_agent_router) +router.include_router(public_download_router) router.include_router(workflow_embed_router) router.include_router(knowledge_base_router) -@router.get("/health") -async def health(): +class HealthResponse(BaseModel): + status: str + version: str + backend_api_endpoint: str + + +@router.get("/health", response_model=HealthResponse) +async def health() -> HealthResponse: + from api.constants import APP_VERSION, BACKEND_API_ENDPOINT + logger.debug("Health endpoint called") - return {"message": "OK"} + return HealthResponse( + status="ok", + version=APP_VERSION, + backend_api_endpoint=BACKEND_API_ENDPOINT, + ) diff --git a/api/routes/public_download.py b/api/routes/public_download.py new file mode 100644 index 0000000..38793d9 --- /dev/null +++ b/api/routes/public_download.py @@ -0,0 +1,95 @@ +"""Public download endpoints for workflow recordings and transcripts. + +These endpoints provide secure, token-based public access to workflow artifacts +without requiring authentication. Tokens are generated on-demand when webhooks +are executed and included in the webhook payload. +""" + +from typing import Literal + +from fastapi import APIRouter, HTTPException, Query +from fastapi.responses import RedirectResponse +from loguru import logger + +from api.db import db_client +from api.services.storage import get_storage_for_backend + +router = APIRouter(prefix="/public/download") + + +@router.get("/workflow/{token}/{artifact_type}") +async def download_workflow_artifact( + token: str, + artifact_type: Literal["recording", "transcript"], + inline: bool = Query( + default=False, description="Display inline in browser instead of download" + ), +): + """Download a workflow recording or transcript via public access token. + + This endpoint: + 1. Validates the public access token + 2. Looks up the corresponding workflow run + 3. Generates a signed URL for the requested artifact + 4. Redirects to the signed URL + + Args: + token: The public access token (UUID format) + artifact_type: Type of artifact - "recording" or "transcript" + inline: If true, sets Content-Disposition to inline for browser preview + + Returns: + RedirectResponse to the signed URL (302 redirect) + + Raises: + HTTPException 404: If token is invalid or artifact not found + """ + # 1. Lookup workflow run by token + workflow_run = await db_client.get_workflow_run_by_public_token(token) + if not workflow_run: + logger.warning(f"Invalid public access token: {token[:8]}...") + raise HTTPException(status_code=404, detail="Invalid or expired token") + + # 2. Get file path based on artifact type + if artifact_type == "recording": + file_path = workflow_run.recording_url + else: # transcript + file_path = workflow_run.transcript_url + + if not file_path: + logger.warning( + f"Artifact not found: type={artifact_type}, workflow_run_id={workflow_run.id}" + ) + raise HTTPException( + status_code=404, + detail=f"No {artifact_type} available for this workflow run", + ) + + # 3. Get storage backend for this workflow run + try: + storage = get_storage_for_backend(workflow_run.storage_backend) + except ValueError as e: + logger.error(f"Invalid storage backend: {workflow_run.storage_backend}") + raise HTTPException(status_code=500, detail="Storage configuration error") + + # 4. Generate signed URL (1 hour expiration) + try: + signed_url = await storage.aget_signed_url( + file_path=file_path, + expiration=3600, # 1 hour + force_inline=inline, + ) + except Exception as e: + logger.error(f"Failed to generate signed URL: {e}") + raise HTTPException(status_code=500, detail="Failed to generate download URL") + + if not signed_url: + logger.error(f"Storage returned None for signed URL: {file_path}") + raise HTTPException(status_code=500, detail="Failed to generate download URL") + + logger.info( + f"Generated signed URL for {artifact_type}: workflow_run_id={workflow_run.id}, token={token[:8]}..." + ) + + # 5. Redirect to signed URL + return RedirectResponse(url=signed_url, status_code=302) diff --git a/api/routes/workflow.py b/api/routes/workflow.py index f8e47eb..126314e 100644 --- a/api/routes/workflow.py +++ b/api/routes/workflow.py @@ -97,6 +97,24 @@ class WorkflowResponse(BaseModel): workflow_configurations: dict | None = None +class WorkflowListResponse(BaseModel): + """Lightweight response for workflow listings (excludes large fields).""" + + id: int + name: str + status: str + created_at: datetime + total_runs: int + + +class WorkflowCountResponse(BaseModel): + """Response for workflow count endpoint.""" + + total: int + active: int + archived: int + + class WorkflowTemplateResponse(BaseModel): id: int template_name: str @@ -359,6 +377,26 @@ class WorkflowSummaryResponse(BaseModel): name: str +@router.get("/count") +async def get_workflow_count( + user: UserModel = Depends(get_user), +) -> WorkflowCountResponse: + """Get workflow counts for the authenticated user's organization. + + This is a lightweight endpoint for checking if the user has workflows, + useful for redirect logic without fetching full workflow data. + """ + counts = await db_client.get_workflow_counts( + organization_id=user.selected_organization_id + ) + + return WorkflowCountResponse( + total=counts["total"], + active=counts["active"], + archived=counts["archived"], + ) + + @router.get("/fetch") async def get_workflows( user: UserModel = Depends(get_user), @@ -366,45 +404,43 @@ async def get_workflows( None, description="Filter by status - can be single value (active/archived) or comma-separated (active,archived)", ), -) -> List[WorkflowResponse]: - """Get all workflows for the authenticated user's organization""" +) -> List[WorkflowListResponse]: + """Get all workflows for the authenticated user's organization. + + Returns a lightweight response with only essential fields for listing. + Use GET /workflow/fetch/{workflow_id} to get full workflow details. + """ # Handle comma-separated status values if status and "," in status: # Split comma-separated values and fetch workflows for each status status_list = [s.strip() for s in status.split(",")] all_workflows = [] for status_value in status_list: - workflows = await db_client.get_all_workflows( + workflows = await db_client.get_all_workflows_for_listing( organization_id=user.selected_organization_id, status=status_value ) all_workflows.extend(workflows) workflows = all_workflows else: # Single status or no status filter - workflows = await db_client.get_all_workflows( + workflows = await db_client.get_all_workflows_for_listing( organization_id=user.selected_organization_id, status=status ) - # Get run counts for each workflow - workflow_responses = [] - for workflow in workflows: - run_count = await db_client.get_workflow_run_count(workflow.id) - workflow_responses.append( - { - "id": workflow.id, - "name": workflow.name, - "status": workflow.status, - "created_at": workflow.created_at, - "workflow_definition": workflow.workflow_definition_with_fallback, - "current_definition_id": workflow.current_definition_id, - "template_context_variables": workflow.template_context_variables, - "call_disposition_codes": workflow.call_disposition_codes, - "workflow_configurations": workflow.workflow_configurations, - "total_runs": run_count, - } - ) + # Get run counts for all workflows in a single query + workflow_ids = [workflow.id for workflow in workflows] + run_counts = await db_client.get_workflow_run_counts(workflow_ids) - return workflow_responses + return [ + WorkflowListResponse( + id=workflow.id, + name=workflow.name, + status=workflow.status, + created_at=workflow.created_at, + total_runs=run_counts.get(workflow.id, 0), + ) + for workflow in workflows + ] @router.get("/fetch/{workflow_id}") diff --git a/api/services/campaign/call_dispatcher.py b/api/services/campaign/call_dispatcher.py index d32bb32..353a42d 100644 --- a/api/services/campaign/call_dispatcher.py +++ b/api/services/campaign/call_dispatcher.py @@ -170,13 +170,6 @@ class CampaignCallDispatcher: ) raise ValueError(f"Workflow {campaign.workflow_id} not found") - # Merge context variables (queued_run context already includes retry info if applicable) - initial_context = { - **workflow.template_context_variables, - **queued_run.context_variables, - "campaign_id": campaign.id, - } - # Extract phone number phone_number = queued_run.context_variables.get("phone_number") if not phone_number: @@ -186,13 +179,25 @@ class CampaignCallDispatcher: ) raise ValueError(f"No phone number in queued run {queued_run.id}") - # Create workflow run with queued_run_id tracking - workflow_run_name = f"WR-CAMPAIGN-{campaign.id}-{queued_run.id}" - # Get provider first to determine the mode provider = await self.get_telephony_provider(campaign.organization_id) workflow_run_mode = provider.PROVIDER_NAME + + logger.info(f"Provider name: {provider.PROVIDER_NAME}") + logger.info(f"Queued run context: {queued_run.context_variables}") + # Merge context variables (queued_run context already includes retry info if applicable) + initial_context = { + **workflow.template_context_variables, + **queued_run.context_variables, + "campaign_id": campaign.id, + "provider": provider.PROVIDER_NAME, + } + + logger.info(f"Final initial_context: {initial_context}") + + # Create workflow run with queued_run_id tracking + workflow_run_name = f"WR-CAMPAIGN-{campaign.id}-{queued_run.id}" try: workflow_run = await db_client.create_workflow_run( name=workflow_run_name, @@ -243,6 +248,8 @@ class CampaignCallDispatcher: to_number=phone_number, webhook_url=webhook_url, workflow_run_id=workflow_run.id, + workflow_id=campaign.workflow_id, + user_id=campaign.created_by, ) # Store provider type and metadata in gathered_context diff --git a/api/services/configuration/registry.py b/api/services/configuration/registry.py index 8ea8409..f1d0b69 100644 --- a/api/services/configuration/registry.py +++ b/api/services/configuration/registry.py @@ -300,7 +300,7 @@ TTSConfig = Annotated[ ###################################################### STT ######################################################################## -DEEPGRAM_STT_MODELS = ["nova-2", "nova-3-general"] +DEEPGRAM_STT_MODELS = ["nova-2", "nova-3-general", "flux-general-en"] DEEPGRAM_LANGUAGES = [ "multi", "en", diff --git a/api/services/looptalk/core/pipeline_builder.py b/api/services/looptalk/core/pipeline_builder.py index 0de82fd..8e1ceb5 100644 --- a/api/services/looptalk/core/pipeline_builder.py +++ b/api/services/looptalk/core/pipeline_builder.py @@ -103,7 +103,6 @@ class LoopTalkPipelineBuilder: # Set the context and audio_buffer after creation engine.set_context(context) - engine.set_audio_buffer(audio_buffer) context_aggregator = LLMContextAggregatorPair(context) diff --git a/api/services/looptalk/internal_serializer.py b/api/services/looptalk/internal_serializer.py index 0f59b50..75d89ea 100644 --- a/api/services/looptalk/internal_serializer.py +++ b/api/services/looptalk/internal_serializer.py @@ -12,9 +12,8 @@ from pipecat.frames.frames import ( Frame, InputAudioRawFrame, OutputAudioRawFrame, - StartFrame, ) -from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType +from pipecat.serializers.base_serializer import FrameSerializer class InternalFrameSerializer(FrameSerializer): @@ -24,15 +23,6 @@ class InternalFrameSerializer(FrameSerializer): preventing control frames from creating infinite loops. """ - @property - def type(self) -> FrameSerializerType: - """Internal transport uses binary frames.""" - return FrameSerializerType.BINARY - - async def setup(self, frame: StartFrame): - """No setup required for internal transport.""" - pass - async def serialize(self, frame: Frame) -> bytes | None: """Only serialize audio frames for transmission between agents.""" # Only pass audio frames between agents diff --git a/api/services/pipecat/event_handlers.py b/api/services/pipecat/event_handlers.py index f96c36f..1e68109 100644 --- a/api/services/pipecat/event_handlers.py +++ b/api/services/pipecat/event_handlers.py @@ -22,16 +22,21 @@ from pipecat.pipeline.task import PipelineTask from pipecat.processors.audio.audio_buffer_processor import AudioBufferProcessor -def register_transport_event_handlers( +def register_event_handlers( task: PipelineTask, transport, - workflow_run_id, + workflow_run_id: int, engine: PipecatEngine, audio_buffer: AudioBufferProcessor, + in_memory_logs_buffer: InMemoryLogsBuffer, + pipeline_metrics_aggregator: PipelineMetricsAggregator, audio_config=AudioConfig, ): - """Register event handlers for transport events""" + """Register all event handlers for transport and task events. + Returns: + Tuple of (in_memory_audio_buffer, in_memory_transcript_buffer) for use by other handlers. + """ # Initialize in-memory buffers with proper audio configuration sample_rate = audio_config.pipeline_sample_rate if audio_config else 16000 num_channels = 1 # Pipeline audio is always mono @@ -48,13 +53,35 @@ def register_transport_event_handlers( ) in_memory_transcript_buffer = InMemoryTranscriptBuffer(workflow_run_id) + # Track both events to ensure LLM is only triggered after both occur + ready_state = { + "pipeline_started": False, + "client_connected": False, + "llm_triggered": False, + } + + async def maybe_trigger_llm(): + """Trigger LLM only after both pipeline_started and client_connected events.""" + if ( + ready_state["pipeline_started"] + and ready_state["client_connected"] + and not ready_state["llm_triggered"] + ): + ready_state["llm_triggered"] = True + logger.debug( + "Both pipeline_started and client_connected received - triggering initial LLM generation" + ) + await engine.llm.queue_frame(LLMContextFrame(engine.context)) + @transport.event_handler("on_client_connected") - async def on_client_connected(transport, participant): - logger.debug("In on_client_connected callback handler - initializing workflow") + async def on_client_connected(_transport, _participant): + logger.debug("In on_client_connected callback handler") await audio_buffer.start_recording() + ready_state["client_connected"] = True + await maybe_trigger_llm() @transport.event_handler("on_client_disconnected") - async def on_client_disconnected(transport, participant): + async def on_client_disconnected(_transport, _participant): call_disposed = engine.is_call_disposed() logger.debug( @@ -69,33 +96,16 @@ def register_transport_event_handlers( if not call_disposed: await task.cancel() - # Return the buffers so they can be passed to other handlers - return in_memory_audio_buffer, in_memory_transcript_buffer - - -def register_task_event_handler( - workflow_run_id: int, - engine: PipecatEngine, - task: PipelineTask, - transport, - audio_buffer: AudioBufferProcessor, - in_memory_audio_buffer: InMemoryAudioBuffer, - in_memory_transcript_buffer: InMemoryTranscriptBuffer, - in_memory_logs_buffer: InMemoryLogsBuffer, - pipeline_metrics_aggregator: PipelineMetricsAggregator, -): @task.event_handler("on_pipeline_started") - async def on_pipeline_started(task: PipelineTask, frame: Frame): - logger.debug( - "In on_pipeline_started callback handler - triggering initial LLM generation" - ) - # Trigger initial LLM generation after pipeline has started - await engine.llm.queue_frame(LLMContextFrame(engine.context)) + async def on_pipeline_started(_task: PipelineTask, _frame: Frame): + logger.debug("In on_pipeline_started callback handler") + ready_state["pipeline_started"] = True + await maybe_trigger_llm() @task.event_handler("on_pipeline_finished") async def on_pipeline_finished( task: PipelineTask, - frame: Frame, + _frame: Frame, ): logger.debug(f"In on_pipeline_finished callback handler") @@ -207,14 +217,13 @@ def register_task_event_handler( if workflow_run and workflow_run.campaign_id: await campaign_call_dispatcher.release_call_slot(workflow_run_id) - # Write buffers to temp files and enqueue S3 upload + # Write buffers to temp files and enqueue combined processing task + audio_temp_path = None + transcript_temp_path = None + try: - # Only upload if buffers have content if not in_memory_audio_buffer.is_empty: audio_temp_path = await in_memory_audio_buffer.write_to_temp_file() - await enqueue_job( - FunctionNames.UPLOAD_AUDIO_TO_S3, workflow_run_id, audio_temp_path - ) else: logger.debug("Audio buffer is empty, skipping upload") @@ -222,11 +231,6 @@ def register_task_event_handler( transcript_temp_path = ( await in_memory_transcript_buffer.write_to_temp_file() ) - await enqueue_job( - FunctionNames.UPLOAD_TRANSCRIPT_TO_S3, - workflow_run_id, - transcript_temp_path, - ) else: logger.debug("Transcript buffer is empty, skipping upload") @@ -234,10 +238,18 @@ def register_task_event_handler( logger.error(f"Error preparing buffers for S3 upload: {e}", exc_info=True) await enqueue_job(FunctionNames.CALCULATE_WORKFLOW_RUN_COST, workflow_run_id) + + # Combined task: uploads artifacts then runs integrations sequentially await enqueue_job( - FunctionNames.RUN_INTEGRATIONS_POST_WORKFLOW_RUN, workflow_run_id + FunctionNames.PROCESS_WORKFLOW_COMPLETION, + workflow_run_id, + audio_temp_path, + transcript_temp_path, ) + # Return the buffers so they can be passed to other handlers + return in_memory_audio_buffer, in_memory_transcript_buffer + def register_audio_data_handler( audio_buffer: AudioBufferProcessor, @@ -260,18 +272,26 @@ def register_audio_data_handler( # Could implement overflow to disk here if needed -def register_transcript_handler( - transcript, workflow_run_id, in_memory_buffer: InMemoryTranscriptBuffer +def register_transcript_handlers( + user_aggregator, + assistant_aggregator, + workflow_run_id, + in_memory_buffer: InMemoryTranscriptBuffer, ): - """Register event handler for transcript updates""" + """Register event handlers for transcript updates on context aggregators. - @transcript.event_handler("on_transcript_update") - async def on_transcript_update(processor, frame): - transcript_text = "" - for msg in frame.messages: - timestamp = f"[{msg.timestamp}] " if msg.timestamp else "" - line = f"{timestamp}{msg.role}: {msg.content}\n" - transcript_text += line + Uses the on_user_turn_stopped and on_assistant_turn_stopped events to capture + transcripts as turns complete, following the event-based pattern. + """ - # Use in-memory buffer - await in_memory_buffer.append(transcript_text) + @user_aggregator.event_handler("on_user_turn_stopped") + async def on_user_turn_stopped(aggregator, strategy, message): + timestamp = f"[{message.timestamp}] " if message.timestamp else "" + line = f"{timestamp}user: {message.content}\n" + await in_memory_buffer.append(line) + + @assistant_aggregator.event_handler("on_assistant_turn_stopped") + async def on_assistant_turn_stopped(aggregator, message): + timestamp = f"[{message.timestamp}] " if message.timestamp else "" + line = f"{timestamp}assistant: {message.content}\n" + await in_memory_buffer.append(line) diff --git a/api/services/pipecat/pipeline_builder.py b/api/services/pipecat/pipeline_builder.py index 853f32d..0f6de5d 100644 --- a/api/services/pipecat/pipeline_builder.py +++ b/api/services/pipecat/pipeline_builder.py @@ -1,5 +1,4 @@ import os -from typing import TYPE_CHECKING from loguru import logger @@ -11,14 +10,10 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.audio.audio_buffer_processor import AudioBufferProcessor -from pipecat.processors.transcript_processor import TranscriptProcessor from pipecat.utils.context import turn_var -if TYPE_CHECKING: - from api.services.workflow.pipecat_engine import PipecatEngine - -def create_pipeline_components(audio_config: AudioConfig, engine: "PipecatEngine"): +def create_pipeline_components(audio_config: AudioConfig): """Create and return the main pipeline components with proper audio configuration""" logger.info(f"Creating pipeline components with audio config: {audio_config}") @@ -28,28 +23,21 @@ def create_pipeline_components(audio_config: AudioConfig, engine: "PipecatEngine buffer_size=audio_config.buffer_size_bytes, ) - transcript = TranscriptProcessor( - assistant_correct_aggregation_callback=engine.create_aggregation_correction_callback() - ) - context = LLMContext() - return audio_buffer, transcript, context + return audio_buffer, context def build_pipeline( transport, stt, - transcript, audio_buffer, llm, tts, user_context_aggregator, assistant_context_aggregator, pipeline_engine_callback_processor, - stt_mute_filter, pipeline_metrics_aggregator, - user_idle_disconnect, voicemail_detector=None, ): """Build the main pipeline with all components. @@ -63,7 +51,7 @@ def build_pipeline( # Build processors list with optional voicemail detection processors = [ transport.input(), # Transport user input - stt, # STT (audio_passthrough=True by default, passes InputAudioRawFrame) + stt, ] # Insert voicemail detector after STT if enabled @@ -76,16 +64,12 @@ def build_pipeline( # Continue with the rest of the pipeline processors.extend( [ - stt_mute_filter, # STTMuteFilters don't let VAD related events pass through if muted - user_idle_disconnect, - transcript.user(), user_context_aggregator, llm, # LLM pipeline_engine_callback_processor, tts, # TTS transport.output(), # Transport bot output audio_buffer, # AudioBufferProcessor - records both input and output audio - transcript.assistant(), assistant_context_aggregator, # Assistant spoken responses pipeline_metrics_aggregator, ] @@ -98,7 +82,6 @@ def create_pipeline_task(pipeline, workflow_run_id, audio_config: AudioConfig = """Create a pipeline task with appropriate parameters""" # Set up pipeline params with audio configuration if provided pipeline_params = PipelineParams( - allow_interruptions=True, enable_metrics=True, enable_usage_metrics=True, send_initial_empty_metrics=False, @@ -119,6 +102,7 @@ def create_pipeline_task(pipeline, workflow_run_id, audio_config: AudioConfig = pipeline, params=pipeline_params, enable_tracing=ENABLE_TRACING, + enable_rtvi=False, conversation_id=f"{workflow_run_id}", ) diff --git a/api/services/pipecat/run_pipeline.py b/api/services/pipecat/run_pipeline.py index 11ae7cc..91b1e18 100644 --- a/api/services/pipecat/run_pipeline.py +++ b/api/services/pipecat/run_pipeline.py @@ -7,12 +7,12 @@ from loguru import logger from api.db import db_client from api.db.models import WorkflowModel from api.enums import WorkflowRunMode +from api.services.configuration.registry import ServiceProviders from api.services.pipecat.audio_config import AudioConfig, create_audio_config from api.services.pipecat.event_handlers import ( register_audio_data_handler, - register_task_event_handler, - register_transcript_handler, - register_transport_event_handlers, + register_event_handlers, + register_transcript_handlers, ) from api.services.pipecat.in_memory_buffers import InMemoryLogsBuffer from api.services.pipecat.pipeline_builder import ( @@ -46,20 +46,25 @@ from api.services.workflow.pipecat_engine import PipecatEngine from api.services.workflow.workflow import WorkflowGraph from pipecat.extensions.voicemail.voicemail_detector import VoicemailDetector from pipecat.pipeline.base_task import PipelineTaskParams -from pipecat.processors.aggregators.llm_response import ( +from pipecat.processors.aggregators.llm_response_universal import ( LLMAssistantAggregatorParams, + LLMContextAggregatorPair, LLMUserAggregatorParams, ) -from pipecat.processors.aggregators.llm_response_universal import ( - LLMContextAggregatorPair, -) -from pipecat.processors.filters.stt_mute_filter import ( - STTMuteConfig, - STTMuteFilter, - STTMuteStrategy, -) -from pipecat.processors.user_idle_processor import UserIdleProcessor from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection +from pipecat.turns.user_mute import MuteUntilFirstBotCompleteUserMuteStrategy +from pipecat.turns.user_start import ( + ExternalUserTurnStartStrategy, + TranscriptionUserTurnStartStrategy, +) +from pipecat.turns.user_start.vad_user_turn_start_strategy import ( + VADUserTurnStartStrategy, +) +from pipecat.turns.user_stop import ( + ExternalUserTurnStopStrategy, + TranscriptionUserTurnStopStrategy, +) +from pipecat.turns.user_turn_strategies import UserTurnStrategies from pipecat.utils.context import set_current_run_id from pipecat.utils.enums import EndTaskReason from pipecat.utils.tracing.context_registry import ContextProviderRegistry @@ -517,12 +522,11 @@ async def _run_pipeline( embeddings_model=embeddings_model, ) - # Create pipeline components with audio configuration and engine - audio_buffer, transcript, context = create_pipeline_components(audio_config, engine) + # Create pipeline components with audio configuration + audio_buffer, context = create_pipeline_components(audio_config) # Set the context and audio_buffer after creation engine.set_context(context) - engine.set_audio_buffer(audio_buffer) # Set Stasis connection for immediate transfers (if available) if stasis_connection: @@ -532,7 +536,31 @@ async def _run_pipeline( expect_stripped_words=True, correct_aggregation_callback=engine.create_aggregation_correction_callback(), ) - user_params = LLMUserAggregatorParams(enable_emulated_vad_interruptions=True) + + # Configure turn strategies based on STT provider and model + # Deepgram Flux uses external turn detection (VAD + External start/stop) + # Other models use transcription-based turn detection with smart turn analyzer + is_deepgram_flux = ( + user_config.stt.provider == ServiceProviders.DEEPGRAM.value + and user_config.stt.model == "flux-general-en" + ) + + if is_deepgram_flux: + user_turn_strategies = UserTurnStrategies( + start=[VADUserTurnStartStrategy(), ExternalUserTurnStartStrategy()], + stop=[ExternalUserTurnStopStrategy()], + ) + else: + user_turn_strategies = UserTurnStrategies( + start=[VADUserTurnStartStrategy(), TranscriptionUserTurnStartStrategy()], + stop=[TranscriptionUserTurnStopStrategy()], + ) + + user_params = LLMUserAggregatorParams( + user_turn_strategies=user_turn_strategies, + user_mute_strategies=[MuteUntilFirstBotCompleteUserMuteStrategy()], + user_idle_timeout=max_user_idle_timeout, + ) context_aggregator = LLMContextAggregatorPair( context, assistant_params=assistant_params, user_params=user_params ) @@ -547,25 +575,20 @@ async def _run_pipeline( pipeline_metrics_aggregator = PipelineMetricsAggregator() - # Create STT mute filter using the selected strategies and the engine's callback - stt_mute_filter = STTMuteFilter( - config=STTMuteConfig( - strategies={ - STTMuteStrategy.MUTE_UNTIL_FIRST_BOT_COMPLETE, - STTMuteStrategy.CUSTOM, - }, - should_mute_callback=engine.create_should_mute_callback(), - ) - ) - - # Use engine's user idle callback with configured timeout - user_idle_disconnect = UserIdleProcessor( - callback=engine.create_user_idle_callback(), timeout=max_user_idle_timeout - ) - user_context_aggregator = context_aggregator.user() assistant_context_aggregator = context_aggregator.assistant() + # Register user idle event handlers + user_idle_handler = engine.create_user_idle_handler() + + @user_context_aggregator.event_handler("on_user_turn_idle") + async def on_user_turn_idle(aggregator): + await user_idle_handler.handle_idle(aggregator) + + @user_context_aggregator.event_handler("on_user_turn_started") + async def on_user_turn_started(aggregator, strategy): + user_idle_handler.reset() + # Create voicemail detector if enabled in the workflow's start node voicemail_detector = None start_node = workflow_graph.nodes.get(workflow_graph.start_node_id) @@ -592,16 +615,13 @@ async def _run_pipeline( pipeline = build_pipeline( transport, stt, - transcript, audio_buffer, llm, tts, user_context_aggregator, assistant_context_aggregator, pipeline_engine_callback_processor, - stt_mute_filter, pipeline_metrics_aggregator, - user_idle_disconnect, voicemail_detector=voicemail_detector, ) @@ -614,18 +634,6 @@ async def _run_pipeline( # Initialize the engine to set the initial context await engine.initialize() - # Register event handlers - in_memory_audio_buffer, in_memory_transcript_buffer = ( - register_transport_event_handlers( - task, - transport, - workflow_run_id, - engine=engine, - audio_buffer=audio_buffer, - audio_config=audio_config, - ) - ) - # Add real-time feedback observer if WebSocket sender is available # Note: ws_sender was already fetched earlier for node_transition_callback if ws_sender: @@ -635,21 +643,24 @@ async def _run_pipeline( ) task.add_observer(feedback_observer) - register_task_event_handler( - workflow_run_id, - engine, + # Register event handlers + in_memory_audio_buffer, in_memory_transcript_buffer = register_event_handlers( task, transport, - audio_buffer, - in_memory_audio_buffer, - in_memory_transcript_buffer, - in_memory_logs_buffer, - pipeline_metrics_aggregator, + workflow_run_id, + engine=engine, + audio_buffer=audio_buffer, + in_memory_logs_buffer=in_memory_logs_buffer, + pipeline_metrics_aggregator=pipeline_metrics_aggregator, + audio_config=audio_config, ) register_audio_data_handler(audio_buffer, workflow_run_id, in_memory_audio_buffer) - register_transcript_handler( - transcript, workflow_run_id, in_memory_transcript_buffer + register_transcript_handlers( + user_context_aggregator, + assistant_context_aggregator, + workflow_run_id, + in_memory_transcript_buffer, ) try: diff --git a/api/services/pipecat/service_factory.py b/api/services/pipecat/service_factory.py index ab11f18..4fd1445 100644 --- a/api/services/pipecat/service_factory.py +++ b/api/services/pipecat/service_factory.py @@ -7,6 +7,7 @@ from api.constants import MPS_API_URL from api.services.configuration.registry import ServiceProviders from pipecat.services.azure.llm import AzureLLMService from pipecat.services.cartesia.stt import CartesiaSTTService +from pipecat.services.deepgram.flux.stt import DeepgramFluxSTTService from pipecat.services.deepgram.stt import DeepgramSTTService, LiveOptions from pipecat.services.deepgram.tts import DeepgramTTSService from pipecat.services.dograh.llm import DograhLLMService @@ -34,6 +35,20 @@ def create_stt_service(user_config): f"Creating STT service: provider={user_config.stt.provider}, model={user_config.stt.model}" ) if user_config.stt.provider == ServiceProviders.DEEPGRAM.value: + # Check if using Flux model (English-only, no language selection) + if user_config.stt.model == "flux-general-en": + logger.debug("Using DeepGram Flux Model") + return DeepgramFluxSTTService( + api_key=user_config.stt.api_key, + model=user_config.stt.model, + params=DeepgramFluxSTTService.InputParams( + eot_timeout_ms=3000, + eot_threshold=0.7, + ), + should_interrupt=False, # Let UserAggregator take care of sending InterruptionFrame + ) + + # Other models than flux # Use language from user config, defaulting to "multi" for multilingual support language = getattr(user_config.stt, "language", None) or "multi" live_options = LiveOptions( @@ -44,7 +59,9 @@ def create_stt_service(user_config): ) logger.debug(f"Using DeepGram Model - {user_config.stt.model}") return DeepgramSTTService( - live_options=live_options, api_key=user_config.stt.api_key + live_options=live_options, + api_key=user_config.stt.api_key, + should_interrupt=False, # Let UserAggregator take care of sending InterruptionFrame ) elif user_config.stt.provider == ServiceProviders.OPENAI.value: return OpenAISTTService( diff --git a/api/services/pipecat/transport_setup.py b/api/services/pipecat/transport_setup.py index 4798841..237829b 100644 --- a/api/services/pipecat/transport_setup.py +++ b/api/services/pipecat/transport_setup.py @@ -2,10 +2,9 @@ import os from fastapi import WebSocket -from api.constants import APP_ROOT_DIR, ENABLE_RNNOISE, ENABLE_SMART_TURN +from api.constants import APP_ROOT_DIR from api.db import db_client from api.enums import OrganizationConfigurationKey -from api.services.looptalk.internal_transport import InternalTransport from api.services.pipecat.audio_config import AudioConfig from api.services.telephony.stasis_rtp_connection import StasisRTPConnection from api.services.telephony.stasis_rtp_serializer import StasisRTPFrameSerializer @@ -13,11 +12,8 @@ from api.services.telephony.stasis_rtp_transport import ( StasisRTPTransport, StasisRTPTransportParams, ) -from pipecat.audio.filters.rnnoise_filter import RNNoiseFilter from pipecat.audio.mixers.silence_mixer import SilenceAudioMixer from pipecat.audio.mixers.soundfile_mixer import SoundfileMixer -from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams -from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3 from pipecat.audio.vad.silero import SileroVADAnalyzer, VADParams from pipecat.serializers.twilio import TwilioFrameSerializer from pipecat.serializers.vobiz import VobizFrameSerializer @@ -35,19 +31,6 @@ librnnoise_path = os.path.normpath( ) -def create_turn_analyzer(workflow_run_id: int, audio_config: AudioConfig): - """Create a turn analyzer backed by the local Smart Turn HTTP service. - - Args: - workflow_run_id: ID of the workflow run for turn analyzer context - audio_config: Audio configuration containing pipeline sample rate - """ - if ENABLE_SMART_TURN: - return LocalSmartTurnAnalyzerV3(params=SmartTurnParams()) - - return None - - async def create_twilio_transport( websocket_client: WebSocket, stream_sid: str, @@ -78,8 +61,6 @@ async def create_twilio_transport( f"Incomplete Twilio configuration for organization {organization_id}" ) - turn_analyzer = create_turn_analyzer(workflow_run_id, audio_config) - serializer = TwilioFrameSerializer( stream_sid=stream_sid, call_sid=call_sid, @@ -119,11 +100,7 @@ async def create_twilio_transport( if ambient_noise_config and ambient_noise_config.get("enabled", False) else SilenceAudioMixer() ), - turn_analyzer=turn_analyzer, serializer=serializer, - audio_in_filter=RNNoiseFilter(library_path=librnnoise_path) - if ENABLE_RNNOISE - else None, ), ) @@ -158,8 +135,6 @@ async def create_cloudonix_transport( f"Required: bearer_token, domain_id" ) - turn_analyzer = create_turn_analyzer(workflow_run_id, audio_config) - from pipecat.serializers.cloudonix import CloudonixFrameSerializer serializer = CloudonixFrameSerializer( @@ -202,11 +177,7 @@ async def create_cloudonix_transport( if ambient_noise_config and ambient_noise_config.get("enabled", False) else SilenceAudioMixer() ), - turn_analyzer=turn_analyzer, serializer=serializer, - audio_in_filter=RNNoiseFilter(library_path=librnnoise_path) - if ENABLE_RNNOISE - else None, ), ) @@ -238,8 +209,6 @@ async def create_vonage_transport( f"Incomplete Vonage configuration for organization {organization_id}" ) - turn_analyzer = create_turn_analyzer(workflow_run_id, audio_config) - serializer = VonageFrameSerializer( call_uuid=call_uuid, application_id=application_id, @@ -283,11 +252,7 @@ async def create_vonage_transport( if ambient_noise_config and ambient_noise_config.get("enabled", False) else SilenceAudioMixer() ), - turn_analyzer=turn_analyzer, serializer=serializer, - audio_in_filter=RNNoiseFilter(library_path=librnnoise_path) - if ENABLE_RNNOISE - else None, ), ) @@ -337,8 +302,6 @@ async def create_vobiz_transport( f"from_numbers={len(config.get('from_numbers', []))} numbers" ) - turn_analyzer = create_turn_analyzer(workflow_run_id, audio_config) - # Use VobizFrameSerializer for Vobiz WebSocket protocol serializer = VobizFrameSerializer( stream_id=stream_id, @@ -389,11 +352,7 @@ async def create_vobiz_transport( if ambient_noise_config and ambient_noise_config.get("enabled", False) else SilenceAudioMixer() ), - turn_analyzer=turn_analyzer, serializer=serializer, - audio_in_filter=RNNoiseFilter(library_path=librnnoise_path) - if ENABLE_RNNOISE - else None, ), ) @@ -411,7 +370,6 @@ def create_webrtc_transport( ambient_noise_config: dict | None = None, ): """Create a transport for WebRTC connections""" - turn_analyzer = create_turn_analyzer(workflow_run_id, audio_config) return SmallWebRTCTransport( webrtc_connection=webrtc_connection, @@ -445,10 +403,6 @@ def create_webrtc_transport( if ambient_noise_config and ambient_noise_config.get("enabled", False) else SilenceAudioMixer() ), - turn_analyzer=turn_analyzer, - audio_in_filter=RNNoiseFilter(library_path=librnnoise_path) - if ENABLE_RNNOISE - else None, ), ) @@ -461,7 +415,6 @@ def create_stasis_transport( ambient_noise_config: dict | None = None, ): """Create a transport for ARI connections""" - turn_analyzer = create_turn_analyzer(workflow_run_id, audio_config) serializer = StasisRTPFrameSerializer( StasisRTPFrameSerializer.InputParams( @@ -502,11 +455,7 @@ def create_stasis_transport( if ambient_noise_config and ambient_noise_config.get("enabled", False) else SilenceAudioMixer() ), - turn_analyzer=turn_analyzer, serializer=serializer, - audio_in_filter=RNNoiseFilter(library_path=librnnoise_path) - if ENABLE_RNNOISE - else None, ), ) @@ -528,46 +477,44 @@ def create_internal_transport( Returns: InternalTransport instance configured with turn analyzer """ - turn_analyzer = create_turn_analyzer(workflow_run_id, audio_config) + pass + # Commented out because looptalk coming in the regular import flow + # was causing issue. May be move this to looptalk/orchestrator.py # Create and return the internal transport with latency - return InternalTransport( - params=TransportParams( - audio_out_enabled=True, - audio_out_sample_rate=audio_config.transport_out_sample_rate, - audio_out_channels=1, - audio_in_enabled=True, - audio_in_sample_rate=audio_config.transport_in_sample_rate, - audio_in_channels=1, - vad_analyzer=( - SileroVADAnalyzer( - params=VADParams( - confidence=vad_config.get("confidence", 0.7), - start_secs=vad_config.get("start_seconds", 0.4), - stop_secs=vad_config.get("stop_seconds", 0.8), - min_volume=vad_config.get("minimum_volume", 0.6), - ) - ) - if vad_config - else SileroVADAnalyzer() - ), - audio_out_mixer=( - SoundfileMixer( - sound_files={ - "office": APP_ROOT_DIR - / "assets" - / f"office-ambience-{audio_config.transport_out_sample_rate}-mono.wav" - }, - default_sound="office", - volume=ambient_noise_config.get("volume", 0.3), - ) - if ambient_noise_config and ambient_noise_config.get("enabled", False) - else SilenceAudioMixer() - ), - turn_analyzer=turn_analyzer, - audio_in_filter=RNNoiseFilter(library_path=librnnoise_path) - if ENABLE_RNNOISE - else None, - ), - latency_seconds=latency_seconds, - ) + # return InternalTransport( + # params=TransportParams( + # audio_out_enabled=True, + # audio_out_sample_rate=audio_config.transport_out_sample_rate, + # audio_out_channels=1, + # audio_in_enabled=True, + # audio_in_sample_rate=audio_config.transport_in_sample_rate, + # audio_in_channels=1, + # vad_analyzer=( + # SileroVADAnalyzer( + # params=VADParams( + # confidence=vad_config.get("confidence", 0.7), + # start_secs=vad_config.get("start_seconds", 0.4), + # stop_secs=vad_config.get("stop_seconds", 0.8), + # min_volume=vad_config.get("minimum_volume", 0.6), + # ) + # ) + # if vad_config + # else SileroVADAnalyzer() + # ), + # audio_out_mixer=( + # SoundfileMixer( + # sound_files={ + # "office": APP_ROOT_DIR + # / "assets" + # / f"office-ambience-{audio_config.transport_out_sample_rate}-mono.wav" + # }, + # default_sound="office", + # volume=ambient_noise_config.get("volume", 0.3), + # ) + # if ambient_noise_config and ambient_noise_config.get("enabled", False) + # else SilenceAudioMixer() + # ), + # ), + # latency_seconds=latency_seconds, + # ) diff --git a/api/services/telephony/stasis_rtp_serializer.py b/api/services/telephony/stasis_rtp_serializer.py index 751d68b..c4caf02 100644 --- a/api/services/telephony/stasis_rtp_serializer.py +++ b/api/services/telephony/stasis_rtp_serializer.py @@ -15,6 +15,8 @@ The serializer: from typing import Optional from loguru import logger +from pydantic import BaseModel + from pipecat.audio.utils import create_default_resampler, pcm_to_ulaw, ulaw_to_pcm from pipecat.frames.frames import ( AudioRawFrame, @@ -22,8 +24,7 @@ from pipecat.frames.frames import ( InputAudioRawFrame, StartFrame, ) -from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType -from pydantic import BaseModel +from pipecat.serializers.base_serializer import FrameSerializer class StasisRTPFrameSerializer(FrameSerializer): @@ -59,11 +60,6 @@ class StasisRTPFrameSerializer(FrameSerializer): # Resampler shared between encode / decode paths self._resampler = create_default_resampler() - @property - def type(self) -> FrameSerializerType: - """Stasis uses raw bytes → BINARY.""" - return FrameSerializerType.BINARY - async def setup(self, frame: StartFrame): """Remember pipeline configuration.""" self._sample_rate = self._params.sample_rate or frame.audio_in_sample_rate diff --git a/api/services/workflow/pipecat_engine.py b/api/services/workflow/pipecat_engine.py index 5cbbd5f..d3b3cc1 100644 --- a/api/services/workflow/pipecat_engine.py +++ b/api/services/workflow/pipecat_engine.py @@ -19,7 +19,6 @@ from pipecat.utils.enums import EndTaskReason if TYPE_CHECKING: from api.services.telephony.stasis_rtp_connection import StasisRTPConnection - from pipecat.processors.audio.audio_buffer_processor import AudioBuffer from pipecat.services.anthropic.llm import AnthropicLLMService from pipecat.services.google.llm import GoogleLLMService from pipecat.services.openai.llm import OpenAILLMService @@ -64,7 +63,6 @@ class PipecatEngine: transport: Optional[BaseTransport] = None, workflow: WorkflowGraph, call_context_vars: dict, - audio_buffer: Optional["AudioBuffer"] = None, workflow_run_id: Optional[int] = None, node_transition_callback: Optional[ Callable[[str, Optional[str]], Awaitable[None]] @@ -78,7 +76,6 @@ class PipecatEngine: self.transport = transport self.workflow = workflow self._call_context_vars = call_context_vars - self._audio_buffer = audio_buffer self._workflow_run_id = workflow_run_id self._node_transition_callback = node_transition_callback self._initialized = False @@ -204,6 +201,7 @@ class PipecatEngine: logger.info(f"Arguments: {function_call_params.arguments}") await self.set_node(transition_to_node) try: + async def on_context_updated() -> None: """ pipecat framework will run this function after the function call result has been updated in the context. @@ -215,6 +213,12 @@ class PipecatEngine: self._current_node ) + # Queue EndFrame if we just transitioned to EndNode + if self._current_node.is_end: + await self.send_end_task_frame( + EndTaskReason.USER_QUALIFIED.value + ) + result = {"status": "done"} properties = FunctionCallResultProperties( @@ -478,8 +482,6 @@ class PipecatEngine: if node.extraction_enabled and node.extraction_variables: await self._perform_variable_extraction_if_needed(node) - await self.send_end_task_frame(EndTaskReason.USER_QUALIFIED.value) - async def _handle_agent_node(self, node: Node) -> None: """Handle agent node execution.""" if node.is_static: @@ -680,12 +682,12 @@ class PipecatEngine: """ return engine_callbacks.create_should_mute_callback(self) - def create_user_idle_callback(self): + def create_user_idle_handler(self): """ - This callback is called when the user is idle for a certain duration. - We use this to either play the static text or end the call + Returns a UserIdleHandler that manages user-idle timeouts with state. + The handler tracks retry count and handles escalating prompts. """ - return engine_callbacks.create_user_idle_callback(self) + return engine_callbacks.create_user_idle_handler(self) def create_max_duration_callback(self): """ @@ -721,14 +723,6 @@ class PipecatEngine: """ self.task = task - def set_audio_buffer(self, audio_buffer: "AudioBuffer") -> None: - """Set the audio buffer. - - This allows setting the audio buffer after the engine has been created, - which is useful when the audio buffer needs to be created after the engine. - """ - self._audio_buffer = audio_buffer - def set_stasis_connection( self, connection: Optional["StasisRTPConnection"] ) -> None: diff --git a/api/services/workflow/pipecat_engine_callbacks.py b/api/services/workflow/pipecat_engine_callbacks.py index 6449026..a6ac51c 100644 --- a/api/services/workflow/pipecat_engine_callbacks.py +++ b/api/services/workflow/pipecat_engine_callbacks.py @@ -23,7 +23,6 @@ from pipecat.utils.enums import EndTaskReason if TYPE_CHECKING: from api.services.workflow.pipecat_engine import PipecatEngine - from pipecat.processors.user_idle_processor import UserIdleProcessor # --------------------------------------------------------------------------- @@ -57,33 +56,43 @@ def create_should_mute_callback( # --------------------------------------------------------------------------- -def create_user_idle_callback(engine: "PipecatEngine"): - """Return a callback that handles user-idle timeouts.""" +class UserIdleHandler: + """Helper class to manage user idle retry logic with state.""" - async def handle_user_idle( - user_idle: "UserIdleProcessor", retry_count: int - ) -> bool: - logger.debug(f"Handling user_idle, attempt: {retry_count}") + def __init__(self, engine: "PipecatEngine"): + self._engine = engine + self._retry_count = 0 - if retry_count == 1: + def reset(self): + """Reset the retry count when user becomes active.""" + self._retry_count = 0 + + async def handle_idle(self, aggregator): + """Handle user idle event with escalating prompts.""" + self._retry_count += 1 + logger.debug(f"Handling user_idle, attempt: {self._retry_count}") + + if self._retry_count == 1: message = { "role": "system", "content": "The user has been quiet. Politely and briefly ask if they're still there in the language that the user has been speaking so far.", } - await user_idle.push_frame(LLMMessagesAppendFrame([message], run_llm=True)) - return True + await aggregator.push_frame(LLMMessagesAppendFrame([message], run_llm=True)) + return message = { "role": "system", "content": "The user has been quiet. We will be disconnecting the call now. Wish them a good day in the language that the user has been speaking so far.", } - await user_idle.push_frame(LLMMessagesAppendFrame([message], run_llm=True)) - await engine.send_end_task_frame( + await aggregator.push_frame(LLMMessagesAppendFrame([message], run_llm=True)) + await self._engine.send_end_task_frame( EndTaskReason.USER_IDLE_MAX_DURATION_EXCEEDED.value ) - return False - return handle_user_idle + +def create_user_idle_handler(engine: "PipecatEngine") -> UserIdleHandler: + """Return a UserIdleHandler that manages user-idle timeouts with state.""" + return UserIdleHandler(engine) # --------------------------------------------------------------------------- diff --git a/api/tasks/arq.py b/api/tasks/arq.py index 8e8f58c..102e94e 100644 --- a/api/tasks/arq.py +++ b/api/tasks/arq.py @@ -49,8 +49,7 @@ from api.tasks.campaign_tasks import ( from api.tasks.knowledge_base_processing import process_knowledge_base_document from api.tasks.run_integrations import run_integrations_post_workflow_run from api.tasks.s3_upload import ( - upload_audio_to_s3, - upload_transcript_to_s3, + process_workflow_completion, upload_voicemail_audio_to_s3, ) @@ -59,9 +58,8 @@ class WorkerSettings: functions = [ calculate_workflow_run_cost, run_integrations_post_workflow_run, - upload_audio_to_s3, - upload_transcript_to_s3, upload_voicemail_audio_to_s3, + process_workflow_completion, sync_campaign_source, process_campaign_batch, monitor_campaign_progress, diff --git a/api/tasks/function_names.py b/api/tasks/function_names.py index c3e7486..e3aeb54 100644 --- a/api/tasks/function_names.py +++ b/api/tasks/function_names.py @@ -1,8 +1,7 @@ class FunctionNames: CALCULATE_WORKFLOW_RUN_COST = "calculate_workflow_run_cost" RUN_INTEGRATIONS_POST_WORKFLOW_RUN = "run_integrations_post_workflow_run" - UPLOAD_AUDIO_TO_S3 = "upload_audio_to_s3" - UPLOAD_TRANSCRIPT_TO_S3 = "upload_transcript_to_s3" + PROCESS_WORKFLOW_COMPLETION = "process_workflow_completion" UPLOAD_VOICEMAIL_AUDIO_TO_S3 = "upload_voicemail_audio_to_s3" SYNC_CAMPAIGN_SOURCE = "sync_campaign_source" PROCESS_CAMPAIGN_BATCH = "process_campaign_batch" diff --git a/api/tasks/run_integrations.py b/api/tasks/run_integrations.py index 44e981e..8a3d77c 100644 --- a/api/tasks/run_integrations.py +++ b/api/tasks/run_integrations.py @@ -1,10 +1,11 @@ """Execute webhook integrations after workflow run completion.""" -from typing import Any, Dict +from typing import Any, Dict, Optional import httpx from loguru import logger +from api.constants import BACKEND_API_ENDPOINT from api.db import db_client from api.db.models import WorkflowRunModel from api.utils.credential_auth import build_auth_header @@ -54,10 +55,13 @@ async def run_integrations_post_workflow_run(_ctx, workflow_run_id: int): logger.info(f"Found {len(webhook_nodes)} webhook nodes to execute") - # Step 4: Build render context - render_context = _build_render_context(workflow_run) + # Step 4: Generate public access token (on-demand, only when webhooks exist) + public_token = await db_client.ensure_public_access_token(workflow_run_id) - # Step 5: Execute each webhook node + # Step 5: Build render context + render_context = _build_render_context(workflow_run, public_token) + + # Step 6: Execute each webhook node for node in webhook_nodes: webhook_data = node.get("data", {}) try: @@ -77,9 +81,19 @@ async def run_integrations_post_workflow_run(_ctx, workflow_run_id: int): raise -def _build_render_context(workflow_run: WorkflowRunModel) -> Dict[str, Any]: - """Build the context dict for template rendering.""" - return { +def _build_render_context( + workflow_run: WorkflowRunModel, public_token: Optional[str] = None +) -> Dict[str, Any]: + """Build the context dict for template rendering. + + Args: + workflow_run: The workflow run model + public_token: Optional public access token for download URLs + + Returns: + Dict containing all fields available for template rendering + """ + context = { # Top-level fields "workflow_run_id": workflow_run.id, "workflow_run_name": workflow_run.name, @@ -89,10 +103,25 @@ def _build_render_context(workflow_run: WorkflowRunModel) -> Dict[str, Any]: "initial_context": workflow_run.initial_context or {}, "gathered_context": workflow_run.gathered_context or {}, "cost_info": workflow_run.usage_info or {}, - "recording_url": getattr(workflow_run, "recording_url", None), - "transcript_url": getattr(workflow_run, "transcript_url", None), } + # Add public download URLs if token is available + if public_token: + base_url = ( + f"{BACKEND_API_ENDPOINT}/api/v1/public/download/workflow/{public_token}" + ) + context["recording_url"] = ( + f"{base_url}/recording" if workflow_run.recording_url else None + ) + context["transcript_url"] = ( + f"{base_url}/transcript" if workflow_run.transcript_url else None + ) + else: + context["recording_url"] = workflow_run.recording_url + context["transcript_url"] = workflow_run.transcript_url + + return context + async def _execute_webhook_node( webhook_data: Dict[str, Any], diff --git a/api/tasks/s3_upload.py b/api/tasks/s3_upload.py index 9faf432..d1c55a6 100644 --- a/api/tasks/s3_upload.py +++ b/api/tasks/s3_upload.py @@ -1,129 +1,27 @@ import os +from typing import Optional from loguru import logger -from pipecat.utils.context import set_current_run_id from api.db import db_client from api.services.storage import get_current_storage_backend, storage_fs - - -async def upload_audio_to_s3(ctx, workflow_run_id: int, temp_file_path: str): - """Upload audio file from temp path to S3.""" - run_id = str(workflow_run_id) - set_current_run_id(run_id) - - logger.info(f"Starting audio upload to S3 from {temp_file_path}") - - try: - # Verify temp file exists - if not os.path.exists(temp_file_path): - logger.error(f"Temp audio file not found: {temp_file_path}") - raise FileNotFoundError(f"Temp audio file not found: {temp_file_path}") - - file_size = os.path.getsize(temp_file_path) - logger.debug(f"Audio file size: {file_size} bytes") - - recording_url = f"recordings/{workflow_run_id}.wav" - storage_backend = get_current_storage_backend() - - logger.info( - f"UPLOAD: Using {storage_backend.name} (value: {storage_backend.value}) for audio upload - workflow_run_id: {workflow_run_id}" - ) - - await storage_fs.aupload_file(temp_file_path, recording_url) - - # Update DB with recording URL and storage backend - await db_client.update_workflow_run( - run_id=workflow_run_id, - recording_url=recording_url, - storage_backend=storage_backend.value, - ) - - logger.info( - f"Successfully uploaded audio to {storage_backend.name}: {recording_url} (stored backend: {storage_backend.name})" - ) - - except Exception as e: - logger.error(f"Error uploading audio to S3 for workflow {workflow_run_id}: {e}") - raise - finally: - # Clean up temp file - if os.path.exists(temp_file_path): - try: - os.remove(temp_file_path) - logger.debug(f"Cleaned up temp audio file: {temp_file_path}") - except Exception as e: - logger.warning( - f"Failed to clean up temp audio file {temp_file_path}: {e}" - ) - - -async def upload_transcript_to_s3(ctx, workflow_run_id: int, temp_file_path: str): - """Upload transcript file from temp path to S3.""" - run_id = str(workflow_run_id) - set_current_run_id(run_id) - - logger.info(f"Starting transcript upload to S3 from {temp_file_path}") - - try: - # Verify temp file exists - if not os.path.exists(temp_file_path): - logger.error(f"Temp transcript file not found: {temp_file_path}") - raise FileNotFoundError(f"Temp transcript file not found: {temp_file_path}") - - file_size = os.path.getsize(temp_file_path) - logger.debug(f"Transcript file size: {file_size} bytes") - - transcript_url = f"transcripts/{workflow_run_id}.txt" - storage_backend = get_current_storage_backend() - - logger.info( - f"UPLOAD: Using {storage_backend.name} (value: {storage_backend.value}) for transcript upload - workflow_run_id: {workflow_run_id}" - ) - - await storage_fs.aupload_file(temp_file_path, transcript_url) - - # Update DB with transcript URL and storage backend - await db_client.update_workflow_run( - run_id=workflow_run_id, - transcript_url=transcript_url, - storage_backend=storage_backend.value, - ) - - logger.info( - f"Successfully uploaded transcript to {storage_backend.name}: {transcript_url} (stored backend: {storage_backend.name})" - ) - - except Exception as e: - logger.error( - f"Error uploading transcript to S3 for workflow {workflow_run_id}: {e}" - ) - raise - finally: - # Clean up temp file - if os.path.exists(temp_file_path): - try: - os.remove(temp_file_path) - logger.debug(f"Cleaned up temp transcript file: {temp_file_path}") - except Exception as e: - logger.warning( - f"Failed to clean up temp transcript file {temp_file_path}: {e}" - ) +from api.tasks.run_integrations import run_integrations_post_workflow_run +from pipecat.utils.context import set_current_run_id async def upload_voicemail_audio_to_s3( - ctx, + _ctx, workflow_run_id: int, temp_file_path: str, s3_key: str, ): """Upload voicemail detection audio from temp file to S3. - This function is similar to upload_audio_to_s3 but handles voicemail-specific - paths and doesn't update the workflow run's recording_url field. + Handles voicemail-specific paths and doesn't update the workflow run's + recording_url field. Args: - ctx: ARQ context + _ctx: ARQ context (unused) workflow_run_id: The workflow run ID temp_file_path: Path to the temporary WAV file s3_key: The S3 key where the file should be uploaded @@ -161,7 +59,7 @@ async def upload_voicemail_audio_to_s3( ) raise finally: - # Clean up temp file (same pattern as upload_audio_to_s3) + # Clean up temp file if os.path.exists(temp_file_path): try: os.remove(temp_file_path) @@ -170,3 +68,104 @@ async def upload_voicemail_audio_to_s3( logger.warning( f"Failed to clean up temp voicemail audio file {temp_file_path}: {e}" ) + + +async def process_workflow_completion( + _ctx, + workflow_run_id: int, + audio_temp_path: Optional[str] = None, + transcript_temp_path: Optional[str] = None, +): + """Process workflow completion: upload artifacts and run integrations. + + This task combines audio upload, transcript upload, and webhook integrations + into a single sequential task to ensure integrations run after uploads complete. + + Args: + _ctx: ARQ context (unused) + workflow_run_id: The workflow run ID + audio_temp_path: Optional path to temp audio file + transcript_temp_path: Optional path to temp transcript file + """ + run_id = str(workflow_run_id) + set_current_run_id(run_id) + + logger.info(f"Processing workflow completion for run {workflow_run_id}") + + storage_backend = get_current_storage_backend() + + # Step 1: Upload audio if provided + if audio_temp_path: + try: + if os.path.exists(audio_temp_path): + file_size = os.path.getsize(audio_temp_path) + logger.debug(f"Audio file size: {file_size} bytes") + + recording_url = f"recordings/{workflow_run_id}.wav" + logger.info( + f"Uploading audio to {storage_backend.name} - workflow_run_id: {workflow_run_id}" + ) + + await storage_fs.aupload_file(audio_temp_path, recording_url) + await db_client.update_workflow_run( + run_id=workflow_run_id, + recording_url=recording_url, + storage_backend=storage_backend.value, + ) + logger.info(f"Successfully uploaded audio: {recording_url}") + else: + logger.warning(f"Audio temp file not found: {audio_temp_path}") + except Exception as e: + logger.error(f"Error uploading audio for workflow {workflow_run_id}: {e}") + finally: + if audio_temp_path and os.path.exists(audio_temp_path): + try: + os.remove(audio_temp_path) + logger.debug(f"Cleaned up temp audio file: {audio_temp_path}") + except Exception as e: + logger.warning(f"Failed to clean up temp audio file: {e}") + + # Step 2: Upload transcript if provided + if transcript_temp_path: + try: + if os.path.exists(transcript_temp_path): + file_size = os.path.getsize(transcript_temp_path) + logger.debug(f"Transcript file size: {file_size} bytes") + + transcript_url = f"transcripts/{workflow_run_id}.txt" + logger.info( + f"Uploading transcript to {storage_backend.name} - workflow_run_id: {workflow_run_id}" + ) + + await storage_fs.aupload_file(transcript_temp_path, transcript_url) + await db_client.update_workflow_run( + run_id=workflow_run_id, + transcript_url=transcript_url, + storage_backend=storage_backend.value, + ) + logger.info(f"Successfully uploaded transcript: {transcript_url}") + else: + logger.warning( + f"Transcript temp file not found: {transcript_temp_path}" + ) + except Exception as e: + logger.error( + f"Error uploading transcript for workflow {workflow_run_id}: {e}" + ) + finally: + if transcript_temp_path and os.path.exists(transcript_temp_path): + try: + os.remove(transcript_temp_path) + logger.debug( + f"Cleaned up temp transcript file: {transcript_temp_path}" + ) + except Exception as e: + logger.warning(f"Failed to clean up temp transcript file: {e}") + + # Step 3: Run webhook integrations (after uploads are complete) + try: + await run_integrations_post_workflow_run(_ctx, workflow_run_id) + except Exception as e: + logger.error(f"Error running integrations for workflow {workflow_run_id}: {e}") + + logger.info(f"Completed workflow completion processing for run {workflow_run_id}") diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 9f97778..a7fe452 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -1,5 +1,5 @@ -from dataclasses import dataclass -from typing import Any, Dict +from dataclasses import dataclass, field +from typing import Any, Dict, Optional from unittest.mock import Mock import pytest @@ -28,6 +28,87 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor START_CALL_SYSTEM_PROMPT = "start_call_system_prompt" END_CALL_SYSTEM_PROMPT = "end_call_system_prompt" +# Default workflow definition for mocking database WorkflowModel +DEFAULT_WORKFLOW_DEFINITION = { + "nodes": [ + { + "id": "1", + "type": "startCall", + "position": {"x": 0, "y": 0}, + "data": { + "name": "Start", + "prompt": START_CALL_SYSTEM_PROMPT, + "is_start": True, + "allow_interrupt": False, + "add_global_prompt": False, + }, + }, + { + "id": "2", + "type": "endCall", + "position": {"x": 0, "y": 200}, + "data": { + "name": "End", + "prompt": END_CALL_SYSTEM_PROMPT, + "is_end": True, + "allow_interrupt": False, + "add_global_prompt": False, + }, + }, + ], + "edges": [ + { + "id": "1-2", + "source": "1", + "target": "2", + "data": {"label": "End", "condition": "End the call"}, + } + ], +} + + +@dataclass +class MockWorkflowModel: + """Mock database WorkflowModel for testing. + + This mimics the structure of the database WorkflowModel, not the parsed WorkflowGraph. + Use this when mocking db_client.get_workflow() responses. + """ + + workflow_id: int = 1 + organization_id: int = 1 + workflow_configurations: Dict[str, Any] = field(default_factory=dict) + workflow_definition_with_fallback: Dict[str, Any] = field(default_factory=dict) + + def __post_init__(self): + if not self.workflow_definition_with_fallback: + self.workflow_definition_with_fallback = DEFAULT_WORKFLOW_DEFINITION.copy() + + +@dataclass +class MockWorkflowRun: + """Mock database WorkflowRun for testing. + + Use this when mocking db_client.get_workflow_run() responses. + """ + + is_completed: bool = False + initial_context: Dict[str, Any] = field(default_factory=dict) + gathered_context: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class MockUserConfig: + """Mock user configuration for testing. + + Use this when mocking db_client.get_user_configurations() responses. + """ + + stt: Optional[Any] = None + tts: Optional[Any] = None + llm: Optional[Any] = None + embeddings: Optional[Any] = None + class MockTransportProcessor(FrameProcessor): """ @@ -41,7 +122,7 @@ class MockTransportProcessor(FrameProcessor): Args: emit_bot_speaking: If True, also emits BotSpeakingFrame on TTSAudioRawFrame - which is needed for UserIdleProcessor to start conversation tracking. Default True. + which is needed for user idle tracking to start conversation tracking. Default True. """ def __init__( @@ -63,7 +144,7 @@ class MockTransportProcessor(FrameProcessor): BotStartedSpeakingFrame(), direction=FrameDirection.UPSTREAM ) elif isinstance(frame, TTSAudioRawFrame): - # Emit BotSpeakingFrame - this is what triggers the UserIdleProcessor + # Emit BotSpeakingFrame - this is what triggers user idle tracking # to start conversation tracking if self._emit_bot_speaking: await self.push_frame(BotSpeakingFrame()) @@ -101,6 +182,24 @@ def mock_engine(): return engine +@pytest.fixture +def mock_workflow_model(): + """Create a mock WorkflowModel for testing database responses.""" + return MockWorkflowModel() + + +@pytest.fixture +def mock_workflow_run(): + """Create a mock WorkflowRun for testing database responses.""" + return MockWorkflowRun() + + +@pytest.fixture +def mock_user_config(): + """Create a mock user configuration for testing.""" + return MockUserConfig() + + @pytest.fixture def sample_tools(): """Create sample mock tools for testing.""" diff --git a/api/tests/test_pipecat_engine_context_update.py b/api/tests/test_pipecat_engine_context_update.py index e37db03..1a67665 100644 --- a/api/tests/test_pipecat_engine_context_update.py +++ b/api/tests/test_pipecat_engine_context_update.py @@ -42,7 +42,6 @@ from pipecat.processors.aggregators.llm_response_universal import ( ) from pipecat.tests import MockLLMService, MockTTSService - # Define prompts for test nodes START_NODE_PROMPT = "Start Node System Prompt" AGENT_NODE_PROMPT = "Agent Node System Prompt" @@ -143,14 +142,20 @@ class ContextCapturingMockLLM(MockLLMService): msg_copy = dict(msg) # Copy content to avoid reference issues if "content" in msg_copy: - msg_copy["content"] = str(msg_copy["content"]) if msg_copy["content"] else None + msg_copy["content"] = ( + str(msg_copy["content"]) if msg_copy["content"] else None + ) messages_snapshot.append(msg_copy) - self.captured_contexts.append({ - "step": self._current_step, - "messages": messages_snapshot, - "system_prompt": messages_snapshot[0]["content"] if messages_snapshot else None, - }) + self.captured_contexts.append( + { + "step": self._current_step, + "messages": messages_snapshot, + "system_prompt": messages_snapshot[0]["content"] + if messages_snapshot + else None, + } + ) # Call parent implementation to stream the mock chunks return await super()._stream_chat_completions_universal_context(context) @@ -306,14 +311,26 @@ class TestContextUpdateBeforeNextCompletion: transition completes. The test verifies the context is still correctly updated. """ # Step 0 (Start node): call collect_info to transition to agent - step_0_chunks = MockLLMService.create_multiple_function_call_chunks([ - {"name": "collect_info", "arguments": {}, "tool_call_id": "call_transition_1"}, - ]) + step_0_chunks = MockLLMService.create_multiple_function_call_chunks( + [ + { + "name": "collect_info", + "arguments": {}, + "tool_call_id": "call_transition_1", + }, + ] + ) # Step 1 (Agent node): call end_call to transition to end - step_1_chunks = MockLLMService.create_multiple_function_call_chunks([ - {"name": "end_call", "arguments": {}, "tool_call_id": "call_transition_2"}, - ]) + step_1_chunks = MockLLMService.create_multiple_function_call_chunks( + [ + { + "name": "end_call", + "arguments": {}, + "tool_call_id": "call_transition_2", + }, + ] + ) # Step 2 (End node): text response (end node has no outgoing edges) step_2_chunks = MockLLMService.create_text_chunks("Goodbye!") @@ -327,7 +344,7 @@ class TestContextUpdateBeforeNextCompletion: ) # Should have been called 3 times: start node, agent node, end node - assert llm.get_current_step() == 2, ( + assert llm.get_current_step() == 3, ( f"Expected 3 LLM generations (start, agent, end), got {llm.get_current_step()}" ) @@ -376,14 +393,26 @@ class TestContextUpdateBeforeNextCompletion: is handled correctly. """ # Step 0 (Start node): call collect_info to transition to agent - step_0_chunks = MockLLMService.create_multiple_function_call_chunks([ - {"name": "collect_info", "arguments": {}, "tool_call_id": "call_transition_1"}, - ]) + step_0_chunks = MockLLMService.create_multiple_function_call_chunks( + [ + { + "name": "collect_info", + "arguments": {}, + "tool_call_id": "call_transition_1", + }, + ] + ) # Step 1 (Agent node): call end_call to transition to end - step_1_chunks = MockLLMService.create_multiple_function_call_chunks([ - {"name": "end_call", "arguments": {}, "tool_call_id": "call_transition_2"}, - ]) + step_1_chunks = MockLLMService.create_multiple_function_call_chunks( + [ + { + "name": "end_call", + "arguments": {}, + "tool_call_id": "call_transition_2", + }, + ] + ) # Step 2 (End node): text response step_2_chunks = MockLLMService.create_text_chunks("Goodbye!") @@ -397,7 +426,7 @@ class TestContextUpdateBeforeNextCompletion: ) # Verify all three nodes were executed - assert llm.get_current_step() == 2, ( + assert llm.get_current_step() == 3, ( f"Expected 3 steps, got {llm.get_current_step()}" ) @@ -408,8 +437,7 @@ class TestContextUpdateBeforeNextCompletion: assert AGENT_NODE_PROMPT in llm.get_system_prompt_at_step(1) # Step 2: End node - should have end prompt - # FIXME - EndFrame is getting processed before LLMContextFrame - # assert END_NODE_PROMPT in llm.get_system_prompt_at_step(2) + assert END_NODE_PROMPT in llm.get_system_prompt_at_step(2) # Verify each subsequent step has the previous tool results step_1_ctx = llm.get_context_at_step(1) @@ -423,14 +451,14 @@ class TestContextUpdateBeforeNextCompletion: assert step_1_has_tool, "Agent node should see collect_info tool result" # Step 2 should have tool results from both transitions - # FIXME - EndFrame is getting processed before LLMContextFrame - # step_2_tool_messages = [ - # msg for msg in step_2_ctx["messages"] - # if msg.get("role") == "tool" or msg.get("tool_call_id") - # ] - # assert len(step_2_tool_messages) >= 2, ( - # f"End node should see at least 2 tool results, got {len(step_2_tool_messages)}" - # ) + step_2_tool_messages = [ + msg + for msg in step_2_ctx["messages"] + if msg.get("role") == "tool" or msg.get("tool_call_id") + ] + assert len(step_2_tool_messages) >= 2, ( + f"End node should see at least 2 tool results, got {len(step_2_tool_messages)}" + ) @pytest.mark.asyncio async def test_context_messages_preserve_conversation_history( @@ -444,14 +472,26 @@ class TestContextUpdateBeforeNextCompletion: - Tool call messages and results """ # Step 0 (Start node): call collect_info to transition to agent - step_0_chunks = MockLLMService.create_multiple_function_call_chunks([ - {"name": "collect_info", "arguments": {}, "tool_call_id": "call_transition_1"}, - ]) + step_0_chunks = MockLLMService.create_multiple_function_call_chunks( + [ + { + "name": "collect_info", + "arguments": {}, + "tool_call_id": "call_transition_1", + }, + ] + ) # Step 1 (Agent node): call end_call to transition to end - step_1_chunks = MockLLMService.create_multiple_function_call_chunks([ - {"name": "end_call", "arguments": {}, "tool_call_id": "call_transition_2"}, - ]) + step_1_chunks = MockLLMService.create_multiple_function_call_chunks( + [ + { + "name": "end_call", + "arguments": {}, + "tool_call_id": "call_transition_2", + }, + ] + ) # Step 2 (End node): text response step_2_chunks = MockLLMService.create_text_chunks("Goodbye!") @@ -472,18 +512,15 @@ class TestContextUpdateBeforeNextCompletion: assert len(ctx_1["messages"]) > len(ctx_0["messages"]), ( "Context at step 1 should have more messages than step 0" ) - - # FIXME - # assert len(ctx_2["messages"]) > len(ctx_1["messages"]), ( - # "Context at step 2 should have more messages than step 1" - # ) + + assert len(ctx_2["messages"]) > len(ctx_1["messages"]), ( + "Context at step 2 should have more messages than step 1" + ) # Verify assistant messages are accumulated - # FIXME - # assistant_messages_at_step_2 = [ - # msg for msg in ctx_2["messages"] - # if msg.get("role") == "assistant" - # ] - # assert len(assistant_messages_at_step_2) >= 2, ( - # "Should have at least 2 assistant messages by step 2" - # ) + assistant_messages_at_step_2 = [ + msg for msg in ctx_2["messages"] if msg.get("role") == "assistant" + ] + assert len(assistant_messages_at_step_2) >= 2, ( + "Should have at least 2 assistant messages by step 2" + ) diff --git a/api/tests/test_pipeline_cancellation.py b/api/tests/test_pipeline_cancellation.py new file mode 100644 index 0000000..45f04c5 --- /dev/null +++ b/api/tests/test_pipeline_cancellation.py @@ -0,0 +1,100 @@ +import asyncio + +import pytest +from loguru import logger + +from pipecat.frames.frames import ( + EndTaskFrame, + Frame, + InterruptionTaskFrame, + LLMRunFrame, +) +from pipecat.pipeline.base_task import PipelineTaskParams +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.task import PipelineTask +from pipecat.processors.frame_processor import FrameDirection, FrameProcessor + + +class MockTransport(FrameProcessor): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + await self.push_frame(frame, direction) + + +class BusyWaitProcessor(FrameProcessor): + def __init__(self, wait_time=5.0, **kwargs): + super().__init__(**kwargs) + self._wait_time = wait_time + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + if isinstance(frame, LLMRunFrame): + # Simulate a delay, which can happen sometimes due to slow LLM Inferencing or + # other reasons + try: + logger.debug(f"{self} sleeping with frame: {frame}") + await asyncio.sleep(5) + logger.debug(f"{self} woke up with frame: {frame}") + except asyncio.CancelledError: + logger.debug(f"{self} was cancelled") + raise + await self.push_frame(frame, direction) + + +@pytest.mark.asyncio +async def test_interruption_with_blocked_end_frame(): + busy_wait_processor = BusyWaitProcessor(wait_time=5) + transport = MockTransport() + pipeline = Pipeline([transport, busy_wait_processor]) + + task = PipelineTask(pipeline) + + async def run_pipeline(): + loop = asyncio.get_running_loop() + params = PipelineTaskParams(loop=loop) + await task.run(params=params) + + async def queue_frame(): + await task.queue_frames([LLMRunFrame()]) + + # Send EndTaskFrame to simulate EndFrame + await asyncio.sleep(0.1) + await transport.queue_frame(EndTaskFrame(), direction=FrameDirection.UPSTREAM) + + # Simulate an Interruption, which can happen if the user + # has started to speak + await asyncio.sleep(0.1) + await transport.queue_frame( + InterruptionTaskFrame(), direction=FrameDirection.UPSTREAM + ) + + # Create tasks explicitly for better control + pipeline_task = asyncio.create_task(run_pipeline()) + queue_task = asyncio.create_task(queue_frame()) + + # Wait with timeout + done, pending = await asyncio.wait( + [pipeline_task, queue_task], + timeout=1.0, + return_when=asyncio.ALL_COMPLETED, + ) + + # If there are pending tasks, we timed out + if pending: + # Cancel all pending tasks + for t in pending: + t.cancel() + + # Give limited time for cleanup, then move on regardless + try: + await asyncio.wait_for( + asyncio.gather(*pending, return_exceptions=True), + timeout=1.0, + ) + except asyncio.TimeoutError: + pass # Cleanup took too long, continue anyway + + pytest.fail("Test timed out after 1 second") diff --git a/api/tests/test_user_idle_handler.py b/api/tests/test_user_idle_handler.py index 4d73abf..ac5252c 100644 --- a/api/tests/test_user_idle_handler.py +++ b/api/tests/test_user_idle_handler.py @@ -1,10 +1,10 @@ """ Simulates a user idle condition and tests the behaviour -of the user idle processor. +of the user idle handler. This module tests the behavior when the user becomes idle during a conversation, -ensuring the UserIdleProcessor properly triggers the callback and the engine -handles it correctly. +ensuring the user_idle_timeout in LLMUserAggregatorParams properly triggers +the on_user_turn_idle event and the engine handles it correctly. """ import asyncio @@ -23,8 +23,8 @@ from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response import LLMAssistantAggregatorParams from pipecat.processors.aggregators.llm_response_universal import ( LLMContextAggregatorPair, + LLMUserAggregatorParams, ) -from pipecat.processors.user_idle_processor import UserIdleProcessor from pipecat.tests import MockLLMService, MockTTSService @@ -32,8 +32,8 @@ async def run_pipeline_with_user_idle( workflow: WorkflowGraph, user_idle_timeout: float = 0.2, mock_steps: list | None = None, -) -> tuple[MockLLMService, LLMContext, UserIdleProcessor]: - """Run a pipeline with UserIdleProcessor and simulate user idle condition. +) -> tuple[MockLLMService, LLMContext]: + """Run a pipeline with user_idle_timeout and simulate user idle condition. Args: workflow: The workflow graph to use. @@ -42,7 +42,7 @@ async def run_pipeline_with_user_idle( defaults to a simple greeting followed by text responses. Returns: - Tuple of (MockLLMService, LLMContext, UserIdleProcessor) for assertions. + Tuple of (MockLLMService, LLMContext) for assertions. """ # Create mock responses - bot will speak first, then respond to idle prompts # Step 1: Initial greeting @@ -64,10 +64,11 @@ async def run_pipeline_with_user_idle( # Create LLM context context = LLMContext() - # Create context aggregator with both user and assistant aggregators + # Create context aggregator with user_idle_timeout in user_params assistant_params = LLMAssistantAggregatorParams(expect_stripped_words=True) + user_params = LLMUserAggregatorParams(user_idle_timeout=user_idle_timeout) context_aggregator = LLMContextAggregatorPair( - context, assistant_params=assistant_params + context, assistant_params=assistant_params, user_params=user_params ) user_context_aggregator = context_aggregator.user() assistant_context_aggregator = context_aggregator.assistant() @@ -81,18 +82,20 @@ async def run_pipeline_with_user_idle( workflow_run_id=1, ) - # Create UserIdleProcessor with engine's callback and a short timeout - user_idle_processor = UserIdleProcessor( - callback=engine.create_user_idle_callback(), - timeout=user_idle_timeout, - ) + # Register user idle event handlers + user_idle_handler = engine.create_user_idle_handler() - # Build the pipeline: - # llm -> mock_transport -> user_idle_processor -> assistant_context_aggregator - # The user_context_aggregator would normally be at the start for user input + @user_context_aggregator.event_handler("on_user_turn_idle") + async def on_user_turn_idle(aggregator): + await user_idle_handler.handle_idle(aggregator) + + @user_context_aggregator.event_handler("on_user_turn_started") + async def on_user_turn_started(aggregator, strategy): + user_idle_handler.reset() + + # Build the pipeline pipeline = Pipeline( [ - user_idle_processor, user_context_aggregator, llm, tts, @@ -154,11 +157,11 @@ async def run_pipeline_with_user_idle( return_exceptions=True, ) - return llm, context, user_idle_processor + return llm, context class TestUserIdleHandler: - """Test user idle handling through PipecatEngine and UserIdleProcessor.""" + """Test user idle handling through PipecatEngine and UserIdleHandler.""" @pytest.mark.asyncio async def test_user_idle_triggers_callback(self, simple_workflow: WorkflowGraph): @@ -167,13 +170,13 @@ class TestUserIdleHandler: This test verifies that when: 1. The bot starts speaking (triggers conversation tracking) 2. No user input is received for the timeout period - 3. The UserIdleProcessor triggers the idle callback + 3. The on_user_turn_idle event triggers the idle handler - The engine's user idle callback should: + The engine's user idle handler should: - First retry: Send a message asking if user is still there - Second retry: Send goodbye message and end the call """ - llm, context, user_idle_processor = await run_pipeline_with_user_idle( + llm, context = await run_pipeline_with_user_idle( workflow=simple_workflow, user_idle_timeout=0.2, # Short timeout for faster test ) @@ -220,7 +223,7 @@ class TestUserIdleHandler: MockLLMService.create_text_chunks("Response 3"), ] - llm, context, user_idle_processor = await run_pipeline_with_user_idle( + llm, context = await run_pipeline_with_user_idle( workflow=three_node_workflow, user_idle_timeout=0.2, mock_steps=mock_steps, diff --git a/docker-compose.yaml b/docker-compose.yaml index 26f2144..7dc661f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,6 @@ services: postgres: - image: postgres:17 + image: pgvector/pgvector:pg17 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -83,6 +83,10 @@ services: ENVIRONMENT: "local" LOG_LEVEL: "INFO" + # Replace this environment variable if you are using a custom + # domain to host the stack + BACKEND_API_ENDPOINT: "http://localhost:8000" + # Database configuration (using containerized postgres) DATABASE_URL: "postgresql+asyncpg://postgres:postgres@postgres:5432/postgres" diff --git a/docs/deployment/custom-domain.mdx b/docs/deployment/custom-domain.mdx index 073b080..e2bedbb 100644 --- a/docs/deployment/custom-domain.mdx +++ b/docs/deployment/custom-domain.mdx @@ -162,6 +162,10 @@ server { } ``` +### Add environment variable + +Replace `BACKEND_API_ENDPOINT` environment variable the `docker-compose.yaml` with your custom domain with the scheme. + ### Start Dograh Services Start Dograh with the updated configuration: diff --git a/evals/stt/README.md b/evals/stt/README.md new file mode 100644 index 0000000..6e2802b --- /dev/null +++ b/evals/stt/README.md @@ -0,0 +1,135 @@ +# STT Evaluation Benchmark + +Benchmark for comparing Speech-to-Text providers using **WebSocket streaming** with focus on: +- **Speaker diarization** - identifying who said what +- **Keyterm boosting** - improving recognition of specific terms (Deepgram) + +## Providers + +| Provider | Diarization | Keyterm Boost | Streaming | +|----------|-------------|---------------|-----------| +| Deepgram | Yes | Yes | WebSocket (v1/v2) | +| Speechmatics | Yes | Additional vocab | WebSocket RT | + +## Setup + +```bash +# Install dependencies +pip install websockets + +# Set API keys +export DEEPGRAM_API_KEY="your-key" +export SPEECHMATICS_API_KEY="your-key" +``` + +**Note:** Requires `ffmpeg` installed for audio conversion to PCM16. + +## Usage + +Run from the project root directory: + +```bash +# Test both providers with diarization +python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize + +# Test only Deepgram +python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize --providers deepgram + +# Test with keyterm boosting (Deepgram) +python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize --keyterms "Dograh" "Pipecat" + +# Use different sample rate (default: 8000 Hz) +python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize --sample-rate 16000 + +# Show word-level timings +python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize --show-words + +# Save results to JSON +python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize --save +``` + +## CLI Options + +| Option | Description | +|--------|-------------| +| `audio_file` | Path to audio file (relative to evals/stt/ or absolute) | +| `--providers` | Providers to test: `deepgram`, `speechmatics` (default: both) | +| `--diarize` | Enable speaker diarization | +| `--keyterms` | Keywords to boost (Deepgram) / additional vocab (Speechmatics) | +| `--language` | Language code (default: en) | +| `--sample-rate` | Audio sample rate for streaming (default: 8000) | +| `--show-words` | Show individual word timings | +| `--save` | Save results to JSON in `results/` | + +## Directory Structure + +``` +evals/stt/ +├── audio/ # Audio test files +│ └── multi_speaker.m4a +├── results/ # Saved benchmark results (JSON) +├── providers/ # STT provider implementations +│ ├── base.py # Base classes +│ ├── deepgram_provider.py # WebSocket streaming +│ └── speechmatics_provider.py # WebSocket streaming +├── audio_streamer.py # PCM16 audio file streamer +├── benchmark.py # Main runner script +└── README.md +``` + +## How It Works + +1. **Audio Conversion**: The `AudioStreamer` converts any audio file to raw PCM16 using ffmpeg +2. **WebSocket Connection**: Providers connect to their respective WebSocket APIs +3. **Streaming**: Audio is sent in chunks (configurable sample rate, default 8kHz) +4. **Result Collection**: Transcripts and speaker info are collected from WebSocket responses +5. **Comparison**: Results are parsed into a common format for comparison + +## Output Example + +``` +Audio file: /path/to/audio/multi_speaker.m4a +Providers: ['deepgram', 'speechmatics'] +Diarization: True +Sample rate: 8000 Hz + +============================================================ +Provider: DEEPGRAM +============================================================ + +Duration: 45.32s +Speakers detected: 2 - ['0', '1'] + +Transcript: +Hello, welcome to the demo... + +--- Speaker Segments --- +[0.0s] Speaker 0: Hello, welcome to the demo. +[2.5s] Speaker 1: Thanks for having me. +... + +============================================================ +COMPARISON SUMMARY +============================================================ + +Provider Duration Speakers Words +--------------------------------------------- +deepgram 45.32 2 312 +speechmatics 45.32 2 308 +``` + +## Adding New Providers + +1. Create a new file in `providers/` (e.g., `whisper_provider.py`) +2. Implement the `STTProvider` abstract class with WebSocket streaming +3. Use `AudioStreamer` for PCM16 conversion +4. Add to `providers/__init__.py` +5. Add to `benchmark.py` provider choices + +## API Documentation + +- Deepgram Streaming: https://developers.deepgram.com/docs/live-streaming-audio +- Deepgram Diarization: https://developers.deepgram.com/docs/diarization +- Deepgram Keyterms: https://developers.deepgram.com/docs/keyterm +- Speechmatics RT API: https://docs.speechmatics.com/rt-api-ref +- Speechmatics Diarization: https://docs.speechmatics.com/features/diarization diff --git a/evals/stt/__init__.py b/evals/stt/__init__.py new file mode 100644 index 0000000..8c12ae8 --- /dev/null +++ b/evals/stt/__init__.py @@ -0,0 +1 @@ +# STT Evaluation Benchmark diff --git a/evals/stt/audio/multi_speaker.m4a b/evals/stt/audio/multi_speaker.m4a new file mode 100644 index 0000000..d1ab1f5 Binary files /dev/null and b/evals/stt/audio/multi_speaker.m4a differ diff --git a/evals/stt/audio/nope.m4a b/evals/stt/audio/nope.m4a new file mode 100644 index 0000000..f9c86c2 Binary files /dev/null and b/evals/stt/audio/nope.m4a differ diff --git a/evals/stt/audio/not_so_sure.m4a b/evals/stt/audio/not_so_sure.m4a new file mode 100644 index 0000000..acaae9a Binary files /dev/null and b/evals/stt/audio/not_so_sure.m4a differ diff --git a/evals/stt/audio/vad.m4a b/evals/stt/audio/vad.m4a new file mode 100644 index 0000000..e0488c7 Binary files /dev/null and b/evals/stt/audio/vad.m4a differ diff --git a/evals/stt/audio/yes.m4a b/evals/stt/audio/yes.m4a new file mode 100644 index 0000000..3c6e5a9 Binary files /dev/null and b/evals/stt/audio/yes.m4a differ diff --git a/evals/stt/audio_streamer.py b/evals/stt/audio_streamer.py new file mode 100644 index 0000000..d9c98fc --- /dev/null +++ b/evals/stt/audio_streamer.py @@ -0,0 +1,140 @@ +"""Audio file streamer - converts audio files to PCM16 streams.""" + +import asyncio +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import AsyncIterator + + +@dataclass +class AudioConfig: + """Audio streaming configuration.""" + + sample_rate: int = 8000 + channels: int = 1 + sample_width: int = 2 # 16-bit = 2 bytes + chunk_duration_ms: int = 80 # Send chunks every 80ms + + @property + def chunk_size(self) -> int: + """Bytes per chunk based on duration.""" + samples_per_chunk = int(self.sample_rate * self.chunk_duration_ms / 1000) + return samples_per_chunk * self.channels * self.sample_width + + +class AudioStreamer: + """Streams audio files as PCM16 chunks. + + Converts any audio format to raw PCM16 using ffmpeg and streams + in real-time chunks to simulate live audio. + """ + + def __init__(self, config: AudioConfig | None = None): + self.config = config or AudioConfig() + + def convert_to_pcm16(self, audio_path: Path) -> bytes: + """Convert audio file to raw PCM16 bytes using ffmpeg. + + Args: + audio_path: Path to input audio file + + Returns: + Raw PCM16 audio bytes + """ + cmd = [ + "ffmpeg", + "-i", + str(audio_path), + "-f", + "s16le", # signed 16-bit little-endian + "-acodec", + "pcm_s16le", + "-ar", + str(self.config.sample_rate), + "-ac", + str(self.config.channels), + "-", # output to stdout + ] + + result = subprocess.run( + cmd, + capture_output=True, + check=True, + ) + return result.stdout + + async def stream_file( + self, + audio_path: Path, + realtime: bool = True, + trailing_silence_seconds: float = 0.0, + ) -> AsyncIterator[bytes]: + """Stream audio file as PCM16 chunks. + + Args: + audio_path: Path to audio file + realtime: If True, add delays to simulate real-time streaming + trailing_silence_seconds: Seconds of silence to append after audio ends. + Useful for capturing pending end-of-turn events from STT providers. + + Yields: + PCM16 audio chunks + """ + # Convert entire file to PCM16 + pcm_data = self.convert_to_pcm16(audio_path) + + chunk_size = self.config.chunk_size + delay = self.config.chunk_duration_ms / 1000.0 if realtime else 0 + + # Stream audio chunks + for i in range(0, len(pcm_data), chunk_size): + chunk = pcm_data[i : i + chunk_size] + if chunk: + yield chunk + if realtime and delay > 0: + await asyncio.sleep(delay) + + # Stream trailing silence if requested + if trailing_silence_seconds > 0: + silence_chunk = bytes(chunk_size) # Zero-filled bytes = silence + num_silence_chunks = int(trailing_silence_seconds / (self.config.chunk_duration_ms / 1000.0)) + + for _ in range(num_silence_chunks): + yield silence_chunk + if realtime and delay > 0: + await asyncio.sleep(delay) + + async def stream_file_fast(self, audio_path: Path) -> AsyncIterator[bytes]: + """Stream audio file as fast as possible (no real-time delay). + + Args: + audio_path: Path to audio file + + Yields: + PCM16 audio chunks + """ + async for chunk in self.stream_file(audio_path, realtime=False): + yield chunk + + def get_duration(self, audio_path: Path) -> float: + """Get audio file duration in seconds. + + Args: + audio_path: Path to audio file + + Returns: + Duration in seconds + """ + cmd = [ + "ffprobe", + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=noprint_wrappers=1:nokey=1", + str(audio_path), + ] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return float(result.stdout.strip()) diff --git a/evals/stt/benchmark.py b/evals/stt/benchmark.py new file mode 100644 index 0000000..7740cee --- /dev/null +++ b/evals/stt/benchmark.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""STT Benchmark Runner. + +Compare speech-to-text transcription across providers with focus on: +- Speaker diarization accuracy +- Keyword/keyterm recognition +- Transcription quality + +Usage: + python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize + python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize --providers deepgram + python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize --keyterms "Dograh" "Pipecat" +""" + +import argparse +import asyncio +import json +import sys +from datetime import datetime +from pathlib import Path +from typing import Any + +from evals.stt.providers import ( + DeepgramProvider, + DeepgramFluxProvider, + SpeechmaticsProvider, + LocalSmartTurnProvider, + STTProvider, + TranscriptionResult, +) + + +def get_provider(name: str) -> STTProvider: + """Get provider instance by name.""" + providers = { + "deepgram": DeepgramProvider, + "deepgram-flux": DeepgramFluxProvider, + "speechmatics": SpeechmaticsProvider, + "local-smart-turn": LocalSmartTurnProvider, + } + if name not in providers: + raise ValueError(f"Unknown provider: {name}. Available: {list(providers.keys())}") + return providers[name]() + + +async def run_transcription( + provider: STTProvider, + audio_path: Path, + diarize: bool = False, + keyterms: list[str] | None = None, + **kwargs: Any, +) -> TranscriptionResult: + """Run transcription with a provider.""" + print(f"\n{'='*60}") + print(f"Provider: {provider.name.upper()}") + print(f"{'='*60}") + + try: + result = await provider.transcribe( + audio_path, + diarize=diarize, + keyterms=keyterms, + **kwargs, + ) + return result + except Exception as e: + print(f"Error with {provider.name}: {e}") + raise + + +def print_result(result: TranscriptionResult, show_words: bool = False) -> None: + """Print transcription result.""" + print(f"\nDuration: {result.duration:.2f}s") + print(f"Speakers detected: {len(result.speakers)} - {result.speakers}") + print(f"\nTranscript:\n{result.transcript}") + + if result.speakers: + print(f"\n--- Speaker Segments ---") + for segment in result.get_speaker_segments(): + speaker = segment["speaker"] or "?" + text = segment["text"] + start = segment["start"] + print(f"[{start:.1f}s] Speaker {speaker}: {text}") + + if show_words: + print(f"\n--- Words ---") + for word in result.words[:50]: # First 50 words + speaker_info = f" (S{word.speaker})" if word.speaker else "" + print(f" {word.start:.2f}s: {word.word}{speaker_info} [{word.confidence:.2f}]") + if len(result.words) > 50: + print(f" ... and {len(result.words) - 50} more words") + + +def save_results( + results: list[TranscriptionResult], + output_dir: Path, + audio_name: str, +) -> Path: + """Save results to JSON file.""" + output_dir.mkdir(parents=True, exist_ok=True) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_file = output_dir / f"{audio_name}_{timestamp}.json" + + output_data = { + "timestamp": timestamp, + "audio_file": audio_name, + "results": [r.to_dict() for r in results], + } + + with open(output_file, "w") as f: + json.dump(output_data, f, indent=2) + + print(f"\nResults saved to: {output_file}") + return output_file + + +def compare_results(results: list[TranscriptionResult]) -> None: + """Compare results across providers.""" + if len(results) < 2: + return + + print(f"\n{'='*60}") + print("COMPARISON SUMMARY") + print(f"{'='*60}") + + print(f"\n{'Provider':<15} {'Duration':<10} {'Speakers':<10} {'Words':<10}") + print("-" * 45) + for r in results: + print(f"{r.provider:<15} {r.duration:<10.2f} {len(r.speakers):<10} {len(r.words):<10}") + + # Compare speaker counts + speaker_counts = {r.provider: len(r.speakers) for r in results} + if len(set(speaker_counts.values())) > 1: + print(f"\nNote: Providers detected different speaker counts: {speaker_counts}") + + +async def main() -> int: + parser = argparse.ArgumentParser( + description="STT Benchmark - Compare transcription providers", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize + python -m evals.stt.benchmark audio/multi_speaker.m4a --diarize --providers deepgram + python -m evals.stt.benchmark audio/multi_speaker.m4a --keyterms "Dograh" "API" + """, + ) + parser.add_argument( + "audio_file", + type=str, + help="Path to audio file (relative to evals/stt/ or absolute)", + ) + parser.add_argument( + "--providers", + nargs="+", + default=["deepgram", "speechmatics"], + choices=["deepgram", "deepgram-flux", "speechmatics", "local-smart-turn"], + help="Providers to test (default: all)", + ) + parser.add_argument( + "--diarize", + action="store_true", + help="Enable speaker diarization", + ) + parser.add_argument( + "--keyterms", + nargs="+", + help="Keywords to boost (Deepgram only)", + ) + parser.add_argument( + "--language", + default="en", + help="Language code (default: en)", + ) + parser.add_argument( + "--sample-rate", + type=int, + default=8000, + help="Audio sample rate for streaming (default: 8000)", + ) + parser.add_argument( + "--show-words", + action="store_true", + help="Show individual word timings", + ) + parser.add_argument( + "--save", + action="store_true", + help="Save results to JSON file", + ) + parser.add_argument( + "--output-dir", + type=str, + default="results", + help="Output directory for results (default: results)", + ) + + args = parser.parse_args() + + # Resolve audio path + script_dir = Path(__file__).parent + audio_path = Path(args.audio_file) + if not audio_path.is_absolute(): + audio_path = script_dir / audio_path + + if not audio_path.exists(): + print(f"Error: Audio file not found: {audio_path}") + return 1 + + print(f"Audio file: {audio_path}") + print(f"Providers: {args.providers}") + print(f"Diarization: {args.diarize}") + print(f"Sample rate: {args.sample_rate} Hz") + if args.keyterms: + print(f"Keyterms: {args.keyterms}") + + results: list[TranscriptionResult] = [] + + for provider_name in args.providers: + try: + provider = get_provider(provider_name) + result = await run_transcription( + provider, + audio_path, + diarize=args.diarize, + keyterms=args.keyterms, + language=args.language, + sample_rate=args.sample_rate, + ) + print_result(result, show_words=args.show_words) + results.append(result) + except Exception as e: + print(f"\nFailed to run {provider_name}: {e}") + continue + + if len(results) > 1: + compare_results(results) + + if args.save and results: + output_dir = script_dir / args.output_dir + save_results(results, output_dir, audio_path.stem) + + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) diff --git a/evals/stt/event_capture.py b/evals/stt/event_capture.py new file mode 100644 index 0000000..7370dfb --- /dev/null +++ b/evals/stt/event_capture.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +"""STT Event Capture Runner. + +Streams audio to STT providers and captures raw WebSocket events with timestamps +for visualization in the web UI. + +Usage: + python -m evals.stt.event_capture audio/multi_speaker.m4a --provider deepgram + python -m evals.stt.event_capture audio/multi_speaker.m4a --provider speechmatics +""" + +import argparse +import asyncio +import json +import sys +from dataclasses import asdict, dataclass, field +from datetime import datetime +from pathlib import Path +from typing import Any, Callable + +from evals.stt.audio_streamer import AudioStreamer +from evals.stt.providers import ( + DeepgramFluxProvider, + DeepgramProvider, + SpeechmaticsProvider, + STTProvider, +) + + +@dataclass +class CapturedEvent: + """A captured WebSocket event with timestamp.""" + + timestamp: float # Time since stream start (seconds) + event_type: str # e.g., "Results", "TurnInfo", "AddTranscript" + data: dict[str, Any] # Raw event payload + + def to_dict(self) -> dict[str, Any]: + return { + "timestamp": self.timestamp, + "event_type": self.event_type, + "data": self.data, + } + + +@dataclass +class EventCaptureResult: + """Result from event capture session.""" + + audio_file: str + audio_path: str # Relative path to audio from results dir + provider: str + duration: float + created_at: str + events: list[CapturedEvent] = field(default_factory=list) + transcript: str = "" # Final transcript for reference + + def to_dict(self) -> dict[str, Any]: + return { + "audio_file": self.audio_file, + "audio_path": self.audio_path, + "provider": self.provider, + "duration": self.duration, + "created_at": self.created_at, + "events": [e.to_dict() for e in self.events], + "transcript": self.transcript, + } + + +EventCallback = Callable[[str, dict[str, Any]], None] + + +def get_provider(name: str) -> STTProvider: + """Get provider instance by name.""" + providers = { + "deepgram": DeepgramProvider, + "deepgram-flux": DeepgramFluxProvider, + "speechmatics": SpeechmaticsProvider, + } + if name not in providers: + raise ValueError(f"Unknown provider: {name}. Available: {list(providers.keys())}") + return providers[name]() + + +async def capture_events( + provider: STTProvider, + audio_path: Path, + sample_rate: int = 8000, + **kwargs: Any, +) -> EventCaptureResult: + """Capture WebSocket events from a provider. + + Args: + provider: The STT provider to use + audio_path: Path to the audio file + sample_rate: Audio sample rate + **kwargs: Additional provider parameters + + Returns: + EventCaptureResult with all captured events + """ + # Get audio duration + streamer = AudioStreamer() + duration = streamer.get_duration(audio_path) + + # Event list and start time + events: list[CapturedEvent] = [] + start_time: float | None = None + + def on_event(event_type: str, data: dict[str, Any]) -> None: + """Callback for capturing events.""" + nonlocal start_time + if start_time is None: + start_time = asyncio.get_event_loop().time() + + timestamp = asyncio.get_event_loop().time() - start_time + events.append(CapturedEvent(timestamp=timestamp, event_type=event_type, data=data)) + + # Run transcription with event callback + result = await provider.transcribe( + audio_path, + sample_rate=sample_rate, + on_event=on_event, + **kwargs, + ) + + return EventCaptureResult( + audio_file=audio_path.name, + audio_path=f"../audio/{audio_path.name}", + provider=provider.name, + duration=duration, + created_at=datetime.now().isoformat(), + events=events, + transcript=result.transcript, + ) + + +def save_result(result: EventCaptureResult, output_dir: Path) -> Path: + """Save capture result to JSON file. + + Args: + result: The capture result to save + output_dir: Directory to save results + + Returns: + Path to the saved file + """ + output_dir.mkdir(parents=True, exist_ok=True) + + # Format: {audio_name}-{provider}.json + audio_name = Path(result.audio_file).stem + output_file = output_dir / f"{audio_name}-{result.provider}.json" + + with open(output_file, "w") as f: + json.dump(result.to_dict(), f, indent=2) + + return output_file + + +async def main() -> int: + parser = argparse.ArgumentParser( + description="STT Event Capture - Capture WebSocket events for visualization", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python -m evals.stt.event_capture audio/multi_speaker.m4a --provider deepgram + python -m evals.stt.event_capture audio/multi_speaker.m4a --provider speechmatics --diarize + """, + ) + parser.add_argument( + "audio_file", + type=str, + help="Path to audio file (relative to evals/stt/ or absolute)", + ) + parser.add_argument( + "--provider", + required=True, + choices=["deepgram", "deepgram-flux", "speechmatics"], + help="STT provider to use", + ) + parser.add_argument( + "--sample-rate", + type=int, + default=8000, + help="Audio sample rate for streaming (default: 8000)", + ) + parser.add_argument( + "--diarize", + action="store_true", + help="Enable speaker diarization", + ) + parser.add_argument( + "--output-dir", + type=str, + default="results", + help="Output directory for results (default: results)", + ) + + args = parser.parse_args() + + # Resolve audio path + script_dir = Path(__file__).parent + audio_path = Path(args.audio_file) + if not audio_path.is_absolute(): + audio_path = script_dir / audio_path + + if not audio_path.exists(): + print(f"Error: Audio file not found: {audio_path}") + return 1 + + print(f"Audio file: {audio_path}") + print(f"Provider: {args.provider}") + print(f"Sample rate: {args.sample_rate} Hz") + print(f"Diarization: {args.diarize}") + + try: + provider = get_provider(args.provider) + print(f"\nCapturing events from {provider.name}...") + + result = await capture_events( + provider, + audio_path, + sample_rate=args.sample_rate, + diarize=args.diarize, + ) + + output_dir = script_dir / args.output_dir + output_file = save_result(result, output_dir) + + print(f"\nCapture complete!") + print(f" Duration: {result.duration:.2f}s") + print(f" Events: {len(result.events)}") + print(f" Saved to: {output_file}") + + # Show first few events + print(f"\nFirst 5 events:") + for event in result.events[:5]: + print(f" [{event.timestamp:.2f}s] {event.event_type}") + + return 0 + + except Exception as e: + print(f"\nError: {e}") + import traceback + + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) diff --git a/evals/stt/providers/__init__.py b/evals/stt/providers/__init__.py new file mode 100644 index 0000000..5df045c --- /dev/null +++ b/evals/stt/providers/__init__.py @@ -0,0 +1,16 @@ +from .base import EventCallback, STTProvider, TranscriptionResult, Word +from .deepgram_provider import DeepgramProvider +from .deepgram_flux_provider import DeepgramFluxProvider +from .speechmatics_provider import SpeechmaticsProvider +from .local_smart_turn_provider import LocalSmartTurnProvider + +__all__ = [ + "EventCallback", + "STTProvider", + "TranscriptionResult", + "Word", + "DeepgramProvider", + "DeepgramFluxProvider", + "SpeechmaticsProvider", + "LocalSmartTurnProvider", +] diff --git a/evals/stt/providers/base.py b/evals/stt/providers/base.py new file mode 100644 index 0000000..de41265 --- /dev/null +++ b/evals/stt/providers/base.py @@ -0,0 +1,128 @@ +"""Base classes for STT providers.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Callable + +# Event callback type: (event_type, data) -> None +EventCallback = Callable[[str, dict[str, Any]], None] + + +@dataclass +class Word: + """Represents a transcribed word with metadata.""" + + word: str + start: float + end: float + confidence: float + speaker: str | None = None + speaker_confidence: float | None = None + + def to_dict(self) -> dict[str, Any]: + return { + "word": self.word, + "start": self.start, + "end": self.end, + "confidence": self.confidence, + "speaker": self.speaker, + "speaker_confidence": self.speaker_confidence, + } + + +@dataclass +class TranscriptionResult: + """Result from STT transcription.""" + + provider: str + transcript: str + words: list[Word] + speakers: list[str] + duration: float + raw_response: dict[str, Any] = field(default_factory=dict) + params: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return { + "provider": self.provider, + "transcript": self.transcript, + "words": [w.to_dict() for w in self.words], + "speakers": self.speakers, + "duration": self.duration, + "params": self.params, + } + + def get_speaker_segments(self) -> list[dict[str, Any]]: + """Get transcript segmented by speaker.""" + if not self.words: + return [] + + segments = [] + current_speaker = None + current_text = [] + segment_start = 0.0 + + for word in self.words: + if word.speaker != current_speaker: + if current_text: + segments.append( + { + "speaker": current_speaker, + "text": " ".join(current_text), + "start": segment_start, + "end": self.words[len(segments) - 1].end + if segments + else word.start, + } + ) + current_speaker = word.speaker + current_text = [word.word] + segment_start = word.start + else: + current_text.append(word.word) + + if current_text: + segments.append( + { + "speaker": current_speaker, + "text": " ".join(current_text), + "start": segment_start, + "end": self.words[-1].end if self.words else 0.0, + } + ) + + return segments + + +class STTProvider(ABC): + """Abstract base class for STT providers.""" + + @property + @abstractmethod + def name(self) -> str: + """Provider name.""" + pass + + @abstractmethod + async def transcribe( + self, + audio_path: Path, + diarize: bool = False, + keyterms: list[str] | None = None, + on_event: EventCallback | None = None, + **kwargs: Any, + ) -> TranscriptionResult: + """Transcribe audio file. + + Args: + audio_path: Path to the audio file + diarize: Enable speaker diarization + keyterms: List of keywords to boost (if supported) + on_event: Optional callback for raw WebSocket events (event_type, data) + **kwargs: Provider-specific parameters + + Returns: + TranscriptionResult with transcript and metadata + """ + pass diff --git a/evals/stt/providers/deepgram_flux_provider.py b/evals/stt/providers/deepgram_flux_provider.py new file mode 100644 index 0000000..860dbd2 --- /dev/null +++ b/evals/stt/providers/deepgram_flux_provider.py @@ -0,0 +1,235 @@ +"""Deepgram Flux STT provider with WebSocket streaming. + +Flux is Deepgram's conversational AI model with built-in turn detection. +It has a different API than Nova models - no language/punctuate/diarize params. +""" + +import asyncio +import json +import os +from pathlib import Path +from typing import Any +from urllib.parse import urlencode + +from loguru import logger + +from ..audio_streamer import AudioConfig, AudioStreamer +from .base import EventCallback, STTProvider, TranscriptionResult, Word + +try: + from websockets.asyncio.client import connect as websocket_connect +except ImportError: + raise ImportError("websockets required: pip install websockets") + + +class DeepgramFluxProvider(STTProvider): + """Deepgram Flux Speech-to-Text provider with WebSocket streaming. + + Flux is optimized for conversational AI with built-in turn detection. + + Key differences from Nova: + - Uses v2 API endpoint + - Only supports English (flux-general-en) + - No punctuate, diarize, or language params + - Has turn detection events (StartOfTurn, EndOfTurn, EagerEndOfTurn) + - Supports keyterm boosting + + API Docs: https://developers.deepgram.com/docs/ + """ + + WS_URL = "wss://api.deepgram.com/v2/listen" + + def __init__(self, api_key: str | None = None): + self.api_key = api_key or os.getenv("DEEPGRAM_API_KEY") + if not self.api_key: + raise ValueError( + "Deepgram API key required. Set DEEPGRAM_API_KEY env var or pass api_key." + ) + + @property + def name(self) -> str: + return "deepgram-flux" + + async def transcribe( + self, + audio_path: Path, + diarize: bool = False, # Ignored - Flux doesn't support diarization + keyterms: list[str] | None = None, + on_event: EventCallback | None = None, + model: str = "flux-general-en", + sample_rate: int = 16000, + eot_threshold: float | None = 0.70, + eot_timeout_ms: int | None = 3000, + eager_eot_threshold: float | None = None, + trailing_silence_seconds: float = 3.0, + **kwargs: Any, + ) -> TranscriptionResult: + """Transcribe audio using Deepgram Flux WebSocket streaming. + + Args: + audio_path: Path to audio file + diarize: IGNORED - Flux does not support diarization + keyterms: List of keywords to boost recognition + on_event: Optional callback for raw WebSocket events + model: Flux model (default: flux-general-en) + sample_rate: Audio sample rate (default: 16000 for Flux) + eot_threshold: End-of-turn confidence threshold (0-1, default 0.7) + eot_timeout_ms: Timeout in ms to force end of turn (default 5000) + eager_eot_threshold: Threshold for eager end-of-turn events + trailing_silence_seconds: Seconds of silence after audio to capture pending events + **kwargs: Additional Flux parameters + + Returns: + TranscriptionResult with transcript (no speaker info - Flux doesn't support diarization) + """ + if diarize: + logger.warning("Flux does not support diarization - ignoring diarize=True") + + # Build query params - Flux only supports specific params + params: dict[str, Any] = { + "model": model, + "encoding": "linear16", + "sample_rate": sample_rate, + } + + # Flux-specific turn detection params + if eot_threshold is not None: + params["eot_threshold"] = eot_threshold + if eot_timeout_ms is not None: + params["eot_timeout_ms"] = eot_timeout_ms + if eager_eot_threshold is not None: + params["eager_eot_threshold"] = eager_eot_threshold + + # Build URL with params + url_parts = [f"{k}={v}" for k, v in params.items()] + + # Add keyterms (repeated params) + if keyterms: + for term in keyterms: + url_parts.append(urlencode({"keyterm": term})) + + ws_url = f"{self.WS_URL}?{'&'.join(url_parts)}" + logger.debug(f"Flux WebSocket URL: {ws_url}") + + # Setup audio streamer + audio_config = AudioConfig(sample_rate=sample_rate) + streamer = AudioStreamer(audio_config) + + # Collect results + all_transcripts: list[dict[str, Any]] = [] + final_transcript = "" + duration = 0.0 + connected = asyncio.Event() + + async with websocket_connect( + ws_url, + additional_headers={"Authorization": f"Token {self.api_key}"}, + ) as ws: + + async def send_audio(): + """Send audio chunks to Deepgram Flux.""" + await connected.wait() + + chunk_no = 0 + async for chunk in streamer.stream_file( + audio_path, trailing_silence_seconds=trailing_silence_seconds + ): + logger.trace(f"[deepgram-flux] Sent audio chunk {chunk_no}") + await ws.send(chunk) + chunk_no += 1 + + async def receive_messages(): + """Receive and collect Flux messages.""" + nonlocal all_transcripts, final_transcript, duration + + async for message in ws: + if isinstance(message, str): + data = json.loads(message) + msg_type = data.get("type") + logger.debug(f"[deepgram-flux] Received {msg_type}: {data}") + + # Emit event via callback if provided + if on_event and msg_type: + on_event(msg_type, data) + + if msg_type == "Connected": + logger.info("[deepgram-flux] Connected") + connected.set() + + elif msg_type == "TurnInfo": + event = data.get("event") + transcript = data.get("transcript", "") + words = data.get("words", []) + + if event == "EndOfTurn": + if transcript: + final_transcript += transcript + " " + if words: + all_transcripts.append({ + "transcript": transcript, + "words": words, + }) + # Get duration from last word + if words: + last_word = words[-1] + duration = max(duration, last_word.get("end", 0)) + + elif event == "TurnResumed": + logger.debug("TurnResumed") + + elif msg_type == "Error": + raise Exception(f"Deepgram Flux error: {data}") + + # Run send and receive concurrently + send_task = asyncio.create_task(send_audio()) + receive_task = asyncio.create_task(receive_messages()) + + await send_task + + logger.debug("[deepgram-flux] Send task done") + try: + await asyncio.wait_for(receive_task, timeout=10.0) + except asyncio.TimeoutError: + pass + + return self._parse_results( + all_transcripts, final_transcript.strip(), duration, params, keyterms + ) + + def _parse_results( + self, + transcripts: list[dict[str, Any]], + final_transcript: str, + duration: float, + params: dict[str, Any], + keyterms: list[str] | None, + ) -> TranscriptionResult: + """Parse collected Flux results into TranscriptionResult.""" + words = [] + + for turn in transcripts: + for w in turn.get("words", []): + words.append( + Word( + word=w.get("word", ""), + start=w.get("start", 0.0), + end=w.get("end", 0.0), + confidence=w.get("confidence", 0.0), + speaker=None, # Flux doesn't support diarization + speaker_confidence=None, + ) + ) + + stored_params = dict(params) + if keyterms: + stored_params["keyterms"] = keyterms + + return TranscriptionResult( + provider=self.name, + transcript=final_transcript, + words=words, + speakers=[], # Flux doesn't support diarization + duration=duration, + raw_response={"transcripts": transcripts}, + params=stored_params, + ) diff --git a/evals/stt/providers/deepgram_provider.py b/evals/stt/providers/deepgram_provider.py new file mode 100644 index 0000000..14311a4 --- /dev/null +++ b/evals/stt/providers/deepgram_provider.py @@ -0,0 +1,236 @@ +"""Deepgram STT provider with WebSocket streaming.""" + +import asyncio +import json +import os +from pathlib import Path +from typing import Any +from urllib.parse import urlencode + +from ..audio_streamer import AudioConfig, AudioStreamer +from .base import EventCallback, STTProvider, TranscriptionResult, Word +from loguru import logger + +try: + from websockets.asyncio.client import connect as websocket_connect +except ImportError: + raise ImportError("websockets required: pip install websockets") + + +class DeepgramProvider(STTProvider): + """Deepgram Nova Speech-to-Text provider with WebSocket streaming. + + API Docs: https://developers.deepgram.com/docs/ + + Supports: + - Speaker diarization via `diarize=true` + - Keyterm boosting via `keyterm` parameter + - Real-time streaming via WebSocket + - Multiple languages + - Punctuation + + For Flux models, use DeepgramFluxProvider instead. + """ + + WS_URL = "wss://api.deepgram.com/v1/listen" + + def __init__(self, api_key: str | None = None): + self.api_key = api_key or os.getenv("DEEPGRAM_API_KEY") + if not self.api_key: + raise ValueError( + "Deepgram API key required. Set DEEPGRAM_API_KEY env var or pass api_key." + ) + + @property + def name(self) -> str: + return "deepgram" + + async def transcribe( + self, + audio_path: Path, + diarize: bool = False, + keyterms: list[str] | None = None, + on_event: EventCallback | None = None, + model: str = "nova-3-general", + language: str = "en", + sample_rate: int = 8000, + punctuate: bool = True, + trailing_silence_seconds: float = 3.0, + **kwargs: Any, + ) -> TranscriptionResult: + """Transcribe audio using Deepgram Nova WebSocket streaming. + + Args: + audio_path: Path to audio file + diarize: Enable speaker diarization + keyterms: List of keywords to boost recognition + on_event: Optional callback for raw WebSocket events + model: Deepgram Nova model (nova-3, nova-2, etc.) + language: Language code + sample_rate: Audio sample rate for streaming + punctuate: Add punctuation + trailing_silence_seconds: Seconds of silence after audio to capture pending events + **kwargs: Additional Deepgram parameters + + Returns: + TranscriptionResult with transcript and speaker info + """ + # Build query params + params: dict[str, Any] = { + "model": model, + "language": language, + "punctuate": str(punctuate).lower(), + "encoding": "linear16", + "sample_rate": sample_rate, + "channels": 1, + "interim_results": "true", + "smart_format": "true", + "profanity_filter": "true", + "vad_events": "true", + "utterance_end_ms": "1000" + } + + if diarize: + params["diarize"] = "true" + + # Build URL with params + url_parts = [f"{k}={v}" for k, v in params.items()] + + # Add keyterms (repeated params) + if keyterms: + for term in keyterms: + url_parts.append(urlencode({"keyterm": term})) + + # Add extra kwargs + for k, v in kwargs.items(): + url_parts.append(f"{k}={v}") + + ws_url = f"{self.WS_URL}?{'&'.join(url_parts)}" + logger.debug(f"Deepgram WebSocket URL: {ws_url}") + + # Setup audio streamer + audio_config = AudioConfig(sample_rate=sample_rate) + streamer = AudioStreamer(audio_config) + + # Collect results + all_words: list[dict[str, Any]] = [] + final_transcript = "" + duration = 0.0 + + try: + async with websocket_connect( + ws_url, + additional_headers={"Authorization": f"Token {self.api_key}"}, + ) as ws: + # Create tasks for sending and receiving + send_complete = asyncio.Event() + + async def send_audio(): + """Send audio chunks to Deepgram.""" + chunk_no = 0 + async for chunk in streamer.stream_file( + audio_path, trailing_silence_seconds=trailing_silence_seconds + ): + logger.trace(f"[deepgram] Sent audio chunk {chunk_no}") + await ws.send(chunk) + chunk_no += 1 + # Send close message + logger.debug(f"[deepgram] Sending CloseStream after {chunk_no} chunks") + await ws.send(json.dumps({"type": "CloseStream"})) + send_complete.set() + + async def receive_transcripts(): + """Receive and collect transcription results.""" + nonlocal all_words, final_transcript, duration + + async for message in ws: + if isinstance(message, str): + data = json.loads(message) + msg_type = data.get("type") + logger.debug(f"[deepgram] Received {msg_type}: {data}") + + # Emit event via callback if provided + if on_event and msg_type: + on_event(msg_type, data) + + if msg_type == "Results": + # Nova-style response + channel = data.get("channel", {}) + alternatives = channel.get("alternatives", []) + if alternatives: + alt = alternatives[0] + words = alt.get("words", []) + all_words.extend(words) + + # Check if final + if data.get("is_final"): + final_transcript += alt.get("transcript", "") + " " + duration = max( + duration, data.get("duration", 0) + data.get("start", 0) + ) + + elif msg_type == "Metadata": + # Get duration from metadata + duration = data.get("duration", duration) + + elif msg_type == "Error": + raise Exception(f"Deepgram error: {data}") + + # Run send and receive concurrently + send_task = asyncio.create_task(send_audio()) + receive_task = asyncio.create_task(receive_transcripts()) + + # Wait for send to complete, then wait a bit for final results + await send_task + try: + await asyncio.wait_for(receive_task, timeout=5.0) + except asyncio.TimeoutError: + pass # Normal - websocket closes after final results + except Exception as e: + logger.exception(e) + + return self._parse_results( + all_words, final_transcript.strip(), duration, params, keyterms + ) + + def _parse_results( + self, + raw_words: list[dict[str, Any]], + transcript: str, + duration: float, + params: dict[str, Any], + keyterms: list[str] | None, + ) -> TranscriptionResult: + """Parse collected results into TranscriptionResult.""" + words = [] + speakers_set: set[str] = set() + + for w in raw_words: + speaker = str(w.get("speaker", "")) if "speaker" in w else None + if speaker: + speakers_set.add(speaker) + + words.append( + Word( + word=w.get("word", ""), + start=w.get("start", 0.0), + end=w.get("end", 0.0), + confidence=w.get("confidence", 0.0), + speaker=speaker, + speaker_confidence=w.get("speaker_confidence"), + ) + ) + + stored_params = dict(params) + if keyterms: + stored_params["keyterms"] = keyterms + + return TranscriptionResult( + provider=self.name, + transcript=transcript, + words=words, + speakers=sorted(speakers_set), + duration=duration, + raw_response={"words": raw_words}, + params=stored_params, + ) diff --git a/evals/stt/providers/local_smart_turn_provider.py b/evals/stt/providers/local_smart_turn_provider.py new file mode 100644 index 0000000..2133ef1 --- /dev/null +++ b/evals/stt/providers/local_smart_turn_provider.py @@ -0,0 +1,287 @@ +"""Local Smart Turn provider for benchmarking end-of-turn detection. + +Uses the pipecat smart-turn-v3 ONNX model for local ML-based turn detection. +This is NOT an STT provider - it only detects when a speaker has finished talking. +""" + +import os +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np +from loguru import logger + +from ..audio_streamer import AudioConfig, AudioStreamer +from .base import EventCallback, STTProvider, TranscriptionResult, Word + +try: + import onnxruntime as ort + from transformers import WhisperFeatureExtractor +except ImportError: + raise ImportError( + "onnxruntime and transformers required: pip install onnxruntime transformers" + ) + + +@dataclass +class TurnEvent: + """Represents a detected turn event.""" + timestamp: float # Time in audio when turn was detected + probability: float # Model confidence + prediction: int # 1=complete, 0=incomplete + inference_time_ms: float + + +class LocalSmartTurnProvider(STTProvider): + """Local Smart Turn provider for end-of-turn detection benchmarking. + + Uses the smart-turn-v3 ONNX model to detect when speakers finish talking. + This is useful for comparing turn detection accuracy against cloud services + like Deepgram Flux's built-in turn detection. + + NOTE: This provider does NOT produce transcripts - only turn detection events. + """ + + # Smart turn model requires 16kHz audio + REQUIRED_SAMPLE_RATE = 16000 + # Model analyzes 8 seconds of audio + WINDOW_SECONDS = 8 + + def __init__( + self, + model_path: str | None = None, + cpu_count: int = 1, + ): + """Initialize the local smart turn provider. + + Args: + model_path: Path to ONNX model file. If None, uses bundled model. + cpu_count: Number of CPUs for inference (default: 1) + """ + self.model_path = model_path + self.cpu_count = cpu_count + self._session = None + self._feature_extractor = None + + def _load_model(self): + """Lazy load the ONNX model.""" + if self._session is not None: + return + + model_path = self.model_path + + if not model_path: + # Try to load bundled model from pipecat + model_name = "smart-turn-v3.1-cpu.onnx" + package_path = "pipecat.audio.turn.smart_turn.data" + + try: + import importlib_resources as impresources + model_path = str(impresources.files(package_path).joinpath(model_name)) + except Exception: + from importlib import resources as impresources + try: + with impresources.path(package_path, model_name) as f: + model_path = str(f) + except Exception: + model_path = str(impresources.files(package_path).joinpath(model_name)) + + logger.info(f"[local-smart-turn] Loading model from {model_path}") + + # Configure ONNX runtime + so = ort.SessionOptions() + so.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + so.inter_op_num_threads = 1 + so.intra_op_num_threads = self.cpu_count + so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + + self._feature_extractor = WhisperFeatureExtractor(chunk_length=8) + self._session = ort.InferenceSession(model_path, sess_options=so) + + logger.info("[local-smart-turn] Model loaded") + + @property + def name(self) -> str: + return "local-smart-turn" + + def _predict_endpoint(self, audio_array: np.ndarray) -> dict[str, Any]: + """Predict end-of-turn using the ONNX model. + + Args: + audio_array: Audio samples as float32 numpy array (16kHz) + + Returns: + Dict with prediction (0/1) and probability + """ + # Truncate to last 8 seconds or pad to 8 seconds + max_samples = self.WINDOW_SECONDS * self.REQUIRED_SAMPLE_RATE + if len(audio_array) > max_samples: + audio_array = audio_array[-max_samples:] + elif len(audio_array) < max_samples: + padding = max_samples - len(audio_array) + audio_array = np.pad(audio_array, (padding, 0), mode="constant", constant_values=0) + + # Process using Whisper's feature extractor + inputs = self._feature_extractor( + audio_array, + sampling_rate=self.REQUIRED_SAMPLE_RATE, + return_tensors="np", + padding="max_length", + max_length=self.WINDOW_SECONDS * self.REQUIRED_SAMPLE_RATE, + truncation=True, + do_normalize=True, + ) + + # Extract features for ONNX + input_features = inputs.input_features.squeeze(0).astype(np.float32) + input_features = np.expand_dims(input_features, axis=0) + + # Run inference + start_time = time.perf_counter() + outputs = self._session.run(None, {"input_features": input_features}) + inference_time = (time.perf_counter() - start_time) * 1000 + + # Extract probability (model returns sigmoid probabilities) + probability = outputs[0][0].item() + prediction = 1 if probability > 0.5 else 0 + + return { + "prediction": prediction, + "probability": probability, + "inference_time_ms": inference_time, + } + + async def transcribe( + self, + audio_path: Path, + diarize: bool = False, # Ignored - not applicable + keyterms: list[str] | None = None, # Ignored - not applicable + on_event: EventCallback | None = None, # Ignored - not applicable + sample_rate: int = 16000, # Must be 16kHz for smart turn + analysis_interval_ms: int = 500, # How often to check for turn completion + **kwargs: Any, + ) -> TranscriptionResult: + """Analyze audio for turn detection events. + + NOTE: This does NOT produce transcripts. It detects when speakers + finish talking using ML-based turn detection. + + Args: + audio_path: Path to audio file + diarize: Ignored (not applicable for turn detection) + keyterms: Ignored (not applicable for turn detection) + on_event: Ignored (not applicable for turn detection) + sample_rate: Must be 16000 Hz for smart turn model + analysis_interval_ms: How often to run turn detection (ms) + **kwargs: Additional parameters (ignored) + + Returns: + TranscriptionResult with turn detection events in raw_response + """ + if sample_rate != self.REQUIRED_SAMPLE_RATE: + logger.warning( + f"[local-smart-turn] Sample rate must be {self.REQUIRED_SAMPLE_RATE}Hz, " + f"overriding {sample_rate}Hz" + ) + sample_rate = self.REQUIRED_SAMPLE_RATE + + # Load model if not already loaded + self._load_model() + + # Setup audio streamer at 16kHz + audio_config = AudioConfig(sample_rate=sample_rate) + streamer = AudioStreamer(audio_config) + + # Get audio duration + duration = streamer.get_duration(audio_path) + logger.info(f"[local-smart-turn] Processing {audio_path} ({duration:.2f}s)") + + # Collect all audio first (smart turn needs to analyze segments) + pcm_data = streamer.convert_to_pcm16(audio_path) + + # Convert to float32 for model + audio_int16 = np.frombuffer(pcm_data, dtype=np.int16) + audio_float32 = audio_int16.astype(np.float32) / 32768.0 + + # Analyze at intervals + turn_events: list[TurnEvent] = [] + samples_per_interval = int(sample_rate * analysis_interval_ms / 1000) + window_samples = self.WINDOW_SECONDS * sample_rate + + chunk_no = 0 + for end_sample in range(samples_per_interval, len(audio_float32), samples_per_interval): + # Get window of audio ending at current position + start_sample = max(0, end_sample - window_samples) + audio_window = audio_float32[start_sample:end_sample] + + current_time = end_sample / sample_rate + logger.debug(f"[local-smart-turn] Analyzing chunk {chunk_no} at {current_time:.2f}s") + + result = self._predict_endpoint(audio_window) + + turn_events.append(TurnEvent( + timestamp=current_time, + probability=result["probability"], + prediction=result["prediction"], + inference_time_ms=result["inference_time_ms"], + )) + + if result["prediction"] == 1: + logger.info( + f"[local-smart-turn] Turn complete at {current_time:.2f}s " + f"(prob={result['probability']:.3f})" + f"(inf time ms={result["inference_time_ms"]})" + ) + + chunk_no += 1 + + # Create result + # Convert turn events to word-like format for compatibility + words = [] + for event in turn_events: + if event.prediction == 1: + words.append(Word( + word=f"[END_OF_TURN prob={event.probability:.2f}]", + start=event.timestamp - 0.1, + end=event.timestamp, + confidence=event.probability, + speaker=None, + speaker_confidence=None, + )) + + # Count completed turns + completed_turns = sum(1 for e in turn_events if e.prediction == 1) + + params = { + "sample_rate": sample_rate, + "analysis_interval_ms": analysis_interval_ms, + "window_seconds": self.WINDOW_SECONDS, + } + + return TranscriptionResult( + provider=self.name, + transcript=f"[Turn detection only - {completed_turns} turns detected]", + words=words, + speakers=[], # Not applicable + duration=duration, + raw_response={ + "turn_events": [ + { + "timestamp": e.timestamp, + "probability": e.probability, + "prediction": e.prediction, + "inference_time_ms": e.inference_time_ms, + } + for e in turn_events + ], + "completed_turns": completed_turns, + "total_analyses": len(turn_events), + "avg_inference_time_ms": ( + sum(e.inference_time_ms for e in turn_events) / len(turn_events) + if turn_events else 0 + ), + }, + params=params, + ) diff --git a/evals/stt/providers/speechmatics_provider.py b/evals/stt/providers/speechmatics_provider.py new file mode 100644 index 0000000..1efd51e --- /dev/null +++ b/evals/stt/providers/speechmatics_provider.py @@ -0,0 +1,258 @@ +"""Speechmatics STT provider with WebSocket streaming.""" + +import asyncio +import json +import os +from pathlib import Path +from typing import Any + +from loguru import logger + +from ..audio_streamer import AudioConfig, AudioStreamer +from .base import EventCallback, STTProvider, TranscriptionResult, Word + +try: + from websockets.asyncio.client import connect as websocket_connect +except ImportError: + raise ImportError("websockets required: pip install websockets") + + +class SpeechmaticsProvider(STTProvider): + """Speechmatics Speech-to-Text provider with WebSocket streaming. + + API Docs: https://docs.speechmatics.com/ + + Supports: + - Speaker diarization via `diarization: "speaker"` config + - Speaker sensitivity tuning + - Real-time streaming via WebSocket + """ + + def __init__(self, api_key: str | None = None, region: str = "eu2"): + self.api_key = api_key or os.getenv("SPEECHMATICS_API_KEY") + if not self.api_key: + raise ValueError( + "Speechmatics API key required. Set SPEECHMATICS_API_KEY env var or pass api_key." + ) + # Set region-specific endpoint + self.ws_url = f"wss://{region}.rt.speechmatics.com/v2" + + @property + def name(self) -> str: + return "speechmatics" + + async def transcribe( + self, + audio_path: Path, + diarize: bool = False, + keyterms: list[str] | None = None, + on_event: EventCallback | None = None, + language: str = "en", + operating_point: str = "enhanced", + sample_rate: int = 8000, + speaker_sensitivity: float | None = None, + max_speakers: int | None = None, + trailing_silence_seconds: float = 3.0, + **kwargs: Any, + ) -> TranscriptionResult: + """Transcribe audio using Speechmatics WebSocket streaming. + + Args: + audio_path: Path to audio file + diarize: Enable speaker diarization + keyterms: Additional vocabulary (limited support) + on_event: Optional callback for raw WebSocket events + language: Language code + operating_point: "standard" or "enhanced" + sample_rate: Audio sample rate for streaming + speaker_sensitivity: 0.0-1.0, higher = more speakers detected + max_speakers: Maximum number of speakers to detect + trailing_silence_seconds: Seconds of silence after audio to capture pending events + **kwargs: Additional config parameters + + Returns: + TranscriptionResult with transcript and speaker info + """ + # Build transcription config for StartRecognition message + transcription_config: dict[str, Any] = { + "language": language, + "operating_point": operating_point, + "enable_partials": False, + } + + if diarize: + transcription_config["diarization"] = "speaker" + if speaker_sensitivity is not None: + transcription_config["speaker_diarization_config"] = { + "speaker_sensitivity": speaker_sensitivity + } + if max_speakers is not None: + if "speaker_diarization_config" not in transcription_config: + transcription_config["speaker_diarization_config"] = {} + transcription_config["speaker_diarization_config"]["max_speakers"] = max_speakers + + # Add additional vocabulary if provided + if keyterms: + transcription_config["additional_vocab"] = [{"content": term} for term in keyterms] + + # Audio format config + audio_format = { + "type": "raw", + "encoding": "pcm_s16le", + "sample_rate": sample_rate, + } + + # Store params for result + params = { + "diarize": diarize, + "language": language, + "operating_point": operating_point, + "sample_rate": sample_rate, + "speaker_sensitivity": speaker_sensitivity, + "max_speakers": max_speakers, + } + + # Setup audio streamer + audio_config = AudioConfig(sample_rate=sample_rate) + streamer = AudioStreamer(audio_config) + + # Collect results + all_results: list[dict[str, Any]] = [] + recognition_started = asyncio.Event() + transcription_complete = asyncio.Event() + + async with websocket_connect( + self.ws_url, + additional_headers={"Authorization": f"Bearer {self.api_key}"}, + ) as ws: + # Send StartRecognition message + start_msg = { + "message": "StartRecognition", + "transcription_config": transcription_config, + "audio_format": audio_format, + } + await ws.send(json.dumps(start_msg)) + + async def send_audio(): + """Send audio chunks after recognition starts.""" + await recognition_started.wait() + + chunk_no = 0 + async for chunk in streamer.stream_file( + audio_path, trailing_silence_seconds=trailing_silence_seconds + ): + logger.debug(f"[speechmatics] Sent audio chunk {chunk_no}") + await ws.send(chunk) + chunk_no += 1 + + # Signal end of audio with last sequence number + logger.debug(f"[speechmatics] Sending EndOfStream after {chunk_no} chunks") + await ws.send(json.dumps({"message": "EndOfStream", "last_seq_no": chunk_no})) + + async def receive_messages(): + """Receive and process messages.""" + nonlocal all_results + + async for message in ws: + if isinstance(message, str): + data = json.loads(message) + msg_type = data.get("message") + logger.debug(f"[speechmatics] Received {msg_type}: {data}") + + # Emit event via callback if provided + if on_event and msg_type: + on_event(msg_type, data) + + if msg_type == "RecognitionStarted": + logger.info("[speechmatics] Connected") + recognition_started.set() + + elif msg_type == "AddTranscript": + # Final transcript segment + results = data.get("results", []) + all_results.extend(results) + + elif msg_type == "EndOfTranscript": + transcription_complete.set() + return + + elif msg_type == "Error": + raise Exception(f"Speechmatics error: {data}") + + elif msg_type == "Warning": + logger.warning(f"[speechmatics] Warning: {data.get('reason')}") + + # Run send and receive concurrently + send_task = asyncio.create_task(send_audio()) + receive_task = asyncio.create_task(receive_messages()) + + # Wait for completion + await send_task + try: + await asyncio.wait_for(transcription_complete.wait(), timeout=30.0) + except asyncio.TimeoutError: + pass + + receive_task.cancel() + try: + await receive_task + except asyncio.CancelledError: + pass + + return self._parse_results(all_results, params) + + def _parse_results( + self, + results: list[dict[str, Any]], + params: dict[str, Any], + ) -> TranscriptionResult: + """Parse Speechmatics results.""" + words = [] + speakers_set: set[str] = set() + transcript_parts = [] + duration = 0.0 + + for item in results: + item_type = item.get("type") + alternatives = item.get("alternatives", []) + + if not alternatives: + continue + + alt = alternatives[0] + content = alt.get("content", "") + speaker = alt.get("speaker") + + if speaker: + speakers_set.add(speaker) + + end_time = item.get("end_time", 0.0) + duration = max(duration, end_time) + + if item_type == "word": + words.append( + Word( + word=content, + start=item.get("start_time", 0.0), + end=end_time, + confidence=alt.get("confidence", 0.0), + speaker=speaker, + speaker_confidence=None, + ) + ) + transcript_parts.append(content) + elif item_type == "punctuation": + if transcript_parts: + transcript_parts[-1] += content + + transcript = " ".join(transcript_parts) + + return TranscriptionResult( + provider=self.name, + transcript=transcript, + words=words, + speakers=sorted(speakers_set), + duration=duration, + raw_response={"results": results}, + params=params, + ) diff --git a/evals/stt/results/multi_speaker-deepgram-flux.json b/evals/stt/results/multi_speaker-deepgram-flux.json new file mode 100644 index 0000000..7754af7 --- /dev/null +++ b/evals/stt/results/multi_speaker-deepgram-flux.json @@ -0,0 +1,867 @@ +{ + "audio_file": "multi_speaker.m4a", + "audio_path": "../audio/multi_speaker.m4a", + "provider": "deepgram-flux", + "duration": 7.987664, + "created_at": "2026-01-20T12:21:59.183902", + "events": [ + { + "timestamp": 3.1916191801428795e-05, + "event_type": "Connected", + "data": { + "type": "Connected", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "sequence_id": 0 + } + }, + { + "timestamp": 0.6468284581787884, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.2195, + "sequence_id": 1 + } + }, + { + "timestamp": 0.8891876661218703, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.167, + "sequence_id": 2 + } + }, + { + "timestamp": 1.0987569580320269, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.72, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1045, + "sequence_id": 3 + } + }, + { + "timestamp": 1.356455208035186, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.96, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.3054, + "sequence_id": 4 + } + }, + { + "timestamp": 1.6076077912002802, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.2, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.2996, + "sequence_id": 5 + } + }, + { + "timestamp": 1.831926790997386, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.44, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1659, + "sequence_id": 6 + } + }, + { + "timestamp": 2.0988957500085235, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.6800001, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0922, + "sequence_id": 7 + } + }, + { + "timestamp": 2.320036916062236, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.9200001, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1154, + "sequence_id": 8 + } + }, + { + "timestamp": 2.5783222501631826, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.16, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0789, + "sequence_id": 9 + } + }, + { + "timestamp": 2.805098250042647, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.4, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.028, + "sequence_id": 10 + } + }, + { + "timestamp": 3.0677467910572886, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.6399999, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0544, + "sequence_id": 11 + } + }, + { + "timestamp": 3.3053550410550088, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.88, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0221, + "sequence_id": 12 + } + }, + { + "timestamp": 3.5730851250700653, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 3.12, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0896, + "sequence_id": 13 + } + }, + { + "timestamp": 3.7986690001562238, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 3.3600001, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0837, + "sequence_id": 14 + } + }, + { + "timestamp": 4.056284500053152, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 3.6, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0217, + "sequence_id": 15 + } + }, + { + "timestamp": 4.2824959580320865, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 3.84, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0277, + "sequence_id": 16 + } + }, + { + "timestamp": 4.541013500187546, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 4.08, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0636, + "sequence_id": 17 + } + }, + { + "timestamp": 4.7826515410561115, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 4.32, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.092, + "sequence_id": 18 + } + }, + { + "timestamp": 5.044063208159059, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 4.56, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1632, + "sequence_id": 19 + } + }, + { + "timestamp": 5.277323708171025, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 4.8, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1748, + "sequence_id": 20 + } + }, + { + "timestamp": 5.519584750058129, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 5.04, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1267, + "sequence_id": 21 + } + }, + { + "timestamp": 5.761642290977761, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 5.28, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.085, + "sequence_id": 22 + } + }, + { + "timestamp": 5.985961250029504, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 5.52, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0726, + "sequence_id": 23 + } + }, + { + "timestamp": 6.235282083041966, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 5.76, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1489, + "sequence_id": 24 + } + }, + { + "timestamp": 6.479744625044987, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.0, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1815, + "sequence_id": 25 + } + }, + { + "timestamp": 6.722758750198409, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1548, + "sequence_id": 26 + } + }, + { + "timestamp": 7.02101350016892, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1779, + "sequence_id": 27 + } + }, + { + "timestamp": 7.2554090830963105, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.7200003, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1924, + "sequence_id": 28 + } + }, + { + "timestamp": 7.495738583151251, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.96, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0734, + "sequence_id": 29 + } + }, + { + "timestamp": 7.695259500062093, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 7.2, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0621, + "sequence_id": 30 + } + }, + { + "timestamp": 7.9374284581281245, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 7.44, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0523, + "sequence_id": 31 + } + }, + { + "timestamp": 8.201127333100885, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 7.68, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0868, + "sequence_id": 32 + } + }, + { + "timestamp": 8.452570000197738, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 7.92, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1788, + "sequence_id": 33 + } + }, + { + "timestamp": 8.6957666662056, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 8.16, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.3462, + "sequence_id": 34 + } + }, + { + "timestamp": 8.937032666057348, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 8.4, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.3477, + "sequence_id": 35 + } + }, + { + "timestamp": 9.179693832993507, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 8.64, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.2825, + "sequence_id": 36 + } + }, + { + "timestamp": 9.439219749998301, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 8.88, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1785, + "sequence_id": 37 + } + }, + { + "timestamp": 9.65257745818235, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 9.12, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.119, + "sequence_id": 38 + } + }, + { + "timestamp": 9.894739540992305, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 9.36, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0948, + "sequence_id": 39 + } + }, + { + "timestamp": 10.137037916108966, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 9.6, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0836, + "sequence_id": 40 + } + }, + { + "timestamp": 10.37885733298026, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 9.84, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0648, + "sequence_id": 41 + } + }, + { + "timestamp": 10.640081625198945, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 10.08, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0426, + "sequence_id": 42 + } + }, + { + "timestamp": 10.882513708202168, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 10.32, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0297, + "sequence_id": 43 + } + }, + { + "timestamp": 11.11375533300452, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 10.56, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0247, + "sequence_id": 44 + } + }, + { + "timestamp": 11.356210750062019, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 10.8, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0134, + "sequence_id": 45 + } + }, + { + "timestamp": 11.60117325000465, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 11.04, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0102, + "sequence_id": 46 + } + }, + { + "timestamp": 11.859979416010901, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 11.28, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0089, + "sequence_id": 47 + } + }, + { + "timestamp": 12.093679000157863, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 11.52, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0074, + "sequence_id": 48 + } + }, + { + "timestamp": 12.334945333190262, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 11.76, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.007, + "sequence_id": 49 + } + }, + { + "timestamp": 12.588809041073546, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 12.0, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0067, + "sequence_id": 50 + } + }, + { + "timestamp": 12.83585675014183, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 12.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0042, + "sequence_id": 51 + } + }, + { + "timestamp": 13.075434750178829, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 12.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0047, + "sequence_id": 52 + } + }, + { + "timestamp": 13.31491966615431, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "63038896-d7d9-4186-995f-16056c3306d5", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 12.72, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0036, + "sequence_id": 53 + } + } + ], + "transcript": "" +} \ No newline at end of file diff --git a/evals/stt/results/multi_speaker-deepgram.json b/evals/stt/results/multi_speaker-deepgram.json new file mode 100644 index 0000000..24f174a --- /dev/null +++ b/evals/stt/results/multi_speaker-deepgram.json @@ -0,0 +1,637 @@ +{ + "audio_file": "multi_speaker.m4a", + "audio_path": "../audio/multi_speaker.m4a", + "provider": "deepgram", + "duration": 7.987664, + "created_at": "2026-01-20T12:15:06.097292", + "events": [ + { + "timestamp": 2.50060111284256e-07, + "event_type": "SpeechStarted", + "data": { + "type": "SpeechStarted", + "channel": [ + 0, + 1 + ], + "timestamp": 0.13 + } + }, + { + "timestamp": 0.9085824999492615, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.0399375, + "start": 0.0, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "Biggest pleasure", + "confidence": 0.7919922, + "words": [ + { + "word": "biggest", + "start": 0.0, + "end": 0.39999998, + "confidence": 0.7919922, + "punctuated_word": "Biggest" + }, + { + "word": "pleasure", + "start": 0.39999998, + "end": 0.79999995, + "confidence": 0.77734375, + "punctuated_word": "pleasure" + } + ] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 1.9669485830236226, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.0799375, + "start": 0.0, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 3.0349432919174433, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 3.1199374, + "start": 0.0, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "Please give a text that I am just trying to", + "confidence": 0.4921875, + "words": [ + { + "word": "please", + "start": 0.48, + "end": 0.79999995, + "confidence": 0.19970703, + "punctuated_word": "Please" + }, + { + "word": "give", + "start": 0.79999995, + "end": 1.04, + "confidence": 0.2849121, + "punctuated_word": "give" + }, + { + "word": "a", + "start": 0.96, + "end": 1.1999999, + "confidence": 0.4921875, + "punctuated_word": "a" + }, + { + "word": "text", + "start": 1.1999999, + "end": 1.5999999, + "confidence": 0.4482422, + "punctuated_word": "text" + }, + { + "word": "that", + "start": 1.5999999, + "end": 2.1599998, + "confidence": 0.5317383, + "punctuated_word": "that" + }, + { + "word": "i", + "start": 2.1599998, + "end": 2.32, + "confidence": 0.984375, + "punctuated_word": "I" + }, + { + "word": "am", + "start": 2.32, + "end": 2.48, + "confidence": 0.5024414, + "punctuated_word": "am" + }, + { + "word": "just", + "start": 2.48, + "end": 2.6399999, + "confidence": 0.27416992, + "punctuated_word": "just" + }, + { + "word": "trying", + "start": 2.6399999, + "end": 2.96, + "confidence": 0.19909668, + "punctuated_word": "trying" + }, + { + "word": "to", + "start": 2.96, + "end": 3.04, + "confidence": 0.7060547, + "punctuated_word": "to" + } + ] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 4.100316457916051, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 4.1599374, + "start": 0.0, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "Is the test that I am just trying do so. Multiple", + "confidence": 0.7207031, + "words": [ + { + "word": "is", + "start": 0.24, + "end": 0.79999995, + "confidence": 0.83251953, + "punctuated_word": "Is" + }, + { + "word": "the", + "start": 0.88, + "end": 1.12, + "confidence": 0.14794922, + "punctuated_word": "the" + }, + { + "word": "test", + "start": 1.12, + "end": 1.52, + "confidence": 0.7207031, + "punctuated_word": "test" + }, + { + "word": "that", + "start": 1.52, + "end": 2.1599998, + "confidence": 0.40307617, + "punctuated_word": "that" + }, + { + "word": "i", + "start": 2.1599998, + "end": 2.3999999, + "confidence": 0.99316406, + "punctuated_word": "I" + }, + { + "word": "am", + "start": 2.3999999, + "end": 2.48, + "confidence": 0.52783203, + "punctuated_word": "am" + }, + { + "word": "just", + "start": 2.48, + "end": 2.72, + "confidence": 0.27270508, + "punctuated_word": "just" + }, + { + "word": "trying", + "start": 2.72, + "end": 3.12, + "confidence": 0.81591797, + "punctuated_word": "trying" + }, + { + "word": "do", + "start": 3.12, + "end": 3.28, + "confidence": 0.9116211, + "punctuated_word": "do" + }, + { + "word": "so", + "start": 3.28, + "end": 3.4399998, + "confidence": 0.37774658, + "punctuated_word": "so." + }, + { + "word": "multiple", + "start": 3.6, + "end": 3.84, + "confidence": 0.74072266, + "punctuated_word": "Multiple" + } + ] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 4.506603500107303, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 4.53, + "start": 0.0, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "Is the test that I am testing multiple speaker", + "confidence": 0.65966797, + "words": [ + { + "word": "is", + "start": 0.24, + "end": 0.39999998, + "confidence": 0.83984375, + "punctuated_word": "Is" + }, + { + "word": "the", + "start": 0.39999998, + "end": 0.79999995, + "confidence": 0.15722656, + "punctuated_word": "the" + }, + { + "word": "test", + "start": 1.12, + "end": 1.52, + "confidence": 0.8588867, + "punctuated_word": "test" + }, + { + "word": "that", + "start": 1.52, + "end": 2.1599998, + "confidence": 0.35107422, + "punctuated_word": "that" + }, + { + "word": "i", + "start": 2.1599998, + "end": 2.32, + "confidence": 0.99121094, + "punctuated_word": "I" + }, + { + "word": "am", + "start": 2.32, + "end": 2.48, + "confidence": 0.6010742, + "punctuated_word": "am" + }, + { + "word": "testing", + "start": 2.48, + "end": 3.12, + "confidence": 0.9526367, + "punctuated_word": "testing" + }, + { + "word": "multiple", + "start": 3.4399998, + "end": 3.84, + "confidence": 0.65966797, + "punctuated_word": "multiple" + }, + { + "word": "speaker", + "start": 3.84, + "end": 4.3199997, + "confidence": 0.20446777, + "punctuated_word": "speaker" + } + ] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 4.648572708014399, + "event_type": "SpeechStarted", + "data": { + "type": "SpeechStarted", + "channel": [ + 0, + 1 + ], + "timestamp": 4.63 + } + }, + { + "timestamp": 5.556989792035893, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.0699372, + "start": 4.53, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 6.615257542114705, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.08, + "start": 4.53, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 6.769657667027786, + "event_type": "SpeechStarted", + "data": { + "type": "SpeechStarted", + "channel": [ + 0, + 1 + ], + "timestamp": 6.72 + } + }, + { + "timestamp": 7.672739624977112, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.0099998, + "start": 6.61, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 8.081677624955773, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 0.3676877, + "start": 7.62, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 8.083154707914218, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 0.0, + "start": 7.9876876, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 8.083194707985967, + "event_type": "Metadata", + "data": { + "type": "Metadata", + "transaction_key": "deprecated", + "request_id": "39481f46-cd5f-40b1-9a55-a6635d8c06d9", + "sha256": "a6f954deb3fb3bf7a3c420061d5dd968251ba401d6304e6cd2fc9f396c12da77", + "created": "2026-01-20T06:44:57.522Z", + "duration": 7.9876876, + "channels": 1, + "models": [ + "40bd3654-e622-47c4-a111-63a61b23bfe8" + ], + "model_info": { + "40bd3654-e622-47c4-a111-63a61b23bfe8": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + } + } + } + } + ], + "transcript": "Is the test that I am testing multiple speaker" +} \ No newline at end of file diff --git a/evals/stt/results/nope-deepgram-flux.json b/evals/stt/results/nope-deepgram-flux.json new file mode 100644 index 0000000..a26cf69 --- /dev/null +++ b/evals/stt/results/nope-deepgram-flux.json @@ -0,0 +1,445 @@ +{ + "audio_file": "nope.m4a", + "audio_path": "../audio/nope.m4a", + "provider": "deepgram-flux", + "duration": 3.390113, + "created_at": "2026-01-20T13:34:04.075559", + "events": [ + { + "timestamp": 3.3294782042503357e-07, + "event_type": "Connected", + "data": { + "type": "Connected", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "sequence_id": 0 + } + }, + { + "timestamp": 0.6400237919297069, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1726, + "sequence_id": 1 + } + }, + { + "timestamp": 0.850623874925077, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0643, + "sequence_id": 2 + } + }, + { + "timestamp": 1.0877662498969585, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.72, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0343, + "sequence_id": 3 + } + }, + { + "timestamp": 1.3602930000051856, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.96, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.023, + "sequence_id": 4 + } + }, + { + "timestamp": 1.5734205420594662, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "StartOfTurn", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.2, + "transcript": "No.", + "words": [ + { + "word": "No.", + "confidence": 0.9956 + } + ], + "end_of_turn_confidence": 0.1445, + "sequence_id": 5 + } + }, + { + "timestamp": 1.7732612078543752, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "EndOfTurn", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.36, + "transcript": "No.", + "words": [ + { + "word": "No.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.7266, + "sequence_id": 6 + } + }, + { + "timestamp": 2.0032672078814358, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 1.6, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.2114, + "sequence_id": 7 + } + }, + { + "timestamp": 2.272528207860887, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 1.8399999, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.2886, + "sequence_id": 8 + } + }, + { + "timestamp": 2.4770477078855038, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 2.08, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1366, + "sequence_id": 9 + } + }, + { + "timestamp": 2.7586996669415385, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 2.32, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0687, + "sequence_id": 10 + } + }, + { + "timestamp": 2.9688463748898357, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 2.56, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0571, + "sequence_id": 11 + } + }, + { + "timestamp": 3.2333728750236332, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 2.8, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0284, + "sequence_id": 12 + } + }, + { + "timestamp": 3.4381651668809354, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 3.04, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0352, + "sequence_id": 13 + } + }, + { + "timestamp": 3.7163160829804838, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 3.28, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0211, + "sequence_id": 14 + } + }, + { + "timestamp": 3.936306041898206, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 3.52, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0123, + "sequence_id": 15 + } + }, + { + "timestamp": 4.212840874912217, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 3.76, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0399, + "sequence_id": 16 + } + }, + { + "timestamp": 4.417071416974068, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 4.0, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0503, + "sequence_id": 17 + } + }, + { + "timestamp": 4.685962416930124, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 4.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0443, + "sequence_id": 18 + } + }, + { + "timestamp": 4.898042541928589, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 4.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0367, + "sequence_id": 19 + } + }, + { + "timestamp": 5.167347207898274, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 4.7200003, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0221, + "sequence_id": 20 + } + }, + { + "timestamp": 5.415992958005518, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 4.96, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1116, + "sequence_id": 21 + } + }, + { + "timestamp": 5.703707166947424, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 5.2, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0883, + "sequence_id": 22 + } + }, + { + "timestamp": 5.923421707935631, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 5.44, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0663, + "sequence_id": 23 + } + }, + { + "timestamp": 6.128664416959509, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 5.68, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0324, + "sequence_id": 24 + } + }, + { + "timestamp": 6.382756792008877, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 5.92, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0138, + "sequence_id": 25 + } + }, + { + "timestamp": 6.629080249927938, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "b42d9771-4a63-4c7f-aa89-33370cd70d23", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.36, + "audio_window_end": 6.16, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0064, + "sequence_id": 26 + } + } + ], + "transcript": "No." +} \ No newline at end of file diff --git a/evals/stt/results/not_so_sure-deepgram-flux.json b/evals/stt/results/not_so_sure-deepgram-flux.json new file mode 100644 index 0000000..540ba0e --- /dev/null +++ b/evals/stt/results/not_so_sure-deepgram-flux.json @@ -0,0 +1,678 @@ +{ + "audio_file": "not_so_sure.m4a", + "audio_path": "../audio/not_so_sure.m4a", + "provider": "deepgram-flux", + "duration": 3.784853, + "created_at": "2026-01-20T13:34:30.619814", + "events": [ + { + "timestamp": 4.1606836020946503e-07, + "event_type": "Connected", + "data": { + "type": "Connected", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "sequence_id": 0 + } + }, + { + "timestamp": 0.6479636249132454, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.2837, + "sequence_id": 1 + } + }, + { + "timestamp": 0.8711565409321338, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1409, + "sequence_id": 2 + } + }, + { + "timestamp": 1.0940386659931391, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.72, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.103, + "sequence_id": 3 + } + }, + { + "timestamp": 1.3378053328488022, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "StartOfTurn", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.96, + "transcript": "I don", + "words": [ + { + "word": "I", + "confidence": 0.8521 + }, + { + "word": "don", + "confidence": 0.9858 + } + ], + "end_of_turn_confidence": 0.1526, + "sequence_id": 4 + } + }, + { + "timestamp": 1.575752625009045, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.2, + "transcript": "I don't know", + "words": [ + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "don't", + "confidence": 1.0 + }, + { + "word": "know", + "confidence": 0.9956 + } + ], + "end_of_turn_confidence": 0.0815, + "sequence_id": 5 + } + }, + { + "timestamp": 1.809568207943812, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.44, + "transcript": "I don't know. I", + "words": [ + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "don't", + "confidence": 1.0 + }, + { + "word": "know.", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 0.9995 + } + ], + "end_of_turn_confidence": 0.0533, + "sequence_id": 6 + } + }, + { + "timestamp": 2.0778977079316974, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.6800001, + "transcript": "I don't know. I'm not", + "words": [ + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "don't", + "confidence": 1.0 + }, + { + "word": "know.", + "confidence": 1.0 + }, + { + "word": "I'm", + "confidence": 1.0 + }, + { + "word": "not", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0296, + "sequence_id": 7 + } + }, + { + "timestamp": 2.3323032909538597, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.9200001, + "transcript": "I don't know. I'm not sure she", + "words": [ + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "don't", + "confidence": 1.0 + }, + { + "word": "know.", + "confidence": 1.0 + }, + { + "word": "I'm", + "confidence": 1.0 + }, + { + "word": "not", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 0.9692 + }, + { + "word": "she", + "confidence": 0.6968 + } + ], + "end_of_turn_confidence": 0.1591, + "sequence_id": 8 + } + }, + { + "timestamp": 2.563972583040595, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.16, + "transcript": "I don't know. I'm not so sure.", + "words": [ + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "don't", + "confidence": 1.0 + }, + { + "word": "know.", + "confidence": 1.0 + }, + { + "word": "I'm", + "confidence": 1.0 + }, + { + "word": "not", + "confidence": 1.0 + }, + { + "word": "so", + "confidence": 0.9971 + }, + { + "word": "sure.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.5312, + "sequence_id": 9 + } + }, + { + "timestamp": 2.766235665883869, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "EndOfTurn", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.32, + "transcript": "I don't know. I'm not so sure.", + "words": [ + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "don't", + "confidence": 1.0 + }, + { + "word": "know.", + "confidence": 1.0 + }, + { + "word": "I'm", + "confidence": 1.0 + }, + { + "word": "not", + "confidence": 1.0 + }, + { + "word": "so", + "confidence": 0.9971 + }, + { + "word": "sure.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.7129, + "sequence_id": 10 + } + }, + { + "timestamp": 2.980985000031069, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 2.56, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.6235, + "sequence_id": 11 + } + }, + { + "timestamp": 3.040183125063777, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 2.6399999, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.7163, + "sequence_id": 12 + } + }, + { + "timestamp": 3.134053166024387, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 2.72, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.7603, + "sequence_id": 13 + } + }, + { + "timestamp": 3.200523457955569, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 2.8, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.8013, + "sequence_id": 14 + } + }, + { + "timestamp": 3.3396010829601437, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 2.88, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.8052, + "sequence_id": 15 + } + }, + { + "timestamp": 3.462065916042775, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 3.04, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.6968, + "sequence_id": 16 + } + }, + { + "timestamp": 3.532107833074406, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 3.12, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.7026, + "sequence_id": 17 + } + }, + { + "timestamp": 3.6854247499722987, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 3.28, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.6123, + "sequence_id": 18 + } + }, + { + "timestamp": 3.9346718329470605, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 3.52, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.4551, + "sequence_id": 19 + } + }, + { + "timestamp": 4.174561291001737, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 3.76, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.293, + "sequence_id": 20 + } + }, + { + "timestamp": 4.423174874857068, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 4.0, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1186, + "sequence_id": 21 + } + }, + { + "timestamp": 4.661856249906123, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 4.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1186, + "sequence_id": 22 + } + }, + { + "timestamp": 4.934342915890738, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 4.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0629, + "sequence_id": 23 + } + }, + { + "timestamp": 5.1988217500038445, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 4.7200003, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0302, + "sequence_id": 24 + } + }, + { + "timestamp": 5.868438957957551, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 4.96, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0104, + "sequence_id": 25 + } + }, + { + "timestamp": 5.924830165924504, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 5.2, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0039, + "sequence_id": 26 + } + }, + { + "timestamp": 6.008775374852121, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 5.44, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.003, + "sequence_id": 27 + } + }, + { + "timestamp": 6.224981207866222, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 5.68, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0027, + "sequence_id": 28 + } + }, + { + "timestamp": 6.400387583067641, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 5.92, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0944, + "sequence_id": 29 + } + }, + { + "timestamp": 6.6102081660646945, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 6.16, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.083, + "sequence_id": 30 + } + }, + { + "timestamp": 6.853603166062385, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 6.4, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0674, + "sequence_id": 31 + } + }, + { + "timestamp": 7.1176844160072505, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "badd4484-3b22-42c5-bd5f-13fd2014021b", + "event": "Update", + "turn_index": 1, + "audio_window_start": 2.32, + "audio_window_end": 6.64, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0348, + "sequence_id": 32 + } + } + ], + "transcript": "I don't know. I'm not so sure." +} \ No newline at end of file diff --git a/evals/stt/results/not_so_sure-speechmatics.json b/evals/stt/results/not_so_sure-speechmatics.json new file mode 100644 index 0000000..3893801 --- /dev/null +++ b/evals/stt/results/not_so_sure-speechmatics.json @@ -0,0 +1,936 @@ +{ + "audio_file": "not_so_sure.m4a", + "audio_path": "../audio/not_so_sure.m4a", + "provider": "speechmatics", + "duration": 3.784853, + "created_at": "2026-01-20T13:38:01.957263", + "events": [ + { + "timestamp": 2.50060111284256e-07, + "event_type": "Info", + "data": { + "message": "Info", + "type": "concurrent_session_usage", + "reason": "1 concurrent sessions active out of quota 2", + "usage": 1, + "quota": 2, + "last_updated": "2026-01-20T08:07:53Z" + } + }, + { + "timestamp": 0.17636274988763034, + "event_type": "RecognitionStarted", + "data": { + "message": "RecognitionStarted", + "orchestrator_version": "2026.01.09+e449221ca0+14.12.0", + "id": "ff50bcc6-03cc-4609-b52b-c61492be97b0", + "language_pack_info": { + "adapted": false, + "itn": true, + "language_description": "English", + "word_delimiter": " ", + "writing_direction": "left-to-right" + } + } + }, + { + "timestamp": 0.1765422080643475, + "event_type": "Info", + "data": { + "message": "Info", + "type": "recognition_quality", + "reason": "Running recognition using a broadcast model quality.", + "quality": "broadcast" + } + }, + { + "timestamp": 0.44156987499445677, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 1 + } + }, + { + "timestamp": 0.5090052080340683, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 2 + } + }, + { + "timestamp": 0.5927771248389035, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 3 + } + }, + { + "timestamp": 0.6792412919458002, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 4 + } + }, + { + "timestamp": 0.7540834578685462, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 5 + } + }, + { + "timestamp": 0.8363401249516755, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 6 + } + }, + { + "timestamp": 0.916276125004515, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 7 + } + }, + { + "timestamp": 1.0025545828975737, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 8 + } + }, + { + "timestamp": 1.0930295418947935, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 9 + } + }, + { + "timestamp": 1.1681176249403507, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 10 + } + }, + { + "timestamp": 1.2440201670397073, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 11 + } + }, + { + "timestamp": 1.3254928330425173, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 12 + } + }, + { + "timestamp": 1.411379124969244, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 13 + } + }, + { + "timestamp": 1.4989973329938948, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 14 + } + }, + { + "timestamp": 1.569762917002663, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 15 + } + }, + { + "timestamp": 1.6669557499699295, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 16 + } + }, + { + "timestamp": 1.7321407499257475, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 17 + } + }, + { + "timestamp": 1.8123597078956664, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 18 + } + }, + { + "timestamp": 1.89311487483792, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 19 + } + }, + { + "timestamp": 1.99575070803985, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 20 + } + }, + { + "timestamp": 2.0635348330251873, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 21 + } + }, + { + "timestamp": 2.136281125014648, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 22 + } + }, + { + "timestamp": 2.2212352079804987, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 23 + } + }, + { + "timestamp": 2.300102249952033, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 24 + } + }, + { + "timestamp": 2.3838018749374896, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 25 + } + }, + { + "timestamp": 2.4612751249223948, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 26 + } + }, + { + "timestamp": 2.5520844168495387, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 27 + } + }, + { + "timestamp": 2.6254100420046598, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 28 + } + }, + { + "timestamp": 2.7110170419327915, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 29 + } + }, + { + "timestamp": 2.793728666845709, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 30 + } + }, + { + "timestamp": 2.8698849170468748, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 31 + } + }, + { + "timestamp": 2.9517348748631775, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 32 + } + }, + { + "timestamp": 3.034996416885406, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 33 + } + }, + { + "timestamp": 3.1222795830108225, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 34 + } + }, + { + "timestamp": 3.2133053748402745, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 35 + } + }, + { + "timestamp": 3.2794892080128193, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 36 + } + }, + { + "timestamp": 3.360972832888365, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 37 + } + }, + { + "timestamp": 3.480351625010371, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 38 + } + }, + { + "timestamp": 3.527200457872823, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 39 + } + }, + { + "timestamp": 3.614834832958877, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 40 + } + }, + { + "timestamp": 3.7000621668994427, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 41 + } + }, + { + "timestamp": 3.7709098330233246, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 42 + } + }, + { + "timestamp": 3.870571249863133, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 43 + } + }, + { + "timestamp": 3.9319135828409344, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 44 + } + }, + { + "timestamp": 4.0240056668408215, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 45 + } + }, + { + "timestamp": 4.1135993748903275, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 46 + } + }, + { + "timestamp": 4.178906166926026, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 47 + } + }, + { + "timestamp": 4.262735291849822, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 48 + } + }, + { + "timestamp": 4.3524885000661016, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 49 + } + }, + { + "timestamp": 4.42170758289285, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 50 + } + }, + { + "timestamp": 4.503200083039701, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 51 + } + }, + { + "timestamp": 4.588893749983981, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 52 + } + }, + { + "timestamp": 4.6728779170662165, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 53 + } + }, + { + "timestamp": 4.749415792059153, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 54 + } + }, + { + "timestamp": 4.834314750041813, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 55 + } + }, + { + "timestamp": 4.934304124908522, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 56 + } + }, + { + "timestamp": 5.015187042066827, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 57 + } + }, + { + "timestamp": 5.083739625057206, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 58 + } + }, + { + "timestamp": 5.15739579196088, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 59 + } + }, + { + "timestamp": 5.254215708002448, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 60 + } + }, + { + "timestamp": 5.319055167026818, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 61 + } + }, + { + "timestamp": 5.422228208044544, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 62 + } + }, + { + "timestamp": 5.493815457914025, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 63 + } + }, + { + "timestamp": 5.562712874962017, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 64 + } + }, + { + "timestamp": 5.677756666904315, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 65 + } + }, + { + "timestamp": 5.728489124914631, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 66 + } + }, + { + "timestamp": 5.73234708304517, + "event_type": "AddTranscript", + "data": { + "message": "AddTranscript", + "format": "2.9", + "results": [ + { + "alternatives": [ + { + "confidence": 1.0, + "content": "I", + "language": "en" + } + ], + "end_time": 0.8, + "start_time": 0.64, + "type": "word" + } + ], + "metadata": { + "end_time": 0.8, + "start_time": 0.0, + "transcript": "I " + } + } + }, + { + "timestamp": 5.831468666903675, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 67 + } + }, + { + "timestamp": 5.9311752079520375, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 68 + } + }, + { + "timestamp": 5.970860542031005, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 69 + } + }, + { + "timestamp": 6.0573643748648465, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 70 + } + }, + { + "timestamp": 6.071638958062977, + "event_type": "AddTranscript", + "data": { + "message": "AddTranscript", + "format": "2.9", + "results": [ + { + "alternatives": [ + { + "confidence": 1.0, + "content": "don't", + "language": "en" + } + ], + "end_time": 1.08, + "start_time": 0.84, + "type": "word" + }, + { + "alternatives": [ + { + "confidence": 1.0, + "content": "know", + "language": "en" + } + ], + "end_time": 1.2, + "start_time": 1.08, + "type": "word" + }, + { + "alternatives": [ + { + "confidence": 1.0, + "content": ".", + "language": "en" + } + ], + "attaches_to": "previous", + "end_time": 1.2, + "is_eos": true, + "start_time": 1.2, + "type": "punctuation" + } + ], + "metadata": { + "end_time": 1.2, + "start_time": 0.8, + "transcript": "don't know. " + } + } + }, + { + "timestamp": 6.143923291936517, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 71 + } + }, + { + "timestamp": 6.229828458046541, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 72 + } + }, + { + "timestamp": 6.297467292053625, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 73 + } + }, + { + "timestamp": 6.388417499838397, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 74 + } + }, + { + "timestamp": 6.46747541683726, + "event_type": "AddTranscript", + "data": { + "message": "AddTranscript", + "format": "2.9", + "results": [ + { + "alternatives": [ + { + "confidence": 1.0, + "content": "I'm", + "language": "en" + } + ], + "end_time": 1.4, + "start_time": 1.2, + "type": "word" + }, + { + "alternatives": [ + { + "confidence": 1.0, + "content": "not", + "language": "en" + } + ], + "end_time": 1.56, + "start_time": 1.4, + "type": "word" + } + ], + "metadata": { + "end_time": 1.56, + "start_time": 1.2, + "transcript": "I'm not " + } + } + }, + { + "timestamp": 6.467542249942198, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 75 + } + }, + { + "timestamp": 6.571689167059958, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 76 + } + }, + { + "timestamp": 6.633496082853526, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 77 + } + }, + { + "timestamp": 6.705628624884412, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 78 + } + }, + { + "timestamp": 6.791943500051275, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 79 + } + }, + { + "timestamp": 6.8231504168361425, + "event_type": "AddTranscript", + "data": { + "message": "AddTranscript", + "format": "2.9", + "results": [ + { + "alternatives": [ + { + "confidence": 1.0, + "content": "so", + "language": "en" + } + ], + "end_time": 1.72, + "start_time": 1.56, + "type": "word" + } + ], + "metadata": { + "end_time": 1.72, + "start_time": 1.56, + "transcript": "so " + } + } + }, + { + "timestamp": 6.889297208050266, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 80 + } + }, + { + "timestamp": 6.96820458304137, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 81 + } + }, + { + "timestamp": 7.030788874952123, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 82 + } + }, + { + "timestamp": 7.114988874876872, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 83 + } + }, + { + "timestamp": 7.1660370419267565, + "event_type": "AddTranscript", + "data": { + "message": "AddTranscript", + "format": "2.9", + "results": [ + { + "alternatives": [ + { + "confidence": 1.0, + "content": "sure", + "language": "en" + } + ], + "end_time": 2.2, + "start_time": 1.76, + "type": "word" + }, + { + "alternatives": [ + { + "confidence": 1.0, + "content": ".", + "language": "en" + } + ], + "attaches_to": "previous", + "end_time": 2.2, + "is_eos": true, + "start_time": 2.2, + "type": "punctuation" + } + ], + "metadata": { + "end_time": 2.2, + "start_time": 1.72, + "transcript": "sure. " + } + } + }, + { + "timestamp": 7.197767958045006, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 84 + } + }, + { + "timestamp": 7.281636083032936, + "event_type": "AudioAdded", + "data": { + "message": "AudioAdded", + "seq_no": 85 + } + }, + { + "timestamp": 7.966639708029106, + "event_type": "AddTranscript", + "data": { + "message": "AddTranscript", + "format": "2.9", + "results": [], + "metadata": { + "end_time": 6.72, + "start_time": 2.28, + "transcript": "" + } + } + }, + { + "timestamp": 7.966674832860008, + "event_type": "EndOfTranscript", + "data": { + "message": "EndOfTranscript" + } + } + ], + "transcript": "I don't know. I'm not so sure." +} \ No newline at end of file diff --git a/evals/stt/results/vad-deepgram-flux.json b/evals/stt/results/vad-deepgram-flux.json new file mode 100644 index 0000000..a93db46 --- /dev/null +++ b/evals/stt/results/vad-deepgram-flux.json @@ -0,0 +1,2931 @@ +{ + "audio_file": "vad.m4a", + "audio_path": "../audio/vad.m4a", + "provider": "deepgram-flux", + "duration": 13.606893, + "created_at": "2026-01-20T13:06:16.523794", + "events": [ + { + "timestamp": 2.0791776478290558e-07, + "event_type": "Connected", + "data": { + "type": "Connected", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "sequence_id": 0 + } + }, + { + "timestamp": 0.5991181668359786, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0739, + "sequence_id": 1 + } + }, + { + "timestamp": 0.8387298330198973, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0226, + "sequence_id": 2 + } + }, + { + "timestamp": 1.0813759579323232, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.72, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0106, + "sequence_id": 3 + } + }, + { + "timestamp": 1.329924332909286, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.96, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0058, + "sequence_id": 4 + } + }, + { + "timestamp": 1.5782449168618768, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.2, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0292, + "sequence_id": 5 + } + }, + { + "timestamp": 1.8236550828441978, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.44, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0247, + "sequence_id": 6 + } + }, + { + "timestamp": 2.073379916837439, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.6800001, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.009, + "sequence_id": 7 + } + }, + { + "timestamp": 2.3028037920594215, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.9200001, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0031, + "sequence_id": 8 + } + }, + { + "timestamp": 2.5454757078550756, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.16, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0017, + "sequence_id": 9 + } + }, + { + "timestamp": 2.8026564170140773, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.4, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0024, + "sequence_id": 10 + } + }, + { + "timestamp": 3.034466916928068, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.6399999, + "transcript": "The", + "words": [ + { + "word": "The", + "confidence": 0.5684 + } + ], + "end_of_turn_confidence": 0.0023, + "sequence_id": 11 + } + }, + { + "timestamp": 3.280960999894887, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "StartOfTurn", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 2.88, + "transcript": "This is", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0304, + "sequence_id": 12 + } + }, + { + "timestamp": 3.507918791845441, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 3.12, + "transcript": "This is", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0345, + "sequence_id": 13 + } + }, + { + "timestamp": 3.767754166852683, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 3.3600001, + "transcript": "This is", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0382, + "sequence_id": 14 + } + }, + { + "timestamp": 4.002102291909978, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 3.6, + "transcript": "This is some", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "some", + "confidence": 0.5229 + } + ], + "end_of_turn_confidence": 0.002, + "sequence_id": 15 + } + }, + { + "timestamp": 4.240494291996583, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 3.84, + "transcript": "This is something", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 0.9692 + } + ], + "end_of_turn_confidence": 0.01, + "sequence_id": 16 + } + }, + { + "timestamp": 4.523886707844213, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 4.08, + "transcript": "This is something that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 0.9941 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0206, + "sequence_id": 17 + } + }, + { + "timestamp": 4.734341792063788, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 4.32, + "transcript": "This is something that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 0.9985 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0133, + "sequence_id": 18 + } + }, + { + "timestamp": 5.001597333000973, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 4.56, + "transcript": "This is something that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 0.9995 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0151, + "sequence_id": 19 + } + }, + { + "timestamp": 5.227020208025351, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 4.8, + "transcript": "This is something that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0108, + "sequence_id": 20 + } + }, + { + "timestamp": 5.476216417038813, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 5.04, + "transcript": "This is something that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.017, + "sequence_id": 21 + } + }, + { + "timestamp": 5.706772041972727, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 5.28, + "transcript": "This is something that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0259, + "sequence_id": 22 + } + }, + { + "timestamp": 5.9626319168601185, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 5.52, + "transcript": "This is something that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0107, + "sequence_id": 23 + } + }, + { + "timestamp": 6.2007496249862015, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 5.76, + "transcript": "This is something that I am", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9551 + } + ], + "end_of_turn_confidence": 0.006, + "sequence_id": 24 + } + }, + { + "timestamp": 6.439131374936551, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.0, + "transcript": "This is something that I am just", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9868 + }, + { + "word": "just", + "confidence": 0.7397 + } + ], + "end_of_turn_confidence": 0.0036, + "sequence_id": 25 + } + }, + { + "timestamp": 6.685360916890204, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.24, + "transcript": "This is something that I am testing", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.999 + }, + { + "word": "testing", + "confidence": 0.9897 + } + ], + "end_of_turn_confidence": 0.0223, + "sequence_id": 26 + } + }, + { + "timestamp": 6.922765875002369, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.48, + "transcript": "This is something that I am testing to", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.9995 + }, + { + "word": "to", + "confidence": 0.9907 + } + ], + "end_of_turn_confidence": 0.0156, + "sequence_id": 27 + } + }, + { + "timestamp": 7.1796076248865575, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.7200003, + "transcript": "This is something that I am testing to", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.033, + "sequence_id": 28 + } + }, + { + "timestamp": 7.420775583013892, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 6.96, + "transcript": "This is something that I am testing to", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0189, + "sequence_id": 29 + } + }, + { + "timestamp": 7.662735000019893, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 7.2, + "transcript": "This is something that I am testing to", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.9985 + }, + { + "word": "to", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0316, + "sequence_id": 30 + } + }, + { + "timestamp": 7.901641249889508, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 7.44, + "transcript": "This is something that I am testing to", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.9985 + }, + { + "word": "to", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0467, + "sequence_id": 31 + } + }, + { + "timestamp": 8.193162374896929, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 7.68, + "transcript": "This is something that I am testing to", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.998 + }, + { + "word": "to", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0161, + "sequence_id": 32 + } + }, + { + "timestamp": 8.390170000027865, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 7.92, + "transcript": "This is something that I am testing to", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.9976 + }, + { + "word": "to", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0323, + "sequence_id": 33 + } + }, + { + "timestamp": 8.670882124919444, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 8.16, + "transcript": "This is something that I am testing to make", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.9976 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 0.9697 + } + ], + "end_of_turn_confidence": 0.0029, + "sequence_id": 34 + } + }, + { + "timestamp": 8.921790292020887, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 8.4, + "transcript": "This is something that I am testing to make sure", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.9985 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 0.9995 + }, + { + "word": "sure", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0081, + "sequence_id": 35 + } + }, + { + "timestamp": 9.150760042015463, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 8.64, + "transcript": "This is something that I am testing to make sure that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.002, + "sequence_id": 36 + } + }, + { + "timestamp": 9.389485124964267, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 8.88, + "transcript": "This is something that I am testing to make sure that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0078, + "sequence_id": 37 + } + }, + { + "timestamp": 10.12114379205741, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 9.12, + "transcript": "This is something that I am testing to make sure that", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0092, + "sequence_id": 38 + } + }, + { + "timestamp": 10.164927457924932, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 9.36, + "transcript": "This is something that I am testing to make sure that the web", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.6372 + }, + { + "word": "web", + "confidence": 0.2715 + } + ], + "end_of_turn_confidence": 0.0168, + "sequence_id": 39 + } + }, + { + "timestamp": 10.287556666880846, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 9.6, + "transcript": "This is something that I am testing to make sure that the bag", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.7988 + }, + { + "word": "bag", + "confidence": 0.4683 + } + ], + "end_of_turn_confidence": 0.0435, + "sequence_id": 40 + } + }, + { + "timestamp": 10.371591208036989, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 9.84, + "transcript": "This is something that I am testing to make sure that bad", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "bad", + "confidence": 0.5386 + } + ], + "end_of_turn_confidence": 0.0258, + "sequence_id": 41 + } + }, + { + "timestamp": 10.592813499970362, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 10.08, + "transcript": "This is something that I am testing to make sure that bad", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "bad", + "confidence": 0.7607 + } + ], + "end_of_turn_confidence": 0.0147, + "sequence_id": 42 + } + }, + { + "timestamp": 10.8256475829985, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 10.32, + "transcript": "This is something that I am testing to make sure that bad is", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "bad", + "confidence": 0.6484 + }, + { + "word": "is", + "confidence": 0.9995 + } + ], + "end_of_turn_confidence": 0.0019, + "sequence_id": 43 + } + }, + { + "timestamp": 11.079555582953617, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 10.56, + "transcript": "This is something that I am testing to make sure that the VAT is", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.2808 + }, + { + "word": "VAT", + "confidence": 0.3472 + }, + { + "word": "is", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0118, + "sequence_id": 44 + } + }, + { + "timestamp": 11.314390541985631, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 10.8, + "transcript": "This is something that I am testing to make sure that that is what", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 0.2949 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "what", + "confidence": 0.6787 + } + ], + "end_of_turn_confidence": 0.0087, + "sequence_id": 45 + } + }, + { + "timestamp": 11.60575837502256, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 11.04, + "transcript": "This is something that I am testing to make sure that that is working", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 0.2474 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "working", + "confidence": 0.998 + } + ], + "end_of_turn_confidence": 0.0225, + "sequence_id": 46 + } + }, + { + "timestamp": 11.842209457885474, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 11.28, + "transcript": "This is something that I am testing to make sure that VAD is working properly", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "VAD", + "confidence": 0.999 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "working", + "confidence": 0.9995 + }, + { + "word": "properly", + "confidence": 0.8716 + } + ], + "end_of_turn_confidence": 0.0368, + "sequence_id": 47 + } + }, + { + "timestamp": 12.073602374875918, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 11.52, + "transcript": "This is something that I am testing to make sure that the rad is working properly", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.3406 + }, + { + "word": "rad", + "confidence": 0.1747 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "working", + "confidence": 1.0 + }, + { + "word": "properly", + "confidence": 0.998 + } + ], + "end_of_turn_confidence": 0.0746, + "sequence_id": 48 + } + }, + { + "timestamp": 12.294721042038873, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 11.76, + "transcript": "This is something that I am testing to make sure that the VAT is working properly.", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.5513 + }, + { + "word": "VAT", + "confidence": 0.205 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "working", + "confidence": 1.0 + }, + { + "word": "properly.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.2715, + "sequence_id": 49 + } + }, + { + "timestamp": 12.542759666917846, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 12.0, + "transcript": "This is something that I am testing to make sure that the VAT is working properly.", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.5513 + }, + { + "word": "VAT", + "confidence": 0.1903 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "working", + "confidence": 1.0 + }, + { + "word": "properly.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.0775, + "sequence_id": 50 + } + }, + { + "timestamp": 12.783193832961842, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 12.24, + "transcript": "This is something that I am testing to make sure that the rad is working properly.", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.5513 + }, + { + "word": "rad", + "confidence": 0.1826 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "working", + "confidence": 1.0 + }, + { + "word": "properly.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.1837, + "sequence_id": 51 + } + }, + { + "timestamp": 13.045388832921162, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 12.48, + "transcript": "This is something that I am testing to make sure that the rad is working properly.", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.5513 + }, + { + "word": "rad", + "confidence": 0.1855 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "working", + "confidence": 1.0 + }, + { + "word": "properly.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.4099, + "sequence_id": 52 + } + }, + { + "timestamp": 13.27779766684398, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 12.72, + "transcript": "This is something that I am testing to make sure that the rad is working properly.", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.5513 + }, + { + "word": "rad", + "confidence": 0.1855 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "working", + "confidence": 1.0 + }, + { + "word": "properly.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.7061, + "sequence_id": 53 + } + }, + { + "timestamp": 13.36575054191053, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "EndOfTurn", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 12.8, + "transcript": "This is something that I am testing to make sure that the rad is working properly.", + "words": [ + { + "word": "This", + "confidence": 1.0 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "something", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "I", + "confidence": 1.0 + }, + { + "word": "am", + "confidence": 0.9995 + }, + { + "word": "testing", + "confidence": 0.999 + }, + { + "word": "to", + "confidence": 1.0 + }, + { + "word": "make", + "confidence": 1.0 + }, + { + "word": "sure", + "confidence": 1.0 + }, + { + "word": "that", + "confidence": 1.0 + }, + { + "word": "the", + "confidence": 0.5513 + }, + { + "word": "rad", + "confidence": 0.1859 + }, + { + "word": "is", + "confidence": 1.0 + }, + { + "word": "working", + "confidence": 1.0 + }, + { + "word": "properly.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.791, + "sequence_id": 54 + } + }, + { + "timestamp": 13.604034666903317, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 13.04, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.5913, + "sequence_id": 55 + } + }, + { + "timestamp": 13.757088999962434, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 13.2, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.7568, + "sequence_id": 56 + } + }, + { + "timestamp": 13.844318666961044, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 13.28, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.8135, + "sequence_id": 57 + } + }, + { + "timestamp": 14.096143374918029, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 13.52, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.6445, + "sequence_id": 58 + } + }, + { + "timestamp": 14.32369429199025, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 13.76, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.6685, + "sequence_id": 59 + } + }, + { + "timestamp": 14.57952274987474, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 14.0, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.6743, + "sequence_id": 60 + } + }, + { + "timestamp": 14.821225458057597, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 14.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.3857, + "sequence_id": 61 + } + }, + { + "timestamp": 15.078599707921967, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 14.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1913, + "sequence_id": 62 + } + }, + { + "timestamp": 15.8081968750339, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 14.72, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1469, + "sequence_id": 63 + } + }, + { + "timestamp": 15.853020707843825, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 14.96, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0952, + "sequence_id": 64 + } + }, + { + "timestamp": 15.972035791957751, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 15.2, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0655, + "sequence_id": 65 + } + }, + { + "timestamp": 16.056484082946554, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 15.44, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0452, + "sequence_id": 66 + } + }, + { + "timestamp": 16.30366349988617, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 15.68, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0339, + "sequence_id": 67 + } + }, + { + "timestamp": 16.55612133303657, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 15.92, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0199, + "sequence_id": 68 + } + }, + { + "timestamp": 16.809007041854784, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 16.16, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0143, + "sequence_id": 69 + } + }, + { + "timestamp": 17.008537916932255, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 16.4, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.3384, + "sequence_id": 70 + } + }, + { + "timestamp": 17.2497119999025, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 16.64, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.3823, + "sequence_id": 71 + } + }, + { + "timestamp": 17.51993320789188, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 16.88, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.3843, + "sequence_id": 72 + } + }, + { + "timestamp": 17.765261041931808, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 17.12, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.2246, + "sequence_id": 73 + } + }, + { + "timestamp": 18.005616874899715, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 17.36, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1076, + "sequence_id": 74 + } + }, + { + "timestamp": 18.270230083027855, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 17.6, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0357, + "sequence_id": 75 + } + }, + { + "timestamp": 18.490703457966447, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 17.84, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0112, + "sequence_id": 76 + } + }, + { + "timestamp": 18.75762120797299, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 18.08, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0053, + "sequence_id": 77 + } + }, + { + "timestamp": 18.954981041839346, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 18.32, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0018, + "sequence_id": 78 + } + }, + { + "timestamp": 19.206135916989297, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "d7bb925b-1ba7-4fff-a36e-5077b6c580f7", + "event": "Update", + "turn_index": 1, + "audio_window_start": 12.8, + "audio_window_end": 18.56, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0008, + "sequence_id": 79 + } + } + ], + "transcript": "This is something that I am testing to make sure that the rad is working properly." +} \ No newline at end of file diff --git a/evals/stt/results/vad-deepgram.json b/evals/stt/results/vad-deepgram.json new file mode 100644 index 0000000..e2f6666 --- /dev/null +++ b/evals/stt/results/vad-deepgram.json @@ -0,0 +1,1131 @@ +{ + "audio_file": "vad.m4a", + "audio_path": "../audio/vad.m4a", + "provider": "deepgram", + "duration": 13.606893, + "created_at": "2026-01-20T13:23:22.720144", + "events": [ + { + "timestamp": 2.1248124539852142e-06, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.0399375, + "start": 0.0, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 0.5033409169409424, + "event_type": "SpeechStarted", + "data": { + "type": "SpeechStarted", + "channel": [ + 0, + 1 + ], + "timestamp": 1.1 + } + }, + { + "timestamp": 1.0483719168696553, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.0799375, + "start": 0.0, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 1.2717794578056782, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.23, + "start": 0.0, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 1.3922397918067873, + "event_type": "SpeechStarted", + "data": { + "type": "SpeechStarted", + "channel": [ + 0, + 1 + ], + "timestamp": 2.51 + } + }, + { + "timestamp": 2.141541542019695, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.0499375, + "start": 2.23, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "This is", + "confidence": 0.99902344, + "words": [ + { + "word": "this", + "start": 2.31, + "end": 2.71, + "confidence": 0.8803711, + "punctuated_word": "This" + }, + { + "word": "is", + "start": 2.71, + "end": 2.87, + "confidence": 0.99902344, + "punctuated_word": "is" + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 3.198163916822523, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.0899377, + "start": 2.23, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "This is something that", + "confidence": 0.9970703, + "words": [ + { + "word": "this", + "start": 2.31, + "end": 2.71, + "confidence": 0.9223633, + "punctuated_word": "This" + }, + { + "word": "is", + "start": 2.71, + "end": 2.95, + "confidence": 1.0, + "punctuated_word": "is" + }, + { + "word": "something", + "start": 2.95, + "end": 3.67, + "confidence": 0.93408203, + "punctuated_word": "something" + }, + { + "word": "that", + "start": 3.67, + "end": 3.99, + "confidence": 0.9970703, + "punctuated_word": "that" + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 3.610837416956201, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.4299998, + "start": 2.23, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "This is something that", + "confidence": 0.9980469, + "words": [ + { + "word": "this", + "start": 2.31, + "end": 2.71, + "confidence": 0.9448242, + "punctuated_word": "This" + }, + { + "word": "is", + "start": 2.71, + "end": 2.95, + "confidence": 1.0, + "punctuated_word": "is" + }, + { + "word": "something", + "start": 2.95, + "end": 3.75, + "confidence": 0.9291992, + "punctuated_word": "something" + }, + { + "word": "that", + "start": 3.75, + "end": 4.0699997, + "confidence": 0.9980469, + "punctuated_word": "that" + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 4.484807749977335, + "event_type": "SpeechStarted", + "data": { + "type": "SpeechStarted", + "channel": [ + 0, + 1 + ], + "timestamp": 5.55 + } + }, + { + "timestamp": 4.5869907499291, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.0199375, + "start": 4.66, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "I", + "confidence": 0.9301758, + "words": [ + { + "word": "i", + "start": 5.2999997, + "end": 5.54, + "confidence": 0.9301758, + "punctuated_word": "I" + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 4.587112749926746, + "event_type": "UtteranceEnd", + "data": { + "type": "UtteranceEnd", + "channel": [ + 0, + 1 + ], + "last_word_end": 4.0699997 + } + }, + { + "timestamp": 5.644599041901529, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.0599375, + "start": 4.66, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "I am testing to", + "confidence": 0.9980469, + "words": [ + { + "word": "i", + "start": 5.3799996, + "end": 5.62, + "confidence": 0.9980469, + "punctuated_word": "I" + }, + { + "word": "am", + "start": 5.62, + "end": 5.8599997, + "confidence": 0.96240234, + "punctuated_word": "am" + }, + { + "word": "testing", + "start": 5.8599997, + "end": 6.2599998, + "confidence": 1.0, + "punctuated_word": "testing" + }, + { + "word": "to", + "start": 6.2599998, + "end": 6.5, + "confidence": 0.9892578, + "punctuated_word": "to" + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 5.970383666921407, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.31, + "start": 4.66, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "I am testing to", + "confidence": 0.99902344, + "words": [ + { + "word": "i", + "start": 5.3799996, + "end": 5.62, + "confidence": 0.99902344, + "punctuated_word": "I" + }, + { + "word": "am", + "start": 5.62, + "end": 5.8599997, + "confidence": 0.95410156, + "punctuated_word": "am" + }, + { + "word": "testing", + "start": 5.8599997, + "end": 6.2599998, + "confidence": 1.0, + "punctuated_word": "testing" + }, + { + "word": "to", + "start": 6.2599998, + "end": 6.58, + "confidence": 0.99316406, + "punctuated_word": "to" + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 6.601957832928747, + "event_type": "SpeechStarted", + "data": { + "type": "SpeechStarted", + "channel": [ + 0, + 1 + ], + "timestamp": 7.62 + } + }, + { + "timestamp": 6.9396405830048025, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.0299377, + "start": 6.97, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 6.939695374807343, + "event_type": "UtteranceEnd", + "data": { + "type": "UtteranceEnd", + "channel": [ + 0, + 1 + ], + "last_word_end": 6.58 + } + }, + { + "timestamp": 8.006767624989152, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.0699382, + "start": 6.97, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "make sure that", + "confidence": 0.99902344, + "words": [ + { + "word": "make", + "start": 7.5299997, + "end": 8.17, + "confidence": 0.99902344, + "punctuated_word": "make" + }, + { + "word": "sure", + "start": 8.17, + "end": 8.41, + "confidence": 1.0, + "punctuated_word": "sure" + }, + { + "word": "that", + "start": 8.41, + "end": 8.65, + "confidence": 0.9980469, + "punctuated_word": "that" + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 8.733146582962945, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.7199998, + "start": 6.97, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "make sure that that", + "confidence": 0.99902344, + "words": [ + { + "word": "make", + "start": 7.5299997, + "end": 8.25, + "confidence": 0.99902344, + "punctuated_word": "make" + }, + { + "word": "sure", + "start": 8.25, + "end": 8.41, + "confidence": 1.0, + "punctuated_word": "sure" + }, + { + "word": "that", + "start": 8.41, + "end": 8.969999, + "confidence": 0.9980469, + "punctuated_word": "that" + }, + { + "word": "that", + "start": 8.969999, + "end": 9.37, + "confidence": 0.34033203, + "punctuated_word": "that" + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 8.792208916973323, + "event_type": "SpeechStarted", + "data": { + "type": "SpeechStarted", + "channel": [ + 0, + 1 + ], + "timestamp": 9.79 + } + }, + { + "timestamp": 9.710732833016664, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.0299377, + "start": 9.69, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "is", + "confidence": 0.99316406, + "words": [ + { + "word": "is", + "start": 10.089999, + "end": 10.33, + "confidence": 0.99316406, + "punctuated_word": "is" + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 10.779752457980067, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.9800005, + "start": 9.69, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "is working properly.", + "confidence": 0.93603516, + "words": [ + { + "word": "is", + "start": 10.089999, + "end": 10.41, + "confidence": 0.93603516, + "punctuated_word": "is" + }, + { + "word": "working", + "start": 10.41, + "end": 11.049999, + "confidence": 0.99121094, + "punctuated_word": "working" + }, + { + "word": "properly", + "start": 11.049999, + "end": 11.45, + "confidence": 0.7998047, + "punctuated_word": "properly." + } + ] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 11.75167812500149, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 1.0499372, + "start": 11.67, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 11.75173195800744, + "event_type": "UtteranceEnd", + "data": { + "type": "UtteranceEnd", + "channel": [ + 0, + 1 + ], + "last_word_end": 11.45 + } + }, + { + "timestamp": 13.053302166983485, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.0968122, + "start": 11.67, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 13.95449158293195, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 3.1368122, + "start": 11.67, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 15.400284249801189, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 4.176812, + "start": 11.67, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 15.893622082890943, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 3.0199995, + "start": 11.67, + "is_final": true, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 15.894181541865692, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 2.0368128, + "start": 14.69, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 16.9369917078875, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 3.0768137, + "start": 14.69, + "is_final": false, + "speech_final": false, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 18.010773707879707, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 3.876813, + "start": 14.69, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 18.01088787498884, + "event_type": "Results", + "data": { + "type": "Results", + "channel_index": [ + 0, + 1 + ], + "duration": 0.0, + "start": 18.566813, + "is_final": true, + "speech_final": true, + "channel": { + "alternatives": [ + { + "transcript": "", + "confidence": 0.0, + "words": [] + } + ] + }, + "metadata": { + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "model_info": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + }, + "model_uuid": "40bd3654-e622-47c4-a111-63a61b23bfe8" + }, + "from_finalize": false + } + }, + { + "timestamp": 18.01097366702743, + "event_type": "Metadata", + "data": { + "type": "Metadata", + "transaction_key": "deprecated", + "request_id": "02f8e10c-b08e-4ffd-952e-3cde2adcbf2f", + "sha256": "8dd2361b38ded42a65ec3b79bd9f64742e3bec916a0eb8783f2204e55cb663fc", + "created": "2026-01-20T07:53:03.141Z", + "duration": 18.566813, + "channels": 1, + "models": [ + "40bd3654-e622-47c4-a111-63a61b23bfe8" + ], + "model_info": { + "40bd3654-e622-47c4-a111-63a61b23bfe8": { + "name": "general-nova-3", + "version": "2025-04-17.21547", + "arch": "nova-3" + } + } + } + } + ], + "transcript": "This is something that I am testing to make sure that that is working properly." +} \ No newline at end of file diff --git a/evals/stt/results/yes-deepgram-flux.json b/evals/stt/results/yes-deepgram-flux.json new file mode 100644 index 0000000..7f70551 --- /dev/null +++ b/evals/stt/results/yes-deepgram-flux.json @@ -0,0 +1,402 @@ +{ + "audio_file": "yes.m4a", + "audio_path": "../audio/yes.m4a", + "provider": "deepgram-flux", + "duration": 2.507755, + "created_at": "2026-01-20T13:33:37.737569", + "events": [ + { + "timestamp": 2.0791776478290558e-07, + "event_type": "Connected", + "data": { + "type": "Connected", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "sequence_id": 0 + } + }, + { + "timestamp": 0.6149860408622772, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.2494, + "sequence_id": 1 + } + }, + { + "timestamp": 0.8699209159240127, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1246, + "sequence_id": 2 + } + }, + { + "timestamp": 1.0665327080059797, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.72, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0557, + "sequence_id": 3 + } + }, + { + "timestamp": 1.319559457944706, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "StartOfTurn", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 0.96, + "transcript": "Yes.", + "words": [ + { + "word": "Yes.", + "confidence": 0.9761 + } + ], + "end_of_turn_confidence": 0.0793, + "sequence_id": 4 + } + }, + { + "timestamp": 1.5604322908911854, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.2, + "transcript": "Yes.", + "words": [ + { + "word": "Yes.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.5703, + "sequence_id": 5 + } + }, + { + "timestamp": 1.6325784579385072, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "EndOfTurn", + "turn_index": 0, + "audio_window_start": 0.0, + "audio_window_end": 1.28, + "transcript": "Yes.", + "words": [ + { + "word": "Yes.", + "confidence": 1.0 + } + ], + "end_of_turn_confidence": 0.7026, + "sequence_id": 6 + } + }, + { + "timestamp": 1.897370790829882, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 1.52, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.4883, + "sequence_id": 7 + } + }, + { + "timestamp": 2.117000916041434, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 1.76, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.3801, + "sequence_id": 8 + } + }, + { + "timestamp": 2.3733394159935415, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 2.0, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.2346, + "sequence_id": 9 + } + }, + { + "timestamp": 2.6072654998861253, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 2.24, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1049, + "sequence_id": 10 + } + }, + { + "timestamp": 2.85038537485525, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 2.48, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.075, + "sequence_id": 11 + } + }, + { + "timestamp": 3.091235165949911, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 2.72, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0218, + "sequence_id": 12 + } + }, + { + "timestamp": 3.3325049998238683, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 2.96, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.03, + "sequence_id": 13 + } + }, + { + "timestamp": 3.577521916013211, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 3.2, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0189, + "sequence_id": 14 + } + }, + { + "timestamp": 3.8645569998770952, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 3.44, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0118, + "sequence_id": 15 + } + }, + { + "timestamp": 4.106258499901742, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 3.68, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0089, + "sequence_id": 16 + } + }, + { + "timestamp": 4.346511875046417, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 3.92, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0073, + "sequence_id": 17 + } + }, + { + "timestamp": 4.589668208034709, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 4.16, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0053, + "sequence_id": 18 + } + }, + { + "timestamp": 4.826804416021332, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 4.4, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0034, + "sequence_id": 19 + } + }, + { + "timestamp": 5.060472874902189, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 4.64, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0024, + "sequence_id": 20 + } + }, + { + "timestamp": 5.304136332822964, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 4.88, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1091, + "sequence_id": 21 + } + }, + { + "timestamp": 5.544230999890715, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 5.12, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.1007, + "sequence_id": 22 + } + }, + { + "timestamp": 5.779906540876254, + "event_type": "TurnInfo", + "data": { + "type": "TurnInfo", + "request_id": "277cf8d3-27b0-439b-a04e-707598e13489", + "event": "Update", + "turn_index": 1, + "audio_window_start": 1.28, + "audio_window_end": 5.36, + "transcript": "", + "words": [], + "end_of_turn_confidence": 0.0565, + "sequence_id": 23 + } + } + ], + "transcript": "Yes." +} \ No newline at end of file diff --git a/evals/visualizer/.gitignore b/evals/visualizer/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/evals/visualizer/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/evals/visualizer/README.md b/evals/visualizer/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/evals/visualizer/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/evals/visualizer/eslint.config.mjs b/evals/visualizer/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/evals/visualizer/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/evals/visualizer/next.config.ts b/evals/visualizer/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/evals/visualizer/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/evals/visualizer/package.json b/evals/visualizer/package.json new file mode 100644 index 0000000..5d44c50 --- /dev/null +++ b/evals/visualizer/package.json @@ -0,0 +1,26 @@ +{ + "name": "visualizer", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "next": "16.1.4", + "react": "19.2.3", + "react-dom": "19.2.3" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.4", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/evals/visualizer/pnpm-lock.yaml b/evals/visualizer/pnpm-lock.yaml new file mode 100644 index 0000000..11a9e03 --- /dev/null +++ b/evals/visualizer/pnpm-lock.yaml @@ -0,0 +1,4008 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + next: + specifier: 16.1.4 + version: 16.1.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.18 + '@types/node': + specifier: ^20 + version: 20.19.30 + '@types/react': + specifier: ^19 + version: 19.2.8 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.8) + eslint: + specifier: ^9 + version: 9.39.2(jiti@2.6.1) + eslint-config-next: + specifier: 16.1.4 + version: 16.1.4(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + tailwindcss: + specifier: ^4 + version: 4.1.18 + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.6': + resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.6': + resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.6': + resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.6': + resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@16.1.4': + resolution: {integrity: sha512-gkrXnZyxPUy0Gg6SrPQPccbNVLSP3vmW8LU5dwEttEEC1RwDivk8w4O+sZIjFvPrSICXyhQDCG+y3VmjlJf+9A==} + + '@next/eslint-plugin-next@16.1.4': + resolution: {integrity: sha512-38WMjGP8y+1MN4bcZFs+GTcBe0iem5GGTzFE5GWW/dWdRKde7LOXH3lQT2QuoquVWyfl2S0fQRchGmeacGZ4Wg==} + + '@next/swc-darwin-arm64@16.1.4': + resolution: {integrity: sha512-T8atLKuvk13XQUdVLCv1ZzMPgLPW0+DWWbHSQXs0/3TjPrKNxTmUIhOEaoEyl3Z82k8h/gEtqyuoZGv6+Ugawg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.1.4': + resolution: {integrity: sha512-AKC/qVjUGUQDSPI6gESTx0xOnOPQ5gttogNS3o6bA83yiaSZJek0Am5yXy82F1KcZCx3DdOwdGPZpQCluonuxg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.1.4': + resolution: {integrity: sha512-POQ65+pnYOkZNdngWfMEt7r53bzWiKkVNbjpmCt1Zb3V6lxJNXSsjwRuTQ8P/kguxDC8LRkqaL3vvsFrce4dMQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.1.4': + resolution: {integrity: sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.1.4': + resolution: {integrity: sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.1.4': + resolution: {integrity: sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.1.4': + resolution: {integrity: sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.1.4': + resolution: {integrity: sha512-JSVlm9MDhmTXw/sO2PE/MRj+G6XOSMZB+BcZ0a7d6KwVFZVpkHcb2okyoYFBaco6LeiL53BBklRlOrDDbOeE5w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.18': + resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.8': + resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} + + '@typescript-eslint/eslint-plugin@8.53.1': + resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.53.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.53.1': + resolution: {integrity: sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.53.1': + resolution: {integrity: sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.53.1': + resolution: {integrity: sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.53.1': + resolution: {integrity: sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.53.1': + resolution: {integrity: sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.53.1': + resolution: {integrity: sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.53.1': + resolution: {integrity: sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.53.1': + resolution: {integrity: sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.53.1': + resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.15: + resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} + hasBin: true + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001765: + resolution: {integrity: sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@16.1.4: + resolution: {integrity: sha512-iCrrNolUPpn/ythx0HcyNRfUBgTkaNBXByisKUbusPGCl8DMkDXXAu7exlSTSLGTIsH9lFE/c4s/3Qiyv2qwdA==} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@16.1.4: + resolution: {integrity: sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.53.1: + resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.6': {} + + '@babel/core@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.6': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/traverse@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@16.1.4': {} + + '@next/eslint-plugin-next@16.1.4': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@16.1.4': + optional: true + + '@next/swc-darwin-x64@16.1.4': + optional: true + + '@next/swc-linux-arm64-gnu@16.1.4': + optional: true + + '@next/swc-linux-arm64-musl@16.1.4': + optional: true + + '@next/swc-linux-x64-gnu@16.1.4': + optional: true + + '@next/swc-linux-x64-musl@16.1.4': + optional: true + + '@next/swc-win32-arm64-msvc@16.1.4': + optional: true + + '@next/swc-win32-x64-msvc@16.1.4': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@rtsao/scc@1.1.0': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/postcss@4.1.18': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + postcss: 8.5.6 + tailwindcss: 4.1.18 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@20.19.30': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.2.3(@types/react@19.2.8)': + dependencies: + '@types/react': 19.2.8 + + '@types/react@19.2.8': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.1 + eslint: 9.39.2(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.53.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.53.1': + dependencies: + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/visitor-keys': 8.53.1 + + '@typescript-eslint/tsconfig-utils@8.53.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.53.1': {} + + '@typescript-eslint/typescript-estree@8.53.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.53.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.53.1': + dependencies: + '@typescript-eslint/types': 8.53.1 + eslint-visitor-keys: 4.2.1 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.11.1: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.15: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.15 + caniuse-lite: 1.0.30001765 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001765: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + client-only@0.0.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-libc@2.1.2: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.267: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.2: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@16.1.4(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.1.4 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) + globals: 16.4.0 + typescript-eslint: 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.2(jiti@2.6.1) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@babel/core': 7.28.6 + '@babel/parser': 7.28.6 + eslint: 9.39.2(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.3.5 + zod-validation-error: 4.0.2(zod@4.3.5) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 9.39.2(jiti@2.6.1) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.3 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + next@16.1.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@next/env': 16.1.4 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.9.15 + caniuse-lite: 1.0.30001765 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.4 + '@next/swc-darwin-x64': 16.1.4 + '@next/swc-linux-arm64-gnu': 16.1.4 + '@next/swc-linux-arm64-musl': 16.1.4 + '@next/swc-linux-x64-gnu': 16.1.4 + '@next/swc-linux-x64-musl': 16.1.4 + '@next/swc-win32-arm64-msvc': 16.1.4 + '@next/swc-win32-x64-msvc': 16.1.4 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-releases@2.0.27: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react@19.2.3: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.3: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.3): + dependencies: + client-only: 0.0.1 + react: 19.2.3 + optionalDependencies: + '@babel/core': 7.28.6 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.3.5): + dependencies: + zod: 4.3.5 + + zod@4.3.5: {} diff --git a/evals/visualizer/pnpm-workspace.yaml b/evals/visualizer/pnpm-workspace.yaml new file mode 100644 index 0000000..4851e0c --- /dev/null +++ b/evals/visualizer/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - . +ignoredBuiltDependencies: + - sharp + - unrs-resolver diff --git a/evals/visualizer/postcss.config.mjs b/evals/visualizer/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/evals/visualizer/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/evals/visualizer/public/file.svg b/evals/visualizer/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/evals/visualizer/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/evals/visualizer/public/globe.svg b/evals/visualizer/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/evals/visualizer/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/evals/visualizer/public/next.svg b/evals/visualizer/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/evals/visualizer/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/evals/visualizer/public/vercel.svg b/evals/visualizer/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/evals/visualizer/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/evals/visualizer/public/window.svg b/evals/visualizer/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/evals/visualizer/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/evals/visualizer/src/app/api/audio/[filename]/route.ts b/evals/visualizer/src/app/api/audio/[filename]/route.ts new file mode 100644 index 0000000..e7dba65 --- /dev/null +++ b/evals/visualizer/src/app/api/audio/[filename]/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +const AUDIO_DIR = path.join(process.cwd(), "..", "stt", "audio"); + +const MIME_TYPES: Record = { + ".mp3": "audio/mpeg", + ".wav": "audio/wav", + ".m4a": "audio/mp4", + ".ogg": "audio/ogg", + ".webm": "audio/webm", +}; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ filename: string }> } +) { + try { + const { filename } = await params; + const filePath = path.join(AUDIO_DIR, filename); + + if (!fs.existsSync(filePath)) { + return NextResponse.json({ error: "Audio file not found" }, { status: 404 }); + } + + const ext = path.extname(filename).toLowerCase(); + const contentType = MIME_TYPES[ext] || "application/octet-stream"; + + const fileBuffer = fs.readFileSync(filePath); + + return new NextResponse(fileBuffer, { + headers: { + "Content-Type": contentType, + "Content-Length": fileBuffer.length.toString(), + }, + }); + } catch (error) { + console.error("Error serving audio:", error); + return NextResponse.json({ error: "Failed to serve audio" }, { status: 500 }); + } +} diff --git a/evals/visualizer/src/app/api/results/[id]/route.ts b/evals/visualizer/src/app/api/results/[id]/route.ts new file mode 100644 index 0000000..08711f4 --- /dev/null +++ b/evals/visualizer/src/app/api/results/[id]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +const RESULTS_DIR = path.join(process.cwd(), "..", "stt", "results"); + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const filePath = path.join(RESULTS_DIR, `${id}.json`); + + if (!fs.existsSync(filePath)) { + return NextResponse.json({ error: "Result not found" }, { status: 404 }); + } + + const content = fs.readFileSync(filePath, "utf-8"); + const data = JSON.parse(content); + + return NextResponse.json(data); + } catch (error) { + console.error("Error reading result:", error); + return NextResponse.json({ error: "Failed to read result" }, { status: 500 }); + } +} diff --git a/evals/visualizer/src/app/api/results/route.ts b/evals/visualizer/src/app/api/results/route.ts new file mode 100644 index 0000000..08b2095 --- /dev/null +++ b/evals/visualizer/src/app/api/results/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; +import { ResultSummary, EventCaptureResult } from "@/types"; + +const RESULTS_DIR = path.join(process.cwd(), "..", "stt", "results"); + +export async function GET() { + try { + if (!fs.existsSync(RESULTS_DIR)) { + return NextResponse.json([]); + } + + const files = fs.readdirSync(RESULTS_DIR).filter((f) => f.endsWith(".json")); + const results: ResultSummary[] = []; + + for (const file of files) { + try { + const filePath = path.join(RESULTS_DIR, file); + const content = fs.readFileSync(filePath, "utf-8"); + const data: EventCaptureResult = JSON.parse(content); + + results.push({ + id: file.replace(".json", ""), + audio_file: data.audio_file, + provider: data.provider, + duration: data.duration, + created_at: data.created_at, + event_count: data.events.length, + }); + } catch { + console.error(`Failed to parse ${file}`); + } + } + + // Sort by created_at descending + results.sort( + (a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + + return NextResponse.json(results); + } catch (error) { + console.error("Error reading results:", error); + return NextResponse.json({ error: "Failed to read results" }, { status: 500 }); + } +} diff --git a/evals/visualizer/src/app/favicon.ico b/evals/visualizer/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/evals/visualizer/src/app/favicon.ico differ diff --git a/evals/visualizer/src/app/globals.css b/evals/visualizer/src/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/evals/visualizer/src/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/evals/visualizer/src/app/layout.tsx b/evals/visualizer/src/app/layout.tsx new file mode 100644 index 0000000..fc37a19 --- /dev/null +++ b/evals/visualizer/src/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "STT Event Visualizer", + description: "Visualize WebSocket events from STT providers", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/evals/visualizer/src/app/page.tsx b/evals/visualizer/src/app/page.tsx new file mode 100644 index 0000000..422c61d --- /dev/null +++ b/evals/visualizer/src/app/page.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { ResultSummary } from "@/types"; + +function formatDuration(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, "0")}`; +} + +function formatDate(isoString: string): string { + const date = new Date(isoString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +const PROVIDER_COLORS: Record = { + deepgram: "bg-blue-500/20 text-blue-300", + "deepgram-flux": "bg-green-500/20 text-green-300", + speechmatics: "bg-purple-500/20 text-purple-300", +}; + +export default function Home() { + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchResults() { + try { + const response = await fetch("/api/results"); + if (!response.ok) { + throw new Error("Failed to fetch results"); + } + const data = await response.json(); + setResults(data); + } catch (err) { + setError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setLoading(false); + } + } + + fetchResults(); + }, []); + + return ( +
+
+
+

STT Event Visualizer

+

+ Visualize captured WebSocket events from STT providers +

+
+ + {loading && ( +
+
+
+ )} + + {error && ( +
+ {error} +
+ )} + + {!loading && !error && results.length === 0 && ( +
+

No results found

+

+ Run the event capture script to generate results: +

+ + python -m evals.stt.event_capture audio/multi_speaker.m4a --provider deepgram + +
+ )} + + {!loading && !error && results.length > 0 && ( +
+ {results.map((result) => ( + +
+
+
+ {result.audio_file} + + {result.provider} + +
+
+ {formatDate(result.created_at)} +
+
+
+
+ {formatDuration(result.duration)} +
+
+ {result.event_count} events +
+
+
+ + ))} +
+ )} +
+
+ ); +} diff --git a/evals/visualizer/src/app/view/[id]/page.tsx b/evals/visualizer/src/app/view/[id]/page.tsx new file mode 100644 index 0000000..a217312 --- /dev/null +++ b/evals/visualizer/src/app/view/[id]/page.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { useParams } from "next/navigation"; +import Link from "next/link"; +import { EventCaptureResult } from "@/types"; +import AudioPlayer from "@/components/AudioPlayer"; +import EventTimeline from "@/components/EventTimeline"; +import EventList from "@/components/EventList"; + +const PROVIDER_COLORS: Record = { + deepgram: "bg-blue-500/20 text-blue-300", + "deepgram-flux": "bg-green-500/20 text-green-300", + speechmatics: "bg-purple-500/20 text-purple-300", +}; + +export default function ViewPage() { + const params = useParams(); + const id = params.id as string; + + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentTime, setCurrentTime] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + + useEffect(() => { + async function fetchResult() { + try { + const response = await fetch(`/api/results/${id}`); + if (!response.ok) { + if (response.status === 404) { + throw new Error("Result not found"); + } + throw new Error("Failed to fetch result"); + } + const data = await response.json(); + setResult(data); + } catch (err) { + setError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setLoading(false); + } + } + + if (id) { + fetchResult(); + } + }, [id]); + + const handleTimeUpdate = useCallback((time: number) => { + setCurrentTime(time); + }, []); + + const handlePlayingChange = useCallback((playing: boolean) => { + setIsPlaying(playing); + }, []); + + const handleSeek = useCallback((time: number) => { + setCurrentTime(time); + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+
+ + ← Back to results + +
{error}
+
+
+ ); + } + + if (!result) { + return null; + } + + const audioUrl = `/api/audio/${result.audio_file}`; + + return ( +
+
+ {/* Header */} +
+ + ← Back to results + +
+

{result.audio_file}

+ + {result.provider} + +
+ {result.transcript && ( +

+ {result.transcript} +

+ )} +
+ + {/* Main content */} +
+ {/* Left column: Audio player and timeline */} +
+ + + + + {/* Transcript section */} + {result.transcript && ( +
+
+ Final Transcript +
+

{result.transcript}

+
+ )} +
+ + {/* Right column: Event list */} +
+ +
+
+
+
+ ); +} diff --git a/evals/visualizer/src/components/AudioPlayer.tsx b/evals/visualizer/src/components/AudioPlayer.tsx new file mode 100644 index 0000000..e5795e5 --- /dev/null +++ b/evals/visualizer/src/components/AudioPlayer.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useRef, useEffect, useState, useCallback } from "react"; + +interface AudioPlayerProps { + audioUrl: string; + duration: number; + currentTime: number; + onTimeUpdate: (time: number) => void; + onPlayingChange: (playing: boolean) => void; +} + +function formatTime(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, "0")}`; +} + +export default function AudioPlayer({ + audioUrl, + duration, + currentTime, + onTimeUpdate, + onPlayingChange, +}: AudioPlayerProps) { + const audioRef = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); + const [internalTime, setInternalTime] = useState(0); + + useEffect(() => { + const audio = audioRef.current; + if (!audio) return; + + const handleTimeUpdate = () => { + setInternalTime(audio.currentTime); + onTimeUpdate(audio.currentTime); + }; + + const handlePlay = () => { + setIsPlaying(true); + onPlayingChange(true); + }; + + const handlePause = () => { + setIsPlaying(false); + onPlayingChange(false); + }; + + const handleEnded = () => { + setIsPlaying(false); + onPlayingChange(false); + }; + + audio.addEventListener("timeupdate", handleTimeUpdate); + audio.addEventListener("play", handlePlay); + audio.addEventListener("pause", handlePause); + audio.addEventListener("ended", handleEnded); + + return () => { + audio.removeEventListener("timeupdate", handleTimeUpdate); + audio.removeEventListener("play", handlePlay); + audio.removeEventListener("pause", handlePause); + audio.removeEventListener("ended", handleEnded); + }; + }, [onTimeUpdate, onPlayingChange]); + + // Seek to currentTime when it changes externally + useEffect(() => { + const audio = audioRef.current; + if (!audio) return; + + // Only seek if the difference is significant (user clicked timeline) + if (Math.abs(audio.currentTime - currentTime) > 0.5) { + audio.currentTime = currentTime; + } + }, [currentTime]); + + const togglePlay = useCallback(() => { + const audio = audioRef.current; + if (!audio) return; + + if (isPlaying) { + audio.pause(); + } else { + audio.play(); + } + }, [isPlaying]); + + const handleSeek = useCallback((e: React.ChangeEvent) => { + const audio = audioRef.current; + if (!audio) return; + + const newTime = parseFloat(e.target.value); + audio.currentTime = newTime; + setInternalTime(newTime); + onTimeUpdate(newTime); + }, [onTimeUpdate]); + + return ( +
+
+ ); +} diff --git a/evals/visualizer/src/components/EventList.tsx b/evals/visualizer/src/components/EventList.tsx new file mode 100644 index 0000000..02a3f9e --- /dev/null +++ b/evals/visualizer/src/components/EventList.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useEffect, useRef, useMemo, useState } from "react"; +import { CapturedEvent } from "@/types"; +import { DeepgramEventItem, FluxEventItem, SpeechmaticsEventItem } from "./events"; + +interface EventListProps { + events: CapturedEvent[]; + currentTime: number; + onSeek: (time: number) => void; + provider: string; +} + +function formatTime(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + const ms = Math.floor((seconds % 1) * 100); + return `${mins}:${secs.toString().padStart(2, "0")}.${ms.toString().padStart(2, "0")}`; +} + +function getEventItemComponent(provider: string) { + if (provider === "deepgram-flux") { + return FluxEventItem; + } + if (provider === "speechmatics") { + return SpeechmaticsEventItem; + } + // Default to Deepgram Nova + return DeepgramEventItem; +} + +export default function EventList({ + events, + currentTime, + onSeek, + provider, +}: EventListProps) { + const containerRef = useRef(null); + const [expandedEvents, setExpandedEvents] = useState>(new Set()); + const [autoScroll, setAutoScroll] = useState(true); + + const EventItemComponent = getEventItemComponent(provider); + + // Find the current event index based on time + const currentEventIndex = useMemo(() => { + for (let i = events.length - 1; i >= 0; i--) { + if (events[i].timestamp <= currentTime) { + return i; + } + } + return -1; + }, [events, currentTime]); + + // Auto-scroll to current event + useEffect(() => { + if (!autoScroll || currentEventIndex < 0) return; + + const container = containerRef.current; + if (!container) return; + + const eventElement = container.querySelector(`[data-index="${currentEventIndex}"]`); + if (eventElement) { + eventElement.scrollIntoView({ behavior: "smooth", block: "center" }); + } + }, [currentEventIndex, autoScroll]); + + const toggleExpand = (index: number) => { + setExpandedEvents((prev) => { + const next = new Set(prev); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + }; + + return ( +
+
+
+ Events ({events.length}) +
+ +
+ +
+ {events.map((event, index) => { + const isCurrent = index === currentEventIndex; + const isExpanded = expandedEvents.has(index); + + return ( +
onSeek(event.timestamp)} + > +
+ {/* Current indicator */} +
+ {isCurrent ? ( +
+ ) : ( +
+ )} +
+ + {/* Timestamp */} + + {formatTime(event.timestamp)} + + + {/* Provider-specific event item */} + toggleExpand(index)} + /> +
+
+ ); + })} +
+
+ ); +} diff --git a/evals/visualizer/src/components/EventTimeline.tsx b/evals/visualizer/src/components/EventTimeline.tsx new file mode 100644 index 0000000..2efa8f2 --- /dev/null +++ b/evals/visualizer/src/components/EventTimeline.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useMemo } from "react"; +import { CapturedEvent } from "@/types"; + +interface EventTimelineProps { + events: CapturedEvent[]; + duration: number; + currentTime: number; + onSeek: (time: number) => void; +} + +const EVENT_COLORS: Record = { + Results: "bg-blue-500", + TurnInfo: "bg-green-500", + AddTranscript: "bg-purple-500", + Connected: "bg-yellow-500", + RecognitionStarted: "bg-yellow-500", + EndOfTranscript: "bg-red-500", + Metadata: "bg-gray-500", + Error: "bg-red-600", + default: "bg-zinc-400", +}; + +function formatTime(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, "0")}`; +} + +export default function EventTimeline({ + events, + duration, + currentTime, + onSeek, +}: EventTimelineProps) { + const timeMarkers = useMemo(() => { + const markers: number[] = []; + const interval = Math.ceil(duration / 6); + for (let i = 0; i <= duration; i += interval) { + markers.push(i); + } + if (markers[markers.length - 1] !== Math.floor(duration)) { + markers.push(Math.floor(duration)); + } + return markers; + }, [duration]); + + const handleClick = (e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left; + const percent = x / rect.width; + const time = percent * duration; + onSeek(Math.max(0, Math.min(time, duration))); + }; + + const progressPercent = (currentTime / duration) * 100; + + return ( +
+
Event Timeline
+ +
+ {/* Progress indicator */} +
+ + {/* Current time indicator */} +
+ + {/* Event markers */} +
+ {events.map((event, index) => { + const leftPercent = Math.min((event.timestamp / duration) * 100, 100); + const colorClass = + EVENT_COLORS[event.event_type] || EVENT_COLORS.default; + + return ( +
+ ); + })} +
+
+ + {/* Time markers */} +
+ {timeMarkers.map((time, index) => ( + {formatTime(time)} + ))} +
+ + {/* Legend */} +
+ {Object.entries(EVENT_COLORS) + .filter(([key]) => key !== "default") + .slice(0, 6) + .map(([eventType, colorClass]) => ( +
+
+ {eventType} +
+ ))} +
+
+ ); +} diff --git a/evals/visualizer/src/components/events/DeepgramEventItem.tsx b/evals/visualizer/src/components/events/DeepgramEventItem.tsx new file mode 100644 index 0000000..7419658 --- /dev/null +++ b/evals/visualizer/src/components/events/DeepgramEventItem.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { CapturedEvent } from "@/types"; + +interface DeepgramEventItemProps { + event: CapturedEvent; + isExpanded: boolean; + onToggleExpand: () => void; +} + +const EVENT_COLORS: Record = { + Results: "text-blue-400 bg-blue-500/10", + SpeechStarted: "text-yellow-400 bg-yellow-500/10", + Metadata: "text-gray-400 bg-gray-500/10", + UtteranceEnd: "text-red-500 bg-red-600/10", + default: "text-zinc-400 bg-zinc-500/10", +}; + +function getTranscript(event: CapturedEvent): string { + const data = event.data; + const channel = data.channel as Record | undefined; + if (channel) { + const alternatives = channel.alternatives as Array<{ transcript?: string }> | undefined; + if (alternatives?.[0]?.transcript) { + return alternatives[0].transcript; + } + } + return ""; +} + +export default function DeepgramEventItem({ + event, + isExpanded, + onToggleExpand, +}: DeepgramEventItemProps) { + const colorClass = EVENT_COLORS[event.event_type] || EVENT_COLORS.default; + const data = event.data; + + const transcript = getTranscript(event); + const isFinal = data.is_final as boolean | undefined; + const speechFinal = data.speech_final as boolean | undefined; + + // For non-Results events + const isConnection = event.event_type === "Connected"; + const isMetadata = event.event_type === "Metadata"; + + return ( +
+
+ + {event.event_type} + + + {/* Final/Partial indicator for Results */} + {isFinal !== undefined && ( + + {isFinal ? "Final" : "Partial"} + + )} + + {/* Speech Final indicator */} + {speechFinal && ( + + Speech Final + + )} +
+ + {/* Transcript or status message */} +
+ {transcript} +
+ + {/* Expand/collapse button */} + + + {/* Expanded JSON view */} + {isExpanded && ( +
+          {JSON.stringify(event.data, null, 2)}
+        
+ )} +
+ ); +} diff --git a/evals/visualizer/src/components/events/FluxEventItem.tsx b/evals/visualizer/src/components/events/FluxEventItem.tsx new file mode 100644 index 0000000..6e85e5f --- /dev/null +++ b/evals/visualizer/src/components/events/FluxEventItem.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { CapturedEvent } from "@/types"; + +interface FluxEventItemProps { + event: CapturedEvent; + isExpanded: boolean; + onToggleExpand: () => void; +} + +const EVENT_COLORS: Record = { + TurnInfo: "text-green-400 bg-green-500/10", + Connected: "text-yellow-400 bg-yellow-500/10", + Error: "text-red-500 bg-red-600/10", + default: "text-zinc-400 bg-zinc-500/10", +}; + +const FLUX_EVENT_COLORS: Record = { + Update: "text-amber-300 bg-amber-500/20", + EndOfTurn: "text-emerald-300 bg-emerald-500/20", + EagerEndOfTurn: "text-cyan-300 bg-cyan-500/20", + StartOfTurn: "text-blue-300 bg-blue-500/20", + TurnResumed: "text-purple-300 bg-purple-500/20", + default: "text-zinc-300 bg-zinc-500/20", +}; + +export default function FluxEventItem({ + event, + isExpanded, + onToggleExpand, +}: FluxEventItemProps) { + const colorClass = EVENT_COLORS[event.event_type] || EVENT_COLORS.default; + const data = event.data; + + // Flux TurnInfo fields + const fluxEvent = data.event as string | undefined; + const transcript = data.transcript as string | undefined; + const endOfTurnConfidence = data.end_of_turn_confidence as number | undefined; + const turnIndex = data.turn_index as number | undefined; + + const isFinal = fluxEvent === "EndOfTurn"; + const fluxEventColor = fluxEvent + ? FLUX_EVENT_COLORS[fluxEvent] || FLUX_EVENT_COLORS.default + : ""; + + // For non-TurnInfo events + const isConnection = event.event_type === "Connected"; + + return ( +
+
+ + {event.event_type} + + + {/* Flux sub-event type */} + {fluxEvent && ( + + {fluxEvent} + + )} + + {/* Final/Partial indicator */} + {fluxEvent && ( + + {isFinal ? "Final" : "Partial"} + + )} + + {/* Turn index */} + {turnIndex !== undefined && ( + + Turn {turnIndex} + + )} + + {/* EOT confidence */} + {endOfTurnConfidence !== undefined && ( + + EOT: {(endOfTurnConfidence * 100).toFixed(1)}% + + )} +
+ + {/* Transcript or status message */} +
+ {transcript || (isConnection ? "[Connected]" : `[${fluxEvent || event.event_type}]`)} +
+ + {/* Expand/collapse button */} + + + {/* Expanded JSON view */} + {isExpanded && ( +
+          {JSON.stringify(event.data, null, 2)}
+        
+ )} +
+ ); +} diff --git a/evals/visualizer/src/components/events/SpeechmaticsEventItem.tsx b/evals/visualizer/src/components/events/SpeechmaticsEventItem.tsx new file mode 100644 index 0000000..e6411d3 --- /dev/null +++ b/evals/visualizer/src/components/events/SpeechmaticsEventItem.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { CapturedEvent } from "@/types"; + +interface SpeechmaticsEventItemProps { + event: CapturedEvent; + isExpanded: boolean; + onToggleExpand: () => void; +} + +const EVENT_COLORS: Record = { + AddTranscript: "text-purple-400 bg-purple-500/10", + RecognitionStarted: "text-yellow-400 bg-yellow-500/10", + EndOfTranscript: "text-red-400 bg-red-500/10", + Warning: "text-orange-400 bg-orange-500/10", + Error: "text-red-500 bg-red-600/10", + default: "text-zinc-400 bg-zinc-500/10", +}; + +function getTranscript(event: CapturedEvent): string { + const data = event.data; + const results = data.results as Array<{ + type?: string; + alternatives?: Array<{ content?: string }>; + }> | undefined; + + if (results) { + const words = results + .filter((r) => r.type === "word" && r.alternatives?.[0]?.content) + .map((r) => r.alternatives![0].content) + .join(" "); + return words; + } + return ""; +} + +export default function SpeechmaticsEventItem({ + event, + isExpanded, + onToggleExpand, +}: SpeechmaticsEventItemProps) { + const colorClass = EVENT_COLORS[event.event_type] || EVENT_COLORS.default; + const data = event.data; + + const transcript = getTranscript(event); + + // Status events + const isRecognitionStarted = event.event_type === "RecognitionStarted"; + const isEndOfTranscript = event.event_type === "EndOfTranscript"; + const isWarning = event.event_type === "Warning"; + + // Warning reason + const warningReason = isWarning ? (data.reason as string | undefined) : undefined; + + return ( +
+
+ + {event.event_type} + + + {/* AddTranscript is always final in Speechmatics */} + {event.event_type === "AddTranscript" && ( + + Final + + )} +
+ + {/* Transcript or status message */} +
+ {transcript || + (isRecognitionStarted + ? "[Recognition Started]" + : isEndOfTranscript + ? "[End of Transcript]" + : isWarning + ? `[Warning: ${warningReason || "unknown"}]` + : `[${event.event_type}]`)} +
+ + {/* Expand/collapse button */} + + + {/* Expanded JSON view */} + {isExpanded && ( +
+          {JSON.stringify(event.data, null, 2)}
+        
+ )} +
+ ); +} diff --git a/evals/visualizer/src/components/events/index.ts b/evals/visualizer/src/components/events/index.ts new file mode 100644 index 0000000..e436198 --- /dev/null +++ b/evals/visualizer/src/components/events/index.ts @@ -0,0 +1,3 @@ +export { default as DeepgramEventItem } from "./DeepgramEventItem"; +export { default as FluxEventItem } from "./FluxEventItem"; +export { default as SpeechmaticsEventItem } from "./SpeechmaticsEventItem"; diff --git a/evals/visualizer/src/types/index.ts b/evals/visualizer/src/types/index.ts new file mode 100644 index 0000000..787e89d --- /dev/null +++ b/evals/visualizer/src/types/index.ts @@ -0,0 +1,24 @@ +export interface CapturedEvent { + timestamp: number; + event_type: string; + data: Record; +} + +export interface EventCaptureResult { + audio_file: string; + audio_path: string; + provider: string; + duration: number; + created_at: string; + events: CapturedEvent[]; + transcript: string; +} + +export interface ResultSummary { + id: string; + audio_file: string; + provider: string; + duration: number; + created_at: string; + event_count: number; +} diff --git a/evals/visualizer/tsconfig.json b/evals/visualizer/tsconfig.json new file mode 100644 index 0000000..cf9c65d --- /dev/null +++ b/evals/visualizer/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/pipecat b/pipecat index a1d3062..f11fad8 160000 --- a/pipecat +++ b/pipecat @@ -1 +1 @@ -Subproject commit a1d3062446240b6b27ebc787d28578e4561e7441 +Subproject commit f11fad8f3e90e06b1625b9dc49c13e26f3c9e716 diff --git a/release-please-config.json b/release-please-config.json index 1e07b35..83acde6 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -16,6 +16,11 @@ "type": "json", "path": "ui/package.json", "jsonpath": "$.version" + }, + { + "type": "toml", + "path": "api/pyproject.toml", + "key": "project.version" } ] } diff --git a/ui/src/app/after-sign-in/page.tsx b/ui/src/app/after-sign-in/page.tsx index 21f0ea8..39796e9 100644 --- a/ui/src/app/after-sign-in/page.tsx +++ b/ui/src/app/after-sign-in/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; -import { getWorkflowsApiV1WorkflowFetchGet } from "@/client/sdk.gen"; +import { getWorkflowCountApiV1WorkflowCountGet } from "@/client/sdk.gen"; import { getServerAccessToken,getServerAuthProvider, getServerUser } from "@/lib/auth/server"; import logger from '@/lib/logger'; import { getRedirectUrl } from "@/lib/utils"; @@ -34,21 +34,18 @@ export default async function AfterSignInPage() { try { const accessToken = await getServerAccessToken(); if (accessToken) { - const workflowsResponse = await getWorkflowsApiV1WorkflowFetchGet({ + const countResponse = await getWorkflowCountApiV1WorkflowCountGet({ headers: { Authorization: `Bearer ${accessToken}`, }, }); - const workflows = workflowsResponse.data ? (Array.isArray(workflowsResponse.data) ? workflowsResponse.data : [workflowsResponse.data]) : []; - const activeWorkflows = workflows.filter(w => w.status === 'active'); - logger.debug('[AfterSignInPage] Found workflows:', { - total: workflows.length, - active: activeWorkflows.length + total: countResponse.data?.total, + active: countResponse.data?.active }); - if (activeWorkflows.length > 0) { + if (countResponse.data && countResponse.data.active > 0) { logger.debug('[AfterSignInPage] Redirecting to /workflow - user has workflows'); redirect('/workflow'); } else { diff --git a/ui/src/app/api/config/version/route.ts b/ui/src/app/api/config/version/route.ts new file mode 100644 index 0000000..241085f --- /dev/null +++ b/ui/src/app/api/config/version/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from "next/server"; + +import { healthApiV1HealthGet } from "@/client/sdk.gen"; +import type { HealthResponse } from "@/client/types.gen"; + +// Import version from package.json at build time +import packageJson from "../../../../../package.json"; + +export async function GET() { + const uiVersion = packageJson.version || "dev"; + + // Fetch backend version and config from health endpoint + let apiVersion = "unknown"; + let backendApiEndpoint: string | null = null; + + try { + const response = await healthApiV1HealthGet(); + if (response.data) { + const data = response.data as HealthResponse; + apiVersion = data.version; + backendApiEndpoint = data.backend_api_endpoint; + } + } catch { + // Backend might not be reachable during build or in some deployments + apiVersion = "unavailable"; + } + + return NextResponse.json({ + ui: uiVersion, + api: apiVersion, + backendApiEndpoint, + }); +} diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index b0a4ced..060fdd3 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -9,6 +9,7 @@ import AppLayout from "@/components/layout/AppLayout"; import PostHogIdentify from "@/components/PostHogIdentify"; import SpinLoader from "@/components/SpinLoader"; import { Toaster } from "@/components/ui/sonner"; +import { AppConfigProvider } from "@/context/AppConfigContext"; import { OnboardingProvider } from "@/context/OnboardingContext"; import { UserConfigProvider } from "@/context/UserConfigContext"; import { AuthProvider } from "@/lib/auth"; @@ -59,18 +60,20 @@ export default function RootLayout({ - }> - - - - - {children} - - - - - - + + }> + + + + + {children} + + + + + + + diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index ae6e151..60a175f 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -1,7 +1,7 @@ import { isNextRouterError } from "next/dist/client/components/is-next-router-error"; import { redirect } from "next/navigation"; -import { getWorkflowsApiV1WorkflowFetchGet } from "@/client/sdk.gen"; +import { getWorkflowCountApiV1WorkflowCountGet } from "@/client/sdk.gen"; import SignInClient from "@/components/SignInClient"; import { getServerAccessToken,getServerAuthProvider,getServerUser } from "@/lib/auth/server"; import logger from '@/lib/logger'; @@ -21,21 +21,18 @@ export default async function Home() { try { const accessToken = await getServerAccessToken(); if (accessToken) { - const workflowsResponse = await getWorkflowsApiV1WorkflowFetchGet({ + const countResponse = await getWorkflowCountApiV1WorkflowCountGet({ headers: { Authorization: `Bearer ${accessToken}`, }, }); - const workflows = workflowsResponse.data ? (Array.isArray(workflowsResponse.data) ? workflowsResponse.data : [workflowsResponse.data]) : []; - const activeWorkflows = workflows.filter(w => w.status === 'active'); - logger.debug('[HomePage] Found workflows for local provider:', { - total: workflows.length, - active: activeWorkflows.length + total: countResponse.data?.total, + active: countResponse.data?.active }); - if (activeWorkflows.length > 0) { + if (countResponse.data && countResponse.data.active > 0) { logger.debug('[HomePage] Redirecting to /workflow - user has workflows'); redirect('/workflow'); } else { diff --git a/ui/src/app/usage/page.tsx b/ui/src/app/usage/page.tsx index 688b47b..d38b7ce 100644 --- a/ui/src/app/usage/page.tsx +++ b/ui/src/app/usage/page.tsx @@ -326,14 +326,64 @@ export default function UsagePage() { isDisabled={savingTimezone || userConfigLoading} placeholder={userConfigLoading ? "Loading..." : "Select timezone"} styles={{ - control: (base) => ({ + control: (base, state) => ({ ...base, minHeight: '36px', fontSize: '14px', + backgroundColor: 'var(--background)', + borderColor: state.isFocused ? 'var(--ring)' : 'var(--border)', + boxShadow: state.isFocused ? '0 0 0 2px color-mix(in srgb, var(--ring) 20%, transparent)' : 'none', + '&:hover': { + borderColor: 'var(--border)', + }, }), menu: (base) => ({ ...base, zIndex: 9999, + backgroundColor: 'var(--popover)', + border: '1px solid var(--border)', + boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + }), + menuList: (base) => ({ + ...base, + backgroundColor: 'var(--popover)', + padding: 0, + }), + option: (base, state) => ({ + ...base, + backgroundColor: state.isSelected + ? 'var(--accent)' + : state.isFocused + ? 'var(--accent)' + : 'var(--popover)', + color: 'var(--foreground)', + cursor: 'pointer', + '&:active': { + backgroundColor: 'var(--accent)', + }, + }), + singleValue: (base) => ({ + ...base, + color: 'var(--foreground)', + }), + input: (base) => ({ + ...base, + color: 'var(--foreground)', + }), + placeholder: (base) => ({ + ...base, + color: 'var(--muted-foreground)', + }), + indicatorSeparator: (base) => ({ + ...base, + backgroundColor: 'var(--border)', + }), + dropdownIndicator: (base) => ({ + ...base, + color: 'var(--muted-foreground)', + '&:hover': { + color: 'var(--foreground)', + }, }), }} /> diff --git a/ui/src/client/sdk.gen.ts b/ui/src/client/sdk.gen.ts index c925c76..57a9786 100644 --- a/ui/src/client/sdk.gen.ts +++ b/ui/src/client/sdk.gen.ts @@ -3,7 +3,7 @@ import type { Client,Options as ClientOptions, TDataShape } from '@hey-api/client-fetch'; import { client as _heyApiClient } from './client.gen'; -import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; +import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; export type Options = ClientOptions & { /** @@ -251,9 +251,26 @@ export const createWorkflowFromTemplateApiV1WorkflowCreateTemplatePost = (options?: Options) => { + return (options?.client ?? _heyApiClient).get({ + url: '/api/v1/workflow/count', + ...options + }); +}; + /** * Get Workflows - * Get all workflows for the authenticated user's organization + * Get all workflows for the authenticated user's organization. + * + * Returns a lightweight response with only essential fields for listing. + * Use GET /workflow/fetch/{workflow_id} to get full workflow details. */ export const getWorkflowsApiV1WorkflowFetchGet = (options?: Options) => { return (options?.client ?? _heyApiClient).get({ @@ -1300,6 +1317,34 @@ export const initiateCallApiV1PublicAgentUuidPost = (options: Options) => { + return (options.client ?? _heyApiClient).get({ + url: '/api/v1/public/download/workflow/{token}/{artifact_type}', + ...options + }); +}; + /** * Deactivate Embed Token * Deactivate the embed token for a workflow. @@ -1461,7 +1506,7 @@ export const searchChunksApiV1KnowledgeBaseSearchPost = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/api/v1/health', ...options }); diff --git a/ui/src/client/types.gen.ts b/ui/src/client/types.gen.ts index 9e1a028..cd1eeb9 100644 --- a/ui/src/client/types.gen.ts +++ b/ui/src/client/types.gen.ts @@ -524,6 +524,12 @@ export type HttpValidationError = { detail?: Array; }; +export type HealthResponse = { + status: string; + version: string; + backend_api_endpoint: string; +}; + /** * Configuration for HTTP API tools. */ @@ -1042,6 +1048,15 @@ export type VonageConfigurationResponse = { */ export type WebhookCredentialType = 'none' | 'api_key' | 'bearer_token' | 'basic_auth' | 'custom_header'; +/** + * Response for workflow count endpoint. + */ +export type WorkflowCountResponse = { + total: number; + active: number; + archived: number; +}; + export type WorkflowError = { kind: ItemKind; id: string | null; @@ -1049,6 +1064,17 @@ export type WorkflowError = { message: string; }; +/** + * Lightweight response for workflow listings (excludes large fields). + */ +export type WorkflowListResponse = { + id: number; + name: string; + status: string; + created_at: string; + total_runs: number; +}; + export type WorkflowOption = { id: number; name: string; @@ -1391,6 +1417,7 @@ export type HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData = { 'x-twilio-signature'?: string | null; 'x-vobiz-signature'?: string | null; 'x-vobiz-timestamp'?: string | null; + 'x-cx-apikey'?: string | null; }; path: { workflow_id: number; @@ -1655,6 +1682,39 @@ export type CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponses = export type CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse = CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponses[keyof CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponses]; +export type GetWorkflowCountApiV1WorkflowCountGetData = { + body?: never; + headers?: { + authorization?: string | null; + 'X-API-Key'?: string | null; + }; + path?: never; + query?: never; + url: '/api/v1/workflow/count'; +}; + +export type GetWorkflowCountApiV1WorkflowCountGetErrors = { + /** + * Not found + */ + 404: unknown; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetWorkflowCountApiV1WorkflowCountGetError = GetWorkflowCountApiV1WorkflowCountGetErrors[keyof GetWorkflowCountApiV1WorkflowCountGetErrors]; + +export type GetWorkflowCountApiV1WorkflowCountGetResponses = { + /** + * Successful Response + */ + 200: WorkflowCountResponse; +}; + +export type GetWorkflowCountApiV1WorkflowCountGetResponse = GetWorkflowCountApiV1WorkflowCountGetResponses[keyof GetWorkflowCountApiV1WorkflowCountGetResponses]; + export type GetWorkflowsApiV1WorkflowFetchGetData = { body?: never; headers?: { @@ -1688,7 +1748,7 @@ export type GetWorkflowsApiV1WorkflowFetchGetResponses = { /** * Successful Response */ - 200: Array; + 200: Array; }; export type GetWorkflowsApiV1WorkflowFetchGetResponse = GetWorkflowsApiV1WorkflowFetchGetResponses[keyof GetWorkflowsApiV1WorkflowFetchGetResponses]; @@ -4168,6 +4228,41 @@ export type InitiateCallApiV1PublicAgentUuidPostResponses = { export type InitiateCallApiV1PublicAgentUuidPostResponse = InitiateCallApiV1PublicAgentUuidPostResponses[keyof InitiateCallApiV1PublicAgentUuidPostResponses]; +export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData = { + body?: never; + path: { + token: string; + artifact_type: 'recording' | 'transcript'; + }; + query?: { + /** + * Display inline in browser instead of download + */ + inline?: boolean; + }; + url: '/api/v1/public/download/workflow/{token}/{artifact_type}'; +}; + +export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetErrors = { + /** + * Not found + */ + 404: unknown; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError = DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetErrors[keyof DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetErrors]; + +export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + export type DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData = { body?: never; headers?: { @@ -4500,9 +4595,11 @@ export type HealthApiV1HealthGetResponses = { /** * Successful Response */ - 200: unknown; + 200: HealthResponse; }; +export type HealthApiV1HealthGetResponse = HealthApiV1HealthGetResponses[keyof HealthApiV1HealthGetResponses]; + export type ClientOptions = { baseUrl: 'http://127.0.0.1:8000' | (string & {}); }; diff --git a/ui/src/components/MediaPreviewDialog.tsx b/ui/src/components/MediaPreviewDialog.tsx index a463d48..fd406f2 100644 --- a/ui/src/components/MediaPreviewDialog.tsx +++ b/ui/src/components/MediaPreviewDialog.tsx @@ -22,6 +22,7 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) { const [isOpen, setIsOpen] = useState(false); const [mediaType, setMediaType] = useState<'audio' | 'transcript' | null>(null); const [mediaSignedUrl, setMediaSignedUrl] = useState(null); + const [transcriptContent, setTranscriptContent] = useState(null); const [selectedRunId, setSelectedRunId] = useState(null); const [mediaDownloadKey, setMediaDownloadKey] = useState(null); const [mediaLoading, setMediaLoading] = useState(false); @@ -47,6 +48,7 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) { async (fileKey: string | null, runId: number) => { if (!fileKey || !accessToken) return; setMediaLoading(true); + setTranscriptContent(null); const signed = await getSignedUrl(fileKey, accessToken, true); if (signed) { setMediaType('transcript'); @@ -54,6 +56,14 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) { setMediaDownloadKey(fileKey); setSelectedRunId(runId); setIsOpen(true); + // Fetch transcript content with proper UTF-8 encoding + try { + const response = await fetch(signed); + const text = await response.text(); + setTranscriptContent(text); + } catch (error) { + console.error('Error fetching transcript:', error); + } } setMediaLoading(false); }, @@ -84,12 +94,10 @@ export function MediaPreviewDialog({ accessToken }: MediaPreviewDialogProps) {