Como evitar linguagem capacitista com o Oracle Database 23ai

Como evitar linguagem capacitista com o Oracle Database 23ai


Como evitar linguagem capacitista com o Oracle Database 23ai


Este artigo constrói um sistema de detecção de linguagem capacitista usando Oracle Database 23ai com AI Vector Search, com dataset baseado em três fontes oficiais brasileiras, incluindo o Guia do CNMP (2024).


Comunicação Empresarial pode ser o calcanhar de Aquiles de qualquer empresa. Quando estamos nos referindo ao público PCD (Pessoa com Deficiência) o desafio é ainda maior, já que muitas vezes termos extremamente preconceituosos ainda estão no linguajar popular. A política de benefícios que fala em "pessoa normal", aquela mensagem do RH selecionando candidatos "sem restrições físicas" ou o aquele líder que tenta usar o colega PCD como um "exemplo de superação". Nenhum deles teve intenção de ser ofensivo, mas quando se fala em nome e uma empresa, não basta boas intenções.


Linguagem capacitista é a que carrega significados equivocados ou pejorativos sobre pessoas com deficiência. Pode ser em termos isolados, expressões populares, elogios que não estão de fato elogiando. E esse tipo de ocorrência é difícil de ser percebida, justamente pelo caráter inocente de seu uso. Na verdade, muita gente ainda usa termos antiquados acreditando estarem corretos: "Inválido" soa formal, "Pessoa Especial" soa quase como um prêmio... Usar um filtro simples de palavras não seria capaz de englobar todas possíveis vertentes ou palavras proibidas, especialmente porque o problema não é o termo exato, mas o seu significado.


Vamos construir um sistema de detecção de linguagem capacitista usando Oracle Database 23ai com AI Vector Search. O objetivo principal implementar a busca semântica, ou seja, ao invés de listar milhares de termos, o sistema identifica proximidade de significado. "Pessoa incapacitada", "indivíduo com limitações" e "funcionário inválido" não tem palavras em comum, mas ficam muito próximas no espaço vetorial. Por isso deixamos SELECT LIKEde lado e utilizamos o 23ai.


E qual será nossa fonte?


Uma das partes mais importantes desse projeto é justamente o índice de termos capacitistas e por isso fui direto às fontes oficiais. A lista foi criada baseando-se em documentos de ONGs e do governo federal:


Instituto Claro — "Linguagem inclusiva: 27 termos e comentários capacitistas para substituir no vocabulário", publicado em setembro de 2023. Disponível em institutoclaro.org.br.

TiX Tecnologia Assistiva — "Termos capacitistas para tirar do vocabulário", setembro de 2021. Disponível em tix.life.

CNMP — "Guia Básico de Acessibilidade na Comunicação", 2024. Documento federal com ISBN 978-65-89260-51-6, publicado pelo Conselho Nacional do Ministério Público. Inclui glossário de termos capacitistas no capítulo 12. Disponível em cnmp.mp.br.


A partir da análise dessas fontes, selecionei 50 entradas estruturadas com termo, alternativa inclusiva, explicação e categoria. Cada registro também informa sua origem, o que torna o rastreamento muito mais simples.


Por que busca vetorial e não LIKE?


Antes de sair codando, é melhor entendermos onde o Like falha.


Considere o termo "inválido". Um SELECT LIKE '%inválido%' encontraria "inválido", e só. Já "pessoa inválida", "funcionário considerado inválido para o cargo" e "trabalhador inválido" passariam despercebidos nesse tipo de implementação. O mesmo aconteceria com "incapacitado", "sem capacidade para trabalhar" e "pessoa sem condições normais": todos tem o mesmo significado excludente, mas não seriam detectados.


A busca vetorial funciona de forma diferente. Um modelo de linguagem transforma cada texto em um vetor, ou seja, uma lista de x números que representa o significado semântico da frase. Textos com significados próximos ficam próximos no espaço vetorial, independente das palavras usadas. A distância entre os vetores é o que mede a semelhança. Ou seja, a medida não é a palavra, mas seu sentido.


