feat(voz): legenda por ênfase, forced align, IA local e correções de zoom/revisão

Trabalho da branch feat/revisao-enfases: pipeline de edição por voz ganha
alinhamento forçado (whisperx), roteirização por LLM local (Ollama), e a
etapa 5 (revisão de frases) passa a refletir de verdade o que é aplicado.

- generate_subtitles_by_emphasis: legenda comum cobre o clipe inteiro,
  legenda dinâmica só nas frases de ênfase, e a comum é desativada
  (enabled="0") onde a dinâmica cobre, em vez de nunca ser gerada ali.
- validate_subtitle_layout ignora títulos com enabled="0" — corrige falso
  positivo de colisão contra o que está desativado no lugar dele.
- Corrige zoom/marcador sendo descartado quando a borda encosta exatamente
  no início de um corte.
- Etapa 5 do Assistente: recarrega quando as decisões da IA mudam (com
  fresh=true, ignorando a revisão salva antiga) — resolve a dessincronia
  entre "ativa" na tela e o que já foi cortado no FCPXML.
- Etapa "Processar" reaplica as decisões da revisão (_phrase_actions.json)
  antes da cadeia de remoção de silêncio/legendas — antes, desativar uma
  frase na etapa 5 não tinha efeito nenhum no vídeo final.
- Etapa "Concluído" fundida em "Processar" — abrir no Final Cut/Finder
  aparece assim que termina, sem slide extra.
- Palavra clicável na etapa 5 agora funciona como toggle (clique de novo
  desfaz) e mostra a própria ênfase (sublinhado colorido + peso da fonte).
- fcpxml/forced_align.py, fcpxml/llm_local.py, ai_edit.py: alinhamento
  fonético via whisperx e roteirização local via Ollama/Gemma.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
