Skip to content
Search docs

Generation, Candidates, and saving

Generate playable Audio and an inspectable Score from style, lyrics, and seed, or pass explicitly selected ABC to GenerateFromScore. A successful generation creates a Candidate; explicit save creates a Version. This is an optional API guide; the formal Web supports inspecting, editing and selecting Scores, or starting Cover from Reference Audio. Default fake mode uses original CPU fixtures and cannot prove model quality.

Run from the repository root. FastAPI owns its uv environment and lockfile and requires no Torch, CUDA, or running ComfyUI.

Terminal window
uv sync --project services/api --locked --python 3.12.13
$env:MUSIC_API_RUNTIME_MODE = "fake"
uv run --project services/api --no-sync music-api serve --data-dir data/application-fake --port 8000

Open http://127.0.0.1:8000/docs. The application saves SQLite and Asset files in the selected directory. Use that same directory after restart. Use separate directories for fake and comfyui; an existing directory cannot change its mode.

In another terminal at the repository root, create a UTF-8 lyrics file such as data/morning-lyrics.txt:

[Verse]
Morning gathers on the window
Let the quiet carry us home

Run the complete example. It creates a Project and submits Generate. Pass --project-id <PROJECT_ID> to reuse a Project. Use a new output directory.

Terminal window
uv run --project services/api --no-sync python services/api/examples/generate_save.py generate --style "gentle folk pop" --lyrics-file data/morning-lyrics.txt --seed 2026192201 --output-dir data/morning-candidate-01

The terminal prints project_id and the Candidate. The download directory contains audio.flac and score.abc. Listen and inspect the Score before choosing to save. Fake Audio is an identified test tone. This command does not create a Version.

The HTTP request is POST /projects/{project_id}/jobs/generate. Trimmed style and lyrics must be nonempty and contain at most 1024 and 10000 characters. Seed must be an integer from 0 to 2^63−1. max_seconds accepts 0–360: 0 follows the lyrics (the model floor is about 40 seconds); any other value is a seconds ceiling. The result stays within the ceiling and may end earlier when the lyrics finish.

The request returns 202 and an application Job id. Read GET /projects/{project_id}/jobs/{job_id}. The five states are queued, running, completed, failed, and cancelled. A successful result contains candidate_id, audio_asset_id, abc_asset_id, and score_id. The application marks completed only after Score validation, full FLAC decoding, and whole-output-set import.

List Candidates with GET /projects/{project_id}/candidates, or read /candidates/{candidate_id}. A Candidate retains inputs, execution settings, Workflow/Runtime provenance, and output facts. Generated output currently requires PCM16 FLAC, 48 kHz stereo; the duration must fall between 1 second and the ceiling +2 (360 +2 for auto), with agreement between all decoded frames and the STREAMINFO declaration. Legal streaming FLAC with a zero declared sample count cannot yet be confirmed under this verified profile.

Creators can complete the same operation in the formal Web using the editing and regeneration tutorial. Explicitly select a valid saved Score on its page; the Job and result show actual submitted ABC and source parent. The API and CLI below support scripted creation.

Keep the same Morning song Project. Download ABC from a generated or transcribed Score, copy it to a new file, and change one note. Retain the native Vocal / Ins two-voice header, complete bars and meter. General ABC that renders in a browser is not necessarily supported by the pinned inference plugin. Compare the minimal native ABC. This operation accepts only the plugin’s two-voice dialect, with notes in at least one voice. A transcription with rests throughout Vocal and melody in Ins is also supported.

For example, change D4 to F4 in a downloaded Score and save it as data/morning-selected.abc. Inspect and listen to the Score, then explicitly select this file. This command reuses the Project and source Score to create a new Audio Candidate:

Terminal window
uv run --project services/api --no-sync python services/api/examples/generate_save.py from-score --project-id <PROJECT_ID> --source-score-id <SOURCE_SCORE_ID> --abc-file data/morning-selected.abc --parent-version-id <SOURCE_VERSION_ID> --style "gentle folk pop" --lyrics-file data/morning-lyrics.txt --seed 2026410001 --output-dir data/morning-score-candidate-01

SOURCE_SCORE_ID comes from the original Candidate’s score_id or the transcription Job result. When starting from a saved Version, SOURCE_VERSION_ID must be the same-Project Version that owns this Score. Omit --parent-version-id when starting only from a transcribed Score. The command preserves the original Score, Assets and Version, and does not save a new Version automatically. Listen to the new audio.flac and judge whether the melody edit and style are worth keeping.