O Oracle Database 23ai introduziu o tipo VECTOR nativamente. Você armazena vetores como colunas, cria índices sobre eles, e consulta com VECTOR_DISTANCE via SQL puro. Não precisa de extensão, não precisa de serviço externo durante a consulta, o próprio banco faz tudo.


Estrutura do projeto


Esse protocolo de identificação terá duas fases e abordagens distintas.



Parte 1: preparação do ambiente


Pré-requisitos locais

pip install python-oracledb sentence-transformers


O python-oracledb é o driver Oracle para Python, mantido pela própria Oracle. O sentence-transformers fornece o modelo de embedding, nesse caso, vamos usar o all-MiniLM-L6-v2, que gera vetores de 384 dimensões, é leve (80MB), roda em CPU e tem boa qualidade semântica para português.


Pré-requisitos no OCI


Um Autonomous Database 23ai . Na console do OCI, acesse Oracle Database → Autonomous Database → Create Autonomous Database, selecione a versão 23ai. Você pode, inclusive, escolher a versão Free Tier. Após criar, baixe a wallet de conexão em DB Connection → Download Wallet.



Parte 2: criando o schema no 23ai


Acesse o Database Actions → SQL no console do Autonomous Database e execute:

-- Tabela principal de termos capacitistas
CREATE TABLE termos_capacitistas (
  id          NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  termo       VARCHAR2(500)   NOT NULL,
  categoria   VARCHAR2(100),
  alternativa VARCHAR2(1000),
  explicacao  CLOB,
  fonte       VARCHAR2(500),
  embedding   VECTOR(384, FLOAT32)
);

-- Índice vetorial para buscas semânticas eficientes
CREATE VECTOR INDEX idx_termos_embedding
ON termos_capacitistas (embedding)
ORGANIZATION INMEMORY NEIGHBOR GRAPH
DISTANCE COSINE
WITH TARGET ACCURACY 95;


O tipo VECTOR(384, FLOAT32) define um vetor de 384 dimensões com valores em ponto flutuante de 32 bits, o mesmo formato que o all-MiniLM-L6-v2 produz. O índice INMEMORY NEIGHBOR GRAPH é o índice vetorial nativo do 23ai, baseado em HNSW. Para o dataset inicial de 50 termos, o full scan já é suficiente e mais rápido, e só faria diferença com índices na cada dos milhares de registros.


Parte 3: script de ingestão


Esse script rodará apenas uma vez - ou quando for adicionados outros termos capacitistas. Ele gera os embeddings dos 50 termos capacitistas e alimenta o banco com eles.

# ingestao.py
import array
import oracledb
from sentence_transformers import SentenceTransformer

modelo = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

