mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-02 13:48:26 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8acc4845f |
@@ -13,10 +13,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Crowdin files
|
||||
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
|
||||
@@ -30,36 +30,36 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '--target backend-production -f Dockerfile'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
target: backend-production
|
||||
@@ -76,36 +76,36 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
@@ -123,36 +123,36 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/dinum-frontend/Dockerfile
|
||||
@@ -170,30 +170,30 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/summary/Dockerfile --target production'
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
docker-context: './src/summary'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./src/summary
|
||||
file: ./src/summary/Dockerfile
|
||||
@@ -219,30 +219,30 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/meet-agents
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/agents/Dockerfile --target production'
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
docker-context: './src/agents'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./src/agents
|
||||
file: ./src/agents/Dockerfile
|
||||
@@ -273,7 +273,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: numerique-gouv/action-argocd-webhook-notification@cac2ee67896eb13e84e804f60c4271370424eaa8 # main
|
||||
- uses: numerique-gouv/action-argocd-webhook-notification@main
|
||||
id: notify
|
||||
with:
|
||||
deployment_repo_path: "${{ secrets.DEPLOYMENT_REPO_URL }}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: meet Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
@@ -26,17 +26,17 @@ jobs:
|
||||
- name: Enforce absence of print statements in code
|
||||
if: always()
|
||||
run: |
|
||||
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude).github/workflows/**' | grep "print("
|
||||
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/meet.yml' | grep "print("
|
||||
- name: Check absence of fixup commits
|
||||
if: always()
|
||||
run: |
|
||||
! git log | grep 'fixup!'
|
||||
- name: Install uv
|
||||
- name: Install gitlint
|
||||
if: always()
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
run: pip install --user requests gitlint
|
||||
- name: Lint commit messages added to main
|
||||
if: always()
|
||||
run: uvx --no-build --from gitlint-core==0.19.1 gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
@@ -77,15 +77,15 @@ jobs:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
uses: actions/cache@v5
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -93,11 +93,11 @@ jobs:
|
||||
|
||||
- name: Install yarn
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: npm install -g --ignore-scripts yarn@1.22.22
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install node dependencies
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build mails
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Cache mail templates
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
@@ -119,22 +119,22 @@ jobs:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run --no-sync --no-build ruff format . --diff
|
||||
run: uv run ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run --no-sync --no-build ruff check .
|
||||
run: uv run ruff check .
|
||||
- name: Lint code with pylint
|
||||
run: uv run --no-sync --no-build pylint meet demo core
|
||||
run: uv run pylint meet demo core
|
||||
|
||||
lint-agents:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -145,19 +145,19 @@ jobs:
|
||||
working-directory: src/agents
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras --no-build
|
||||
run: uv sync --locked --all-extras
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run --no-sync --no-build ruff format . --diff
|
||||
run: uv run ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run --no-sync --no-build ruff check .
|
||||
run: uv run ruff check .
|
||||
|
||||
lint-summary:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -168,19 +168,18 @@ jobs:
|
||||
working-directory: src/summary
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
cache: "pip"
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run --no-sync --no-build ruff format . --diff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run --no-sync --no-build ruff check .
|
||||
run: ~/.local/bin/ruff check .
|
||||
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -236,7 +235,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Create writable /data
|
||||
run: |
|
||||
@@ -244,7 +243,7 @@ jobs:
|
||||
sudo mkdir -p /data/static
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
uses: actions/cache@v5
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -263,9 +262,7 @@ jobs:
|
||||
# Tool to wait for a service to be ready
|
||||
- name: Install Dockerize
|
||||
run: |
|
||||
curl --proto "=https" --proto-redir "=https" --tlsv1.2 -sSLf \
|
||||
https://github.com/jwilder/dockerize/releases/download/v0.8.0/dockerize-linux-amd64-v0.8.0.tar.gz |
|
||||
sudo tar -C /usr/local/bin -xzv
|
||||
curl -sSL https://github.com/jwilder/dockerize/releases/download/v0.8.0/dockerize-linux-amd64-v0.8.0.tar.gz | sudo tar -C /usr/local/bin -xzv
|
||||
|
||||
- name: Wait for MinIO to be ready
|
||||
run: |
|
||||
@@ -280,11 +277,11 @@ jobs:
|
||||
mc mb meet/meet-media-storage"
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install the dependencies
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
@@ -294,10 +291,10 @@ jobs:
|
||||
sudo apt-get install -y gettext
|
||||
|
||||
- name: Generate a MO file from strings extracted from the project
|
||||
run: uv run --no-sync --no-build python manage.py compilemessages
|
||||
run: uv run python manage.py compilemessages
|
||||
|
||||
- name: Run tests
|
||||
run: uv run --no-sync --no-build pytest -n 2
|
||||
run: uv run pytest -n 2
|
||||
|
||||
test-summary:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -323,7 +320,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
@@ -331,18 +328,16 @@ jobs:
|
||||
sudo apt-get install -y ffmpeg
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
|
||||
- name: Run summary tests
|
||||
run: uv run --no-sync --no-build pytest
|
||||
run: ~/.local/bin/pytest
|
||||
|
||||
lint-front:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -350,10 +345,10 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && npm ci --ignore-scripts
|
||||
run: cd src/frontend/ && npm ci
|
||||
|
||||
- name: Check linting
|
||||
run: cd src/frontend/ && npm run lint
|
||||
@@ -370,10 +365,10 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
run: npm ci
|
||||
|
||||
- name: Check linting
|
||||
run: npm run lint
|
||||
@@ -391,10 +386,10 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
run: npm ci
|
||||
|
||||
- name: Build SDK
|
||||
run: npm run build
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
run: rm -rf ./src/helm/extra
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
uses: azure/setup-helm@v4
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Publish Helm charts
|
||||
uses: numerique-gouv/helm-gh-pages@2cf477ae49d7c70037ceb1685803f4f7bad9b981 # add-overwrite-option
|
||||
uses: numerique-gouv/helm-gh-pages@add-overwrite-option
|
||||
with:
|
||||
charts_dir: ./src/helm
|
||||
linting: on
|
||||
|
||||
@@ -10,23 +10,8 @@ and this project adheres to
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) add per-recording encoding quality presets to start-recording API
|
||||
|
||||
### Changed
|
||||
git
|
||||
- 💥(backend) replace recording encoding options with a profile model
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) keep the sending resolution picked while the camera is off #1667
|
||||
|
||||
## [1.30.0] - 2026-09-01
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(agent) support Voxtral realtime as inference engine
|
||||
- 🌐(i18n) add Spanish language support
|
||||
- ✨(frontend) expose publish permissions on the media state element #1661
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -34,10 +19,6 @@ git
|
||||
- ♻️(backend) always finalize recordings using the LiveKit egress_ended webhook
|
||||
- ⬆️(frontend) upgrade posthog-js from 1.409.5 to 1.414.0
|
||||
- ⬆️(frontend) upgrade @fontsource-variable/lexend from 5.2.11 to 5.3.0
|
||||
- ⬆️(frontend) upgrade @fontsource/opendyslexic from 5.2.5 to 5.3.0
|
||||
- ⬆️(addons) upgrade core-js from 3.49.0 to 3.50.0
|
||||
- ♻️(backend) factorize s3 client creation in utils
|
||||
- ♿️(frontend) close side panel with Escape key #1507
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
-124
@@ -16,130 +16,6 @@ the following command inside your docker container:
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Recording encoding settings replaced by a resolution/profile model
|
||||
|
||||
The `RECORDING_ENCODING_*` settings introduced in v1.16.0 exposed raw encoder
|
||||
values (width, height, framerate, bitrate). They are replaced by two named and configurable sets of
|
||||
dimensions, a **resolution** (default: `540p`, `720p`, `1080p`) and a **profile**
|
||||
(default: `talking_heads`, `text`, `mixed`, `full`), which are resolved to the width, height,
|
||||
fps and video bitrate.
|
||||
|
||||
**The following environment variables are no longer read. If they are still set in
|
||||
your deployment they are silently ignored, and your recordings will be encoded with
|
||||
the new defaults instead of your tuned values.**
|
||||
|
||||
| Removed variable | Replaced by |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `RECORDING_ENCODING_ENABLED` | Nothing. A default encoding is now always built (see below). **Not** `RECORDING_CUSTOM_ENCODING_ENABLED`, which gates a different feature. |
|
||||
| `RECORDING_ENCODING_WIDTH` | The `width` of the entry selected by `RECORDING_ENCODING_DEFAULT_RESOLUTION` in `RECORDING_ENCODING_AVAILABLE_RESOLUTIONS`. |
|
||||
| `RECORDING_ENCODING_HEIGHT` | The `height` of that same entry. |
|
||||
| `RECORDING_ENCODING_FRAMERATE` | The `fps` of the profile selected by `RECORDING_ENCODING_DEFAULT_PROFILE` in `RECORDING_ENCODING_AVAILABLE_PROFILES`. |
|
||||
| `RECORDING_ENCODING_VIDEO_BITRATE_KBPS` | That profile's `kbps`. |
|
||||
|
||||
`RECORDING_ENCODING_AUDIO_BITRATE_KBPS` and `RECORDING_ENCODING_KEY_FRAME_INTERVAL_S`
|
||||
keep their names and meaning. The keyframe interval now defaults to `0` (unset,
|
||||
encoder's choice) instead of `4.0`.
|
||||
|
||||
#### If you never set `RECORDING_ENCODING_ENABLED=True`
|
||||
|
||||
The shipped defaults (`RECORDING_ENCODING_DEFAULT_PROFILE=full`,
|
||||
`RECORDING_ENCODING_DEFAULT_RESOLUTION=720p`) match LiveKit's built-in
|
||||
`H264_720P_30` preset: 1280×720, 30 fps, 3000 kbps H.264 MAIN, 128 kbps AAC.
|
||||
Video output is therefore unchanged.
|
||||
|
||||
Audio and keyframing may not be. These values are now sent explicitly as advanced
|
||||
`EncodingOptions` rather than relying on LiveKit's preset, so
|
||||
`RECORDING_ENCODING_AUDIO_BITRATE_KBPS` and `RECORDING_ENCODING_KEY_FRAME_INTERVAL_S`
|
||||
now apply to every recording. They previously applied only when
|
||||
`RECORDING_ENCODING_ENABLED` was `True`. **If you set either of them while the
|
||||
feature was disabled, they had no effect and now do**; check them before upgrading.
|
||||
|
||||
If you never set them, no action is required: 128 kbps AAC is what the preset used,
|
||||
and the keyframe interval now defaults to `0`, which leaves the field unset so the
|
||||
encoder keeps picking it as before. Set `RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=4.0`
|
||||
if you want fixed 4-second keyframes (the value the setting defaulted to while it
|
||||
was gated behind `RECORDING_ENCODING_ENABLED`).
|
||||
|
||||
To keep letting LiveKit pick the encoding instead, set either default to an empty
|
||||
value:
|
||||
|
||||
```
|
||||
RECORDING_ENCODING_DEFAULT_RESOLUTION=
|
||||
RECORDING_ENCODING_DEFAULT_PROFILE=
|
||||
```
|
||||
|
||||
#### If you had tuned `RECORDING_ENCODING_*` values
|
||||
|
||||
Translate your old values into a default resolution and a default profile. Declare your own resolution and/or profile. Both maps are read from the
|
||||
environment as a single-line Python/JSON dict literal (parsed with
|
||||
`ast.literal_eval`, so use double-quoted keys and no trailing commas, and do not
|
||||
add outer quotes in `.env`-style files):
|
||||
|
||||
```bash
|
||||
RECORDING_ENCODING_AVAILABLE_RESOLUTIONS={"540p": {"width": 960, "height": 540}, "720p": {"width": 1280, "height": 720}, "1080p": {"width": 1920, "height": 1080}}
|
||||
RECORDING_ENCODING_AVAILABLE_PROFILES={"my_old_profile": {"fps": 15, "kbps": {"540p": 350, "720p": 600, "1080p": 1100}}}
|
||||
RECORDING_ENCODING_DEFAULT_RESOLUTION=720p
|
||||
RECORDING_ENCODING_DEFAULT_PROFILE=my_old_profile
|
||||
```
|
||||
|
||||
Both maps are validated at startup and a malformed one raises a `ValueError`:
|
||||
|
||||
- every entry of `RECORDING_ENCODING_AVAILABLE_RESOLUTIONS` must declare `width` and
|
||||
`height`, and every entry of `RECORDING_ENCODING_AVAILABLE_PROFILES` an `fps` and a
|
||||
`kbps` map;
|
||||
- every profile must define a `kbps` entry for **exactly** the keys of
|
||||
`RECORDING_ENCODING_AVAILABLE_RESOLUTIONS`; overriding one of the two maps usually
|
||||
means overriding both;
|
||||
- `RECORDING_ENCODING_DEFAULT_RESOLUTION` and `RECORDING_ENCODING_DEFAULT_PROFILE`,
|
||||
when non-empty, must be keys of their respective map.
|
||||
|
||||
#### Breaking: custom worker services must accept `encoding_options`
|
||||
|
||||
Only concerns deployments pointing `RECORDING_WORKER_CLASSES` at their own worker
|
||||
class. The shipped `VideoCompositeEgressService` and `AudioCompositeEgressService`
|
||||
are already updated.
|
||||
|
||||
The `WorkerService` protocol's `start()` takes a third argument, and the mediator
|
||||
now always passes it as a keyword when the recording carries no per-recording encoding:
|
||||
|
||||
```python
|
||||
# before
|
||||
def start(self, room_id: str, recording_id: str) -> str: ...
|
||||
|
||||
# now
|
||||
def start(
|
||||
self,
|
||||
room_id: str,
|
||||
recording_id: str,
|
||||
encoding_options: Optional[Dict[str, Any]] = None,
|
||||
) -> str: ...
|
||||
```
|
||||
|
||||
#### Optional: per-recording encoding
|
||||
|
||||
`RECORDING_CUSTOM_ENCODING_ENABLED` (default `False`) toggles whether the
|
||||
start-recording API accepts an `encoding` object
|
||||
(`{"resolution": "720p", "profile": "talking_heads"}`, `profile` optional. It
|
||||
falls back to `RECORDING_ENCODING_DEFAULT_PROFILE`) that overrides the default for
|
||||
a single recording. It does not enable or disable the
|
||||
default encoding, which is built from the two `RECORDING_ENCODING_DEFAULT_*`
|
||||
settings either way. Leaving it at `False` preserves the previous behaviour, where
|
||||
every recording uses the server-side encoding: requests carrying
|
||||
`options.encoding` are rejected with a `400` before the recording is created, so
|
||||
nothing is persisted and no egress is started.
|
||||
|
||||
Before enabling it:
|
||||
|
||||
- clients can only pick keys you declared; there is no way to send a raw width or bitrate
|
||||
- as of this implementation, the frontend never sends `encoding`
|
||||
- `encoding` is accepted but ignored for `transcript` recordings, whose audio-only
|
||||
egress has no video encoding to configure.
|
||||
|
||||
See [docs/features/recording.md](docs/features/recording.md#tuning-recording-encoding)
|
||||
for the full setting reference, the shipped profile table and the tuning caveats.
|
||||
|
||||
## v1.30.0
|
||||
|
||||
### Removing S3 storage-event webhooks for recordings
|
||||
|
||||
Recordings were previously confirmed as saved by an S3 storage-event webhook posting to `/api/v1.0/recordings/storage-hook/`. That endpoint has been removed: recordings are now always finalized from LiveKit's own `egress_ended` webhook, which has been the default path since v1.22.0.
|
||||
|
||||
@@ -110,12 +110,6 @@ cd -
|
||||
# Update summary pyproject.toml
|
||||
update_python_version "summary"
|
||||
|
||||
# Run uv lock in summary
|
||||
print_info "Running uv lock in summary..."
|
||||
cd "src/summary"
|
||||
uv lock
|
||||
cd -
|
||||
|
||||
# Update agents pyproject.toml
|
||||
update_python_version "agents"
|
||||
|
||||
@@ -169,7 +163,6 @@ echo " - src/mail/package.json"
|
||||
echo " - src/backend/pyproject.toml"
|
||||
echo " - src/backend/uv.lock"
|
||||
echo " - src/summary/pyproject.toml"
|
||||
echo " - src/summary/uv.lock"
|
||||
echo " - src/agents/pyproject.toml"
|
||||
echo " - src/agents/uv.lock"
|
||||
echo " - CHANGELOG.md"
|
||||
|
||||
@@ -213,8 +213,6 @@ services:
|
||||
- "7880:7880"
|
||||
- "7881:7881"
|
||||
- "7882:7882/udp"
|
||||
- "3478:3478/udp"
|
||||
- "30000-30100:30000-30100/udp"
|
||||
volumes:
|
||||
- ./docker/livekit/config/livekit-server.yaml:/config.yaml
|
||||
depends_on:
|
||||
|
||||
@@ -8,16 +8,3 @@ webhook:
|
||||
api_key: devkey
|
||||
urls:
|
||||
- http://app-dev:8000/api/v1.0/rooms/webhooks-livekit/
|
||||
|
||||
turn:
|
||||
enabled: true
|
||||
domain: turn.127.0.0.1.nip.io
|
||||
udp_port: 3478
|
||||
tls_port: 0
|
||||
external_tls: false
|
||||
relay_range_start: 30000
|
||||
relay_range_end: 30100
|
||||
allow_restricted_peer_cidrs:
|
||||
- 192.168.0.0/16
|
||||
- 172.16.0.0/12
|
||||
|
||||
|
||||
+34
-41
@@ -93,13 +93,13 @@ sequenceDiagram
|
||||
| **RECORDING_WORKER_CLASSES** | Dict | `{ "screen_recording": "core.recording.worker.services.VideoCompositeEgressService", "transcript": "core.recording.worker.services.AudioCompositeEgressService" }` | Maps recording types to their worker service classes. |
|
||||
| **RECORDING_EXPIRATION_DAYS** | Integer | `None` | Number of days before recordings expire. Should match bucket lifecycle policy. Set to `None` for no expiration. |
|
||||
| **RECORDING_MAX_DURATION** | Integer | `None` | Maximum duration of a recording in milliseconds. Must be synced with the LiveKit Egress configuration. Set to None for unlimited duration. When the maximum duration is reached, the recording is automatically stopped and saved, and the user is prompted in the frontend with an alert message. |
|
||||
| **RECORDING_CUSTOM_ENCODING_ENABLED** | Boolean | `False` | Whether the start-recording API accepts a per-recording `encoding` object (resolution/profile) that overrides the default. When `False`, the API rejects per-recording `encoding`; when `True`, clients may pick from the available resolutions/profiles. The default encoding below is applied regardless of this flag. See [Tuning recording encoding](#tuning-recording-encoding). |
|
||||
| **RECORDING_ENCODING_AVAILABLE_RESOLUTIONS** | Dict | `{"540p": {"width": 960, "height": 540}, "720p": {"width": 1280, "height": 720}, "1080p": {"width": 1920, "height": 1080}}` | Maps a resolution name to its `{"width", "height"}` in pixels. Both the default encoding and the per-recording start-recording API pick from these keys. |
|
||||
| **RECORDING_ENCODING_AVAILABLE_PROFILES** | Dict | `{"full": {"fps": 30, "kbps": {…}}, …}` | Maps a profile name to `{"fps", "kbps": {resolution: video_bitrate_kbps}}`. Every profile must define a bitrate for each available resolution (validated at startup). |
|
||||
| **RECORDING_ENCODING_DEFAULT_RESOLUTION** | String | `"720p"` | Resolution used by the default encoding. When set, must be a key of `RECORDING_ENCODING_AVAILABLE_RESOLUTIONS`. Leave unset (together with, or instead of, the default profile) to disable the custom default encoding and fall back to LiveKit's built-in preset (a startup warning is emitted). |
|
||||
| **RECORDING_ENCODING_DEFAULT_PROFILE** | String | `"full"` | Profile used by the default encoding. When set, must be a key of `RECORDING_ENCODING_AVAILABLE_PROFILES`. Leave unset (together with, or instead of, the default resolution) to disable the custom default encoding and fall back to LiveKit's built-in preset (a startup warning is emitted). |
|
||||
| **RECORDING_ENCODING_AUDIO_BITRATE_KBPS** | Integer | `128` | AAC audio bitrate in kbps used in the default encoding. |
|
||||
| **RECORDING_ENCODING_KEY_FRAME_INTERVAL_S** | Float | `0.0` | Keyframe interval in seconds. Drives seek granularity in the recorded MP4 (a player can only seek to keyframe boundaries). Larger values give the encoder slightly more bits for non-keyframe content at a fixed bitrate. `0` leaves the field unset, letting the encoder pick; `4.0` is a standard VOD value. |
|
||||
| **RECORDING_ENCODING_ENABLED** | Boolean | `False` | When `False`, LiveKit Egress uses its built-in `H264_720P_30` preset. When `True`, the `RECORDING_ENCODING_*` values below are sent to LiveKit as advanced `EncodingOptions`. See [Tuning recording encoding](#tuning-recording-encoding). |
|
||||
| **RECORDING_ENCODING_WIDTH** | Integer | `1280` | Recording video width in pixels. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_HEIGHT** | Integer | `720` | Recording video height in pixels. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_FRAMERATE** | Integer | `30` | Recording video framerate (fps). Directly impacts egress worker CPU (roughly linear). Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_VIDEO_BITRATE_KBPS** | Integer | `3000` | H.264 MAIN video bitrate in kbps. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_AUDIO_BITRATE_KBPS** | Integer | `128` | AAC audio bitrate in kbps. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_KEY_FRAME_INTERVAL_S** | Float | `4.0` | Keyframe interval in seconds. Drives seek granularity in the recorded MP4 (a player can only seek to keyframe boundaries). Larger values give the encoder slightly more bits for non-keyframe content at a fixed bitrate. `4.0` is a standard VOD value. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
@@ -130,59 +130,52 @@ This allows you to verify which recordings are in progress, troubleshoot egress
|
||||
|
||||
## Tuning recording encoding
|
||||
|
||||
Every video recording is encoded from a default resolved from `RECORDING_ENCODING_DEFAULT_PROFILE` + `RECORDING_ENCODING_DEFAULT_RESOLUTION` and passed to LiveKit as advanced `EncodingOptions`. The shipped defaults (`full` profile) match LiveKit's built-in `H264_720P_30` preset. For a one-hour meeting that produces a file of roughly **1.4 GB**, which is often heavier than necessary for talking-head content and screen sharing; lowering the default profile/resolution shrinks it. If either default is left unset, no custom default encoding is built: a warning is logged at startup and LiveKit's built-in preset is used instead.
|
||||
By default, LiveKit Egress records with the built-in `H264_720P_30` preset: 1280×720 at 30 fps, 3000 kbps H.264 MAIN video and 128 kbps AAC audio. For a one-hour meeting this produces a file of roughly **1.4 GB**, which is often heavier than necessary for talking-head content and screen sharing.
|
||||
|
||||
Encoding is chosen from two maps: `RECORDING_ENCODING_AVAILABLE_RESOLUTIONS` (`resolution → {"width", "height"}`) and `RECORDING_ENCODING_AVAILABLE_PROFILES` (`profile → {"fps", "kbps": {resolution: video_bitrate_kbps}}`):
|
||||
|
||||
- **Default**: `RECORDING_ENCODING_DEFAULT_PROFILE` + `RECORDING_ENCODING_DEFAULT_RESOLUTION` set the encoding used by every recording that doesn't override it. Leave either unset to fall back to LiveKit's built-in preset (a startup warning is emitted).
|
||||
- **Per recording (opt-in)**: set `RECORDING_CUSTOM_ENCODING_ENABLED=True` to let clients override the default per recording. The start-recording API then accepts an `encoding` object selecting a `resolution` (required) and `profile` (optional): a resolution-only request keeps `RECORDING_ENCODING_DEFAULT_PROFILE` for fps and bitrate, so clients can only pick from pre-defined values. When `RECORDING_CUSTOM_ENCODING_ENABLED=False`, the API rejects any per-recording `encoding` and the default is used.
|
||||
|
||||
The resolved values are passed straight through LiveKit's `EncodingOptions.advanced` to the GStreamer pipeline (`x264enc` for video, `faac` for audio), so there are no hidden conversions — what the profile/resolution resolve to is what the encoder receives.
|
||||
The `RECORDING_ENCODING_*` settings let operators override this preset without modifying the source. Values are passed straight through LiveKit's `EncodingOptions.advanced` to the GStreamer pipeline (`x264enc` for video, `faac` for audio), so there are no hidden conversions — what you set is what the encoder receives.
|
||||
|
||||
### How values map to GStreamer
|
||||
|
||||
| Resolved value | GStreamer element | Property |
|
||||
| ----------------------------------------- | ----------------- | ---------------------------------- |
|
||||
| resolution `width` / `height` | capsfilter | `video/x-raw,width=W,height=H` |
|
||||
| profile `fps` | capsfilter | `framerate=F/1` |
|
||||
| profile `kbps[resolution]` | `x264enc` | `bitrate=kbps` (kilobits) |
|
||||
| `RECORDING_ENCODING_KEY_FRAME_INTERVAL_S` | `x264enc` | `key-int-max = interval × fps` |
|
||||
| `RECORDING_ENCODING_AUDIO_BITRATE_KBPS` | `faac` | `bitrate = kbps × 1000` (bits) |
|
||||
| Setting | GStreamer element | Property |
|
||||
| ------------------------------------- | ----------------- | ---------------------------------- |
|
||||
| `RECORDING_ENCODING_WIDTH/HEIGHT` | capsfilter | `video/x-raw,width=W,height=H` |
|
||||
| `RECORDING_ENCODING_FRAMERATE` | capsfilter | `framerate=F/1` |
|
||||
| `RECORDING_ENCODING_VIDEO_BITRATE_KBPS` | `x264enc` | `bitrate=kbps` (kilobits) |
|
||||
| `RECORDING_ENCODING_KEY_FRAME_INTERVAL_S` | `x264enc` | `key-int-max = interval × fps` |
|
||||
| `RECORDING_ENCODING_AUDIO_BITRATE_KBPS` | `faac` | `bitrate = kbps × 1000` (bits) |
|
||||
|
||||
The H.264 profile is fixed to MAIN and the x264 `speed-preset` to `veryfast` by LiveKit (real-time constraint) — lowering the framerate is therefore the main lever to save CPU, while lowering the bitrate is the main lever to shrink the output file.
|
||||
|
||||
### Built-in profiles
|
||||
### Reference profiles
|
||||
|
||||
The default `RECORDING_ENCODING_AVAILABLE_PROFILES` ship four profiles. Framerate is fixed per profile; video bitrate (kbps) scales with resolution so quality stays consistent across sizes. File size scales roughly with `framerate × bitrate`, and so does egress CPU cost.
|
||||
Rough 30-minute file-size estimates assume video + audio bitrate multiplied by duration. Actual sizes vary with content (static talking heads compress better than heavy screen motion). Egress CPU figures are indicative, measured on a single Ryzen laptop core saturated by the default preset (= 100 %); scaling is roughly linear with `framerate × bitrate` but the absolute numbers depend on the host hardware.
|
||||
|
||||
| Profile | FPS | 540p (kbps) | 720p (kbps) | 1080p (kbps) | Suitable for |
|
||||
| --------------- | --- | ----------- | ----------- | ------------ | -------------------------------------------------- |
|
||||
| `talking_heads` | 15 | 400 | 700 | 1200 | Talking-head dominant meetings + occasional slides |
|
||||
| `text` | 15 | 600 | 1000 | 1800 | Frequent dense screen sharing (decks, IDE, docs) |
|
||||
| `mixed` | 20 | 900 | 1500 | 2500 | Mixed content, moderate motion |
|
||||
| `full` | 30 | 2000 | 3000 | 4500 | Highest fidelity; closest to the LiveKit default preset |
|
||||
| Profile | Resolution | FPS | Video (kbps) | Audio (kbps) | Keyframe (s) | ~ size / 30 min | Egress CPU (vs. default) | Suitable for |
|
||||
| ---------------------- | ---------- | --- | ------------ | ------------ | ------------ | --------------- | ------------------------ | --------------------------------------------------- |
|
||||
| Default (preset) | 1280×720 | 30 | 3000 | 128 | 4 | **~690 MB** | 100 % | Unchanged LiveKit behaviour |
|
||||
| Balanced | 1280×720 | 20 | 1000 | 96 | 4 | ~240 MB | ~67 % | Mixed content, moderate motion |
|
||||
| **Low CPU / small file** | 1280×720 | 15 | 600 | 64 | 4 | **~150 MB** | ~50 % | Talking-head dominant meetings + occasional slides ★ |
|
||||
| Slide-heavy | 1280×720 | 15 | 900 | 64 | 4 | ~210 MB | ~55 % | Frequent dense screen sharing (decks, IDE, docs) |
|
||||
| Minimum CPU | 960×540 | 15 | 500 | 64 | 4 | ~125 MB | ~30 % | Voice-first meetings, readable text not required |
|
||||
| Audio-heavy fallback | 1280×720 | 10 | 400 | 96 | 4 | ~110 MB | ~35 % | Long webinars, low motion |
|
||||
|
||||
To pick a profile per recording (requires `RECORDING_CUSTOM_ENCODING_ENABLED=True`), the client sends it in the start-recording request:
|
||||
★ Recommended starting point for typical LaSuite Meet usage.
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "screen_recording",
|
||||
"options": {"encoding": {"resolution": "720p", "profile": "talking_heads"}}
|
||||
}
|
||||
```
|
||||
|
||||
To change the default encoding applied to every recording:
|
||||
Environment variables for the **Low CPU / small file** profile:
|
||||
|
||||
```bash
|
||||
RECORDING_ENCODING_DEFAULT_RESOLUTION=720p
|
||||
RECORDING_ENCODING_DEFAULT_PROFILE=talking_heads
|
||||
RECORDING_ENCODING_ENABLED=True
|
||||
RECORDING_ENCODING_WIDTH=1280
|
||||
RECORDING_ENCODING_HEIGHT=720
|
||||
RECORDING_ENCODING_FRAMERATE=15
|
||||
RECORDING_ENCODING_VIDEO_BITRATE_KBPS=600
|
||||
RECORDING_ENCODING_AUDIO_BITRATE_KBPS=64
|
||||
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=4.0
|
||||
```
|
||||
|
||||
### Caveats
|
||||
|
||||
- **Screen-share readability — think bits/frame, not bitrate**: at 720p, text legibility starts to break down below ~40 kbits/frame (= `bitrate ÷ framerate`). The `talking_heads` profile (700 kbps × 15 fps) sits just above that threshold, comfortable for talking heads with occasional slide sharing. The same bitrate at 30 fps would only deliver ~23 kbits/frame and visibly blur dense slides — which is why **lowering framerate is a more screen-share-friendly lever than lowering bitrate**. For deck-heavy or IDE-share meetings, prefer the **`text`** profile (1000 kbps × 15 fps ≈ 67 kbits/frame).
|
||||
- **Screen-share readability — think bits/frame, not bitrate**: at 720p, text legibility starts to break down below ~40 kbits/frame (= `bitrate ÷ framerate`). The recommended preset (600 kbps × 15 fps) sits at exactly that threshold, comfortable for talking heads with occasional slide sharing. The same 600 kbps at 30 fps would only deliver 20 kbits/frame and visibly blur dense slides — which is why **lowering framerate is a more screen-share-friendly lever than lowering bitrate**. For deck-heavy or IDE-share meetings, prefer the **Slide-heavy** profile (900 kbps × 15 fps ≈ 60 kbits/frame).
|
||||
- **Motion handling**: the `veryfast` x264 preset is set by LiveKit and cannot be overridden here. Low-bitrate settings will therefore show more artefacts on fast motion than an offline re-encode with a slower preset would. This is the other reason FPS reduction is the safer tuning lever for meeting recordings.
|
||||
- **Audio**: AAC at 64 kbps stereo is transparent for voice but starts to compress music noticeably. Keep 128 kbps if you expect music playback in meetings.
|
||||
- **Codec choice**: H.264 MAIN is hardcoded on purpose. Switching to HEVC or VP9 would increase egress CPU cost 2×–5×, defeating the goal of this tuning.
|
||||
|
||||
@@ -3,14 +3,14 @@ Gitlint extra rule to validate that the message title is of the form
|
||||
"<gitmoji>(<scope>) <subject>"
|
||||
"""
|
||||
|
||||
import json
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import urllib.request
|
||||
|
||||
import requests
|
||||
|
||||
from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
|
||||
|
||||
GITMOJIS_URL = "https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json"
|
||||
|
||||
|
||||
class GitmojiTitle(LineRule):
|
||||
"""
|
||||
@@ -28,9 +28,10 @@ class GitmojiTitle(LineRule):
|
||||
Download the list possible gitmojis from the project's github repository and check that
|
||||
title contains one of them.
|
||||
"""
|
||||
with urllib.request.urlopen(GITMOJIS_URL, timeout=10) as response:
|
||||
gitmojis = json.load(response)["gitmojis"]
|
||||
emojis = [re.escape(item["emoji"]) for item in gitmojis]
|
||||
gitmojis = requests.get(
|
||||
"https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json"
|
||||
).json()["gitmojis"]
|
||||
emojis = [item["emoji"] for item in gitmojis]
|
||||
pattern = r"^({:s})\(.*\)\s[a-z].*$".format("|".join(emojis))
|
||||
if not re.search(pattern, title):
|
||||
violation_msg = 'Title does not match regex "<gitmoji>(<scope>) <subject>"'
|
||||
|
||||
Generated
+5
-8
@@ -9,8 +9,8 @@
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "3.50.0",
|
||||
"i18next": "26.3.6",
|
||||
"core-js": "3.49.0",
|
||||
"i18next": "^26.3.6",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
},
|
||||
@@ -6863,14 +6863,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.50.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz",
|
||||
"integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
|
||||
"version": "3.49.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
|
||||
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.50.0",
|
||||
"core-js": "3.49.0",
|
||||
"i18next": "26.3.6",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
|
||||
Generated
+168
-189
@@ -524,11 +524,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.5.0"
|
||||
version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -669,43 +672,43 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "grpcio"
|
||||
version = "1.83.1"
|
||||
version = "1.83.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e2/b1/46539f5050d7c316a13396d185451f95084a74ddc68b12d818595bef0377/grpcio-1.83.1.tar.gz", hash = "sha256:9cee6fcbf2eb57c4b49451787bfa87be8efc1ca02a0b327dd4b54d44502e362b", size = 13445033, upload-time = "2026-08-28T07:09:11.464Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24", size = 13428824, upload-time = "2026-07-23T15:20:37.759Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/9e/a3ba13e08bbee5bf6e57597dfe4823961fd7e94c0b8afe3a4bb7dca639f3/grpcio-1.83.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:5acd14c6ddf047de62cbf8745b11103ea91abbf57d1b8edd5395ccd9fcd13abb", size = 6303170, upload-time = "2026-08-28T07:08:08.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/ae/65ce56a2527faa17d02cba4c2231c74047ad898be339486ba87f093bfb66/grpcio-1.83.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:16138031a47b771860a16a975b53087f4fd5bbdbb2c03a188c5d90ad65d2bdae", size = 12165806, upload-time = "2026-08-28T07:08:10.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/91/40432480088a2243d360864de072ed5b78c4ebbaabd29c28918f1e1b1454/grpcio-1.83.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5ccc26715fd4defca5e129e280dd883b1737b65045ec50ffe22ce42104089519", size = 6872490, upload-time = "2026-08-28T07:08:12.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/62/3da2300c8c79fd20a78a8a4bb6251e5068d9af33bc8fd389b98fec35e8a3/grpcio-1.83.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b74f2a1d9ab1dfa3e263ef33d581613679b78d0884babf11671af26e45570ead", size = 7618367, upload-time = "2026-08-28T07:08:14.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/19/9fc702e31a631262d7a752fa699f6022821e707fefc8bff49b1550a57729/grpcio-1.83.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72578aa07a4008f17521ef52debcc3acfd1e2c5426243bc3ffb56a38bfe610b7", size = 7040936, upload-time = "2026-08-28T07:08:15.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/56/95933cc44cba2429765fa065c951dd529e5771b119d9d2481b4646f1d6a5/grpcio-1.83.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c12e1fc59c6dc26d10d9144453ddc6cbfe4cd4c31e874ed2d0132f88e685eb8b", size = 7573096, upload-time = "2026-08-28T07:08:17.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/80/af63359da06b016de48cb111f144703a10043850dafa43ae0a038907b9e8/grpcio-1.83.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4910b62f7d12197160bfb7de06d876d64dd12d43483e8292f98f49ca09b628d9", size = 8609442, upload-time = "2026-08-28T07:08:19.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/fa/f0586c56bdfb8a7a2adda01e0ac2413447cde3141ab09411a5d5afdcffd3/grpcio-1.83.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e703effe3ae779925c82ac24fdb82cf4105e1096810151ed9501c5f34546b9c", size = 7984321, upload-time = "2026-08-28T07:08:22.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8a/14ec05669f9eb295801e26c2ea8c561a1b786b0e3557c2c22131165ab010/grpcio-1.83.1-cp312-cp312-win32.whl", hash = "sha256:a2aea8bd6e0a34f12cbaddb7bb70bec836818789fa5c7ab7572c6b745396a2d4", size = 4395604, upload-time = "2026-08-28T07:08:24.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/37/8c2f7cc16089e36a3fbacaacc7a3d043912aa0d2dfae5556f6450414ea6e/grpcio-1.83.1-cp312-cp312-win_amd64.whl", hash = "sha256:583bf2e8255040a4a312f9572dfe62a05271437b149550e1a536d5c47d2d1e8a", size = 5161512, upload-time = "2026-08-28T07:08:25.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fd/d1fc58933bf88c9209f89dc570c810f1aa57cb04b3459cf2b26f61e32112/grpcio-1.83.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:8d228e253b77865efcbdd7b5894ca882c9e0ea98c02b7d20582e61ded8dfd4b5", size = 6305628, upload-time = "2026-08-28T07:08:27.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/49/0b40bae059c619505c9b751cee6caa208e4904e290aaefa1728c4c2c67a5/grpcio-1.83.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0468b627f2987c9a77f7580030207cbd85457ffe52998beff4f0b5c38c58a72c", size = 12156839, upload-time = "2026-08-28T07:08:30.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/4b/e8c0d635da0ee5ddd9950c8d540f5dcdd0ef1854a382cc55496a487a8d31/grpcio-1.83.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6a282e81530cead60bbd752cc04950a57f224379e9821495d6a35bd5ce9b1f4", size = 6877036, upload-time = "2026-08-28T07:08:32.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/d4/760a33f339a7dd3d5f4b3e0e9bec5472d95592a80f887b2e9dab4e41cfbc/grpcio-1.83.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:947d945f52e8ecf3cafd2bb7113502a16ccfda3e12c854443094de32d83ad432", size = 7624404, upload-time = "2026-08-28T07:08:34.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/ec/bd798654b06fb42a92b57d1dc1b530084fa89ed442806fcd0a833a36f9b3/grpcio-1.83.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:55656318d5dd387077396dffb929171ca3966e24bfead9a6c5dba9f889062cb4", size = 7042942, upload-time = "2026-08-28T07:08:36.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/b0/c00f86614566dd0961825cf0f43d4f96a74371d9d95f952bcbc4b86d9a27/grpcio-1.83.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9daf5acf4fc9d5f5627229969c2580a91e511779d76e4ccdeb9f4770f05d8bc2", size = 7576937, upload-time = "2026-08-28T07:08:38.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/38/85eff43a5c89dc666a252b5c9f8e9ab03f89e11c95b6263d2933f08fdbe7/grpcio-1.83.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7b94174cbca93316888f805efbeb08f1c020f7b7493d2d50cc4f6b64ebb7e8bd", size = 8608391, upload-time = "2026-08-28T07:08:40.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/4e/82835483e2f812494be865e7965c0d626cb9e71ab0d83a420d75aea4ad67/grpcio-1.83.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65c5a7210911ffe0f67b1cdc5308f9854b6d1f1b345e3e49ab7cac1ba50fa346", size = 7980060, upload-time = "2026-08-28T07:08:42.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/68a98bef733fef704fbcfb3957c8dba67e3e38ca7a7fea851195bc97c648/grpcio-1.83.1-cp313-cp313-win32.whl", hash = "sha256:179368d9361854616ce6f397d4716e07480129652752fcbcfc5a7260455ad6f2", size = 4395226, upload-time = "2026-08-28T07:08:44.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a0/df4de3b51d37ac8fb0320bb9668381ce2bd3b7aa990880bfc56a8a26f665/grpcio-1.83.1-cp313-cp313-win_amd64.whl", hash = "sha256:2e57af456385491a76e13c4aada8c8f43a8e47051e06ea97a9dbe2a49654e6db", size = 5160273, upload-time = "2026-08-28T07:08:46.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/9c/484d981d8b90c4e6abf3030bd2ed747e84d1eb192b3ec9cbb41e0b73e4bf/grpcio-1.83.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b3c87ca908296bf125f841d3e1a2225a2b39aaa8ed7a57e7ccde465ee519bab", size = 6306089, upload-time = "2026-08-28T07:08:48.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/01/0afec1c92e4f292f74a44ecf75eabbf40903125b8c4df103c9868d6338da/grpcio-1.83.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c0f3f20c90e72a171917ae65706500b096a1c3eb5f162c3ce702a2e25635f132", size = 12170381, upload-time = "2026-08-28T07:08:50.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/5a/e9a2383804433a0a61d6d93777ad321c7f36ac1cfdaa4c6d1a7c9ac846b7/grpcio-1.83.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81bbf35a46bf8cad2dfbb2eccc19c711befb58b288acb534bbcd0d74283202a6", size = 6883286, upload-time = "2026-08-28T07:08:53.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/e7/f8ca8f76994e14c70b9a0052e82f10de497a23db450c36379c9716ebfc4d/grpcio-1.83.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:215cec07d11176507387bda4bf2751816e880f9bff8dc1ca524bfbb8ed8f2fad", size = 7624293, upload-time = "2026-08-28T07:08:55.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/7a/4b672814b0cd0fe63bdd735379d88b165759f3144ab023ad8ec5fc4d53ac/grpcio-1.83.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:abce7d43ec29cd39230fa8339de1a07643b55adc412a454850fbd875349950ff", size = 7044346, upload-time = "2026-08-28T07:08:57.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b8/d89fe60e4239ad51be333dd9cc703741d449a35064e51f8a0b5bfa755432/grpcio-1.83.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e256f95a40e3b0183a98556fb7164d24b97eeb353123ccabfcba94712b35ee2a", size = 7584187, upload-time = "2026-08-28T07:08:59.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b2/b290d7402633d9166e4dd47e6f5f74a24ce10a8340b84455896ebc349f85/grpcio-1.83.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2110059146fb0ea216e1ffddb29377b5cc2fd412a5b0a92e102616bd5edf18c2", size = 8608730, upload-time = "2026-08-28T07:09:02.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/44/fa89e44d1b5cf5b9fa71b2fd7abf506f182fd43917231a92fbf1ea326b02/grpcio-1.83.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20d944d967843f8183f9f23d5916388362e5f8eeeae855bbe4354d906dc9f31b", size = 7983283, upload-time = "2026-08-28T07:09:05.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/b8/9db73ed1f35ffa76124ac574bf296d06a359798dfd6b50d382f2b8a060a1/grpcio-1.83.1-cp314-cp314-win32.whl", hash = "sha256:623c87c6d4a1cb30d82c4e896f95477050f2e01b4a1f8cf91ff2b1abdf89c457", size = 4474327, upload-time = "2026-08-28T07:09:07.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/22/fc9a622d885a7a37ff972a12faaef443d74e47407181da70d0ab62ab41f0/grpcio-1.83.1-cp314-cp314-win_amd64.whl", hash = "sha256:47e6934ad38779271e2e7cc5f78a63a407cf3d98114c65c1fdbcd3f5a716f29b", size = 5302032, upload-time = "2026-08-28T07:09:09.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/2b/51e32514a4e9b715375c99721aadff0f24164cc2049b8269eda4de82a814/grpcio-1.83.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930", size = 6303167, upload-time = "2026-07-23T15:19:33.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/33/b5b50fc2c6fbe350e04814047bb2d409feec7b36ef8b170254c050e06bc0/grpcio-1.83.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da", size = 12160538, upload-time = "2026-07-23T15:19:35.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5f/734e72e7b9f79bcf0b2c270b8d3bca0e4ebb97a27a50d06240b145f6d41e/grpcio-1.83.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16", size = 6869310, upload-time = "2026-07-23T15:19:38.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/17/a1735f215b2a5cd43c38b79eac072ad197e61be9829905b6b29550abd0db/grpcio-1.83.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf", size = 7613472, upload-time = "2026-07-23T15:19:40.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/78/c9e81f806ac704b6b145cb01628db398985b1f8dfdc10e23b55fb0902b3d/grpcio-1.83.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd", size = 7040616, upload-time = "2026-07-23T15:19:42.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/ba/94cd5af859876049d340480acbb61a959096c84b567f215534faa78d0424/grpcio-1.83.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c", size = 7570491, upload-time = "2026-07-23T15:19:44.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/15/108d30d5a5c964312ae8b9cb0e8cc5b3c1cc68d8f757cca52b3565534d26/grpcio-1.83.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5", size = 8605036, upload-time = "2026-07-23T15:19:46.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/23/3828ae13c3db8233d123ad612747665817b952d8a954f32390230b582336/grpcio-1.83.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5", size = 7981587, upload-time = "2026-07-23T15:19:48.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/5b/77af31228f55f55a2a5112bb0077ad0a1c4d23dbb0c2853a62475bbdcc14/grpcio-1.83.0-cp312-cp312-win32.whl", hash = "sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc", size = 4394004, upload-time = "2026-07-23T15:19:50.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/da/f706e39550e7a3732ce2b9c5926107a93d74a802775b19b642a6df27dc96/grpcio-1.83.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df", size = 5158525, upload-time = "2026-07-23T15:19:52.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/eb/135daaa713f32d33b8f99b4153b3f8dc3b2a124996ac15581bf9ebdad3c3/grpcio-1.83.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0", size = 6304480, upload-time = "2026-07-23T15:19:53.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/a1/121806ce69f23138dabe06aa595b0e5f1ae051a37e4c1954eed7d692c800/grpcio-1.83.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1", size = 12154419, upload-time = "2026-07-23T15:19:56.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/e8/d0389e09cd6b4c4d3089b92967ae4e3ffd64795bd349bf2f85cd6656d3da/grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867", size = 6873200, upload-time = "2026-07-23T15:19:58.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/51/f464c1d211fa50d5adbabe1b2e519948d99c13757052bfc9ea7afa28e284/grpcio-1.83.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881", size = 7618811, upload-time = "2026-07-23T15:20:00.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/c0/539fe0832f2dd6500a28f5263071623fb34e8d4867aec632ccf81bd21156/grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f", size = 7042310, upload-time = "2026-07-23T15:20:02.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/ca/ccf617d37ffa72567fa8e005ec7090c99da922799be2fb9847c8b21ca18c/grpcio-1.83.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61", size = 7575412, upload-time = "2026-07-23T15:20:04.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/b9/fd8d5245f823a8e0fd35d90e20ea3aa4acd47f8d5318fa8df307df52dec6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45", size = 8604248, upload-time = "2026-07-23T15:20:06.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1e/f37632fc11db72dfa4bba86c3a43e54358e53030df111ecae5e91a733ad6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c", size = 7977458, upload-time = "2026-07-23T15:20:09.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/b6/d70b69ae5c0cfc341b9ba474980e4ed99cbf05c0e4a14e9eee8cb73db0a5/grpcio-1.83.0-cp313-cp313-win32.whl", hash = "sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf", size = 4393993, upload-time = "2026-07-23T15:20:11.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/13/45d4cccb555cf4c476226979bf3d2fd0b0254216f7564c3a053e35117efc/grpcio-1.83.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735", size = 5159650, upload-time = "2026-07-23T15:20:12.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/60/f2cca8147ea213d3e43ae9158d03ad04e020fdf32ff027253e1fe93f921d/grpcio-1.83.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa", size = 6305607, upload-time = "2026-07-23T15:20:15.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/ab/d3874931d123a95e83a3ebf8aa04537988fb62425cedb8bf3cefc5ad41b2/grpcio-1.83.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c", size = 12166617, upload-time = "2026-07-23T15:20:17.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ff/6f18f9426b69306f4e00a9add3b0ee2748da8aad53836ef80cab0d62d04f/grpcio-1.83.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b", size = 6880213, upload-time = "2026-07-23T15:20:19.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/21/706d1147c6b93b98f179240c13991fbcc56880eba0c868abb1ad40d8a0a6/grpcio-1.83.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c", size = 7618335, upload-time = "2026-07-23T15:20:22.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/04/1a8443c889115ec9e213a213e86bc93a71ee9088027e5befa09aaa0edd9d/grpcio-1.83.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df", size = 7043416, upload-time = "2026-07-23T15:20:24.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c6/94e0fee5b12bc1da1370185b680988db6f739d19b42d9959db01a7ea50bf/grpcio-1.83.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b", size = 7583253, upload-time = "2026-07-23T15:20:26.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/97/de1ccb671fb85575bc5192faedf9ecdbdf5b390d2e6584dcf552bcbd370e/grpcio-1.83.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404", size = 8605102, upload-time = "2026-07-23T15:20:28.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0f/0e0ec749a7034ffcbaa050e39779872950ead90c22e7e0116be3f28b2b46/grpcio-1.83.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af", size = 7979826, upload-time = "2026-07-23T15:20:31.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/fa/c3fda157287f64bc65acee6c5aa90c41acf9e0d3a8e69a265eecff6d00a1/grpcio-1.83.0-cp314-cp314-win32.whl", hash = "sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33", size = 4471765, upload-time = "2026-07-23T15:20:33.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/00/b1b26431c9d54eee11724fd6e5585473a2ed47fbc1fb95e5204906a642ce/grpcio-1.83.0-cp314-cp314-win_amd64.whl", hash = "sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9", size = 5298932, upload-time = "2026-07-23T15:20:35.48Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -950,7 +953,7 @@ codecs = [
|
||||
|
||||
[[package]]
|
||||
name = "livekit-api"
|
||||
version = "1.2.1"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -959,9 +962,9 @@ dependencies = [
|
||||
{ name = "pyjwt" },
|
||||
{ name = "types-protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/0d/ff9502faa5e9785563e9648313a030fab65ed7f6ff8cf52d7bc6c9e01c97/livekit_api-1.2.1.tar.gz", hash = "sha256:eee78dba493b736bc8422660debc7cf89340f1b80c63890a36a18d99c938045c", size = 21780, upload-time = "2026-08-27T14:06:57.419Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/19/36ff6712ec638a4b7dad4d8f03795952e401dc31db0b04cddec7892650da/livekit_api-1.2.0.tar.gz", hash = "sha256:a89817b3bca9584873786ff07209839308217537a42f95ecb2609aafaa109ddc", size = 20778, upload-time = "2026-07-11T23:20:54.781Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/c8/bc29cf16427fc446aa4487c552503cef42eebfcb920902bb54b7180e40d3/livekit_api-1.2.1-py3-none-any.whl", hash = "sha256:aa15b0a194c9e8167d4261bc61381682df23f98bc6d77760fcded93d2ce4b4ca", size = 27586, upload-time = "2026-08-27T14:06:56.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e7/8926f16d4bc1b2e0ae46d4a507321bb899396d263a757f1adaabcd3b3867/livekit_api-1.2.0-py3-none-any.whl", hash = "sha256:307f8e5cfb0358c3ca091814ab768af55896022151bcd7f951954ccefa036a24", size = 26499, upload-time = "2026-07-11T23:20:53.736Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1056,15 +1059,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "livekit-protocol"
|
||||
version = "1.1.26"
|
||||
version = "1.1.24"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
{ name = "types-protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/49/733f3655717bd1bdccfbb4ab958ac7f51d7e17d37e61b98de2f6b2e0c107/livekit_protocol-1.1.26.tar.gz", hash = "sha256:de8d291abb2efd026fbe23394ee4c05358195f09102d3c13335978ba04d7bdb8", size = 123833, upload-time = "2026-08-28T17:15:50.225Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/dc/65c106d689916e78e47d79d4f890f6e83b680442c67ed4909b44425084ea/livekit_protocol-1.1.24.tar.gz", hash = "sha256:b0a5699d3a4c4e42c3d37416dc3ed3c817c527c317c4cf3351d0bbe52887b9d4", size = 122624, upload-time = "2026-08-20T22:07:24.06Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/a9/b9a9a04b921b1aaef9312c3a16523cd4ddb508dcc4be18ca0e300b750b7e/livekit_protocol-1.1.26-py3-none-any.whl", hash = "sha256:37db1272487518df2ce3e13624a5217d1e158c6832fe01166bb3fb1875a8ddd9", size = 150535, upload-time = "2026-08-28T17:15:48.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/1f/e06cbbaea37bbe12cab1d169767f3fff9b385b019e6bdcd02fb9c94df63d/livekit_protocol-1.1.24-py3-none-any.whl", hash = "sha256:794463c4ed209fc884194470595052de652477d92b10d81bb5b0f9dcf53050f9", size = 149448, upload-time = "2026-08-20T22:07:22.699Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1106,76 +1109,54 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "msgpack"
|
||||
version = "1.2.2"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/44/ea2100ec54d30c46ee9dba10a3bfb79b655e96c6df237238a3234c75869b/msgpack-1.2.2.tar.gz", hash = "sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a", size = 187025, upload-time = "2026-08-27T10:03:47.793Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/31/78/90c15bebb1a72667349ca62d4507e9d9369e7f8f76b95f490b823d3622e5/msgpack-1.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc", size = 84275, upload-time = "2026-08-27T10:02:07.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/88/c2b6d8e81571da87aa232c0e34a3f3a0e618e6235892065ec82d1d81fc7a/msgpack-1.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3", size = 83970, upload-time = "2026-08-27T10:02:08.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/c0/d3ede9f5d16acb4c05a9281859f1e99ef9f877a928eb78454c37f70db001/msgpack-1.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba", size = 409401, upload-time = "2026-08-27T10:02:09.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/f0/29f591bea185616cf417645ac03bd3ad9b317483ad8572160e325f7fe777/msgpack-1.2.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923", size = 420619, upload-time = "2026-08-27T10:02:11.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/8e/c70c8c9180c5ddf4440eb8658ebead98e22e7686fbf84f6b165031430750/msgpack-1.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f", size = 379747, upload-time = "2026-08-27T10:02:13.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/9a/f10ce11fa62700c9ab87a22e65b9ca272f7f673ddd31aeb2de6ae272ad35/msgpack-1.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367", size = 398944, upload-time = "2026-08-27T10:02:15.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/fe/d7be978456ff8552e69a8e270d882e7530e01513c096b293d83df03753ea/msgpack-1.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc", size = 373979, upload-time = "2026-08-27T10:02:16.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/af/91b0d8d3fb3063e259daee3ea8515cea6282f68f4b0e5f0b6fea25762c6e/msgpack-1.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3", size = 417781, upload-time = "2026-08-27T10:02:18.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/3c/ce8e9efe1fd9e95c78b3705e4300ba7feba3dc6c00fb76259895db155518/msgpack-1.2.2-cp312-cp312-win32.whl", hash = "sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4", size = 65267, upload-time = "2026-08-27T10:02:19.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/98/a33b8b4af14e3476bb0da1b8c36ef7a0f28dcf95db1c5e68ff88cb89d591/msgpack-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3", size = 72275, upload-time = "2026-08-27T10:02:21.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/5e/2f323a33a6aba5bd4b2d8b430e4fab21d92cd91c093b49ee287bc166ee54/msgpack-1.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640", size = 65488, upload-time = "2026-08-27T10:02:22.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/eb/42f31c5a48811787ff59a9869721f70a49654d65ab6c455f4463c39b044e/msgpack-1.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533", size = 83911, upload-time = "2026-08-27T10:02:24.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/54/10c6c16ddba8a5112e3680176b838e3694e4aad7284f9daa6d6d70d98817/msgpack-1.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34", size = 83734, upload-time = "2026-08-27T10:02:25.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/75/35823e4419df8792191b2a17ae3fe71b41d02c162b2c491c94d1a87f0caa/msgpack-1.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc", size = 405635, upload-time = "2026-08-27T10:02:27.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/d3/6592e4064619b04f2dd0054c5fa13e37e3d55eb26044483d871fadb2f46b/msgpack-1.2.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9", size = 417332, upload-time = "2026-08-27T10:02:28.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a1/b21c6818a545e9a4a976ac954a5c250eecde9a02e0ec82f415473dab1324/msgpack-1.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b", size = 374378, upload-time = "2026-08-27T10:02:30.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/8b/7ada15c7b64151d6dbb562d1b091520efb2c37acf2403b1d4ae13797b27d/msgpack-1.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b", size = 395809, upload-time = "2026-08-27T10:02:32.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f7/96283e50f7020df4dfeacc55612b7a210c8cdf0dda48bc262f1f9b3e4c49/msgpack-1.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5", size = 373495, upload-time = "2026-08-27T10:02:33.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/fe/1548dede9d9ca482f2d424a2e110a9705d4e02627a16b8bc8d10ce0208a2/msgpack-1.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab", size = 414360, upload-time = "2026-08-27T10:02:35.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/9d/4419b8f86c219174b1fb8bbd7faaf84a548935f7b1916d028401b9433417/msgpack-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd", size = 65196, upload-time = "2026-08-27T10:02:37.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f8/593f5caf0dacab41cde1564c5f0419e61af55ec9628006205e8fd5eb5e03/msgpack-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f", size = 72203, upload-time = "2026-08-27T10:02:38.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/9e/c6ef92046b4a2bbb9d3aa0cb581cbf4a4051afccf6e5fb301a1bd3086f39/msgpack-1.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896", size = 65435, upload-time = "2026-08-27T10:02:39.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/50/3e92c403346652cabd08cb8faceef847bae917ea3b3c81b64a5b6d09ed41/msgpack-1.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04", size = 84315, upload-time = "2026-08-27T10:02:41.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/dc/8efe6dd96a12ab043930cb4cffb40b6e7f061491d6ec7a3d2b75ef1fda42/msgpack-1.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77", size = 84634, upload-time = "2026-08-27T10:02:42.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/89/996573095bf7b038c04dd65ddbc4f1a4d381b0f7a44ff9186f3c7b8325c2/msgpack-1.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe", size = 404194, upload-time = "2026-08-27T10:02:44.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/4e/46f5a5d949dbd054dab60cb15aac7ac6ae6774c134532893414689bf2f53/msgpack-1.2.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f", size = 412343, upload-time = "2026-08-27T10:02:45.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e8/739a94197358a313307e6e9e7d8d22ef66add39222de911a44161aa96920/msgpack-1.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea", size = 372620, upload-time = "2026-08-27T10:02:47.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d4/09b92e1fcdccea9466bfae45455367ac52362ae445d96a602e51b7a8df73/msgpack-1.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b", size = 394603, upload-time = "2026-08-27T10:02:49.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/db/d11bd6f258a60703dcdc7a3772818ad0c2f602ee4c2acfb24088c6c3ebc3/msgpack-1.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5", size = 372666, upload-time = "2026-08-27T10:02:50.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/cf/fbbbac0c6e5fbb9d51abc23e3b5fe8620f5c01e0588797cf664a623bb9e1/msgpack-1.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54", size = 410889, upload-time = "2026-08-27T10:02:52.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/60/8366558da954095e04e7fbc351f9387d87a682feaee9a235ceda966f794b/msgpack-1.2.2-cp314-cp314-win32.whl", hash = "sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248", size = 66774, upload-time = "2026-08-27T10:02:53.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/3d/1ce873c8057c65e4fbb076ffe1c99c9ae39d90a00a2540d7b06c652a292f/msgpack-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc", size = 73424, upload-time = "2026-08-27T10:02:55.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/55/e36f2a33e38657f33850d74e0bf256838a0d45802c298cc501a32bffcc08/msgpack-1.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8", size = 67657, upload-time = "2026-08-27T10:02:56.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/58/7e764b957bae80ae281a9cb28761068c8bae8d5c6ac0873e43cc69d176c7/msgpack-1.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650", size = 86594, upload-time = "2026-08-27T10:02:57.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/f0/250f5985b6ee533e60d357571a808aaae03c54118294dc3db7158e27feb1/msgpack-1.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c", size = 87374, upload-time = "2026-08-27T10:02:59.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/2c/126ec8f187877c5f688631c543d1d3a3d75b2e66b83fb9de3ed7c13a39b6/msgpack-1.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3", size = 428157, upload-time = "2026-08-27T10:03:00.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/21/d2d81d50aaedb14147d01f22094185794db3ad8a8791b60afacba0627c89/msgpack-1.2.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3", size = 426669, upload-time = "2026-08-27T10:03:02.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/fc/f7d484ee5b572719608e7ffad569bea22ff11309a96ca2fae85eec94226b/msgpack-1.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22", size = 380625, upload-time = "2026-08-27T10:03:04.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/c4/b924cbd5516676f4e612329f18602a833bd055ffbe27f808eeba0f01bfea/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839", size = 411328, upload-time = "2026-08-27T10:03:05.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/9d/0c1d9683a951a80f270c3b7dac1022c18b9307617344dd44d904135d5e12/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929", size = 377892, upload-time = "2026-08-27T10:03:07.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/bb/bf22338cdd22e0b40c8f28468cea5f3d9c320244c095d8303364bc012c41/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7", size = 419426, upload-time = "2026-08-27T10:03:09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/42/6d02c19a01abd8d7ce817c321d2ee6af1a8e24d584dca619d1b6576a83bf/msgpack-1.2.2-cp314-cp314t-win32.whl", hash = "sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e", size = 71810, upload-time = "2026-08-27T10:03:10.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/df/fda3a204415dab0a8c0db5461ef7205416ea52bd8581c5cafd361be07f3b/msgpack-1.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36", size = 78919, upload-time = "2026-08-27T10:03:12.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d4/4b4b0ef25a86deca91feaf7252ca885ba4f2ada40461379120122a04fe96/msgpack-1.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f", size = 71925, upload-time = "2026-08-27T10:03:13.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/92/4b44bc8f3243ef8cf9cb5368c17a299d45b9df858f6dfdd98a0482dbbb37/msgpack-1.2.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e1b99ad34613d5f8477fa5cf99bc4eaeaf27965588007c102370cd9a78fe9de5", size = 84293, upload-time = "2026-08-27T10:03:14.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/05/c992bb65744665a41b5bf531fc0e1619bae0901f57738228ded90023c151/msgpack-1.2.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0fbc1bed8a535389b41882cfae66376e248cd1680eaa94fd83193c73e1d24986", size = 84490, upload-time = "2026-08-27T10:03:16.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/bf/7f53b9e6709a4df7f9b9b81dc65f9dfaa32caf65bee94986ec2cb8fa07f1/msgpack-1.2.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06d95f61de7afe4f4ff908a6feebfcb070d0582ac87c9cf3cedf8551cf634516", size = 405332, upload-time = "2026-08-27T10:03:17.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/5a/305c4dca14b50d0b51fb88ef04ec125b8f0be3e2ce730dcc62dbaa651cc5/msgpack-1.2.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5c696ae7cd7166b3657261adb855b461ff31f07823fdbae9de8bf80adfccc21", size = 416798, upload-time = "2026-08-27T10:03:19.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/df/a645102b4cdfd9a94201cac4e900e9c1429fc16d86aa311c06eef82528c9/msgpack-1.2.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0708afbf6a9587f0bfe479a9825c141d14d91e2f6a5c8103cf28bc96f4edb5d9", size = 377312, upload-time = "2026-08-27T10:03:20.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/26/c56d8d086d3fb1077bb48092b158b5ea2eee08b279e10c191275f13bc980/msgpack-1.2.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:226a62ffe99fe54c5c61d910ec64c3449b7766c3280bd286bf6c94838dde239a", size = 395182, upload-time = "2026-08-27T10:03:22.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/b5/3d46ba367a565e536d8d2a61eebcee71b1dc803da3ce74a22313b573d6fa/msgpack-1.2.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9fd7f32e2f0fb334e7ecc5adb5cf0458785bd3a9d9d86f950e1715f101cebce5", size = 377945, upload-time = "2026-08-27T10:03:24.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/2c/d5d2df273ed5306357da25b69400fd8d7a53c4d87d8976604b677484d61c/msgpack-1.2.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9db1ba1c1e6a84245a9dd866265b56b8a1e9461549cc72ed296d8cbfbd32961b", size = 413341, upload-time = "2026-08-27T10:03:25.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/fb/32613bced3cad47b40b1b73dd04d687121349d83f748efc2575929121903/msgpack-1.2.2-cp315-cp315-win32.whl", hash = "sha256:e2eb7ea0ac3911a7aac9d8aaa36d40f216d99455b3274cd3fac38181bcd910cf", size = 66730, upload-time = "2026-08-27T10:03:27.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/56/d86171f7251015e9312e5a7f9fdd4cf89752fc2114b88fed453d2a040c66/msgpack-1.2.2-cp315-cp315-win_amd64.whl", hash = "sha256:9352e6cdb510a7b1a5d3ccaccec730e82e50cf3484a3af7bdaab19e23b9589ff", size = 73477, upload-time = "2026-08-27T10:03:28.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/1a/56b90f6defef61700b86baca3637c15f62ac0f9b21ab0f16613ab9d1f101/msgpack-1.2.2-cp315-cp315-win_arm64.whl", hash = "sha256:29cc2d5291711a52956a79a51f41c732329df39ad727c886bd8f0b5b9237a808", size = 67660, upload-time = "2026-08-27T10:03:29.895Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/12751ca0d8ec874701b54c392c2b19f51af8dd1de40a92a10e356f0aaf58/msgpack-1.2.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d886baa46b2532135e7320067e6a44edb09ba5883a6096b0f9c044533984b8a8", size = 86462, upload-time = "2026-08-27T10:03:31.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/4c/cf6d12a3d709fe5f9771dd917c35e6ebcd55597a5b792287382fde056c95/msgpack-1.2.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53679573c75cce5f82359e0bd4e6a97809a6b9a9b7a48fd1ba592f4a82cddc84", size = 87412, upload-time = "2026-08-27T10:03:32.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/0d/0aac5752d1708dcb458f8754db34a4999514db3df2d2b798b9381293f638/msgpack-1.2.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3c247d457ae9079974c7ce3c665396754a6d2baff7eaa51332212a8a5a3f13b", size = 422057, upload-time = "2026-08-27T10:03:34.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/30/70f281a3685b04aaf235a5237da11b978a02a865a5a479186205177ad676/msgpack-1.2.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352ed831042549cca8be23780e1fe7c9177e65ff02bf183509c4b4d33f671782", size = 422696, upload-time = "2026-08-27T10:03:35.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/6d/f76e8425efb0aa38988cd778ae290bfa120491d80d26872d88bb52fedb3f/msgpack-1.2.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f80361592c13d7226b4379c8941529b63fe1a9d0e05d2de8f3306b70e522b53f", size = 376495, upload-time = "2026-08-27T10:03:37.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/77/0809aa9b52b2868f7d01862dc14073708f0440421a65197b48453480034c/msgpack-1.2.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:68df2947921d449f6dcfeafd86cb2cdde13327a8b447534bbe4ee5aaf32a5695", size = 404683, upload-time = "2026-08-27T10:03:38.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/d2/4e5ac915ba120172d210ef00165c5e6276c8a65db3a4a5cf36e946b83e23/msgpack-1.2.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:51dd39d23cfdea0400ed3ff2d29d1e83bd951d3aea79dc89be5b701a09edfe23", size = 375087, upload-time = "2026-08-27T10:03:40.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/e3/8051d53e5495c87c6cf27eb42fb680361017037f87f322bdaf525f71e4a2/msgpack-1.2.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b13b59e66f107cca1ba708dd5307179870ca1b15b19fcee7ccf722e5308d9212", size = 414421, upload-time = "2026-08-27T10:03:42.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/4e/13783aa7c17414d7186c72c49bc718366f75e49f0ea58d4f81cb63ac3187/msgpack-1.2.2-cp315-cp315t-win32.whl", hash = "sha256:8c6321a414f8b4a8dc43976b2fa8349156434ca9adedd9a187b796f7e1d3d3fc", size = 71790, upload-time = "2026-08-27T10:03:43.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/9d/1d02994c7ae2603c98100984428ff0f67443572133bc18eca6058f732c1b/msgpack-1.2.2-cp315-cp315t-win_amd64.whl", hash = "sha256:6f53285f20d592ed309ee19e509cc4c77a3bda1db02ad67e8a0949bb227a5a6d", size = 78766, upload-time = "2026-08-27T10:03:45.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/54/89ed16e6f966a050dc78b0e94a545025211b07ce9f4bdfe07dff70c03fc2/msgpack-1.2.2-cp315-cp315t-win_arm64.whl", hash = "sha256:a378e12ccc06d76efde115caf4073b7e5ff3cc18291d1341f9e65fb882e3f754", size = 71819, upload-time = "2026-08-27T10:03:46.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1393,7 +1374,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "3.6.0"
|
||||
version = "3.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -1403,9 +1384,9 @@ dependencies = [
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/76/913b755a1a6b54e2d9140eb8d488aa0d47c7359b1d7eac5e864cb7913bbf/openai-3.6.0.tar.gz", hash = "sha256:18fe3f6e96390ef41ee27b152fc9effefca321c33673bd9b956a572493d3ab9b", size = 1455376, upload-time = "2026-08-28T22:29:18.268Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/9c/ba0c292b4032ede74c249ca314ad64eb1bb5a03a843f6e01facb02f80cd8/openai-3.3.1.tar.gz", hash = "sha256:6f22807de1a976c932cecda620e8172a8c3fdbaeed29c7f21564e0c2410edf56", size = 1282113, upload-time = "2026-08-19T16:31:35.006Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/94/805b87ecc951c49ec8f247f5e8eb324ab064bd2ad73b6a0e704dd49aa073/openai-3.6.0-py3-none-any.whl", hash = "sha256:508e2158bf971687f953b62e44b02f207792c815aac306816386d7ba34d37f5f", size = 1699841, upload-time = "2026-08-28T22:29:16.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/db/2b7a1b3de659bb82aef979116c74e809982b13e42c057759767552b5155f/openai-3.3.1-py3-none-any.whl", hash = "sha256:9652df7fdf8ee6f5bd58e0a12f2b1d414a18e0f06bb7a9a57c8643a5f5469bd3", size = 1690337, upload-time = "2026-08-19T16:31:32.812Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1717,7 +1698,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.5"
|
||||
version = "2.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
@@ -1725,84 +1706,84 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.5"
|
||||
version = "2.46.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1989,7 +1970,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.27.2"
|
||||
version = "0.27.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -1997,18 +1978,18 @@ dependencies = [
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-protobuf"
|
||||
version = "7.35.1.20260827"
|
||||
version = "7.35.1.20260826"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/07/541ffc94a4baee7d92ec91d91018aa684f97ece608b74619863e3cfd9cb3/types_protobuf-7.35.1.20260827.tar.gz", hash = "sha256:5a98c18177cfd968d6a0de55ecfc4221aa829ef88bcd86104298e028516b04af", size = 69646, upload-time = "2026-08-27T12:06:04.93Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/8d/63939858680cda558e43de17822e1bcdf74fb3d1cdf5a80ce7a90a0c2a0f/types_protobuf-7.35.1.20260826.tar.gz", hash = "sha256:e7083f061500bcbf6d1f32eb83dfff0983d1583b028ae7a791dbe33f47e8bf3a", size = 69626, upload-time = "2026-08-26T02:54:39.711Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/e2/7ce7c4802bcf3732155dedc23fe3d2b506a387d6b42da61261886f42af80/types_protobuf-7.35.1.20260827-py3-none-any.whl", hash = "sha256:7cc452a012678a6a889287d4cb702ea54b9f90a6ab0f3aa4c019d21f3ae7529b", size = 86412, upload-time = "2026-08-27T12:06:03.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/a8/24fa08c2b9307ead7e9c3565de007e47315ca396c78e9cc1350643737052/types_protobuf-7.35.1.20260826-py3-none-any.whl", hash = "sha256:49adc6d989e146b7b2ba4498aeae36937619d4b95a21cd20025fc88b2cd29b62", size = 86412, upload-time = "2026-08-26T02:54:38.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2131,7 +2112,6 @@ wheels = [
|
||||
name = "websockets"
|
||||
version = "17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/72/fba934cb3dff7a85d811820efffcd141ddd52b5a2a01637f64551373ff4d/websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a", size = 187520, upload-time = "2026-08-26T17:25:33.063Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/0d/098f23c4c858e5de9459ffc554fa07d5493fbcfca7f040b5800cf1cecc35/websockets-17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3", size = 217015, upload-time = "2026-08-26T14:55:45.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/86/bc1317b1a4d8c4688e2a7e564b5e004dab44c2534d7ca05de6ae9a863fca/websockets-17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98", size = 214692, upload-time = "2026-08-26T14:55:46.366Z" },
|
||||
@@ -2253,7 +2233,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/f5/99857c3dd9676749f33e3668665a34ad6099505fb8d75eb084f49f7807a9/websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3", size = 217130, upload-time = "2026-08-26T14:58:20.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/84/77599922ab441bfe61508f97dab2c71f8e114d31793993ea54011db16199/websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110", size = 217448, upload-time = "2026-08-26T14:58:21.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/3c/8b9a225b523f06a9389be81f1b0ab07c49bec6014742e6aa359c1f920f1f/websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81", size = 217372, upload-time = "2026-08-26T14:58:22.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -13,12 +13,7 @@ from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_pydantic_field.rest_framework import SchemaField
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
)
|
||||
from pydantic import BaseModel, Field, field_serializer
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@@ -249,49 +244,6 @@ class BaseValidationOnlySerializer(serializers.Serializer):
|
||||
raise NotImplementedError(f"{self.__class__.__name__} is validation-only")
|
||||
|
||||
|
||||
class EncodingConfig(BaseModel):
|
||||
"""Configuration options for recording encoding.
|
||||
|
||||
The allowed `resolution` and `profile` values are derived at validation time
|
||||
from ``settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS`` and
|
||||
``settings.RECORDING_ENCODING_AVAILABLE_PROFILES``, so adding a resolution or profile
|
||||
to those maps is enough to make it accepted here.
|
||||
|
||||
Attributes:
|
||||
resolution: Target video resolution.
|
||||
profile: Encoding profile to fps and kbps. When `None`,
|
||||
`settings.RECORDING_ENCODING_DEFAULT_PROFILE` applies.
|
||||
"""
|
||||
|
||||
resolution: str
|
||||
profile: str | None = None
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
@field_validator("resolution")
|
||||
@classmethod
|
||||
def _validate_resolution(cls, value):
|
||||
"""Reject resolutions absent from RECORDING_ENCODING_AVAILABLE_RESOLUTIONS."""
|
||||
allowed = set(settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS)
|
||||
if value not in allowed:
|
||||
raise ValueError(
|
||||
f"Invalid resolution '{value}'. Choose from {sorted(allowed)}."
|
||||
)
|
||||
return value
|
||||
|
||||
@field_validator("profile")
|
||||
@classmethod
|
||||
def _validate_profile(cls, value):
|
||||
"""Reject profiles absent from RECORDING_ENCODING_AVAILABLE_PROFILES."""
|
||||
if value is None:
|
||||
return None
|
||||
allowed = set(settings.RECORDING_ENCODING_AVAILABLE_PROFILES)
|
||||
if value not in allowed:
|
||||
raise ValueError(
|
||||
f"Invalid profile '{value}'. Choose from {sorted(allowed)}."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class RecordingOptions(BaseModel):
|
||||
"""Configuration options for recording.
|
||||
|
||||
@@ -312,7 +264,7 @@ class RecordingOptions(BaseModel):
|
||||
transcribe: bool | None = None
|
||||
collect_metadata: bool | None = None
|
||||
original_mode: Literal["screen_recording", "transcript"] | None = None
|
||||
encoding: EncodingConfig | None = None
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ from core.recording.worker.exceptions import (
|
||||
RecordingStopError,
|
||||
)
|
||||
from core.recording.worker.factories import (
|
||||
build_encoding_options,
|
||||
get_worker_service,
|
||||
)
|
||||
from core.recording.worker.mediator import (
|
||||
@@ -401,34 +400,12 @@ class RoomViewSet(
|
||||
options = serializer.validated_data.get("options")
|
||||
room = self.get_object()
|
||||
|
||||
if (
|
||||
options is not None
|
||||
and options.encoding is not None
|
||||
and not settings.RECORDING_CUSTOM_ENCODING_ENABLED
|
||||
):
|
||||
# Per-recording encoding selection is gated by
|
||||
# RECORDING_CUSTOM_ENCODING_ENABLED. When disabled, recordings use
|
||||
# encoding defined by RECORDING_ENCODING_DEFAULT_RESOLUTION
|
||||
# and RECORDING_ENCODING_DEFAULT_PROFILE.
|
||||
return drf_response.Response(
|
||||
{"detail": "Per-recording encoding selection is disabled."},
|
||||
status=drf_status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
options_data = options.model_dump(exclude_none=True) if options else {}
|
||||
if options is not None and options.encoding is not None:
|
||||
# Persist the resolved encoding (concrete width/height/framerate/
|
||||
# bitrate) alongside the requested resolution/profile for traceability.
|
||||
options_data["encoding"]["resolved"] = build_encoding_options(
|
||||
options.encoding.resolution, options.encoding.profile
|
||||
)
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
recording = models.Recording.objects.create(
|
||||
room=room,
|
||||
mode=mode,
|
||||
options=options_data,
|
||||
options=options.model_dump(exclude_none=True) if options else {},
|
||||
)
|
||||
models.RecordingAccess.objects.create(
|
||||
user=self.request.user,
|
||||
|
||||
@@ -22,46 +22,6 @@ _RECORDING_AUDIO_CODEC = livekit_api.AudioCodec.AAC
|
||||
_RECORDING_AUDIO_FREQUENCY_HZ = 48000
|
||||
|
||||
|
||||
def build_encoding_options(resolution, profile=None):
|
||||
"""Assemble the LiveKit ``EncodingOptions`` kwargs for a resolution/profile.
|
||||
|
||||
Single source of truth shared by the default encoding
|
||||
(``WorkerServiceConfig.from_settings``) and the per-recording encoding
|
||||
persisted by the start-recording API, so both paths always produce the
|
||||
same shape.
|
||||
|
||||
The profile-independent fields (audio bitrate, keyframe interval and the
|
||||
pinned codec / frequency constants) are always included.
|
||||
|
||||
An omitted profile falls back to RECORDING_ENCODING_DEFAULT_PROFILE.
|
||||
Framerate and bitrate are left to LiveKit only when the operator
|
||||
declared no default profile at all.
|
||||
"""
|
||||
profile = profile or settings.RECORDING_ENCODING_DEFAULT_PROFILE
|
||||
|
||||
options: Dict[str, Any] = {
|
||||
"audio_bitrate": settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS,
|
||||
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
|
||||
"video_codec": _RECORDING_VIDEO_CODEC,
|
||||
"audio_codec": _RECORDING_AUDIO_CODEC,
|
||||
"audio_frequency": _RECORDING_AUDIO_FREQUENCY_HZ,
|
||||
}
|
||||
|
||||
if resolution:
|
||||
resolution_config = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[
|
||||
resolution
|
||||
]
|
||||
options["width"] = resolution_config["width"]
|
||||
options["height"] = resolution_config["height"]
|
||||
|
||||
if resolution and profile:
|
||||
profile_config = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
|
||||
options["framerate"] = profile_config["fps"]
|
||||
options["video_bitrate"] = profile_config["kbps"][resolution]
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkerServiceConfig:
|
||||
"""Declare Worker Service common configurations"""
|
||||
@@ -78,16 +38,22 @@ class WorkerServiceConfig:
|
||||
|
||||
logger.debug("Loading WorkerServiceConfig from settings.")
|
||||
|
||||
# The default encoding is resolved from the default profile/resolution and
|
||||
# applied to every recording that carries no per-recording encoding.
|
||||
# When either default is missing, we leave this as None so LiveKit falls
|
||||
# back to its built-in preset.
|
||||
resolution = settings.RECORDING_ENCODING_DEFAULT_RESOLUTION
|
||||
profile = settings.RECORDING_ENCODING_DEFAULT_PROFILE
|
||||
|
||||
encoding_options: Optional[Dict[str, Any]] = None
|
||||
if resolution and profile:
|
||||
encoding_options = build_encoding_options(resolution, profile)
|
||||
if settings.RECORDING_ENCODING_ENABLED:
|
||||
# Single source of truth for the EncodingOptions kwargs:
|
||||
# operator-tunable values live in Django settings, codec / frequency
|
||||
# are pinned constants. The services layer only unpacks this dict.
|
||||
encoding_options = {
|
||||
"width": settings.RECORDING_ENCODING_WIDTH,
|
||||
"height": settings.RECORDING_ENCODING_HEIGHT,
|
||||
"framerate": settings.RECORDING_ENCODING_FRAMERATE,
|
||||
"video_bitrate": settings.RECORDING_ENCODING_VIDEO_BITRATE_KBPS,
|
||||
"audio_bitrate": settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS,
|
||||
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
|
||||
"video_codec": _RECORDING_VIDEO_CODEC,
|
||||
"audio_codec": _RECORDING_AUDIO_CODEC,
|
||||
"audio_frequency": _RECORDING_AUDIO_FREQUENCY_HZ,
|
||||
}
|
||||
|
||||
return cls(
|
||||
output_folder=settings.RECORDING_OUTPUT_FOLDER,
|
||||
@@ -112,12 +78,7 @@ class WorkerService(Protocol):
|
||||
def __init__(self, config: WorkerServiceConfig):
|
||||
"""Initialize the service with the given configuration."""
|
||||
|
||||
def start(
|
||||
self,
|
||||
room_id: str,
|
||||
recording_id: str,
|
||||
encoding_options: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
def start(self, room_id: str, recording_id: str) -> str:
|
||||
"""Start a recording for a specified room."""
|
||||
|
||||
def stop(self, worker_id: str) -> str:
|
||||
|
||||
@@ -51,11 +51,8 @@ class WorkerServiceMediator:
|
||||
raise RecordingStartError()
|
||||
|
||||
room_name = str(recording.room.id)
|
||||
encoding_options = (recording.options.get("encoding") or {}).get("resolved")
|
||||
try:
|
||||
worker_id = self._worker_service.start(
|
||||
room_name, recording.id, encoding_options=encoding_options
|
||||
)
|
||||
worker_id = self._worker_service.start(room_name, recording.id)
|
||||
except (WorkerRequestError, WorkerConnectionError, WorkerResponseError) as e:
|
||||
logger.exception(
|
||||
"Failed to start recording for room %s: %s", recording.room.slug, e
|
||||
|
||||
@@ -76,28 +76,28 @@ class BaseEgressService:
|
||||
|
||||
return "FAILED_TO_STOP"
|
||||
|
||||
def start(self, room_name, recording_id, encoding_options=None):
|
||||
def start(self, room_name, recording_id):
|
||||
"""Start the egress process for a recording (not implemented in the base class).
|
||||
Each derived class must implement this method, providing the necessary parameters for
|
||||
its specific egress type (e.g. audio_only, streaming output).
|
||||
"""
|
||||
raise NotImplementedError("Subclass must implement this method.")
|
||||
|
||||
def _resolve_encoding_options(self, encoding_options):
|
||||
"""Build a LiveKit EncodingOptions from a resolved kwargs dict, or None.
|
||||
|
||||
``encoding_options`` is the per-recording dict persisted by the API in
|
||||
``recording.options["encoding"]["resolved"]``; it falls back to the
|
||||
default encoding carried by the service config.
|
||||
def _build_encoding_options(self):
|
||||
"""Build a LiveKit EncodingOptions from the service config, or None.
|
||||
|
||||
When None is returned, the caller should omit the `advanced` field so
|
||||
LiveKit Egress falls back to its built-in preset (H264_720P_30).
|
||||
|
||||
The full EncodingOptions kwargs (operator-tunable values + pinned
|
||||
codec / frequency constants) are assembled in `WorkerServiceConfig`,
|
||||
so this method is a thin protobuf adapter.
|
||||
"""
|
||||
encoding_options = encoding_options or self._config.encoding_options
|
||||
if not encoding_options:
|
||||
opts = self._config.encoding_options
|
||||
if not opts:
|
||||
return None
|
||||
|
||||
return livekit_api.EncodingOptions(**encoding_options)
|
||||
return livekit_api.EncodingOptions(**opts)
|
||||
|
||||
|
||||
class VideoCompositeEgressService(BaseEgressService):
|
||||
@@ -105,7 +105,7 @@ class VideoCompositeEgressService(BaseEgressService):
|
||||
|
||||
hrid = "video-recording-composite-livekit-egress"
|
||||
|
||||
def start(self, room_name, recording_id, encoding_options=None):
|
||||
def start(self, room_name, recording_id):
|
||||
"""Start the video composite egress process for a recording."""
|
||||
|
||||
# Save room's recording as a mp4 video file.
|
||||
@@ -126,7 +126,7 @@ class VideoCompositeEgressService(BaseEgressService):
|
||||
"layout": "speaker-light",
|
||||
}
|
||||
|
||||
advanced = self._resolve_encoding_options(encoding_options)
|
||||
advanced = self._build_encoding_options()
|
||||
if advanced is not None:
|
||||
request_kwargs["advanced"] = advanced
|
||||
|
||||
@@ -145,13 +145,8 @@ class AudioCompositeEgressService(BaseEgressService):
|
||||
|
||||
hrid = "audio-recording-composite-livekit-egress"
|
||||
|
||||
def start(self, room_name, recording_id, encoding_options=None):
|
||||
"""Start the audio composite egress process for a recording.
|
||||
|
||||
``encoding_options`` is accepted for signature compatibility with the
|
||||
WorkerService protocol but ignored: audio-only egress has no
|
||||
encoding to configure.
|
||||
"""
|
||||
def start(self, room_name, recording_id):
|
||||
"""Start the audio composite egress process for a recording."""
|
||||
|
||||
# Save room's recording as an ogg audio file.
|
||||
file_type = livekit_api.EncodedFileType.OGG
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
"""Tests for the per-recording encoding resolution in BaseEgressService."""
|
||||
|
||||
# pylint: disable=protected-access,redefined-outer-name,unused-argument,no-member
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import override_settings
|
||||
|
||||
import pytest
|
||||
from livekit import api as livekit_api
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
|
||||
from core.api.serializers import EncodingConfig
|
||||
from core.recording.worker.factories import build_encoding_options
|
||||
from core.recording.worker.services import VideoCompositeEgressService
|
||||
|
||||
|
||||
def make_config():
|
||||
"""Build a minimal WorkerServiceConfig-like mock for service instantiation."""
|
||||
config = Mock()
|
||||
config.bucket_args = {
|
||||
"endpoint": "https://s3.test.com",
|
||||
"access_key": "test_key",
|
||||
"secret": "test_secret",
|
||||
"region": "test-region",
|
||||
"bucket": "test-bucket",
|
||||
"force_path_style": True,
|
||||
}
|
||||
config.encoding_options = None
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service():
|
||||
"""Return a VideoCompositeEgressService with mocked handle_request."""
|
||||
svc = VideoCompositeEgressService(make_config())
|
||||
svc._handle_request = Mock()
|
||||
return svc
|
||||
|
||||
|
||||
# --- build_encoding_options ---
|
||||
|
||||
|
||||
def test_build_options_without_profile_uses_default_profile():
|
||||
"""A resolution-only config should fall back to the default profile.
|
||||
|
||||
Left unset, framerate and video_bitrate take LiveKit's own EncodingOptions
|
||||
defaults. The profile-independent fields (audio bitrate, keyframe interval,
|
||||
codec/frequency pins) are always present, matching the default encoding.
|
||||
"""
|
||||
default_profile = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[
|
||||
settings.RECORDING_ENCODING_DEFAULT_PROFILE
|
||||
]
|
||||
|
||||
resolved = build_encoding_options("540p")
|
||||
|
||||
assert resolved == {
|
||||
"audio_bitrate": settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS,
|
||||
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
|
||||
"video_codec": livekit_api.VideoCodec.H264_MAIN,
|
||||
"audio_codec": livekit_api.AudioCodec.AAC,
|
||||
"audio_frequency": 48000,
|
||||
"width": 960,
|
||||
"height": 540,
|
||||
"framerate": default_profile["fps"],
|
||||
"video_bitrate": default_profile["kbps"]["540p"],
|
||||
}
|
||||
|
||||
|
||||
@override_settings(RECORDING_ENCODING_DEFAULT_PROFILE="")
|
||||
def test_build_options_omits_profile_fields_without_default_profile():
|
||||
"""With no default profile declared, framerate/bitrate are left to LiveKit."""
|
||||
resolved = build_encoding_options("720p", None)
|
||||
|
||||
assert resolved == {
|
||||
"audio_bitrate": settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS,
|
||||
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
|
||||
"video_codec": livekit_api.VideoCodec.H264_MAIN,
|
||||
"audio_codec": livekit_api.AudioCodec.AAC,
|
||||
"audio_frequency": 48000,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
}
|
||||
assert "framerate" not in resolved
|
||||
assert "video_bitrate" not in resolved
|
||||
|
||||
|
||||
def test_encoding_config_requires_resolution():
|
||||
"""A profile-only or empty encoding config should be rejected at validation."""
|
||||
with pytest.raises(PydanticValidationError):
|
||||
EncodingConfig(profile="mixed")
|
||||
with pytest.raises(PydanticValidationError):
|
||||
EncodingConfig()
|
||||
|
||||
|
||||
# --- _resolve_encoding_options ---
|
||||
|
||||
|
||||
@pytest.mark.parametrize("encoding_options", [None, {}])
|
||||
def test_resolve_options_returns_none_when_empty(service, encoding_options):
|
||||
"""Resolver should return None when the resolved dict is empty or missing."""
|
||||
assert service._resolve_encoding_options(encoding_options) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resolution",
|
||||
list(settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"profile",
|
||||
list(settings.RECORDING_ENCODING_AVAILABLE_PROFILES),
|
||||
)
|
||||
def test_resolve_profile_resolution_combinations(service, profile, resolution):
|
||||
"""Every (profile, resolution) pair should resolve to the values from settings."""
|
||||
resolution_config = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[resolution]
|
||||
expected_width = resolution_config["width"]
|
||||
expected_height = resolution_config["height"]
|
||||
profile_config = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
|
||||
expected_fps = profile_config["fps"]
|
||||
expected_bitrate = profile_config["kbps"][resolution]
|
||||
|
||||
resolved = build_encoding_options(resolution, profile)
|
||||
result = service._resolve_encoding_options(resolved)
|
||||
|
||||
assert result.width == expected_width
|
||||
assert result.height == expected_height
|
||||
assert result.framerate == expected_fps
|
||||
assert result.video_bitrate == expected_bitrate
|
||||
# Profile-independent fields match the default encoding, never dropped.
|
||||
assert result.audio_bitrate == settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS
|
||||
assert result.video_codec == livekit_api.VideoCodec.H264_MAIN
|
||||
assert result.audio_codec == livekit_api.AudioCodec.AAC
|
||||
assert result.audio_frequency == 48000
|
||||
|
||||
|
||||
def test_resolve_options_none_profile_uses_default_profile(service):
|
||||
"""A missing profile should resolve to the default profile's fps/bitrate."""
|
||||
default_profile = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[
|
||||
settings.RECORDING_ENCODING_DEFAULT_PROFILE
|
||||
]
|
||||
|
||||
resolved = build_encoding_options("720p", None)
|
||||
result = service._resolve_encoding_options(resolved)
|
||||
|
||||
assert result.width == 1280
|
||||
assert result.height == 720
|
||||
assert result.framerate == default_profile["fps"]
|
||||
assert result.video_bitrate == default_profile["kbps"]["720p"]
|
||||
assert result.audio_bitrate == settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS
|
||||
assert result.video_codec == livekit_api.VideoCodec.H264_MAIN
|
||||
|
||||
|
||||
@override_settings(RECORDING_ENCODING_DEFAULT_PROFILE="")
|
||||
def test_resolve_options_passes_zero_when_no_default_profile(service):
|
||||
"""With no default profile, fps/bitrate reach LiveKit unset (protobuf 0).
|
||||
|
||||
The pinned codec / audio fields are still applied.
|
||||
"""
|
||||
resolved = build_encoding_options("720p", None)
|
||||
result = service._resolve_encoding_options(resolved)
|
||||
|
||||
assert result.width == 1280
|
||||
assert result.height == 720
|
||||
assert result.framerate == 0
|
||||
assert result.video_bitrate == 0
|
||||
assert result.audio_bitrate == settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS
|
||||
assert result.video_codec == livekit_api.VideoCodec.H264_MAIN
|
||||
assert result.audio_codec == livekit_api.AudioCodec.AAC
|
||||
@@ -40,16 +40,6 @@ def test_settings():
|
||||
"AWS_S3_SECRET_ACCESS_KEY": "test_secret",
|
||||
"AWS_S3_REGION_NAME": "test-region",
|
||||
"AWS_STORAGE_BUCKET_NAME": "test-bucket",
|
||||
"RECORDING_ENCODING_AVAILABLE_RESOLUTIONS": {
|
||||
"720p": {"width": 1280, "height": 720}
|
||||
},
|
||||
"RECORDING_ENCODING_AVAILABLE_PROFILES": {
|
||||
"full": {"fps": 30, "kbps": {"720p": 3000}}
|
||||
},
|
||||
"RECORDING_ENCODING_DEFAULT_RESOLUTION": "720p",
|
||||
"RECORDING_ENCODING_DEFAULT_PROFILE": "full",
|
||||
"RECORDING_ENCODING_AUDIO_BITRATE_KBPS": 128,
|
||||
"RECORDING_ENCODING_KEY_FRAME_INTERVAL_S": 4.0,
|
||||
}
|
||||
|
||||
# Use override_settings to properly patch Django settings
|
||||
@@ -76,18 +66,8 @@ def test_config_initialization(default_config):
|
||||
"bucket": "test-bucket",
|
||||
"force_path_style": True,
|
||||
}
|
||||
# The default encoding is always resolved from the default profile/resolution.
|
||||
assert default_config.encoding_options == {
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"framerate": 30,
|
||||
"video_bitrate": 3000,
|
||||
"audio_bitrate": 128,
|
||||
"key_frame_interval": 4.0,
|
||||
"video_codec": livekit_api_codec.VideoCodec.H264_MAIN,
|
||||
"audio_codec": livekit_api_codec.AudioCodec.AAC,
|
||||
"audio_frequency": 48000,
|
||||
}
|
||||
# Encoding override is opt-in; disabled by default.
|
||||
assert default_config.encoding_options is None
|
||||
|
||||
|
||||
def test_config_immutability(default_config):
|
||||
@@ -96,7 +76,6 @@ def test_config_immutability(default_config):
|
||||
default_config.output_folder = "new/path"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("custom_encoding_enabled", [True, False])
|
||||
@override_settings(
|
||||
RECORDING_OUTPUT_FOLDER="/test/output",
|
||||
LIVEKIT_CONFIGURATION={"server": "test.example.com"},
|
||||
@@ -105,25 +84,23 @@ def test_config_immutability(default_config):
|
||||
AWS_S3_SECRET_ACCESS_KEY="test_secret",
|
||||
AWS_S3_REGION_NAME="test-region",
|
||||
AWS_STORAGE_BUCKET_NAME="test-bucket",
|
||||
RECORDING_ENCODING_AVAILABLE_RESOLUTIONS={"720p": {"width": 1280, "height": 720}},
|
||||
RECORDING_ENCODING_AVAILABLE_PROFILES={"low": {"fps": 15, "kbps": {"720p": 600}}},
|
||||
RECORDING_ENCODING_DEFAULT_RESOLUTION="720p",
|
||||
RECORDING_ENCODING_DEFAULT_PROFILE="low",
|
||||
RECORDING_ENCODING_ENABLED=True,
|
||||
RECORDING_ENCODING_WIDTH=1280,
|
||||
RECORDING_ENCODING_HEIGHT=720,
|
||||
RECORDING_ENCODING_FRAMERATE=15,
|
||||
RECORDING_ENCODING_VIDEO_BITRATE_KBPS=600,
|
||||
RECORDING_ENCODING_AUDIO_BITRATE_KBPS=64,
|
||||
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=10.0,
|
||||
)
|
||||
def test_config_encoding_options_default(custom_encoding_enabled):
|
||||
"""The default encoding is always resolved from the default profile/resolution.
|
||||
def test_config_encoding_options_enabled():
|
||||
"""When RECORDING_ENCODING_ENABLED is True, encoding options are populated.
|
||||
|
||||
The default fallback resolves the default profile/resolution and mixes those
|
||||
operator-tunable values with pinned codec / frequency constants. This works
|
||||
regardless of RECORDING_CUSTOM_ENCODING_ENABLED, which only gates the
|
||||
per-recording API, so both toggle states produce the same default.
|
||||
The dict mixes operator-tunable values from settings with pinned codec /
|
||||
frequency constants, so the services layer can simply unpack it.
|
||||
"""
|
||||
|
||||
with override_settings(RECORDING_CUSTOM_ENCODING_ENABLED=custom_encoding_enabled):
|
||||
WorkerServiceConfig.from_settings.cache_clear()
|
||||
config = WorkerServiceConfig.from_settings()
|
||||
WorkerServiceConfig.from_settings.cache_clear()
|
||||
config = WorkerServiceConfig.from_settings()
|
||||
|
||||
assert config.encoding_options == {
|
||||
"width": 1280,
|
||||
@@ -138,27 +115,6 @@ def test_config_encoding_options_default(custom_encoding_enabled):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("default_resolution", "default_profile"),
|
||||
[("", "full"), ("720p", ""), ("", "")],
|
||||
)
|
||||
def test_config_encoding_options_none_when_default_missing(
|
||||
test_settings, default_resolution, default_profile
|
||||
):
|
||||
"""A missing default resolution/profile leaves encoding_options None.
|
||||
|
||||
The service then omits the `advanced` field so LiveKit uses its built-in preset.
|
||||
"""
|
||||
with override_settings(
|
||||
RECORDING_ENCODING_DEFAULT_RESOLUTION=default_resolution,
|
||||
RECORDING_ENCODING_DEFAULT_PROFILE=default_profile,
|
||||
):
|
||||
WorkerServiceConfig.from_settings.cache_clear()
|
||||
config = WorkerServiceConfig.from_settings()
|
||||
|
||||
assert config.encoding_options is None
|
||||
|
||||
|
||||
@override_settings(
|
||||
RECORDING_OUTPUT_FOLDER="/test/output",
|
||||
LIVEKIT_CONFIGURATION={"server": "test.example.com"},
|
||||
|
||||
@@ -50,7 +50,7 @@ def test_start_recording_success(mock_update_metadata, mediator, mock_worker_ser
|
||||
# Verify worker service call
|
||||
expected_room_name = str(mock_recording.room.id)
|
||||
mock_worker_service.start.assert_called_once_with(
|
||||
expected_room_name, mock_recording.id, encoding_options=None
|
||||
expected_room_name, mock_recording.id
|
||||
)
|
||||
|
||||
# Verify recording updates
|
||||
@@ -64,38 +64,6 @@ def test_start_recording_success(mock_update_metadata, mediator, mock_worker_ser
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
|
||||
def test_start_recording_passes_resolved_encoding(
|
||||
mock_update_metadata, mediator, mock_worker_service
|
||||
):
|
||||
"""The resolved encoding persisted in recording.options reaches the worker."""
|
||||
mock_worker_service.start.return_value = "test-worker-123"
|
||||
|
||||
resolved = {
|
||||
"key_frame_interval": 4.0,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"framerate": 15,
|
||||
"video_bitrate": 700,
|
||||
}
|
||||
mock_recording = RecordingFactory(
|
||||
status=RecordingStatusChoices.INITIATED,
|
||||
worker_id=None,
|
||||
options={
|
||||
"encoding": {
|
||||
"resolution": "720p",
|
||||
"profile": "talking_heads",
|
||||
"resolved": resolved,
|
||||
}
|
||||
},
|
||||
)
|
||||
mediator.start(mock_recording)
|
||||
|
||||
mock_worker_service.start.assert_called_once_with(
|
||||
str(mock_recording.room.id), mock_recording.id, encoding_options=resolved
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error_class", [WorkerRequestError, WorkerConnectionError, WorkerResponseError]
|
||||
)
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
Test rooms API endpoints in the Meet core app: start recording.
|
||||
"""
|
||||
|
||||
# pylint: disable=redefined-outer-name,unused-argument,no-member
|
||||
# pylint: disable=redefined-outer-name,unused-argument
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from livekit import api as livekit_api
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from ...factories import RoomFactory, UserFactory
|
||||
@@ -471,224 +470,6 @@ def test_start_recording_options_unknown_field_rejected(settings):
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_start_recording_options_encoding_valid(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""Should accept a valid encoding configuration."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
settings.RECORDING_CUSTOM_ENCODING_ENABLED = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{
|
||||
"mode": "screen_recording",
|
||||
"options": {"encoding": {"resolution": "720p", "profile": "talking_heads"}},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
def test_start_recording_options_encoding_rejected_when_custom_encoding_disabled(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""Per-recording encoding is rejected when RECORDING_CUSTOM_ENCODING_ENABLED is off."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
settings.RECORDING_CUSTOM_ENCODING_ENABLED = False
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{
|
||||
"mode": "screen_recording",
|
||||
"options": {"encoding": {"resolution": "720p", "profile": "talking_heads"}},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert not Recording.objects.filter(room=room).exists()
|
||||
|
||||
|
||||
def test_start_recording_persists_resolved_encoding(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""The resolved encoding should be persisted in recording.options alongside
|
||||
the requested resolution/profile for traceability."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
settings.RECORDING_CUSTOM_ENCODING_ENABLED = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{
|
||||
"mode": "screen_recording",
|
||||
"options": {"encoding": {"resolution": "720p", "profile": "talking_heads"}},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options["encoding"] == {
|
||||
"resolution": "720p",
|
||||
"profile": "talking_heads",
|
||||
"resolved": {
|
||||
"audio_bitrate": settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS,
|
||||
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
|
||||
"video_codec": livekit_api.VideoCodec.H264_MAIN,
|
||||
"audio_codec": livekit_api.AudioCodec.AAC,
|
||||
"audio_frequency": 48000,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"framerate": 15,
|
||||
"video_bitrate": 700,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_start_recording_resolution_only_uses_default_profile(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""An encoding without a profile should resolve the default profile."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
settings.RECORDING_CUSTOM_ENCODING_ENABLED = True
|
||||
settings.RECORDING_ENCODING_DEFAULT_PROFILE = "talking_heads"
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"encoding": {"resolution": "540p"}}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
resolved = recording.options["encoding"]["resolved"]
|
||||
assert resolved["width"] == 960
|
||||
assert resolved["height"] == 540
|
||||
assert resolved["framerate"] == 15
|
||||
assert resolved["video_bitrate"] == 400
|
||||
# The requested payload is persisted as sent: no profile was asked for.
|
||||
assert "profile" not in recording.options["encoding"]
|
||||
|
||||
|
||||
def test_start_recording_forwards_resolved_encoding_to_worker(
|
||||
settings, mock_worker_service, mock_worker_service_factory
|
||||
):
|
||||
"""The resolved encoding should passed on to the worker."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
settings.RECORDING_CUSTOM_ENCODING_ENABLED = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
mock_worker_service.start.return_value = "egress-123"
|
||||
|
||||
with mock.patch("core.services.room_management.RoomManagement.update_metadata"):
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{
|
||||
"mode": "screen_recording",
|
||||
"options": {
|
||||
"encoding": {"resolution": "720p", "profile": "talking_heads"}
|
||||
},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
mock_worker_service.start.assert_called_once_with(
|
||||
str(room.id),
|
||||
recording.id,
|
||||
encoding_options=recording.options["encoding"]["resolved"],
|
||||
)
|
||||
|
||||
|
||||
def test_start_recording_options_encoding_invalid_resolution(settings):
|
||||
"""Should reject invalid encoding resolution values."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
settings.RECORDING_CUSTOM_ENCODING_ENABLED = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"encoding": {"resolution": "4K"}}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_start_recording_options_encoding_unknown_key_rejected(settings):
|
||||
"""Should reject unknown keys in encoding configuration."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
settings.RECORDING_CUSTOM_ENCODING_ENABLED = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{
|
||||
"mode": "screen_recording",
|
||||
"options": {"encoding": {"bitrate": 9000}},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_start_recording_options_without_encoding_unchanged(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""Requests without encoding should keep existing options behavior."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"language": "fr"}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {"language": "fr"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["foo", 12])
|
||||
def test_start_recording_options_invalid_transcribe_type(settings, value):
|
||||
"""Should reject non-boolean transcribe values."""
|
||||
|
||||
+40
-27
@@ -381,31 +381,6 @@ def detect_mimetype(file_buffer: bytes, filename: str | None = None) -> str:
|
||||
return mimetype_from_content or "application/octet-stream"
|
||||
|
||||
|
||||
def _get_s3_client(*, override_domain: bool = True):
|
||||
"""Return an S3 client, honoring the AWS_S3_DOMAIN_REPLACE endpoint override.
|
||||
|
||||
AWS_S3_DOMAIN_REPLACE is used when the backend and frontend reach object
|
||||
storage under different domains (this is the case in the docker compose stack
|
||||
used in development: the frontend connects to the object storage on localhost
|
||||
while the backend uses the object storage service name declared in the stack).
|
||||
The domain name is used to compute the signature, so it can't be changed
|
||||
dynamically by the frontend; we build a dedicated boto3 client pointed at that
|
||||
endpoint. Otherwise we reuse the default storage client.
|
||||
"""
|
||||
if settings.AWS_S3_DOMAIN_REPLACE and override_domain:
|
||||
return boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_S3_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_S3_SECRET_ACCESS_KEY,
|
||||
endpoint_url=settings.AWS_S3_DOMAIN_REPLACE,
|
||||
config=botocore.client.Config(
|
||||
region_name=settings.AWS_S3_REGION_NAME,
|
||||
signature_version=settings.AWS_S3_SIGNATURE_VERSION,
|
||||
),
|
||||
)
|
||||
return default_storage.connection.meta.client
|
||||
|
||||
|
||||
def generate_upload_policy(file):
|
||||
"""
|
||||
Generate a S3 upload policy for a given file.
|
||||
@@ -416,7 +391,26 @@ def generate_upload_policy(file):
|
||||
|
||||
key = file.temporary_file_key
|
||||
|
||||
s3_client = _get_s3_client()
|
||||
# This settings should be used if the backend application and the frontend application
|
||||
# can't connect to the object storage with the same domain. This is the case in the
|
||||
# docker compose stack used in development. The frontend application will use localhost
|
||||
# to connect to the object storage while the backend application will use the object storage
|
||||
# service name declared in the docker compose stack.
|
||||
# This is needed because the domain name is used to compute the signature. So it can't be
|
||||
# changed dynamically by the frontend application.
|
||||
if settings.AWS_S3_DOMAIN_REPLACE:
|
||||
s3_client = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_S3_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_S3_SECRET_ACCESS_KEY,
|
||||
endpoint_url=settings.AWS_S3_DOMAIN_REPLACE,
|
||||
config=botocore.client.Config(
|
||||
region_name=settings.AWS_S3_REGION_NAME,
|
||||
signature_version=settings.AWS_S3_SIGNATURE_VERSION,
|
||||
),
|
||||
)
|
||||
else:
|
||||
s3_client = default_storage.connection.meta.client
|
||||
|
||||
# Generate the policy
|
||||
policy = s3_client.generate_presigned_url(
|
||||
@@ -437,7 +431,26 @@ def generate_download_s3_url(
|
||||
if not key:
|
||||
raise ValueError("key cannot be empty")
|
||||
|
||||
s3_client = _get_s3_client(override_domain=override_domain)
|
||||
# This setting should be used if the backend application and the frontend application
|
||||
# can't connect to the object storage with the same domain. This is the case in the
|
||||
# docker compose stack used in development. The frontend application will use localhost
|
||||
# to connect to the object storage while the backend application will use the object storage
|
||||
# service name declared in the docker compose stack.
|
||||
# This is needed because the domain name is used to compute the signature. So it can't be
|
||||
# changed dynamically by the frontend application.
|
||||
if settings.AWS_S3_DOMAIN_REPLACE and override_domain:
|
||||
s3_client = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_S3_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_S3_SECRET_ACCESS_KEY,
|
||||
endpoint_url=settings.AWS_S3_DOMAIN_REPLACE,
|
||||
config=botocore.client.Config(
|
||||
region_name=settings.AWS_S3_REGION_NAME,
|
||||
signature_version=settings.AWS_S3_SIGNATURE_VERSION,
|
||||
),
|
||||
)
|
||||
else:
|
||||
s3_client = default_storage.connection.meta.client
|
||||
|
||||
return s3_client.generate_presigned_url(
|
||||
ClientMethod="get_object",
|
||||
|
||||
+20
-173
@@ -23,8 +23,6 @@ import dj_database_url
|
||||
import sentry_sdk
|
||||
from configurations import Configuration, values
|
||||
from lasuite.configuration.values import SecretFileValue
|
||||
from pydantic import BaseModel, PositiveInt, TypeAdapter
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.logging import ignore_logger
|
||||
|
||||
@@ -55,28 +53,6 @@ def get_release():
|
||||
return "NA" # Default: not available
|
||||
|
||||
|
||||
class Resolution(BaseModel):
|
||||
"""Shape of a RECORDING_ENCODING_AVAILABLE_RESOLUTIONS entry."""
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
width: PositiveInt
|
||||
height: PositiveInt
|
||||
|
||||
|
||||
class Profile(BaseModel):
|
||||
"""Shape of a RECORDING_ENCODING_AVAILABLE_PROFILES entry."""
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
fps: PositiveInt
|
||||
kbps: dict[str, PositiveInt]
|
||||
|
||||
|
||||
RESOLUTION_MAP_ADAPTER = TypeAdapter(dict[str, Resolution])
|
||||
PROFILE_MAP_ADAPTER = TypeAdapter(dict[str, Profile])
|
||||
|
||||
|
||||
class Base(Configuration):
|
||||
"""
|
||||
This is the base configuration every configuration (aka environment) should inherit from. It
|
||||
@@ -769,81 +745,35 @@ class Base(Configuration):
|
||||
# These settings affect screen recordings handled by VideoCompositeEgressService;
|
||||
# they are silently ignored by AudioCompositeEgressService (audio-only transcript
|
||||
# recordings), whose request never carries advanced EncodingOptions.
|
||||
#
|
||||
# A default encoding is applied to every recording: it is resolved from the default
|
||||
# profile and resolution below and passed to LiveKit as EncodingOptions (advanced),
|
||||
# replacing LiveKit's built-in H264_720P_30 preset. Lowering framerate and bitrate
|
||||
# reduces output file size and CPU load on the egress worker. If either
|
||||
# RECORDING_ENCODING_DEFAULT_RESOLUTION or RECORDING_ENCODING_DEFAULT_PROFILE is
|
||||
# unset, no default encoding is built (a startup warning is emitted) and LiveKit's
|
||||
# built-in preset is used instead.
|
||||
#
|
||||
# RECORDING_CUSTOM_ENCODING_ENABLED gates whether the start-recording API lets a
|
||||
# client override that default per recording (via an `encoding` object selecting a
|
||||
# resolution/profile). When False, the API rejects per-recording `encoding` and
|
||||
# every recording uses the default; when True, clients may pick from the
|
||||
# available resolutions/profiles below.
|
||||
RECORDING_CUSTOM_ENCODING_ENABLED = values.BooleanValue(
|
||||
False, environ_name="RECORDING_CUSTOM_ENCODING_ENABLED", environ_prefix=None
|
||||
# When disabled, LiveKit falls back to its built-in H264_720P_30 preset
|
||||
# (1280x720, 30 fps, 3000 kbps H.264 MAIN video, 128 kbps AAC audio).
|
||||
# When enabled, the values below are passed to LiveKit as EncodingOptions
|
||||
# (advanced) and replace the preset. Lowering framerate and bitrate reduces
|
||||
# output file size and CPU load on the egress worker.
|
||||
RECORDING_ENCODING_ENABLED = values.BooleanValue(
|
||||
False, environ_name="RECORDING_ENCODING_ENABLED", environ_prefix=None
|
||||
)
|
||||
|
||||
# Map resolution string -> {"width", "height"} in pixels.
|
||||
RECORDING_ENCODING_AVAILABLE_RESOLUTIONS = values.DictValue(
|
||||
{
|
||||
"540p": {"width": 960, "height": 540},
|
||||
"720p": {"width": 1280, "height": 720},
|
||||
"1080p": {"width": 1920, "height": 1080},
|
||||
},
|
||||
environ_name="RECORDING_ENCODING_AVAILABLE_RESOLUTIONS",
|
||||
RECORDING_ENCODING_WIDTH = values.PositiveIntegerValue(
|
||||
1280, environ_name="RECORDING_ENCODING_WIDTH", environ_prefix=None
|
||||
)
|
||||
RECORDING_ENCODING_HEIGHT = values.PositiveIntegerValue(
|
||||
720, environ_name="RECORDING_ENCODING_HEIGHT", environ_prefix=None
|
||||
)
|
||||
RECORDING_ENCODING_FRAMERATE = values.PositiveIntegerValue(
|
||||
30, environ_name="RECORDING_ENCODING_FRAMERATE", environ_prefix=None
|
||||
)
|
||||
RECORDING_ENCODING_VIDEO_BITRATE_KBPS = values.PositiveIntegerValue(
|
||||
3000,
|
||||
environ_name="RECORDING_ENCODING_VIDEO_BITRATE_KBPS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Map profile string -> {"fps", "kbps": {resolution: video_bitrate_kbps}}.
|
||||
# Bitrate scales with resolution so quality stays consistent across sizes.
|
||||
RECORDING_ENCODING_AVAILABLE_PROFILES = values.DictValue(
|
||||
{
|
||||
"talking_heads": {
|
||||
"fps": 15,
|
||||
"kbps": {"540p": 400, "720p": 700, "1080p": 1200},
|
||||
},
|
||||
"text": {
|
||||
"fps": 15,
|
||||
"kbps": {"540p": 600, "720p": 1000, "1080p": 1800},
|
||||
},
|
||||
"mixed": {
|
||||
"fps": 20,
|
||||
"kbps": {"540p": 900, "720p": 1500, "1080p": 2500},
|
||||
},
|
||||
"full": {
|
||||
"fps": 30,
|
||||
"kbps": {"540p": 2000, "720p": 3000, "1080p": 4500},
|
||||
},
|
||||
},
|
||||
environ_name="RECORDING_ENCODING_AVAILABLE_PROFILES",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Defaults used when no profile/resolution is specified per recording.
|
||||
# Must be keys of the two dicts above (validated at startup).
|
||||
RECORDING_ENCODING_DEFAULT_PROFILE = values.Value(
|
||||
"full",
|
||||
environ_name="RECORDING_ENCODING_DEFAULT_PROFILE",
|
||||
environ_prefix=None,
|
||||
)
|
||||
RECORDING_ENCODING_DEFAULT_RESOLUTION = values.Value(
|
||||
"720p",
|
||||
environ_name="RECORDING_ENCODING_DEFAULT_RESOLUTION",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Settings independent of profile/resolution.
|
||||
RECORDING_ENCODING_AUDIO_BITRATE_KBPS = values.PositiveIntegerValue(
|
||||
128,
|
||||
environ_name="RECORDING_ENCODING_AUDIO_BITRATE_KBPS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S = values.FloatValue(
|
||||
0.0,
|
||||
4.0,
|
||||
environ_name="RECORDING_ENCODING_KEY_FRAME_INTERVAL_S",
|
||||
environ_prefix=None,
|
||||
)
|
||||
@@ -851,7 +781,6 @@ class Base(Configuration):
|
||||
SUMMARY_SERVICE_VERSION = values.PositiveIntegerValue(
|
||||
1, environ_name="SUMMARY_SERVICE_VERSION", environ_prefix=None
|
||||
)
|
||||
|
||||
SUMMARY_SERVICE_ENDPOINT = values.Value(
|
||||
None, environ_name="SUMMARY_SERVICE_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
@@ -1240,86 +1169,6 @@ class Base(Configuration):
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _check_recording_encoding_maps(cls):
|
||||
"""Ensure the per-recording encoding maps are well-formed and consistent.
|
||||
|
||||
Each entry of RECORDING_ENCODING_AVAILABLE_RESOLUTIONS must declare a width and
|
||||
a height, each entry of RECORDING_ENCODING_AVAILABLE_PROFILES an fps and a kbps
|
||||
map, and every profile must define a bitrate for each declared resolution.
|
||||
|
||||
The default profile / resolution feed the default encoding. When either is
|
||||
missing, no custom default encoding can be built: a warning is emitted and
|
||||
recordings fall back to LiveKit's built-in preset. When both are set, they
|
||||
must reference keys that actually exist in the maps above.
|
||||
"""
|
||||
resolutions = set(cls.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS)
|
||||
profiles = set(cls.RECORDING_ENCODING_AVAILABLE_PROFILES)
|
||||
|
||||
for name, adapter in (
|
||||
("RECORDING_ENCODING_AVAILABLE_RESOLUTIONS", RESOLUTION_MAP_ADAPTER),
|
||||
("RECORDING_ENCODING_AVAILABLE_PROFILES", PROFILE_MAP_ADAPTER),
|
||||
):
|
||||
try:
|
||||
adapter.validate_python(getattr(cls, name))
|
||||
except PydanticValidationError as exc:
|
||||
raise ValueError(f"{name} is malformed: {exc}") from exc
|
||||
|
||||
for (
|
||||
profile,
|
||||
profile_config,
|
||||
) in cls.RECORDING_ENCODING_AVAILABLE_PROFILES.items(): # pylint: disable=no-member
|
||||
profile_resolutions = set(profile_config["kbps"])
|
||||
if profile_resolutions != resolutions:
|
||||
raise ValueError(
|
||||
f"Profile '{profile}' in RECORDING_ENCODING_AVAILABLE_PROFILES must "
|
||||
"define a bitrate for exactly the resolutions in "
|
||||
"RECORDING_ENCODING_AVAILABLE_RESOLUTIONS, mismatch on: "
|
||||
f"{resolutions ^ profile_resolutions}"
|
||||
)
|
||||
|
||||
# Check that default resolutions and profiles are actually defined
|
||||
if (
|
||||
cls.RECORDING_ENCODING_DEFAULT_RESOLUTION
|
||||
and cls.RECORDING_ENCODING_DEFAULT_RESOLUTION not in resolutions
|
||||
):
|
||||
raise ValueError(
|
||||
"RECORDING_ENCODING_DEFAULT_RESOLUTION "
|
||||
f"'{cls.RECORDING_ENCODING_DEFAULT_RESOLUTION}' is not a key of "
|
||||
f"RECORDING_ENCODING_AVAILABLE_RESOLUTIONS ({sorted(resolutions)})."
|
||||
)
|
||||
if (
|
||||
cls.RECORDING_ENCODING_DEFAULT_PROFILE
|
||||
and cls.RECORDING_ENCODING_DEFAULT_PROFILE not in profiles
|
||||
):
|
||||
raise ValueError(
|
||||
"RECORDING_ENCODING_DEFAULT_PROFILE "
|
||||
f"'{cls.RECORDING_ENCODING_DEFAULT_PROFILE}' is not a key of "
|
||||
f"RECORDING_ENCODING_AVAILABLE_PROFILES ({sorted(profiles)})."
|
||||
)
|
||||
|
||||
missing = [
|
||||
name
|
||||
for name, value in (
|
||||
(
|
||||
"RECORDING_ENCODING_DEFAULT_RESOLUTION",
|
||||
cls.RECORDING_ENCODING_DEFAULT_RESOLUTION,
|
||||
),
|
||||
(
|
||||
"RECORDING_ENCODING_DEFAULT_PROFILE",
|
||||
cls.RECORDING_ENCODING_DEFAULT_PROFILE,
|
||||
),
|
||||
)
|
||||
if not value
|
||||
]
|
||||
if missing:
|
||||
warnings.warn(
|
||||
f"{' and '.join(missing)} not set; recordings will use LiveKit's "
|
||||
"built-in encoding preset instead of a custom default encoding.",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def post_setup(cls):
|
||||
"""Post setup configuration.
|
||||
@@ -1333,8 +1182,6 @@ class Base(Configuration):
|
||||
"FILE_UPLOAD_TMP_PATH cannot be the same as FILE_UPLOAD_PATH"
|
||||
)
|
||||
|
||||
cls._check_recording_encoding_maps()
|
||||
|
||||
if (
|
||||
cls.SUMMARY_SERVICE_VERSION == 1
|
||||
and cls.SUMMARY_SERVICE_ENDPOINT is not None
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.30.0"
|
||||
version = "1.29.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
Generated
+1
-1
@@ -1187,7 +1187,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meet"
|
||||
version = "1.30.0"
|
||||
version = "1.29.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
Generated
+6
-6
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "1.30.0",
|
||||
"version": "1.29.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "1.30.0",
|
||||
"version": "1.29.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
||||
"@fontsource-variable/lexend": "5.3.0",
|
||||
"@fontsource/opendyslexic": "5.3.0",
|
||||
"@fontsource/opendyslexic": "5.2.5",
|
||||
"@libreaudio/la-call": "0.1.4",
|
||||
"@livekit/components-react": "2.9.23",
|
||||
"@livekit/components-styles": "1.2.0",
|
||||
@@ -785,9 +785,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/opendyslexic": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/opendyslexic/-/opendyslexic-5.3.0.tgz",
|
||||
"integrity": "sha512-BVYIW/ghc1U2iAghhYTN+GFWiZ3lnCy8Jf2KkZC23aHQHBNcMqKT718zckfqVxRx4qsXJmIMPWvczirRuhmtsg==",
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/opendyslexic/-/opendyslexic-5.2.5.tgz",
|
||||
"integrity": "sha512-NNS9aaPQx2TlaTvb3vTEjw3xz8lKj23mBc+6rM00mSNFDygdoll0/nLMHFtDKKrBT6sMfY6TFFPOR0D9ktdspg==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.30.0",
|
||||
"version": "1.29.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -17,7 +17,7 @@
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
||||
"@fontsource-variable/lexend": "5.3.0",
|
||||
"@fontsource/opendyslexic": "5.3.0",
|
||||
"@fontsource/opendyslexic": "5.2.5",
|
||||
"@libreaudio/la-call": "0.1.4",
|
||||
"@livekit/components-react": "2.9.23",
|
||||
"@livekit/components-styles": "1.2.0",
|
||||
|
||||
@@ -51,7 +51,7 @@ export const ChatTextArea = () => {
|
||||
const isDisabled = !textAreaValue.trim() || isSending
|
||||
|
||||
const onKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key !== 'Escape') e.stopPropagation()
|
||||
e.stopPropagation()
|
||||
if (e.key !== 'Enter' || (e.key === 'Enter' && e.shiftKey) || isDisabled)
|
||||
return
|
||||
e.preventDefault()
|
||||
|
||||
@@ -1,595 +0,0 @@
|
||||
import { CSSProperties, useState } from 'react'
|
||||
import { useConnectionState, useRoomContext } from '@livekit/components-react'
|
||||
import { ConnectionState } from 'livekit-client'
|
||||
import { StatsSnapshot, TrackRow, useWebRTCStats } from './useWebRTCStats'
|
||||
import { readRoomConfig } from './roomConfig'
|
||||
import {
|
||||
forceTransport,
|
||||
releaseForcedTransport,
|
||||
Scenario,
|
||||
SCENARIOS,
|
||||
setDownlinkCap,
|
||||
setUplinkCap,
|
||||
stepBitrate,
|
||||
TransportMode,
|
||||
transportModeFromRoute,
|
||||
} from './simulation'
|
||||
|
||||
const SANS =
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
||||
|
||||
const COLOR = {
|
||||
text: '#e8e8e8',
|
||||
muted: '#9a9a9a',
|
||||
faint: '#6f6f6f',
|
||||
hairline: '#2c2d31',
|
||||
border: '#3d3e44',
|
||||
surface: '#1b1c1e',
|
||||
chartBg: '#141517',
|
||||
down: '#f6821f',
|
||||
up: '#5a9cf8',
|
||||
ok: '#10b981',
|
||||
bad: '#f05a4a',
|
||||
busy: '#e5a13c',
|
||||
}
|
||||
|
||||
const stateColor = (state: ConnectionState): string => {
|
||||
switch (state) {
|
||||
case ConnectionState.Connected:
|
||||
return COLOR.ok
|
||||
case ConnectionState.Reconnecting:
|
||||
case ConnectionState.SignalReconnecting:
|
||||
return COLOR.busy
|
||||
case ConnectionState.Connecting:
|
||||
return COLOR.up
|
||||
case ConnectionState.Disconnected:
|
||||
return COLOR.bad
|
||||
}
|
||||
}
|
||||
|
||||
const styles: Record<string, CSSProperties> = {
|
||||
toggle: {
|
||||
position: 'fixed',
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
zIndex: 9999,
|
||||
fontFamily: SANS,
|
||||
fontSize: 12,
|
||||
lineHeight: 1,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 999,
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: COLOR.border,
|
||||
background: COLOR.surface,
|
||||
color: COLOR.text,
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 10px #0006',
|
||||
},
|
||||
panel: {
|
||||
position: 'fixed',
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
zIndex: 9999,
|
||||
width: 460,
|
||||
maxWidth: 'calc(100vw - 24px)',
|
||||
maxHeight: 'calc(100vh - 24px)',
|
||||
overflowY: 'auto',
|
||||
fontFamily: SANS,
|
||||
fontSize: 11,
|
||||
lineHeight: 1.5,
|
||||
color: COLOR.text,
|
||||
background: COLOR.surface,
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: COLOR.border,
|
||||
borderRadius: 10,
|
||||
padding: 14,
|
||||
boxShadow: '0 10px 34px #00000080',
|
||||
},
|
||||
sectionTitle: {
|
||||
margin: '14px 0 5px',
|
||||
color: COLOR.faint,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1.2,
|
||||
fontSize: 9,
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
metricsLine: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
margin: '0 0 10px',
|
||||
paddingBottom: 10,
|
||||
borderBottom: `1px solid ${COLOR.hairline}`,
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
},
|
||||
legend: {
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
marginTop: 3,
|
||||
color: COLOR.faint,
|
||||
fontSize: 10,
|
||||
},
|
||||
list: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
configList: { display: 'flex', flexWrap: 'wrap' },
|
||||
configRow: {
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
flex: '0 0 50%',
|
||||
minWidth: 0,
|
||||
boxSizing: 'border-box',
|
||||
padding: '1px 8px 1px 0',
|
||||
},
|
||||
configLabel: {
|
||||
color: COLOR.muted,
|
||||
flex: '0 0 45%',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
configValue: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
trackRow: {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
padding: '3px 0',
|
||||
borderBottom: `1px solid ${COLOR.hairline}`,
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
trackLabel: {
|
||||
flex: '0 0 34%',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
trackCodec: {
|
||||
flex: '0 0 11%',
|
||||
fontFamily: MONO,
|
||||
fontSize: 10,
|
||||
color: COLOR.muted,
|
||||
},
|
||||
trackKbps: {
|
||||
flex: '0 0 9%',
|
||||
textAlign: 'right',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
},
|
||||
trackDetail: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
mono: { fontFamily: MONO, fontSize: 10, color: COLOR.muted },
|
||||
button: {
|
||||
fontFamily: SANS,
|
||||
fontSize: 11,
|
||||
padding: '3px 9px',
|
||||
borderRadius: 5,
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: COLOR.border,
|
||||
background: '#232428',
|
||||
color: COLOR.text,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
buttonActive: { borderColor: COLOR.down, color: COLOR.down },
|
||||
buttonDisabled: { color: COLOR.faint, cursor: 'not-allowed' },
|
||||
row: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
|
||||
stepperValue: {
|
||||
minWidth: 92,
|
||||
textAlign: 'center',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: COLOR.hairline,
|
||||
borderRadius: 4,
|
||||
padding: '2px 6px',
|
||||
background: COLOR.chartBg,
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
},
|
||||
close: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: COLOR.faint,
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
},
|
||||
}
|
||||
|
||||
const Sparkline = ({ history }: { history: StatsSnapshot[] }) => {
|
||||
const width = 430
|
||||
const height = 54
|
||||
const max = Math.max(...history.map((s) => Math.max(s.upKbps, s.downKbps)), 1)
|
||||
const points = (pick: (s: StatsSnapshot) => number) =>
|
||||
history
|
||||
.map(
|
||||
(s, i) =>
|
||||
`${((i / Math.max(history.length - 1, 1)) * width).toFixed(1)},${(
|
||||
height -
|
||||
(pick(s) / max) * (height - 6) -
|
||||
3
|
||||
).toFixed(1)}`
|
||||
)
|
||||
.join(' ')
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
role="img"
|
||||
aria-label="Bandwidth over the last minute"
|
||||
style={{
|
||||
display: 'block',
|
||||
background: COLOR.chartBg,
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${COLOR.hairline}`,
|
||||
}}
|
||||
>
|
||||
{history.length >= 2 && (
|
||||
<>
|
||||
<polyline
|
||||
points={points((s) => s.downKbps)}
|
||||
fill="none"
|
||||
stroke={COLOR.down}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<polyline
|
||||
points={points((s) => s.upKbps)}
|
||||
fill="none"
|
||||
stroke={COLOR.up}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
{/* Legend outside the plot: text over the lines was unreadable. */}
|
||||
<div style={styles.legend}>
|
||||
<span>
|
||||
<span style={{ color: COLOR.down }}>●</span> down
|
||||
</span>
|
||||
<span>
|
||||
<span style={{ color: COLOR.up }}>●</span> up
|
||||
</span>
|
||||
<span>last 60s · max {Math.round(max)} kbps</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CapStepper = ({
|
||||
label,
|
||||
hint,
|
||||
valueKbps,
|
||||
onChange,
|
||||
}: {
|
||||
label: string
|
||||
hint: string
|
||||
valueKbps: number | null
|
||||
onChange: (kbps: number | null) => void
|
||||
}) => (
|
||||
<div style={styles.row}>
|
||||
<span style={{ flex: '0 0 70px' }}>{label}</span>
|
||||
<button
|
||||
type="button"
|
||||
style={styles.button}
|
||||
aria-label={`Decrease ${label} bandwidth`}
|
||||
onClick={() => onChange(stepBitrate(valueKbps, 'decrease'))}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span
|
||||
style={{
|
||||
...styles.stepperValue,
|
||||
...(valueKbps !== null ? { color: COLOR.down } : {}),
|
||||
}}
|
||||
>
|
||||
{valueKbps === null ? 'unlimited' : `≤ ${valueKbps} kbps`}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
style={styles.button}
|
||||
aria-label={`Increase ${label} bandwidth`}
|
||||
onClick={() => onChange(stepBitrate(valueKbps, 'increase'))}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<span style={styles.mono}>{hint}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
const TrackList = ({ title, rows }: { title: string; rows: TrackRow[] }) => (
|
||||
<>
|
||||
<div style={styles.sectionTitle}>
|
||||
<span>
|
||||
{title} ({rows.length})
|
||||
</span>
|
||||
<span>
|
||||
{Math.round(rows.reduce((sum, t) => sum + t.kbps, 0))} kbps media
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.list}>
|
||||
{rows.map((track) => (
|
||||
<div key={track.key} style={styles.trackRow}>
|
||||
<span style={styles.trackLabel}>{track.label}</span>
|
||||
<span style={styles.trackCodec}>{track.codec ?? '–'}</span>
|
||||
<span style={styles.trackKbps}>{Math.round(track.kbps)}</span>
|
||||
<span style={styles.trackDetail}>
|
||||
{[
|
||||
track.res,
|
||||
track.fps !== undefined && `${Math.round(track.fps)}fps`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{rows.length === 0 && <span style={{ color: COLOR.faint }}>none</span>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
const MeetDevtools = () => {
|
||||
const room = useRoomContext()
|
||||
const connState = useConnectionState(room)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [firedScenario, setFiredScenario] = useState<string>()
|
||||
const [uplinkCap, setUplinkCapState] = useState<number | null>(null)
|
||||
const [downlinkCap, setDownlinkCapState] = useState<number | null>(null)
|
||||
const { snapshot, history } = useWebRTCStats(room, open)
|
||||
|
||||
const transportMode: TransportMode = transportModeFromRoute(snapshot?.route)
|
||||
|
||||
const fireScenario = (scenario: Scenario) => {
|
||||
setFiredScenario(scenario.id)
|
||||
scenario.run(room).catch((e) => {
|
||||
console.warn('[MeetDevtools] simulateScenario failed', e)
|
||||
})
|
||||
window.setTimeout(
|
||||
() =>
|
||||
setFiredScenario((current) =>
|
||||
current === scenario.id ? undefined : current
|
||||
),
|
||||
1200
|
||||
)
|
||||
}
|
||||
|
||||
if (!open) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
style={styles.toggle}
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="Open WebRTC devtools"
|
||||
>
|
||||
<span style={{ color: stateColor(connState) }}>●</span> rtc
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const config = readRoomConfig(room)
|
||||
const published = snapshot?.tracks.filter((t) => t.dir === 'up') ?? []
|
||||
const subscribed = snapshot?.tracks.filter((t) => t.dir === 'down') ?? []
|
||||
const turnProtocols = snapshot?.turnProtocols ?? []
|
||||
const transports: Array<{
|
||||
mode: TransportMode
|
||||
label: string
|
||||
requires?: string
|
||||
title: string
|
||||
}> = [
|
||||
{
|
||||
mode: 'auto',
|
||||
label: 'auto (udp)',
|
||||
title:
|
||||
'clears the server-cached transport preference and relay-only policy, then full reconnect',
|
||||
},
|
||||
{
|
||||
mode: 'tcp',
|
||||
label: 'tcp',
|
||||
title: 'force-tcp — server prefers TCP candidates (ICE/TCP)',
|
||||
},
|
||||
{
|
||||
mode: 'turn-udp',
|
||||
label: 'turn:udp',
|
||||
requires: 'udp',
|
||||
title:
|
||||
'client-side: iceTransportPolicy relay + full reconnect (no server hook exists)',
|
||||
},
|
||||
{
|
||||
mode: 'turn-tcp',
|
||||
label: 'turn:tcp',
|
||||
requires: 'tcp',
|
||||
title: 'force-tcp — lands on TURN/TCP when it is the TCP path',
|
||||
},
|
||||
{
|
||||
mode: 'turn-tls',
|
||||
label: 'turn:tls',
|
||||
requires: 'tls',
|
||||
title: 'force-tls — server switches you to TURN over TLS',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section style={styles.panel} aria-label="WebRTC devtools">
|
||||
<div style={styles.metricsLine}>
|
||||
<span>
|
||||
<span style={{ color: COLOR.down }}>↓</span> {snapshot?.downKbps ?? 0}{' '}
|
||||
kbps
|
||||
</span>
|
||||
<span>
|
||||
<span style={{ color: COLOR.up }}>↑</span> {snapshot?.upKbps ?? 0}{' '}
|
||||
kbps
|
||||
</span>
|
||||
<span style={{ color: COLOR.muted }}>
|
||||
rtt{' '}
|
||||
<span style={{ color: COLOR.text }}>{snapshot?.rttMs ?? '–'}</span> ms
|
||||
</span>
|
||||
<span style={{ color: COLOR.muted }}>
|
||||
jitter{' '}
|
||||
<span style={{ color: COLOR.text }}>{snapshot?.jitterMs ?? '–'}</span>{' '}
|
||||
ms
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: stateColor(connState),
|
||||
fontSize: 10,
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
● {connState}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
style={styles.close}
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="Close WebRTC devtools"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Sparkline history={history} />
|
||||
|
||||
<div style={styles.sectionTitle}>room configuration (live)</div>
|
||||
<div style={styles.configList}>
|
||||
{config.map((entry) => (
|
||||
<div key={entry.label} style={styles.configRow}>
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
entry.on === undefined
|
||||
? COLOR.faint
|
||||
: entry.on
|
||||
? COLOR.ok
|
||||
: COLOR.bad,
|
||||
}}
|
||||
>
|
||||
●
|
||||
</span>
|
||||
<span style={styles.configLabel}>{entry.label}</span>
|
||||
<span style={styles.configValue} title={entry.value}>
|
||||
{entry.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<TrackList title="published ↑" rows={published} />
|
||||
<TrackList title="subscribed ↓" rows={subscribed} />
|
||||
|
||||
<div style={styles.sectionTitle}>bandwidth</div>
|
||||
<div style={{ ...styles.list, gap: 4 }}>
|
||||
<CapStepper
|
||||
label="uplink"
|
||||
hint="encoder cap (setParameters)"
|
||||
valueKbps={uplinkCap}
|
||||
onChange={(kbps) => {
|
||||
setUplinkCapState(kbps)
|
||||
void setUplinkCap(room, kbps)
|
||||
}}
|
||||
/>
|
||||
<CapStepper
|
||||
label="downlink"
|
||||
hint="SFU limit (subscriber-bandwidth)"
|
||||
valueKbps={downlinkCap}
|
||||
onChange={(kbps) => {
|
||||
setDownlinkCapState(kbps)
|
||||
void setDownlinkCap(room, kbps).catch((e) =>
|
||||
console.warn('[MeetDevtools] downlink cap failed', e)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={styles.sectionTitle}>transport</div>
|
||||
<div style={styles.row}>
|
||||
{transports.map(({ mode, label, requires, title }) => {
|
||||
const available = !requires || turnProtocols.includes(requires)
|
||||
const active = transportMode === mode
|
||||
const clickable = available && !active
|
||||
return (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
aria-pressed={active}
|
||||
disabled={!clickable}
|
||||
title={available ? title : `${title} — not configured`}
|
||||
style={{
|
||||
...styles.button,
|
||||
...(clickable || active ? {} : styles.buttonDisabled),
|
||||
...(active ? styles.buttonActive : {}),
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!clickable) return
|
||||
void (
|
||||
mode === 'auto'
|
||||
? releaseForcedTransport(room)
|
||||
: forceTransport(
|
||||
room,
|
||||
mode as 'tcp' | 'turn-udp' | 'turn-tcp' | 'turn-tls'
|
||||
)
|
||||
).catch((e) =>
|
||||
console.warn('[MeetDevtools] transport change failed', e)
|
||||
)
|
||||
}}
|
||||
>
|
||||
{requires && (
|
||||
<span style={{ color: available ? COLOR.ok : COLOR.faint }}>
|
||||
●{' '}
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<span style={styles.mono}>
|
||||
route:{' '}
|
||||
{snapshot?.route
|
||||
? [
|
||||
snapshot.route.protocol,
|
||||
snapshot.route.type,
|
||||
snapshot.route.relayProtocol &&
|
||||
`relay:${snapshot.route.relayProtocol}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('·')
|
||||
: '–'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={styles.sectionTitle}>connection scenarios</div>
|
||||
<div style={styles.row}>
|
||||
{SCENARIOS.map((scenario) => (
|
||||
<button
|
||||
key={scenario.id}
|
||||
type="button"
|
||||
title={`${scenario.side}-side simulation — momentary, watch the state dot`}
|
||||
style={{
|
||||
...styles.button,
|
||||
...(firedScenario === scenario.id ? styles.buttonActive : {}),
|
||||
}}
|
||||
onClick={() => fireScenario(scenario)}
|
||||
>
|
||||
{scenario.side === 'server' ? '☁ ' : ''}
|
||||
{scenario.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default MeetDevtools
|
||||
@@ -1,14 +0,0 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
|
||||
const LazyPanel = import.meta.env.DEV
|
||||
? lazy(() => import('./MeetDevtools'))
|
||||
: null
|
||||
|
||||
export const MeetDevtools = () => {
|
||||
if (!LazyPanel) return null
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyPanel />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Room } from 'livekit-client'
|
||||
|
||||
export type ConfigEntry = {
|
||||
label: string
|
||||
value: string
|
||||
/** true = feature actively on, false = off, undefined = informational */
|
||||
on?: boolean
|
||||
}
|
||||
|
||||
const formatBackupCodec = (backup: unknown): string => {
|
||||
if (backup === undefined || backup === true) return 'auto'
|
||||
if (backup === false) return 'off'
|
||||
if (typeof backup === 'object' && backup !== null && 'codec' in backup) {
|
||||
return String((backup as { codec: unknown }).codec)
|
||||
}
|
||||
return String(backup)
|
||||
}
|
||||
|
||||
export const readRoomConfig = (room: Room): ConfigEntry[] => {
|
||||
const options = room.options
|
||||
const publish = options.publishDefaults
|
||||
const adaptive = options.adaptiveStream
|
||||
|
||||
const entries: ConfigEntry[] = [
|
||||
{
|
||||
label: 'adaptiveStream',
|
||||
value:
|
||||
typeof adaptive === 'object'
|
||||
? JSON.stringify(adaptive)
|
||||
: String(!!adaptive),
|
||||
on: !!adaptive,
|
||||
},
|
||||
{
|
||||
label: 'dynacast',
|
||||
value: String(!!options.dynacast),
|
||||
on: !!options.dynacast,
|
||||
},
|
||||
{
|
||||
label: 'e2ee',
|
||||
value: String(room.isE2EEEnabled),
|
||||
on: room.isE2EEEnabled,
|
||||
},
|
||||
{
|
||||
label: 'videoCodec',
|
||||
value: publish?.videoCodec ?? 'default',
|
||||
},
|
||||
{
|
||||
label: 'backupCodec',
|
||||
value: formatBackupCodec(publish?.backupCodec),
|
||||
},
|
||||
{
|
||||
label: 'simulcast',
|
||||
value: String(publish?.simulcast ?? true),
|
||||
on: publish?.simulcast ?? true,
|
||||
},
|
||||
{
|
||||
label: 'audio dtx',
|
||||
value: String(publish?.dtx ?? true),
|
||||
on: publish?.dtx ?? true,
|
||||
},
|
||||
{
|
||||
label: 'audio red',
|
||||
value: String(publish?.red ?? true),
|
||||
on: publish?.red ?? true,
|
||||
},
|
||||
{
|
||||
label: 'quality (local)',
|
||||
value: room.localParticipant.connectionQuality,
|
||||
},
|
||||
]
|
||||
|
||||
const server = room.serverInfo
|
||||
if (server) {
|
||||
entries.push({
|
||||
label: 'server',
|
||||
value: [
|
||||
server.version && `v${server.version}`,
|
||||
server.region,
|
||||
server.protocol !== undefined && `proto ${server.protocol}`,
|
||||
server.edition !== undefined && `edition ${server.edition}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · '),
|
||||
})
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
import { Room, Track } from 'livekit-client'
|
||||
// Transitive dependency of livekit-client (pinned by it); used only to
|
||||
// build the one signal request room.simulateScenario cannot express.
|
||||
import { SimulateScenario } from '@livekit/protocol'
|
||||
|
||||
/** Shared bandwidth ladder for the −/+ steppers; null = unlimited. */
|
||||
export const BITRATE_LADDER_KBPS: Array<number | null> = [
|
||||
null,
|
||||
2000,
|
||||
1000,
|
||||
600,
|
||||
300,
|
||||
150,
|
||||
]
|
||||
|
||||
export const stepBitrate = (
|
||||
current: number | null,
|
||||
direction: 'decrease' | 'increase'
|
||||
): number | null => {
|
||||
const index = BITRATE_LADDER_KBPS.indexOf(current)
|
||||
const safeIndex = index === -1 ? 0 : index
|
||||
const next =
|
||||
direction === 'decrease'
|
||||
? Math.min(safeIndex + 1, BITRATE_LADDER_KBPS.length - 1)
|
||||
: Math.max(safeIndex - 1, 0)
|
||||
return BITRATE_LADDER_KBPS[next]
|
||||
}
|
||||
|
||||
const savedEncodings = new WeakMap<RTCRtpSender, Array<number | undefined>>()
|
||||
|
||||
export const setUplinkCap = async (
|
||||
room: Room,
|
||||
kbps: number | null
|
||||
): Promise<void> => {
|
||||
const senders: RTCRtpSender[] = []
|
||||
room.localParticipant.trackPublications.forEach((pub) => {
|
||||
const track = pub.track
|
||||
if (track?.kind === Track.Kind.Video && track.sender) {
|
||||
senders.push(track.sender)
|
||||
}
|
||||
})
|
||||
|
||||
for (const sender of senders) {
|
||||
const params = sender.getParameters()
|
||||
if (!params.encodings || params.encodings.length === 0) continue
|
||||
|
||||
if (kbps === null) {
|
||||
const original = savedEncodings.get(sender)
|
||||
params.encodings.forEach((encoding, i) => {
|
||||
encoding.maxBitrate = original?.[i]
|
||||
})
|
||||
savedEncodings.delete(sender)
|
||||
} else {
|
||||
if (!savedEncodings.has(sender)) {
|
||||
savedEncodings.set(
|
||||
sender,
|
||||
params.encodings.map((encoding) => encoding.maxBitrate)
|
||||
)
|
||||
}
|
||||
const activeCount =
|
||||
params.encodings.filter((encoding) => encoding.active !== false)
|
||||
.length || 1
|
||||
// Split the budget across active simulcast layers / SVC encoding.
|
||||
const perEncoding = Math.max(
|
||||
30_000,
|
||||
Math.floor((kbps * 1000) / activeCount)
|
||||
)
|
||||
params.encodings.forEach((encoding) => {
|
||||
encoding.maxBitrate = perEncoding
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await sender.setParameters(params)
|
||||
} catch (e) {
|
||||
console.warn('[MeetDevtools] setParameters failed', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setDownlinkCap = (
|
||||
room: Room,
|
||||
kbps: number | null
|
||||
): Promise<void> =>
|
||||
room.simulateScenario('subscriber-bandwidth', kbps === null ? 0 : kbps * 1000)
|
||||
|
||||
export type Scenario = {
|
||||
id: string
|
||||
label: string
|
||||
/** where the simulation happens */
|
||||
side: 'client' | 'server'
|
||||
run: (room: Room) => Promise<void>
|
||||
}
|
||||
|
||||
export const SCENARIOS: Scenario[] = [
|
||||
{
|
||||
id: 'resume',
|
||||
label: 'reconnect (resume)',
|
||||
side: 'client',
|
||||
// Replays a signaling WebSocket loss; media keeps flowing, client resumes.
|
||||
run: (room) => room.simulateScenario('signal-reconnect'),
|
||||
},
|
||||
{
|
||||
id: 'resume-fail',
|
||||
label: 'reconnect (resume fails)',
|
||||
side: 'client',
|
||||
// Same, but the next resume attempt fails → exercises the retry ladder.
|
||||
run: (room) => room.simulateScenario('resume-reconnect'),
|
||||
},
|
||||
{
|
||||
id: 'full-reconnect',
|
||||
label: 'full reconnect',
|
||||
side: 'client',
|
||||
// Complete rejoin with brand-new peer connections.
|
||||
run: (room) => room.simulateScenario('full-reconnect'),
|
||||
},
|
||||
{
|
||||
id: 'migration',
|
||||
label: 'server migration',
|
||||
side: 'server',
|
||||
run: (room) => room.simulateScenario('migration'),
|
||||
},
|
||||
{
|
||||
id: 'node-failure',
|
||||
label: 'SFU node failure',
|
||||
side: 'server',
|
||||
run: (room) => room.simulateScenario('node-failure'),
|
||||
},
|
||||
{
|
||||
id: 'server-leave',
|
||||
label: 'server disconnect',
|
||||
side: 'server',
|
||||
// Server-initiated leave: the closest thing to "you got kicked".
|
||||
run: (room) => room.simulateScenario('server-leave'),
|
||||
},
|
||||
]
|
||||
|
||||
export type TransportMode =
|
||||
| 'auto'
|
||||
| 'tcp'
|
||||
| 'turn-udp'
|
||||
| 'turn-tcp'
|
||||
| 'turn-tls'
|
||||
|
||||
const clearServerTransportPreference = (room: Room): Promise<void> =>
|
||||
room.engine.client.sendSimulateScenario(
|
||||
new SimulateScenario({
|
||||
scenario: { case: 'switchCandidateProtocol', value: 0 },
|
||||
})
|
||||
)
|
||||
|
||||
const setRelayOnly = (room: Room, relay: boolean) => {
|
||||
room.engine.rtcConfig = {
|
||||
...room.engine.rtcConfig,
|
||||
iceTransportPolicy: relay ? 'relay' : 'all',
|
||||
}
|
||||
}
|
||||
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
export const forceTransport = async (
|
||||
room: Room,
|
||||
mode: 'tcp' | 'turn-udp' | 'turn-tcp' | 'turn-tls'
|
||||
): Promise<void> => {
|
||||
if (mode === 'turn-udp') {
|
||||
await clearServerTransportPreference(room)
|
||||
setRelayOnly(room, true)
|
||||
await settle()
|
||||
return room.simulateScenario('full-reconnect')
|
||||
}
|
||||
setRelayOnly(room, false)
|
||||
return room.simulateScenario(mode === 'turn-tls' ? 'force-tls' : 'force-tcp')
|
||||
}
|
||||
|
||||
export const releaseForcedTransport = async (room: Room): Promise<void> => {
|
||||
setRelayOnly(room, false)
|
||||
await clearServerTransportPreference(room)
|
||||
await settle()
|
||||
await room.simulateScenario('full-reconnect')
|
||||
}
|
||||
|
||||
export const transportModeFromRoute = (route?: {
|
||||
protocol?: string
|
||||
relayProtocol?: string
|
||||
}): TransportMode => {
|
||||
// relayProtocol is the client→TURN leg; protocol alone means no relay.
|
||||
if (route?.relayProtocol === 'tls') return 'turn-tls'
|
||||
if (route?.relayProtocol === 'tcp') return 'turn-tcp'
|
||||
if (route?.relayProtocol === 'udp') return 'turn-udp'
|
||||
if (route?.protocol === 'tcp') return 'tcp'
|
||||
return 'auto'
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Room } from 'livekit-client'
|
||||
|
||||
export type TrackRow = {
|
||||
key: string
|
||||
dir: 'up' | 'down'
|
||||
label: string
|
||||
codec?: string
|
||||
kbps: number
|
||||
fps?: number
|
||||
res?: string
|
||||
}
|
||||
|
||||
export type StatsSnapshot = {
|
||||
ts: number
|
||||
/** wire totals from transport stats (includes headers, RTCP, FEC) */
|
||||
upKbps: number
|
||||
downKbps: number
|
||||
rttMs?: number
|
||||
/** worst inbound RTP jitter across subscribed tracks */
|
||||
jitterMs?: number
|
||||
availableOutKbps?: number
|
||||
/** selected ICE route of the publisher transport (measured, not assumed) */
|
||||
route?: { protocol?: string; type?: string; relayProtocol?: string }
|
||||
/**
|
||||
* relayProtocol values of gathered relay local candidates — i.e. which
|
||||
* client→TURN transports are actually configured (udp/tcp/tls). Empty
|
||||
* when no TURN server is configured.
|
||||
*/
|
||||
turnProtocols: string[]
|
||||
tracks: TrackRow[]
|
||||
}
|
||||
|
||||
type StatDict = Record<string, unknown>
|
||||
type Counters = Record<string, number>
|
||||
|
||||
const asNumber = (v: unknown): number | undefined =>
|
||||
typeof v === 'number' && Number.isFinite(v) ? v : undefined
|
||||
|
||||
const asString = (v: unknown): string | undefined =>
|
||||
typeof v === 'string' ? v : undefined
|
||||
|
||||
const shortCodec = (mimeType?: string) =>
|
||||
mimeType ? mimeType.replace(/^(audio|video)\//, '') : undefined
|
||||
|
||||
const buildTrackLabels = (room: Room) => {
|
||||
const labels = new Map<string, string>()
|
||||
room.localParticipant.trackPublications.forEach((pub) => {
|
||||
const id = pub.track?.mediaStreamTrack?.id
|
||||
if (id) labels.set(id, `local ${pub.source}`)
|
||||
})
|
||||
room.remoteParticipants.forEach((participant) => {
|
||||
// Keep rows scannable: participant names capped at 10 chars.
|
||||
const rawName = participant.name || participant.identity
|
||||
const name = rawName.length > 10 ? `${rawName.slice(0, 10)}.` : rawName
|
||||
participant.trackPublications.forEach((pub) => {
|
||||
const id = pub.track?.mediaStreamTrack?.id
|
||||
if (id) labels.set(id, `${name} ${pub.source}`)
|
||||
})
|
||||
})
|
||||
return labels
|
||||
}
|
||||
|
||||
export const useWebRTCStats = (
|
||||
room: Room,
|
||||
enabled: boolean,
|
||||
intervalMs = 1000
|
||||
) => {
|
||||
// Single source of truth: the snapshot is just the last history entry.
|
||||
const [history, setHistory] = useState<StatsSnapshot[]>([])
|
||||
const prevRef = useRef(new Map<string, Counters>())
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return
|
||||
|
||||
let cancelled = false
|
||||
const prev = prevRef.current
|
||||
|
||||
/** Per-stat counter deltas; returns 0 on the first sighting. */
|
||||
const deltas = (key: string, now: Counters): Counters => {
|
||||
const before = prev.get(key)
|
||||
prev.set(key, now)
|
||||
const out: Counters = {}
|
||||
for (const [name, value] of Object.entries(now)) {
|
||||
out[name] = before?.[name] !== undefined ? value - before[name] : 0
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const collect = async () => {
|
||||
const reports: Array<{ pc: 'pub' | 'sub'; report: RTCStatsReport }> = []
|
||||
try {
|
||||
// Not public API — see file header.
|
||||
const manager = room.engine?.pcManager
|
||||
const pub = await manager?.publisher?.getStats()
|
||||
if (pub) reports.push({ pc: 'pub', report: pub })
|
||||
const sub = await manager?.subscriber?.getStats()
|
||||
if (sub) reports.push({ pc: 'sub', report: sub })
|
||||
} catch {
|
||||
// Engine not ready or SDK internals changed; panel shows nothing.
|
||||
}
|
||||
if (cancelled || reports.length === 0) return
|
||||
|
||||
const labels = buildTrackLabels(room)
|
||||
const tracks: TrackRow[] = []
|
||||
let upKbps = 0
|
||||
let downKbps = 0
|
||||
let rttMs: number | undefined
|
||||
let jitterMs: number | undefined
|
||||
let availableOutKbps: number | undefined
|
||||
let route: StatsSnapshot['route']
|
||||
const turnProtocols = new Set<string>()
|
||||
|
||||
for (const { pc, report } of reports) {
|
||||
const byId = new Map<string, StatDict>()
|
||||
report.forEach((stat) => byId.set(stat.id as string, stat as StatDict))
|
||||
|
||||
report.forEach((raw) => {
|
||||
const stat = raw as StatDict
|
||||
const type = asString(stat.type)
|
||||
const ts = asNumber(stat.timestamp) ?? Date.now()
|
||||
const key = `${pc}:${asString(stat.id) ?? ''}`
|
||||
|
||||
if (
|
||||
type === 'local-candidate' &&
|
||||
asString(stat.candidateType) === 'relay'
|
||||
) {
|
||||
// Relay candidates are only gathered when a TURN server is
|
||||
// configured and reachable; relayProtocol says how the client
|
||||
// reaches it (udp/tcp/tls).
|
||||
turnProtocols.add(
|
||||
asString(stat.relayProtocol) ?? asString(stat.protocol) ?? 'udp'
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'transport') {
|
||||
const d = deltas(key, {
|
||||
sent: asNumber(stat.bytesSent) ?? 0,
|
||||
received: asNumber(stat.bytesReceived) ?? 0,
|
||||
ts,
|
||||
})
|
||||
if (d.ts > 0) {
|
||||
upKbps += Math.max(0, (d.sent * 8) / d.ts)
|
||||
downKbps += Math.max(0, (d.received * 8) / d.ts)
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'candidate-pair' && stat.nominated === true) {
|
||||
const rtt = asNumber(stat.currentRoundTripTime)
|
||||
if (rtt !== undefined) rttMs = Math.round(rtt * 1000)
|
||||
const available = asNumber(stat.availableOutgoingBitrate)
|
||||
if (available !== undefined && pc === 'pub') {
|
||||
availableOutKbps = Math.round(available / 1000)
|
||||
}
|
||||
if (pc === 'pub') {
|
||||
const local = byId.get(asString(stat.localCandidateId) ?? '')
|
||||
route = {
|
||||
protocol: asString(local?.protocol),
|
||||
type: asString(local?.candidateType),
|
||||
relayProtocol: asString(local?.relayProtocol),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'outbound-rtp' || type === 'inbound-rtp') {
|
||||
const isUp = type === 'outbound-rtp'
|
||||
|
||||
if (!isUp) {
|
||||
const jitter = asNumber(stat.jitter)
|
||||
if (jitter !== undefined) {
|
||||
const ms = Math.round(jitter * 1000)
|
||||
if (jitterMs === undefined || ms > jitterMs) jitterMs = ms
|
||||
}
|
||||
}
|
||||
|
||||
const d = deltas(key, {
|
||||
bytes: asNumber(isUp ? stat.bytesSent : stat.bytesReceived) ?? 0,
|
||||
ts,
|
||||
})
|
||||
const kbps = d.ts > 0 ? Math.max(0, (d.bytes * 8) / d.ts) : 0
|
||||
|
||||
// Resolve codec + source track.
|
||||
const codecStat = byId.get(asString(stat.codecId) ?? '')
|
||||
let msTrackId = asString(stat.trackIdentifier)
|
||||
if (!msTrackId && isUp) {
|
||||
const mediaSource = byId.get(asString(stat.mediaSourceId) ?? '')
|
||||
msTrackId = asString(mediaSource?.trackIdentifier)
|
||||
}
|
||||
const rid = asString(stat.rid)
|
||||
const baseLabel =
|
||||
(msTrackId && labels.get(msTrackId)) ??
|
||||
`${asString(stat.kind) ?? 'media'} ssrc ${asNumber(stat.ssrc) ?? '?'}`
|
||||
|
||||
const width = asNumber(stat.frameWidth)
|
||||
const height = asNumber(stat.frameHeight)
|
||||
|
||||
tracks.push({
|
||||
key,
|
||||
dir: isUp ? 'up' : 'down',
|
||||
label: rid ? `${baseLabel} [${rid}]` : baseLabel,
|
||||
codec: shortCodec(asString(codecStat?.mimeType)),
|
||||
kbps,
|
||||
fps: asNumber(stat.framesPerSecond),
|
||||
res: width && height ? `${width}x${height}` : undefined,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Stable order (direction, then label): sorting by bitrate would
|
||||
// reshuffle rows on every tick as kbps fluctuates.
|
||||
tracks.sort((a, b) =>
|
||||
a.dir === b.dir
|
||||
? a.label.localeCompare(b.label)
|
||||
: a.dir === 'up'
|
||||
? -1
|
||||
: 1
|
||||
)
|
||||
|
||||
const next: StatsSnapshot = {
|
||||
ts: Date.now(),
|
||||
upKbps: Math.round(upKbps),
|
||||
downKbps: Math.round(downKbps),
|
||||
rttMs,
|
||||
jitterMs,
|
||||
availableOutKbps,
|
||||
route,
|
||||
turnProtocols: Array.from(turnProtocols).sort(),
|
||||
tracks,
|
||||
}
|
||||
setHistory((h) => [...h.slice(-59), next])
|
||||
}
|
||||
|
||||
void collect()
|
||||
const id = window.setInterval(() => void collect(), intervalMs)
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.clearInterval(id)
|
||||
}
|
||||
}, [room, enabled, intervalMs])
|
||||
|
||||
return { snapshot: history[history.length - 1], history }
|
||||
}
|
||||
@@ -43,7 +43,6 @@ import { useSnapshot } from 'valtio'
|
||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||
import { userStore } from '@/stores/user'
|
||||
import { WatchMediaDeviceErrors } from './WatchMediaDeviceErrors'
|
||||
import { MeetDevtools } from '@/features/devtools'
|
||||
import { VOICE_AUDIO_CONSTRAINTS } from '@/features/rooms/livekit/utils/constants'
|
||||
|
||||
export const Conference = ({
|
||||
@@ -299,7 +298,6 @@ export const Conference = ({
|
||||
<VideoConference />
|
||||
{!isMobile && <InviteDialog mode={mode} />}
|
||||
<PictureInPictureConference />
|
||||
<MeetDevtools />
|
||||
</LiveKitRoom>
|
||||
</Screen>
|
||||
</QueryAware>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useLocalParticipant } from '@livekit/components-react'
|
||||
import { TrackSource } from '@livekit/protocol'
|
||||
import { useEffect } from 'react'
|
||||
import { useCanPublishTrack } from '@/features/rooms/livekit/hooks/useCanPublishTrack'
|
||||
|
||||
export const MEDIA_STATE_ELEMENT_ID = 'media-state'
|
||||
export const MEDIA_STATE_CHANGED_EVENT = 'media-state-changed'
|
||||
@@ -9,8 +7,6 @@ export const MEDIA_STATE_CHANGED_EVENT = 'media-state-changed'
|
||||
export type MediaStateChangedDetail = {
|
||||
microphoneEnabled: boolean
|
||||
cameraEnabled: boolean
|
||||
canPublishMicrophone: boolean
|
||||
canPublishCamera: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,15 +16,9 @@ export type MediaStateChangedDetail = {
|
||||
*
|
||||
* const el = document.getElementById('media-state')
|
||||
* new MutationObserver(...).observe(el, { attributes: true })
|
||||
*
|
||||
* The publish permissions are exposed alongside the state: an external tool
|
||||
* cannot tell a muted microphone from one it is not allowed to unmute, and
|
||||
* would otherwise offer a control that silently does nothing.
|
||||
*/
|
||||
export const MediaStateObserver = () => {
|
||||
const { isMicrophoneEnabled, isCameraEnabled } = useLocalParticipant()
|
||||
const canPublishMicrophone = useCanPublishTrack(TrackSource.MICROPHONE)
|
||||
const canPublishCamera = useCanPublishTrack(TrackSource.CAMERA)
|
||||
|
||||
useEffect(() => {
|
||||
window.dispatchEvent(
|
||||
@@ -36,17 +26,10 @@ export const MediaStateObserver = () => {
|
||||
detail: {
|
||||
microphoneEnabled: isMicrophoneEnabled,
|
||||
cameraEnabled: isCameraEnabled,
|
||||
canPublishMicrophone,
|
||||
canPublishCamera,
|
||||
},
|
||||
})
|
||||
)
|
||||
}, [
|
||||
isMicrophoneEnabled,
|
||||
isCameraEnabled,
|
||||
canPublishMicrophone,
|
||||
canPublishCamera,
|
||||
])
|
||||
}, [isMicrophoneEnabled, isCameraEnabled])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -54,8 +37,6 @@ export const MediaStateObserver = () => {
|
||||
style={{ display: 'none' }}
|
||||
data-microphone-enabled={isMicrophoneEnabled ? 'true' : 'false'}
|
||||
data-camera-enabled={isCameraEnabled ? 'true' : 'false'}
|
||||
data-can-publish-microphone={canPublishMicrophone ? 'true' : 'false'}
|
||||
data-can-publish-camera={canPublishCamera ? 'true' : 'false'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import { Info } from './Info'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar'
|
||||
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
|
||||
import { useEscapeToClose } from '@/hooks/useEscapeToClose'
|
||||
import { srOnly } from '@/styles/a11y'
|
||||
|
||||
type StyledSidePanelProps = {
|
||||
title: string
|
||||
@@ -26,7 +24,6 @@ type StyledSidePanelProps = {
|
||||
onClose: () => void
|
||||
isClosed: boolean
|
||||
closeButtonTooltip: string
|
||||
escapeHint: string
|
||||
isSubmenu: boolean
|
||||
onBack: () => void
|
||||
backButtonLabel: string
|
||||
@@ -43,7 +40,6 @@ const StyledSidePanel = React.forwardRef<HTMLElement, StyledSidePanelProps>(
|
||||
isClosed,
|
||||
isReactionToolbarOpen,
|
||||
closeButtonTooltip,
|
||||
escapeHint,
|
||||
isSubmenu = false,
|
||||
onBack,
|
||||
backButtonLabel,
|
||||
@@ -88,11 +84,7 @@ const StyledSidePanel = React.forwardRef<HTMLElement, StyledSidePanelProps>(
|
||||
}}
|
||||
aria-hidden={isClosed}
|
||||
aria-label={ariaLabel}
|
||||
aria-describedby="side-panel-escape-hint"
|
||||
>
|
||||
<span id="side-panel-escape-hint" className={srOnly}>
|
||||
{escapeHint}
|
||||
</span>
|
||||
<HStack alignItems="center">
|
||||
{isSubmenu && (
|
||||
<Button
|
||||
@@ -202,8 +194,6 @@ export const SidePanel = () => {
|
||||
activeKey: activePanelId,
|
||||
})
|
||||
|
||||
useEscapeToClose(isSidePanelOpen, asideRef, closeSidePanel)
|
||||
|
||||
return (
|
||||
<StyledSidePanel
|
||||
ref={asideRef}
|
||||
@@ -213,7 +203,6 @@ export const SidePanel = () => {
|
||||
closeButtonTooltip={t('closeButton', {
|
||||
content: t(`content.${activeSubPanelId || activePanelId}`),
|
||||
})}
|
||||
escapeHint={t('escapeHint')}
|
||||
isClosed={!isSidePanelOpen}
|
||||
isSubmenu={isSubPanelOpen}
|
||||
isReactionToolbarOpen={isReactionToolbarOpen}
|
||||
|
||||
+2
-6
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||
import { Button, Popover } from '@/primitives'
|
||||
import { RiArrowUpSLine, RiImageCircleAiFill } from '@remixicon/react'
|
||||
import { Track, type VideoCaptureOptions, VideoPresets } from 'livekit-client'
|
||||
import { Track, type VideoCaptureOptions } from 'livekit-client'
|
||||
|
||||
import { ToggleDevice } from './ToggleDevice'
|
||||
import { css } from '@/styled-system/css'
|
||||
@@ -57,8 +57,7 @@ export const VideoDeviceControl = ({
|
||||
}: VideoDeviceControlProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
||||
|
||||
const { videoDeviceId, processorConfig, videoPublishResolution } =
|
||||
useSnapshot(userChoicesStore)
|
||||
const { videoDeviceId, processorConfig } = useSnapshot(userChoicesStore)
|
||||
|
||||
const onChange = React.useCallback(
|
||||
(enabled: boolean, isUserInitiated: boolean) =>
|
||||
@@ -98,9 +97,6 @@ export const VideoDeviceControl = ({
|
||||
|
||||
await toggle(!trackProps.enabled, {
|
||||
processor: processor,
|
||||
...(videoPublishResolution && {
|
||||
resolution: VideoPresets[videoPublishResolution].resolution,
|
||||
}),
|
||||
} as VideoCaptureOptions)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,7 @@ const EMPTY_PROPS = {}
|
||||
|
||||
export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
const { t } = useTranslation('settings', { keyPrefix: 'video' })
|
||||
const room = useRoomContext()
|
||||
const { localParticipant, remoteParticipants } = room
|
||||
const { localParticipant, remoteParticipants } = useRoomContext()
|
||||
|
||||
const {
|
||||
videoDeviceId,
|
||||
@@ -71,20 +70,19 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
}
|
||||
|
||||
const handleVideoResolutionChange = async (key: 'h720' | 'h360' | 'h180') => {
|
||||
saveVideoPublishResolution(key)
|
||||
const videoTrack = localParticipant.getTrackPublication(
|
||||
const videoPublication = localParticipant.getTrackPublication(
|
||||
Track.Source.Camera
|
||||
)?.track
|
||||
if (!videoTrack) {
|
||||
return
|
||||
)
|
||||
const videoTrack = videoPublication?.track
|
||||
if (videoTrack) {
|
||||
saveVideoPublishResolution(key)
|
||||
await videoTrack.restartTrack({
|
||||
resolution: VideoPresets[key].resolution,
|
||||
deviceId: { exact: videoDeviceId },
|
||||
processor:
|
||||
BackgroundProcessorFactory.fromProcessorConfig(processorConfig),
|
||||
})
|
||||
}
|
||||
|
||||
await videoTrack.restartTrack({
|
||||
resolution: VideoPresets[key].resolution,
|
||||
deviceId: { exact: videoDeviceId },
|
||||
processor:
|
||||
BackgroundProcessorFactory.fromProcessorConfig(processorConfig),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useEffect, useRef, type RefObject } from 'react'
|
||||
|
||||
export const useEscapeToClose = (
|
||||
isActive: boolean,
|
||||
containerRef: RefObject<HTMLElement | null>,
|
||||
onClose: () => void
|
||||
) => {
|
||||
const onCloseRef = useRef(onClose)
|
||||
useEffect(() => {
|
||||
onCloseRef.current = onClose
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) return
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return
|
||||
if (!containerRef.current?.contains(document.activeElement)) return
|
||||
e.stopPropagation()
|
||||
onCloseRef.current()
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [isActive, containerRef])
|
||||
}
|
||||
@@ -435,8 +435,7 @@
|
||||
"tools": "Weitere Tools",
|
||||
"info": "Meeting-Informationen"
|
||||
},
|
||||
"closeButton": "{{content}} ausblenden (Escape)",
|
||||
"escapeHint": "Escape drücken zum Schließen"
|
||||
"closeButton": "{{content}} ausblenden"
|
||||
},
|
||||
"chat": {
|
||||
"disclaimer": "Die Nachrichten sind nur für Teilnehmende zum Zeitpunkt des Sendens sichtbar. Alle Nachrichten werden am Ende des Meetings gelöscht.",
|
||||
|
||||
@@ -435,8 +435,7 @@
|
||||
"tools": "more tools",
|
||||
"info": "meeting information"
|
||||
},
|
||||
"closeButton": "Hide {{content}} (Escape)",
|
||||
"escapeHint": "Press Escape to close"
|
||||
"closeButton": "Hide {{content}}"
|
||||
},
|
||||
"chat": {
|
||||
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call.",
|
||||
|
||||
@@ -435,8 +435,7 @@
|
||||
"tools": "outils de réunion",
|
||||
"info": "informations sur la réunion"
|
||||
},
|
||||
"closeButton": "Masquer {{content}} (Échap)",
|
||||
"escapeHint": "Appuyez sur Échap pour fermer"
|
||||
"closeButton": "Masquer {{content}}"
|
||||
},
|
||||
"chat": {
|
||||
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel.",
|
||||
|
||||
@@ -435,8 +435,7 @@
|
||||
"tools": "meer tools",
|
||||
"info": "vergaderinformatie"
|
||||
},
|
||||
"closeButton": "Verberg {{content}} (Escape)",
|
||||
"escapeHint": "Druk op Escape om te sluiten"
|
||||
"closeButton": "Verberg {{content}}"
|
||||
},
|
||||
"chat": {
|
||||
"disclaimer": "De berichten zijn alleen voor de deelnemers zichtbaar op het moment dat ze worden verzonden. Alle berichten worden verwijderd aan het einde van het gesprek.",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.30.0",
|
||||
"version": "1.29.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "1.30.0",
|
||||
"version": "1.29.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.6.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.30.0",
|
||||
"version": "1.29.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.30.0",
|
||||
"version": "1.29.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "1.30.0",
|
||||
"version": "1.29.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.30.0",
|
||||
"version": "1.29.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "1.30.0"
|
||||
requires-python = ">=3.13"
|
||||
version = "1.29.0"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
Generated
-1891
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user