Classificador fiscal NCM: top-3 + confiança calibrada + abstenção, offline na sua CPU
Project description
ncm-classificador
Classificador fiscal NCM: descrição livre de item de NF-e → top-3 códigos NCM com confiança calibrada e abstenção. Roda offline, na sua CPU — nenhum dado sai da máquina.
Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.
Instalação
Via pip (com suporte à API)
pip install ncm-classificador[api]
Sem a flag [api], instala apenas a CLI (sem dependências do servidor FastAPI).
Uso
A CLI resolve o pacote-modelo nesta ordem: --pacote DIR > env NCM_PACOTE > ~/.ncm/pacote.
1. Classificar item único
ncm classificar "PARAF SEXT ZINC M8X40 DIN933"
Saída real (tabela rich — código + confiança calibrada; a descrição textual do NCM
não faz parte do pacote exportado):
NCM para: PARAF SEXT ZINC M8X40 DIN933
┌───────────────┬─────────────────────┐
│ candidato NCM │ confiança calibrada │
├───────────────┼─────────────────────┤
│ 73181500 │ 98.3% │
│ 73181100 │ 0.8% │
│ 86079900 │ 0.1% │
└───────────────┴─────────────────────┘
Ferramenta de apoio · caráter orientativo · a responsabilidade pela
classificação é do contribuinte/contador.
Quando o item abstém, uma linha extra em destaque aparece antes do disclaimer:
ABSTEVE — confiança insuficiente; escale a um contador.
Com --json, a saída é uma linha NDJSON (mesmo shape usado pela API — ver adiante):
ncm classificar "PARAF SEXT ZINC M8X40 DIN933" --json
{"descricao": "PARAF SEXT ZINC M8X40 DIN933", "top3": [{"ncm8": "73181500", "confianca": 0.983291}, {"ncm8": "73181100", "confianca": 0.008137}, {"ncm8": "86079900", "confianca": 0.001092}], "abstem": false, "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.", "versao_pacote": "d-1"}
2. Classificar lote (arquivo CSV)
ncm lote itens.csv > resultado.csv
Não existe --saida: o resultado sai no stdout e é redirecionado com >. (--json
também é aceito e emite uma linha NDJSON por item, no mesmo formato do item 1.)
Entrada (itens.csv, coluna obrigatória descricao):
descricao
PARAF SEXT ZINC M8X40 DIN933
PORCAS M8 ISO 4032
Saída real (resultado.csv) — colunas ncm8_N/confianca_N (não ncm_N), campo
abstem (não abstencao), e rodapé com o disclaimer:
descricao,ncm8_1,confianca_1,ncm8_2,confianca_2,ncm8_3,confianca_3,abstem
PARAF SEXT ZINC M8X40 DIN933,73181500,0.983291,73181100,0.008137,86079900,0.001092,false
PORCAS M8 ISO 4032,73229090,0.352457,73181600,0.033459,73012000,0.017840,true
# Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.
Note que a segunda linha abstém (abstem=true, confiança do 1º candidato abaixo do
piso) — a saída real, não um exemplo forjado.
3. Consultar informações do modelo
ncm info
Saída real:
Pacote de inferência NCM
┌──────────────────────────┬────────────────────────────────────────┐
│ campo │ valor │
├──────────────────────────┼────────────────────────────────────────┤
│ versão do pacote │ d-1 │
│ modelo base │ neuralmind/bert-large-portuguese-cased │
│ corrida / data de export │ d / 2026-07-17 │
│ temperatura (T) │ 0.786 │
│ pisos de abstenção │ conf 0.55 · margem 0.1 │
│ classes (folhas NCM) │ 9748 │
│ integridade sha256 │ ok (conferida ao carregar) │
└──────────────────────────┴────────────────────────────────────────┘
Ferramenta de apoio · caráter orientativo · a responsabilidade pela
classificação é do contribuinte/contador.
ncm info mostra proveniência e configuração de calibração do pacote carregado — não
expõe métricas de acurácia/ECE do conjunto de validação (essas vivem nos artefatos de
avaliação do treino, não no pacote de produto).
Servidor API
Iniciar o servidor
ncm servir --host 0.0.0.0 --porta 8000
Variáveis de ambiente obrigatórias/opcionais
| Variável | Tipo | Padrão | Descrição |
|---|---|---|---|
NCM_API_KEYS |
string | (obrigatória) | Chave(s) de API separadas por , ex: chave1,chave2. Sem isto o servidor não sobe (fail-closed) |
NCM_RATE_LIMIT_RPM |
int | 120 | Limite de requisições por minuto por chave (token bucket em memória; zera no restart) |
NCM_PACOTE |
path | ~/.ncm/pacote |
Caminho até o diretório do pacote-modelo (no Docker, tipicamente /pacote, ver seção Docker) |
Endpoints
POST /v1/classificar
Classifica um item único.
Request:
curl -X POST http://localhost:8000/v1/classificar \
-H "X-API-Key: sua-chave-api" \
-H "Content-Type: application/json" \
-d '{"descricao": "PARAF SEXT ZINC M8X40 DIN933"}'
Response (200) — mesmo shape do --json da CLI (carga real, capturada rodando o
servidor local):
{
"descricao": "PARAF SEXT ZINC M8X40 DIN933",
"top3": [
{"ncm8": "73181500", "confianca": 0.983291},
{"ncm8": "73181100", "confianca": 0.008137},
{"ncm8": "86079900", "confianca": 0.001092}
],
"abstem": false,
"disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
"versao_pacote": "d-1"
}
descricao aceita 1 a 2000 caracteres. Corpo fora desse contrato (campo errado, string
vazia/longa demais) devolve 422 no formato problem+json (RFC 9457).
POST /v1/classificar-lote
Classifica múltiplos itens (máx. 200 por requisição). O corpo é {"descricoes": [...]}
— uma lista de strings, não uma lista de objetos {"descricao": ...}.
Request:
curl -X POST http://localhost:8000/v1/classificar-lote \
-H "X-API-Key: sua-chave-api" \
-H "Content-Type: application/json" \
-d '{
"descricoes": [
"PARAF SEXT ZINC M8X40 DIN933",
"PORCAS M8 ISO 4032"
]
}'
Response (200) — envelope com disclaimer/versao_pacote no topo, e cada resultado
com o mesmo shape do endpoint acima (também carregando disclaimer/versao_pacote):
{
"resultados": [
{
"descricao": "PARAF SEXT ZINC M8X40 DIN933",
"top3": [
{"ncm8": "73181500", "confianca": 0.983291},
{"ncm8": "73181100", "confianca": 0.008137},
{"ncm8": "86079900", "confianca": 0.001092}
],
"abstem": false,
"disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
"versao_pacote": "d-1"
},
{
"descricao": "PORCAS M8 ISO 4032",
"top3": [
{"ncm8": "73229090", "confianca": 0.352457},
{"ncm8": "73181600", "confianca": 0.033459},
{"ncm8": "73012000", "confianca": 0.017840}
],
"abstem": true,
"disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
"versao_pacote": "d-1"
}
],
"disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
"versao_pacote": "d-1"
}
O custo no rate limit é len(descricoes) tokens do balde — um lote de 50 itens custa 50,
não 1.
GET /v1/info
Retorna proveniência e configuração de calibração do pacote carregado. Não expõe
métricas de acurácia/ECE (não fazem parte do pacote de produto — ver seção ncm info
acima).
Request:
curl -H "X-API-Key: sua-chave-api" http://localhost:8000/v1/info
Response (200) — capturada rodando o servidor local com o pacote d-1:
{
"versao_pacote": "d-1",
"modelo_base": "neuralmind/bert-large-portuguese-cased",
"corrida": "d",
"data_export": "2026-07-17",
"temperatura": 0.7862830393715502,
"piso_confianca": 0.55,
"piso_margem": 0.1,
"classes": 9748,
"disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador."
}
GET /healthz
Verifica saúde do servidor (usado pelo Docker HEALTHCHECK). Não exige X-API-Key.
Request:
curl http://localhost:8000/healthz
Response (200):
{"status": "ok"}
Docker
Build
Deve ser executado na raiz do repositório (o Dockerfile copia o wheel de dist/):
# 1. Construir o wheel
uv build packages/ncm-classificador
# 2. Construir a imagem Docker
docker build -f packages/ncm-classificador/Dockerfile -t ncm-classificador .
Run
O pacote-modelo (1,3 GB) não é embarcado na imagem — entra como volume:
docker run \
-p 8000:8000 \
-v /caminho/local/do/pacote:/pacote:ro \
-e NCM_API_KEYS=sua-chave-api \
-e NCM_RATE_LIMIT_RPM=120 \
ncm-classificador
Notas:
/caminho/local/do/pacoteé o diretório local com o pacote-modelo baixado do Hugging Face (ver seção "Obtendo o pacote-modelo" abaixo)-v ... :romonta em modo somente-leitura (segurança)- Porta exposta: 8000 (ajuste com
-p HOST:8000conforme necessário) - O servidor está pronto quando logs mostram
Application startup complete
Obtendo o pacote-modelo
O modelo treinado é distribuído separadamente, no Hugging Face:
https://huggingface.co/DominuZ/ncm-classificador-bertimbau (CC BY 4.0).
O pacote de inferência são estes 5 itens do repositório: modelo.onnx (~1,3 GB),
tokenizer/, labels.json, inferencia.json e manifest.json — a integridade
(SHA-256 do manifesto) é conferida automaticamente antes de qualquer resposta.
pip install huggingface_hub
hf download DominuZ/ncm-classificador-bertimbau \
modelo.onnx labels.json inferencia.json manifest.json tokenizer/tokenizer.json tokenizer/tokenizer_config.json \
--local-dir ~/.ncm/pacote
Uma vez baixado, aponte a variável de ambiente NCM_PACOTE para o diretório
(ou use o padrão ~/.ncm/pacote, que dispensa a variável):
export NCM_PACOTE=~/.ncm/pacote
ncm classificar "PARAF SEXT ZINC M8X40 DIN933"
ncm servir
Ou via Docker:
docker run -v /seu/caminho/pacote:/pacote -e NCM_PACOTE=/pacote ...
Limitações e abstenção
O modelo abstém em casos ambíguos ou com baixa confiança na predição (atualmente ~48% dos itens). A abstenção é um recurso, não um defeito: itens abstidos devem ser revisados manualmente pela equipe de conformidade ou contador.
Exemplo real de abstenção (saída do ncm lote, ver seção de uso acima): o item
PORCAS M8 ISO 4032 recebeu abstem=true porque o 1º candidato (73229090) ficou em
35,2% de confiança, abaixo do piso de 55% configurado no pacote d-1. Na CLI interativa
(ncm classificar), abstenção aparece como a linha
ABSTEVE — confiança insuficiente; escale a um contador logo abaixo da tabela.
Links e suporte
- Modelo (pesos + pacote ONNX): https://huggingface.co/DominuZ/ncm-classificador-bertimbau — o model card documenta métricas, calibração, dados de treino e limitações
- Benchmark público (RFB-bench): https://huggingface.co/datasets/DominuZ/rfb-bench
- Dúvidas e problemas: use as discussões (aba Community) do repositório do modelo no Hugging Face
Licença: código MIT · pesos do modelo CC BY 4.0 Versão: 0.1.0 Última atualização: 2026-07-17
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ncm_classificador-0.1.0.tar.gz.
File metadata
- Download URL: ncm_classificador-0.1.0.tar.gz
- Upload date:
- Size: 17.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f585c90f790fc331c87a8d6334bfd0a1b4f832ecabe7ab4d5df05116f2eef57c
|
|
| MD5 |
23c7aa54d369cb0fd424216d19bdb1b4
|
|
| BLAKE2b-256 |
b3c97e411f9b3467732df2428dbaa7bcb9c8eeb2d894ee637ce996fb1106390f
|
File details
Details for the file ncm_classificador-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ncm_classificador-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b0e9312081f08051d27aded39bb2bf689330ea29a7ba09c3dcd4e7685dfcca17
|
|
| MD5 |
97cb3cc3c95d00b016fddc81daa56978
|
|
| BLAKE2b-256 |
cee8d116613bb688e48ca8b83704ec5175c10bcb39b95c0fd3d9977151047f25
|