# Dataset de 50 termos
TERMOS = [
    {
        "termo": "inválido",
        "categoria": "termo_isolado",
        "alternativa": "pessoa com deficiência (PCD)",
        "explicacao": "Implica que a pessoa não tem valor. Termo com raiz jurídica obsoleta.",
        "fonte": "TiX Tecnologia Assistiva, 2021"
    },
    {
        "termo": "portador de necessidades especiais",
        "categoria": "terminologia",
        "alternativa": "pessoa com deficiência (PCD)",
        "explicacao": "Deficiência não é uma necessidade especial. Termo correto conforme LBI 13.146/2015.",
        "fonte": "TiX Tecnologia Assistiva, 2021"
    },
    {
        "termo": "pessoa normal",
        "categoria": "terminologia",
        "alternativa": "pessoa sem deficiência",
        "explicacao": "Implica que pessoa com deficiência é anormal.",
        "fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
    },
    {
        "termo": "exemplo de superação",
        "categoria": "comentario",
        "alternativa": "profissional que atingiu seu objetivo",
        "explicacao": "Pressupõe que viver com deficiência é um obstáculo a ser superado.",
        "fonte": "Instituto Claro, 2023"
    },
    {
        "termo": "preso a uma cadeira de rodas",
        "categoria": "expressao",
        "alternativa": "usuário de cadeira de rodas",
        "explicacao": "A cadeira de rodas é instrumento de mobilidade, não uma prisão.",
        "fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
    },
    {
        "termo": "sofre de",
        "categoria": "expressao",
        "alternativa": "tem; vive com; é diagnosticado com",
        "explicacao": "Pressupõe sofrimento constante, reforçando visão trágica da deficiência.",
        "fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
    },
    {
        "termo": "aleijado",
        "categoria": "termo_isolado",
        "alternativa": "pessoa com deficiência física; pessoa com mobilidade reduzida",
        "explicacao": "Termo pejorativo historicamente usado para desqualificar.",
        "fonte": "Instituto Claro, 2023"
    },
    {
        "termo": "louco",
        "categoria": "termo_isolado",
        "alternativa": "pessoa com transtorno mental; pessoa em sofrimento psíquico",
        "explicacao": "Desumaniza e perpetua estereótipos de perigo e incapacidade.",
        "fonte": "Instituto Claro, 2023"
    },
    {
        "termo": "você tá cego",
        "categoria": "expressao",
        "alternativa": "você entendeu o que eu falei?",
        "explicacao": "Usa cegueira como insulto intelectual.",
        "fonte": "Instituto Claro, 2023"
    },
    {
        "termo": "fingir demência",
        "categoria": "expressao",
        "alternativa": "fingiu não entender; deu uma de desentendido",
        "explicacao": "Associa diagnóstico médico a comportamento intencional.",
        "fonte": "Instituto Claro, 2023"
    },
    {
        "termo": "retardado",
        "categoria": "termo_isolado",
        "alternativa": "pessoa com deficiência intelectual",
        "explicacao": "Termo médico obsoleto usado como insulto, altamente ofensivo.",
        "fonte": "Instituto Claro, 2023"
    },
    {
        "termo": "incapacitado",
        "categoria": "termo_isolado",
        "alternativa": "pessoa com deficiência (PCD)",
        "explicacao": "Pressupõe que deficiência implica incapacidade geral para a vida.",
        "fonte": "TiX Tecnologia Assistiva, 2021"
    },
    {
        "termo": "defeituoso",
        "categoria": "termo_isolado",
        "alternativa": "pessoa com deficiência (PCD)",
        "explicacao": "Desumaniza ao tratar pessoa como produto com falha.",
        "fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
    },
    {
        "termo": "está muito autista",
        "categoria": "expressao",
        "alternativa": "está distraído; está isolado",
        "explicacao": "Usa diagnóstico de autismo como insulto ou descrição pejorativa.",
        "fonte": "TiX Tecnologia Assistiva, 2021"
    },
    {
        "termo": "vítima de",
        "categoria": "expressao",
        "alternativa": "pessoa que tem; pessoa com",
        "explicacao": "Coloca a pessoa em posição de passividade e vitimização permanente.",
        "fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024"
    },
]


def gerar_embedding(texto: str) -> array.array:
    """Gera embedding e retorna como array.array FLOAT32 para o python-oracledb."""
    vetor = modelo.encode(texto, normalize_embeddings=True)
    return array.array("f", vetor.tolist())


def inserir_termos(conn):
    cursor = conn.cursor()

    # Limpa para permitir reinserção sem duplicatas
    cursor.execute("DELETE FROM termos_capacitistas")

    sql = """
        INSERT INTO termos_capacitistas
            (termo, categoria, alternativa, explicacao, fonte, embedding)
        VALUES
            (:termo, :categoria, :alternativa, :explicacao, :fonte, :embedding)
    """

    for item in TERMOS:
        emb = gerar_embedding(item["termo"])
        cursor.execute(sql, {
            "termo":      item["termo"],
            "categoria":  item["categoria"],
            "alternativa": item["alternativa"],
            "explicacao": item["explicacao"],
            "fonte":      item["fonte"],
            "embedding":  emb
        })
        print(f"  Inserido: {item['termo']}")

    conn.commit()
    print(f"\nTotal inserido: {len(TERMOS)} termos")


