# Project export: BioPrep AI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: AI-powered platform that audits biosensor signals and images, automatically generates preprocessing pipelines, self-tests them, and exports production-ready Dockerized workflows.
- Devpost: https://devpost.com/software/bioprep-ai
- GitHub: https://github.com/KharfiIslam/preprocess-ops
- Video: https://www.youtube.com/embed/D3xlC5VUsek?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — KharfiIslam (19 commits), Dhia El Hak Amani (1 commits)

## Devpost submission (written by the team)

### Inspiration

Biomedical AI starts with high-quality data, but preparing that data is often slow, repetitive, and difficult. Researchers spend countless hours cleaning biosignals, enhancing medical images, selecting preprocessing techniques, validating results, and documenting every step before training a single model. We wanted to build an intelligent assistant that automates this entire workflow while remaining transparent, reproducible, and trustworthy. Instead of replacing domain experts, BioPrep AI accelerates their work by generating validated preprocessing pipelines that can be inspected, tested, and deployed.

### What it does

BioPrep AI transforms raw biosensor data into production-ready preprocessing pipelines. Users simply upload a biosignal (EEG, ECG, EMG, etc.) or biomedical image and describe their objective in plain English. The platform then: Audits the input data Detects quality issues automatically Generates a custom preprocessing pipeline using AI Executes the generated code in a secure sandbox Automatically retries if the pipeline fails Computes before-and-after quality metrics Explains the improvements Generates a complete HTML report Produces a Docker-ready deployment package The platform is available through both a command-line interface and an intuitive Streamlit web application.

### How we built it

BioPrep AI combines deterministic scientific computing with modern AI. Our stack includes: Python OpenAI-compatible models (Codex/OpenAI API) Streamlit NumPy SciPy OpenCV Pillow PyTest Docker The workflow begins by profiling the uploaded data using deterministic signal processing or computer vision techniques. This profile is passed to an LLM, which generates a preprocessing pipeline constrained by trusted templates. Every generated pipeline is executed inside a sandbox, automatically tested, and repaired if necessary. Finally, quality metrics, reports, and deployment artifacts are generated automatically.

### Challenges we ran into

The biggest challenge was making AI-generated code reliable enough for scientific workflows. Instead of trusting generated code directly, we designed a self-healing execution system that validates every generated pipeline. If execution fails, the system retries using the error context before falling back to trusted preprocessing methods. Another challenge was creating a unified architecture capable of handling both biosignals and biomedical images while producing consistent reports and outputs.

### Accomplishments we're proud of

Built a unified preprocessing platform for signals and images Automated pipeline generation using AI Implemented self-healing pipeline execution Added automatic quality evaluation with before/after metrics Generated explainable HTML reports Created both a CLI and a modern Streamlit interface Enabled Docker-ready deployment for reproducible research

### What we learned

This project reinforced that AI performs best when combined with deterministic validation rather than being trusted blindly. We also learned the importance of explainability and reproducibility in scientific software. Researchers need to understand why preprocessing improves data quality, not just receive cleaned outputs.

### What's next

Our roadmap includes: Support for more biosensor modalities Automatic hyperparameter optimization Integration with cloud storage providers Collaboration features for research teams Expanded preprocessing libraries One-click deployment to cloud platforms Integration with downstream machine learning training pipelines

## README (from the GitHub repository)

# BioPrep-AI

