Processamento de Mídia¶
Visão Geral¶
O sistema processa automaticamente mídias recebidas via WhatsApp, extraindo informações textuais para enriquecer o contexto das conversas com IA:
| Tipo de Mídia | Processamento | Resultado |
|---|---|---|
| Áudio/Voz | Transcrição via OpenAI Whisper | Texto transcrito |
| Imagem | Descrição via Claude Vision | Descrição textual da imagem |
| Vídeo | Não processado | Apenas armazena |
| Documento | Não processado | Apenas armazena |
| Sticker | Não processado | Apenas armazena |
Arquitetura¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ Recebimento de Mídia │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ WhatsApp ──▶ Evolution API ──▶ Webhook ──▶ ProcessMediaStep │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 1. Baixa mídia via Evolution API │ │
│ │ 2. Salva no Cloudflare R2 │ │
│ │ 3. Processa conforme tipo │ │
│ └─────────────┬──────────────────────┘ │
│ │ │
│ ┌──────────────────────┼──────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐│
│ │ Áudio │ │ Imagem │ │ Outros ││
│ │ │ │ │ │ ││
│ │ Whisper │ │Claude Vision │ │ N/A ││
│ │ (OpenAI) │ │ (Anthropic) │ │ ││
│ └──────┬───────┘ └──────┬───────┘ └──────────┘│
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ ExtractedText │ │
│ │ (Texto extraído salvo na entidade Message) │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Custo de Créditos¶
O processamento de mídia consome mais créditos do que respostas textuais, refletindo o custo maior das APIs de IA envolvidas:
| Operação | Créditos | Descrição |
|---|---|---|
| Texto (Chat) | 1 | Resposta textual padrão |
| Imagem (Vision) | 3 | Análise via Claude Vision |
| Áudio (Transcription) | 3 | Transcrição via OpenAI Whisper |
Custos configurados em CreditCostConfig (classe estática): Text = 1, Image = 3, Audio = 3.
Veja subscription-enforcement.md para detalhes sobre o fluxo de consumo de créditos.
Flags de Análise de Mídia por Instância¶
Cada instância WhatsApp possui flags que controlam se mídias são processadas:
| Flag | Default | Descrição |
|---|---|---|
AllowImageAnalysis |
true |
Habilita descrição de imagem via Claude Vision |
AllowAudioAnalysis |
true |
Habilita transcrição de áudio via Whisper |
Comportamento quando desabilitado: - A instância envia uma resposta padrão (canned response) informando que não processa aquele tipo de mídia - Nenhum crédito é consumido - A mídia é salva normalmente no R2 (apenas a extração de texto é ignorada) - Útil para instâncias que não precisam processar mídia, reduzindo custos
Configurável via endpoints de atualização da instância. Veja instances.md.
Transcrição de Áudio¶
Configuracao¶
A configuracao de transcricao e definida via classe estatica com campos const (hardcoded em tempo de compilacao), nao via appsettings.json:
TranscriptionConfig (Ciba.Infrastructure/Configuration/TranscriptionConfig.cs):
public static class TranscriptionConfig
{
public const string SectionName = "Transcription";
public const bool Enabled = true;
public const string Provider = "openai";
public const string Model = "whisper-1";
public const string DefaultLanguage = "pt";
public const int TimeoutSeconds = 60;
}
| Campo | Descricao | Valor |
|---|---|---|
Enabled |
Habilita/desabilita transcricao | true |
Provider |
Provider de transcricao | openai |
Model |
Modelo do Whisper | whisper-1 |
DefaultLanguage |
Idioma padrao (ISO 639-1) | pt |
TimeoutSeconds |
Timeout para audios longos | 60 |
Formatos Suportados¶
audio/ogg(WhatsApp voice messages)audio/mpeg(MP3)audio/mp4(M4A)audio/wavaudio/webm
Retry Policy¶
- 2 retries com backoff exponencial (4s, 8s)
- Retry em erros transientes e rate limit (429)
Fluxo¶
- Áudio recebido via webhook
- Mídia baixada do Evolution API (base64)
- Convertido para bytes e salvo no R2
- Enviado ao Whisper API para transcrição
- Texto transcrito salvo em
Message.ExtractedText - Claude recebe o texto transcrito no contexto
Descrição de Imagem¶
Configuração¶
A descrição de imagem usa o LLM configurado (Claude Vision). Não requer configuração adicional.
Formatos Suportados¶
image/jpegimage/pngimage/gifimage/webp
Fluxo¶
- Imagem recebida via webhook
- Mídia baixada do Evolution API (base64)
- Salva no R2
- Enviada ao Claude Vision com prompt de descrição
- Descrição textual salva em
Message.ExtractedText - Claude recebe a descrição no contexto
Prompt de Descrição¶
Sem caption:
"Descreva esta imagem de forma concisa em português (máximo 2-3 frases). Foque no conteúdo principal."
Com caption:
"O usuário enviou esta imagem com a legenda: \"{caption}\". Descreva o conteúdo da imagem de forma concisa em português (máximo 2-3 frases)."
Limites de Extração de Mídia¶
Extração por Batch¶
O sistema limita a quantidade de mídias que passam por extração de texto (OCR/transcrição) em cada batch de agregação para controlar custos de IA:
| Configuração | Valor Padrão | Descrição |
|---|---|---|
MaxMediaExtractionsPerBatch |
3 | Padrão de mídias com extração por batch (configurável por instância via MaxMediaPerMessage, 1-10) |
MaxAudioBytesForTranscription |
2 MB (2.097.152 bytes) | Tamanho máximo de áudio para transcrição Whisper |
Comportamento:
- As mídias mais recentes do batch recebem extração (OCR ou transcrição), limitadas por WhatsAppInstance.MaxMediaPerMessage (padrão 3, máximo 10)
- Mídias além do limite são salvas normalmente no R2, mas sem extração
- Áudios acima de 2 MB são salvos no R2 mas não transcritos (~2 MB de OGG/Opus corresponde a ~5 min de áudio)
- Ambos os limites sao definidos como constantes na classe estatica AppConfig
Configuracao¶
AppConfig (Ciba.Infrastructure/Configuration/AppConfig.cs) e uma classe estatica com campos const (hardcoded em tempo de compilacao), nao configuravel via appsettings.json:
public static class AppConfig
{
// ... outros campos ...
public const int MaxMediaExtractionsPerBatch = 3; // fallback quando instância não tem MaxMediaPerMessage
public const int MaxAudioBytesForTranscription = 2 * 1024 * 1024; // ~2 MB
}
Implementação¶
A lógica de limite fica no ProcessMediaStep da pipeline de agregação:
- Coleta todas as mensagens com mídia do batch
- Seleciona as N mais recentes (por ordem reversa) para extração
- Para cada mensagem, chama
MediaProcessingService.ProcessAsynccom o flagskipExtraction - Mensagens fora do limite recebem
skipExtraction = true(salva no R2 sem OCR/transcrição)
Processamento de Mídia no Playground¶
Visão Geral¶
O Playground do agente permite testar processamento de mídia (imagens e áudio) diretamente pelo portal administrativo, sem necessidade de enviar mensagens via WhatsApp.
Endpoint: describe-image¶
Realiza OCR/descrição de imagem via Claude Vision, retornando o texto extraído para uso no chat do playground.
Handler: PlaygroundDescribeImageHandler
Fluxo:
1. Frontend envia imagem em base64 + MIME type + caption opcional
2. Handler executa em paralelo:
- Descrição da imagem via IImageDescriptionService (Claude Vision)
- Upload da imagem no Cloudflare R2 via PlaygroundMediaHelper
3. Retorna a descrição textual e a URL da mídia no R2
4. Frontend usa o texto extraído como contexto antes de enviar ao LLM
PlaygroundMediaHelper¶
Helper compartilhado (PlaygroundMediaHelper) centraliza o upload de mídia do playground para o R2:
- Usado por
PlaygroundDescribeImageHandlerePlaygroundTranscribeHandler - Upload é fire-and-forget tolerante: se falhar, retorna
nulle o processamento continua - Armazena mídias no path de playground do tenant/agente via
IFileStorageService.SavePlaygroundMediaAsync
Fluxo no Frontend¶
┌─────────────────────────────────────────────────────┐
│ Playground (AgentPlaygroundTab) │
├─────────────────────────────────────────────────────┤
│ │
│ Usuário anexa imagem │
│ │ │
│ ▼ │
│ POST /describe-image (base64 + mimeType) │
│ │ │
│ ├──▶ Claude Vision (OCR/descrição) │
│ └──▶ R2 Upload (paralelo) │
│ │ │
│ ▼ │
│ Texto extraído retornado ao frontend │
│ │ │
│ ▼ │
│ Frontend inclui texto no contexto do chat │
│ POST /chat (mensagens com texto extraído) │
│ │
└─────────────────────────────────────────────────────┘
Modelo de Dados¶
Message¶
| Campo | Tipo | Descrição |
|---|---|---|
Content |
string? | Texto/caption original do usuário |
ExtractedText |
string? | Texto extraído (transcrição ou descrição) |
MediaUrl |
string? | URL da mídia no R2 |
MediaMimeType |
string? | MIME type do arquivo |
MediaFileName |
string? | Nome do arquivo |
Type |
MessageType | Text, Image, Audio, etc. |
Comportamento por Tipo¶
| Tipo | Content | ExtractedText |
|---|---|---|
| Texto | Texto do usuário | null |
| Imagem sem caption | "" ou null |
Descrição da imagem |
| Imagem com caption | Caption do usuário | Descrição da imagem |
| Áudio | "" ou null |
Texto transcrito |
| Vídeo | Caption (se houver) | null |
| Documento | Caption (se houver) | null |
Contexto para o LLM¶
O metodo ExtractMessageContent (em AiResponseService) combina Content e ExtractedText para montar o contexto enviado ao LLM:
private static string ExtractMessageContent(Message message)
{
// Mensagens de midia do assistant (anexos enviados) -- nota sintetica
if (message.Role == MessageRole.Assistant &&
message.Type != MessageType.Text &&
!string.IsNullOrEmpty(message.MediaFileName))
{
var label = message.Type switch { ... };
return $"[Enviei {label}: {message.MediaFileName}]";
}
// Mensagens de midia do usuario
if (message.Role == MessageRole.User && message.Type != MessageType.Text)
{
// Com extracao -- adicionar rotulo de origem
if (!string.IsNullOrEmpty(message.ExtractedText))
{
var mediaType = message.Type switch { ... };
return MessageContentFormatter.Format(message.Content, message.ExtractedText, mediaType);
}
// Sem extracao -- nota minima
...
}
return message.Content ?? string.Empty;
}
Exemplo - Imagem com caption:
O que é isso?
Uma foto de uma pizza margherita em uma caixa de papelão branca. A pizza tem molho de tomate, mozzarella derretida e folhas de manjericão fresco.
Arquivos Relacionados¶
| Arquivo | Descricao |
|---|---|
Ciba.Infrastructure/Services/Transcription/IAudioTranscriptionService.cs |
Interface de transcricao |
Ciba.Infrastructure/Services/Transcription/WhisperTranscriptionService.cs |
Implementacao Whisper |
Ciba.Infrastructure/Configuration/TranscriptionConfig.cs |
Configuracao (classe estatica com constantes) |
Ciba.Infrastructure/Services/Transcription/TranscriptionResult.cs |
DTO de resultado |
Ciba.Infrastructure/Services/Transcription/NoOpTranscriptionService.cs |
Implementacao desabilitada |
Ciba.Api/Features/Webhook/Pipelines/MessageAggregationPipeline/ProcessMediaStep.cs |
Processamento de midia no pipeline de agregacao |
Ciba.Api/Features/Agents/Playground/PlaygroundDescribeImageHandler.cs |
OCR de imagem no playground |
Ciba.Api/Features/Agents/Playground/PlaygroundMediaHelper.cs |
Helper de upload R2 para playground |
Ciba.Infrastructure/Configuration/AppConfig.cs |
Configuracao de limites (classe estatica com constantes) |
Ciba.Infrastructure/Configuration/CreditCostConfig.cs |
Custos de credito por tipo de midia (classe estatica) |
Tratamento de Erros¶
| Erro | Tratamento |
|---|---|
| Transcrição falha | Log warning, ExtractedText fica null |
| Descrição de imagem falha | Log warning, ExtractedText fica null |
| Download de mídia falha | Log warning, mídia não processada |
| Timeout | Retry automático (Polly) |
| Rate limit (429) | Retry com backoff exponencial |
Monitoramento¶
Logs¶
// Transcrição bem-sucedida
_logger.LogInformation(
"Audio transcribed for conversation {ConversationId}: {Duration}s, {TextLength} chars",
conversationId, durationSeconds, textLength);
// Descrição bem-sucedida
_logger.LogInformation(
"Image described for conversation {ConversationId}: {DescriptionLength} chars",
conversationId, descriptionLength);
// Falhas
_logger.LogWarning(ex, "Failed to transcribe audio for conversation {ConversationId}", conversationId);
_logger.LogWarning(ex, "Failed to describe image for conversation {ConversationId}", conversationId);
Métricas¶
- Tempo de transcrição por áudio
- Taxa de sucesso/falha
- Tamanho médio dos textos extraídos