Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
name: CI

on:
push:
branches: ["main", "claude/**", "feature/**"]
pull_request:
branches: ["main"]

jobs:
# ── 1. Python: lint ────────────────────────────────────────────────
python-lint:
name: Python lint (ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip

- name: Install ruff
run: pip install ruff

# E402 = import not at top (pre-existing lazy imports in main.py)
# F601 = duplicate dict keys (pre-existing in word-map lookups)
# W292 = no newline at end of file (pre-existing in service files)
# E701/E702 = multiple statements on one line (established style in backend/ws and scripts)
# E722 = bare except (pre-existing in scripts)
- name: Lint backend
run: ruff check backend/ --select E,F,W --ignore E501,E402,F601,W292,E701,E702

- name: Lint sasl_transformer
run: ruff check sasl_transformer/ --select E,F,W --ignore E501,E402,W292

- name: Lint scripts and converter
run: ruff check scripts/ convert_signs.py --select E,F,W --ignore E501,W292,E701,E702,E722

- name: Lint tests
run: ruff check tests/ --select E,F,W --ignore E501,E402,W292,E701,E702

# ── 2. Python: tests ───────────────────────────────────────────────
python-tests:
name: Python tests (pytest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip

- name: Install dependencies
run: |
pip install \
fastapi \
uvicorn \
"pydantic==2.10.0" \
pydantic-settings \
python-dotenv \
requests \
httpx \
pytest \
pytest-asyncio \
websockets \
numpy

- name: Run tests
env:
ANTHROPIC_API_KEY: test-key-ci
GEMINI_API_KEY: ""
OLLAMA_MODEL: amandla
run: pytest tests/ -v --tb=short

# ── 3. Python: import smoke test ───────────────────────────────────
python-imports:
name: Python import check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip

- name: Install dependencies
run: |
pip install \
"pydantic==2.10.0" \
pydantic-settings \
python-dotenv \
numpy

- name: Smoke-import all new modules
env:
ANTHROPIC_API_KEY: test-key-ci
GEMINI_API_KEY: ""
run: |
python -c "from sasl_transformer.models import GlossToken, TranslationRequest, TranslationResponse, SignType; print('models OK')"
python -c "from sasl_transformer.grammar_rules import SASL_SYSTEM_PROMPT, ARTICLES_TO_DROP; print('grammar_rules OK')"
python -c "from sasl_transformer.sign_library import SignLibrary; print('sign_library OK')"
python -c "from convert_signs import extract_all_frames, select_keyframes, build_keyframe_entry; print('convert_signs OK')"
python -c "import ast; ast.parse(open('scripts/record_signs.py', encoding='utf-8').read()); print('record_signs.py syntax OK')"
python -c "import ast; ast.parse(open('scripts/merge_sign_data.py', encoding='utf-8').read()); print('merge_sign_data.py syntax OK')"

# ── 4. JavaScript: syntax check ────────────────────────────────────
js-syntax:
name: JavaScript syntax check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Check signs_library.js
run: node --check signs_library.js

- name: Check avatar.js
run: node --check src/windows/deaf/avatar.js

- name: Check src/main.js
run: node --check src/main.js

- name: Check signs_library_v2.js (if present)
run: |
if [ -f signs_library_v2.js ]; then
node --check signs_library_v2.js
else
echo "signs_library_v2.js not present, skipping"
fi

# ── 5. Sign library structural check ──────────────────────────────
signs-library-check:
name: signs_library.js structural check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Write check script
run: |
cat > "$GITHUB_WORKSPACE/check_lib.js" << 'CHECKEOF'
const lib = require('./signs_library.js');

const required = [
'SIGN_LIBRARY', 'TransitionEngine', 'sentenceToSigns',
'signWithFrames', 'prebakeFrameQuats', 'findFrame', 'slerpBetweenFrames',
'lerpHandShape', 'slerpArmPose', 'armToQuat',
'Easing', 'HS',
];
for (const key of required) {
if (!lib[key]) {
console.error('MISSING export: ' + key);
process.exit(1);
}
}
console.log('OK: all ' + required.length + ' exports present');

const count = Object.keys(lib.SIGN_LIBRARY).length;
// main consolidated the library to 130 curated signs
if (count < 100) {
console.error('Only ' + count + ' signs — expected >= 100');
process.exit(1);
}
console.log('OK: sign count = ' + count);

const TE = lib.TransitionEngine;
for (const fn of ['begin', 'tick', 'isDone']) {
if (typeof TE[fn] !== 'function') {
console.error('TransitionEngine missing: ' + fn);
process.exit(1);
}
}
console.log('OK: TransitionEngine methods present');

const makeFrame = function(t) {
const arm = {sh:{x:0,y:0,z:0}, el:{x:0,y:0,z:0}, wr:{x:0,y:0,z:0}, hand:null};
return {t: t, R: arm, L: arm};
};
const frames = [makeFrame(0.0), makeFrame(0.5), makeFrame(1.0)];
const result = lib.findFrame(frames, 0.75);
if (result.a.t !== 0.5 || result.b.t !== 1.0) {
console.error('findFrame(0.75) returned wrong bracket: a.t=' + result.a.t + ' b.t=' + result.b.t);
process.exit(1);
}
if (Math.abs(result.localT - 0.5) > 0.001) {
console.error('findFrame(0.75) localT=' + result.localT + ', expected 0.5');
process.exit(1);
}
console.log('OK: findFrame interpolation correct');
console.log('All checks passed.');
CHECKEOF

- name: Run structural check
run: node "$GITHUB_WORKSPACE/check_lib.js"
1 change: 0 additions & 1 deletion backend/harps/datasets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"""

from typing import Protocol, Dict, Any, List, Tuple, Optional
import numpy as np

# Canonical sample type used throughout HARPS
Sample = Dict[str, Any] # {"X": np.ndarray(T,J,C), "y": int, "meta": dict}
Expand Down
5 changes: 2 additions & 3 deletions backend/harps/experiments/ablation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@

from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any
from typing import List, Dict
from pathlib import Path
import json
import numpy as np

from ..models import MLPClassifier
from ..train import MLPTrainer, TrainConfigMLP, save_checkpoint
from ..train import MLPTrainer, TrainConfigMLP
from ..utils import FeatureScaler, compute_metrics, log_result
from .pipelines import Pipelines, PipelineConfig, make_feature_dict

Expand Down
3 changes: 1 addition & 2 deletions backend/harps/experiments/pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import numpy as np

from ..transforms import (
Compose,
PersonCentricNormalize,
UniformFrameSample,
)
Expand Down Expand Up @@ -236,6 +235,6 @@ def make_feature_dict(
raise ValueError(f"Unknown feature set: {name!r}. Choose from {list(_dispatch)}")
try:
result[name] = fn(samples)
except ImportError as e:
except ImportError:
result[name] = None # iisignature unavailable
return result
2 changes: 1 addition & 1 deletion backend/harps/train/_spike_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""

from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import List
import csv
from pathlib import Path
Expand Down
1 change: 0 additions & 1 deletion backend/harps/train/mlp_trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from __future__ import annotations
from dataclasses import dataclass

import numpy as np
import torch.nn as nn

from .trainer import Trainer, TrainConfig
Expand Down
4 changes: 2 additions & 2 deletions backend/harps/train/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"""

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional, List, Tuple
from dataclasses import dataclass
from typing import Optional, List
import math

import torch
Expand Down
1 change: 0 additions & 1 deletion backend/harps/transforms/spatial/psf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"""

from __future__ import annotations
from typing import Optional
import numpy as np

try:
Expand Down
1 change: 0 additions & 1 deletion backend/harps/transforms/temporal/psf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"""

from __future__ import annotations
from typing import Optional
import numpy as np

try:
Expand Down
1 change: 0 additions & 1 deletion backend/harps/utils/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""

import os
import numpy as np
import pandas as pd
from sklearn.metrics import accuracy_score, f1_score, confusion_matrix

Expand Down
2 changes: 2 additions & 0 deletions backend/routers/speech.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ async def upload_speech(
"signs": sasl["signs"],
"language": result.get("language", "en"),
"confidence": result.get("confidence", 0.0),
"sign_coverage": sasl.get("sign_coverage", 1.0),
"fingerspelled_words": sasl.get("fingerspelled", []),
}
except HTTPException:
raise
Expand Down
2 changes: 0 additions & 2 deletions backend/services/harps_recognizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"""

from __future__ import annotations
import asyncio
import json
import logging
import os
Expand Down Expand Up @@ -81,7 +80,6 @@ def _ensure_loaded(self) -> bool:
logger.warning("HARPS checkpoint not found at %s — falling back to Ollama", _CKPT_PATH)
return False
try:
import torch
from backend.harps.train.checkpoint import load_checkpoint
from backend.harps.models import MLPClassifier
from backend.harps.utils.scaler import FeatureScaler
Expand Down
2 changes: 1 addition & 1 deletion backend/services/mediapipe_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"""

from __future__ import annotations
from typing import List, Dict, Any, Optional
from typing import List, Dict, Optional
import numpy as np

# Number of MediaPipe landmarks per hand
Expand Down
4 changes: 4 additions & 0 deletions backend/services/sasl_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ def _build_result(signs, gloss_text, english, **extras):
return _build_result(
sign_names, response.gloss_text, text,
non_manual_markers=response.non_manual_markers or [],
sign_coverage=response.sign_coverage,
fingerspelled=response.fingerspelled_words,
)
except Exception as exc:
logger.warning("[SASL] Transformer failed, falling back: %s", exc)
Expand All @@ -231,6 +233,8 @@ def _build_result(signs, gloss_text, english, **extras):
return _build_result(
rule_signs, rule_response.gloss_text, text,
non_manual_markers=rule_response.non_manual_markers or [],
sign_coverage=rule_response.sign_coverage,
fingerspelled=rule_response.fingerspelled_words,
)
except Exception as rule_err:
logger.warning("[SASL] Rule-based fallback failed: %s", rule_err)
Expand Down
1 change: 0 additions & 1 deletion backend/services/sign_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

from __future__ import annotations
from collections import deque
from typing import List, Optional
import numpy as np


Expand Down
4 changes: 4 additions & 0 deletions backend/ws/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ async def _handle_text(websocket, session, session_id, msg):
"original_input": sasl.get("original_input"),
"session_id": session_id,
"non_manual_markers": sasl.get("non_manual_markers", []),
"sign_coverage": sasl.get("sign_coverage", 1.0),
"fingerspelled_words": sasl.get("fingerspelled", []),
}
await broadcast(session, websocket, out)
await broadcast_all(session, {"type": "turn", "speaker": "hearing"})
Expand Down Expand Up @@ -497,6 +499,8 @@ async def _handle_speech_upload(websocket, session, session_id, msg):
"original_input": sasl.get("original_input"),
"session_id": session_id,
"non_manual_markers": sasl.get("non_manual_markers", []),
"sign_coverage": sasl.get("sign_coverage", 1.0),
"fingerspelled_words": sasl.get("fingerspelled", []),
}
await broadcast(session, websocket, signs_msg)
await broadcast_all(session, {"type": "turn", "speaker": "hearing"})
Expand Down
Loading
Loading