criado repositório jhonny-editor no Gitea adicionado script admin/deploy.command com commit automático atualizado admin/DEV-NOTES.md com template limpo Resumo: - 32 arquivos alterados - 16 novos - 15 modificados - 0 removidos 16 files changed, 638 insertions(+), 61 deletions(-) Arquivos: - code/engine/skills/boas-praticas-oo.md -> .agents/skills/boas-praticas-oo/SKILL.md - AGENTS.md - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/engine/integracoes/visual/apple_vision.py - code/engine/integracoes/visual/apple_vision_runner.swift - code/engine/integracoes/visual/extrator_ffmpeg.py - code/engine/scanner/__init__.py - code/engine/scanner/configuracao_visual.py - code/engine/scanner/coordenacao/__init__.py - code/engine/scanner/retakes/adaptador_de_falas.py - code/engine/scanner/retakes/agrupador_de_takes.py - code/engine/scanner/retakes/detector_de_reinicios.py - code/engine/scanner/retakes/modelos_de_retakes.py - code/engine/scanner/visual.py - .jhonny/ - .retakes/ - CODING_STANDARDS.md - code/engine/executar_retakes.py - code/engine/persistencia/ - code/engine/scanner/retakes/__init__.py - code/engine/scanner/retakes/carregador_de_artefatos.py - code/engine/scanner/retakes/classificador_de_retakes.py - code/engine/scanner/retakes/coordenador_de_retakes.py - code/engine/scanner/retakes/deteccao_de_retakes.py - code/engine/scanner/retakes/gerador_de_evidencias.py - code/engine/scanner/retakes/repositorio_de_retakes.py - code/engine/testes/test_consultas_de_persistencia.py - code/engine/testes/test_deteccao_de_retakes.py - code/engine/testes/test_persistencia_sqlite.py - code/relatorios/audio/arquivos/92f97877259a86a8095b1eecafdefe357212a12247d04b78df60e0c0ae38b2ea/
382 lines
19 KiB
Swift
382 lines
19 KiB
Swift
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<T: VNRequest>(_ 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..<altura {
|
|
for x in 0..<largura where bytes[y * stride + x] > 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 }
|
|
}
|
|
|
|
/// Processa um pedido por linha de stdin e escreve uma resposta por linha em stdout.
|
|
/// Mantém o processo vivo entre quadros para não recarregar Vision/Foundation Models a
|
|
/// cada frame — o custo de inicialização é o que tornava a análise lenta em lote.
|
|
@main struct Principal {
|
|
static func main() async {
|
|
while let linha = readLine(strippingNewline: true) {
|
|
guard !linha.isEmpty, let entradaDados = linha.data(using: .utf8) else { continue }
|
|
let saida: Saida
|
|
do {
|
|
let entrada = try JSONDecoder().decode(Entrada.self, from: entradaDados)
|
|
var resultado = entrada.frames?.count ?? 0 > 1 ? analisarSequencia(entrada) : analisarVision(entrada)
|
|
if entrada.resumir, let resumo = await interpretar(resultado) {
|
|
resultado = Saida(evidencias: resultado.evidencias + [Evidencia(
|
|
tipo: "interpretacao_editorial", valor: ["texto": .texto(resumo)],
|
|
confianca: nil, modelo: "AppleFoundationModels")], avisos: resultado.avisos)
|
|
}
|
|
saida = resultado
|
|
} catch {
|
|
saida = Saida(evidencias: [], avisos: ["erro_fatal: \(error.localizedDescription)"])
|
|
}
|
|
guard let dados = try? JSONEncoder().encode(saida), let linhaDeSaida = String(data: dados, encoding: .utf8) else {
|
|
FileHandle.standardError.write(Data("Falha ao serializar resposta\n".utf8))
|
|
continue
|
|
}
|
|
print(linhaDeSaida)
|
|
fflush(stdout)
|
|
}
|
|
}
|
|
}
|