Generate playable and explainable canon variants from a monophonic melody.
Project description
Kithairon
Kithairon is a small symbolic music compiler that turns a monophonic melody into several playable canon variants. It uses explicit canon transformations to guarantee strict canon candidates, then applies explainable harmony and voice-leading rules to score them. When strict candidates are weak, optional repair and CP-SAT solver engines can produce relaxed canon variants while preserving a clear link to the original melody.
Use it from the canonize CLI with MIDI or MusicXML input. Each generation run writes playable files, a machine-readable result file, the resolved config, and a Markdown report.
Read the hosted documentation at https://kithairon-docs.pages.dev.
30-Second Demo
Run Kithairon on a longer Bach-derived MusicXML fragment:
uv run canonize generate examples/melodies/bach_wtc1_c_major_prelude_upper.musicxml \
--out tmp/demo \
--engine repair \
--top-k 12
The input is a two-measure monophonic incipit derived from the public-domain BWV 846 entry shipped in the music21 corpus. The generated output includes playable canon candidates with:
tmp/demo/candidates/*.musicxml: a score you can open in a notation editor.tmp/demo/candidates/*.mid: a playable MIDI file.tmp/demo/report.md: the score summary and rule penalties.tmp/demo/visualization.json: data for the web visualization.
The preview below uses the rank 1 repair candidate from this run: repaired inversion, delay 4, score 86.0. In an external listening pass over the main demo candidates, this was the most pleasant clip even though a stricter transposition candidate scored higher under Kithairon's rule penalties.
Listen to the selected candidate: MP3 or MIDI.
To inspect the same run in the browser, start the visualization API and frontend as described in Visualization, then upload examples/melodies/bach_wtc1_c_major_prelude_upper.musicxml.
The score breakdown view shows the tradeoff: the selected clip sounds better than the best strict fallback, but it still pays weak-beat dissonance penalties under the current scoring model.
Additional preview picks are available in docs/assets/demo: the best strict fallback, output-strict-transposition-delay4.*, and a second strict alternative with a wider delay.
Requirements
- Python 3.12 or newer
uv
Install
Install the default package and development dependencies:
uv sync
After the package-index release is published, install the CLI from PyPI with:
uv tool install kithairon-canon
Install OR-Tools when you want the CP-SAT solver engine:
uv sync --extra solver
Install documentation dependencies only when previewing the MkDocs site:
uv sync --extra docs
Quickstart
Validate an input melody:
uv run canonize validate examples/melodies/scale_c_major.musicxml
Generate the top three strict canon candidates:
uv run canonize generate examples/melodies/scale_c_major.musicxml \
--out tmp/strict \
--engine strict \
--top-k 3
The output directory contains:
results.json: full candidate data, scores, violations, transforms, and output paths.report.md: a readable summary of top candidates and penalty reasons.resolved_config.toml: the exact config used for the run.visualization.json: normalized data for the web visualization.artifact_index.json: the safe download map used by the API and web UI.candidates/*.musicxmlandcandidates/*.mid: playable exports for each candidate.
A successful command returns a compact JSON payload:
{
"status": "ok",
"engine": "strict",
"out": "tmp/strict",
"candidates": 3,
"results": "tmp/strict/results.json",
"report": "tmp/strict/report.md",
"resolved_config": "tmp/strict/resolved_config.toml",
"visualization": "tmp/strict/visualization.json",
"artifact_index": "tmp/strict/artifact_index.json"
}
If --out already exists and is not empty, Kithairon writes to a suffixed directory such as tmp/strict-1. To replace the existing output directory, put --overwrite before the subcommand:
uv run canonize --overwrite generate examples/melodies/scale_c_major.musicxml \
--out tmp/strict \
--engine strict
Composition Assistance
Use the browser UI when you want to keep iterating on one candidate instead of only generating a fresh run.
Start the API server from the repository root:
uv sync --group dev --extra solver --extra visual
uv run canonize-web --output-root ./runs --host 127.0.0.1 --port 8000
Start the frontend in another shell:
cd web
pnpm install --frozen-lockfile
pnpm dev
Open the Vite URL, upload a melody, and select a candidate. From there you can:
- run local
Phrase Polishover a bar range - keep one voice fixed and rewrite the other with fixed-voice invention
- translate feedback such as
bass too staticortoo repetitiveinto structured polish requests - trigger polish directly from phrase, cadence, and bass findings in the
Analysispanel - keep saved experiment variants in
Candidate Laband reopen them after reload for more polish, compare, or export
See Visualization for the full browser workflow.
Engine Examples
Strict generation keeps the follower voice as an exact transform of the input melody:
uv run canonize generate examples/melodies/scale_c_major.musicxml \
--out tmp/strict \
--engine strict \
--top-k 3
Repair generation starts from strict candidates and edits a limited number of follower notes when that improves the score:
uv run canonize generate examples/melodies/bad_for_canon.musicxml \
--out tmp/repair \
--engine repair \
--top-k 3
Solver generation uses OR-Tools CP-SAT to search for a relaxed follower voice under edit limits:
uv sync --extra solver
uv run canonize generate examples/melodies/bad_for_canon.musicxml \
--out tmp/solver \
--engine solver \
--top-k 1
Auto generation ranks strict candidates first. If the strict pool falls below the configured quality thresholds, it adds repair candidates. It may add solver candidates when the solver dependency is installed and [solver].enabled = true in the resolved config:
uv run canonize generate examples/melodies/bad_for_canon.musicxml \
--out tmp/auto \
--engine auto \
--top-k 4
Strict And Relaxed Canons
A strict canon has strict_canon: true and canon_label: "strict canon" in results.json. The follower is exactly produced from the source melody by the recorded transform_spec, for example transposition, inversion, retrograde, augmentation, or diminution with a delay.
A relaxed canon has strict_canon: false and canon_label: "relaxed canon". It still records the source transform and candidate lineage, but the repair or solver engine may change follower pitches under configured edit limits. The report includes the edit plan and the rule penalties that remain after the edits.
For the musical assumptions behind the score, see Scoring And Rules.
Input Files
Supported input formats:
- MIDI:
.mid,.midi - MusicXML:
.musicxml,.xml,.mxl
Inputs must be monophonic by default. If a MusicXML file contains chords and you want to extract one note from each chord, choose a chord policy:
uv run canonize validate path/to/melody.musicxml --chord-policy top-note
For multi-part scores, select a part with --part-policy and --part-index during validation, or put the same settings in a config file.
Config
Print the resolved default config:
uv run canonize config resolve
Write a TOML config and reuse it:
uv run canonize config resolve --out kithairon.toml
uv run canonize generate examples/melodies/scale_c_major.musicxml \
--config kithairon.toml \
--out tmp/from-config
CLI flags override config values for the same run:
uv run canonize generate examples/melodies/scale_c_major.musicxml \
--config kithairon.toml \
--engine repair \
--score-profile renaissance-lite \
--top-k 2 \
--out tmp/repair-from-config
The scoring profile defaults to pop-lite. Use --score-profile permissive for looser ranking or --score-profile renaissance-lite for stricter counterpoint-style penalties. The selected profile is recorded in resolved_config.toml, results.json, and the visualization payload.
Errors
Common user errors return JSON diagnostics instead of Python tracebacks. Examples include missing input files, unsupported formats, chord or polyphonic input, invalid config, and missing solver dependencies.
To debug an unexpected exception, put --debug before the subcommand:
uv run canonize --debug generate examples/melodies/scale_c_major.musicxml \
--out tmp/debug \
--engine strict
Development
Run the full local gate:
uv run ruff check .
uv run ruff format --check .
uv run pyright
uv run coverage run -m pytest
uv run coverage report
uv run pytest benchmarks/test_performance_budgets.py -q
bash scripts/build-docs.sh
Run the Cremona refactor audit:
uv run coverage run -m pytest -q
uv run coverage json -o coverage.json
uv run cremona scan --baseline quality/refactor-baseline.json --coverage-json coverage.json --fail-on-regression
Refresh the committed baseline only after structural debt is intentionally reduced or Cremona changes its baseline schema:
uv run cremona scan --update-baseline
Preview the documentation site:
uv run --extra docs mkdocs serve
Build the documentation site for deployment:
bash scripts/build-docs.sh
Deploy the documentation with Cloudflare Pages Git integration using pip install uv && uv run --extra docs mkdocs build --strict as the build command and site as the build output directory. See Deployment for the full Pages settings.
See Package Distribution before publishing to a Python package index. PyPI releases use the kithairon-canon distribution name.
See CONTRIBUTING.md for pull request checks and CHANGELOG.md for release notes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kithairon_canon-0.1.2.tar.gz.
File metadata
- Download URL: kithairon_canon-0.1.2.tar.gz
- Upload date:
- Size: 74.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a795c3e8728e314cb676c059e781cfe0b697c84aefb3ae2586db5d9538d19ed7
|
|
| MD5 |
d7b65831bd083b48f8e22cdd70fc9101
|
|
| BLAKE2b-256 |
5f8f5ebdba437c3b2e4bd4b0d8b61ee67347d2ee8ecc13b877db687384c7e0e5
|
File details
Details for the file kithairon_canon-0.1.2-py3-none-any.whl.
File metadata
- Download URL: kithairon_canon-0.1.2-py3-none-any.whl
- Upload date:
- Size: 105.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01963e70efa67e1d1a7560a2da358531d4aef3a245dc787222180330f2963cb1
|
|
| MD5 |
e98619cd779e62ffd17382f8f66f8fa6
|
|
| BLAKE2b-256 |
a45d200972737bfa86e34e356fc7055e5f9aaf2d2377342efd248a2c85240d3f
|