Python

A small, typed wrapper over the command-line tool. No dependencies; Python 3.9 or later.

Install

Install the command-line tool first (ChorusPop ▸ Install Command-Line Tool), then:

pip install https://choruspop.com/download/choruspop-1.1.0-py3-none-any.whl

The package isn't on PyPI. It finds the tool on your PATH, then inside /Applications/ChorusPop.app, or wherever $CHORUSPOP_CLI points.

Make a song

import choruspop

def show(job):
    print(f"\r{job.title}: {job.status} {job.progress:.0%} {job.stage or ''}", end="", flush=True)

result = choruspop.make(
    "[verse]\nNeon on the river\nLights across the bay\n[chorus]\nTurn it up",
    title="Neon River", genre="synthwave", moods=["dreamy"], voice="female",
    length=30,              # seconds: 30–290 in steps of 10
    format="flac", out="~/Desktop/",
    progress=show,          # called on every progress event
    timeout=900,            # stop waiting after 15 minutes
)
print("\n", result.files)   # [PosixPath('/Users/me/Desktop/Neon River.flac')]

make waits for the song, then writes the file to out (a file or a folder; the current folder by default) or, with music=True, to ~/Music/ChorusPop.

Progress, timeouts and errors

  • progress: a function called with a Job each time the job moves: status, progress (0–1), stage, eta_seconds.
  • timeout: seconds to wait. When it passes, make and strip raise JobTimeout with the job as it stood; the song keeps going in ChorusPop, and status(job.id, wait=True) picks it up again.
  • wait=False: queue it and return at once; result.job.id is what to follow.
  • A job that fails or is cancelled raises JobFailed. Anything ChorusPop refuses raises ChorusPopError with a code such as invalid, not_found or queue_full (all codes).
from choruspop import JobTimeout, JobFailed

try:
    result = choruspop.make(lyrics, genre="rock", length=210, timeout=1800, progress=show)
except JobTimeout as e:
    print("still going:", e.job.id)
except JobFailed as e:
    print("didn't make it:", e.job.message)

# Or queue it, then follow it: status() returns the job as it stands.
job = choruspop.make(lyrics, genre="rock", length=210, wait=False).job
job = choruspop.status(job.id, wait=True, timeout=1800, progress=show)
if job.done:
    files = choruspop.export(job.song_id, format="flac", out="~/Desktop/")

Versions and exports

choruspop.strip("latest", "instrumental", format="alac", out="~/Desktop/")   # remove vocals
choruspop.strip("Neon River", "vocals")                                   # vocals only
choruspop.strip("Neon River", "stems", out="~/Desktop/")                  # 4 files in a folder

choruspop.export("Neon River", version="instrumental", format="flac", music=True)
choruspop.export("latest", lrc=True)                                      # with synced lyrics

for song in choruspop.list("neon", limit=5):
    print(song.id[:8], song.title, song.style, song.seed, song.versions)

detail = choruspop.show("latest")
print(detail.lyrics, detail.files)

Examples

A batch of songs from a CSV

# songs.csv: title,genre,mood,length,lyrics_file
import csv, choruspop
from pathlib import Path

with open("songs.csv", newline="") as f:
    rows = list(csv.DictReader(f))

for i, row in enumerate(rows, 1):
    result = choruspop.make(
        Path(row["lyrics_file"]).read_text(),
        title=row["title"], genre=row["genre"], moods=[row["mood"]],
        length=int(row["length"]), album="Demo tape",
        format="flac", out="out/",
        progress=lambda job: print(f"\r[{i}/{len(rows)}] {job.title} {job.progress:.0%}", end=""),
    )
    print(f"\r[{i}/{len(rows)}] {result.files[0]}")

ChorusPop makes one song at a time, so waiting for each before the next costs nothing, and the queue never fills.

Stems for every song

import choruspop

for song in choruspop.list(limit=200):
    if "stems" in song.versions:
        continue
    result = choruspop.strip(song.id, "stems", format="flac", out="stems/")
    print(song.title, "→", result.files[0].parent)

Variations on a seed

base = choruspop.show("Neon River")
lyrics, seed = base.lyrics, base.song.seed

for voice in ("female", "male"):
    choruspop.make(lyrics, genre=base.song.genre, moods=base.song.moods,
                   instruments=base.song.instruments, voice=voice,
                   seed=seed, length=base.song.length_sec, title=f"Neon River ({voice})")

MCP

python3 -m choruspop.mcp (or the choruspop-mcp script) runs the MCP server, for clients you'd rather point at Python:

{ "mcpServers": { "choruspop": { "command": "python3", "args": ["-m", "choruspop.mcp"] } } }

choruspop.agents_guide() returns the guide for AI agents, for your own agent's system prompt.

Reference

Every function is on the module (choruspop.make(…)) and on ChorusPop(cli=…), for a particular copy of the tool. A song is its id, the first 6+ characters of one, its exact title, or "latest".

