diff --git a/code/cep-plugin/index.html b/code/cep-plugin/index.html
index 4c7ccd5..292403f 100755
--- a/code/cep-plugin/index.html
+++ b/code/cep-plugin/index.html
@@ -260,8 +260,22 @@
Escolha o que será lido no arquivo original. O Scanner usa os tempos da timeline, mas não exporta frames pelo Premiere.
+
+
+ Timeline e faixas
+
+ Abra a sequência desejada e detecte as faixas para selecioná-las.
+
-
+
Perfil padrão pronto.
Relatório pronto
diff --git a/code/cep-plugin/main.js b/code/cep-plugin/main.js
index a8dfc92..7afe55e 100755
--- a/code/cep-plugin/main.js
+++ b/code/cep-plugin/main.js
@@ -1119,6 +1119,7 @@ function scannerOutputRoot() { return path.join(os.homedir(), ".premiere-mcp", "
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) {
+ if (kind === "audio" && !selected.length) selected = tracks.map(function (track) { return track.index; });
var target = document.getElementById(kind === "video" ? "scannerVideoTracks" : "scannerAudioTracks");
target.innerHTML = "";
if (!tracks.length) { target.textContent = "Nenhuma faixa encontrada."; return; }
@@ -1131,13 +1132,34 @@ function scannerUpdateIntervalLabel() {
document.getElementById("scannerIntervalValue").textContent = texto;
}
+function scannerModoAtual() {
+ var selecionado = document.querySelector('input[name="scannerMode"]:checked');
+ return selecionado ? selecionado.value : "som";
+}
+
+function scannerAtualizarModo() {
+ var modo = scannerModoAtual();
+ var imagem = document.getElementById("scannerImageOptions");
+ var som = document.getElementById("scannerAudioOptions");
+ var ajuda = document.getElementById("scannerModeHelp");
+ var botao = document.getElementById("btnScannerProcess");
+ if (imagem) imagem.hidden = modo === "som";
+ if (som) som.hidden = modo === "imagem";
+ if (ajuda) ajuda.textContent = modo === "som"
+ ? "O Scanner vai ignorar completamente a análise de frames e processar apenas as faixas de áudio."
+ : modo === "imagem"
+ ? "O Scanner vai ignorar a transcrição e processar apenas frames, cenas e evidências visuais."
+ : "O Scanner vai executar os dois fluxos e manter os resultados separados no relatório.";
+ if (botao) botao.textContent = modo === "som" ? "Processar áudio" : modo === "imagem" ? "Analisar imagem" : "Processar áudio + imagem";
+}
+
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), resolucao_maxima: Number(document.getElementById("scannerResolution").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() } };
+ return { versao: 1, modo_de_analise: scannerModoAtual(), intervalo_em_segundos: Number(document.getElementById("scannerInterval").value), resolucao_maxima: Number(document.getElementById("scannerResolution").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() {
@@ -1146,7 +1168,10 @@ function saveScannerProfile() {
function loadScannerProfile() {
var saved; try { saved = JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY) || "null"); } catch (_) { saved = null; }
- if (!saved) { scannerUpdateIntervalLabel(); return; }
+ if (!saved) { scannerUpdateIntervalLabel(); scannerAtualizarModo(); return; }
+ var modo = saved.modo_de_analise || "som";
+ var seletorDeModo = document.querySelector('input[name="scannerMode"][value="' + modo + '"]');
+ if (seletorDeModo) seletorDeModo.checked = true;
document.getElementById("scannerInterval").value = String(saved.intervalo_em_segundos || 1);
scannerUpdateIntervalLabel();
document.getElementById("scannerResolution").value = String(saved.resolucao_maxima != null ? saved.resolucao_maxima : 1080);
@@ -1155,6 +1180,7 @@ function loadScannerProfile() {
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(); }
+ scannerAtualizarModo();
}
function renderScannerTranscriptionOptions() {
@@ -1194,7 +1220,9 @@ function scannerDetectTracks() {
function scannerProcess() {
if (!scannerTimeline) { showToast("err", "Primeiro detecte as faixas da sequência."); return; }
var profile; try { profile=scannerProfileFromScreen(); } catch (error) { showToast("err", error.message); return; }
- if (!profile.faixas_de_video.length && !profile.faixas_de_audio.length) { showToast("err", "Selecione ao menos uma faixa."); return; }
+ if (profile.modo_de_analise === "som" && !profile.faixas_de_audio.length) { showToast("err", "Selecione ao menos uma faixa de áudio."); return; }
+ if (profile.modo_de_analise === "imagem" && !profile.faixas_de_video.length) { showToast("err", "Selecione ao menos uma faixa de vídeo."); return; }
+ if (profile.modo_de_analise === "ambos" && (!profile.faixas_de_video.length || !profile.faixas_de_audio.length)) { showToast("err", "Selecione ao menos uma faixa de vídeo e uma de áudio."); return; }
saveScannerProfile(); var outputRoot=scannerOutputRoot(); ensureDir(outputRoot);
var stamp=new Date().toISOString().replace(/[:.]/g,"-");
var input=path.join(outputRoot,"pedido-"+stamp+".json");
diff --git a/code/cep-plugin/styles.css b/code/cep-plugin/styles.css
index 5c2d41a..8fc7733 100755
--- a/code/cep-plugin/styles.css
+++ b/code/cep-plugin/styles.css
@@ -156,6 +156,15 @@ h1, h2, h3 { margin: 0; }
.scanner-track-list .checkbox-field { margin: 2px 0; padding: 5px; border: 0; background: none; }
.scanner-options-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-items: start; }
.scanner-options-grid .card { margin: 0 0 10px; }
+.scanner-mode-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
+.scanner-mode-option { display: flex; gap: 8px; align-items: flex-start; min-height: 54px; padding: 10px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-deep); cursor: pointer; }
+.scanner-mode-option:has(input:checked) { border-color: var(--accent); background: rgba(124, 77, 255, .12); }
+.scanner-mode-option input { margin-top: 2px; accent-color: var(--accent); }
+.scanner-mode-option strong, .scanner-mode-option small { display: block; }
+.scanner-mode-option strong { color: var(--text); font-size: 11px; }
+.scanner-mode-option small { margin-top: 4px; color: var(--text-muted); font-size: 10px; line-height: 1.3; }
+.scanner-track-discovery { margin-top: 10px; }
+.scanner-media-card[hidden] { display: none; }
.scanner-media-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-items: start; }
.scanner-media-card { margin: 0 0 10px; }
.scanner-media-card h3 { margin: 0; font-size: 12px; }
@@ -657,7 +666,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
.tab-button { font-size: 0; gap: 0; padding: 10px 2px; }
.tab-icon { font-size: 14px; }
.field-grid { grid-template-columns: minmax(0, 1fr); }
- .scanner-track-grid, .scanner-options-grid, .scanner-media-grid { grid-template-columns: minmax(0, 1fr); }
+ .scanner-track-grid, .scanner-options-grid, .scanner-media-grid, .scanner-mode-options { grid-template-columns: minmax(0, 1fr); }
.status-panel { grid-template-columns: 38px minmax(0, 1fr); }
.command-stat { grid-column: 2; padding: 8px 0 0; text-align: left; }
.command-stat strong, .command-stat span { display: inline; }
diff --git a/code/engine/executar_scanner.py b/code/engine/executar_scanner.py
index ffe902c..dcaead0 100644
--- a/code/engine/executar_scanner.py
+++ b/code/engine/executar_scanner.py
@@ -59,7 +59,9 @@ 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"
+ analisar_imagem = configuracao.modo_de_analise in {"imagem", "ambos"}
+ analisar_som = configuracao.modo_de_analise in {"som", "ambos"}
+ clipes = [clipe for faixa in timeline.faixas if analisar_imagem and 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
@@ -105,7 +107,8 @@ def executar(entrada: Path, saida: Path) -> Path:
"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]
+ if analisar_som and 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}
diff --git a/code/engine/scanner/configuracao_visual.py b/code/engine/scanner/configuracao_visual.py
index 74c114e..2f76233 100644
--- a/code/engine/scanner/configuracao_visual.py
+++ b/code/engine/scanner/configuracao_visual.py
@@ -38,6 +38,7 @@ class ConfiguracaoVisualDoScanner:
resolucao_maxima: int = 1080
faixas_de_video: tuple[int, ...] = ()
faixas_de_audio: tuple[int, ...] = ()
+ modo_de_analise: str = "ambos"
recursos_vision: frozenset[str] = RECURSOS_VISION
detectar_cenas: bool = True
analisar_continuidade: bool = True
@@ -56,6 +57,8 @@ class ConfiguracaoVisualDoScanner:
maximo_de_frames_interpretados: int | None = None
def __post_init__(self) -> None:
+ if self.modo_de_analise not in {"som", "imagem", "ambos"}:
+ raise ValueError("modo_de_analise deve ser som, imagem ou ambos.")
if not 0.04 <= self.intervalo_em_segundos <= 60:
raise ValueError("intervalo_em_segundos deve estar entre 0,04 e 60.")
if self.resolucao_maxima != 0 and not 240 <= self.resolucao_maxima <= 8640:
@@ -83,6 +86,7 @@ class ConfiguracaoVisualDoScanner:
resolucao_maxima=int(dados.get("resolucao_maxima", 1080)),
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", ())))),
+ modo_de_analise=str(dados.get("modo_de_analise", "ambos")),
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)),
@@ -103,6 +107,7 @@ class ConfiguracaoVisualDoScanner:
"resolucao_maxima": self.resolucao_maxima,
"faixas_de_video": list(self.faixas_de_video),
"faixas_de_audio": list(self.faixas_de_audio),
+ "modo_de_analise": self.modo_de_analise,
"recursos_vision": sorted(self.recursos_vision),
"detectar_cenas": self.detectar_cenas,
"analisar_continuidade": self.analisar_continuidade,