Skip to content
Closed
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
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI

on:
push:
branches: [ main, development ]
pull_request:
branches: [ main, development ]

jobs:
test:
name: test (${{ matrix.os }}, py${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest ]
python-version: [ "3.11", "3.12" ]

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: "pyproject.toml"

- name: Install Linux dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libportaudio2 libasound2-dev

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev,nlp,whisper]

- name: Run Ruff Linter
run: ruff check src/

- name: Run Mypy Type Checker
run: mypy src/

- name: Run Safety Check
run: safety check

- name: Run Pytest with Coverage
# GUI tests might require mock display on Ubuntu. Let's use xvfb on Ubuntu runner.
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get install -y xvfb
xvfb-run --server-args="-screen 0 1024x768x24" pytest tests/ -v --cov=src --cov-fail-under=80 --cov-report=xml

- name: Run Pytest with Coverage (Windows)
if: matrix.os == 'windows-latest'
run: pytest tests/ -v --cov=src --cov-fail-under=80 --cov-report=xml
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ models/
logs/
*.log
.gemini/
.coverage
htmlcov/
.pytest_cache/
.mypy_cache/
.ruff_cache/
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Taskify Makefile target shortcuts

.PHONY: dist clean test lint typecheck

dist:
python packaging/build.py

clean:
python -c "import shutil; [shutil.rmtree(p, ignore_errors=True) for p in ('build', 'dist')]"

test:
pytest tests/ -v

lint:
ruff check src/ --fix

typecheck:
mypy src/
144 changes: 144 additions & 0 deletions docs/adr/001-mobile-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# ADR 001 — Mobile Compatibility Strategy (Android via Chaquopy / Kivy)

