DEV Community

Cover image for Criando uma API OCR com FaaS na Azure – Parte 3: Processando OCR com Timer Trigger

Criando uma API OCR com FaaS na Azure – Parte 3: Processando OCR com Timer Trigger

Na primeira parte dessa série, a gente construiu uma Function App que recebe uma imagem por HTTP e salva no Azure Blob Storage de forma segura, usando identidade gerenciada. Na segunda parte, registramos no banco de dados (Azure Postgres SQL) os metadados dessas imagens, seguindo uma arquitetura em camadas com boas práticas de SOLID.

Agora vamos dar o próximo passo: processar de verdade o OCR das imagens pendentes usando uma Azure Function com Timer Trigger. Spoiler: vamos usar Azure Cognitive Services para extrair o texto, atualizar o banco com o resultado (e marcar se o conteúdo é uma receita médica), tudo isso aplicado com um modelo DDD leve e foco em separação de responsabilidades. Bora lá?


⏰ Por que usar um Timer Trigger no processamento OCR?

Você deve estar se perguntando: por que não processar o OCR assim que a imagem chega? Poderíamos acoplar o OCR no upload (ou usar um Blob Trigger para cada imagem), mas usar um Timer Trigger traz benefícios importantes:

  • Processamento assíncrono: O usuário não precisa esperar o OCR completar no momento do upload. A imagem é enviada e armazenada rapidamente como "pendente", e o processamento pesado acontece em segundo plano.
  • Resiliência e retries: Com um timer rodando periodicamente, se algum processamento falhar (por exemplo, serviço OCR indisponível), a próxima execução pode tentar de novo, ou podemos lidar com falhas marcando status de erro sem travar a experiência do usuário.
  • Lote e escalonamento: Podemos processar várias imagens em uma única execução do timer. Em momentos de pico, as Functions escalam e processam pendentes no próximo intervalo, dando flexibilidade para ajustar frequência conforme volume.
  • Separação de responsabilidades: Mantemos a Function HTTP focada apenas em receber e armazenar a imagem, enquanto a lógica de OCR fica isolada em outra Function. Isso deixa cada função menor, mais simples de entender e testar.

No final o Timer Trigger atua como um cron job na nossa arquitetura serverless, acionando periodicamente o código que verifica imagens pendentes e realiza o OCR. Essa abordagem melhora a experiência (upload rápido) e a robustez do sistema.

🔄 Visão geral do fluxo de processamento OCR

Antes de meter a mão no código, vamos entender o fluxo completo agora com o Timer Trigger no pedaço. Reaproveitando a arquitetura dos artigos anteriores, o processo fica assim:

  1. Upload & Cadastro: A imagem é enviada via HTTP (Function de upload), validada e armazenada no Blob Storage. No mesmo passo, inserimos um registro no banco de dados OcrImages com status "pending" (pendente) e outras infos (nome do arquivo, URL, etc.).
  2. Timer dispara: Periodicamente (por exemplo, a cada 5 minutos), a Function Timer Trigger é executada.
  3. Busca pendentes: A Function de timer consulta no banco quais imagens estão com status "pending".
  4. Processa cada imagem: Para cada imagem pendente, o código busca o arquivo no Blob Storage, envia para o serviço de OCR do Azure Cognitive Services e obtém o texto extraído.
  5. Analisa resultado: Com o texto em mãos, aplicamos uma regra simples para identificar se aquele texto parece ser de uma receita médica (por exemplo, procurando palavras-chave específicas).
  6. Atualiza banco: Marcamos o registro da imagem no banco como "processed" (processado), armazenando o texto extraído e indicando se é uma receita (IsPrescription = true/false). Em caso de falha no OCR, podemos marcar o status como "error" para evitar retries infinitos ou analisar depois.
  7. (Opcional) Próximos passos: Com os dados processados, poderíamos ter outra Function para expor o resultado via API ou notificar quem enviou a imagem. (Isso fica como gancho para possíveis melhorias futuras!)

Esse fluxo garante que nenhuma imagem fique esquecida: qualquer arquivo pendente será eventualmente processado pelo OCR na próxima execução do timer. Agora, vamos ver como implementar isso passo a passo em código, mantendo nosso projeto organizado.

🏗️ Atualização da arquitetura do projeto

Vamos adicionar alguns componentes novos à estrutura de pastas para suportar o processamento OCR e o Timer Trigger, seguindo a mesma pegada de organização limpa (DDD leve) dos artigos anteriores. Nossa arquitetura de projeto agora fica assim:

