🤖

ML Service Setup

ML Service - Setup & Development Guide

📋 Visão Geral

Serviço Python dedicado para Machine Learning no ecossistema Audissey, incluindo:

🚀 Quick Start

1. Criar Virtual Environment

cd apps/ml-service

# Criar venv
python3.11 -m venv venv

# Ativar (Mac/Linux)
source venv/bin/activate

# Ativar (Windows)
venv\Scripts\activate

2. Instalar Dependências

# Produção
pip install -r requirements.txt

# Desenvolvimento
pip install -r requirements-dev.txt

lightfm 1.17 não instala com pip install. O setup.py usa
__builtins__.__LIGHTFM_SETUP__, que rebenta sob PEP 517 (e o próprio pip download já corre o
PEP 517 nas versões recentes do pip). Constrói-se a partir do sdist, como no Dockerfile, com
Python 3.11 e Cython < 3. O .venv da raiz do repo é Python 3.9 sem lightfm e não serve para os
testes do recomendador (os testes com LightFM real fazem importorskip).

cd apps/ml-service
uv venv --python 3.11 .venv && P=$PWD/.venv/bin/python   # .venv está no .gitignore
uv pip install --python $P "setuptools<75" "cython<3" "numpy==1.26.3" wheel
# sem lightfm; torch/transformers não são precisos para os testes do recomendador
grep -v -E '^(lightfm|transformers|sentence-transformers)==' requirements.txt > /tmp/req-nolfm.txt
uv pip install --python $P -r /tmp/req-nolfm.txt -r requirements-dev.txt
mkdir -p /tmp/lfm && curl -fsSL -o /tmp/lfm/lightfm-1.17.tar.gz \
  https://files.pythonhosted.org/packages/source/l/lightfm/lightfm-1.17.tar.gz
tar xzf /tmp/lfm/lightfm-1.17.tar.gz -C /tmp/lfm
(cd /tmp/lfm/lightfm-1.17 && $P setup.py bdist_wheel)
uv pip install --python $P /tmp/lfm/lightfm-1.17/dist/lightfm-1.17-*.whl
$P -m pytest tests/unit -q && $P -m ruff check src tests && $P -m black --check src tests

No macOS o LightFM compila sem OpenMP (avisa que usa 1 thread); os resultados não mudam.

3. Configurar Environment

# Copiar exemplo
cp .env.example .env

# Editar .env
nano .env

4. Rodar Serviço

# Modo desenvolvimento (auto-reload)
uvicorn src.main:app --reload --port 8000

# Ou usando Python
python -m src.main

Acesse:

🧪 Testes

Rodar Todos os Testes

pytest

Com Coverage

pytest --cov=src --cov-report=html
open htmlcov/index.html

Apenas Unit Tests

pytest tests/unit/ -v

Teste Específico

pytest tests/unit/test_config.py::test_settings_defaults -v

🔍 Linting & Formatting

Ruff (Linting)

# Check
ruff check src/ tests/

# Auto-fix
ruff check --fix src/ tests/

Black (Formatting)

# Check
black --check src/ tests/

# Format
black src/ tests/

isort (Import Order)

# Check
isort --check-only src/ tests/

# Fix
isort src/ tests/

mypy (Type Checking)

mypy src/ --ignore-missing-imports

Rodar Tudo

# Script helper
cat > lint.sh << 'EOF'
#!/bin/bash
echo "🔍 Running ruff..."
ruff check src/ tests/

echo "🎨 Running black..."
black --check src/ tests/

echo "📦 Running isort..."
isort --check-only src/ tests/

echo "🔍 Running mypy..."
mypy src/ --ignore-missing-imports
EOF

chmod +x lint.sh
./lint.sh

🐳 Docker

Build

docker build -t audissey-ml:latest -f Dockerfile .

Run

docker run -p 8000:8000 \
  -e MONGO_URI="mongodb://host.docker.internal:27017/audissey" \
  -e REDIS_URL="redis://host.docker.internal:6379" \
  audissey-ml:latest

Com docker-compose

# docker-compose.yml
services:
  ml-service:
    build:
      context: ./apps/ml-service
      dockerfile: Dockerfile
    ports:
      - '8000:8000'
    environment:
      - MONGO_URI=mongodb://mongo:27017/audissey
      - REDIS_URL=redis://redis:6379
      - ENVIRONMENT=development
    depends_on:
      - mongo
      - redis

📂 Estrutura do Projeto

