feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Integrações externas da engine."""
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>apple-speech-transcriber</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.genialsistemas.apple-speech-transcriber</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Apple Speech Transcriber</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
<string>O scanner precisa reconhecer a fala dos clipes da timeline para gerar a transcrição.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,3 @@
|
||||
from .provider_de_transcricao_apple import ProviderDeTranscricaoApple
|
||||
|
||||
__all__ = ["ProviderDeTranscricaoApple"]
|
||||
@@ -0,0 +1,44 @@
|
||||
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
|
||||
}
|
||||
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)
|
||||
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 {
|
||||
["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)!)
|
||||
}
|
||||
if error != nil || result?.isFinal == true { self.finalizar(nil, 0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func finalizar(_ mensagem: String?, _ codigo: Int32) {
|
||||
if let mensagem { fputs(mensagem + "\n", stderr) }
|
||||
NSApplication.shared.terminate(nil)
|
||||
exit(codigo)
|
||||
}
|
||||
}
|
||||
|
||||
let app = NSApplication.shared
|
||||
let delegate = AppDelegate()
|
||||
app.delegate = delegate
|
||||
app.setActivationPolicy(.accessory)
|
||||
app.run()
|
||||
@@ -0,0 +1,28 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from ...scanner.modelos import SegmentoDeTranscricao
|
||||
|
||||
|
||||
class ProviderDeTranscricaoApple:
|
||||
"""Provider nativo macOS Speech; o áudio não passa por API Groq."""
|
||||
|
||||
def __init__(self, executavel: str = "/private/tmp/AppleSpeechTranscriber.app/Contents/MacOS/apple-speech-transcriber", locale: str = "pt-BR",
|
||||
somente_no_dispositivo: bool = False) -> None:
|
||||
self.executavel = executavel
|
||||
self.locale = locale
|
||||
self.somente_no_dispositivo = somente_no_dispositivo
|
||||
|
||||
def transcrever(self, clipe: Any) -> list[SegmentoDeTranscricao]:
|
||||
arquivo = Path(clipe.arquivo)
|
||||
if not arquivo.is_file():
|
||||
raise FileNotFoundError(f"Arquivo de áudio não encontrado: {arquivo}")
|
||||
resultado = subprocess.run(
|
||||
[self.executavel, str(arquivo), self.locale, str(self.somente_no_dispositivo).lower()],
|
||||
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"))
|
||||
for s in dados.get("segmentos", [])]
|
||||
@@ -0,0 +1,3 @@
|
||||
from .provider_de_transcricao_groq import ProviderDeTranscricaoGroq
|
||||
|
||||
__all__ = ["ProviderDeTranscricaoGroq"]
|
||||
@@ -0,0 +1,66 @@
|
||||
import json
|
||||
import mimetypes
|
||||
import ssl
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from urllib import request
|
||||
|
||||
from ...scanner.modelos import SegmentoDeTranscricao
|
||||
|
||||
|
||||
class ProviderDeTranscricaoGroq:
|
||||
"""Provider Groq compatível com o contrato de transcrição do scanner."""
|
||||
|
||||
endpoint = "https://api.groq.com/openai/v1/audio/transcriptions"
|
||||
|
||||
def __init__(self, api_key: str, modelo: str = "whisper-large-v3-turbo",
|
||||
idioma: str = "pt", tempo_limite: float = 120.0,
|
||||
requisicao: Callable[..., Any] | None = None) -> None:
|
||||
if not api_key.strip():
|
||||
raise ValueError("A chave da API Groq é obrigatória.")
|
||||
self.api_key = api_key.strip()
|
||||
self.modelo = modelo
|
||||
self.idioma = idioma
|
||||
self.tempo_limite = tempo_limite
|
||||
self._requisicao = requisicao or request.urlopen
|
||||
try:
|
||||
import certifi
|
||||
self._contexto_ssl = ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
self._contexto_ssl = ssl.create_default_context()
|
||||
|
||||
def transcrever(self, clipe: Any) -> list[SegmentoDeTranscricao]:
|
||||
arquivo = getattr(clipe, "arquivo", None)
|
||||
if not arquivo:
|
||||
raise ValueError("O clipe não possui arquivo de origem para transcrição.")
|
||||
caminho = Path(arquivo)
|
||||
if not caminho.is_file():
|
||||
raise FileNotFoundError(f"Arquivo do clipe não encontrado: {caminho}")
|
||||
corpo, tipo = self._multipart(caminho)
|
||||
req = request.Request(self.endpoint, data=corpo, method="POST", headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": tipo,
|
||||
})
|
||||
argumentos = {"timeout": self.tempo_limite}
|
||||
if self._requisicao is request.urlopen:
|
||||
argumentos["context"] = self._contexto_ssl
|
||||
with self._requisicao(req, **argumentos) as resposta:
|
||||
dados = json.loads(resposta.read().decode("utf-8"))
|
||||
return [SegmentoDeTranscricao(float(item["start"]), float(item["end"]),
|
||||
str(item.get("text", "")).strip(),
|
||||
None)
|
||||
for item in dados.get("segments", [])]
|
||||
|
||||
def _multipart(self, caminho: Path) -> tuple[bytes, str]:
|
||||
limite = "----engine-groq-" + uuid.uuid4().hex
|
||||
mime = mimetypes.guess_type(caminho.name)[0] or "application/octet-stream"
|
||||
partes: list[bytes] = []
|
||||
campos = {"model": self.modelo, "language": self.idioma,
|
||||
"response_format": "verbose_json", "timestamp_granularities[]": "segment"}
|
||||
for nome, valor in campos.items():
|
||||
partes.append(f"--{limite}\r\nContent-Disposition: form-data; name=\"{nome}\"\r\n\r\n{valor}\r\n".encode())
|
||||
partes.append(f"--{limite}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{caminho.name}\"\r\nContent-Type: {mime}\r\n\r\n".encode())
|
||||
partes.append(caminho.read_bytes())
|
||||
partes.append(f"\r\n--{limite}--\r\n".encode())
|
||||
return b"".join(partes), f"multipart/form-data; boundary={limite}"
|
||||
@@ -0,0 +1,3 @@
|
||||
from .extracao_de_audio import ExtracaoDeAudio, ParteDeAudio
|
||||
|
||||
__all__ = ["ExtracaoDeAudio", "ParteDeAudio"]
|
||||
@@ -0,0 +1,63 @@
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParteDeAudio:
|
||||
caminho: Path
|
||||
inicio: float
|
||||
fim: float
|
||||
|
||||
|
||||
class ExtracaoDeAudio:
|
||||
"""Prepara mídia local para providers de transcrição."""
|
||||
|
||||
def __init__(self, ffmpeg: str = "ffmpeg", ffprobe: str = "ffprobe",
|
||||
duracao_da_parte: float = 600.0) -> None:
|
||||
self.ffmpeg = ffmpeg
|
||||
self.ffprobe = ffprobe
|
||||
self.duracao_da_parte = duracao_da_parte
|
||||
|
||||
def duracao(self, arquivo: str | Path) -> float:
|
||||
caminho = self._validar(arquivo)
|
||||
resultado = subprocess.run(
|
||||
[self.ffprobe, "-v", "error", "-show_entries", "format=duration",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1", str(caminho)],
|
||||
check=True, capture_output=True, text=True,
|
||||
)
|
||||
return float(resultado.stdout.strip())
|
||||
|
||||
def extrair(self, arquivo: str | Path, destino: str | Path) -> list[ParteDeAudio]:
|
||||
return self.extrair_intervalo(arquivo, destino, 0.0, None)
|
||||
|
||||
def extrair_intervalo(self, arquivo: str | Path, destino: str | Path,
|
||||
inicio: float = 0.0, fim: float | None = None) -> list[ParteDeAudio]:
|
||||
origem = self._validar(arquivo)
|
||||
pasta = Path(destino)
|
||||
pasta.mkdir(parents=True, exist_ok=True)
|
||||
duracao_total = self.duracao(origem)
|
||||
inicio = max(0.0, inicio)
|
||||
fim = min(fim if fim is not None else duracao_total, duracao_total)
|
||||
total = fim
|
||||
partes: list[ParteDeAudio] = []
|
||||
inicio = 0.0
|
||||
indice = 0
|
||||
while inicio < total:
|
||||
fim_da_parte = min(inicio + self.duracao_da_parte, total)
|
||||
saida = pasta / f"parte-{indice:04d}.wav"
|
||||
subprocess.run([
|
||||
self.ffmpeg, "-y", "-ss", str(inicio), "-i", str(origem),
|
||||
"-t", str(fim_da_parte - inicio), "-vn", "-ac", "1", "-ar", "16000",
|
||||
"-c:a", "pcm_s16le", str(saida),
|
||||
], check=True, capture_output=True, text=True)
|
||||
partes.append(ParteDeAudio(saida, inicio, fim_da_parte))
|
||||
inicio = fim_da_parte
|
||||
indice += 1
|
||||
return partes
|
||||
|
||||
def _validar(self, arquivo: str | Path) -> Path:
|
||||
caminho = Path(arquivo)
|
||||
if not caminho.is_file():
|
||||
raise FileNotFoundError(f"Arquivo de mídia não encontrado: {caminho}")
|
||||
return caminho
|
||||
@@ -0,0 +1,7 @@
|
||||
from .cliente_mcp import ClienteMCP, ClienteMCPPorStdio
|
||||
from .erros_mcp import ErroMCP, ErroDeConexaoMCP, ErroDeRespostaMCP, ErroDeFerramentaMCP
|
||||
|
||||
__all__ = ["ClienteMCP", "ClienteMCPPorStdio", "ErroMCP", "ErroDeConexaoMCP", "ErroDeRespostaMCP", "ErroDeFerramentaMCP"]
|
||||
from .sessao_mcp import SessaoMCP
|
||||
|
||||
__all__ = ["SessaoMCP"]
|
||||
@@ -0,0 +1,71 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import json
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
|
||||
class ClienteMCP(ABC):
|
||||
"""Define a comunicação técnica com o servidor MCP."""
|
||||
|
||||
@abstractmethod
|
||||
def conectar(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def desconectar(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def esta_conectado(self) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]: ...
|
||||
|
||||
|
||||
class ClienteMCPPorStdio(ClienteMCP):
|
||||
"""Cliente MCP para o servidor local do Premiere via stdio."""
|
||||
|
||||
def __init__(self, comando: list[str], tempo_limite: float = 30.0) -> None:
|
||||
self.comando = comando
|
||||
self.tempo_limite = tempo_limite
|
||||
self._processo: subprocess.Popen[str] | None = None
|
||||
self._proximo_id = 1
|
||||
|
||||
def conectar(self) -> None:
|
||||
if self._processo is not None:
|
||||
return
|
||||
self._processo = subprocess.Popen(
|
||||
self.comando,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
self._enviar("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "engine", "version": "0.1.0"}})
|
||||
|
||||
def desconectar(self) -> None:
|
||||
if self._processo is not None:
|
||||
self._processo.terminate()
|
||||
self._processo = None
|
||||
|
||||
def esta_conectado(self) -> bool:
|
||||
return self._processo is not None and self._processo.poll() is None
|
||||
|
||||
def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
if not self.esta_conectado():
|
||||
raise RuntimeError("Cliente MCP não está conectado.")
|
||||
resposta = self._enviar("tools/call", {"name": nome_da_ferramenta, "arguments": argumentos or {}})
|
||||
if resposta.get("error"):
|
||||
raise RuntimeError(f"Erro MCP: {resposta['error']}")
|
||||
return resposta.get("result", {})
|
||||
|
||||
def _enviar(self, metodo: str, parametros: dict[str, Any]) -> dict[str, Any]:
|
||||
if self._processo is None or self._processo.stdin is None or self._processo.stdout is None:
|
||||
raise RuntimeError("Processo MCP indisponível.")
|
||||
identificador = self._proximo_id
|
||||
self._proximo_id += 1
|
||||
self._processo.stdin.write(json.dumps({"jsonrpc": "2.0", "id": identificador, "method": metodo, "params": parametros}) + "\n")
|
||||
self._processo.stdin.flush()
|
||||
linha = self._processo.stdout.readline()
|
||||
if not linha:
|
||||
raise RuntimeError("O MCP encerrou sem retornar resposta.")
|
||||
return json.loads(linha)
|
||||
@@ -0,0 +1,5 @@
|
||||
from .conversor_de_timeline import ConversorDeTimeline
|
||||
from .conversor_de_faixas import ConversorDeFaixas
|
||||
from .conversor_de_clipes import ConversorDeClipes
|
||||
|
||||
__all__ = ["ConversorDeTimeline", "ConversorDeFaixas", "ConversorDeClipes"]
|
||||
@@ -0,0 +1,26 @@
|
||||
from typing import Any
|
||||
|
||||
from ....dominio import Clipe, IntervaloDeTempo
|
||||
from ....scanner.modelos import ErroDeAnalise
|
||||
|
||||
|
||||
class ConversorDeClipes:
|
||||
def converter(self, dados: dict[str, Any], identificador_da_faixa: str, indice: int = 0) -> Clipe:
|
||||
inicio = dados.get("start", dados.get("timeline_start"))
|
||||
fim = dados.get("end", dados.get("timeline_end"))
|
||||
identificador = dados.get("nodeId", dados.get("id"))
|
||||
if identificador is None or inicio is None or fim is None:
|
||||
raise ErroDeAnalise("resposta_incompleta", "Clipe sem identificador ou intervalo obrigatório.",
|
||||
f"clipes[{indice}]", {"campos": list(dados)})
|
||||
try:
|
||||
intervalo = IntervaloDeTempo(float(inicio), float(fim))
|
||||
origem_inicio = dados.get("inPoint", dados.get("source_start"))
|
||||
origem_fim = dados.get("outPoint", dados.get("source_end"))
|
||||
origem = (IntervaloDeTempo(float(origem_inicio), float(origem_fim))
|
||||
if origem_inicio is not None and origem_fim is not None else None)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ErroDeAnalise("intervalo_invalido", str(exc), f"clipes[{indice}]") from exc
|
||||
return Clipe(str(identificador), str(dados.get("name", "")), intervalo, origem,
|
||||
dados.get("sourceFile"), identificador_da_faixa,
|
||||
bool(dados.get("offline", False)), dict(dados.get("metadata", {})))
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from typing import Any
|
||||
from ....dominio import Faixa
|
||||
from .conversor_de_clipes import ConversorDeClipes
|
||||
|
||||
|
||||
class ConversorDeFaixas:
|
||||
def __init__(self, conversor_de_clipes: ConversorDeClipes | None = None) -> None:
|
||||
self.conversor_de_clipes = conversor_de_clipes or ConversorDeClipes()
|
||||
|
||||
def converter(self, dados: dict[str, Any], tipo: str, indice: int) -> Faixa:
|
||||
identificador = str(dados.get("id", f"{tipo}_{indice}"))
|
||||
faixa = Faixa(identificador, str(dados.get("name", "")), tipo,
|
||||
int(dados.get("index", indice)))
|
||||
faixa.clipes = [self.conversor_de_clipes.converter(clipe, identificador, i)
|
||||
for i, clipe in enumerate(dados.get("clips", []))]
|
||||
return faixa
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from typing import Any
|
||||
|
||||
from ....dominio import Timeline
|
||||
from .conversor_de_faixas import ConversorDeFaixas
|
||||
|
||||
|
||||
class ConversorDeTimeline:
|
||||
"""Converte a resposta externa do MCP em objetos do domínio."""
|
||||
|
||||
def __init__(self, conversor_de_faixas: ConversorDeFaixas | None = None) -> None:
|
||||
self.conversor_de_faixas = conversor_de_faixas or ConversorDeFaixas()
|
||||
|
||||
def converter(self, dados_brutos: dict[str, Any]) -> Timeline:
|
||||
if not isinstance(dados_brutos, dict) or dados_brutos.get("id") is None:
|
||||
from ....scanner.modelos import ErroDeAnalise
|
||||
raise ErroDeAnalise("timeline_invalida", "A timeline precisa de um identificador.", "timeline.id")
|
||||
timeline = Timeline(
|
||||
identificador=str(dados_brutos["id"]),
|
||||
nome=str(dados_brutos.get("name", "")),
|
||||
duracao=dados_brutos.get("duration"),
|
||||
taxa_de_quadros=dados_brutos.get("frameRate"),
|
||||
largura=dados_brutos.get("frameSizeHorizontal"),
|
||||
altura=dados_brutos.get("frameSizeVertical"),
|
||||
)
|
||||
faixas_de_video = dados_brutos.get("videoTracks", dados_brutos.get("video_tracks", []))
|
||||
faixas_de_audio = dados_brutos.get("audioTracks", dados_brutos.get("audio_tracks", []))
|
||||
for tipo, faixas in (("video", faixas_de_video), ("audio", faixas_de_audio)):
|
||||
timeline.faixas.extend(self.conversor_de_faixas.converter(f, tipo, i)
|
||||
for i, f in enumerate(faixas))
|
||||
return timeline
|
||||
@@ -0,0 +1,14 @@
|
||||
class ErroMCP(Exception):
|
||||
"""Erro geral da integração com o MCP."""
|
||||
|
||||
|
||||
class ErroDeConexaoMCP(ErroMCP):
|
||||
"""Erro ao conectar ao MCP."""
|
||||
|
||||
|
||||
class ErroDeFerramentaMCP(ErroMCP):
|
||||
"""Erro ao executar uma ferramenta MCP."""
|
||||
|
||||
|
||||
class ErroDeRespostaMCP(ErroMCP):
|
||||
"""Erro quando a resposta do MCP é inválida."""
|
||||
@@ -0,0 +1,3 @@
|
||||
from .acesso_ao_editor import AcessoAClipes, AcessoAoEditor, AcessoAFaixas, AcessoASequencia, AcessoATimeline
|
||||
|
||||
__all__ = ["AcessoAoEditor", "AcessoATimeline", "AcessoASequencia", "AcessoAFaixas", "AcessoAClipes"]
|
||||
@@ -0,0 +1,47 @@
|
||||
from typing import Any
|
||||
|
||||
from ..cliente_mcp import ClienteMCP
|
||||
|
||||
|
||||
class AcessoATimeline:
|
||||
"""Realiza consultas técnicas da timeline por meio do MCP."""
|
||||
|
||||
def __init__(self, cliente_mcp: ClienteMCP, nome_da_ferramenta: str = "get_active_sequence") -> None:
|
||||
self.cliente_mcp = cliente_mcp
|
||||
self.nome_da_ferramenta = nome_da_ferramenta
|
||||
|
||||
def obter_timeline_ativa(self) -> dict[str, Any]:
|
||||
resposta = self.cliente_mcp.chamar(self.nome_da_ferramenta, {})
|
||||
conteudo_estruturado = resposta.get("structuredContent", {})
|
||||
return conteudo_estruturado.get("data", resposta)
|
||||
|
||||
|
||||
class AcessoASequencia:
|
||||
"""Consulta somente os dados da sequência ativa."""
|
||||
|
||||
def __init__(self, acesso_a_timeline: AcessoATimeline) -> None:
|
||||
self.acesso_a_timeline = acesso_a_timeline
|
||||
|
||||
def obter_sequencia_ativa(self) -> dict[str, Any]:
|
||||
return self.acesso_a_timeline.obter_timeline_ativa()
|
||||
|
||||
|
||||
class AcessoAFaixas:
|
||||
def obter_faixas(self, sequencia: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return list(sequencia.get("videoTracks", sequencia.get("video_tracks", []))) + list(
|
||||
sequencia.get("audioTracks", sequencia.get("audio_tracks", [])))
|
||||
|
||||
|
||||
class AcessoAClipes:
|
||||
def obter_clipes(self, faixa: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return list(faixa.get("clips", []))
|
||||
|
||||
|
||||
class AcessoAoEditor:
|
||||
"""Expõe operações de leitura compreensíveis pelo domínio."""
|
||||
|
||||
def __init__(self, acesso_a_timeline: AcessoATimeline) -> None:
|
||||
self.acesso_a_timeline = acesso_a_timeline
|
||||
|
||||
def obter_timeline_ativa(self) -> dict[str, Any]:
|
||||
return self.acesso_a_timeline.obter_timeline_ativa()
|
||||
@@ -0,0 +1,17 @@
|
||||
from contextlib import AbstractContextManager
|
||||
from .cliente_mcp import ClienteMCP
|
||||
|
||||
|
||||
class SessaoMCP(AbstractContextManager["SessaoMCP"]):
|
||||
"""Garante o ciclo de vida de uma conexão MCP durante uma leitura."""
|
||||
|
||||
def __init__(self, cliente: ClienteMCP) -> None:
|
||||
self.cliente = cliente
|
||||
|
||||
def __enter__(self) -> "SessaoMCP":
|
||||
self.cliente.conectar()
|
||||
return self
|
||||
|
||||
def __exit__(self, tipo, valor, traceback) -> None:
|
||||
self.cliente.desconectar()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .provider_de_transcricao_local import ProviderDeTranscricaoLocal
|
||||
|
||||
__all__ = ["ProviderDeTranscricaoLocal"]
|
||||
@@ -0,0 +1,21 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ...scanner.modelos import SegmentoDeTranscricao
|
||||
|
||||
|
||||
class ProviderDeTranscricaoLocal:
|
||||
"""Provider local usando faster-whisper, sem transmitir mídia."""
|
||||
|
||||
def __init__(self, modelo: str, dispositivo: str = "cpu", tipo_de_calculo: str = "int8",
|
||||
idioma: str = "pt") -> None:
|
||||
from faster_whisper import WhisperModel
|
||||
self.modelo = WhisperModel(modelo, device=dispositivo, compute_type=tipo_de_calculo)
|
||||
self.idioma = idioma
|
||||
|
||||
def transcrever(self, clipe: Any) -> list[SegmentoDeTranscricao]:
|
||||
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]
|
||||
Reference in New Issue
Block a user