/ocr-function-app
├── application/
│   ├── UploadImageService.ts
│   └── ProcessImageService.ts         # Novo service de aplicação para processar OCR
├── domain/
│   ├── IImageStorage.ts
│   ├── IImageRepository.ts
│   └── IImageAnalyzer.ts             # Nova interface de domínio para serviço de OCR
├── infrastructure/
│   ├── AzureBlobStorage.ts
│   ├── OcrImageRepository.ts
│   └── AzureOcrService.ts            # Implementação do OCR usando Azure Cognitive Services
├── validations/
│   └── ContentTypeValidator.ts
├── HttpAddToBlob/                    # Function HTTP (upload da imagem)
│   ├── index.ts
│   └── function.json
├── TimerProcessOcr/                  # Function Timer (processamento OCR)
│   ├── index.ts
│   └── function.json
├── host.json
├── local.settings.json
└── package.json
Enter fullscreen mode Exit fullscreen mode

O que há de novo? Introduzimos a interface IImageAnalyzer e sua implementação AzureOcrService para encapsular a chamada ao serviço de OCR. Criamos também um ProcessImageService na camada de aplicação, que orquestra tudo: chama o repositório para pegar as imagens pendentes, usa o storage para obter o arquivo e o analyzer para extrair texto, então atualiza o repositório com o resultado.

🎨 Dica: Atualize também o esquema da tabela no banco de dados para armazenar o resultado do OCR. Podemos adicionar uma coluna TextContent em OcrImages para guardar o texto extraído (NVARCHAR(MAX), por exemplo). Assim, além de Status e IsPrescription, teremos onde salvar o texto da receita.

No arquivo function.json da nossa Timer Function, definimos um agendamento. Por exemplo, para rodar a cada 5 minutos:

{
  "schedule": "0 */5 * * * *",  // NCRONTAB expression: cada 5 minutos
  "useMonitor": true,
  "name": "myTimer",
  "type": "timerTrigger",
  "direction": "in"
}
Enter fullscreen mode Exit fullscreen mode

Com a casa arrumada, vamos implementar as peças novas começando pelo “contrato” de OCR.

📄 Definindo o contrato de análise de imagem (OCR)

Seguindo a abordagem de dependências invertidas, definimos uma interface de domínio para a análise de imagem. Essa interface abstrai o que o serviço de OCR precisa fazer, sem amarrar nosso código a um provider específico. No nosso caso, é basicamente “receber uma imagem e devolver o texto e flag de receita”.

export interface IImageAnalyzer {
  analyze(imageBuffer: Buffer): Promise<{ text: string; isPrescription: boolean }>;
}
Enter fullscreen mode Exit fullscreen mode

O que essa interface diz? Que qualquer implementação de IImageAnalyzer deve ter um método analyze que recebe os bytes de uma imagem (Buffer) e retorna um objeto com o texto extraído (text) e um booleano indicando se é uma receita médica (isPrescription).

Com isso, podemos implementar essa interface usando o serviço de OCR da Azure. Isso nos dá flexibilidade: se amanhã quisermos trocar o Azure Cognitive Services por outro OCR, é só criar outra classe que implemente IImageAnalyzer e tá feito, sem mexer no restante do código.

⚙️ Implementando o OCR com Azure Cognitive Services

Agora, vamos ao código que realmente faz o OCR usando a Azure. Para isso, vamos usar o Azure Cognitive Services Computer Vision (que oferece OCR tanto para texto impresso quanto manuscrito). Precisaremos de endpoint e chave de um recurso de Computer Vision (ou do serviço cognitivo específico de OCR). Armazene esses valores em variáveis de ambiente, por exemplo: COGNITIVE_ENDPOINT e COGNITIVE_KEY – nada de hardcode de segredos no código!

Vamos à implementação da classe AzureOcrService dentro de infrastructure/:

import { IImageAnalyzer } from "../domain/IImageAnalyzer";
import { ComputerVisionClient } from "@azure/cognitiveservices-computervision";
import { ApiKeyCredentials } from "@azure/ms-rest-js";

export class AzureOcrService implements IImageAnalyzer {
  private readonly client: ComputerVisionClient;

  constructor(endpointUrl: string, apiKey: string) {
    // Autentica no serviço de Computer Vision usando a chave de API
    const creds = new ApiKeyCredentials({ inHeader: { "Ocp-Apim-Subscription-Key": apiKey } });
    this.client = new ComputerVisionClient(creds, endpointUrl);
  }