apps/ml-service/
├── src/
│   ├── __init__.py
│   ├── main.py              # FastAPI app
│   ├── config.py            # Settings (Pydantic)
│   │
│   ├── models/              # ML Models
│   │   ├── __init__.py
│   │   ├── recommender.py   # Recommendation engine
│   │   └── embeddings.py    # Text/Audio embeddings
│   │
│   ├── services/            # Business logic
│   │   ├── __init__.py
│   │   └── classification.py # Genre classification
│   │
│   └── utils/
│       ├── __init__.py
│       └── helpers.py
│
├── tests/
│   ├── __init__.py
│   ├── conftest.py          # Pytest fixtures
│   ├── unit/
│   │   ├── test_config.py
│   │   ├── test_main.py
│   │   ├── test_embeddings.py
│   │   ├── test_recommender.py
│   │   └── test_classification.py
│   └── integration/
│
├── models/                   # Trained models (gitignored)
├── requirements.txt
├── requirements-dev.txt
├── pyproject.toml
├── Dockerfile
├── .env.example
├── .gitignore
└── README.md

🔧 Configuração

Variáveis de Ambiente

Ver .env.example para todas as opções.

Principais:

# Aplicação
ENVIRONMENT=development|production
LOG_LEVEL=DEBUG|INFO|WARNING|ERROR

# Database
MONGO_URI=mongodb://localhost:27017/audissey
REDIS_URL=redis://localhost:6379

# ML
ML_MODEL_PATH=./models
EMBEDDING_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
RECOMMENDATION_WEIGHT_BY_SIGNAL=false   # ver "Sinal de tempo de escuta no treino"

Sinal de tempo de escuta no treino

listenTimeMsByContent (snapshot ml_training_data) tem chaves "<tipo>:<id>" (radio:<id>,
episode:<id>, podcast:<id>). Cada par user-item recebe uma força pelo tempo de escuta desse
item: 5 min ou mais: 1.0; 30 s a 5 min: 0.5; menos de 30 s: 0.1; sem tempo de escuta: 0.

Plays por item. O worker grava playsByContent (número de play_started do dia por item, mesmas
chaves "<tipo>:<id>") em UserFeatures.dailyMetrics[] e em cada linha do snapshot. Quando o item
tem mais de 1 play, a força é multiplicada por 1 + ln(plays) (2 plays: ×1.69; 5: ×2.61; 20: ×4.0):
um item repetido pesa mais, sem que um único utilizador que repete muito domine o treino. Os plays
só escalam tempo de escuta: um item com plays e sem escuta continua com força 0. Snapshots gravados
antes desta mudança não têm o campo (e um item ouvido sem play_started nesse dia não tem entrada),
e ficam só com o escalão de escuta. playsByContentType continua a alimentar os totais do utilizador.

O snapshot é um cross-join utilizador × catálogo: cada linha de um utilizador repete os mesmos
mapas diários, por isso os totais do utilizador (total_plays, total_listen_ms, total_skips) são
lidos uma vez por utilizador. Antes eram somados por linha (× tamanho do catálogo), o que punha
qualquer utilizador com 1 play em eng:high.

Sem a flag o modelo não aprende nada. No cross-join todos os itens são positivos de todos os
utilizadores, e o WARP do LightFM volta a sortear um negativo que também é positivo: nunca
encontra um negativo e nunca atualiza. Os embeddings ficam no estado aleatório inicial (teste
test_lightfm_learns_nothing_from_the_unweighted_cross_join; confirmado nos snapshots de staging).
sample_weight sozinho não muda isto: escala atualizações que não acontecem.

Flag RECOMMENDATION_WEIGHT_BY_SIGNAL (default false). Ligada, o treino usa só os pares com
tempo de escuta e passa a força como sample_weight; os índices de utilizadores e itens continuam
a ser os do snapshot inteiro, por isso todos continuam a ter recomendações. Se o snapshot não tiver
nenhum tempo de escuta, treina sem pesos (como com a flag desligada) e regista um aviso. Valor vazio
conta como false.

Medir o impacto (só leitura). A partir de apps/ml-service, com MONGO_URI / MONGO_DB_NAME
de dev ou staging no ambiente ou em .env:

python -m scripts.compare_listen_signal --users 50 --top-k 10   # --snapshot-date, --max-rows N