The request is POST /projects/{project_id}/jobs/generate-from-score. It contains abc, source_score_id, optional parent_version_id, and Generate’s style, lyrics, seed and max_seconds (0–360; 0 follows the lyrics). ABC has a 100000-character limit. Source Score and parent must belong to this Project. Invalid or unsupported ABC is rejected before a Job is created. A missing capability is rejected explicitly; the operation cannot fall back to ordinary Generate.

The formal ABC editor can save edits independently. POST /projects/{project_id}/scores/validate checks abc with the pinned parser without a Job or GPU call. POST /projects/{project_id}/scores saves the original text as a new ABC Asset and Score, with optional source_score_id, parent_version_id and stable save_id. First save returns 201. Replaying the same id, Project, source, parent and text hash returns 200 with the same Score; different intent returns 409 score_save_conflict. job_id=null explicitly identifies independent saving, without an automatic Candidate or Version.

When continuing from an edited Score, use its actual new id as source_score_id. The parent may own the original source or be that Score’s recorded editing parent. Consecutive edits inherit it, preserving old ids and files. For score_commit_unconfirmed, resource_id identifies a Score to read back. Retain the same save_id and input during explicit recovery.

Job and Candidate inputs.abc retain the request text. provenance.selected_score records its hash, effective_abc, the effective input hash, transformations and adapter version. The pinned plugin replans when an internal %yue2-words marker does not match, and rearranges bare scores without section comments. Version 1 removes matching internal markers and adds a neutral section comment to bare scores, preserving notes, bars, meter, tempo, voices and chords. Existing generated and transcribed scores with sections need no such conversion. The record distinguishes the original and actual inference text. It does not promise identical audio before and after comment processing.

Editing the local file after queueing or running cannot change the submitted Job. For another edit, select valid ABC again and explicitly submit a new Job. The output Score must equal the effective submitted text. Audio still receives complete FLAC validation. Failed output cannot become a Candidate or Version.

A GenerateFromScore Candidate retains its submitted source parent. Omit parent on save and the application still uses that parent. An explicitly different parent returns 409 source_parent_mismatch. Inspect ABC and parent in the new Version, then read the original Version again to verify that both creative results remain.

Replace placeholders with the preceding output:

Terminal window
uv run --project services/api --no-sync python services/api/examples/generate_save.py save --project-id <PROJECT_ID> --candidate-id <CANDIDATE_ID> --name "First morning"

This sends POST /projects/{project_id}/versions. Add --parent-version-id <VERSION_ID> to branch from an existing Version in the same Project. Save copies the Candidate’s inputs, provenance, and output snapshot. Later generation preserves earlier snapshots and Assets. Trimmed names must be nonempty and contain at most 200 characters.

First save returns 201. Repeating the same Candidate, name, and parent returns 200 with the same Version, including concurrent requests. A changed name or parent for a saved Candidate returns 409 version_already_saved with the existing Version id. Generate another Candidate for a new save intent.

List GET /projects/{project_id}/versions, or read /versions/{version_id}. Download Audio or ABC through the application Asset /content address. Stop and restart the API using the same data directory to read history. Application files do not depend on temporary inference outputs.

Recover from failures and use real Runtime

Section titled “Recover from failures and use real Runtime”
  • 422 invalid_request: correct empty inputs, seed, or unsupported fields, then submit.
  • 422 score_invalid: retain the edit file, correct the native header, notes or incomplete bars, then select it again. Rendering general ABC does not replace native inference validation.
  • 503 capability_missing, 404 score_not_found / parent_version_not_found, 409 source_parent_mismatch: check current capability and select related sources from this Project. score_result_mismatch means Runtime output replaced the selected Score. Retain the failed Job and snapshot, check the mapping, then explicitly retry.
  • Failed Job: read its error and recovery_required and retain that Job. Missing outputs, invalid Score, incomplete FLAC, or unconfirmed outputs cannot create Candidate/Version. Check original work through the restart recovery guide. When a new attempt is needed, confirm a safe terminal outcome and explicitly retry using cancellation and retry. Inference is not repeated automatically.
  • 404 candidate_not_found / parent_version_not_found: choose a Candidate or saved parent from the target Project. Cross-Project references cannot be saved.
  • 503 version_commit_unconfirmed: first query Version using the error’s resource_id. For a repeat save of a known Version, that id still identifies the existing Version when both acknowledgement and independent readback fail. Read it if present. If absent, restore database access and retry the same Candidate, name, and parent. Unconfirmed saves preserve existing Assets and snapshots.
  • 409 asset_unavailable / asset_path_invalid: restore the original application file or backed-up mapping. Runtime paths cannot replace application ids.