  async analyze(imageBuffer: Buffer): Promise<{ text: string; isPrescription: boolean }> {
    // Chama a API de OCR da Azure passando a imagem (bytes) para reconhecimento
    const ocrResult = await this.client.recognizePrintedTextInStream(true, imageBuffer);

    // Concatena todas as palavras detectadas em uma única string
    let fullText = "";
    for (const region of ocrResult.regions || []) {
      for (const line of region.lines || []) {
        const lineText = line.words.map(w => w.text).join(" ");
        fullText += lineText + "\n";
      }
    }
    fullText = fullText.trim();

    // Aplica uma lógica simples para identificar se o texto parece uma receita médica
    // (por exemplo, contém palavras comuns em receitas, como "mg", "ml", ou "Receita")
    const prescriptionRegex = /receita|medicamento|mg|ml|dose/i;
    const isPrescription = prescriptionRegex.test(fullText);

    return { text: fullText, isPrescription };
  }
}
Enter fullscreen mode Exit fullscreen mode

Alguns pontos sobre o código acima:

  • Usamos o cliente da Azure Cognitive Services para Computer Vision. Criamos o client com as credenciais da API (usando a chave de subscrição). Poderíamos usar DefaultAzureCredential aqui se o serviço Cognitivo suportasse AAD, mas geralmente o caminho rápido é a chave mesmo (mantendo ela segura em configs ou Azure Key Vault).
  • O método recognizePrintedTextInStream tenta extrair texto da imagem. Passamos true como primeiro parâmetro para indicar que pode haver texto manuscrito (handwritten) também. Esse método retorna uma estrutura com regiões, linhas e palavras detectadas.
  • Iteramos pelo resultado para montar uma string completa fullText com todo o texto encontrado. Separamos linhas com \n só para manter a formatação aproximada.
  • Para identificar se é receita médica, fazemos uma verificação bem básica usando regex. Procuramos palavras-chave típicas. Claro, isso é uma simplificação: em produção, talvez envolveria um modelo ML ou validação mais elaborada. Mas para fins didáticos, essa heurística serve.
  • Retornamos o texto extraído e o booleano isPrescription dentro de um objeto, conforme definido na interface.

Com a parte de OCR pronta, vamos integrar isso no restante do sistema – precisamos buscar as imagens pendentes e atualizar o banco com esse resultado.

🔗 Atualizando o repositório para incluir resultados do OCR

Nossa interface IImageRepository até então tinha apenas o método save (para inserir um novo registro no upload). Vamos estendê-la para suportar duas operações essenciais para o processamento OCR: buscar imagens pendentes e atualizar o registro após processamento.

export interface IImageRepository {
  save(fileName: string, url: string): Promise<void>;
  findPending(): Promise<{ id: number; fileName: string; url: string }[]>;
  updateResult(id: number, text: string, isPrescription: boolean, status: string): Promise<void>;
}
Enter fullscreen mode Exit fullscreen mode

Implementando esses novos métodos na nossa classe OcrImageRepository (que usa Postgres via pg):

import { Pool } from "pg";
import { IImageRepository } from "../domain/IImageRepository";

export class OcrImageRepository implements IImageRepository {
  constructor(private readonly pool: Pool) {}

  async save(fileName: string, url: string): Promise<void> {
    // (mesmo código do artigo anterior para inserir nova imagem)
    await this.pool.query(
      `INSERT INTO OcrImages (FileName, Url) VALUES ($1, $2)`,
      [fileName, url]
    );
  }

  async findPending(): Promise<{ id: number; fileName: string; url: string }[]> {
    const result = await this.pool.query(`SELECT Id, FileName, Url FROM OcrImages WHERE Status = 'pending'`);
    return result.rows.map(row => ({
      id: row.id,
      fileName: row.filename,
      url: row.url
    }));
  }

