Skip to main content

songleaf

Dense, readable one-page song sheets: lyrics and chords on one A4 page, with the lyrics as large as will fit.

pip install songleaf
python -m songleaf sheet "wonderwall oasis"    # -> a one-page PDF in ~/.local/share/songleaf/sheets/

The default source is the Kaggle chords-and-lyrics corpus (about 135,000 songs), read from a local copy of its zip (see Sources).

What the sheet looks like

  • One page, largest type that fits. The lyric font size is found by search for each song. On 2,000 randomly sampled corpus songs, the default layout's median came out around 23 pt, and 1,999 of the 2,000 fit on one page. A song that cannot fit at the minimum size spills onto a second page.
  • Any script the font covers. Text is set in DejaVu Sans, bundled with the package and embedded in each PDF (Bitstream Vera licence, beside the font files), so Turkish, Greek, Cyrillic and other non-Latin-1 text draws correctly. It is wider than Helvetica, so fitted sizes are somewhat smaller than the numbers above, which were measured in Helvetica; DenseStyle(lyric_font="Helvetica", ...) restores the old look.
  • Chords take no line of their own. They are smaller and blue, start over the syllable they land on, and overlap the tops of the letters of their lyric line.
  • Lines share rows. Consecutive lines of a paragraph are packed onto one row, separated by a light /. A blank line or a new section starts a new row.
  • Small labels. Sections are grey prefixes (V1, Ch, Br); title, artist, capo and key are one small line at the top.

Layouts

That is the dense layout. Other layouts trade looks for larger lyrics. Name one by joining options with +:

option what it changes
overlap chords sit lower, on the words themselves, outlined in white and drawn over them: no extra height for chords
inline chords go in the lyric line, right before the syllable they land on; ‹G marks a chord that sounds before its syllable
packed rows break where the lyrics break (between couplets, not inside a rhyming pair or a repeated line) instead of as late as fits
two-column two columns under a full-width heading
shaded light bands for choruses and bridges, dark grey for lines already sung, chords coloured by function in the song's key (tonic darker, outside the key in orange)
python -m songleaf sheet "wonderwall oasis" --layout inline
python -m songleaf sheet "wonderwall oasis" --layout overlap+packed+two-column+shaded

The comparison of the largest font size each layout fits, over the same 2,000 songs, is on songleaf#4.

Command line

python -m songleaf search "wonderwall oasis"              # fuzzy: typos and word order don't matter
python -m songleaf search --artist oasis --lyrics "roads winding"
python -m songleaf sheet "wonderwall oasis" --output wonderwall.pdf
python -m songleaf sheet "wonderwall oasis" --pick 2      # the second-best match
python -m songleaf songs                                  # songs already stored
python -m songleaf sheet kaggle_chords:1234               # a song by its key: no search
python -m songleaf sheet kaggle_chords:1234 --refresh     # fetch it again from its source
python -m songleaf sheet "wonderwall oasis" --transpose 2 # chords up a whole tone, spelled for the new key
python -m songleaf sheet "wonderwall oasis" --scale 0.8   # 80% of the size that fills the page
python -m songleaf song kaggle_chords:1234                # the song as data: text, chords, every key's chord table
python -m songleaf build-index                            # once: search from an index, not the whole corpus in memory

The songleaf console script is the same command.

Python

import songleaf

hits = songleaf.search("wonderwall oasis")
song = songleaf.get_song(hits[0].key)  # a Song: lyrics text + annotations
songleaf.render_dense_a4(song, "wonderwall.pdf")  # {'font_size': ..., 'pages': 1, ...}
songleaf.render_sheet(song, "wonderwall-inline.pdf", layout="inline+two-column")

songleaf.sheet("wonderwall oasis")  # search, store and render in one call
songleaf.sheet("wonderwall oasis", layout="overlap+shaded")

A layout is a SheetLayout(chords=..., packing=..., columns=..., shading=..., style=DenseStyle(...)). chords and packing also take your own strategy objects (a ChordPlacement, or a packer function with the signature of songleaf.packing.pack_greedy):

from functools import partial
from songleaf import SheetLayout, render_sheet
from songleaf.packing import pack_structured

layout = SheetLayout(
    chords="inline", packing=partial(pack_structured, row_cost=5), columns=2
)
render_sheet(song, "sheet.pdf", layout=layout)

The song model

A Song is lyrics text plus standoff annotations anchored on character offsets, so annotation layers never touch the text:

  • section: spans its lines, {"label": "Chorus"};
  • chord: a point on the first character of the syllable the chord lands on, {"symbol": "G/B", "timing": "at"} ("before" marks a chord that sounds ahead of that syllable);
  • score: a link from a stretch of lyrics to a score snippet, {"source", "id", "url", "format"}.

Metadata (title, artist, capo, key) and provenance (source, id, url, licence) travel with the song, and Song.to_dict() is its JSON form.

from songleaf import Song, chord, section, parse_chords_over_lyrics