| Field | Value |
|-------|-------|
| **Status** | Accepted |
| **Date** | 2026-07-18 |
| **Deciders** | Nihal Rodge |
| **Issue** | [#21 — Refactor core pipeline to support Android deployment](https://github.com/MrSpideyNihal/Taskify/issues/21) |

---

## Context

Taskify's desktop implementation depends on several packages that are not available or
appropriate on Android:

| Package | Reason for desktop-only classification |
|---------|---------------------------------------|
| `sounddevice` | Wraps PortAudio — not available on Android |
| `soundfile` | Wraps libsndfile — not packaged for Android |
| `vosk` (PyPI wheel) | Desktop CPython wheel; Android uses the Vosk Android AAR |
| `customtkinter` | Wraps Tkinter/Tk — not available on Android |
| `httpx` | Used only for Ollama LLM backend (network calls) — not needed in mobile-only NLP mode |

The goal is to allow the pure-Python pipeline logic (scheduler, storage, NLP/rule-based
extraction, export) to be installed and used on Android without pulling in any of the
above packages.

---

## Decision

### 1. `core_mobile` extras group in `pyproject.toml`

A new optional-dependency group `core_mobile` is introduced:

```toml
[project.optional-dependencies]
core_mobile = [
"numpy>=1.24.0", # Used by audio DSP helpers and NLP numeric ops
"click>=8.1.0", # CLI entry-point (headless / shell usage)
]
```

Installing with:
```bash
pip install taskify[core_mobile]
```
…pulls **only** `numpy` and `click` alongside the pure-Python core, without
`sounddevice`, `soundfile`, `vosk`, `customtkinter`, or `httpx`.

### 2. Dependency boundaries per subsystem

```
taskify/
├── audio/ # DESKTOP ONLY — wraps sounddevice / soundfile
│ Android replacement: AudioRecord via Chaquopy Java bridge
├── stt/
│ ├── vosk_engine.py # DESKTOP ONLY — PyPI vosk wheel
│ ├── whisper_engine.py # DESKTOP ONLY — faster-whisper
│ └── base.py # MOBILE SAFE — abstract STTEngine interface
│ Android target: Vosk Android AAR via Chaquopy JNI
├── llm/
│ ├── ollama_backend.py # DESKTOP ONLY — requires httpx + running Ollama daemon
│ ├── nlp_backend.py # MOBILE SAFE — pure spaCy / rule-based, no network
│ └── base.py # MOBILE SAFE — abstract LLMBackend interface
├── pipeline/ # MOBILE SAFE — scheduler is platform-agnostic threading
├── storage/ # MOBILE SAFE — sqlite3 is built into CPython/Chaquopy
├── export/ # MOBILE SAFE — file I/O only
├── config/ # MOBILE SAFE — TOML parsing, stdlib only
├── ui/ # DESKTOP ONLY — CustomTkinter / Tkinter
│ Android replacement: Kivy layout or native Android XML
└── cli/ # MOBILE SAFE — Click commands work headlessly
```

### 3. Recommended mobile STT target

Use the [Vosk Android AAR](https://alphacephei.com/vosk/android) via Chaquopy's Java
interop bridge. The existing `STTEngine` abstract base class in `stt/base.py` already
defines the interface; a `VoskAndroidEngine` adapter should:

1. Accept audio bytes from `AudioRecord` via Chaquopy.
2. Call `model.recognize(bytes)` through the AAR JNI bridge.
3. Return a `TranscriptSegment` — identical to the desktop flow.

### 4. Recommended mobile LLM/NLP target

Use **rule-based NLP only** (`nlp_backend.py`). The spaCy model (`en_core_web_sm`) must
be bundled as a `.whl` asset inside the APK via Chaquopy's `requirements`. Ollama
backend is excluded — no outbound network calls are permitted under the privacy policy.

### 5. GUI strategy

Two viable paths:

| Option | Trade-offs |
|--------|-----------|
| **Kivy** | Pure-Python, cross-platform, mature widget set. Requires porting UI from CustomTkinter. |
| **Native Android XML + Chaquopy** | Native look and feel, better performance. Requires Java/Kotlin UI layer calling Python pipeline via Chaquopy. Recommended for production. |

---

## Consequences

### Positive
- `pip install taskify[core_mobile]` works without any native audio or GUI libraries.
- The abstract `STTEngine` and `LLMBackend` interfaces mean no core pipeline changes
are needed to swap in Android-native implementations.
- `storage/`, `pipeline/`, and `export/` are already fully mobile-safe.

### Negative / Risks
- Vosk Android AAR bridge requires Chaquopy in the Android build; this is a one-time
setup cost but is well-documented.
- spaCy `en_core_web_sm` is ~12 MB; bundling it in the APK increases app size.
- A `VoskAndroidEngine` adapter class must be written and tested on-device.

### Deferred
- The actual Kivy or native Android UI implementation.
- The `VoskAndroidEngine` adapter class.
- Play Store packaging and signing.

---

## Compliance Check

```
pip install taskify[core_mobile] # numpy, click only — no desktop packages
pip install taskify[nlp] # adds spaCy for NLP extraction
pip install taskify[whisper] # adds faster-whisper (desktop only)
pip install taskify[dev] # full development toolchain
pip install taskify # full desktop install
```

The `core_mobile` extras group satisfies the acceptance criterion:
> *`pip install taskify[core_mobile]` installs without any desktop-only packages.*

---

## References

- [Vosk Android AAR documentation](https://alphacephei.com/vosk/android)
- [Chaquopy — Python for Android](https://chaquo.com/chaquopy/)
- [Kivy cross-platform framework](https://kivy.org/)
- [ADR format — Michael Nygard](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
45 changes: 45 additions & 0 deletions packaging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Taskify standalone packaging instructions

This directory holds the specifications and tooling to bundle the Python codebase, CustomTkinter frontend styling, and dependency components into a single standalone binary.

## Requirements

Ensure development dependencies are installed:
```bash
pip install -e .[dev]
```

## Building stand-alone executables

To compile the binary for your active platform:

```bash
python packaging/build.py
```

Or run via the Makefile:
```bash
make dist
```

Upon success, PyInstaller generates output directories:
- `build/`: Temporary files generated during build time.
- `dist/`: Stands for distribution. Contains the single executable `taskify` (or `taskify.exe` on Windows).

## Local model dependency setup

Because Taskify is a fully offline application, speech-to-text models must be available locally. The compiled binary does not include model directories by default to save executable file footprint.

If model directories are missing, the application downloads defaults to the user directory on first launch, or they can be downloaded manually via:

```bash
# Download default Vosk english model
dist/taskify download-model --engine vosk

# Download default Whisper model
dist/taskify download-model --engine whisper
```

Offline models are stored at:
- **Vosk:** `~/.local/share/taskify/models/vosk/` (on Windows `~` refers to `C:\Users\<username>`)
- **Whisper:** standard cache path managed by `huggingface_hub`.
55 changes: 55 additions & 0 deletions packaging/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Programmatic execution of PyInstaller builds for standalone executables."""

import os
import subprocess
import sys
from pathlib import Path


def main() -> None:
"""Execute the PyInstaller command pointing to packaging spec file."""
workspace_dir = Path(__file__).parent.parent.resolve()
spec_file = workspace_dir / "packaging" / "taskify.spec"

if not spec_file.exists():
print(f"Error: Specification file not found at: {spec_file}", file=sys.stderr)
sys.exit(1)

print("=== Taskify Packaging Builder ===")
print(f"Targeting Specification: {spec_file}")
print(f"Working Directory: {workspace_dir}")

# Build the PyInstaller command list
cmd = [
"pyinstaller",
"--clean",
"--noconfirm",
"--distpath",
str(workspace_dir / "dist"),
"--workpath",
str(workspace_dir / "build"),
str(spec_file),
]

print(f"Executing: {' '.join(cmd)}")
try:
# Run pyinstaller build process
result = subprocess.run(
cmd, cwd=str(workspace_dir), check=True, capture_output=False
)
print("=== Build process finished successfully. ===")
sys.exit(result.returncode)
except subprocess.CalledProcessError as e:
print(f"Error: PyInstaller build failed with status {e.returncode}", file=sys.stderr)
sys.exit(e.returncode)
except FileNotFoundError:
print(
"Error: PyInstaller command not found. Please install the dev packages: "
"pip install -e .[dev]",
file=sys.stderr,
)
sys.exit(127)


if __name__ == "__main__":
main()
Loading
Loading