Só corre numa lista fechada de bases: audissey-dev ou audissey-staging, ou qualquer base num
servidor local (todos os hosts do MONGO_URI são localhost / 127.0.0.1, esquema mongodb://)
com --allow-local. Tudo o resto é recusado antes da primeira query, incluindo audissey (a base de
produção, que também é o default da configuração quando falta MONGO_DB_NAME). A mensagem de recusa
nunca mostra o URI nem os hosts.

Treina em memória (1 thread) o modelo sem pesos (flag desligada) e o modelo com pesos (flag ligada)
e imprime só agregados: se o modelo sem pesos saiu do estado inicial, a sobreposição do top-K entre
os dois e o noise floor de cada um (mesmos dados, dois seeds), e quantos itens ouvidos por cada
utilizador aparecem no seu top-K (in-sample: o mesmo snapshot treina e avalia, é otimista).

Medição em audissey-staging (Out 2026, top-10, seeds 42/43):

Snapshot Utilizadores Pares ouvidos Noise floor sem / com pesos Com vs sem pesos Itens ouvidos no top-10 sem / com
2026-10-05 3 4 0.000 / 0.133 0.033 0/4 / 1/4
2026-10-02 3 5 0.000 / 0.433 0.000 0/5 / 3/5
2026-09-25 3 4 0.000 / 0.200 0.000 0/4 / 3/4
2026-09-21 2 5 0.000 / 0.350 0.000 0/5 / 2/5

Staging tem 2–3 utilizadores e 1 com escuta por dia (50 000 itens): os números mostram o mecanismo
(o modelo sem pesos é aleatório; com pesos aprende), não a qualidade em produção.

O cross-join cortava utilizadores. Staging tem 10 utilizadores ativos com userfeatures, mas
até 2026-10-16 cada snapshot só tinha os 3 primeiros: com lotes de 50 e pausa de 1 s cada
utilizador custava ~19 min de escrita e o orçamento de 45 min parava o ciclo depois do 3.º; cada
execução escrevia 1 dia e o cursor ficou ~11 dias atrasado. O 92e5e62a passa a fazer snapshot só de
ontem e leva os lotes do VG (1000 / 100) ao worker. Depois do deploy entram todos, mas 10 × 50 000 =
500 000 linhas por dia é o limite de leitura do treino. Proposta de snapshot só com pares com
interação + catálogo uma vez: ML_SNAPSHOT_SLIM_PROPOSAL.md.

Modelos

Embedding Model

Classification Model

🔄 CI/CD Pipeline

Pipeline CI (azure-pipelines-ml.yml)

Triggers:

Stages:

  1. Lint & Format

    • Ruff linting
    • Black formatting check
    • isort import order
    • mypy type checking (optional)
  2. Unit Tests

    • pytest com coverage
    • Publicar resultados de testes
    • Publicar coverage report
  3. Dependency Check

    • safety (vulnerabilidades conhecidas)
    • pip-audit (audit dependencies)

Rodar Pipeline Localmente

# Simular CI pipeline
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt

# Lint
ruff check src/ tests/
black --check src/ tests/
isort --check-only src/ tests/

# Test
pytest --cov=src --cov-report=term

# Security
pip install safety
safety check

📊 Monitoramento

Logs

# src/main.py
import logging

logger = logging.getLogger(__name__)
logger.info("Processing recommendation")

Métricas (Futuro)

Health Check

curl http://localhost:8000/health

🔗 Integração com API Principal

Via HTTP Client (NestJS)

// apps/api/src/app/ml/ml.service.ts
import { HttpService } from '@nestjs/axios';

const response = await this.httpService.post(`${ML_SERVICE_URL}/api/v1/recommend`, {
  userId: '123',
  contentType: 'podcast',
  limit: 10,
});

Via Message Queue (Futuro)

# Processamento assíncrono via BullMQ

📝 Desenvolvimento

Adicionar Nova Feature

  1. Criar branch

    git checkout dev
    git pull origin dev
    git checkout -b feature/sentiment-analysis
    
  2. Implementar

    # Criar módulo
    touch src/services/sentiment.py
    touch tests/unit/test_sentiment.py
    
  3. Testar

    pytest tests/unit/test_sentiment.py -v
    
  4. Lint

    ruff check src/services/sentiment.py
    black src/services/sentiment.py
    
  5. Commit

    git add .
    git commit -m "feat(ml): adicionar análise de sentimento"
    git push -u origin feature/sentiment-analysis
    
  6. PR

    • Abrir PR no Azure DevOps
    • CI pipeline roda automaticamente
    • Aguardar aprovação

Adicionar Dependência

# Adicionar em requirements.txt
echo "scikit-learn==1.4.0" >> requirements.txt

# Instalar
pip install -r requirements.txt

# Testar
pytest

# Commit
git add requirements.txt
git commit -m "chore(deps): adicionar scikit-learn"

🐛 Troubleshooting

Import Errors

# Garantir que venv está ativo
which python  # deve apontar para venv/bin/python

# Reinstalar dependências
pip install --force-reinstall -r requirements.txt

Testes Falhando

# Rodar com verbosidade
pytest -vv

# Rodar teste específico
pytest tests/unit/test_config.py::test_settings_defaults -vv

# Debug com pdb
pytest --pdb

Type Errors (mypy)

# Ignorar imports externos
mypy src/ --ignore-missing-imports

# Configurar em pyproject.toml

Pipeline Falhando

  1. Lint errors: Rodar ruff check --fix e black localmente
  2. Test failures: Rodar pytest localmente
  3. Dependencies: Verificar requirements.txt está atualizado

📚 Recursos

Documentação

Tutoriais

Tools

🤝 Contribuir

Ver docs/GIT_WORKFLOW.md


Última atualização: 27 de dezembro de 2025
Versão: 0.1.0
Python: 3.11+