choruspop.make()

make(
    lyrics: str = '',
    *,
    title: Optional[str] = None,
    style: Optional[str] = None,
    genre: Optional[str] = None,
    moods: Sequence[str] = (),
    voice: Optional[str] = None,
    instruments: Sequence[str] = (),
    length: int = 60,
    seed: Optional[int] = None,
    album: Optional[str] = None,
    key: Optional[str] = None,
    instrumental: bool = False,
    plan: Optional[PathLike] = None,
    midi: Optional[PathLike] = None,
    roles: Optional[str] = None,
    format: Format = 'm4a',
    out: Optional[PathLike] = None,
    music: bool = False,
    lrc: bool = False,
    wait: bool = True,
    timeout: Optional[float] = None,
    progress: Optional[ProgressCallback] = None
) -> JobResult

Make a song. length is seconds, 30–290 in steps of 10.

key ("D minor", "F#", "Ebm") is exact: the plan is moved to it. plan (an .abc file) or midi (a .mid file, with roles like "voice=1,lead=5,harmony=2+3") is sung instead of a plan the model writes; lyrics are optional with them. instrumental=True has no singing (leave lyrics empty); it's experimental — some seeds still sing.

Waits (wait=True) and writes the file to out (a file or folder; the current folder by default) or the Music folder (music=True). Songs take minutes. With timeout, raises JobTimeout when it passes — the song keeps going, and its file still goes to out when it's done.

choruspop.strip()

strip(
    song: str,
    kind: StripKind = 'instrumental',
    *,
    format: Format = 'm4a',
    out: Optional[PathLike] = None,
    music: bool = False,
    wait: bool = True,
    timeout: Optional[float] = None,
    progress: Optional[ProgressCallback] = None
) -> JobResult

Make a version of a song: instrumental (vocals removed), vocals (only the singing) or stems (drums, bass, other, vocals). song is an id, the start of one, a title, or "latest".

choruspop.export()

export(
    song: str,
    *,
    format: Format = 'm4a',
    version: Version = 'original',
    out: Optional[PathLike] = None,
    music: bool = False,
    lrc: bool = False
) -> List[Path]

Write a song (or a version already made) as a tagged audio file.

choruspop.list()

list(
    query: Optional[str] = None,
    *,
    album: Optional[str] = None,
    limit: Optional[int] = None
) -> List[Song]

Songs, newest first. query matches titles, lyrics and style.

choruspop.show()

show(
    song: str
) -> SongDetail

One song's details, lyrics, and the files saved for it in the Music folder.

choruspop.status()

status(
    job: Optional[str] = None,
    *,
    wait: bool = False,
    timeout: Optional[float] = None,
    progress: Optional[ProgressCallback] = None
) -> Union[Job, List[Job]]

A job (following it with wait), or every active job.

choruspop.cancel()

cancel(
    job: str
) -> Job

Stop a running job, or take a queued one out of the queue.

choruspop.version()

version(
    
) -> Dict[str, Any]

The tool's and the app's versions.

choruspop.tracks()

tracks(
    midi: PathLike
) -> List[MidiTrack]

A MIDI file's tracks, with the roles make gives them without roles.

Types

Job

A song being made, or a version (instrumental, vocals, stems) of one.

FieldType
idstr
kindLiteral['song', 'instrumental', 'vocals', 'stems']
statusStatus
progressfloat
titlestr
stageOptional[str]
song_idOptional[str]
eta_secondsOptional[int]
length_secOptional[int]
messageOptional[str]
donebool (property)
finishedbool (property)

JobResult

FieldType
jobJob
filesList[Path]

Song

FieldType
idstr
titlestr
stylestr
genrestr
moodsList[str]
voicestr
instrumentsList[str]
style_fedstr
length_secint
audio_secfloat
seedint
created_atstr
albumsList[str]
versionsList[str]

SongDetail

FieldType
songSong
lyricsstr
filesDict[str, Path]
tempoOptional[float]
keyOptional[str]
meterOptional[str]
plan_pathOptional[Path]

Errors

ExceptionWhen
ChorusPopErrorThe tool or the app said no. ``code`` is one of: usage, invalid, not_found, queue_full, not_supported, unavailable, protocol, internal, failed, timeout.
JobFailed (ChorusPopError)The job failed or was cancelled.
JobTimeout (ChorusPopError)Stopped waiting; the job keeps going in ChorusPop (poll ``status``).

choruspop.agents_guide()

agents_guide() -> str

The guide for AI agents driving ChorusPop (AGENTS.md): the tools, how jobs work, lyrics and style, errors, etiquette and the license.

choruspop.find_cli()

find_cli(explicit: Optional[PathLike] = None) -> str

explicit, else $CHORUSPOP_CLI, else choruspop on PATH, else the one inside /Applications/ChorusPop.app.