A CLI tool and Streamlit web UI that takes a raw biosensor file (signal or image) plus a plain-English description of the sensor and goal, audits it (deterministic math for signals, vision model + CV fallback for images), then generates, self-tests, and containerizes a custom preprocessing pipeline — with a self-healing retry loop if generated code fails.

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21510444.svg)](https://doi.org/10.5281/zenodo.21510444)
---
## Built with Codex & GPT-5.6

Built for OpenAI Build Week 2026, using OpenAI's models in two distinct roles:

Development — Codex was used throughout development to generate and refine
project files and code, including scaffolding the CLI structure and generated-
pipeline templates, refactoring the sandboxing and self-test generation flow,
and sharpening ideas during iteration (tightening error handling, fixing test
isolation issues, improving the audit-report generator).

Runtime — GPT-5.6 is supported as one of several interchangeable, OpenAI-
compatible LLM providers (see "LLM providers" below). When configured, it is
used for interpreting biosensor files during audit (vision-based image
analysis, signal reasoning), selecting preprocessing stages based on the
sensor description and audit results, and explaining quality-impact metrics
in plain language. If no LLM is configured, or a call fails/times out, the
tool falls back to deterministic, offline-safe logic automatically.

---
## Requirements

| Requirement | Version |
|---|---|
| **Python** | 3.11 or higher |
| **OS** | Windows, macOS, or Linux |
| **Disk space** | ~50 MB (dependencies) |
| **Internet** | Only needed for LLM providers (optional) |

### Python packages (installed automatically)

**Core (always installed):**
- `numpy >= 1.26` — array math
- `scipy >= 1.11` — signal processing (bandpass, PSD, Savitzky-Golay)
- `opencv-python-headless >= 4.8` — image processing
- `pillow >= 10.0` — image I/O
- `typer >= 0.12` — CLI framework
- `pytest >= 8.0` — test runner
- `rich >= 13.0` — styled terminal output

**Optional — LLM support:**
- `openai >= 1.40` — enables LLM-powered stage selection and metric annotations

**Optional — Web UI:**
- `streamlit >= 1.30` — browser-based interface with file upload, preview, and downloads

**Optional — file watcher:**
- `watchdog >= 4.0` — efficient file change detection for watch mode

---

## Installation (step by step)

### 1. Clone the repo

```bash
git clone https://github.com/KharfiIslam/BioPrep-AI.git
cd BioPrep-AI-main
```

### 2. Check your Python version

```bash
python --version
# Must show 3.11 or higher
```

If you don't have Python 3.11+, download it from https://www.python.org/downloads/

### 3. Install all dependencies

**Windows (double-click `run.bat`):**
A launcher menu will appear — pick option 3 to install, then 1 or 2 to run.

**Or run from terminal:**

```bash
# Install everything (core + LLM + web UI):
python -m pip install -e ".[ui,llm]"

# Or install only core (no LLM, no web UI):
python -m pip install -e .

# Or use requirements.txt directly:
python -m pip install -r requirements.txt
```

### 4. Generate sample files (optional)

```bash
python scripts/make_samples.py
```

This creates demo files in `samples/` (EEG CSV, ECG NPY, lateral-flow JPG).

### 5. Verify installation

```bash
python -m pytest tests/ -v
# Should show all tests passing
```

---

## How to run

### Option A: Launcher script (easiest)

**Windows:** Double-click `run.bat` — a menu appears:

```
============================================
   preprocess-ops launcher
============================================

  Choose an option:

    1) CLI menu (terminal)
    2) Web UI (browser)
    3) Install / update dependencies
    4) Run tests
    5) Quit

  Enter number (1-5):
```

Pick **1** for the terminal menu, **2** for the browser UI.

**Linux / macOS:**
```bash
chmod +x run.sh
./run.sh
```

### Option B: CLI menu (interactive)

```bash
python -m preprocess_ops
```

A rich terminal menu guides you through:
1. Audit a file
2. Generate a pipeline (audit + sandbox + self-heal)
3. Full demo (generate → test → build → report)
4. Run tests
5. Build Dockerfile
6. Configure LLM
7. Launch web UI
8. Quit

### Option C: CLI one-shot commands

```bash
# Audit a signal file:
python -m preprocess_ops audit samples/eeg_sample.csv \
  --describe "3-electrode dry EEG, want it clean for seizure classification"

# Generate a pipeline (offline, no LLM):
python -m preprocess_ops generate samples/eeg_sample.csv \
  --describe "dry EEG cleanup" --offline

# Generate with LLM + full report:
python -m preprocess_ops generate samples/lateral_flow_strip.jpg \
  --describe "lateral flow strip quantification" --offline

# Run all tests:
python -m preprocess_ops test

# Build Docker image:
python -m preprocess_ops build --run

# Batch process a directory:
python -m preprocess_ops batch samples/ --describe "batch cleanup" --offline

# Watch for file changes:
python -m preprocess_ops watch samples/eeg_sample.csv --poll 2
```

### Option D: Web UI (browser)

```bash
python -m streamlit run preprocess_ops/ui/streamlit_app.py
```

Opens `http://localhost:8501` with four pages:

| Page | What it does |
|---|---|
| **Home** | Overview, feature summary, LLM status |
| **Pipeline** | Upload file → preview → configure → run → download artifacts |
| **LLM Config** | Set up provider, API key, model — save for reuse |
| **Batch** | Upload multiple files, process all at once |

---

## What the tool produces

Every pipeline run creates these files in `output/`:

| File | Description |
|---|---|
| `profile.json` | Audit results (shape, sampling rate, PSD peaks, lighting, blur, ROI) |
| `pipeline.py` | Generated, inspectable, editable preprocessing script |
| `cleaned.npy` / `cleaned.png` | Cleaned output (signal or image) |
| `cleaned.metrics.json` | ROI coordinates or intensity metrics (images only) |
| `quality_comparison.json` | Before/after quality deltas with percent changes |
| `audit_report.html` | One-page HTML report bundling everything |
| `test_generated_pipeline.py` | Pytest contract test for the generated pipeline |
| `test_result.json` | Test pass/fail status and pytest output |
| `generation.json` | Pipeline metadata (attempts, fallback status, sandbox transcript) |

---

## Quality metrics

Every run computes before/after quality deltas:

```
╭──────────── QUALITY IMPACT ────────────╮
│  Noise (raw)      0.412                │
│  Noise (cleaned)  0.087   ▼ 78.9%      │
│  Sharpness (raw)  152.7                │
│  Sharpness (clean) 210.3   ▲ 37.7%     │
╰─────────────────────────────────────────╯
```

**Metric annotations** explain *why* each metric changed:

- **LLM-generated** — configured model explains each delta in plain English
- **Rule-based** — deterministic fallback using a stage-effect knowledge base
- **Mixed** — LLM explains some metrics, rule-based covers the rest

---

## Audit report

The HTML report bundles everything into a single shareable document:

1. **Audit summary** — modality, profiler, shape, sampling rate / lighting / blur / ROI
2. **Quality impact** — before/after metrics with percent deltas
3. **Why metrics changed** — LLM or rule-based annotations per metric
4. **Sandbox result** — pipeline source, attempts, transcript
5. **Generated pipeline** — full syntax-highlighted code
6. **Test results** — PASSED/FAILED with pytest output
7. **Artifacts** — file paths

Generate it with:
```bash
python -m preprocess_ops report --output-dir output/
```

---

## LLM providers (optional)

Install LLM support:
```bash
python -m pip install -e ".[llm]"
```

| Provider | Setup |
|---|---|
| **OpenAI** | `PREPROCESS_OPS_PROVIDER=openai` + `OPENAI_API_KEY=...` |
| **Groq** | `PREPROCESS_OPS_PROVIDER=groq` + `GROQ_API_KEY=...` |
| **OpenRouter** | `openrouter` + key — Kimi, GLM, and many others |
| **Kimi / Moonshot** | `PREPROCESS_OPS_PROVIDER=kimi` + API key |
| **GLM / Zhipu** | `PREPROCESS_OPS_PROVID

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 45 recognized source files, 167 KB.
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- Streamlit (technology) — detected in the code
- CSS (language) — claimed on Devpost, not found in the code
- Docker (technology) — claimed on Devpost, not found in the code
- HTML (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (53 of 53)

```
.gitignore
docker/Dockerfile.template
LICENSE
preprocess_ops/__init__.py
preprocess_ops/__main__.py
preprocess_ops/annotations.py
preprocess_ops/audit/__init__.py
preprocess_ops/audit/dispatcher.py
preprocess_ops/audit/image_profiler.py
preprocess_ops/audit/signal_io.py
preprocess_ops/audit/signal_profiler.py
preprocess_ops/cli.py
preprocess_ops/constants.py
preprocess_ops/filters/__init__.py
preprocess_ops/filters/image/__init__.py
preprocess_ops/filters/image/color_space_transform.py
preprocess_ops/filters/image/illumination_correction.py
preprocess_ops/filters/image/intensity_quantify.py
preprocess_ops/filters/image/roi_detection.py
preprocess_ops/filters/signal/__init__.py
preprocess_ops/filters/signal/baseline_correction.py
preprocess_ops/filters/signal/butterworth_bandpass.py
preprocess_ops/filters/signal/savgol_smooth.py
preprocess_ops/filters/signal/savgol.py
preprocess_ops/llm.py
preprocess_ops/logging.py
preprocess_ops/metrics.py
preprocess_ops/present.py
preprocess_ops/report.py
preprocess_ops/service.py
preprocess_ops/synth/__init__.py
preprocess_ops/synth/class_template.py
preprocess_ops/synth/generator.py
preprocess_ops/synth/prompt_templates.py
preprocess_ops/testgen/__init__.py
preprocess_ops/testgen/test_template.py
preprocess_ops/ui/__init__.py
preprocess_ops/ui/streamlit_app.py
pyproject.toml
README.md
requirements.txt
run.bat
run.sh
samples/ecg_sample.npy
samples/eeg_sample.csv
scripts/make_samples.py
tests/test_audit.py
tests/test_generator.py
tests/test_image_filters.py
tests/test_llm.py
tests/test_metrics.py
tests/test_signal_filters.py
Updates.md
```

### Dependencies

- pyproject.toml: numpy@>=1.26, openai@>=1.40, opencv-python-headless@>=4.8, pillow@>=10.0, pytest@>=8.0, rich@>=13.0, scipy@>=1.11, streamlit@>=1.30, typer@>=0.12, watchdog@>=4.0
- requirements.txt: numpy@>=1.26, openai@>=1.40, opencv-python-headless@>=4.8, pillow@>=10.0, pytest@>=8.0, rich@>=13.0, scipy@>=1.11, streamlit@>=1.30, typer@>=0.12, watchdog@>=4.0

### Recent commits (newest first)

- Update README.md
- Add DOI badge to README
- Update clone instructions in README
- Update README.md
- Update README.md
- Update README.md
- Update LICENSE
- Create LICENSE
- Add files via upload
- Add files via upload
- Add files via upload
- Add files via upload
- Add files via upload
- Add files via upload
- Add files via upload
- Add files via upload
- Add files via upload
- Increase timeout for chat_vision function
- Refactor tests to use tmp_path for isolation
- Surface vision fallback reason in audit output

## Key source files (fetched from GitHub, selected and truncated for size)

### Updates.md

```markdown
# Updates.md — preprocess-ops session changelog

This document captures every change made to `preprocess-ops` in this working session, grouped by theme. All work keeps the project's core guarantees: deterministic signal audit, scaffolded/whitelisted generation, sandbox self-healing, and a shared `profile.json` contract.

---

## 1. Signal correctness & robustness

- **Multi-channel signal support** (new module `preprocess_ops/audit/signal_io.py`)
  - Single shared loader `load_signal_file` used by the profiler, the generated `pipeline.py` template, and the generated test — removes duplicated `genfromtxt` parsing.
  - Preserves channel structure: `profile["shape"] = [n_samples, n_channels]` and `profile["n_channels"]` are reported; the generated pipeline and sandbox check operate per-channel (along `axis=0`).
- **Loud sampling-rate assumption**
  - When no rate is supplied and none can be inferred from timestamps, `signal_profiler` emits a `UserWarning` and sets `profile["sampling_rate_assumed"] = true`. The CLI prints a red warning.
- **Band/PSD upper-bound guard**
  - `signal_profiler` warns when the Nyquist is at/below the default 45 Hz bandpass highcut, and reports `spectral.nyquist_hz` and `spectral.band_warning` in the profile.
- **Timestamp false-positive fix** (`signal_io`)
  - A first column is only treated as a timestamp when it is strictly monotonic (increasing or decreasing). An oscillating signal channel no longer triggers a spurious "looks like time" warning. Warns if it looks time-like but isn't monotonic.

## 2. Audit & generation hardening

- **Profile contract validation** (`audit/dispatcher.py`)
  - `validate_profile(profile)` rejects malformed profiles (`InvalidProfileError`) before they reach the generator. Enforced inside `audit_file`.
- **Render provenance gate** (`synth/class_template.py`)
  - `render_pipeline` now calls `_validate_render_config`, refusing any config whose modality or stages fall outside the trusted whitelist (defense-in-depth against a tampered `profile.json`/`generation.json`).
- **Stronger image sandbox** (`synth/generator.py`)
  - Beyond decode failure, the sandbox now rejects empty (0×0) image outputs and non-finite pixel values.
- **Secure LLM config** (`llm.py`)
  - `save_llm_config_file` now `chmod 600` the key file (best-effort, cross-platform).
- **Trimmed LLM prompt** (`synth/prompt_templates.py`)
  - `_summarize_profile` sends only relevant fields (peaks, rate, ROI, assessment) instead of the full statistics blob.
- **`max_tokens` on chat calls** (`llm.py`)
  - `chat_text`/`chat_vision` now pass `max_tokens=1024` to avoid truncated JSON.
- **Redacted sandbox transcript** (`synth/generator.py`)
  - `generation.json` stores a truncated, path-scrubbed transcript (`_redact_transcript`); the full transcript is still available via debug logging.
- **Lighter trusted offline defaults** (`synth/generator.py`)
  - Offline/fallback runs a conservative default stage set: `butterworth_bandpass` for signals; 
[truncated — 2828 more characters]
```

### requirements.txt

```
# Core dependencies (required)
numpy>=1.26
scipy>=1.11
opencv-python-headless>=4.8
pillow>=10.0
typer>=0.12
pytest>=8.0
rich>=13.0

# Optional: LLM support (for smarter pipeline proposals + metric annotations)
openai>=1.40

# Optional: Web UI (Streamlit)
streamlit>=1.30

# Optional: file watcher (for watch mode)
watchdog>=4.0

```

### pyproject.toml

```
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "preprocess-ops"
version = "0.1.0"
description = "Audit raw biosensor files and generate self-tested preprocessing pipelines."
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
  "numpy>=1.26",
  "scipy>=1.11",
  "opencv-python-headless>=4.8",
  "pillow>=10.0",
  "typer>=0.12",
  "pytest>=8.0",
  "rich>=13.0",
]