song = Song(
    "Paper boats drift", [section(0, 17, "Verse 1"), chord(0, "G"), chord(6, "D")]
)
song = parse_chords_over_lyrics(
    "G     D\nPaper boats drift"
)  # the same chords, from a chart

Storage

Songs are kept in a MutableMapping (songleaf.song_store()): one JSON file per song under ~/.local/share/songleaf/songs/. Rendered sheets go to ~/.local/share/songleaf/sheets/. Set SONGLEAF_DATA_DIR to move both.

Sources

KaggleChordsSource reads the chords-and-lyrics dataset through sung. It needs a local copy of the zip: put it at ~/.local/share/songleaf/datasets/chords-and-lyrics/chords-and-lyrics-dataset.zip (under the data root, so SONGLEAF_DATA_DIR moves it too), point SUNG_CHORDS_AND_LYRICS_ZIP at it, or keep it where haggle downloads it ($HAGGLE_ROOTDIR/zips/eitanbentora/chords-and-lyrics-dataset.zip). Without a local copy, sung downloads it from Kaggle, which needs Kaggle credentials. The corpus was scraped from a chords site, so keep it to personal use.

Loading the corpus takes about five seconds on the first search in a process, and it stays in memory: about 1.1 GB, 1.6 GB once a lyrics search has run. Run songleaf build-index once to avoid that. It writes one SQLite file beside the zip (about 190 MB: titles, artists, compressed charts and a full-text index of the words), and from then on the default source reads it. A process then holds about 80 MB for search, a lyrics search takes milliseconds instead of seconds, and the songs and their keys are the same. Run it again after replacing the zip.

Other sources surveyed, plus the Ultimate Guitar / MuseScore decisions, are in misc/docs/sources.md.

LocalFolderSource indexes a folder of files you exported yourself from a paid chords/tab or score site (Ultimate Guitar, MuseScore, ...) — text charts are parsed like the Kaggle corpus, score/tab files (Guitar Pro, MusicXML, MSCZ, MIDI, PDF) are attached as a score link. Default folder: ~/.local/share/songleaf/imports/ (moved by SONGLEAF_DATA_DIR).

Web app

songleaf.web.mk_app() is a FastAPI app over the same functions: search, the stored songs, the layout options, and the sheet as JSON, PDF and a PNG per page (for phone browsers that show no PDF inline). It also serves its own page: a search box, and a song view with two modes. Read draws the song as text, with each chord over the syllable it lands on and lines wrapping between words, so it suits any screen. Print shows the PDF sheet with the layout options as toggles. Both share a size control (pixels when reading, a percentage of the page-filling size when printing), transposition (a key picker that lists every chord of the song beside what it becomes), and a "look" panel for the colour and opacity of lyrics and chords, chord size, line spacing and font. Opacity prints as the matching shade on white paper. The page is a two-line HTML shell that loads its script and style from the app, so it changes with the package.

pip install "songleaf[web]"
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from songleaf.web import mk_app, page_shell

site = FastAPI()
site.mount("/api/songleaf", mk_app())
site.get("/songleaf/", response_class=HTMLResponse)(
    lambda: page_shell(api="/api/songleaf", base="/songleaf")
)

The app has no login of its own: put it behind whatever gates the host. It takes the same sources= and store= as sheet. A rendered sheet is kept in memory (sheet_cache_size=, default 32), so the page's requests for one sheet render it once.

Extending

Three keyword arguments are the extension points, on sheet (and search for sources):

  • sources=: objects with a name, search(query, *, title, artist, lyrics, limit) returning Hits, and get(song_id) returning a Song;
  • store=: any MutableMapping[str, Song] (a dict works);
  • renderer=: any (song, output) -> dict function (songleaf.make_renderer("inline+shaded", page_size="LETTER") makes one from a layout). It replaces layout=.
songleaf.sheet("paper boats", sources=[my_source], store={}, renderer=my_renderer)

Metadata

Release files for songleaf 0.0.8

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

Source distribution (sdist)

Source distribution for songleaf 0.0.8
File Size Uploaded
songleaf-0.0.8.tar.gz 817.6 kB Details

Built distribution (wheel)

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

Total release size: 1.6 MB

Release files / songleaf-0.0.8.tar.gz

Download URL songleaf-0.0.8.tar.gz
Size 817.6 kB
Tags Source
SHA-256 checksum
How to use checksums
60baeafe72803cc3ac07d06c6c6e9274d3736399a74103e86d40c104de5f5c3d
BLAKE2b-256 checksum
How to use checksums
5600677a48394bc0bbea86248bd0122e039f45bebef2737981ff1c6a22534881
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / songleaf-0.0.8-py3-none-any.whl

Download URL songleaf-0.0.8-py3-none-any.whl
Size 806.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1f696c5f53039f2f34d959af423d21c6755215b9ca2117cc33d4ab476b717ba8
BLAKE2b-256 checksum
How to use checksums
9afa4ad6595d19287829ee202e0329a16f8fcbf4fe812748fb5e6316eafc1584
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.0.11

2 release files

0.0.10

2 release files

0.0.9

2 release files

This release

0.0.8 This release

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

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