The GPU resource owner validates real generation on the fixed Runtime that passed P0. Set MUSIC_API_RUNTIME_MODE=comfyui and MUSIC_API_RUNTIME_EVIDENCE_PATH to a fresh owner receipt, and use a separate application directory. Collect or refresh the receipt through Runtime diagnostic evidence sources; historical Doctor reports cannot replace the current receipt. Reuse Runtime preparation and the verified short-song Workflow. CPU fixture checks do not replace real Runtime acceptance.

The complete HTTP example comes from version-controlled source:

"""Run from the repository root with the independent API project's Python."""
import argparse
import json
from pathlib import Path
import time
import httpx
def main() -> None:
parser = argparse.ArgumentParser(description="Generate an unsaved Candidate or explicitly save one as a Version.")
parser.add_argument("command", choices=["generate", "from-score", "save"])
parser.add_argument("--url", default="http://127.0.0.1:8000")
parser.add_argument("--project-id")
parser.add_argument("--style")
parser.add_argument("--lyrics-file", type=Path)
parser.add_argument("--seed", type=int, default=2026192201)
parser.add_argument("--output-dir", type=Path)
parser.add_argument("--candidate-id")
parser.add_argument("--name")
parser.add_argument("--parent-version-id")
parser.add_argument("--source-score-id")
parser.add_argument("--abc-file", type=Path)
args = parser.parse_args()
if args.command in {"generate", "from-score"} and (not args.style or not args.lyrics_file or not args.output_dir):
parser.error("generation requires --style, --lyrics-file, and --output-dir")
if args.command == "from-score" and (not args.project_id or not args.source_score_id or not args.abc_file):
parser.error("from-score requires --project-id, --source-score-id, and --abc-file")
if args.command == "save" and (not args.project_id or not args.candidate_id or not args.name):
parser.error("save requires --project-id, --candidate-id, and --name")
if args.command in {"generate", "from-score"} and args.output_dir.exists():
parser.error("use a new output directory to preserve previous downloads")
with httpx.Client(base_url=args.url, timeout=15) as client:
project_id = args.project_id
if args.command in {"generate", "from-score"}:
if project_id is None:
response = client.post("/projects", json={"name": "Morning song"})
response.raise_for_status()
project_id = response.json()["id"]
base = "/projects/" + project_id
inputs = {"style": args.style, "lyrics": args.lyrics_file.read_text(encoding="utf-8"), "seed": args.seed}
if args.command == "from-score":
inputs.update(abc=args.abc_file.read_text(encoding="utf-8"), source_score_id=args.source_score_id, parent_version_id=args.parent_version_id)
response = client.post(base + "/jobs/" + ("generate-from-score" if args.command == "from-score" else "generate"), json=inputs)
response.raise_for_status()
job_id = response.json()["id"]
deadline = time.monotonic() + 180
while True:
response = client.get(base + "/jobs/" + job_id)
response.raise_for_status()
job = response.json()
if job["status"] in {"completed", "failed", "cancelled"}:
break
if time.monotonic() >= deadline:
raise RuntimeError("Wait expired; inspect Project " + project_id + " Job " + job_id + " before retrying.")
time.sleep(0.2)
if job["status"] != "completed":
raise RuntimeError(json.dumps(job, ensure_ascii=False))
response = client.get(base + "/candidates/" + job["result"]["candidate_id"])
response.raise_for_status()
candidate = response.json()
args.output_dir.mkdir(parents=True)
for name, asset_id in [("audio.flac", candidate["audio_asset_id"]), ("score.abc", candidate["output_snapshot"]["score"]["abc_asset_id"])]:
response = client.get(base + "/assets/" + asset_id + "/content")
response.raise_for_status()
(args.output_dir / name).write_bytes(response.content)
value = {"project_id": project_id, "candidate": candidate, "output_dir": str(args.output_dir)}
else:
intent = {"candidate_id": args.candidate_id, "name": args.name, "parent_version_id": args.parent_version_id}
response = client.post("/projects/" + project_id + "/versions", json=intent)
response.raise_for_status()
value = {"project_id": project_id, "version": response.json()}
print(json.dumps(value, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()

Complete example: services/api/examples/generate_save.py ↗

Continue with style, lyrics, and seed ideas, or look up the API contract reference and application configuration when needed. See the maintenance record for actual checks and limits. The documentation site and repository bodies share one source.

Source version: 1ce3268Page source: docs/guides/generate-save-api.en.md