[project.optional-dependencies]
llm = ["openai>=1.40"]
watch = ["watchdog>=4.0"]
ui = ["streamlit>=1.30"]

[project.scripts]
preprocess-ops = "preprocess_ops.cli:app"
preprocess-ops-web = "preprocess_ops.ui.streamlit_app:main"

[tool.setuptools.packages.find]
where = ["."]
include = ["preprocess_ops*"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]

```

### preprocess_ops/cli.py

```python
"""Typer CLI for audit → generate → test → build, plus a simple interactive menu."""

from __future__ import annotations

import json
import os
from pathlib import Path
import shutil
import subprocess
import sys

import typer

from preprocess_ops.audit.dispatcher import UnsupportedInputError, audit_file
from preprocess_ops.constants import DEFAULT_OUTPUT_DIR, IMAGE_SUFFIXES, PROJECT_ROOT, SIGNAL_SUFFIXES
from preprocess_ops.llm import (
    LOCAL_PROVIDERS,
    NEEDS_BASE_URL,
    PROVIDER_DEFAULTS,
    apply_llm_settings,
    clear_llm_settings,
    load_llm_config_file,
    llm_is_available,
    mask_api_key,
    resolve_llm_config,
    sanitize_api_key,
    save_llm_config_file,
)
from preprocess_ops.logging import configure_logging
from preprocess_ops.present import (
    print_audit,
    print_error,
    print_generate_result,
    print_json,
    print_quality_impact,
    print_stage_plan,
    print_welcome,
    status_line,
)
from preprocess_ops.report import load_report_inputs, write_audit_report
from preprocess_ops.service import run_pipeline, run_tests, save_quality_comparison, _extract_stages, _compute_annotations
from preprocess_ops.synth.generator import PipelineGenerator
from preprocess_ops.testgen.test_template import write_generated_test

