Skip to main content

kernpy: Python Humdrum **kern and **mens utilities

🌐 Parte del ecosistema MuRET / OMR. La documentación y memoria de sincronización cross-project (armonización de versiones, arquitectura, onboarding, pendientes que cruzan repos) vive en MuRET-powered.

License: AGPL v3 Python Version PyPI Docs Tests Contributions welcome

Python package that provides comprehensive tools for working with symbolic modern and mensural notations in Humdrum format. kernpy is a fully open-source project open to contributions.

It extends the canonic tools for processing Humdrum, humlib, for mainly machine learning tasks.

Documentation

Visit the online website: https://kernpy.pages.dev/

Index:

Code examples

Basic Usage

Load a **kern/**mens file into a kp.Document.

import kernpy as kp

# Read a **kern file
document, errors = kp.load("path/to/file.krn")

Load a **kern/**mens from a string into a kp.Document.

import kernpy as kp

document, errors = kp.loads("**kern\n*clefC3\n*k[b-e-a-]\n*M3/4\n4e-\n4g\n4c\n=1\n4r\n2cc;\n==\n*-")

Validate the duration of the notes in the score.

import kernpy as kp

document, errors = kp.load("path/to/file.krn", 
        raise_on_duration_mismatch=True) # raise an exception here

document, errors = kp.load("path/to/file.krn", 
        raise_on_duration_mismatch=True,
        meter_signature_fallback_if_not_found="4/4")  # if there is no meter signature found, use 4/4 as fallback for validating the durations. By default it raises an exception

Create a new standardized file from a kp.Document.

import kernpy as kp

kp.dump(document, "newfile.krn")

Save the document in a string from a kp.Document.

import kernpy as kp

content = kp.dumps(document)

Exploring different options when creating new files

Only use the specified spines in spine_types.

import kernpy as kp

# only export the **kern spines
kp.dump(document, "newfile_core.krn",
        spine_types=['**kern'])

# only export the **text spines
kp.dump(document, "newfile_lyrics.krn",
        spine_types=['**text])
                     
# only export **kern and **text spines     
kp.dump(document, "newfile_core_and_lyrics.krn",
        spine_types=['**text'])
  • The categories are hierarchically defined in the TokenCategory class. See the hierarchy as a tree
import kernpy as kp


print(kp.TokenCategory.tree())

Tree:

.
├── STRUCTURAL
│   ├── HEADER
│   └── SPINE_OPERATION
├── CORE
│   ├── NOTE_REST
│   │   ├── DURATION
│   │   ├── NOTE
│   │   │   ├── PITCH
│   │   │   ├── DECORATION
│   │   │   └── ALTERATION
│   │   └── REST
│   ├── CHORD
│   ├── EMPTY
│   └── ERROR
├── SIGNATURES
│   ├── CLEF
│   ├── TIME_SIGNATURE
│   ├── METER_SYMBOL
│   ├── KEY_SIGNATURE
│   └── KEY_TOKEN
├── ENGRAVED_SYMBOLS
├── OTHER_CONTEXTUAL
├── BARLINES
├── COMMENTS
│   ├── FIELD_COMMENTS
│   └── LINE_COMMENTS
├── DYNAMICS
├── HARMONY
├── FINGERING
├── LYRICS
├── INSTRUMENTS
├── IMAGE_ANNOTATIONS
│   ├── BOUNDING_BOXES
│   └── LINE_BREAK
├── OTHER
├── MHXM
└── ROOT
  • Use include for selecting the **kern semantic categories to use. The output only contains what is passed. By default, all the categories are included.
import kernpy as kp


kp.dump(document, "newfile_only_clefs.krn",
        include={kp.TokenCategory.CLEF})
kp.dump(document, "newfile_only_durations_and_bounding_boxes.krn",
        include={kp.TokenCategory.DURATION, kp.TokenCategory.BOUNDING_BOXES})
  • Use exclude for selecting the **kern semantic categories to not use. The output contains everything except what is passed. By default, any category is excluded.
import kernpy as kp

kp.dump(document, "newfile_without_pitches.krn",
        exclude={kp.TokenCategory.PITCH})
kp.dump(document, "newfile_without_durations_or_rests.krn",
        exclude={kp.TokenCategory.BARLINES, kp.TokenCategory.REST})
  • Use include and exclude together to select the **kern semantic categories to use. The output combines both.
import kernpy as kp

kp.dump(document, "newfile_custom.krn",
        include=kp.BEKERN_CATEGORIES,  # Preloaded set of simple categories
        exclude={kp.TokenCategory.PITCH})

# Inspect the BEKERN preloaded categories
print(kp.BEKERN_CATEGORIES)
  • Use encoding to select how the categories are split. By default, the normalizedKern encoding is used.
import kernpy as kp

kp.dump(document, "newfile_normalized.krn",
        encoding=kp.Encoding.normalizedKern)  # Default encoding

Select the proper Humdrum **kern encoding:

kernpy provides different encodings to export the content each symbol in different formats.

Encoding Output example Description
kern 2.bb-_L Traditional Humdrum **kern encoding
extended_kern 2@.@bb@-·_·L Tokenised version of Humdrum **kern encoding
basic_kern 2.bb- Basic Humdrum **kern encoding: same of kern but with less semantic categories
basic_extended_kern 2@.@bb@- Tokenised version of the Basic Extended Humdrum **kern encoding
agnostic_kern 2.bb-_L Agnostic: pitches and *k[...] remapped via clef to G-clef spelling; note accidentals as shown on the score
agnostic_extended_kern 2@.@bb@-·_·L Tokenised agnostic: same pitch/*k[...] remap and score-visible accidentals as agnostic_kern

Use the Encoding enum class to select the encoding:

import kernpy as kp

doc, _ = kp.load('test/resources/legacy/chor048.krn')

kern_content = kp.dumps(doc, encoding=kp.Encoding.normalizedKern)
ekern_content = kp.dumps(doc, encoding=kp.Encoding.normalizedExtendedKern)
bkern_content = kp.dumps(doc, encoding=kp.Encoding.basicKern)
bekern_content = kp.dumps(doc, encoding=kp.Encoding.basicExtendedKern)
agnostic_content = kp.dumps(doc, encoding=kp.Encoding.agnosticKern)
agnostic_extended_content = kp.dumps(doc, encoding=kp.Encoding.agnosticExtendedKern)
  • Use from_measure and to_measure to select the measures to export. By default, all the measures are exported.
import kernpy as kp

kp.dump(document, "newfile_1_to_10.krn",
        from_measure=1,  # First from measure 1
        to_measure=10)   # Last measure exported
  • Use spine_ids to select the spines to export. By default, all the spines are exported.
import kernpy as kp

kp.dump(document, "newfile_1_and_2.krn",
        spine_ids=[0, 1])  # Export only the first and the second spine
  • Use show_measure_numbers to select if the measure numbers are shown. By default, the measure numbers are shown.
import kernpy as kp

kp.dump(document, "newfile_no_measure_numbers.krn",
        show_measure_numbers=False)  # Do not show measure numbers
  • Use all the options at the same time.
import kernpy as kp

kp.dump(document, "newfile.krn",
        spine_types=['**kern'],  # Export only the **kern spines
        include=kp.BEKERN_CATEGORIES,  # Token categories to include
        exclude={kp.TokenCategory.PITCH},  # Token categories to exclude
        encoding=kp.Encoding.eKern,  # Kern encoding
        from_measure=1,  # First from measure 1
        to_measure=10,  # Last measure exported
        spine_ids=[0, 1],  # Export only the first and the second spine
        show_measure_numbers=False,  # Do not show measure numbers
        )

Exploring kernpy utilities.

  • Spines analysis Retrieve all the spine types of the document.
import kernpy as kp

kp.spine_types(document)
# ['**kern', '**kern', '**kern', '**kern', '**root', '**harm']

kp.spine_types(document, headers=None)
# ['**kern', '**kern', '**kern', '**kern', '**root', '**harm']

kp.spine_types(document, headers=['**kern'])
# ['**kern', '**kern', '**kern', '**kern']
  • Get specific **kern spines.
import kernpy as kp

def how_many_instrumental_spines(document):
    print(kp.spine_types(document, ['**kern']))
    return len(kp.spine_types(document, ['**kern']))
# ['**kern', '**kern', '**kern', '**kern']
# 4

def has_voice(document):
    return len(kp.spine_types(document, ['**text'])) > 0
# True
  • Check if the document is monophonic or polyphonic.

This function checks if the document has only one **kern spine and has no chords in the spine.

import kernpy as kp

is_score_monophonic = kp.is_monophonic(document)
# True

How many measures are there in the document? Which measures do you want to export?

After reading the score into the Document object. You can get some useful data:

first_measure: int = document.get_first_measure()
last_measure: int = document.measures_count()

Iterate over all the measures of the document.

import kernpy as kp

doc, _ = kp.load('test/resources/legacy/chor048.krn')  # 10 measures score
for i in range(doc.get_first_measure(), doc.measures_count(), 1):  # from 1 to 11, step 1
    # Export only the i-th measure (1 long measure scores)
    content_ith_measure = kp.dumps(doc, from_measure=i, to_measure=i)
    
    # Export the i-th measure and the next 4 measures (5 long measure scores)
    if i + 4 <= doc.measures_count():
        content_longer = kp.dumps(doc, from_measure=i, to_measure=i + 4)
    ...

It is easier to iterate over all the measures using the for measure in doc: loop (using the __ iter__ method):

import kernpy as kp

for measure in doc:
    content = kp.dumps(doc, from_measure=measure, to_measure=measure)
    ...

Exploring the page bounding boxes.

import kernpy as kp

# Iterate over the pages using the bounding boxes
doc, _ = kp.load('kern_having_bounding_boxes.krn')

# Inspect the bounding boxes
print(doc.page_bounding_boxes)


def are_there_bounding_boxes(doc):
   return len(doc.get_all_tokens(filter_by_categories=[kp.TokenCategory.BOUNDING_BOXES])) > 0


# True

# Iterate over the pages
for page_label, bounding_box_measure in doc.page_bounding_boxes.items():
   print(f"Page: {page_label}"
         f"Bounding box: {bounding_box_measure}"
         f"from_measure: {bounding_box_measure.from_measure}"
         f"to_measure+1: {bounding_box_measure.to_measure}")  # TODO: Check bounds
   kp.dump(doc, f"foo_{page_label}.ekrn",
           spine_types=['**kern'],
           token_categories=kp.BEKERN_CATEGORIES,
           encoding=kp.Encoding.eKern,
           from_measure=bounding_box_measure.from_measure,
           to_measure=bounding_box_measure.to_measure - 1  # TODO: Check bounds            
           )

Merge different full kern scores

import kernpy as kp
# Merge is available.

# Merge two valid **kern contents
score_a = '**kern\n*clefG2\n=1\n4c\n4d\n4e\n4f\n*-\n'
score_b = '**kern\n*clefG2\n=1\n4a\n4c\n4d\n4c\n*-\n'
merged_doc, merge_indexes = kp.merge([score_a, score_b])

print(merge_indexes)
print(kp.dumps(merged_doc))

Concatenate sorted fragments of the same score

import kernpy as kp

fragment_a = '**kern\n*clefG2\n=1\n4c\n4d\n4e\n4f\n*-\n'
fragment_b = '=2\n4a\n4c\n4d\n4c\n*-\n=3\n4a\n4c\n4d\n4c\n*-\n'
fragment_c = '=4\n4a\n4c\n4d\n4c\n*-\n=5\n4a\n4c\n4d\n4c\n*-\n'
fragment_d = '=6\n4a\n4c\n4d\n4c\n*-\n=7\n4a\n4c\n4d\n4c\n*-\n==*-'
fragments = [fragment_a, fragment_b, fragment_c, fragment_d]

doc_merged, indexes = kp.concat(fragments)
for index_pair in indexes:
    from_measure, to_measure = index_pair
    print(f'From measure: {from_measure}, To measure: {to_measure}')
    print(kp.dumps(doc_merged, from_measure=from_measure, to_measure=to_measure))

# Sometimes is useful having a different separator between the fragments rather than the default one (newline)...
doc_merged, indexes = kp.concat(fragments, separator='')

Inspect the Document class functions

import kernpy as kp
doc, _ = kp.load('test/resources/legacy/chor048.krn')  # 10 measures score

frequencies = doc.frequencies()  # All the token categories
filtered_frequencies = doc.frequencies(filter_by_categories=[kp.TokenCategory.SIGNATURES])
frequencies['*k[f#c#]']
# {
#   'occurrences': 4,
#   'category': SIGNATURES,
# }

# Get all the tokens in the document
all_tokens: [kp.Token] = doc.get_all_tokens()
all_tokens_encodings: [str] = doc.get_all_tokens_encodings()

# Get the unique tokens in the document (vocabulary)
unique_tokens: [kp.Token] = doc.get_unique_tokens()
unique_token_encodings: [str] = doc.get_unique_token_encodings()

# Get the line comments in the document
document.get_metacomments()
# ['!!!COM: Coltrane', '!!!voices: 1', '!!!OPR: Blue Train']
document.get_metacomments(KeyComment='COM')
# ['!!!COM: Coltrane']
document.get_metacomments(KeyComment='COM', clear=True)
# ['Coltrane']
document.get_metacomments(KeyComment='non_existing_key')
# []

Transpose

  • Inspect what intervals are available for transposing.
import kernpy as kp

print(kp.AVAILABLE_INTERVALS)
  • Transpose the document to a specific interval.
import kernpy as kp

doc, err = kp.load('test/resources/legacy/chor048.krn')  # 10 measures score
higher_octave_doc = doc.to_transposed('octave', 'up')

kp.dump(higher_octave_doc, 'higher_octave.krn')

On your own

  • Handle the document if needed.
import kernpy as kp

# Access the document tree
print(document.tree)
# <kernpy.core.document.DocumentTree object at 0x7f8b3b3b3d30>

# View the tree-based Document structure for debugging.
kp.graph(document, '/tmp/graph.dot')
# Render the graph 
# - using Graphviz extension in your IDE
# - in the browser here: https://dreampuf.github.io/GraphvizOnline/

Installation

pip3 install kernpy
uv add kernpy
# always ensure you have the latest version installed

Documentation

Documentation available at https://kernpy.pages.dev/

Kernpy also supports been executed as a module. Find out the available commands:

python -m kernpy --help
python -m kernpy <command> <options>

Run tests:

uv sync --group test
uv run pytest test

Build documentation:

uv sync --group docs
uv run mkdocs serve/build

Contributing

We welcome contributions from the community! If you'd like to contribute to the project, please follow these steps:

Go to the file CONTRIBUTING.md for more information on how to contribute.

Create release:

After committing the changes, run:

uv version --bump patch|minor|major

Commit again the the update changes and push the to origin/main.

Then, create the release in GitHub and add the release notes. The release will be automatically published in PyPI. Use the same version number as the generated one in the previous step!

Citation:

@inproceedings{kernpy_cerveto_mec_2025,
  title        = {kernpy: a Humdrum **Kern Oriented Python Package for Optical Music Recognition Tasks},
  author       = {Cerveto-Serrano, Joan and Rizo, David and Calvo-Zaragoza, Jorge},
  editor       = {Lewis, David and Plaksin, Anna and Stremel, Sophie},
  booktitle    = {Proceedings of the Music Encoding Conference 2025},
  year         = {2025},
  address      = {London, United Kingdom},
  publisher    = {Knowledge Commons},
  doi          = {10.17613/qhvtd-hkv52},
}

Acknowledgements

This paper is supported by grant CISEJI/2023/9 from “Programa para el apoyo a personas investigadoras con talento (Plan GenT) de la Generalitat Valenciana”.

Generalitat Valenciana

Metadata

Release files for kernpy 1.9.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kernpy 1.9.3
File Size Uploaded
kernpy-1.9.3.tar.gz 6.8 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for kernpy 1.9.3
File Interpreter ABI Platform
kernpy-1.9.3-py3-none-any.whl Python 3 none any Details

Total release size: 6.9 MB

Release files / kernpy-1.9.3.tar.gz

Download URL kernpy-1.9.3.tar.gz
Size 6.8 MB
Tags Source
SHA-256 checksum
How to use checksums
03baf513ca9ae530aee3e9710e81d056fe9c22d94d69c7ee9fe9af6f4295be68
BLAKE2b-256 checksum
How to use checksums
824d1f7245f86ee4585a8d51679304d223dee6b9968f81e00d35e195577ecbe3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / kernpy-1.9.3-py3-none-any.whl

Download URL kernpy-1.9.3-py3-none-any.whl
Size 166.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
adb081559b2f2fa9a581143a36d5c2bd66d03304e59fc089f738c3ae7c7b49ac
BLAKE2b-256 checksum
How to use checksums
3ad985c387e081028b0fcbec2556f69cb97ca92d35f195657fd997f2fe2316a5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

1.9.3 This release

2 release files

1.9.0

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.2

2 release files

1.6.1

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page