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
Jobeach time the job moves:status,progress(0–1),stage,eta_seconds. - timeout: seconds to wait. When it passes,
makeandstripraiseJobTimeoutwith the job as it stood; the song keeps going in ChorusPop, andstatus(job.id, wait=True)picks it up again. - wait=False: queue it and return at once;
result.job.idis what to follow. - A job that fails or is cancelled raises
JobFailed. Anything ChorusPop refuses raisesChorusPopErrorwith acodesuch asinvalid,not_foundorqueue_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.
| Field | Type |
|---|---|
id | str |
kind | Literal['song', 'instrumental', 'vocals', 'stems'] |
status | Status |
progress | float |
title | str |
stage | Optional[str] |
song_id | Optional[str] |
eta_seconds | Optional[int] |
length_sec | Optional[int] |
message | Optional[str] |
done | bool (property) |
finished | bool (property) |
JobResult
| Field | Type |
|---|---|
job | Job |
files | List[Path] |
Song
| Field | Type |
|---|---|
id | str |
title | str |
style | str |
genre | str |
moods | List[str] |
voice | str |
instruments | List[str] |
style_fed | str |
length_sec | int |
audio_sec | float |
seed | int |
created_at | str |
albums | List[str] |
versions | List[str] |
SongDetail
| Field | Type |
|---|---|
song | Song |
lyrics | str |
files | Dict[str, Path] |
tempo | Optional[float] |
key | Optional[str] |
meter | Optional[str] |
plan_path | Optional[Path] |
Errors
| Exception | When |
|---|---|
ChorusPopError | The 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.