app = typer.Typer(
    help="Audit and generate robust biosensor preprocessing pipelines.",
    invoke_without_command=True,
    no_args_is_help=False,
)


def _print_audit(profile: dict[str, object]) -> None:
    print_audit(profile)


def resolve_output_dir(output_dir: Path, run_id: str | None) -> Path:
    """Isolate runs: an explicit --run-id nests under the default output dir."""
    if run_id:
        return DEFAULT_OUTPUT_DIR / run_id
    return output_dir


def _parse_roi(value: str | None) -> dict[str, int] | None:
    """Parse a 'x,y,width,height' ROI hint; return None when not supplied."""
    if not value:
        return None
    parts = [piece.strip() for piece in value.split(",")]
    if len(parts) != 4:
        raise typer.BadParameter("--roi must be 'x,y,width,height'.")
    try:
        return {key: int(piece) for key, piece in zip(("x", "y", "width", "height"), parts)}
    except ValueError:
        raise typer.BadParameter("--roi values must be integers.")


def _run_audit(
    file: Path,
    describe: str = "",
    sampling_rate: float | None = None,
    offline: bool = False,
    output_dir: Path = DEFAULT_OUTPUT_DIR,
    roi: dict[str, int] | None = None,
) -> dict[str, object]:
    profile = audit_file(file, describe, sampling_rate, offline, roi=roi)
    output_dir.mkdir(parents=True, exist_ok=True)
    profile_path = output_dir / "profile.json"
    profile_path.write_text(json.dumps(profile, indent=2, allow_nan=False), encoding="utf-8")
    _print_audit(profile)
    typer.secho(f"Saved profile: {profile_path}", fg=typer.colors.GREEN)
    return profile


