diff --git a/:memory:.ses b/:memory:.ses
new file mode 100644
index 0000000..b7104b7
--- /dev/null
+++ b/:memory:.ses
@@ -0,0 +1,2 @@
+1788878781647
+0de531b6-352d-4673-95fd-f920138f27ea
diff --git a/AGENTS.md b/AGENTS.md
index e37e1a5..435323b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,6 +2,10 @@
## Project Skills
+## Interface do painel
+
+Quando o usuário pedir uma implementação “na tela”, “no painel” ou “na interface do MCP”, use o painel CEP em `code/cep-plugin/`, identificado por **MCP for Adobe Premiere Pro** e pelas abas **Editar vídeo**, **Scanner**, **Conexão**, **Modelos** e **Ajustes**. Essa é a interface de produto; não criar fluxos de interface alternativos em `code/uxp-plugin/`.
+
Este projeto usa skills do mattpocock/skills para manter integridade e organização do código.
### Skills disponíveis
diff --git a/admin/DEV-NOTES.md b/admin/DEV-NOTES.md
index a9d0a9b..f22064f 100644
--- a/admin/DEV-NOTES.md
+++ b/admin/DEV-NOTES.md
@@ -1,33 +1,23 @@
-# DEV-NOTES — Registro de implementações (Ronald)
+# DEV-NOTES — Registro de Alterações (Jhonny Editor)
-> Este arquivo é a **fonte da mensagem de commit**.
-> O script `admin/update.command` lê automaticamente a seção `## NOVO NESTE LOTE`
-> abaixo (se houver texto) e usa o conteúdo no corpo da mensagem no momento do commit.
->
> **Como usar:**
-> 1. Ao trabalhar, adicione bullets em `## NOVO NESTE LOTE` descrevendo o que mudou.
-> 2. Depois rode `admin/update.command` — ele monta o commit com esse texto + resumo
-> automático dos arquivos alterados, faz build/deploy, commita e dá push.
-> 3. Após o script rodar com sucesso, você pode mover os bullets desta seção para
-> `## Histórico` e esvaziar `NOVO NESTE LOTE`.
+> 1. Ao fazer alterações, adicione bullets na seção `## NOVO NESTE LOTE`
+> 2. Rod `admin/deploy.command` — ele lê essas notas e usa como descrição do commit
+> 3. Após o deploy, mova os bullets para `## Histórico` e esvazie `NOVO NESTE LOTE`
---
-## NOVO NESTE LOTE
-
-- adicionado admin/update.command (automatizacao de build+deploy+commit+push)
-- adicionado admin/DEV-NOTES.md (fonte de texto para a mensagem de commit)
-## Uso / lembretes (não apagar)
-- `admin/update.command` → build + deploy + commit + push em um só
-- `admin/DEV-NOTES.md` → fonte da mensagem (seção NOVO NESTE LOTE)
+## NOVO NESTE LOTE
+- criado repositório jhonny-editor no Gitea
+- adicionado script admin/deploy.command com commit automático
+- atualizado admin/DEV-NOTES.md com template limpo
## Histórico
-- Versão inicial: inicialização do repositório Ronald (backend OpenCut + painéis UXP/CEP + admin)
+
-## Anotações / pendências
-- Os PNGs de `code/extension/design/` (~46 MB) são mockups de system design — avaliar se mantém ou otimiza.
-- `admin/VPS-ACCESS.md` é o único arquivo com credenciais e NÃO deve ser commitado (já está no .gitignore).
\ No newline at end of file
+## Notas Técnicas
+
+
+## Anotações / Pendências
+- repositório: https://gitea.nacarmed.cloud/joaohenrique/jhonny-editor
+- remote SSH: ssh://git@gitea.nacarmed.cloud:2222/joaohenrique/jhonny-editor.git
diff --git a/admin/Jhonny.command b/admin/Jhonny.command
new file mode 100755
index 0000000..f58b3e4
--- /dev/null
+++ b/admin/Jhonny.command
@@ -0,0 +1,47 @@
+#!/bin/zsh
+# ============================================================
+# Jhonny Editor - admin/Jhonny.command
+#
+# PONTO DE ENTRADA UNICO: builda/instala o painel do Premiere e,
+# se tudo deu certo, commita + faz push.
+#
+# 1. PremiereMCP.command build TS, reinstala a copia do painel
+# CEP, confere venv Python, reindexa RAG
+# 2. deploy.command commit + push (usa as notas de
+# admin/DEV-NOTES.md)
+#
+# Se o passo 1 falhar, o script PARA antes de commitar - nada de
+# publicar painel quebrado.
+#
+# Uso:
+# admin/Jhonny.command ciclo completo (pede confirmacao pro push)
+# AUTO=1 admin/Jhonny.command nao pergunta antes do push
+# SKIP_BUILD=1 admin/Jhonny.command pula o build (so commit+push)
+# SKIP_PYTHON=1 admin/Jhonny.command repassado ao PremiereMCP.command
+# SKIP_RAG=1 admin/Jhonny.command repassado ao PremiereMCP.command
+# ============================================================
+set -u
+
+ADMIN="$(cd "$(dirname "$0")" && pwd -P)"
+
+GRN="$(tput setaf 2 2>/dev/null)"; YLW="$(tput setaf 3 2>/dev/null)"
+RD="$(tput setaf 1 2>/dev/null)"; BLU="$(tput setaf 4 2>/dev/null)"
+NC="$(tput sgr0 2>/dev/null)"
+step() { echo; echo "${BLU}============================================================${NC}"
+ echo "${BLU} $*${NC}"
+ echo "${BLU}============================================================${NC}"; }
+ok() { echo "${GRN}-> OK ${NC}$*"; }
+die() { echo "${RD}-> Erro: ${NC}$*" >&2; exit 1; }
+
+if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
+ step "1/2 - PremiereMCP.command (build + instalar painel)"
+ "$ADMIN/PremiereMCP.command" || die "PremiereMCP.command falhou - corrija antes de publicar"
+ ok "build/instalação concluída"
+else
+ ok "SKIP_BUILD=1 - pulando etapa de build"
+fi
+
+step "2/2 - deploy.command (commit + push)"
+"$ADMIN/deploy.command" || die "deploy.command falhou"
+
+ok "Jhonny.command concluído"
diff --git a/admin/commit.command b/admin/commit.command
deleted file mode 100755
index 28212a6..0000000
--- a/admin/commit.command
+++ /dev/null
@@ -1,127 +0,0 @@
-#!/bin/zsh
-# ============================================================
-# OpenCut Ronald - admin/commit.command
-#
-# COMMIT: monta a mensagem, commita e envia para o Gitea.
-# 1. Le a secao 'NOVO NESTE LOTE' do admin/DEV-NOTES.md
-# 2. Junta o resumo automatico dos arquivos alterados
-# 3. git add -A + commit + push
-#
-# O push e a unica etapa que sai desta maquina, entao ele pede
-# confirmacao. Use AUTO=1 para rodar sem perguntar (ex.: dentro do
-# OpenCut.command em modo automatico).
-#
-# Uso:
-# admin/commit.command commita e pergunta antes do push
-# DRY=1 admin/commit.command so mostra o plano, nao commita
-# AUTO=1 admin/commit.command commita e faz push sem perguntar
-# ============================================================
-set -u
-umask 077
-
-ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
-ADMIN="$ROOT/admin"
-NOTES="$ADMIN/DEV-NOTES.md"
-REMOTE="origin"
-
-GRN="$(tput setaf 2 2>/dev/null)"; YLW="$(tput setaf 3 2>/dev/null)"
-RD="$(tput setaf 1 2>/dev/null)"; BLU="$(tput setaf 4 2>/dev/null)"
-NC="$(tput sgr0 2>/dev/null)"
-say() { echo "${BLU}== ${NC}$*"; }
-ok() { echo "${GRN}-> OK ${NC}$*"; }
-warn() { echo "${YLW}-> Aviso: ${NC}$*"; }
-die() { echo "${RD}-> Erro: ${NC}$*" >&2; exit 1; }
-
-cd "$ROOT" || die "nao consegui entrar em $ROOT"
-[[ -d .git ]] || die "nao e um repositorio git em $ROOT"
-git remote get-url "$REMOTE" >/dev/null 2>&1 || die "remote '$REMOTE' nao configurado"
-
-echo "${BLU}============================================================${NC}"
-say "commit.command - commit + push"
-say "repositorio: $(git remote get-url "$REMOTE")"
-say "branch: $(git branch --show-current)"
-echo "${BLU}============================================================${NC}"
-
-# ------------------------------------------------------------
-# 1) Ha o que commitar?
-# ------------------------------------------------------------
-CHANGES="$(git status --porcelain)"
-if [[ -z "$CHANGES" ]]; then
- ok "Nenhuma alteracao para commitar."
- exit 0
-fi
-
-say "Alteracoes detectadas:"
-echo "$CHANGES" | sed 's/^/ /' | head -60
-echo " ... ($(echo "$CHANGES" | grep -c .) itens)"
-echo
-
-# ------------------------------------------------------------
-# 2) Mensagem do commit
-# ------------------------------------------------------------
-MSG_NOTA=""
-if [[ -f "$NOTES" && -s "$NOTES" ]]; then
- MSG_NOTA="$(awk '
- /^## NOVO NESTE LOTE/ {f=1; next}
- /^## / && f {exit}
- f
- ' "$NOTES" 2>/dev/null \
- | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' \
- | grep -v '^$' \
- | awk '/^/&&c{c=0;next} c{next} {print}' \
- | sed 's/^[-*] //' \
- | grep -v '(_vazio' || true)"
-fi
-
-FILES="$(git status --porcelain | sed 's/^.../ - /' | head -80)"
-STAT="$(git diff --stat HEAD | tail -1)"
-
-if [[ -n "$MSG_NOTA" ]]; then
- BODY="$MSG_NOTA"
-else
- BODY="(sem nota em DEV-NOTES.md; veja os arquivos alterados abaixo)"
-fi
-
-COMMIT_MSG="update automatizado: alteracoes no projeto
-$(printf '%s\n' "$BODY")
-
-$STAT
-
-Arquivos alterados:
-$FILES"
-
-say "Mensagem do commit a ser usada:"
-echo "----------------------------------------------"
-echo "$COMMIT_MSG"
-echo "----------------------------------------------"
-echo
-
-if [[ "${DRY:-0}" = "1" ]]; then
- say "DRY=1 - nao vou commitar/push."
- exit 0
-fi
-
-# ------------------------------------------------------------
-# 3) Commit
-# ------------------------------------------------------------
-say "Adicionando arquivos e commitando..."
-git add -A
-git commit -m "$COMMIT_MSG" || die "commit falhou"
-ok "commit criado: $(git rev-parse --short HEAD)"
-echo
-
-# ------------------------------------------------------------
-# 4) Push (confirmado - e o passo que publica)
-# ------------------------------------------------------------
-if [[ "${AUTO:-0}" != "1" ]]; then
- printf "Enviar para '%s' agora? [s/N] " "$REMOTE"
- read -r RESP
- case "$RESP" in
- [sS]|[sS][iI][mM]|[yY]|[yY][eE][sS]) ;;
- *) warn "push cancelado - o commit ficou local (envie depois com: git push $REMOTE HEAD)"; exit 0 ;;
- esac
-fi
-
-say "Fazendo push para '$REMOTE'..."
-git push "$REMOTE" HEAD || die "push falhou"
-ok "push concluido"
diff --git a/admin/deploy.command b/admin/deploy.command
index e8d14fb..083c543 100755
--- a/admin/deploy.command
+++ b/admin/deploy.command
@@ -1,128 +1,147 @@
#!/bin/zsh
# ============================================================
-# OpenCut Ronald - admin/deploy.command
+# Jhonny Editor - admin/deploy.command
#
-# DEPLOY: garante o backend OpenCut no ar em http://127.0.0.1:5679
-# e sobe a ponte de live-updates (WebSocket) que o painel consome.
+# DEPLOY COMMIT: verifica alterações, documenta e faz push
#
-# E este o script que o painel do Premiere dispara quando pede
-# "iniciar backend" (via ~/.opencut/launcher.command). Por isso ele
-# nao pergunta nada e nao mexe no git: roda sozinho e sai.
+# Fluxo:
+# 1. Verifica se há alterações no repositório
+# 2. Lê as notas do admin/DEV-NOTES.md
+# 3. Monta mensagem de commit descritiva
+# 4. Faz commit e push automático
#
# Uso:
-# admin/deploy.command sobe (ou recupera) o backend
-# OPENCUT_RESTART=1 admin/deploy.command reinicia o backend atual
-# OPENCUT_NO_OPEN=1 admin/deploy.command nao abre o navegador
+# admin/deploy.command commit + push interativo
+# AUTO=1 admin/deploy.command commit + push automático
+# DRY=1 admin/deploy.command mostra plano sem executar
# ============================================================
set -u
+umask 077
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
-CODE="$ROOT/code"
-START_SCRIPT="$CODE/start-opencut.sh"
-RUNTIME_DIR="$CODE/.opencut-runtime"
-LOG="$RUNTIME_DIR/server.log"
-URL="http://127.0.0.1:5679"
-HEALTH="$URL/health"
-RESTART="${OPENCUT_RESTART:-0}"
+ADMIN="$ROOT/admin"
+NOTES="$ADMIN/DEV-NOTES.md"
+REMOTE="origin"
+BRANCH="main"
-notify() { osascript -e "display notification \"$1\" with title \"OpenCut\"" 2>/dev/null || true; }
-open_ui() { [ "${OPENCUT_NO_OPEN:-0}" = "1" ] || open "$URL" 2>/dev/null || true; }
+GRN="$(tput setaf 2 2>/dev/null)"; YLW="$(tput setaf 3 2>/dev/null)"
+RD="$(tput setaf 1 2>/dev/null)"; BLU="$(tput setaf 4 2>/dev/null)"
+NC="$(tput sgr0 2>/dev/null)"
+say() { echo "${BLU}== ${NC}$*"; }
+ok() { echo "${GRN}-> OK ${NC}$*"; }
+warn() { echo "${YLW}-> Aviso: ${NC}$*"; }
+die() { echo "${RD}-> Erro: ${NC}$*" >&2; exit 1; }
-server_ok() { curl -s -m 5 "$HEALTH" 2>/dev/null | grep -q '"status":"ok"'; }
+cd "$ROOT" || die "nao consegui entrar em $ROOT"
+[[ -d .git ]] || die "nao e um repositorio git em $ROOT"
+git remote get-url "$REMOTE" >/dev/null 2>&1 || die "remote '$REMOTE' nao configurado"
-stop_opencut_pids() {
- local signal="$1"
- local pid
- for pid in ${(f)PGREP}; do
- [ -n "$pid" ] && kill "$signal" "$pid" 2>/dev/null || true
- done
-}
+echo "${BLU}============================================================${NC}"
+say "Jhonny Editor - Deploy com Commit Automático"
+say "repositorio: $(git remote get-url "$REMOTE")"
+say "branch: $(git branch --show-current)"
+echo "${BLU}============================================================${NC}"
# ------------------------------------------------------------
-# Ponte de live-updates (WebSocket)
-#
-# O backend nao sobe a ponte sozinho: ela so existe apos um POST em
-# /ws/start, e esse endpoint exige o token CSRF que o /health entrega.
-# Sem este passo o painel abre mostrando "Bridge stopped" e o usuario
-# precisa clicar em "Start" na mao a cada boot.
+# 1) Ha o que commitar?
# ------------------------------------------------------------
-start_bridge() {
- if curl -s -m 5 "$URL/ws/status" 2>/dev/null | grep -q '"running":true'; then
- echo "-> ponte de live-updates ja ativa"
- return 0
- fi
- local token
- token="$(curl -s -m 5 -H "Origin: $URL" "$HEALTH" 2>/dev/null \
- | /usr/bin/python3 -c 'import sys,json
-try: print(json.load(sys.stdin).get("csrf_token") or "")
-except Exception: print("")' 2>/dev/null)"
- if [ -z "$token" ]; then
- echo "-> Aviso: /health nao devolveu token CSRF; ponte nao iniciada"
- return 1
- fi
- if curl -s -m 10 -X POST "$URL/ws/start" \
- -H "Content-Type: application/json" \
- -H "X-OpenCut-Token: $token" \
- -H "Origin: $URL" -d '{}' 2>/dev/null | grep -q '"running":true'; then
- echo "-> ponte de live-updates ativa"
- return 0
- fi
- echo "-> Aviso: a ponte de live-updates nao subiu (veja $LOG)"
- return 1
-}
-
-# 1) Ja esta no ar?
-if server_ok && [ "$RESTART" != "1" ]; then
- echo "-> backend ja rodando em $URL"
- start_bridge
- notify "Backend ja esta rodando em $URL"
- open_ui
- exit 0
+CHANGES="$(git status --porcelain)"
+if [[ -z "$CHANGES" ]]; then
+ ok "Nenhuma alteracao para commitar."
+ exit 0
fi
-if [ "$RESTART" = "1" ]; then
- echo "-> reinicio solicitado; substituindo o backend atual"
+say "Alteracoes detectadas:"
+echo "$CHANGES" | sed 's/^/ /' | head -60
+echo " ... ($(echo "$CHANGES" | grep -c .) itens)"
+echo
+
+# ------------------------------------------------------------
+# 2) Extrair notas do DEV-NOTES.md
+# ------------------------------------------------------------
+MSG_NOTA=""
+if [[ -f "$NOTES" && -s "$NOTES" ]]; then
+ MSG_NOTA="$(awk '
+ /^## NOVO NESTE LOTE/ {f=1; next}
+ /^## / && f {exit}
+ f
+ ' "$NOTES" 2>/dev/null \
+ | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' \
+ | grep -v '^$' \
+ | awk '/^/&&c{c=0;next} c{next} {print}' \
+ | sed 's/^[-*] //' \
+ | grep -v '(_vazio' || true)"
fi
-# 2) Processo antigo travado? Derruba antes de subir outro.
-PGREP=$(pgrep -f 'opencut.server' || true)
-if [ -n "$PGREP" ]; then
- notify "Backend travado detectado - reiniciando..."
- echo "Encerrando processo(s) antigo(s): $PGREP"
- stop_opencut_pids TERM
- sleep 3
- PGREP=$(pgrep -f 'opencut.server' || true)
- [ -n "$PGREP" ] && stop_opencut_pids KILL
- sleep 1
+# ------------------------------------------------------------
+# 3) Montar estatísticas
+# ------------------------------------------------------------
+FILES="$(git status --porcelain | sed 's/^.../ - /' | head -80)"
+STAT="$(git diff --stat HEAD | tail -1)"
+TOTAL_FILES=$(echo "$CHANGES" | grep -c .)
+NEW_FILES=$(echo "$CHANGES" | grep -c '^??' || true)
+MODIFIED_FILES=$(echo "$CHANGES" | grep -c '^ M\|^M' || true)
+DELETED_FILES=$(echo "$CHANGES" | grep -c '^ D\|^D' || true)
+
+# ------------------------------------------------------------
+# 4) Montar mensagem do commit
+# ------------------------------------------------------------
+if [[ -n "$MSG_NOTA" ]]; then
+ BODY="$MSG_NOTA"
+else
+ BODY="(sem nota em DEV-NOTES.md)"
fi
-# 3) Sobe pelo start-opencut.sh (venv, modelos, ffmpeg e tmp no Merongo)
-if [ ! -x "$START_SCRIPT" ]; then
- echo "Erro: $START_SCRIPT nao encontrado ou sem permissao de execucao" >&2
- notify "Falha: start-opencut.sh ausente"
- exit 1
+COMMIT_MSG="feat: $(echo "$BODY" | head -1 | cut -c1-50)
+
+$BODY
+
+Resumo:
+- $TOTAL_FILES arquivos alterados
+- $NEW_FILES novos
+- $MODIFIED_FILES modificados
+- $DELETED_FILES removidos
+
+$STAT
+
+Arquivos:
+$FILES"
+
+say "Mensagem do commit:"
+echo "----------------------------------------------"
+echo "$COMMIT_MSG"
+echo "----------------------------------------------"
+echo
+
+if [[ "${DRY:-0}" = "1" ]]; then
+ say "DRY=1 - nao vou commitar/push."
+ exit 0
fi
-mkdir -p "$RUNTIME_DIR"
-mkdir -p "/Volumes/Merongo/SISTEMAS/OPENCUT-MEDIA/tmp"
-nohup "$START_SCRIPT" > "$LOG" 2>&1 &
-NEW_PID=$!
-echo "Backend iniciado (PID $NEW_PID). Aguardando health check..."
-# 4) Espera ate 60s pelo health check
-for i in {1..30}; do
- if server_ok; then
- start_bridge
- notify "Backend pronto em $URL"
- open_ui
- exit 0
- fi
- if ! kill -0 $NEW_PID 2>/dev/null; then
- break
- fi
- sleep 2
-done
+# ------------------------------------------------------------
+# 5) Commit
+# ------------------------------------------------------------
+say "Adicionando arquivos e commitando..."
+git add -A
+git commit -m "$COMMIT_MSG" || die "commit falhou"
+ok "commit criado: $(git rev-parse --short HEAD)"
+echo
-echo "Falha ao iniciar. Log em $LOG" >&2
-notify "Falha ao iniciar o backend. Veja $LOG"
-tail -30 "$LOG"
-exit 1
+# ------------------------------------------------------------
+# 6) Push (automático ou interativo)
+# ------------------------------------------------------------
+if [[ "${AUTO:-0}" = "1" ]]; then
+ RESP="s"
+else
+ printf "Enviar para '%s' agora? [s/N] " "$REMOTE"
+ read -r RESP
+fi
+
+case "$RESP" in
+ [sS]|[sS][iI][mM]|[yY]|[yY][eE][sS]) ;;
+ *) warn "push cancelado - commit local salvo"; exit 0 ;;
+esac
+
+say "Fazendo push para '$REMOTE'..."
+git push "$REMOTE" "$BRANCH" || die "push falhou"
+ok "push concluido - deploy finalizado!"
diff --git a/admin/OpenCut.command b/admin/inativos/OpenCut.command
similarity index 100%
rename from admin/OpenCut.command
rename to admin/inativos/OpenCut.command
diff --git a/admin/update.command b/admin/inativos/update.command
similarity index 100%
rename from admin/update.command
rename to admin/inativos/update.command
diff --git a/code/cep-plugin/index.html b/code/cep-plugin/index.html
index f3005d8..78a5b9b 100755
--- a/code/cep-plugin/index.html
+++ b/code/cep-plugin/index.html
@@ -25,6 +25,9 @@
+
@@ -218,6 +221,44 @@
+
+
+
Escolha o que será lido no arquivo original. O Scanner usa os tempos da timeline, mas não exporta frames pelo Premiere.
+
+
Amostragem e faixas
+
+
+
Use a amostragem precisa somente no trecho que será reenquadrado.
+
+
Abra a sequência desejada e detecte as faixas para selecioná-las.
+
Faixas de vídeo
Faixas de áudio
+
+
Transcrição
—
+
+
+
Hugging Face não configurado
+
A lista inclui somente modelos encontrados nas pastas locais configuradas. O token do Hugging Face é usado apenas na diarização; nunca é salvo no perfil nem no relatório.
+
+
O que detectar
+
+
+
+
+
+
+
+
+
Leitura temporal
+
+
+
+
+
+
Perfil padrão pronto.
+
Relatório pronto
+
+
+
diff --git a/code/cep-plugin/main.js b/code/cep-plugin/main.js
index 2184408..deb2799 100755
--- a/code/cep-plugin/main.js
+++ b/code/cep-plugin/main.js
@@ -148,6 +148,7 @@ function stepState(n) {
function switchTab(name) {
var tabs = [
{ key: "silence", panel: "tabSilence", btn: "tabBtnSilence" },
+ { key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" },
{ key: "bridge", panel: "tabBridge", btn: "tabBtnBridge" },
{ key: "models", panel: "tabModels", btn: "tabBtnModels" },
{ key: "settings", panel: "tabSettings", btn: "tabBtnSettings" },
@@ -164,8 +165,116 @@ function switchTab(name) {
if (name === "models") renderModelList();
if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); }
if (name === "settings") renderEditorPersonalities();
+ if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); }
}
+// ---- Perfil do Scanner -----------------------------------------------------
+// O painel e o Engine compartilham este contrato versionado. O perfil não
+// contém caminhos de mídia; estes continuam sendo resolvidos pelo MCP.
+var SCANNER_PROFILE_KEY = "mcp_scanner_profile_v1";
+var SCANNER_ALL_RESOURCES = ["faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "codigos", "categorias", "estetica", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas"];
+
+var scannerTimeline = null;
+var scannerReportPath = null;
+var SCANNER_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_scanner.py";
+
+// `path` e `os` são carregados mais abaixo, junto com o runtime Node do CEP.
+// Calcular isso aqui interrompia todo o script antes da inicialização do painel.
+function scannerOutputRoot() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); }
+
+function scannerSelectedTracks(kind) { return Array.prototype.slice.call(document.querySelectorAll("[data-scanner-track='" + kind + "']:checked")).map(function (item) { return Number(item.value); }).sort(function (a, b) { return a - b; }); }
+
+function scannerRenderTracks(kind, tracks, selected) {
+ var target = document.getElementById(kind === "video" ? "scannerVideoTracks" : "scannerAudioTracks");
+ target.innerHTML = "";
+ if (!tracks.length) { target.textContent = "Nenhuma faixa encontrada."; return; }
+ tracks.forEach(function (track) { var label = document.createElement("label"); label.className = "checkbox-field"; var input = document.createElement("input"); input.type = "checkbox"; input.setAttribute("data-scanner-track", kind); input.value = track.index; input.checked = selected.indexOf(track.index) >= 0; label.appendChild(input); label.appendChild(document.createTextNode(" " + track.name + " · " + track.numClips + " clipe(s)")); target.appendChild(label); });
+}
+
+function scannerProfileFromScreen() {
+ var enabled = Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]:checked")).map(function (item) { return item.getAttribute("data-scanner-resource"); });
+ function include(source, extras) { if (enabled.indexOf(source) >= 0) extras.forEach(function (item) { if (enabled.indexOf(item) < 0) enabled.push(item); }); }
+ include("faces", ["qualidade_facial"]); include("pose", ["maos"]); include("ocr", ["codigos"]); include("estetica", ["horizonte", "retangulos", "contornos"]);
+ var modelo = document.getElementById("scannerTranscribeModel").value;
+ var opcao = document.getElementById("scannerTranscribeModel").selectedOptions[0];
+ return { versao: 1, intervalo_em_segundos: Number(document.getElementById("scannerInterval").value), faixas_de_video: scannerSelectedTracks("video"), faixas_de_audio: scannerSelectedTracks("audio"), recursos_vision: SCANNER_ALL_RESOURCES.filter(function (item) { return enabled.indexOf(item) >= 0; }), detectar_cenas: document.getElementById("scannerScenes").checked, analisar_continuidade: document.getElementById("scannerContinuity").checked, preparar_reenquadramento: document.getElementById("scannerReframe").checked, interpretar_cena: document.getElementById("scannerInterpret").checked, transcricao: { modelo: modelo, caminho_modelo: opcao && opcao.dataset.path || "", idioma: document.getElementById("scannerTranscribeLanguage").value, diarizacao: document.getElementById("scannerDiarization").checked, usar_hugging_face: !!loadHfToken() } };
+}
+
+function saveScannerProfile() {
+ try { var profile = scannerProfileFromScreen(); localStorage.setItem(SCANNER_PROFILE_KEY, JSON.stringify(profile)); document.getElementById("scannerProfileStatus").textContent = "Perfil salvo: " + profile.intervalo_em_segundos + " s por frame; " + profile.faixas_de_video.length + " faixa(s) de vídeo."; showToast("ok", "Perfil do Scanner salvo."); } catch (error) { showToast("err", error.message || String(error)); }
+}
+
+function loadScannerProfile() {
+ var saved; try { saved = JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY) || "null"); } catch (_) { saved = null; }
+ if (!saved) return;
+ document.getElementById("scannerInterval").value = String(saved.intervalo_em_segundos || 1);
+ if (scannerTimeline) { scannerRenderTracks("video", scannerTimeline.videoTracks, saved.faixas_de_video || []); scannerRenderTracks("audio", scannerTimeline.audioTracks, saved.faixas_de_audio || []); }
+ var resources = saved.recursos_vision || SCANNER_ALL_RESOURCES;
+ Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]")).forEach(function (item) { item.checked = resources.indexOf(item.getAttribute("data-scanner-resource")) >= 0; });
+ document.getElementById("scannerScenes").checked = saved.detectar_cenas !== false; document.getElementById("scannerContinuity").checked = saved.analisar_continuidade !== false; document.getElementById("scannerReframe").checked = !!saved.preparar_reenquadramento; document.getElementById("scannerInterpret").checked = saved.interpretar_cena !== false;
+ if (saved.transcricao) { document.getElementById("scannerTranscribeModel").value = saved.transcricao.modelo || ""; document.getElementById("scannerTranscribeLanguage").value = saved.transcricao.idioma || loadLanguage(); document.getElementById("scannerDiarization").checked = !!saved.transcricao.diarizacao && !!loadHfToken(); }
+}
+
+function renderScannerTranscriptionOptions() {
+ var modelos=document.getElementById("scannerTranscribeModel"), idiomas=document.getElementById("scannerTranscribeLanguage"); if (!modelos || !idiomas) return;
+ var atual=modelos.value; modelos.innerHTML=""; var disponiveis=discoverPortugueseModelsOnMerongo();
+ if (!disponiveis.length) { var vazio=document.createElement("option"); vazio.value=""; vazio.textContent="Nenhum modelo com português encontrado no Merongo"; modelos.appendChild(vazio); } else { disponiveis.forEach(function(item){var option=document.createElement("option");option.value=item.nome;option.dataset.path=item.caminho;option.textContent=item.nome+" · "+formatMB(item.tamanho);modelos.appendChild(option);}); if (atual) modelos.value=atual; }
+ idiomas.innerHTML=""; LANGUAGE_CATALOG.forEach(function(item){var option=document.createElement("option");option.value=item.value;option.textContent=item.label;idiomas.appendChild(option);}); idiomas.value=loadLanguage(); document.getElementById("scannerModelCount").textContent=disponiveis.length+" instalado(s)"; renderScannerHuggingFaceStatus();
+}
+
+function renderScannerHuggingFaceStatus() {
+ var status = document.getElementById("scannerHfStatus");
+ var text = document.getElementById("scannerHfStatusText");
+ var diarization = document.getElementById("scannerDiarization");
+ if (!status || !text || !diarization) return;
+ var available = !!loadHfToken();
+ status.setAttribute("data-state", available ? "valid" : "unset");
+ text.textContent = available ? "Hugging Face disponível para identificar locutores" : "Configure o token do Hugging Face para identificar locutores";
+ diarization.disabled = !available;
+ if (!available) diarization.checked = false;
+}
+
+var MERONGO_MODEL_ROOTS = ["/Volumes/Merongo/SISTEMAS/WHISPERX/whisper/models", "/Volumes/Merongo/SISTEMAS/MODELOSIA/hf-cache", "/Volumes/Merongo/Applications/WhisperX/huggingface/hub", "/Volumes/Merongo/Applications/Trasncritor", "/Volumes/Merongo/SISTEMAS/AUX"];
+function discoverPortugueseModelsOnMerongo() {
+ var encontrados = {}, roots = [effectiveModelsPath()].concat(MERONGO_MODEL_ROOTS);
+ function walk(dir, profundidade) { if (profundidade > 5) return; var entries; try { entries=fs.readdirSync(dir,{withFileTypes:true}); } catch (_) { return; } entries.forEach(function(entry) { if (!entry.isDirectory() || entry.name === ".locks" || entry.name === ".git") return; var full=path.join(dir,entry.name); if (fs.existsSync(path.join(full,"model.bin")) && modelSupportsPortuguese(full)) { var nome=modelNameFromPath(full); if (!encontrados[nome]) encontrados[nome]={nome:nome,caminho:full,tamanho:folderSizeMB(full)}; return; } walk(full,profundidade+1); }); }
+ roots.filter(function(root,index){return root && roots.indexOf(root)===index;}).forEach(function(root){walk(root,0);});
+ return Object.keys(encontrados).sort().map(function(nome){return encontrados[nome];});
+}
+function modelSupportsPortuguese(folder) { try { var tokenizer=path.join(folder,"tokenizer.json"); if (!fs.existsSync(tokenizer)) return false; return fs.readFileSync(tokenizer,"utf-8").indexOf("<|pt|>") >= 0; } catch (_) { return false; } }
+function modelNameFromPath(folder) { var value=String(folder).replace(/.*faster-whisper-/i,"").replace(/[\\/].*$/,""); return value || path.basename(folder); }
+
+function scannerDetectTracks() {
+ setBusy("btnScannerDetectTracks", true); document.getElementById("scannerTracksHelp").textContent = "Lendo as faixas da sequência ativa…";
+ cs.evalScript("(function(){try{var s=app.project.activeSequence;if(!s)return JSON.stringify({ok:false,error:'Nenhuma sequência ativa.'});function tracks(c,a){for(var i=0;i IdDaExecucao: ...
+ def obter_status(self, execucao: IdDaExecucao) -> StatusDaIndexacao: ...
+ def buscar(self, consulta: str, opcoes: OpcoesDeBusca | None = None) -> list[ResultadoDeBusca]: ...
+ def obter_ativo(self, identificador: str) -> AtivoDeVideo | None: ...
+```
+
+`indexar_pasta` inicia ou agenda uma execução idempotente e retorna imediatamente. O progresso é consultado pelo identificador da execução. `buscar` retorna resultados agrupados por ativo, com trechos temporais e evidências que justificam cada resultado.
+
+O módulo esconde a coordenação entre descoberta incremental, análise, persistência e indexação de busca. Adapters internos podem variar sem alterar essa interface.
+
+### 3. Indexação incremental é regra do domínio
+
+Cada ativo deve registrar:
+
+- impressão digital observada;
+- versão do contrato de análise;
+- versão de cada provider/modelo usado;
+- etapas concluídas;
+- etapas com erro ou aviso;
+- último status e última execução.
+
+Um arquivo inalterado e já concluído não deve ser analisado novamente. Um arquivo novo entra na fila. Um arquivo cuja impressão digital mudou invalida apenas as etapas derivadas daquele conteúdo. Se somente um provider ou sua versão mudou, a política pode invalidar apenas a etapa correspondente.
+
+Arquivos removidos da pasta não devem ser apagados imediatamente do índice: passam a `ausente_na_origem`, preservando resultados históricos e permitindo recuperação caso retornem. A remoção definitiva deve ser uma operação explícita futura.
+
+## Módulos e responsabilidades
+
+### `biblioteca`
+
+Módulo profundo e seam principal. Recebe a intenção do usuário, cria uma execução, delega o trabalho ao coordenador e expõe status, resultados e erros normalizados.
+
+Não conhece detalhes de providers nem executa chamadas de sistema diretamente.
+
+### `descoberta_da_pasta`
+
+Percorre a pasta autorizada, respeitando configuração de recursão, extensões suportadas, exclusões e links simbólicos. Produz candidatos de arquivos de vídeo e reconcilia o snapshot atual com o último snapshot persistido.
+
+Não extrai metadados, não abre frames e não chama IA.
+
+### `identidade_do_ativo`
+
+Calcula e compara a impressão digital do arquivo. Encapsula a política de “novo”, “inalterado”, “alterado” e “ausente”. Deve ser determinística e testável com um filesystem falso.
+
+### `coordenacao_da_indexacao`
+
+Transforma candidatos em trabalhos por ativo e etapa, respeita dependências, atualiza progresso, trata cancelamento, permite retomada e aplica a política de erro por ativo.
+
+Dependências sugeridas:
+
+```text
+descoberta
+ └── metadados
+ ├── amostragem_de_frames ── análise_visual ── segmentos_visuais
+ └── extração_de_audio ── transcrição ── segmentos_de_fala
+ └── análise_de_audio
+
+segmentos + evidências ── consolidação_temporal ── indexação_de_busca
+```
+
+Metadados devem ser pré-requisito para calcular duração e amostragem. O ramo visual e o ramo de áudio podem executar em paralelo. A consolidação e a indexação só ocorrem depois dos ramos habilitados, sem exigir que todos tenham sucesso.
+
+### `analise_de_conteudo_audiovisual`
+
+Orquestra os adapters já existentes no projeto:
+
+- `ExtracaoDeMetadados` para dados técnicos;
+- `ExtratorDeQuadros` e `DetectorDeCenas` para amostragem e intervalos;
+- analisadores OpenCV, Apple Vision, ONNX ou MediaPipe para evidências visuais;
+- adapters Whisper, Apple Speech ou Groq para transcrição;
+- diarização e emoção local quando habilitadas.
+
+O resultado deve usar modelos do domínio, especialmente `EvidenciaVisual`, `CenaVisual` e `SegmentoDeTranscricao`. Um provider indisponível gera aviso de capacidade e não deve apagar evidências produzidas por outros providers.
+
+O módulo não deve pedir que um modelo de linguagem “assista” ao arquivo inteiro. A análise deve trabalhar com amostras temporais, cenas e agregação de evidências. Uma descrição de cena pode ser criada a partir de um conjunto limitado de frames, sempre mantendo os timestamps que a sustentam.
+
+### `consolidacao_temporal`
+
+Converte observações pontuais em intervalos úteis para busca. Observações do mesmo tipo e valor semelhante podem ser agrupadas quando estão próximas, com uma margem configurável. O resultado precisa conservar as observações originais, pois elas são a evidência auditável do intervalo consolidado.
+
+Esse módulo é o lugar correto para responder “em que momento” e para evitar que a busca retorne apenas o arquivo inteiro.
+
+### `persistencia_do_indice`
+
+Adapter responsável por salvar e ler o estado durável. A primeira implementação deve ser local e transacional, preferencialmente SQLite, com arquivos de frames/áudio tratados como cache reconstruível fora do banco.
+
+Interface mínima:
+
+```python
+class RepositorioDaBiblioteca(Protocol):
+ def reconciliar_snapshot(self, snapshot: SnapshotDaPasta) -> ResultadoDaReconciliacao: ...
+ def salvar_resultado(self, resultado: ResultadoDoAtivo) -> None: ...
+ def obter_trabalho_pendente(self, limite: int) -> list[TrabalhoDeIndexacao]: ...
+ def atualizar_status(self, status: StatusDaIndexacao) -> None: ...
+ def buscar_evidencias(self, consulta: ConsultaNormalizada) -> list[ResultadoDeBusca]: ...
+```
+
+O contrato deve permitir um adapter em memória para testes. Nenhum provider deve escrever diretamente no banco.
+
+### `busca_semantica`
+
+Recebe texto livre, normaliza a consulta e combina três fontes:
+
+1. busca lexical em nomes, transcrições, rótulos e descrições;
+2. busca vetorial em descrições de cenas, transcrições e evidências;
+3. filtros estruturados por ativo, intervalo, tipo de evidência, confiança e status.
+
+O MVP deve priorizar busca híbrida com ranking explicável. Cada resultado deve informar score, trecho, arquivo e evidências correspondentes. Busca vetorial sem evidência temporal não atende ao requisito.
+
+Um adapter vetorial pode ser adicionado depois. A persistência deve permitir começar com SQLite FTS e embeddings opcionais, sem acoplar o domínio a um banco vetorial específico.
+
+### `integracao_com_timeline_e_editor`
+
+Traduz um `ResultadoDeBusca` para ações de revisão: abrir o arquivo, posicionar o playhead, criar marcador ou propor um clipe para o fluxo editorial. A integração deve ser somente leitura/proposição na primeira versão.
+
+Não deve alterar a timeline automaticamente nem tratar um resultado sem revisão como autorização de edição.
+
+## Modelo de dados lógico
+
+```text
+bibliotecas
+ id, raiz, configuracao_json, criada_em, atualizada_em
+
+ativos
+ id, biblioteca_id, caminho, nome, extensao, status_origem,
+ tamanho, modificado_em, fingerprint, criado_em, atualizado_em
+
+metadados_dos_ativos
+ ativo_id, duracao, largura, altura, fps, codecs, formato, json_extra
+
+execucoes_de_indexacao
+ id, biblioteca_id, status, motivo, iniciada_em, finalizada_em,
+ total_trabalhos, concluidos, falhos, cancelada_em
+
+trabalhos_de_indexacao
+ id, execucao_id, ativo_id, etapa, versao, status, tentativas,
+ erro, iniciada_em, finalizada_em
+
+segmentos_de_conteudo
+ id, ativo_id, inicio, fim, tipo, resumo, confianca, origem
+
+evidencias
+ id, segmento_id, tipo, valor_json, inicio, fim, confianca,
+ provider, modelo, versao
+
+transcricoes
+ id, ativo_id, inicio, fim, texto, falante, confianca, provider, versao
+
+representacoes_de_busca
+ id, alvo_tipo, alvo_id, texto, embedding, indice_lexical
+```
+
+Os intervalos são sempre relativos ao arquivo de origem. Quando houver uso numa timeline, a tradução para o intervalo da timeline pertence à integração com o editor, usando o `intervalo_na_origem` do domínio existente.
+
+## Fluxo completo
+
+```text
+Usuário seleciona pasta
+ ↓
+BibliotecaDeVideos.indexar_pasta()
+ ↓
+Execução persistida + trabalhos pendentes
+ ↓
+Snapshot da pasta e reconciliação por fingerprint
+ ↓
+Metadados por ativo novo/alterado
+ ↓
+Ramos visual e áudio em segundo plano
+ ↓
+Consolidação de cenas, evidências e falas
+ ↓
+Atualização transacional do índice lexical/vetorial
+ ↓
+Status consultável e busca com timestamps
+```
+
+Cada trabalho deve ser retomável. A gravação de uma etapa deve ser atômica: o índice não pode anunciar uma etapa concluída antes de seus dados e sua versão estarem persistidos.
+
+## Contrato de status e falhas
+
+Status da biblioteca: `nao_indexada`, `indexando`, `parcial`, `concluida`, `falhou` ou `ausente_na_origem`.
+
+Status da etapa: `pendente`, `em_execucao`, `concluida`, `concluida_com_avisos`, `falhou`, `cancelada`.
+
+Falha de um arquivo não deve interromper toda a biblioteca. Falha de descoberta ou persistência deve interromper a execução, pois torna o resultado inconsistente. Falha de um provider deve marcar a capacidade correspondente como indisponível e preservar as demais etapas.
+
+O status deve conter progresso por contagem de trabalhos e por ativo; percentual baseado apenas em duração de vídeo pode ficar enganoso quando há arquivos muito diferentes.
+
+## Escopo recomendado do MVP
+
+1. Seleção e persistência de uma biblioteca local.
+2. Descoberta recursiva de extensões configuradas.
+3. Fingerprint e reconciliação incremental.
+4. Metadados via FFprobe.
+5. Amostragem de frames e análise visual já disponível localmente.
+6. Transcrição por um adapter configurado, com timestamps.
+7. Segmentos temporais e evidências persistidos em SQLite.
+8. Busca lexical por nome, transcrição, rótulos e descrições normalizadas.
+9. Status, retomada e cancelamento do processamento.
+10. Resultado com caminho, intervalo, confiança e evidência.
+
+Ficam para uma segunda etapa: embeddings, ranking híbrido, diarização avançada, reconhecimento de identidade, monitoramento contínuo por filesystem watcher, agrupamento de takes semelhantes e criação automática de marcadores no Premiere.
+
+## Integração com o que já existe
+
+O novo módulo deve reutilizar os adapters de `code/engine/integracoes/midia`, `code/engine/integracoes/visual` e `code/engine/integracoes/whisper`. A implementação atual de `scanner` pode continuar atendendo análises de timeline; o novo coordenador transforma `AtivoDeVideo` em uma entrada compatível com os adapters, sem fazer o domínio da biblioteca depender de `Timeline`.
+
+O `DescobertaDeArquivos` existente contém parte da política de validação local e pode fornecer um adapter compartilhado, desde que sua interface não passe a conhecer biblioteca, fila ou persistência. A análise visual local já produz evidências temporais adequadas, mas a etapa de consolidação deve ficar fora do detector de cenas para manter a separação entre observação e indexação.
+
+As limitações documentadas em `advanced-feature-support.ts` permanecem válidas: o sistema não deve alegar acesso ao índice nativo do Premiere nem iniciar/monitorar operações não expostas por API pública. A biblioteca local é uma capacidade independente.
+
+## Testabilidade e seams
+
+O domínio deve ser testável sem FFmpeg, GPU, macOS Vision, rede ou arquivos reais. Adapters necessários para testes:
+
+- filesystem que retorna snapshots controlados;
+- calculador de fingerprint determinístico;
+- provider de metadados falso;
+- extrator de frames falso;
+- providers visual e de transcrição falsos;
+- relógio injetável;
+- repositório em memória;
+- fila síncrona ou executor controlado;
+- ranking lexical/vetorial falso.
+
+Testes prioritários:
+
+- arquivo inalterado não gera trabalho novamente;
+- novo arquivo gera apenas as etapas necessárias;
+- arquivo alterado invalida resultados derivados;
+- mudança de versão de provider invalida somente sua etapa;
+- falha visual não apaga transcrição;
+- timestamps nunca ultrapassam a duração conhecida;
+- resultados de busca preservam evidências e intervalo;
+- retomada continua do último trabalho persistido;
+- cancelamento não deixa etapa marcada como concluída;
+- arquivo removido vira ausente sem perder histórico.
+
+## Decisões em aberto
+
+Antes da implementação, ainda precisamos escolher:
+
+1. banco local definitivo: SQLite puro, SQLite com FTS5 e embeddings em arquivo, ou outro adapter;
+2. executor em segundo plano: processo Python separado, thread controlada ou integração com o host;
+3. provider visual padrão do MVP: OpenCV, Apple Vision ou configuração por perfil;
+4. provider de transcrição padrão e política de privacidade para enviar áudio a serviços externos;
+5. extensões, exclusões e limite de profundidade da pasta;
+6. política de retenção do cache de frames e áudio;
+7. formato do contrato de descrição semântica produzido pelo modelo de IA.
+
+Essas escolhas não devem alterar a interface de `BibliotecaDeVideos`; devem apenas selecionar adapters e configuração.
+
+## Catálogo SQLite e processamento contínuo
+
+### SQLite como catálogo, não como depósito de mídia
+
+SQLite é adequado para o catálogo local porque oferece transações, consultas relacionais, FTS5 para busca textual e baixo custo operacional. O banco não deve armazenar vídeos, áudio extraído ou imagens em escala. Esses dados ficam na pasta original ou em um cache controlado; no banco ficam referências, metadados, resultados compactos e estado de processamento.
+
+O arquivo do catálogo deve ficar fora da pasta indexada, em um diretório de dados do aplicativo. Assim, uma pasta pode ser removida, movida ou compartilhada sem levar o estado interno do agente junto com ela. A configuração deve permitir uma biblioteca por pasta e várias bibliotecas no mesmo catálogo.
+
+Para lidar com nomes repetidos, a chave do ativo não será `nome`. O cadastro deve usar um identificador interno e único, por exemplo:
+
+```text
+identificador_do_ativo = UUID interno
+identidade_do_conteudo = hash do conteúdo + tamanho
+localizacao = biblioteca + caminho relativo normalizado
+```
+
+O caminho relativo distingue duas cópias iguais em locais diferentes; a identidade do conteúdo permite reconhecer um arquivo renomeado ou movido dentro da mesma biblioteca. O cálculo deve ser progressivo: primeiro comparar tamanho e data de modificação, depois calcular uma impressão digital parcial; o hash completo fica reservado para arquivos suspeitos, duplicatas ou confirmação de identidade.
+
+### Estado persistido por etapa
+
+O catálogo deve tratar a análise como um conjunto de trabalhos independentes, não como uma operação monolítica por vídeo. Cada trabalho tem `ativo_id`, `etapa`, `versao_da_etapa`, `status`, `progresso`, `checkpoint`, `tentativas`, `erro` e timestamps.
+
+Checkpoints possíveis:
+
+- último timestamp de frame processado;
+- último intervalo de áudio transcrito;
+- identificador da janela de cena atual;
+- lote de evidências já persistido;
+- versão do modelo e parâmetros efetivos.
+
+Um checkpoint só é confirmado junto com os resultados daquele lote. Em caso de interrupção, o executor retoma a partir do último checkpoint confirmado, com uma pequena sobreposição temporal para não perder eventos na transição entre lotes. A escrita deve ser idempotente usando uma chave lógica como `ativo + etapa + versão + intervalo + provider`.
+
+### Worker de baixa prioridade
+
+O processamento contínuo deve ser implementado como um worker controlado pelo sistema, com uma iteração curta e cooperativa. Ele não deve manter vídeos, frames ou áudios inteiros em memória.
+
+Política inicial:
+
+- um ativo por vez por padrão;
+- um lote pequeno de frames por vez;
+- áudio extraído em arquivo temporário ou stream, nunca inteiro em memória;
+- transação SQLite curta por lote;
+- pausa entre lotes quando a máquina estiver ocupada;
+- suspensão com bateria, modo de economia de energia, temperatura alta ou pressão de memória;
+- limite configurável de CPU, memória, espaço de cache e tempo por ciclo;
+- cancelamento cooperativo verificado entre frames, lotes e etapas;
+- processos de provider isolados quando uma biblioteca nativa puder bloquear ou consumir memória excessiva.
+
+O worker deve consultar uma política de recursos antes de iniciar cada lote:
+
+```python
+class PoliticaDeRecursos(Protocol):
+ def pode_executar(self, trabalho: TrabalhoDeIndexacao) -> bool: ...
+ def tamanho_do_lote(self, trabalho: TrabalhoDeIndexacao) -> int: ...
+ def deve_pausar(self) -> bool: ...
+```
+
+O objetivo não é manter o agente analisando a qualquer custo, mas aproveitar períodos ociosos. Se a máquina estiver ocupada, o worker deve dormir e deixar o sistema responsivo. A prioridade do processo e a afinidade de CPU são detalhes de um adapter do host, não regras espalhadas pelo domínio.
+
+### Varredura contínua
+
+O modo contínuo deve combinar duas estratégias:
+
+1. uma varredura periódica e lenta da pasta, que é a fonte de verdade;
+2. um watcher opcional para antecipar a descoberta de arquivos novos ou alterados.
+
+O watcher não deve iniciar análise diretamente. Ele apenas marca a biblioteca como “precisa reconciliar”; a próxima varredura confirma o estado, evitando arquivos ainda sendo copiados. Um arquivo só entra na fila depois de permanecer estável por um intervalo configurável e passar por uma leitura mínima de metadados.
+
+Ao iniciar, retomar ou acordar após ocioso, o worker deve:
+
+```text
+reconciliar pasta → atualizar origem dos ativos → recalcular trabalhos necessários
+→ escolher próximo trabalho → processar lote → persistir checkpoint → repetir
+```
+
+### Frequência de frames
+
+Não existe uma frequência única ideal. Analisar todos os frames é caro e, para busca semântica, geralmente redundante. A recomendação para o MVP é uma amostragem em camadas:
+
+| Camada | Frequência inicial | Finalidade |
+|---|---:|---|
+| triagem | 1 frame a cada 2–5 s | descobrir duração, atividade e mudanças grosseiras |
+| cena ativa | 1–2 frames/s | descrever objetos, pessoas e composição |
+| transição | frequência temporariamente maior | refinar início/fim de uma mudança de cena |
+| áudio | janelas contínuas | transcrição e detecção de fala/silêncio |
+
+O valor de `1 frame/s` é um bom ponto de partida para vídeos comuns, mas deve ser uma configuração, não uma regra fixa. Conteúdo com movimento rápido, cortes frequentes, texto na tela ou ações curtas precisa de amostragem adaptativa. Conteúdo estático pode reduzir a frequência quando os frames consecutivos têm baixa diferença visual.
+
+O detector deve aumentar a frequência localmente quando detectar mudança de cena, movimento relevante, fala, texto novo ou baixa confiança. Deve reduzir a frequência quando houver continuidade visual, silêncio prolongado ou repetição de frames. Cada evidência precisa registrar a amostragem usada, para que a ausência de detecção não seja interpretada como prova de ausência.
+
+### Transcrição
+
+O adapter local baseado em `faster-whisper` é a escolha padrão recomendada para o catálogo: mantém o processamento privado, entrega timestamps e já se encaixa nos providers existentes. O tamanho do modelo deve ser configurável por perfil de recurso:
+
+- perfil econômico: modelo menor, CPU e baixa prioridade;
+- perfil equilibrado: modelo intermediário, possivelmente aceleração disponível;
+- perfil qualidade: modelo maior, somente quando solicitado ou quando houver tempo ocioso.
+
+Apple Speech pode ser um adapter preferencial em macOS quando o requisito for baixo consumo e o idioma suportado for suficiente. Groq ou outro provider remoto deve ser opt-in, com consentimento explícito, indicação de que o áudio sai da máquina e registro do provider usado. A transcrição persistida deve guardar idioma, modelo, provider, confiança e timestamps; trocar o modelo invalida somente a etapa de transcrição, não os metadados nem necessariamente a análise visual.
+
+### Combinação de fontes
+
+O cadastro útil para o agente deve manter as fontes separadas e criar uma representação consolidada:
+
+```text
+metadados técnicos
+ + transcrição temporal
+ + evidências visuais temporais
+ + evidências de áudio
+ + mudanças de cena
+ ↓
+ segmento de conteúdo
+ ↓
+ texto indexável + filtros + evidências
+```
+
+Um segmento pode ter o resumo “pessoa entra em uma sala enquanto fala”, mas deve apontar para: o intervalo temporal, os frames que sustentam “pessoa” e “entra”, e o trecho de transcrição que sustenta “fala”. O resumo serve para recuperação; as evidências servem para auditoria, ranking e revisão humana.
+
+### Estratégia de prioridade
+
+A prioridade deve ser calculada no momento de escolher o próximo trabalho, sem alterar a identidade nem o resultado do ativo. Uma pontuação inicial pode considerar:
+
+```text
+prioridade = intenção explícita
+ + ativo aberto ou usado recentemente no editor
+ + arquivo novo ou alterado
+ + etapa necessária para uma busca pendente
+ + tamanho/tempo estimado que permita concluir um lote
+ - custo estimado
+ - idade da última tentativa com falha
+```
+
+Categorias práticas:
+
+1. urgente: ativo solicitado numa busca ou aberto para edição;
+2. alta: arquivo novo, alterado ou associado ao projeto atual;
+3. normal: arquivos ainda não indexados;
+4. baixa: reprocessamento por melhoria de modelo ou análise opcional.
+
+Para evitar starvation, trabalhos de baixa prioridade recebem um aumento gradual de prioridade conforme envelhecem. Um arquivo grande não deve bloquear a fila inteira: o scheduler o divide em lotes e alterna com trabalhos menores.
+
+## Configuração operacional proposta
+
+```python
+@dataclass(frozen=True)
+class ConfiguracaoDaBiblioteca:
+ extensoes: tuple[str, ...] = (".mp4", ".mov", ".mxf", ".mkv", ".avi")
+ recursiva: bool = True
+ intervalo_da_varredura_em_segundos: int = 900
+ estabilidade_do_arquivo_em_segundos: int = 30
+ frequencia_de_triagem_em_fps: float = 0.25
+ frequencia_de_cena_em_fps: float = 1.0
+ tamanho_do_lote_de_frames: int = 32
+ concorrencia: int = 1
+ limite_de_cache_em_gb: float = 10.0
+ permitir_provider_remoto: bool = False
+ perfil_de_recursos: str = "economico"
+```
+
+Os defaults favorecem responsividade e privacidade. A configuração não deve expor detalhes de cada biblioteca de visão; deve selecionar perfis e permitir ajustes apenas onde houver evidência de necessidade.
+
+## Fases de evolução
+
+### Fase 1 — catálogo confiável
+
+SQLite, descoberta periódica, identidade por fingerprint, metadados, fila persistida, checkpoints, análise visual em baixa frequência, transcrição local e busca textual temporal.
+
+### Fase 2 — recuperação semântica
+
+Embeddings para segmentos e transcrições, busca híbrida, reranking e consultas por conceitos não presentes literalmente no texto. O embedding deve ser versionado e reconstruível; não deve ser a única representação do conhecimento.
+
+### Fase 3 — integração editorial
+
+Busca a partir do contexto da timeline, abertura no trecho encontrado, marcadores de revisão e propostas de stringout. Qualquer mutação na timeline continua exigindo uma etapa explícita de revisão e autorização.
diff --git a/code/engine/executar_scanner.py b/code/engine/executar_scanner.py
new file mode 100644
index 0000000..b756f26
--- /dev/null
+++ b/code/engine/executar_scanner.py
@@ -0,0 +1,136 @@
+"""Entrada local do painel CEP para uma execução configurada do Scanner."""
+
+import argparse
+import json
+import math
+import os
+from pathlib import Path
+import tempfile
+import re
+
+from engine.integracoes.premiere.conversores import ConversorDeTimeline
+from engine.scanner import ConfiguracaoVisualDoScanner, criar_detector_apple_vision, gerar_relatorio_visual
+from engine.scanner.transcricao_da_timeline import TranscricaoDaTimeline
+
+
+MODELO_DIARIZACAO_PADRAO = "pyannote/speaker-diarization-community-1"
+
+
+def nome_seguro(nome: str) -> str:
+ return re.sub(r"[^A-Za-z0-9._-]+", "-", nome.strip()).strip(".-") or "timeline"
+
+
+def executar(entrada: Path, saida: Path) -> Path:
+ pedido = json.loads(entrada.read_text(encoding="utf-8"))
+ configuracao = ConfiguracaoVisualDoScanner.de_dict(pedido["perfil"])
+ timeline = ConversorDeTimeline().converter(pedido["timeline"])
+ clipes = [clipe for faixa in timeline.faixas if faixa.tipo == "video"
+ and faixa.indice in configuracao.faixas_de_video for clipe in faixa.clipes]
+ total_estimado = sum(max(1, math.ceil((clipe.intervalo_na_origem or clipe.intervalo_na_timeline).duracao /
+ configuracao.intervalo_em_segundos)) + 1 for clipe in clipes) or 1
+ concluidos = 0
+
+ def emitir(etapa: str, percentual: float, clipe: str = "") -> None:
+ print(json.dumps({"evento": "progresso", "etapa": etapa, "percentual": round(percentual, 1),
+ "clipe": clipe}, ensure_ascii=False), flush=True)
+
+ resultados = []
+ for clipe in clipes:
+ peso = max(1, math.ceil((clipe.intervalo_na_origem or clipe.intervalo_na_timeline).duracao /
+ configuracao.intervalo_em_segundos)) + 1
+ inicio_do_clipe = concluidos
+
+ def progresso_atual(atual: int, total: int, nome: str = clipe.nome,
+ base: int = inicio_do_clipe, peso_do_clipe: int = peso) -> None:
+ emitir("Analisando frames", 100 * (base + peso_do_clipe * atual / max(total, 1)) / total_estimado, nome)
+
+ detector = criar_detector_apple_vision(Path(tempfile.gettempdir()) / "premiere-mcp-scanner",
+ configuracao=configuracao)
+ detector.ao_progresso = progresso_atual
+ emitir("Extraindo frames", 100 * inicio_do_clipe / total_estimado, clipe.nome)
+ try:
+ resultado = detector.detectar(clipe)
+ arquivo = Path(tempfile.gettempdir()) / f"scanner-{clipe.identificador}.json"
+ gerar_relatorio_visual(clipe, resultado, arquivo, configuracao.intervalo_em_segundos)
+ resultados.append(json.loads(arquivo.read_text(encoding="utf-8")))
+ except Exception as erro:
+ resultados.append({"clipe": {"identificador": clipe.identificador, "nome": clipe.nome,
+ "arquivo_original": clipe.arquivo}, "erro": str(erro)})
+ concluidos += peso
+ faixas_de_audio = [faixa for faixa in timeline.faixas
+ if faixa.tipo == "audio" and faixa.indice in configuracao.faixas_de_audio]
+ audio = [{"indice": faixa.indice, "nome": faixa.nome,
+ "clipes": [{"identificador": clipe.identificador, "nome": clipe.nome,
+ "inicio": clipe.intervalo_na_timeline.inicio, "fim": clipe.intervalo_na_timeline.fim}
+ for clipe in faixa.clipes]}
+ for faixa in faixas_de_audio]
+ caso = nome_seguro(timeline.nome)
+ pasta = saida.parent / caso
+ pasta.mkdir(parents=True, exist_ok=True)
+ saida = pasta / f"{caso}-resultado.json"
+ detalhado = pasta / "detalhado"
+ detalhado.mkdir(parents=True, exist_ok=True)
+ geometria = []
+ for resultado in resultados:
+ for frame in resultado.get("observacoes_por_frame", []):
+ fatos = [item for item in frame["evidencias"] if item["tipo"] in {"rosto", "pessoa", "pose", "mao"}]
+ if fatos:
+ geometria.append({"clipe": resultado["clipe"]["identificador"], "timestamp_na_origem": frame["timestamp_na_origem"], "evidencias": fatos})
+ frame["evidencias"] = [item for item in frame["evidencias"] if item not in fatos]
+ (detalhado / "geometria.jsonl").write_text("".join(json.dumps(item, ensure_ascii=False) + "\n" for item in geometria), encoding="utf-8")
+ video_arquivo = f"{caso}-video.json"
+ (pasta / video_arquivo).write_text(json.dumps({"clipes": resultados}, ensure_ascii=False, indent=2), encoding="utf-8")
+ audio_arquivos = []
+ transcricoes_por_clipe: dict[str, list[dict]] = {}
+ transcricao_configurada = pedido["perfil"].get("transcricao", {})
+ if faixas_de_audio and transcricao_configurada.get("caminho_modelo"):
+ try:
+ from engine.integracoes.whisper import ProviderDeTranscricaoLocal
+ from engine.dominio import Timeline
+ emitir("Transcrevendo áudio", 5)
+ provider = ProviderDeTranscricaoLocal(transcricao_configurada["caminho_modelo"],
+ idioma=transcricao_configurada.get("idioma", "pt"))
+ diarizador = None
+ if transcricao_configurada.get("diarizacao"):
+ if not os.environ.get("HF_TOKEN"):
+ transcricao_configurada = {**transcricao_configurada,
+ "aviso_diarizacao": "Token do Hugging Face não configurado."}
+ else:
+ from engine.integracoes.huggingface import ProviderDeDiarizacaoHuggingFace
+ modelo_diarizacao = os.environ.get("HF_DIARIZATION_MODEL", MODELO_DIARIZACAO_PADRAO)
+ diarizador = ProviderDeDiarizacaoHuggingFace(modelo_diarizacao)
+ transcricao_configurada = {**transcricao_configurada,
+ "modelo_diarizacao": modelo_diarizacao}
+ transcricoes = TranscricaoDaTimeline(provider, diretoria_de_trabalho=pasta / ".cache-audio",
+ diarizador=diarizador).executar(
+ Timeline(timeline.identificador, timeline.nome, faixas=faixas_de_audio))
+ for transcricao in transcricoes:
+ transcricoes_por_clipe[transcricao.identificador_do_clipe] = [
+ {"inicio": item.inicio, "fim": item.fim, "texto": item.texto,
+ "confianca": item.confianca, "falante": item.falante}
+ for item in transcricao.segmentos]
+ except Exception as erro:
+ transcricao_configurada = {**transcricao_configurada, "erro": str(erro)}
+ for faixa in audio:
+ arquivo = f"{caso}-audio-faixa-{faixa['indice'] + 1}.json"
+ segmentos = [{"clipe": clipe["identificador"], "segmentos": transcricoes_por_clipe.get(clipe["identificador"], [])}
+ for clipe in faixa["clipes"]]
+ (pasta / arquivo).write_text(json.dumps({"faixa": faixa, "transcricao": transcricao_configurada,
+ "segmentos_por_clipe": segmentos,
+ "status": "concluida" if transcricoes_por_clipe else "indisponivel"},
+ ensure_ascii=False, indent=2), encoding="utf-8")
+ audio_arquivos.append(arquivo)
+ saida.write_text(json.dumps({"versao": 1, "perfil": pedido["perfil"],
+ "sequencia": {"id": timeline.identificador, "nome": timeline.nome},
+ "artefatos": {"audio": audio_arquivos, "video": video_arquivo,
+ "geometria": "detalhado/geometria.jsonl"}}, ensure_ascii=False, indent=2), encoding="utf-8")
+ emitir("Relatório JSON gerado", 100)
+ return saida
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("entrada", type=Path)
+ parser.add_argument("saida", type=Path)
+ args = parser.parse_args()
+ print(json.dumps({"evento": "concluido", "arquivo": str(executar(args.entrada, args.saida))}, ensure_ascii=False))
diff --git a/code/engine/gerar_relatorio_timeline.py b/code/engine/gerar_relatorio_timeline.py
index d14d6a0..30bafb8 100644
--- a/code/engine/gerar_relatorio_timeline.py
+++ b/code/engine/gerar_relatorio_timeline.py
@@ -1,7 +1,9 @@
import json
from pathlib import Path
from engine.integracoes.midia import ExtracaoDeAudio
-from engine.integracoes.groq import ProviderDeTranscricaoGroq
+from engine.integracoes.whisper import ProviderDeTranscricaoLocal
+from engine.integracoes.huggingface import (ProviderDeDiarizacaoHuggingFace,
+ ProviderDeEmocaoHuggingFace)
from engine.integracoes.premiere.cliente_mcp import ClienteMCPPorStdio
from engine.integracoes.premiere.conversores import ConversorDeTimeline
from engine.scanner.transcricao_da_timeline import TranscricaoDaTimeline
@@ -10,7 +12,7 @@ ARQUIVO = Path('/Volumes/Merongo/PROJETOS/03 - Mastopexia/0E6A8290.MP4')
CHAVE = json.loads((Path.home()/'.premiere-mcp/config.json').read_text()).get('groqApiKey','')
SAIDA = Path('/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/relatorios')
-cliente = ClienteMCPPorStdio(['node', 'dist/index.js'])
+cliente = ClienteMCPPorStdio(['node', str(Path(__file__).resolve().parents[1] / 'dist/index.js')])
cliente.conectar()
bruta = cliente.chamar('get_active_sequence', {})['structuredContent']['data']
timeline = ConversorDeTimeline().converter(bruta)
@@ -18,8 +20,13 @@ for faixa in timeline.faixas:
for clipe in faixa.clipes:
if clipe.nome == '0E6A8290.MP4': clipe.arquivo = str(ARQUIVO)
-provider = ProviderDeTranscricaoGroq(CHAVE)
-transcricoes = TranscricaoDaTimeline(provider, ExtracaoDeAudio(duracao_da_parte=600), SAIDA/'audio').executar(timeline)
+MODELO = Path('/Volumes/Merongo/SISTEMAS/MODELOSIA/hf-cache/hub/models--mobiuslabsgmbh--faster-whisper-large-v3-turbo/snapshots/0a363e9161cbc7ed1431c9597a8ceaf0c4f78fcf')
+provider = ProviderDeTranscricaoLocal(str(MODELO), idioma='pt')
+MODELO_EMOCAO = Path('/Volumes/Merongo/SISTEMAS/MODELOSIA/models/wav2vec2-xls-r-300m-pt-br-spontaneous-speech-emotion-recognition')
+analisador_de_emocao = ProviderDeEmocaoHuggingFace(str(MODELO_EMOCAO))
+MODELO_DIARIZACAO = Path('/Volumes/Merongo/SISTEMAS/MODELOSIA/models/pyannote-speaker-diarization-community-1')
+diarizador = ProviderDeDiarizacaoHuggingFace(str(MODELO_DIARIZACAO))
+transcricoes = TranscricaoDaTimeline(provider, ExtracaoDeAudio(duracao_da_parte=600), SAIDA/'audio', analisador_de_emocao, diarizador).executar(timeline)
linhas = [f'# Relatório de transcrição — {timeline.nome}', '', f'Duração: {bruta["end"]:.3f} s',
f'Clipes processados: {sum(len(f.clipes) for f in timeline.faixas if f.tipo == "video")}', '']
@@ -32,5 +39,17 @@ for faixa in timeline.faixas:
texto = resultado.texto if resultado else ''
linhas += [f'## {clipe.identificador} — {clipe.intervalo_na_timeline.inicio:.3f}s–{clipe.intervalo_na_timeline.fim:.3f}s',
f'Origem: {intervalo.inicio:.3f}s–{intervalo.fim:.3f}s', '', texto or '_Sem fala detectada._', '']
+ if resultado and resultado.segmentos:
+ linhas.append('Emoções detectadas por segmento:')
+ for segmento in resultado.segmentos:
+ if segmento.emocao:
+ voz = f', voz {segmento.voz_aparente}' if segmento.voz_aparente else ''
+ linhas.append(f'- [{segmento.inicio:.3f}s–{segmento.fim:.3f}s] {segmento.falante or "falante_indefinido"}: {segmento.emocao} ({segmento.confianca_emocao:.2f}{voz})')
+ linhas.append('')
+ for segmento in resultado.segmentos if resultado else []:
+ for palavra in segmento.palavras:
+ linhas.append(f'- [{palavra.inicio:.3f}s–{palavra.fim:.3f}s] {palavra.texto}')
+ if resultado and resultado.segmentos:
+ linhas.append('')
(SAIDA/'transcricao-timeline.md').write_text('\n'.join(linhas), encoding='utf-8')
print(SAIDA/'transcricao-timeline.md')
diff --git a/code/engine/integracoes/apple_speech/apple_speech_transcriber.swift b/code/engine/integracoes/apple_speech/apple_speech_transcriber.swift
index 5b5d3a3..28289f2 100644
--- a/code/engine/integracoes/apple_speech/apple_speech_transcriber.swift
+++ b/code/engine/integracoes/apple_speech/apple_speech_transcriber.swift
@@ -1,44 +1,82 @@
-import AppKit
import Foundation
import Speech
-final class AppDelegate: NSObject, NSApplicationDelegate {
- func applicationDidFinishLaunching(_ notification: Notification) {
- guard CommandLine.arguments.count >= 3 else { finalizar("uso: arquivo locale [somente_no_dispositivo]", 2); return }
- let url = URL(fileURLWithPath: CommandLine.arguments[1])
- let locale = Locale(identifier: CommandLine.arguments[2])
- let somenteNoDispositivo = CommandLine.arguments.count > 3 && CommandLine.arguments[3] == "true"
- guard let recognizer = SFSpeechRecognizer(locale: locale), recognizer.isAvailable else {
- finalizar("Apple Speech indisponível para o locale solicitado", 3); return
+/// Encapsula o ciclo de vida do Apple Speech para a Engine.
+///
+/// A classe não conhece o scanner, o catálogo ou a persistência. Sua única
+/// responsabilidade é converter um arquivo de mídia em JSON temporal. O
+/// processo é Foundation-only para poder ser executado como CLI sem iniciar
+/// AppKit ou NSApplication.
+final class TranscritorDeFalaApple {
+ private let semaforo = DispatchSemaphore(value: 0)
+ private var payload: [String: Any] = ["segmentos": []]
+ private var codigoDeSaida: Int32 = 0
+ private var finalizado = false
+
+ func executar(argumentos: [String]) -> Int32 {
+ guard argumentos.count >= 3 else {
+ return finalizar("uso: apple-speech-transcriber arquivo locale [somente_no_dispositivo]", 2)
}
- SFSpeechRecognizer.requestAuthorization { status in
- guard status == .authorized else { self.finalizar("Permissão do Apple Speech não concedida", 4); return }
- let request = SFSpeechURLRecognitionRequest(url: url)
+
+ let arquivo = URL(fileURLWithPath: argumentos[1])
+ let locale = Locale(identifier: argumentos[2])
+ let somenteNoDispositivo = argumentos.count > 3 && argumentos[3] == "true"
+
+ guard let recognizer = SFSpeechRecognizer(locale: locale), recognizer.isAvailable else {
+ return finalizar("Apple Speech indisponível para o locale solicitado", 3)
+ }
+
+ SFSpeechRecognizer.requestAuthorization { [weak self] status in
+ guard let self else { return }
+ guard status == .authorized else {
+ self.finalizar("Permissão do Apple Speech não concedida", 4)
+ return
+ }
+
+ let request = SFSpeechURLRecognitionRequest(url: arquivo)
request.shouldReportPartialResults = false
- if somenteNoDispositivo && recognizer.supportsOnDeviceRecognition { request.requiresOnDeviceRecognition = true }
- recognizer.recognitionTask(with: request) { result, error in
- if let result = result, result.isFinal {
- let segmentos = result.bestTranscription.segments.map {
+ if somenteNoDispositivo && recognizer.supportsOnDeviceRecognition {
+ request.requiresOnDeviceRecognition = true
+ }
+ recognizer.recognitionTask(with: request) { [weak self] resultado, erro in
+ guard let self else { return }
+ if let resultado, resultado.isFinal {
+ self.payload["segmentos"] = resultado.bestTranscription.segments.map {
["inicio": $0.timestamp, "fim": $0.timestamp + $0.duration,
"texto": $0.substring, "confianca": $0.confidence] as [String: Any]
}
- let data = try! JSONSerialization.data(withJSONObject: ["segmentos": segmentos])
- print(String(data: data, encoding: .utf8)!)
+ self.finalizar(nil, 0)
+ } else if let erro {
+ self.finalizar("Falha no Apple Speech: \(erro.localizedDescription)", 5)
}
- if error != nil || result?.isFinal == true { self.finalizar(nil, 0) }
}
}
+
+ semaforo.wait()
+ imprimirResultado()
+ return codigoDeSaida
}
- private func finalizar(_ mensagem: String?, _ codigo: Int32) {
+ @discardableResult
+ private func finalizar(_ mensagem: String?, _ codigo: Int32) -> Int32 {
+ guard !finalizado else { return codigoDeSaida }
+ finalizado = true
if let mensagem { fputs(mensagem + "\n", stderr) }
- NSApplication.shared.terminate(nil)
- exit(codigo)
+ codigoDeSaida = codigo
+ semaforo.signal()
+ return codigo
+ }
+
+ private func imprimirResultado() {
+ guard let dados = try? JSONSerialization.data(withJSONObject: payload),
+ let texto = String(data: dados, encoding: .utf8) else {
+ fputs("Não foi possível serializar o resultado do Apple Speech\n", stderr)
+ codigoDeSaida = 6
+ return
+ }
+ print(texto)
}
}
-let app = NSApplication.shared
-let delegate = AppDelegate()
-app.delegate = delegate
-app.setActivationPolicy(.accessory)
-app.run()
+let transcritor = TranscritorDeFalaApple()
+exit(transcritor.executar(argumentos: CommandLine.arguments))
diff --git a/code/engine/integracoes/apple_speech/provider_de_transcricao_apple.py b/code/engine/integracoes/apple_speech/provider_de_transcricao_apple.py
index 95374f2..220a50f 100644
--- a/code/engine/integracoes/apple_speech/provider_de_transcricao_apple.py
+++ b/code/engine/integracoes/apple_speech/provider_de_transcricao_apple.py
@@ -7,10 +7,10 @@ from ...scanner.modelos import SegmentoDeTranscricao
class ProviderDeTranscricaoApple:
- """Provider nativo macOS Speech; o áudio não passa por API Groq."""
+ """Provider nativo macOS Speech, local por padrão e sem AppKit."""
def __init__(self, executavel: str = "/private/tmp/AppleSpeechTranscriber.app/Contents/MacOS/apple-speech-transcriber", locale: str = "pt-BR",
- somente_no_dispositivo: bool = False) -> None:
+ somente_no_dispositivo: bool = True) -> None:
self.executavel = executavel
self.locale = locale
self.somente_no_dispositivo = somente_no_dispositivo
@@ -24,5 +24,5 @@ class ProviderDeTranscricaoApple:
check=True, capture_output=True, text=True,
)
dados = json.loads(resultado.stdout)
- return [SegmentoDeTranscricao(float(s["inicio"]), float(s["fim"]), str(s["texto"]), s.get("confianca"))
+ return [SegmentoDeTranscricao(float(s["inicio"]), float(s["fim"]), str(s["texto"]).strip(), s.get("confianca"))
for s in dados.get("segmentos", [])]
diff --git a/code/engine/integracoes/huggingface/__init__.py b/code/engine/integracoes/huggingface/__init__.py
new file mode 100644
index 0000000..bf51217
--- /dev/null
+++ b/code/engine/integracoes/huggingface/__init__.py
@@ -0,0 +1,4 @@
+from .provider_de_emocao_local import ProviderDeEmocaoHuggingFace
+from .provider_de_diarizacao_local import ProviderDeDiarizacaoHuggingFace
+
+__all__ = ["ProviderDeDiarizacaoHuggingFace", "ProviderDeEmocaoHuggingFace"]
diff --git a/code/engine/integracoes/huggingface/provider_de_diarizacao_local.py b/code/engine/integracoes/huggingface/provider_de_diarizacao_local.py
new file mode 100644
index 0000000..4e66e0e
--- /dev/null
+++ b/code/engine/integracoes/huggingface/provider_de_diarizacao_local.py
@@ -0,0 +1,21 @@
+from pathlib import Path
+
+
+class ProviderDeDiarizacaoHuggingFace:
+ """Identifica intervalos de falas por participante em áudio local."""
+
+ def __init__(self, modelo: str) -> None:
+ from pyannote.audio import Pipeline
+ self.pipeline = Pipeline.from_pretrained(modelo)
+
+ def analisar(self, arquivo: str | Path) -> list[tuple[float, float, str]]:
+ import soundfile as sf
+ import torch
+ audio, taxa = sf.read(str(arquivo), dtype="float32")
+ if getattr(audio, "ndim", 1) > 1:
+ audio = audio.mean(axis=1)
+ saida = self.pipeline({"waveform": torch.from_numpy(audio).unsqueeze(0),
+ "sample_rate": taxa})
+ diarizacao = getattr(saida, "exclusive_speaker_diarization", saida)
+ return [(float(turno.start), float(turno.end), str(falante))
+ for turno, _, falante in diarizacao.itertracks(yield_label=True)]
diff --git a/code/engine/integracoes/huggingface/provider_de_emocao_local.py b/code/engine/integracoes/huggingface/provider_de_emocao_local.py
new file mode 100644
index 0000000..d77b6f2
--- /dev/null
+++ b/code/engine/integracoes/huggingface/provider_de_emocao_local.py
@@ -0,0 +1,35 @@
+from pathlib import Path
+from typing import Any
+
+
+class ProviderDeEmocaoHuggingFace:
+ """Classifica a emoção de uma fala localmente com Transformers."""
+
+ def __init__(self, modelo: str = "superb/wav2vec2-base-superb-er") -> None:
+ from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
+ self.modelo = modelo
+ self.processador = AutoFeatureExtractor.from_pretrained(modelo, local_files_only=True)
+ self.classificador = AutoModelForAudioClassification.from_pretrained(
+ modelo, local_files_only=True
+ )
+ self.classificador.eval()
+
+ def analisar(self, arquivo: str | Path, inicio: float, fim: float) -> dict[str, Any]:
+ import soundfile as sf
+ import torch
+ audio, taxa = sf.read(str(arquivo), start=max(0, int(inicio * 16000)),
+ stop=max(0, int(fim * 16000)), dtype="float32")
+ if getattr(audio, "ndim", 1) > 1:
+ audio = audio.mean(axis=1)
+ entradas = self.processador(audio, sampling_rate=taxa, return_tensors="pt")
+ with torch.no_grad():
+ logits = self.classificador(**entradas).logits
+ probabilidades = torch.softmax(logits[0], dim=-1)
+ indice = int(torch.argmax(probabilidades))
+ rotulo = self.classificador.config.id2label[indice]
+ voz_aparente = {"non-neutral-female": "feminina",
+ "non-neutral-male": "masculina"}.get(rotulo)
+ return {"emocao": self.classificador.config.id2label[indice],
+ "confianca": float(probabilidades[indice]),
+ "voz_aparente": voz_aparente,
+ "confianca_voz": float(probabilidades[indice]) if voz_aparente else None}
diff --git a/code/engine/integracoes/midia/__init__.py b/code/engine/integracoes/midia/__init__.py
index 8ee4b8e..1be4bf4 100644
--- a/code/engine/integracoes/midia/__init__.py
+++ b/code/engine/integracoes/midia/__init__.py
@@ -1,3 +1,4 @@
from .extracao_de_audio import ExtracaoDeAudio, ParteDeAudio
+from .extracao_de_metadados import ExtracaoDeMetadados, MetadadosDoArquivo
-__all__ = ["ExtracaoDeAudio", "ParteDeAudio"]
+__all__ = ["ExtracaoDeAudio", "ExtracaoDeMetadados", "MetadadosDoArquivo", "ParteDeAudio"]
diff --git a/code/engine/integracoes/midia/extracao_de_metadados.py b/code/engine/integracoes/midia/extracao_de_metadados.py
new file mode 100644
index 0000000..2e559a8
--- /dev/null
+++ b/code/engine/integracoes/midia/extracao_de_metadados.py
@@ -0,0 +1,106 @@
+import json
+import subprocess
+from dataclasses import dataclass
+from fractions import Fraction
+from pathlib import Path
+from typing import Any
+
+from ...scanner.modelos import ErroDeAnalise
+
+
+@dataclass(frozen=True)
+class MetadadosDoArquivo:
+ caminho: Path
+ formato: str | None = None
+ duracao: float | None = None
+ codec_de_video: str | None = None
+ codec_de_audio: str | None = None
+ largura: int | None = None
+ altura: int | None = None
+ taxa_de_quadros: float | None = None
+ canais_de_audio: int | None = None
+ taxa_de_amostragem: int | None = None
+ tamanho_em_bytes: int | None = None
+ orientacao: str | None = None
+ timecode: str | None = None
+
+
+class ExtracaoDeMetadados:
+ """Extrai metadados técnicos via ffprobe, com cache por caminho normalizado."""
+
+ def __init__(self, ffprobe: str = "ffprobe") -> None:
+ self.ffprobe = ffprobe
+ self._cache: dict[Path, MetadadosDoArquivo] = {}
+
+ def extrair(self, arquivo: str | Path) -> MetadadosDoArquivo:
+ caminho = Path(arquivo).expanduser().resolve(strict=False)
+ if not caminho.is_file():
+ raise ErroDeAnalise("arquivo_inacessivel", "Arquivo de mídia não encontrado.", str(caminho))
+ if caminho in self._cache:
+ return self._cache[caminho]
+ try:
+ processo = subprocess.run(
+ [self.ffprobe, "-v", "error", "-print_format", "json", "-show_format", "-show_streams", str(caminho)],
+ check=True, capture_output=True, text=True,
+ )
+ dados = json.loads(processo.stdout)
+ except (OSError, subprocess.SubprocessError, json.JSONDecodeError) as exc:
+ raise ErroDeAnalise("metadados_indisponiveis", f"Não foi possível ler os metadados: {exc}", str(caminho)) from exc
+
+ metadados = self._converter(caminho, dados)
+ self._cache[caminho] = metadados
+ return metadados
+
+ def limpar_cache(self) -> None:
+ self._cache.clear()
+
+ @classmethod
+ def _converter(cls, caminho: Path, dados: dict[str, Any]) -> MetadadosDoArquivo:
+ formato = dados.get("format") or {}
+ streams = dados.get("streams") or []
+ video = next((item for item in streams if item.get("codec_type") == "video"), {})
+ audio = next((item for item in streams if item.get("codec_type") == "audio"), {})
+ duracao = cls._float(formato.get("duration"))
+ if duracao is None:
+ duracao = cls._float(video.get("duration") or audio.get("duration"))
+ return MetadadosDoArquivo(
+ caminho=caminho,
+ formato=cls._format_name(formato.get("format_name")),
+ duracao=duracao,
+ codec_de_video=video.get("codec_name") or None,
+ codec_de_audio=audio.get("codec_name") or None,
+ largura=cls._int(video.get("width")), altura=cls._int(video.get("height")),
+ taxa_de_quadros=cls._fps(video.get("avg_frame_rate") or video.get("r_frame_rate")),
+ canais_de_audio=cls._int(audio.get("channels")),
+ taxa_de_amostragem=cls._int(audio.get("sample_rate")),
+ tamanho_em_bytes=cls._int(formato.get("size")) or caminho.stat().st_size,
+ orientacao=video.get("side_data_list", [{}])[0].get("rotation") if video.get("side_data_list") else None,
+ timecode=(formato.get("tags") or {}).get("timecode") or (video.get("tags") or {}).get("timecode"),
+ )
+
+ @staticmethod
+ def _float(valor: Any) -> float | None:
+ try:
+ return None if valor in (None, "", "N/A") else float(valor)
+ except (TypeError, ValueError):
+ return None
+
+ @staticmethod
+ def _int(valor: Any) -> int | None:
+ try:
+ return None if valor in (None, "", "N/A") else int(valor)
+ except (TypeError, ValueError):
+ return None
+
+ @staticmethod
+ def _fps(valor: Any) -> float | None:
+ if valor in (None, "", "0/0", "N/A"):
+ return None
+ try:
+ return float(Fraction(str(valor)))
+ except (ValueError, ZeroDivisionError):
+ return None
+
+ @staticmethod
+ def _format_name(valor: Any) -> str | None:
+ return str(valor).split(",", 1)[0] if valor else None
diff --git a/code/engine/integracoes/visual/README.md b/code/engine/integracoes/visual/README.md
new file mode 100644
index 0000000..7968de5
--- /dev/null
+++ b/code/engine/integracoes/visual/README.md
@@ -0,0 +1,161 @@
+# Análise visual local
+
+## Configuração no painel
+
+A configuração do Scanner é apresentada na aba **Scanner** do painel CEP em
+`code/cep-plugin/`. O perfil salvo segue o contrato versionado de
+`ConfiguracaoVisualDoScanner`; ele define amostragem, faixas, recursos Vision,
+cenas, continuidade, reenquadramento e interpretação antes de montar os
+adapters.
+
+## Acesso à timeline do Premiere
+
+Quando a análise precisar ler a timeline, a sequência ativa, as faixas ou os
+clipes do Premiere, use sempre a classe existente
+`engine.integracoes.premiere.leitura.AcessoAoEditor`. O módulo visual não deve
+criar chamadas MCP, abrir processos do Premiere ou acessar o bridge diretamente.
+
+O acesso deve seguir esta composição:
+
+```python
+from engine.integracoes.premiere.leitura import AcessoAoEditor, AcessoATimeline
+from engine.scanner import DescobertaDaTimeline
+from engine.integracoes.premiere.conversores import ConversorDeTimeline
+
+acesso_ao_editor = AcessoAoEditor(AcessoATimeline(cliente_mcp))
+descoberta = DescobertaDaTimeline(acesso_ao_editor, ConversorDeTimeline())
+```
+
+Depois, a timeline convertida entra no `ContextoDeAnalise` e o Scanner executa
+`AnaliseVisualDaTimeline`. A classe de acesso isola o MCP e preserva a separação
+entre a integração com o Premiere e os analyzers locais.
+
+Não duplicar esse acesso em novos adapters ou analyzers. Para testes, injetar um
+fake de `AcessoAoEditor`/`AcessoATimeline` ou usar uma timeline de domínio pronta.
+
+## Captura para análise visual
+
+A análise visual não exporta um frame renderizado pelo Premiere. Para cada clipe
+de vídeo, o fluxo usa os campos retornados pelo MCP em `get_active_sequence`:
+
+1. `sourceFile` identifica o arquivo original do `projectItem`.
+2. `inPoint` e `outPoint` delimitam o trecho usado pelo clipe.
+3. O extrator abre `sourceFile` localmente e amostra o primeiro frame do trecho,
+ usando `inPoint` como tempo inicial na origem.
+4. Vision/OpenCV/ONNX recebem esse frame persistido, mantendo o timestamp de
+ origem; nenhum frame é exportado da timeline do Premiere.
+
+Portanto, `start`/`end` são tempos da timeline e `inPoint`/`outPoint` são tempos
+do arquivo original. Não misturar esses relógios ao analisar um clipe.
+
+O Scanner usa apenas as interfaces em `contratos.py`. As bibliotecas externas
+ficam em adapters concretos, para que possam ser habilitadas, substituídas ou
+testadas isoladamente.
+
+| Papel | Adapter |
+| --- | --- |
+| Extrair frames | `ExtratorDeQuadrosOpenCV` |
+| Medir qualidade | `AnalisadorDeQualidadeOpenCV` |
+| Rosto e olhos visíveis | `AnalisadorDeRostosOpenCV` |
+| Composição e área para legendas | `AnalisadorDeComposicaoOpenCV` |
+| Tremor/movimento de câmera | `AnalisadorDeTremorOpenCV` |
+| Continuidade e frame congelado | `AnalisadorDeContinuidadeOpenCV` |
+| Detectar objetos | `AnalisadorDeObjetosONNX` |
+| Detectar pose e mãos | `AnalisadorDePoseMediaPipe` |
+| OCR e faces macOS | `AnalisadorAppleVision` |
+| Vision + Apple Intelligence (Swift isolado) | `AnalisadorAppleVisionNativo` |
+| Similaridade temporal por feature print | `AnalisadorSequenciaAppleVisionNativo` |
+| Extrair frames para Vision sem OpenCV | `ExtratorDeQuadrosFFmpeg` |
+| Detectar cenas | `DetectorDeCenasPySceneDetect` |
+
+`AnalisadorDeObjetosONNX` exige dois adapters específicos do modelo:
+`preparar(imagem, entradas)` e `decodificar(saidas, quadro)`. Isso mantém os
+detalhes de cada arquivo ONNX fora do Scanner e permite trocar de modelo sem
+alterar a interface do domínio.
+
+`AnalisadorDePoseMediaPipe` recebe caminhos explícitos para os arquivos `.task`
+de pose e mãos. Os modelos ficam em `/Volumes/Merongo/SISTEMAS/MODELOSIA/mediapipe/`
+e não são acoplados ao pacote Python. Ele executa em subprocesso: falhas nativas
+do MediaPipe são contidas e retornam como indisponibilidade do adapter, sem
+encerrar o processo do Scanner.
+
+`AnalisadorDeRostosOpenCV` usa os arquivos locais
+`haarcascade_frontalface_default.xml` e `haarcascade_eye.xml`. Quando a
+distribuição do OpenCV não os incluir, forneça `diretorio_de_modelos` apontando
+para a pasta que os contém.
+
+`AnalisadorAppleVisionNativo` é a opção Apple recomendada. Seu runner Swift
+executa faces/landmarks, qualidade facial, pessoas, pose, mãos, OCR,
+classificação e estética de forma independente. A interpretação editorial pelo
+`FoundationModels` só é criada a partir dessas evidências e é salva em separado.
+Falhas nativas por recurso viram `diagnostico_apple_vision`, sem derrubar o
+Scanner nem descartar os fatos que funcionaram.
+
+Além de rostos, pessoas, pose, mãos, OCR, categorias e estética, o runner
+Apple retorna códigos/QR, horizonte, retângulos, densidade de contornos,
+ocupação da máscara de pessoas e similaridade visual entre frames. Os valores
+são fatos normalizados; descrição, contexto e ação devem ser derivados depois,
+a partir dessas evidências temporizadas.
+
+## Montagem
+
+```python
+from engine.integracoes.visual import (
+ AnalisadorDeQualidadeOpenCV,
+ AnalisadorDeRostosOpenCV,
+ AnalisadorDeComposicaoOpenCV,
+ AnalisadorDeTremorOpenCV,
+ AnalisadorDeContinuidadeOpenCV,
+ DetectorDeCenasPySceneDetect,
+ ExtratorDeQuadrosOpenCV,
+)
+from engine.scanner.visual import DetectorDeCenas
+
+detector = DetectorDeCenas(
+ ExtratorDeQuadrosOpenCV(intervalo_em_segundos=1.0, diretorio_de_cache=".frames"),
+ analisadores_de_frame=[
+ AnalisadorDeQualidadeOpenCV(), AnalisadorDeRostosOpenCV(),
+ AnalisadorDeComposicaoOpenCV(),
+ ],
+ analisadores_de_sequencia=[
+ AnalisadorDeTremorOpenCV(), AnalisadorDeContinuidadeOpenCV(),
+ ],
+ detectores_de_intervalo=[DetectorDeCenasPySceneDetect()],
+)
+```
+
+Depois, passe esse `DetectorDeCenas` para `AnaliseVisualDaTimeline` ao montar o
+`PipelineDoScanner`.
+
+Para o caminho local padrão, a montagem pode ser reduzida a:
+
+```python
+from engine.scanner import AnaliseVisualDaTimeline, PipelineDoScanner, criar_detector_visual_local
+
+pipeline = PipelineDoScanner([AnaliseVisualDaTimeline(criar_detector_visual_local())])
+contexto = pipeline.executar(contexto)
+```
+
+O resultado fica em `contexto.caracteristicas_visuais` (por clipe),
+`contexto.cenas_visuais` (cenas com observações) e `contexto.avisos`.
+Quando OpenCV/FFmpeg ou outro adapter opcional não estiver disponível, o clipe
+é marcado com `disponivel=False` e os demais continuam sendo processados.
+
+## Montagem recomendada para Apple Vision
+
+```python
+from engine.scanner import AnaliseVisualDaTimeline, PipelineDoScanner, criar_detector_apple_vision
+
+pipeline = PipelineDoScanner([
+ AnaliseVisualDaTimeline(criar_detector_apple_vision(intervalo_em_segundos=1.0)),
+])
+```
+
+Esse detector extrai PNGs do arquivo original com FFmpeg, usando o intervalo
+`inPoint`/`outPoint` do clipe, e não exporta imagens renderizadas pelo Premiere.
+Ele também recua a amostra no fim de vídeos muito curtos quando não há um frame
+decodificável exatamente no timestamp solicitado.
+
+Os analyzers de sequência não tentam concluir a intenção de uma pessoa. Eles
+retornam indícios temporais (`possivel_tremor`, `possivel_descontinuidade` e
+`possivel_frame_congelado`) para que a camada editorial decida como tratá-los.
diff --git a/code/engine/integracoes/visual/__init__.py b/code/engine/integracoes/visual/__init__.py
new file mode 100644
index 0000000..574aa00
--- /dev/null
+++ b/code/engine/integracoes/visual/__init__.py
@@ -0,0 +1,20 @@
+from .analisadores import (AnalisadorAppleVision, AnalisadorDeComposicaoOpenCV,
+ AnalisadorDeContinuidadeOpenCV, AnalisadorDeObjetosONNX,
+ AnalisadorDePoseMediaPipe, AnalisadorDeQualidadeOpenCV,
+ AnalisadorDeRostosOpenCV, AnalisadorDeTremorOpenCV)
+from .apple_vision import (AnalisadorAppleVisionNativo, AnalisadorSequenciaAppleVisionNativo,
+ ExecutorDoRunnerApple)
+from .contratos import (AnalisadorDeFrame, AnalisadorDeSequenciaDeQuadros,
+ DetectorDeIntervalosDeCena, ExtratorDeQuadros)
+from .detectores_de_cena import DetectorDeCenasPySceneDetect
+from .extrator_open_cv import ExtratorDeQuadrosOpenCV
+from .extrator_ffmpeg import ExtratorDeQuadrosFFmpeg
+from .modelos import QuadroDeVideo
+
+__all__ = ["AnalisadorAppleVision", "AnalisadorAppleVisionNativo", "AnalisadorSequenciaAppleVisionNativo", "AnalisadorDeComposicaoOpenCV",
+ "AnalisadorDeContinuidadeOpenCV", "AnalisadorDeFrame",
+ "AnalisadorDeObjetosONNX", "AnalisadorDePoseMediaPipe",
+ "AnalisadorDeQualidadeOpenCV", "AnalisadorDeRostosOpenCV",
+ "AnalisadorDeSequenciaDeQuadros", "AnalisadorDeTremorOpenCV",
+ "DetectorDeCenasPySceneDetect", "DetectorDeIntervalosDeCena",
+ "ExecutorDoRunnerApple", "ExtratorDeQuadros", "ExtratorDeQuadrosFFmpeg", "ExtratorDeQuadrosOpenCV", "QuadroDeVideo"]
diff --git a/code/engine/integracoes/visual/analisadores.py b/code/engine/integracoes/visual/analisadores.py
new file mode 100644
index 0000000..c805239
--- /dev/null
+++ b/code/engine/integracoes/visual/analisadores.py
@@ -0,0 +1,331 @@
+import json
+from pathlib import Path
+import subprocess
+import sys
+from typing import Any, Callable
+
+from ...scanner.modelos import EvidenciaVisual
+from .contratos import AnalisadorDeFrame, AnalisadorDeSequenciaDeQuadros
+from .modelos import QuadroDeVideo
+
+
+class AnalisadorDeQualidadeOpenCV(AnalisadorDeFrame):
+ """Mede nitidez, brilho e contraste de cada frame usando OpenCV."""
+
+ nome = "opencv"
+
+ def __init__(self, cv2_module: Any | None = None) -> None:
+ self._cv2 = cv2_module
+
+ @property
+ def cv2(self) -> Any:
+ if self._cv2 is None:
+ try:
+ import cv2
+ except ImportError as exc:
+ raise RuntimeError("OpenCV não está instalado. Instale opencv-python.") from exc
+ self._cv2 = cv2
+ return self._cv2
+
+ def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]:
+ imagem = quadro.imagem
+ cinza = self.cv2.cvtColor(imagem, self.cv2.COLOR_BGR2GRAY) if len(imagem.shape) == 3 else imagem
+ media, desvio = self.cv2.meanStdDev(cinza)
+ nitidez = float(self.cv2.Laplacian(cinza, self.cv2.CV_64F).var())
+ valor = {
+ "largura": quadro.largura,
+ "altura": quadro.altura,
+ "brilho": float(media[0][0]),
+ "contraste": float(desvio[0][0]),
+ "nitidez_laplaciana": nitidez,
+ }
+ return [EvidenciaVisual("qualidade", quadro.timestamp, quadro.timestamp, valor,
+ provider=self.nome)]
+
+
+class _AdapterOpenCV:
+ """Implementação compartilhada para adapters OpenCV, com importação tardia."""
+
+ def __init__(self, cv2_module: Any | None = None) -> None:
+ self._cv2 = cv2_module
+
+ @property
+ def cv2(self) -> Any:
+ if self._cv2 is None:
+ try:
+ import cv2
+ except ImportError as exc:
+ raise RuntimeError("OpenCV não está instalado. Instale opencv-python.") from exc
+ self._cv2 = cv2
+ return self._cv2
+
+
+class AnalisadorDeRostosOpenCV(_AdapterOpenCV, AnalisadorDeFrame):
+ """Encontra rostos e olhos localmente; não infere identidade nem emoção."""
+
+ nome = "opencv_haar"
+
+ def __init__(self, escala: float = 1.1, vizinhos_minimos: int = 5,
+ diretorio_de_modelos: str | Path | None = None,
+ tamanho_minimo_relativo: float = 0.04,
+ cv2_module: Any | None = None) -> None:
+ super().__init__(cv2_module)
+ self.escala = escala
+ self.vizinhos_minimos = vizinhos_minimos
+ self.diretorio_de_modelos = Path(diretorio_de_modelos) if diretorio_de_modelos else None
+ self.tamanho_minimo_relativo = tamanho_minimo_relativo
+ self._detectores: tuple[Any, Any] | None = None
+
+ def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]:
+ detector_de_rostos, detector_de_olhos = self._obter_detectores()
+ cinza = self.cv2.cvtColor(quadro.imagem, self.cv2.COLOR_BGR2GRAY)
+ rostos = detector_de_rostos.detectMultiScale(cinza, scaleFactor=self.escala,
+ minNeighbors=self.vizinhos_minimos)
+ evidencias: list[EvidenciaVisual] = []
+ for x, y, largura, altura in rostos:
+ caixa = _caixa_normalizada(x, y, largura, altura, quadro.largura, quadro.altura)
+ if min(caixa["largura"], caixa["altura"]) < self.tamanho_minimo_relativo:
+ continue
+ rosto = EvidenciaVisual("rosto", quadro.timestamp, quadro.timestamp,
+ {"bounding_box": caixa, "proximo_da_borda": _proximo_da_borda(caixa)}, provider=self.nome)
+ olhos = detector_de_olhos.detectMultiScale(cinza[y:y + altura, x:x + largura],
+ scaleFactor=1.1, minNeighbors=4)
+ evidencias.extend((rosto, EvidenciaVisual("olhos_visiveis", quadro.timestamp, quadro.timestamp,
+ {"quantidade": len(olhos), "rosto": caixa}, provider=self.nome)))
+ return evidencias
+
+ def _obter_detectores(self) -> tuple[Any, Any]:
+ if self._detectores is None:
+ base = self.diretorio_de_modelos or Path(self.cv2.data.haarcascades)
+ rostos = self.cv2.CascadeClassifier(str(base / "haarcascade_frontalface_default.xml"))
+ olhos = self.cv2.CascadeClassifier(str(base / "haarcascade_eye.xml"))
+ if rostos.empty() or olhos.empty():
+ raise RuntimeError(f"Cascades Haar do OpenCV não estão disponíveis em: {base}")
+ self._detectores = rostos, olhos
+ return self._detectores
+
+
+class AnalisadorDeComposicaoOpenCV(_AdapterOpenCV, AnalisadorDeFrame):
+ """Mede orientação, poluição visual e disponibilidade das zonas para legendas."""
+
+ nome = "opencv_composicao"
+
+ def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]:
+ cinza = self.cv2.cvtColor(quadro.imagem, self.cv2.COLOR_BGR2GRAY)
+ bordas = self.cv2.Canny(cinza, 80, 160)
+ densidade_de_bordas = float((bordas > 0).mean())
+ terco_inferior = cinza[int(quadro.altura * 2 / 3):, :]
+ zona_de_legenda_livre = float((self.cv2.Canny(terco_inferior, 80, 160) > 0).mean()) < 0.08
+ valor = {
+ "orientacao": "vertical" if quadro.altura > quadro.largura else "horizontal",
+ "densidade_de_bordas": densidade_de_bordas,
+ "fundo_visual_poluido": densidade_de_bordas > 0.16,
+ "zona_inferior_livre_para_legenda": zona_de_legenda_livre,
+ }
+ return [EvidenciaVisual("composicao", quadro.timestamp, quadro.timestamp, valor, provider=self.nome)]
+
+
+class AnalisadorDeTremorOpenCV(_AdapterOpenCV, AnalisadorDeSequenciaDeQuadros):
+ """Estima deslocamento global entre frames para sinalizar possível tremor de câmera."""
+
+ nome = "opencv_movimento"
+
+ def analisar(self, quadros: list[QuadroDeVideo]) -> list[EvidenciaVisual]:
+ if len(quadros) < 2:
+ return []
+ deslocamentos = [_deslocamento_global(self.cv2, anterior.imagem, atual.imagem)
+ for anterior, atual in zip(quadros, quadros[1:])]
+ deslocamentos = [item for item in deslocamentos if item is not None]
+ if not deslocamentos:
+ return []
+ medio = sum(deslocamentos) / len(deslocamentos)
+ variacao = sum(abs(item - medio) for item in deslocamentos) / len(deslocamentos)
+ inicio, fim = quadros[0].timestamp, quadros[-1].timestamp
+ return [EvidenciaVisual("movimento_de_camera", inicio, fim, {
+ "deslocamento_medio_pixels": medio,
+ "variacao_do_deslocamento": variacao,
+ "possivel_tremor": variacao > 2.0 and medio > 1.0,
+ "amostras": len(deslocamentos),
+ }, provider=self.nome)]
+
+
+class AnalisadorDeContinuidadeOpenCV(_AdapterOpenCV, AnalisadorDeSequenciaDeQuadros):
+ """Compara pares de frames e retorna indícios, não certezas, de descontinuidade ou congelamento."""
+
+ nome = "opencv_continuidade"
+
+ def analisar(self, quadros: list[QuadroDeVideo]) -> list[EvidenciaVisual]:
+ evidencias: list[EvidenciaVisual] = []
+ for anterior, atual in zip(quadros, quadros[1:]):
+ cinza_anterior = self.cv2.cvtColor(anterior.imagem, self.cv2.COLOR_BGR2GRAY)
+ cinza_atual = self.cv2.cvtColor(atual.imagem, self.cv2.COLOR_BGR2GRAY)
+ diferenca = float(self.cv2.absdiff(cinza_anterior, cinza_atual).mean())
+ evidencias.append(EvidenciaVisual("continuidade", anterior.timestamp, atual.timestamp, {
+ "diferenca_media_de_luminancia": diferenca,
+ "possivel_frame_congelado": diferenca < 0.8,
+ "possivel_descontinuidade": diferenca > 38.0,
+ }, provider=self.nome))
+ return evidencias
+
+
+class AnalisadorDeObjetosONNX(AnalisadorDeFrame):
+ """Adapter de modelo ONNX; pré e pós-processamento pertencem ao modelo escolhido."""
+
+ nome = "onnxruntime"
+
+ def __init__(self, modelo: str | Path, preparar: Callable[[Any, list[str]], dict[str, Any]],
+ decodificar: Callable[[list[Any], QuadroDeVideo], list[dict[str, Any]]],
+ usar_coreml: bool = True, ort_module: Any | None = None) -> None:
+ self.modelo = str(modelo)
+ self.preparar = preparar
+ self.decodificar = decodificar
+ self._ort = ort_module
+ ort = self.ort
+ preferidos = ["CoreMLExecutionProvider", "CPUExecutionProvider"] if usar_coreml else ["CPUExecutionProvider"]
+ disponiveis = set(ort.get_available_providers())
+ self.providers = [provider for provider in preferidos if provider in disponiveis]
+ if not self.providers:
+ raise RuntimeError("ONNX Runtime não possui provider compatível neste ambiente.")
+ self.sessao = ort.InferenceSession(self.modelo, providers=self.providers)
+ self.entradas = [entrada.name for entrada in self.sessao.get_inputs()]
+
+ @property
+ def ort(self) -> Any:
+ if self._ort is None:
+ try:
+ import onnxruntime
+ except ImportError as exc:
+ raise RuntimeError("ONNX Runtime não está instalado. Instale onnxruntime.") from exc
+ self._ort = onnxruntime
+ return self._ort
+
+ def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]:
+ entradas = self.preparar(quadro.imagem, self.entradas)
+ saidas = self.sessao.run(None, entradas)
+ deteccoes = self.decodificar(saidas, quadro)
+ return [EvidenciaVisual("objeto", quadro.timestamp, quadro.timestamp, deteccao,
+ confianca=deteccao.get("confianca"), provider=self.nome,
+ modelo=Path(self.modelo).name) for deteccao in deteccoes]
+
+
+class AnalisadorDePoseMediaPipe(AnalisadorDeFrame):
+ """Adapter MediaPipe Tasks para pose e mãos em um frame."""
+
+ nome = "mediapipe"
+
+ def __init__(self, modelo_de_pose: str | Path | None = None,
+ modelo_de_maos: str | Path | None = None) -> None:
+ self.modelo_de_pose = Path(modelo_de_pose) if modelo_de_pose else None
+ self.modelo_de_maos = Path(modelo_de_maos) if modelo_de_maos else None
+ if self.modelo_de_pose is None and self.modelo_de_maos is None:
+ raise ValueError("Informe ao menos um modelo MediaPipe de pose ou mãos.")
+ def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]:
+ if quadro.caminho is None:
+ raise ValueError("MediaPipe requer que o frame tenha caminho persistido em disco.")
+ for modelo in (self.modelo_de_pose, self.modelo_de_maos):
+ if modelo:
+ self._validar_modelo(modelo)
+ carga = {
+ "frame": str(quadro.caminho), "pose": str(self.modelo_de_pose) if self.modelo_de_pose else None,
+ "maos": str(self.modelo_de_maos) if self.modelo_de_maos else None,
+ }
+ processo = subprocess.run(
+ [sys.executable, "-m", "engine.integracoes.visual.mediapipe_runner"],
+ input=json.dumps(carga), capture_output=True, text=True, timeout=60,
+ )
+ if processo.returncode != 0:
+ detalhe = processo.stderr.strip().splitlines()[-1] if processo.stderr.strip() else "falha nativa sem diagnóstico"
+ raise RuntimeError(f"MediaPipe falhou em processo isolado (código {processo.returncode}): {detalhe}")
+ dados = json.loads(processo.stdout)
+ return [EvidenciaVisual(item["tipo"], quadro.timestamp, quadro.timestamp, item["valor"],
+ provider=self.nome) for item in dados]
+
+ @staticmethod
+ def _validar_modelo(caminho: Path) -> None:
+ if not caminho.is_file():
+ raise FileNotFoundError(f"Modelo MediaPipe não encontrado: {caminho}")
+
+
+class AnalisadorAppleVision(AnalisadorDeFrame):
+ """Usa PyObjC/Vision para OCR e detecção de faces em frames persistidos."""
+
+ nome = "apple_vision"
+
+ def __init__(self, reconhecer_texto: bool = True, detectar_faces: bool = True) -> None:
+ self.reconhecer_texto = reconhecer_texto
+ self.detectar_faces = detectar_faces
+
+ def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]:
+ if quadro.caminho is None:
+ raise ValueError("Apple Vision requer que o frame tenha caminho persistido em disco.")
+ try:
+ from Foundation import NSURL
+ from Vision import VNDetectFaceRectanglesRequest, VNImageRequestHandler, VNRecognizeTextRequest
+ except ImportError as exc:
+ raise RuntimeError("Apple Vision requer PyObjC e macOS.") from exc
+ requisicoes = []
+ texto = VNRecognizeTextRequest.alloc().initWithCompletionHandler_(None) if self.reconhecer_texto else None
+ faces = VNDetectFaceRectanglesRequest.alloc().initWithCompletionHandler_(None) if self.detectar_faces else None
+ if texto:
+ requisicoes.append(texto)
+ if faces:
+ requisicoes.append(faces)
+ handler = VNImageRequestHandler.alloc().initWithURL_options_(NSURL.fileURLWithPath_(str(quadro.caminho)), {})
+ sucesso, erro = handler.performRequests_error_(requisicoes, None)
+ if not sucesso:
+ detalhe = str(erro) if erro else "o macOS não retornou detalhe; verifique Vision/Neural Engine no host"
+ raise RuntimeError(f"Apple Vision falhou: {detalhe}")
+ evidencias: list[EvidenciaVisual] = []
+ if texto:
+ for observacao in texto.results() or []:
+ candidatos = observacao.topCandidates_(1)
+ if candidatos:
+ candidato = candidatos[0]
+ evidencias.append(EvidenciaVisual("ocr", quadro.timestamp, quadro.timestamp,
+ {"texto": str(candidato.string()), "bounding_box": _retangulo(observacao.boundingBox())},
+ confianca=float(candidato.confidence()), provider=self.nome))
+ if faces:
+ for observacao in faces.results() or []:
+ evidencias.append(EvidenciaVisual("rosto", quadro.timestamp, quadro.timestamp,
+ {"bounding_box": _retangulo(observacao.boundingBox())}, provider=self.nome))
+ return evidencias
+
+
+def _retangulo(retangulo: Any) -> dict[str, float]:
+ return {"x": float(retangulo.origin.x), "y": float(retangulo.origin.y),
+ "largura": float(retangulo.size.width), "altura": float(retangulo.size.height)}
+
+
+def _caixa_normalizada(x: int, y: int, largura: int, altura: int,
+ largura_do_frame: int, altura_do_frame: int) -> dict[str, float]:
+ return {"x": float(x / largura_do_frame), "y": float(y / altura_do_frame),
+ "largura": float(largura / largura_do_frame), "altura": float(altura / altura_do_frame)}
+
+
+def _proximo_da_borda(caixa: dict[str, float], margem: float = 0.04) -> bool:
+ return (caixa["x"] < margem or caixa["y"] < margem
+ or caixa["x"] + caixa["largura"] > 1 - margem
+ or caixa["y"] + caixa["altura"] > 1 - margem)
+
+
+def _deslocamento_global(cv2: Any, imagem_anterior: Any, imagem_atual: Any) -> float | None:
+ """Retorna o módulo do deslocamento de câmera, descartando pares sem pontos úteis."""
+ import math
+
+ anterior = cv2.cvtColor(imagem_anterior, cv2.COLOR_BGR2GRAY)
+ atual = cv2.cvtColor(imagem_atual, cv2.COLOR_BGR2GRAY)
+ pontos = cv2.goodFeaturesToTrack(anterior, maxCorners=150, qualityLevel=0.01,
+ minDistance=12, blockSize=7)
+ if pontos is None or len(pontos) < 8:
+ return None
+ encontrados, status, _ = cv2.calcOpticalFlowPyrLK(anterior, atual, pontos, None)
+ if encontrados is None or status is None:
+ return None
+ origem = pontos[status.ravel() == 1]
+ destino = encontrados[status.ravel() == 1]
+ if len(origem) < 8:
+ return None
+ matriz, _ = cv2.estimateAffinePartial2D(origem, destino, method=cv2.RANSAC)
+ if matriz is None:
+ return None
+ return math.hypot(float(matriz[0, 2]), float(matriz[1, 2]))
diff --git a/code/engine/integracoes/visual/apple_vision.py b/code/engine/integracoes/visual/apple_vision.py
new file mode 100644
index 0000000..768e0a9
--- /dev/null
+++ b/code/engine/integracoes/visual/apple_vision.py
@@ -0,0 +1,126 @@
+"""Adapter Python para o runner Swift que concentra Vision e Apple Intelligence."""
+
+import json
+import os
+from pathlib import Path
+import subprocess
+import tempfile
+from typing import Any, Callable
+
+from ...scanner.modelos import EvidenciaVisual
+from .contratos import AnalisadorDeFrame, AnalisadorDeSequenciaDeQuadros
+from .modelos import QuadroDeVideo
+
+
+RECURSOS_PADRAO = (
+ "faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "categorias",
+ "estetica", "codigos", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas",
+)
+
+
+class ExecutorDoRunnerApple:
+ """Compila o runner Swift quando necessário e o executa em processo isolado."""
+
+ def __init__(self, fonte: str | Path | None = None, compilador: str = "swiftc",
+ diretorio_de_build: str | Path | None = None) -> None:
+ self.fonte = Path(fonte) if fonte else Path(__file__).with_name("apple_vision_runner.swift")
+ self.compilador = compilador
+ self.diretorio_de_build = Path(diretorio_de_build) if diretorio_de_build else (
+ Path(tempfile.gettempdir()) / "jhonny-apple-vision")
+
+ def executar(self, carga: dict[str, Any], timeout: float) -> dict[str, Any]:
+ executavel = self._garantir_compilado()
+ processo = subprocess.run([str(executavel)], input=json.dumps(carga), capture_output=True,
+ text=True, timeout=timeout)
+ if processo.returncode != 0:
+ detalhe = processo.stderr.strip() or "runner Apple terminou sem diagnóstico"
+ raise RuntimeError(f"Runner Apple Vision falhou: {detalhe}")
+ try:
+ return json.loads(processo.stdout)
+ except json.JSONDecodeError as exc:
+ raise RuntimeError(f"Runner Apple Vision devolveu JSON inválido: {processo.stdout!r}") from exc
+
+ def _garantir_compilado(self) -> Path:
+ if not self.fonte.is_file():
+ raise FileNotFoundError(f"Fonte do runner Apple não encontrada: {self.fonte}")
+ self.diretorio_de_build.mkdir(parents=True, exist_ok=True)
+ executavel = self.diretorio_de_build / "apple-vision-runner"
+ if not executavel.exists() or executavel.stat().st_mtime < self.fonte.stat().st_mtime:
+ ambiente = os.environ | {"CLANG_MODULE_CACHE_PATH": str(self.diretorio_de_build / "module-cache")}
+ processo = subprocess.run([self.compilador, "-parse-as-library", str(self.fonte), "-o", str(executavel)],
+ capture_output=True, text=True, timeout=120, env=ambiente)
+ if processo.returncode != 0:
+ raise RuntimeError(f"Não foi possível compilar runner Apple Vision: {processo.stderr.strip()}")
+ return executavel
+
+
+class AnalisadorAppleVisionNativo(AnalisadorDeFrame):
+ """Módulo profundo: pede fatos visuais nativos e opcionalmente interpretação local.
+
+ A interface recebe somente um `QuadroDeVideo`; Vision, Foundation Models,
+ Swift, compilação e erros nativos permanecem atrás deste adapter.
+ """
+
+ nome = "apple_vision"
+
+ def __init__(self, recursos: tuple[str, ...] = RECURSOS_PADRAO,
+ interpretar_com_apple_intelligence: bool = True,
+ executor: ExecutorDoRunnerApple | None = None,
+ timeout_em_segundos: float = 90.0) -> None:
+ self.recursos = recursos
+ self.interpretar_com_apple_intelligence = interpretar_com_apple_intelligence
+ self.executor = executor or ExecutorDoRunnerApple()
+ self.timeout_em_segundos = timeout_em_segundos
+
+ def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]:
+ if quadro.caminho is None:
+ raise ValueError("Apple Vision requer que o frame tenha caminho persistido em disco.")
+ dados = self.executor.executar({"frame": str(quadro.caminho), "recursos": list(self.recursos),
+ "resumir": self.interpretar_com_apple_intelligence},
+ self.timeout_em_segundos)
+ evidencias = [self._converter(item, quadro) for item in dados.get("evidencias", [])]
+ avisos = dados.get("avisos", [])
+ if avisos:
+ evidencias.append(EvidenciaVisual("diagnostico_apple_vision", quadro.timestamp, quadro.timestamp,
+ {"avisos": avisos, "recursos_solicitados": list(self.recursos)}, provider=self.nome))
+ return evidencias
+
+ def _converter(self, item: dict[str, Any], quadro: QuadroDeVideo) -> EvidenciaVisual:
+ return EvidenciaVisual(item["tipo"], quadro.timestamp, quadro.timestamp, item["valor"],
+ confianca=item.get("confianca"), provider=self.nome,
+ modelo=item.get("modelo"))
+
+
+class AnalisadorSequenciaAppleVisionNativo(AnalisadorDeSequenciaDeQuadros):
+ """Compara frames com feature prints nativos sem expor o protocolo Swift."""
+
+ nome = "apple_vision_sequencia"
+
+ def __init__(self, executor: ExecutorDoRunnerApple | None = None,
+ timeout_em_segundos: float = 90.0) -> None:
+ self.executor = executor or ExecutorDoRunnerApple()
+ self.timeout_em_segundos = timeout_em_segundos
+
+ def analisar(self, quadros: list[QuadroDeVideo]) -> list[EvidenciaVisual]:
+ if len(quadros) < 2:
+ return []
+ if any(quadro.caminho is None for quadro in quadros):
+ raise ValueError("Apple Vision de sequência requer frames persistidos em disco.")
+ dados = self.executor.executar({"frames": [str(quadro.caminho) for quadro in quadros],
+ "recursos": [], "resumir": False}, self.timeout_em_segundos)
+ evidencias: list[EvidenciaVisual] = []
+ for item in dados.get("evidencias", []):
+ valor = dict(item["valor"])
+ inicio = int(valor.pop("frame_inicial", 0))
+ fim = int(valor.pop("frame_final", inicio + 1))
+ if inicio < 0 or fim >= len(quadros) or fim < inicio:
+ raise RuntimeError("Runner Apple Vision devolveu índices temporais inválidos.")
+ evidencias.append(EvidenciaVisual(item["tipo"], quadros[inicio].timestamp,
+ quadros[fim].timestamp, valor,
+ confianca=item.get("confianca"), provider=self.nome,
+ modelo=item.get("modelo")))
+ avisos = dados.get("avisos", [])
+ if avisos:
+ evidencias.append(EvidenciaVisual("diagnostico_apple_vision", quadros[0].timestamp,
+ quadros[-1].timestamp, {"avisos": avisos}, provider=self.nome))
+ return evidencias
diff --git a/code/engine/integracoes/visual/apple_vision_runner.swift b/code/engine/integracoes/visual/apple_vision_runner.swift
new file mode 100644
index 0000000..cd94991
--- /dev/null
+++ b/code/engine/integracoes/visual/apple_vision_runner.swift
@@ -0,0 +1,368 @@
+import Foundation
+import ImageIO
+import Vision
+import FoundationModels
+import CoreVideo
+
+struct Entrada: Decodable {
+ let frame: String?
+ let frames: [String]?
+ let recursos: [String]
+ let resumir: Bool
+}
+
+struct Evidencia: Encodable {
+ let tipo: String
+ let valor: [String: JSONValue]
+ let confianca: Double?
+ let modelo: String?
+}
+
+struct Saida: Encodable {
+ let evidencias: [Evidencia]
+ let avisos: [String]
+}
+
+enum JSONValue: Encodable {
+ case texto(String), numero(Double), booleano(Bool), objeto([String: JSONValue]), lista([JSONValue]), nulo
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ switch self {
+ case .texto(let value): try container.encode(value)
+ case .numero(let value): try container.encode(value)
+ case .booleano(let value): try container.encode(value)
+ case .objeto(let value): try container.encode(value)
+ case .lista(let value): try container.encode(value)
+ case .nulo: try container.encodeNil()
+ }
+ }
+}
+
+func caixa(_ rect: CGRect) -> [String: JSONValue] {
+ ["x": .numero(rect.origin.x), "y": .numero(rect.origin.y),
+ "largura": .numero(rect.size.width), "altura": .numero(rect.size.height)]
+}
+
+func ponto(_ point: CGPoint) -> JSONValue {
+ .objeto(["x": .numero(point.x), "y": .numero(point.y)])
+}
+
+func ponto(_ point: VNRecognizedPoint) -> JSONValue {
+ .objeto(["x": .numero(point.location.x), "y": .numero(point.location.y),
+ "confianca": .numero(Double(point.confidence))])
+}
+
+func executar(_ request: T, em url: URL) throws -> [VNObservation] {
+ guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
+ let imagem = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
+ throw NSError(domain: "JhonnyAppleVision", code: 1,
+ userInfo: [NSLocalizedDescriptionKey: "ImageIO não conseguiu decodificar o frame"])
+ }
+ let handler = VNImageRequestHandler(cgImage: imagem, options: [:])
+ try handler.perform([request])
+ return request.results ?? []
+}
+
+func coberturaDaMascara(_ pixelBuffer: CVPixelBuffer) -> Double {
+ CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
+ defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
+ guard let base = CVPixelBufferGetBaseAddress(pixelBuffer) else { return 0 }
+ let altura = CVPixelBufferGetHeight(pixelBuffer)
+ let largura = CVPixelBufferGetWidth(pixelBuffer)
+ let stride = CVPixelBufferGetBytesPerRow(pixelBuffer)
+ let bytes = base.assumingMemoryBound(to: UInt8.self)
+ var ocupados = 0
+ for y in 0.. 12 { ocupados += 1 }
+ }
+ return Double(ocupados) / Double(max(1, altura * largura))
+}
+
+func analisarVision(_ entrada: Entrada) -> Saida {
+ guard let frame = entrada.frame else {
+ return Saida(evidencias: [], avisos: ["frame: caminho obrigatório para análise individual"])
+ }
+ let url = URL(fileURLWithPath: frame)
+ var evidencias: [Evidencia] = []
+ var avisos: [String] = []
+
+ func tentar(_ recurso: String, _ bloco: () throws -> [Evidencia]) {
+ guard entrada.recursos.contains(recurso) else { return }
+ do { evidencias.append(contentsOf: try bloco()) }
+ catch { avisos.append("\(recurso): \(error.localizedDescription)") }
+ }
+
+ tentar("faces") {
+ let request = VNDetectFaceLandmarksRequest()
+ return try executar(request, em: url).compactMap { observacao -> Evidencia? in
+ guard let face = observacao as? VNFaceObservation else { return nil }
+ var valor: [String: JSONValue] = ["bounding_box": .objeto(caixa(face.boundingBox))]
+ if let landmarks = face.landmarks {
+ let grupos: [(String, VNFaceLandmarkRegion2D?)] = [
+ ("olho_esquerdo", landmarks.leftEye), ("olho_direito", landmarks.rightEye),
+ ("sobrancelha_esquerda", landmarks.leftEyebrow), ("sobrancelha_direita", landmarks.rightEyebrow),
+ ("nariz", landmarks.nose), ("labios", landmarks.outerLips), ("rosto", landmarks.faceContour)
+ ]
+ valor["landmarks"] = .objeto(Dictionary(uniqueKeysWithValues: grupos.compactMap { nome, regiao in
+ guard let regiao else { return nil }
+ return (nome, .lista(regiao.normalizedPoints.map { .objeto(["x": .numero($0.x), "y": .numero($0.y)]) }))
+ }))
+ }
+ return Evidencia(tipo: "rosto", valor: valor, confianca: Double(face.confidence), modelo: "VNDetectFaceLandmarksRequest")
+ }
+ }
+
+ tentar("qualidade_facial") {
+ let request = VNDetectFaceCaptureQualityRequest()
+ return try executar(request, em: url).compactMap { observacao -> Evidencia? in
+ guard let face = observacao as? VNFaceObservation, let qualidade = face.faceCaptureQuality else { return nil }
+ return Evidencia(tipo: "qualidade_do_rosto", valor: ["bounding_box": .objeto(caixa(face.boundingBox)),
+ "score": .numero(Double(qualidade))], confianca: Double(face.confidence), modelo: "VNDetectFaceCaptureQualityRequest")
+ }
+ }
+
+ tentar("pessoas") {
+ let request = VNDetectHumanRectanglesRequest()
+ request.upperBodyOnly = false
+ return try executar(request, em: url).compactMap { observacao in
+ guard let pessoa = observacao as? VNHumanObservation else { return nil }
+ return Evidencia(tipo: "pessoa", valor: ["bounding_box": .objeto(caixa(pessoa.boundingBox)),
+ "ocupacao_do_quadro": .numero(pessoa.boundingBox.width * pessoa.boundingBox.height)],
+ confianca: Double(pessoa.confidence), modelo: "VNDetectHumanRectanglesRequest")
+ }
+ }
+
+ tentar("pose") {
+ let request = VNDetectHumanBodyPoseRequest()
+ return try executar(request, em: url).compactMap { observacao in
+ guard let pose = observacao as? VNHumanBodyPoseObservation else { return nil }
+ let pontos = try? pose.recognizedPoints(.all)
+ let dados: [String: JSONValue] = pontos.map { Dictionary(uniqueKeysWithValues: $0.map { (String(describing: $0.key), ponto($0.value)) }) } ?? [:]
+ return Evidencia(tipo: "pose", valor: ["pontos": .objeto(dados)], confianca: Double(pose.confidence), modelo: "VNDetectHumanBodyPoseRequest")
+ }
+ }
+
+ tentar("maos") {
+ let request = VNDetectHumanHandPoseRequest()
+ request.maximumHandCount = 4
+ return try executar(request, em: url).compactMap { observacao in
+ guard let mao = observacao as? VNHumanHandPoseObservation else { return nil }
+ let pontos = try? mao.recognizedPoints(.all)
+ let dados: [String: JSONValue] = pontos.map { Dictionary(uniqueKeysWithValues: $0.map { (String(describing: $0.key), ponto($0.value)) }) } ?? [:]
+ return Evidencia(tipo: "mao", valor: ["pontos": .objeto(dados)], confianca: Double(mao.confidence), modelo: "VNDetectHumanHandPoseRequest")
+ }
+ }
+
+ tentar("ocr") {
+ let request = VNRecognizeTextRequest()
+ request.recognitionLevel = .accurate
+ request.recognitionLanguages = ["pt-BR", "en-US"]
+ return try executar(request, em: url).compactMap { observacao in
+ guard let texto = observacao as? VNRecognizedTextObservation,
+ let candidato = texto.topCandidates(1).first else { return nil }
+ return Evidencia(tipo: "ocr", valor: ["texto": .texto(candidato.string),
+ "bounding_box": .objeto(caixa(texto.boundingBox))], confianca: Double(candidato.confidence), modelo: "VNRecognizeTextRequest")
+ }
+ }
+
+ tentar("categorias") {
+ let request = VNClassifyImageRequest()
+ return try executar(request, em: url).compactMap { observacao in
+ guard let categoria = observacao as? VNClassificationObservation, categoria.confidence >= 0.2 else { return nil }
+ return Evidencia(tipo: "categoria", valor: ["identificador": .texto(categoria.identifier)],
+ confianca: Double(categoria.confidence), modelo: "VNClassifyImageRequest")
+ }
+ }
+
+ tentar("estetica") {
+ let request = VNCalculateImageAestheticsScoresRequest()
+ return try executar(request, em: url).compactMap { observacao in
+ guard let score = observacao as? VNImageAestheticsScoresObservation else { return nil }
+ return Evidencia(tipo: "estetica", valor: ["score_global": .numero(Double(score.overallScore))],
+ confianca: nil, modelo: "VNCalculateImageAestheticsScoresRequest")
+ }
+ }
+
+ tentar("codigos") {
+ let request = VNDetectBarcodesRequest()
+ return try executar(request, em: url).compactMap { observacao in
+ guard let codigo = observacao as? VNBarcodeObservation else { return nil }
+ return Evidencia(tipo: "codigo", valor: ["payload": .texto(codigo.payloadStringValue ?? ""),
+ "simbologia": .texto(codigo.symbology.rawValue), "bounding_box": .objeto(caixa(codigo.boundingBox))],
+ confianca: Double(codigo.confidence), modelo: "VNDetectBarcodesRequest")
+ }
+ }
+
+ tentar("horizonte") {
+ let request = VNDetectHorizonRequest()
+ return try executar(request, em: url).compactMap { observacao in
+ guard let horizonte = observacao as? VNHorizonObservation else { return nil }
+ return Evidencia(tipo: "horizonte", valor: ["angulo_radianos": .numero(Double(horizonte.angle))],
+ confianca: Double(horizonte.confidence), modelo: "VNDetectHorizonRequest")
+ }
+ }
+
+ tentar("retangulos") {
+ let request = VNDetectRectanglesRequest()
+ return try executar(request, em: url).compactMap { observacao in
+ guard let retangulo = observacao as? VNRectangleObservation else { return nil }
+ return Evidencia(tipo: "retangulo", valor: ["bounding_box": .objeto(caixa(retangulo.boundingBox)),
+ "cantos": .objeto(["superior_esquerdo": ponto(retangulo.topLeft),
+ "superior_direito": ponto(retangulo.topRight),
+ "inferior_esquerdo": ponto(retangulo.bottomLeft),
+ "inferior_direito": ponto(retangulo.bottomRight)])],
+ confianca: Double(retangulo.confidence), modelo: "VNDetectRectanglesRequest")
+ }
+ }
+
+ tentar("contornos") {
+ let request = VNDetectContoursRequest()
+ return try executar(request, em: url).compactMap { observacao in
+ guard let contornos = observacao as? VNContoursObservation else { return nil }
+ return Evidencia(tipo: "contornos", valor: ["quantidade_principal": .numero(Double(contornos.topLevelContours.count))],
+ confianca: Double(contornos.confidence), modelo: "VNDetectContoursRequest")
+ }
+ }
+
+ tentar("segmentacao_de_pessoas") {
+ let request = VNGeneratePersonSegmentationRequest()
+ request.qualityLevel = .balanced
+ return try executar(request, em: url).compactMap { observacao in
+ guard let mascara = observacao as? VNPixelBufferObservation else { return nil }
+ return Evidencia(tipo: "segmentacao_de_pessoas", valor: ["ocupacao_do_quadro": .numero(coberturaDaMascara(mascara.pixelBuffer))],
+ confianca: nil, modelo: "VNGeneratePersonSegmentationRequest")
+ }
+ }
+
+ return Saida(evidencias: evidencias, avisos: avisos)
+}
+
+func featurePrint(_ url: URL) throws -> VNFeaturePrintObservation {
+ let request = VNGenerateImageFeaturePrintRequest()
+ guard let resultado = try executar(request, em: url).first as? VNFeaturePrintObservation else {
+ throw NSError(domain: "JhonnyAppleVision", code: 2, userInfo: [NSLocalizedDescriptionKey: "Vision não produziu feature print"])
+ }
+ return resultado
+}
+
+func analisarSequencia(_ entrada: Entrada) -> Saida {
+ let caminhos = entrada.frames ?? []
+ guard caminhos.count >= 2 else { return Saida(evidencias: [], avisos: ["sequencia: informe ao menos dois frames"]) }
+ var evidencias: [Evidencia] = []
+ var avisos: [String] = []
+ for indice in 0..<(caminhos.count - 1) {
+ do {
+ var distancia: Float = 0
+ try featurePrint(URL(fileURLWithPath: caminhos[indice])).computeDistance(
+ &distancia, to: featurePrint(URL(fileURLWithPath: caminhos[indice + 1])))
+ evidencias.append(Evidencia(tipo: "similaridade_visual", valor: [
+ "frame_inicial": .numero(Double(indice)), "frame_final": .numero(Double(indice + 1)),
+ "distancia_feature_print": .numero(Double(distancia)),
+ "possivel_mudanca_de_cena": .booleano(distancia > 12),
+ ], confianca: nil, modelo: "VNGenerateImageFeaturePrintRequest"))
+ } catch { avisos.append("similaridade_visual[\(indice)]: \(error.localizedDescription)") }
+ }
+ return Saida(evidencias: evidencias, avisos: avisos)
+}
+
+extension Dictionary where Key == String, Value == JSONValue {
+ func numero(_ chave: String) -> Double? {
+ if case .numero(let valor)? = self[chave] { return valor }
+ return nil
+ }
+ func texto(_ chave: String) -> String? {
+ if case .texto(let valor)? = self[chave] { return valor }
+ return nil
+ }
+ func booleano(_ chave: String) -> Bool? {
+ if case .booleano(let valor)? = self[chave] { return valor }
+ return nil
+ }
+ func objeto(_ chave: String) -> [String: JSONValue]? {
+ if case .objeto(let valor)? = self[chave] { return valor }
+ return nil
+ }
+}
+
+/// Traduz uma evidência em uma frase factual com os valores medidos, nunca só o nome do tipo —
+/// é isso que alimenta a interpretação editorial, então precisa carregar o fato, não só o rótulo.
+func descreverEvidencia(_ evidencia: Evidencia) -> String {
+ let valor = evidencia.valor
+ switch evidencia.tipo {
+ case "rosto":
+ let grupos = valor.objeto("landmarks")?.count ?? 0
+ return "rosto detectado (\(grupos) grupos de landmarks mapeados)"
+ case "qualidade_do_rosto":
+ guard let score = valor.numero("score") else { return "qualidade do rosto avaliada" }
+ return "qualidade do rosto: score \(String(format: "%.2f", score))"
+ case "pessoa":
+ guard let ocupacao = valor.numero("ocupacao_do_quadro") else { return "pessoa detectada" }
+ return "pessoa ocupando \(String(format: "%.0f", ocupacao * 100))% do quadro"
+ case "pose":
+ let pontos = valor.objeto("pontos")?.count ?? 0
+ return "pose corporal com \(pontos) pontos reconhecidos"
+ case "mao":
+ let pontos = valor.objeto("pontos")?.count ?? 0
+ return "mão com \(pontos) pontos reconhecidos"
+ case "ocr":
+ guard let texto = valor.texto("texto"), !texto.isEmpty else { return "nenhum texto legível na imagem" }
+ return "texto detectado na imagem: \"\(texto)\""
+ case "categoria":
+ guard let identificador = valor.texto("identificador") else { return "categoria detectada" }
+ let confianca = evidencia.confianca.map { String(format: "%.0f%%", $0 * 100) } ?? "confiança desconhecida"
+ return "categoria \"\(identificador)\" (\(confianca))"
+ case "estetica":
+ guard let score = valor.numero("score_global") else { return "score estético calculado" }
+ return "score estético global: \(String(format: "%.2f", score))"
+ case "codigo":
+ guard let payload = valor.texto("payload"), !payload.isEmpty else { return "código detectado sem payload legível" }
+ return "código detectado: \"\(payload)\""
+ case "horizonte":
+ guard let angulo = valor.numero("angulo_radianos") else { return "horizonte detectado" }
+ return "horizonte inclinado \(String(format: "%.1f", angulo * 180 / .pi))°"
+ case "retangulo":
+ guard let bbox = valor.objeto("bounding_box"), let largura = bbox.numero("largura"), let altura = bbox.numero("altura") else {
+ return "retângulo/documento detectado no quadro"
+ }
+ return "retângulo detectado ocupando \(String(format: "%.0f", largura * 100))%x\(String(format: "%.0f", altura * 100))% do quadro"
+ case "contornos":
+ guard let quantidade = valor.numero("quantidade_principal") else { return "contornos detectados" }
+ return "\(Int(quantidade)) contornos principais detectados"
+ case "segmentacao_de_pessoas":
+ guard let ocupacao = valor.numero("ocupacao_do_quadro") else { return "segmentação de pessoa calculada" }
+ return "silhueta de pessoa cobrindo \(String(format: "%.0f", ocupacao * 100))% do quadro"
+ case "similaridade_visual":
+ let mudanca = valor.booleano("possivel_mudanca_de_cena") ?? false
+ return mudanca ? "possível corte de cena entre os quadros" : "quadros visualmente semelhantes, sem corte de cena"
+ default:
+ return evidencia.tipo
+ }
+}
+
+func interpretar(_ saida: Saida) async -> String? {
+ guard SystemLanguageModel.default.isAvailable else { return nil }
+ let fatos = saida.evidencias.map(descreverEvidencia).joined(separator: "; ")
+ guard !fatos.isEmpty else { return nil }
+ do {
+ let sessao = LanguageModelSession(instructions: "Você é um assistente editorial. Descreva apenas fatos explicitamente fornecidos; não invente pessoas, emoções, intenção ou identidade. Responda em uma frase curta em português.")
+ return try await sessao.respond(to: "Evidências visuais disponíveis: \(fatos). Gere uma observação editorial conservadora.").content
+ } catch { return nil }
+}
+
+@main struct Principal {
+ static func main() async {
+ do {
+ let entrada = try JSONDecoder().decode(Entrada.self, from: FileHandle.standardInput.readDataToEndOfFile())
+ var saida = entrada.frames?.count ?? 0 > 1 ? analisarSequencia(entrada) : analisarVision(entrada)
+ if entrada.resumir, let resumo = await interpretar(saida) {
+ saida = Saida(evidencias: saida.evidencias + [Evidencia(tipo: "interpretacao_editorial", valor: ["texto": .texto(resumo)], confianca: nil, modelo: "AppleFoundationModels")], avisos: saida.avisos)
+ }
+ let dados = try JSONEncoder().encode(saida)
+ FileHandle.standardOutput.write(dados)
+ } catch {
+ FileHandle.standardError.write(Data("\(error.localizedDescription)\n".utf8))
+ exit(1)
+ }
+ }
+}
diff --git a/code/engine/integracoes/visual/contratos.py b/code/engine/integracoes/visual/contratos.py
new file mode 100644
index 0000000..08c5518
--- /dev/null
+++ b/code/engine/integracoes/visual/contratos.py
@@ -0,0 +1,40 @@
+from abc import ABC, abstractmethod
+from typing import Any
+
+from ...dominio import Clipe
+from ...scanner.modelos import Cena, EvidenciaVisual
+from .modelos import QuadroDeVideo
+
+
+class ExtratorDeQuadros(ABC):
+ """Interface para obter uma amostra temporal de frames de um clipe."""
+
+ @abstractmethod
+ def extrair(self, clipe: Clipe) -> list[QuadroDeVideo]: ...
+
+
+class AnalisadorDeFrame(ABC):
+ """Interface comum: recebe um frame e devolve evidências do domínio."""
+
+ nome: str
+
+ @abstractmethod
+ def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]: ...
+
+
+class AnalisadorDeSequenciaDeQuadros(ABC):
+ """Interface para evidências que só existem ao comparar vários frames."""
+
+ nome: str
+
+ @abstractmethod
+ def analisar(self, quadros: list[QuadroDeVideo]) -> list[EvidenciaVisual]: ...
+
+
+class DetectorDeIntervalosDeCena(ABC):
+ """Interface para algoritmos que encontram intervalos de cena no clipe."""
+
+ nome: str
+
+ @abstractmethod
+ def detectar(self, clipe: Clipe, quadros: list[QuadroDeVideo]) -> list[Cena]: ...
diff --git a/code/engine/integracoes/visual/detectores_de_cena.py b/code/engine/integracoes/visual/detectores_de_cena.py
new file mode 100644
index 0000000..37c7c85
--- /dev/null
+++ b/code/engine/integracoes/visual/detectores_de_cena.py
@@ -0,0 +1,34 @@
+from typing import Any
+
+from ...dominio import Clipe
+from ...scanner.modelos import Cena
+from .contratos import DetectorDeIntervalosDeCena
+from .modelos import QuadroDeVideo
+
+
+class DetectorDeCenasPySceneDetect(DetectorDeIntervalosDeCena):
+ """Adapter PySceneDetect que devolve somente cenas do domínio."""
+
+ nome = "pyscenedetect"
+
+ def __init__(self, limiar: float = 27.0, detector: Any | None = None,
+ detectar_funcao: Any | None = None) -> None:
+ self.limiar = limiar
+ self._detector = detector
+ self._detectar_funcao = detectar_funcao
+
+ def detectar(self, clipe: Clipe, quadros: list[QuadroDeVideo]) -> list[Cena]:
+ if not clipe.arquivo:
+ raise ValueError("Clipe não possui arquivo de origem.")
+ if self._detectar_funcao is None:
+ try:
+ from scenedetect import ContentDetector, detect
+ except ImportError as exc:
+ raise RuntimeError("PySceneDetect não está instalado. Instale scenedetect.") from exc
+ detector = self._detector or ContentDetector(threshold=self.limiar)
+ detectar = lambda arquivo: detect(arquivo, detector)
+ else:
+ detectar = self._detectar_funcao
+ resultado = detectar(str(clipe.arquivo))
+ return [Cena(float(inicio.get_seconds()), float(fim.get_seconds()), referencias=(float(inicio.get_seconds()),))
+ for inicio, fim in resultado]
diff --git a/code/engine/integracoes/visual/extrator_ffmpeg.py b/code/engine/integracoes/visual/extrator_ffmpeg.py
new file mode 100644
index 0000000..402c127
--- /dev/null
+++ b/code/engine/integracoes/visual/extrator_ffmpeg.py
@@ -0,0 +1,77 @@
+"""Extrator de frames para Vision usando somente ferramentas locais do sistema."""
+
+import json
+from pathlib import Path
+import subprocess
+
+from ...dominio import Clipe
+from .contratos import ExtratorDeQuadros
+from .extrator_open_cv import ExtratorDeQuadrosOpenCV
+from .modelos import QuadroDeVideo
+
+
+class ExtratorDeQuadrosFFmpeg(ExtratorDeQuadros):
+ """Persiste amostras de um arquivo original sem carregar OpenCV no processo."""
+
+ def __init__(self, intervalo_em_segundos: float = 1.0,
+ diretorio_de_cache: str | Path = ".frames",
+ maximo_de_quadros: int | None = None,
+ ffmpeg: str = "ffmpeg", ffprobe: str = "ffprobe") -> None:
+ if intervalo_em_segundos <= 0:
+ raise ValueError("intervalo_em_segundos deve ser positivo.")
+ if maximo_de_quadros is not None and maximo_de_quadros < 1:
+ raise ValueError("maximo_de_quadros deve ser maior que zero.")
+ self.intervalo_em_segundos = intervalo_em_segundos
+ self.diretorio_de_cache = Path(diretorio_de_cache)
+ self.maximo_de_quadros = maximo_de_quadros
+ self.ffmpeg = ffmpeg
+ self.ffprobe = ffprobe
+
+ def extrair(self, clipe: Clipe) -> list[QuadroDeVideo]:
+ if not clipe.arquivo:
+ raise ValueError("Clipe não possui arquivo de origem.")
+ arquivo = Path(clipe.arquivo)
+ if not arquivo.is_file():
+ raise FileNotFoundError(f"Arquivo do clipe não encontrado: {arquivo}")
+ largura, altura, duracao = self._metadados(arquivo)
+ inicio, fim = ExtratorDeQuadrosOpenCV._intervalo_de_origem(clipe, duracao)
+ timestamps = ExtratorDeQuadrosOpenCV(self.intervalo_em_segundos,
+ maximo_de_quadros=self.maximo_de_quadros)._timestamps(inicio, fim)
+ resultado: list[QuadroDeVideo] = []
+ for indice, timestamp in enumerate(timestamps):
+ # PNG evita a recodificação JPEG de YUV limitado, que falha em parte
+ # dos vídeos móveis e ainda preserva melhor OCR/segmentação para Vision.
+ destino = self.diretorio_de_cache / arquivo.stem / f"frame-{indice:06d}.png"
+ destino.parent.mkdir(parents=True, exist_ok=True)
+ amostrado, processo = self._extrair_frame(arquivo, destino, timestamp, inicio)
+ if processo.returncode != 0 or not destino.is_file():
+ detalhe = processo.stderr.strip() or "ffmpeg não produziu o frame"
+ raise RuntimeError(f"Não foi possível extrair frame em {timestamp}s: {detalhe}")
+ resultado.append(QuadroDeVideo(amostrado, indice, largura, altura, None, destino))
+ return resultado
+
+ def _extrair_frame(self, arquivo: Path, destino: Path, timestamp: float,
+ inicio: float) -> tuple[float, subprocess.CompletedProcess[str]]:
+ """Recua um pouco no fim do arquivo se o decoder não encontrar frame no timestamp."""
+ ultimo: subprocess.CompletedProcess[str] | None = None
+ for candidato in (timestamp, max(inicio, timestamp - 0.1)):
+ if destino.exists():
+ destino.unlink()
+ processo = subprocess.run([self.ffmpeg, "-hide_banner", "-loglevel", "error", "-ss", str(candidato),
+ "-i", str(arquivo), "-frames:v", "1", "-y", str(destino)],
+ capture_output=True, text=True, timeout=60)
+ if processo.returncode == 0 and destino.is_file():
+ return candidato, processo
+ ultimo = processo
+ assert ultimo is not None
+ return timestamp, ultimo
+
+ def _metadados(self, arquivo: Path) -> tuple[int, int, float]:
+ processo = subprocess.run([self.ffprobe, "-v", "error", "-select_streams", "v:0",
+ "-show_entries", "stream=width,height:format=duration",
+ "-of", "json", str(arquivo)], capture_output=True, text=True, timeout=30)
+ if processo.returncode != 0:
+ raise RuntimeError(processo.stderr.strip() or "ffprobe não conseguiu ler o vídeo")
+ dados = json.loads(processo.stdout)
+ stream = (dados.get("streams") or [{}])[0]
+ return int(stream["width"]), int(stream["height"]), float((dados.get("format") or {}).get("duration") or 0)
diff --git a/code/engine/integracoes/visual/extrator_open_cv.py b/code/engine/integracoes/visual/extrator_open_cv.py
new file mode 100644
index 0000000..897e2ea
--- /dev/null
+++ b/code/engine/integracoes/visual/extrator_open_cv.py
@@ -0,0 +1,87 @@
+from pathlib import Path
+from typing import Any
+
+from ...dominio import Clipe
+from .contratos import ExtratorDeQuadros
+from .modelos import QuadroDeVideo
+
+
+class ExtratorDeQuadrosOpenCV(ExtratorDeQuadros):
+ """Extrai frames em intervalos regulares sem expor detalhes do OpenCV."""
+
+ def __init__(self, intervalo_em_segundos: float = 1.0,
+ diretorio_de_cache: str | Path | None = None,
+ maximo_de_quadros: int | None = None,
+ cv2_module: Any | None = None) -> None:
+ if intervalo_em_segundos <= 0:
+ raise ValueError("intervalo_em_segundos deve ser positivo.")
+ if maximo_de_quadros is not None and maximo_de_quadros < 1:
+ raise ValueError("maximo_de_quadros deve ser maior que zero.")
+ self.intervalo_em_segundos = intervalo_em_segundos
+ self.diretorio_de_cache = Path(diretorio_de_cache) if diretorio_de_cache else None
+ self.maximo_de_quadros = maximo_de_quadros
+ self._cv2 = cv2_module
+
+ @property
+ def cv2(self) -> Any:
+ if self._cv2 is None:
+ try:
+ import cv2
+ except ImportError as exc:
+ raise RuntimeError("OpenCV não está instalado. Instale opencv-python.") from exc
+ self._cv2 = cv2
+ return self._cv2
+
+ def extrair(self, clipe: Clipe) -> list[QuadroDeVideo]:
+ if not clipe.arquivo:
+ raise ValueError("Clipe não possui arquivo de origem.")
+ caminho = Path(clipe.arquivo)
+ if not caminho.is_file():
+ raise FileNotFoundError(f"Arquivo do clipe não encontrado: {caminho}")
+ captura = self.cv2.VideoCapture(str(caminho))
+ if not captura.isOpened():
+ raise RuntimeError(f"OpenCV não conseguiu abrir o vídeo: {caminho}")
+ try:
+ fps = float(captura.get(self.cv2.CAP_PROP_FPS) or 0)
+ total_de_frames = int(captura.get(self.cv2.CAP_PROP_FRAME_COUNT) or 0)
+ duracao = total_de_frames / fps if fps > 0 else 0.0
+ inicio, fim = self._intervalo_de_origem(clipe, duracao)
+ timestamps = self._timestamps(inicio, fim)
+ resultado: list[QuadroDeVideo] = []
+ for indice, timestamp in enumerate(timestamps):
+ captura.set(self.cv2.CAP_PROP_POS_MSEC, timestamp * 1000.0)
+ sucesso, imagem = captura.read()
+ if not sucesso or imagem is None:
+ continue
+ altura, largura = imagem.shape[:2]
+ salvo = self._salvar(caminho, indice, imagem)
+ resultado.append(QuadroDeVideo(timestamp, indice, largura, altura, imagem, salvo))
+ return resultado
+ finally:
+ captura.release()
+
+ def _timestamps(self, inicio: float, fim: float) -> list[float]:
+ if fim <= inicio:
+ return [inicio]
+ quantidade = int((fim - inicio) / self.intervalo_em_segundos) + 1
+ limite = max(inicio, fim - 0.001)
+ timestamps = [min(inicio + indice * self.intervalo_em_segundos, limite)
+ for indice in range(quantidade)]
+ return timestamps[:self.maximo_de_quadros]
+
+ @staticmethod
+ def _intervalo_de_origem(clipe: Clipe, duracao: float) -> tuple[float, float]:
+ if clipe.intervalo_na_origem is None:
+ return 0.0, duracao
+ inicio = max(0.0, clipe.intervalo_na_origem.inicio)
+ fim = min(duracao, clipe.intervalo_na_origem.fim) if duracao > 0 else clipe.intervalo_na_origem.fim
+ return inicio, max(inicio, fim)
+
+ def _salvar(self, arquivo: Path, indice: int, imagem: Any) -> Path | None:
+ if self.diretorio_de_cache is None:
+ return None
+ destino = self.diretorio_de_cache / arquivo.stem / f"frame-{indice:06d}.jpg"
+ destino.parent.mkdir(parents=True, exist_ok=True)
+ if not self.cv2.imwrite(str(destino), imagem):
+ raise RuntimeError(f"Não foi possível salvar frame: {destino}")
+ return destino
diff --git a/code/engine/integracoes/visual/mediapipe_runner.py b/code/engine/integracoes/visual/mediapipe_runner.py
new file mode 100644
index 0000000..737496f
--- /dev/null
+++ b/code/engine/integracoes/visual/mediapipe_runner.py
@@ -0,0 +1,47 @@
+"""Executável interno isolado para evitar que falhas nativas do MediaPipe derrubem o Scanner."""
+
+import json
+import sys
+from pathlib import Path
+
+
+def _pontos(landmarks, visibilidade: bool = False):
+ return [{"x": ponto.x, "y": ponto.y, "z": ponto.z,
+ **({"visibilidade": getattr(ponto, "visibility", None)} if visibilidade else {})}
+ for ponto in landmarks]
+
+
+def executar(carga: dict) -> list[dict]:
+ import cv2
+ import mediapipe as mp
+
+ imagem_bgr = cv2.imread(carga["frame"])
+ if imagem_bgr is None:
+ raise FileNotFoundError(f"Frame não encontrado: {carga['frame']}")
+ imagem = mp.Image(image_format=mp.ImageFormat.SRGB, data=imagem_bgr[:, :, ::-1])
+ resultado: list[dict] = []
+ if carga.get("pose"):
+ opcoes = mp.tasks.vision.PoseLandmarkerOptions(
+ base_options=mp.tasks.BaseOptions(model_asset_path=carga["pose"], delegate=mp.tasks.BaseOptions.Delegate.CPU),
+ running_mode=mp.tasks.vision.RunningMode.IMAGE,
+ )
+ with mp.tasks.vision.PoseLandmarker.create_from_options(opcoes) as detector:
+ for pose in detector.detect(imagem).pose_landmarks:
+ resultado.append({"tipo": "pose", "valor": {"pontos": _pontos(pose, visibilidade=True)}})
+ if carga.get("maos"):
+ opcoes = mp.tasks.vision.HandLandmarkerOptions(
+ base_options=mp.tasks.BaseOptions(model_asset_path=carga["maos"], delegate=mp.tasks.BaseOptions.Delegate.CPU),
+ running_mode=mp.tasks.vision.RunningMode.IMAGE, num_hands=4,
+ )
+ with mp.tasks.vision.HandLandmarker.create_from_options(opcoes) as detector:
+ for mao in detector.detect(imagem).hand_landmarks:
+ resultado.append({"tipo": "mao", "valor": {"pontos": _pontos(mao)}})
+ return resultado
+
+
+if __name__ == "__main__":
+ try:
+ print(json.dumps(executar(json.loads(sys.stdin.read()))))
+ except Exception as erro:
+ print(str(erro), file=sys.stderr)
+ raise
diff --git a/code/engine/integracoes/visual/modelos.py b/code/engine/integracoes/visual/modelos.py
new file mode 100644
index 0000000..29f7b5d
--- /dev/null
+++ b/code/engine/integracoes/visual/modelos.py
@@ -0,0 +1,15 @@
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+
+
+@dataclass(frozen=True)
+class QuadroDeVideo:
+ """Frame extraído, com timestamp relativo ao arquivo de origem."""
+
+ timestamp: float
+ indice: int
+ largura: int
+ altura: int
+ imagem: Any
+ caminho: Path | None = None
diff --git a/code/engine/integracoes/whisper/provider_de_transcricao_local.py b/code/engine/integracoes/whisper/provider_de_transcricao_local.py
index dec45be..3d0991c 100644
--- a/code/engine/integracoes/whisper/provider_de_transcricao_local.py
+++ b/code/engine/integracoes/whisper/provider_de_transcricao_local.py
@@ -1,7 +1,7 @@
from pathlib import Path
from typing import Any
-from ...scanner.modelos import SegmentoDeTranscricao
+from ...scanner.modelos import PalavraDeTranscricao, SegmentoDeTranscricao
class ProviderDeTranscricaoLocal:
@@ -9,6 +9,9 @@ class ProviderDeTranscricaoLocal:
def __init__(self, modelo: str, dispositivo: str = "cpu", tipo_de_calculo: str = "int8",
idioma: str = "pt") -> None:
+ self.nome_do_modelo = Path(modelo).name
+ if "faster-whisper-" in modelo:
+ self.nome_do_modelo = modelo.split("faster-whisper-", 1)[1].split("/", 1)[0]
from faster_whisper import WhisperModel
self.modelo = WhisperModel(modelo, device=dispositivo, compute_type=tipo_de_calculo)
self.idioma = idioma
@@ -17,5 +20,9 @@ class ProviderDeTranscricaoLocal:
arquivo = Path(clipe.arquivo)
if not arquivo.is_file():
raise FileNotFoundError(f"Arquivo de áudio não encontrado: {arquivo}")
- segmentos, _ = self.modelo.transcribe(str(arquivo), language=self.idioma, vad_filter=True)
- return [SegmentoDeTranscricao(float(s.start), float(s.end), s.text.strip()) for s in segmentos]
+ segmentos, _ = self.modelo.transcribe(str(arquivo), language=self.idioma,
+ vad_filter=True, word_timestamps=True)
+ return [SegmentoDeTranscricao(float(s.start), float(s.end), s.text.strip(),
+ None, tuple(PalavraDeTranscricao(w.word.strip(), float(w.start), float(w.end),
+ getattr(w, "probability", None))
+ for w in (s.words or []) if w.word.strip())) for s in segmentos]
diff --git a/code/engine/requirements-visual.txt b/code/engine/requirements-visual.txt
new file mode 100644
index 0000000..1e10590
--- /dev/null
+++ b/code/engine/requirements-visual.txt
@@ -0,0 +1,7 @@
+# Dependências opcionais para os adapters locais de análise visual.
+opencv-python
+scenedetect
+onnxruntime
+mediapipe
+# Apenas macOS; permite usar Vision diretamente a partir do Python.
+pyobjc-framework-Vision
diff --git a/code/engine/scanner/__init__.py b/code/engine/scanner/__init__.py
index d9d008b..dc2a4bc 100644
--- a/code/engine/scanner/__init__.py
+++ b/code/engine/scanner/__init__.py
@@ -1,7 +1,22 @@
from .coordenacao import AnalisadorDeTimeline, ContextoDeAnalise, PipelineDoScanner
-
-__all__ = ["AnalisadorDeTimeline", "ContextoDeAnalise", "PipelineDoScanner"]
-from .modelos import Cena, ErroDeAnalise, Evento, ResultadoDaAnalise, SegmentoDeTranscricao, StatusDaAnalise
+from .descoberta import ArquivoDescoberto, DescobertaDeArquivos, LeitorLocalDeArquivos
+from .metadados import ExtracaoDeMetadados, MetadadosDoArquivo
+from .modelos import (Cena, CenaVisual, EvidenciaVisual, ErroDeAnalise, Evento, ObservacaoVisual,
+ PalavraDeTranscricao,
+ ResultadoDaAnalise, SegmentoDeTranscricao, StatusDaAnalise)
from .transcricao_da_timeline import TranscricaoDaTimeline, TranscricaoDoClipe
+from .visual import (AnaliseVisualDaTimeline, DetectorDeCenas, ResultadoVisualDoClipe,
+ criar_detector_apple_vision, criar_detector_visual_local)
+from .relatorio_visual import gerar_relatorio_visual, gerar_resumo_visual_markdown
+from .configuracao_visual import ConfiguracaoVisualDoScanner
-__all__ = ["Cena", "ErroDeAnalise", "Evento", "ResultadoDaAnalise", "SegmentoDeTranscricao", "StatusDaAnalise", "TranscricaoDaTimeline", "TranscricaoDoClipe"]
+__all__ = ["AnaliseVisualDaTimeline", "AnalisadorDeTimeline", "ArquivoDescoberto",
+ "Cena", "CenaVisual", "ContextoDeAnalise", "criar_detector_apple_vision", "criar_detector_visual_local", "DescobertaDeArquivos", "DetectorDeCenas",
+ "ConfiguracaoVisualDoScanner",
+ "EvidenciaVisual", "ErroDeAnalise", "Evento", "ExtracaoDeMetadados",
+ "gerar_relatorio_visual",
+ "gerar_resumo_visual_markdown",
+ "LeitorLocalDeArquivos", "MetadadosDoArquivo", "ObservacaoVisual", "PalavraDeTranscricao",
+ "PipelineDoScanner", "ResultadoDaAnalise", "ResultadoVisualDoClipe",
+ "SegmentoDeTranscricao", "StatusDaAnalise", "TranscricaoDaTimeline",
+ "TranscricaoDoClipe"]
diff --git a/code/engine/scanner/configuracao_visual.py b/code/engine/scanner/configuracao_visual.py
new file mode 100644
index 0000000..11e3726
--- /dev/null
+++ b/code/engine/scanner/configuracao_visual.py
@@ -0,0 +1,60 @@
+"""Perfil declarativo que liga a configuração do painel à leitura visual."""
+
+from dataclasses import dataclass
+from typing import Any
+
+
+RECURSOS_VISION = frozenset((
+ "faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "categorias",
+ "estetica", "codigos", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas",
+))
+
+
+@dataclass(frozen=True)
+class ConfiguracaoVisualDoScanner:
+ """Interface curta para uma execução de leitura visual da timeline."""
+
+ intervalo_em_segundos: float = 1.0
+ faixas_de_video: tuple[int, ...] = ()
+ faixas_de_audio: tuple[int, ...] = ()
+ recursos_vision: frozenset[str] = RECURSOS_VISION
+ detectar_cenas: bool = True
+ analisar_continuidade: bool = True
+ preparar_reenquadramento: bool = False
+ interpretar_cena: bool = True
+
+ def __post_init__(self) -> None:
+ if not 0.04 <= self.intervalo_em_segundos <= 60:
+ raise ValueError("intervalo_em_segundos deve estar entre 0,04 e 60.")
+ desconhecidos = self.recursos_vision - RECURSOS_VISION
+ if desconhecidos:
+ raise ValueError(f"Recursos Vision desconhecidos: {', '.join(sorted(desconhecidos))}")
+ if any(indice < 0 for indice in self.faixas_de_video + self.faixas_de_audio):
+ raise ValueError("Índices de faixa não podem ser negativos.")
+
+ @classmethod
+ def de_dict(cls, dados: dict[str, Any]) -> "ConfiguracaoVisualDoScanner":
+ """Lê o mesmo JSON produzido pelo painel, sem vazar detalhes de adapters."""
+ return cls(
+ intervalo_em_segundos=float(dados.get("intervalo_em_segundos", 1)),
+ faixas_de_video=tuple(sorted(set(int(item) for item in dados.get("faixas_de_video", ())))),
+ faixas_de_audio=tuple(sorted(set(int(item) for item in dados.get("faixas_de_audio", ())))),
+ recursos_vision=frozenset(dados.get("recursos_vision", RECURSOS_VISION)),
+ detectar_cenas=bool(dados.get("detectar_cenas", True)),
+ analisar_continuidade=bool(dados.get("analisar_continuidade", True)),
+ preparar_reenquadramento=bool(dados.get("preparar_reenquadramento", False)),
+ interpretar_cena=bool(dados.get("interpretar_cena", True)),
+ )
+
+ def para_dict(self) -> dict[str, Any]:
+ return {
+ "versao": 1,
+ "intervalo_em_segundos": self.intervalo_em_segundos,
+ "faixas_de_video": list(self.faixas_de_video),
+ "faixas_de_audio": list(self.faixas_de_audio),
+ "recursos_vision": sorted(self.recursos_vision),
+ "detectar_cenas": self.detectar_cenas,
+ "analisar_continuidade": self.analisar_continuidade,
+ "preparar_reenquadramento": self.preparar_reenquadramento,
+ "interpretar_cena": self.interpretar_cena,
+ }
diff --git a/code/engine/scanner/coordenacao/__init__.py b/code/engine/scanner/coordenacao/__init__.py
index 8dbeec4..a59e005 100644
--- a/code/engine/scanner/coordenacao/__init__.py
+++ b/code/engine/scanner/coordenacao/__init__.py
@@ -14,7 +14,10 @@ class ContextoDeAnalise:
transcricoes: dict[str, list[Any]] = field(default_factory=dict)
caracteristicas_visuais: dict[str, dict[str, Any]] = field(default_factory=dict)
cenas: list[Any] = field(default_factory=list)
+ cenas_visuais: list[Any] = field(default_factory=list)
eventos: list[Any] = field(default_factory=list)
+ arquivos: dict[str, Any] = field(default_factory=dict)
+ metadados_dos_arquivos: dict[str, Any] = field(default_factory=dict)
class Analisador(Protocol):
@@ -42,5 +45,5 @@ class AnalisadorDeTimeline:
def __init__(self, pipeline: PipelineDoScanner) -> None:
self.pipeline = pipeline
- def analisar(self) -> ContextoDeAnalise:
- return self.pipeline.executar(ContextoDeAnalise())
+ def analisar(self, timeline: Timeline | None = None) -> ContextoDeAnalise:
+ return self.pipeline.executar(ContextoDeAnalise(timeline=timeline))
diff --git a/code/engine/scanner/descoberta/__init__.py b/code/engine/scanner/descoberta/__init__.py
index a1a2821..51f2ced 100644
--- a/code/engine/scanner/descoberta/__init__.py
+++ b/code/engine/scanner/descoberta/__init__.py
@@ -1,3 +1,4 @@
from .descoberta_da_timeline import DescobertaDaTimeline
+from .descoberta_de_arquivos import ArquivoDescoberto, DescobertaDeArquivos, LeitorLocalDeArquivos
-__all__ = ["DescobertaDaTimeline"]
+__all__ = ["ArquivoDescoberto", "DescobertaDaTimeline", "DescobertaDeArquivos", "LeitorLocalDeArquivos"]
diff --git a/code/engine/scanner/descoberta/descoberta_de_arquivos.py b/code/engine/scanner/descoberta/descoberta_de_arquivos.py
new file mode 100644
index 0000000..cd10dcf
--- /dev/null
+++ b/code/engine/scanner/descoberta/descoberta_de_arquivos.py
@@ -0,0 +1,97 @@
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Protocol
+
+from ...dominio import Clipe
+from ...scanner.modelos import ErroDeAnalise
+from ..coordenacao import ContextoDeAnalise
+
+
+@dataclass(frozen=True)
+class ArquivoDescoberto:
+ """Resultado da validação do arquivo associado a um clipe."""
+
+ caminho: Path
+ existe: bool
+ acessivel: bool
+ tipo_de_midia: str | None = None
+ tamanho_em_bytes: int | None = None
+
+ @property
+ def offline(self) -> bool:
+ return not self.existe or not self.acessivel
+
+
+class LeitorDeArquivos(Protocol):
+ def descobrir(self, caminho: str | Path) -> ArquivoDescoberto: ...
+
+
+class LeitorLocalDeArquivos:
+ """Adapter que resolve e valida caminhos no sistema de arquivos local."""
+
+ def descobrir(self, caminho: str | Path) -> ArquivoDescoberto:
+ caminho_resolvido = Path(caminho).expanduser().resolve(strict=False)
+ existe = caminho_resolvido.is_file()
+ acessivel = existe
+ tamanho = None
+ if existe:
+ try:
+ tamanho = caminho_resolvido.stat().st_size
+ with caminho_resolvido.open("rb"):
+ pass
+ except (OSError, PermissionError):
+ acessivel = False
+ return ArquivoDescoberto(
+ caminho=caminho_resolvido,
+ existe=existe,
+ acessivel=acessivel,
+ tipo_de_midia=caminho_resolvido.suffix.lower().lstrip(".") or None,
+ tamanho_em_bytes=tamanho,
+ )
+
+
+class DescobertaDeArquivos:
+ """Relaciona os clipes da timeline aos arquivos físicos de origem."""
+
+ nome = "descoberta_de_arquivos"
+
+ def __init__(self, leitor: LeitorDeArquivos | None = None) -> None:
+ self.leitor = leitor or LeitorLocalDeArquivos()
+
+ def executar(self, contexto: ContextoDeAnalise) -> ContextoDeAnalise:
+ if contexto.timeline is None:
+ return contexto
+
+ vistos: dict[Path, str] = {}
+ for clipe in self._clipes(contexto):
+ if not clipe.arquivo:
+ clipe.offline = True
+ self._registrar_erro(contexto, "arquivo_ausente", "Clipe sem sourceFile.", clipe)
+ continue
+ try:
+ descoberto = self.leitor.descobrir(clipe.arquivo)
+ except (OSError, ValueError, TypeError) as exc:
+ clipe.offline = True
+ self._registrar_erro(contexto, "arquivo_inacessivel", str(exc), clipe)
+ continue
+
+ clipe.arquivo = str(descoberto.caminho)
+ clipe.offline = descoberto.offline
+ contexto.arquivos[clipe.identificador] = descoberto
+ if descoberto.offline:
+ self._registrar_erro(contexto, "arquivo_inacessivel", "Arquivo não encontrado ou sem permissão de leitura.", clipe)
+ elif descoberto.caminho in vistos:
+ contexto.avisos.append(
+ f"Arquivo duplicado entre os clipes {vistos[descoberto.caminho]} e {clipe.identificador}."
+ )
+ else:
+ vistos[descoberto.caminho] = clipe.identificador
+ return contexto
+
+ @staticmethod
+ def _clipes(contexto: ContextoDeAnalise) -> list[Clipe]:
+ return [clipe for faixa in contexto.timeline.faixas for clipe in faixa.clipes]
+
+ @staticmethod
+ def _registrar_erro(contexto: ContextoDeAnalise, codigo: str, mensagem: str, clipe: Clipe) -> None:
+ contexto.erros.append(str(ErroDeAnalise(codigo, mensagem, f"clipe[{clipe.identificador}].sourceFile")))
diff --git a/code/engine/scanner/metadados.py b/code/engine/scanner/metadados.py
new file mode 100644
index 0000000..8a90e81
--- /dev/null
+++ b/code/engine/scanner/metadados.py
@@ -0,0 +1,40 @@
+from typing import Protocol
+
+from ..integracoes.midia.extracao_de_metadados import (
+ ExtracaoDeMetadados as ExtratorDeMetadados,
+ MetadadosDoArquivo,
+)
+from .coordenacao import ContextoDeAnalise
+
+
+class ExtratorDeMetadadosProtocol(Protocol):
+ def extrair(self, arquivo: str) -> MetadadosDoArquivo: ...
+
+
+class ExtracaoDeMetadados:
+ """Enriquece cada clipe com os metadados técnicos do seu arquivo."""
+
+ nome = "extracao_de_metadados"
+
+ def __init__(self, extrator: ExtratorDeMetadadosProtocol | None = None) -> None:
+ self.extrator = extrator or ExtratorDeMetadados()
+
+ def executar(self, contexto: ContextoDeAnalise) -> ContextoDeAnalise:
+ for identificador, arquivo in contexto.arquivos.items():
+ if arquivo.offline:
+ continue
+ try:
+ metadados = self.extrator.extrair(str(arquivo.caminho))
+ contexto.metadados_dos_arquivos[identificador] = metadados
+ for faixa in contexto.timeline.faixas if contexto.timeline else []:
+ for clipe in faixa.clipes:
+ if clipe.identificador == identificador:
+ clipe.metadados["arquivo"] = metadados
+ break
+ except Exception as exc:
+ # Uma mídia inválida não deve descartar os resultados dos demais clipes.
+ contexto.erros.append(f"metadados_indisponiveis: {arquivo.caminho}: {exc}")
+ return contexto
+
+
+__all__ = ["ExtracaoDeMetadados", "MetadadosDoArquivo"]
diff --git a/code/engine/scanner/modelos.py b/code/engine/scanner/modelos.py
index ebb5c6b..2f48ec7 100644
--- a/code/engine/scanner/modelos.py
+++ b/code/engine/scanner/modelos.py
@@ -28,12 +28,32 @@ class ResultadoDaAnalise:
avisos: list[str] = field(default_factory=list)
+@dataclass(frozen=True)
+class PalavraDeTranscricao:
+ texto: str
+ inicio: float
+ fim: float
+ confianca: float | None = None
+ falante: str | None = None
+
+ def __post_init__(self) -> None:
+ if self.inicio < 0 or self.fim < self.inicio:
+ raise ValueError("Intervalo de palavra inválido.")
+
+
@dataclass(frozen=True)
class SegmentoDeTranscricao:
inicio: float
fim: float
texto: str
confianca: float | None = None
+ palavras: tuple[PalavraDeTranscricao, ...] = ()
+ emocao: str | None = None
+ confianca_emocao: float | None = None
+ caracteristicas_acusticas: dict[str, float] = field(default_factory=dict)
+ falante: str | None = None
+ voz_aparente: str | None = None
+ confianca_voz: float | None = None
def __post_init__(self) -> None:
if self.inicio < 0 or self.fim < self.inicio:
@@ -54,3 +74,38 @@ class Evento:
inicio: float
fim: float
confianca: float | None = None
+
+
+@dataclass(frozen=True)
+class EvidenciaVisual:
+ """Fato visual normalizado, independente da biblioteca que o produziu."""
+
+ tipo: str
+ inicio: float
+ fim: float
+ valor: dict[str, Any]
+ confianca: float | None = None
+ provider: str = ""
+ modelo: str | None = None
+
+ def __post_init__(self) -> None:
+ if self.inicio < 0 or self.fim < self.inicio:
+ raise ValueError("Intervalo de evidência visual inválido.")
+
+
+ObservacaoVisual = EvidenciaVisual
+
+
+@dataclass(frozen=True)
+class CenaVisual:
+ """Intervalo visual enriquecido com as observações que o sustentam."""
+
+ identificador_do_clipe: str
+ inicio: float
+ fim: float
+ observacoes: tuple[EvidenciaVisual, ...] = ()
+ referencias_de_cena: tuple[float, ...] = ()
+
+ def __post_init__(self) -> None:
+ if self.inicio < 0 or self.fim < self.inicio:
+ raise ValueError("Intervalo de cena visual inválido.")
diff --git a/code/engine/scanner/relatorio_visual.py b/code/engine/scanner/relatorio_visual.py
new file mode 100644
index 0000000..a8ed9a3
--- /dev/null
+++ b/code/engine/scanner/relatorio_visual.py
@@ -0,0 +1,79 @@
+"""Exportação estruturada de uma leitura visual de clipe."""
+
+from collections import defaultdict
+from dataclasses import asdict
+from datetime import datetime, timezone
+import json
+from pathlib import Path
+
+from ..dominio import Clipe
+from .visual import ResultadoVisualDoClipe
+
+
+VERSAO_DO_RELATORIO_VISUAL = "1.0"
+
+
+def gerar_relatorio_visual(clipe: Clipe, resultado: ResultadoVisualDoClipe,
+ destino: str | Path, intervalo_de_amostragem: float) -> Path:
+ """Grava um JSON autocontido, com fatos por frame e fatos temporais separados."""
+ if intervalo_de_amostragem <= 0:
+ raise ValueError("intervalo_de_amostragem deve ser positivo.")
+ caminho = Path(destino)
+ caminho.parent.mkdir(parents=True, exist_ok=True)
+ caminho.write_text(json.dumps(_dados_do_relatorio(clipe, resultado, intervalo_de_amostragem),
+ ensure_ascii=False, indent=2), encoding="utf-8")
+ return caminho
+
+
+def gerar_resumo_visual_markdown(clipe: Clipe, resultado: ResultadoVisualDoClipe,
+ destino: str | Path, intervalo_de_amostragem: float) -> Path:
+ """Grava uma visão humana do mesmo resultado exportado em JSON."""
+ caminho = Path(destino)
+ caminho.parent.mkdir(parents=True, exist_ok=True)
+ dados = _dados_do_relatorio(clipe, resultado, intervalo_de_amostragem)
+ linhas = [f"# Relatório visual — {clipe.nome}", "",
+ f"- Clipe: `{clipe.identificador}`",
+ f"- Origem: `{clipe.arquivo}`",
+ f"- Timeline: {clipe.intervalo_na_timeline.inicio:.3f}s–{clipe.intervalo_na_timeline.fim:.3f}s",
+ f"- Amostragem: a cada {intervalo_de_amostragem:g} segundo(s)", "",
+ "## Observações por frame", ""]
+ for frame in dados["observacoes_por_frame"]:
+ tipos = ", ".join(item["tipo"] for item in frame["evidencias"])
+ linhas.extend([f"### Origem {frame['timestamp_na_origem']:.3f}s", "", tipos or "Sem evidências.", ""])
+ linhas.extend(["## Continuidade", ""])
+ for evidencia in dados["evidencias_temporais"]:
+ linhas.append(f"- {evidencia['tipo']}: {evidencia['inicio']:.3f}s–{evidencia['fim']:.3f}s — "
+ f"`{json.dumps(evidencia['valor'], ensure_ascii=False)}`")
+ caminho.write_text("\n".join(linhas) + "\n", encoding="utf-8")
+ return caminho
+
+
+def _dados_do_relatorio(clipe: Clipe, resultado: ResultadoVisualDoClipe,
+ intervalo_de_amostragem: float) -> dict:
+ por_frame = defaultdict(list)
+ temporais = []
+ for evidencia in resultado.evidencias:
+ item = asdict(evidencia)
+ if evidencia.inicio == evidencia.fim:
+ por_frame[evidencia.inicio].append(item)
+ else:
+ temporais.append(item)
+ origem = clipe.intervalo_na_origem
+ return {
+ "versao": VERSAO_DO_RELATORIO_VISUAL,
+ "gerado_em": datetime.now(timezone.utc).isoformat(),
+ "clipe": {
+ "identificador": clipe.identificador,
+ "nome": clipe.nome,
+ "arquivo_original": clipe.arquivo,
+ "intervalo_na_timeline": asdict(clipe.intervalo_na_timeline),
+ "intervalo_na_origem": asdict(origem) if origem else None,
+ },
+ "amostragem": {"intervalo_em_segundos": intervalo_de_amostragem},
+ "observacoes_por_frame": [
+ {"timestamp_na_origem": timestamp, "evidencias": evidencias}
+ for timestamp, evidencias in sorted(por_frame.items())
+ ],
+ "evidencias_temporais": temporais,
+ "cenas": [asdict(cena) for cena in resultado.cenas_visuais],
+ }
diff --git a/code/engine/scanner/retakes/adaptador_de_falas.py b/code/engine/scanner/retakes/adaptador_de_falas.py
new file mode 100644
index 0000000..1827cce
--- /dev/null
+++ b/code/engine/scanner/retakes/adaptador_de_falas.py
@@ -0,0 +1,116 @@
+"""Converte a transcrição existente em falas ordenadas para a análise.
+
+O módulo de retakes não realiza transcrição. Ele recebe a saída do
+``TranscricaoDaTimeline`` (lista de ``TranscricaoDoClipe``) e a converte
+em ``Fala``s ordenadas ao longo da timeline, preservando o vínculo com o
+segmento/clipe de origem e as palavras alinhadas quando disponíveis.
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Iterable
+
+from ..transcricao_da_timeline import TranscricaoDoClipe
+from .modelos_de_retakes import Fala
+
+# Sinais de erro mais comuns vindos dos providers de transcrição.
+_PALAVRAS_DE_ERRO = {"", "...", "…"}
+
+_VOYELS = "aeiouáéíóúâêôãõàèìòù"
+_CONSOANTES = "bcdfghjklmnpqrstvwxyz"
+_PADRAO_SILABA = re.compile(
+ rf"([{_VOYELS}][^aeiouáéíóúâêôãõàèìòù]*)|([{_CONSOANTES}]+[aeiouáéíóúâêôãõàèìòù]?)",
+ re.IGNORECASE,
+)
+
+
+def _normalizar(texto: str) -> str:
+ """Minúsculas, sem pontuação e sem espaços duplicados."""
+ sem_pontuacao = re.sub(r"[^\w\s]", " ", texto)
+ return " ".join(sem_pontuacao.lower().split())
+
+
+def _prefixo_comum(a: list[str], b: list[str]) -> int:
+ n = 0
+ for x, y in zip(a, b):
+ if x != y:
+ break
+ n += 1
+ return n
+
+
+def _sucesso_de_silabas(a: list[str], b: list[str]) -> float:
+ if not a or not b:
+ return 0.0
+ silabas_a = [_PADRAO_SILABA.findall(p)[0][0] for p in a]
+ silabas_b = [_PADRAO_SILABA.findall(p)[0][0] for p in b]
+ n = _prefixo_comum(silabas_a, silabas_b)
+ return n / max(len(silabas_a), len(silabas_b))
+
+
+def _e_ruido(texto: str) -> bool:
+ texto_limpo = _normalizar(texto)
+ if texto_limpo in _PALAVRAS_DE_ERRO:
+ return True
+ # Fala em branco ou "vazia" por provedor.
+ return not texto_limpo
+
+
+class AdaptadorDeFalas:
+ """Transforma a transcrição da timeline em falas prontas para análise.
+
+ Recebe uma coleção de ``TranscricaoDoClipe`` (uma por faixa de áudio do
+ vídeo) e devolve as falas ordenadas por tempo. O ``video_id`` é um rótulo
+ informado pelo chamador; quando ausente, usa o identificador da timeline.
+ """
+
+ def __init__(self, video_id: str | None = None, faixa_id: str | None = None) -> None:
+ self.video_id = video_id
+ self.faixa_id = faixa_id
+
+ def converter(
+ self,
+ transcricoes: Iterable[TranscricaoDoClipe],
+ video_id: str | None = None,
+ faixa_id: str | None = None,
+ ) -> list[Fala]:
+ video_id = video_id or self.video_id or "video"
+ faixa_id = faixa_id or self.faixa_id or "faixa"
+
+ falas: list[Fala] = []
+ for transcricao in transcricoes:
+ segmento_id = transcricao.identificador_do_clipe
+ for segmento in transcricao.segmentos:
+ if _e_ruido(segmento.texto):
+ continue
+ falas.append(Fala(
+ id=f"{segmento_id}:{segmento.inicio:.3f}",
+ video_id=video_id,
+ faixa_id=faixa_id,
+ segmento_id=segmento_id,
+ ordem=-1, # preenchida após a ordenação
+ inicio=segmento.inicio,
+ fim=segmento.fim,
+ texto=segmento.texto,
+ texto_normalizado=_normalizar(segmento.texto),
+ palavras=segmento.palavras,
+ falante=segmento.falante,
+ ))
+
+ falas.sort(key=lambda item: (item.inicio, item.fim))
+ for indice, fala in enumerate(falas):
+ falas[indice] = Fala(
+ id=fala.id,
+ video_id=fala.video_id,
+ faixa_id=fala.faixa_id,
+ segmento_id=fala.segmento_id,
+ ordem=indice,
+ inicio=fala.inicio,
+ fim=fala.fim,
+ texto=fala.texto,
+ texto_normalizado=fala.texto_normalizado,
+ palavras=fala.palavras,
+ falante=fala.falante,
+ )
+ return falas
\ No newline at end of file
diff --git a/code/engine/scanner/retakes/agrupador_de_takes.py b/code/engine/scanner/retakes/agrupador_de_takes.py
new file mode 100644
index 0000000..f37d45f
--- /dev/null
+++ b/code/engine/scanner/retakes/agrupador_de_takes.py
@@ -0,0 +1,148 @@
+"""Agrupamento de falas que representam retakes da mesma fala ou ideia.
+
+Trabalha com uma janela temporal: compara cada fala com as ``n`` seguintes
+(e só as da mesma faixa de áudio), evitando agrupar frases iguais que
+aparecem em partes muito distantes do vídeo. Não decide a classificação —
+apenas forma grupos candidatos e reúne as métricas.
+"""
+
+from __future__ import annotations
+
+from typing import Iterable
+
+from .analisador_de_intervalos import AnalisadorDeIntervalos
+from .comparador_de_falas import ComparadorDeFalas
+from .comparador_semantico import ComparadorSemantico
+from .detector_de_reinicios import DetectorDeReinicios
+from .modelos_de_retakes import (
+ AgrupamentoDeFalas,
+ Fala,
+ ParAgrupado,
+ ResultadoDoIntervalo,
+ SinalDeReinicio,
+)
+
+# Limiares de candidatura de um par a pertencer a um grupo de retake.
+_SIMILARIDADE_MINIMA_CANDIDATO = 0.55
+_SIMILARIDADE_FORTE = 0.75
+_PROXIMIDADE_AUXILIAR = 0.60
+
+# Janela padrão de falas a serem comparadas com cada uma.
+_JANELA_PADRAO = 5
+
+# Região de dúvida que dispara a comparação semântica.
+_ZONA_DE_DUVIDA_INFERIOR = 0.60
+_ZONA_DE_DUVIDA_SUPERIOR = 0.85
+
+
+class AgrupadorDeTakes:
+ """Gera grupos candidatos conectando falas semelhantes e próximas."""
+
+ def __init__(
+ self,
+ comparador: ComparadorDeFalas,
+ comparador_semantico: ComparadorSemantico,
+ analisador_de_intervalos: AnalisadorDeIntervalos,
+ detector_de_reinicios: DetectorDeReinicios,
+ janela: int = _JANELA_PADRAO,
+ ) -> None:
+ self.comparador = comparador
+ self.comparador_semantico = comparador_semantico
+ self.analisador_de_intervalos = analisador_de_intervalos
+ self.detector_de_reinicios = detector_de_reinicios
+ self.janela = janela
+
+ def agrupar(self, falas: Iterable[Fala]) -> list[AgrupamentoDeFalas]:
+ falas = sorted(falas, key=lambda item: (item.ordem, item.inicio))
+ sinais = {sinal.fala_id: sinal for sinal in self.detector_de_reinicios.detectar(falas)}
+ arestas: list[tuple[int, int, ParAgrupado]] = []
+
+ for indice, fala_a in enumerate(falas):
+ limite = min(len(falas), indice + 1 + self.janela)
+ for jindice in range(indice + 1, limite):
+ fala_b = falas[jindice]
+ if fala_a.faixa_id != fala_b.faixa_id:
+ continue
+ par = self._avaliar_par(fala_a, fala_b, sinais)
+ if par is not None:
+ arestas.append((indice, jindice, par))
+
+ return self._agrupar_por_arestas(falas, arestas, sinais)
+
+ def _avaliar_par(
+ self,
+ fala_a: Fala,
+ fala_b: Fala,
+ sinais: dict[str, SinalDeReinicio],
+ ) -> ParAgrupado | None:
+ comparacao = self.comparador.comparar(fala_a, fala_b)
+ intervalo = self.analisador_de_intervalos.analisar(fala_a, fala_b)
+
+ tem_reinicio = sinais.get(fala_b.id) is not None
+ semantica = 0.0
+
+ # Comparação semântica só na zona de dúvida, para evitar custo.
+ texto = comparacao.similaridade_final
+ if _ZONA_DE_DUVIDA_INFERIOR <= texto <= _ZONA_DE_DUVIDA_SUPERIOR:
+ semantica = self.comparador_semantico.comparar(fala_a, fala_b)
+
+ melhor = max(comparacao.similaridade_final, semantica)
+ candidato = (
+ melhor >= _SIMILARIDADE_FORTE
+ or (melhor >= _SIMILARIDADE_MINIMA_CANDIDATO
+ and intervalo.proximidade_temporal >= _PROXIMIDADE_AUXILIAR
+ and (tem_reinicio or intervalo.indicio_de_nova_tentativa))
+ )
+ if candidato:
+ return ParAgrupado(fala_a, fala_b, comparacao, intervalo,
+ round(semantica, 4))
+ return None
+
+ @staticmethod
+ def _agrupar_por_arestas(
+ falas: list[Fala],
+ arestas: list[tuple[int, int, ParAgrupado]],
+ sinais: dict[str, SinalDeReinicio],
+ ) -> list[AgrupamentoDeFalas]:
+ pai = list(range(len(falas)))
+
+ def encontrar(x: int) -> int:
+ while pai[x] != x:
+ pai[x] = pai[pai[x]]
+ x = pai[x]
+ return x
+
+ arestas.sort(key=lambda item: (item[0], item[1]))
+ for a, b, _ in arestas:
+ raiz_a, raiz_b = encontrar(a), encontrar(b)
+ if raiz_a != raiz_b:
+ pai[raiz_b] = raiz_a
+
+ componentes: dict[int, list[int]] = {}
+ for indice in range(len(falas)):
+ componentes.setdefault(encontrar(indice), []).append(indice)
+
+ grupos: list[AgrupamentoDeFalas] = []
+ for inds in componentes.values():
+ if len(inds) < 2:
+ continue
+ inds.sort()
+ fala_ids = tuple(falas[indice].id for indice in inds)
+ pares_por_chave: dict[tuple[str, str], ParAgrupado] = {
+ (par.fala_a.id, par.fala_b.id): par
+ for _, _, par in arestas
+ if par.fala_a.id in fala_ids and par.fala_b.id in fala_ids
+ }
+ pares: list[ParAgrupado] = []
+ for a, b in zip(inds, inds[1:]):
+ chave = (falas[a].id, falas[b].id)
+ par = pares_por_chave.get(chave)
+ if par is None:
+ par = pares_por_chave.get((falas[b].id, falas[a].id))
+ if par is not None:
+ pares.append(par)
+ sinais_do_grupo = tuple(
+ sinais[fala_id] for fala_id in fala_ids if fala_id in sinais)
+ if pares:
+ grupos.append(AgrupamentoDeFalas(fala_ids, tuple(pares), sinais_do_grupo))
+ return grupos
\ No newline at end of file
diff --git a/code/engine/scanner/retakes/analisador_de_intervalos.py b/code/engine/scanner/retakes/analisador_de_intervalos.py
new file mode 100644
index 0000000..cbaf95a
--- /dev/null
+++ b/code/engine/scanner/retakes/analisador_de_intervalos.py
@@ -0,0 +1,41 @@
+"""Análise da relação temporal entre falas.
+
+Verifica quanto tempo há entre duas falas, se estão na mesma faixa/segmento
+e se a proximidade sugere uma nova tentativa. Um intervalo muito grande
+reduz a confiança de que seja um retake imediato.
+"""
+
+from __future__ import annotations
+
+from .modelos_de_retakes import Fala, ResultadoDoIntervalo
+
+# A partir de quantos segundos o intervalo passa a não sugerir retake.
+_INTERVALO_MAXIMO = 12.0
+
+
+class AnalisadorDeIntervalos:
+ """Calcula a proximidade temporal e o indício de nova tentativa."""
+
+ def __init__(self, intervalo_maximo: float = _INTERVALO_MAXIMO) -> None:
+ self.intervalo_maximo = intervalo_maximo
+
+ def analisar(self, fala_a: Fala, fala_b: Fala) -> ResultadoDoIntervalo:
+ intervalo = max(0.0, fala_b.inicio - fala_a.fim)
+ mesma_faixa = fala_a.faixa_id == fala_b.faixa_id
+ mesmo_segmento = mesma_faixa and fala_a.segmento_id == fala_b.segmento_id
+
+ if intervalo >= self.intervalo_maximo:
+ proximidade = 0.0
+ elif intervalo <= 0:
+ proximidade = 1.0
+ else:
+ # Decai suavemente com o intervalo: 1.0 → ~0 em 12s.
+ proximidade = max(0.0, 1.0 - intervalo / self.intervalo_maximo)
+
+ indicio = mesma_faixa and intervalo <= self.intervalo_maximo and proximidade >= 0.5
+ return ResultadoDoIntervalo(
+ intervalo_em_segundos=round(intervalo, 3),
+ mesmo_segmento=mesmo_segmento,
+ proximidade_temporal=round(proximidade, 4),
+ indicio_de_nova_tentativa=bool(indicio),
+ )
\ No newline at end of file
diff --git a/code/engine/scanner/retakes/comparador_de_falas.py b/code/engine/scanner/retakes/comparador_de_falas.py
new file mode 100644
index 0000000..bb7a8a7
--- /dev/null
+++ b/code/engine/scanner/retakes/comparador_de_falas.py
@@ -0,0 +1,112 @@
+"""Comparação textual entre falas.
+
+Responsável pelos algoritmos de similaridade: Jaccard (conjunto de
+palavras), sequência (ordem das palavras via maior subsequência comum) e
+similaridade do início/fim da frase. Não decide nada sozinho — apenas
+produz métricas para o classificador.
+"""
+
+from __future__ import annotations
+
+import re
+
+from .modelos_de_retakes import Fala, ResultadoDaComparacao
+
+
+def _normalizar(texto: str) -> str:
+ """Minúsculas, sem pontuação e sem espaços duplicados."""
+ sem_pontuacao = re.sub(r"[^\w\s]", " ", texto)
+ return " ".join(sem_pontuacao.lower().split())
+
+
+def _lcs(a: list[str], b: list[str]) -> int:
+ """Tamanho da maior subsequência comum preservando a ordem."""
+ anterior = [0] * (len(b) + 1)
+ for palavra_a in a:
+ atual = [0] * (len(b) + 1)
+ for j, palavra_b in enumerate(b):
+ if palavra_a == palavra_b:
+ atual[j + 1] = anterior[j] + 1
+ else:
+ atual[j + 1] = max(atual[j], anterior[j + 1])
+ anterior = atual
+ return anterior[-1]
+
+
+class ComparadorDeFalas:
+ """Compara duas falas e calcula as métricas de similaridade textual."""
+
+ @staticmethod
+ def normalizar(texto: str) -> str:
+ return _normalizar(texto)
+
+ @staticmethod
+ def _palavras_de(fala: Fala) -> list[str]:
+ return fala.texto_normalizado.split() or _normalizar(fala.texto).split()
+
+ def comparar(self, fala_a: Fala, fala_b: Fala) -> ResultadoDaComparacao:
+ palavras_a = self._palavras_de(fala_a)
+ palavras_b = self._palavras_de(fala_b)
+
+ if not palavras_a or not palavras_b:
+ return ResultadoDaComparacao(fala_a.id, fala_b.id)
+
+ # 1. Jaccard — conjunto de palavras (rápido, ótimo candidato).
+ conjunto_a = set(palavras_a)
+ conjunto_b = set(palavras_b)
+ intersecao = len(conjunto_a & conjunto_b)
+ uniao = len(conjunto_a | conjunto_b)
+ jaccard = intersecao / uniao if uniao else 0.0
+
+ # 2. Sequência — ordem das palavras via LCS normalizada.
+ lcs = _lcs(palavras_a, palavras_b)
+ sequencia = lcs / max(len(palavras_a), len(palavras_b))
+
+ # 3. Início e final da frase.
+ inicio = self._similaridade_de_prefijo(palavras_a, palavras_b)
+ final = self._similaridade_de_sufixo(palavras_a, palavras_b)
+
+ # 4. Similaridade final ponderada.
+ fim = 0.45 * sequencia + 0.30 * jaccard + 0.15 * inicio + 0.10 * final
+ return ResultadoDaComparacao(
+ fala_a_id=fala_a.id,
+ fala_b_id=fala_b.id,
+ similaridade_jaccard=round(jaccard, 4),
+ similaridade_de_sequencia=round(sequencia, 4),
+ similaridade_do_inicio=round(inicio, 4),
+ similaridade_do_final=round(final, 4),
+ similaridade_final=round(min(1.0, fim), 4),
+ )
+
+ @staticmethod
+ def _similaridade_de_prefijo(a: list[str], b: list[str]) -> float:
+ if not a or not b:
+ return 0.0
+ n = 0
+ for x, y in zip(a, b):
+ if x != y:
+ break
+ n += 1
+ return n / max(len(a), len(b))
+
+ @staticmethod
+ def _similaridade_de_sufixo(a: list[str], b: list[str]) -> float:
+ if not a or not b:
+ return 0.0
+ n = 0
+ for x, y in zip(reversed(a), reversed(b)):
+ if x != y:
+ break
+ n += 1
+ return n / max(len(a), len(b))
+
+ @staticmethod
+ def prefixo_comum_em_palavras(texto_a: str, texto_b: str) -> int:
+ a = _normalizar(texto_a).split()
+ b = _normalizar(texto_b).split()
+ n = 0
+ for x, y in zip(a, b):
+ if x != y:
+ break
+ n += 1
+ return n
\ No newline at end of file
diff --git a/code/engine/scanner/retakes/comparador_semantico.py b/code/engine/scanner/retakes/comparador_semantico.py
new file mode 100644
index 0000000..f62cfe9
--- /dev/null
+++ b/code/engine/scanner/retakes/comparador_semantico.py
@@ -0,0 +1,113 @@
+"""Comparação semântica entre falas (opcional).
+
+A similaridade textual não cobre casos em que as palavras são diferentes
+mas a ideia é a mesma. Este módulo define um protocolo para providers de
+embeddings e uma implementação local (via Hugging Face Transformers), além
+de um fallback puramente lexical usado quando nenhum provider está
+disponível.
+
+O `ComparadorSemantico` nunca decide sozinho que há retake — apenas
+fornece uma métrica adicional para o classificador.
+"""
+
+from __future__ import annotations
+
+import math
+from typing import Protocol
+
+from .modelos_de_retakes import Fala
+
+
+class ProviderDeSimilaridadeSemantica(Protocol):
+ """Contrato para quem calcula similaridade semântica entre dois textos."""
+
+ def similaridade(self, texto_a: str, texto_b: str) -> float: ...
+
+
+class ComparadorLexicalSemantico:
+ """Fallback puramente lexical para quando não há embeddings.
+
+ Reconstrói uma "similaridade semântica" a partir de palavras que
+ compartilham a mesma raiz (stemming simples por prefixo comum) e de
+ sinônimos frequentes em pt-PT. Destina-se a permitir executar a análise
+ sem carregar modelos pesados.
+ """
+
+ _PARES_SINONIMOS = (
+ ({"mostrar", "explicar", "demonstrar", "apresentar"},),
+ ({"sistema", "programa", "aplicativo", "software"},),
+ ({"configurar", "configuracao", "instalar", "ajustar"},),
+ )
+
+ def similaridade(self, texto_a: str, texto_b: str) -> float:
+ palavras_a = {p for p in self._palavras(texto_a)}
+ palavras_b = {p for p in self._palavras(texto_b)}
+ if not palavras_a or not palavras_b:
+ return 0.0
+
+ intersecao = 0.0
+ for palavra_a in palavras_a:
+ for palavra_b in palavras_b:
+ if palavra_a == palavra_b:
+ intersecao += 1.0
+ elif self._mesma_raiz(palavra_a, palavra_b):
+ intersecao += 0.7
+ elif self._mesmo_sinonimo(palavra_a, palavra_b):
+ intersecao += 0.6
+ tam = max(len(palavras_a), len(palavras_b))
+ return round(min(1.0, intersecao / tam), 4)
+
+ @staticmethod
+ def _palavras(texto: str) -> list[str]:
+ return [p for p in texto.lower().split()]
+
+ @staticmethod
+ def _mesma_raiz(a: str, b: str) -> bool:
+ raiz = min(len(a), len(b), 4)
+ return raiz >= 4 and a[:raiz] == b[:raiz]
+
+ @staticmethod
+ def _mesmo_sinonimo(a: str, b: str) -> bool:
+ return any(a in grupo and b in grupo for grupo in ComparadorLexicalSemantico._PARES_SINONIMOS)
+
+
+class ComparadorSemantico:
+ """Calcula similaridade semântica usando um provider injetável.
+
+ Quando o provider é ``None``, cai no ``ComparadorLexicalSemantico``
+ para não bloquear a análise na ausência de modelos locais.
+ """
+
+ def __init__(self, provider: ProviderDeSimilaridadeSemantica | None = None) -> None:
+ self.provider = provider or ComparadorLexicalSemantico()
+
+ def comparar(self, fala_a: Fala, fala_b: Fala) -> float:
+ return float(self.provider.similaridade(fala_a.texto, fala_b.texto))
+
+
+class ProviderDeSimilaridadeHuggingFace:
+ """Embeds os textos localmente com um modelo de embeddings.
+
+ O modelo é carregado de forma preguiçosa para não custar nada até ser
+ de fato necessário (zona de dúvida do classificador).
+ """
+
+ def __init__(self, modelo: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2") -> None:
+ self.modelo = modelo
+ self._encoder = None
+
+ def similaridade(self, texto_a: str, texto_b: str) -> float:
+ if self._encoder is None:
+ from sentence_transformers import SentenceTransformer
+ self._encoder = SentenceTransformer(self.modelo)
+ embeddings = self._encoder.encode([texto_a, texto_b], normalize_embeddings=True)
+ return float(_cosseno(embeddings[0], embeddings[1]))
+
+
+def _cosseno(a: list[float], b: list[float]) -> float:
+ produto = sum(x * y for x, y in zip(a, b))
+ norma_a = math.sqrt(sum(x * x for x in a))
+ norma_b = math.sqrt(sum(y * y for y in b))
+ if norma_a == 0 or norma_b == 0:
+ return 0.0
+ return max(0.0, min(1.0, produto / (norma_a * norma_b)))
\ No newline at end of file
diff --git a/code/engine/scanner/retakes/detector_de_reinicios.py b/code/engine/scanner/retakes/detector_de_reinicios.py
new file mode 100644
index 0000000..2ea54ce
--- /dev/null
+++ b/code/engine/scanner/retakes/detector_de_reinicios.py
@@ -0,0 +1,86 @@
+"""Detecção de sinais de reinício/recomeço de fala.
+
+Analisa padrões de que a pessoa começou novamente uma ideia: repetição do
+início de uma frase, pausas longas, marcadores explícitos ("não", "pera",
+"vamos de novo", "desculpa") e repetição de palavras consecutivas.
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Iterable
+
+from .comparador_de_falas import ComparadorDeFalas
+from .modelos_de_retakes import Fala, SinalDeReinicio
+
+_MARCADORES = (
+ "não", "pera", "peraí", "pera ai", "espera", "vamos de novo", "de novo",
+ "vamos recomeçar", "recomeçar", "desculpa", "desculpe", "deixa eu ver",
+ "vou repetir", "esquece", "hmm", "hm", "uhm", "tá", "ta",
+)
+
+_RE_MARCADOR = re.compile(
+ r"\b(?:" + "|".join(re.escape(m) for m in _MARCADORES) + r")\b",
+ re.IGNORECASE,
+)
+
+_PAUSA_LONGA = 1.2 # segundos a partir dos quais a pausa sugere recomeço.
+
+
+class DetectorDeReinicios:
+ """Encontra ``SinalDeReinicio`` em cada fala em relação à anterior."""
+
+ def __init__(
+ self,
+ comparador: ComparadorDeFalas | None = None,
+ pausa_longa: float = _PAUSA_LONGA,
+ ) -> None:
+ self.comparador = comparador or ComparadorDeFalas()
+ self.pausa_longa = pausa_longa
+
+ def detectar(self, falas: Iterable[Fala]) -> list[SinalDeReinicio]:
+ falas = list(falas)
+ sinais: list[SinalDeReinicio] = []
+ for indice, fala in enumerate(falas):
+ sinal = self._analisar(fala, falas[indice - 1] if indice > 0 else None)
+ if sinal:
+ sinais.append(sinal)
+ return sinais
+
+ def _analisar(self, fala: Fala, anterior: Fala | None) -> SinalDeReinicio | None:
+ evidencias: list[str] = []
+ intensidades: list[float] = []
+ palavras = fala.texto_normalizado.split()
+
+ # 1. Marcadores explícitos de recomeço no início.
+ if palavras and _RE_MARCADOR.match(palavras[0]):
+ evidencias.append("marcador explícito de recomeço")
+ intensidades.append(0.9)
+
+ if anterior is None:
+ if intensidades:
+ return SinalDeReinicio(fala.id, "reinicio_explicito",
+ round(max(intensidades), 3), evidencias)
+ return None
+
+ # 2. Pausa longa antes da fala.
+ pausa = fala.inicio - anterior.fim
+ if pausa >= self.pausa_longa:
+ evidencias.append(f"pausa de {pausa:.1f} segundos")
+ intensidades.append(min(1.0, 0.5 + pausa / 8.0))
+
+ # 3. Repetição do início da frase anterior.
+ prefixo = self.comparador.prefixo_comum_em_palavras(anterior.texto, fala.texto)
+ if prefixo >= 3:
+ evidencias.append(f"repetição das primeiras {prefixo} palavras")
+ intensidades.append(min(1.0, 0.4 + prefixo * 0.08))
+
+ if not evidencias:
+ return None
+
+ return SinalDeReinicio(
+ fala_id=fala.id,
+ tipo="repeticao_do_inicio" if any("repetição" in e for e in evidencias) else "reinicio_pos_pausa",
+ intensidade=round(min(1.0, max(intensidades)), 3),
+ evidencias=evidencias,
+ )
\ No newline at end of file
diff --git a/code/engine/scanner/retakes/modelos_de_retakes.py b/code/engine/scanner/retakes/modelos_de_retakes.py
new file mode 100644
index 0000000..b967c25
--- /dev/null
+++ b/code/engine/scanner/retakes/modelos_de_retakes.py
@@ -0,0 +1,154 @@
+"""Modelos de dados do submódulo de detecção de retakes.
+
+Mantemos os mesmos princípios de ``scanner/modelos.py``: dataclasses
+imutáveis (``frozen=True``) com validação em ``__post_init__`` e sem
+dependência de implementações concretas de providers.
+"""
+
+from __future__ import annotations
+
+import uuid
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+
+from ..modelos import PalavraDeTranscricao
+
+
+@dataclass(frozen=True)
+class Fala:
+ """Uma fala normalizada, já posicionada na timeline do vídeo.
+
+ Corresponde a um ``SegmentoDeTranscricao`` convertido pelo adaptador,
+ mas carrega o contexto necessário para a análise temporal e de
+ agrupamento: vídeo, faixa de áudio, número de sequência e o vínculo
+ com o segmento/clipe de origem.
+ """
+
+ id: str
+ video_id: str
+ faixa_id: str
+ segmento_id: str
+ ordem: int
+ inicio: float
+ fim: float
+ texto: str
+ texto_normalizado: str = ""
+ palavras: tuple[PalavraDeTranscricao, ...] = ()
+ falante: str | None = None
+
+ def __post_init__(self) -> None:
+ if self.inicio < 0 or self.fim < self.inicio:
+ raise ValueError(f"Intervalo da fala {self.id} inválido.")
+
+
+@dataclass(frozen=True)
+class SinalDeReinicio:
+ """Indício de que uma fala recomeçou uma ideia já iniciada."""
+
+ fala_id: str
+ tipo: str
+ intensidade: float
+ evidencias: list[str] = field(default_factory=list)
+
+ def __post_init__(self) -> None:
+ if not 0.0 <= self.intensidade <= 1.0:
+ raise ValueError(f"Intensidade do reinício {self.fala_id} fora de [0, 1].")
+
+
+@dataclass(frozen=True)
+class ResultadoDaComparacao:
+ """Resultado da comparação de duas falas, textual e por sequência."""
+
+ fala_a_id: str
+ fala_b_id: str
+ similaridade_jaccard: float = 0.0
+ similaridade_de_sequencia: float = 0.0
+ similaridade_do_inicio: float = 0.0
+ similaridade_do_final: float = 0.0
+ similaridade_final: float = 0.0
+
+
+@dataclass(frozen=True)
+class ResultadoDoIntervalo:
+ """Relação temporal entre duas falas."""
+
+ intervalo_em_segundos: float
+ mesmo_segmento: bool = False
+ proximidade_temporal: float = 0.0
+ indicio_de_nova_tentativa: bool = False
+
+
+@dataclass(frozen=True)
+class EvidenciaDeRetake:
+ """Uma evidência legível que sustenta a classificação de um grupo."""
+
+ tipo: str
+ descricao: str
+ valor: float | None = None
+
+
+@dataclass(frozen=True)
+class TomadaDeRetake:
+ """Uma tomada (fala) pertencente a um grupo de retakes."""
+
+ ordem: int
+ fala_id: str
+ segmento_id: str
+ inicio: float
+ fim: float
+ texto: str
+ similaridade_com_anterior: float = 0.0
+ confianca: float = 0.0
+
+
+def gerar_id_do_grupo() -> str:
+ """Gera um id curto e monótono para um grupo de retakes."""
+ return f"retake_{uuid.uuid4().hex[:6]}"
+
+
+@dataclass(frozen=True)
+class ParAgrupado:
+ """Um par de falas que o agrupador juntou como candidato a retake."""
+
+ fala_a: Fala
+ fala_b: Fala
+ comparacao: ResultadoDaComparacao
+ intervalo: ResultadoDoIntervalo
+ similaridade_semantica: float = 0.0
+
+
+@dataclass(frozen=True)
+class AgrupamentoDeFalas:
+ """Um grupo candidato formado pelo agrupador, antes da classificação."""
+
+ fala_ids: tuple[str, ...]
+ pares: tuple[ParAgrupado, ...]
+ sinais_de_reinicio: tuple[SinalDeReinicio, ...] = ()
+
+
+@dataclass(frozen=True)
+class GrupoDeRetake:
+ """Agrupa as tomadas que representam tentativas da mesma fala."""
+
+ id: str
+ video_id: str
+ faixa_id: str
+ tipo: str
+ confianca: float
+ tomadas: list[TomadaDeRetake] = field(default_factory=list)
+ evidencias: list[EvidenciaDeRetake] = field(default_factory=list)
+ criado_em: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
+
+ def __post_init__(self) -> None:
+ if not 0.0 <= self.confianca <= 1.0:
+ raise ValueError(f"Confiança do grupo {self.id} fora de [0, 1].")
+
+ @property
+ def tomada_principal_id(self) -> str | None:
+ if not self.tomadas:
+ return None
+ return max(self.tomadas, key=lambda item: item.confianca).fala_id
+
+ @property
+ def fala_ids(self) -> list[str]:
+ return [tomada.fala_id for tomada in self.tomadas]
\ No newline at end of file
diff --git a/code/engine/scanner/transcricao_da_timeline.py b/code/engine/scanner/transcricao_da_timeline.py
index 23a92ec..21802eb 100644
--- a/code/engine/scanner/transcricao_da_timeline.py
+++ b/code/engine/scanner/transcricao_da_timeline.py
@@ -1,10 +1,11 @@
from dataclasses import asdict, dataclass
+import hashlib
import json
from pathlib import Path
from typing import Any, Callable
from ..integracoes.midia import ExtracaoDeAudio
-from .modelos import SegmentoDeTranscricao
+from .modelos import PalavraDeTranscricao, SegmentoDeTranscricao
@dataclass(frozen=True)
@@ -14,6 +15,7 @@ class TranscricaoDoClipe:
inicio_na_timeline: float
fim_na_timeline: float
segmentos: list[SegmentoDeTranscricao]
+ intervalo_na_origem: tuple[float, float] | None = None
@property
def texto(self) -> str:
@@ -21,39 +23,168 @@ class TranscricaoDoClipe:
class TranscricaoDaTimeline:
- """Transcreve somente os intervalos efetivamente usados na timeline."""
+ """Transcreve cada arquivo uma vez e projeta o resultado nos cortes.
+
+ A primeira versão suporta apenas mapeamento linear em velocidade normal.
+ Os timestamps retornados pelo provider são sempre relativos ao arquivo
+ original; somente a cópia materializada para cada clipe recebe timestamps
+ da timeline.
+ """
def __init__(self, provider: Any, extrator: ExtracaoDeAudio | None = None,
- diretoria_de_trabalho: str | Path = ".transcricao") -> None:
+ diretoria_de_trabalho: str | Path = ".transcricao",
+ analisador_de_emocao: Any | None = None,
+ diarizador: Any | None = None) -> None:
self.provider = provider
self.extrator = extrator or ExtracaoDeAudio()
self.diretoria_de_trabalho = Path(diretoria_de_trabalho)
+ self.analisador_de_emocao = analisador_de_emocao
+ self.diarizador = diarizador
def executar(self, timeline: Any) -> list[TranscricaoDoClipe]:
- resultados: list[TranscricaoDoClipe] = []
+ clipes: list[Any] = []
for faixa in timeline.faixas:
for clipe in faixa.clipes:
if not clipe.arquivo or clipe.offline:
continue
- intervalo = clipe.intervalo_na_timeline
- pasta = self.diretoria_de_trabalho / clipe.identificador
- origem = clipe.intervalo_na_origem
- partes = self.extrator.extrair_intervalo(
- clipe.arquivo, pasta, origem.inicio if origem else 0.0,
- origem.fim if origem else None,
- )
- segmentos: list[SegmentoDeTranscricao] = []
- for parte in partes:
- for segmento in self.provider.transcrever(type("Audio", (), {"arquivo": str(parte.caminho)})()):
- base_origem = origem.inicio if origem else 0.0
- inicio = max(intervalo.inicio, intervalo.inicio + (parte.inicio - base_origem) + segmento.inicio)
- fim = min(intervalo.fim, intervalo.inicio + (parte.inicio - base_origem) + segmento.fim)
- if inicio < intervalo.fim and fim > inicio:
- segmentos.append(SegmentoDeTranscricao(inicio, fim, segmento.texto, segmento.confianca))
- resultados.append(TranscricaoDoClipe(clipe.identificador, clipe.arquivo,
- intervalo.inicio, intervalo.fim, segmentos))
+ clipes.append(clipe)
+
+ transcricoes: dict[str, list[SegmentoDeTranscricao]] = {}
+ for clipe in clipes:
+ chave = self._chave_do_arquivo(clipe.arquivo)
+ if chave not in transcricoes:
+ transcricoes[chave] = self._transcrever_arquivo(clipe.arquivo, chave)
+
+ resultados: list[TranscricaoDoClipe] = []
+ vistos: set[tuple[str, float, float, float, float]] = set()
+ for clipe in clipes:
+ intervalo = clipe.intervalo_na_timeline
+ origem = clipe.intervalo_na_origem
+ inicio_origem = origem.inicio if origem else 0.0
+ fim_origem = origem.fim if origem else float("inf")
+ chave = (self._chave_do_arquivo(clipe.arquivo), inicio_origem, fim_origem,
+ intervalo.inicio, intervalo.fim)
+ # Vídeo e áudio vinculados podem representar o mesmo corte.
+ if chave in vistos:
+ continue
+ vistos.add(chave)
+ segmentos = []
+ for segmento in transcricoes[chave[0]]:
+ fim = min(segmento.fim, fim_origem)
+ inicio = max(segmento.inicio, inicio_origem)
+ if inicio < fim:
+ palavras = tuple(
+ PalavraDeTranscricao(
+ palavra.texto,
+ intervalo.inicio + max(palavra.inicio, inicio_origem) - inicio_origem,
+ intervalo.inicio + min(palavra.fim, fim_origem) - inicio_origem,
+ palavra.confianca,
+ palavra.falante,
+ )
+ for palavra in segmento.palavras
+ if palavra.inicio < fim_origem and palavra.fim > inicio_origem
+ )
+ segmentos.append(SegmentoDeTranscricao(
+ intervalo.inicio + inicio - inicio_origem,
+ intervalo.inicio + fim - inicio_origem,
+ segmento.texto, segmento.confianca, palavras,
+ segmento.emocao, segmento.confianca_emocao,
+ segmento.caracteristicas_acusticas, segmento.falante,
+ segmento.voz_aparente, segmento.confianca_voz))
+ resultados.append(TranscricaoDoClipe(clipe.identificador, clipe.arquivo,
+ intervalo.inicio, intervalo.fim, segmentos,
+ (inicio_origem, fim_origem) if origem else None))
return resultados
+ def _chave_do_arquivo(self, arquivo: str) -> str:
+ caminho = Path(arquivo).expanduser().resolve()
+ digest = hashlib.sha256()
+ with caminho.open("rb") as conteudo:
+ for bloco in iter(lambda: conteudo.read(1024 * 1024), b""):
+ digest.update(bloco)
+ return digest.hexdigest()
+
+ def _transcrever_arquivo(self, arquivo: str, chave: str) -> list[SegmentoDeTranscricao]:
+ pasta = self.diretoria_de_trabalho / "arquivos" / chave
+ nome_arquivo = Path(arquivo).stem
+ modelo = self._nome_do_modelo()
+ prefixo = f"transcricao-{self._nome_seguro(nome_arquivo)}-{self._nome_seguro(modelo)}"
+ cache = pasta / f"{prefixo}.json"
+ if cache.is_file():
+ dados = json.loads(cache.read_text(encoding="utf-8"))
+ if all("palavras" in item and
+ (self.analisador_de_emocao is None or
+ ("emocao" in item and "voz_aparente" in item))
+ for item in dados):
+ return [self._segmento_do_json(item) for item in dados]
+ partes = self.extrator.extrair(arquivo, pasta / "audio")
+ segmentos: list[SegmentoDeTranscricao] = []
+ for parte in partes:
+ falas = self._diarizar(parte.caminho)
+ for segmento in self.provider.transcrever(type("Audio", (), {"arquivo": str(parte.caminho)})()):
+ analise = self._analisar_emocao(parte.caminho, segmento.inicio, segmento.fim)
+ falante = self._falante_em(falas, segmento.inicio, segmento.fim)
+ segmentos.append(SegmentoDeTranscricao(parte.inicio + segmento.inicio,
+ parte.inicio + segmento.fim, segmento.texto, segmento.confianca,
+ tuple(PalavraDeTranscricao(p.texto, parte.inicio + p.inicio,
+ parte.inicio + p.fim, p.confianca,
+ self._falante_em(falas, p.inicio, p.fim))
+ for p in segmento.palavras),
+ analise.get("emocao"), analise.get("confianca"),
+ dict(analise.get("caracteristicas_acusticas", {})), falante,
+ analise.get("voz_aparente"), analise.get("confianca_voz")))
+ pasta.mkdir(parents=True, exist_ok=True)
+ cache.write_text(json.dumps([asdict(item) for item in segmentos], ensure_ascii=False, indent=2), encoding="utf-8")
+ (pasta / f"{prefixo}.txt").write_text(
+ " ".join(item.texto for item in segmentos if item.texto).strip() + "\n",
+ encoding="utf-8",
+ )
+ return segmentos
+
+ @staticmethod
+ def _segmento_do_json(item: dict[str, Any]) -> SegmentoDeTranscricao:
+ palavras = tuple(PalavraDeTranscricao(str(p["texto"]), float(p["inicio"]),
+ float(p["fim"]), p.get("confianca"),
+ p.get("falante"))
+ for p in item.get("palavras", []))
+ return SegmentoDeTranscricao(float(item["inicio"]), float(item["fim"]),
+ str(item["texto"]), item.get("confianca"), palavras,
+ item.get("emocao"), item.get("confianca_emocao"),
+ dict(item.get("caracteristicas_acusticas", {})), item.get("falante"),
+ item.get("voz_aparente"), item.get("confianca_voz"))
+
+ def _analisar_emocao(self, arquivo: Path, inicio: float, fim: float) -> dict[str, Any]:
+ if self.analisador_de_emocao is None:
+ return {}
+ return dict(self.analisador_de_emocao.analisar(arquivo, inicio, fim))
+
+ def _diarizar(self, arquivo: Path) -> list[tuple[float, float, str]]:
+ if self.diarizador is None:
+ return []
+ return list(self.diarizador.analisar(arquivo))
+
+ @staticmethod
+ def _falante_em(falas: list[tuple[float, float, str]], inicio: float, fim: float) -> str | None:
+ sobreposicoes = [(min(fim, saida) - max(inicio, entrada), falante)
+ for entrada, saida, falante in falas
+ if entrada < fim and saida > inicio]
+ return max(sobreposicoes, default=(0.0, None))[1]
+
+ def _nome_do_modelo(self) -> str:
+ """Obtém o nome público do modelo sem acoplar o scanner ao provider."""
+ modelo = getattr(self.provider, "nome_do_modelo", None)
+ if modelo:
+ return str(modelo)
+ modelo = getattr(self.provider, "modelo", None)
+ if modelo:
+ return Path(str(modelo)).name
+ return self.provider.__class__.__name__.lower()
+
+ @staticmethod
+ def _nome_seguro(valor: str) -> str:
+ return "".join(caractere if caractere.isalnum() or caractere in "._-" else "_"
+ for caractere in valor).strip("._") or "arquivo"
+
@staticmethod
def gerar_relatorio(resultados: list[TranscricaoDoClipe], destino: str | Path) -> Path:
caminho = Path(destino)
diff --git a/code/engine/scanner/visual.py b/code/engine/scanner/visual.py
new file mode 100644
index 0000000..43363c1
--- /dev/null
+++ b/code/engine/scanner/visual.py
@@ -0,0 +1,184 @@
+from collections.abc import Callable
+from dataclasses import dataclass, field
+
+from ..dominio import Clipe
+from ..integracoes.visual.contratos import (AnalisadorDeFrame, AnalisadorDeSequenciaDeQuadros,
+ DetectorDeIntervalosDeCena, ExtratorDeQuadros)
+from ..integracoes.visual.modelos import QuadroDeVideo
+from .modelos import Cena, CenaVisual, EvidenciaVisual
+
+
+@dataclass(frozen=True)
+class ResultadoVisualDoClipe:
+ identificador_do_clipe: str
+ evidencias: list[EvidenciaVisual] = field(default_factory=list)
+ cenas: list[Cena] = field(default_factory=list)
+ cenas_visuais: list[CenaVisual] = field(default_factory=list)
+
+
+class DetectorDeCenas:
+ """Módulo profundo que orquestra frames, analisadores e cenas de um clipe.
+
+ A interface não expõe OpenCV, ONNX, MediaPipe, PySceneDetect ou Vision.
+ Cada adapter pode ser trocado sem alterar chamadores nem resultados do domínio.
+ """
+
+ def __init__(self, extrator: ExtratorDeQuadros,
+ analisadores_de_frame: list[AnalisadorDeFrame] | None = None,
+ detectores_de_intervalo: list[DetectorDeIntervalosDeCena] | None = None,
+ analisadores_de_sequencia: list[AnalisadorDeSequenciaDeQuadros] | None = None,
+ ao_progresso: Callable[[int, int], None] | None = None) -> None:
+ self.extrator = extrator
+ self.analisadores_de_frame = analisadores_de_frame or []
+ self.analisadores_de_sequencia = analisadores_de_sequencia or []
+ self.detectores_de_intervalo = detectores_de_intervalo or []
+ self.ao_progresso = ao_progresso
+
+ def detectar(self, clipe: Clipe) -> ResultadoVisualDoClipe:
+ quadros = self.extrator.extrair(clipe)
+ total = len(quadros) * len(self.analisadores_de_frame) + len(self.analisadores_de_sequencia)
+ progresso = [0]
+ evidencias = self._analisar_quadros(quadros, progresso, total) + self._analisar_sequencia(quadros, progresso, total)
+ cenas = self._detectar_intervalos(clipe, quadros)
+ cenas_visuais = [CenaVisual(
+ clipe.identificador, cena.inicio, cena.fim,
+ tuple(item for item in evidencias if item.inicio <= cena.fim and item.fim >= cena.inicio),
+ cena.referencias,
+ ) for cena in cenas]
+ return ResultadoVisualDoClipe(clipe.identificador, evidencias, cenas, cenas_visuais)
+
+ def _analisar_quadros(self, quadros: list[QuadroDeVideo], progresso: list[int] | None = None,
+ total: int = 0) -> list[EvidenciaVisual]:
+ resultado: list[EvidenciaVisual] = []
+ for quadro in quadros:
+ for analisador in self.analisadores_de_frame:
+ resultado.extend(analisador.analisar(quadro))
+ self._avancar_progresso(progresso, total)
+ return resultado
+
+ def _analisar_sequencia(self, quadros: list[QuadroDeVideo], progresso: list[int] | None = None,
+ total: int = 0) -> list[EvidenciaVisual]:
+ resultado: list[EvidenciaVisual] = []
+ for analisador in self.analisadores_de_sequencia:
+ resultado.extend(analisador.analisar(quadros))
+ self._avancar_progresso(progresso, total)
+ return resultado
+
+ def _avancar_progresso(self, progresso: list[int] | None, total: int) -> None:
+ if progresso is None or total <= 0:
+ return
+ progresso[0] += 1
+ if self.ao_progresso:
+ self.ao_progresso(progresso[0], total)
+
+ def _detectar_intervalos(self, clipe: Clipe, quadros: list[QuadroDeVideo]) -> list[Cena]:
+ cenas: list[Cena] = []
+ for detector in self.detectores_de_intervalo:
+ cenas.extend(detector.detectar(clipe, quadros))
+ cenas = self._consolidar_cenas(cenas)
+ if not cenas and quadros:
+ cenas = [Cena(quadros[0].timestamp, quadros[-1].timestamp)]
+ return cenas
+
+ @staticmethod
+ def _consolidar_cenas(cenas: list[Cena]) -> list[Cena]:
+ resultado: list[Cena] = []
+ for cena in sorted(cenas, key=lambda item: (item.inicio, item.fim)):
+ if resultado and cena.inicio <= resultado[-1].fim:
+ anterior = resultado[-1]
+ resultado[-1] = Cena(anterior.inicio, max(anterior.fim, cena.fim),
+ anterior.confianca or cena.confianca,
+ anterior.referencias + cena.referencias)
+ else:
+ resultado.append(cena)
+ return resultado
+
+
+class AnaliseVisualDaTimeline:
+ """Etapa do Scanner que guarda evidências e cenas por clipe no contexto."""
+
+ nome = "analise_visual_local"
+
+ def __init__(self, detector: DetectorDeCenas, continuar_em_falha: bool = True) -> None:
+ self.detector = detector
+ self.continuar_em_falha = continuar_em_falha
+
+ def executar(self, contexto):
+ if contexto.timeline is None:
+ return contexto
+ for faixa in contexto.timeline.faixas:
+ for clipe in faixa.clipes:
+ if clipe.offline or not clipe.arquivo:
+ continue
+ try:
+ resultado = self.detector.detectar(clipe)
+ except Exception as exc:
+ if not self.continuar_em_falha:
+ raise
+ contexto.avisos.append(
+ f"analise_visual_indisponivel: clipe {clipe.identificador}: {exc}"
+ )
+ contexto.caracteristicas_visuais[clipe.identificador] = {
+ "evidencias": [], "cenas": [], "cenas_visuais": [], "disponivel": False,
+ }
+ continue
+ contexto.caracteristicas_visuais[clipe.identificador] = {
+ "evidencias": resultado.evidencias,
+ "cenas": resultado.cenas,
+ "cenas_visuais": resultado.cenas_visuais,
+ "disponivel": True,
+ }
+ contexto.cenas.extend(resultado.cenas)
+ contexto.cenas_visuais.extend(resultado.cenas_visuais)
+ contexto.cenas = DetectorDeCenas._consolidar_cenas(contexto.cenas)
+ return contexto
+
+
+def criar_detector_visual_local(
+ diretorio_de_cache: str | None = ".frames",
+ intervalo_em_segundos: float = 1.0,
+) -> DetectorDeCenas:
+ """Monta o detector local padrão com uma interface curta para o Scanner."""
+ from ..integracoes.visual import (
+ AnalisadorDeComposicaoOpenCV,
+ AnalisadorDeContinuidadeOpenCV,
+ AnalisadorDeQualidadeOpenCV,
+ ExtratorDeQuadrosOpenCV,
+ )
+
+ return DetectorDeCenas(
+ ExtratorDeQuadrosOpenCV(
+ intervalo_em_segundos=intervalo_em_segundos,
+ diretorio_de_cache=diretorio_de_cache,
+ ),
+ analisadores_de_frame=[
+ AnalisadorDeQualidadeOpenCV(),
+ AnalisadorDeComposicaoOpenCV(),
+ ],
+ analisadores_de_sequencia=[AnalisadorDeContinuidadeOpenCV()],
+ )
+
+
+def criar_detector_apple_vision(
+ diretorio_de_cache: str | None = ".frames",
+ intervalo_em_segundos: float = 1.0,
+ configuracao=None,
+) -> DetectorDeCenas:
+ """Monta a análise local de cena baseada em Vision e arquivos de origem."""
+ from .configuracao_visual import ConfiguracaoVisualDoScanner
+ from ..integracoes.visual import (AnalisadorAppleVisionNativo,
+ AnalisadorSequenciaAppleVisionNativo,
+ ExtratorDeQuadrosFFmpeg)
+
+ perfil = configuracao or ConfiguracaoVisualDoScanner(intervalo_em_segundos=intervalo_em_segundos)
+
+ return DetectorDeCenas(
+ ExtratorDeQuadrosFFmpeg(intervalo_em_segundos=perfil.intervalo_em_segundos,
+ diretorio_de_cache=diretorio_de_cache or ".frames"),
+ analisadores_de_frame=[AnalisadorAppleVisionNativo(
+ recursos=tuple(sorted(perfil.recursos_vision)),
+ interpretar_com_apple_intelligence=perfil.interpretar_cena,
+ )],
+ analisadores_de_sequencia=[AnalisadorSequenciaAppleVisionNativo()]
+ if perfil.analisar_continuidade else [],
+ )
diff --git a/code/engine/testes/test_analise_visual_local.py b/code/engine/testes/test_analise_visual_local.py
new file mode 100644
index 0000000..ad43a63
--- /dev/null
+++ b/code/engine/testes/test_analise_visual_local.py
@@ -0,0 +1,216 @@
+import unittest
+from pathlib import Path
+import json
+import tempfile
+
+from engine.dominio import Clipe, Faixa, IntervaloDeTempo, Timeline
+from engine.integracoes.visual.contratos import (AnalisadorDeFrame, AnalisadorDeSequenciaDeQuadros,
+ DetectorDeIntervalosDeCena,
+ ExtratorDeQuadros)
+from engine.integracoes.visual.modelos import QuadroDeVideo
+from engine.integracoes.visual.extrator_open_cv import ExtratorDeQuadrosOpenCV
+from engine.integracoes.visual.apple_vision import (AnalisadorAppleVisionNativo,
+ AnalisadorSequenciaAppleVisionNativo)
+from engine.scanner.coordenacao import ContextoDeAnalise
+from engine.scanner.modelos import Cena, EvidenciaVisual
+from engine.scanner.visual import AnaliseVisualDaTimeline, DetectorDeCenas, ResultadoVisualDoClipe
+from engine.scanner.relatorio_visual import gerar_relatorio_visual, gerar_resumo_visual_markdown
+from engine.scanner.configuracao_visual import ConfiguracaoVisualDoScanner
+
+
+class ExtratorSimulado(ExtratorDeQuadros):
+ def extrair(self, clipe):
+ return [
+ QuadroDeVideo(0.0, 0, 1920, 1080, "frame-0"),
+ QuadroDeVideo(1.0, 1, 1920, 1080, "frame-1"),
+ ]
+
+
+class AnalisadorSimulado(AnalisadorDeFrame):
+ nome = "simulado"
+
+ def analisar(self, quadro):
+ return [EvidenciaVisual("qualidade", quadro.timestamp, quadro.timestamp,
+ {"nitidez_laplaciana": 42.0}, provider=self.nome)]
+
+
+class DetectorSimulado(DetectorDeIntervalosDeCena):
+ nome = "simulado"
+
+ def detectar(self, clipe, quadros):
+ return [Cena(0.0, 1.0, referencias=(0.0,)), Cena(1.0, 2.0, referencias=(1.0,))]
+
+
+class AnalisadorTemporalSimulado(AnalisadorDeSequenciaDeQuadros):
+ nome = "temporal_simulado"
+
+ def analisar(self, quadros):
+ return [EvidenciaVisual("continuidade", quadros[0].timestamp, quadros[-1].timestamp,
+ {"possivel_descontinuidade": False}, provider=self.nome)]
+
+
+class ExecutorAppleSimulado:
+ def executar(self, carga, timeout):
+ self.carga, self.timeout = carga, timeout
+ return {"evidencias": [{"tipo": "pessoa", "valor": {"ocupacao_do_quadro": 0.3},
+ "confianca": 0.9, "modelo": "VNDetectHumanRectanglesRequest"}],
+ "avisos": ["ocr: indisponível"]}
+
+
+class ExecutorAppleSequenciaSimulado:
+ def executar(self, carga, timeout):
+ self.carga, self.timeout = carga, timeout
+ return {"evidencias": [{"tipo": "similaridade_visual", "valor": {
+ "frame_inicial": 0, "frame_final": 1, "distancia_feature_print": 0.2,
+ "possivel_mudanca_de_cena": False,
+ }, "modelo": "VNGenerateImageFeaturePrintRequest"}], "avisos": []}
+
+
+class TesteAnaliseVisualLocal(unittest.TestCase):
+ def setUp(self):
+ self.clipe = Clipe("clip-1", "camera", IntervaloDeTempo(0, 2), arquivo="/video.mp4")
+
+ def test_detector_envia_cada_frame_aos_analisadores_e_consolida_cenas(self):
+ detector = DetectorDeCenas(ExtratorSimulado(), [AnalisadorSimulado()], [DetectorSimulado()])
+
+ resultado = detector.detectar(self.clipe)
+
+ self.assertEqual(resultado.identificador_do_clipe, "clip-1")
+ self.assertEqual([item.inicio for item in resultado.evidencias], [0.0, 1.0])
+ self.assertEqual(len(resultado.cenas), 1)
+ self.assertEqual((resultado.cenas[0].inicio, resultado.cenas[0].fim), (0.0, 2.0))
+
+ def test_etapa_da_timeline_guarda_evidencias_por_clipe(self):
+ detector = DetectorDeCenas(ExtratorSimulado(), [AnalisadorSimulado()], [DetectorSimulado()])
+ timeline = Timeline("timeline-1", "Principal", faixas=[
+ Faixa("video-1", "V1", "video", 0, [self.clipe]),
+ ])
+ contexto = ContextoDeAnalise(timeline=timeline)
+
+ AnaliseVisualDaTimeline(detector).executar(contexto)
+
+ self.assertEqual(len(contexto.caracteristicas_visuais["clip-1"]["evidencias"]), 2)
+ self.assertEqual(len(contexto.cenas), 1)
+
+ def test_detector_executa_analisadores_de_sequencia_uma_vez_por_clipe(self):
+ detector = DetectorDeCenas(ExtratorSimulado(), analisadores_de_sequencia=[AnalisadorTemporalSimulado()])
+
+ resultado = detector.detectar(self.clipe)
+
+ self.assertEqual(len(resultado.evidencias), 1)
+ self.assertEqual(resultado.evidencias[0].tipo, "continuidade")
+
+ def test_fluxo_produz_cena_visual_com_observacoes_e_continuidade(self):
+ detector = DetectorDeCenas(
+ ExtratorSimulado(), [AnalisadorSimulado()],
+ analisadores_de_sequencia=[AnalisadorTemporalSimulado()],
+ )
+ contexto = ContextoDeAnalise(timeline=Timeline(
+ "timeline-1", "Principal", faixas=[Faixa("video-1", "V1", "video", 0, [self.clipe])]
+ ))
+
+ AnaliseVisualDaTimeline(detector).executar(contexto)
+
+ cena = contexto.cenas_visuais[0]
+ self.assertEqual(cena.identificador_do_clipe, "clip-1")
+ self.assertEqual([item.tipo for item in cena.observacoes], ["qualidade", "qualidade", "continuidade"])
+ self.assertTrue(contexto.caracteristicas_visuais["clip-1"]["disponivel"])
+
+ def test_falha_de_adapter_e_registrada_sem_interromper_outro_clipe(self):
+ class ExtratorQueFalha(ExtratorDeQuadros):
+ def extrair(self, clipe):
+ if clipe.identificador == "clip-1":
+ raise RuntimeError("OpenCV não instalado")
+ return [QuadroDeVideo(0.0, 0, 1920, 1080, "frame")]
+
+ segundo = Clipe("clip-2", "camera-2", IntervaloDeTempo(2, 3), arquivo="/video-2.mp4")
+ timeline = Timeline("timeline-1", "Principal", faixas=[
+ Faixa("video-1", "V1", "video", 0, [self.clipe, segundo]),
+ ])
+ contexto = ContextoDeAnalise(timeline=timeline)
+ detector = DetectorDeCenas(ExtratorQueFalha(), [AnalisadorSimulado()])
+
+ AnaliseVisualDaTimeline(detector).executar(contexto)
+
+ self.assertFalse(contexto.caracteristicas_visuais["clip-1"]["disponivel"])
+ self.assertTrue(contexto.caracteristicas_visuais["clip-2"]["disponivel"])
+ self.assertEqual(len(contexto.avisos), 1)
+
+ def test_adapter_apple_converte_fatos_e_preserva_avisos_por_recurso(self):
+ executor = ExecutorAppleSimulado()
+ quadro = QuadroDeVideo(0.0, 0, 1920, 1080, "frame", Path("/frame.jpg"))
+
+ evidencias = AnalisadorAppleVisionNativo(executor=executor).analisar(quadro)
+
+ self.assertEqual(evidencias[0].tipo, "pessoa")
+ self.assertEqual(evidencias[0].modelo, "VNDetectHumanRectanglesRequest")
+ self.assertEqual(evidencias[1].tipo, "diagnostico_apple_vision")
+ self.assertTrue(executor.carga["resumir"])
+
+ def test_adapter_apple_de_sequencia_converte_indices_em_intervalos(self):
+ executor = ExecutorAppleSequenciaSimulado()
+ quadros = [QuadroDeVideo(2.0, 0, 1920, 1080, "frame", Path("/frame-0.jpg")),
+ QuadroDeVideo(3.0, 1, 1920, 1080, "frame", Path("/frame-1.jpg"))]
+
+ evidencias = AnalisadorSequenciaAppleVisionNativo(executor=executor).analisar(quadros)
+
+ self.assertEqual(evidencias[0].tipo, "similaridade_visual")
+ self.assertEqual((evidencias[0].inicio, evidencias[0].fim), (2.0, 3.0))
+ self.assertEqual(evidencias[0].valor["distancia_feature_print"], 0.2)
+ self.assertEqual(executor.carga["frames"], ["/frame-0.jpg", "/frame-1.jpg"])
+
+ def test_evidencia_rejeita_intervalo_invalido(self):
+ with self.assertRaises(ValueError):
+ EvidenciaVisual("objeto", 2.0, 1.0, {})
+
+ def test_relatorio_visual_separa_frames_de_evidencias_temporais(self):
+ resultado = ResultadoVisualDoClipe("clip-1", [
+ EvidenciaVisual("categoria", 2.0, 2.0, {"identificador": "drink"}),
+ EvidenciaVisual("similaridade_visual", 2.0, 3.0, {"distancia": 0.2}),
+ ])
+ with tempfile.TemporaryDirectory() as pasta:
+ caminho = gerar_relatorio_visual(self.clipe, resultado, Path(pasta) / "visual.json", 1.0)
+ dados = json.loads(caminho.read_text(encoding="utf-8"))
+
+ self.assertEqual(dados["clipe"]["identificador"], "clip-1")
+ self.assertEqual(dados["observacoes_por_frame"][0]["timestamp_na_origem"], 2.0)
+ self.assertEqual(dados["observacoes_por_frame"][0]["evidencias"][0]["tipo"], "categoria")
+ self.assertEqual(dados["evidencias_temporais"][0]["tipo"], "similaridade_visual")
+
+ def test_resumo_visual_markdown_lista_frames_e_transicoes(self):
+ resultado = ResultadoVisualDoClipe("clip-1", [
+ EvidenciaVisual("categoria", 2.0, 2.0, {"identificador": "drink"}),
+ EvidenciaVisual("similaridade_visual", 2.0, 3.0, {"distancia": 0.2}),
+ ])
+ with tempfile.TemporaryDirectory() as pasta:
+ caminho = gerar_resumo_visual_markdown(self.clipe, resultado, Path(pasta) / "visual.md", 1.0)
+ conteudo = caminho.read_text(encoding="utf-8")
+
+ self.assertIn("Origem 2.000s", conteudo)
+ self.assertIn("similaridade_visual", conteudo)
+
+ def test_configuracao_visual_valida_o_perfil_do_painel(self):
+ perfil = ConfiguracaoVisualDoScanner.de_dict({
+ "intervalo_em_segundos": 5,
+ "faixas_de_video": [2, 0, 2],
+ "recursos_vision": ["faces", "pessoas"],
+ "analisar_continuidade": False,
+ })
+
+ self.assertEqual(perfil.faixas_de_video, (0, 2))
+ self.assertEqual(perfil.para_dict()["recursos_vision"], ["faces", "pessoas"])
+ self.assertFalse(perfil.analisar_continuidade)
+
+ def test_extrator_respeita_intervalo_na_origem_e_limite_de_frames(self):
+ clipe = Clipe("clip-1", "camera", IntervaloDeTempo(0, 2),
+ intervalo_na_origem=IntervaloDeTempo(2.3, 5.0), arquivo="/video.mp4")
+ extrator = ExtratorDeQuadrosOpenCV(intervalo_em_segundos=1.0, maximo_de_quadros=1,
+ cv2_module=object())
+
+ inicio, fim = extrator._intervalo_de_origem(clipe, 10.0)
+
+ self.assertEqual(extrator._timestamps(inicio, fim), [2.3])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/code/engine/testes/test_arquivos_e_metadados.py b/code/engine/testes/test_arquivos_e_metadados.py
new file mode 100644
index 0000000..83587e3
--- /dev/null
+++ b/code/engine/testes/test_arquivos_e_metadados.py
@@ -0,0 +1,86 @@
+import json
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+from engine.dominio import Clipe, Faixa, IntervaloDeTempo, Timeline
+from engine.scanner.coordenacao import ContextoDeAnalise
+from engine.scanner.descoberta import ArquivoDescoberto, DescobertaDeArquivos
+from engine.scanner.metadados import ExtracaoDeMetadados
+from engine.integracoes.midia.extracao_de_metadados import ExtracaoDeMetadados as Adapter
+
+
+class TesteArquivosEMetadados(unittest.TestCase):
+ def _contexto(self, *arquivos: str | None) -> ContextoDeAnalise:
+ clipes = [Clipe(str(i), f"clipe-{i}", IntervaloDeTempo(0, 1), arquivo=arquivo)
+ for i, arquivo in enumerate(arquivos)]
+ timeline = Timeline("timeline", "Principal", faixas=[Faixa("v1", "V1", "video", 0, clipes)])
+ return ContextoDeAnalise(timeline=timeline)
+
+ def test_descoberta_normaliza_caminho_e_marca_offline(self):
+ with tempfile.TemporaryDirectory() as pasta:
+ arquivo = Path(pasta) / "camera.mp4"
+ arquivo.touch()
+ contexto = self._contexto(str(arquivo), str(Path(pasta) / "ausente.mp4"), None)
+
+ DescobertaDeArquivos().executar(contexto)
+
+ self.assertEqual(contexto.timeline.faixas[0].clipes[0].arquivo, str(arquivo.resolve()))
+ self.assertFalse(contexto.timeline.faixas[0].clipes[0].offline)
+ self.assertTrue(contexto.timeline.faixas[0].clipes[1].offline)
+ self.assertTrue(contexto.timeline.faixas[0].clipes[2].offline)
+ self.assertEqual(len(contexto.erros), 2)
+
+ def test_descoberta_identifica_arquivo_duplicado(self):
+ with tempfile.TemporaryDirectory() as pasta:
+ arquivo = Path(pasta) / "camera.mov"
+ arquivo.touch()
+ contexto = self._contexto(str(arquivo), str(arquivo))
+
+ DescobertaDeArquivos().executar(contexto)
+
+ self.assertEqual(len(contexto.avisos), 1)
+
+ def test_extracao_converte_ffprobe_e_usa_cache(self):
+ dados = {"format": {"format_name": "mov,mp4,m4a", "duration": "12.5", "size": "900"},
+ "streams": [{"codec_type": "video", "codec_name": "h264", "width": 1920,
+ "height": 1080, "avg_frame_rate": "30000/1001"},
+ {"codec_type": "audio", "codec_name": "aac", "channels": 2,
+ "sample_rate": "48000"}]}
+ with tempfile.TemporaryDirectory() as pasta:
+ arquivo = Path(pasta) / "camera.mp4"
+ arquivo.touch()
+ adapter = Adapter(ffprobe="ffprobe-simulado")
+ processo = type("Processo", (), {"stdout": json.dumps(dados)})()
+ with patch("engine.integracoes.midia.extracao_de_metadados.subprocess.run", return_value=processo) as run:
+ primeiro = adapter.extrair(arquivo)
+ segundo = adapter.extrair(arquivo)
+
+ self.assertEqual(primeiro.codec_de_video, "h264")
+ self.assertEqual(primeiro.codec_de_audio, "aac")
+ self.assertEqual((primeiro.largura, primeiro.altura), (1920, 1080))
+ self.assertAlmostEqual(primeiro.taxa_de_quadros, 29.97002997, places=5)
+ self.assertEqual(primeiro.duracao, 12.5)
+ self.assertIs(primeiro, segundo)
+ run.assert_called_once()
+
+ def test_etapa_de_metadados_ignora_arquivos_offline(self):
+ class ExtratorSimulado:
+ def extrair(self, arquivo):
+ return "metadados"
+
+ with tempfile.TemporaryDirectory() as pasta:
+ arquivo = Path(pasta) / "camera.mp4"
+ arquivo.touch()
+ contexto = self._contexto(str(arquivo), str(Path(pasta) / "ausente.mp4"))
+ DescobertaDeArquivos().executar(contexto)
+
+ ExtracaoDeMetadados(ExtratorSimulado()).executar(contexto)
+
+ self.assertEqual(contexto.metadados_dos_arquivos["0"], "metadados")
+ self.assertNotIn("1", contexto.metadados_dos_arquivos)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/code/engine/testes/test_provider_de_transcricao_apple.py b/code/engine/testes/test_provider_de_transcricao_apple.py
new file mode 100644
index 0000000..36a0d9f
--- /dev/null
+++ b/code/engine/testes/test_provider_de_transcricao_apple.py
@@ -0,0 +1,32 @@
+import json
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+from engine.integracoes.apple_speech import ProviderDeTranscricaoApple
+
+
+class TesteProviderDeTranscricaoApple(unittest.TestCase):
+ @patch("engine.integracoes.apple_speech.provider_de_transcricao_apple.subprocess.run")
+ def test_transcreve_em_pt_br_no_dispositivo_e_preserva_timestamps(self, executar):
+ executar.return_value = type("Resultado", (), {
+ "stdout": json.dumps({"segmentos": [
+ {"inicio": 1.25, "fim": 2.75, "texto": " Olá mundo ", "confianca": 0.93},
+ ]}),
+ })()
+
+ with tempfile.TemporaryDirectory() as pasta:
+ arquivo = Path(pasta) / "video.mp4"
+ arquivo.write_bytes(b"video")
+ clipe = type("Clipe", (), {"arquivo": str(arquivo)})()
+ resultado = ProviderDeTranscricaoApple(executavel="transcritor").transcrever(clipe)
+
+ self.assertEqual(resultado[0].texto, "Olá mundo")
+ self.assertEqual((resultado[0].inicio, resultado[0].fim), (1.25, 2.75))
+ comando = executar.call_args.args[0]
+ self.assertEqual(comando, ["transcritor", str(arquivo), "pt-BR", "true"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/code/relatorios/analise-brools/0E6A8870.md b/code/relatorios/analise-brools/0E6A8870.md
new file mode 100644
index 0000000..8d38eb8
--- /dev/null
+++ b/code/relatorios/analise-brools/0E6A8870.md
@@ -0,0 +1,31 @@
+# Teste de transcrição local — `0E6A8870.MP4`
+
+## Configuração
+
+- Modelo: `faster-whisper-base`
+- Idioma: `pt`
+- Dispositivo: CPU
+- Quantização: `int8`
+- Provider: `ProviderDeTranscricaoLocal`
+- Origem: `/Volumes/Merongo/PROJETOS/brools/0E6A8870.MP4`
+- Duração do vídeo: aproximadamente `6,05 s`
+
+## Desempenho
+
+| Medição | Resultado |
+|---|---:|
+| Carregamento do modelo | 1,849 s |
+| Transcrição | 0,802 s |
+| Memória máxima do processo | aproximadamente 665 MB |
+
+## Resultado temporal
+
+| Início | Fim | Texto reconhecido |
+|---:|---:|---|
+| 0,000 s | 6,000 s | “a mais é fazer bem pesado, porque não vai fazer as boguinhas que a gente ame de 11, então aqui...” |
+
+## Observações
+
+O processamento local funcionou sem autorização do Apple Speech e sem acesso de rede. O modelo `base` foi rápido para este arquivo curto, mas a transcrição apresenta baixa clareza em parte do trecho; a qualidade deve ser comparada com o modelo `small` antes de escolher o perfil definitivo.
+
+Para o catálogo SQLite, este resultado deve ser persistido como um segmento de transcrição associado ao identificador do ativo e à versão do modelo. O modelo carregado deve permanecer reutilizável pelo worker, evitando pagar o custo de 1,849 s a cada arquivo.
diff --git a/code/relatorios/analise-brools/0E6A8873.md b/code/relatorios/analise-brools/0E6A8873.md
new file mode 100644
index 0000000..5d6494c
--- /dev/null
+++ b/code/relatorios/analise-brools/0E6A8873.md
@@ -0,0 +1,64 @@
+# Teste de indexação — `0E6A8873.MP4`
+
+## Identificação do ativo
+
+- Origem: `/Volumes/Merongo/PROJETOS/brools/0E6A8873.MP4`
+- Tamanho: `354.817.464` bytes
+- Fingerprint parcial SHA-256: `d1072c11b67454a6e2eb0422ca037b1648a1ed05725ab2064c2e01d7194f2fdc`
+- Duração: `23,315 s`
+- Vídeo: H.264, `3840 × 2160`, aproximadamente `23,976 FPS`
+- Áudio: AAC, 2 canais, `48 kHz`
+
+## Resultado por etapa
+
+| Etapa | Status | Tempo | Resultado |
+|---|---|---:|---|
+| FFprobe/metadados | concluída | 0,097 s | Metadados técnicos lidos |
+| Extração de áudio | concluída | 0,078 s | WAV mono, 16 kHz, 745.570 bytes |
+| Amostragem visual simples | concluída | 2,147 s | 23 frames, 1 FPS, reduzidos para 640 px |
+| Apple Speech `pt-BR` on-device | autorização pendente | 0,011 s | Código 4: permissão não concedida |
+
+## Interpretação
+
+O pipeline barato de preparação é viável para processamento em segundo plano. Para este arquivo, a leitura técnica e a extração de áudio são praticamente instantâneas; a amostragem visual reduzida é a etapa mais custosa entre as etapas executadas, mas ainda terminou em aproximadamente 2,1 segundos.
+
+O Apple Speech não apresentou mais o crash `SIGABRT`. O processo Foundation-only iniciou normalmente e retornou um erro controlado de autorização. Depois que o bundle receber permissão de reconhecimento de fala no macOS, esta mesma etapa poderá ser repetida sem alterar o catálogo nem as etapas já concluídas.
+
+## Como isso será armazenado no SQLite
+
+Este teste deverá produzir, conceitualmente:
+
+```text
+ativos
+ id = UUID interno
+ caminho_relativo = 0E6A8873.MP4
+ fingerprint = d1072...
+ tamanho = 354817464
+ duracao = 23.314958
+ status = parcial
+
+trabalhos_de_indexacao
+ ativo_id = ...
+ etapa = metadados
+ status = concluida
+ tempo = 0.097
+
+trabalhos_de_indexacao
+ ativo_id = ...
+ etapa = amostragem_visual
+ status = concluida
+ checkpoint = 23.315
+
+trabalhos_de_indexacao
+ ativo_id = ...
+ etapa = transcricao
+ status = aguardando_autorizacao
+ provider = Apple Speech
+ locale = pt-BR
+```
+
+Os frames e o WAV são cache reconstruível e não devem ser tratados como o cadastro principal. Quando houver transcrição e análise visual, seus segmentos e evidências serão gravados com timestamps no arquivo de origem.
+
+## Próximo teste
+
+Conceder a permissão do Apple Speech ao bundle do transcritor e repetir somente a etapa de transcrição deste arquivo. Em seguida, comparar a qualidade do resultado antes de enfileirar os demais 23 arquivos.
diff --git a/code/relatorios/analise-brools/proximos-4-cenas.md b/code/relatorios/analise-brools/proximos-4-cenas.md
new file mode 100644
index 0000000..05143ff
--- /dev/null
+++ b/code/relatorios/analise-brools/proximos-4-cenas.md
@@ -0,0 +1,50 @@
+# Descrição preliminar das cenas — próximos quatro vídeos
+
+## Escopo
+
+Este relatório complementa `proximos-4-transcricoes.md`. A transcrição foi executada
+com `ProviderDeTranscricaoLocal`; a análise visual foi tentada através de
+`AnalisadorAppleVisionNativo.analisar(QuadroDeVideo)`, usando frames PNG reduzidos
+para 640 px de largura.
+
+## Observações preliminares dos frames amostrados
+
+Estas descrições são uma inspeção visual dos frames de teste e ainda não devem ser
+gravadas como evidência automática definitiva no catálogo.
+
+| Arquivo | Descrição preliminar |
+|---|---|
+| `0E6A8871.MP4` | Close de uma mão segurando uma caneta sobre um tablet. A tela mostra um diagrama de rosto com marcações/pontos; a ação principal é indicar ou marcar regiões do rosto. |
+| `0E6A8872.MP4` | Mulher de blusa vermelha e cabelo preso demonstra algo usando uma caneta e um tablet com diagrama facial. Há alternância entre enquadramento da apresentadora e close do tablet/caneta. |
+| `0E6A8873.MP4` | Demonstração presencial: mulher de blusa vermelha aparece junto de outra pessoa em posição reclinada e, em outro momento, junto de uma pessoa sentada. A atividade visível parece envolver indicação/avaliação da região facial, mas a finalidade não foi inferida. |
+| `0E6A8874.MP4` | Plano médio frontal da mulher de blusa vermelha falando e gesticulando. Em alguns momentos ela aponta ou toca o próprio rosto; há um objeto/tablet no primeiro plano. |
+
+## Resultado técnico da tentativa automática
+
+O adapter existente foi chamado corretamente, sem criar uma implementação paralela:
+
+```text
+AnalisadorAppleVisionNativo(
+ recursos=("pessoas", "categorias", "ocr"),
+ interpretar_com_apple_intelligence=False,
+).analisar(QuadroDeVideo(...))
+```
+
+O retorno não trouxe `pessoas`, `categorias` nem `ocr`. Trouxe somente uma
+evidência `diagnostico_apple_vision`, com estes avisos:
+
+- `pessoas`: falha ao carregar o modelo ANE do Vision;
+- `ocr`: erro genérico do framework;
+- `categorias`: falha ao criar `CVPixelBufferPool` com dimensões zeradas.
+
+O fallback `AnalisadorDeRostosOpenCV`, `AnalisadorDeComposicaoOpenCV` e
+`AnalisadorDeQualidadeOpenCV` também não pôde ser executado neste teste porque o
+interpretador usado pelo engine não possui `cv2` instalado.
+
+## Decisão para o catálogo
+
+As descrições acima devem ficar, por enquanto, como resultado de validação manual
+do teste. O catálogo deve armazenar a etapa visual como `parcial`/`indisponível`,
+preservando os avisos técnicos e os frames amostrados para reprocessamento. Não
+devemos registrar como fato automático algo que o Vision não confirmou.
+
diff --git a/code/relatorios/analise-brools/proximos-4-transcricoes.md b/code/relatorios/analise-brools/proximos-4-transcricoes.md
new file mode 100644
index 0000000..184e74a
--- /dev/null
+++ b/code/relatorios/analise-brools/proximos-4-transcricoes.md
@@ -0,0 +1,59 @@
+# Teste de transcrição — próximos quatro vídeos
+
+## Configuração comum
+
+- Provider: `ProviderDeTranscricaoLocal`
+- Modelo: `faster-whisper-base`
+- Idioma: `pt-BR`
+- Dispositivo: CPU
+- Cálculo: `int8`
+- Modelo carregado uma única vez: `1,613 s`
+- Memória máxima observada no processo: aproximadamente `805 MB`
+
+## Desempenho
+
+| Arquivo | Duração | Tempo de transcrição |
+|---|---:|---:|
+| `0E6A8871.MP4` | 5,797 s | 4,213 s |
+| `0E6A8872.MP4` | 17,726 s | 1,377 s |
+| `0E6A8873.MP4` | 23,315 s | 1,737 s |
+| `0E6A8874.MP4` | 14,556 s | 1,300 s |
+
+Tempo de transcrição dos quatro arquivos: `8,627 s`, além do carregamento inicial do modelo.
+
+## Segmentos reconhecidos
+
+### `0E6A8871.MP4`
+
+- `[0,000–3,840]` Tende um pouquinho mais aqui, acho que pode dratar um pouquinho mais a pé, se eu
+- `[3,840–5,580]` doutora do entratante, a noite, ouzo.
+
+### `0E6A8872.MP4`
+
+- `[0,000–3,360]` Então, por isso que eu vou fazer tanto, não se paguei nem com um pouquinho mais lá para trás.
+- `[4,120–10,580]` E, espero, por biculares aqui, que é o musco orbicular da boca, que ele serve com o esfímpere, ele fecha sim, né?
+- `[10,900–14,700]` Então, quando você faz o biquinho, fica três movinhas de um lado,
+- `[15,020–17,680]` é uma, dois de cada lado, basicamente só que essas daqui são um pouco...
+
+### `0E6A8873.MP4`
+
+- `[0,000–4,700]` é que essa parte aqui em perior tem alguns que soem aqui, tem um pouquinho de
+- `[4,700–8,960]` classidente de pés, né? Então isso, quando eu passei em dia, tem uma certa
+- `[8,960–12,840]` classidade, a gente não bloqueia tanto justamente porque esse bloqueia
+- `[12,840–15,940]` vai parecer que está mais classindo ainda, porque daí é um
+- `[15,940–20,180]` muito bom explotido, isso vai ficar mais explotidamente. Então quando tem uma
+- `[20,180–23,260]` sobre eu, quando você tem bolsa, daí a gente não faz aqui...
+
+### `0E6A8874.MP4`
+
+- `[0,000–2,740]` O profissão da pele, porque a gente está tratando músculo,
+- `[2,860–5,880]` a gente está enfraquecendo músculos que estão mudando impressões na pele.
+- `[6,280–8,900]` Mas quando a pele está muito fininha que é a parte da Pávera,
+- `[9,020–12,700]` a gente tem que tratar a pele como um todo, da parte de dermatológica, entendeu?
+- `[13,000–14,520]` Então, por isso que a gente não pode fazer...
+
+## Avaliação
+
+O desempenho é suficiente para um worker de baixa prioridade, especialmente porque o modelo fica carregado e é reutilizado. A qualidade do `base` é compreensível, mas há erros fonéticos relevantes em termos técnicos. Para o catálogo, esses segmentos ainda são úteis para busca aproximada; para texto final de edição, o próximo experimento deve comparar o mesmo conjunto com `faster-whisper-small`.
+
+Cada segmento deverá ser salvo no SQLite com `ativo_id`, `inicio`, `fim`, `texto`, `modelo`, `provider`, `idioma`, `confianca` quando disponível e versão da etapa.
diff --git a/code/relatorios/analise-visual/000f4763/0E6A8290/frame-000000.jpg b/code/relatorios/analise-visual/000f4763/0E6A8290/frame-000000.jpg
new file mode 100644
index 0000000..d664fa4
Binary files /dev/null and b/code/relatorios/analise-visual/000f4763/0E6A8290/frame-000000.jpg differ
diff --git a/code/relatorios/analise-visual/000f4763/primeiro-frame.md b/code/relatorios/analise-visual/000f4763/primeiro-frame.md
new file mode 100644
index 0000000..50f6b17
--- /dev/null
+++ b/code/relatorios/analise-visual/000f4763/primeiro-frame.md
@@ -0,0 +1,37 @@
+# Análise do primeiro frame — corte `000f4763`
+
+- Arquivo: `0E6A8290.MP4`
+- Início do corte na timeline: `0.000s`
+- Primeiro frame efetivo do corte na origem: `2.3023s`
+- Resolução: `3840 × 2160`
+- Frame extraído: `frame-000000.jpg`
+
+## OpenCV
+
+- Brilho médio: `114.71 / 255`
+- Contraste (desvio padrão): `61.81`
+- Nitidez (variância do Laplaciano): `9.73`
+
+A métrica de nitidez é baixa para este frame; há aparência de foco suave e/ou
+desfoque de movimento. Brilho e contraste estão em uma faixa utilizável, sem
+indício de subexposição extrema no cálculo global.
+
+## Observação visual
+
+O frame mostra uma pessoa em plano médio, bem exposta, diante de fundo neutro
+desfocado. A imagem foi registrada deitada no arquivo, apesar da sequência ser
+vertical; a correção de orientação deve entrar como etapa própria antes de
+qualquer análise semântica ou geração de preview.
+
+## Providers indisponíveis neste host
+
+- MediaPipe 1.0.1: abort nativo em `DrishtiMetalHelper` durante a criação do
+ `PoseLandmarker` no macOS 26.6.2. O adapter agora roda em subprocesso para
+ conter esse abort.
+- Apple Vision: o host não conseguiu criar o plano de inferência do modelo
+ nativo (`ANECF error`) neste momento. O adapter retorna indisponibilidade sem
+ interromper o Scanner.
+- ONNX: instalado, mas ainda não há um modelo ONNX e seu decodificador
+ configurados para detecção de pessoas/objetos.
+- PySceneDetect: instalado, mas não se aplica a um único frame; ele precisa de
+ uma sequência de frames para identificar uma transição.
diff --git a/code/relatorios/analise-visual/000f4765/V1-segmento-02-vision-completo.json b/code/relatorios/analise-visual/000f4765/V1-segmento-02-vision-completo.json
new file mode 100644
index 0000000..9026b16
--- /dev/null
+++ b/code/relatorios/analise-visual/000f4765/V1-segmento-02-vision-completo.json
@@ -0,0 +1,7787 @@
+{
+ "versao": "1.0",
+ "gerado_em": "2026-09-08T18:57:57.181939+00:00",
+ "clipe": {
+ "identificador": "000f4765",
+ "nome": "0E6A8290.MP4",
+ "arquivo_original": "/Volumes/Merongo/PROJETOS/03 - Mastopexia/0E6A8290.MP4",
+ "intervalo_na_timeline": {
+ "inicio": 38.2715666666667,
+ "fim": 42.4090333333333
+ },
+ "intervalo_na_origem": {
+ "inicio": 41.8084333333333,
+ "fim": 45.9459
+ }
+ },
+ "amostragem": {
+ "intervalo_em_segundos": 1.0
+ },
+ "observacoes_por_frame": [
+ {
+ "timestamp_na_origem": 41.8084333333333,
+ "evidencias": [
+ {
+ "tipo": "rosto",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "largura": 0.1713569313287735,
+ "x": 0.5904472470283508,
+ "altura": 0.30463454127311707,
+ "y": 0.443860799074173
+ },
+ "landmarks": {
+ "rosto": [
+ {
+ "y": 0.12155722081661224,
+ "x": 0.6621209979057312
+ },
+ {
+ "y": 0.11992400884628296,
+ "x": 0.5520904064178467
+ },
+ {
+ "y": 0.128090038895607,
+ "x": 0.44097042083740234
+ },
+ {
+ "y": 0.14387769997119904,
+ "x": 0.3303951025009155
+ },
+ {
+ "y": 0.1749086081981659,
+ "x": 0.22363276779651642
+ },
+ {
+ "y": 0.23207080364227295,
+ "x": 0.1283092349767685
+ },
+ {
+ "y": 0.3115534782409668,
+ "x": 0.05096099525690079
+ },
+ {
+ "y": 0.4068238139152527,
+ "x": -0.005688415374606848
+ },
+ {
+ "y": 0.5157042145729065,
+ "x": -0.025842532515525818
+ },
+ {
+ "y": 0.6237679719924927,
+ "x": -0.0029648856725543737
+ },
+ {
+ "y": 0.7165884971618652,
+ "x": 0.05586335062980652
+ },
+ {
+ "y": 0.7952545881271362,
+ "x": 0.13430099189281464
+ },
+ {
+ "y": 0.851736307144165,
+ "x": 0.23125864565372467
+ },
+ {
+ "y": 0.884128212928772,
+ "x": 0.3385657072067261
+ },
+ {
+ "y": 0.9025017619132996,
+ "x": 0.4491409957408905
+ },
+ {
+ "y": 0.9128453731536865,
+ "x": 0.5602610111236572
+ },
+ {
+ "y": 0.9128453731536865,
+ "x": 0.6708362698554993
+ }
+ ],
+ "labios": [
+ {
+ "y": 0.6332949995994568,
+ "x": 0.308606892824173
+ },
+ {
+ "y": 0.5949146747589111,
+ "x": 0.3298504054546356
+ },
+ {
+ "y": 0.5543567538261414,
+ "x": 0.3407445251941681
+ },
+ {
+ "y": 0.5173373818397522,
+ "x": 0.33311864733695984
+ },
+ {
+ "y": 0.4803180694580078,
+ "x": 0.3396551012992859
+ },
+ {
+ "y": 0.43785473704338074,
+ "x": 0.3282162845134735
+ },
+ {
+ "y": 0.3975689709186554,
+ "x": 0.30751746892929077
+ },
+ {
+ "y": 0.3643604815006256,
+ "x": 0.2808268666267395
+ },
+ {
+ "y": 0.40246859192848206,
+ "x": 0.2350715845823288
+ },
+ {
+ "y": 0.45690879225730896,
+ "x": 0.2051127552986145
+ },
+ {
+ "y": 0.5162485837936401,
+ "x": 0.19530805945396423
+ },
+ {
+ "y": 0.5736830234527588,
+ "x": 0.2040233463048935
+ },
+ {
+ "y": 0.6270343661308289,
+ "x": 0.2334374636411667
+ },
+ {
+ "y": 0.6635093092918396,
+ "x": 0.2797374725341797
+ }
+ ],
+ "nariz": [
+ {
+ "y": 0.5211482048034668,
+ "x": 0.6278045177459717
+ },
+ {
+ "y": 0.607435941696167,
+ "x": 0.5175015926361084
+ },
+ {
+ "y": 0.6096135377883911,
+ "x": 0.4327998161315918
+ },
+ {
+ "y": 0.5647003650665283,
+ "x": 0.4295315742492676
+ },
+ {
+ "y": 0.5206038355827332,
+ "x": 0.4180927574634552
+ },
+ {
+ "y": 0.4759628474712372,
+ "x": 0.42898687720298767
+ },
+ {
+ "y": 0.4296886920928955,
+ "x": 0.431710422039032
+ },
+ {
+ "y": 0.43241071701049805,
+ "x": 0.516139805316925
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "y": 0.7838221192359924,
+ "x": 0.6825474500656128
+ },
+ {
+ "y": 0.7372757792472839,
+ "x": 0.7176809906959534
+ },
+ {
+ "y": 0.6626927256584167,
+ "x": 0.7073315978050232
+ },
+ {
+ "y": 0.6194127798080444,
+ "x": 0.6659339666366577
+ },
+ {
+ "y": 0.6705865263938904,
+ "x": 0.6580356955528259
+ },
+ {
+ "y": 0.7391811609268188,
+ "x": 0.6577633619308472
+ }
+ ],
+ "olho_direito": [
+ {
+ "y": 0.2571132779121399,
+ "x": 0.6727427840232849
+ },
+ {
+ "y": 0.30338746309280396,
+ "x": 0.7095103859901428
+ },
+ {
+ "y": 0.37797051668167114,
+ "x": 0.7018845081329346
+ },
+ {
+ "y": 0.42097827792167664,
+ "x": 0.6621209979057312
+ },
+ {
+ "y": 0.3714376986026764,
+ "x": 0.6531333327293396
+ },
+ {
+ "y": 0.30338746309280396,
+ "x": 0.6495927572250366
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "y": 0.1787194162607193,
+ "x": 0.7691556811332703
+ },
+ {
+ "y": 0.31046468019485474,
+ "x": 0.8210389614105225
+ },
+ {
+ "y": 0.4487427771091461,
+ "x": 0.7931227684020996
+ },
+ {
+ "y": 0.4427543580532074,
+ "x": 0.7443715929985046
+ },
+ {
+ "y": 0.31264227628707886,
+ "x": 0.7705174684524536
+ },
+ {
+ "y": 0.18307463824748993,
+ "x": 0.7424651384353638
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "y": 0.8660268187522888,
+ "x": 0.7780071496963501
+ },
+ {
+ "y": 0.7326483726501465,
+ "x": 0.8307074904441833
+ },
+ {
+ "y": 0.5930092930793762,
+ "x": 0.795437753200531
+ },
+ {
+ "y": 0.5998142957687378,
+ "x": 0.7484568953514099
+ },
+ {
+ "y": 0.7315595746040344,
+ "x": 0.779777467250824
+ },
+ {
+ "y": 0.8611271977424622,
+ "x": 0.7514527440071106
+ }
+ ]
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "largura": 0.17041203379631042,
+ "x": 0.5908492803573608,
+ "altura": 0.3029547333717346,
+ "y": 0.4386913478374481
+ },
+ "score": 0.78466796875
+ },
+ "confianca": 0.8615030646324158,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "largura": 0.8351151645820785,
+ "x": 0,
+ "altura": 0.8055555845387496,
+ "y": 0.18640351543823877
+ },
+ "ocupacao_do_quadro": 0.6727316845620903
+ },
+ "confianca": 0.7146623134613037,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "x": 0,
+ "confianca": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "y": 0.23268580436706543,
+ "confianca": 0.583984375,
+ "x": 0.23195724189281464
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "y": 0.6754071712493896,
+ "confianca": 0.90673828125,
+ "x": 0.7044726610183716
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "x": 0.7018452882766724,
+ "confianca": 0.460205078125,
+ "y": 0.4677010178565979
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "x": 0,
+ "confianca": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "x": 0,
+ "confianca": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "x": 0.560464084148407,
+ "confianca": 0.2021484375,
+ "y": 0.4213942289352417
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "x": 0.4931042790412903,
+ "confianca": 0.4306640625,
+ "y": 0.8207171559333801
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "x": 0.2440168559551239,
+ "confianca": 0.59375,
+ "y": 0.37958621978759766
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "y": 0.31064724922180176,
+ "confianca": 0.135986328125,
+ "x": 0.4927447736263275
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "x": 0.6764724850654602,
+ "confianca": 0.7626953125,
+ "y": 0.6156721115112305
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "x": 0.5267841815948486,
+ "confianca": 0.31640625,
+ "y": 0.6210556924343109
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "x": 0.7054939866065979,
+ "confianca": 0.85546875,
+ "y": 0.5601228475570679
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "x": 0.22904758155345917,
+ "confianca": 0.304443359375,
+ "y": 0.9515209794044495
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "y": 0.8904433250427246,
+ "confianca": 0.75146484375,
+ "x": 0.25067535042762756
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "x": 0.6699463129043579,
+ "confianca": 0.634765625,
+ "y": 0.7397646903991699
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "x": 0.22996927797794342,
+ "confianca": 0.865234375,
+ "y": 0.5335726737976074
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.6088806986808777,
+ "confianca": 0.90087890625,
+ "x": 0.1985943764448166
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "x": 0.22330962121486664,
+ "confianca": 0.84375,
+ "y": 0.4761231541633606
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "x": 0.3122074604034424,
+ "confianca": 0.68017578125,
+ "y": 0.5486577749252319
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.25278791785240173,
+ "confianca": 0.86328125,
+ "y": 0.5695041418075562
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "x": 0.2520199716091156,
+ "confianca": 0.79345703125,
+ "y": 0.38338571786880493
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "x": 0.24605269730091095,
+ "confianca": 0.89453125,
+ "y": 0.6107781529426575
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "x": 0.2054852694272995,
+ "confianca": 0.8876953125,
+ "y": 0.5696834325790405
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "x": 0.3052910566329956,
+ "confianca": 0.92578125,
+ "y": 0.6033934950828552
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "x": 0.2581619620323181,
+ "confianca": 0.86474609375,
+ "y": 0.5324119329452515
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "x": 0.30373576283454895,
+ "confianca": 0.88232421875,
+ "y": 0.5642706155776978
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.5349075794219971,
+ "confianca": 0.87841796875,
+ "x": 0.21328125894069672
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.3011344373226166,
+ "confianca": 0.6630859375,
+ "y": 0.47717082500457764
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "x": 0.22375056147575378,
+ "confianca": 0.8505859375,
+ "y": 0.5721350312232971
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.5847370624542236,
+ "confianca": 0.876953125,
+ "x": 0.3368803858757019
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.46988868713378906,
+ "confianca": 0.8896484375,
+ "x": 0.2903258800506592
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.611436128616333,
+ "confianca": 0.90966796875,
+ "x": 0.21892817318439484
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "y": 0.528741717338562,
+ "confianca": 0.87939453125,
+ "x": 0.2991749942302704
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "x": 0.2852766811847687,
+ "confianca": 0.68212890625,
+ "y": 0.41689014434814453
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "x": 0.23896922171115875,
+ "confianca": 0.8515625,
+ "y": 0.4722064137458801
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.4710060954093933,
+ "confianca": 0.8857421875,
+ "x": 0.2621763348579407
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "x": 0.2741371989250183,
+ "confianca": 0.8642578125,
+ "y": 0.7822844386100769
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "x": 0.19635368883609772,
+ "confianca": 0.89794921875,
+ "y": 0.6625171899795532
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.6925556659698486,
+ "confianca": 0.556640625,
+ "x": 0.3099360466003418
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.7878786325454712,
+ "confianca": 0.7919921875,
+ "x": 0.23164664208889008
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "x": 0.26079192757606506,
+ "confianca": 0.75048828125,
+ "y": 0.8528368473052979
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.2634032666683197,
+ "confianca": 0.833984375,
+ "y": 0.7037360072135925
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "x": 0.2539822459220886,
+ "confianca": 0.8544921875,
+ "y": 0.6691070795059204
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.6976571083068848,
+ "confianca": 0.81787109375,
+ "x": 0.20619571208953857
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.2981734573841095,
+ "confianca": 0.5546875,
+ "y": 0.7584294080734253
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.7313187122344971,
+ "confianca": 0.853515625,
+ "x": 0.2678661048412323
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "x": 0.31161436438560486,
+ "confianca": 0.798828125,
+ "y": 0.6908643245697021
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "x": 0.21749383211135864,
+ "confianca": 0.8330078125,
+ "y": 0.7309919595718384
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "x": 0.3188052177429199,
+ "confianca": 0.6298828125,
+ "y": 0.6521797776222229
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "x": 0.23260445892810822,
+ "confianca": 0.81494140625,
+ "y": 0.7003704309463501
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.6375858783721924,
+ "confianca": 0.77880859375,
+ "x": 0.32952558994293213
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "x": 0.29953861236572266,
+ "confianca": 0.8525390625,
+ "y": 0.7783830761909485
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.6618906259536743,
+ "confianca": 0.88232421875,
+ "x": 0.22528143227100372
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "x": 0.250380277633667,
+ "confianca": 0.86328125,
+ "y": 0.7851277589797974
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "x": 0.3072136640548706,
+ "confianca": 0.88037109375,
+ "y": 0.7269657254219055
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "x": 0.2899397015571594,
+ "confianca": 0.67333984375,
+ "y": 0.8231823444366455
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.7339023351669312,
+ "confianca": 0.87109375,
+ "x": 0.2402215451002121
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.7866212725639343,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.78662109375,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "score_global": 0.62353515625
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "retangulo",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "largura": 0.2843138575553894,
+ "x": 0.5710967779159546,
+ "altura": 0.4256795644760132,
+ "y": 0.396381139755249
+ },
+ "cantos": {
+ "superior_direito": {
+ "y": 0.7012904286384583,
+ "x": 0.855410635471344
+ },
+ "inferior_direito": {
+ "y": 0.40620118379592896,
+ "x": 0.8050353527069092
+ },
+ "inferior_esquerdo": {
+ "y": 0.396381139755249,
+ "x": 0.5977870225906372
+ },
+ "superior_esquerdo": {
+ "y": 0.8220607042312622,
+ "x": 0.5710967779159546
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectRectanglesRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4842325846354167
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "texto": "As fotografias apresentadas são de alta qualidade, destacando os contornos e a estética da pessoa."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ }
+ ]
+ },
+ {
+ "timestamp_na_origem": 42.8084333333333,
+ "evidencias": [
+ {
+ "tipo": "rosto",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "y": 0.44231587648391724,
+ "altura": 0.307657927274704,
+ "largura": 0.17305758595466614,
+ "x": 0.5922093987464905
+ },
+ "landmarks": {
+ "nariz": [
+ {
+ "y": 0.5261019468307495,
+ "x": 0.6188080310821533
+ },
+ {
+ "y": 0.6085143685340881,
+ "x": 0.5022847652435303
+ },
+ {
+ "y": 0.6058123707771301,
+ "x": 0.4136081039905548
+ },
+ {
+ "y": 0.5633901953697205,
+ "x": 0.4146895408630371
+ },
+ {
+ "y": 0.5212382674217224,
+ "x": 0.40766027569770813
+ },
+ {
+ "y": 0.47962671518325806,
+ "x": 0.415770947933197
+ },
+ {
+ "y": 0.4374747574329376,
+ "x": 0.415770947933197
+ },
+ {
+ "y": 0.43909597396850586,
+ "x": 0.5039069056510925
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "y": 0.7898218035697937,
+ "x": 0.6682831645011902
+ },
+ {
+ "y": 0.7433465719223022,
+ "x": 0.704781174659729
+ },
+ {
+ "y": 0.6682296395301819,
+ "x": 0.6961297988891602
+ },
+ {
+ "y": 0.6260777115821838,
+ "x": 0.653954267501831
+ },
+ {
+ "y": 0.6766059994697571,
+ "x": 0.6436808109283447
+ },
+ {
+ "y": 0.7449678182601929,
+ "x": 0.64259934425354
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "y": 0.1937500238418579,
+ "x": 0.7660167217254639
+ },
+ {
+ "y": 0.321286678314209,
+ "x": 0.8145455718040466
+ },
+ {
+ "y": 0.45584869384765625,
+ "x": 0.7871044874191284
+ },
+ {
+ "y": 0.44882336258888245,
+ "x": 0.7393866777420044
+ },
+ {
+ "y": 0.3229079246520996,
+ "x": 0.7653408050537109
+ },
+ {
+ "y": 0.19753289222717285,
+ "x": 0.7393866777420044
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "y": 0.8698024153709412,
+ "x": 0.7702072262763977
+ },
+ {
+ "y": 0.7390233278274536,
+ "x": 0.8218451738357544
+ },
+ {
+ "y": 0.6009486317634583,
+ "x": 0.7883210778236389
+ },
+ {
+ "y": 0.6077038049697876,
+ "x": 0.7423605918884277
+ },
+ {
+ "y": 0.7371318936347961,
+ "x": 0.7716941833496094
+ },
+ {
+ "y": 0.8649387359619141,
+ "x": 0.7437123656272888
+ }
+ ],
+ "rosto": [
+ {
+ "y": 0.13754743337631226,
+ "x": 0.6582800149917603
+ },
+ {
+ "y": 0.13322414457798004,
+ "x": 0.5509487986564636
+ },
+ {
+ "y": 0.14078988134860992,
+ "x": 0.4422658085823059
+ },
+ {
+ "y": 0.1553809493780136,
+ "x": 0.3335828185081482
+ },
+ {
+ "y": 0.18294183909893036,
+ "x": 0.22814412415027618
+ },
+ {
+ "y": 0.23536156117916107,
+ "x": 0.1329789161682129
+ },
+ {
+ "y": 0.3104785084724426,
+ "x": 0.054035067558288574
+ },
+ {
+ "y": 0.40126731991767883,
+ "x": -0.005443180445581675
+ },
+ {
+ "y": 0.5077279806137085,
+ "x": -0.028153056278824806
+ },
+ {
+ "y": 0.6155397295951843,
+ "x": -0.0070653147995471954
+ },
+ {
+ "y": 0.7090305685997009,
+ "x": 0.04970937594771385
+ },
+ {
+ "y": 0.7887409925460815,
+ "x": 0.1270311027765274
+ },
+ {
+ "y": 0.8473754525184631,
+ "x": 0.22165557742118835
+ },
+ {
+ "y": 0.8829073905944824,
+ "x": 0.32817572355270386
+ },
+ {
+ "y": 0.9037131071090698,
+ "x": 0.4379401206970215
+ },
+ {
+ "y": 0.9170882701873779,
+ "x": 0.5485155582427979
+ },
+ {
+ "y": 0.9187095165252686,
+ "x": 0.6588206887245178
+ }
+ ],
+ "olho_direito": [
+ {
+ "y": 0.2634628713130951,
+ "x": 0.6653092503547668
+ },
+ {
+ "y": 0.3093976676464081,
+ "x": 0.7023479342460632
+ },
+ {
+ "y": 0.38397419452667236,
+ "x": 0.6942372918128967
+ },
+ {
+ "y": 0.42720696330070496,
+ "x": 0.6542246341705322
+ },
+ {
+ "y": 0.37694889307022095,
+ "x": 0.6439511179924011
+ },
+ {
+ "y": 0.308857262134552,
+ "x": 0.6407068967819214
+ }
+ ],
+ "labios": [
+ {
+ "y": 0.6128376722335815,
+ "x": 0.2843781113624573
+ },
+ {
+ "y": 0.5820343494415283,
+ "x": 0.30654725432395935
+ },
+ {
+ "y": 0.5487991571426392,
+ "x": 0.3211464583873749
+ },
+ {
+ "y": 0.5152937173843384,
+ "x": 0.3141172230243683
+ },
+ {
+ "y": 0.4817883372306824,
+ "x": 0.3216871917247772
+ },
+ {
+ "y": 0.44612130522727966,
+ "x": 0.30871009826660156
+ },
+ {
+ "y": 0.41315633058547974,
+ "x": 0.2876223623752594
+ },
+ {
+ "y": 0.385595440864563,
+ "x": 0.26274964213371277
+ },
+ {
+ "y": 0.4169391989707947,
+ "x": 0.22273699939250946
+ },
+ {
+ "y": 0.46071237325668335,
+ "x": 0.1946200132369995
+ },
+ {
+ "y": 0.5104300379753113,
+ "x": 0.18488721549510956
+ },
+ {
+ "y": 0.5596073269844055,
+ "x": 0.19299788773059845
+ },
+ {
+ "y": 0.6041911244392395,
+ "x": 0.2200334519147873
+ },
+ {
+ "y": 0.6366156935691833,
+ "x": 0.2578832507133484
+ }
+ ]
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "y": 0.4326367974281311,
+ "altura": 0.3094255030155182,
+ "x": 0.5923697352409363,
+ "largura": 0.17405183613300323
+ },
+ "score": 0.74560546875
+ },
+ "confianca": 0.8566718697547913,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.6771149435954088,
+ "bounding_box": {
+ "y": 0.18567254356822155,
+ "altura": 0.8092105329472411,
+ "x": 0.00041118422829497026,
+ "largura": 0.8367599234395474
+ }
+ },
+ "confianca": 0.7385188937187195,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "y": 0.8246943950653076,
+ "confianca": 0.403564453125,
+ "x": 0.4901951849460602
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "y": 0.6214334964752197,
+ "confianca": 0.75390625,
+ "x": 0.6754902601242065
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "y": 0.3119756579399109,
+ "confianca": 0.239013671875,
+ "x": 0.48893752694129944
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "y": 0.5626635551452637,
+ "confianca": 0.83349609375,
+ "x": 0.7052652835845947
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "y": 0.2354884147644043,
+ "confianca": 0.5771484375,
+ "x": 0.2293081283569336
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "y": 0.4734950661659241,
+ "confianca": 0.45654296875,
+ "x": 0.7029047608375549
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "y": 0.6794328689575195,
+ "confianca": 0.91943359375,
+ "x": 0.7046099305152893
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "y": 0.5683350265026093,
+ "confianca": 0.3212890625,
+ "x": 0.4895663559436798
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "y": 0.9528840780258179,
+ "confianca": 0.287353515625,
+ "x": 0.27095717191696167
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "y": 0.7450624704360962,
+ "confianca": 0.57080078125,
+ "x": 0.6722405552864075
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "y": 0.3825128674507141,
+ "confianca": 0.609375,
+ "x": 0.24042096734046936
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "y": 0.8838084936141968,
+ "confianca": 0.77978515625,
+ "x": 0.2462765872478485
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.5787098407745361,
+ "confianca": 0.88427734375,
+ "x": 0.2029336392879486
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "y": 0.52995765209198,
+ "confianca": 0.88525390625,
+ "x": 0.294769287109375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.551985502243042,
+ "confianca": 0.8115234375,
+ "x": 0.3123168349266052
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.5828185677528381,
+ "confianca": 0.92431640625,
+ "x": 0.33720800280570984
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "y": 0.6048422455787659,
+ "confianca": 0.931640625,
+ "x": 0.30211153626441956
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.6205059289932251,
+ "confianca": 0.91162109375,
+ "x": 0.21364377439022064
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.5377410650253296,
+ "confianca": 0.880859375,
+ "x": 0.2539910078048706
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.47290313243865967,
+ "confianca": 0.8828125,
+ "x": 0.2845867872238159
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.47642379999160767,
+ "confianca": 0.892578125,
+ "x": 0.25691884756088257
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.5429835319519043,
+ "confianca": 0.880859375,
+ "x": 0.21032053232192993
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.541413426399231,
+ "confianca": 0.86376953125,
+ "x": 0.22669470310211182
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "y": 0.5759070515632629,
+ "confianca": 0.8486328125,
+ "x": 0.2492918074131012
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "y": 0.41546493768692017,
+ "confianca": 0.71728515625,
+ "x": 0.28145360946655273
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "y": 0.3794711232185364,
+ "confianca": 0.79638671875,
+ "x": 0.24654172360897064
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.4816918969154358,
+ "confianca": 0.86083984375,
+ "x": 0.23396262526512146
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.48514413833618164,
+ "confianca": 0.701171875,
+ "x": 0.3020922541618347
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.5654369592666626,
+ "confianca": 0.91796875,
+ "x": 0.2995390295982361
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.6177207231521606,
+ "confianca": 0.88818359375,
+ "x": 0.24132758378982544
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.48619210720062256,
+ "confianca": 0.833984375,
+ "x": 0.22056065499782562
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.5806756615638733,
+ "confianca": 0.85107421875,
+ "x": 0.22008036077022552
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.6181018352508545,
+ "confianca": 0.89306640625,
+ "x": 0.19620148837566376
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.6792576313018799,
+ "confianca": 0.8291015625,
+ "x": 0.2052418440580368
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "y": 0.7125279903411865,
+ "confianca": 0.86279296875,
+ "x": 0.3039427697658539
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.7126101851463318,
+ "confianca": 0.401123046875,
+ "x": 0.2952648103237152
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.6397429704666138,
+ "confianca": 0.83984375,
+ "x": 0.2192586064338684
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "y": 0.6352751851081848,
+ "confianca": 0.650390625,
+ "x": 0.3193773031234741
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.6875754594802856,
+ "confianca": 0.22265625,
+ "x": 0.2926397919654846
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.7157774567604065,
+ "confianca": 0.8837890625,
+ "x": 0.26281729340553284
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.7652794718742371,
+ "confianca": 0.85107421875,
+ "x": 0.2944953441619873
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.7661572098731995,
+ "confianca": 0.86083984375,
+ "x": 0.2682417035102844
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.7138841152191162,
+ "confianca": 0.8125,
+ "x": 0.21498611569404602
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.715700626373291,
+ "confianca": 0.865234375,
+ "x": 0.23597568273544312
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "y": 0.6861793994903564,
+ "confianca": 0.82861328125,
+ "x": 0.25870639085769653
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "y": 0.8412413001060486,
+ "confianca": 0.75439453125,
+ "x": 0.2556542754173279
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.7679053544998169,
+ "confianca": 0.84423828125,
+ "x": 0.24433951079845428
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.6774369478225708,
+ "confianca": 0.80615234375,
+ "x": 0.3093108832836151
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "y": 0.8142095804214478,
+ "confianca": 0.6728515625,
+ "x": 0.28512266278266907
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.7608442902565002,
+ "confianca": 0.5810546875,
+ "x": 0.2937327027320862
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.6480720043182373,
+ "confianca": 0.8330078125,
+ "x": 0.24711398780345917
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.6797212362289429,
+ "confianca": 0.865234375,
+ "x": 0.22839006781578064
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.6392928957939148,
+ "confianca": 0.87109375,
+ "x": 0.194611594080925
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.7705070376396179,
+ "confianca": 0.849609375,
+ "x": 0.2266223281621933
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.7885743975639343,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.78857421875,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "score_global": 0.65087890625
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "retangulo",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "y": 0.40177270770072937,
+ "altura": 0.4242468774318695,
+ "largura": 0.2787071466445923,
+ "x": 0.5711026787757874
+ },
+ "cantos": {
+ "superior_esquerdo": {
+ "y": 0.8260195851325989,
+ "x": 0.5711026787757874
+ },
+ "inferior_direito": {
+ "y": 0.40177270770072937,
+ "x": 0.812043309211731
+ },
+ "superior_direito": {
+ "y": 0.7073580026626587,
+ "x": 0.8498098254203796
+ },
+ "inferior_esquerdo": {
+ "y": 0.40224143862724304,
+ "x": 0.6066030859947205
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectRectanglesRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4851938883463542
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "texto": "O rosto é de uma pessoa jovem com cabelo curto e escuro, de expressão neutra. A qualidade do rosto é alta, com detalhes claros e bem definidos. A pessoa está em uma pose casual, com as mãos ao lado do corpo. Há duas mãos visíveis no quadro, ambas de mãos abertas. A categoria do rosto é de uma mulher. Outra categoria presente no quadro é de um homem. A estética do rosto é natural e realista. O horizonte está no fundo do quadro, além do qual há um retângulo. Os contornos do rosto e das mãos são bem definidos, e a segmentação de pessoas é clara e precisa."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ }
+ ]
+ },
+ {
+ "timestamp_na_origem": 43.8084333333333,
+ "evidencias": [
+ {
+ "tipo": "rosto",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.30600544810295105,
+ "y": 0.43403658270835876,
+ "x": 0.5989925265312195,
+ "largura": 0.17212805151939392
+ },
+ "landmarks": {
+ "labios": [
+ {
+ "y": 0.6504712104797363,
+ "x": 0.2890128493309021
+ },
+ {
+ "y": 0.6080748438835144,
+ "x": 0.30522432923316956
+ },
+ {
+ "y": 0.5656785368919373,
+ "x": 0.3133300542831421
+ },
+ {
+ "y": 0.5270627737045288,
+ "x": 0.30522432923316956
+ },
+ {
+ "y": 0.4892570972442627,
+ "x": 0.311708927154541
+ },
+ {
+ "y": 0.44605064392089844,
+ "x": 0.3030627965927124
+ },
+ {
+ "y": 0.4039243459701538,
+ "x": 0.287391722202301
+ },
+ {
+ "y": 0.3671988546848297,
+ "x": 0.26631680130958557
+ },
+ {
+ "y": 0.4077048897743225,
+ "x": 0.2171420156955719
+ },
+ {
+ "y": 0.4644133746623993,
+ "x": 0.18471907079219818
+ },
+ {
+ "y": 0.5276028513908386,
+ "x": 0.1739114224910736
+ },
+ {
+ "y": 0.5899821519851685,
+ "x": 0.1836383044719696
+ },
+ {
+ "y": 0.6469606757164001,
+ "x": 0.21552085876464844
+ },
+ {
+ "y": 0.6863865852355957,
+ "x": 0.2652360498905182
+ }
+ ],
+ "nariz": [
+ {
+ "y": 0.5276028513908386,
+ "x": 0.6040557622909546
+ },
+ {
+ "y": 0.617256224155426,
+ "x": 0.4957091212272644
+ },
+ {
+ "y": 0.6202266812324524,
+ "x": 0.41059887409210205
+ },
+ {
+ "y": 0.5729696154594421,
+ "x": 0.4073565900325775
+ },
+ {
+ "y": 0.5265226364135742,
+ "x": 0.39546817541122437
+ },
+ {
+ "y": 0.4800757169723511,
+ "x": 0.4068162143230438
+ },
+ {
+ "y": 0.4325486123561859,
+ "x": 0.41005849838256836
+ },
+ {
+ "y": 0.4352490305900574,
+ "x": 0.4948985278606415
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "y": 0.7922424077987671,
+ "x": 0.6529603600502014
+ },
+ {
+ "y": 0.7449853420257568,
+ "x": 0.6910573244094849
+ },
+ {
+ "y": 0.6688339114189148,
+ "x": 0.6802496910095215
+ },
+ {
+ "y": 0.6264376044273376,
+ "x": 0.6353979110717773
+ },
+ {
+ "y": 0.6774752140045166,
+ "x": 0.6251306533813477
+ },
+ {
+ "y": 0.7471456527709961,
+ "x": 0.6256710290908813
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "y": 0.19221268594264984,
+ "x": 0.7545522451400757
+ },
+ {
+ "y": 0.3223721385002136,
+ "x": 0.8046727180480957
+ },
+ {
+ "y": 0.45793241262435913,
+ "x": 0.7750867605209351
+ },
+ {
+ "y": 0.45145145058631897,
+ "x": 0.7267225384712219
+ },
+ {
+ "y": 0.32399237155914307,
+ "x": 0.7545522451400757
+ },
+ {
+ "y": 0.19653333723545074,
+ "x": 0.7278033494949341
+ }
+ ],
+ "rosto": [
+ {
+ "y": 0.1376645267009735,
+ "x": 0.6416123509407043
+ },
+ {
+ "y": 0.13496412336826324,
+ "x": 0.5324550867080688
+ },
+ {
+ "y": 0.14198517799377441,
+ "x": 0.4219469130039215
+ },
+ {
+ "y": 0.15710744261741638,
+ "x": 0.3122493028640747
+ },
+ {
+ "y": 0.1868118792772293,
+ "x": 0.20525360107421875
+ },
+ {
+ "y": 0.2440604418516159,
+ "x": 0.11122707277536392
+ },
+ {
+ "y": 0.32291221618652344,
+ "x": 0.03341201692819595
+ },
+ {
+ "y": 0.41688627004623413,
+ "x": -0.023328127339482307
+ },
+ {
+ "y": 0.5249024033546448,
+ "x": -0.044403038918972015
+ },
+ {
+ "y": 0.6323785185813904,
+ "x": -0.021166598424315453
+ },
+ {
+ "y": 0.7239221930503845,
+ "x": 0.03719469532370567
+ },
+ {
+ "y": 0.8027739524841309,
+ "x": 0.11392898857593536
+ },
+ {
+ "y": 0.8596174716949463,
+ "x": 0.20849590003490448
+ },
+ {
+ "y": 0.892427384853363,
+ "x": 0.31441083550453186
+ },
+ {
+ "y": 0.9088998436927795,
+ "x": 0.42464882135391235
+ },
+ {
+ "y": 0.9184862375259399,
+ "x": 0.5348868370056152
+ },
+ {
+ "y": 0.9182162284851074,
+ "x": 0.644044041633606
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "y": 0.875549852848053,
+ "x": 0.7599560618400574
+ },
+ {
+ "y": 0.7449853420257568,
+ "x": 0.8121029734611511
+ },
+ {
+ "y": 0.6072647571563721,
+ "x": 0.7757622599601746
+ },
+ {
+ "y": 0.6148258447647095,
+ "x": 0.7291542887687683
+ },
+ {
+ "y": 0.7436351180076599,
+ "x": 0.7617123126983643
+ },
+ {
+ "y": 0.8712291717529297,
+ "x": 0.7337475419044495
+ }
+ ],
+ "olho_direito": [
+ {
+ "y": 0.265123575925827,
+ "x": 0.6478267312049866
+ },
+ {
+ "y": 0.31049036979675293,
+ "x": 0.6870044469833374
+ },
+ {
+ "y": 0.3866417407989502,
+ "x": 0.6770073771476746
+ },
+ {
+ "y": 0.43038830161094666,
+ "x": 0.63431715965271
+ },
+ {
+ "y": 0.3796207010746002,
+ "x": 0.6235095262527466
+ },
+ {
+ "y": 0.31049036979675293,
+ "x": 0.6213480234146118
+ }
+ ]
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "score": 0.82177734375,
+ "bounding_box": {
+ "altura": 0.3067805767059326,
+ "x": 0.5961717963218689,
+ "y": 0.428731232881546,
+ "largura": 0.1725640743970871
+ }
+ },
+ "confianca": 0.8648970127105713,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.8333333854438276,
+ "x": 0.001644736913179881,
+ "y": 0.1666666503189592,
+ "largura": 0.8347040054412863
+ },
+ "ocupacao_do_quadro": 0.6955867146979102
+ },
+ "confianca": 0.7371953725814819,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "x": 0.2541317939758301,
+ "y": 0.955114483833313,
+ "confianca": 0.417236328125
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "x": 0.7073267698287964,
+ "confianca": 0.8310546875,
+ "y": 0.5492178201675415
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "y": 1,
+ "x": 0,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "x": 0,
+ "confianca": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "y": 0.9019889831542969,
+ "x": 0.24847324192523956,
+ "confianca": 0.7421875
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "y": 0.46096473932266235,
+ "confianca": 0.52294921875,
+ "x": 0.7028272747993469
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "y": 0.5644422173500061,
+ "confianca": 0.31689453125,
+ "x": 0.4922351837158203
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "y": 1,
+ "x": 0,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "y": 0.8207155466079712,
+ "x": 0.4964037835597992,
+ "confianca": 0.427734375
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "x": 0.48806658387184143,
+ "y": 0.308168888092041,
+ "confianca": 0.2060546875
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "x": 0.6787952780723572,
+ "y": 0.7347357869148254,
+ "confianca": 0.55908203125
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "y": 1,
+ "x": 0,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "y": 0.6041240692138672,
+ "confianca": 0.771484375,
+ "x": 0.6781229972839355
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "y": 0.30549144744873047,
+ "x": 0.24358662962913513,
+ "confianca": 0.40478515625
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "y": 0.665906548500061,
+ "x": 0.7058812379837036,
+ "confianca": 0.8798828125
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "y": 1,
+ "x": 0,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "y": 0.22589260339736938,
+ "x": 0.22988224029541016,
+ "confianca": 0.52392578125
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.41664886474609375,
+ "confianca": 0.82763671875,
+ "x": 0.22734349966049194
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.4433097243309021,
+ "confianca": 0.8701171875,
+ "x": 0.2997828423976898
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "x": 0.30574584007263184,
+ "confianca": 0.826171875,
+ "y": 0.4712445139884949
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.3756043314933777,
+ "x": 0.2356368750333786,
+ "confianca": 0.82470703125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.3738619089126587,
+ "x": 0.25909894704818726,
+ "confianca": 0.88037109375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.47697317600250244,
+ "x": 0.2425447702407837,
+ "confianca": 0.8935546875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "x": 0.25588104128837585,
+ "y": 0.4183407425880432,
+ "confianca": 0.85595703125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.38063567876815796,
+ "x": 0.22104859352111816,
+ "confianca": 0.79345703125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.25067564845085144,
+ "confianca": 0.82421875,
+ "y": 0.4451698660850525
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.5099343061447144,
+ "confianca": 0.8720703125,
+ "x": 0.33272117376327515
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.37415146827697754,
+ "confianca": 0.86474609375,
+ "x": 0.2858639657497406
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.4254671335220337,
+ "x": 0.20807614922523499,
+ "confianca": 0.8212890625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.4703480005264282,
+ "confianca": 0.87548828125,
+ "x": 0.2113194614648819
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.4537249803543091,
+ "x": 0.19627533853054047,
+ "confianca": 0.87890625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.40504950284957886,
+ "confianca": 0.521484375,
+ "x": 0.28551316261291504
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "x": 0.2944768965244293,
+ "y": 0.41727161407470703,
+ "confianca": 0.8984375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.4748254418373108,
+ "x": 0.304047167301178,
+ "confianca": 0.765625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "y": 0.33696943521499634,
+ "confianca": 0.669921875,
+ "x": 0.27598780393600464
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "x": 0.24664856493473053,
+ "confianca": 0.82275390625,
+ "y": 0.2963494062423706
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.48113882541656494,
+ "x": 0.1826593428850174,
+ "confianca": 0.9091796875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "x": 0.21964502334594727,
+ "confianca": 0.7685546875,
+ "y": 0.44222742319107056
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.7572119235992432,
+ "x": 0.23795345425605774,
+ "confianca": 0.8544921875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "y": 0.7169313430786133,
+ "confianca": 0.796875,
+ "x": 0.3179456889629364
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.7410318851470947,
+ "confianca": 0.845703125,
+ "x": 0.3101845979690552
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.8043133020401001,
+ "confianca": 0.85302734375,
+ "x": 0.24955791234970093
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "x": 0.27290114760398865,
+ "confianca": 0.8505859375,
+ "y": 0.801953911781311
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "x": 0.2498348206281662,
+ "confianca": 0.837890625,
+ "y": 0.7068570852279663
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.760705292224884,
+ "x": 0.2666851580142975,
+ "confianca": 0.85205078125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.8029155731201172,
+ "x": 0.23146219551563263,
+ "confianca": 0.814453125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.2589128315448761,
+ "y": 0.7370247840881348,
+ "confianca": 0.7900390625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "x": 0.33405426144599915,
+ "confianca": 0.9013671875,
+ "y": 0.6527906656265259
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.79771888256073,
+ "x": 0.29569679498672485,
+ "confianca": 0.8369140625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "x": 0.2152380645275116,
+ "confianca": 0.833984375,
+ "y": 0.7499773502349854
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.6911904811859131,
+ "confianca": 0.8603515625,
+ "x": 0.22073163092136383
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.717841386795044,
+ "x": 0.20297609269618988,
+ "confianca": 0.86669921875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.29906606674194336,
+ "confianca": 0.6376953125,
+ "y": 0.7559511065483093
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "x": 0.2864466905593872,
+ "y": 0.8304828405380249,
+ "confianca": 0.6806640625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.693830132484436,
+ "x": 0.3129860758781433,
+ "confianca": 0.81103515625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "x": 0.30399829149246216,
+ "y": 0.7661673426628113,
+ "confianca": 0.78173828125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "y": 0.8689616918563843,
+ "x": 0.2592129409313202,
+ "confianca": 0.75390625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "x": 0.19054484367370605,
+ "confianca": 0.88525390625,
+ "y": 0.6822276711463928
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.7259582281112671,
+ "x": 0.22809100151062012,
+ "confianca": 0.80126953125
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.8041994571685791,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.80419921875,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "score_global": 0.640625
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "retangulo",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "cantos": {
+ "superior_direito": {
+ "y": 0.6926843523979187,
+ "x": 0.8623307347297668
+ },
+ "superior_esquerdo": {
+ "y": 0.8083095550537109,
+ "x": 0.5894560217857361
+ },
+ "inferior_esquerdo": {
+ "y": 0.3894540071487427,
+ "x": 0.599976122379303
+ },
+ "inferior_direito": {
+ "y": 0.3975215256214142,
+ "x": 0.7932700514793396
+ }
+ },
+ "bounding_box": {
+ "altura": 0.41885554790496826,
+ "x": 0.5894560217857361,
+ "y": 0.3894540071487427,
+ "largura": 0.27287471294403076
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectRectanglesRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4823862711588542
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "texto": "As imagens apresentadas são de pessoas, todas com expressões neutras e poses neutras, e possuem alta qualidade de imagem. As pessoas estão em um ambiente aberto, com um horizonte claro e um céu azul."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ }
+ ]
+ },
+ {
+ "timestamp_na_origem": 44.8084333333333,
+ "evidencias": [
+ {
+ "tipo": "rosto",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "landmarks": {
+ "olho_direito": [
+ {
+ "x": 0.6931342482566833,
+ "y": 0.26874667406082153
+ },
+ {
+ "x": 0.7280840873718262,
+ "y": 0.31460943818092346
+ },
+ {
+ "x": 0.7195467352867126,
+ "y": 0.3876698613166809
+ },
+ {
+ "x": 0.6811285614967346,
+ "y": 0.43139946460723877
+ },
+ {
+ "x": 0.6717908382415771,
+ "y": 0.3818036913871765
+ },
+ {
+ "x": 0.6691229343414307,
+ "y": 0.31460943818092346
+ }
+ ],
+ "rosto": [
+ {
+ "x": 0.6904663443565369,
+ "y": 0.13809116184711456
+ },
+ {
+ "x": 0.5802809000015259,
+ "y": 0.1359580159187317
+ },
+ {
+ "x": 0.46929511427879333,
+ "y": 0.14342403411865234
+ },
+ {
+ "x": 0.3583092987537384,
+ "y": 0.15835610032081604
+ },
+ {
+ "x": 0.2510586082935333,
+ "y": 0.18928678333759308
+ },
+ {
+ "x": 0.15501320362091064,
+ "y": 0.24581529200077057
+ },
+ {
+ "x": 0.0755089595913887,
+ "y": 0.3242086172103882
+ },
+ {
+ "x": 0.014680201187729836,
+ "y": 0.4175339639186859
+ },
+ {
+ "x": -0.00719680730253458,
+ "y": 0.5265913605690002
+ },
+ {
+ "x": 0.01734812930226326,
+ "y": 0.6353820562362671
+ },
+ {
+ "x": 0.07871046662330627,
+ "y": 0.7273741960525513
+ },
+ {
+ "x": 0.1582147181034088,
+ "y": 0.806167483329773
+ },
+ {
+ "x": 0.2553272843360901,
+ "y": 0.8644291758537292
+ },
+ {
+ "x": 0.36311158537864685,
+ "y": 0.8972263336181641
+ },
+ {
+ "x": 0.4740973711013794,
+ "y": 0.9142915606498718
+ },
+ {
+ "x": 0.5866839289665222,
+ "y": 0.9237574338912964
+ },
+ {
+ "x": 0.6976697444915771,
+ "y": 0.923224151134491
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "y": 0.7921686768531799,
+ "x": 0.7024720311164856
+ },
+ {
+ "y": 0.7455059885978699,
+ "x": 0.7366214990615845
+ },
+ {
+ "y": 0.6716456413269043,
+ "x": 0.725149393081665
+ },
+ {
+ "y": 0.6284493207931519,
+ "x": 0.6840633153915405
+ },
+ {
+ "y": 0.6793782711029053,
+ "x": 0.6755259037017822
+ },
+ {
+ "y": 0.7476391196250916,
+ "x": 0.676326334476471
+ }
+ ],
+ "nariz": [
+ {
+ "x": 0.6469790935516357,
+ "y": 0.5289911031723022
+ },
+ {
+ "x": 0.5407955646514893,
+ "y": 0.6191167831420898
+ },
+ {
+ "x": 0.45755621790885925,
+ "y": 0.6231164336204529
+ },
+ {
+ "x": 0.4522203803062439,
+ "y": 0.5751205086708069
+ },
+ {
+ "x": 0.4388807415962219,
+ "y": 0.5287244915962219
+ },
+ {
+ "x": 0.4516867697238922,
+ "y": 0.48206180334091187
+ },
+ {
+ "x": 0.45755621790885925,
+ "y": 0.4345991909503937
+ },
+ {
+ "x": 0.5397284030914307,
+ "y": 0.438332200050354
+ }
+ ],
+ "labios": [
+ {
+ "x": 0.3358987271785736,
+ "y": 0.6545804142951965
+ },
+ {
+ "x": 0.35243988037109375,
+ "y": 0.611117422580719
+ },
+ {
+ "x": 0.3599100708961487,
+ "y": 0.5665879249572754
+ },
+ {
+ "x": 0.35190626978874207,
+ "y": 0.5276579260826111
+ },
+ {
+ "x": 0.3588429093360901,
+ "y": 0.4889945387840271
+ },
+ {
+ "x": 0.35137268900871277,
+ "y": 0.4447316527366638
+ },
+ {
+ "x": 0.3353651165962219,
+ "y": 0.4015353322029114
+ },
+ {
+ "x": 0.31295454502105713,
+ "y": 0.36420518159866333
+ },
+ {
+ "x": 0.25746163725852966,
+ "y": 0.40313521027565
+ },
+ {
+ "x": 0.22064423561096191,
+ "y": 0.462330162525177
+ },
+ {
+ "x": 0.20837175846099854,
+ "y": 0.5281912088394165
+ },
+ {
+ "x": 0.21850988268852234,
+ "y": 0.5945855379104614
+ },
+ {
+ "x": 0.2558608651161194,
+ "y": 0.6532471776008606
+ },
+ {
+ "x": 0.31082019209861755,
+ "y": 0.6913772821426392
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "x": 0.7998513579368591,
+ "y": 0.19248650968074799
+ },
+ {
+ "x": 0.8449393510818481,
+ "y": 0.32260873913764954
+ },
+ {
+ "x": 0.8130576014518738,
+ "y": 0.4564639925956726
+ },
+ {
+ "x": 0.7654350996017456,
+ "y": 0.4500645399093628
+ },
+ {
+ "x": 0.796116292476654,
+ "y": 0.3236753046512604
+ },
+ {
+ "x": 0.7734388709068298,
+ "y": 0.19675281643867493
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "x": 0.8125240206718445,
+ "y": 0.8741616606712341
+ },
+ {
+ "x": 0.8578788042068481,
+ "y": 0.7415063381195068
+ },
+ {
+ "x": 0.8167927265167236,
+ "y": 0.6055179238319397
+ },
+ {
+ "x": 0.7713045477867126,
+ "y": 0.6140505075454712
+ },
+ {
+ "x": 0.8078551292419434,
+ "y": 0.7417729496955872
+ },
+ {
+ "x": 0.7863783240318298,
+ "y": 0.8700286746025085
+ }
+ ]
+ },
+ "bounding_box": {
+ "y": 0.42103415727615356,
+ "altura": 0.3091081380844116,
+ "largura": 0.17387333512306213,
+ "x": 0.5870870351791382
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "score": 0.81884765625,
+ "bounding_box": {
+ "y": 0.4200640618801117,
+ "altura": 0.30747029185295105,
+ "largura": 0.17295202612876892,
+ "x": 0.5924886465072632
+ }
+ },
+ "confianca": 0.8763939738273621,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "y": 0.16703213625396782,
+ "altura": 0.8333333966142527,
+ "largura": 0.831825675156065,
+ "x": 0.003495065853195753
+ },
+ "ocupacao_do_quadro": 0.6931881152687476
+ },
+ "confianca": 0.7348734736442566,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "y": 0.7244069576263428,
+ "confianca": 0.58837890625,
+ "x": 0.6797021627426147
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "confianca": 0.349365234375,
+ "y": 0.29906171560287476,
+ "x": 0.23292621970176697
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "y": 0.22405678033828735,
+ "confianca": 0.53515625,
+ "x": 0.22977295517921448
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "y": 0.5401444435119629,
+ "confianca": 0.86083984375,
+ "x": 0.7055337429046631
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "y": 0.5576659142971039,
+ "confianca": 0.3468017578125,
+ "x": 0.48951660096645355
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "y": 0.655911922454834,
+ "confianca": 0.91357421875,
+ "x": 0.7049773931503296
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "confianca": 0,
+ "y": 1,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "y": 0.5913517475128174,
+ "confianca": 0.7529296875,
+ "x": 0.6756554841995239
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "y": 0.2971024513244629,
+ "confianca": 0.267822265625,
+ "x": 0.48464494943618774
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "confianca": 0,
+ "y": 1,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "y": 0.8182293772697449,
+ "confianca": 0.42578125,
+ "x": 0.49438825249671936
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "y": 0.4532662034034729,
+ "confianca": 0.417236328125,
+ "x": 0.7011292576789856
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "confianca": 0.79833984375,
+ "y": 0.9070571660995483,
+ "x": 0.2446686029434204
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "y": 0.9545372724533081,
+ "confianca": 0.4140625,
+ "x": 0.26311594247817993
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.4970378875732422,
+ "confianca": 0.892578125,
+ "x": 0.3226650059223175
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.4270194172859192,
+ "confianca": 0.8232421875,
+ "x": 0.20418210327625275
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "confianca": 0.84033203125,
+ "y": 0.4022483229637146,
+ "x": 0.2424001544713974
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.45535868406295776,
+ "confianca": 0.84375,
+ "x": 0.19546756148338318
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "confianca": 0.86669921875,
+ "y": 0.4576696753501892,
+ "x": 0.2884030044078827
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.4606165885925293,
+ "confianca": 0.87109375,
+ "x": 0.2247980535030365
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "y": 0.42788004875183105,
+ "confianca": 0.7861328125,
+ "x": 0.23596486449241638
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.4653133153915405,
+ "confianca": 0.79345703125,
+ "x": 0.2949221432209015
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.36707639694213867,
+ "confianca": 0.7724609375,
+ "x": 0.22241614758968353
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "confianca": 0.87060546875,
+ "y": 0.4010241627693176,
+ "x": 0.2802160978317261
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.43612730503082275,
+ "confianca": 0.8984375,
+ "x": 0.18047595024108887
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.36190277338027954,
+ "confianca": 0.86669921875,
+ "x": 0.24656204879283905
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "y": 0.28538113832473755,
+ "confianca": 0.80029296875,
+ "x": 0.23563727736473083
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.42696332931518555,
+ "confianca": 0.81982421875,
+ "x": 0.2846023738384247
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "confianca": 0.60009765625,
+ "y": 0.3264840841293335,
+ "x": 0.26690784096717834
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.36233073472976685,
+ "confianca": 0.853515625,
+ "x": 0.2730115056037903
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.4667367935180664,
+ "confianca": 0.87890625,
+ "x": 0.16764038801193237
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.3753747344017029,
+ "confianca": 0.76025390625,
+ "x": 0.2064296156167984
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.40287142992019653,
+ "confianca": 0.50244140625,
+ "x": 0.27863943576812744
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.40222257375717163,
+ "confianca": 0.78759765625,
+ "x": 0.2123108208179474
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.409648060798645,
+ "confianca": 0.79931640625,
+ "x": 0.1911434382200241
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.7681536674499512,
+ "confianca": 0.6044921875,
+ "x": 0.2936704754829407
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.7393319606781006,
+ "confianca": 0.8349609375,
+ "x": 0.2160249501466751
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.7778792381286621,
+ "confianca": 0.87548828125,
+ "x": 0.25603219866752625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.7072114944458008,
+ "confianca": 0.8330078125,
+ "x": 0.20502296090126038
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "y": 0.7343935966491699,
+ "confianca": 0.77880859375,
+ "x": 0.3041329085826874
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "y": 0.7533847093582153,
+ "confianca": 0.83154296875,
+ "x": 0.24720267951488495
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.7061077952384949,
+ "confianca": 0.78662109375,
+ "x": 0.30506694316864014
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.7257874011993408,
+ "confianca": 0.869140625,
+ "x": 0.23451513051986694
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.7345017790794373,
+ "confianca": 0.8740234375,
+ "x": 0.1915646642446518
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.8211532831192017,
+ "confianca": 0.80322265625,
+ "x": 0.24224410951137543
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "y": 0.780459463596344,
+ "confianca": 0.75439453125,
+ "x": 0.2939385771751404
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.8178697824478149,
+ "confianca": 0.87060546875,
+ "x": 0.2641657888889313
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "confianca": 0.81982421875,
+ "y": 0.8123520612716675,
+ "x": 0.28709232807159424
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "confianca": 0.77001953125,
+ "y": 0.8841433525085449,
+ "x": 0.25333037972450256
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "y": 0.8447343707084656,
+ "confianca": 0.69677734375,
+ "x": 0.2805657386779785
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.7565067410469055,
+ "confianca": 0.75244140625,
+ "x": 0.2980576157569885
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.7013180255889893,
+ "confianca": 0.89892578125,
+ "x": 0.17815668880939484
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.6629536151885986,
+ "confianca": 0.88818359375,
+ "x": 0.32562360167503357
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.8195067644119263,
+ "confianca": 0.798828125,
+ "x": 0.22469928860664368
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.7714981436729431,
+ "confianca": 0.8408203125,
+ "x": 0.22778409719467163
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.7658321857452393,
+ "confianca": 0.85498046875,
+ "x": 0.20509029924869537
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.8227543234825134,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.82275390625,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "score_global": 0.65234375
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4822896321614583
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "texto": "O conteúdo é considerado de baixo padrão visual, com qualidade de imagem baixa e falta de detalhes."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ }
+ ]
+ },
+ {
+ "timestamp_na_origem": 45.8084333333333,
+ "evidencias": [
+ {
+ "tipo": "rosto",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.3048616945743561,
+ "x": 0.5905489325523376,
+ "largura": 0.17148470878601074,
+ "y": 0.41484758257865906
+ },
+ "landmarks": {
+ "labios": [
+ {
+ "x": 0.3294019401073456,
+ "y": 0.6685875058174133
+ },
+ {
+ "x": 0.3476579785346985,
+ "y": 0.6235095262527466
+ },
+ {
+ "x": 0.356249064207077,
+ "y": 0.5773582458496094
+ },
+ {
+ "x": 0.3476579785346985,
+ "y": 0.5379149913787842
+ },
+ {
+ "x": 0.3546382188796997,
+ "y": 0.49793511629104614
+ },
+ {
+ "x": 0.34604716300964355,
+ "y": 0.45232048630714417
+ },
+ {
+ "x": 0.32832804322242737,
+ "y": 0.40777912735939026
+ },
+ {
+ "x": 0.30309176445007324,
+ "y": 0.3702141344547272
+ },
+ {
+ "x": 0.25100836157798767,
+ "y": 0.4120722711086273
+ },
+ {
+ "x": 0.217180997133255,
+ "y": 0.4721762537956238
+ },
+ {
+ "x": 0.20697909593582153,
+ "y": 0.538451611995697
+ },
+ {
+ "x": 0.21557016670703888,
+ "y": 0.6041903495788574
+ },
+ {
+ "x": 0.24886059761047363,
+ "y": 0.6650993227958679
+ },
+ {
+ "x": 0.3014809489250183,
+ "y": 0.7061524987220764
+ }
+ ],
+ "nariz": [
+ {
+ "x": 0.643244743347168,
+ "y": 0.5389882922172546
+ },
+ {
+ "x": 0.5369301438331604,
+ "y": 0.6320957541465759
+ },
+ {
+ "x": 0.45182478427886963,
+ "y": 0.636120617389679
+ },
+ {
+ "x": 0.44645535945892334,
+ "y": 0.5867494940757751
+ },
+ {
+ "x": 0.4330317974090576,
+ "y": 0.538451611995697
+ },
+ {
+ "x": 0.44591841101646423,
+ "y": 0.4909587502479553
+ },
+ {
+ "x": 0.4507509171962738,
+ "y": 0.4415876269340515
+ },
+ {
+ "x": 0.5350508689880371,
+ "y": 0.4448074698448181
+ }
+ ],
+ "rosto": [
+ {
+ "x": 0.6749243140220642,
+ "y": 0.14536084234714508
+ },
+ {
+ "x": 0.5651196241378784,
+ "y": 0.14267763495445251
+ },
+ {
+ "x": 0.45343562960624695,
+ "y": 0.15019063651561737
+ },
+ {
+ "x": 0.3428255021572113,
+ "y": 0.1652166247367859
+ },
+ {
+ "x": 0.2354370355606079,
+ "y": 0.19634190201759338
+ },
+ {
+ "x": 0.14039824903011322,
+ "y": 0.2542993128299713
+ },
+ {
+ "x": 0.06254160404205322,
+ "y": 0.33372244238853455
+ },
+ {
+ "x": 0.004014893900603056,
+ "y": 0.42924484610557556
+ },
+ {
+ "x": -0.016925856471061707,
+ "y": 0.538451611995697
+ },
+ {
+ "x": 0.006699605379253626,
+ "y": 0.6476584076881409
+ },
+ {
+ "x": 0.06630020588636398,
+ "y": 0.7421075105667114
+ },
+ {
+ "x": 0.1446937769651413,
+ "y": 0.8217989802360535
+ },
+ {
+ "x": 0.2413433939218521,
+ "y": 0.8801589012145996
+ },
+ {
+ "x": 0.3492687940597534,
+ "y": 0.9130282402038574
+ },
+ {
+ "x": 0.4609528183937073,
+ "y": 0.9296641945838928
+ },
+ {
+ "x": 0.5737106800079346,
+ "y": 0.9383845925331116
+ },
+ {
+ "x": 0.6848577857017517,
+ "y": 0.9367746710777283
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "x": 0.6929119229316711,
+ "y": 0.8051630854606628
+ },
+ {
+ "x": 0.7219067811965942,
+ "y": 0.7590118050575256
+ },
+ {
+ "x": 0.713047206401825,
+ "y": 0.6873700022697449
+ },
+ {
+ "x": 0.6789513826370239,
+ "y": 0.6430969834327698
+ },
+ {
+ "x": 0.671434223651886,
+ "y": 0.6930047273635864
+ },
+ {
+ "x": 0.671434223651886,
+ "y": 0.7603533864021301
+ }
+ ],
+ "olho_direito": [
+ {
+ "x": 0.6810991764068604,
+ "y": 0.2757650315761566
+ },
+ {
+ "x": 0.7117048501968384,
+ "y": 0.32084301114082336
+ },
+ {
+ "x": 0.7057985067367554,
+ "y": 0.3922165036201477
+ },
+ {
+ "x": 0.6746558547019958,
+ "y": 0.4367578327655792
+ },
+ {
+ "x": 0.6657962799072266,
+ "y": 0.3884599804878235
+ },
+ {
+ "x": 0.66203773021698,
+ "y": 0.32191628217697144
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "x": 0.8092941641807556,
+ "y": 0.8865985870361328
+ },
+ {
+ "x": 0.8593640327453613,
+ "y": 0.7557919025421143
+ },
+ {
+ "x": 0.8219122886657715,
+ "y": 0.6184114217758179
+ },
+ {
+ "x": 0.7750640511512756,
+ "y": 0.6261926889419556
+ },
+ {
+ "x": 0.8084887266159058,
+ "y": 0.7549869418144226
+ },
+ {
+ "x": 0.7827154994010925,
+ "y": 0.8821712732315063
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "x": 0.7923804521560669,
+ "y": 0.19902510941028595
+ },
+ {
+ "x": 0.843658447265625,
+ "y": 0.32728272676467896
+ },
+ {
+ "x": 0.8166770935058594,
+ "y": 0.46358996629714966
+ },
+ {
+ "x": 0.767815351486206,
+ "y": 0.4576869010925293
+ },
+ {
+ "x": 0.794125497341156,
+ "y": 0.3299659490585327
+ },
+ {
+ "x": 0.7656675577163696,
+ "y": 0.203318253159523
+ }
+ ]
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.3047473132610321,
+ "x": 0.5927258133888245,
+ "largura": 0.1714203655719757,
+ "y": 0.41545891761779785
+ },
+ "score": 0.8193359375
+ },
+ "confianca": 0.8586644530296326,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.7960526598737254,
+ "largura": 0.8338816086957722,
+ "x": 0.0020559211996825145,
+ "y": 0.18640351543823877
+ },
+ "ocupacao_do_quadro": 0.6638136726220506
+ },
+ "confianca": 0.7221387624740601,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "confianca": 0.6376953125,
+ "x": 0.22600647807121277,
+ "y": 0.22354799509048462
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "x": 0.7021476030349731,
+ "y": 0.4485406279563904,
+ "confianca": 0.390869140625
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "confianca": 0.384033203125,
+ "x": 0.49015775322914124,
+ "y": 0.8162255883216858
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "confianca": 0.7578125,
+ "x": 0.677581250667572,
+ "y": 0.5859968662261963
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "confianca": 0.266845703125,
+ "x": 0.4840978682041168,
+ "y": 0.29488569498062134
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "confianca": 0.869140625,
+ "x": 0.7078356742858887,
+ "y": 0.5349680185317993
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "confianca": 0,
+ "x": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "confianca": 0.325439453125,
+ "x": 0.48712781071662903,
+ "y": 0.5555556416511536
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "confianca": 0.41552734375,
+ "x": 0.25118356943130493,
+ "y": 0.9522526264190674
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "confianca": 0.7451171875,
+ "x": 0.2383592426776886,
+ "y": 0.8572207689285278
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "confianca": 0.11328125,
+ "x": 0.4829295575618744,
+ "y": 0.2988712191581726
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "confianca": 0,
+ "x": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "confianca": 0.88916015625,
+ "x": 0.7047059535980225,
+ "y": 0.6497069597244263
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "x": 0.6791032552719116,
+ "y": 0.7198467254638672,
+ "confianca": 0.568359375
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "x": 0.23537585139274597,
+ "y": 0.39188677072525024,
+ "confianca": 0.67626953125
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "confianca": 0.837890625,
+ "x": 0.21237154304981232,
+ "y": 0.5405677556991577
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "confianca": 0.8046875,
+ "x": 0.2069569081068039,
+ "y": 0.4918268918991089
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "confianca": 0.6484375,
+ "x": 0.27213576436042786,
+ "y": 0.43000656366348267
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "confianca": 0.88232421875,
+ "x": 0.17992296814918518,
+ "y": 0.5649950504302979
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "confianca": 0.845703125,
+ "x": 0.24271273612976074,
+ "y": 0.49319934844970703
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "confianca": 0.814453125,
+ "x": 0.30187535285949707,
+ "y": 0.562044620513916
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "confianca": 0.873046875,
+ "x": 0.279433935880661,
+ "y": 0.5438458919525146
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "confianca": 0.70458984375,
+ "x": 0.23729173839092255,
+ "y": 0.3917447328567505
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "confianca": 0.89404296875,
+ "x": 0.32412055134773254,
+ "y": 0.5858803987503052
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "confianca": 0.89599609375,
+ "x": 0.16503798961639404,
+ "y": 0.6001318097114563
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "confianca": 0.896484375,
+ "x": 0.1934015154838562,
+ "y": 0.6025126576423645
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "x": 0.2394176721572876,
+ "y": 0.5436995029449463,
+ "confianca": 0.78173828125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.23425446450710297,
+ "y": 0.5718506574630737,
+ "confianca": 0.79052734375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "x": 0.1932907998561859,
+ "y": 0.5382059216499329,
+ "confianca": 0.8251953125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "x": 0.2210693061351776,
+ "y": 0.4918123483657837,
+ "confianca": 0.845703125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "x": 0.27053216099739075,
+ "y": 0.49006903171539307,
+ "confianca": 0.86279296875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "confianca": 0.8515625,
+ "x": 0.20238138735294342,
+ "y": 0.5697805881500244
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "confianca": 0.888671875,
+ "x": 0.22341114282608032,
+ "y": 0.607007622718811
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "x": 0.2833065390586853,
+ "y": 0.5786314010620117,
+ "confianca": 0.88330078125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.2897697389125824,
+ "y": 0.4989052414894104,
+ "confianca": 0.66796875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "confianca": 0.88525390625,
+ "x": 0.28424352407455444,
+ "y": 0.6150540113449097
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "confianca": 0.8896484375,
+ "x": 0.22126957774162292,
+ "y": 0.6758057475090027
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "confianca": 0.595703125,
+ "x": 0.2793814539909363,
+ "y": 0.7819703221321106
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "confianca": 0.8203125,
+ "x": 0.2182837277650833,
+ "y": 0.7367929220199585
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "x": 0.18898409605026245,
+ "y": 0.6506218910217285,
+ "confianca": 0.8271484375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "confianca": 0.82470703125,
+ "x": 0.2575186789035797,
+ "y": 0.726038932800293
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "confianca": 0.73828125,
+ "x": 0.3050326704978943,
+ "y": 0.6397110223770142
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "x": 0.2901390492916107,
+ "y": 0.673798143863678,
+ "confianca": 0.85693359375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "confianca": 0.875,
+ "x": 0.3251243829727173,
+ "y": 0.5975621938705444
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "confianca": 0.66552734375,
+ "x": 0.25064393877983093,
+ "y": 0.8180357813835144
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "x": 0.1745586097240448,
+ "y": 0.6160863637924194,
+ "confianca": 0.87255859375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "confianca": 0.74462890625,
+ "x": 0.19889232516288757,
+ "y": 0.5964786410331726
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "confianca": 0.8564453125,
+ "x": 0.2463550865650177,
+ "y": 0.6707710027694702
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.2393283098936081,
+ "y": 0.6429398059844971,
+ "confianca": 0.8203125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "confianca": 0.8046875,
+ "x": 0.2015678435564041,
+ "y": 0.6828827857971191
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "x": 0.23549892008304596,
+ "y": 0.7316040992736816,
+ "confianca": 0.83984375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "confianca": 0.83642578125,
+ "x": 0.28408193588256836,
+ "y": 0.7248408198356628
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "confianca": 0.84716796875,
+ "x": 0.21126261353492737,
+ "y": 0.6380172371864319
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "confianca": 0.81787109375,
+ "x": 0.2265276461839676,
+ "y": 0.6084071397781372
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "x": 0.29219216108322144,
+ "y": 0.6443238258361816,
+ "confianca": 0.84130859375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.2872682809829712,
+ "y": 0.7175851464271545,
+ "confianca": 0.443115234375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "confianca": 0.9052734375,
+ "x": 0.28997111320495605,
+ "y": 0.6121447086334229
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.8076175451278687,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.8076171875,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "score_global": 0.72021484375
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "retangulo",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.41220104694366455,
+ "largura": 0.28954267501831055,
+ "x": 0.5509835481643677,
+ "y": 0.38818782567977905
+ },
+ "cantos": {
+ "superior_esquerdo": {
+ "x": 0.5509835481643677,
+ "y": 0.8003888726234436
+ },
+ "superior_direito": {
+ "x": 0.8405262231826782,
+ "y": 0.668338418006897
+ },
+ "inferior_direito": {
+ "x": 0.8109997510910034,
+ "y": 0.38818782567977905
+ },
+ "inferior_esquerdo": {
+ "x": 0.6109850406646729,
+ "y": 0.39280039072036743
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectRectanglesRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4877827962239583
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "texto": "As imagens exibem uma pessoa de perfil, com o rosto centrado e a mao direita visível, enquanto a esquerda está fechada. A qualidade do rosto é boa, e a pose é natural. A categoria \"pessoa\" é claramente identificada, e a estética é considerada adequada. O horizonte está presente no fundo, e o retângulo é bem definido. Os contornos são claros, e a segmentação de pessoas é precisa."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ }
+ ]
+ }
+ ],
+ "evidencias_temporais": [
+ {
+ "tipo": "similaridade_visual",
+ "inicio": 41.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "possivel_mudanca_de_cena": false,
+ "distancia_feature_print": 0.13040070235729218
+ },
+ "confianca": null,
+ "provider": "apple_vision_sequencia",
+ "modelo": "VNGenerateImageFeaturePrintRequest"
+ },
+ {
+ "tipo": "similaridade_visual",
+ "inicio": 42.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "possivel_mudanca_de_cena": false,
+ "distancia_feature_print": 0.1921818107366562
+ },
+ "confianca": null,
+ "provider": "apple_vision_sequencia",
+ "modelo": "VNGenerateImageFeaturePrintRequest"
+ },
+ {
+ "tipo": "similaridade_visual",
+ "inicio": 43.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "possivel_mudanca_de_cena": false,
+ "distancia_feature_print": 0.15198232233524323
+ },
+ "confianca": null,
+ "provider": "apple_vision_sequencia",
+ "modelo": "VNGenerateImageFeaturePrintRequest"
+ },
+ {
+ "tipo": "similaridade_visual",
+ "inicio": 44.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "possivel_mudanca_de_cena": false,
+ "distancia_feature_print": 0.20895899832248688
+ },
+ "confianca": null,
+ "provider": "apple_vision_sequencia",
+ "modelo": "VNGenerateImageFeaturePrintRequest"
+ }
+ ],
+ "cenas": [
+ {
+ "identificador_do_clipe": "000f4765",
+ "inicio": 41.8084333333333,
+ "fim": 45.8084333333333,
+ "observacoes": [
+ {
+ "tipo": "rosto",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "largura": 0.1713569313287735,
+ "x": 0.5904472470283508,
+ "altura": 0.30463454127311707,
+ "y": 0.443860799074173
+ },
+ "landmarks": {
+ "rosto": [
+ {
+ "y": 0.12155722081661224,
+ "x": 0.6621209979057312
+ },
+ {
+ "y": 0.11992400884628296,
+ "x": 0.5520904064178467
+ },
+ {
+ "y": 0.128090038895607,
+ "x": 0.44097042083740234
+ },
+ {
+ "y": 0.14387769997119904,
+ "x": 0.3303951025009155
+ },
+ {
+ "y": 0.1749086081981659,
+ "x": 0.22363276779651642
+ },
+ {
+ "y": 0.23207080364227295,
+ "x": 0.1283092349767685
+ },
+ {
+ "y": 0.3115534782409668,
+ "x": 0.05096099525690079
+ },
+ {
+ "y": 0.4068238139152527,
+ "x": -0.005688415374606848
+ },
+ {
+ "y": 0.5157042145729065,
+ "x": -0.025842532515525818
+ },
+ {
+ "y": 0.6237679719924927,
+ "x": -0.0029648856725543737
+ },
+ {
+ "y": 0.7165884971618652,
+ "x": 0.05586335062980652
+ },
+ {
+ "y": 0.7952545881271362,
+ "x": 0.13430099189281464
+ },
+ {
+ "y": 0.851736307144165,
+ "x": 0.23125864565372467
+ },
+ {
+ "y": 0.884128212928772,
+ "x": 0.3385657072067261
+ },
+ {
+ "y": 0.9025017619132996,
+ "x": 0.4491409957408905
+ },
+ {
+ "y": 0.9128453731536865,
+ "x": 0.5602610111236572
+ },
+ {
+ "y": 0.9128453731536865,
+ "x": 0.6708362698554993
+ }
+ ],
+ "labios": [
+ {
+ "y": 0.6332949995994568,
+ "x": 0.308606892824173
+ },
+ {
+ "y": 0.5949146747589111,
+ "x": 0.3298504054546356
+ },
+ {
+ "y": 0.5543567538261414,
+ "x": 0.3407445251941681
+ },
+ {
+ "y": 0.5173373818397522,
+ "x": 0.33311864733695984
+ },
+ {
+ "y": 0.4803180694580078,
+ "x": 0.3396551012992859
+ },
+ {
+ "y": 0.43785473704338074,
+ "x": 0.3282162845134735
+ },
+ {
+ "y": 0.3975689709186554,
+ "x": 0.30751746892929077
+ },
+ {
+ "y": 0.3643604815006256,
+ "x": 0.2808268666267395
+ },
+ {
+ "y": 0.40246859192848206,
+ "x": 0.2350715845823288
+ },
+ {
+ "y": 0.45690879225730896,
+ "x": 0.2051127552986145
+ },
+ {
+ "y": 0.5162485837936401,
+ "x": 0.19530805945396423
+ },
+ {
+ "y": 0.5736830234527588,
+ "x": 0.2040233463048935
+ },
+ {
+ "y": 0.6270343661308289,
+ "x": 0.2334374636411667
+ },
+ {
+ "y": 0.6635093092918396,
+ "x": 0.2797374725341797
+ }
+ ],
+ "nariz": [
+ {
+ "y": 0.5211482048034668,
+ "x": 0.6278045177459717
+ },
+ {
+ "y": 0.607435941696167,
+ "x": 0.5175015926361084
+ },
+ {
+ "y": 0.6096135377883911,
+ "x": 0.4327998161315918
+ },
+ {
+ "y": 0.5647003650665283,
+ "x": 0.4295315742492676
+ },
+ {
+ "y": 0.5206038355827332,
+ "x": 0.4180927574634552
+ },
+ {
+ "y": 0.4759628474712372,
+ "x": 0.42898687720298767
+ },
+ {
+ "y": 0.4296886920928955,
+ "x": 0.431710422039032
+ },
+ {
+ "y": 0.43241071701049805,
+ "x": 0.516139805316925
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "y": 0.7838221192359924,
+ "x": 0.6825474500656128
+ },
+ {
+ "y": 0.7372757792472839,
+ "x": 0.7176809906959534
+ },
+ {
+ "y": 0.6626927256584167,
+ "x": 0.7073315978050232
+ },
+ {
+ "y": 0.6194127798080444,
+ "x": 0.6659339666366577
+ },
+ {
+ "y": 0.6705865263938904,
+ "x": 0.6580356955528259
+ },
+ {
+ "y": 0.7391811609268188,
+ "x": 0.6577633619308472
+ }
+ ],
+ "olho_direito": [
+ {
+ "y": 0.2571132779121399,
+ "x": 0.6727427840232849
+ },
+ {
+ "y": 0.30338746309280396,
+ "x": 0.7095103859901428
+ },
+ {
+ "y": 0.37797051668167114,
+ "x": 0.7018845081329346
+ },
+ {
+ "y": 0.42097827792167664,
+ "x": 0.6621209979057312
+ },
+ {
+ "y": 0.3714376986026764,
+ "x": 0.6531333327293396
+ },
+ {
+ "y": 0.30338746309280396,
+ "x": 0.6495927572250366
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "y": 0.1787194162607193,
+ "x": 0.7691556811332703
+ },
+ {
+ "y": 0.31046468019485474,
+ "x": 0.8210389614105225
+ },
+ {
+ "y": 0.4487427771091461,
+ "x": 0.7931227684020996
+ },
+ {
+ "y": 0.4427543580532074,
+ "x": 0.7443715929985046
+ },
+ {
+ "y": 0.31264227628707886,
+ "x": 0.7705174684524536
+ },
+ {
+ "y": 0.18307463824748993,
+ "x": 0.7424651384353638
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "y": 0.8660268187522888,
+ "x": 0.7780071496963501
+ },
+ {
+ "y": 0.7326483726501465,
+ "x": 0.8307074904441833
+ },
+ {
+ "y": 0.5930092930793762,
+ "x": 0.795437753200531
+ },
+ {
+ "y": 0.5998142957687378,
+ "x": 0.7484568953514099
+ },
+ {
+ "y": 0.7315595746040344,
+ "x": 0.779777467250824
+ },
+ {
+ "y": 0.8611271977424622,
+ "x": 0.7514527440071106
+ }
+ ]
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "largura": 0.17041203379631042,
+ "x": 0.5908492803573608,
+ "altura": 0.3029547333717346,
+ "y": 0.4386913478374481
+ },
+ "score": 0.78466796875
+ },
+ "confianca": 0.8615030646324158,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "largura": 0.8351151645820785,
+ "x": 0,
+ "altura": 0.8055555845387496,
+ "y": 0.18640351543823877
+ },
+ "ocupacao_do_quadro": 0.6727316845620903
+ },
+ "confianca": 0.7146623134613037,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "x": 0,
+ "confianca": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "y": 0.23268580436706543,
+ "confianca": 0.583984375,
+ "x": 0.23195724189281464
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "y": 0.6754071712493896,
+ "confianca": 0.90673828125,
+ "x": 0.7044726610183716
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "x": 0.7018452882766724,
+ "confianca": 0.460205078125,
+ "y": 0.4677010178565979
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "x": 0,
+ "confianca": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "x": 0,
+ "confianca": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "x": 0.560464084148407,
+ "confianca": 0.2021484375,
+ "y": 0.4213942289352417
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "x": 0.4931042790412903,
+ "confianca": 0.4306640625,
+ "y": 0.8207171559333801
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "x": 0.2440168559551239,
+ "confianca": 0.59375,
+ "y": 0.37958621978759766
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "y": 0.31064724922180176,
+ "confianca": 0.135986328125,
+ "x": 0.4927447736263275
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "x": 0.6764724850654602,
+ "confianca": 0.7626953125,
+ "y": 0.6156721115112305
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "x": 0.5267841815948486,
+ "confianca": 0.31640625,
+ "y": 0.6210556924343109
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "x": 0.7054939866065979,
+ "confianca": 0.85546875,
+ "y": 0.5601228475570679
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "x": 0.22904758155345917,
+ "confianca": 0.304443359375,
+ "y": 0.9515209794044495
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "y": 0.8904433250427246,
+ "confianca": 0.75146484375,
+ "x": 0.25067535042762756
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "x": 0.6699463129043579,
+ "confianca": 0.634765625,
+ "y": 0.7397646903991699
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "x": 0.22996927797794342,
+ "confianca": 0.865234375,
+ "y": 0.5335726737976074
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.6088806986808777,
+ "confianca": 0.90087890625,
+ "x": 0.1985943764448166
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "x": 0.22330962121486664,
+ "confianca": 0.84375,
+ "y": 0.4761231541633606
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "x": 0.3122074604034424,
+ "confianca": 0.68017578125,
+ "y": 0.5486577749252319
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.25278791785240173,
+ "confianca": 0.86328125,
+ "y": 0.5695041418075562
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "x": 0.2520199716091156,
+ "confianca": 0.79345703125,
+ "y": 0.38338571786880493
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "x": 0.24605269730091095,
+ "confianca": 0.89453125,
+ "y": 0.6107781529426575
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "x": 0.2054852694272995,
+ "confianca": 0.8876953125,
+ "y": 0.5696834325790405
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "x": 0.3052910566329956,
+ "confianca": 0.92578125,
+ "y": 0.6033934950828552
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "x": 0.2581619620323181,
+ "confianca": 0.86474609375,
+ "y": 0.5324119329452515
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "x": 0.30373576283454895,
+ "confianca": 0.88232421875,
+ "y": 0.5642706155776978
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.5349075794219971,
+ "confianca": 0.87841796875,
+ "x": 0.21328125894069672
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.3011344373226166,
+ "confianca": 0.6630859375,
+ "y": 0.47717082500457764
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "x": 0.22375056147575378,
+ "confianca": 0.8505859375,
+ "y": 0.5721350312232971
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.5847370624542236,
+ "confianca": 0.876953125,
+ "x": 0.3368803858757019
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.46988868713378906,
+ "confianca": 0.8896484375,
+ "x": 0.2903258800506592
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.611436128616333,
+ "confianca": 0.90966796875,
+ "x": 0.21892817318439484
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "y": 0.528741717338562,
+ "confianca": 0.87939453125,
+ "x": 0.2991749942302704
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "x": 0.2852766811847687,
+ "confianca": 0.68212890625,
+ "y": 0.41689014434814453
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "x": 0.23896922171115875,
+ "confianca": 0.8515625,
+ "y": 0.4722064137458801
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.4710060954093933,
+ "confianca": 0.8857421875,
+ "x": 0.2621763348579407
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "x": 0.2741371989250183,
+ "confianca": 0.8642578125,
+ "y": 0.7822844386100769
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "x": 0.19635368883609772,
+ "confianca": 0.89794921875,
+ "y": 0.6625171899795532
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.6925556659698486,
+ "confianca": 0.556640625,
+ "x": 0.3099360466003418
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.7878786325454712,
+ "confianca": 0.7919921875,
+ "x": 0.23164664208889008
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "x": 0.26079192757606506,
+ "confianca": 0.75048828125,
+ "y": 0.8528368473052979
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.2634032666683197,
+ "confianca": 0.833984375,
+ "y": 0.7037360072135925
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "x": 0.2539822459220886,
+ "confianca": 0.8544921875,
+ "y": 0.6691070795059204
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.6976571083068848,
+ "confianca": 0.81787109375,
+ "x": 0.20619571208953857
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.2981734573841095,
+ "confianca": 0.5546875,
+ "y": 0.7584294080734253
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.7313187122344971,
+ "confianca": 0.853515625,
+ "x": 0.2678661048412323
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "x": 0.31161436438560486,
+ "confianca": 0.798828125,
+ "y": 0.6908643245697021
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "x": 0.21749383211135864,
+ "confianca": 0.8330078125,
+ "y": 0.7309919595718384
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "x": 0.3188052177429199,
+ "confianca": 0.6298828125,
+ "y": 0.6521797776222229
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "x": 0.23260445892810822,
+ "confianca": 0.81494140625,
+ "y": 0.7003704309463501
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.6375858783721924,
+ "confianca": 0.77880859375,
+ "x": 0.32952558994293213
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "x": 0.29953861236572266,
+ "confianca": 0.8525390625,
+ "y": 0.7783830761909485
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.6618906259536743,
+ "confianca": 0.88232421875,
+ "x": 0.22528143227100372
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "x": 0.250380277633667,
+ "confianca": 0.86328125,
+ "y": 0.7851277589797974
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "x": 0.3072136640548706,
+ "confianca": 0.88037109375,
+ "y": 0.7269657254219055
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "x": 0.2899397015571594,
+ "confianca": 0.67333984375,
+ "y": 0.8231823444366455
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.7339023351669312,
+ "confianca": 0.87109375,
+ "x": 0.2402215451002121
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.7866212725639343,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.78662109375,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "score_global": 0.62353515625
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "retangulo",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "largura": 0.2843138575553894,
+ "x": 0.5710967779159546,
+ "altura": 0.4256795644760132,
+ "y": 0.396381139755249
+ },
+ "cantos": {
+ "superior_direito": {
+ "y": 0.7012904286384583,
+ "x": 0.855410635471344
+ },
+ "inferior_direito": {
+ "y": 0.40620118379592896,
+ "x": 0.8050353527069092
+ },
+ "inferior_esquerdo": {
+ "y": 0.396381139755249,
+ "x": 0.5977870225906372
+ },
+ "superior_esquerdo": {
+ "y": 0.8220607042312622,
+ "x": 0.5710967779159546
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectRectanglesRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4842325846354167
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 41.8084333333333,
+ "fim": 41.8084333333333,
+ "valor": {
+ "texto": "As fotografias apresentadas são de alta qualidade, destacando os contornos e a estética da pessoa."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ },
+ {
+ "tipo": "rosto",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "y": 0.44231587648391724,
+ "altura": 0.307657927274704,
+ "largura": 0.17305758595466614,
+ "x": 0.5922093987464905
+ },
+ "landmarks": {
+ "nariz": [
+ {
+ "y": 0.5261019468307495,
+ "x": 0.6188080310821533
+ },
+ {
+ "y": 0.6085143685340881,
+ "x": 0.5022847652435303
+ },
+ {
+ "y": 0.6058123707771301,
+ "x": 0.4136081039905548
+ },
+ {
+ "y": 0.5633901953697205,
+ "x": 0.4146895408630371
+ },
+ {
+ "y": 0.5212382674217224,
+ "x": 0.40766027569770813
+ },
+ {
+ "y": 0.47962671518325806,
+ "x": 0.415770947933197
+ },
+ {
+ "y": 0.4374747574329376,
+ "x": 0.415770947933197
+ },
+ {
+ "y": 0.43909597396850586,
+ "x": 0.5039069056510925
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "y": 0.7898218035697937,
+ "x": 0.6682831645011902
+ },
+ {
+ "y": 0.7433465719223022,
+ "x": 0.704781174659729
+ },
+ {
+ "y": 0.6682296395301819,
+ "x": 0.6961297988891602
+ },
+ {
+ "y": 0.6260777115821838,
+ "x": 0.653954267501831
+ },
+ {
+ "y": 0.6766059994697571,
+ "x": 0.6436808109283447
+ },
+ {
+ "y": 0.7449678182601929,
+ "x": 0.64259934425354
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "y": 0.1937500238418579,
+ "x": 0.7660167217254639
+ },
+ {
+ "y": 0.321286678314209,
+ "x": 0.8145455718040466
+ },
+ {
+ "y": 0.45584869384765625,
+ "x": 0.7871044874191284
+ },
+ {
+ "y": 0.44882336258888245,
+ "x": 0.7393866777420044
+ },
+ {
+ "y": 0.3229079246520996,
+ "x": 0.7653408050537109
+ },
+ {
+ "y": 0.19753289222717285,
+ "x": 0.7393866777420044
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "y": 0.8698024153709412,
+ "x": 0.7702072262763977
+ },
+ {
+ "y": 0.7390233278274536,
+ "x": 0.8218451738357544
+ },
+ {
+ "y": 0.6009486317634583,
+ "x": 0.7883210778236389
+ },
+ {
+ "y": 0.6077038049697876,
+ "x": 0.7423605918884277
+ },
+ {
+ "y": 0.7371318936347961,
+ "x": 0.7716941833496094
+ },
+ {
+ "y": 0.8649387359619141,
+ "x": 0.7437123656272888
+ }
+ ],
+ "rosto": [
+ {
+ "y": 0.13754743337631226,
+ "x": 0.6582800149917603
+ },
+ {
+ "y": 0.13322414457798004,
+ "x": 0.5509487986564636
+ },
+ {
+ "y": 0.14078988134860992,
+ "x": 0.4422658085823059
+ },
+ {
+ "y": 0.1553809493780136,
+ "x": 0.3335828185081482
+ },
+ {
+ "y": 0.18294183909893036,
+ "x": 0.22814412415027618
+ },
+ {
+ "y": 0.23536156117916107,
+ "x": 0.1329789161682129
+ },
+ {
+ "y": 0.3104785084724426,
+ "x": 0.054035067558288574
+ },
+ {
+ "y": 0.40126731991767883,
+ "x": -0.005443180445581675
+ },
+ {
+ "y": 0.5077279806137085,
+ "x": -0.028153056278824806
+ },
+ {
+ "y": 0.6155397295951843,
+ "x": -0.0070653147995471954
+ },
+ {
+ "y": 0.7090305685997009,
+ "x": 0.04970937594771385
+ },
+ {
+ "y": 0.7887409925460815,
+ "x": 0.1270311027765274
+ },
+ {
+ "y": 0.8473754525184631,
+ "x": 0.22165557742118835
+ },
+ {
+ "y": 0.8829073905944824,
+ "x": 0.32817572355270386
+ },
+ {
+ "y": 0.9037131071090698,
+ "x": 0.4379401206970215
+ },
+ {
+ "y": 0.9170882701873779,
+ "x": 0.5485155582427979
+ },
+ {
+ "y": 0.9187095165252686,
+ "x": 0.6588206887245178
+ }
+ ],
+ "olho_direito": [
+ {
+ "y": 0.2634628713130951,
+ "x": 0.6653092503547668
+ },
+ {
+ "y": 0.3093976676464081,
+ "x": 0.7023479342460632
+ },
+ {
+ "y": 0.38397419452667236,
+ "x": 0.6942372918128967
+ },
+ {
+ "y": 0.42720696330070496,
+ "x": 0.6542246341705322
+ },
+ {
+ "y": 0.37694889307022095,
+ "x": 0.6439511179924011
+ },
+ {
+ "y": 0.308857262134552,
+ "x": 0.6407068967819214
+ }
+ ],
+ "labios": [
+ {
+ "y": 0.6128376722335815,
+ "x": 0.2843781113624573
+ },
+ {
+ "y": 0.5820343494415283,
+ "x": 0.30654725432395935
+ },
+ {
+ "y": 0.5487991571426392,
+ "x": 0.3211464583873749
+ },
+ {
+ "y": 0.5152937173843384,
+ "x": 0.3141172230243683
+ },
+ {
+ "y": 0.4817883372306824,
+ "x": 0.3216871917247772
+ },
+ {
+ "y": 0.44612130522727966,
+ "x": 0.30871009826660156
+ },
+ {
+ "y": 0.41315633058547974,
+ "x": 0.2876223623752594
+ },
+ {
+ "y": 0.385595440864563,
+ "x": 0.26274964213371277
+ },
+ {
+ "y": 0.4169391989707947,
+ "x": 0.22273699939250946
+ },
+ {
+ "y": 0.46071237325668335,
+ "x": 0.1946200132369995
+ },
+ {
+ "y": 0.5104300379753113,
+ "x": 0.18488721549510956
+ },
+ {
+ "y": 0.5596073269844055,
+ "x": 0.19299788773059845
+ },
+ {
+ "y": 0.6041911244392395,
+ "x": 0.2200334519147873
+ },
+ {
+ "y": 0.6366156935691833,
+ "x": 0.2578832507133484
+ }
+ ]
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "y": 0.4326367974281311,
+ "altura": 0.3094255030155182,
+ "x": 0.5923697352409363,
+ "largura": 0.17405183613300323
+ },
+ "score": 0.74560546875
+ },
+ "confianca": 0.8566718697547913,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.6771149435954088,
+ "bounding_box": {
+ "y": 0.18567254356822155,
+ "altura": 0.8092105329472411,
+ "x": 0.00041118422829497026,
+ "largura": 0.8367599234395474
+ }
+ },
+ "confianca": 0.7385188937187195,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "y": 0.8246943950653076,
+ "confianca": 0.403564453125,
+ "x": 0.4901951849460602
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "y": 0.6214334964752197,
+ "confianca": 0.75390625,
+ "x": 0.6754902601242065
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "y": 0.3119756579399109,
+ "confianca": 0.239013671875,
+ "x": 0.48893752694129944
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "y": 0.5626635551452637,
+ "confianca": 0.83349609375,
+ "x": 0.7052652835845947
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "y": 0.2354884147644043,
+ "confianca": 0.5771484375,
+ "x": 0.2293081283569336
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "y": 0.4734950661659241,
+ "confianca": 0.45654296875,
+ "x": 0.7029047608375549
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "y": 0.6794328689575195,
+ "confianca": 0.91943359375,
+ "x": 0.7046099305152893
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "y": 0.5683350265026093,
+ "confianca": 0.3212890625,
+ "x": 0.4895663559436798
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "y": 0.9528840780258179,
+ "confianca": 0.287353515625,
+ "x": 0.27095717191696167
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "y": 0.7450624704360962,
+ "confianca": 0.57080078125,
+ "x": 0.6722405552864075
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "y": 0.3825128674507141,
+ "confianca": 0.609375,
+ "x": 0.24042096734046936
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "y": 0.8838084936141968,
+ "confianca": 0.77978515625,
+ "x": 0.2462765872478485
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.5787098407745361,
+ "confianca": 0.88427734375,
+ "x": 0.2029336392879486
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "y": 0.52995765209198,
+ "confianca": 0.88525390625,
+ "x": 0.294769287109375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.551985502243042,
+ "confianca": 0.8115234375,
+ "x": 0.3123168349266052
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.5828185677528381,
+ "confianca": 0.92431640625,
+ "x": 0.33720800280570984
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "y": 0.6048422455787659,
+ "confianca": 0.931640625,
+ "x": 0.30211153626441956
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.6205059289932251,
+ "confianca": 0.91162109375,
+ "x": 0.21364377439022064
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.5377410650253296,
+ "confianca": 0.880859375,
+ "x": 0.2539910078048706
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.47290313243865967,
+ "confianca": 0.8828125,
+ "x": 0.2845867872238159
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.47642379999160767,
+ "confianca": 0.892578125,
+ "x": 0.25691884756088257
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.5429835319519043,
+ "confianca": 0.880859375,
+ "x": 0.21032053232192993
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.541413426399231,
+ "confianca": 0.86376953125,
+ "x": 0.22669470310211182
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "y": 0.5759070515632629,
+ "confianca": 0.8486328125,
+ "x": 0.2492918074131012
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "y": 0.41546493768692017,
+ "confianca": 0.71728515625,
+ "x": 0.28145360946655273
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "y": 0.3794711232185364,
+ "confianca": 0.79638671875,
+ "x": 0.24654172360897064
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.4816918969154358,
+ "confianca": 0.86083984375,
+ "x": 0.23396262526512146
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.48514413833618164,
+ "confianca": 0.701171875,
+ "x": 0.3020922541618347
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.5654369592666626,
+ "confianca": 0.91796875,
+ "x": 0.2995390295982361
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.6177207231521606,
+ "confianca": 0.88818359375,
+ "x": 0.24132758378982544
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.48619210720062256,
+ "confianca": 0.833984375,
+ "x": 0.22056065499782562
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.5806756615638733,
+ "confianca": 0.85107421875,
+ "x": 0.22008036077022552
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.6181018352508545,
+ "confianca": 0.89306640625,
+ "x": 0.19620148837566376
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.6792576313018799,
+ "confianca": 0.8291015625,
+ "x": 0.2052418440580368
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "y": 0.7125279903411865,
+ "confianca": 0.86279296875,
+ "x": 0.3039427697658539
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.7126101851463318,
+ "confianca": 0.401123046875,
+ "x": 0.2952648103237152
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.6397429704666138,
+ "confianca": 0.83984375,
+ "x": 0.2192586064338684
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "y": 0.6352751851081848,
+ "confianca": 0.650390625,
+ "x": 0.3193773031234741
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.6875754594802856,
+ "confianca": 0.22265625,
+ "x": 0.2926397919654846
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.7157774567604065,
+ "confianca": 0.8837890625,
+ "x": 0.26281729340553284
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.7652794718742371,
+ "confianca": 0.85107421875,
+ "x": 0.2944953441619873
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.7661572098731995,
+ "confianca": 0.86083984375,
+ "x": 0.2682417035102844
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.7138841152191162,
+ "confianca": 0.8125,
+ "x": 0.21498611569404602
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.715700626373291,
+ "confianca": 0.865234375,
+ "x": 0.23597568273544312
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "y": 0.6861793994903564,
+ "confianca": 0.82861328125,
+ "x": 0.25870639085769653
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "y": 0.8412413001060486,
+ "confianca": 0.75439453125,
+ "x": 0.2556542754173279
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.7679053544998169,
+ "confianca": 0.84423828125,
+ "x": 0.24433951079845428
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.6774369478225708,
+ "confianca": 0.80615234375,
+ "x": 0.3093108832836151
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "y": 0.8142095804214478,
+ "confianca": 0.6728515625,
+ "x": 0.28512266278266907
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.7608442902565002,
+ "confianca": 0.5810546875,
+ "x": 0.2937327027320862
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.6480720043182373,
+ "confianca": 0.8330078125,
+ "x": 0.24711398780345917
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.6797212362289429,
+ "confianca": 0.865234375,
+ "x": 0.22839006781578064
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.6392928957939148,
+ "confianca": 0.87109375,
+ "x": 0.194611594080925
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.7705070376396179,
+ "confianca": 0.849609375,
+ "x": 0.2266223281621933
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.7885743975639343,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.78857421875,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "score_global": 0.65087890625
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "retangulo",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "y": 0.40177270770072937,
+ "altura": 0.4242468774318695,
+ "largura": 0.2787071466445923,
+ "x": 0.5711026787757874
+ },
+ "cantos": {
+ "superior_esquerdo": {
+ "y": 0.8260195851325989,
+ "x": 0.5711026787757874
+ },
+ "inferior_direito": {
+ "y": 0.40177270770072937,
+ "x": 0.812043309211731
+ },
+ "superior_direito": {
+ "y": 0.7073580026626587,
+ "x": 0.8498098254203796
+ },
+ "inferior_esquerdo": {
+ "y": 0.40224143862724304,
+ "x": 0.6066030859947205
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectRectanglesRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4851938883463542
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 42.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "texto": "O rosto é de uma pessoa jovem com cabelo curto e escuro, de expressão neutra. A qualidade do rosto é alta, com detalhes claros e bem definidos. A pessoa está em uma pose casual, com as mãos ao lado do corpo. Há duas mãos visíveis no quadro, ambas de mãos abertas. A categoria do rosto é de uma mulher. Outra categoria presente no quadro é de um homem. A estética do rosto é natural e realista. O horizonte está no fundo do quadro, além do qual há um retângulo. Os contornos do rosto e das mãos são bem definidos, e a segmentação de pessoas é clara e precisa."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ },
+ {
+ "tipo": "rosto",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.30600544810295105,
+ "y": 0.43403658270835876,
+ "x": 0.5989925265312195,
+ "largura": 0.17212805151939392
+ },
+ "landmarks": {
+ "labios": [
+ {
+ "y": 0.6504712104797363,
+ "x": 0.2890128493309021
+ },
+ {
+ "y": 0.6080748438835144,
+ "x": 0.30522432923316956
+ },
+ {
+ "y": 0.5656785368919373,
+ "x": 0.3133300542831421
+ },
+ {
+ "y": 0.5270627737045288,
+ "x": 0.30522432923316956
+ },
+ {
+ "y": 0.4892570972442627,
+ "x": 0.311708927154541
+ },
+ {
+ "y": 0.44605064392089844,
+ "x": 0.3030627965927124
+ },
+ {
+ "y": 0.4039243459701538,
+ "x": 0.287391722202301
+ },
+ {
+ "y": 0.3671988546848297,
+ "x": 0.26631680130958557
+ },
+ {
+ "y": 0.4077048897743225,
+ "x": 0.2171420156955719
+ },
+ {
+ "y": 0.4644133746623993,
+ "x": 0.18471907079219818
+ },
+ {
+ "y": 0.5276028513908386,
+ "x": 0.1739114224910736
+ },
+ {
+ "y": 0.5899821519851685,
+ "x": 0.1836383044719696
+ },
+ {
+ "y": 0.6469606757164001,
+ "x": 0.21552085876464844
+ },
+ {
+ "y": 0.6863865852355957,
+ "x": 0.2652360498905182
+ }
+ ],
+ "nariz": [
+ {
+ "y": 0.5276028513908386,
+ "x": 0.6040557622909546
+ },
+ {
+ "y": 0.617256224155426,
+ "x": 0.4957091212272644
+ },
+ {
+ "y": 0.6202266812324524,
+ "x": 0.41059887409210205
+ },
+ {
+ "y": 0.5729696154594421,
+ "x": 0.4073565900325775
+ },
+ {
+ "y": 0.5265226364135742,
+ "x": 0.39546817541122437
+ },
+ {
+ "y": 0.4800757169723511,
+ "x": 0.4068162143230438
+ },
+ {
+ "y": 0.4325486123561859,
+ "x": 0.41005849838256836
+ },
+ {
+ "y": 0.4352490305900574,
+ "x": 0.4948985278606415
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "y": 0.7922424077987671,
+ "x": 0.6529603600502014
+ },
+ {
+ "y": 0.7449853420257568,
+ "x": 0.6910573244094849
+ },
+ {
+ "y": 0.6688339114189148,
+ "x": 0.6802496910095215
+ },
+ {
+ "y": 0.6264376044273376,
+ "x": 0.6353979110717773
+ },
+ {
+ "y": 0.6774752140045166,
+ "x": 0.6251306533813477
+ },
+ {
+ "y": 0.7471456527709961,
+ "x": 0.6256710290908813
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "y": 0.19221268594264984,
+ "x": 0.7545522451400757
+ },
+ {
+ "y": 0.3223721385002136,
+ "x": 0.8046727180480957
+ },
+ {
+ "y": 0.45793241262435913,
+ "x": 0.7750867605209351
+ },
+ {
+ "y": 0.45145145058631897,
+ "x": 0.7267225384712219
+ },
+ {
+ "y": 0.32399237155914307,
+ "x": 0.7545522451400757
+ },
+ {
+ "y": 0.19653333723545074,
+ "x": 0.7278033494949341
+ }
+ ],
+ "rosto": [
+ {
+ "y": 0.1376645267009735,
+ "x": 0.6416123509407043
+ },
+ {
+ "y": 0.13496412336826324,
+ "x": 0.5324550867080688
+ },
+ {
+ "y": 0.14198517799377441,
+ "x": 0.4219469130039215
+ },
+ {
+ "y": 0.15710744261741638,
+ "x": 0.3122493028640747
+ },
+ {
+ "y": 0.1868118792772293,
+ "x": 0.20525360107421875
+ },
+ {
+ "y": 0.2440604418516159,
+ "x": 0.11122707277536392
+ },
+ {
+ "y": 0.32291221618652344,
+ "x": 0.03341201692819595
+ },
+ {
+ "y": 0.41688627004623413,
+ "x": -0.023328127339482307
+ },
+ {
+ "y": 0.5249024033546448,
+ "x": -0.044403038918972015
+ },
+ {
+ "y": 0.6323785185813904,
+ "x": -0.021166598424315453
+ },
+ {
+ "y": 0.7239221930503845,
+ "x": 0.03719469532370567
+ },
+ {
+ "y": 0.8027739524841309,
+ "x": 0.11392898857593536
+ },
+ {
+ "y": 0.8596174716949463,
+ "x": 0.20849590003490448
+ },
+ {
+ "y": 0.892427384853363,
+ "x": 0.31441083550453186
+ },
+ {
+ "y": 0.9088998436927795,
+ "x": 0.42464882135391235
+ },
+ {
+ "y": 0.9184862375259399,
+ "x": 0.5348868370056152
+ },
+ {
+ "y": 0.9182162284851074,
+ "x": 0.644044041633606
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "y": 0.875549852848053,
+ "x": 0.7599560618400574
+ },
+ {
+ "y": 0.7449853420257568,
+ "x": 0.8121029734611511
+ },
+ {
+ "y": 0.6072647571563721,
+ "x": 0.7757622599601746
+ },
+ {
+ "y": 0.6148258447647095,
+ "x": 0.7291542887687683
+ },
+ {
+ "y": 0.7436351180076599,
+ "x": 0.7617123126983643
+ },
+ {
+ "y": 0.8712291717529297,
+ "x": 0.7337475419044495
+ }
+ ],
+ "olho_direito": [
+ {
+ "y": 0.265123575925827,
+ "x": 0.6478267312049866
+ },
+ {
+ "y": 0.31049036979675293,
+ "x": 0.6870044469833374
+ },
+ {
+ "y": 0.3866417407989502,
+ "x": 0.6770073771476746
+ },
+ {
+ "y": 0.43038830161094666,
+ "x": 0.63431715965271
+ },
+ {
+ "y": 0.3796207010746002,
+ "x": 0.6235095262527466
+ },
+ {
+ "y": 0.31049036979675293,
+ "x": 0.6213480234146118
+ }
+ ]
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "score": 0.82177734375,
+ "bounding_box": {
+ "altura": 0.3067805767059326,
+ "x": 0.5961717963218689,
+ "y": 0.428731232881546,
+ "largura": 0.1725640743970871
+ }
+ },
+ "confianca": 0.8648970127105713,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.8333333854438276,
+ "x": 0.001644736913179881,
+ "y": 0.1666666503189592,
+ "largura": 0.8347040054412863
+ },
+ "ocupacao_do_quadro": 0.6955867146979102
+ },
+ "confianca": 0.7371953725814819,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "x": 0.2541317939758301,
+ "y": 0.955114483833313,
+ "confianca": 0.417236328125
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "x": 0.7073267698287964,
+ "confianca": 0.8310546875,
+ "y": 0.5492178201675415
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "y": 1,
+ "x": 0,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "x": 0,
+ "confianca": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "y": 0.9019889831542969,
+ "x": 0.24847324192523956,
+ "confianca": 0.7421875
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "y": 0.46096473932266235,
+ "confianca": 0.52294921875,
+ "x": 0.7028272747993469
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "y": 0.5644422173500061,
+ "confianca": 0.31689453125,
+ "x": 0.4922351837158203
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "y": 1,
+ "x": 0,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "y": 0.8207155466079712,
+ "x": 0.4964037835597992,
+ "confianca": 0.427734375
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "x": 0.48806658387184143,
+ "y": 0.308168888092041,
+ "confianca": 0.2060546875
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "x": 0.6787952780723572,
+ "y": 0.7347357869148254,
+ "confianca": 0.55908203125
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "y": 1,
+ "x": 0,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "y": 0.6041240692138672,
+ "confianca": 0.771484375,
+ "x": 0.6781229972839355
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "y": 0.30549144744873047,
+ "x": 0.24358662962913513,
+ "confianca": 0.40478515625
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "y": 0.665906548500061,
+ "x": 0.7058812379837036,
+ "confianca": 0.8798828125
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "y": 1,
+ "x": 0,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "y": 0.22589260339736938,
+ "x": 0.22988224029541016,
+ "confianca": 0.52392578125
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.41664886474609375,
+ "confianca": 0.82763671875,
+ "x": 0.22734349966049194
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.4433097243309021,
+ "confianca": 0.8701171875,
+ "x": 0.2997828423976898
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "x": 0.30574584007263184,
+ "confianca": 0.826171875,
+ "y": 0.4712445139884949
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.3756043314933777,
+ "x": 0.2356368750333786,
+ "confianca": 0.82470703125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.3738619089126587,
+ "x": 0.25909894704818726,
+ "confianca": 0.88037109375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.47697317600250244,
+ "x": 0.2425447702407837,
+ "confianca": 0.8935546875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "x": 0.25588104128837585,
+ "y": 0.4183407425880432,
+ "confianca": 0.85595703125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.38063567876815796,
+ "x": 0.22104859352111816,
+ "confianca": 0.79345703125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.25067564845085144,
+ "confianca": 0.82421875,
+ "y": 0.4451698660850525
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.5099343061447144,
+ "confianca": 0.8720703125,
+ "x": 0.33272117376327515
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.37415146827697754,
+ "confianca": 0.86474609375,
+ "x": 0.2858639657497406
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.4254671335220337,
+ "x": 0.20807614922523499,
+ "confianca": 0.8212890625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.4703480005264282,
+ "confianca": 0.87548828125,
+ "x": 0.2113194614648819
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.4537249803543091,
+ "x": 0.19627533853054047,
+ "confianca": 0.87890625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.40504950284957886,
+ "confianca": 0.521484375,
+ "x": 0.28551316261291504
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "x": 0.2944768965244293,
+ "y": 0.41727161407470703,
+ "confianca": 0.8984375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.4748254418373108,
+ "x": 0.304047167301178,
+ "confianca": 0.765625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "y": 0.33696943521499634,
+ "confianca": 0.669921875,
+ "x": 0.27598780393600464
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "x": 0.24664856493473053,
+ "confianca": 0.82275390625,
+ "y": 0.2963494062423706
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.48113882541656494,
+ "x": 0.1826593428850174,
+ "confianca": 0.9091796875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "x": 0.21964502334594727,
+ "confianca": 0.7685546875,
+ "y": 0.44222742319107056
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.7572119235992432,
+ "x": 0.23795345425605774,
+ "confianca": 0.8544921875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "y": 0.7169313430786133,
+ "confianca": 0.796875,
+ "x": 0.3179456889629364
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.7410318851470947,
+ "confianca": 0.845703125,
+ "x": 0.3101845979690552
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.8043133020401001,
+ "confianca": 0.85302734375,
+ "x": 0.24955791234970093
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "x": 0.27290114760398865,
+ "confianca": 0.8505859375,
+ "y": 0.801953911781311
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "x": 0.2498348206281662,
+ "confianca": 0.837890625,
+ "y": 0.7068570852279663
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.760705292224884,
+ "x": 0.2666851580142975,
+ "confianca": 0.85205078125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.8029155731201172,
+ "x": 0.23146219551563263,
+ "confianca": 0.814453125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.2589128315448761,
+ "y": 0.7370247840881348,
+ "confianca": 0.7900390625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "x": 0.33405426144599915,
+ "confianca": 0.9013671875,
+ "y": 0.6527906656265259
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.79771888256073,
+ "x": 0.29569679498672485,
+ "confianca": 0.8369140625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "x": 0.2152380645275116,
+ "confianca": 0.833984375,
+ "y": 0.7499773502349854
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.6911904811859131,
+ "confianca": 0.8603515625,
+ "x": 0.22073163092136383
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.717841386795044,
+ "x": 0.20297609269618988,
+ "confianca": 0.86669921875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.29906606674194336,
+ "confianca": 0.6376953125,
+ "y": 0.7559511065483093
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "x": 0.2864466905593872,
+ "y": 0.8304828405380249,
+ "confianca": 0.6806640625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.693830132484436,
+ "x": 0.3129860758781433,
+ "confianca": 0.81103515625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "x": 0.30399829149246216,
+ "y": 0.7661673426628113,
+ "confianca": 0.78173828125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "y": 0.8689616918563843,
+ "x": 0.2592129409313202,
+ "confianca": 0.75390625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "x": 0.19054484367370605,
+ "confianca": 0.88525390625,
+ "y": 0.6822276711463928
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.7259582281112671,
+ "x": 0.22809100151062012,
+ "confianca": 0.80126953125
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.8041994571685791,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.80419921875,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "score_global": 0.640625
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "retangulo",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "cantos": {
+ "superior_direito": {
+ "y": 0.6926843523979187,
+ "x": 0.8623307347297668
+ },
+ "superior_esquerdo": {
+ "y": 0.8083095550537109,
+ "x": 0.5894560217857361
+ },
+ "inferior_esquerdo": {
+ "y": 0.3894540071487427,
+ "x": 0.599976122379303
+ },
+ "inferior_direito": {
+ "y": 0.3975215256214142,
+ "x": 0.7932700514793396
+ }
+ },
+ "bounding_box": {
+ "altura": 0.41885554790496826,
+ "x": 0.5894560217857361,
+ "y": 0.3894540071487427,
+ "largura": 0.27287471294403076
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectRectanglesRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4823862711588542
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 43.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "texto": "As imagens apresentadas são de pessoas, todas com expressões neutras e poses neutras, e possuem alta qualidade de imagem. As pessoas estão em um ambiente aberto, com um horizonte claro e um céu azul."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ },
+ {
+ "tipo": "rosto",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "landmarks": {
+ "olho_direito": [
+ {
+ "x": 0.6931342482566833,
+ "y": 0.26874667406082153
+ },
+ {
+ "x": 0.7280840873718262,
+ "y": 0.31460943818092346
+ },
+ {
+ "x": 0.7195467352867126,
+ "y": 0.3876698613166809
+ },
+ {
+ "x": 0.6811285614967346,
+ "y": 0.43139946460723877
+ },
+ {
+ "x": 0.6717908382415771,
+ "y": 0.3818036913871765
+ },
+ {
+ "x": 0.6691229343414307,
+ "y": 0.31460943818092346
+ }
+ ],
+ "rosto": [
+ {
+ "x": 0.6904663443565369,
+ "y": 0.13809116184711456
+ },
+ {
+ "x": 0.5802809000015259,
+ "y": 0.1359580159187317
+ },
+ {
+ "x": 0.46929511427879333,
+ "y": 0.14342403411865234
+ },
+ {
+ "x": 0.3583092987537384,
+ "y": 0.15835610032081604
+ },
+ {
+ "x": 0.2510586082935333,
+ "y": 0.18928678333759308
+ },
+ {
+ "x": 0.15501320362091064,
+ "y": 0.24581529200077057
+ },
+ {
+ "x": 0.0755089595913887,
+ "y": 0.3242086172103882
+ },
+ {
+ "x": 0.014680201187729836,
+ "y": 0.4175339639186859
+ },
+ {
+ "x": -0.00719680730253458,
+ "y": 0.5265913605690002
+ },
+ {
+ "x": 0.01734812930226326,
+ "y": 0.6353820562362671
+ },
+ {
+ "x": 0.07871046662330627,
+ "y": 0.7273741960525513
+ },
+ {
+ "x": 0.1582147181034088,
+ "y": 0.806167483329773
+ },
+ {
+ "x": 0.2553272843360901,
+ "y": 0.8644291758537292
+ },
+ {
+ "x": 0.36311158537864685,
+ "y": 0.8972263336181641
+ },
+ {
+ "x": 0.4740973711013794,
+ "y": 0.9142915606498718
+ },
+ {
+ "x": 0.5866839289665222,
+ "y": 0.9237574338912964
+ },
+ {
+ "x": 0.6976697444915771,
+ "y": 0.923224151134491
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "y": 0.7921686768531799,
+ "x": 0.7024720311164856
+ },
+ {
+ "y": 0.7455059885978699,
+ "x": 0.7366214990615845
+ },
+ {
+ "y": 0.6716456413269043,
+ "x": 0.725149393081665
+ },
+ {
+ "y": 0.6284493207931519,
+ "x": 0.6840633153915405
+ },
+ {
+ "y": 0.6793782711029053,
+ "x": 0.6755259037017822
+ },
+ {
+ "y": 0.7476391196250916,
+ "x": 0.676326334476471
+ }
+ ],
+ "nariz": [
+ {
+ "x": 0.6469790935516357,
+ "y": 0.5289911031723022
+ },
+ {
+ "x": 0.5407955646514893,
+ "y": 0.6191167831420898
+ },
+ {
+ "x": 0.45755621790885925,
+ "y": 0.6231164336204529
+ },
+ {
+ "x": 0.4522203803062439,
+ "y": 0.5751205086708069
+ },
+ {
+ "x": 0.4388807415962219,
+ "y": 0.5287244915962219
+ },
+ {
+ "x": 0.4516867697238922,
+ "y": 0.48206180334091187
+ },
+ {
+ "x": 0.45755621790885925,
+ "y": 0.4345991909503937
+ },
+ {
+ "x": 0.5397284030914307,
+ "y": 0.438332200050354
+ }
+ ],
+ "labios": [
+ {
+ "x": 0.3358987271785736,
+ "y": 0.6545804142951965
+ },
+ {
+ "x": 0.35243988037109375,
+ "y": 0.611117422580719
+ },
+ {
+ "x": 0.3599100708961487,
+ "y": 0.5665879249572754
+ },
+ {
+ "x": 0.35190626978874207,
+ "y": 0.5276579260826111
+ },
+ {
+ "x": 0.3588429093360901,
+ "y": 0.4889945387840271
+ },
+ {
+ "x": 0.35137268900871277,
+ "y": 0.4447316527366638
+ },
+ {
+ "x": 0.3353651165962219,
+ "y": 0.4015353322029114
+ },
+ {
+ "x": 0.31295454502105713,
+ "y": 0.36420518159866333
+ },
+ {
+ "x": 0.25746163725852966,
+ "y": 0.40313521027565
+ },
+ {
+ "x": 0.22064423561096191,
+ "y": 0.462330162525177
+ },
+ {
+ "x": 0.20837175846099854,
+ "y": 0.5281912088394165
+ },
+ {
+ "x": 0.21850988268852234,
+ "y": 0.5945855379104614
+ },
+ {
+ "x": 0.2558608651161194,
+ "y": 0.6532471776008606
+ },
+ {
+ "x": 0.31082019209861755,
+ "y": 0.6913772821426392
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "x": 0.7998513579368591,
+ "y": 0.19248650968074799
+ },
+ {
+ "x": 0.8449393510818481,
+ "y": 0.32260873913764954
+ },
+ {
+ "x": 0.8130576014518738,
+ "y": 0.4564639925956726
+ },
+ {
+ "x": 0.7654350996017456,
+ "y": 0.4500645399093628
+ },
+ {
+ "x": 0.796116292476654,
+ "y": 0.3236753046512604
+ },
+ {
+ "x": 0.7734388709068298,
+ "y": 0.19675281643867493
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "x": 0.8125240206718445,
+ "y": 0.8741616606712341
+ },
+ {
+ "x": 0.8578788042068481,
+ "y": 0.7415063381195068
+ },
+ {
+ "x": 0.8167927265167236,
+ "y": 0.6055179238319397
+ },
+ {
+ "x": 0.7713045477867126,
+ "y": 0.6140505075454712
+ },
+ {
+ "x": 0.8078551292419434,
+ "y": 0.7417729496955872
+ },
+ {
+ "x": 0.7863783240318298,
+ "y": 0.8700286746025085
+ }
+ ]
+ },
+ "bounding_box": {
+ "y": 0.42103415727615356,
+ "altura": 0.3091081380844116,
+ "largura": 0.17387333512306213,
+ "x": 0.5870870351791382
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "score": 0.81884765625,
+ "bounding_box": {
+ "y": 0.4200640618801117,
+ "altura": 0.30747029185295105,
+ "largura": 0.17295202612876892,
+ "x": 0.5924886465072632
+ }
+ },
+ "confianca": 0.8763939738273621,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "y": 0.16703213625396782,
+ "altura": 0.8333333966142527,
+ "largura": 0.831825675156065,
+ "x": 0.003495065853195753
+ },
+ "ocupacao_do_quadro": 0.6931881152687476
+ },
+ "confianca": 0.7348734736442566,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "y": 0.7244069576263428,
+ "confianca": 0.58837890625,
+ "x": 0.6797021627426147
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "confianca": 0.349365234375,
+ "y": 0.29906171560287476,
+ "x": 0.23292621970176697
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "y": 0.22405678033828735,
+ "confianca": 0.53515625,
+ "x": 0.22977295517921448
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "y": 0.5401444435119629,
+ "confianca": 0.86083984375,
+ "x": 0.7055337429046631
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "y": 0.5576659142971039,
+ "confianca": 0.3468017578125,
+ "x": 0.48951660096645355
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "y": 0.655911922454834,
+ "confianca": 0.91357421875,
+ "x": 0.7049773931503296
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "confianca": 0,
+ "y": 1,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "y": 0.5913517475128174,
+ "confianca": 0.7529296875,
+ "x": 0.6756554841995239
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "y": 0.2971024513244629,
+ "confianca": 0.267822265625,
+ "x": 0.48464494943618774
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "confianca": 0,
+ "y": 1,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "y": 0.8182293772697449,
+ "confianca": 0.42578125,
+ "x": 0.49438825249671936
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "y": 0.4532662034034729,
+ "confianca": 0.417236328125,
+ "x": 0.7011292576789856
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "confianca": 0.79833984375,
+ "y": 0.9070571660995483,
+ "x": 0.2446686029434204
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "y": 0.9545372724533081,
+ "confianca": 0.4140625,
+ "x": 0.26311594247817993
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "y": 1,
+ "confianca": 0,
+ "x": 0
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.4970378875732422,
+ "confianca": 0.892578125,
+ "x": 0.3226650059223175
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.4270194172859192,
+ "confianca": 0.8232421875,
+ "x": 0.20418210327625275
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "confianca": 0.84033203125,
+ "y": 0.4022483229637146,
+ "x": 0.2424001544713974
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.45535868406295776,
+ "confianca": 0.84375,
+ "x": 0.19546756148338318
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "confianca": 0.86669921875,
+ "y": 0.4576696753501892,
+ "x": 0.2884030044078827
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.4606165885925293,
+ "confianca": 0.87109375,
+ "x": 0.2247980535030365
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "y": 0.42788004875183105,
+ "confianca": 0.7861328125,
+ "x": 0.23596486449241638
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.4653133153915405,
+ "confianca": 0.79345703125,
+ "x": 0.2949221432209015
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.36707639694213867,
+ "confianca": 0.7724609375,
+ "x": 0.22241614758968353
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "confianca": 0.87060546875,
+ "y": 0.4010241627693176,
+ "x": 0.2802160978317261
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.43612730503082275,
+ "confianca": 0.8984375,
+ "x": 0.18047595024108887
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.36190277338027954,
+ "confianca": 0.86669921875,
+ "x": 0.24656204879283905
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "y": 0.28538113832473755,
+ "confianca": 0.80029296875,
+ "x": 0.23563727736473083
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.42696332931518555,
+ "confianca": 0.81982421875,
+ "x": 0.2846023738384247
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "confianca": 0.60009765625,
+ "y": 0.3264840841293335,
+ "x": 0.26690784096717834
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "y": 0.36233073472976685,
+ "confianca": 0.853515625,
+ "x": 0.2730115056037903
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.4667367935180664,
+ "confianca": 0.87890625,
+ "x": 0.16764038801193237
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.3753747344017029,
+ "confianca": 0.76025390625,
+ "x": 0.2064296156167984
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.40287142992019653,
+ "confianca": 0.50244140625,
+ "x": 0.27863943576812744
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.40222257375717163,
+ "confianca": 0.78759765625,
+ "x": 0.2123108208179474
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.409648060798645,
+ "confianca": 0.79931640625,
+ "x": 0.1911434382200241
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "y": 0.7681536674499512,
+ "confianca": 0.6044921875,
+ "x": 0.2936704754829407
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "y": 0.7393319606781006,
+ "confianca": 0.8349609375,
+ "x": 0.2160249501466751
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "y": 0.7778792381286621,
+ "confianca": 0.87548828125,
+ "x": 0.25603219866752625
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "y": 0.7072114944458008,
+ "confianca": 0.8330078125,
+ "x": 0.20502296090126038
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "y": 0.7343935966491699,
+ "confianca": 0.77880859375,
+ "x": 0.3041329085826874
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "y": 0.7533847093582153,
+ "confianca": 0.83154296875,
+ "x": 0.24720267951488495
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "y": 0.7061077952384949,
+ "confianca": 0.78662109375,
+ "x": 0.30506694316864014
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "y": 0.7257874011993408,
+ "confianca": 0.869140625,
+ "x": 0.23451513051986694
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "y": 0.7345017790794373,
+ "confianca": 0.8740234375,
+ "x": 0.1915646642446518
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "y": 0.8211532831192017,
+ "confianca": 0.80322265625,
+ "x": 0.24224410951137543
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "y": 0.780459463596344,
+ "confianca": 0.75439453125,
+ "x": 0.2939385771751404
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "y": 0.8178697824478149,
+ "confianca": 0.87060546875,
+ "x": 0.2641657888889313
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "confianca": 0.81982421875,
+ "y": 0.8123520612716675,
+ "x": 0.28709232807159424
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "confianca": 0.77001953125,
+ "y": 0.8841433525085449,
+ "x": 0.25333037972450256
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "y": 0.8447343707084656,
+ "confianca": 0.69677734375,
+ "x": 0.2805657386779785
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "y": 0.7565067410469055,
+ "confianca": 0.75244140625,
+ "x": 0.2980576157569885
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "y": 0.7013180255889893,
+ "confianca": 0.89892578125,
+ "x": 0.17815668880939484
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "y": 0.6629536151885986,
+ "confianca": 0.88818359375,
+ "x": 0.32562360167503357
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "y": 0.8195067644119263,
+ "confianca": 0.798828125,
+ "x": 0.22469928860664368
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "y": 0.7714981436729431,
+ "confianca": 0.8408203125,
+ "x": 0.22778409719467163
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "y": 0.7658321857452393,
+ "confianca": 0.85498046875,
+ "x": 0.20509029924869537
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.8227543234825134,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.82275390625,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "score_global": 0.65234375
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4822896321614583
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 44.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "texto": "O conteúdo é considerado de baixo padrão visual, com qualidade de imagem baixa e falta de detalhes."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ },
+ {
+ "tipo": "rosto",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.3048616945743561,
+ "x": 0.5905489325523376,
+ "largura": 0.17148470878601074,
+ "y": 0.41484758257865906
+ },
+ "landmarks": {
+ "labios": [
+ {
+ "x": 0.3294019401073456,
+ "y": 0.6685875058174133
+ },
+ {
+ "x": 0.3476579785346985,
+ "y": 0.6235095262527466
+ },
+ {
+ "x": 0.356249064207077,
+ "y": 0.5773582458496094
+ },
+ {
+ "x": 0.3476579785346985,
+ "y": 0.5379149913787842
+ },
+ {
+ "x": 0.3546382188796997,
+ "y": 0.49793511629104614
+ },
+ {
+ "x": 0.34604716300964355,
+ "y": 0.45232048630714417
+ },
+ {
+ "x": 0.32832804322242737,
+ "y": 0.40777912735939026
+ },
+ {
+ "x": 0.30309176445007324,
+ "y": 0.3702141344547272
+ },
+ {
+ "x": 0.25100836157798767,
+ "y": 0.4120722711086273
+ },
+ {
+ "x": 0.217180997133255,
+ "y": 0.4721762537956238
+ },
+ {
+ "x": 0.20697909593582153,
+ "y": 0.538451611995697
+ },
+ {
+ "x": 0.21557016670703888,
+ "y": 0.6041903495788574
+ },
+ {
+ "x": 0.24886059761047363,
+ "y": 0.6650993227958679
+ },
+ {
+ "x": 0.3014809489250183,
+ "y": 0.7061524987220764
+ }
+ ],
+ "nariz": [
+ {
+ "x": 0.643244743347168,
+ "y": 0.5389882922172546
+ },
+ {
+ "x": 0.5369301438331604,
+ "y": 0.6320957541465759
+ },
+ {
+ "x": 0.45182478427886963,
+ "y": 0.636120617389679
+ },
+ {
+ "x": 0.44645535945892334,
+ "y": 0.5867494940757751
+ },
+ {
+ "x": 0.4330317974090576,
+ "y": 0.538451611995697
+ },
+ {
+ "x": 0.44591841101646423,
+ "y": 0.4909587502479553
+ },
+ {
+ "x": 0.4507509171962738,
+ "y": 0.4415876269340515
+ },
+ {
+ "x": 0.5350508689880371,
+ "y": 0.4448074698448181
+ }
+ ],
+ "rosto": [
+ {
+ "x": 0.6749243140220642,
+ "y": 0.14536084234714508
+ },
+ {
+ "x": 0.5651196241378784,
+ "y": 0.14267763495445251
+ },
+ {
+ "x": 0.45343562960624695,
+ "y": 0.15019063651561737
+ },
+ {
+ "x": 0.3428255021572113,
+ "y": 0.1652166247367859
+ },
+ {
+ "x": 0.2354370355606079,
+ "y": 0.19634190201759338
+ },
+ {
+ "x": 0.14039824903011322,
+ "y": 0.2542993128299713
+ },
+ {
+ "x": 0.06254160404205322,
+ "y": 0.33372244238853455
+ },
+ {
+ "x": 0.004014893900603056,
+ "y": 0.42924484610557556
+ },
+ {
+ "x": -0.016925856471061707,
+ "y": 0.538451611995697
+ },
+ {
+ "x": 0.006699605379253626,
+ "y": 0.6476584076881409
+ },
+ {
+ "x": 0.06630020588636398,
+ "y": 0.7421075105667114
+ },
+ {
+ "x": 0.1446937769651413,
+ "y": 0.8217989802360535
+ },
+ {
+ "x": 0.2413433939218521,
+ "y": 0.8801589012145996
+ },
+ {
+ "x": 0.3492687940597534,
+ "y": 0.9130282402038574
+ },
+ {
+ "x": 0.4609528183937073,
+ "y": 0.9296641945838928
+ },
+ {
+ "x": 0.5737106800079346,
+ "y": 0.9383845925331116
+ },
+ {
+ "x": 0.6848577857017517,
+ "y": 0.9367746710777283
+ }
+ ],
+ "olho_esquerdo": [
+ {
+ "x": 0.6929119229316711,
+ "y": 0.8051630854606628
+ },
+ {
+ "x": 0.7219067811965942,
+ "y": 0.7590118050575256
+ },
+ {
+ "x": 0.713047206401825,
+ "y": 0.6873700022697449
+ },
+ {
+ "x": 0.6789513826370239,
+ "y": 0.6430969834327698
+ },
+ {
+ "x": 0.671434223651886,
+ "y": 0.6930047273635864
+ },
+ {
+ "x": 0.671434223651886,
+ "y": 0.7603533864021301
+ }
+ ],
+ "olho_direito": [
+ {
+ "x": 0.6810991764068604,
+ "y": 0.2757650315761566
+ },
+ {
+ "x": 0.7117048501968384,
+ "y": 0.32084301114082336
+ },
+ {
+ "x": 0.7057985067367554,
+ "y": 0.3922165036201477
+ },
+ {
+ "x": 0.6746558547019958,
+ "y": 0.4367578327655792
+ },
+ {
+ "x": 0.6657962799072266,
+ "y": 0.3884599804878235
+ },
+ {
+ "x": 0.66203773021698,
+ "y": 0.32191628217697144
+ }
+ ],
+ "sobrancelha_esquerda": [
+ {
+ "x": 0.8092941641807556,
+ "y": 0.8865985870361328
+ },
+ {
+ "x": 0.8593640327453613,
+ "y": 0.7557919025421143
+ },
+ {
+ "x": 0.8219122886657715,
+ "y": 0.6184114217758179
+ },
+ {
+ "x": 0.7750640511512756,
+ "y": 0.6261926889419556
+ },
+ {
+ "x": 0.8084887266159058,
+ "y": 0.7549869418144226
+ },
+ {
+ "x": 0.7827154994010925,
+ "y": 0.8821712732315063
+ }
+ ],
+ "sobrancelha_direita": [
+ {
+ "x": 0.7923804521560669,
+ "y": 0.19902510941028595
+ },
+ {
+ "x": 0.843658447265625,
+ "y": 0.32728272676467896
+ },
+ {
+ "x": 0.8166770935058594,
+ "y": 0.46358996629714966
+ },
+ {
+ "x": 0.767815351486206,
+ "y": 0.4576869010925293
+ },
+ {
+ "x": 0.794125497341156,
+ "y": 0.3299659490585327
+ },
+ {
+ "x": 0.7656675577163696,
+ "y": 0.203318253159523
+ }
+ ]
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceLandmarksRequest"
+ },
+ {
+ "tipo": "qualidade_do_rosto",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.3047473132610321,
+ "x": 0.5927258133888245,
+ "largura": 0.1714203655719757,
+ "y": 0.41545891761779785
+ },
+ "score": 0.8193359375
+ },
+ "confianca": 0.8586644530296326,
+ "provider": "apple_vision",
+ "modelo": "VNDetectFaceCaptureQualityRequest"
+ },
+ {
+ "tipo": "pessoa",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.7960526598737254,
+ "largura": 0.8338816086957722,
+ "x": 0.0020559211996825145,
+ "y": 0.18640351543823877
+ },
+ "ocupacao_do_quadro": 0.6638136726220506
+ },
+ "confianca": 0.7221387624740601,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanRectanglesRequest"
+ },
+ {
+ "tipo": "pose",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_forearm_joint)": {
+ "confianca": 0.6376953125,
+ "x": 0.22600647807121277,
+ "y": 0.22354799509048462
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_ear_joint)": {
+ "x": 0.7021476030349731,
+ "y": 0.4485406279563904,
+ "confianca": 0.390869140625
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: root)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_shoulder_1_joint)": {
+ "confianca": 0.384033203125,
+ "x": 0.49015775322914124,
+ "y": 0.8162255883216858
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_upLeg_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: head_joint)": {
+ "confianca": 0.7578125,
+ "x": 0.677581250667572,
+ "y": 0.5859968662261963
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_upLeg_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_shoulder_1_joint)": {
+ "confianca": 0.266845703125,
+ "x": 0.4840978682041168,
+ "y": 0.29488569498062134
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_eye_joint)": {
+ "confianca": 0.869140625,
+ "x": 0.7078356742858887,
+ "y": 0.5349680185317993
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_leg_joint)": {
+ "confianca": 0,
+ "x": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: neck_1_joint)": {
+ "confianca": 0.325439453125,
+ "x": 0.48712781071662903,
+ "y": 0.5555556416511536
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_forearm_joint)": {
+ "confianca": 0.41552734375,
+ "x": 0.25118356943130493,
+ "y": 0.9522526264190674
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_hand_joint)": {
+ "confianca": 0.7451171875,
+ "x": 0.2383592426776886,
+ "y": 0.8572207689285278
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_leg_joint)": {
+ "confianca": 0.11328125,
+ "x": 0.4829295575618744,
+ "y": 0.2988712191581726
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_foot_joint)": {
+ "confianca": 0,
+ "x": 0,
+ "y": 1
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_eye_joint)": {
+ "confianca": 0.88916015625,
+ "x": 0.7047059535980225,
+ "y": 0.6497069597244263
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: right_ear_joint)": {
+ "x": 0.6791032552719116,
+ "y": 0.7198467254638672,
+ "confianca": 0.568359375
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_hand_joint)": {
+ "x": 0.23537585139274597,
+ "y": 0.39188677072525024,
+ "confianca": 0.67626953125
+ },
+ "VNHumanBodyPoseObservationJointName(_rawValue: left_foot_joint)": {
+ "x": 0,
+ "y": 1,
+ "confianca": 0
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanBodyPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "confianca": 0.837890625,
+ "x": 0.21237154304981232,
+ "y": 0.5405677556991577
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "confianca": 0.8046875,
+ "x": 0.2069569081068039,
+ "y": 0.4918268918991089
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "confianca": 0.6484375,
+ "x": 0.27213576436042786,
+ "y": 0.43000656366348267
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "confianca": 0.88232421875,
+ "x": 0.17992296814918518,
+ "y": 0.5649950504302979
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "confianca": 0.845703125,
+ "x": 0.24271273612976074,
+ "y": 0.49319934844970703
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "confianca": 0.814453125,
+ "x": 0.30187535285949707,
+ "y": 0.562044620513916
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "confianca": 0.873046875,
+ "x": 0.279433935880661,
+ "y": 0.5438458919525146
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "confianca": 0.70458984375,
+ "x": 0.23729173839092255,
+ "y": 0.3917447328567505
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "confianca": 0.89404296875,
+ "x": 0.32412055134773254,
+ "y": 0.5858803987503052
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "confianca": 0.89599609375,
+ "x": 0.16503798961639404,
+ "y": 0.6001318097114563
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "confianca": 0.896484375,
+ "x": 0.1934015154838562,
+ "y": 0.6025126576423645
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "x": 0.2394176721572876,
+ "y": 0.5436995029449463,
+ "confianca": 0.78173828125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.23425446450710297,
+ "y": 0.5718506574630737,
+ "confianca": 0.79052734375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "x": 0.1932907998561859,
+ "y": 0.5382059216499329,
+ "confianca": 0.8251953125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "x": 0.2210693061351776,
+ "y": 0.4918123483657837,
+ "confianca": 0.845703125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "x": 0.27053216099739075,
+ "y": 0.49006903171539307,
+ "confianca": 0.86279296875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "confianca": 0.8515625,
+ "x": 0.20238138735294342,
+ "y": 0.5697805881500244
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "confianca": 0.888671875,
+ "x": 0.22341114282608032,
+ "y": 0.607007622718811
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "x": 0.2833065390586853,
+ "y": 0.5786314010620117,
+ "confianca": 0.88330078125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.2897697389125824,
+ "y": 0.4989052414894104,
+ "confianca": 0.66796875
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "confianca": 0.88525390625,
+ "x": 0.28424352407455444,
+ "y": 0.6150540113449097
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "mao",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "pontos": {
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRPIP)": {
+ "confianca": 0.8896484375,
+ "x": 0.22126957774162292,
+ "y": 0.6758057475090027
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTCMC)": {
+ "confianca": 0.595703125,
+ "x": 0.2793814539909363,
+ "y": 0.7819703221321106
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPMCP)": {
+ "confianca": 0.8203125,
+ "x": 0.2182837277650833,
+ "y": 0.7367929220199585
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPDIP)": {
+ "x": 0.18898409605026245,
+ "y": 0.6506218910217285,
+ "confianca": 0.8271484375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMMCP)": {
+ "confianca": 0.82470703125,
+ "x": 0.2575186789035797,
+ "y": 0.726038932800293
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTIP)": {
+ "confianca": 0.73828125,
+ "x": 0.3050326704978943,
+ "y": 0.6397110223770142
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIPIP)": {
+ "x": 0.2901390492916107,
+ "y": 0.673798143863678,
+ "confianca": 0.85693359375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTTIP)": {
+ "confianca": 0.875,
+ "x": 0.3251243829727173,
+ "y": 0.5975621938705444
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKWRI)": {
+ "confianca": 0.66552734375,
+ "x": 0.25064393877983093,
+ "y": 0.8180357813835144
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPTIP)": {
+ "x": 0.1745586097240448,
+ "y": 0.6160863637924194,
+ "confianca": 0.87255859375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRTIP)": {
+ "confianca": 0.74462890625,
+ "x": 0.19889232516288757,
+ "y": 0.5964786410331726
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMPIP)": {
+ "confianca": 0.8564453125,
+ "x": 0.2463550865650177,
+ "y": 0.6707710027694702
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMDIP)": {
+ "x": 0.2393283098936081,
+ "y": 0.6429398059844971,
+ "confianca": 0.8203125
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKPPIP)": {
+ "confianca": 0.8046875,
+ "x": 0.2015678435564041,
+ "y": 0.6828827857971191
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRMCP)": {
+ "x": 0.23549892008304596,
+ "y": 0.7316040992736816,
+ "confianca": 0.83984375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIMCP)": {
+ "confianca": 0.83642578125,
+ "x": 0.28408193588256836,
+ "y": 0.7248408198356628
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKRDIP)": {
+ "confianca": 0.84716796875,
+ "x": 0.21126261353492737,
+ "y": 0.6380172371864319
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKMTIP)": {
+ "confianca": 0.81787109375,
+ "x": 0.2265276461839676,
+ "y": 0.6084071397781372
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKIDIP)": {
+ "x": 0.29219216108322144,
+ "y": 0.6443238258361816,
+ "confianca": 0.84130859375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKTMP)": {
+ "x": 0.2872682809829712,
+ "y": 0.7175851464271545,
+ "confianca": 0.443115234375
+ },
+ "VNHumanHandPoseObservationJointName(_rawValue: VNHLKITIP)": {
+ "confianca": 0.9052734375,
+ "x": 0.28997111320495605,
+ "y": 0.6121447086334229
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHumanHandPoseRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "identificador": "people"
+ },
+ "confianca": 0.8076175451278687,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "categoria",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "identificador": "adult"
+ },
+ "confianca": 0.8076171875,
+ "provider": "apple_vision",
+ "modelo": "VNClassifyImageRequest"
+ },
+ {
+ "tipo": "estetica",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "score_global": 0.72021484375
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNCalculateImageAestheticsScoresRequest"
+ },
+ {
+ "tipo": "horizonte",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "angulo_radianos": 0.01745329238474369
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectHorizonRequest"
+ },
+ {
+ "tipo": "retangulo",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "bounding_box": {
+ "altura": 0.41220104694366455,
+ "largura": 0.28954267501831055,
+ "x": 0.5509835481643677,
+ "y": 0.38818782567977905
+ },
+ "cantos": {
+ "superior_esquerdo": {
+ "x": 0.5509835481643677,
+ "y": 0.8003888726234436
+ },
+ "superior_direito": {
+ "x": 0.8405262231826782,
+ "y": 0.668338418006897
+ },
+ "inferior_direito": {
+ "x": 0.8109997510910034,
+ "y": 0.38818782567977905
+ },
+ "inferior_esquerdo": {
+ "x": 0.6109850406646729,
+ "y": 0.39280039072036743
+ }
+ }
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectRectanglesRequest"
+ },
+ {
+ "tipo": "contornos",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "quantidade_principal": 5
+ },
+ "confianca": 1,
+ "provider": "apple_vision",
+ "modelo": "VNDetectContoursRequest"
+ },
+ {
+ "tipo": "segmentacao_de_pessoas",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "ocupacao_do_quadro": 0.4877827962239583
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "VNGeneratePersonSegmentationRequest"
+ },
+ {
+ "tipo": "interpretacao_editorial",
+ "inicio": 45.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "texto": "As imagens exibem uma pessoa de perfil, com o rosto centrado e a mao direita visível, enquanto a esquerda está fechada. A qualidade do rosto é boa, e a pose é natural. A categoria \"pessoa\" é claramente identificada, e a estética é considerada adequada. O horizonte está presente no fundo, e o retângulo é bem definido. Os contornos são claros, e a segmentação de pessoas é precisa."
+ },
+ "confianca": null,
+ "provider": "apple_vision",
+ "modelo": "AppleFoundationModels"
+ },
+ {
+ "tipo": "similaridade_visual",
+ "inicio": 41.8084333333333,
+ "fim": 42.8084333333333,
+ "valor": {
+ "possivel_mudanca_de_cena": false,
+ "distancia_feature_print": 0.13040070235729218
+ },
+ "confianca": null,
+ "provider": "apple_vision_sequencia",
+ "modelo": "VNGenerateImageFeaturePrintRequest"
+ },
+ {
+ "tipo": "similaridade_visual",
+ "inicio": 42.8084333333333,
+ "fim": 43.8084333333333,
+ "valor": {
+ "possivel_mudanca_de_cena": false,
+ "distancia_feature_print": 0.1921818107366562
+ },
+ "confianca": null,
+ "provider": "apple_vision_sequencia",
+ "modelo": "VNGenerateImageFeaturePrintRequest"
+ },
+ {
+ "tipo": "similaridade_visual",
+ "inicio": 43.8084333333333,
+ "fim": 44.8084333333333,
+ "valor": {
+ "possivel_mudanca_de_cena": false,
+ "distancia_feature_print": 0.15198232233524323
+ },
+ "confianca": null,
+ "provider": "apple_vision_sequencia",
+ "modelo": "VNGenerateImageFeaturePrintRequest"
+ },
+ {
+ "tipo": "similaridade_visual",
+ "inicio": 44.8084333333333,
+ "fim": 45.8084333333333,
+ "valor": {
+ "possivel_mudanca_de_cena": false,
+ "distancia_feature_print": 0.20895899832248688
+ },
+ "confianca": null,
+ "provider": "apple_vision_sequencia",
+ "modelo": "VNGenerateImageFeaturePrintRequest"
+ }
+ ],
+ "referencias_de_cena": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/relatorios/analise-visual/000f4765/V1-segmento-02-vision-completo.md b/code/relatorios/analise-visual/000f4765/V1-segmento-02-vision-completo.md
new file mode 100644
index 0000000..249a1dc
--- /dev/null
+++ b/code/relatorios/analise-visual/000f4765/V1-segmento-02-vision-completo.md
@@ -0,0 +1,35 @@
+# Relatório visual — 0E6A8290.MP4
+
+- Clipe: `000f4765`
+- Origem: `/Volumes/Merongo/PROJETOS/03 - Mastopexia/0E6A8290.MP4`
+- Timeline: 38.272s–42.409s
+- Amostragem: a cada 1 segundo(s)
+
+## Observações por frame
+
+### Origem 41.808s
+
+rosto, qualidade_do_rosto, pessoa, pose, mao, mao, categoria, categoria, estetica, horizonte, retangulo, contornos, segmentacao_de_pessoas, interpretacao_editorial
+
+### Origem 42.808s
+
+rosto, qualidade_do_rosto, pessoa, pose, mao, mao, categoria, categoria, estetica, horizonte, retangulo, contornos, segmentacao_de_pessoas, interpretacao_editorial
+
+### Origem 43.808s
+
+rosto, qualidade_do_rosto, pessoa, pose, mao, mao, categoria, categoria, estetica, horizonte, retangulo, contornos, segmentacao_de_pessoas, interpretacao_editorial
+
+### Origem 44.808s
+
+rosto, qualidade_do_rosto, pessoa, pose, mao, mao, categoria, categoria, estetica, horizonte, contornos, segmentacao_de_pessoas, interpretacao_editorial
+
+### Origem 45.808s
+
+rosto, qualidade_do_rosto, pessoa, pose, mao, mao, categoria, categoria, estetica, horizonte, retangulo, contornos, segmentacao_de_pessoas, interpretacao_editorial
+
+## Continuidade
+
+- similaridade_visual: 41.808s–42.808s — `{"possivel_mudanca_de_cena": false, "distancia_feature_print": 0.13040070235729218}`
+- similaridade_visual: 42.808s–43.808s — `{"possivel_mudanca_de_cena": false, "distancia_feature_print": 0.1921818107366562}`
+- similaridade_visual: 43.808s–44.808s — `{"possivel_mudanca_de_cena": false, "distancia_feature_print": 0.15198232233524323}`
+- similaridade_visual: 44.808s–45.808s — `{"possivel_mudanca_de_cena": false, "distancia_feature_print": 0.20895899832248688}`
diff --git a/code/relatorios/analise-visual/tempo-zero/0E6A8290/frame-000000.jpg b/code/relatorios/analise-visual/tempo-zero/0E6A8290/frame-000000.jpg
new file mode 100644
index 0000000..d664fa4
Binary files /dev/null and b/code/relatorios/analise-visual/tempo-zero/0E6A8290/frame-000000.jpg differ
diff --git a/code/relatorios/analise-visual/tempo-zero/video20251013_1041/frame-000000.jpg b/code/relatorios/analise-visual/tempo-zero/video20251013_1041/frame-000000.jpg
new file mode 100644
index 0000000..7422c67
Binary files /dev/null and b/code/relatorios/analise-visual/tempo-zero/video20251013_1041/frame-000000.jpg differ
diff --git a/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/audio/parte-0000.wav b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/audio/parte-0000.wav
new file mode 100644
index 0000000..0fc3348
Binary files /dev/null and b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/audio/parte-0000.wav differ
diff --git a/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-large-v3-turbo.json b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-large-v3-turbo.json
new file mode 100644
index 0000000..1321b4f
--- /dev/null
+++ b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-large-v3-turbo.json
@@ -0,0 +1,2444 @@
+[
+ {
+ "inicio": 2.03,
+ "fim": 8.11,
+ "texto": "Aquela mama com um formato mais estruturado, que valoriza o seu colo, que dá aquele ar de elegância.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Aquela",
+ "inicio": 2.03,
+ "fim": 2.75,
+ "confianca": 0.9264702498912811
+ },
+ {
+ "texto": "mama",
+ "inicio": 2.75,
+ "fim": 2.99,
+ "confianca": 0.9747146964073181
+ },
+ {
+ "texto": "com",
+ "inicio": 2.99,
+ "fim": 3.21,
+ "confianca": 0.9862388968467712
+ },
+ {
+ "texto": "um",
+ "inicio": 3.21,
+ "fim": 3.39,
+ "confianca": 0.8068708777427673
+ },
+ {
+ "texto": "formato",
+ "inicio": 3.39,
+ "fim": 3.79,
+ "confianca": 0.9970538020133972
+ },
+ {
+ "texto": "mais",
+ "inicio": 3.79,
+ "fim": 3.99,
+ "confianca": 0.9950518012046814
+ },
+ {
+ "texto": "estruturado,",
+ "inicio": 3.99,
+ "fim": 4.79,
+ "confianca": 0.9983626008033752
+ },
+ {
+ "texto": "que",
+ "inicio": 4.95,
+ "fim": 4.97,
+ "confianca": 0.985352098941803
+ },
+ {
+ "texto": "valoriza",
+ "inicio": 4.97,
+ "fim": 5.63,
+ "confianca": 0.9956299960613251
+ },
+ {
+ "texto": "o",
+ "inicio": 5.63,
+ "fim": 5.77,
+ "confianca": 0.9043883681297302
+ },
+ {
+ "texto": "seu",
+ "inicio": 5.77,
+ "fim": 5.93,
+ "confianca": 0.9896597266197205
+ },
+ {
+ "texto": "colo,",
+ "inicio": 5.93,
+ "fim": 6.39,
+ "confianca": 0.9981563687324524
+ },
+ {
+ "texto": "que",
+ "inicio": 6.51,
+ "fim": 6.51,
+ "confianca": 0.9678516983985901
+ },
+ {
+ "texto": "dá",
+ "inicio": 6.51,
+ "fim": 6.61,
+ "confianca": 0.9688865542411804
+ },
+ {
+ "texto": "aquele",
+ "inicio": 6.61,
+ "fim": 6.87,
+ "confianca": 0.9957236051559448
+ },
+ {
+ "texto": "ar",
+ "inicio": 6.87,
+ "fim": 7.17,
+ "confianca": 0.9953576922416687
+ },
+ {
+ "texto": "de",
+ "inicio": 7.17,
+ "fim": 7.31,
+ "confianca": 0.9986974596977234
+ },
+ {
+ "texto": "elegância.",
+ "inicio": 7.31,
+ "fim": 8.11,
+ "confianca": 0.9645151197910309
+ }
+ ],
+ "emocao": "hap",
+ "confianca_emocao": 0.5765212774276733,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 8.51,
+ "fim": 10.75,
+ "texto": "Ah, isso é o desejo de muitas mulheres, né?",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Ah,",
+ "inicio": 8.51,
+ "fim": 8.81,
+ "confianca": 0.44014161825180054
+ },
+ {
+ "texto": "isso",
+ "inicio": 8.81,
+ "fim": 9.01,
+ "confianca": 0.9809916615486145
+ },
+ {
+ "texto": "é",
+ "inicio": 9.01,
+ "fim": 9.13,
+ "confianca": 0.9853127598762512
+ },
+ {
+ "texto": "o",
+ "inicio": 9.13,
+ "fim": 9.25,
+ "confianca": 0.4644903838634491
+ },
+ {
+ "texto": "desejo",
+ "inicio": 9.25,
+ "fim": 9.37,
+ "confianca": 0.9988842904567719
+ },
+ {
+ "texto": "de",
+ "inicio": 9.37,
+ "fim": 9.51,
+ "confianca": 0.9997370839118958
+ },
+ {
+ "texto": "muitas",
+ "inicio": 9.51,
+ "fim": 9.91,
+ "confianca": 0.999232292175293
+ },
+ {
+ "texto": "mulheres,",
+ "inicio": 9.91,
+ "fim": 10.47,
+ "confianca": 0.9994701743125916
+ },
+ {
+ "texto": "né?",
+ "inicio": 10.67,
+ "fim": 10.75,
+ "confianca": 0.9981162548065186
+ }
+ ],
+ "emocao": "hap",
+ "confianca_emocao": 0.9701253771781921,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 11.03,
+ "fim": 14.65,
+ "texto": "Com o tempo, nosso corpo muda e nossas mamas também mudam.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Com",
+ "inicio": 11.03,
+ "fim": 11.59,
+ "confianca": 0.9988278746604919
+ },
+ {
+ "texto": "o",
+ "inicio": 11.59,
+ "fim": 11.77,
+ "confianca": 0.9999394416809082
+ },
+ {
+ "texto": "tempo,",
+ "inicio": 11.77,
+ "fim": 12.05,
+ "confianca": 0.999923586845398
+ },
+ {
+ "texto": "nosso",
+ "inicio": 12.11,
+ "fim": 12.33,
+ "confianca": 0.9841220378875732
+ },
+ {
+ "texto": "corpo",
+ "inicio": 12.33,
+ "fim": 12.67,
+ "confianca": 0.999916672706604
+ },
+ {
+ "texto": "muda",
+ "inicio": 12.67,
+ "fim": 13.09,
+ "confianca": 0.999943733215332
+ },
+ {
+ "texto": "e",
+ "inicio": 13.09,
+ "fim": 13.17,
+ "confianca": 0.9933417439460754
+ },
+ {
+ "texto": "nossas",
+ "inicio": 13.17,
+ "fim": 13.45,
+ "confianca": 0.9646458029747009
+ },
+ {
+ "texto": "mamas",
+ "inicio": 13.45,
+ "fim": 13.81,
+ "confianca": 0.9969677329063416
+ },
+ {
+ "texto": "também",
+ "inicio": 13.81,
+ "fim": 14.15,
+ "confianca": 0.9997397065162659
+ },
+ {
+ "texto": "mudam.",
+ "inicio": 14.15,
+ "fim": 14.65,
+ "confianca": 0.9998239576816559
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.9649906754493713,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 15.11,
+ "fim": 21.89,
+ "texto": "É a amamentação, perda de peso, efeito sanfona, tudo isso altera o formato e a sustentação das mamas.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "É",
+ "inicio": 15.11,
+ "fim": 15.25,
+ "confianca": 0.7894551753997803
+ },
+ {
+ "texto": "a",
+ "inicio": 15.25,
+ "fim": 15.35,
+ "confianca": 0.8575582504272461
+ },
+ {
+ "texto": "amamentação,",
+ "inicio": 15.35,
+ "fim": 16.01,
+ "confianca": 0.9831895232200623
+ },
+ {
+ "texto": "perda",
+ "inicio": 16.31,
+ "fim": 16.59,
+ "confianca": 0.9967639446258545
+ },
+ {
+ "texto": "de",
+ "inicio": 16.59,
+ "fim": 16.75,
+ "confianca": 0.9999964237213135
+ },
+ {
+ "texto": "peso,",
+ "inicio": 16.75,
+ "fim": 17.09,
+ "confianca": 0.9998589754104614
+ },
+ {
+ "texto": "efeito",
+ "inicio": 17.35,
+ "fim": 17.69,
+ "confianca": 0.996035099029541
+ },
+ {
+ "texto": "sanfona,",
+ "inicio": 17.69,
+ "fim": 18.41,
+ "confianca": 0.9875755707422892
+ },
+ {
+ "texto": "tudo",
+ "inicio": 18.69,
+ "fim": 18.95,
+ "confianca": 0.9996480941772461
+ },
+ {
+ "texto": "isso",
+ "inicio": 18.95,
+ "fim": 19.23,
+ "confianca": 0.9998124241828918
+ },
+ {
+ "texto": "altera",
+ "inicio": 19.23,
+ "fim": 19.77,
+ "confianca": 0.9997493326663971
+ },
+ {
+ "texto": "o",
+ "inicio": 19.77,
+ "fim": 19.89,
+ "confianca": 0.9999505281448364
+ },
+ {
+ "texto": "formato",
+ "inicio": 19.89,
+ "fim": 20.41,
+ "confianca": 0.9999050199985504
+ },
+ {
+ "texto": "e",
+ "inicio": 20.41,
+ "fim": 20.51,
+ "confianca": 0.9999613761901855
+ },
+ {
+ "texto": "a",
+ "inicio": 20.51,
+ "fim": 20.57,
+ "confianca": 0.9970609545707703
+ },
+ {
+ "texto": "sustentação",
+ "inicio": 20.57,
+ "fim": 21.23,
+ "confianca": 0.9998900294303894
+ },
+ {
+ "texto": "das",
+ "inicio": 21.23,
+ "fim": 21.47,
+ "confianca": 0.999723494052887
+ },
+ {
+ "texto": "mamas.",
+ "inicio": 21.47,
+ "fim": 21.89,
+ "confianca": 0.9988084435462952
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.7887659668922424,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 22.27,
+ "fim": 30.43,
+ "texto": "Então a mastopexia é uma cirurgia que reestrutura a mama e trata a flacidez, o excesso de pele e a queda das mamas.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Então",
+ "inicio": 22.27,
+ "fim": 22.61,
+ "confianca": 0.9991346001625061
+ },
+ {
+ "texto": "a",
+ "inicio": 22.61,
+ "fim": 22.77,
+ "confianca": 0.5840352177619934
+ },
+ {
+ "texto": "mastopexia",
+ "inicio": 22.77,
+ "fim": 23.73,
+ "confianca": 0.9964476525783539
+ },
+ {
+ "texto": "é",
+ "inicio": 23.73,
+ "fim": 23.95,
+ "confianca": 0.9998210072517395
+ },
+ {
+ "texto": "uma",
+ "inicio": 23.95,
+ "fim": 24.05,
+ "confianca": 0.9999511241912842
+ },
+ {
+ "texto": "cirurgia",
+ "inicio": 24.05,
+ "fim": 24.53,
+ "confianca": 0.999950647354126
+ },
+ {
+ "texto": "que",
+ "inicio": 24.53,
+ "fim": 24.71,
+ "confianca": 0.9999852180480957
+ },
+ {
+ "texto": "reestrutura",
+ "inicio": 24.71,
+ "fim": 25.55,
+ "confianca": 0.9993419945240021
+ },
+ {
+ "texto": "a",
+ "inicio": 25.55,
+ "fim": 25.71,
+ "confianca": 0.991851270198822
+ },
+ {
+ "texto": "mama",
+ "inicio": 25.71,
+ "fim": 25.93,
+ "confianca": 0.9989797472953796
+ },
+ {
+ "texto": "e",
+ "inicio": 25.93,
+ "fim": 26.39,
+ "confianca": 0.9984443783760071
+ },
+ {
+ "texto": "trata",
+ "inicio": 26.39,
+ "fim": 26.75,
+ "confianca": 0.9994363188743591
+ },
+ {
+ "texto": "a",
+ "inicio": 26.75,
+ "fim": 26.95,
+ "confianca": 0.9907048344612122
+ },
+ {
+ "texto": "flacidez,",
+ "inicio": 26.95,
+ "fim": 27.51,
+ "confianca": 0.998292863368988
+ },
+ {
+ "texto": "o",
+ "inicio": 27.95,
+ "fim": 28.01,
+ "confianca": 0.999603807926178
+ },
+ {
+ "texto": "excesso",
+ "inicio": 28.01,
+ "fim": 28.47,
+ "confianca": 0.9999604821205139
+ },
+ {
+ "texto": "de",
+ "inicio": 28.47,
+ "fim": 28.59,
+ "confianca": 0.9999386072158813
+ },
+ {
+ "texto": "pele",
+ "inicio": 28.59,
+ "fim": 28.93,
+ "confianca": 0.999897837638855
+ },
+ {
+ "texto": "e",
+ "inicio": 28.93,
+ "fim": 29.29,
+ "confianca": 0.9996216297149658
+ },
+ {
+ "texto": "a",
+ "inicio": 29.29,
+ "fim": 29.45,
+ "confianca": 0.9999599456787109
+ },
+ {
+ "texto": "queda",
+ "inicio": 29.45,
+ "fim": 29.73,
+ "confianca": 0.9998713731765747
+ },
+ {
+ "texto": "das",
+ "inicio": 29.73,
+ "fim": 29.99,
+ "confianca": 0.9994627833366394
+ },
+ {
+ "texto": "mamas.",
+ "inicio": 29.99,
+ "fim": 30.43,
+ "confianca": 0.9993627667427063
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.7571883201599121,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 30.43,
+ "fim": 38.33,
+ "texto": "E associada a essa cirurgia, a gente faz a inserção de prótese de silicone, para dar aquele formato e volume mais proporcional para o seu tórax.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "E",
+ "inicio": 30.43,
+ "fim": 30.85,
+ "confianca": 0.5894289016723633
+ },
+ {
+ "texto": "associada",
+ "inicio": 30.85,
+ "fim": 31.35,
+ "confianca": 0.6594655811786652
+ },
+ {
+ "texto": "a",
+ "inicio": 31.35,
+ "fim": 31.47,
+ "confianca": 0.897717297077179
+ },
+ {
+ "texto": "essa",
+ "inicio": 31.47,
+ "fim": 31.53,
+ "confianca": 0.8455350995063782
+ },
+ {
+ "texto": "cirurgia,",
+ "inicio": 31.53,
+ "fim": 31.99,
+ "confianca": 0.999173084894816
+ },
+ {
+ "texto": "a",
+ "inicio": 32.11,
+ "fim": 32.11,
+ "confianca": 0.9518707990646362
+ },
+ {
+ "texto": "gente",
+ "inicio": 32.11,
+ "fim": 32.23,
+ "confianca": 0.9992013573646545
+ },
+ {
+ "texto": "faz",
+ "inicio": 32.23,
+ "fim": 32.49,
+ "confianca": 0.9876646399497986
+ },
+ {
+ "texto": "a",
+ "inicio": 32.49,
+ "fim": 32.59,
+ "confianca": 0.9677696824073792
+ },
+ {
+ "texto": "inserção",
+ "inicio": 32.59,
+ "fim": 33.15,
+ "confianca": 0.9988690217336019
+ },
+ {
+ "texto": "de",
+ "inicio": 33.15,
+ "fim": 33.33,
+ "confianca": 0.9849644303321838
+ },
+ {
+ "texto": "prótese",
+ "inicio": 33.33,
+ "fim": 33.71,
+ "confianca": 0.7809635897477468
+ },
+ {
+ "texto": "de",
+ "inicio": 33.71,
+ "fim": 33.87,
+ "confianca": 0.9951323866844177
+ },
+ {
+ "texto": "silicone,",
+ "inicio": 33.87,
+ "fim": 34.33,
+ "confianca": 0.9957072734832764
+ },
+ {
+ "texto": "para",
+ "inicio": 34.57,
+ "fim": 34.81,
+ "confianca": 0.4802093505859375
+ },
+ {
+ "texto": "dar",
+ "inicio": 34.81,
+ "fim": 34.95,
+ "confianca": 0.9930907487869263
+ },
+ {
+ "texto": "aquele",
+ "inicio": 34.95,
+ "fim": 35.19,
+ "confianca": 0.9828141927719116
+ },
+ {
+ "texto": "formato",
+ "inicio": 35.19,
+ "fim": 35.85,
+ "confianca": 0.9779646098613739
+ },
+ {
+ "texto": "e",
+ "inicio": 35.85,
+ "fim": 35.99,
+ "confianca": 0.9925166964530945
+ },
+ {
+ "texto": "volume",
+ "inicio": 35.99,
+ "fim": 36.49,
+ "confianca": 0.9926032423973083
+ },
+ {
+ "texto": "mais",
+ "inicio": 36.49,
+ "fim": 36.75,
+ "confianca": 0.9760299921035767
+ },
+ {
+ "texto": "proporcional",
+ "inicio": 36.75,
+ "fim": 37.49,
+ "confianca": 0.9983233610788981
+ },
+ {
+ "texto": "para",
+ "inicio": 37.49,
+ "fim": 37.71,
+ "confianca": 0.9276042580604553
+ },
+ {
+ "texto": "o",
+ "inicio": 37.71,
+ "fim": 37.71,
+ "confianca": 0.9747331142425537
+ },
+ {
+ "texto": "seu",
+ "inicio": 37.71,
+ "fim": 37.87,
+ "confianca": 0.9897112846374512
+ },
+ {
+ "texto": "tórax.",
+ "inicio": 37.87,
+ "fim": 38.33,
+ "confianca": 0.9962517321109772
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.6729611158370972,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 38.59,
+ "fim": 40.71,
+ "texto": "E tudo isso associado a medida...",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "E",
+ "inicio": 38.59,
+ "fim": 38.91,
+ "confianca": 0.9462378025054932
+ },
+ {
+ "texto": "tudo",
+ "inicio": 38.91,
+ "fim": 39.09,
+ "confianca": 0.9966186285018921
+ },
+ {
+ "texto": "isso",
+ "inicio": 39.09,
+ "fim": 39.29,
+ "confianca": 0.9977166652679443
+ },
+ {
+ "texto": "associado",
+ "inicio": 39.29,
+ "fim": 39.89,
+ "confianca": 0.9223548471927643
+ },
+ {
+ "texto": "a",
+ "inicio": 39.89,
+ "fim": 39.95,
+ "confianca": 0.6501992344856262
+ },
+ {
+ "texto": "medida...",
+ "inicio": 39.95,
+ "fim": 40.71,
+ "confianca": 0.6888546794652939
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.8055887222290039,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 42.6,
+ "fim": 43.86,
+ "texto": "Amor, eu tô intacta!",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Amor,",
+ "inicio": 42.6,
+ "fim": 43.02,
+ "confianca": 0.8357453048229218
+ },
+ {
+ "texto": "eu",
+ "inicio": 43.1,
+ "fim": 43.2,
+ "confianca": 0.996441662311554
+ },
+ {
+ "texto": "tô",
+ "inicio": 43.2,
+ "fim": 43.34,
+ "confianca": 0.5258065462112427
+ },
+ {
+ "texto": "intacta!",
+ "inicio": 43.34,
+ "fim": 43.86,
+ "confianca": 0.9264104068279266
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.6746968030929565,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 47.63,
+ "fim": 48.93,
+ "texto": "Mas eu vou ter que falar tudo de novo?",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Mas",
+ "inicio": 47.63,
+ "fim": 47.87,
+ "confianca": 0.9972424507141113
+ },
+ {
+ "texto": "eu",
+ "inicio": 47.87,
+ "fim": 47.97,
+ "confianca": 0.9942136406898499
+ },
+ {
+ "texto": "vou",
+ "inicio": 47.97,
+ "fim": 48.03,
+ "confianca": 0.9995300769805908
+ },
+ {
+ "texto": "ter",
+ "inicio": 48.03,
+ "fim": 48.11,
+ "confianca": 0.9992952346801758
+ },
+ {
+ "texto": "que",
+ "inicio": 48.11,
+ "fim": 48.17,
+ "confianca": 0.9998376369476318
+ },
+ {
+ "texto": "falar",
+ "inicio": 48.17,
+ "fim": 48.31,
+ "confianca": 0.9991962313652039
+ },
+ {
+ "texto": "tudo",
+ "inicio": 48.31,
+ "fim": 48.51,
+ "confianca": 0.9997907280921936
+ },
+ {
+ "texto": "de",
+ "inicio": 48.51,
+ "fim": 48.63,
+ "confianca": 0.9995586276054382
+ },
+ {
+ "texto": "novo?",
+ "inicio": 48.63,
+ "fim": 48.93,
+ "confianca": 0.9999394416809082
+ }
+ ],
+ "emocao": "hap",
+ "confianca_emocao": 0.9591291546821594,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 55.44,
+ "fim": 56.38,
+ "texto": "Tudo de novo?",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Tudo",
+ "inicio": 55.44,
+ "fim": 55.92,
+ "confianca": 0.9665936231613159
+ },
+ {
+ "texto": "de",
+ "inicio": 55.92,
+ "fim": 56.08,
+ "confianca": 0.9998071789741516
+ },
+ {
+ "texto": "novo?",
+ "inicio": 56.08,
+ "fim": 56.38,
+ "confianca": 0.9999430179595947
+ }
+ ],
+ "emocao": "neu",
+ "confianca_emocao": 0.3705732822418213,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 64.94,
+ "fim": 69.38,
+ "texto": "Aquela mama com um formato mais estruturado, com o colo que...",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Aquela",
+ "inicio": 64.94,
+ "fim": 65.42,
+ "confianca": 0.9938247203826904
+ },
+ {
+ "texto": "mama",
+ "inicio": 65.42,
+ "fim": 65.68,
+ "confianca": 0.9934725761413574
+ },
+ {
+ "texto": "com",
+ "inicio": 65.68,
+ "fim": 65.92,
+ "confianca": 0.9995280504226685
+ },
+ {
+ "texto": "um",
+ "inicio": 65.92,
+ "fim": 66.08,
+ "confianca": 0.6648920774459839
+ },
+ {
+ "texto": "formato",
+ "inicio": 66.08,
+ "fim": 66.5,
+ "confianca": 0.9998356699943542
+ },
+ {
+ "texto": "mais",
+ "inicio": 66.5,
+ "fim": 66.72,
+ "confianca": 0.9997789263725281
+ },
+ {
+ "texto": "estruturado,",
+ "inicio": 66.72,
+ "fim": 67.7,
+ "confianca": 0.9999080499013265
+ },
+ {
+ "texto": "com",
+ "inicio": 67.88,
+ "fim": 68.12,
+ "confianca": 0.9896643757820129
+ },
+ {
+ "texto": "o",
+ "inicio": 68.12,
+ "fim": 68.3,
+ "confianca": 0.8102707862854004
+ },
+ {
+ "texto": "colo",
+ "inicio": 68.3,
+ "fim": 68.58,
+ "confianca": 0.9983736574649811
+ },
+ {
+ "texto": "que...",
+ "inicio": 68.58,
+ "fim": 69.38,
+ "confianca": 0.9934795796871185
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.9573915004730225,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 70.16,
+ "fim": 71.38,
+ "texto": "Amor, é que eu...",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Amor,",
+ "inicio": 70.16,
+ "fim": 70.64,
+ "confianca": 0.7914476990699768
+ },
+ {
+ "texto": "é",
+ "inicio": 70.76,
+ "fim": 70.8,
+ "confianca": 0.2712390422821045
+ },
+ {
+ "texto": "que",
+ "inicio": 70.8,
+ "fim": 70.88,
+ "confianca": 0.45143935084342957
+ },
+ {
+ "texto": "eu...",
+ "inicio": 70.88,
+ "fim": 71.38,
+ "confianca": 0.6671668887138367
+ }
+ ],
+ "emocao": "hap",
+ "confianca_emocao": 0.9356822967529297,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 71.38,
+ "fim": 73.7,
+ "texto": "Isso, só clica aí agora, na tela.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Isso,",
+ "inicio": 71.38,
+ "fim": 71.92,
+ "confianca": 0.8202244639396667
+ },
+ {
+ "texto": "só",
+ "inicio": 72.18,
+ "fim": 72.78,
+ "confianca": 0.9419981241226196
+ },
+ {
+ "texto": "clica",
+ "inicio": 72.78,
+ "fim": 73.06,
+ "confianca": 0.9918303489685059
+ },
+ {
+ "texto": "aí",
+ "inicio": 73.06,
+ "fim": 73.2,
+ "confianca": 0.997858464717865
+ },
+ {
+ "texto": "agora,",
+ "inicio": 73.2,
+ "fim": 73.4,
+ "confianca": 0.9673430919647217
+ },
+ {
+ "texto": "na",
+ "inicio": 73.56,
+ "fim": 73.62,
+ "confianca": 0.7569358944892883
+ },
+ {
+ "texto": "tela.",
+ "inicio": 73.62,
+ "fim": 73.7,
+ "confianca": 0.9829341769218445
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.7996239066123962,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 86.42,
+ "fim": 92.38,
+ "texto": "Aquela mama com um formato mais estruturado, que valoriza o seu colo, dá um ar de elegância.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Aquela",
+ "inicio": 86.42,
+ "fim": 86.92,
+ "confianca": 0.9833610951900482
+ },
+ {
+ "texto": "mama",
+ "inicio": 86.92,
+ "fim": 87.14,
+ "confianca": 0.9733269810676575
+ },
+ {
+ "texto": "com",
+ "inicio": 87.14,
+ "fim": 87.38,
+ "confianca": 0.9981223940849304
+ },
+ {
+ "texto": "um",
+ "inicio": 87.38,
+ "fim": 87.58,
+ "confianca": 0.7777116894721985
+ },
+ {
+ "texto": "formato",
+ "inicio": 87.58,
+ "fim": 88.04,
+ "confianca": 0.9995945692062378
+ },
+ {
+ "texto": "mais",
+ "inicio": 88.04,
+ "fim": 88.24,
+ "confianca": 0.9986615180969238
+ },
+ {
+ "texto": "estruturado,",
+ "inicio": 88.24,
+ "fim": 89.1,
+ "confianca": 0.9998505711555481
+ },
+ {
+ "texto": "que",
+ "inicio": 89.26,
+ "fim": 89.32,
+ "confianca": 0.9964319467544556
+ },
+ {
+ "texto": "valoriza",
+ "inicio": 89.32,
+ "fim": 89.94,
+ "confianca": 0.9996447563171387
+ },
+ {
+ "texto": "o",
+ "inicio": 89.94,
+ "fim": 90.08,
+ "confianca": 0.9921292066574097
+ },
+ {
+ "texto": "seu",
+ "inicio": 90.08,
+ "fim": 90.22,
+ "confianca": 0.9994630217552185
+ },
+ {
+ "texto": "colo,",
+ "inicio": 90.22,
+ "fim": 90.82,
+ "confianca": 0.9991680383682251
+ },
+ {
+ "texto": "dá",
+ "inicio": 90.9,
+ "fim": 90.98,
+ "confianca": 0.9939993619918823
+ },
+ {
+ "texto": "um",
+ "inicio": 90.98,
+ "fim": 91.14,
+ "confianca": 0.9997010231018066
+ },
+ {
+ "texto": "ar",
+ "inicio": 91.14,
+ "fim": 91.42,
+ "confianca": 0.9987478256225586
+ },
+ {
+ "texto": "de",
+ "inicio": 91.42,
+ "fim": 91.58,
+ "confianca": 0.9997878670692444
+ },
+ {
+ "texto": "elegância.",
+ "inicio": 91.58,
+ "fim": 92.38,
+ "confianca": 0.9992329180240631
+ }
+ ],
+ "emocao": "hap",
+ "confianca_emocao": 0.48946771025657654,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 92.7,
+ "fim": 94.84,
+ "texto": "Ah, isso é desejo de muitas mulheres, né?",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Ah,",
+ "inicio": 92.7,
+ "fim": 93.08,
+ "confianca": 0.198680117726326
+ },
+ {
+ "texto": "isso",
+ "inicio": 93.08,
+ "fim": 93.24,
+ "confianca": 0.9888452887535095
+ },
+ {
+ "texto": "é",
+ "inicio": 93.24,
+ "fim": 93.32,
+ "confianca": 0.9522721171379089
+ },
+ {
+ "texto": "desejo",
+ "inicio": 93.32,
+ "fim": 93.62,
+ "confianca": 0.7885190546512604
+ },
+ {
+ "texto": "de",
+ "inicio": 93.62,
+ "fim": 93.76,
+ "confianca": 0.9993330836296082
+ },
+ {
+ "texto": "muitas",
+ "inicio": 93.76,
+ "fim": 94.06,
+ "confianca": 0.9997499585151672
+ },
+ {
+ "texto": "mulheres,",
+ "inicio": 94.06,
+ "fim": 94.58,
+ "confianca": 0.9997798800468445
+ },
+ {
+ "texto": "né?",
+ "inicio": 94.74,
+ "fim": 94.84,
+ "confianca": 0.9996660947799683
+ }
+ ],
+ "emocao": "hap",
+ "confianca_emocao": 0.6742347478866577,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 95.3,
+ "fim": 98.7,
+ "texto": "Com o tempo, o corpo muda e as nossas mamas também mudam.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Com",
+ "inicio": 95.3,
+ "fim": 95.68,
+ "confianca": 0.9992000460624695
+ },
+ {
+ "texto": "o",
+ "inicio": 95.68,
+ "fim": 95.84,
+ "confianca": 0.9999667406082153
+ },
+ {
+ "texto": "tempo,",
+ "inicio": 95.84,
+ "fim": 96.1,
+ "confianca": 0.9999513626098633
+ },
+ {
+ "texto": "o",
+ "inicio": 96.22,
+ "fim": 96.32,
+ "confianca": 0.9999099969863892
+ },
+ {
+ "texto": "corpo",
+ "inicio": 96.32,
+ "fim": 96.62,
+ "confianca": 0.9999480247497559
+ },
+ {
+ "texto": "muda",
+ "inicio": 96.62,
+ "fim": 97.1,
+ "confianca": 0.9999667406082153
+ },
+ {
+ "texto": "e",
+ "inicio": 97.1,
+ "fim": 97.24,
+ "confianca": 0.9907146692276001
+ },
+ {
+ "texto": "as",
+ "inicio": 97.24,
+ "fim": 97.36,
+ "confianca": 0.9960784316062927
+ },
+ {
+ "texto": "nossas",
+ "inicio": 97.36,
+ "fim": 97.62,
+ "confianca": 0.9997072815895081
+ },
+ {
+ "texto": "mamas",
+ "inicio": 97.62,
+ "fim": 97.86,
+ "confianca": 0.9963950514793396
+ },
+ {
+ "texto": "também",
+ "inicio": 97.86,
+ "fim": 98.2,
+ "confianca": 0.9998277425765991
+ },
+ {
+ "texto": "mudam.",
+ "inicio": 98.2,
+ "fim": 98.7,
+ "confianca": 0.9998317062854767
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.9833848476409912,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 99.14,
+ "fim": 106.28,
+ "texto": "É a amamentação, perda de peso, efeito sanfona, tudo isso modifica o formato e a sustentação das mamas.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "É",
+ "inicio": 99.14,
+ "fim": 99.28,
+ "confianca": 0.8710682988166809
+ },
+ {
+ "texto": "a",
+ "inicio": 99.28,
+ "fim": 99.36,
+ "confianca": 0.8386006951332092
+ },
+ {
+ "texto": "amamentação,",
+ "inicio": 99.36,
+ "fim": 100.04,
+ "confianca": 0.9817183613777161
+ },
+ {
+ "texto": "perda",
+ "inicio": 100.34,
+ "fim": 100.7,
+ "confianca": 0.9982805252075195
+ },
+ {
+ "texto": "de",
+ "inicio": 100.7,
+ "fim": 100.86,
+ "confianca": 0.9999972581863403
+ },
+ {
+ "texto": "peso,",
+ "inicio": 100.86,
+ "fim": 101.24,
+ "confianca": 0.999923825263977
+ },
+ {
+ "texto": "efeito",
+ "inicio": 101.52,
+ "fim": 101.9,
+ "confianca": 0.9965123534202576
+ },
+ {
+ "texto": "sanfona,",
+ "inicio": 101.9,
+ "fim": 102.6,
+ "confianca": 0.9958927234013876
+ },
+ {
+ "texto": "tudo",
+ "inicio": 102.82,
+ "fim": 103.2,
+ "confianca": 0.9990012049674988
+ },
+ {
+ "texto": "isso",
+ "inicio": 103.2,
+ "fim": 103.5,
+ "confianca": 0.9998515844345093
+ },
+ {
+ "texto": "modifica",
+ "inicio": 103.5,
+ "fim": 104.12,
+ "confianca": 0.999665230512619
+ },
+ {
+ "texto": "o",
+ "inicio": 104.12,
+ "fim": 104.28,
+ "confianca": 0.9999849796295166
+ },
+ {
+ "texto": "formato",
+ "inicio": 104.28,
+ "fim": 104.78,
+ "confianca": 0.9999090135097504
+ },
+ {
+ "texto": "e",
+ "inicio": 104.78,
+ "fim": 104.88,
+ "confianca": 0.9999743700027466
+ },
+ {
+ "texto": "a",
+ "inicio": 104.88,
+ "fim": 104.96,
+ "confianca": 0.9921758770942688
+ },
+ {
+ "texto": "sustentação",
+ "inicio": 104.96,
+ "fim": 105.62,
+ "confianca": 0.999913493792216
+ },
+ {
+ "texto": "das",
+ "inicio": 105.62,
+ "fim": 105.86,
+ "confianca": 0.9996123909950256
+ },
+ {
+ "texto": "mamas.",
+ "inicio": 105.86,
+ "fim": 106.28,
+ "confianca": 0.9992197453975677
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.9241726398468018,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 106.82,
+ "fim": 109.9,
+ "texto": "A mastopexia é a cirurgia que reestrutura a mama.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "A",
+ "inicio": 106.82,
+ "fim": 107.04,
+ "confianca": 0.9922789335250854
+ },
+ {
+ "texto": "mastopexia",
+ "inicio": 107.04,
+ "fim": 107.86,
+ "confianca": 0.9937713593244553
+ },
+ {
+ "texto": "é",
+ "inicio": 107.86,
+ "fim": 108.0,
+ "confianca": 0.9497213959693909
+ },
+ {
+ "texto": "a",
+ "inicio": 108.0,
+ "fim": 108.02,
+ "confianca": 0.9884999394416809
+ },
+ {
+ "texto": "cirurgia",
+ "inicio": 108.02,
+ "fim": 108.4,
+ "confianca": 0.9999552965164185
+ },
+ {
+ "texto": "que",
+ "inicio": 108.4,
+ "fim": 108.56,
+ "confianca": 0.9999958276748657
+ },
+ {
+ "texto": "reestrutura",
+ "inicio": 108.56,
+ "fim": 109.48,
+ "confianca": 0.9994795471429825
+ },
+ {
+ "texto": "a",
+ "inicio": 109.48,
+ "fim": 109.66,
+ "confianca": 0.9990593791007996
+ },
+ {
+ "texto": "mama.",
+ "inicio": 109.66,
+ "fim": 109.9,
+ "confianca": 0.9994478821754456
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.9679931998252869,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 109.9,
+ "fim": 114.14,
+ "texto": "Ela trata a flacidez de pele, o excesso de pele...",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Ela",
+ "inicio": 109.9,
+ "fim": 110.38,
+ "confianca": 0.902750551700592
+ },
+ {
+ "texto": "trata",
+ "inicio": 110.38,
+ "fim": 110.72,
+ "confianca": 0.9997432827949524
+ },
+ {
+ "texto": "a",
+ "inicio": 110.72,
+ "fim": 110.9,
+ "confianca": 0.9805799126625061
+ },
+ {
+ "texto": "flacidez",
+ "inicio": 110.9,
+ "fim": 111.38,
+ "confianca": 0.9960743188858032
+ },
+ {
+ "texto": "de",
+ "inicio": 111.38,
+ "fim": 111.58,
+ "confianca": 0.9990584254264832
+ },
+ {
+ "texto": "pele,",
+ "inicio": 111.58,
+ "fim": 112.0,
+ "confianca": 0.9999256134033203
+ },
+ {
+ "texto": "o",
+ "inicio": 112.32,
+ "fim": 112.54,
+ "confianca": 0.9998817443847656
+ },
+ {
+ "texto": "excesso",
+ "inicio": 112.54,
+ "fim": 113.12,
+ "confianca": 0.9999726414680481
+ },
+ {
+ "texto": "de",
+ "inicio": 113.12,
+ "fim": 113.22,
+ "confianca": 0.9999003410339355
+ },
+ {
+ "texto": "pele...",
+ "inicio": 113.22,
+ "fim": 114.14,
+ "confianca": 0.7541303038597107
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.8817266225814819,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 114.14,
+ "fim": 115.8,
+ "texto": "Posso começar da mastopexia?",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Posso",
+ "inicio": 114.14,
+ "fim": 114.88,
+ "confianca": 0.9813877940177917
+ },
+ {
+ "texto": "começar",
+ "inicio": 114.88,
+ "fim": 115.1,
+ "confianca": 0.9957549571990967
+ },
+ {
+ "texto": "da",
+ "inicio": 115.1,
+ "fim": 115.24,
+ "confianca": 0.9381835460662842
+ },
+ {
+ "texto": "mastopexia?",
+ "inicio": 115.24,
+ "fim": 115.8,
+ "confianca": 0.989023819565773
+ }
+ ],
+ "emocao": "neu",
+ "confianca_emocao": 0.9022887349128723,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 122.9,
+ "fim": 126.1,
+ "texto": "A mastopexia é a cirurgia que reestrutura a sua mama.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "A",
+ "inicio": 122.9,
+ "fim": 123.38,
+ "confianca": 0.715907871723175
+ },
+ {
+ "texto": "mastopexia",
+ "inicio": 123.38,
+ "fim": 123.86,
+ "confianca": 0.9996132254600525
+ },
+ {
+ "texto": "é",
+ "inicio": 123.86,
+ "fim": 124.0,
+ "confianca": 0.999687671661377
+ },
+ {
+ "texto": "a",
+ "inicio": 124.0,
+ "fim": 124.08,
+ "confianca": 0.9976223111152649
+ },
+ {
+ "texto": "cirurgia",
+ "inicio": 124.08,
+ "fim": 124.48,
+ "confianca": 0.9999473492304484
+ },
+ {
+ "texto": "que",
+ "inicio": 124.48,
+ "fim": 124.64,
+ "confianca": 0.9999884366989136
+ },
+ {
+ "texto": "reestrutura",
+ "inicio": 124.64,
+ "fim": 125.48,
+ "confianca": 0.9992972612380981
+ },
+ {
+ "texto": "a",
+ "inicio": 125.48,
+ "fim": 125.66,
+ "confianca": 0.547847330570221
+ },
+ {
+ "texto": "sua",
+ "inicio": 125.66,
+ "fim": 125.74,
+ "confianca": 0.9998185038566589
+ },
+ {
+ "texto": "mama.",
+ "inicio": 125.74,
+ "fim": 126.1,
+ "confianca": 0.997516393661499
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.8306940793991089,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 127.66,
+ "fim": 134.98,
+ "texto": "A mastopexia é a cirurgia que reestrutura a sua mama, tratando a flacidez, o excesso de pele e a queda das mamas.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "A",
+ "inicio": 127.66,
+ "fim": 128.14,
+ "confianca": 0.980974018573761
+ },
+ {
+ "texto": "mastopexia",
+ "inicio": 128.14,
+ "fim": 128.62,
+ "confianca": 0.9997156858444214
+ },
+ {
+ "texto": "é",
+ "inicio": 128.62,
+ "fim": 128.82,
+ "confianca": 0.9999074935913086
+ },
+ {
+ "texto": "a",
+ "inicio": 128.82,
+ "fim": 128.82,
+ "confianca": 0.9998544454574585
+ },
+ {
+ "texto": "cirurgia",
+ "inicio": 128.82,
+ "fim": 129.16,
+ "confianca": 0.9999821583429972
+ },
+ {
+ "texto": "que",
+ "inicio": 129.16,
+ "fim": 129.34,
+ "confianca": 0.999996542930603
+ },
+ {
+ "texto": "reestrutura",
+ "inicio": 129.34,
+ "fim": 130.18,
+ "confianca": 0.9999186098575592
+ },
+ {
+ "texto": "a",
+ "inicio": 130.18,
+ "fim": 130.34,
+ "confianca": 0.9985576272010803
+ },
+ {
+ "texto": "sua",
+ "inicio": 130.34,
+ "fim": 130.42,
+ "confianca": 0.9999587535858154
+ },
+ {
+ "texto": "mama,",
+ "inicio": 130.42,
+ "fim": 130.76,
+ "confianca": 0.9996709823608398
+ },
+ {
+ "texto": "tratando",
+ "inicio": 130.98,
+ "fim": 131.52,
+ "confianca": 0.9996646344661713
+ },
+ {
+ "texto": "a",
+ "inicio": 131.52,
+ "fim": 131.62,
+ "confianca": 0.9995397329330444
+ },
+ {
+ "texto": "flacidez,",
+ "inicio": 131.62,
+ "fim": 132.2,
+ "confianca": 0.9997584422429403
+ },
+ {
+ "texto": "o",
+ "inicio": 132.54,
+ "fim": 132.7,
+ "confianca": 0.9999841451644897
+ },
+ {
+ "texto": "excesso",
+ "inicio": 132.7,
+ "fim": 133.14,
+ "confianca": 0.9999890327453613
+ },
+ {
+ "texto": "de",
+ "inicio": 133.14,
+ "fim": 133.24,
+ "confianca": 0.9999953508377075
+ },
+ {
+ "texto": "pele",
+ "inicio": 133.24,
+ "fim": 133.62,
+ "confianca": 0.9999836683273315
+ },
+ {
+ "texto": "e",
+ "inicio": 133.62,
+ "fim": 133.92,
+ "confianca": 0.9996115565299988
+ },
+ {
+ "texto": "a",
+ "inicio": 133.92,
+ "fim": 134.04,
+ "confianca": 0.999983549118042
+ },
+ {
+ "texto": "queda",
+ "inicio": 134.04,
+ "fim": 134.34,
+ "confianca": 0.9998502731323242
+ },
+ {
+ "texto": "das",
+ "inicio": 134.34,
+ "fim": 134.58,
+ "confianca": 0.9991242289543152
+ },
+ {
+ "texto": "mamas.",
+ "inicio": 134.58,
+ "fim": 134.98,
+ "confianca": 0.9980533421039581
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.9167826771736145,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 135.16,
+ "fim": 141.8,
+ "texto": "E associada a essa cirurgia, a gente insere próteses de silicone para dar um formato e um volume mais proporcional para o seu corpo.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "E",
+ "inicio": 135.16,
+ "fim": 135.38,
+ "confianca": 0.9959718585014343
+ },
+ {
+ "texto": "associada",
+ "inicio": 135.38,
+ "fim": 136.02,
+ "confianca": 0.9754234552383423
+ },
+ {
+ "texto": "a",
+ "inicio": 136.02,
+ "fim": 136.14,
+ "confianca": 0.9935924410820007
+ },
+ {
+ "texto": "essa",
+ "inicio": 136.14,
+ "fim": 136.22,
+ "confianca": 0.9961512684822083
+ },
+ {
+ "texto": "cirurgia,",
+ "inicio": 136.22,
+ "fim": 136.76,
+ "confianca": 0.9999638001124064
+ },
+ {
+ "texto": "a",
+ "inicio": 136.88,
+ "fim": 136.9,
+ "confianca": 0.99781334400177
+ },
+ {
+ "texto": "gente",
+ "inicio": 136.9,
+ "fim": 137.04,
+ "confianca": 0.9999687671661377
+ },
+ {
+ "texto": "insere",
+ "inicio": 137.04,
+ "fim": 137.48,
+ "confianca": 0.9997843205928802
+ },
+ {
+ "texto": "próteses",
+ "inicio": 137.48,
+ "fim": 138.02,
+ "confianca": 0.9775620301564535
+ },
+ {
+ "texto": "de",
+ "inicio": 138.02,
+ "fim": 138.12,
+ "confianca": 0.9999855756759644
+ },
+ {
+ "texto": "silicone",
+ "inicio": 138.12,
+ "fim": 138.54,
+ "confianca": 0.9998171925544739
+ },
+ {
+ "texto": "para",
+ "inicio": 138.54,
+ "fim": 138.82,
+ "confianca": 0.6930963397026062
+ },
+ {
+ "texto": "dar",
+ "inicio": 138.82,
+ "fim": 138.96,
+ "confianca": 0.9998010993003845
+ },
+ {
+ "texto": "um",
+ "inicio": 138.96,
+ "fim": 139.1,
+ "confianca": 0.9999232292175293
+ },
+ {
+ "texto": "formato",
+ "inicio": 139.1,
+ "fim": 139.64,
+ "confianca": 0.9993737936019897
+ },
+ {
+ "texto": "e",
+ "inicio": 139.64,
+ "fim": 139.74,
+ "confianca": 0.9978823065757751
+ },
+ {
+ "texto": "um",
+ "inicio": 139.74,
+ "fim": 139.84,
+ "confianca": 0.9993959665298462
+ },
+ {
+ "texto": "volume",
+ "inicio": 139.84,
+ "fim": 140.24,
+ "confianca": 0.9998761415481567
+ },
+ {
+ "texto": "mais",
+ "inicio": 140.24,
+ "fim": 140.5,
+ "confianca": 0.9996341466903687
+ },
+ {
+ "texto": "proporcional",
+ "inicio": 140.5,
+ "fim": 141.06,
+ "confianca": 0.9998394449551901
+ },
+ {
+ "texto": "para",
+ "inicio": 141.06,
+ "fim": 141.24,
+ "confianca": 0.9934318661689758
+ },
+ {
+ "texto": "o",
+ "inicio": 141.24,
+ "fim": 141.28,
+ "confianca": 0.9998941421508789
+ },
+ {
+ "texto": "seu",
+ "inicio": 141.28,
+ "fim": 141.42,
+ "confianca": 0.999889612197876
+ },
+ {
+ "texto": "corpo.",
+ "inicio": 141.42,
+ "fim": 141.8,
+ "confianca": 0.9998681545257568
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.9144425988197327,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 141.8,
+ "fim": 150.32,
+ "texto": "E associada a técnicas modernas, como sutiã interna e alça muscular, a gente consegue manter por mais tempo o formato e a sustentação das mamas.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "E",
+ "inicio": 141.8,
+ "fim": 142.42,
+ "confianca": 0.9653200507164001
+ },
+ {
+ "texto": "associada",
+ "inicio": 142.42,
+ "fim": 142.88,
+ "confianca": 0.851288229227066
+ },
+ {
+ "texto": "a",
+ "inicio": 142.88,
+ "fim": 143.04,
+ "confianca": 0.995076596736908
+ },
+ {
+ "texto": "técnicas",
+ "inicio": 143.04,
+ "fim": 143.36,
+ "confianca": 0.9997820258140564
+ },
+ {
+ "texto": "modernas,",
+ "inicio": 143.36,
+ "fim": 144.0,
+ "confianca": 0.999106228351593
+ },
+ {
+ "texto": "como",
+ "inicio": 144.12,
+ "fim": 144.18,
+ "confianca": 0.9997701048851013
+ },
+ {
+ "texto": "sutiã",
+ "inicio": 144.18,
+ "fim": 144.62,
+ "confianca": 0.9824154376983643
+ },
+ {
+ "texto": "interna",
+ "inicio": 144.62,
+ "fim": 145.08,
+ "confianca": 0.9994798600673676
+ },
+ {
+ "texto": "e",
+ "inicio": 145.08,
+ "fim": 145.16,
+ "confianca": 0.9993234872817993
+ },
+ {
+ "texto": "alça",
+ "inicio": 145.16,
+ "fim": 145.5,
+ "confianca": 0.9961681663990021
+ },
+ {
+ "texto": "muscular,",
+ "inicio": 145.5,
+ "fim": 145.92,
+ "confianca": 0.9999213218688965
+ },
+ {
+ "texto": "a",
+ "inicio": 146.4,
+ "fim": 146.46,
+ "confianca": 0.997974693775177
+ },
+ {
+ "texto": "gente",
+ "inicio": 146.46,
+ "fim": 146.58,
+ "confianca": 0.9998992681503296
+ },
+ {
+ "texto": "consegue",
+ "inicio": 146.58,
+ "fim": 146.96,
+ "confianca": 0.9996154308319092
+ },
+ {
+ "texto": "manter",
+ "inicio": 146.96,
+ "fim": 147.44,
+ "confianca": 0.9998646974563599
+ },
+ {
+ "texto": "por",
+ "inicio": 147.44,
+ "fim": 147.68,
+ "confianca": 0.9944728016853333
+ },
+ {
+ "texto": "mais",
+ "inicio": 147.68,
+ "fim": 147.96,
+ "confianca": 0.9998822212219238
+ },
+ {
+ "texto": "tempo",
+ "inicio": 147.96,
+ "fim": 148.32,
+ "confianca": 0.9998179078102112
+ },
+ {
+ "texto": "o",
+ "inicio": 148.32,
+ "fim": 148.46,
+ "confianca": 0.9984386563301086
+ },
+ {
+ "texto": "formato",
+ "inicio": 148.46,
+ "fim": 148.92,
+ "confianca": 0.9999219179153442
+ },
+ {
+ "texto": "e",
+ "inicio": 148.92,
+ "fim": 149.02,
+ "confianca": 0.9998112320899963
+ },
+ {
+ "texto": "a",
+ "inicio": 149.02,
+ "fim": 149.12,
+ "confianca": 0.9604440927505493
+ },
+ {
+ "texto": "sustentação",
+ "inicio": 149.12,
+ "fim": 149.66,
+ "confianca": 0.9999233484268188
+ },
+ {
+ "texto": "das",
+ "inicio": 149.66,
+ "fim": 149.86,
+ "confianca": 0.999104917049408
+ },
+ {
+ "texto": "mamas.",
+ "inicio": 149.86,
+ "fim": 150.32,
+ "confianca": 0.9966182708740234
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.7816937565803528,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 150.78,
+ "fim": 152.96,
+ "texto": "Vou falar só a última frase, solta aqui.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Vou",
+ "inicio": 150.78,
+ "fim": 151.28,
+ "confianca": 0.570104718208313
+ },
+ {
+ "texto": "falar",
+ "inicio": 151.28,
+ "fim": 151.48,
+ "confianca": 0.9885298609733582
+ },
+ {
+ "texto": "só",
+ "inicio": 151.48,
+ "fim": 151.62,
+ "confianca": 0.9880200028419495
+ },
+ {
+ "texto": "a",
+ "inicio": 151.62,
+ "fim": 151.7,
+ "confianca": 0.9984942674636841
+ },
+ {
+ "texto": "última",
+ "inicio": 151.7,
+ "fim": 151.86,
+ "confianca": 0.9988417029380798
+ },
+ {
+ "texto": "frase,",
+ "inicio": 151.86,
+ "fim": 152.34,
+ "confianca": 0.999735414981842
+ },
+ {
+ "texto": "solta",
+ "inicio": 152.48,
+ "fim": 152.74,
+ "confianca": 0.8792521357536316
+ },
+ {
+ "texto": "aqui.",
+ "inicio": 152.74,
+ "fim": 152.96,
+ "confianca": 0.7710581421852112
+ }
+ ],
+ "emocao": "neu",
+ "confianca_emocao": 0.9800121188163757,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 153.64,
+ "fim": 154.26,
+ "texto": "Não pegou?",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Não",
+ "inicio": 153.64,
+ "fim": 153.92,
+ "confianca": 0.9978139400482178
+ },
+ {
+ "texto": "pegou?",
+ "inicio": 153.92,
+ "fim": 154.26,
+ "confianca": 0.999887079000473
+ }
+ ],
+ "emocao": "neu",
+ "confianca_emocao": 0.6393876671791077,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 159.85,
+ "fim": 160.43,
+ "texto": "Aumenta.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Aumenta.",
+ "inicio": 159.85,
+ "fim": 160.43,
+ "confianca": 0.9993709921836853
+ }
+ ],
+ "emocao": "hap",
+ "confianca_emocao": 0.6867960095405579,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 180.49,
+ "fim": 181.33,
+ "texto": "E aí, continua?",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "E",
+ "inicio": 180.49,
+ "fim": 180.83,
+ "confianca": 0.9997554421424866
+ },
+ {
+ "texto": "aí,",
+ "inicio": 180.83,
+ "fim": 180.95,
+ "confianca": 0.9982985854148865
+ },
+ {
+ "texto": "continua?",
+ "inicio": 181.03,
+ "fim": 181.33,
+ "confianca": 0.999734103679657
+ }
+ ],
+ "emocao": "hap",
+ "confianca_emocao": 0.7485158443450928,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 184.1,
+ "fim": 188.64,
+ "texto": "Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Então",
+ "inicio": 184.1,
+ "fim": 184.68,
+ "confianca": 0.9410873651504517
+ },
+ {
+ "texto": "é",
+ "inicio": 184.68,
+ "fim": 184.82,
+ "confianca": 0.8670706748962402
+ },
+ {
+ "texto": "devolver",
+ "inicio": 184.82,
+ "fim": 185.24,
+ "confianca": 0.9998443325360616
+ },
+ {
+ "texto": "forma",
+ "inicio": 185.24,
+ "fim": 185.68,
+ "confianca": 0.9978957176208496
+ },
+ {
+ "texto": "e",
+ "inicio": 185.68,
+ "fim": 185.8,
+ "confianca": 0.9989676475524902
+ },
+ {
+ "texto": "sustentação",
+ "inicio": 185.8,
+ "fim": 186.5,
+ "confianca": 0.9999355872472128
+ },
+ {
+ "texto": "de",
+ "inicio": 186.5,
+ "fim": 187.0,
+ "confianca": 0.9641322493553162
+ },
+ {
+ "texto": "um",
+ "inicio": 187.0,
+ "fim": 187.06,
+ "confianca": 0.9997968077659607
+ },
+ {
+ "texto": "jeito",
+ "inicio": 187.06,
+ "fim": 187.24,
+ "confianca": 0.9999758005142212
+ },
+ {
+ "texto": "que",
+ "inicio": 187.24,
+ "fim": 187.38,
+ "confianca": 0.9999332427978516
+ },
+ {
+ "texto": "pareça",
+ "inicio": 187.38,
+ "fim": 187.7,
+ "confianca": 0.8473417460918427
+ },
+ {
+ "texto": "que",
+ "inicio": 187.7,
+ "fim": 187.84,
+ "confianca": 0.9983774423599243
+ },
+ {
+ "texto": "sempre",
+ "inicio": 187.84,
+ "fim": 188.12,
+ "confianca": 0.9999487400054932
+ },
+ {
+ "texto": "foi",
+ "inicio": 188.12,
+ "fim": 188.36,
+ "confianca": 0.9999474287033081
+ },
+ {
+ "texto": "assim.",
+ "inicio": 188.36,
+ "fim": 188.64,
+ "confianca": 0.9999321699142456
+ }
+ ],
+ "emocao": "hap",
+ "confianca_emocao": 0.5587806105613708,
+ "caracteristicas_acusticas": {}
+ },
+ {
+ "inicio": 189.86,
+ "fim": 194.62,
+ "texto": "Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim.",
+ "confianca": null,
+ "palavras": [
+ {
+ "texto": "Então",
+ "inicio": 189.86,
+ "fim": 190.44,
+ "confianca": 0.9961023330688477
+ },
+ {
+ "texto": "é",
+ "inicio": 190.44,
+ "fim": 190.56,
+ "confianca": 0.9995197057723999
+ },
+ {
+ "texto": "devolver",
+ "inicio": 190.56,
+ "fim": 191.08,
+ "confianca": 0.9999773899714152
+ },
+ {
+ "texto": "forma",
+ "inicio": 191.08,
+ "fim": 191.58,
+ "confianca": 0.9998617172241211
+ },
+ {
+ "texto": "e",
+ "inicio": 191.58,
+ "fim": 191.66,
+ "confianca": 0.9999783039093018
+ },
+ {
+ "texto": "sustentação",
+ "inicio": 191.66,
+ "fim": 192.4,
+ "confianca": 0.9999794562657675
+ },
+ {
+ "texto": "de",
+ "inicio": 192.4,
+ "fim": 192.6,
+ "confianca": 0.9999773502349854
+ },
+ {
+ "texto": "um",
+ "inicio": 192.6,
+ "fim": 192.7,
+ "confianca": 0.9999616146087646
+ },
+ {
+ "texto": "jeito",
+ "inicio": 192.7,
+ "fim": 192.94,
+ "confianca": 0.9999750852584839
+ },
+ {
+ "texto": "que",
+ "inicio": 192.94,
+ "fim": 193.14,
+ "confianca": 0.9999600648880005
+ },
+ {
+ "texto": "pareça",
+ "inicio": 193.14,
+ "fim": 193.58,
+ "confianca": 0.9612461626529694
+ },
+ {
+ "texto": "que",
+ "inicio": 193.58,
+ "fim": 193.74,
+ "confianca": 0.9999704360961914
+ },
+ {
+ "texto": "sempre",
+ "inicio": 193.74,
+ "fim": 194.06,
+ "confianca": 0.999976396560669
+ },
+ {
+ "texto": "foi",
+ "inicio": 194.06,
+ "fim": 194.3,
+ "confianca": 0.9999736547470093
+ },
+ {
+ "texto": "assim.",
+ "inicio": 194.3,
+ "fim": 194.62,
+ "confianca": 0.9999501705169678
+ }
+ ],
+ "emocao": "ang",
+ "confianca_emocao": 0.770022988319397,
+ "caracteristicas_acusticas": {}
+ }
+]
\ No newline at end of file
diff --git a/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-large-v3-turbo.txt b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-large-v3-turbo.txt
new file mode 100644
index 0000000..0f4026c
--- /dev/null
+++ b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-large-v3-turbo.txt
@@ -0,0 +1 @@
+Aquela mama com um formato mais estruturado, que valoriza o seu colo, que dá aquele ar de elegância. Ah, isso é o desejo de muitas mulheres, né? Com o tempo, nosso corpo muda e nossas mamas também mudam. É a amamentação, perda de peso, efeito sanfona, tudo isso altera o formato e a sustentação das mamas. Então a mastopexia é uma cirurgia que reestrutura a mama e trata a flacidez, o excesso de pele e a queda das mamas. E associada a essa cirurgia, a gente faz a inserção de prótese de silicone, para dar aquele formato e volume mais proporcional para o seu tórax. E tudo isso associado a medida... Amor, eu tô intacta! Mas eu vou ter que falar tudo de novo? Tudo de novo? Aquela mama com um formato mais estruturado, com o colo que... Amor, é que eu... Isso, só clica aí agora, na tela. Aquela mama com um formato mais estruturado, que valoriza o seu colo, dá um ar de elegância. Ah, isso é desejo de muitas mulheres, né? Com o tempo, o corpo muda e as nossas mamas também mudam. É a amamentação, perda de peso, efeito sanfona, tudo isso modifica o formato e a sustentação das mamas. A mastopexia é a cirurgia que reestrutura a mama. Ela trata a flacidez de pele, o excesso de pele... Posso começar da mastopexia? A mastopexia é a cirurgia que reestrutura a sua mama. A mastopexia é a cirurgia que reestrutura a sua mama, tratando a flacidez, o excesso de pele e a queda das mamas. E associada a essa cirurgia, a gente insere próteses de silicone para dar um formato e um volume mais proporcional para o seu corpo. E associada a técnicas modernas, como sutiã interna e alça muscular, a gente consegue manter por mais tempo o formato e a sustentação das mamas. Vou falar só a última frase, solta aqui. Não pegou? Aumenta. E aí, continua? Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim. Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim.
diff --git a/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-whisper-large-v3-turbo.json b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-whisper-large-v3-turbo.json
new file mode 100644
index 0000000..9fcbb9f
--- /dev/null
+++ b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-whisper-large-v3-turbo.json
@@ -0,0 +1,182 @@
+[
+ {
+ "inicio": 2.23,
+ "fim": 8.11,
+ "texto": "Aquela mama com um formato mais estruturado, que valoriza o seu colo, que dá aquele ar de elegância.",
+ "confianca": null
+ },
+ {
+ "inicio": 8.67,
+ "fim": 10.79,
+ "texto": "Ah, isso é o desejo de muitas mulheres, né?",
+ "confianca": null
+ },
+ {
+ "inicio": 11.43,
+ "fim": 14.67,
+ "texto": "Com o tempo, nosso corpo muda e nossas mamas também mudam.",
+ "confianca": null
+ },
+ {
+ "inicio": 15.15,
+ "fim": 21.89,
+ "texto": "É a amamentação, perda de peso, efeito sanfona, tudo isso altera o formato e a sustentação das mamas.",
+ "confianca": null
+ },
+ {
+ "inicio": 22.41,
+ "fim": 30.43,
+ "texto": "Então a mastopexia é uma cirurgia que reestrutura a mama e trata a flacidez, o excesso de pele e a queda das mamas.",
+ "confianca": null
+ },
+ {
+ "inicio": 30.43,
+ "fim": 38.35,
+ "texto": "E associada a essa cirurgia, a gente faz a inserção de prótese de silicone, para dar aquele formato e volume mais proporcional para o seu tórax.",
+ "confianca": null
+ },
+ {
+ "inicio": 38.83,
+ "fim": 42.8,
+ "texto": "E tudo isso associado a medida...",
+ "confianca": null
+ },
+ {
+ "inicio": 42.8,
+ "fim": 47.43,
+ "texto": "Amor, eu tô intacta!",
+ "confianca": null
+ },
+ {
+ "inicio": 47.73,
+ "fim": 48.95,
+ "texto": "Mas eu vou ter que falar tudo de novo?",
+ "confianca": null
+ },
+ {
+ "inicio": 55.68,
+ "fim": 56.38,
+ "texto": "Tudo de novo?",
+ "confianca": null
+ },
+ {
+ "inicio": 64.98,
+ "fim": 69.38,
+ "texto": "Aquela mama com um formato mais estruturado, com o colo que...",
+ "confianca": null
+ },
+ {
+ "inicio": 70.38,
+ "fim": 71.64,
+ "texto": "Amor, é que eu...",
+ "confianca": null
+ },
+ {
+ "inicio": 71.64,
+ "fim": 73.78,
+ "texto": "Isso, só clica aí agora, na tela.",
+ "confianca": null
+ },
+ {
+ "inicio": 73.78,
+ "fim": 92.38,
+ "texto": "Aquela mama com um formato mais estruturado, que valoriza o seu colo, dá um ar de elegância.",
+ "confianca": null
+ },
+ {
+ "inicio": 92.96,
+ "fim": 94.88,
+ "texto": "Ah, isso é desejo de muitas mulheres, né?",
+ "confianca": null
+ },
+ {
+ "inicio": 95.48,
+ "fim": 98.74,
+ "texto": "Com o tempo, o corpo muda e as nossas mamas também mudam.",
+ "confianca": null
+ },
+ {
+ "inicio": 99.2,
+ "fim": 106.3,
+ "texto": "É a amamentação, perda de peso, efeito sanfona, tudo isso modifica o formato e a sustentação das mamas.",
+ "confianca": null
+ },
+ {
+ "inicio": 106.94,
+ "fim": 109.92,
+ "texto": "A mastopexia é a cirurgia que reestrutura a mama.",
+ "confianca": null
+ },
+ {
+ "inicio": 109.92,
+ "fim": 113.92,
+ "texto": "Ela trata a flacidez de pele, o excesso de pele...",
+ "confianca": null
+ },
+ {
+ "inicio": 113.92,
+ "fim": 115.82,
+ "texto": "Posso começar da mastopexia?",
+ "confianca": null
+ },
+ {
+ "inicio": 116.26,
+ "fim": 126.12,
+ "texto": "A mastopexia é a cirurgia que reestrutura a sua mama.",
+ "confianca": null
+ },
+ {
+ "inicio": 127.22,
+ "fim": 135.0,
+ "texto": "A mastopexia é a cirurgia que reestrutura a sua mama, tratando a flacidez, o excesso de pele e a queda das mamas.",
+ "confianca": null
+ },
+ {
+ "inicio": 135.32,
+ "fim": 141.8,
+ "texto": "E associada a essa cirurgia, a gente insere próteses de silicone para dar um formato e um volume mais proporcional para o seu corpo.",
+ "confianca": null
+ },
+ {
+ "inicio": 141.8,
+ "fim": 150.34,
+ "texto": "E associada a técnicas modernas, como sutiã interna e alça muscular, a gente consegue manter por mais tempo o formato e a sustentação das mamas.",
+ "confianca": null
+ },
+ {
+ "inicio": 151.14,
+ "fim": 153.0,
+ "texto": "Vou falar só a última frase, solta aqui.",
+ "confianca": null
+ },
+ {
+ "inicio": 153.82,
+ "fim": 154.26,
+ "texto": "Não pegou?",
+ "confianca": null
+ },
+ {
+ "inicio": 159.91,
+ "fim": 160.47,
+ "texto": "Aumenta.",
+ "confianca": null
+ },
+ {
+ "inicio": 180.75,
+ "fim": 181.35,
+ "texto": "E aí, continua?",
+ "confianca": null
+ },
+ {
+ "inicio": 184.14,
+ "fim": 188.7,
+ "texto": "Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim.",
+ "confianca": null
+ },
+ {
+ "inicio": 189.9,
+ "fim": 194.64,
+ "texto": "Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim.",
+ "confianca": null
+ }
+]
\ No newline at end of file
diff --git a/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-whisper-large-v3-turbo.txt b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-whisper-large-v3-turbo.txt
new file mode 100644
index 0000000..0f4026c
--- /dev/null
+++ b/code/relatorios/audio/arquivos/08686ef1208583d4784e896461940963e2d2eba11a256ae3928a51b88f146bd1/transcricao-0E6A8290-whisper-large-v3-turbo.txt
@@ -0,0 +1 @@
+Aquela mama com um formato mais estruturado, que valoriza o seu colo, que dá aquele ar de elegância. Ah, isso é o desejo de muitas mulheres, né? Com o tempo, nosso corpo muda e nossas mamas também mudam. É a amamentação, perda de peso, efeito sanfona, tudo isso altera o formato e a sustentação das mamas. Então a mastopexia é uma cirurgia que reestrutura a mama e trata a flacidez, o excesso de pele e a queda das mamas. E associada a essa cirurgia, a gente faz a inserção de prótese de silicone, para dar aquele formato e volume mais proporcional para o seu tórax. E tudo isso associado a medida... Amor, eu tô intacta! Mas eu vou ter que falar tudo de novo? Tudo de novo? Aquela mama com um formato mais estruturado, com o colo que... Amor, é que eu... Isso, só clica aí agora, na tela. Aquela mama com um formato mais estruturado, que valoriza o seu colo, dá um ar de elegância. Ah, isso é desejo de muitas mulheres, né? Com o tempo, o corpo muda e as nossas mamas também mudam. É a amamentação, perda de peso, efeito sanfona, tudo isso modifica o formato e a sustentação das mamas. A mastopexia é a cirurgia que reestrutura a mama. Ela trata a flacidez de pele, o excesso de pele... Posso começar da mastopexia? A mastopexia é a cirurgia que reestrutura a sua mama. A mastopexia é a cirurgia que reestrutura a sua mama, tratando a flacidez, o excesso de pele e a queda das mamas. E associada a essa cirurgia, a gente insere próteses de silicone para dar um formato e um volume mais proporcional para o seu corpo. E associada a técnicas modernas, como sutiã interna e alça muscular, a gente consegue manter por mais tempo o formato e a sustentação das mamas. Vou falar só a última frase, solta aqui. Não pegou? Aumenta. E aí, continua? Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim. Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim.
diff --git a/code/relatorios/transcricao-timeline.md b/code/relatorios/transcricao-timeline.md
new file mode 100644
index 0000000..4d47acf
--- /dev/null
+++ b/code/relatorios/transcricao-timeline.md
@@ -0,0 +1,554 @@
+# Relatório de transcrição — VIdeo Laryssa — sem corte
+
+Duração: 141.475 s
+Clipes processados: 25
+
+## 000f4763 — 0.000s–38.272s
+Origem: 2.302s–40.574s
+
+Aquela mama com um formato mais estruturado, que valoriza o seu colo, que dá aquele ar de elegância. Ah, isso é o desejo de muitas mulheres, né? Com o tempo, nosso corpo muda e nossas mamas também mudam. É a amamentação, perda de peso, efeito sanfona, tudo isso altera o formato e a sustentação das mamas. Então a mastopexia é uma cirurgia que reestrutura a mama e trata a flacidez, o excesso de pele e a queda das mamas. E associada a essa cirurgia, a gente faz a inserção de prótese de silicone, para dar aquele formato e volume mais proporcional para o seu tórax. E tudo isso associado a medida...
+
+Emoções detectadas por segmento:
+- [0.000s–5.808s] hap (0.58)
+- [6.208s–8.448s] hap (0.97)
+- [8.728s–12.348s] ang (0.96)
+- [12.808s–19.588s] ang (0.79)
+- [19.968s–28.128s] ang (0.76)
+- [28.128s–36.028s] ang (0.67)
+- [36.288s–38.272s] ang (0.81)
+
+- [0.000s–0.448s] Aquela
+- [0.448s–0.688s] mama
+- [0.688s–0.908s] com
+- [0.908s–1.088s] um
+- [1.088s–1.488s] formato
+- [1.488s–1.688s] mais
+- [1.688s–2.488s] estruturado,
+- [2.648s–2.668s] que
+- [2.668s–3.328s] valoriza
+- [3.328s–3.468s] o
+- [3.468s–3.628s] seu
+- [3.628s–4.088s] colo,
+- [4.208s–4.208s] que
+- [4.208s–4.308s] dá
+- [4.308s–4.568s] aquele
+- [4.568s–4.868s] ar
+- [4.868s–5.008s] de
+- [5.008s–5.808s] elegância.
+- [6.208s–6.508s] Ah,
+- [6.508s–6.708s] isso
+- [6.708s–6.828s] é
+- [6.828s–6.948s] o
+- [6.948s–7.068s] desejo
+- [7.068s–7.208s] de
+- [7.208s–7.608s] muitas
+- [7.608s–8.168s] mulheres,
+- [8.368s–8.448s] né?
+- [8.728s–9.288s] Com
+- [9.288s–9.468s] o
+- [9.468s–9.748s] tempo,
+- [9.808s–10.028s] nosso
+- [10.028s–10.368s] corpo
+- [10.368s–10.788s] muda
+- [10.788s–10.868s] e
+- [10.868s–11.148s] nossas
+- [11.148s–11.508s] mamas
+- [11.508s–11.848s] também
+- [11.848s–12.348s] mudam.
+- [12.808s–12.948s] É
+- [12.948s–13.048s] a
+- [13.048s–13.708s] amamentação,
+- [14.008s–14.288s] perda
+- [14.288s–14.448s] de
+- [14.448s–14.788s] peso,
+- [15.048s–15.388s] efeito
+- [15.388s–16.108s] sanfona,
+- [16.388s–16.648s] tudo
+- [16.648s–16.928s] isso
+- [16.928s–17.468s] altera
+- [17.468s–17.588s] o
+- [17.588s–18.108s] formato
+- [18.108s–18.208s] e
+- [18.208s–18.268s] a
+- [18.268s–18.928s] sustentação
+- [18.928s–19.168s] das
+- [19.168s–19.588s] mamas.
+- [19.968s–20.308s] Então
+- [20.308s–20.468s] a
+- [20.468s–21.428s] mastopexia
+- [21.428s–21.648s] é
+- [21.648s–21.748s] uma
+- [21.748s–22.228s] cirurgia
+- [22.228s–22.408s] que
+- [22.408s–23.248s] reestrutura
+- [23.248s–23.408s] a
+- [23.408s–23.628s] mama
+- [23.628s–24.088s] e
+- [24.088s–24.448s] trata
+- [24.448s–24.648s] a
+- [24.648s–25.208s] flacidez,
+- [25.648s–25.708s] o
+- [25.708s–26.168s] excesso
+- [26.168s–26.288s] de
+- [26.288s–26.628s] pele
+- [26.628s–26.988s] e
+- [26.988s–27.148s] a
+- [27.148s–27.428s] queda
+- [27.428s–27.688s] das
+- [27.688s–28.128s] mamas.
+- [28.128s–28.548s] E
+- [28.548s–29.048s] associada
+- [29.048s–29.168s] a
+- [29.168s–29.228s] essa
+- [29.228s–29.688s] cirurgia,
+- [29.808s–29.808s] a
+- [29.808s–29.928s] gente
+- [29.928s–30.188s] faz
+- [30.188s–30.288s] a
+- [30.288s–30.848s] inserção
+- [30.848s–31.028s] de
+- [31.028s–31.408s] prótese
+- [31.408s–31.568s] de
+- [31.568s–32.028s] silicone,
+- [32.268s–32.508s] para
+- [32.508s–32.648s] dar
+- [32.648s–32.888s] aquele
+- [32.888s–33.548s] formato
+- [33.548s–33.688s] e
+- [33.688s–34.188s] volume
+- [34.188s–34.448s] mais
+- [34.448s–35.188s] proporcional
+- [35.188s–35.408s] para
+- [35.408s–35.408s] o
+- [35.408s–35.568s] seu
+- [35.568s–36.028s] tórax.
+- [36.288s–36.608s] E
+- [36.608s–36.788s] tudo
+- [36.788s–36.988s] isso
+- [36.988s–37.588s] associado
+- [37.588s–37.648s] a
+- [37.648s–38.272s] medida...
+
+## 000f4765 — 38.272s–42.409s
+Origem: 41.808s–45.946s
+
+Amor, eu tô intacta!
+
+Emoções detectadas por segmento:
+- [39.063s–40.323s] ang (0.67)
+
+- [39.063s–39.483s] Amor,
+- [39.563s–39.663s] eu
+- [39.663s–39.803s] tô
+- [39.803s–40.323s] intacta!
+
+## 000f4767 — 42.409s–44.344s
+Origem: 47.547s–49.483s
+
+Mas eu vou ter que falar tudo de novo?
+
+Emoções detectadas por segmento:
+- [42.492s–43.792s] hap (0.96)
+
+- [42.492s–42.732s] Mas
+- [42.732s–42.832s] eu
+- [42.832s–42.892s] vou
+- [42.892s–42.972s] ter
+- [42.972s–43.032s] que
+- [43.032s–43.172s] falar
+- [43.172s–43.372s] tudo
+- [43.372s–43.492s] de
+- [43.492s–43.792s] novo?
+
+## 000f4769 — 44.344s–46.346s
+Origem: 51.418s–53.420s
+
+_Sem fala detectada._
+
+## 000f476b — 46.346s–47.614s
+Origem: 55.622s–56.890s
+
+Tudo de novo?
+
+Emoções detectadas por segmento:
+- [46.346s–47.104s] neu (0.37)
+
+- [46.346s–46.644s] Tudo
+- [46.644s–46.804s] de
+- [46.804s–47.104s] novo?
+
+## 000f476d — 47.614s–49.383s
+Origem: 59.059s–60.827s
+
+_Sem fala detectada._
+
+## 000f476f — 49.383s–53.453s
+Origem: 64.932s–69.002s
+
+Aquela mama com um formato mais estruturado, com o colo que...
+
+Emoções detectadas por segmento:
+- [49.391s–53.453s] ang (0.96)
+
+- [49.391s–49.871s] Aquela
+- [49.871s–50.131s] mama
+- [50.131s–50.371s] com
+- [50.371s–50.531s] um
+- [50.531s–50.951s] formato
+- [50.951s–51.171s] mais
+- [51.171s–52.151s] estruturado,
+- [52.331s–52.571s] com
+- [52.571s–52.751s] o
+- [52.751s–53.031s] colo
+- [53.031s–53.453s] que...
+
+## 000f4771 — 53.453s–57.224s
+Origem: 70.270s–74.041s
+
+Amor, é que eu... Isso, só clica aí agora, na tela.
+
+Emoções detectadas por segmento:
+- [53.453s–54.563s] hap (0.94)
+- [54.563s–56.883s] ang (0.80)
+
+- [53.453s–53.823s] Amor,
+- [53.943s–53.983s] é
+- [53.983s–54.063s] que
+- [54.063s–54.563s] eu...
+- [54.563s–55.103s] Isso,
+- [55.363s–55.963s] só
+- [55.963s–56.243s] clica
+- [56.243s–56.383s] aí
+- [56.383s–56.583s] agora,
+- [56.743s–56.803s] na
+- [56.803s–56.883s] tela.
+
+## 000f4773 — 57.224s–58.091s
+Origem: 81.448s–82.316s
+
+_Sem fala detectada._
+
+## 000f4775 — 58.091s–85.953s
+Origem: 86.186s–114.047s
+
+Aquela mama com um formato mais estruturado, que valoriza o seu colo, dá um ar de elegância. Ah, isso é desejo de muitas mulheres, né? Com o tempo, o corpo muda e as nossas mamas também mudam. É a amamentação, perda de peso, efeito sanfona, tudo isso modifica o formato e a sustentação das mamas. A mastopexia é a cirurgia que reestrutura a mama. Ela trata a flacidez de pele, o excesso de pele...
+
+Emoções detectadas por segmento:
+- [58.325s–64.285s] hap (0.49)
+- [64.605s–66.745s] hap (0.67)
+- [67.205s–70.605s] ang (0.98)
+- [71.045s–78.185s] ang (0.92)
+- [78.725s–81.805s] ang (0.97)
+- [81.805s–85.953s] ang (0.88)
+
+- [58.325s–58.825s] Aquela
+- [58.825s–59.045s] mama
+- [59.045s–59.285s] com
+- [59.285s–59.485s] um
+- [59.485s–59.945s] formato
+- [59.945s–60.145s] mais
+- [60.145s–61.005s] estruturado,
+- [61.165s–61.225s] que
+- [61.225s–61.845s] valoriza
+- [61.845s–61.985s] o
+- [61.985s–62.125s] seu
+- [62.125s–62.725s] colo,
+- [62.805s–62.885s] dá
+- [62.885s–63.045s] um
+- [63.045s–63.325s] ar
+- [63.325s–63.485s] de
+- [63.485s–64.285s] elegância.
+- [64.605s–64.985s] Ah,
+- [64.985s–65.145s] isso
+- [65.145s–65.225s] é
+- [65.225s–65.525s] desejo
+- [65.525s–65.665s] de
+- [65.665s–65.965s] muitas
+- [65.965s–66.485s] mulheres,
+- [66.645s–66.745s] né?
+- [67.205s–67.585s] Com
+- [67.585s–67.745s] o
+- [67.745s–68.005s] tempo,
+- [68.125s–68.225s] o
+- [68.225s–68.525s] corpo
+- [68.525s–69.005s] muda
+- [69.005s–69.145s] e
+- [69.145s–69.265s] as
+- [69.265s–69.525s] nossas
+- [69.525s–69.765s] mamas
+- [69.765s–70.105s] também
+- [70.105s–70.605s] mudam.
+- [71.045s–71.185s] É
+- [71.185s–71.265s] a
+- [71.265s–71.945s] amamentação,
+- [72.245s–72.605s] perda
+- [72.605s–72.765s] de
+- [72.765s–73.145s] peso,
+- [73.425s–73.805s] efeito
+- [73.805s–74.505s] sanfona,
+- [74.725s–75.105s] tudo
+- [75.105s–75.405s] isso
+- [75.405s–76.025s] modifica
+- [76.025s–76.185s] o
+- [76.185s–76.685s] formato
+- [76.685s–76.785s] e
+- [76.785s–76.865s] a
+- [76.865s–77.525s] sustentação
+- [77.525s–77.765s] das
+- [77.765s–78.185s] mamas.
+- [78.725s–78.945s] A
+- [78.945s–79.765s] mastopexia
+- [79.765s–79.905s] é
+- [79.905s–79.925s] a
+- [79.925s–80.305s] cirurgia
+- [80.305s–80.465s] que
+- [80.465s–81.385s] reestrutura
+- [81.385s–81.565s] a
+- [81.565s–81.805s] mama.
+- [81.805s–82.285s] Ela
+- [82.285s–82.625s] trata
+- [82.625s–82.805s] a
+- [82.805s–83.285s] flacidez
+- [83.285s–83.485s] de
+- [83.485s–83.905s] pele,
+- [84.225s–84.445s] o
+- [84.445s–85.025s] excesso
+- [85.025s–85.125s] de
+- [85.125s–85.953s] pele...
+
+## 000f4777 — 85.953s–87.487s
+Origem: 114.615s–116.149s
+
+Posso começar da mastopexia?
+
+Emoções detectadas por segmento:
+- [85.953s–87.138s] neu (0.90)
+
+- [85.953s–86.218s] Posso
+- [86.218s–86.438s] começar
+- [86.438s–86.578s] da
+- [86.578s–87.138s] mastopexia?
+
+## 000f4779 — 87.487s–91.158s
+Origem: 116.650s–120.320s
+
+_Sem fala detectada._
+
+## 000f477b — 91.158s–94.761s
+Origem: 122.856s–126.460s
+
+A mastopexia é a cirurgia que reestrutura a sua mama.
+
+Emoções detectadas por segmento:
+- [91.202s–94.402s] ang (0.83)
+
+- [91.202s–91.682s] A
+- [91.682s–92.162s] mastopexia
+- [92.162s–92.302s] é
+- [92.302s–92.382s] a
+- [92.382s–92.782s] cirurgia
+- [92.782s–92.942s] que
+- [92.942s–93.782s] reestrutura
+- [93.782s–93.962s] a
+- [93.962s–94.042s] sua
+- [94.042s–94.402s] mama.
+
+## 000f477d — 94.761s–117.851s
+Origem: 127.561s–150.650s
+
+A mastopexia é a cirurgia que reestrutura a sua mama, tratando a flacidez, o excesso de pele e a queda das mamas. E associada a essa cirurgia, a gente insere próteses de silicone para dar um formato e um volume mais proporcional para o seu corpo. E associada a técnicas modernas, como sutiã interna e alça muscular, a gente consegue manter por mais tempo o formato e a sustentação das mamas.
+
+Emoções detectadas por segmento:
+- [94.861s–102.181s] ang (0.92)
+- [102.361s–109.001s] ang (0.91)
+- [109.001s–117.521s] ang (0.78)
+
+- [94.861s–95.341s] A
+- [95.341s–95.821s] mastopexia
+- [95.821s–96.021s] é
+- [96.021s–96.021s] a
+- [96.021s–96.361s] cirurgia
+- [96.361s–96.541s] que
+- [96.541s–97.381s] reestrutura
+- [97.381s–97.541s] a
+- [97.541s–97.621s] sua
+- [97.621s–97.961s] mama,
+- [98.181s–98.721s] tratando
+- [98.721s–98.821s] a
+- [98.821s–99.401s] flacidez,
+- [99.741s–99.901s] o
+- [99.901s–100.341s] excesso
+- [100.341s–100.441s] de
+- [100.441s–100.821s] pele
+- [100.821s–101.121s] e
+- [101.121s–101.241s] a
+- [101.241s–101.541s] queda
+- [101.541s–101.781s] das
+- [101.781s–102.181s] mamas.
+- [102.361s–102.581s] E
+- [102.581s–103.221s] associada
+- [103.221s–103.341s] a
+- [103.341s–103.421s] essa
+- [103.421s–103.961s] cirurgia,
+- [104.081s–104.101s] a
+- [104.101s–104.241s] gente
+- [104.241s–104.681s] insere
+- [104.681s–105.221s] próteses
+- [105.221s–105.321s] de
+- [105.321s–105.741s] silicone
+- [105.741s–106.021s] para
+- [106.021s–106.161s] dar
+- [106.161s–106.301s] um
+- [106.301s–106.841s] formato
+- [106.841s–106.941s] e
+- [106.941s–107.041s] um
+- [107.041s–107.441s] volume
+- [107.441s–107.701s] mais
+- [107.701s–108.261s] proporcional
+- [108.261s–108.441s] para
+- [108.441s–108.481s] o
+- [108.481s–108.621s] seu
+- [108.621s–109.001s] corpo.
+- [109.001s–109.621s] E
+- [109.621s–110.081s] associada
+- [110.081s–110.241s] a
+- [110.241s–110.561s] técnicas
+- [110.561s–111.201s] modernas,
+- [111.321s–111.381s] como
+- [111.381s–111.821s] sutiã
+- [111.821s–112.281s] interna
+- [112.281s–112.361s] e
+- [112.361s–112.701s] alça
+- [112.701s–113.121s] muscular,
+- [113.601s–113.661s] a
+- [113.661s–113.781s] gente
+- [113.781s–114.161s] consegue
+- [114.161s–114.641s] manter
+- [114.641s–114.881s] por
+- [114.881s–115.161s] mais
+- [115.161s–115.521s] tempo
+- [115.521s–115.661s] o
+- [115.661s–116.121s] formato
+- [116.121s–116.221s] e
+- [116.221s–116.321s] a
+- [116.321s–116.861s] sustentação
+- [116.861s–117.061s] das
+- [117.061s–117.521s] mamas.
+
+## 000f477f — 117.851s–120.287s
+Origem: 150.951s–153.387s
+
+Vou falar só a última frase, solta aqui.
+
+Emoções detectadas por segmento:
+- [117.851s–119.860s] neu (0.98)
+
+- [117.851s–118.180s] Vou
+- [118.180s–118.380s] falar
+- [118.380s–118.520s] só
+- [118.520s–118.600s] a
+- [118.600s–118.760s] última
+- [118.760s–119.240s] frase,
+- [119.380s–119.640s] solta
+- [119.640s–119.860s] aqui.
+
+## 000f4781 — 120.287s–121.221s
+Origem: 153.720s–154.655s
+
+Não pegou?
+
+Emoções detectadas por segmento:
+- [120.287s–120.827s] neu (0.64)
+
+- [120.287s–120.487s] Não
+- [120.487s–120.827s] pegou?
+
+## 000f4783 — 121.221s–123.690s
+Origem: 156.356s–158.825s
+
+_Sem fala detectada._
+
+## 000f4785 — 123.690s–126.360s
+Origem: 159.726s–162.396s
+
+Aumenta.
+
+Emoções detectadas por segmento:
+- [123.814s–124.394s] hap (0.69)
+
+- [123.814s–124.394s] Aumenta.
+
+## 000f4787 — 126.360s–127.794s
+Origem: 172.305s–173.740s
+
+_Sem fala detectada._
+
+## 000f4789 — 127.794s–129.429s
+Origem: 174.741s–176.376s
+
+_Sem fala detectada._
+
+## 000f478b — 129.429s–131.164s
+Origem: 180.547s–182.282s
+
+E aí, continua?
+
+Emoções detectadas por segmento:
+- [129.429s–130.212s] hap (0.75)
+
+- [129.429s–129.712s] E
+- [129.712s–129.832s] aí,
+- [129.912s–130.212s] continua?
+
+## 000f478d — 131.164s–136.403s
+Origem: 183.784s–189.022s
+
+Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim.
+
+Emoções detectadas por segmento:
+- [131.481s–136.021s] hap (0.56)
+
+- [131.481s–132.061s] Então
+- [132.061s–132.201s] é
+- [132.201s–132.621s] devolver
+- [132.621s–133.061s] forma
+- [133.061s–133.181s] e
+- [133.181s–133.881s] sustentação
+- [133.881s–134.381s] de
+- [134.381s–134.441s] um
+- [134.441s–134.621s] jeito
+- [134.621s–134.761s] que
+- [134.761s–135.081s] pareça
+- [135.081s–135.221s] que
+- [135.221s–135.501s] sempre
+- [135.501s–135.741s] foi
+- [135.741s–136.021s] assim.
+
+## 000f478f — 136.403s–141.274s
+Origem: 190.123s–194.995s
+
+Então é devolver forma e sustentação de um jeito que pareça que sempre foi assim.
+
+Emoções detectadas por segmento:
+- [136.403s–140.900s] ang (0.77)
+
+- [136.403s–136.720s] Então
+- [136.720s–136.840s] é
+- [136.840s–137.360s] devolver
+- [137.360s–137.860s] forma
+- [137.860s–137.940s] e
+- [137.940s–138.680s] sustentação
+- [138.680s–138.880s] de
+- [138.880s–138.980s] um
+- [138.980s–139.220s] jeito
+- [139.220s–139.420s] que
+- [139.420s–139.860s] pareça
+- [139.860s–140.020s] que
+- [140.020s–140.340s] sempre
+- [140.340s–140.580s] foi
+- [140.580s–140.900s] assim.
+
+## 000f4791 — 141.274s–141.475s
+Origem: 196.463s–196.663s
+
+_Sem fala detectada._
diff --git a/code/src/tools/discovery.ts b/code/src/tools/discovery.ts
index 404f671..c2cf602 100755
--- a/code/src/tools/discovery.ts
+++ b/code/src/tools/discovery.ts
@@ -137,7 +137,14 @@ export function getDiscoveryTools(bridgeOptions: BridgeOptions) {
end: __ticksToSeconds(clip.end.ticks),
inPoint: __ticksToSeconds(clip.inPoint.ticks),
outPoint: __ticksToSeconds(clip.outPoint.ticks),
- duration: __ticksToSeconds(clip.duration.ticks)
+ duration: __ticksToSeconds(clip.duration.ticks),
+ sourceFile: clip.projectItem && clip.projectItem.getMediaPath
+ ? String(clip.projectItem.getMediaPath() || "")
+ : "",
+ sourceProjectItemId: clip.projectItem ? String(clip.projectItem.nodeId || "") : "",
+ sourceOffline: clip.projectItem && clip.projectItem.isOffline
+ ? !!clip.projectItem.isOffline()
+ : false
});
}
videoTracks.push({