-
Notifications
You must be signed in to change notification settings - Fork 0
feat: estrutura inicial da Etapa 1 do Visão de Cria #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Ambiente geral | ||
| TZ=UTC | ||
| APP_ENV=development | ||
| CORS_ORIGINS=http://localhost:3000 | ||
|
|
||
| # Banco | ||
| POSTGRES_DB=visaodecria | ||
| POSTGRES_USER=visaouser | ||
| POSTGRES_PASSWORD=visaopass | ||
| DATABASE_URL=postgresql+asyncpg://visaouser:visaopass@postgres:5432/visaodecria | ||
|
|
||
| # Redis/Celery | ||
| REDIS_URL=redis://redis:6379/0 | ||
| CELERY_BROKER_URL=redis://redis:6379/1 | ||
| CELERY_RESULT_BACKEND=redis://redis:6379/2 | ||
|
|
||
| # MinIO | ||
| MINIO_ROOT_USER=minioadmin | ||
| MINIO_ROOT_PASSWORD=minioadmin123 | ||
| MINIO_ENDPOINT=minio:9000 | ||
| MINIO_SECURE=false | ||
|
|
||
| # Frontend | ||
| NEXT_PUBLIC_API_URL=http://localhost:8000 | ||
| NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws/live |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # Dependências | ||
| node_modules/ | ||
|
|
||
| # Python | ||
| __pycache__/ | ||
| *.pyc | ||
| .venv/ | ||
|
|
||
| # Next.js | ||
| .next/ | ||
| out/ | ||
|
|
||
| # Ambiente | ||
| .env | ||
| .env.* | ||
| !.env.example | ||
|
|
||
| # Logs | ||
| *.log | ||
|
|
||
| # SO | ||
| .DS_Store |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,33 +1,64 @@ | ||
| # Build Instructions for APK | ||
| # 🥋 Visão de Cria | ||
|
|
||
| To build the APK for this project, follow the steps below: | ||
| Plataforma de análise de combate em tempo real (Boxe, MMA, BJJ Gi e No-Gi), otimizada para baixa latência e uso em produção com overlay para OBS. | ||
|
|
||
| 1. **Clone the Repository** | ||
| Run the following command to clone the repository: | ||
| ```bash | ||
| git clone https://github.com/ringuemkt-rgb/cria-do-tatame.git | ||
| cd cria-do-tatame | ||
| ``` | ||
| ## O que foi otimizado nesta revisão completa | ||
|
|
||
| 2. **Install Dependencies** | ||
| Use Gradle to install all necessary dependencies: | ||
| ```bash | ||
| ./gradlew build | ||
| ``` | ||
| - ✅ API com `liveness` e `readiness` cacheado para reduzir custo por requisição. | ||
| - ✅ Motor de análise técnica modular (detecção de modalidade, regras BJJ, pontuação e insights). | ||
| - ✅ Endpoint de simulação para validar pipeline técnico sem depender de vídeo real. | ||
| - ✅ Testes unitários cobrindo regras, pontuação e geração de insight. | ||
| - ✅ Frontend com dashboard/overlay prontos para operação inicial. | ||
|
|
||
| 3. **Build the APK** | ||
| Now you can build the APK by running: | ||
| ```bash | ||
| ./gradlew assembleDebug | ||
| ``` | ||
| The APK will be located in the `app/build/outputs/apk/debug/` directory. | ||
| ## Arquitetura base | ||
|
|
||
| --- | ||
| - **Backend:** FastAPI + Celery + Redis + PostgreSQL/TimescaleDB + MinIO | ||
| - **Análise:** módulos dedicados em `backend/app/analysis/` | ||
| - **Frontend:** Next.js 14 + Tailwind | ||
| - **Infra local:** Docker Compose | ||
|
|
||
| # Using GitHub Actions | ||
| ## Endpoints principais | ||
|
|
||
| This repository integrates GitHub Actions to automate workflows. Here's how to use it: | ||
| - `GET /api/v1/health/live` | ||
| - `GET /api/v1/health/ready` | ||
| - `POST /api/v1/modality/override` | ||
| - `POST /api/v1/analysis/simulate` | ||
|
|
||
| 1. **Continuous Integration**: Whenever a push is made to the main branch, GitHub Actions will automatically trigger the CI workflow to run tests and build the project. | ||
| ## Rodando localmente | ||
|
|
||
| 2. **Configuration**: You can find the configuration files under the `.github/workflows/` directory. Modify these YAML files to customize the workflow as per your requirements. | ||
| ```bash | ||
| cp .env.example .env | ||
| docker compose up --build | ||
| ``` | ||
|
|
||
| ## Testes rápidos | ||
|
|
||
| ```bash | ||
| python -m compileall backend/app | ||
| PYTHONPATH=backend pytest -q backend/tests | ||
| cd frontend && npm run lint && npm run build | ||
| ``` | ||
|
|
||
| ## Simulação da análise técnica (exemplo) | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:8000/api/v1/analysis/simulate \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{ | ||
| "modalidade": "bjj_gi", | ||
| "eventos": [ | ||
| {"atleta":"azul","acao":"guard_pass","duracao_ms":3500,"confianca":0.93}, | ||
| {"atleta":"branco","acao":"stalling","duracao_ms":900,"confianca":0.71} | ||
| ] | ||
| }' | ||
| ``` | ||
|
|
||
| ## Inspirações técnicas (adaptação para nosso contexto) | ||
|
|
||
| A organização do pipeline foi estruturada com referências de boas práticas de ecossistemas abertos como: | ||
| - arquitetura de inferência modular (separação detecção/classificação/regras); | ||
| - contracts tipados para eventos; | ||
| - readiness para orquestração; | ||
| - processamento incremental para stream. | ||
|
|
||
| Tudo foi adaptado para o contexto do **Visão de Cria**, priorizando pt-BR, regras de luta e integração com OBS. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| FROM python:3.11-slim | ||
|
|
||
| ENV PYTHONDONTWRITEBYTECODE=1 \ | ||
| PYTHONUNBUFFERED=1 \ | ||
| PIP_NO_CACHE_DIR=1 | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| ffmpeg \ | ||
| curl \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| COPY requirements.txt /tmp/requirements.txt | ||
| RUN pip install --upgrade pip==25.0.1 && pip install -r /tmp/requirements.txt | ||
|
|
||
| COPY . /app | ||
| RUN useradd --create-home --shell /bin/bash appuser && chown -R appuser:appuser /app | ||
|
|
||
| USER appuser | ||
|
|
||
| EXPOSE 8000 | ||
|
|
||
| CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Módulos de análise técnica de combate.""" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| """Classificador leve de ações de grappling (baseline de produção).""" | ||
|
|
||
|
|
||
| class GrapplingClassifier: | ||
| taxonomy = { | ||
| "takedown_attempt": "Tentativa de queda", | ||
| "takedown_complete": "Queda executada", | ||
| "guard_pull": "Puxada para guarda", | ||
| "sweep_attempt": "Tentativa de raspagem", | ||
| "sweep_success": "Raspagem finalizada", | ||
| "guard_pass_attempt": "Tentativa de passagem", | ||
| "guard_pass_complete": "Guarda passada", | ||
| "mount_established": "Montada consolidada", | ||
| "back_control": "Pegada nas costas", | ||
| "submission_attempt": "Tentativa de finalização", | ||
| "submission_defense": "Defesa de finalização", | ||
| "stalling": "Evitação de combate", | ||
| "illegal_grip": "Pegada irregular", | ||
| } | ||
|
|
||
| def classify_window(self, action_probs: dict[str, float]) -> dict[str, str | float]: | ||
| """Mapeia distribuição de probabilidades para melhor ação técnica.""" | ||
|
|
||
| if not action_probs: | ||
| return {"codigo": "unknown", "rotulo": "Sem evento", "confianca": 0.0} | ||
|
|
||
| codigo = max(action_probs, key=action_probs.get) | ||
| return { | ||
| "codigo": codigo, | ||
| "rotulo": self.taxonomy.get(codigo, "Evento desconhecido"), | ||
| "confianca": float(action_probs[codigo]), | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||||||||||||||||||
| """Geração de insights técnicos em pt-BR.""" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from app.schemas.analysis import FightEvent, MatchContext | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class TechnicalInsightEngine: | ||||||||||||||||||||||
| templates = { | ||||||||||||||||||||||
| "guard_pass_complete": [ | ||||||||||||||||||||||
| "✅ Passagem de guarda técnica com controle de quadril e cabeça alinhada.", | ||||||||||||||||||||||
| "🔍 Boa progressão: pressão alta + quebra de postura antes da passagem.", | ||||||||||||||||||||||
| "⚠️ Atenção: braço exposto durante a passagem. Risco de triângulo.", | ||||||||||||||||||||||
| ], | ||||||||||||||||||||||
| "sweep_success": [ | ||||||||||||||||||||||
| "✅ Raspagem executada com timing excelente e transferência de peso limpa.", | ||||||||||||||||||||||
| "🔍 Excelente alavanca: ombro no solo + quadril elevado.", | ||||||||||||||||||||||
| "⚠️ Base estreita no final da raspagem. Possível contra-raspagem.", | ||||||||||||||||||||||
| ], | ||||||||||||||||||||||
| "submission_attempt": [ | ||||||||||||||||||||||
| "🔍 Finalização bem montada: isolamento de membro + controle de postura.", | ||||||||||||||||||||||
| "⚠️ Falta de pressão contínua. Defesa adversária recuperando espaço.", | ||||||||||||||||||||||
| "✅ Sequência de transição impecável: guarda → costas → mata-leão.", | ||||||||||||||||||||||
| ], | ||||||||||||||||||||||
|
Comment on lines
+18
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Template ordering for The selection logic at lines 29–32 treats index 1 as "low-confidence cautionary" and index 2 as "biomechanical warning". That holds for 🐛 Proposed reorder to match siblings "submission_attempt": [
- "🔍 Finalização bem montada: isolamento de membro + controle de postura.",
- "⚠️ Falta de pressão contínua. Defesa adversária recuperando espaço.",
"✅ Sequência de transição impecável: guarda → costas → mata-leão.",
+ "🔍 Finalização bem montada: isolamento de membro + controle de postura.",
+ "⚠️ Falta de pressão contínua. Defesa adversária recuperando espaço.",
],Longer term, consider replacing the positional index with explicit categories (e.g. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def generate(self, event: FightEvent, context: MatchContext) -> str: | ||||||||||||||||||||||
| opcoes = self.templates.get(event.tipo, ["🔍 Evento técnico detectado."]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| indice = 0 | ||||||||||||||||||||||
| if event.confianca < 0.6 and len(opcoes) > 1: | ||||||||||||||||||||||
| indice = 1 | ||||||||||||||||||||||
| if event.biomecanica_flags and len(opcoes) > 2: | ||||||||||||||||||||||
| indice = 2 | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| base_msg = opcoes[indice] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if event.biomecanica_flags: | ||||||||||||||||||||||
| alertas = ", ".join(event.biomecanica_flags) | ||||||||||||||||||||||
| base_msg += f" | 🦴 Ajustes biomecânicos: {alertas}." | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if context.modalidade in {"bjj_gi", "bjj_nogi"}: | ||||||||||||||||||||||
| base_msg += " (Referência CBJJ/IBJJF)" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return base_msg | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| """Detector leve de modalidade com heurísticas dos primeiros segundos.""" | ||
|
|
||
| from app.schemas.analysis import ModalityResult | ||
|
|
||
|
|
||
| class ModalityDetector: | ||
| """Combina sinais visuais/temporais para inferir modalidade rapidamente.""" | ||
|
|
||
| def detect_from_signals(self, sinais: dict[str, float]) -> ModalityResult: | ||
| """Detecta modalidade usando sinais sintéticos para etapa inicial da API.""" | ||
|
|
||
| gi = sinais.get("gi", 0.0) | ||
| rashguard = sinais.get("rashguard", 0.0) | ||
| ground = sinais.get("ground_time_ratio", 0.0) | ||
| strikes = sinais.get("strikes_detected", 0.0) | ||
| cage = sinais.get("cage", 0.0) | ||
|
|
||
| if gi > 0.7 and ground > 0.6: | ||
| return ModalityResult(modality="bjj_gi", confidence=0.92) | ||
| if rashguard > 0.7 and ground > 0.5: | ||
| return ModalityResult(modality="bjj_nogi", confidence=0.89) | ||
| if strikes > 5 and ground < 0.3: | ||
| return ModalityResult(modality="mma" if cage > 0.5 else "boxe", confidence=0.95) | ||
|
|
||
| return ModalityResult(modality="unknown", confidence=0.0, requires_manual=True) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Pacote de regras por modalidade.""" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| """Motor de regras BJJ (IBJJF/ADCC simplificado para etapa atual).""" | ||
|
|
||
| from typing import Literal | ||
|
|
||
| from app.schemas.analysis import GrapplingAction | ||
|
|
||
|
|
||
| class BJJRulesEngine: | ||
| def __init__(self, modality: Literal["gi", "nogi"]): | ||
| self.modality = modality | ||
| self.points_config = self._load_config() | ||
|
|
||
| def _load_config(self) -> dict[str, int]: | ||
| return { | ||
| "takedown": 2, | ||
| "sweep": 2, | ||
| "guard_pass": 3, | ||
| "knee_on_belly": 2, | ||
| "mount": 4, | ||
| "back_control": 4, | ||
| } | ||
|
|
||
| def validate_action(self, action: GrapplingAction) -> dict[str, str | bool | int | None]: | ||
| """Valida se ação gera ponto/vantagem/penalidade.""" | ||
|
|
||
| if action.acao == "illegal_grip" and self.modality == "nogi": | ||
| return {"valid": False, "points": 0, "advantage": False, "penalty": "pegada irregular"} | ||
|
|
||
| if action.acao == "stalling": | ||
| return {"valid": False, "points": 0, "advantage": False, "penalty": "evitação de combate"} | ||
|
|
||
| points = self.points_config.get(action.acao, 0) | ||
| advantage = self.calculate_advantage(action.acao, action.duracao_ms) | ||
| return {"valid": True, "points": points, "advantage": advantage, "penalty": None} | ||
|
Comment on lines
+26
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When 🛡️ Suggested fix- if action.acao == "illegal_grip" and self.modality == "nogi":
- return {"valid": False, "points": 0, "advantage": False, "penalty": "pegada irregular"}
+ if action.acao == "illegal_grip":
+ return {"valid": False, "points": 0, "advantage": False, "penalty": "pegada irregular"}🤖 Prompt for AI Agents |
||
|
|
||
| @staticmethod | ||
| def calculate_advantage(action: str, duration_ms: int) -> bool: | ||
| return duration_ms >= 3000 and action in {"near_pass", "near_back", "near_mount"} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| """Motor unificado de pontuação para modalidade de grappling.""" | ||
|
|
||
| from collections import defaultdict | ||
|
|
||
| from app.analysis.rules.bjj_rules_engine import BJJRulesEngine | ||
| from app.schemas.analysis import GrapplingAction | ||
|
|
||
|
|
||
| class ScoringEngine: | ||
| def __init__(self, modalidade: str): | ||
| if modalidade not in {"bjj_gi", "bjj_nogi"}: | ||
| raise ValueError("Modalidade ainda não suportada no scoring_engine atual") | ||
| self.modalidade = modalidade | ||
| self.rules = BJJRulesEngine("gi" if modalidade == "bjj_gi" else "nogi") | ||
| self.placar = defaultdict(int) | ||
| self.vantagens = defaultdict(int) | ||
| self.penalidades = defaultdict(int) | ||
|
|
||
| def process_event(self, action: GrapplingAction) -> None: | ||
| resultado = self.rules.validate_action(action) | ||
| atleta = action.atleta | ||
|
|
||
| if resultado["penalty"]: | ||
| self.penalidades[atleta] += 1 | ||
| return | ||
|
|
||
| if resultado["valid"] and int(resultado["points"]) > 0: | ||
| self.placar[atleta] += int(resultado["points"]) | ||
|
|
||
| if bool(resultado["advantage"]): | ||
| self.vantagens[atleta] += 1 | ||
|
|
||
| def snapshot(self) -> dict[str, dict[str, int]]: | ||
| return { | ||
| "placar": {"azul": self.placar["azul"], "branco": self.placar["branco"]}, | ||
| "vantagens": {"azul": self.vantagens["azul"], "branco": self.vantagens["branco"]}, | ||
| "penalidades": {"azul": self.penalidades["azul"], "branco": self.penalidades["branco"]}, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """Rotas de simulação para validação rápida de análise técnica.""" | ||
|
|
||
| from fastapi import APIRouter | ||
|
|
||
| from app.analysis.insight_generator import TechnicalInsightEngine | ||
| from app.analysis.scoring_engine import ScoringEngine | ||
| from app.schemas.analysis import ( | ||
| FightEvent, | ||
| MatchContext, | ||
| SimulateAnalysisRequest, | ||
| SimulateAnalysisResponse, | ||
| ) | ||
|
|
||
| router = APIRouter(prefix="/analysis", tags=["analysis"]) | ||
|
|
||
|
|
||
| @router.post("/simulate", response_model=SimulateAnalysisResponse) | ||
| async def simulate_analysis(payload: SimulateAnalysisRequest) -> SimulateAnalysisResponse: | ||
| """Simula pontuação e insights a partir de eventos de grappling enviados pela API.""" | ||
|
|
||
| scoring = ScoringEngine(modalidade=payload.modalidade) | ||
| insight_engine = TechnicalInsightEngine() | ||
| insights: list[str] = [] | ||
|
|
||
| for evento in payload.eventos: | ||
| scoring.process_event(evento) | ||
|
|
||
| tipo = "submission_attempt" if "mount" in evento.acao else "sweep_success" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The simulation endpoint currently derives Useful? React with 👍 / 👎. |
||
| insight = insight_engine.generate( | ||
| FightEvent(tipo=tipo, confianca=evento.confianca, atleta=evento.atleta), | ||
| MatchContext(modalidade=payload.modalidade), | ||
| ) | ||
| insights.append(insight) | ||
|
Comment on lines
+28
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect Two problems on line 28:
Use equality and an explicit mapping so each action maps to a meaningful 🔧 Suggested fix+ACAO_TO_TIPO: dict[str, str] = {
+ "mount": "submission_attempt",
+ "back_control": "submission_attempt",
+ "sweep": "sweep_success",
+ "guard_pass": "pass_success",
+ "takedown": "takedown_success",
+ "knee_on_belly": "control_success",
+ "near_pass": "pass_attempt",
+ "near_back": "back_attempt",
+ "near_mount": "mount_attempt",
+ "illegal_grip": "foul",
+ "stalling": "foul",
+}
+
`@router.post`("/simulate", response_model=SimulateAnalysisResponse)
async def simulate_analysis(payload: SimulateAnalysisRequest) -> SimulateAnalysisResponse:
@@
for evento in payload.eventos:
scoring.process_event(evento)
- tipo = "submission_attempt" if "mount" in evento.acao else "sweep_success"
+ tipo = ACAO_TO_TIPO.get(evento.acao, "generic_event")
insight = insight_engine.generate(🤖 Prompt for AI Agents |
||
|
|
||
| snapshot = scoring.snapshot() | ||
| return SimulateAnalysisResponse( | ||
| placar=snapshot["placar"], | ||
| vantagens=snapshot["vantagens"], | ||
| penalidades=snapshot["penalidades"], | ||
| insights=insights, | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.