def _on_attempt(kind: str, attempt: int, info: dict[str, object]) -> None:
    if kind == "propose":
        used = "LLM" if info["used_llm"] else "trusted"
        status_line(f"attempt {attempt}: proposing from {used} - stages {info['stages']}")
    elif kind == "sandbox":
        ok = bool(info["ok"])
        note = "" if ok else f" ({str(info['transcript'])[:120]})"
        status_line(f"attempt {attempt}: sandbox {'self-test passed' if ok else 'failed'}{note}", ok=ok)


def _run_generate(
    file: Path,
    describe: str = "",
    sampling_rate: float | None = None,
    offline: bool = False,
    output_dir: Path = DEFAULT_OUTPUT_DIR,
    roi: dict[str, int] | None = None,
    as_json: bool = False,
    show_code: bool = True,
    run_tests: bool = False,
    write_report: bool = True,
) -> object:
    if as_json:
        result = run_pipeline(
            file,
            describe=describe,
            sampling_rate=sampling_rate,
            offline=offline,
            output_dir=output_dir.resolve(),
            roi=roi,
            run_tests_flag=run_tests,
            write_report=write_report,
            on_attempt=_on_attempt,
        )
        _print_audit(result.profile)
        print_json(result.generation)
        return result.generation

    profile = audit_file(file, describe, sampling_rate, offline, roi=roi)
    _print_audit(profile)
    generator = PipelineGenerator(offline=offline)
    generation = generator.generate(
        file.resolve(), profile, describe, output_dir.resolve(), on_attempt=_on_attempt
    )
    test_path = write_generated_test(
        output_dir.resolve(), file.resolve(), Path(generation.cleaned_path), profile["modality"]
    )
    quality_comparison = save_quality_comparison(
        file.resolve(),
        Path(generation.cleaned_path),
        profile["modality"],
        output_dir.resolve(),
    )
    print_quality_impact(quality_comparison)
    report_path = None
    if write_report:
        from dataclasses import asdict

        stages = _extract_stages(Path(generation.pipeline_path))
        annotations, annotations_source = _compute_annotations(
            quality_comparison, generation, stages, describe
        )
        report_path = write_audit_report(
            input_path=file.resolve(),
            output_dir=output_dir.resolve(),
            profile=profile,
            quality_comparison=quality_comparison,
            generation=asdict(generation),
            description=describe,
            metric_annotations=annotations,
            annotations_source=annotations_source,
        )
    print_generate_result(generation, test_path, show_code=show_code)
    if report_path is not None:
        typer.secho(f"Saved audit report: {report_path}", fg=typer.colors.GREEN)
    return generation