João Henrique
2026-08-21 18:26:04 -04:00
co-authored by Claude Sonnet 5
parent 711c397dfe
commit 7b5aed79ee
36 changed files with 2922 additions and 624 deletions
+15 -5
View File
@@ -28,11 +28,21 @@ minutos.
`apply_voice_actions` aplica direto, mas é para **teste**. O produto do seu
trabalho é a lista de decisões.
**Para onde ela vai:** o usuário cola o seu JSON no app, e ele abre na etapa 5
do Assistente — uma tela onde cada frase do roteiro aparece com a sua decisão
já marcada, para ser revisada antes de gerar. Você é o **ponto de partida** da
edição, não a palavra final; escreva decisões defensáveis e motivos legíveis.
Como o app traduz cada ação sua: `criterios/10-revisao-humana.md`.
**Caminho automatizado (sem wizard, sem copiar-e-colar):** a tool
`generate_voice_script` (MCP) / comando `generate_voice_script` (ponte do app)
corre o fluxo fechado: transcreve → `build_voice_timeline` → entrega a timeline
a um **modelo local Ollama (Gemma 3 / Llama)** que age exatamente como este
skill descreve (separa roteiro de bastidor, escolhe tomadas, decide zoom/corte)
→ devolve o roteiro legível **e** o JSON de ações, e opcionalmente aplica no
FCPXML. O cliente fica em `code/fcpxml/llm_local.py`; o prompt que embute este
contrato está em `_SYSTEM_PROMPT`. Use essa tool quando o usuário pedir para
"rodar tudo internamente" ou "gerar o roteiro por IA local".
**Para onde ela vai (modo manual):** o usuário cola o seu JSON no app, e ele
abre na etapa 5 do Assistente — uma tela onde cada frase do roteiro aparece com
a sua decisão já marcada, para ser revisada antes de gerar. Você é o **ponto de
partida** da edição, não a palavra final; escreva decisões defensáveis e motivos
legíveis. Como o app traduz cada ação sua: `criterios/10-revisao-humana.md`.
## Ordem de trabalho
@@ -53,14 +53,19 @@ use para decidir; existem para permitir a reanálise da Fase 2.
## O timestamp por palavra tem um viés conhecido
O início de cada palavra vem sistematicamente **adiantado em ~0,3-0,5s** em
relação ao ataque real da fala — medido em material real com ffmpeg
(`astats`), consistente em 6 pontos do mesmo vídeo. O fim da palavra não
tem esse problema (erro de poucos centésimos). Causa: `word_timestamps` do
faster-whisper deriva por atenção cruzada, sem alinhamento forçado — ver
`05_EXPERIENCIAS.md`, entrada de 2026-08-19.
relação ao ataque real da fala — medido em material real com ffmpeg (`astats`),
consistente em 6 pontos do mesmo vídeo. O fim da palavra não tem esse problema
(erro de poucos centésimos). Causa: `word_timestamps` do faster-whisper deriva
por atenção cruzada, sem alinhamento forçado — ver `05_EXPERIENCIAS.md`, entrada
de 2026-08-19.
**Quando o pipeline já corrigiu isso:** se `layers.alignment` for `true`
(transcript gerado com alinhamento forçado fonético via whisperx, implementado
depois desse aviso), o viés foi removido na origem — **não aplique o offset
manual** abaixo. O aviso vale só para transcripts antigos sem `layers.alignment`.
Isso não é "reestimar no olho" — é um bug de medição na fonte, não um
julgamento seu. Na prática:
julgamento seu. Na prática (somente sem `layers.alignment`):
- Ao posicionar um `zoom` cujo `start` precisa cair exatamente na palavra
(não uma frase inteira), some **+0,3 a +0,4s** ao timestamp do JSON antes
@@ -69,6 +74,3 @@ julgamento seu. Na prática:
- **Não aplique essa correção a `gap_before` para decidir corte** — a régua
de silêncio (`06-texto-corte-marcador.md`) já é conservadora o bastante
para absorver esse erro; corrigir os dois ao mesmo tempo é redundante.
- Se um dia o pipeline ganhar alinhamento forçado (WhisperX), este aviso
perde a razão de existir — confira se `layers` ou a versão do documento
já indicam isso antes de aplicar o offset manualmente.
+12 -5
View File
@@ -9,7 +9,7 @@ normalmente; a regra é sobre a comunicação com o usuário.
## What This Is
MCP server that reads/writes Final Cut Pro XML (FCPXML) files. 74 tools for timeline analysis, batch editing, QC, generation, multi-track support, media relink, NLE export, transcript-based editing (local Whisper), and LIVE FCP control (push_to_fcp / list_fcp_libraries via Apple events). Reads FCPXML 1.8–1.14 (incl. `.fcpxmld` bundles with sidecar preservation), writes 1.13 by default. Dual-mode (XML + Live) direction: `code/docs/CAPABILITY-AUDIT-2026-06.md`.
MCP server that reads/writes Final Cut Pro XML (FCPXML) files. 77 tools for timeline analysis, batch editing, QC, generation, multi-track support, media relink, NLE export, transcript-based editing (local Whisper), LIVE FCP control (push_to_fcp / list_fcp_libraries via Apple events), and local-LLM voice scripting (editar-por-voz against Ollama/Gemma 3). Reads FCPXML 1.8–1.14 (incl. `.fcpxmld` bundles with sidecar preservation), writes 1.13 by default. Dual-mode (XML + Live) direction: `code/docs/CAPABILITY-AUDIT-2026-06.md`.
## Architecture
@@ -21,7 +21,7 @@ timeline — as duas delegam a `fcpxml/`.
```
code/server.py — MCP entry point (592 linhas). Só dispatch: TOOL_HANDLERS.
code/server_tools/ — Os handlers das 74 tools, um módulo por categoria.
code/server_tools/ — Os handlers das 77 tools, um módulo por categoria.
code/server_tools/_shared/ — Helpers compartilhados (paths, project, formatting,
captions, detection, media).
@@ -135,12 +135,19 @@ não passar. Equivalente a rodar manualmente os dois comandos abaixo.
Sempre que uma alteração for feita no app (MacApp/) durante o período de
implementação, **compile e rode o programa localmente no computador** para
validar visualmente a alteração, além de rodar os testes:
validar visualmente a alteração, além de rodar os testes. O comando padrão
para isso — que fecha a instância anterior, recompila e abre o app para
conferência — é:
```bash
cd code && ./MacApp/build_app.sh --run # compila e abre o app localmente
admin/run_app.command # compila e abre o app localmente (padrão de revisão)
```
Equivalente a `cd code && ./MacApp/build_app.sh --run`, mas desacoplado do
Terminal. **Toda vez que uma alteração for concluída, rode este arquivo
automaticamente** para já conseguirmos revisar o que foi feito antes de
fechar a tarefa.
Regra geral: após qualquer alteração, o app deve ser executado localmente
antes de concluir a tarefa. Se houver erro de compilação, corrija antes de
seguir.
@@ -154,7 +161,7 @@ CI runs both on every push to main. If either fails, the commit gets an X on Git
## Testing
1454 tests across 42 files, all under `code/tests/`. Um teste fora dessa pasta não roda (`testpaths = ["tests"]`) — se você criar um em outro lugar, confirme que a contagem total subiu. Cobertura por área: `test_models.py` (TimeValue/Timecode/Clip/Timeline), `test_writer.py` (insert/marker/trim/delete/split/speed), `test_server.py` (handlers e dispatch), `test_rough_cut.py`, `test_features_v05.py` (connected clips, roles, diff, reformat, silêncio, export), `test_marker_pipeline.py`, `test_refactored_helpers.py`, `test_transcribe.py`, `test_media_intel.py` (pula sem ffmpeg; o CI instala), `test_phrase_review.py` (revisão de frases da etapa 5) e `test_models_api.py` (comandos da ponte). Fixtures: `examples/sample.fcpxml` e XML inline. Os testes criam temporários e limpam depois.
1498 tests across 43 files, all under `code/tests/`. Um teste fora dessa pasta não roda (`testpaths = ["tests"]`) — se você criar um em outro lugar, confirme que a contagem total subiu. Cobertura por área: `test_models.py` (TimeValue/Timecode/Clip/Timeline), `test_writer.py` (insert/marker/trim/delete/split/speed), `test_server.py` (handlers e dispatch), `test_rough_cut.py`, `test_features_v05.py` (connected clips, roles, diff, reformat, silêncio, export), `test_marker_pipeline.py`, `test_refactored_helpers.py`, `test_transcribe.py`, `test_media_intel.py` (pula sem ffmpeg; o CI instala), `test_phrase_review.py` (revisão de frases da etapa 5) e `test_models_api.py` (comandos da ponte). Fixtures: `examples/sample.fcpxml` e XML inline. Os testes criam temporários e limpam depois.
## FCPXML Gotchas
+14
View File
@@ -105,6 +105,20 @@ def _project_media_paths(path: str) -> list[str]:
media_paths.append(mp)
return media_paths
def _project_media_rotations(path: str) -> dict[str, float]:
"""Degrees each source media was rotated by via a Transform filter on its
clip in the FCPXML — keyed by the same resolved media path
``_project_media_paths`` returns, so the two can be joined by media_path."""
proj = parse_fcpxml(path)
tl = proj.primary_timeline or (proj.timelines[0] if proj.timelines else None)
rotations: dict[str, float] = {}
if tl is not None:
for clip in getattr(tl, "clips", []):
mp = media_src_to_path(clip.media_path or "")
if mp and clip.rotation:
rotations[mp] = clip.rotation
return rotations
def _voice_timeline_json_path(media_path: str, output_dir: str = "") -> Path:
p = Path(media_path)
if output_dir:
+95 -1
View File
@@ -7,6 +7,7 @@ from __future__ import annotations
import asyncio
import json
import re
from pathlib import Path
from fcpxml.model_manager import (
@@ -23,6 +24,7 @@ from .shared import (
_load_cached_transcript,
_load_cached_voice_timeline,
_project_media_paths,
_project_media_rotations,
_transcript_json_path,
_voice_timeline_json_path,
)
@@ -54,6 +56,7 @@ def cmd_analyze_voice(args: dict) -> int:
try:
media_paths = _project_media_paths(path)
rotations = _project_media_rotations(path)
except Exception as exc:
shared.emit({"ok": False, "error": f"Erro ao ler o projeto: {exc}"})
return 1
@@ -93,7 +96,7 @@ def cmd_analyze_voice(args: dict) -> int:
contents = asyncio.run(handle_build_voice_timeline({
"media_path": mp, "model": model, "language": language,
"hf_token": token, "num_speakers": num_speakers,
"output_dir": output_dir,
"output_dir": output_dir, "rotation": rotations.get(mp, 0.0),
}))
except Exception as exc:
shared.emit({"ok": False, "error": f"Falha analisando {Path(mp).name}: {exc}"})
@@ -204,3 +207,94 @@ def cmd_apply_voice_actions(args: dict) -> int:
break
shared.emit({"ok": True, "path": out_path, "message": message})
return 0
def cmd_generate_voice_script(args: dict) -> int:
"""Run the ENTIRE voice-edit pass against a LOCAL model, inside the engine.
Transcribe (cached) -> build the voice timeline -> hand it to a local
Ollama model (Gemma 3 / Llama) that directs the edit -> return the readable
script (roteiro) and the action JSON, and optionally apply to a FCPXML. No
wizard, no copy-paste: the model's decisions are validated and applied by
the same pipeline the rules engine uses.
Args (all optional except one of ``media_path`` / ``voice_timeline``):
media_path audio/video to analyze and direct (required when there is
no voice_timeline yet)
voice_timeline path to an existing _voice_timeline.json; when given the
analysis is reused and media_path is not required
filepath optional FCPXML to apply the decisions to (non-destructive)
model local model Ollama serves (default gemma3:12b)
base_url Ollama base URL (default http://localhost:11434)
model_size whisper size if transcription is needed
language ISO language hint for transcription
hf_token HuggingFace token for diarization
num_speakers known speaker count, if any
output_dir folder for the timeline/review/actions JSON
apply_to_fcpxml apply to filepath when given (default true)
-> {"ok": true, "message": "...", "roteiro_path", "actions_path",
"applied_path"} or {"ok": false, "error": "..."}
"""
media_path = str(args.get("media_path", ""))
voice_timeline = str(args.get("voice_timeline", ""))
if not voice_timeline and (not media_path or not Path(media_path).exists()):
shared.emit({"ok": False, "error": "Arquivo de mídia não encontrado (informe media_path ou voice_timeline)."})
return 1
from server import handle_generate_voice_script
try:
contents = asyncio.run(handle_generate_voice_script({
"media_path": media_path,
"voice_timeline": args.get("voice_timeline"),
"filepath": args.get("filepath"),
"model": args.get("model"),
"base_url": args.get("base_url"),
"model_size": args.get("model_size"),
"language": args.get("language"),
"hf_token": args.get("hf_token"),
"num_speakers": args.get("num_speakers"),
"output_dir": args.get("output_dir"),
"apply_to_fcpxml": args.get("apply_to_fcpxml", True),
}))
except Exception as exc:
shared.emit({"ok": False, "error": f"Falha ao gerar roteiro por IA local: {exc}"})
return 1
message = "\n".join(getattr(c, "text", str(c)) for c in contents)
def _path_after(label: str) -> str:
m = re.search(rf"\*\*{label}\*\*: (.+)", message)
return m.group(1).strip() if m else ""
roteiro_path = _path_after(r"Roteiro \(legível\)")
actions_path = _path_after("Ações JSON")
applied_path = ""
for line in message.splitlines():
if line.startswith("- **Saved to**:"):
applied_path = line.split("`")[1] if "`" in line else ""
break
shared.emit({
"ok": True,
"message": message,
"roteiro_path": roteiro_path,
"actions_path": actions_path,
"applied_path": applied_path,
})
return 0
def cmd_list_ollama_models(args: dict) -> int:
"""List the models Ollama currently serves, for the app's model picker.
Args:
base_url Ollama base URL (default http://localhost:11434)
-> {"ok": true, "models": ["gemma3:12b", ...]} (empty list if Ollama
is unreachable, so the UI can fall back to a text field)
"""
from fcpxml.llm_local import list_ollama_models
base_url = str(args.get("base_url") or "http://localhost:11434")
models = list_ollama_models(base_url=base_url)
shared.emit({"ok": True, "models": models})
return 0
+23
View File
@@ -68,6 +68,27 @@ Commands:
-> {"ok": true, "review_path", "actions_path", "emphasis_count",
"removed_count"}
generate_voice_script {"media_path": "...", "voice_timeline": "...", "filepath": "...",
"model": "gemma3:12b", "base_url": "http://localhost:11434",
"model_size": "base", "language": "pt"|"auto"|null,
"hf_token": "..."|null, "num_speakers": ""|null,
"output_dir": "...", "apply_to_fcpxml": true}
The WHOLE voice-edit pass run INSIDE the engine against a LOCAL model
(Ollama running Gemma 3 / Llama) — no wizard, no copy-paste. If
`voice_timeline` is given (the analysis from an earlier step), it is
reused and the transcription/acoustics are skipped; otherwise
`media_path` is transcribed and analyzed. Then the local model directs
the edit -> returns the readable script (roteiro) + the action JSON,
and optionally applies to `filepath` (FCPXML, non-destructive).
-> {"ok": true, "message", "roteiro_path", "actions_path", "applied_path"}
or {"ok": false, "error": "..."}
list_ollama_models {"base_url": "http://localhost:11434"}
Lists the models Ollama currently serves, for the app's model picker
in step 4 (generate script by local AI). Empty list if Ollama is
unreachable, so the UI falls back to a free-text field.
-> {"ok": true, "models": ["gemma3:12b", ...]}
dynamic_subtitle_config {}
-> {"ok": true, "band_height", "block_center_y", "line_gap", "font",
"font_size", "emphasis_font", "emphasis_face", "emphasis_size",
@@ -220,6 +241,8 @@ def main() -> int:
"plain_subtitle_config": subtitles.cmd_plain_subtitle_config,
"set_plain_subtitle_config": subtitles.cmd_set_plain_subtitle_config,
"apply_voice_actions": voice.cmd_apply_voice_actions,
"generate_voice_script": voice.cmd_generate_voice_script,
"list_ollama_models": voice.cmd_list_ollama_models,
"build_phrase_review": review.cmd_build_phrase_review,
"save_phrase_review": review.cmd_save_phrase_review,
"project_config": project.cmd_project_config,
+1
View File
@@ -0,0 +1 @@
analysis/
+1 -1
View File
@@ -86,7 +86,7 @@ G-ART/
├── Engine/ # Esta documentação
├── docs/ # WORKFLOWS, CAPABILITY-AUDIT, specs
├── examples/ # Fixture de teste (sample.fcpxml)
└── tests/ # 1.454 testes / 42 suítes
└── tests/ # 1.466 testes / 42 suítes
```
---
+5 -5
View File
@@ -7,7 +7,7 @@
> mudança de código. Se algo aqui divergir do código, **o código está certo e
> este documento está velho** — corrija-o no mesmo commit.
Última varredura: 2026-08-19 · 74 ferramentas MCP · 1.454 testes · versão `0.6.35`
Última varredura: 2026-08-19 · 77 ferramentas MCP · 1.498 testes · versão `0.6.35`
---
@@ -29,7 +29,7 @@ entrada diferentes** para o mesmo motor:
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────────┐
│ admin/models_api.py │ │ server.py + server_tools/ │
│ + admin/api/ │ │ 74 tools, dispatch, schemas │
│ + admin/api/ │ │ 77 tools, dispatch, schemas │
│ 37 comandos da ponte │ │ NÃO tem lógica de timeline │
└───────────┬──────────────┘ └───────────────┬──────────────┘
└───────────────┬────────────────────┘
@@ -65,7 +65,7 @@ no mesmo engine, então uma correção ali vale para as duas.
| **Deps opcionais** | `librosa`/`ffmpeg`/`huggingface_hub` importados **lazy**, degradam com `None`. |
| **Idioma** | Comunicação com o usuário em português. Código e comentários em inglês. |
| **Validação** | `./Engine/run_after_fix.sh` **sempre** após cada correção. |
| **App** | Alterou `MacApp/`? Compile e rode: `./MacApp/build_app.sh --run`. |
| **App** | Alterou `MacApp/`? Compile e rode: `admin/run_app.command` (padrão de revisão; equivale a `./MacApp/build_app.sh --run`). |
---
@@ -140,7 +140,7 @@ programático. Round-trips sempre voltam pelas ferramentas XML.
| Controle Live do FCP | `fcpxml/live.py` |
| Segurança XML | `fcpxml/safe_xml.py` |
| Validação contra DTDs da Apple | `fcpxml/dtd.py` |
| Transporte MCP (74 tools) | `server.py` + `server_tools/` |
| Transporte MCP (77 tools) | `server.py` + `server_tools/` |
| Ponte com o app (37 comandos) | `admin/models_api.py` + `admin/api/` |
| Interface do usuário | `MacApp/Sources/` |
@@ -174,4 +174,4 @@ Se você precisar disso, a lógica está no lugar errado.
| Uma **regra de edição** nova | `fcpxml/` sempre. Se você está escrevendo `if` sobre timeline fora de `fcpxml/`, pare. |
O trabalho principal é **sempre** no engine. As camadas de cima são finas de
propósito: é o que permite testar 1.454 casos sem abrir o app nem subir o MCP.
propósito: é o que permite testar 1.498 casos sem abrir o app nem subir o MCP.
+3 -2
View File
@@ -26,13 +26,14 @@ Versão: `0.6.35` · Última varredura: 2026-08-19
| `text_layout.py` | 901 | Diagramação das legendas dinâmicas |
| `rough_cut.py` | 798 | Geração de timelines novas |
| `model_manager.py` | 748 | Modelos Whisper: catálogo, download, config |
| `voice_timeline.py` | 594 | O JSON de voz que a IA lê |
| `voice_timeline.py` | 600 | O JSON de voz que a IA lê |
| `phrase_review.py` | 547 | Revisão de frases (etapa 5 do assistente) |
| `collision.py` | 472 | Colisão entre títulos na tela |
| `font_metrics.py` | 445 | Largura real de glifos por fonte |
| `templates.py` | 387 | Templates de timeline |
| `parser.py` | 367 | FCPXML → objetos Python |
| `transcribe.py` | 300 | Transcrição Whisper e corte por texto |
| `transcribe.py` | 332 | Transcrição Whisper e corte por texto |
| `forced_align.py` | 181 | Alinhamento forçado opcional (whisperx/wav2vec2) que corrige o viés de ~0,4s no início das palavras |
| `live.py` | 273 | Modo Live (push_to_fcp) |
| `diff.py` | 269 | Comparação de timelines |
| `voice_actions.py` | 263 | Decisões de edição (cut/zoom/text/marker) |
+37 -7
View File
@@ -1,6 +1,6 @@
# 03 — Camada MCP (`server.py` + `server_tools/`) — 74 ferramentas
# 03 — Camada MCP (`server.py` + `server_tools/`) — 77 ferramentas
> **Escopo:** As 74 ferramentas MCP: helpers, categorias e como criar uma nova.
> **Escopo:** As 77 ferramentas MCP: helpers, categorias e como criar uma nova.
> **Não cobre:** Lógica de edição, que mora no engine (→ 02) · comandos do app (→ 08)
`server.py` (592 linhas) é só o transporte: dispatch por dicionário
@@ -43,7 +43,7 @@ continua funcionando. A coluna diz o módulo real, para quando você precisar
| `_cut_transcript_spans()` | `_shared/media.py` | Corte por trecho falado |
| `_apply_placed_action()` | `_shared/media.py` | Aplica zoom/text/marker já posicionado |
## As 74 ferramentas por categoria
## As 77 ferramentas por categoria
### Timeline & análise (Projeto)
`list_projects`, `analyze_timeline`, `list_clips`, `list_markers`, `list_connected_clips`,
@@ -82,18 +82,31 @@ continua funcionando. A coluna diz o módulo real, para quando você precisar
### Voz (análise → decisão → aplicação)
`analyze_voice_features`, `build_voice_timeline`, `refine_voice_timeline`,
`remove_speakers`, `apply_voice_actions`, `get_voice_analysis_config`,
`save_voice_analysis_config`.
`remove_speakers`, `apply_voice_actions`, `generate_voice_script`,
`get_voice_analysis_config`, `save_voice_analysis_config`.
O fluxo é sempre o mesmo: `build_voice_timeline` mede (caro, roda uma vez) →
O fluxo manual é: `build_voice_timeline` mede (caro, roda uma vez) →
o modelo decide os cortes → **`refine_voice_timeline` renormaliza sobre o que
sobrou** (barato, sem reabrir áudio) e propõe as janelas de zoom → o modelo
corta a lista pelo ritmo → `apply_voice_actions` aplica. Pular a renormalização
faz o ranking de ênfase apontar para as palavras erradas (ver
`05_EXPERIENCIAS.md`).
`generate_voice_script` é o fluxo **automático e fechado** (sem wizard, sem
copiar-e-colar): transcreve (cache) → `build_voice_timeline` → entrega a
timeline a um **modelo local Ollama** que dirige a edição → devolve o roteiro
legível (markdown) **e** o JSON de ações, e opcionalmente aplica num FCPXML.
O cliente fica em `fcpxml/llm_local.py`; o modelo é tratado como entrada não
confiável e cada ação é validada por `parse_actions`. Padrão:
`qwen2.5:7b-instruct-q4_K_M` (troca de `gemma3:12b` — não cabia em máquina de
8GB de RAM; Gemma 3 4B foi testado antes e falhou por apagar o roteiro
principal em vez de só cortar bastidor). Passe `model=` para usar outro
servido pelo Ollama.
### Legendas dinâmicas (geração → validação → aplicação)
`generate_dynamic_subtitles`, `validate_subtitle_layout`, `transcript_markers`.
`generate_dynamic_subtitles`, `generate_plain_subtitles`,
`generate_subtitles_by_emphasis`, `validate_subtitle_layout`,
`transcript_markers`.
**Sempre gere e depois valide — nunca dê a geração como pronta sem
`validate_subtitle_layout`.** A composição garante "sem sobreposição" só
@@ -111,6 +124,23 @@ severidade probable/severe → investigar CADA colisão pela fração exata do
XML antes de mudar código (ver checklist abaixo)
```
**`generate_subtitles_by_emphasis`** gera as duas legendas numa passada só —
mas não divide as palavras entre elas. A comum é gerada **completa, do início
ao fim do clipe**, sempre; a dinâmica é gerada só sobre as frases marcadas
como ênfase na etapa 5 (zoom aplicado, nível ≥ 1); e onde a dinâmica cobre um
trecho, os títulos comuns daquele trecho recebem `enabled="0"` — continuam no
XML (editáveis/reativáveis no Final Cut), só não são desenhados. É a tradução
literal de `10-revisao-humana.md` (skill `editar-por-voz`): "a frase de
ênfase recebe zoom E legenda dinâmica; as demais recebem legenda comum" —
sem nunca deixar um vão sem legenda nenhuma se a ênfase for desativada depois
(a comum já estava lá, só desligada). A decisão vem de
`<mídia>_phrase_actions.json["emphasis_spans"]`, escrito por
`save_phrase_review` quando o editor termina a etapa 5 — sem esse arquivo (ou
sem `zoom`/`text` marcados na revisão), a tool gera só a comum, tudo ligado,
e avisa no relatório ("Sem revisão de ênfase"). Não expõe overrides de estilo
por chamada — usa a config salva ("Legendas Dinâmicas"/plain); para estilo
pontual, use `generate_dynamic_subtitles`/`generate_plain_subtitles` direto.
**Antes de atribuir uma colisão ao gerador, confirme que é o gerador.**
Um `<title>` de nome estranho (`ref` diferente, params tipo `Auto-Shrink`/
`Left Margin` que `_make_text_title_clip` nunca escreve) é conteúdo humano
+51 -3
View File
@@ -31,7 +31,7 @@ que merece entrada.
| 11 | 2026-08-18 | Preview das legendas dinâmicas desproporcional ao render do FCP (stagger/gap/canvas divergentes) e `inactive_color` exposto sem efeito | `resolvido` |
| 12 | 2026-08-18 | Espaço de coordenadas do modelo "Text": `fontSize`, `kerning` e `Position` no espaço do quadro — converter só o tamanho descolou o espaçamento | `resolvido` |
| 13 | 2026-08-19 | Reanálise de ênfase implementada no Engine mas sem ferramenta MCP — Fase 4 da skill era inexecutável | `resolvido` |
| 14 | 2026-08-19 | Offset sistemático de ~0,4s no timing por palavra (faster-whisper sem alinhamento forçado) — corrigido manualmente no teste, WhisperX pendente | `parcialmente resolvido` |
| 14 | 2026-08-19 | Offset sistemático de ~0,4s no timing por palavra (faster-whisper sem alinhamento forçado) — agora corrigido em pipeline por alinhamento forçado opcional | `resolvido` |
| 15 | 2026-08-19 | `add_zoom` perdia o enquadramento real (voltava a 100%) quando dois zooms caiam no mesmo clipe pós-corte; agora empilha ou substitui conforme as janelas se sobrepõem | `resolvido` |
| 16 | 2026-08-19 | `validate_subtitle_layout` acusava colisão severa em títulos que só se tocam na borda, por não-associatividade de float; 7 de 8 colisões reportadas no teste real eram falso positivo | `resolvido` |
| 17 | 2026-08-19 | Linha de ênfase das legendas dinâmicas sem limite de largura — palavra longa/maiúscula estourava o frame inteiro; auto-fit encolhe até caber, nunca abaixo do corpo | `resolvido` |
@@ -43,6 +43,7 @@ que merece entrada.
| 23 | 2026-08-19 | Dividir `writer.py` em pacote quebrou `@patch('fcpxml.writer.subprocess')` — a suíte protege comportamento, não localização | `resolvido` |
| 24 | 2026-08-19 | `admin/test_models_api.py` existia mas estava fora de `testpaths` — 13 testes que nunca rodaram | `resolvido` |
| 25 | 2026-08-20 | `admin/api/shared.py` apontava para `admin/code` (inexistente) após a divisão — install editável mascarou o bug em toda validação anterior | `resolvido` |
| 26 | 2026-08-21 | `generate_voice_script` (IA local/Ollama) caía com "Falha ao gerar roteiro por IA local" — prompt embutia a timeline inteira (47k tokens) e estourava `num_ctx`; e `response.json()` de conexão caída escapava como `JSONDecodeError` | `resolvido` |
> Mantenha o índice acima sempre sincronizado com as entradas mais recentes.
@@ -118,9 +119,9 @@ Use o bloco abaixo como modelo. Uma entrada = um problema resolvido/reconhecido.
- **Por que isso importa mais do que parece:** o erro contamina toda decisão temporal a jusante — zoom disparava ~0,4s antes da palavra-alvo, `gap_before` subestimava pausas reais na mesma medida (o que afeta diretamente a régua de silêncio recém-adotada), e as folgas de corte saíam erradas nas emendas.
- **Decisão tomada:** não rodei `remove_media_silence` bruto sobre o corte. A detecção (ffmpeg, limiar -30dB/0,5s) não distingue "batida entre frases dentro da régua de 1,5s" de "ar morto de emenda" — cortar ambos teria apertado frases fluidas. Corrigi os tempos manualmente medindo o ataque real nos pontos críticos (cabeça, 2 emendas, cauda, 3 zooms) e refiz o corte numa passada só.
- **Solução adotada (paliativa, aplicada manualmente neste teste):** medir o RMS real com `ffmpeg -af astats=metadata=1:reset=1:length=0.05,ametadata=print` em janelas curtas ao redor de cada ponto crítico antes de fixar um corte ou zoom que dependa de precisão de frame. Não é o padrão do sistema — é o que cobre a lacuna até o alinhamento forçado existir.
- **Solução estrutural ainda pendente:** ligar o WhisperX (ou alinhamento forçado equivalente) em `transcribe.py`, o que levaria o erro de ~400ms para ~30ms e corrigiria zoom, corte e `gap_before` de uma vez, sem paliativo por projeto. Não implementado ainda — é mudança de pipeline, exige regerar todos os `_transcript.json`/`_voice_timeline.json` existentes.
- **Solução estrutural implementada:** `transcribe.py` agora roda alinhamento forçado fonético (wav2vec2 via whisperx) como passo opcional pós-transcrição, em `fcpxml/forced_align.py` (classe `ForcedAligner`). O erro cai de ~400ms para ~30ms e corrige zoom, corte e `gap_before` de uma vez. É **dependência opcional** (`[align]` extra / pacote `whisperx` do PyPI) — quando ausente ou em qualquer falha, degrada e devolve os tempos brutos sem quebrar a transcrição. O `transcript` traz `"alignment": true/false` e o `voice_timeline` expõe `layers.alignment`, para quem lê o JSON saber se o offset manual ainda é necessário. Não reaproveitamos código da pasta `WHISPERX/` local (problemas conhecidos) — só a ideia documentada aqui. Exige regerar os `_transcript.json`/`_voice_timeline.json` existentes para aplicar nos caches antigos.
- **Aprendizado:** "não reestime tempos no olho" (critério 01) continua certo para decisão *editorial* — mas não cobre erro sistemático de *medição* na fonte dos tempos. Um offset constante e na mesma direção, em vários pontos do material, é sinal de bug no pipeline de transcrição, não de julgamento errado sobre o material. Vale conferir com uma amostra de áudio real antes de confiar cegamente em timestamp de word-level de qualquer fonte nova.
- **Estado:** `parcialmente resolvido` — paliativo documentado e aplicado neste teste; correção estrutural (WhisperX) pendente de implementação.
- **Estado:** `resolvido` — alinhamento forçado implementado em `transcribe.py`/`fcpxml/forced_align.py`; paliativo de medição manual mantido apenas para transcripts antigos sem `layers.alignment=true`.
---
@@ -1366,3 +1367,50 @@ o outro; percentil entrega um punhado útil nos dois casos.
instaladas por fora do mecanismo sendo testado — ou o teste prova que o
ambiente de teste está bem configurado, não que o código está certo.
- **Estado:** `resolvido`
---
## Entrada #26 — Prompt da IA local estoura o contexto do Ollama (e erro de parse escapa)
- **Sintoma:** botão "Gerar roteiro por IA local" (etapa 4 do assistente)
devolvia "Falha ao gerar roteiro por IA local". Rodando a ponte direto, o
erro real aparecia como *"Server disconnected without sending a response"*
ou *"Connection refused"* do Ollama, e 0 decisões ("Decisões do modelo: 0").
- **Causa raiz (dupla):**
1. `build_edit_messages` embutia o JSON da voice timeline **inteiro** no
prompt. Uma gravação de 3min vira ~188KB / **~47k tokens** (cada palavra
carrega energia, pitch, arousal, valence, `samples`…). Como `num_ctx`
estava em 32768, o prompt estourava a janela e o Ollama **dropava a
conexão** sem resposta.
2. Quando a conexão cai sem resposta, `httpx` entrega um body vazio e
`response.json()` lançava `JSONDecodeError` — que **não** é
`httpx.HTTPError`, então escapava do `try/except` de `ollama_chat` e
virava a exceção genérica que o `cmd_generate_voice_script` transforma
em `ok:false` com a mensagem "Falha ao gerar roteiro por IA local: …".
- **Correção (em `fcpxml/llm_local.py` + `server_tools/voice.py`):**
- `build_edit_messages` agora projeta a timeline (**`_project_timeline`**):
mantém só `text`/`start`/`end`/`speaker`/`emphasis`/`pause_before` das
palavras e `id`/`name` dos locutores; descarta `layers`, `scales`,
`samples` e os floats de áudio. Caiu de ~47k para **~17k tokens** (69KB).
- Salvaguarda `_shrink_to_fit`: se ainda passar de `max_chars` (110k),
remove os `words` dos segmentos de menor `peak_emphasis` até caber.
- `ollama_chat` envolve `post`+`raise_for_status`+`json()` num único
`except Exception` que relança como `RuntimeError` claro — fim do
`JSONDecodeError` escapando.
- `_extract_json` agora desembrulha a lista de 1 elemento `[{source,
actions}]` que alguns modelos devolvem, senão o `parse_actions` tratava o
objeto-wrapper como uma ação sem `kind` e rejeitava tudo (0 decisões).
- `handle_generate_voice_script` levanta `RuntimeError` com a causa quando o
modelo não devolve nenhuma decisão utilizável, então o app mostra a
mensagem real ("O modelo local não devolveu decisões utilizáveis: …")
em vez do genérico.
- **Validação:** `tests/test_llm_local.py` ganhou `test_build_edit_messages_is_compact`
(prompt < raw, sem `samples`/`energy_raw`/`pitch_hz`) e
`test_ollama_chat_wraps_empty_response`. Ponte testada com Ollama mockado
nos dois sentidos (sucesso aplica; falha → `ok:false` com msg clara).
- **Estado:** `resolvido`
> **Aprendizado:** modelo local tem contexto finito — nunca embutir o objeto
> de análise cru no prompt; projetar só o que a decisão usa. E qualquer parse
> de resposta de servidor local deve tratar body vazio/quebrado como erro de
> transporte, não como sucesso mudo.
+6 -5
View File
@@ -19,7 +19,7 @@ o resultado.
```bash
cd code && ./MacApp/build_app.sh # compila e monta o .app
cd code && ./MacApp/build_app.sh --run # compila e abre
admin/run_app.command # compila, fecha a instância antiga e abre (padrão de revisão)
```
Consequências práticas, todas já sentidas:
@@ -121,14 +121,15 @@ quando o botão "Continuar" libera.
| 1 | Projeto | Escolhe a pasta de saída e o `.fcpxml` | `project_config` |
| 2 | Transcrever | Transcreve toda a mídia do projeto | `transcribe` |
| 3 | Analisar voz | Mede ênfase, locutores, emoção | `analyze_voice` |
| 4 | Decisões da IA | Copia para o chat, cola o JSON de volta, aplica | `apply_voice_actions` |
| 4 | Decisões da IA | Copia para o chat **ou** gera por IA local (Ollama/Gemma 3), aplica | `apply_voice_actions` / `generate_voice_script` |
| 5 | **Revisar ênfases** | Lapida frase a frase — ver seção 5 | `build_phrase_review` / `save_phrase_review` |
| 6 | Processar | Silêncios, preenchimento, legendas | vários, em cadeia |
| 7 | Concluído | Abre no FCP ou mostra no Finder | — |
**A etapa 4 é a única manual do fluxo**, e de propósito: o julgamento de qual
tomada usar e onde dar zoom é conversa com uma IA (skill `editar-por-voz`), não
um botão. O app monta o pedido pronto no clipboard e recebe o JSON de volta.
**A etapa 4 tem duas saídas:**
- **Manual (chat):** o app monta o pedido pronto no clipboard (skill `editar-por-voz`) e recebe o JSON de volta — o julgamento de qual tomada usar e onde dar zoom fica com a IA numa conversa.
- **Automática (IA local):** botão "Gerar roteiro por IA local (Ollama/Gemma 3)". Ele manda a *voice timeline inteira* (o arquivo) junto com o brief para um modelo local (Ollama), que decide cortes/zooms/textos de uma vez, devolve o roteiro legível + o JSON de ações e já aplica no FCPXML (non-destructive). Não precisa sair do app nem colar nada. O modelo é escolhido num **picker que lista os modelos instalados no Ollama** (populado via `list_ollama_models` quando a etapa abre); se o Ollama estiver fora do ar, cai para um campo de texto livre. Troque para `llama3` etc. se tiver outro modelo. Requer o Ollama rodando em `localhost:11434`.
**Etapa 1 — armadilha registrada:** não escolha como "o projeto" um arquivo já
gerado pelo fluxo (`_voice_edit`, `_silence_removed`, …). Os cortes de voz
+4 -4
View File
@@ -7,7 +7,7 @@ Este é o documento de rota. Os outros descrevem o que **é**; este diz o que
**fazer** e por onde começar quando chega uma implementação, uma melhoria ou
uma correção.
Última varredura: 2026-08-19 · 1.454 testes · lint zerado
Última varredura: 2026-08-19 · 1.466 testes · lint zerado
---
@@ -98,8 +98,8 @@ quanto arquivo gigante.
## 4. Checklist antes de dar algo por pronto
```bash
cd code && ./Engine/run_after_fix.sh # lint zerado + 1.454 testes
cd code && ./MacApp/build_app.sh --run # se mexeu no app
cd code && ./Engine/run_after_fix.sh # lint zerado + 1.466 testes
admin/run_app.command # se mexeu no app (padrão de revisão)
```
E, além do script:
@@ -150,7 +150,7 @@ Estão em `01_ARCHITECTURE.md` §2 e valem repetir as três que mais custaram:
- [01 Arquitetura](01_ARCHITECTURE.md) — camadas e onde cada coisa mora
- [02 Módulos](02_MODULES.md) — mapa do engine, módulo a módulo
- [03 Server/Tools](03_SERVER_TOOLS.md) — as 74 ferramentas MCP
- [03 Server/Tools](03_SERVER_TOOLS.md) — as 77 ferramentas MCP
- [04 Testes & Workflow](04_TESTS_AND_WORKFLOW.md)
- [05 Experiências](05_EXPERIENCIAS.md) — o que já quebrou e por quê
- [06 Boas Práticas](06_BOAS_PRATICAS.md)
+27 -11
View File
@@ -31,15 +31,31 @@ struct CaptionsView: View {
@AppStorage("capSampleAfter") private var sampleAfter = "sua legenda"
@AppStorage("capShowGuides") private var showsGuides = true
private let fontChoices = [
"Helvetica Neue", "Helvetica", "Arial", "Avenir Next",
"Futura", "SF Pro Display", "Georgia", "Impact",
]
/// Todas as famílias de fonte instaladas no macOS (sistema + usuário), as
/// usadas por padrão primeiro, para o seletor listar tudo sem hardcode.
private static let installedFontFamilies: [String] = {
var families = NSFontManager.shared.availableFontFamilies
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
let preferred = ["Helvetica Neue", "Playfair Display", "Georgia", "Didot"]
for family in preferred.reversed() {
if let idx = families.firstIndex(of: family) {
families.remove(at: idx)
families.insert(family, at: 0)
}
}
return families
}()
private let emphasisFontChoices = [
"Playfair Display", "Georgia", "Didot", "Futura",
"Avenir Next", "Times New Roman", "Helvetica Neue", "Impact",
]
/// Lista para um picker: todas as famílias instaladas e, se o valor salvo
/// não estiver entre elas (ex.: fonte de outro Mac), ele entra no topo
/// para o seletor continuar exibindo a escolha atual.
private func fontChoices(for current: String) -> [String] {
var list = Self.installedFontFamilies
if !list.contains(current) {
list.insert(current, at: 0)
}
return list
}
private let emphasisFaceChoices = [
"Medium Italic", "Italic", "Bold Italic", "Bold", "Regular", "Light Italic",
@@ -211,7 +227,7 @@ struct CaptionsView: View {
private var bodySection: some View {
Section("Linhas de apoio") {
Picker("Fonte", selection: bound(\.font)) {
ForEach(fontChoices, id: \.self) { Text($0).tag($0) }
ForEach(fontChoices(for: config.font), id: \.self) { Text($0).tag($0) }
}
slider(
"Tamanho",
@@ -226,7 +242,7 @@ struct CaptionsView: View {
private var emphasisSection: some View {
Section("Palavra de ênfase") {
Picker("Fonte", selection: bound(\.emphasisFont)) {
ForEach(emphasisFontChoices, id: \.self) { Text($0).tag($0) }
ForEach(fontChoices(for: config.emphasisFont), id: \.self) { Text($0).tag($0) }
}
Picker("Estilo", selection: bound(\.emphasisFace)) {
ForEach(emphasisFaceChoices, id: \.self) { Text($0).tag($0) }
@@ -244,7 +260,7 @@ struct CaptionsView: View {
private var plainSubtitleSection: some View {
Section("Legenda comum") {
Picker("Fonte", selection: plainBound(\.font)) {
ForEach(fontChoices, id: \.self) { Text($0).tag($0) }
ForEach(fontChoices(for: plainConfig.font), id: \.self) { Text($0).tag($0) }
}
slider(
"Tamanho",
+76 -63
View File
@@ -28,30 +28,6 @@ final class PhraseReviewModel: ObservableObject {
/// In/out the editor dragged on the timeline, in source seconds.
@Published var rangeStart: Double?
@Published var rangeEnd: Double?
/// Aspect ratio of the footage as recorded.
@Published var videoAspect: Double = 16.0 / 9.0
/// Aspect ratio the project delivers in, read from the .fcpxml. It is
/// routinely *not* the footage's: these takes are shot horizontal and
/// delivered vertical, so previewing the raw frame would show a crop the
/// audience never sees — and the emphasis decisions are about what lands on
/// screen. Nil until the project is known.
@Published var projectAspect: Double?
/// Whether the preview crops to the delivery frame. On by default whenever
/// the two aspects disagree.
@Published var matchProjectFraming = true
/// What the preview should actually draw.
var previewAspect: Double {
guard matchProjectFraming, let projectAspect else { return videoAspect }
return projectAspect
}
/// True when the delivery frame differs enough from the footage that the
/// preview is showing a crop rather than the whole take.
var isCropping: Bool {
guard matchProjectFraming, let projectAspect else { return false }
return abs(projectAspect - videoAspect) > 0.01
}
private(set) var source = ""
private(set) var sourcePath = ""
@@ -76,25 +52,17 @@ final class PhraseReviewModel: ObservableObject {
// MARK: - Carregar
/// Builds the review from the voice timeline plus whatever the AI decided.
/// A review saved on a previous visit wins — see `cmd_build_phrase_review`.
/// Reads the delivery format from the project so the preview can frame the
/// take the way it will actually be seen.
func loadProjectFormat(projectPath: String) {
PythonBridge.call(command: "inspect", arguments: ["path": projectPath]) { [weak self] result, _ in
Task { @MainActor in
guard let self,
let timelines = result?["timelines"] as? [[String: Any]],
let first = timelines.first,
let width = first["width"] as? Int, let height = first["height"] as? Int,
width > 0, height > 0
else { return }
self.projectAspect = Double(width) / Double(height)
}
}
}
/// A review saved on a previous visit wins — see `cmd_build_phrase_review` —
/// UNLESS `fresh` is true, in which case that saved review is ignored and
/// `active`/`emphasis`/etc. come straight from this call's `decisionsJSON`.
/// Pass `fresh: true` when the decisions themselves changed since the
/// review was last built (the caller re-pasted/regenerated the AI's JSON
/// and re-ran `apply_voice_actions`) — otherwise the saved review from the
/// PREVIOUS decisions silently wins over the fresh cut it should reflect,
/// which is exactly the desync the wizard's "active" toggle showed against
/// the just-reapplied FCPXML.
func load(voiceTimelinePath: String, decisionsJSON: String,
outputFolder: String? = nil, mediaFolder: String? = nil) {
outputFolder: String? = nil, mediaFolder: String? = nil, fresh: Bool = false) {
self.voiceTimelinePath = voiceTimelinePath
isLoading = true
errorMessage = nil
@@ -102,6 +70,7 @@ final class PhraseReviewModel: ObservableObject {
var arguments: [String: Any] = ["voice_timeline": voiceTimelinePath]
if let outputFolder { arguments["output_dir"] = outputFolder }
if let mediaFolder { arguments["media_dir"] = mediaFolder }
if fresh { arguments["fresh"] = true }
if let data = decisionsJSON.data(using: .utf8),
let parsed = try? JSONSerialization.jsonObject(with: data) {
arguments["actions"] = parsed
@@ -160,7 +129,6 @@ final class PhraseReviewModel: ObservableObject {
let asset = AVURLAsset(url: URL(fileURLWithPath: sourcePath))
let player = AVPlayer(playerItem: AVPlayerItem(asset: asset))
self.player = player
readAspect(from: asset)
// 60 Hz: the same observer drives the playhead *and* decides when to
// jump a removed stretch, so its period is the worst-case amount of cut
// material that can be heard before the skip lands. At 20 Hz that was an
@@ -173,22 +141,6 @@ final class PhraseReviewModel: ObservableObject {
}
}
/// The displayed aspect ratio, honouring the rotation the camera recorded.
/// A phone take is stored 1920×1080 with a 90° transform: reading
/// `naturalSize` alone would call a vertical video horizontal.
private func readAspect(from asset: AVURLAsset) {
Task { [weak self] in
guard let track = try? await asset.loadTracks(withMediaType: .video).first,
let size = try? await track.load(.naturalSize),
let transform = try? await track.load(.preferredTransform)
else { return }
let displayed = size.applying(transform)
let width = abs(displayed.width), height = abs(displayed.height)
guard width > 0, height > 0 else { return }
await MainActor.run { self?.videoAspect = width / height }
}
}
// MARK: - Reprodução
private func tick(_ time: Double) {
@@ -335,9 +287,22 @@ final class PhraseReviewModel: ObservableObject {
/// Trim everything before/after a given word — the text-first way to cut,
/// since the editor reads the line and points at where it should begin.
/// Clicking the word that is ALREADY that edge toggles it back off —
/// the trim on that side resets to the phrase's own start/end — so the
/// same click that sets a boundary also clears it, instead of needing
/// the separate "Inteira" button for a one-sided undo.
func trimToWord(_ word: ReviewWord, edge: TrimEdge, in id: Int) {
guard let phrase = phrases.first(where: { $0.id == id }) else { return }
let epsilon = 0.001
switch edge {
case .start where abs(word.start - phrase.trimStart) < epsilon:
update(id) { $0.trimStart = $0.start }
case .end where abs(word.end - phrase.trimEnd) < epsilon:
update(id) { $0.trimEnd = $0.end }
default:
trim(id, edge: edge, to: edge == .start ? word.start : word.end)
}
}
// MARK: - Trecho marcado e zooms
@@ -400,11 +365,59 @@ final class PhraseReviewModel: ObservableObject {
phrases.filter { $0.active }.reduce(0) { $0 + ($1.trimEnd - $1.trimStart) }
}
// MARK: - Tempo compactado (sem os vãos do que foi cortado)
/// Kept spans of source media, in order, each carrying the position it
/// lands at once every removed stretch between phrases is squeezed out.
/// The timeline draws and scrubs in this space so it reads like the cut
/// itself instead of the raw take with holes in it.
private var keptSegments: [(rawStart: Double, rawEnd: Double, compactStart: Double)] {
var offset = 0.0
var segments: [(Double, Double, Double)] = []
for phrase in phrases.sorted(by: { $0.start < $1.start }) where phrase.active {
guard phrase.trimEnd > phrase.trimStart else { continue }
segments.append((phrase.trimStart, phrase.trimEnd, offset))
offset += phrase.trimEnd - phrase.trimStart
}
return segments
}
/// Maps a raw source-media time to its position on the compacted timeline.
/// Time inside removed material collapses to the boundary of the nearest
/// kept segment, so cut stretches take up no space at all.
func compactTime(_ raw: Double) -> Double {
let segments = keptSegments
for segment in segments {
if raw < segment.rawStart { return segment.compactStart }
if raw <= segment.rawEnd { return segment.compactStart + (raw - segment.rawStart) }
}
guard let last = segments.last else { return 0 }
return raw >= last.rawEnd ? last.compactStart + (last.rawEnd - last.rawStart) : 0
}
/// The inverse of `compactTime`: where a click on the compacted timeline
/// lands in the raw source media, for seeking and scrubbing.
func rawTime(fromCompact compact: Double) -> Double {
let segments = keptSegments
for segment in segments {
let compactEnd = segment.compactStart + (segment.rawEnd - segment.rawStart)
if compact <= compactEnd {
return segment.rawStart + max(0, compact - segment.compactStart)
}
}
return segments.last?.rawEnd ?? 0
}
/// Persists the edited review plus the actions derived from it. Called when
/// the wizard advances — the render itself happens in the next step.
func save(completion: @escaping (String?) -> Void) {
/// Persists the edited review and hands back BOTH paths it wrote:
/// `review_path` (the human-readable `_phrase_review.json`) and
/// `actions_path` (`_phrase_actions.json`, the cut/zoom list derived from
/// it — what `finalizeProcessing` needs to actually apply the review's
/// active/inactive decisions instead of just filing them away).
func save(completion: @escaping (_ reviewPath: String?, _ actionsPath: String?) -> Void) {
guard !voiceTimelinePath.isEmpty, !phrases.isEmpty else {
completion(nil)
completion(nil, nil)
return
}
let arguments: [String: Any] = [
@@ -418,11 +431,11 @@ final class PhraseReviewModel: ObservableObject {
PythonBridge.call(command: "save_phrase_review", arguments: arguments) { result, error in
Task { @MainActor in
if let error {
completion(nil)
completion(nil, nil)
_ = error
return
}
completion(result?["review_path"] as? String)
completion(result?["review_path"] as? String, result?["actions_path"] as? String)
}
}
}
+41 -141
View File
@@ -1,86 +1,32 @@
import AVFoundation
import SwiftUI
/// The video surface, as a plain `AVPlayerLayer` in an `NSView`.
/// The wizard's emphasis-review step.
///
/// AVKit's `VideoPlayer` would be the obvious choice and is a trap here: this
/// app is built by invoking `swiftc` directly (see `MacApp/build_app.sh`), and
/// `_AVKit_SwiftUI` aborts at launch instantiating its generic metadata under
/// that build. A player layer needs only AVFoundation, which links cleanly —
/// and the transport controls live in the timeline's own toolbar anyway, so
/// nothing is lost by dropping AVKit's chrome.
private struct PlayerSurface: NSViewRepresentable {
let player: AVPlayer
/// When true the frame is filled and cropped instead of letterboxed — used
/// to preview horizontal footage inside a vertical delivery frame.
var fills: Bool
func makeNSView(context: Context) -> PlayerLayerView {
let view = PlayerLayerView()
view.player = player
view.fills = fills
return view
}
func updateNSView(_ view: PlayerLayerView, context: Context) {
if view.player !== player { view.player = player }
view.fills = fills
}
}
final class PlayerLayerView: NSView {
private let playerLayer = AVPlayerLayer()
var player: AVPlayer? {
get { playerLayer.player }
set { playerLayer.player = newValue }
}
var fills: Bool = false {
didSet { playerLayer.videoGravity = fills ? .resizeAspectFill : .resizeAspect }
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
wantsLayer = true
layer = CALayer()
layer?.backgroundColor = NSColor.black.cgColor
playerLayer.videoGravity = .resizeAspect
layer?.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
wantsLayer = true
layer = CALayer()
playerLayer.videoGravity = .resizeAspect
layer?.addSublayer(playerLayer)
}
override func layout() {
super.layout()
playerLayer.frame = bounds
}
}
/// The wizard's emphasis-review step, laid out like an editing room: preview on
/// top, timeline across the bottom, and the script as an inspector down the
/// right side.
///
/// The arrangement is the point. Every decision here is about a *sentence*, so
/// the same phrase has to be legible in all three places at once — a block on
/// the timeline, a line of text in the inspector, and a moment in the preview.
/// Selecting in any one of them selects in the other two.
/// Every decision here is about a *sentence* read from the original
/// transcription, so the phrases are listed in full — each line shows the text
/// as it will be said, a switch to keep or drop it from the cut, and the
/// emphasis level. Selecting a line in the list also selects its block on the
/// timeline below, and vice-versa.
struct PhraseReviewView: View {
@ObservedObject var model: PhraseReviewModel
var body: some View {
VSplitView {
HSplitView {
previewPane
.frame(minWidth: 320, idealWidth: 640)
inspectorPane
.frame(minWidth: 300, idealWidth: 360, maxWidth: 520)
VStack(spacing: 0) {
inspectorHeader
Divider()
List(selection: $model.selection) {
ForEach($model.phrases) { $phrase in
PhraseRow(phrase: $phrase, model: model)
.tag(phrase.id)
}
}
.listStyle(.inset)
.onChange(of: model.selection) { _, newValue in
if let newValue { model.goTo(phraseID: newValue) }
}
Divider()
summaryBar
}
.frame(minHeight: 240)
@@ -111,48 +57,6 @@ struct PhraseReviewView: View {
}
}
// MARK: - Preview
private var previewPane: some View {
VStack(spacing: 0) {
if let player = model.player {
// The footage here is usually vertical. Sizing the surface to
// the take's own aspect keeps a 9:16 frame as tall as the pane
// allows instead of shrinking it to fit a horizontal box.
// Framed to what the project delivers, not to what the camera
// recorded: these takes are shot horizontal and cut vertical,
// so the raw frame would show material the audience never sees.
ZStack {
Color.black
PlayerSurface(player: player, fills: model.isCropping)
.aspectRatio(model.previewAspect, contentMode: .fit)
.clipped()
}
.overlay(alignment: .topTrailing) { framingBadge }
} else {
ZStack {
Color.black.opacity(0.85)
VStack(spacing: 10) {
Image(systemName: "film.stack")
.font(.system(size: 28)).foregroundStyle(.secondary)
Text(model.source.isEmpty
? "A análise de voz não registrou qual mídia foi usada."
: "Não achei \(model.source) na pasta do projeto.")
.font(.callout).foregroundStyle(.secondary)
Text("A revisão funciona igual sem o preview — ele só ajuda a conferir o corte.")
.font(.caption).foregroundStyle(.tertiary)
Button("Localizar a mídia…") { pickMedia() }
.buttonStyle(.bordered)
}
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
}
}
Divider()
summaryBar
}
}
private var summaryBar: some View {
HStack(spacing: 16) {
summaryItem("text.quote", "\(model.phrases.count) frases")
@@ -181,28 +85,6 @@ struct PhraseReviewView: View {
String(format: "%02d:%02d finais", Int(seconds) / 60, Int(seconds) % 60)
}
/// Says which frame is on screen, and lets the editor flip to the raw take.
/// Without it a centred crop looks like the footage itself, and someone
/// would judge framing on an approximation without knowing it.
@ViewBuilder
private var framingBadge: some View {
if model.projectAspect != nil, abs((model.projectAspect ?? 0) - model.videoAspect) > 0.01 {
Button {
model.matchProjectFraming.toggle()
} label: {
Label(model.matchProjectFraming ? "Enquadramento do projeto" : "Mídia original",
systemImage: model.matchProjectFraming ? "crop" : "rectangle.expand.vertical")
.font(.caption2)
}
.buttonStyle(.borderless)
.padding(6)
.background(Capsule().fill(.black.opacity(0.45)))
.foregroundStyle(.white)
.padding(8)
.help("A fonte é horizontal e o projeto é vertical — o preview mostra o corte central aproximado. O enquadramento real de cada clipe vem do Final Cut.")
}
}
private func pickMedia() {
let panel = NSOpenPanel()
panel.canChooseFiles = true
@@ -373,14 +255,17 @@ private struct PhraseRow: View {
FlowWords(words: phrase.words, phrase: phrase) { word, edge in
model.trimToWord(word, edge: edge, in: phrase.id)
}
Text("Clique = começa aqui · ⌥clique = termina aqui")
Text("Clique = começa/desfaz aqui · ⌥clique = termina/desfaz aqui · sublinhado = ênfase da palavra")
.font(.caption2).foregroundStyle(.tertiary)
}
.padding(.top, 2)
}
}
/// The phrase's words as wrapping chips, dimmed where they fall outside the trim.
/// The phrase's words as wrapping chips, dimmed where they fall outside the
/// trim and underlined where the acoustics mark them as an emphasis peak —
/// the same word-level signal `05-zoom.md` picks a punch-in's `start` from,
/// made visible instead of buried in the JSON.
private struct FlowWords: View {
let words: [ReviewWord]
let phrase: ReviewPhrase
@@ -394,16 +279,31 @@ private struct FlowWords: View {
alignment: .leading, spacing: 3) {
ForEach(words) { word in
let kept = word.start >= phrase.trimStart - 0.001 && word.end <= phrase.trimEnd + 0.001
let level = EmphasisPalette.levelFromScore(word.emphasis)
Text(word.text)
.font(.caption2)
.fontWeight(level >= 2 ? .semibold : .regular)
.padding(.horizontal, 4)
.padding(.vertical, 2)
.background(
RoundedRectangle(cornerRadius: 3)
.fill(kept ? Color.accentColor.opacity(0.12) : Color.secondary.opacity(0.08))
)
.overlay(alignment: .bottom) {
if level >= 1 {
Rectangle()
.fill(EmphasisPalette.color(level))
.frame(height: 2)
.padding(.horizontal, 3)
}
}
.foregroundStyle(kept ? .primary : .secondary)
.strikethrough(!kept)
.help(
level >= 1
? "Ênfase \(EmphasisPalette.label(level).lowercased()) (\(Int(word.emphasis * 100))%)"
: "Sem ênfase"
)
.onTapGesture {
onTrim(word, NSEvent.modifierFlags.contains(.option) ? .end : .start)
}
+31 -8
View File
@@ -21,6 +21,20 @@ enum EmphasisPalette {
}
}
/// The same 0–3 tiers a phrase's `emphasis` uses, derived from a raw 0–1
/// acoustic score — the thresholds `10-revisao-humana.md` documents for
/// deriving a phrase's level from `peak_emphasis` when no explicit zoom
/// was set, reused here per WORD so a word chip and a phrase row read as
/// the same scale.
static func levelFromScore(_ score: Double) -> Int {
switch score {
case ..<0.25: return 0
case ..<0.45: return 1
case ..<0.65: return 2
default: return 3
}
}
static func speakerColor(_ speaker: String, among speakers: [String]) -> Color {
let palette: [Color] = [.teal, .purple, .green, .indigo, .brown, .cyan]
guard let index = speakers.firstIndex(of: speaker) else { return .gray }
@@ -47,7 +61,9 @@ struct TimelineTracksView: View {
private let trackSpacing: CGFloat = 4
private var pps: CGFloat { CGFloat(model.pixelsPerSecond) }
private var contentWidth: CGFloat { max(320, CGFloat(model.duration) * pps) }
/// Width follows the *kept* duration, not the raw take's — the timeline
/// draws the cut, so removed stretches take no horizontal space.
private var contentWidth: CGFloat { max(320, CGFloat(model.keptDuration) * pps) }
/// Name, icon and height of each lane, in the order they stack. The gutter
/// and the tracks are built from this one list so a label can never drift
@@ -215,8 +231,8 @@ struct TimelineTracksView: View {
Canvas { context, size in
let step = tickStep()
var time = 0.0
while time <= model.duration {
let position = x(time)
while time <= model.keptDuration {
let position = compactX(time)
context.stroke(
Path { $0.move(to: CGPoint(x: position, y: size.height - 6))
$0.addLine(to: CGPoint(x: position, y: size.height)) },
@@ -301,7 +317,8 @@ struct TimelineTracksView: View {
.gesture(
DragGesture(minimumDistance: 1)
.onChanged { value in
let time = phrase.start + Double((value.location.x) / pps)
let compactOrigin = model.compactTime(phrase.start)
let time = model.rawTime(fromCompact: compactOrigin + Double(value.location.x / pps))
model.trim(phrase.id, edge: edge, to: time)
}
)
@@ -397,8 +414,8 @@ struct TimelineTracksView: View {
private var scrubGesture: some Gesture {
DragGesture(minimumDistance: 0)
.onChanged { value in
let from = Double(value.startLocation.x / pps)
let to = Double(value.location.x / pps)
let from = model.rawTime(fromCompact: Double(value.startLocation.x / pps))
let to = model.rawTime(fromCompact: Double(value.location.x / pps))
if abs(value.translation.width) > 3 {
model.setRange(from: from, to: to)
model.seek(to: min(from, to))
@@ -486,10 +503,16 @@ struct TimelineTracksView: View {
// MARK: - Escala
private func x(_ time: Double) -> CGFloat { CGFloat(time) * pps }
/// Pixel position of a raw source-media time, after collapsing whatever
/// lies between it and the previous kept phrase.
private func x(_ time: Double) -> CGFloat { compactX(model.compactTime(time)) }
/// Pixel position of a time already in the compacted (edited) timeline —
/// used for the ruler and playhead, which think in that space directly.
private func compactX(_ compactTime: Double) -> CGFloat { CGFloat(compactTime) * pps }
private func width(from: Double, to: Double) -> CGFloat {
max(0, CGFloat(to - from) * pps)
max(0, CGFloat(model.compactTime(to) - model.compactTime(from)) * pps)
}
/// Ruler spacing that keeps labels ~80pt apart at any zoom.
+190 -31
View File
@@ -9,7 +9,7 @@ import AppKit
/// sair do app e escolher um arquivo na mão) vira copiar/colar assistido
/// dentro da própria tela.
enum WizardStep: Int, CaseIterable, Identifiable {
case projeto, transcricao, analise, exportarChat, revisar, finalizar, concluido
case projeto, transcricao, analise, exportarChat, revisar, finalizar
var id: Int { rawValue }
var titulo: String {
@@ -20,7 +20,6 @@ enum WizardStep: Int, CaseIterable, Identifiable {
case .exportarChat: return "Decisões da IA"
case .revisar: return "Revisar ênfases"
case .finalizar: return "Processar"
case .concluido: return "Concluído"
}
}
}
@@ -54,10 +53,18 @@ struct WizardView: View {
@State private var appliedPath: String?
@State private var skippedVoiceEdit = false
// Passo 4 (alternativa) — gerar o roteiro direto por IA local (Ollama/Gemma 3)
@State private var isGeneratingScript = false
@State private var generateScriptModel = "gemma3:12b"
@State private var generateScriptFeedback = ""
@State private var ollamaModels: [String] = []
// Passo 5 — revisar ênfases
@StateObject private var reviewModel = PhraseReviewModel()
@State private var reviewLoadedFor: String?
@State private var reviewLoadedForDecisions: String?
@State private var phraseReviewPath: String?
@State private var phraseActionsPath: String?
// Passo 6 — processamento final
@State private var finalSilences = true
@@ -178,7 +185,6 @@ struct WizardView: View {
case .exportarChat: exportarChatStep
case .revisar: revisarStep
case .finalizar: finalizarStep
case .concluido: concluidoStep
}
}
@@ -304,6 +310,54 @@ struct WizardView: View {
.font(.callout).foregroundStyle(.secondary)
if let voiceTimelinePath {
// Alternativa automática: em vez de copiar/colar no chat, manda a
// própria voice timeline (o arquivo inteiro) junto com o brief para
// o modelo local (Ollama/Gemma 3) decidir a edição de uma vez —
// cortes, zooms e textos numa única chamada, sem sair do app.
VStack(alignment: .leading, spacing: 8) {
Text("OU gere o roteiro por IA local (Ollama/Gemma 3)").font(.callout.weight(.semibold))
Text("O app envia a voice timeline completa (o arquivo) acompanhada do pedido para o modelo local decidir os cortes, zooms e textos de uma vez. Nada de copiar e colar.")
.font(.caption).foregroundStyle(.secondary)
HStack {
if ollamaModels.isEmpty {
TextField("Modelo (ex.: gemma3:12b, llama3)", text: $generateScriptModel)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 260)
} else {
Picker("Modelo", selection: $generateScriptModel) {
ForEach(ollamaModels, id: \.self) { m in
Text(m).tag(m)
}
}
.pickerStyle(.menu)
.frame(maxWidth: 260)
TextField("Ou outro", text: $generateScriptModel)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 120)
}
Button {
generateScript(voiceTimelinePath: voiceTimelinePath)
} label: {
if isGeneratingScript {
HStack { ProgressView().controlSize(.small); Text("Gerando…") }
} else {
Label("Gerar roteiro por IA local", systemImage: "sparkles")
}
}
.buttonStyle(.borderedProminent)
.disabled(isGeneratingScript || voiceTimelinePath.isEmpty)
}
if !generateScriptFeedback.isEmpty {
Label(generateScriptFeedback, systemImage: "checkmark.circle.fill")
.font(.caption).foregroundStyle(.green)
}
}
.padding(12)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.green.opacity(0.07)))
.onAppear { fetchOllamaModels() }
Divider().padding(.vertical, 4)
Button {
copyForChat(path: voiceTimelinePath)
} label: {
@@ -412,6 +466,11 @@ struct WizardView: View {
.onAppear { loadReviewIfNeeded() }
}
/// Processing and its result live on the SAME slide: the moment the last
/// operation finishes (`finalPath` gets set), the open/reveal buttons
/// appear right below the "Processar" button instead of gating behind a
/// separate "Concluído" step the user has to click into — there was
/// nothing on that slide worth a click of its own.
private var finalizarStep: some View {
VStack(alignment: .leading, spacing: 16) {
Text("6. Finalize o corte").font(.title3.weight(.semibold))
@@ -437,18 +496,11 @@ struct WizardView: View {
.controlSize(.large)
.disabled(isFinalizing || (!finalSilences && !finalFillers && !finalSubtitles && !finalDynamicSubtitles))
if !finalStatus.isEmpty && !isFinalizing {
Text(finalStatus).font(.caption).foregroundStyle(.secondary)
}
}
}
private var concluidoStep: some View {
VStack(alignment: .leading, spacing: 16) {
if let finalPath, !isFinalizing {
Divider().padding(.vertical, 4)
Label("Concluído", systemImage: "checkmark.seal.fill")
.font(.title3.weight(.semibold))
.font(.callout.weight(.semibold))
.foregroundStyle(.green)
if let finalPath {
Text(finalPath).font(.caption).foregroundStyle(.secondary).lineLimit(1).truncationMode(.middle)
HStack {
Button("Abrir no Final Cut Pro") { NSWorkspace.shared.open(URL(fileURLWithPath: finalPath)) }
@@ -456,22 +508,27 @@ struct WizardView: View {
Button("Mostrar no Finder") {
NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: finalPath)])
}
}
}
Divider().padding(.vertical, 8)
Spacer()
Button("Começar outro projeto") { resetWizard() }
}
} else if !finalStatus.isEmpty && !isFinalizing {
Text(finalStatus).font(.caption).foregroundStyle(.secondary)
}
}
}
// MARK: - Navegação
/// `.finalizar` is the last step now — once it has a `finalPath`, the
/// slide's own "Começar outro projeto" button is the way forward, so the
/// footer's "Continuar" would be a second, redundant path to nowhere.
private var navFooter: some View {
HStack {
if step != .projeto && step != .concluido {
if step != .projeto {
Button("Voltar") { goBack() }
}
Spacer()
if step != .concluido {
if step != .finalizar || finalPath == nil {
Button(step == .finalizar ? "Concluir" : "Continuar") { goNext() }
.buttonStyle(.borderedProminent)
.disabled(!canAdvance)
@@ -490,18 +547,19 @@ struct WizardView: View {
// exigir mais uma confirmação.
case .revisar: return true
case .finalizar: return finalPath != nil && !isFinalizing
case .concluido: return false
}
}
private func goNext() {
guard let next = WizardStep(rawValue: step.rawValue + 1) else { return }
// Sair da revisão grava o que foi decidido (e as ações derivadas dela)
// ao lado da análise de voz. Nada é renderizado aqui: a etapa 6 é que
// lê esse arquivo para dar zoom e legenda dinâmica só nas ênfases.
// Sair da revisão grava o que foi decidido e as ações derivadas dela
// (`_phrase_actions.json`) ao lado da análise de voz — é esse arquivo
// que `finalizeProcessing` reaplica na etapa 6, para que desativar uma
// frase aqui realmente a remova do vídeo final, e não só do registro.
if step == .revisar {
reviewModel.save { path in
phraseReviewPath = path
reviewModel.save { reviewPath, actionsPath in
phraseReviewPath = reviewPath
phraseActionsPath = actionsPath
}
}
step = next
@@ -521,7 +579,9 @@ struct WizardView: View {
appliedPath = nil
skippedVoiceEdit = false
reviewLoadedFor = nil
reviewLoadedForDecisions = nil
phraseReviewPath = nil
phraseActionsPath = nil
finalStatus = ""
finalPath = nil
errorMessage = nil
@@ -727,21 +787,33 @@ struct WizardView: View {
copiedFeedback = "Copiado — cole (⌘V) numa conversa com o Claude."
}
/// Monta a revisão uma vez por análise de voz. Voltar e avançar de novo não
/// recarrega: isso jogaria fora as edições manuais em silêncio, que é
/// exatamente o que esta tela existe para preservar.
/// Monta a revisão uma vez por análise de voz. Voltar e avançar de novo com
/// as MESMAS decisões não recarrega: isso jogaria fora as edições manuais
/// em silêncio, que é exatamente o que esta tela existe para preservar.
///
/// Mas se o usuário voltou à etapa 4 e colou/gerou um JSON de decisões
/// DIFERENTE do que gerou a revisão atual, isso é recarregado — e com
/// `fresh: true`, para que o `active`/ênfase recém-derivado dessas
/// decisões novas não seja imediatamente sobrescrito pela revisão salva
/// da visita anterior (`merge_saved_decisions`, do lado Python). Sem isso,
/// a tela ficava presa nas decisões antigas mesmo depois de reaplicar o
/// corte — a dessincronia relatada entre "ativa aqui" e "já cortado no
/// FCPXML".
private func loadReviewIfNeeded() {
guard let voiceTimelinePath, reviewLoadedFor != voiceTimelinePath else { return }
guard let voiceTimelinePath else { return }
let decisionsChanged = reviewLoadedForDecisions != nil && reviewLoadedForDecisions != decisionsText
guard reviewLoadedFor != voiceTimelinePath || decisionsChanged else { return }
reviewLoadedFor = voiceTimelinePath
reviewLoadedForDecisions = decisionsText
// A pasta do projeto e a do .fcpxml entram como onde procurar a mídia:
// a análise de voz guarda só o nome do arquivo, não o caminho.
reviewModel.load(
voiceTimelinePath: voiceTimelinePath,
decisionsJSON: decisionsText,
outputFolder: outputFolder,
mediaFolder: projectPath.map { URL(fileURLWithPath: $0).deletingLastPathComponent().path }
mediaFolder: projectPath.map { URL(fileURLWithPath: $0).deletingLastPathComponent().path },
fresh: decisionsChanged
)
if let projectPath { reviewModel.loadProjectFormat(projectPath: projectPath) }
}
private func applyDecisions() {
@@ -767,6 +839,55 @@ struct WizardView: View {
}
}
/// Etapa 4 (alternativa): manda a voice timeline inteira para um modelo
/// local (Ollama/Gemma 3) que dirige a edição de uma vez — sem copiar e
/// colar. O motor devolve o roteiro legível + o JSON de ações e já aplica
/// no FCPXML (non-destructive), igual ao fluxo manual "Aplicar decisões".
private func fetchOllamaModels() {
guard ollamaModels.isEmpty else { return }
PythonBridge.call(command: "list_ollama_models", arguments: [:]) { result, err in
DispatchQueue.main.async {
if let models = result?["models"] as? [String], !models.isEmpty {
ollamaModels = models
if !models.contains(generateScriptModel) {
generateScriptModel = models.first ?? generateScriptModel
}
}
}
}
}
private func generateScript(voiceTimelinePath: String) {
guard let projectPath, let outputFolder else { return }
isGeneratingScript = true
generateScriptFeedback = ""
errorMessage = nil
PythonBridge.call(command: "generate_voice_script", arguments: [
"voice_timeline": voiceTimelinePath,
"filepath": projectPath,
"output_dir": outputFolder,
"model": generateScriptModel,
"apply_to_fcpxml": true,
]) { result, err in
DispatchQueue.main.async {
isGeneratingScript = false
guard result?["ok"] as? Bool == true else {
errorMessage = result?["error"] as? String ?? err ?? "Falha ao gerar roteiro por IA local."
return
}
// Traz as decisões de volta para a tela de revisão (etapa 5) e
// marca como aplicadas, exatamente como o "Aplicar decisões".
if let actionsPath = result?["actions_path"] as? String,
let content = try? String(contentsOfFile: actionsPath, encoding: .utf8) {
decisionsText = content
}
appliedPath = result?["applied_path"] as? String ?? projectPath
skippedVoiceEdit = false
generateScriptFeedback = "Roteiro gerado e aplicado — revise as ênfases na próxima etapa."
}
}
}
private func finalizeProcessing() {
guard let outputFolder else { return }
let startPath = appliedPath ?? projectPath
@@ -780,7 +901,45 @@ struct WizardView: View {
isFinalizing = true
errorMessage = nil
finalStatus = "Iniciando…"
finalizeStep(operations, index: 0, currentPath: startPath, outputFolder: outputFolder)
applyReviewDecisions(startPath: startPath, outputFolder: outputFolder) { reviewedPath in
finalizeStep(operations, index: 0, currentPath: reviewedPath, outputFolder: outputFolder)
}
}
/// Reapplies whatever the etapa-5 review decided (active/inactive
/// phrases, manual zooms) on top of `startPath` before the finishing
/// chain runs below. Without this, `appliedPath` stayed frozen at
/// whatever `exportarChat`'s `apply_voice_actions` produced BEFORE the
/// human review — so toggling a phrase off in the review only updated
/// `_phrase_actions.json` on disk, never the video the wizard actually
/// exports. A no-op (just hands `startPath` straight through) when the
/// review step was never visited/saved this session.
private func applyReviewDecisions(
startPath: String, outputFolder: String, completion: @escaping (String) -> Void
) {
guard let phraseActionsPath,
let data = try? Data(contentsOf: URL(fileURLWithPath: phraseActionsPath)),
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let actions = parsed["actions"] else {
completion(startPath)
return
}
finalStatus = "Aplicando a revisão…"
PythonBridge.call(command: "apply_voice_actions", arguments: [
"path": startPath,
"output_dir": outputFolder,
"actions": actions,
]) { result, err in
DispatchQueue.main.async {
guard result?["ok"] as? Bool == true else {
isFinalizing = false
errorMessage = result?["error"] as? String ?? err ?? "Falha ao aplicar a revisão."
finalStatus = "Processamento interrompido."
return
}
completion(result?["path"] as? String ?? startPath)
}
}
}
private func finalizeStep(_ operations: [String], index: Int, currentPath: String, outputFolder: String) {
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""AI Voice Editor - Pipeline completo: transcrição + análise acústica → JSON para IA.
Uso:
python ai_edit.py <media_path> [--model base] [--lang pt] [--no-diarize] [--output dir]
Gera dois arquivos na pasta output (ou ao lado do mídia):
<nome>_transcript.json — transcrição com timestamps por palavra
<nome>_voice_timeline.json — timeline de voz com ênfase, pitch, energy, speakers
Esses arquivos são a ENTRADA para a IA analisar e gerar o roteiro/edição.
"""
import argparse
import json
import sys
import time
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description="Pipeline de análise de voz para IA")
parser.add_argument("media", help="Caminho do arquivo de mídia (.mp4, .mov, .wav, etc.)")
parser.add_argument("--model", default="base", help="Modelo Whisper (tiny/base/small/medium/large-v3)")
parser.add_argument("--lang", default=None, help="Idioma (ex: pt, en). Auto-detect se omitido")
parser.add_argument("--hf-token", default=None, help="HuggingFace token para diarização (opcional)")
parser.add_argument("--no-diarize", action="store_true", help="Pular diarização de falantes")
parser.add_argument("--output", default=None, help="Pasta de saída (padrão: ao lado do mídia)")
parser.add_argument("--no-align", action="store_true", help="Pular alinhamento fonético (whisperx)")
args = parser.parse_args()
media_path = Path(args.media).resolve()
if not media_path.is_file():
print(f"ERRO: Arquivo não encontrado: {media_path}", file=sys.stderr)
sys.exit(1)
# Output dir
out_dir = Path(args.output) if args.output else media_path.parent
out_dir.mkdir(parents=True, exist_ok=True)
stem = media_path.stem
# ── Fase 1: Transcrição ──────────────────────────────────────────
print(f"[1/2] Transcrevendo {media_path.name} (modelo: {args.model})...")
t0 = time.time()
# Adiciona code/ ao path para imports do projeto
code_dir = Path(__file__).resolve().parent
sys.path.insert(0, str(code_dir))
from fcpxml.transcribe import transcribe
def transcribe_progress(pct):
bar_len = 30
filled = int(bar_len * pct)
bar = "█" * filled + "░" * (bar_len - filled)
print(f"\r [{bar}] {pct*100:.0f}%", end="", flush=True)
transcript = transcribe(
str(media_path),
model_size=args.model,
language=args.lang,
progress_cb=transcribe_progress,
align=not args.no_align,
)
print() # newline after progress bar
if transcript is None:
print("ERRO: Transcrição falhou. Verifique se faster-whisper está instalado:", file=sys.stderr)
print(" uv pip install faster-whisper", file=sys.stderr)
sys.exit(1)
print(f" → {len(transcript.get('words', []))} palavras, "
f"{len(transcript.get('segments', []))} segmentos, "
f"idioma: {transcript.get('language', '?')}")
# Salva transcrição
transcript_path = out_dir / f"{stem}_transcript.json"
transcript_path.write_text(json.dumps(transcript, indent=2, ensure_ascii=False), encoding="utf-8")
print(f" → Salvo: {transcript_path}")
# ── Fase 2: Análise de voz (timeline) ────────────────────────────
print(f"\n[2/2] Analisando voz (pitch, energia, ênfase)...")
t1 = time.time()
from fcpxml.voice_timeline import build_voice_timeline
def voice_progress(fraction, stage):
print(f"\r {stage} ({fraction*100:.0f}%)", end="", flush=True)
hf_token = None if args.no_diarize else args.hf_token
timeline = build_voice_timeline(
str(media_path),
transcript,
hf_token=hf_token,
progress_cb=voice_progress,
)
print()
# Salva voice timeline
timeline_path = out_dir / f"{stem}_voice_timeline.json"
timeline_path.write_text(json.dumps(timeline, indent=2, ensure_ascii=False), encoding="utf-8")
print(f" → Salvo: {timeline_path}")
# ── Resumo ───────────────────────────────────────────────────────
elapsed = time.time() - t0
summary = timeline.get("summary", {})
layers = timeline.get("layers", {})
n_words = len(transcript.get("words", []))
n_segments = len(transcript.get("segments", []))
n_speakers = len(timeline.get("speakers", []))
duration = transcript.get("duration", 0)
print(f"\n{'='*50}")
print(f" ARQUIVOS GERADOS:")
print(f" {transcript_path}")
print(f" {timeline_path}")
print(f"\n RESUMO:")
print(f" Duração: {duration:.1f}s ({duration/60:.1f}min)")
print(f" Palavras: {n_words}")
print(f" Segmentos: {n_segments}")
print(f" Falantes: {n_speakers}")
print(f" Camadas: transcript={layers.get('transcript')}, "
f"acoustics={layers.get('acoustics')}, "
f"diarization={layers.get('diarization')}")
print(f" Tempo: {elapsed:.1f}s")
print(f"{'='*50}")
print(f"\n→ Pronto! Agora peça à IA para analisar o voice timeline e gerar o roteiro.")
if __name__ == "__main__":
main()
+181
View File
@@ -0,0 +1,181 @@
"""Forced alignment — refine word timestamps against an acoustic model.
Why this exists
--------------
faster-whisper derives word times by cross-attention, which lands every word
*start* systematically ~0.3-0.5s early (the word-end is fine). That bias flows
straight into the voice timeline and makes zoom/cut land on the wrong frame —
measured on real footage in ``Engine/docs/05_EXPERIENCIAS.md`` (#14). Phonetic
forced alignment (wav2vec2, via whisperx) re-anchors each word against the
audio and brings that error down to ~30ms.
Design
------
* The dependency (``whisperx``) is **optional** and imported lazily, exactly
like the rest of this stack (librosa, faster-whisper). When it is missing, or
any step fails, :meth:`ForcedAligner.align` returns the words unchanged, so
transcription never breaks because alignment did.
* The aligner is a single responsibility class: it knows how to turn a
transcript into the shape whisperx wants, call it, and write the refined
times back. ``transcribe.py`` owns the decision of *whether* to align.
* Align models are cached per language on the instance so repeated calls
(e.g. many short clips) don't reload the wav2vec2 weights each time.
"""
import logging
from typing import List, Optional, Sequence
logger = logging.getLogger(__name__)
class ForcedAligner:
"""Refine word-level timestamps with whisperx phonetic forced alignment.
Usage::
aligner = ForcedAligner()
words = aligner.align(words, raw_segments, media_path, language, models_dir)
``words`` and ``raw_segments`` come straight from :func:`transcribe` —
``raw_segments`` carries the per-segment ``words`` lists (the same dict
objects as in ``words``) so the aligner knows which words belong to which
audio window. Returns a list of the *same* word dicts, with ``start``/``end``
overwritten in place where alignment produced a usable time.
"""
def __init__(self, device: Optional[str] = None):
self._device = device
self._models: dict = {}
# -- capability ------------------------------------------------------
@staticmethod
def available() -> bool:
"""Whether whisperx can be imported (the aligner can run at all)."""
try:
import whisperx # noqa: F401
except Exception:
return False
return True
def _resolve_device(self) -> str:
if self._device:
return self._device
try:
import torch
if torch.cuda.is_available():
return "cuda"
except Exception:
pass
return "cpu"
# -- public API ------------------------------------------------------
def align(
self,
words: Sequence[dict],
raw_segments: Sequence[dict],
audio_path: str,
language: str,
models_dir: Optional[str] = None,
) -> List[dict]:
"""Return ``words`` with forced-aligned timestamps where possible.
Falls back to the unchanged ``words`` on any failure (missing
dependency, model load error, audio read error, or a result that
doesn't line up with the input).
"""
if not words or not language:
return list(words)
try:
import whisperx
except Exception:
logger.info("whisperx not installed; skipping forced alignment")
return list(words)
try:
device = self._resolve_device()
align_input = self._build_align_input(words, raw_segments)
audio = whisperx.load_audio(audio_path)
if language not in self._models:
align_model, metadata = whisperx.load_align_model(
language_code=language,
device=device,
model_dir=str(models_dir) if models_dir else None,
)
self._models[language] = (align_model, metadata)
align_model, metadata = self._models[language]
result = whisperx.align(
align_input,
align_model,
metadata,
audio,
device,
return_char_alignments=False,
)
return self._merge_result(words, result.get("segments", []))
except Exception:
logger.warning(
"forced alignment failed for %s; using raw timestamps", audio_path
)
return list(words)
# -- internals -------------------------------------------------------
@staticmethod
def _build_align_input(
words: Sequence[dict], raw_segments: Sequence[dict]
) -> List[dict]:
"""Transcript in whisperx's expected shape: segments -> words.
whisperx.align requires each segment to carry ``text``/``start``/``end``
and a ``words`` list whose entries have ``word``/``start``/``end``/``score``.
We only read ``words`` from ``raw_segments`` (the flattened ``words``
list is the source of truth for counts), so the two stay consistent.
"""
align_segments: List[dict] = []
for seg in raw_segments:
seg_words = [
{
"word": w.get("word", ""),
"start": float(w.get("start", 0.0)),
"end": float(w.get("end", 0.0)),
"score": float(w.get("confidence", 0.0)),
}
for w in seg.get("words", [])
]
align_segments.append(
{
"text": (seg.get("text") or "").strip(),
"start": float(seg.get("start", 0.0)),
"end": float(seg.get("end", 0.0)),
"words": seg_words,
}
)
return align_segments
@staticmethod
def _merge_result(words: Sequence[dict], aligned_segments: Sequence[dict]) -> List[dict]:
"""Walk the aligned output in order and overwrite word times in place.
whisperx preserves word order within and across segments, so a single
running index over the output words lines up with ``words``. A word the
aligner failed to place gets ``None``/``0`` times — we skip those rather
than clobber a good timestamp, and if counts ever diverge we stop and
leave the rest untouched.
"""
out = list(words)
wi = 0
for seg in aligned_segments:
for aw in seg.get("words", []):
if wi >= len(out):
return out
start = aw.get("start")
end = aw.get("end")
if start is None or end is None or end < start:
wi += 1
continue
out[wi]["start"] = float(start)
out[wi]["end"] = float(end)
wi += 1
return out
+312
View File
@@ -0,0 +1,312 @@
"""Local LLM integration — the voice timeline meets a local model.
The voice timeline is *designed* to be handed to a language model: it is the
source of truth between speech analysis and editing, layered so a model can
reason about the narrative without parsing FCPXML. This module is the client
side of that contract. It formats the timeline into the editar-por-voz brief,
calls a local model server (Ollama, running Gemma 3 / Llama locally), and
parses the model's decisions back into a validated list of VoiceActions —
all inside the engine, so there is no wizard, no copy-paste, no manual step.
Transport: Ollama's HTTP chat API at ``http://localhost:11434/api/chat``.
Any model Ollama serves works; the default is Gemma 3 because that is what
runs locally here ("Lama com Gema 3"), but pass ``model=`` to switch.
The model is untrusted input: its JSON is validated row-by-row by
:func:`fcpxml.voice_actions.parse_actions`, so one malformed decision never
discards the edit. The brief is written so the model only ever emits the four
action kinds the applier understands.
"""
from __future__ import annotations
import json
import logging
import re
from typing import Any, Dict, Optional, Sequence, Tuple
import httpx
from .voice_actions import parse_actions
logger = logging.getLogger(__name__)
DEFAULT_BASE_URL = "http://localhost:11434"
# Gemma 3 12B reliably follows the editar-por-voz brief (keep the script, cut
# only backstage chatter; the 4B variant skips the "keep the main content"
# rule and deletes the script) but doesn't fit an 8GB machine. Qwen2.5 7B
# instruct (q4_K_M) is the fallback for constrained hardware — strong at
# strict JSON-schema following, the property this brief leans on hardest.
# Pass ``model=`` to switch to whatever Ollama serves.
DEFAULT_MODEL = "qwen2.5:7b-instruct-q4_K_M"
REQUEST_TIMEOUT = 600.0
# The brief. Ported from the editar-por-voz skill criteria (criterios/01..08),
# condensed into the instructions a model needs to emit valid actions. Kept in
# Portuguese because the decisions and their reasons are read by a human editor.
_SYSTEM_PROMPT = """Você é o editor de vídeo por voz deste sistema. Recebe um JSON de "linha do tempo de voz" — a medição de COMO foi falado (ênfase, energia, pausa, falante) de uma gravação — e devolve as DECISÕES de edição em JSON, nada mais. Você nunca escreve XML.
Regras (siga rigorosamente):
1. LEIA EM CAMADAS. "summary" dá o formato da peça; "segments" é onde você trabalha (cada fala com seu texto e agregados); "segments[].words" dá o instante exato de cada destaque. Não recalcule energia, tom ou ênfase — use os números do JSON.
2. SEPARAR ROTEIRO DE BASTIDOR.
- ROTEIRO = o conteúdo principal que a pessoa quer entregar: explicação, depoimento, roteiro decorado, a mensagem. É isso que VAI FICAR.
- BASTIDOR = papo casual de gravação, cumprimentos, conversa com a equipe ("cara, beleza?", "tá gravando?", "deixa eu ver o celular"), piadas fora do assunto, tomadas interrompidas ou repetidas. É isso que VIRA "cut".
Exemplo: num vídeo sobre mastopexia, a explicação da cirurgia É o roteiro (mantém); o "tá gravando? pois é" antes dela É bastidor (corta).
Use "gap_before" e "take_boundary" (silêncio > ~3s = a câmera parou/recomeçou) para agrupar tomadas — eles marcam ONDE a tomada recomeça, não o que cortar. Nunca corte o conteúdo principal só porque tem ênfase; corte o casual/off-topic.
REGRAS DE OURO:
- MANTENHA o conteúdo principal (explicação, depoimento, roteiro decorado). Ele É o vídeo.
- CORTE SÓ o casual/off-topic: cumprimentos, "tá gravando?", papo com a equipe, olhar o celular, repetições de tomada.
- Em dúvida, MANTENHA a fala. É melhor sobrar conteúdo do que cortar o que era pra ficar.
3. ESCOLHER A MELHOR TOMADA de cada frase quando há repetições: mantenha a mais limpa e corte as outras (cut cobrindo a frase inteira).
4. CORTE (kind "cut"): para REMOVER uma frase, cubra ela inteira (start..end = início..fim da frase). Para APARAR só uma hesitação no começo ou fim, corte só da borda até a palavra (corte de meia frase é ambíguo — passe de 60% e apaga a linha toda). Nunca corte o silêncio entre falas.
5. ZOOM (kind "zoom"): só em palavra de CONTEÚDO bem enfatizada (emphasis alto, não artigo). params.scale entre 1.0 e 3.0 (padrão 1.3 se omitido). Posicione em torno da palavra, segurando até o fim da frase.
6. TEXTO (kind "text"): params.content obrigatório (≤120 chars), fixa um termo central ou callout. MARKER (kind "marker"): opcional params.content vira o nome do marcador. Use para emendas/junções que o editor deve conferir.
7. TEMPOS em segundos da MÍDIA ORIGINAL (exatamente como no JSON). Nunca compense para "depois do corte" — o programa desloca sozinho. end sempre > start, ambos ≥ 0.
8. reason OBRIGATÓRIO em cada ação, em português, embasando a decisão (ex.: 'abertura: "Aquela mama" (ênfase 0.42)'). reason vazio é decisão sem critério.
Responda APENAS com um objeto JSON válido, sem markdown, sem comentário:
{"source": "<nome do arquivo>", "actions": [{"kind": "cut|zoom|text|marker", "start": <float>, "end": <float>, "params": {}, "reason": "<pt>", "speaker": "<id>"}]}
"""
_OUTPUT_REMINDER = """Gere as decisões de edição conforme o brief. Responda SOMENTE o JSON:
{"source": "<nome do arquivo>", "actions": [{"kind": "cut|zoom|text|marker", "start": <float>, "end": <float>, "params": {}, "reason": "<pt>", "speaker": "<id>"}]}
Não inclua explicações nem blocos markdown."""
def ollama_chat(
model: str = DEFAULT_MODEL,
messages: Optional[Sequence[Dict[str, str]]] = None,
base_url: str = DEFAULT_BASE_URL,
temperature: float = 0.2,
timeout: float = REQUEST_TIMEOUT,
num_ctx: int = 32768,
) -> str:
"""One chat completion from a local Ollama server.
Returns the assistant message content. Raises on transport/HTTP errors so
the caller can decide whether to retry or report — a model call is the
one I/O in this pipeline that can legitimately fail mid-run.
"""
payload = {
"model": model,
"messages": list(messages or []),
"stream": False,
"options": {"temperature": temperature, "num_ctx": num_ctx},
}
try:
response = httpx.post(
f"{base_url.rstrip('/')}/api/chat", json=payload, timeout=timeout
)
response.raise_for_status()
data = response.json()
except Exception as exc:
# Covers transport errors AND a dropped connection that yields an empty
# body (httpx/JSONDecodeError) — both must become a RuntimeError so the
# caller reports the failure instead of crashing the whole pipeline.
raise RuntimeError(f"Falha ao falar com o modelo local em {base_url}: {exc}") from exc
return (data.get("message") or {}).get("content", "") or ""
def list_ollama_models(base_url: str = DEFAULT_BASE_URL) -> list[str]:
"""Names of the models Ollama currently serves, for a model picker.
Returns an empty list when Ollama is unreachable so the UI can fall back to
a free-text field instead of erroring.
"""
try:
resp = httpx.get(f"{base_url.rstrip('/')}/api/tags", timeout=10.0)
resp.raise_for_status()
models = resp.json().get("models", [])
names = [m.get("name") for m in models if m.get("name")]
return sorted(names)
except Exception:
return []
def _extract_json(text: str) -> Any:
"""Pull a JSON value out of a model response, tolerating fences/wrappers."""
if not text:
return None
candidate = text.strip()
# Strip a ```json ... ``` (or bare ```) fence if the model added one.
fence = re.search(r"```(?:json)?\s*(.*?)\s*```", candidate, re.DOTALL)
if fence:
candidate = fence.group(1).strip()
# Otherwise take the outermost {...} / [...].
if not candidate.startswith(("{" if True else "", "[")):
start = min(
(i for i, c in enumerate(candidate) if c in "{["),
default=None,
)
end = max(
(i for i, c in enumerate(candidate) if c in "}"),
default=None,
)
if start is not None and end is not None and end > start:
candidate = candidate[start : end + 1]
try:
data = json.loads(candidate)
except json.JSONDecodeError:
return None
# Models sometimes wrap the expected `{"source", "actions"}` object inside a
# single-element list (`[{...}]`). Unwrap that so the actions aren't treated
# as one malformed row.
if (
isinstance(data, list)
and len(data) == 1
and isinstance(data[0], dict)
and "actions" in data[0] # the wrapper carries the actions key
):
data = data[0]
return data
# Only these fields reach the model — the raw timeline also carries heavy
# per-word audio features (energy, pitch, arousal...) and speaker `samples`
# that blow past the model's context window on any real recording. Dropping
# them is what keeps a 3-minute timeline inside `num_ctx`.
_SEGMENT_KEEP = (
"start", "end", "speaker", "text", "gap_before", "take_boundary",
"avg_energy", "peak_emphasis", "emotion", "emotion_confidence",
"arousal", "valence",
)
_WORD_KEEP = ("text", "start", "end", "speaker", "emphasis", "pause_before")
_SPEAKER_KEEP = ("id", "name")
_SKIP_ROOT = ("layers", "scales")
def _project_timeline(timeline: dict) -> dict:
"""Strip the timeline down to what the edit decision actually needs."""
out = {k: v for k, v in timeline.items() if k not in _SKIP_ROOT}
speakers = [
{k: sp[k] for k in _SPEAKER_KEEP if k in sp}
for sp in timeline.get("speakers", [])
]
if speakers:
out["speakers"] = speakers
segs = []
for seg in timeline.get("segments", []):
s = {k: seg[k] for k in _SEGMENT_KEEP if k in seg}
s["words"] = [
{k: w[k] for k in _WORD_KEEP if k in w}
for w in seg.get("words", [])
]
segs.append(s)
out["segments"] = segs
return out
def _shrink_to_fit(compact: dict, max_chars: int) -> dict:
"""Drop word detail from the lowest-emphasis segments until it fits."""
segs = [dict(s) for s in compact.get("segments", [])]
while True:
payload = json.dumps(
{**compact, "segments": segs}, ensure_ascii=False, indent=1
)
if len(payload) <= max_chars or not any(s.get("words") for s in segs):
break
idx = min(
(i for i, s in enumerate(segs) if s.get("words")),
key=lambda i: float(segs[i].get("peak_emphasis", 0.0)),
)
segs[idx] = {**segs[idx], "words": []}
compact = dict(compact)
compact["segments"] = segs
return compact
def build_edit_messages(
timeline: dict, max_words_per_segment: int = 200, max_chars: int = 110000
) -> Tuple[str, str]:
"""The (system, user) pair that sends a timeline to the model.
The user turn carries a *projected* timeline (see :func:`_project_timeline`)
— text, timing, speaker and emphasis only — so a real recording fits in the
model's context window. Very long segments still have their word detail
capped to ``max_words_per_segment`` (most emphatic + boundaries), and if the
whole payload would still exceed ``max_chars`` the lowest-emphasis segments
lose their words until it fits, so we never blow ``num_ctx``.
"""
compact = _project_timeline(timeline)
if max_words_per_segment:
segs = []
for seg in compact["segments"]:
words = seg.get("words", [])
if len(words) > max_words_per_segment:
ranked = sorted(
enumerate(words),
key=lambda kv: float(kv[1].get("emphasis", 0.0)),
reverse=True,
)[: max_words_per_segment - 2]
keep = sorted({0, len(words) - 1} | {i for i, _ in ranked})
seg = {**seg, "words": [words[i] for i in keep]}
segs.append(seg)
compact["segments"] = segs
payload = json.dumps(compact, ensure_ascii=False, indent=1)
if len(payload) > max_chars:
compact = _shrink_to_fit(compact, max_chars)
payload = json.dumps(compact, ensure_ascii=False, indent=1)
user = (
"Linha do tempo de voz (JSON):\n\n"
+ payload
+ "\n\n"
+ _OUTPUT_REMINDER
)
return _SYSTEM_PROMPT, user
def generate_voice_actions(
timeline: dict,
model: str = DEFAULT_MODEL,
base_url: str = DEFAULT_BASE_URL,
temperature: float = 0.2,
timeout: float = REQUEST_TIMEOUT,
num_ctx: int = 32768,
max_words_per_segment: int = 200,
) -> Dict[str, Any]:
"""Ask the local model to direct the edit, returning validated actions.
Returns ``{"actions": [VoiceAction], "raw": str, "errors": [str]}``.
``actions`` is empty when the model returned nothing usable; ``errors``
carries the per-row rejections from :func:`parse_actions` plus any
extraction failure, so the caller can report what went wrong instead of
only the wins.
"""
system, user = build_edit_messages(timeline, max_words_per_segment)
try:
raw = ollama_chat(
model=model,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
base_url=base_url,
temperature=temperature,
timeout=timeout,
num_ctx=num_ctx,
)
except RuntimeError as exc:
return {"actions": [], "raw": "", "errors": [str(exc)]}
data = _extract_json(raw)
if data is None:
return {
"actions": [],
"raw": raw,
"errors": ["O modelo não devolveu um JSON de decisões legível."],
}
actions, errors = parse_actions(data)
return {"actions": actions, "raw": raw, "errors": errors}
+15
View File
@@ -194,6 +194,7 @@ class FCPXMLParser:
media_path=media_path,
audio_role=elem.get('audioRole', ''),
video_role=elem.get('videoRole', ''),
rotation=self._parse_clip_rotation(elem),
)
clip.markers.extend(self._collect_markers(elem))
@@ -205,6 +206,19 @@ class FCPXMLParser:
return clip
def _parse_clip_rotation(self, elem: ET.Element) -> float:
"""Degrees from this clip's ``<adjust-transform rotation="...">`` —
an edit-time correction (e.g. straightening a tilted phone shot),
not the camera's own recorded orientation. FCP writes the rotation
as an attribute on that element, not as a filter param."""
transform = elem.find('adjust-transform')
if transform is None:
return 0.0
try:
return float(transform.get('rotation', '0'))
except ValueError:
return 0.0
def _parse_marker_element(self, elem: ET.Element) -> Optional[Marker]:
"""Parse any marker element (<marker> or <chapter-marker>).
@@ -337,6 +351,7 @@ class FCPXMLParser:
lane=lane, offset=offset, source_start=start,
media_path=media_path, clip_type=elem.tag, role=role,
ref_id=ref, parent_clip_name=parent_name,
rotation=self._parse_clip_rotation(elem),
)
connected.markers.extend(self._collect_markers(elem))
+1
View File
@@ -300,6 +300,7 @@ def build_phrase_review(
"version": PHRASE_REVIEW_VERSION,
"source": source,
"source_path": resolve_source(source, voice_timeline_path, extra_dirs),
"rotation": float(timeline.get("rotation", 0.0)),
"duration": round(phrases[-1]["end"], 3) if phrases else 0.0,
"speakers": timeline.get("speakers", []),
"emotion_available": bool(layers.get("emotion", False)),
+42 -10
View File
@@ -125,18 +125,27 @@ def transcribe(
model_size: str = "base",
language: Optional[str] = None,
progress_cb: Optional[Callable[[float], None]] = None,
align: bool = True,
) -> Optional[dict]:
"""Transcribe an audio/video file locally with word-level timestamps.
Requires the optional ``[transcribe]`` extra (faster-whisper). Returns
``None`` when the model is unavailable or the file is missing/unreadable.
When ``align`` is true (default) and the optional ``whisperx`` dependency is
present, word timestamps are refined by phonetic forced alignment, which
corrects faster-whisper's systematic ~0.3-0.5s early bias on word *starts*
(see ``Engine/docs/05_EXPERIENCIAS.md`` #14). The transcript reports
whether this ran via the ``alignment`` flag, so downstream consumers can
rely on the times without re-measuring.
The model weights are resolved from the configured models directory (see
``model_manager.get_models_dir``), so a model selected/downloaded through
the app is found without an implicit download to the default HF cache.
Returns:
``{"language": str, "duration": float, "text": str,
"alignment": bool,
"segments": [{"text", "start", "end", "start_fmt", "end_fmt"}, ...],
"words": [{"word", "start", "end", "confidence"}, ...]}``
"""
@@ -175,6 +184,7 @@ def transcribe(
vad_filter=True,
)
segments: List[dict] = []
raw_segments: List[dict] = []
words: List[dict] = []
# `info.duration` is known upfront (from the container), so each
# segment's end time — yielded lazily as faster-whisper decodes —
@@ -183,6 +193,20 @@ def transcribe(
for seg in segments_iter:
start = float(seg.start)
end = float(seg.end)
seg_words: List[dict] = []
if progress_cb is not None and total_duration > 0:
progress_cb(min(end / total_duration, 1.0))
for w in seg.words or []:
ws = float(w.start)
we = float(w.end)
word = {
"word": w.word.strip(),
"start": ws,
"end": we,
"confidence": float(w.probability),
}
words.append(word)
seg_words.append(word)
segments.append(
{
"text": seg.text.strip(),
@@ -192,19 +216,26 @@ def transcribe(
"end_fmt": format_timestamp(end),
}
)
if progress_cb is not None and total_duration > 0:
progress_cb(min(end / total_duration, 1.0))
for w in seg.words or []:
ws = float(w.start)
we = float(w.end)
words.append(
raw_segments.append(
{
"word": w.word.strip(),
"start": ws,
"end": we,
"confidence": float(w.probability),
"text": seg.text.strip(),
"start": start,
"end": end,
"words": seg_words,
}
)
alignment_ran = False
if align and raw_segments:
from .forced_align import ForcedAligner
try:
words = ForcedAligner().align(
words, raw_segments, str(file_path), info.language, str(models_dir)
)
alignment_ran = True
except Exception:
logger.warning("forced alignment step failed; keeping raw timestamps")
except Exception:
logger.warning("whisper transcription failed for %s", file_path)
return None
@@ -212,6 +243,7 @@ def transcribe(
"language": info.language,
"duration": float(info.duration),
"text": " ".join(s["text"] for s in segments),
"alignment": alignment_ran,
"segments": segments,
"words": words,
}
+6
View File
@@ -512,6 +512,7 @@ def build_voice_timeline(
emphasis_floor: float = 0.25,
emotion_enabled: bool = False,
emotion_sensitivity: float = 0.5,
rotation: float = 0.0,
progress_cb: Optional[Callable[[float, str], None]] = None,
) -> dict:
"""Build the consolidated voice timeline for one media file.
@@ -545,6 +546,10 @@ def build_voice_timeline(
return {
"version": VOICE_TIMELINE_VERSION,
"source": Path(media_path).name,
# Edit-time correction from the clip's Transform filter in the FCPXML
# (e.g. straightening a tilted phone shot) — 0.0 when the clip has none
# or the caller didn't resolve one.
"rotation": rotation,
"language": transcript.get("language", ""),
# What actually ran, not what was installed — a consumer must be able
# to tell "this speech is flat" from "the acoustics never loaded",
@@ -554,6 +559,7 @@ def build_voice_timeline(
"acoustics": pitch_track is not None or energy_track is not None,
"speakers": tracks is not None,
"emotion": bool(emotion_enabled),
"alignment": bool(transcript.get("alignment")),
},
"scales": VALUE_SCALES,
"summary": _summary(
+7
View File
@@ -550,6 +550,13 @@ class TitlesMixin:
"""
titles = []
for elem in self.root.iter('title'):
# enabled="0" never renders in Final Cut (see
# generate_subtitles_by_emphasis, which disables plain titles
# under an emphasis phrase instead of never creating them) — a
# title that is off by design must not count as a collision
# against the one drawn in its place.
if elem.get('enabled', '1') == '0':
continue
text_el = elem.find('text/text-style')
text = (text_el.text or '').strip() if text_el is not None else ''
style = elem.find('text-style-def/text-style')
+3
View File
@@ -41,6 +41,9 @@ intelligence = [
transcribe = [
"faster-whisper>=1.0.0",
]
align = [
"whisperx>=3.0.0",
]
diarization = [
"pyannote.audio>=3.1",
]
+1
View File
@@ -179,6 +179,7 @@ from server_tools.voice import (
handle_analyze_voice_features,
handle_apply_voice_actions,
handle_build_voice_timeline,
handle_generate_voice_script,
handle_diarize_media,
handle_get_voice_analysis_config,
handle_refine_voice_timeline,
+304
View File
@@ -93,6 +93,25 @@ TOOLS = [
"required": ["filepath"]
}
),
Tool(
name="generate_subtitles_by_emphasis",
description="Generate BOTH subtitle styles over the FULL clip and let them coexist by visibility, not by splitting words: plain static titles (see generate_plain_subtitles) cover every word from start to end; dynamic progressive-composition titles (see generate_dynamic_subtitles) are additionally generated for whichever whole phrases were marked as emphasis in the phrase-review step (etapa 5, zoom applied, level >= 1). Wherever a dynamic phrase is on screen, the plain titles underneath it are set enabled=\"0\" (still present in the FCPXML, editable/re-enable-able in Final Cut, just not rendered) instead of never being generated there — so disabling emphasis later never leaves a silent gap in the plain track. Reads emphasis spans from the media's cached '<media>_phrase_actions.json' (written by save_phrase_review after the app's etapa 5 review) — run the voice-editing wizard through that step first, or nothing is treated as emphasis and every title stays plain and enabled. Style knobs are the saved 'Legendas Dinâmicas'/plain-subtitle configs (~/.fcp-mcp-server/config.json); this tool does not expose per-call style overrides, only the split logic — use generate_dynamic_subtitles/generate_plain_subtitles directly if you need one-off styling.",
inputSchema={
"type": "object",
"properties": {
"filepath": {"type": "string", "description": "Path to FCPXML file"},
"clip_name": {"type": "string", "description": "Only caption the clip with this name (default: all spine clips with matched source media)"},
"model": {"type": "string", "default": "base", "description": "Whisper model size if transcription is needed"},
"language": {"type": "string", "description": "ISO language code hint (e.g. 'pt'); auto-detected if omitted"},
"granularity": {"type": "string", "enum": ["phrase", "word"], "default": "phrase", "description": "Passed through to the dynamic half, same meaning as in generate_dynamic_subtitles"},
"max_words": {"type": "integer", "description": "Max words per block for the plain half. Falls back to saved plain-subtitle config."},
"uppercase": {"type": "boolean", "description": "Uppercase the plain half. Falls back to saved plain-subtitle config."},
"keep_punctuation": {"type": "boolean", "description": "Keep punctuation in the plain half. Falls back to saved plain-subtitle config."},
"output_path": {"type": "string", "description": "Output path (default: adds _emphasis_subtitles suffix)"},
},
"required": ["filepath"]
}
),
]
@@ -140,6 +159,85 @@ def _plain_subtitle_blocks(words: Sequence[dict], max_words: int) -> list[list[d
return blocks
def _phrase_actions_path(media_path: str) -> Path:
"""Where `save_phrase_review` writes emphasis decisions for this media.
Mirrors `phrase_review.review_paths()`'s naming (stem + "_phrase_actions.json"),
without importing that module just for a path — the voice_timeline this would
normally derive from is itself named `<media stem>_voice_timeline.json`, so
stripping straight from the media stem lands on the same file.
"""
stem = Path(media_path).stem
return Path(media_path).with_name(f"{stem}_phrase_actions.json")
def _load_emphasis_spans(media_path: str) -> list[dict]:
"""Load emphasis spans (source-media time) saved by the etapa-5 phrase review.
Returns [] if the review was never run for this media — callers should treat
that as "nothing is emphasis yet", not as an error, since the wizard's later
steps are optional.
"""
path = _phrase_actions_path(media_path)
if not path.is_file():
return []
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return []
spans = data.get("emphasis_spans", [])
return [s for s in spans if isinstance(s, dict) and "start" in s and "end" in s]
def _word_in_spans(word_start: float, word_end: float, spans: Sequence[dict]) -> bool:
"""A word belongs to an emphasis span if its midpoint falls inside it.
Midpoint, not start, so a word straddling a span boundary (which can happen
since spans come from phrase trims, not word timestamps) lands on whichever
side it mostly belongs to instead of always defaulting to one edge.
"""
mid = (word_start + word_end) / 2.0
return any(float(s["start"]) <= mid < float(s["end"]) for s in spans)
def _words_in_spans(words: Sequence[dict], spans: Sequence[dict]) -> list[dict]:
"""The subset of source-time transcript words that fall inside a span.
Feeds only the DYNAMIC half — the plain half always gets every word, full
clip, unfiltered; this is not a partition of the word list into two
disjoint sets, it is "which words also get the dynamic treatment on top".
"""
if not spans:
return []
return [
w for w in words
if _word_in_spans(float(w.get("start", 0.0)), float(w.get("end", w.get("start", 0.0))), spans)
]
def _segments_in_spans(segments: Sequence[dict], spans: Sequence[dict]) -> list[dict]:
"""Keep only the sentences that fall inside an emphasis span (by midpoint).
Feeds the dynamic half's sentence-block builder; segments outside every span
would only produce blocks with no words left in them after the word filter.
"""
if not spans:
return []
kept = []
for seg in segments:
start = float(seg.get("start", 0.0))
end = float(seg.get("end", start))
mid = (start + end) / 2.0
if any(float(s["start"]) <= mid < float(s["end"]) for s in spans):
kept.append(seg)
return kept
def _overlaps_any_span(start: float, end: float, spans: Sequence[tuple[float, float]]) -> bool:
"""Half-open interval overlap: a plain title under this window must hide."""
return any(start < span_end and end > span_start for span_start, span_end in spans)
async def handle_validate_subtitle_layout(arguments: dict) -> Sequence[TextContent]:
"""Validate title/subtitle layout for spatial collisions and safe-area
containment (collision.validate_titles over every <title> in the file)."""
@@ -442,8 +540,214 @@ async def handle_generate_plain_subtitles(arguments: dict) -> Sequence[TextConte
return _text_result(result)
async def handle_generate_subtitles_by_emphasis(arguments: dict) -> Sequence[TextContent]:
"""Generate plain titles for the whole clip and dynamic titles for the
emphasis phrases on top, then hide (enabled="0") the plain titles that
fall under a dynamic phrase — never split the word list between the two.
Plain always covers every word, so turning emphasis off later (editing
the phrase review and re-running) never leaves a silent gap: the plain
title was there all along, just disabled.
"""
model = arguments.get("model", "base")
language = arguments.get("language")
output_dir = arguments.get("output_dir")
clip_filter = arguments.get("clip_name")
granularity = arguments.get("granularity", "phrase")
saved_dynamic = load_dynamic_subtitle_config()
body_color = saved_dynamic["active_color"]
dynamic_config = DynamicSubtitleConfig(
style=WordStyle(
font=saved_dynamic["font"],
font_size=int(saved_dynamic["font_size"]),
active_color=body_color,
inactive_color="0.7 0.7 0.7 1",
emphasis_look=WordLook(
int(saved_dynamic["emphasis_size"]),
saved_dynamic["emphasis_color"] or body_color,
font=saved_dynamic["emphasis_font"],
face=saved_dynamic["emphasis_face"],
kerning=0.0,
),
body_look=WordLook(
int(saved_dynamic["font_size"]),
body_color,
font=saved_dynamic["font"],
face="Bold",
kerning=1.2,
),
),
band_height=float(saved_dynamic["band_height"]),
block_center_y=float(saved_dynamic["block_center_y"]),
granularity=granularity,
text_scale=float(saved_dynamic["text_scale"]),
line_gap=float(saved_dynamic["line_gap"]),
)
saved_plain = load_plain_subtitle_config()
plain_font = saved_plain["font"]
plain_font_size = int(saved_plain["font_size"])
plain_font_color = saved_plain["font_color"]
max_words = max(1, int(arguments.get("max_words", saved_plain["max_words"])))
position_y = float(saved_plain["position_y"])
uppercase = bool(arguments.get("uppercase", saved_plain["uppercase"]))
keep_punctuation = bool(arguments.get("keep_punctuation", saved_plain["keep_punctuation"]))
filepath, output_path, modifier = _setup_modifier(arguments, "_emphasis_subtitles")
added: list[tuple[str, int, int, int, int]] = []
skipped: list[tuple[str, str]] = []
no_review: list[str] = []
spine_clips = [el for _, el in modifier._iter_spine_clips()]
for el in spine_clips:
name = el.get("name", "")
if clip_filter and name != clip_filter:
continue
src = modifier.resources.get(el.get("ref", ""), {}).get("src", "")
media_path = media_src_to_path(src)
if not media_path or not Path(media_path).is_file():
skipped.append((name, "media file missing"))
continue
data, reason = _load_or_transcribe(media_path, model, language, output_dir)
if data is None:
skipped.append((name, reason))
continue
spans = _load_emphasis_spans(media_path)
if not spans:
no_review.append(name)
clip_source_start = modifier.source_file_start(el).to_seconds()
clip_duration = modifier._parse_time(el.get("duration", "0s")).to_seconds()
window_end = clip_source_start + clip_duration
# Clip-relative windows, for deciding which plain titles to hide —
# same coordinate space add_text_title's offsets end up in.
clip_spans = [
(max(0.0, float(s["start"]) - clip_source_start), min(clip_duration, float(s["end"]) - clip_source_start))
for s in spans
if float(s["end"]) > clip_source_start and float(s["start"]) < window_end
]
all_words = data.get("words", [])
dynamic_lines = 0
dynamic_word_count = 0
emphasis_words = _words_in_spans(all_words, spans)
clip_emphasis_words = _words_overlapping_clip(emphasis_words, clip_source_start, window_end)
if clip_emphasis_words:
all_segments = data.get("segments", [])
emphasis_segments = _segments_in_spans(all_segments, spans)
clip_segments = [
{
"start": float(s.get("start", 0.0)) - clip_source_start,
"end": float(s.get("end", 0.0)) - clip_source_start,
}
for s in emphasis_segments
if float(s.get("end", 0.0)) > clip_source_start
and float(s.get("start", 0.0)) < window_end
]
# Pass the element itself, not `name` — see the same note in
# handle_generate_dynamic_subtitles (Engine/docs/05_EXPERIENCIAS.md,
# entry 2026-08-17).
dynamic_lines = len(
modifier.generate_dynamic_subtitles(
el, clip_emphasis_words, dynamic_config, segments=clip_segments
)
)
dynamic_word_count = len(clip_emphasis_words)
# Plain covers EVERY word in the clip — never filtered by emphasis.
# Titles landing under a dynamic phrase are disabled below instead of
# never being created, so turning emphasis off later never leaves a
# silent gap where neither style is on screen.
plain_created = 0
plain_hidden = 0
clip_all_words = _words_overlapping_clip(all_words, clip_source_start, window_end)
blocks = _plain_subtitle_blocks(clip_all_words, max_words)
for block in blocks:
parts = [
_plain_word_text(w.get("word", ""), uppercase=uppercase, keep_punctuation=keep_punctuation)
for w in block
]
text = " ".join(p for p in parts if p).strip()
if not text:
continue
start = max(0.0, min(float(w.get("start", 0.0)) for w in block))
end = max(float(w.get("end", start)) for w in block)
duration = max(end - start, modifier.frame_duration_fraction())
title = modifier.add_text_title(
el,
text,
offset=f"{start:.6f}s",
duration=f"{duration:.6f}s",
lane=20,
position=f"0 {position_y:g}",
font=plain_font,
font_size=plain_font_size,
font_color=plain_font_color,
bold=True,
face=None,
font_scale=1.0,
size_param=plain_font_size,
)
plain_created += 1
if _overlaps_any_span(start, end, clip_spans):
title.set("enabled", "0")
plain_hidden += 1
if dynamic_lines or plain_created:
added.append(
(name, dynamic_lines, plain_created, plain_hidden, dynamic_word_count + len(clip_all_words))
)
else:
skipped.append((name, "no words in clip's source range"))
if not added:
text = "# Subtitles by Emphasis\n\nNo captions generated — file unchanged (nothing saved)."
if skipped:
text += "\n\n## Skipped Clips\n" + _markdown_table(
["Clip", "Reason"], [[n, r] for n, r in skipped]
)
return _text_result(text)
modifier.save(output_path)
total_dynamic = sum(d for _, d, _, _, _ in added)
total_plain = sum(p for _, _, p, _, _ in added)
total_hidden = sum(h for _, _, _, h, _ in added)
total_words = sum(w for _, _, _, _, w in added)
result = "# Subtitles by Emphasis Generated\n\n## Summary\n"
result += (
f"- **Clips Captioned**: {len(added)}\n"
f"- **Dynamic Title Lines (emphasis)**: {total_dynamic}\n"
f"- **Plain Title Blocks (full clip)**: {total_plain}\n"
f"- **Plain Blocks Hidden Under Emphasis (enabled=\"0\")**: {total_hidden}\n"
f"- **Total Words**: {total_words}\n\n"
)
result += _markdown_table(
["Clip", "Dynamic Lines", "Plain Blocks", "Hidden", "Words"],
[[n, str(d), str(p), str(h), str(w)] for n, d, p, h, w in added],
)
if no_review:
result += (
"\n## Sem revisão de ênfase\n"
"Nenhum `_phrase_actions.json` encontrado para: "
+ ", ".join(no_review)
+ " — todas as frases desses clipes saíram como legenda comum. "
"Rode a etapa 5 do Assistente (revisão de frases) antes, se quiser destaque dinâmico.\n"
)
if skipped:
result += "\n## Skipped Clips\n" + _markdown_table(
["Clip", "Reason"], [[n, r] for n, r in skipped]
)
result += f"\n\nSaved to: `{output_path}`\n\n*Transcripts are cached as _transcript.json; emphasis spans from _phrase_actions.json.*"
return _text_result(result)
HANDLERS = {
"validate_subtitle_layout": handle_validate_subtitle_layout,
"generate_dynamic_subtitles": handle_generate_dynamic_subtitles,
"generate_plain_subtitles": handle_generate_plain_subtitles,
"generate_subtitles_by_emphasis": handle_generate_subtitles_by_emphasis,
}
+166
View File
@@ -13,6 +13,7 @@ from mcp.types import TextContent, Tool
from fcpxml.diarize import assign_speakers, build_speakers, diarization_capability, diarize
from fcpxml.emphasis import EmphasisWeights
from fcpxml.llm_local import DEFAULT_BASE_URL, generate_voice_actions
from fcpxml.media_intel import media_src_to_path
from fcpxml.model_manager import (
load_hf_token,
@@ -21,6 +22,7 @@ from fcpxml.model_manager import (
save_voice_analysis_config,
)
from fcpxml.models import TimeValue
from fcpxml.phrase_review import build_phrase_review, save_phrase_review
from fcpxml.voice_actions import parse_actions, resolve_actions, speaker_cut_actions
from fcpxml.voice_features import extract_energy, extract_pitch, features_capability
from fcpxml.voice_timeline import (
@@ -93,6 +95,7 @@ TOOLS = [
"hf_token": {"type": "string", "description": "HuggingFace token for speaker diarization (default: the persisted token; omit to skip diarization)"},
"num_speakers": {"type": "string", "description": "Known number of speakers, if any (default: the persisted setting, else auto-detect)"},
"output_dir": {"type": "string", "description": "Folder to write _voice_timeline.json into (default: next to the media file)"},
"rotation": {"type": "number", "description": "Degrees the clip is rotated by in the FCPXML (e.g. a Transform filter straightening a tilted phone shot). Recorded in the timeline JSON so a preview can apply the same correction. Default 0."},
},
"required": ["media_path"]
}
@@ -167,6 +170,27 @@ TOOLS = [
"required": ["filepath", "actions"]
}
),
Tool(
name="generate_voice_script",
description="Run the WHOLE voice-edit pass internally, no wizard, no copy-paste: reuse an existing voice timeline (or transcribe + build one) -> hand it to a LOCAL model (Ollama running Gemma 3 / Llama) that directs the edit -> return the readable script (roteiro) AND the action JSON, and optionally apply it to a FCPXML. The model reads the full _voice_timeline.json (the whole file goes with the brief) and emits cut/zoom/text/marker decisions per the editar-por-voz brief; decisions are validated row-by-row so one bad row never discards the edit. Times stay in ORIGINAL source seconds; the applier resolves cuts and shifts everything else. Writes _voice_timeline.json, _phrase_review.json, _phrase_actions.json and (when applying) a _voice_edit FCPXML. Defaults to the local model 'gemma3:12b' at http://localhost:11434 — change via model/base_url.",
inputSchema={
"type": "object",
"properties": {
"media_path": {"type": "string", "description": "Path to the audio/video file to analyze and direct (.wav, .mp3, .m4a, .aac, .aif, .flac, .mov, .mp4). Required when there is no voice_timeline yet; ignored when voice_timeline is provided."},
"voice_timeline": {"type": "string", "description": "Path to an existing _voice_timeline.json (e.g. from the assistant's analysis step). When given, it is reused and transcription/acoustics are skipped — the model gets the whole file to direct the edit."},
"filepath": {"type": "string", "description": "Optional FCPXML to apply the decisions to (non-destructive: writes a _voice_edit copy). When omitted, only the script and actions are produced."},
"model": {"type": "string", "default": "gemma3:12b", "description": "Local model Ollama serves (e.g. 'gemma3:12b', 'gemma3:4b', 'llama3')"},
"base_url": {"type": "string", "default": "http://localhost:11434", "description": "Ollama base URL"},
"model_size": {"type": "string", "default": "base", "description": "Whisper model size to use if transcription is needed"},
"language": {"type": "string", "description": "ISO language code hint for transcription, if needed"},
"hf_token": {"type": "string", "description": "HuggingFace token for speaker diarization (omit to skip)"},
"num_speakers": {"type": "string", "description": "Known number of speakers, if any"},
"output_dir": {"type": "string", "description": "Folder to write the timeline/review/actions JSON into (default: next to the media file)"},
"apply_to_fcpxml": {"type": "boolean", "default": True, "description": "When filepath is given, apply the decisions to it. Set false to only produce the script."},
},
"required": []
}
),
Tool(
name="get_voice_analysis_config",
description="Read the persisted Voice Analysis settings: energy threshold, emphasis-index weights (energy/pitch_variation/rate_variation/pause_before/duration), emphasis cutoff for punch-in candidates, and emotion detection toggle/sensitivity. Shared with the MacApp settings screen (~/.fcp-mcp-server/config.json).",
@@ -349,6 +373,7 @@ async def handle_build_voice_timeline(arguments: dict) -> Sequence[TextContent]:
token = str(arguments.get("hf_token") or "").strip() or load_hf_token() or None
num_speakers = str(arguments.get("num_speakers") or "").strip() or load_num_speakers()
output_dir = arguments.get("output_dir")
rotation = float(arguments.get("rotation") or 0.0)
transcript, reason = _load_or_transcribe(media_path, model, language, output_dir)
if transcript is None:
@@ -368,6 +393,7 @@ async def handle_build_voice_timeline(arguments: dict) -> Sequence[TextContent]:
emphasis_floor=config["emphasis_floor"],
emotion_enabled=config["emotion_enabled"],
emotion_sensitivity=config["emotion_sensitivity"],
rotation=rotation,
)
json_path = Path(_validate_output_path(
@@ -742,6 +768,145 @@ async def handle_save_voice_analysis_config(arguments: dict) -> Sequence[TextCon
return _text_result(_voice_analysis_config_text(config))
async def handle_generate_voice_script(arguments: dict) -> Sequence[TextContent]:
"""The whole voice-edit pass, run inside the engine against a local model.
Transcribe (cached) -> build the voice timeline -> ask the local LLM to
direct the edit -> build the readable script (roteiro) + the action JSON ->
optionally apply to a FCPXML. No wizard, no copy-paste: the model's JSON is
parsed and validated like any other decision source, and the applier turns
it into FCPXML the same way it would for the rules engine.
"""
model = "gemma3:12b" if not arguments.get("model") else str(arguments["model"])
base_url = str(arguments.get("base_url") or DEFAULT_BASE_URL)
model_size = arguments.get("model_size", "base")
language = arguments.get("language")
token = str(arguments.get("hf_token") or "").strip() or load_hf_token() or None
num_speakers = str(arguments.get("num_speakers") or "").strip() or load_num_speakers()
output_dir = arguments.get("output_dir")
fcpxml_path = arguments.get("filepath")
apply = bool(arguments.get("apply_to_fcpxml", True)) and bool(fcpxml_path)
# Camino 1: já temos uma voice timeline (etapa de análise do assistente) —
# reaproveita e pula a transcrição/análise acústica/diarização, que é caro.
# Camino 2: só mídia — transcreve e monta a timeline do zero.
vt_arg = arguments.get("voice_timeline")
timeline = load_voice_timeline(Path(vt_arg)) if vt_arg and Path(vt_arg).is_file() else None
media_path = arguments.get("media_path")
if timeline is None:
media_path = _validate_filepath(
media_path, AUDIO_MEDIA_EXTENSIONS, max_size=MAX_MEDIA_FILE_SIZE
)
transcript, reason = _load_or_transcribe(media_path, model_size, language, output_dir)
if transcript is None:
return _text_result(
f"# Roteiro por IA Local\n\nNão foi possível obter a transcrição "
f"({reason}).{_TRANSCRIBE_INSTALL_HINT}"
)
config = load_voice_analysis_config()
timeline = build_voice_timeline(
media_path,
transcript,
hf_token=token,
num_speakers=num_speakers,
weights=EmphasisWeights.from_dict(config["emphasis_weights"]),
peak_percentile=config["peak_percentile"],
emphasis_floor=config["emphasis_floor"],
emotion_enabled=config["emotion_enabled"],
emotion_sensitivity=config["emotion_sensitivity"],
)
vt_arg = str(_validate_output_path(
str(voice_timeline_path(media_path, output_dir)),
anchor_dir=str(Path(output_dir) if output_dir else Path(media_path).parent),
))
save_voice_timeline(timeline, Path(vt_arg))
else:
# A timeline veio pronta; a mídia só é necessária se for aplicar e o
# caller não a passou — deriva do próprio campo `source` da timeline.
if not media_path:
candidate = Path(vt_arg).parent / timeline.get("source", "")
media_path = str(candidate) if candidate.is_file() else None
decision = generate_voice_actions(timeline, model=model, base_url=base_url)
actions = decision["actions"]
errors = list(decision["errors"])
if not actions and errors:
# The model produced nothing usable (transport error or unparseable
# response) — report it clearly instead of a silent "0 decisions".
raise RuntimeError(
"O modelo local não devolveu decisões utilizáveis: " + "; ".join(errors)
)
review = build_phrase_review(
timeline,
[a.as_dict() for a in actions],
voice_timeline_path=str(vt_arg),
)
review_path, actions_path = save_phrase_review(str(vt_arg), review)
roteiro = _roteiro_markdown(review, timeline.get("source", ""))
roteiro_path = Path(vt_arg).with_name(Path(vt_arg).stem.replace("_voice_timeline", "") + "_roteiro.md")
roteiro_path.write_text(roteiro, encoding="utf-8")
applied_text = ""
if apply:
contents = await handle_apply_voice_actions({
"filepath": fcpxml_path,
"actions": [a.as_dict() for a in actions],
"output_dir": output_dir,
})
applied_text = "\n\n" + "\n".join(getattr(c, "text", str(c)) for c in contents)
result = f"""# Roteiro por IA Local ({model})
## Resumo
- **Fonte**: {timeline.get('source', '')}
- **Duração**: {format_duration(timeline['summary']['duration'])}
- **Decisões do modelo**: {len(actions)} (cortes/zoom/texto/marcador)
- **Linha do tempo**: {vt_arg}
- **Roteiro (legível)**: {roteiro_path}
- **Ações JSON**: {actions_path}
- **Revisão de frases**: {review_path}
"""
if errors:
result += "\n## Rejeitado / avisos\n" + "\n".join(f"- {e}" for e in errors) + "\n"
result += "\n---\n\n" + roteiro
result += applied_text
result += "\n\n*Tudo rodou internamente: o modelo local leu a timeline e decidiu a edição; nenhum passo manual foi necessário.*"
return _text_result(result)
def _roteiro_markdown(review: dict, source: str) -> str:
"""The readable script: kept lines (roteiro) then the cut/bastidor lines."""
phrases = review.get("phrases", [])
kept = [p for p in phrases if p.get("active")]
cut = [p for p in phrases if not p.get("active")]
lines = [f"# Roteiro — {source}", ""]
lines.append(f"**{len(kept)} falas mantidas · {len(cut)} cortadas**")
lines.append("")
lines.append("## Roteiro (mantido)")
if not kept:
lines.append("_Nenhuma fala mantida._")
for p in kept:
tag = ""
if p.get("emphasis", 0) >= 1:
tag = f" · zoom nível {p['emphasis']}"
spk = f"[{p.get('speaker', '')}] " if p.get("speaker") else ""
lines.append(f"- {spk}{p.get('text', '')}{tag}")
if p.get("reason"):
lines.append(f" - _decisão_: {p['reason']}")
if cut:
lines.append("")
lines.append("## Cortado / bastidor")
for p in cut:
spk = f"[{p.get('speaker', '')}] " if p.get("speaker") else ""
lines.append(f"- {spk}{p.get('text', '')}")
if p.get("reason"):
lines.append(f" - _por que cortou_: {p['reason']}")
return "\n".join(lines) + "\n"
HANDLERS = {
"diarize_media": handle_diarize_media,
"analyze_voice_features": handle_analyze_voice_features,
@@ -749,6 +914,7 @@ HANDLERS = {
"remove_speakers": handle_remove_speakers,
"refine_voice_timeline": handle_refine_voice_timeline,
"apply_voice_actions": handle_apply_voice_actions,
"generate_voice_script": handle_generate_voice_script,
"get_voice_analysis_config": handle_get_voice_analysis_config,
"save_voice_analysis_config": handle_save_voice_analysis_config,
}
+23
View File
@@ -264,3 +264,26 @@ class TestIntegration:
report = modifier.validate_subtitle_layout()
assert report["summary"]["spatial_collision"] >= 1
assert blocking(report["severity"])
def test_disabled_title_is_excluded_from_validation(self, temp_fcpxml):
"""A title with enabled="0" never renders in Final Cut
(generate_subtitles_by_emphasis disables plain titles under an
emphasis phrase instead of never creating them) — it must not count
as a collision, or as outside-frame/outside-safe-area, against the
title actually drawn in its place."""
modifier = FCPXMLModifier(temp_fcpxml)
titles = modifier.generate_dynamic_subtitles("Interview_A", WORDS, WORD_MODE)
def position(el):
for p in el.findall("param"):
if p.get("name") == "Position":
return p
return None
p0 = position(titles[0])
position(titles[1]).set("value", p0.get("value"))
titles[1].set("enabled", "0")
report = modifier.validate_subtitle_layout()
assert report["summary"]["spatial_collision"] == 0
assert not blocking(report["severity"])
+232
View File
@@ -0,0 +1,232 @@
"""Tests for fcpxml/forced_align.py — optional phonetic forced alignment.
The dependency (whisperx) is not installed in CI, so the core contract under
test is graceful degradation: when whisperx is unavailable the aligner returns
the words unchanged. A second group injects a fake whisperx module to verify
the refined times are written back in order and that malformed results are
skipped rather than clobbering good timestamps.
"""
import sys
import types
from pathlib import Path
import pytest
from fcpxml.forced_align import ForcedAligner
def _words():
return [
{"word": "Um,", "start": 0.0, "end": 0.5, "confidence": 0.9},
{"word": "welcome", "start": 0.5, "end": 1.0, "confidence": 0.9},
{"word": "show.", "start": 1.5, "end": 2.5, "confidence": 0.8},
]
def _raw_segments():
return [
{
"text": "Um, welcome",
"start": 0.0,
"end": 1.0,
"words": _words()[:2],
},
{
"text": "show.",
"start": 1.5,
"end": 2.5,
"words": _words()[2:],
},
]
def _fake_whisperx(shift=0.4):
"""A stand-in whisperx module that "corrects" word starts by ``shift``."""
mod = types.SimpleNamespace()
def load_audio(path):
return [0.0]
def load_align_model(language_code, device, model_dir=None):
return ("MODEL", {"language": language_code})
def align(align_input, align_model, metadata, audio, device,
return_char_alignments=False, chunk_size=30):
segments = []
for seg in align_input:
new_words = []
for w in seg["words"]:
new_words.append(
{
"word": w["word"],
"start": w["start"] + shift,
"end": w["end"] + shift,
"score": w["score"],
}
)
segments.append({**seg, "words": new_words})
return {"segments": segments}
mod.load_audio = load_audio
mod.load_align_model = load_align_model
mod.align = align
return mod
class TestForcedAlignerDegradation:
def test_unavailable_when_whisperx_missing(self):
assert ForcedAligner.available() is False
def test_returns_words_unchanged_when_whisperx_missing(self, monkeypatch):
import builtins
real_import = builtins.__import__
def block(name, *a, **k):
if name == "whisperx":
raise ImportError("blocked")
return real_import(name, *a, **k)
monkeypatch.setattr(builtins, "__import__", block)
result = ForcedAligner().align(_words(), _raw_segments(), "x.wav", "en")
assert result == _words()
def test_skips_when_no_words(self):
assert ForcedAligner().align([], [], "x.wav", "en") == []
class TestForcedAlignerWithWhisperX:
@pytest.fixture
def whisperx(self, monkeypatch):
fake = _fake_whisperx(shift=0.4)
monkeypatch.setitem(sys.modules, "whisperx", fake)
return fake
def test_refines_timestamps_in_order(self, whisperx):
words = _words()
result = ForcedAligner().align(words, _raw_segments(), "x.wav", "en")
assert [w["start"] for w in result] == [0.4, 0.9, 1.9]
assert [w["end"] for w in result] == [0.9, 1.4, 2.9]
# The same dict objects are returned with times overwritten in place.
assert result[0]["start"] == 0.4
assert words[0]["start"] == 0.4
def test_caches_align_model_per_language(self, whisperx, monkeypatch):
calls = {"n": 0}
orig = whisperx.load_align_model
def counting(*a, **k):
calls["n"] += 1
return orig(*a, **k)
whisperx.load_align_model = counting
aligner = ForcedAligner()
aligner.align(_words(), _raw_segments(), "a.wav", "en")
aligner.align(_words(), _raw_segments(), "b.wav", "en")
assert calls["n"] == 1
def test_skips_unusable_word_times(self, monkeypatch):
fake = _fake_whisperx()
# Force one word to come back with None start (alignment failed).
real_align = fake.align
def broken(align_input, *a, **k):
out = real_align(align_input, *a, **k)
out["segments"][0]["words"][0]["start"] = None
return out
fake.align = broken
monkeypatch.setitem(sys.modules, "whisperx", fake)
words = _words()
result = ForcedAligner().align(words, _raw_segments(), "x.wav", "en")
# First word time untouched (None skipped), rest corrected.
assert result[0]["start"] == 0.0
assert result[1]["start"] == 0.9
def test_unexpected_exception_returns_original(self, monkeypatch):
fake = types.SimpleNamespace()
fake.load_audio = lambda p: [0.0]
fake.load_align_model = lambda *a, **k: ("M", {})
fake.align = lambda *a, **k: 1 / 0 # boom
monkeypatch.setitem(sys.modules, "whisperx", fake)
words = _words()
result = ForcedAligner().align(words, _raw_segments(), "x.wav", "en")
assert result == words
class TestTranscribeAlignmentFlag:
"""Wire-up: transcribe() reports whether forced alignment ran."""
def _install_fakes(self, monkeypatch, align_shift=0.4):
# faster_whisper
fw = types.SimpleNamespace()
class _Word:
def __init__(self, word, start, end, prob):
self.word = word
self.start = start
self.end = end
self.probability = prob
class _Seg:
def __init__(self, text, start, end, words):
self.text = text
self.start = start
self.end = end
self.words = words
class _Info:
language = "en"
duration = 2.5
class _Model:
def transcribe(self, path, language=None, word_timestamps=False, vad_filter=False):
seg = _Seg(
"Um, welcome show.",
0.0,
2.5,
[
_Word("Um,", 0.0, 0.5, 0.9),
_Word("welcome", 0.5, 1.0, 0.9),
_Word("show.", 1.5, 2.5, 0.8),
],
)
return iter([seg]), _Info()
fw.WhisperModel = lambda *a, **k: _Model()
monkeypatch.setitem(sys.modules, "faster_whisper", fw)
# whisperx (only needed when align=True)
wx = _fake_whisperx(shift=align_shift)
monkeypatch.setitem(sys.modules, "whisperx", wx)
# model_manager.get_models_dir
import fcpxml.model_manager as mm
monkeypatch.setattr(mm, "get_models_dir", lambda: Path("/tmp"))
def test_alignment_true_when_whisperx_present(self, monkeypatch, tmp_path):
self._install_fakes(monkeypatch)
f = tmp_path / "a.wav"
f.write_bytes(b"RIFF0000WAVE")
from fcpxml.transcribe import transcribe
result = transcribe(str(f), model_size="base", align=True)
assert result is not None
assert result["alignment"] is True
assert result["words"][0]["start"] == pytest.approx(0.4)
def test_alignment_false_when_disabled(self, monkeypatch, tmp_path):
self._install_fakes(monkeypatch)
f = tmp_path / "a.wav"
f.write_bytes(b"RIFF0000WAVE")
from fcpxml.transcribe import transcribe
result = transcribe(str(f), model_size="base", align=False)
assert result is not None
assert result["alignment"] is False
assert result["words"][0]["start"] == 0.0
+259
View File
@@ -0,0 +1,259 @@
"""Tests for the local LLM integration (Ollama): prompt building, JSON
extraction, and turning a model response into validated voice actions.
The model itself is mocked — these tests cover the client contract
(extraction, validation, message shape) without a running Ollama. A real
end-to-end run lives in the manual test harness (see ENGINE notes) because it
needs the local model server.
"""
import json
import pytest
from fcpxml import llm_local
from fcpxml.llm_local import (
_extract_json,
build_edit_messages,
generate_voice_actions,
)
from fcpxml.voice_actions import parse_actions
def _fake_timeline() -> dict:
"""A minimal but well-formed voice timeline (matches build_voice_timeline)."""
return {
"version": "1.0",
"source": "demo.mp4",
"rotation": 0.0,
"language": "pt",
"layers": {
"transcript": True,
"acoustics": True,
"speakers": False,
"emotion": False,
"alignment": False,
},
"scales": {},
"summary": {
"duration": 12.0,
"speaker_count": 1,
"segment_count": 2,
"word_count": 4,
"avg_emphasis": 0.4,
"peak_selection": "top 2%",
"peak_count": 1,
"peak_moments": [],
},
"speakers": [
{
"id": "SPEAKER_00",
"name": "Speaker 1",
"speaking_seconds": 12.0,
"share": 1.0,
"segment_count": 2,
"avg_segment": 6.0,
"word_count": 4,
"samples": [],
}
],
"segments": [
{
"start": 0.0,
"end": 6.0,
"speaker": "SPEAKER_00",
"text": "Hoje vamos falar de mastopexia.",
"gap_before": 0.0,
"take_boundary": False,
"avg_energy": 0.5,
"peak_emphasis": 0.6,
"emotion": "neutral",
"emotion_confidence": 0.0,
"arousal": 0.5,
"valence": 0.6,
"words": [
{"text": "Hoje", "start": 0.1, "end": 0.5, "speaker": "SPEAKER_00",
"energy": 0.3, "pitch_delta": 0.2, "rate_delta": 0.1, "pause_before": 0.1,
"emphasis": 0.3, "emotion": "neutral", "emotion_confidence": 0.0,
"arousal": 0.4, "valence": 0.6, "energy_raw": 0.3, "pitch_hz": 120.0},
{"text": "vamos", "start": 0.6, "end": 0.9, "speaker": "SPEAKER_00",
"energy": 0.4, "pitch_delta": 0.3, "rate_delta": 0.2, "pause_before": 0.0,
"emphasis": 0.5, "emotion": "neutral", "emotion_confidence": 0.0,
"arousal": 0.5, "valence": 0.6, "energy_raw": 0.4, "pitch_hz": 125.0},
],
},
{
"start": 6.0,
"end": 12.0,
"speaker": "SPEAKER_00",
"text": "E aí cara, tá gravando?",
"gap_before": 0.2,
"take_boundary": False,
"avg_energy": 0.4,
"peak_emphasis": 0.3,
"emotion": "neutral",
"emotion_confidence": 0.0,
"arousal": 0.4,
"valence": 0.5,
"words": [
{"text": "E", "start": 6.1, "end": 6.3, "speaker": "SPEAKER_00",
"energy": 0.3, "pitch_delta": 0.2, "rate_delta": 0.1, "pause_before": 0.1,
"emphasis": 0.3, "emotion": "neutral", "emotion_confidence": 0.0,
"arousal": 0.4, "valence": 0.5, "energy_raw": 0.3, "pitch_hz": 120.0},
{"text": "cara", "start": 6.4, "end": 6.7, "speaker": "SPEAKER_00",
"energy": 0.4, "pitch_delta": 0.3, "rate_delta": 0.2, "pause_before": 0.0,
"emphasis": 0.4, "emotion": "neutral", "emotion_confidence": 0.0,
"arousal": 0.4, "valence": 0.5, "energy_raw": 0.4, "pitch_hz": 122.0},
],
},
],
}
def test_extract_json_unfenced():
text = '{"source": "x", "actions": []}'
assert _extract_json(text) == {"source": "x", "actions": []}
def test_extract_json_with_fence():
text = '```json\n{"source": "x", "actions": [{"kind": "cut", "start": 1, "end": 2}]}\n```'
data = _extract_json(text)
assert data["source"] == "x"
assert data["actions"][0]["kind"] == "cut"
def test_extract_json_with_prose_around():
text = 'Aqui está:\n{"source": "x", "actions": []}\nfim.'
assert _extract_json(text) == {"source": "x", "actions": []}
def test_extract_json_invalid_returns_none():
assert _extract_json("no json here") is None
assert _extract_json("") is None
def test_extract_json_unwraps_wrapped_list():
# Some models wrap the expected {"source","actions"} object in a
# single-element list; the actions must still be found.
text = '```json\n[{"source": "x", "actions": [{"kind": "cut", "start": 1, "end": 2}]}]\n```'
data = _extract_json(text)
assert isinstance(data, dict)
assert data["source"] == "x"
assert data["actions"][0]["kind"] == "cut"
def test_build_edit_messages_shape():
system, user = build_edit_messages(_fake_timeline())
assert "editor" in system.lower()
assert "demo.mp4" in user
assert "segments" in user
assert "SOMENTE" in user
def test_generate_voice_actions_parses_model_json(monkeypatch):
canned = json.dumps({
"source": "demo.mp4",
"actions": [
{"kind": "cut", "start": 6.0, "end": 12.0,
"reason": "papo casual com a equipe"},
{"kind": "zoom", "start": 0.1, "end": 0.9, "params": {"scale": 1.3},
"reason": "ênfase em 'vamos'"},
],
})
monkeypatch.setattr(llm_local, "ollama_chat", lambda **kwargs: canned)
result = generate_voice_actions(_fake_timeline(), model="test")
actions, errors = parse_actions([a.as_dict() for a in result["actions"]])
assert errors == []
assert len(actions) == 2
assert {a.kind for a in actions} == {"cut", "zoom"}
assert result["errors"] == []
def test_generate_voice_actions_reports_bad_rows(monkeypatch):
canned = json.dumps({
"source": "demo.mp4",
"actions": [
{"kind": "cut", "start": 6.0, "end": 12.0, "reason": "ok"},
{"kind": "bogus", "start": 1, "end": 2},
{"kind": "zoom", "start": 0.1, "end": 0.05},
],
})
monkeypatch.setattr(llm_local, "ollama_chat", lambda **kwargs: canned)
result = generate_voice_actions(_fake_timeline(), model="test")
assert len(result["actions"]) == 1
assert result["actions"][0].kind == "cut"
assert len(result["errors"]) >= 2
def test_generate_voice_actions_handles_transport_error(monkeypatch):
def _boom(**kwargs):
raise RuntimeError("ollama down")
monkeypatch.setattr(llm_local, "ollama_chat", _boom)
result = generate_voice_actions(_fake_timeline(), model="test")
assert result["actions"] == []
assert any("ollama" in e.lower() for e in result["errors"])
def test_build_edit_messages_is_compact():
"""The prompt must drop the heavy per-word audio features so a real
recording fits in the model context (the 47k-token dump made Ollama drop
the connection)."""
import json as _json
timeline = _fake_timeline()
# pad with the kind of audio detail a real timeline carries
timeline["segments"][0]["words"][0]["samples"] = [0.1, 0.2, 0.3]
timeline["speakers"][0]["samples"] = [1, 2, 3]
raw = _json.dumps(timeline, ensure_ascii=False)
system, user = build_edit_messages(timeline)
assert "demo.mp4" in user and "segments" in user
# projected prompt is materially smaller than the raw timeline
assert len(user) < len(raw)
# heavy fields we deliberately drop never reach the model
assert '"samples"' not in user
assert '"energy_raw"' not in user
assert '"pitch_hz"' not in user
def test_ollama_chat_wraps_empty_response(monkeypatch):
"""A dropped connection that yields an empty body must surface as a clear
RuntimeError (not an unhandled JSONDecodeError crashing the pipeline)."""
class _Resp:
def raise_for_status(self):
pass
def json(self):
raise ValueError("Expecting value: line 1 column 1")
monkeypatch.setattr(llm_local.httpx, "post", lambda *a, **k: _Resp())
try:
llm_local.ollama_chat(model="test", messages=[{"role": "user", "content": "x"}])
except RuntimeError as exc:
assert "Falha ao falar com o modelo local" in str(exc)
else:
raise AssertionError("expected RuntimeError")
def test_list_ollama_models_parses_tags(monkeypatch):
class _Resp:
def raise_for_status(self):
pass
def json(self):
return {"models": [{"name": "gemma3:4b"}, {"name": "gemma3:12b"}]}
monkeypatch.setattr(llm_local.httpx, "get", lambda *a, **k: _Resp())
assert llm_local.list_ollama_models() == ["gemma3:12b", "gemma3:4b"]
def test_list_ollama_models_returns_empty_on_error(monkeypatch):
def _boom(*a, **k):
raise RuntimeError("ollama down")
monkeypatch.setattr(llm_local.httpx, "get", _boom)
assert llm_local.list_ollama_models() == []
+134
View File
@@ -0,0 +1,134 @@
"""Tests for layering subtitle generation by emphasis (etapa 5 review).
Covers the pure helpers in server_tools/subtitles.py that decide which words
also get the dynamic treatment on top of the always-complete plain track, and
which clip-relative windows a plain title must be hidden (enabled="0") under.
"""
import json
from pathlib import Path
from server_tools.subtitles import (
_load_emphasis_spans,
_overlaps_any_span,
_phrase_actions_path,
_segments_in_spans,
_word_in_spans,
_words_in_spans,
)
SPANS = [
{"start": 2.0, "end": 10.7, "level": 1, "text": "abertura"},
{"start": 127.7, "end": 135.0, "level": 2, "text": "mastopexia"},
]
def test_phrase_actions_path_matches_media_stem():
assert _phrase_actions_path("/x/y/0E6A8290.mp4") == Path(
"/x/y/0E6A8290_phrase_actions.json"
)
class TestLoadEmphasisSpans:
def test_missing_file_returns_empty(self, tmp_path):
media = tmp_path / "clip.mp4"
media.write_bytes(b"")
assert _load_emphasis_spans(str(media)) == []
def test_reads_spans_from_sibling_json(self, tmp_path):
media = tmp_path / "clip.mp4"
media.write_bytes(b"")
actions_path = tmp_path / "clip_phrase_actions.json"
actions_path.write_text(
json.dumps({"source": "clip.mp4", "actions": [], "emphasis_spans": SPANS}),
encoding="utf-8",
)
assert _load_emphasis_spans(str(media)) == SPANS
def test_corrupt_json_returns_empty(self, tmp_path):
media = tmp_path / "clip.mp4"
media.write_bytes(b"")
(tmp_path / "clip_phrase_actions.json").write_text("{not json", encoding="utf-8")
assert _load_emphasis_spans(str(media)) == []
def test_missing_emphasis_spans_key_returns_empty(self, tmp_path):
media = tmp_path / "clip.mp4"
media.write_bytes(b"")
(tmp_path / "clip_phrase_actions.json").write_text(
json.dumps({"source": "clip.mp4", "actions": []}), encoding="utf-8"
)
assert _load_emphasis_spans(str(media)) == []
class TestWordInSpans:
def test_word_fully_inside_span(self):
assert _word_in_spans(3.0, 3.4, SPANS) is True
def test_word_fully_outside_every_span(self):
assert _word_in_spans(50.0, 50.4, SPANS) is False
def test_word_straddling_span_boundary_follows_its_midpoint(self):
# midpoint 10.6 -> inside [2.0, 10.7)
assert _word_in_spans(10.4, 10.8, SPANS) is True
# midpoint 10.9 -> outside
assert _word_in_spans(10.7, 11.1, SPANS) is False
def test_span_end_is_exclusive(self):
assert _word_in_spans(10.7, 10.7, SPANS) is False
class TestWordsInSpans:
WORDS = [
{"word": "Aquela", "start": 2.03, "end": 2.69},
{"word": "mama", "start": 2.69, "end": 2.89},
{"word": "fora", "start": 50.0, "end": 50.2},
{"word": "mastopexia", "start": 127.74, "end": 128.58},
]
def test_no_spans_returns_nothing(self):
assert _words_in_spans(self.WORDS, []) == []
def test_keeps_only_words_inside_a_span(self):
kept = _words_in_spans(self.WORDS, SPANS)
assert [w["word"] for w in kept] == ["Aquela", "mama", "mastopexia"]
def test_does_not_remove_words_from_the_source_list(self):
"""This is a filter for the dynamic half, not a partition — the plain
half must still see every word, so this must never mutate `words`."""
before = list(self.WORDS)
_words_in_spans(self.WORDS, SPANS)
assert self.WORDS == before
class TestOverlapsAnySpan:
CLIP_SPANS = [(0.0, 8.64), (60.0, 67.22)]
def test_window_inside_a_span_overlaps(self):
assert _overlaps_any_span(1.0, 2.0, self.CLIP_SPANS) is True
def test_window_outside_every_span_does_not_overlap(self):
assert _overlaps_any_span(20.0, 21.0, self.CLIP_SPANS) is False
def test_window_straddling_a_span_edge_overlaps(self):
assert _overlaps_any_span(8.0, 9.0, self.CLIP_SPANS) is True
def test_touching_but_not_overlapping_is_not_an_overlap(self):
assert _overlaps_any_span(8.64, 9.0, self.CLIP_SPANS) is False
def test_no_spans_never_overlaps(self):
assert _overlaps_any_span(1.0, 2.0, []) is False
class TestSegmentsInSpans:
SEGMENTS = [
{"start": 2.0, "end": 10.67},
{"start": 40.0, "end": 43.0},
{"start": 127.74, "end": 134.96},
]
def test_no_spans_keeps_nothing(self):
assert _segments_in_spans(self.SEGMENTS, []) == []
def test_keeps_only_segments_inside_a_span(self):
kept = _segments_in_spans(self.SEGMENTS, SPANS)
assert kept == [self.SEGMENTS[0], self.SEGMENTS[2]]
Generated
+458 -309
View File
@@ -200,6 +200,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
]
[[package]]
name = "antlr4-python3-runtime"
version = "4.9.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" }
[[package]]
name = "anyio"
version = "4.14.2"
@@ -973,85 +979,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/03/126e963fc3237a416f3085b8a663ebd8ab449ed6c37195b4e0b49597ba0c/ctranslate2-4.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dc9f1abef55579cc02cdc74b3a55df38491ec56d177d6e6039609d61d09ed30e", size = 19499597, upload-time = "2026-07-03T12:40:01.68Z" },
]
[[package]]
name = "cuda-bindings"
version = "13.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cuda-pathfinder" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/21/8464d133752951c154feafb3b65c297e7d80f301183d220bec4c830f1441/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120fcc53d57903df529c3486962c56528cba5b7d6c57c99537320ed9922c8b86", size = 6073403, upload-time = "2026-05-29T23:11:36.22Z" },
{ url = "https://files.pythonhosted.org/packages/a8/1f/5ef51f5fbaa5d4d3201bb3d7555af028ec1aa4416275ccbf73c9e34e3d2d/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9851b0caa8bfd3bc6fa054eaf57bea7c8e9c3a62db2d2621224677f49f3c53d0", size = 6675244, upload-time = "2026-05-29T23:11:38.664Z" },
{ url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" },
{ url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" },
{ url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" },
{ url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" },
{ url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" },
{ url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" },
{ url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" },
{ url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" },
{ url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" },
{ url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" },
]
[[package]]
name = "cuda-pathfinder"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/b4/d088047afe39827556df21118cac9ffd20cc3f968c99a7681494d1eb333c/cuda_pathfinder-1.6.0-py3-none-any.whl", hash = "sha256:1503af579d8379c24bdd65528379bc57039b0455be9f5f9686cf8e473a1fce51", size = 54591, upload-time = "2026-07-21T15:03:56.224Z" },
]
[[package]]
name = "cuda-toolkit"
version = "13.0.3.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" },
]
[package.optional-dependencies]
cublas = [
{ name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cudart = [
{ name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cufft = [
{ name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cufile = [
{ name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cupti = [
{ name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
curand = [
{ name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cusolver = [
{ name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cusparse = [
{ name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
nvjitlink = [
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
nvrtc = [
{ name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
nvtx = [
{ name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
[[package]]
name = "cycler"
version = "0.12.1"
@@ -1129,6 +1056,9 @@ dependencies = [
]
[package.optional-dependencies]
align = [
{ name = "whisperx" },
]
dev = [
{ name = "black" },
{ name = "pytest" },
@@ -1158,8 +1088,9 @@ requires-dist = [
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
{ name = "whisperx", marker = "extra == 'align'", specifier = ">=3.0.0" },
]
provides-extras = ["intelligence", "transcribe", "diarization", "dev"]
provides-extras = ["intelligence", "transcribe", "align", "diarization", "dev"]
[[package]]
name = "filelock"
@@ -1601,22 +1532,21 @@ wheels = [
[[package]]
name = "huggingface-hub"
version = "1.27.0"
version = "0.36.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "filelock" },
{ name = "fsspec" },
{ name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
{ name = "httpx" },
{ name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
{ name = "packaging" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" },
{ url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
]
[[package]]
@@ -2537,6 +2467,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
]
[[package]]
name = "nltk"
version = "3.10.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "defusedxml" },
{ name = "joblib" },
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/e6/fe51d2bb1a3b446f59c5c8165999a9fee208bc346af90a7cbf7657bc0d75/nltk-3.10.3.tar.gz", hash = "sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4", size = 5137152, upload-time = "2026-08-12T23:46:37.258Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" },
]
[[package]]
name = "numba"
version = "0.67.0"
@@ -2810,155 +2756,129 @@ wheels = [
]
[[package]]
name = "nvidia-cublas"
version = "13.1.1.3"
name = "nvidia-cublas-cu12"
version = "12.8.4.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" },
]
[[package]]
name = "nvidia-cuda-cupti-cu12"
version = "12.8.90"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" },
]
[[package]]
name = "nvidia-cuda-nvrtc-cu12"
version = "12.8.93"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" },
]
[[package]]
name = "nvidia-cuda-runtime-cu12"
version = "12.8.90"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" },
]
[[package]]
name = "nvidia-cudnn-cu12"
version = "9.10.2.21"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cuda-nvrtc" },
{ name = "nvidia-cublas-cu12" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
{ url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
]
[[package]]
name = "nvidia-cuda-cupti"
version = "13.0.85"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" },
{ url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" },
]
[[package]]
name = "nvidia-cuda-nvrtc"
version = "13.0.88"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" },
{ url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" },
]
[[package]]
name = "nvidia-cuda-runtime"
version = "13.0.96"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" },
{ url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" },
]
[[package]]
name = "nvidia-cudnn-cu13"
version = "9.20.0.48"
name = "nvidia-cufft-cu12"
version = "11.3.3.83"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
{ name = "nvidia-nvjitlink-cu12" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
{ url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" },
{ url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
]
[[package]]
name = "nvidia-cufft"
version = "12.0.0.61"
name = "nvidia-cufile-cu12"
version = "1.13.1.3"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" },
]
[[package]]
name = "nvidia-curand-cu12"
version = "10.3.9.90"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" },
]
[[package]]
name = "nvidia-cusolver-cu12"
version = "11.7.3.90"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-cublas-cu12" },
{ name = "nvidia-cusparse-cu12" },
{ name = "nvidia-nvjitlink-cu12" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
{ url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" },
{ url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
]
[[package]]
name = "nvidia-cufile"
version = "1.15.1.6"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" },
{ url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" },
]
[[package]]
name = "nvidia-curand"
version = "10.4.0.35"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" },
{ url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" },
]
[[package]]
name = "nvidia-cusolver"
version = "12.0.4.66"
name = "nvidia-cusparse-cu12"
version = "12.5.8.93"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
{ name = "nvidia-cusparse" },
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-nvjitlink-cu12" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
{ url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" },
{ url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
]
[[package]]
name = "nvidia-cusparse"
version = "12.6.3.3"
name = "nvidia-cusparselt-cu12"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
{ url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" },
{ url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" },
]
[[package]]
name = "nvidia-cusparselt-cu13"
version = "0.8.1"
name = "nvidia-nccl-cu12"
version = "2.27.3"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" },
{ url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" },
{ url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" },
]
[[package]]
name = "nvidia-nccl-cu13"
version = "2.29.7"
name = "nvidia-nvjitlink-cu12"
version = "12.8.93"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" },
{ url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" },
{ url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" },
]
[[package]]
name = "nvidia-nvjitlink"
version = "13.3.33"
name = "nvidia-nvtx-cu12"
version = "12.8.90"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" },
{ url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" },
]
[[package]]
name = "nvidia-nvshmem-cu13"
version = "3.4.5"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" },
{ url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" },
]
[[package]]
name = "nvidia-nvtx"
version = "13.0.85"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" },
{ url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" },
{ url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" },
]
[[package]]
@@ -2970,6 +2890,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
]
[[package]]
name = "omegaconf"
version = "2.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "antlr4-python3-runtime" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472, upload-time = "2026-06-11T05:05:12.885Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502, upload-time = "2026-06-11T05:05:09.954Z" },
]
[[package]]
name = "onnxruntime"
version = "1.24.3"
@@ -3649,7 +3582,8 @@ dependencies = [
{ name = "torch" },
{ name = "torch-audiomentations" },
{ name = "torchaudio" },
{ name = "torchcodec" },
{ name = "torchcodec", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" },
{ name = "torchcodec", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform == 'emscripten'" },
{ name = "torchmetrics" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/d7/03b1d00d431c7be1d98635fefb99c2da64d9b84b4b8a06d5302666a96c88/pyannote_audio-4.0.7.tar.gz", hash = "sha256:3a903dfb5ca711d134cc2522561c9158b186836f5d62e8a57c46c6648e8a8229", size = 13963479, upload-time = "2026-06-30T14:28:53.454Z" }
@@ -4181,6 +4115,127 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
]
[[package]]
name = "regex"
version = "2026.7.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/13/bbf7d9d1887fe4a3693527c6caa232c197ea9da91f1212e9672eff60329d/regex-2026.7.19-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b", size = 494009, upload-time = "2026-07-19T00:16:13.602Z" },
{ url = "https://files.pythonhosted.org/packages/a3/19/783688e75a2bec15d50aec0d5e7e317d363808bc82a6eb6750b897bfcd7b/regex-2026.7.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52", size = 295287, upload-time = "2026-07-19T00:16:15.702Z" },
{ url = "https://files.pythonhosted.org/packages/16/01/cefe4f051302ca298d3f3e79ed6dbd933ac84485b9515acdd6a52d70cef7/regex-2026.7.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665", size = 290633, upload-time = "2026-07-19T00:16:17.182Z" },
{ url = "https://files.pythonhosted.org/packages/ae/1e/1045ca2cabb12e8ec41ad0d138e9f3ff1eb079d30a6f51ccf7d709b44aad/regex-2026.7.19-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0", size = 785300, upload-time = "2026-07-19T00:16:18.505Z" },
{ url = "https://files.pythonhosted.org/packages/d5/4a/20e5bca184e90bf1bd187efdb53363f4a7b7b34f01d54ced5740caf104bd/regex-2026.7.19-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951", size = 854079, upload-time = "2026-07-19T00:16:19.909Z" },
{ url = "https://files.pythonhosted.org/packages/09/9b/5a2e59678be3b24aa6a42b2c6d66a48daa212593e9f4096fe7ba577fa9b1/regex-2026.7.19-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3", size = 899496, upload-time = "2026-07-19T00:16:21.453Z" },
{ url = "https://files.pythonhosted.org/packages/48/9a/7317f14ed8ed9fd998d1978b4802b07bc4d79216353c435dbcc1ddd1301f/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c", size = 793541, upload-time = "2026-07-19T00:16:22.991Z" },
{ url = "https://files.pythonhosted.org/packages/6f/53/833c2db3e274d3c191f4c42fe5bfa358e4c8b617d5d7312d31334965fc46/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6", size = 785515, upload-time = "2026-07-19T00:16:24.654Z" },
{ url = "https://files.pythonhosted.org/packages/8d/b9/efb2f9fa151d71db09d4015e1fb92fee47416f01c12164836bbd23e2f3c2/regex-2026.7.19-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175", size = 769556, upload-time = "2026-07-19T00:16:26.207Z" },
{ url = "https://files.pythonhosted.org/packages/81/4d/45610c263f8eadb84e4a1fabd904d81d5176226faa9104ef498bf8a8b285/regex-2026.7.19-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6", size = 774130, upload-time = "2026-07-19T00:16:27.786Z" },
{ url = "https://files.pythonhosted.org/packages/40/95/1b40d87c7a9e5480bec7a87bce9fd67fc3f14b5f106c8ee66d660249072f/regex-2026.7.19-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095", size = 848694, upload-time = "2026-07-19T00:16:29.412Z" },
{ url = "https://files.pythonhosted.org/packages/17/8b/bb45968addd5b394ef9cd9184bd9c65ade1a819dbb2b92b71ad52a0c7907/regex-2026.7.19-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0", size = 758505, upload-time = "2026-07-19T00:16:31.006Z" },
{ url = "https://files.pythonhosted.org/packages/bf/6f/33386c672fbf43e21602135a0f29a97ee251a483f007fe51d10e9b2dbc93/regex-2026.7.19-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a", size = 836985, upload-time = "2026-07-19T00:16:32.459Z" },
{ url = "https://files.pythonhosted.org/packages/22/f1/9112b86e9bb075619862e8e42b604794389f1958faa68fb69bde505dd90e/regex-2026.7.19-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902", size = 782610, upload-time = "2026-07-19T00:16:33.857Z" },
{ url = "https://files.pythonhosted.org/packages/3a/f9/13d460d8a385ca0b0be9e6be80a90968b9293b3e30895543ad2d1d1653e4/regex-2026.7.19-cp310-cp310-win32.whl", hash = "sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e", size = 266772, upload-time = "2026-07-19T00:16:35.194Z" },
{ url = "https://files.pythonhosted.org/packages/9f/90/29addd7a03e1aea402c1f31467e25c80caabc8c3735b88a23cf73b0aa9c2/regex-2026.7.19-cp310-cp310-win_amd64.whl", hash = "sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db", size = 277967, upload-time = "2026-07-19T00:16:36.847Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ba/ecfce06fe66c122bc6f77ae284887a9282e4411fe1e6268c5266611ca054/regex-2026.7.19-cp310-cp310-win_arm64.whl", hash = "sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6", size = 276963, upload-time = "2026-07-19T00:16:38.315Z" },
{ url = "https://files.pythonhosted.org/packages/05/e5/cef4de2bac939280b68d32adc659478845238a8274f2f79c465063f590ad/regex-2026.7.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c", size = 494012, upload-time = "2026-07-19T00:16:39.927Z" },
{ url = "https://files.pythonhosted.org/packages/ff/87/e86f51eb117457bb7803132ffe5cb6e2841e2b5bea4cc85d397f3c6e257d/regex-2026.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae", size = 295281, upload-time = "2026-07-19T00:16:41.433Z" },
{ url = "https://files.pythonhosted.org/packages/41/2e/2360c41d8080a3d9ec7e5c90fad6eab3b50192869d10e9a5609e48c8177b/regex-2026.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5", size = 290615, upload-time = "2026-07-19T00:16:43.058Z" },
{ url = "https://files.pythonhosted.org/packages/cf/69/b65ba4344efbc771b28fe5dde84cbbb6c8f9551165952fe78def5b9dde6a/regex-2026.7.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20", size = 791804, upload-time = "2026-07-19T00:16:44.662Z" },
{ url = "https://files.pythonhosted.org/packages/81/b6/a40dfa0dc6224b36f620c00296eacc830489cbf8c2837b6750dfe6170375/regex-2026.7.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a", size = 861723, upload-time = "2026-07-19T00:16:46.412Z" },
{ url = "https://files.pythonhosted.org/packages/e3/02/735991dee71abd83196a7962f7ed8bf5aa05720ff06e2d3ff896a85e2bbb/regex-2026.7.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7", size = 905932, upload-time = "2026-07-19T00:16:47.956Z" },
{ url = "https://files.pythonhosted.org/packages/45/6c/e7098d8b846ccdbf431d8c081b61e496526a27a28094ed09e0dce21b3f54/regex-2026.7.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc", size = 801407, upload-time = "2026-07-19T00:16:49.43Z" },
{ url = "https://files.pythonhosted.org/packages/8a/18/34b69274e2649bcc7d9b089c2b2983fb2632d8ecf667e359593be9072e79/regex-2026.7.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864", size = 774448, upload-time = "2026-07-19T00:16:51.352Z" },
{ url = "https://files.pythonhosted.org/packages/bb/e6/0a72247d025585fd3800b98e040b84d562a88af6303347100484849f4f01/regex-2026.7.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af", size = 783297, upload-time = "2026-07-19T00:16:53.071Z" },
{ url = "https://files.pythonhosted.org/packages/b1/aa/c4f65ae7dd02a36b323a70c4cff326e1f3442361aaebc9311100a130d54f/regex-2026.7.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5", size = 854736, upload-time = "2026-07-19T00:16:54.607Z" },
{ url = "https://files.pythonhosted.org/packages/62/c3/668082bcc817b9e694189b84997aeba7385b7779faa6711788679c482e35/regex-2026.7.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82", size = 763298, upload-time = "2026-07-19T00:16:56.289Z" },
{ url = "https://files.pythonhosted.org/packages/4b/fb/2d07ad555e7af88aa5f867fdafa47a8d945ee237c20af3ebceb46a820835/regex-2026.7.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78", size = 844430, upload-time = "2026-07-19T00:16:57.933Z" },
{ url = "https://files.pythonhosted.org/packages/51/15/c82a471fe3dce56f03745635b43aa456c40dc0db089e07ef148b331507d1/regex-2026.7.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327", size = 789683, upload-time = "2026-07-19T00:16:59.583Z" },
{ url = "https://files.pythonhosted.org/packages/b5/f4/7532a2c59d56f5398902c20de60f0c9a5d1cd364e42a051b48e1b210be7b/regex-2026.7.19-cp311-cp311-win32.whl", hash = "sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d", size = 266778, upload-time = "2026-07-19T00:17:01.032Z" },
{ url = "https://files.pythonhosted.org/packages/83/2b/cf1bc631db154eb95520d9d5dbc2371ff77a0f014bbf7d748fed8496aa63/regex-2026.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965", size = 277983, upload-time = "2026-07-19T00:17:02.571Z" },
{ url = "https://files.pythonhosted.org/packages/8d/bd/56ceaf170e875d5a6761bf2bfd0d040f1cacc896850d5e40cb29b11bbd06/regex-2026.7.19-cp311-cp311-win_arm64.whl", hash = "sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e", size = 276961, upload-time = "2026-07-19T00:17:04.135Z" },
{ url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" },
{ url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" },
{ url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" },
{ url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" },
{ url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" },
{ url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" },
{ url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" },
{ url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" },
{ url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" },
{ url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" },
{ url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" },
{ url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" },
{ url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" },
{ url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" },
{ url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" },
{ url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" },
{ url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" },
{ url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" },
{ url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" },
{ url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" },
{ url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" },
{ url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" },
{ url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" },
{ url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" },
{ url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" },
{ url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" },
{ url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" },
{ url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" },
{ url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" },
{ url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" },
{ url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" },
{ url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" },
{ url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" },
{ url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" },
{ url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" },
{ url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" },
{ url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" },
{ url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" },
{ url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" },
{ url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" },
{ url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" },
{ url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" },
{ url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" },
{ url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" },
{ url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" },
{ url = "https://files.pythonhosted.org/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd", size = 496750, upload-time = "2026-07-19T00:18:39.633Z" },
{ url = "https://files.pythonhosted.org/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac", size = 297093, upload-time = "2026-07-19T00:18:41.583Z" },
{ url = "https://files.pythonhosted.org/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5", size = 292043, upload-time = "2026-07-19T00:18:43.347Z" },
{ url = "https://files.pythonhosted.org/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3", size = 797214, upload-time = "2026-07-19T00:18:45.362Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053", size = 866433, upload-time = "2026-07-19T00:18:47.315Z" },
{ url = "https://files.pythonhosted.org/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b", size = 911360, upload-time = "2026-07-19T00:18:49.588Z" },
{ url = "https://files.pythonhosted.org/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a", size = 801275, upload-time = "2026-07-19T00:18:51.767Z" },
{ url = "https://files.pythonhosted.org/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1", size = 777131, upload-time = "2026-07-19T00:18:54.053Z" },
{ url = "https://files.pythonhosted.org/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e", size = 785020, upload-time = "2026-07-19T00:18:56.579Z" },
{ url = "https://files.pythonhosted.org/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12", size = 861263, upload-time = "2026-07-19T00:18:58.64Z" },
{ url = "https://files.pythonhosted.org/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2", size = 766199, upload-time = "2026-07-19T00:19:00.705Z" },
{ url = "https://files.pythonhosted.org/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97", size = 851317, upload-time = "2026-07-19T00:19:03.015Z" },
{ url = "https://files.pythonhosted.org/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4", size = 789557, upload-time = "2026-07-19T00:19:05.338Z" },
{ url = "https://files.pythonhosted.org/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa", size = 272531, upload-time = "2026-07-19T00:19:07.487Z" },
{ url = "https://files.pythonhosted.org/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac", size = 280831, upload-time = "2026-07-19T00:19:09.46Z" },
{ url = "https://files.pythonhosted.org/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459", size = 281099, upload-time = "2026-07-19T00:19:11.398Z" },
{ url = "https://files.pythonhosted.org/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3", size = 501121, upload-time = "2026-07-19T00:19:13.425Z" },
{ url = "https://files.pythonhosted.org/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518", size = 299415, upload-time = "2026-07-19T00:19:15.43Z" },
{ url = "https://files.pythonhosted.org/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9", size = 294483, upload-time = "2026-07-19T00:19:17.879Z" },
{ url = "https://files.pythonhosted.org/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435", size = 811833, upload-time = "2026-07-19T00:19:20.102Z" },
{ url = "https://files.pythonhosted.org/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0", size = 871270, upload-time = "2026-07-19T00:19:22.365Z" },
{ url = "https://files.pythonhosted.org/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a", size = 917534, upload-time = "2026-07-19T00:19:24.529Z" },
{ url = "https://files.pythonhosted.org/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276", size = 816135, upload-time = "2026-07-19T00:19:26.919Z" },
{ url = "https://files.pythonhosted.org/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c", size = 785492, upload-time = "2026-07-19T00:19:29.192Z" },
{ url = "https://files.pythonhosted.org/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a", size = 800658, upload-time = "2026-07-19T00:19:31.392Z" },
{ url = "https://files.pythonhosted.org/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009", size = 865073, upload-time = "2026-07-19T00:19:33.485Z" },
{ url = "https://files.pythonhosted.org/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218", size = 773684, upload-time = "2026-07-19T00:19:35.599Z" },
{ url = "https://files.pythonhosted.org/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966", size = 857769, upload-time = "2026-07-19T00:19:37.738Z" },
{ url = "https://files.pythonhosted.org/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44", size = 804546, upload-time = "2026-07-19T00:19:40.229Z" },
{ url = "https://files.pythonhosted.org/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78", size = 274526, upload-time = "2026-07-19T00:19:42.398Z" },
{ url = "https://files.pythonhosted.org/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2", size = 283763, upload-time = "2026-07-19T00:19:44.644Z" },
{ url = "https://files.pythonhosted.org/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547", size = 283451, upload-time = "2026-07-19T00:19:46.639Z" },
]
[[package]]
name = "repath"
version = "0.9.0"
@@ -5018,29 +5073,32 @@ wheels = [
[[package]]
name = "tokenizers"
version = "0.23.1"
version = "0.22.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" }
sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" },
{ url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" },
{ url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" },
{ url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" },
{ url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" },
{ url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" },
{ url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" },
{ url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" },
{ url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" },
{ url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" },
{ url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" },
{ url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" },
{ url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" },
{ url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" },
{ url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
{ url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
{ url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
{ url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
{ url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
{ url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
{ url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
{ url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
{ url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
{ url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
{ url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
{ url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
{ url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
{ url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
{ url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" },
{ url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" },
{ url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" },
{ url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" },
]
[[package]]
@@ -5099,50 +5157,54 @@ wheels = [
[[package]]
name = "torch"
version = "2.13.0"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" },
{ name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" },
{ name = "filelock" },
{ name = "fsspec" },
{ name = "jinja2" },
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
{ name = "setuptools" },
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "setuptools", marker = "python_full_version >= '3.12'" },
{ name = "sympy" },
{ name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" },
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "typing-extensions" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/e7/19894fdb51c7dbaf94f5a79bb0871da0992e8e4241e579cb006da46d2e58/torch-2.13.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:94f0de129916f77b8dc2c7a8eff644cfeddfe59e39c9f55e9f6e17543410281d", size = 111178962, upload-time = "2026-07-08T16:05:49.855Z" },
{ url = "https://files.pythonhosted.org/packages/d1/5c/b1d5de470c54e339b30a92d96683a71bcebd78f5f2a7fc714cd6dc6bbd68/torch-2.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0ab4b69f3ee03a62a002cfbf77b1ca5e88aceb4ea64cb4388bb28f638ddbb045", size = 427198333, upload-time = "2026-07-08T16:05:36.847Z" },
{ url = "https://files.pythonhosted.org/packages/50/c0/68a84105e1fcb8970144b388ff3d3e5dc15a3be28c1e247841f7d7247e41/torch-2.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c78b7b4d04461855a764cf01bae9a462bb88bc93defcfa11235cbc8fdf3e12c4", size = 526555154, upload-time = "2026-07-08T16:05:06.507Z" },
{ url = "https://files.pythonhosted.org/packages/2e/c9/0bb9d097b03cbaf96bb75b15e867347b8e41bfcdfe0539452d17d9e63993/torch-2.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:2bd30b6b730d987fa386ce3898933762c5cb8cc82eb0535211d787cc3ce2dfeb", size = 122015602, upload-time = "2026-07-08T16:05:45.25Z" },
{ url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" },
{ url = "https://files.pythonhosted.org/packages/c2/59/1e3160e18e12aa3038390efab3ce02b36a9d4d6a527ecdd8520dca2e68d8/torch-2.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c", size = 427199369, upload-time = "2026-07-08T16:04:51.054Z" },
{ url = "https://files.pythonhosted.org/packages/01/79/1f2d34ad7034ee1c7ffc1cf8bf0f8213af2a81df6ecdb3997ecec107c09d/torch-2.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7", size = 526574961, upload-time = "2026-07-08T16:04:07.075Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fd/0f2ce40f58aefbdb3392f9acce3c8171940943ae2d661f70558bfa73befb/torch-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330", size = 122015870, upload-time = "2026-07-08T16:05:27.59Z" },
{ url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" },
{ url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" },
{ url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" },
{ url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" },
{ url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" },
{ url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" },
{ url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" },
{ url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" },
{ url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" },
{ url = "https://files.pythonhosted.org/packages/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" },
{ url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" },
{ url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" },
{ url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" },
{ url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" },
{ url = "https://files.pythonhosted.org/packages/63/28/110f7274254f1b8476c561dada127173f994afa2b1ffc044efb773c15650/torch-2.8.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0be92c08b44009d4131d1ff7a8060d10bafdb7ddcb7359ef8d8c5169007ea905", size = 102052793, upload-time = "2025-08-06T14:53:15.852Z" },
{ url = "https://files.pythonhosted.org/packages/70/1c/58da560016f81c339ae14ab16c98153d51c941544ae568da3cb5b1ceb572/torch-2.8.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:89aa9ee820bb39d4d72b794345cccef106b574508dd17dbec457949678c76011", size = 888025420, upload-time = "2025-08-06T14:54:18.014Z" },
{ url = "https://files.pythonhosted.org/packages/70/87/f69752d0dd4ba8218c390f0438130c166fa264a33b7025adb5014b92192c/torch-2.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8e5bf982e87e2b59d932769938b698858c64cc53753894be25629bdf5cf2f46", size = 241363614, upload-time = "2025-08-06T14:53:31.496Z" },
{ url = "https://files.pythonhosted.org/packages/ef/d6/e6d4c57e61c2b2175d3aafbfb779926a2cfd7c32eeda7c543925dceec923/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a3f16a58a9a800f589b26d47ee15aca3acf065546137fc2af039876135f4c760", size = 73611154, upload-time = "2025-08-06T14:53:10.919Z" },
{ url = "https://files.pythonhosted.org/packages/8f/c4/3e7a3887eba14e815e614db70b3b529112d1513d9dae6f4d43e373360b7f/torch-2.8.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:220a06fd7af8b653c35d359dfe1aaf32f65aa85befa342629f716acb134b9710", size = 102073391, upload-time = "2025-08-06T14:53:20.937Z" },
{ url = "https://files.pythonhosted.org/packages/5a/63/4fdc45a0304536e75a5e1b1bbfb1b56dd0e2743c48ee83ca729f7ce44162/torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c12fa219f51a933d5f80eeb3a7a5d0cbe9168c0a14bbb4055f1979431660879b", size = 888063640, upload-time = "2025-08-06T14:55:05.325Z" },
{ url = "https://files.pythonhosted.org/packages/84/57/2f64161769610cf6b1c5ed782bd8a780e18a3c9d48931319f2887fa9d0b1/torch-2.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c7ef765e27551b2fbfc0f41bcf270e1292d9bf79f8e0724848b1682be6e80aa", size = 241366752, upload-time = "2025-08-06T14:53:38.692Z" },
{ url = "https://files.pythonhosted.org/packages/a4/5e/05a5c46085d9b97e928f3f037081d3d2b87fb4b4195030fc099aaec5effc/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:5ae0524688fb6707c57a530c2325e13bb0090b745ba7b4a2cd6a3ce262572916", size = 73621174, upload-time = "2025-08-06T14:53:25.44Z" },
{ url = "https://files.pythonhosted.org/packages/49/0c/2fd4df0d83a495bb5e54dca4474c4ec5f9c62db185421563deeb5dabf609/torch-2.8.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e2fab4153768d433f8ed9279c8133a114a034a61e77a3a104dcdf54388838705", size = 101906089, upload-time = "2025-08-06T14:53:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/99/a8/6acf48d48838fb8fe480597d98a0668c2beb02ee4755cc136de92a0a956f/torch-2.8.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2aca0939fb7e4d842561febbd4ffda67a8e958ff725c1c27e244e85e982173c", size = 887913624, upload-time = "2025-08-06T14:56:44.33Z" },
{ url = "https://files.pythonhosted.org/packages/af/8a/5c87f08e3abd825c7dfecef5a0f1d9aa5df5dd0e3fd1fa2f490a8e512402/torch-2.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:2f4ac52f0130275d7517b03a33d2493bab3693c83dcfadf4f81688ea82147d2e", size = 241326087, upload-time = "2025-08-06T14:53:46.503Z" },
{ url = "https://files.pythonhosted.org/packages/be/66/5c9a321b325aaecb92d4d1855421e3a055abd77903b7dab6575ca07796db/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0", size = 73630478, upload-time = "2025-08-06T14:53:57.144Z" },
{ url = "https://files.pythonhosted.org/packages/10/4e/469ced5a0603245d6a19a556e9053300033f9c5baccf43a3d25ba73e189e/torch-2.8.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2b2f96814e0345f5a5aed9bf9734efa913678ed19caf6dc2cddb7930672d6128", size = 101936856, upload-time = "2025-08-06T14:54:01.526Z" },
{ url = "https://files.pythonhosted.org/packages/16/82/3948e54c01b2109238357c6f86242e6ecbf0c63a1af46906772902f82057/torch-2.8.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:65616ca8ec6f43245e1f5f296603e33923f4c30f93d65e103d9e50c25b35150b", size = 887922844, upload-time = "2025-08-06T14:55:50.78Z" },
{ url = "https://files.pythonhosted.org/packages/e3/54/941ea0a860f2717d86a811adf0c2cd01b3983bdd460d0803053c4e0b8649/torch-2.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:659df54119ae03e83a800addc125856effda88b016dfc54d9f65215c3975be16", size = 241330968, upload-time = "2025-08-06T14:54:45.293Z" },
{ url = "https://files.pythonhosted.org/packages/de/69/8b7b13bba430f5e21d77708b616f767683629fc4f8037564a177d20f90ed/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:1a62a1ec4b0498930e2543535cf70b1bef8c777713de7ceb84cd79115f553767", size = 73915128, upload-time = "2025-08-06T14:54:34.769Z" },
{ url = "https://files.pythonhosted.org/packages/15/0e/8a800e093b7f7430dbaefa80075aee9158ec22e4c4fc3c1a66e4fb96cb4f/torch-2.8.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:83c13411a26fac3d101fe8035a6b0476ae606deb8688e904e796a3534c197def", size = 102020139, upload-time = "2025-08-06T14:54:39.047Z" },
{ url = "https://files.pythonhosted.org/packages/4a/15/5e488ca0bc6162c86a33b58642bc577c84ded17c7b72d97e49b5833e2d73/torch-2.8.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8f0a9d617a66509ded240add3754e462430a6c1fc5589f86c17b433dd808f97a", size = 887990692, upload-time = "2025-08-06T14:56:18.286Z" },
{ url = "https://files.pythonhosted.org/packages/b4/a8/6a04e4b54472fc5dba7ca2341ab219e529f3c07b6941059fbf18dccac31f/torch-2.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a7242b86f42be98ac674b88a4988643b9bc6145437ec8f048fea23f72feb5eca", size = 241603453, upload-time = "2025-08-06T14:55:22.945Z" },
{ url = "https://files.pythonhosted.org/packages/04/6e/650bb7f28f771af0cb791b02348db8b7f5f64f40f6829ee82aa6ce99aabe/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:7b677e17f5a3e69fdef7eb3b9da72622f8d322692930297e4ccb52fefc6c8211", size = 73632395, upload-time = "2025-08-06T14:55:28.645Z" },
]
[[package]]
@@ -5177,67 +5239,73 @@ wheels = [
[[package]]
name = "torchaudio"
version = "2.11.0"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "torch" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/d9/357eb5fe4e19a861e6fa1af4d9f535e8fa8692336e6cf436e8a21262e054/torchaudio-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ebb59c694909eccb5d61b7cc199d297692012c43286e36d92983aa7bad7586d", size = 684145, upload-time = "2026-03-23T18:13:46.671Z" },
{ url = "https://files.pythonhosted.org/packages/2a/79/90de77e73f395bba2fe477f8e82e4ae1d14d6452a706838765e850a5e80c/torchaudio-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:be7ad472acb16d16e98c005f0219b0db06a47dfe8f7b4d177062e1638f871e3b", size = 1626521, upload-time = "2026-03-23T18:13:40.98Z" },
{ url = "https://files.pythonhosted.org/packages/66/dc/5757ed7d8d11a6c14336bcb54e63980979f00005555fec80fb4aa4de5eff/torchaudio-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:5847fe2022b17c6580aeb39c8797a443411cc09edfd9183cd50ac1a3b8ccf97c", size = 1771929, upload-time = "2026-03-23T18:13:43.432Z" },
{ url = "https://files.pythonhosted.org/packages/cf/f4/8ce2417eac66296e45b7aaa69858403fb6a52b1323f8635ec37b4b0f1fa3/torchaudio-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e2da1df4f6fe885c46db350a0dc90a0dff4b54541dff8846faa904d255e2bfe", size = 328661, upload-time = "2026-03-23T18:13:45.77Z" },
{ url = "https://files.pythonhosted.org/packages/94/77/0eec7f175d88f312296bd5b11c23bd58da37c1021f53da3db4df449ce3ee/torchaudio-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:492dd64645e9d0bb843e94f1d9a4d1e31426262ffc594fafecc1697df9df5eb9", size = 684142, upload-time = "2026-03-23T18:13:36.805Z" },
{ url = "https://files.pythonhosted.org/packages/b3/f9/6f7ebe071b44592c85269762b55b63ab0a091b5f479f73544738f7564a1e/torchaudio-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:73dab4841f94d888bc7c2aed7b5547c643edc974306919fe1adfb65d57cccf4b", size = 1626527, upload-time = "2026-03-23T18:13:39.011Z" },
{ url = "https://files.pythonhosted.org/packages/ac/70/17408e0d154d0c894537a88dcbadc48e8ad3b6e1ef4a1dabda5d40245ee0/torchaudio-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1a07ec72fd6f26a588c39b5f029e0130d16bb40bc4221635580bf8fb18fcbc80", size = 1771930, upload-time = "2026-03-23T18:13:37.963Z" },
{ url = "https://files.pythonhosted.org/packages/c9/75/b6d03fc75b409bdaec597274d1bdd4213db716ed16f6801386b31d59c551/torchaudio-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb59ba4452bbbe95d75ad3ef18df9824955625f36698ce9a5998a4a9f3c1ba1d", size = 328658, upload-time = "2026-03-23T18:13:44.545Z" },
{ url = "https://files.pythonhosted.org/packages/f1/b1/77658817acacd01a72b714440c62f419efc4d90170e704e8e7a2c0918988/torchaudio-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1cf1acc883bee9cb906a933572fed6a8a933f86ef34e9ea7d803f72317e8c1b", size = 684226, upload-time = "2026-03-23T18:13:40.023Z" },
{ url = "https://files.pythonhosted.org/packages/78/28/c7adc053039f286c2aca0038b766cbe3294e66fec6b29a820e95128f9ede/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:bc653defca1c16154398517a1adc98d0fb7f1dd08e58ced217558d213c2c6e29", size = 1626670, upload-time = "2026-03-23T18:13:42.162Z" },
{ url = "https://files.pythonhosted.org/packages/88/d8/d6d0f896e064aa67377484efef4911cdcc07bce2929474e1417cc0af18c2/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6503c0bdb29daf2e6281bb70ea2dfe2c3553b782b619eb5d73bdadd8a3f7cecf", size = 1771992, upload-time = "2026-03-23T18:13:33.188Z" },
{ url = "https://files.pythonhosted.org/packages/23/a8/941277ecc39f7a0a169d554302a1f1afd87c1d94a8aec828891916cea59a/torchaudio-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:478110f981e5d40a8d82221732c57a56c85a1d5895fb8fe646e86ee15eded3bd", size = 328663, upload-time = "2026-03-23T18:13:19.218Z" },
{ url = "https://files.pythonhosted.org/packages/fb/9e/f76fcd9877c8c78f258ee34e0fb8291fdb91e6218d582d9ca66b1e4bd4ae/torchaudio-2.11.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e3f9696a9ef1d49acc452159b052370c636406d072e9d8f10895fda87b591ea9", size = 679904, upload-time = "2026-03-23T18:13:28.329Z" },
{ url = "https://files.pythonhosted.org/packages/85/70/249c1498ebdad3e7752866635ec0855fc0dcf898beccda5a9d2b9df8e4d0/torchaudio-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b034d7672f1c415434f48ef17807f2cce47f29e8795338c751d4e596c9fbe8b5", size = 1618523, upload-time = "2026-03-23T18:13:15.703Z" },
{ url = "https://files.pythonhosted.org/packages/4f/98/be13fe35d9aa5c26381c0e453c828a789d15c007f8f7d08c95341d19974d/torchaudio-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1c1101c1243ef0e4063ec63298977e2d3655c15cf88d9eb0a1bd4fe2db9f47ea", size = 1771992, upload-time = "2026-03-23T18:13:35.343Z" },
{ url = "https://files.pythonhosted.org/packages/e2/8b/2bbb3dca6ff28cba0de250874d5ef4fc2822c47a934b59b3974cff3219ef/torchaudio-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:986f4df5ed17b003dc52489468601720090e65f964f8bebccf90eb45bba75744", size = 328662, upload-time = "2026-03-23T18:13:18.308Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ce/52c652d30af7d6e96c8f1735d26131e94708e3f38d852b8fa97958804dd8/torchaudio-2.11.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:bda09ea630ae7207384fb0f28c35e4f8c0d82dd6eba020b6b335ad0caa9fed49", size = 680814, upload-time = "2026-03-23T18:13:17.08Z" },
{ url = "https://files.pythonhosted.org/packages/06/95/1ad1507482e7263e556709a3f5f87fecd375a0742cdaf238806c8e72eaad/torchaudio-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:9fe3083c62e035646483a14e180d33561bdc2eed436c9ab1259c137fb7120b4a", size = 1618546, upload-time = "2026-03-23T18:13:29.686Z" },
{ url = "https://files.pythonhosted.org/packages/98/4c/480328ba07487eb9890406720304d0d460dd7a6a64098614f5aa53b662ca/torchaudio-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:13cff988697ccbad539987599f9dc672f40c417bed67570b365e4e5002bbd096", size = 1771991, upload-time = "2026-03-23T18:13:30.843Z" },
{ url = "https://files.pythonhosted.org/packages/3e/98/5d4790e2d6548768999acd34999d5aeefce8bcc23a07afaa5f03e723f557/torchaudio-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ed404c4399ad7f172c86a47c1b25293d322d1d58e26b10b0456a86cf67d37d84", size = 328661, upload-time = "2026-03-23T18:13:34.359Z" },
{ url = "https://files.pythonhosted.org/packages/39/fe/ffa618b4f0d9732d7df7a2fa2bd48657d896599bc224e5af3c70d46c546b/torchaudio-2.11.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:cc09cd1f6015b8549e7fe255fb1be5346b57e7fee06541d3f3dbb012d8c4715f", size = 679901, upload-time = "2026-03-23T18:13:25.472Z" },
{ url = "https://files.pythonhosted.org/packages/5c/54/f414d7b92dd0b3094a2409c95a97bd6c49aa0620da722a0e55462f9bd9cb/torchaudio-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:79fb3cb99169fd41bd9719647261402a164da0d105a4d81f42a3260844ec5e79", size = 1618527, upload-time = "2026-03-23T18:13:26.68Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a8/bf2e1f6ce24c990192400ae49b4acc1a0d0295b6c6a06bceecdc46ce08de/torchaudio-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:00e9f71ab9c656f0abdb40c515bd65d4658ab0ad380dee27a2efd7d51dabd3d6", size = 1771995, upload-time = "2026-03-23T18:13:23.373Z" },
{ url = "https://files.pythonhosted.org/packages/83/6f/b0efb44e0bfe8dd4d78d76ae3be280354e1fb5c8631c782785d74cd8a7b1/torchaudio-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:1424638adb8bb40087bc7b6eb103e8e4fe398210f09076f33b7b5e61501b5d66", size = 328662, upload-time = "2026-03-23T18:13:32.243Z" },
{ url = "https://files.pythonhosted.org/packages/60/84/1c792b0b700eac9a96772cfd9f96c097b17bca3234a2fde3c64b8063660d/torchaudio-2.11.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:da2725e250866da42a12934c9a6552f65a18b7187fd7a6221387f0e605fb3b96", size = 679926, upload-time = "2026-03-23T18:13:24.452Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a0/62a5842062f739239691f2e57523e0570dd06704ad987755f7644a3afa23/torchaudio-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:1be3767064364ae82705bdf2b15c1e8b41fea82c4cd04d47428a8684b634b6ed", size = 1618552, upload-time = "2026-03-23T18:13:21.09Z" },
{ url = "https://files.pythonhosted.org/packages/6d/89/c293d818f9f899db93bf291b42401c05ae29acfb2e53d5341c30ea703e62/torchaudio-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:67f6edac29ed004652c11db5c19d9debb5d835695930574f564efc8bdd061bba", size = 1771986, upload-time = "2026-03-23T18:13:22.153Z" },
{ url = "https://files.pythonhosted.org/packages/93/f7/ee5da8c03f1a3c7662c6c6a119f24a4b3e646da94be56dce3201e3a6ee9b/torchaudio-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:88fb5e29f670a33d9bac6aabb1d2734460cf6e461bde5cdc352826035851b16d", size = 328661, upload-time = "2026-03-23T18:13:20.1Z" },
{ url = "https://files.pythonhosted.org/packages/30/81/92d34ff136b17ddda872f6d8149f2ca927ad53a37ae26d02cb5f66435772/torchaudio-2.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c2f44cf279f673cfcdd8f576c349eee8bedf8caab351a5dd78b32970cc34a212", size = 1852315, upload-time = "2025-08-06T14:58:32.64Z" },
{ url = "https://files.pythonhosted.org/packages/95/c8/e46c22a3c059844bb0f1b670317c9e538b51728558326dcd9e5fffbf2ec2/torchaudio-2.8.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d3c1b85b26a09832d139f6d6da6b66caeb51d2e16e08f8587665c44a9e1aa8f9", size = 1685620, upload-time = "2025-08-06T14:58:34.045Z" },
{ url = "https://files.pythonhosted.org/packages/d8/f5/69db76b564263f22c1788cc298ab1c4e2391a79fa8ba7b4a3e76d945292a/torchaudio-2.8.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:58f912bf2d289c709b42a55475b2b483becec79d9affb7684b606bb1f896b434", size = 4001714, upload-time = "2025-08-06T14:58:38.951Z" },
{ url = "https://files.pythonhosted.org/packages/f1/18/63adf60a9f0592c6dcea2b37735990881edbbe671e3af3ae79f2da816a50/torchaudio-2.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:4e2b4712ad6d7547ce82d84567c8c29d5e2966ff1d31d94e1644024fb4b2649f", size = 2500313, upload-time = "2025-08-06T14:58:42.441Z" },
{ url = "https://files.pythonhosted.org/packages/dd/bf/6b01ef3defb8d0a772c863588711e9b2b011c27d6b37c1b9d15a359c8442/torchaudio-2.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9276857d241c6de257af765c0f51fc011af38cb725401495121b280913007cf", size = 1859094, upload-time = "2025-08-06T14:58:35.078Z" },
{ url = "https://files.pythonhosted.org/packages/75/ca/da5d0a3bb7d114a8b590ecce14859ea0a05102bb4de68cdd1ed7a90634d6/torchaudio-2.8.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4573c6042950c20278e3608a9a38050ba0bc72e0049e1bbfd249caf859a8029b", size = 1692033, upload-time = "2025-08-06T14:58:37.393Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ef/62ac736d8f906cc414181050e08a495a637dab985186c34bd76ea37efbc0/torchaudio-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:776c0b4ba84b9e3ddf6304b9c47cd63549d7896a6f3d5184ece074cc3d76ed6b", size = 4011716, upload-time = "2025-08-06T14:58:40.138Z" },
{ url = "https://files.pythonhosted.org/packages/14/86/015337c8434abc604b8680371df783f66c421a7f211cbe40a374b0540b6d/torchaudio-2.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:078105bf80f725c0215a0bebac8cb2fb1b3993ab32bdc3fcd50145a5b4127001", size = 2505194, upload-time = "2025-08-06T14:58:57.301Z" },
{ url = "https://files.pythonhosted.org/packages/ac/cc/c2e2a3eb6ee956f73c68541e439916f8146170ea9cc61e72adea5c995312/torchaudio-2.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ddef94bf181e6447cbb05f38beaca8f6c5bb8d2b9ddced1aa3452025b9fc70d3", size = 1856736, upload-time = "2025-08-06T14:58:36.3Z" },
{ url = "https://files.pythonhosted.org/packages/c7/0d/24dad878784f1edd62862f27173781669f0c71eb46368636787d1e364188/torchaudio-2.8.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:862e2e40bf09d865e5df080a84c1a39bbcef40e43140f4b1737eb3a389d3b38f", size = 1692930, upload-time = "2025-08-06T14:58:41.312Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a6/84d80f34472503e9eb82245d7df501c59602d75d7360e717fb9b84f91c5e/torchaudio-2.8.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:93a8583f280fe83ba021aa713319381ea71362cc87b67ee38e97a43cb2254aee", size = 4014607, upload-time = "2025-08-06T14:58:47.234Z" },
{ url = "https://files.pythonhosted.org/packages/43/ab/96ad33afa320738a7cfb4b51ba97e2f3cfb1e04ae3115d5057655103ba4f/torchaudio-2.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:4b82cacd1b8ccd543b1149d8cab257a40dfda8119023d2e3a96c66349c84bffb", size = 2499890, upload-time = "2025-08-06T14:58:55.066Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ea/2a68259c4dbb5fe44ebfdcfa40b115010d8c677221a7ef0f5577f3c4f5f1/torchaudio-2.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f851d32e94ca05e470f0c60e25726ec1e0eb71cb2ca5a0206b7fd03272ccc3c8", size = 1857045, upload-time = "2025-08-06T14:58:51.984Z" },
{ url = "https://files.pythonhosted.org/packages/0d/a3/1c79a8ef29fe403b83bdfc033db852bc2a888b80c406325e5c6fb37a7f2d/torchaudio-2.8.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:09535a9b727c0793cd07c1ace99f3f353626281bcc3e30c2f2314e3ebc9d3f96", size = 1692755, upload-time = "2025-08-06T14:58:50.868Z" },
{ url = "https://files.pythonhosted.org/packages/49/df/61941198e9ac6bcebfdd57e1836e4f3c23409308e3d8d7458f0198a6a366/torchaudio-2.8.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d2a85b124494736241884372fe1c6dd8c15e9bc1931bd325838c5c00238c7378", size = 4013897, upload-time = "2025-08-06T14:59:01.66Z" },
{ url = "https://files.pythonhosted.org/packages/c3/ab/7175d35a4bbc4a465a9f1388571842f16eb6dec5069d7ea9c8c2d7b5b401/torchaudio-2.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:c1b5139c840367a7855a062a06688a416619f6fd2ca46d9b9299b49a7d133dfd", size = 2500085, upload-time = "2025-08-06T14:58:44.95Z" },
{ url = "https://files.pythonhosted.org/packages/34/1a/69b9f8349d9d57953d5e7e445075cbf74000173fb5f5d5d9e9d59415fc63/torchaudio-2.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:68df9c9068984edff8065c2b6656725e6114fe89281b0cf122c7505305fc98a4", size = 1935600, upload-time = "2025-08-06T14:58:46.051Z" },
{ url = "https://files.pythonhosted.org/packages/71/76/40fec21b65bccfdc5c8cdb9d511033ab07a7ad4b05f0a5b07f85c68279fc/torchaudio-2.8.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1951f10ed092f2dda57634f6a3950ef21c9d9352551aa84a9fccd51bbda18095", size = 1704199, upload-time = "2025-08-06T14:58:43.594Z" },
{ url = "https://files.pythonhosted.org/packages/8e/53/95c3363413c2f2009f805144160b093a385f641224465fbcd717449c71fb/torchaudio-2.8.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4f7d97494698d98854129349b12061e8c3398d33bd84c929fa9aed5fd1389f73", size = 4020596, upload-time = "2025-08-06T14:59:03.031Z" },
{ url = "https://files.pythonhosted.org/packages/52/27/7fc2d7435af044ffbe0b9b8e98d99eac096d43f128a5cde23c04825d5dcf/torchaudio-2.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d4a715d09ac28c920d031ee1e60ecbc91e8a5079ad8c61c0277e658436c821a6", size = 2549553, upload-time = "2025-08-06T14:59:00.019Z" },
]
[[package]]
name = "torchcodec"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version < '3.11'",
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/23/ca6bd1bc5e22786596e25d1dd62a6a4e733802940b54726a54fcf5a8795b/torchcodec-0.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae0b7acbc0c1a755ae817a8843715131f24be7807ddc46092d8b49a0fc970eb", size = 2891797, upload-time = "2025-09-08T14:17:48.602Z" },
{ url = "https://files.pythonhosted.org/packages/0d/81/cff42793544b7d3e2ff9a4912542c6d1c7a617aabe8404f8fd3d52453f20/torchcodec-0.7.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a0071096724e8ded6a171457ce4680f646499b4a4d285cdb46e130983f965ce4", size = 1411823, upload-time = "2025-09-08T14:17:39.405Z" },
{ url = "https://files.pythonhosted.org/packages/ad/b9/7f03bf7d42e0f7ab5598d400cb1133d3f227b52aad15d88b2ab9c97fe1ff/torchcodec-0.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:737da9212594bf2f205582512a7a4f56d39591b357bf5a30e72e858cfcedc2ac", size = 1553965, upload-time = "2025-09-08T14:17:57.187Z" },
{ url = "https://files.pythonhosted.org/packages/6a/f1/bb2b5ab929ef3f092cb6508673510ffc2aafd8324493c94a2d41f1c8a683/torchcodec-0.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:967a14b31e04721901ddbf965f9e9f733f328c5e98a51e22f414e25ac32e20ba", size = 3388626, upload-time = "2025-09-08T14:17:50.433Z" },
{ url = "https://files.pythonhosted.org/packages/06/14/8ff28247988365fc47e8471e28cdfd8d037232fcf73abb67ee815ac80f1d/torchcodec-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:afb1c48b52bd4ee8f485f5a427bb4e82380590255a26b8e9e3fe099e0779287f", size = 1419444, upload-time = "2025-09-08T14:17:41.479Z" },
{ url = "https://files.pythonhosted.org/packages/1f/80/04f23dff2c7ac406d2d6b24a52be7654a946d2fdfe158b19341a524dae20/torchcodec-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:a68765cd29159da3cf36eb5716481c617ad9d168fe06418bcde2a9360cc7eb5e", size = 1563430, upload-time = "2025-09-08T14:17:58.571Z" },
{ url = "https://files.pythonhosted.org/packages/c7/b2/6d3e190fcd18c65b35f6da734d4415c72b42c8a72ffc2494d998bad8caf3/torchcodec-0.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9d082bbb599f4f7715bfc3b1afa5bc16d8fb9d852e68084c63f1973cc78a1cb", size = 3574506, upload-time = "2025-09-08T14:17:52.071Z" },
{ url = "https://files.pythonhosted.org/packages/41/10/4a1a8407d0fad37cb43d1f749e7b422e5a0f6def17f3b90ab9ab9a105e32/torchcodec-0.7.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3cd23c3296c9b071d56bb2c534a6a98275d65c1a6a7213cdb72a26ec9f9d2fd8", size = 1421871, upload-time = "2025-09-08T14:17:43.419Z" },
{ url = "https://files.pythonhosted.org/packages/9b/e7/a2fa7ed9c81d7d683d37ca7204007b421c0537132364a9cfd8d577f19a96/torchcodec-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac942831bff02e6041d8718b71c6f63e4e37c05dd95e72863725c9dbef0d4a7b", size = 1565029, upload-time = "2025-09-08T14:18:02.83Z" },
{ url = "https://files.pythonhosted.org/packages/3c/82/7c7691d538f67704b2b2444deb0e234ae564f9329bc9becf66d69998bc9b/torchcodec-0.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:32a0115035a7f0a77fa451f67c101e0273a3a37d33b69e1bcd777f00aceb7340", size = 3537881, upload-time = "2025-09-08T14:17:54.254Z" },
{ url = "https://files.pythonhosted.org/packages/01/25/177ea01d138598ab68d5e3b000789e8617bf97874bd8f761d89093f419ba/torchcodec-0.7.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9c70f910f9f48e6625aacaed534f766e13d447b895dc7299e96d4db9a93f1514", size = 1422493, upload-time = "2025-09-08T14:17:45.41Z" },
{ url = "https://files.pythonhosted.org/packages/5c/a9/e2b6301fbf4590d352e183bef64927f74ef4d4f660cca3ed7a32dda60484/torchcodec-0.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:31b402c9ae3c6e9f33c41fddf7058f9492c443ad55d02f022395f8fa196b58f6", size = 1565405, upload-time = "2025-09-08T14:18:04.217Z" },
]
[[package]]
name = "torchcodec"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/e8/ee512c4b803d22e205d425b601e23451882fa6db102a4514e6805c2d5b10/torchcodec-0.16.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:b37783d709cb60694fe10ac03a7c2e78afdbe4cfcde5ea3f7d55a17ca23dafcd", size = 3895242, upload-time = "2026-08-13T11:55:52.566Z" },
{ url = "https://files.pythonhosted.org/packages/2e/e3/177cc1dee2058ac7ae7ab218e8a7b088153ef0431e6293f1e416d3dd13c5/torchcodec-0.16.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d2eddb85554cf1113935f706cb4f990d2cafc084aca6fbdea894a29d34d1c32", size = 8357251, upload-time = "2026-08-13T11:55:54.374Z" },
{ url = "https://files.pythonhosted.org/packages/b4/97/fde2b19565451a26f42ce02f386b950e39e007fbf3df49c60cc0f01b28b4/torchcodec-0.16.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74862359c18950be2ece0368d2cd2b619901b3bc9b72e17272d116366d6ef92c", size = 9457594, upload-time = "2026-08-13T11:55:56.538Z" },
{ url = "https://files.pythonhosted.org/packages/00/38/80819d6d29594e0ea1ade0aa7245dc7f690310b5d433a0a4c3d9256dad9d/torchcodec-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0db3218a1bbab542aa3d820899895eb3df17346846a25dc2b9f28c92c47e5998", size = 6414896, upload-time = "2026-08-13T11:55:58.709Z" },
{ url = "https://files.pythonhosted.org/packages/1c/93/37ea920f9c7c959ce87bf70ae65915a99ea7b8203cf0f996df9fe33bf2c0/torchcodec-0.16.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:fd732bf687e74ba1b1b273bb272038664bb9c13a0b1747a25b604a7876613a5d", size = 3896719, upload-time = "2026-08-13T11:56:00.532Z" },
{ url = "https://files.pythonhosted.org/packages/ca/5d/d16eeeb67f6407ad8b4b4254a7fd887f20c6080e462a2d5ad7f29730fc5e/torchcodec-0.16.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f2111c9c2b4a76676ea265ffed3eac6031db386ced3b71ff6910cd31b1f4f1a", size = 8360577, upload-time = "2026-08-13T11:56:02.488Z" },
{ url = "https://files.pythonhosted.org/packages/95/8f/f9b71bac4d94686d5dd9c55530dc0ffb16a73b2f6a58ed03be52a1063952/torchcodec-0.16.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1021a918c24184a1ce0b1dd8ca06408fe09ba988ecdaffc53b9dd51a42138ff0", size = 9468280, upload-time = "2026-08-13T11:56:04.142Z" },
{ url = "https://files.pythonhosted.org/packages/8d/ad/bec3e203b8e34ee542fd84c07e252a38cae70f7e2dccf0fa4f065dd6f34e/torchcodec-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:b44990348cb23d94c5455d3d9b4006bd58ba19a6e40d1e9bc02ff62ae070ae0e", size = 6422414, upload-time = "2026-08-13T11:56:06.303Z" },
{ url = "https://files.pythonhosted.org/packages/d7/b9/d1780a0f5e3b4bcddec36e865ddec73b31047cf4fa8456e96c9f91852b12/torchcodec-0.16.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a899bf100cc40a0401fafc14f47c79979311d1df3fbe418c5d1019cde69d607", size = 3898046, upload-time = "2026-08-13T11:56:07.928Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b7/44004280d1e80c492af54d2d94332834091bc59e211827cfc0232aca03a3/torchcodec-0.16.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d26fe96259211115c17fb2bd071fbdf4da264cf04bb88fba006ffffb9c23c5a1", size = 8368173, upload-time = "2026-08-13T11:56:09.724Z" },
{ url = "https://files.pythonhosted.org/packages/ca/5b/2a15225d77fc2ef857bd290a98f1de99f0db03e104f93ce3af7bea704f4a/torchcodec-0.16.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5309c29e31b719f81dba75fcfa49a9ce6a38d069d7f570767466fe634783e7ba", size = 9471149, upload-time = "2026-08-13T11:56:11.499Z" },
{ url = "https://files.pythonhosted.org/packages/0f/77/00f106e48bc20d9e3e72e85077849eabc3a5ae9839ce0df961d48424747d/torchcodec-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4c83b3c02b206b24a5409746babd74ed00bfa0a6320b23714d73c61124397", size = 6427551, upload-time = "2026-08-13T11:56:13.682Z" },
{ url = "https://files.pythonhosted.org/packages/a0/62/59ef3c1f377a08ca965cf71abd763db5cbf8f6a97901ca09dbe0809c4a29/torchcodec-0.16.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:154c5cf6c2dd2801700dcf0939081f614c50dcbf99f5ecc7f6854e98b7959339", size = 3898103, upload-time = "2026-08-13T11:56:15.303Z" },
{ url = "https://files.pythonhosted.org/packages/b6/83/7844821bd5d0a1d8bee0568d417d2b3f0663f379666edd22348f0f90e427/torchcodec-0.16.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d14e43ace5f9fe095287a5b4a39f1c4353c01493749e40ff4e17ebb791f741e8", size = 8368167, upload-time = "2026-08-13T11:56:17.301Z" },
{ url = "https://files.pythonhosted.org/packages/41/1c/9b12cfd462ff2a7cbad52907de9dd1963bca7aa4bf6a4e2d618195af7275/torchcodec-0.16.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26f4c4ee2896ddee5b38bf31ae96e721b853dba6b0b4104ac2e11391ddd7966d", size = 9471404, upload-time = "2026-08-13T11:56:19.143Z" },
{ url = "https://files.pythonhosted.org/packages/38/76/0551e83bb23731806d98469cc8cdc887d55c3a242d7377a52430aa2cd6bf/torchcodec-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:158e1ed469a5300aeb843102f8d6c4354f97a835ce5dca087aa827c135bd4869", size = 6427477, upload-time = "2026-08-13T11:56:21.206Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ae/ba8a4fcc5af759006c156bae38d12aa6bf9635742a8f8175f33ebe37b50e/torchcodec-0.16.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9fcda10720fcbccae8744b8581080e985f7743cb8b365f7ce2bb6f045e7b84fd", size = 3898098, upload-time = "2026-08-13T11:56:22.94Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d2/8769570eeadc4fd66f2638f16d8a48b5266f3458d0d5fb11bb29cc38d1c5/torchcodec-0.16.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2e2742562569eb88230c27570de1116ac361026202447371d9c52dcac324ad4", size = 8369643, upload-time = "2026-08-13T11:56:24.806Z" },
{ url = "https://files.pythonhosted.org/packages/1c/58/07df4dc91580bc878eaf6f0b9b43c0823f67fb3a926e816c6869494b0710/torchcodec-0.16.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12bd2c7eff4056e74c1f57c115d455a5efc2d0f577517242ec66bf30712f5a9f", size = 9472307, upload-time = "2026-08-13T11:56:26.919Z" },
{ url = "https://files.pythonhosted.org/packages/18/74/55fd918f3a1a3b1bcb77d7239ae44cd302e2140c94378fa436691e08e548/torchcodec-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:602a5d8fa1c26d52d3c814e8912ab3a5538889f1384665260061addef2b5d83c", size = 6428265, upload-time = "2026-08-13T11:56:29.003Z" },
{ url = "https://files.pythonhosted.org/packages/45/20/c713f33e492e819979322dab56f2807208f0554f92d3f96caa9dd7cf6bef/torchcodec-0.16.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:6b28fd96c904f157ee43c417c5d9fdc1a6f5709b3039fa85e0fa943004c9439c", size = 3901916, upload-time = "2026-08-13T11:56:30.498Z" },
{ url = "https://files.pythonhosted.org/packages/98/81/a8830cb62a29fb4d4eb7793919f56b7b420c0ce27b5683e09d9b75b8c28f/torchcodec-0.16.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f867dadd3520d0a098bcdcb1a7f23ee99d3fccc4ba3a82f27d5228330e2f41d9", size = 8379402, upload-time = "2026-08-13T11:56:32.359Z" },
{ url = "https://files.pythonhosted.org/packages/ec/db/3028851cb7e349b2c785b0c6423803031409d1a6fd6e9e029055503ec833/torchcodec-0.16.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a273d93170813d6aabb8e6a5366bd4e337ad2ea2d9b07226cca60f0165895945", size = 9478129, upload-time = "2026-08-13T11:56:34.331Z" },
resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
]
[[package]]
@@ -5257,6 +5325,40 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/a2/c7f6ebf546f8f644edf0f999aa98ece106986a77a7b922316bf6414ff825/torchmetrics-1.9.0-py3-none-any.whl", hash = "sha256:bfdcbff3dd1d96b3374bb2496eb39f23c4b28b8a845b6a18c313688e0d2d9ca1", size = 983384, upload-time = "2026-03-09T17:41:19.756Z" },
]
[[package]]
name = "torchvision"
version = "0.23.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "pillow" },
{ name = "torch" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/49/5ad5c3ff4920be0adee9eb4339b4fb3b023a0fc55b9ed8dbc73df92946b8/torchvision-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7266871daca00ad46d1c073e55d972179d12a58fa5c9adec9a3db9bbed71284a", size = 1856885, upload-time = "2025-08-06T14:57:55.024Z" },
{ url = "https://files.pythonhosted.org/packages/25/44/ddd56d1637bac42a8c5da2c8c440d8a28c431f996dd9790f32dd9a96ca6e/torchvision-0.23.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:31c583ba27426a3a04eca8c05450524105c1564db41be6632f7536ef405a6de2", size = 2394251, upload-time = "2025-08-06T14:58:01.725Z" },
{ url = "https://files.pythonhosted.org/packages/93/f3/3cdf55bbf0f737304d997561c34ab0176222e0496b6743b0feab5995182c/torchvision-0.23.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3932bf67256f2d095ce90a9f826f6033694c818856f4bb26794cf2ce64253e53", size = 8627497, upload-time = "2025-08-06T14:58:09.317Z" },
{ url = "https://files.pythonhosted.org/packages/97/90/02afe57c3ef4284c5cf89d3b7ae203829b3a981f72b93a7dd2a3fd2c83c1/torchvision-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:83ee5bf827d61a8af14620c0a61d8608558638ac9c3bac8adb7b27138e2147d1", size = 1600760, upload-time = "2025-08-06T14:57:56.783Z" },
{ url = "https://files.pythonhosted.org/packages/f0/d7/15d3d7bd8d0239211b21673d1bac7bc345a4ad904a8e25bb3fd8a9cf1fbc/torchvision-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49aa20e21f0c2bd458c71d7b449776cbd5f16693dd5807195a820612b8a229b7", size = 1856884, upload-time = "2025-08-06T14:58:00.237Z" },
{ url = "https://files.pythonhosted.org/packages/dd/14/7b44fe766b7d11e064c539d92a172fa9689a53b69029e24f2f1f51e7dc56/torchvision-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01dc33ee24c79148aee7cdbcf34ae8a3c9da1674a591e781577b716d233b1fa6", size = 2395543, upload-time = "2025-08-06T14:58:04.373Z" },
{ url = "https://files.pythonhosted.org/packages/79/9c/fcb09aff941c8147d9e6aa6c8f67412a05622b0c750bcf796be4c85a58d4/torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35c27941831b653f5101edfe62c03d196c13f32139310519e8228f35eae0e96a", size = 8628388, upload-time = "2025-08-06T14:58:07.802Z" },
{ url = "https://files.pythonhosted.org/packages/93/40/3415d890eb357b25a8e0a215d32365a88ecc75a283f75c4e919024b22d97/torchvision-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:09bfde260e7963a15b80c9e442faa9f021c7e7f877ac0a36ca6561b367185013", size = 1600741, upload-time = "2025-08-06T14:57:59.158Z" },
{ url = "https://files.pythonhosted.org/packages/df/1d/0ea0b34bde92a86d42620f29baa6dcbb5c2fc85990316df5cb8f7abb8ea2/torchvision-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e0e2c04a91403e8dd3af9756c6a024a1d9c0ed9c0d592a8314ded8f4fe30d440", size = 1856885, upload-time = "2025-08-06T14:58:06.503Z" },
{ url = "https://files.pythonhosted.org/packages/e2/00/2f6454decc0cd67158c7890364e446aad4b91797087a57a78e72e1a8f8bc/torchvision-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6dd7c4d329a0e03157803031bc856220c6155ef08c26d4f5bbac938acecf0948", size = 2396614, upload-time = "2025-08-06T14:58:03.116Z" },
{ url = "https://files.pythonhosted.org/packages/e4/b5/3e580dcbc16f39a324f3dd71b90edbf02a42548ad44d2b4893cc92b1194b/torchvision-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4e7d31c43bc7cbecbb1a5652ac0106b436aa66e26437585fc2c4b2cf04d6014c", size = 8627108, upload-time = "2025-08-06T14:58:12.956Z" },
{ url = "https://files.pythonhosted.org/packages/82/c1/c2fe6d61e110a8d0de2f94276899a2324a8f1e6aee559eb6b4629ab27466/torchvision-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2e45272abe7b8bf0d06c405e78521b5757be1bd0ed7e5cd78120f7fdd4cbf35", size = 1600723, upload-time = "2025-08-06T14:57:57.986Z" },
{ url = "https://files.pythonhosted.org/packages/91/37/45a5b9407a7900f71d61b2b2f62db4b7c632debca397f205fdcacb502780/torchvision-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1c37e325e09a184b730c3ef51424f383ec5745378dc0eca244520aca29722600", size = 1856886, upload-time = "2025-08-06T14:58:05.491Z" },
{ url = "https://files.pythonhosted.org/packages/ac/da/a06c60fc84fc849377cf035d3b3e9a1c896d52dbad493b963c0f1cdd74d0/torchvision-0.23.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2f7fd6c15f3697e80627b77934f77705f3bc0e98278b989b2655de01f6903e1d", size = 2353112, upload-time = "2025-08-06T14:58:26.265Z" },
{ url = "https://files.pythonhosted.org/packages/a0/27/5ce65ba5c9d3b7d2ccdd79892ab86a2f87ac2ca6638f04bb0280321f1a9c/torchvision-0.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a76fafe113b2977be3a21bf78f115438c1f88631d7a87203acb3dd6ae55889e6", size = 8627658, upload-time = "2025-08-06T14:58:15.999Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e4/028a27b60aa578a2fa99d9d7334ff1871bb17008693ea055a2fdee96da0d/torchvision-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:07d069cb29691ff566e3b7f11f20d91044f079e1dbdc9d72e0655899a9b06938", size = 1600749, upload-time = "2025-08-06T14:58:10.719Z" },
{ url = "https://files.pythonhosted.org/packages/05/35/72f91ad9ac7c19a849dedf083d347dc1123f0adeb401f53974f84f1d04c8/torchvision-0.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:2df618e1143805a7673aaf82cb5720dd9112d4e771983156aaf2ffff692eebf9", size = 2047192, upload-time = "2025-08-06T14:58:11.813Z" },
{ url = "https://files.pythonhosted.org/packages/1d/9d/406cea60a9eb9882145bcd62a184ee61e823e8e1d550cdc3c3ea866a9445/torchvision-0.23.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2a3299d2b1d5a7aed2d3b6ffb69c672ca8830671967eb1cee1497bacd82fe47b", size = 2359295, upload-time = "2025-08-06T14:58:17.469Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f4/34662f71a70fa1e59de99772142f22257ca750de05ccb400b8d2e3809c1d/torchvision-0.23.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:76bc4c0b63d5114aa81281390f8472a12a6a35ce9906e67ea6044e5af4cab60c", size = 8800474, upload-time = "2025-08-06T14:58:22.53Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f5/b5a2d841a8d228b5dbda6d524704408e19e7ca6b7bb0f24490e081da1fa1/torchvision-0.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b9e2dabf0da9c8aa9ea241afb63a8f3e98489e706b22ac3f30416a1be377153b", size = 1527667, upload-time = "2025-08-06T14:58:14.446Z" },
]
[[package]]
name = "tqdm"
version = "4.70.0"
@@ -5270,22 +5372,41 @@ wheels = [
]
[[package]]
name = "triton"
version = "3.7.1"
name = "transformers"
version = "4.57.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "huggingface-hub" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "packaging" },
{ name = "pyyaml" },
{ name = "regex" },
{ name = "requests" },
{ name = "safetensors" },
{ name = "tokenizers" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/ea/629cc37436ca5df93ce98956d09cd2ca1498bfee8ef4972d2fe48b9f958c/triton-3.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3daf64305d6cea88d3334c65ebc9bcd0c64c9564a977084366aa768d57cbcf64", size = 184551013, upload-time = "2026-06-17T20:03:37.551Z" },
{ url = "https://files.pythonhosted.org/packages/15/76/c79c34311625227a288df3e483fc5cdf3d596624cbd4b4758c4cbdc14af3/triton-3.7.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee89fbf782ec2ad50391dd1cf26cbea4f4467154c37f4773026da8fc31c0f58e", size = 197596267, upload-time = "2026-06-17T19:53:06.898Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" },
{ url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" },
{ url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" },
{ url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" },
{ url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" },
{ url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" },
{ url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" },
{ url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" },
{ url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" },
{ url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" },
]
[[package]]
name = "triton"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "setuptools" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/ee/0ee5f64a87eeda19bbad9bc54ae5ca5b98186ed00055281fd40fb4beb10e/triton-3.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ff2785de9bc02f500e085420273bb5cc9c9bb767584a4aa28d6e360cec70128", size = 155430069, upload-time = "2025-07-30T19:58:21.715Z" },
{ url = "https://files.pythonhosted.org/packages/7d/39/43325b3b651d50187e591eefa22e236b2981afcebaefd4f2fc0ea99df191/triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b70f5e6a41e52e48cfc087436c8a28c17ff98db369447bcaff3b887a3ab4467", size = 155531138, upload-time = "2025-07-30T19:58:29.908Z" },
{ url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" },
{ url = "https://files.pythonhosted.org/packages/30/7b/0a685684ed5322d2af0bddefed7906674f67974aa88b0fae6e82e3b766f6/triton-3.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00be2964616f4c619193cb0d1b29a99bd4b001d7dc333816073f92cf2a8ccdeb", size = 155569223, upload-time = "2025-07-30T19:58:44.017Z" },
{ url = "https://files.pythonhosted.org/packages/20/63/8cb444ad5cdb25d999b7d647abac25af0ee37d292afc009940c05b82dda0/triton-3.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7936b18a3499ed62059414d7df563e6c163c5e16c3773678a3ee3d417865035d", size = 155659780, upload-time = "2025-07-30T19:58:51.171Z" },
]
[[package]]
@@ -5341,6 +5462,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" },
]
[[package]]
name = "whisperx"
version = "3.8.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ctranslate2" },
{ name = "faster-whisper" },
{ name = "huggingface-hub" },
{ name = "nltk" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "omegaconf" },
{ name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "pandas", version = "3.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "pyannote-audio" },
{ name = "torch" },
{ name = "torchaudio" },
{ name = "torchcodec", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32'" },
{ name = "torchvision" },
{ name = "transformers" },
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a0/51/4ffeb1dce4f9fa95e1688d6ee6c6f412f67f786a5f4856e906503b8502c9/whisperx-3.8.6.tar.gz", hash = "sha256:d647aecaa6c2f413bb924d722d925878ef300c99093ca49cd6cb7840bdbffe0e", size = 16490881, upload-time = "2026-05-25T12:36:06.72Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/53/d533db85704e64b1709db695fba2dd3b747cb7e979a18bd18dfbf1e44b91/whisperx-3.8.6-py3-none-any.whl", hash = "sha256:cb6d4fcd3fb6c42305cb8b222a33a0b78f6b657e9db3b714345fe43dc0a69c1f", size = 16486941, upload-time = "2026-05-25T12:36:03.885Z" },
]
[[package]]
name = "yarl"
version = "1.24.5"