  async updateResult(id: number, text: string, isPrescription: boolean, status: string): Promise<void> {
    await this.pool.query(
      `UPDATE OcrImages 
       SET TextContent = $1, IsPrescription = $2, Status = $3 
       WHERE Id = $4`,
      [text, isPrescription, status, id]
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Notou como mantemos as responsabilidades bem separadas? O repositório cuida da comunicação com o banco. Ele sabe como inserir, buscar e atualizar registros, mas não sabe nada sobre HTTP ou OCR em si. Assim, se amanhã mudarmos de Postgres para CosmosDB, por exemplo, só essa classe precisaria mudar (ou criaríamos outra implementando a interface).

Boas práticas aplicadas: Até aqui já reforçamos alguns princípios:

  • Single Responsibility: cada classe/função tem um único propósito (UploadImageService só lida com upload, AzureOcrService só faz OCR, OcrImageRepository só mexe com banco, etc.).
  • Dependency Injection: usamos interfaces (IImageStorage, IImageRepository, IImageAnalyzer) e injetamos implementações nas classes. Isso diminui o acoplamento e facilita testes (podemos mocar essas interfaces nos testes).
  • Configuração via ambiente: nenhuma credencial sensível está hardcoded. Blob Storage e Postgres via Managed Identity, chave do Cognitive via variável de ambiente segura.
  • Resiliência: pronto, com a coluna Status e o método findPending, conseguimos reprocessar ou identificar falhas facilmente. Se o OCR falhar, podemos marcar Status = 'error' e evitar ficar em loop infinito.

🤝 Orquestrando tudo no serviço de aplicação (ProcessImageService)

Temos as peças: storage (Blob), repositório (Postgres) e OCR (Cognitive). Falta orquestrar o trabalho de pegar as imagens pendentes, obter seu conteúdo e guardar o resultado. Esse é o papel do nosso ProcessImageService na camada de aplicação. Ele será similar ao UploadImageService que fizemos antes, mas focado no fluxo de processamento:

import { IImageStorage } from "../domain/IImageStorage";
import { IImageRepository } from "../domain/IImageRepository";
import { IImageAnalyzer } from "../domain/IImageAnalyzer";

export class ProcessImageService {
  constructor(
    private readonly imageStorage: IImageStorage,
    private readonly imageRepository: IImageRepository,
    private readonly imageAnalyzer: IImageAnalyzer
  ) {}

  async processPendingImages(): Promise<void> {
    // 1. Busca todas as imagens com status "pending" no banco
    const pendingImages = await this.imageRepository.findPending();
    if (pendingImages.length === 0) {
      return; // nada pendente, sai cedo
    }

    // 2. Loop através das imagens pendentes para processar uma por uma
    for (const image of pendingImages) {
      try {
        // Baixa o arquivo de imagem do Blob Storage (como Buffer)
        const buffer = await this.imageStorage.downloadImage(image.fileName);
        // Extrai o texto e verifica se é receita usando o serviço de OCR
        const { text, isPrescription } = await this.imageAnalyzer.analyze(buffer);
        // Atualiza o registro no banco de dados com o resultado (texto, flag e status "processed")
        await this.imageRepository.updateResult(image.id, text, isPrescription, "processed");
      } catch (err) {
        // Em caso de erro no processamento dessa imagem, registra o erro e atualiza status para "error"
        console.error(`Erro ao processar imagem ${image.fileName}:`, err);
        await this.imageRepository.updateResult(image.id, "", false, "error");
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Vamos dissecar o que acontece aqui:

  • Buscamos as imagens pendentes via imageRepository.findPending(). Se não houver nenhuma (lista vazia), a função retorna imediatamente – nada para fazer.
  • Para cada imagem pendente, fazemos um try/catch individualmente. Isso garante que um erro em uma imagem não interrompa o processamento das demais.
  • Dentro do try:

    • imageStorage.downloadImage(image.fileName): Precisamos obter os bytes da imagem do Blob Storage. Detalhe: No AzureBlobStorage podemos implementar um método downloadImage que usa o SDK do Azure para baixar o blob (por exemplo, blobClient.downloadToBuffer()). Aqui assumimos que nosso IImageStorage foi expandido para ter essa função. Novamente, abstraímos a fonte da imagem; poderia vir de outro lugar, mas para nós vem do Blob.
    • imageAnalyzer.analyze(buffer): Chama o serviço de OCR (AzureOcrService) para extrair o texto. Resultado: texto completo e indicação de receita.
    • imageRepository.updateResult(...): Atualiza o banco com o texto extraído, a flag de receita e define o status como "processed". Agora esse registro não será mais retornado em futuras consultas de pendentes.
  • No catch, capturamos qualquer exceção durante o download ou OCR daquela imagem:

    • Logamos o erro (aqui usei console.error só para ilustrar; numa Azure Function real, usaríamos context.log.error que veremos já já).
    • Atualizamos o status no banco para "error". Poderíamos também guardar uma mensagem de erro se quiséssemos (talvez adicionando uma coluna de erro), mas simplificamos marcando que falhou. Assim evitamos tentar de novo toda vez uma imagem que já deu problema e precisa de intervenção.

Beleza! O serviço de aplicação está completo. Note como ele coordena as interfaces de domínio sem saber detalhes de implementação (não sabe como o blob é baixado, nem como o OCR funciona, nem detalhes do banco). Essa separação deixa tudo mais testável e modular.

🚀 Implementando a Azure Function Timer Trigger

Chegou a hora de codar a Function que amarra tudo isso e realmente roda de tempos em tempos. A nossa Azure Function Timer (TimerProcessOcr/index.ts) será o ponto de entrada que instancia as classes concretas, chama o ProcessImageService e lida com logs/erros de nível mais alto.

import { AzureFunction, Context } from "@azure/functions";
import { DefaultAzureCredential } from "@azure/identity";
import { BlobServiceClient } from "@azure/storage-blob";
import { Pool } from "pg";

import { AzureBlobStorage } from "../infrastructure/AzureBlobStorage";
import { AzureOcrService } from "../infrastructure/AzureOcrService";
import { OcrImageRepository } from "../infrastructure/OcrImageRepository";
import { ProcessImageService } from "../application/ProcessImageService";

// Carregando variáveis de ambiente necessárias
const blobUrl = process.env.BLOB_STORAGE_URL!;
const containerName = process.env.BLOB_CONTAINER_NAME!;
const managedIdentityClientId = process.env.AZURE_STORAGEBLOB_CLIENTID!;
const cognitiveEndpoint = process.env.COGNITIVE_ENDPOINT!;
const cognitiveKey = process.env.COGNITIVE_KEY!;
const dbConfig = {
  host: process.env.PGHOST!,
  database: process.env.PGDATABASE!,
  user: process.env.PGUSER!,
  // A senha virá do token do Managed Identity, obtido adiante
  port: Number(process.env.PGPORT!) || 5432,
  ssl: { rejectUnauthorized: false }
};

const timerTrigger: AzureFunction = async function (context: Context, myTimer: any): Promise<void> {
  const timeStamp = new Date().toISOString();
  context.log(`Timer trigger executado em: ${timeStamp}`);

  try {
    // Instancia credenciais e clientes necessários
    const credential = new DefaultAzureCredential({ managedIdentityClientId });
    const blobStorage = new AzureBlobStorage(blobUrl, containerName, credential);

    // Pega token de acesso ao Postgres via Managed Identity
    const { token: dbPassword } = await credential.getToken("https://ossrdbms-aad.database.windows.net/.default");
    const pool = new Pool({ ...dbConfig, password: dbPassword });
    const repository = new OcrImageRepository(pool);

    // Instancia serviço de OCR com endpoint e chave
    const ocrService = new AzureOcrService(cognitiveEndpoint, cognitiveKey);

    // Cria o serviço de processamento e executa
    const processService = new ProcessImageService(blobStorage, repository, ocrService);
    await processService.processPendingImages();

    context.log("Processamento OCR concluído com sucesso.");
  } catch (err: any) {
    context.log.error("Erro no processamento OCR:", err);
    // Em caso de erro geral, podemos deixar explodir ou apenas logar.
    // Aqui só logamos; a próxima execução do Timer tentará novamente pendentes que não foram processadas.
  }
};

export default timerTrigger;
Enter fullscreen mode Exit fullscreen mode

Vamos destacar algumas coisas importantes nesta Function:

  • Agendamento: O Timer Trigger passa um parâmetro myTimer (que não usamos diretamente aqui) e chamamos essa função de acordo com o cron definido. Usamos context.log para registrar quando a função é executada (bom para monitorar se está rodando nos horários esperados).
  • Credenciais e serviços: Reutilizamos o DefaultAzureCredential com Managed Identity (mesma identidade do storage e do banco) para:

    • Acessar o Blob Storage (instanciando AzureBlobStorage igualzinho à função HTTP de upload).
    • Obter o token para o Postgres (mesma magia do Service Connector da parte 2) e criar o Pool do pg com as configs do banco.
  • AzureOcrService: Instanciamos passando a URL do endpoint do Cognitive Services e a chave. (Lembre de configurar essas env vars no Azure Function App!). Aqui não usamos MI porque o serviço cognitivo ainda requer chave – então armazenamos de forma segura.

  • ProcessImageService: Injetamos as três dependências concretas (storage, repository, ocr) e mandamos ver no processPendingImages(), que implementamos anteriormente.

  • Logs e tratamento de erro: se tudo der certo, logamos uma mensagem de sucesso. Se qualquer etapa dentro do try falhar (por exemplo, erro de conexão com o banco ou credencial), caímos no catch e logamos o erro. Optamos por não re-lançar a exceção, assim a Function não fica marcando falhas contínuas; na próxima invocação do timer, ela tenta de novo processar o que ficou. Dependendo do cenário, você poderia querer algum alerta ou retry imediato, mas um log de erro já permite monitorar via Application Insights.

Com isso, nossa função de Timer está pronta! Ela é bem enxuta, porque delegamos quase tudo para as classes de domínio/aplicação. Se comparar, a maior parte do código é inicializar dependências e tratar erros, a lógica de negócio mesmo (processPendingImages) está isolada e testável.

✅ Boas práticas aplicadas e considerações finais

Nesse artigo, consolidamos a arquitetura completa da nossa API OCR serverless, enfatizando vários pontos de boas práticas:

  • Separation of Concerns (SoC): Cada camada e componente tem responsabilidades claras. As Azure Functions (HTTP e Timer) são apenas entry points finos que repassam o trabalho pesado para serviços de aplicação.
  • DDD leve: Mesmo sem overengineering, organizamos o código em domain, application e infrastructure, o que nos deu uma estrutura limpa. Usamos interfaces para desacoplar implementações concretas (Blob Storage, Postgres, Cognitive Services) da lógica de negócio.
  • SOLID/SRP: Viu como adicionamos funcionalidades (persistência, OCR) sem bagunçar o código existente? Fomos estendendo o sistema em vez de modificar drasticamente o que já funcionava. Isso graças ao respeito ao SRP e à extensão via novas classes.
  • Tratamento de erros e logging: Implementamos validações (na parte 2 para uploads), usamos try/catch para evitar crash total no loop de OCR e adicionamos logs informativos. Em produção, esses logs integrados com o Application Insights ajudariam a monitorar quantas imagens foram processadas, tempos de execução e erros ocorridos.
  • Escalabilidade: A solução aproveita o modelo serverless de Azure Functions, podendo escalar instâncias conforme necessário. O uso de Timer Trigger com processamento por lotes dá flexibilidade para ajustar o intervalo conforme a demanda. Além disso, separando upload e OCR, podemos escalar cada função de forma independente (por exemplo, muitas requisições HTTP de upload não sobrecarregarão o OCR, e vice-versa).

Próximos passos e melhorias

Com o pipeline básico de OCR funcionando, podemos pensar em evoluções:

  • Endpoint de consulta: Criar uma nova Function HTTP que permita buscar o resultado do OCR (texto e flag) dado um ID ou nome de arquivo. Assim, clientes poderiam consultar o status/resultados da imagem enviada.
  • Notificações/Async Response: Integrar com Azure SignalR ou enviar um webhook quando o processamento de uma imagem for concluído, para notificar outro sistema ou UI em tempo real.
  • Monitoramento e métricas: Configurar alertas no Application Insights para erros no OCR ou tempos de processamento muito altos. Também métricas de quantas imagens são processadas por execução, backlog acumulado etc., para ajustar o cron ou escalar recursos.
  • Melhorias de OCR: Usar modelos mais avançados do Azure (como o Azure Form Recognizer ou serviços específicos para leitura de prescrições médicas, se houver) para extrair não só texto bruto, mas campos estruturados (medicamento, dosagem, etc.). Também investir em uma detecção de "receita médica" mais robusta, talvez treinando um modelo de classificação de texto.
  • Limpeza e otimização: Opcionalmente, mover imagens processadas para outro container ou deletá-las após X dias se não forem mais necessárias, economizando espaço. E claro, otimizar consultas (adicionar índices no banco para o campo Status, por exemplo, para buscar pendentes eficientemente).

Chegamos ao final da série "Criando uma API OCR com FaaS na Azure"! 🎉 Vimos desde a concepção da arquitetura, upload seguro com Managed Identity, persistência de metadados, até o processamento OCR assíncrono. Espero que essa jornada passo-a-passo tenha mostrado na prática como construir uma solução serverless robusta, segura e escalável.

Curtiu o conteúdo ou ficou com alguma dúvida? Deixa um comentário aqui embaixo para continuarmos a conversa. Obrigado por acompanhar até aqui – e bora codar! 🚀

Top comments (0)