Não é fácil entender a percepção de um cliente sobre um trabalho, especialmente cara a cara. Nesse ponto, um feedback escrito é uma solução mais confortável para ambos os lados. Mas e quando não estamos falando de só um cliente? E quando temos dezenas? Centenas? Milhares?
Já é possível pedir para uma inteligência artificial analisar o sentimento geral baseado num feedback escrito. Essa análise de sentimento simples devolve uma nota geral: positivo, negativo, neutro ou misto. Mas nem sempre isso é suficiente para uma avaliação correta.
E se não estivermos tratando de clientes, mas de um professor e seus alunos?
O pipeline que vou montar aqui usa quatro serviços Oracle: imagens de cada um dos feedbacks ficarão no Object Storage, passarão por OCR com OCI Vision, e o sentimento de cada um deles será analisado com OCI Language e o gráfico volta para o bucket. Tudo isso sem que o professor precise instalar nada.
O problema
Um professor de história está no espectro autista. Estar no espectro não afeta sua capacidade de ensinar ou o domínio do conteúdo, mas muitas vezes ele sente que sua leitura de sinais sociais em sala de aula pode não ser realista: a postura corporal de uma turma está dizendo que estão tensos ou concentrados, o silêncio indica desmotivação ou foco, a energia do grupo é sinal de motivação ou descontrole?
Para alguém neurotípico, muitas vezes essas leituras não demandam esforço. Para alguém no espectro o custo cognitivo pode ser muito maior, o que pode acabar numa situação não esperada.
Muitas vezes, o resultado disso pode ser apenas incerteza. O professor termina o ano letivo sem saber como cada turma viveu aquele período com ele.
A abordagem
Para entender melhor o andamento de suas turmas, na última prova do ano, junto com as questões, cada aluno recebeu um envelope sem identificação. Dentro haviam três perguntas:
- O que você achou do nosso ano letivo em História?
- Você teve algum problema com o professor?
- Como você avalia o seu aprendizado?
Os alunos são orientados a depositar os envelopes numa caixa após o fim da prova, sem identificação nenhuma. Essa assimetria muda o que as pessoas estão dispostas a dizer.
O problema começa depois. Esse teste é feito em três turmas, trinta envelopes por turma, três respostas cada: são noventa textos livres. Extrair padrões e chegar a uma conclusão do sentimento de cada turma não é simples, ainda mais quando interpretar sinais sociais implícitos já exige mais esforço.
O pipeline
Envelope físico (foto ou scan)
↓
OCI Object Storage ← professor faz upload pelo console
↓
OCI Vision OCR → texto extraído
↓
OCI Language → sentimento por aspecto
↓
Gráfico gerado via Python no Cloud Shell
↓
OCI Object Storage ← resultado salvo no bucket
Quatro serviços Oracle, todos dentro da infraestrutura OCI.
Uma detalhe sobre OCR: a precisão depende da legibilidade do que os alunos escreveram. Como são todos manuscritos, num caso como esse, seria interessante orientar os alunos a usar letra de forma ou caprichar na letra.
Os dados de teste
Para testar e demonstrar os resultados, gerei 90 feedbacks diferentes por AI e separei em três turmas fictícias de trinta alunos, com respostas em português brasileiro simulando perfis distintos.
"1","Gostei. História Antiga foi o meu ponto favorito do ano.","Não tive problema. Ele respeita quando preciso de tempo para responder.","Foi muito positivo. O professor ajudou a estruturar o raciocínio histórico."
Na prática, nesse pipeline, as respostas viriam das respostas reais processadas pelo OCR. Cada CSV tem quatro colunas: id, resposta1, resposta2 e resposta3.
Pré-requisitos
- Bucket criado no Object Storage (aqui:
feedback-turmas) - CSVs ou imagens dos envelopes já no bucket
- Cloud Shell com
~/.oci/configconfigurado - Compartment OCID disponível
- Python com Matplotlib
pip install oci matplotlib numpy --quiet
O código
Setup
import oci
import csv
import io
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from collections import defaultdict
COMPARTMENT_ID = "ocid1.compartment.oc1..SEU_OCID_AQUI"
BUCKET_NAME = "feedback-turmas"
config = oci.config.from_file()
object_storage = oci.object_storage.ObjectStorageClient(config)
vision_client = oci.ai_vision.AIServiceVisionClient(
config,
service_endpoint="https://vision.aiservice.us-chicago-1.oci.oraclecloud.com"
)
ai_client = oci.ai_language.AIServiceLanguageClient(
config,
service_endpoint="https://language.aiservice.us-chicago-1.oci.oraclecloud.com"
)
NAMESPACE = object_storage.get_namespace().data
O NAMESPACE é do tenant, não da região. O mesmo valor serve para qualquer bucket da conta. Os endpoints usados aqui são de Chicago, onde Vision, Language e demais serviços de IA estão disponíveis. Para outras regiões, substituir us-chicago-1 pelo identificador correspondente.
Lendo os CSVs do Bucket
def ler_csv_do_bucket(object_name):
obj = object_storage.get_object(NAMESPACE, BUCKET_NAME, object_name)
content = obj.data.content.decode('utf-8')
return list(csv.DictReader(io.StringIO(content)))
Os arquivos ficam no bucket. O script puxa direto via SDK, sem download local.
OCR com OCI Vision
Com as imagens digitalizadas no bucket, o OCI Vision extrai os textos de cada uma delas e então são enviados ao OCI Language.
def extrair_texto_ocr(image_name):
response = vision_client.analyze_image(
analyze_image_details=oci.ai_vision.models.AnalyzeImageDetails(
features=[
oci.ai_vision.models.ImageTextDetectionFeature(
feature_type="TEXT_DETECTION"
)
],
image=oci.ai_vision.models.ObjectStorageImageDetails(
source="OBJECT_STORAGE",
namespace_name=NAMESPACE,
bucket_name=BUCKET_NAME,
object_name=image_name
)
)
)
if not response.data.image_text:
return ""
return '\n'.join([linha.text for linha in response.data.image_text.lines])
O Vision retorna coordenadas de cada palavra e scores de confiança por linha, mas para este caso, vamos usar apenas o texto extraído. O resultado segue para o Language como documento, no mesmo fluxo dos CSVs.
Análise de sentimento com OCI Language
Aqui um detalhe importante: durante os testes foi preciso omitir o parâmetro language_code. Ao omiti-lo, a detecção automática reconhece o idioma Pt-Br. Indicar "pt" retorna um erro 400.
O limite é de cem documentos por chamada, mas como temos trinta alunos com três respostas cada, nosso total chega em 90 documentos por turma, o que está dentro do limite da ferramenta.
def processar_turma(dados_csv):
documentos = []
for row in dados_csv:
for i, col in enumerate(['resposta1', 'resposta2', 'resposta3'], 1):
documentos.append(
oci.ai_language.models.TextDocument(
key=f"{row['id']}_p{i}",
text=row[col]
# Não passar language_code aqui.
# Passar "pt" explicitamente retorna 400.
)
)
response = ai_client.batch_detect_language_sentiments(
batch_detect_language_sentiments_details=oci.ai_language.models.BatchDetectLanguageSentimentsDetails(
documents=documentos,
compartment_id=COMPARTMENT_ID
)
)
return response.data.documents
def calcular_scores(documentos):
scores = {f'p{i}': defaultdict(int) for i in range(1, 4)}
for doc in documentos:
pergunta = doc.key.split('_')[1]
scores[pergunta][doc.document_sentiment] += 1
return scores
Gráfico e Resultado enviados ao Bucket
O gráfico é gerado por Python com a biblioteca matplotlib, rodando diretamente no Cloud Shell. O Cloud Shell é um ambiente Linux gerenciado pela Oracle, acessível pelo console OCI sem nenhuma instalação local.
O matplotlib é instalado via pip no início do script e não precisa de interface gráfica para funcionar: a renderização usa o backend Agg, que gera o arquivo em memória sem precisar de display. O resultado vai direto para o bucket via Object Storage SDK.

