Files
João Henrique 2c11ba8dda feat: corrigido o KeyError 'totalTracks' no cliente MCP: extração
- corrigido o KeyError 'totalTracks' no cliente MCP: extração de structuredContent.data na resposta do servidor (SDK 2.0+)
- sincronizado cortes de vídeo e áudio usando razor_all_tracks em vez de split_clip por faixa, eliminando drift entre cortes
- atualizado hook para usar razor_all_tracks, isolando vídeo e áudio no mesmo instante
- adicionado simulador razor_all_tracks no dublê de teste

Resumo:
- 5 arquivos alterados
- 0 novos
- 5 modificados
- 0 removidos

 5 files changed, 264 insertions(+), 12 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - code/engine/editor/aplicador_de_plano_de_edicao.py
  - code/engine/editor/escrita/escrita_no_editor.py
  - code/engine/integracoes/visual/apple_vision_runner.swift
  - code/engine/testes/duplos_de_premiere.py
2026-09-10 17:31:10 -04:00

659 lines
31 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 carregarImagem(_ url: URL) throws -> CGImage {
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"])
}
return imagem
}
func executar<T: VNRequest>(_ request: T, em url: URL) throws -> [VNObservation] {
let imagem = try carregarImagem(url)
let handler = VNImageRequestHandler(cgImage: imagem, options: [:])
try handler.perform([request])
return request.results ?? []
}
/// Executa múltiplos requests em lote via um único handler, reutilizando a imagem e o CVPixelBuffer interno.
/// Retorna (resultados, erros_por_indice) para que o chamador possa decidir quais requests retryar.
func executarLote(_ requests: [VNRequest], em url: URL) -> ([[VNObservation]], [Int: Error]) {
guard let imagem = try? carregarImagem(url) else {
let erro = NSError(domain: "JhonnyAppleVision", code: 1,
userInfo: [NSLocalizedDescriptionKey: "ImageIO não conseguiu decodificar o frame"])
let todosErros = Dictionary(uniqueKeysWithValues: requests.indices.map { ($0, erro) })
return (Array(repeating: [], count: requests.count), todosErros)
}
let handler = VNImageRequestHandler(cgImage: imagem, options: [:])
do {
try handler.perform(requests)
} catch {
let todosErros = Dictionary(uniqueKeysWithValues: requests.indices.map { ($0, error) })
return (Array(repeating: [], count: requests.count), todosErros)
}
var resultados: [[VNObservation]] = []
var erros: [Int: Error] = [:]
for (indice, request) in requests.enumerated() {
if let resultadosReais = request.results, !resultadosReais.isEmpty {
resultados.append(resultadosReais)
} else {
resultados.append([])
erros[indice] = NSError(domain: "JhonnyAppleVision", code: 3,
userInfo: [NSLocalizedDescriptionKey: "\(type(of: request)) não produziu resultados"])
}
}
return (resultados, erros)
}
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))
}
/// Analisa brilho, contraste e exposição de um CGImage via histograma de luminosidade.
/// Não usa Vision — é puro CoreImage, custo ~10ms.
func analisarBrilho(_ imagem: CGImage) -> [String: JSONValue] {
let largura = imagem.width
let altura = imagem.height
let totalPixels = Double(largura * altura)
// Converte para grayscale lendo os bytes diretamente
guard let contexto = CGContext(data: nil, width: largura, height: altura,
bitsPerComponent: 8, bytesPerRow: largura,
space: CGColorSpaceCreateDeviceGray(),
bitmapInfo: CGImageAlphaInfo.none.rawValue),
let dados = contexto.data else {
return ["media": .numero(0), "contraste": .numero(0), "exposicao": .texto("indisponivel")]
}
contexto.draw(imagem, in: CGRect(x: 0, y: 0, width: largura, height: altura))
let bytes = dados.assumingMemoryBound(to: UInt8.self)
// Calcula média de luminosidade
var soma: Double = 0
var somaQuadrados: Double = 0
var histograma = [Int](repeating: 0, count: 256)
for i in 0..<(largura * altura) {
let pixel = Double(bytes[i])
soma += pixel
somaQuadrados += pixel * pixel
histograma[Int(pixel)] += 1
}
let media = soma / totalPixels / 255.0 // normalizado 0-1
let variancia = (somaQuadrados / totalPixels) - (soma / totalPixels) * (soma / totalPixels)
let contraste = sqrt(variancia) / 255.0 // desvio padrão normalizado
// Classifica exposição
let exposicao: String
if media < 0.25 {
exposicao = "subexposto"
} else if media < 0.40 {
exposicao = "levemente_subexposto"
} else if media <= 0.65 {
exposicao = "equilibrado"
} else if media <= 0.80 {
exposicao = "levemente_sobreexposto"
} else {
exposicao = "sobreexposto"
}
// Detecta clipping (pixels no limite máximo = informação perdida)
let pixelsBrancos = Double(histograma[255] + histograma[254] + histograma[253])
let pixelsPretos = Double(histograma[0] + histograma[1] + histograma[2])
let ratioClippingBranco = pixelsBrancos / totalPixels
let ratioClippingPreto = pixelsPretos / totalPixels
return [
"media_brilho": .numero(media),
"contraste": .numero(contraste),
"exposicao": .texto(exposicao),
"clipping_branco": .numero(ratioClippingBranco),
"clipping_preto": .numero(ratioClippingPreto),
]
}
func construirRequest(_ recurso: String) -> VNRequest? {
switch recurso {
case "faces":
return VNDetectFaceLandmarksRequest()
case "qualidade_facial":
return VNDetectFaceCaptureQualityRequest()
case "pessoas":
let r = VNDetectHumanRectanglesRequest()
r.upperBodyOnly = false
return r
case "pose":
return VNDetectHumanBodyPoseRequest()
case "maos":
let r = VNDetectHumanHandPoseRequest()
r.maximumHandCount = 4
return r
case "ocr":
let r = VNRecognizeTextRequest()
r.recognitionLevel = .accurate
r.recognitionLanguages = ["pt-BR", "en-US"]
return r
case "categorias":
return VNClassifyImageRequest()
case "estetica":
return VNCalculateImageAestheticsScoresRequest()
case "codigos":
return VNDetectBarcodesRequest()
case "horizonte":
return VNDetectHorizonRequest()
case "retangulos":
return VNDetectRectanglesRequest()
case "contornos":
return VNDetectContoursRequest()
case "segmentacao_de_pessoas":
let r = VNGeneratePersonSegmentationRequest()
r.qualityLevel = .balanced
return r
case "brilho":
return nil // processado separadamente via CoreImage, sem Vision
default:
return nil
}
}
func converterResultados(_ observacoes: [VNObservation], para recurso: String) -> [Evidencia] {
switch recurso {
case "faces":
return observacoes.compactMap { observacao -> Evidencia? in
guard let face = observacao as? VNFaceObservation else { return nil }
var valor: [String: JSONValue] = ["bounding_box": .objeto(caixa(face.boundingBox))]
// Orientação da cabeça (enquadramento)
valor["roll"] = .numero(Double(truncating: face.roll ?? 0))
valor["yaw"] = .numero(Double(truncating: face.yaw ?? 0))
valor["pitch"] = .numero(Double(truncating: face.pitch ?? 0))
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")
}
case "qualidade_facial":
return observacoes.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")
}
case "pessoas":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "pose":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "maos":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "ocr":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "categorias":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "estetica":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "codigos":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "horizonte":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "retangulos":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "contornos":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
case "segmentacao_de_pessoas":
return observacoes.compactMap { observacao -> Evidencia? 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")
}
default:
return []
}
}
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] = []
// 0) Processa recursos que não usam Vision (ex: brilho via CoreImage)
for recurso in entrada.recursos {
if recurso == "brilho" {
if let imagem = try? carregarImagem(url) {
let dados = analisarBrilho(imagem)
evidencias.append(Evidencia(tipo: "brilho", valor: dados, confianca: nil, modelo: "CoreImageHistogram"))
} else {
avisos.append("brilho: não foi possível carregar a imagem")
}
}
}
// 1) Constrói um request por recurso solicitado, preservando a ordem
var recursosSolicitados: [String] = []
var requests: [VNRequest] = []
for recurso in entrada.recursos {
if recurso == "brilho" { continue } // já processado acima
if let request = construirRequest(recurso) {
recursosSolicitados.append(recurso)
requests.append(request)
}
}
guard !requests.isEmpty else {
return Saida(evidencias: evidencias, avisos: avisos)
}
// 2) Executa tudo em lote com um único handler (reutiliza a imagem e o CVPixelBuffer)
let (resultados, erros) = executarLote(requests, em: url)
// 3) Processa os resultados que funcionaram
var falhasParaRetry: [(indice: Int, recurso: String, requestOriginal: VNRequest)] = []
for (indice, recurso) in recursosSolicitados.enumerated() {
let observacoes = resultados[indice]
if !observacoes.isEmpty {
evidencias.append(contentsOf: converterResultados(observacoes, para: recurso))
} else if erros[indice] != nil {
falhasParaRetry.append((indice, recurso, requests[indice]))
avisos.append("\(recurso) (lote): falha na análise")
}
}
// 4) Retry seletivo: tenta novamente com CPU-only para requests que falharam no lote
if !falhasParaRetry.isEmpty {
do {
let imagem = try carregarImagem(url)
let handler = VNImageRequestHandler(cgImage: imagem, options: [:])
for falha in falhasParaRetry {
falha.requestOriginal.usesCPUOnly = true
}
let requestsRetry = falhasParaRetry.map { $0.requestOriginal }
try handler.perform(requestsRetry)
// Remove avisos anteriores dos recursos que foram retryados
for falha in falhasParaRetry {
avisos.removeAll { $0.hasPrefix("\(falha.recurso) (lote):") }
}
for (i, falha) in falhasParaRetry.enumerated() {
if let observacoes = requestsRetry[i].results, !observacoes.isEmpty {
evidencias.append(contentsOf: converterResultados(observacoes, para: falha.recurso))
} else {
// Remove o aviso de lote e registra a falha final
avisos.removeAll { $0.hasPrefix("\(falha.recurso) (lote):") }
avisos.append("\(falha.recurso): falhou mesmo com CPU-only")
}
}
} catch {
// Se o retry com CPU-only também falhou como lote, mantém os avisos originais
avisos.append("retry_cpu_only: \(error.localizedDescription)")
}
}
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 featurePrintLote(_ urls: [URL]) -> [VNFeaturePrintObservation?] {
guard !urls.isEmpty else { return [] }
let imagem: CGImage
do { imagem = try carregarImagem(urls[0]) }
catch { return Array(repeating: nil, count: urls.count) }
let handler = VNImageRequestHandler(cgImage: imagem, options: [:])
let requests = urls.map { _ in VNGenerateImageFeaturePrintRequest() }
do {
try handler.perform(requests)
} catch {
return Array(repeating: nil, count: urls.count)
}
return requests.map { request in
request.results?.first as? VNFeaturePrintObservation
}
}
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] = []
// Para sequência, tenta usar feature print em lote para frames consecutivos
// Começa do frame 0 e processa pares (i, i+1)
var i = 0
while i < caminhos.count - 1 {
let urlAtual = URL(fileURLWithPath: caminhos[i])
let urlProximo = URL(fileURLWithPath: caminhos[i + 1])
do {
let fpAtual = try featurePrint(urlAtual)
let fpProximo = try featurePrint(urlProximo)
var distancia: Float = 0
try fpAtual.computeDistance(&distancia, to: fpProximo)
evidencias.append(Evidencia(tipo: "similaridade_visual", valor: [
"frame_inicial": .numero(Double(i)), "frame_final": .numero(Double(i + 1)),
"distancia_feature_print": .numero(Double(distancia)),
"possivel_mudanca_de_cena": .booleano(distancia > 12),
], confianca: nil, modelo: "VNGenerateImageFeaturePrintRequest"))
} catch {
avisos.append("similaridade_visual[\(i)]: \(error.localizedDescription)")
}
i += 1
}
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
let roll = valor.numero("roll").map { String(format: "%.1f", $0 * 180 / .pi) } ?? "?"
let yaw = valor.numero("yaw").map { String(format: "%.1f", $0 * 180 / .pi) } ?? "?"
return "rosto detectado (\(grupos) landmarks), inclinação=\(roll)° rotação=\(yaw)°"
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"
case "brilho":
guard let media = valor.numero("media_brilho") else { return "análise de brilho" }
let exp = valor.texto("exposicao") ?? "?"
let contraste = valor.numero("contraste") ?? 0
return "brilho \(String(format: "%.0f", media * 100))%, contraste \(String(format: "%.0f", contraste * 100))%, exposição: \(exp)"
default:
return evidencia.tipo
}
}
/// Classificação de ambiente extraída das evidências visuais.
/// Usado pelo Foundation Models para categorizar o cenário do frame.
struct ClassificacaoAmbiente: Encodable {
let ambiente: String
let confianca: Double
let evidencias_chave: [String]
}
/// Taxonomia de ambientes para classificação editorial.
/// Cada ambiente tem descrição de características visuais que o Foundation Models deve procurar.
enum TaxonomiaAmbientes {
static let instrucoes = """
Você identifica o ambiente de cenas de vídeo. Analise as evidências visuais fornecidas \
e classifique o ambiente segundo a taxonomia abaixo. Responda APENAS com JSON válido.
TAXONOMIA DE AMBIENTES:
- centro_cirurgico: pessoas com touca/avental/máscara/camisinha cirúrgica, cores branco/azul cirúrgico, \
mesa cirúrgica, luzes de sala de operação, monitores cardíacos, instrumentos cirúrgicos, campo estéril verde/azul.
- consultorio: mobiliário médico simples, mesa de exame, quadro na parede, equipamento clínico de escritório.
- hospital_ambiente: corredor hospitalar, leito, roupa de paciente, enfermeiros, soro, monitor.
- laboratorio: microscópio, tubos de ensaio, centroides, bancada com reagentes, jaleco branco.
- escritorio: mesa de trabalho, computador, cadeira ergonômica, estantes, iluminação artificial.
- sala_reuniao: mesa comprida, cadeiras ao redor, projetor/tela, quadro branco.
- externo_urbano: rua, edifícios, trânsito, calçada, céu aberto.
- externo_natural: árvores, gramado, água, montanha, praia, campo.
- studio: fundo neutro/escuro, iluminação profissional, equipamento de gravação.
- industria: maquinário, linha de produção, capacete, colete, ambiente fabril.
- domesticos: sala, cozinha, quarto, mobiliário residencial, iluminação caseira.
- academia: equipamentos de musculação, esteira, peso, pessoa se exercitando.
- outro: qualquer ambiente que não se encaixe nas categorias acima.
REGRAS:
1. Analise as evidências visuais (pessoas, roupas, objetos, texto OCR, categorias Vision).
2. Identifique os 2-3 indícios mais fortes para a classificação.
3. Atribua confiança de 0.0 a 1.0 baseada na força dos indícios.
4. Se houver texto OCR relevante (placas, placas de identificação), use como evidência forte.
5. Não invente informações não presentes nas evidências.
"""
static let promptClassificar = """
Classifique o ambiente deste frame de vídeo com base nas evidências visuais fornecidas.
Responda APENAS com JSON no formato: {"ambiente": "categoria", "confianca": 0.0-1.0, "evidencias_chave": ["evidência 1", "evidência 2"]}
"""
}
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: TaxonomiaAmbientes.instrucoes)
let resposta = try await sessao.respond(to: "\(TaxonomiaAmbientes.promptClassificar)\n\nEvidências visuais: \(fatos)")
return resposta.content
} catch { return nil }
}
/// Interpretação editorial legível (frase curta) — mantida para compatibilidade.
func interpretarFrase(_ 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)
}
}
}