def _run_test(output_dir: Path = DEFAULT_OUTPUT_DIR) -> int:
    result = run_tests(output_dir.resolve())
    if result["passed"]:
        typer.secho("Tests passed.", fg=typer.colors.GREEN)
    else:
        typer.echo(result["output"])
    return int(result["returncode"])


def _run_build(output_dir: Path = DEFA
[truncated — 21048 more characters]
```

### run.sh

```shell
#!/usr/bin/env bash
set -e

BOLD="\033[1m"
CYAN="\033[36m"
GREEN="\033[32m"
YELLOW="\033[33m"
RESET="\033[0m"

clear
echo -e "${CYAN}${BOLD}"
echo "============================================"
echo "   preprocess-ops launcher"
echo "============================================"
echo -e "${RESET}"
echo ""
echo "  Choose an option:"
echo ""
echo "    1) CLI menu (terminal)"
echo "    2) Web UI (browser)"
echo "    3) Install / update dependencies"
echo "    4) Run tests"
echo "    5) Quit"
echo ""
read -rp "  Enter number (1-5): " choice

case "$choice" in
    1)
        echo ""
        echo "  Starting CLI menu..."
        echo ""
        python3 -m preprocess_ops
        ;;
    2)
        echo ""
        echo "  Starting Streamlit web UI..."
        echo "  Browser will open at http://localhost:8501"
        echo "  Press Ctrl+C to stop the server."
        echo ""
        python3 -m streamlit run preprocess_ops/ui/streamlit_app.py
        ;;
    3)
        echo ""
        echo "  Installing dependencies..."
        echo ""
        python3 -m pip install -e ".[ui,llm]"
        echo ""
        echo -e "  ${GREEN}Done.${RESET} You can now use options 1 or 2."
        echo ""
        read -rp "  Press Enter to continue..."
        ;;
    4)
        echo ""
        echo "  Running tests..."
        echo ""
        python3 -m pytest tests/ -v
        echo ""
        read -rp "  Press Enter to continue..."
        ;;
    5)
        exit 0
        ;;
    *)
        echo ""
        echo "  Invalid option. Try again."
        sleep 1
        ;;
