Uma caixa de recibos não precisa virar uma noite de digitação. Neste guia, vamos construir um fluxo mínimo que recebe a foto de um recibo, envia a imagem para o HiVision e devolve um objeto estruturado com estabelecimento, data, itens, total, confiança e avisos. Depois, vamos processar um lote e gerar um CSV aberto na planilha.
A ideia importante não é apenas extrair texto. É preservar a dúvida. Se uma sombra esconde o valor total, o sistema não deve inventar um número plausível. Deve marcar o campo como incerto e mandar a foto para revisão.
Primeiro, faça um recibo funcionar
Crie uma pasta vazia, salve o arquivo abaixo como read-receipt.mjs e defina a chave no ambiente:
export HINOW_API_KEY="sua-chave"
node read-receipt.mjs "https://seu-endereco/recibo.jpg"
O exemplo usa uma URL pública para a imagem porque a rota de leitura recebe a foto dentro de uma parte image_url. Em um produto real, você pode hospedar temporariamente o arquivo ou usar o armazenamento que já faz parte do seu sistema.
const imageUrl = process.argv[2];
if (!imageUrl) {
throw new Error("Usage: node read-receipt.mjs IMAGE_URL");
}
const instruction = `
Leia este recibo e responda em JSON neste formato:
{
"estabelecimento": "string",
"data": "AAAA-MM-DD",
"total": 0.0,
"itens": [{ "descricao": "string", "valor": 0.0 }],
"confianca": "alta|media|baixa",
"avisos": ["string"]
}
Não complete informações que não estejam legíveis na imagem.
Se um campo estiver borrado, cortado ou ambíguo, use o melhor valor seguro apenas quando houver evidência clara;
caso contrário, registre a dúvida em avisos e reduza a confiança.
Responda em português do Brasil.
`;
const response = await fetch("https://api.hinow.ai/v1/chat/completions", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.HINOW_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "hinow/hivision",
messages: [{
role: "user",
content: [
{ type: "text", text: instruction },
{ type: "image_url", image_url: { url: imageUrl } }
]
}],
response_format: { type: "json_object" },
temperature: 0
})
});
if (!response.ok) {
throw new Error(await response.text());
}
const data = await response.json();
const receipt = JSON.parse(data.choices[0].message.content);
console.log(JSON.stringify(receipt, null, 2));
A instrução pede uma estrutura previsível, enquanto JSON.parse valida o conteúdo retornado antes de importar. response_format solicita uma resposta em JSON; ainda assim, valide o conteúdo antes de usar. Se a resposta não puder ser interpretada, o script falha em vez de inserir uma linha silenciosamente incompleta.
O esquema também faz uma distinção essencial. total é o dado que você quer usar; confianca e avisos dizem se é seguro usá-lo. Um recibo torto pode ser lido corretamente. Um número borrado pode parecer correto e estar errado. Esse segundo caso é mais perigoso porque passa pela planilha sem chamar atenção.
Depois, transforme várias fotos em CSV
Neste exemplo, o script recebe URLs na linha de comando, faz uma chamada por imagem e imprime um CSV com uma linha por item. O exemplo inclui no resultado recibos marcados como alta ou média e envia os de baixa para revisão. Isso é uma regra implementada pelo código, não uma garantia de que as linhas incluídas estejam corretas.
Salve como receipts-to-csv.mjs:
function escapeCsv(value) {
const text = String(value ?? "");
return `"${text.replaceAll('"', '""')}"`;
}
async function readReceipt(imageUrl) {
const instruction = `
Leia este recibo e responda em JSON neste formato:
{
"estabelecimento": "string",
"data": "AAAA-MM-DD",
"total": 0.0,
"itens": [{ "descricao": "string", "valor": 0.0 }],
"confianca": "alta|media|baixa",
"avisos": ["string"]
}
Não invente texto ou números ilegíveis. Coloque dúvidas em avisos e responda em português do Brasil.
`;
const response = await fetch("https://api.hinow.ai/v1/chat/completions", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.HINOW_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "hinow/hivision",
messages: [{
role: "user",
content: [
{ type: "text", text: instruction },
{ type: "image_url", image_url: { url: imageUrl } }
]
}],
response_format: { type: "json_object" },
temperature: 0
})
});
if (!response.ok) {
throw new Error(await response.text());
}
const data = await response.json();
const content = data.choices[0].message.content;
return JSON.parse(content);
}
const imageUrls = process.argv.slice(2);
if (imageUrls.length === 0) {
throw new Error("Usage: node receipts-to-csv.mjs IMAGE_URL...");
}
const rows = [];
const review = [];
for (const imageUrl of imageUrls) {
const receipt = await readReceipt(imageUrl);
if (receipt.confianca === "baixa") {
review.push({ imageUrl, avisos: receipt.avisos });
continue;
}
for (const item of receipt.itens ?? []) {
rows.push([
receipt.estabelecimento,
receipt.data,
item.descricao,
item.valor,
receipt.total,
receipt.confianca,
imageUrl
]);
}
}
console.log([
["estabelecimento", "data", "descricao", "valor_item", "total_recibo", "confianca", "imagem"],
...rows
].map(row => row.map(escapeCsv).join(",")).join("\n"));
if (review.length > 0) {
console.error("\nRevisão humana necessária:");
console.error(JSON.stringify(review, null, 2));
}
O for sequencial prioriza simplicidade; execuções paralelas podem reduzir o tempo, mas exigem controle de concorrência. Para um protótipo, acompanhar uma imagem por vez facilita descobrir se o problema está na URL, na leitura ou na conversão para CSV.
A conta que decide se vale automatizar
Considere o cenário hipotético usado aqui: digitar um recibo leva 2 minutos. 300 recibos somam 600 minutos, ou 10 horas. Se o fluxo mandar 15% das fotos para revisão e cada revisão levar 2 minutos, a parte humana cai para cerca de 1 hora e meia.
Esses valores são premissas de uma simulação, não uma medição universal. O tempo cresce com uma chamada por recibo e depende da latência do serviço, do tamanho das imagens e da forma como o lote é processado.
Valide antes de confiar no total
Não comece por 300 recibos. Separe um lote pequeno com fotos nítidas, tortas, amassadas, sombreadas e parcialmente ilegíveis. Some manualmente os totais e compare com a soma dos registros extraídos.
A validação precisa olhar especialmente para números. Um nome de estabelecimento errado costuma ser fácil de encontrar. Um total errado que parece normal pode contaminar relatórios, reembolsos e decisões financeiras. Confira também se a soma dos itens faz sentido diante do total do recibo, lembrando que impostos, descontos e taxas podem impedir uma igualdade simples.
Neste fluxo, o campo de confiança serve para direcionar a conferência; ele não substitui a validação. A amostragem manual continua necessária mesmo entre os registros marcados como alta ou média.
O próximo passo é enriquecer, não apagar a trilha
Depois que o CSV estiver correto, você pode adicionar uma segunda etapa para categorizar despesas, por exemplo separando alimentação, transporte e materiais. Faça isso sobre os dados já extraídos, não sobre a foto novamente. O modelo de linguagem recebe uma tarefa menor e o sistema mantém a imagem original ao lado de cada linha.
Também vale guardar o JSON bruto. O CSV é ótimo para abrir na planilha, mas perde parte da estrutura, principalmente a lista de avisos. Manter os dois formatos permite corrigir uma classificação depois sem fotografar tudo de novo.
O objetivo do fluxo é tornar os erros visíveis antes de incorporá-los aos dados oficiais. A caixa de recibos deixa de ser apenas um problema de transcrição quando cada linha carrega o valor lido, a indicação de confiança e o caminho para a imagem original.




