Metadata-Version: 2.5
Name: PCVMF
Version: 0.3.0
Summary: Python Computer Vision Multiprocessing Framework
Author-email: ABDELLAH BOUGATAYA <sience.story@gmail.com>
License: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: numpy>=1.21.0
Requires-Dist: opencv-python>=4.6.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: pyzmq>=24.0.0
Provides-Extra: dev
Requires-Dist: black>=24.0.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.3.0; extra == 'dev'
Description-Content-Type: text/markdown

<p align="center"><img src="img/PCVMF.png" alt="PCVMF Logo" width="180" /></p>

# PCVMF

PCVMF runs computer vision, application control, and sensor workers in independent Python processes. Applications provide plugins and connect their publications through YAML. ZeroMQ carries typed JSON messages; a separate lifecycle channel supervises startup, progress, and shutdown.

Version **0.3.0** introduces a breaking public API. See [migration notes](docs/migration.md). Linux and Python 3.10–3.12 are tested. Scheduling is best-effort, not a hard real-time guarantee.

## Install and run

From a checkout:

```bash
uv sync --frozen --extra dev
uv run pcvmf run
```

Or install the package with `pip install .`, then run `pcvmf run` from any directory. No camera or desktop is needed: packaged defaults use deterministic synthetic frames and disabled visualization. Ctrl+C or SIGTERM shuts the application down.

```bash
pcvmf config validate config/default_config.yaml
pcvmf run --config config/default_config.yaml
python -m pcvmf run
```

`pcvmf` without arguments also runs the packaged demo. Configuration errors exit with code 2; runtime or cleanup failures exit with code 1; normal completion and successful signal shutdown exit with code 0.

## Connect workers

```yaml
logging: {level: INFO}
workers:
  - name: vision
    plugin:
      class: pcvmf.workers:VisionWorker
      options:
        source:
          class: pcvmf.vision.sources:SyntheticSource
          options: {width: 320, height: 240}
        pipeline:
          class: pcvmf.vision.pipeline:ColorTracker
          options: {min_area: 100}
    rate_hz: 30
    publications: [vision/telemetry]
  - name: controller
    plugin: {class: 'pcvmf.workers:ControllerWorker'}
    rate_hz: 50
    subscriptions:
      - {source: vision, topic: vision/telemetry, delivery: latest}
```

Each publisher owns its own endpoint. The runtime allocates a unique IPC directory for each application instance. Subscriptions name an upstream worker and an exact topic; adding another publisher does not require modifying the supervisor or sharing a bind address.

Configuration is checked before processes start: unknown keys, invalid plugin types/options, duplicate names/endpoints, and missing publication references are errors. Plugin imports and validators must not acquire resources. Configuration files select Python code, so use trusted configuration and installed plugins.

See the [configuration and lifecycle reference](docs/configuration.md).

## Develop a plugin

Import extension contracts from `pcvmf.api`. Supply a concrete subclass with a resource-free `validate_options()` method. Put the plugin in your own installable package and reference `your_package.module:Class` in YAML.

```python
from pcvmf.api import ConfigurationError, PipelineResult, VisionPipeline

class MyPipeline(VisionPipeline):
    @classmethod
    def validate_options(cls, options):
        if options:
            raise ConfigurationError("MyPipeline takes no options")

    def initialize(self):
        # Load your model here, inside the vision process.
        pass

    def process(self, frame):
        # frame.image is a BGR NumPy array; return TargetDetection objects.
        return PipelineResult([], "NO_TARGETS")

    def cleanup(self):
        # Must also work if initialize() failed partway through.
        pass
```

Supported contracts include `Worker`, `FrameSource`, `VisionPipeline`, `Controller`, `Visualizer`, `Publisher`, `Subscriber`, and `MessageCodec`. Constructors and validators must remain resource-free. Methods must return within the worker's configured timeouts.

The [plugin guide](docs/plugins.md) explains custom messages and component testing. Two independent examples are provided:

```bash
uv pip install --no-deps ./examples/external_plugins
uv run --no-sync pcvmf run --config examples/vision.yaml
uv run --no-sync pcvmf run --config examples/sensor.yaml
```

`--no-sync` preserves the separately installed example distribution. The vision example supplies an external bright-pixel pipeline. The sensor example supplies a synthetic temperature worker, typed codec, and controller. Both import only public contracts from PCVMF.

## Embed and test

The configuration loader, application runner, and component runners are supported application APIs alongside `pcvmf.api`:

```python
from pcvmf.config import load_config
from pcvmf.runtime import Application

if __name__ == "__main__":
    app = Application(load_config("application.yaml"))
    result = app.run()
    raise SystemExit(result.exit_code)
```

Use the `__main__` guard because workers use multiprocessing's `spawn` context. `Application` does not install signal handlers; an embedding application can call `request_stop()`. Each instance runs once. `RunResult` contains `exit_code`, `errors`, and whether the application became `ready`. An optional `on_event(worker_name, event)` callback receives lifecycle notifications in the supervisor; it must return promptly.

`VisionRunner` and `ControllerRunner` in `pcvmf.runners` accept injectable components and monotonic clocks for synchronous testing without multiprocessing or ZeroMQ. Plugin cleanup does not own framework-provided transports: the runtime closes them after plugin cleanup.

```bash
uv run --frozen pytest
uv run --frozen ruff check .
uv run --frozen black --check .
uv build
```

The suite exercises actual telemetry exchange, multiple publishers, external plugins, scheduling limits, signals, failures, timeouts, and cleanup. If another environment injects unrelated pytest plugins, use `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`.

## Distribution and containers

`requirements.txt` contains locked runtime dependencies; it does not install PCVMF itself. Build and install the wheel separately:

```bash
uv build
uv venv /tmp/pcvmf-wheel
uv pip install --python /tmp/pcvmf-wheel/bin/python -r requirements.txt dist/*.whl
/tmp/pcvmf-wheel/bin/python scripts/smoke.py
```

The smoke script runs the installed demo outside the checkout, waits for readiness, and verifies successful SIGTERM shutdown.

```bash
docker build -t pcvmf:local .
docker run --rm pcvmf:local
docker run --rm pcvmf:local python /opt/pcvmf-smoke.py
```

The image installs a wheel and runs as a non-root user. For a custom application, install its plugin package in a derived image and mount/pass its configuration. GUI support remains opt-in; the standard container runs headlessly.

Regenerate locked requirements after changing dependencies:

```bash
uv lock
uv export --frozen --format requirements-txt --no-hashes --no-dev --no-emit-project -o requirements.txt
```

GitHub and GitLab CI test Python 3.10, 3.11, and 3.12, clean wheel installations, both external examples, and the Docker image. Publishing is handled separately by the release workflow.