esac

```

### preprocess_ops/__main__.py

```python
from preprocess_ops.cli import app

if __name__ == "__main__":
    app()

```

### preprocess_ops/__init__.py

```python
"""preprocess-ops: reproducible preprocessing for raw biosensor files."""

__version__ = "0.1.0"

```

### preprocess_ops/constants.py

```python
from pathlib import Path

SIGNAL_SUFFIXES = {".csv", ".npy"}
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg"}
PROJECT_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "output"
LLM_CONFIG_PATH = PROJECT_ROOT / ".preprocess_ops_llm.json"

```

### preprocess_ops/logging.py

```python
"""Small structured logger with verbosity control shared across the CLI."""

from __future__ import annotations

import logging

_LOGGER = logging.getLogger("preprocess_ops")

_VERBOSE = False


def configure_logging(verbose: bool = False) -> None:
    global _VERBOSE
    _VERBOSE = verbose
    level = logging.DEBUG if verbose else logging.INFO
    if not _LOGGER.handlers:
        handler = logging.StreamHandler()
        handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
        _LOGGER.addHandler(handler)
    _LOGGER.setLevel(level)
    _LOGGER.propagate = False


def get_logger() -> logging.Logger:
    if not _LOGGER.handlers:
        configure_logging(_VERBOSE)
    return _LOGGER


def debug(message: str) -> None:
    get_logger().debug(message)


def info(message: str) -> None:
    get_logger().info(message)

```

### tests/test_image_filters.py

```python
import numpy as np

from preprocess_ops.filters.image import (
    crop_roi,
    illumination_correction,
    intensity_quantify,
    to_lab,
)


def _strip_like_image() -> np.ndarray:
    image = np.full((120, 320, 3), 200, dtype=np.uint8)
    image[:, :80] = (120, 120, 120)
    image[40:80, 140:160] = (30, 30, 200)
    return image


def test_illumination_correction_returns_same_shape():
    image = _strip_like_image()
    corrected = illumination_correction(image)
    assert corrected.shape == image.shape
    assert corrected.dtype == np.uint8


def test_crop_roi_honours_bounds():
    image = _strip_like_image()
    cropped, roi = crop_roi(image, {"x": 10, "y": 20, "width": 50, "height": 40})
    assert cropped.shape == (40, 50, 3)
    assert roi == {"x": 10, "y": 20, "width": 50, "height": 40}


def test_color_and_intensity_metrics_are_finite():
    image = _strip_like_image()
    lab = to_lab(image)
    metrics = intensity_quantify(image)
    assert lab.shape == image.shape
    assert all(np.isfinite(value) for value in metrics.values())

```

[35 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]