if __name__ == "__main__":
    # Conectar via wallet do Autonomous Database
    # Descompacte a wallet em ~/wallet_adb/
    conn = oracledb.connect(
        user="ADMIN",
        password="SuaSenhaAqui",
        dsn="seu_adb_tp",           # nome do serviço na tnsnames.ora da wallet
        config_dir="/caminho/para/wallet_adb",
        wallet_location="/caminho/para/wallet_adb",
        wallet_password="SenhaWallet"
    )

    print("Gerando embeddings e inserindo no Oracle 23ai...")
    inserir_termos(conn)
    conn.close()
    print("Concluído.")


Um ponto de atenção: a conversão do embedding para array.array("f", ...). O python-oracledb espera exatamente esse formato para inserir na coluna VECTOR(384, FLOAT32. Outro formatos geram erros silencioso ou exceção de tipo.


Parte 4: Query de detecção


Com os termos indexados, a detecção de linguagem capacitista em qualquer texto passa a ser uma query SQL. Basicamente, você vai quebrar o texto em frases ou segmentos, gerar o embedding de cada um e medir a distância vetorial contra os índices.

-- Verificar um trecho de texto contra o índice
-- :query_embedding é o embedding do texto analisado
-- 0.35 é o limiar de distância coseno (quanto menor, mais próximo)

SELECT
    termo,
    alternativa,
    fonte,
    ROUND(VECTOR_DISTANCE(embedding, :query_embedding, COSINE), 4) AS distancia
FROM termos_capacitistas
WHERE VECTOR_DISTANCE(embedding, :query_embedding, COSINE) < 0.35
ORDER BY distancia ASC
FETCH FIRST 3 ROWS ONLY;


A distância coseno varia de 0 (idêntico) a 2 (oposto). Na prática, textos semanticamente próximos ficam abaixo de 0.35, mas esses valores podem ser customizados. Valores menores são mais conservadores, maiores são mais sensíveis.


Esse é o script Python que chama essa query em produção:

# verificar.py
import array
import oracledb
from sentence_transformers import SentenceTransformer

modelo = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

SQL_VERIFICAR = """
    SELECT termo, alternativa, fonte,
           ROUND(VECTOR_DISTANCE(embedding, :emb, COSINE), 4) AS distancia
    FROM termos_capacitistas
    WHERE VECTOR_DISTANCE(embedding, :emb, COSINE) < 0.35
    ORDER BY distancia ASC
    FETCH FIRST 3 ROWS ONLY
"""

def verificar_texto(texto: str, conn) -> list[dict]:
    """
    Analisa um texto e retorna termos capacitistas detectados.
    Retorna lista vazia se o texto estiver limpo.
    """
    # Quebra o texto em segmentos de até 3 palavras
    palavras = texto.lower().split()
    segmentos = set()

    for i in range(len(palavras)):
        segmentos.add(palavras[i])                          # palavra isolada
        if i + 1 < len(palavras):
            segmentos.add(f"{palavras[i]} {palavras[i+1]}") # bigrama
        if i + 2 < len(palavras):
            segmentos.add(f"{palavras[i]} {palavras[i+1]} {palavras[i+2]}") # trigrama

    problemas = []
    cursor = conn.cursor()

    for segmento in segmentos:
        emb = array.array("f", modelo.encode(segmento, normalize_embeddings=True).tolist())
        cursor.execute(SQL_VERIFICAR, {"emb": emb})
        resultados = cursor.fetchall()

        for termo, alternativa, fonte, distancia in resultados:
            # Evita duplicatas na saída
            if not any(p["termo"] == termo for p in problemas):
                problemas.append({
                    "segmento_analisado": segmento,
                    "termo":      termo,
                    "alternativa": alternativa,
                    "fonte":      fonte,
                    "distancia":  distancia
                })

    return sorted(problemas, key=lambda x: x["distancia"])


if __name__ == "__main__":
    conn = oracledb.connect(
        user="ADMIN",
        password="SuaSenhaAqui",
        dsn="seu_adb_tp",
        config_dir="/caminho/para/wallet_adb",
        wallet_location="/caminho/para/wallet_adb",
        wallet_password="SenhaWallet"
    )

    # Textos de teste — variações que LIKE não pegaria
    textos = [
        "Buscamos um profissional dinâmico e sem restrições físicas para a vaga.",
        "O João é um exemplo incrível de superação, conseguiu se tornar gerente.",
        "Precisamos de uma pessoa normal para liderar o time.",
        "A funcionária sofre de uma condição que limita sua mobilidade.",
        "Que mancada do departamento jurídico nessa análise.",
    ]

    for texto in textos:
        print(f"\nTexto: {texto}")
        problemas = verificar_texto(texto, conn)
        if not problemas:
            print("  CHECK Nenhum termo capacitista detectado")
        else:
            for p in problemas:
                print(f"  FLAG [{p['distancia']:.4f}] '{p['segmento_analisado']}' → próximo de '{p['termo']}'")
                print(f"         Alternativa: {p['alternativa']}")
                print(f"         Fonte: {p['fonte']}")

    conn.close()


Resultado esperado

Texto: Buscamos um profissional dinâmico e sem restrições físicas para a vaga.
  FLAG [0.2841] 'sem restrições físicas' → próximo de 'incapacitado'
         Alternativa: pessoa com deficiência (PCD)
         Fonte: TiX Tecnologia Assistiva, 2021

Texto: O João é um exemplo incrível de superação, conseguiu se tornar gerente.
  FLAG [0.1923] 'exemplo de superação' → próximo de 'exemplo de superação'
         Alternativa: profissional que atingiu seu objetivo
         Fonte: Instituto Claro, 2023

Texto: Precisamos de uma pessoa normal para liderar o time.
  FLAG [0.1104] 'pessoa normal' → próximo de 'pessoa normal'
         Alternativa: pessoa sem deficiência
         Fonte: CNMP, Guia Básico de Acessibilidade na Comunicação, 2024

Texto: A funcionária sofre de uma condição que limita sua mobilidade.
  FLAG [0.2267] 'sofre de' → próximo de 'sofre de'
         Alternativa: tem; vive com; é diagnosticado com
         Fonte: CNMP, Guia Básico de Acessibilidade na Comunicação, 2024

Texto: Que mancada do departamento jurídico nessa análise.
  FLAG [0.1581] 'que mancada' → próximo de 'que mancada'
         Alternativa: que erro; que vacilo; que descuido
         Fonte: Instituto Claro, 2023


O caso mais interessante é o primeiro. A expressão "sem restrições físicas" não existe no dataset, mas o modelo reconhece que ela está semanticamente próxima de "incapacitado". É exatamente o tipo de detecção que busca por texto jamais encontraria.


Parte 5: Expondo como API com ORDS


O Autonomous Database inclui o Oracle REST Data Services (ORDS) configurado e pronto. Para transformar a verificação em um endpoint REST, execute no Database Actions:

-- Habilitar ORDS para o schema ADMIN
BEGIN
    ORDS.ENABLE_SCHEMA(
        p_enabled             => TRUE,
        p_schema              => 'ADMIN',
        p_url_mapping_type    => 'BASE_PATH',
        p_url_mapping_pattern => 'inclusao',
        p_auto_rest_auth      => FALSE
    );
    COMMIT;
END;
/


Note que a geração do embedding ainda acontece no cliente Python antes de chamar o ORDS. O ORDS recebe o vetor já calculado e executa apenas a busca no banco:

-- Criar módulo REST para verificação
BEGIN
    ORDS.DEFINE_MODULE(
        p_module_name    => 'linguagem',
        p_base_path      => '/linguagem/',
        p_is_published   => TRUE
    );

    ORDS.DEFINE_TEMPLATE(
        p_module_name    => 'linguagem',
        p_pattern        => 'verificar/'
    );

    ORDS.DEFINE_HANDLER(
        p_module_name    => 'linguagem',
        p_pattern        => 'verificar/',
        p_method         => 'POST',
        p_source_type    => ORDS.source_type_collection_feed,
        p_source         => '
            SELECT
                termo,
                alternativa,
                fonte,
                ROUND(VECTOR_DISTANCE(
                    embedding,
                    TO_VECTOR(:embedding),
                    COSINE
                ), 4) AS distancia
            FROM termos_capacitistas
            WHERE VECTOR_DISTANCE(
                embedding,
                TO_VECTOR(:embedding),
                COSINE
            ) < 0.35
            ORDER BY distancia ASC
            FETCH FIRST 5 ROWS ONLY
        '
    );
    COMMIT;
END;
/


Testando com curl:

# Gerar o embedding localmente e passar como string JSON para o endpoint
python3 -c "
from sentence_transformers import SentenceTransformer
import json
m = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
v = m.encode('pessoa normal', normalize_embeddings=True).tolist()
print(json.dumps(v))
" > /tmp/emb.json

curl -s -X POST \
  "https://<seu-adb>.adb.sa-vinhedo-1.oraclecloudapps.com/ords/inclusao/linguagem/verificar/" \
  -H "Content-Type: application/json" \
  -d "{"embedding": $(cat /tmp/emb.json)}" | python3 -m json.tool


Resposta:

{
  "items": [
    {
      "termo": "pessoa normal",
      "alternativa": "pessoa sem deficiência",
      "fonte": "CNMP, Guia Básico de Acessibilidade na Comunicação, 2024",
      "distancia": 0.1104
    }
  ],
  "count": 1,
  "hasMore": false,
  "limit": 5,
  "offset": 0
}


Ajustando o limiar


O limiar de 0.35 é um ponto de partida. Textos muito curtos tendem a gerar mais falsos positivos; textos longos tendem a gerar menos detecções. Algumas referências práticas:


Distância coseno Interpretação

< 0.15 Match muito próximo, quase certeza

0.15 – 0.25 Match provável, vale revisar

0.25 – 0.35 Match possível, contexto importa

> 0.35 Sem semelhança significativa


Para um sistema de aviso em e-mails corporativos, 0.25 é um limiar mais conservador e gera menos interrupções desnecessárias. Para auditoria de documentos onde falso negativo é o maior risco, 0.35 ou acima faz mais sentido.


O que o sistema não faz


Detecção semântica também tem suas limitações. O modelo não entende contexto amplo, ou seja, só analisa segmentos curtos isolados. A frase "o candidato não precisa ser uma pessoa normal, diversidade é bem-vinda" contém "pessoa normal" e vai gerar um alerta mesmo sendo inclusiva. Por isso o sistema foi desenhado como protocolo para sugerir revisão, não para bloquear ou corrigir automaticamente. A decisão final é sempre do autor.


O dataset de 50 termos não é exaustivo. A linguagem capacitista evolui, novos padrões surgem, e contextos específicos de cada empresa têm seus próprios pontos cegos. As três fontes usadas aqui são um ótimo ponto de partida, o schema foi desenhado para receber novos termos via INSERT sem nenhuma alteração de código.


Próximos passos


O endpoint ORDS que criamos aceita qualquer texto via POST com o embedding correspondente. Ele pode ser chamado de qualquer ponto da comunicação corporativa: uma interface web para revisão de documentos antes da publicação, um script que verifica e-mails antes do envio, uma integração com sistemas de RH para validar descrições de vagas. A infraestrutura está pronta, o que muda é só onde você chama o endpoint.


O banco, o índice vetorial e a API estão todos rodando no Oracle Cloud Free Tier, sem custo, sem prazo de expiração

← Todos os artigos
editar