O gráfico produzido tem esse formato, com três painéis, um por pergunta. Cada barra representa o número de respostas classificadas pelo OCI Language em quatro categorias: positiva, negativa, neutra e mista. A categoria mista aparece quando o texto contém sentimentos conflitantes na mesma resposta.
def gerar_e_salvar_grafico(resultados):
turmas = ['turma_a', 'turma_b', 'turma_c']
perguntas_labels = ['Ano letivo', 'Relacao com professor', 'Aprendizado pessoal']
pkeys = ['p1', 'p2', 'p3']
cores = {
'Positive': '#2ecc71',
'Neutral': '#95a5a6',
'Negative': '#e74c3c',
'Mixed': '#f39c12'
}
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for ax, label, pkey in zip(axes, perguntas_labels, pkeys):
categorias = ['Positive', 'Neutral', 'Negative', 'Mixed']
x = np.arange(len(turmas))
w = 0.2
for i, cat in enumerate(categorias):
valores = [resultados[t][pkey].get(cat, 0) for t in turmas]
ax.bar(x + i * w, valores, w, label=cat, color=cores[cat])
ax.set_title(label)
ax.set_xticks(x + w * 1.5)
ax.set_xticklabels(['Turma A', 'Turma B', 'Turma C'])
ax.set_ylabel('Respostas')
ax.legend(fontsize=8)
plt.suptitle('Sentimento por turma: OCI Language', fontsize=13)
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
buf.seek(0)
object_storage.put_object(NAMESPACE, BUCKET_NAME, 'resultado_sentimento.png', buf)
print("Grafico salvo no bucket: resultado_sentimento.png")
O gráfico não passa pelo disco do Cloud Shell. Ele é gerado em memória e vai direto para o bucket.
Executando
turmas = ['turma_a', 'turma_b', 'turma_c']
resultados = {}
for turma in turmas:
print(f"Processando {turma}...")
dados = ler_csv_do_bucket(f"{turma}.csv")
docs = processar_turma(dados)
resultados[turma] = calcular_scores(docs)
gerar_e_salvar_grafico(resultados)
python3 analise_sentimento.py
Processando turma_a...
Processando turma_b...
Processando turma_c...
Grafico salvo no bucket: resultado_sentimento.png
O script fica salvo no $HOME do Cloud Shell e o diretório persiste entre sessões.
O que os dados mostram
Nas respostas da Turma A, as perguntas dois e três andam juntas: quem avaliou negativamente a relação com o professor avaliou negativamente o próprio aprendizado. Cerca de 80% das respostas seguem esse padrão. A pergunta sobre o ano letivo em geral tem mais dispersão, o que indica que os alunos localizam o problema em vez de generalizá-lo.
A Turma B não tem um direcionamento claro. Positivos e negativos distribuídos sem nenhum padrão aparente entre as três perguntas. O tipo de resultado mais difícil de trabalhar.
Já na Turma C há um claro direcionamento da turma. Além da maioria das respostas serem positivas, a relação com o professor e a avaliação do próprio aprendizado andam juntas, com uma consistência de quase 80% que a Turma A mostrou, mas no sentido oposto. Com o gráfico, tudo isso fica nítido sem precisar ler 90 respostas.
Um professor sem acesso a esses dados poderia ler a Turma C como uma turma quieta, mas com esses dados, o quadro é diferente. No nosso caso, a turma C era composta de alunos também neurodivergentes.
O que realmente importa
Esse artigo não é sobre os resultados, até porque eles foram gerados sinteticamente apenas para ser possível criar essa situação exemplo. O importa aqui é a execução de um pipeline simples para análise de sentimento, aplicável em diversas situações, com pouca ou nenhuma adaptação. Desde empresas que precisam analisar milhares de feedback recebidos diariamente, como também o usuário final, especialmente aqueles que podem ter problemas com leitura de sinais sociais, em nosso caso, pessoas no espectro autista.
Como eu.