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(_ 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.. 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 } } 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) } } }