thinking-lm: Quality presets, wire Thinking/LM params, LM planner fro…#45
thinking-lm: Quality presets, wire Thinking/LM params, LM planner fro…#45
Conversation
…m Settings - CreatePanel: Quality preset (Fast/Good/Best/Custom) sets steps, guidance, Thinking, CoT - API/generate_ace: pass thinking, useCot*, lm* and lm_checkpoint_path to pipeline - generate_ace: resolve LM path from Settings (ace_step_lm); no external LLM - Pipeline: accept thinking/CoT/LM kwargs and lm_checkpoint_path; optional _refine_prompt_with_lm when ACE-Step 1.5 LM API available - Settings: LM planner copy says bundled 5Hz LM, no external LLM - ace_step_models API + acestep15_downloader for model list/downloads Co-authored-by: Cursor <cursoragent@cursor.com>
…anced - Quality presets: Basic/Great/Best (both Simple and Advanced), tuned from ACE-Step docs - Simple: Quality buttons + Generation influence sliders (Weirdness, Style, Audio when ref) - Exclude styles (negative prompt) Suno-style above sliders in Simple, and in Advanced - API: pass negativePrompt to generate_track_ace - Remove unused Track Name and Complete Track Classes from Advanced and types Co-authored-by: Cursor <cursoragent@cursor.com>
- Add Title field in Simple mode (same as Custom) - Save title, lyrics, style to track metadata on generation success; songs API uses meta title - Advanced Settings and Music Parameters only in Custom mode; Simple has no advanced section - Add Reference & cover (optional) to Simple so ref/cover not hidden; infer taskType cover when source set - Remove trackName/completeTrackClasses from App genParams Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
This PR expands the ACE-Step integration across the UI + Flask API by adding quality presets, negative prompting, per-job progress/ETA reporting, and a new ACE-Step model management API (list/download/status) including a bundled downloader for packaged builds.
Changes:
- UI: add quality presets, “Exclude styles” (negative prompt), and improved job progress display (percent/steps/ETA).
- API/pipeline: wire Thinking/CoT/LM params through to generation, add per-job progress callback plumbing, and persist track metadata (title/lyrics/style).
- Add
/api/ace-step/*endpoints plus vendoredacestep15_downloaderand update preferences/docs to support DiT/LM selection.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/types.ts | Adds generation progress fields to Song and adds negativePrompt to generation params. |
| ui/services/api.ts | Adds ACE-Step models API typings + endpoints; adds job progress fields to GenerationJob; adds preferences keys. |
| ui/components/SongList.tsx | Displays generation progress percent/steps/ETA while generating. |
| ui/components/SettingsModal.tsx | Adds ACE-Step Models settings UI + downloader progress/cancel UI. |
| ui/components/Player.tsx | Hardens seek/progress calculations against zero/invalid duration. |
| ui/components/CreatePanel.tsx | Adds quality presets, negative prompt, and Simple-mode influence sliders; removes unused fields. |
| ui/App.tsx | Improves audio duration/seek behavior; updates temp-song progress fields from job status polling. |
| music_forge_ui.py | Adds per-thread job id log prefixing and registers the new ACE-Step models blueprint. |
| generate_ace.py | Adds job-progress callback support, LM checkpoint resolution from preferences, and accepts new task types. |
| cdmf_state.py | Adds thread-local current job id helpers for progress/log tagging. |
| cdmf_pipeline_ace_step.py | Adds optional LM prompt refinement hook and forces vocal language tokenization when specified. |
| api/generate.py | Wires Thinking/CoT/LM params, negative prompt, job progress reporting, and saves track metadata on success. |
| api/ace_step_models.py | New API for listing/downloading/cancelling ACE-Step DiT/LM models + discovered checkpoints. |
| api/songs.py | Uses saved metadata title if present (fallback to stem). |
| docs/ACEFORGE_API.md | Documents new preferences keys and /api/ace-step/* routes. |
| acestep15_downloader/* | Vendored downloader with progress callback + cancel support. |
| CDMF.spec | Ensures downloader modules are bundled in the frozen app. |
Comments suppressed due to low confidence (1)
generate_ace.py:588
lego/extract/completeare now accepted task types here, but the current pipeline (cdmf_pipeline_ace_step.ACEStepPipeline.__call__) will force any run withaudio2audio_enable+ref_audio_inputtotask = 'audio2audio', so these tasks won’t execute their intended behavior (they effectively run as audio2audio/retake). Consider rejecting these tasks with a clear error until the full ACE-Step 1.5 task implementation is available, or add explicit handling so the requested task is preserved.
task_norm = (task or "text2music").strip().lower()
if task_norm not in ("text2music", "retake", "repaint", "extend", "cover", "audio2audio", "lego", "extract", "complete"):
task_norm = "text2music"
# Map UI task names to pipeline task: cover and audio2audio both run as retake
# (pipeline will set task to "audio2audio" when ref_audio_input is passed).
if task_norm in ("cover", "audio2audio"):
task_norm = "retake"
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const genParams = { | ||
| customMode: params.customMode, | ||
| songDescription: params.songDescription, | ||
| lyrics: params.lyrics, | ||
| style: params.style, | ||
| title: params.title, | ||
| instrumental: params.instrumental, | ||
| vocalLanguage: params.vocalLanguage, | ||
| duration: params.duration, | ||
| bpm: params.bpm, | ||
| keyScale: params.keyScale, | ||
| timeSignature: params.timeSignature, | ||
| inferenceSteps: params.inferenceSteps, | ||
| guidanceScale: params.guidanceScale, | ||
| batchSize: params.batchSize, | ||
| randomSeed: params.randomSeed, | ||
| seed: params.seed, | ||
| thinking: params.thinking, | ||
| audioFormat: params.audioFormat, | ||
| inferMethod: params.inferMethod, | ||
| shift: params.shift, | ||
| lmTemperature: params.lmTemperature, | ||
| lmCfgScale: params.lmCfgScale, | ||
| lmTopK: params.lmTopK, | ||
| lmTopP: params.lmTopP, | ||
| lmNegativePrompt: params.lmNegativePrompt, | ||
| referenceAudioUrl: params.referenceAudioUrl, | ||
| sourceAudioUrl: params.sourceAudioUrl, | ||
| audioCodes: params.audioCodes, | ||
| repaintingStart: params.repaintingStart, | ||
| repaintingEnd: params.repaintingEnd, | ||
| instruction: params.instruction, | ||
| audioCoverStrength: params.audioCoverStrength, | ||
| taskType: params.taskType, | ||
| useAdg: params.useAdg, | ||
| cfgIntervalStart: params.cfgIntervalStart, | ||
| cfgIntervalEnd: params.cfgIntervalEnd, | ||
| customTimesteps: params.customTimesteps, | ||
| useCotMetas: params.useCotMetas, | ||
| useCotCaption: params.useCotCaption, | ||
| useCotLanguage: params.useCotLanguage, | ||
| autogen: params.autogen, | ||
| constrainedDecodingDebug: params.constrainedDecodingDebug, | ||
| allowLmBatch: params.allowLmBatch, | ||
| getScores: params.getScores, | ||
| getLrc: params.getLrc, | ||
| scoreScale: params.scoreScale, | ||
| lmBatchChunkSize: params.lmBatchChunkSize, | ||
| trackName: params.trackName, | ||
| completeTrackClasses: params.completeTrackClasses, | ||
| isFormatCaption: params.isFormatCaption, | ||
| ...(prefs.output_dir ? { outputDir: prefs.output_dir } : {}), | ||
| }; |
There was a problem hiding this comment.
negativePrompt is included in ui/types.ts and is populated in CreatePanel, but it is not forwarded into the genParams object sent to POST /api/generate here. As a result, the backend never receives the negative prompt and api/generate.py will always see it as empty. Add negativePrompt: params.negativePrompt (or equivalent mapping) when building genParams.
| const ditVal = prefs.ace_step_dit_model ?? 'turbo'; | ||
| setAceStepDitModel(ACE_STEP_DIT_OPTIONS.some((o) => o.value === ditVal) ? ditVal : 'turbo'); | ||
| const lmVal = prefs.ace_step_lm ?? '1.7B'; | ||
| setAceStepLm(ACE_STEP_LM_OPTIONS.some((o) => o.value === lmVal) ? lmVal : '1.7B'); |
There was a problem hiding this comment.
This validation only allows ace_step_dit_model values that match ACE_STEP_DIT_OPTIONS, so any previously selected discovered/custom DiT model id from the checkpoints folder will be overwritten to turbo when opening Settings. To support discovered models, keep the raw preference value here and validate/sanitize only after aceStepList is loaded (or validate against both preset + discovered ids).
| use_cot_metas=True, | ||
| use_cot_caption=True, | ||
| use_cot_language=True, | ||
| lm_temperature=0.85, | ||
| lm_cfg_scale=2.0, | ||
| lm_top_k=0, | ||
| lm_top_p=0.9, | ||
| lm_negative_prompt="NO USER INPUT", |
There was a problem hiding this comment.
use_cot_metas, use_cot_caption, use_cot_language, lm_cfg_scale, and lm_negative_prompt are accepted here but currently unused, which is misleading and makes it hard to reason about which LM settings actually take effect. Either wire these parameters into the ACE-Step LM call (if supported) or remove them from the helper signature until they are implemented.
| use_cot_metas=True, | |
| use_cot_caption=True, | |
| use_cot_language=True, | |
| lm_temperature=0.85, | |
| lm_cfg_scale=2.0, | |
| lm_top_k=0, | |
| lm_top_p=0.9, | |
| lm_negative_prompt="NO USER INPUT", | |
| lm_temperature=0.85, | |
| lm_top_k=0, | |
| lm_top_p=0.9, |
|
|
||
| from pathlib import Path | ||
| import subprocess | ||
| import sys |
There was a problem hiding this comment.
sys is imported but never used in this module. Removing unused imports helps keep the API module clean and avoids lint warnings.
| import sys |
| @bp.route("/models", methods=["GET"]) | ||
| @bp.route("/models/", methods=["GET"]) | ||
| def list_models(): | ||
| """ | ||
| GET /api/ace-step/models | ||
| Returns known DiT/LM models with installed status plus all discovered model dirs on disk (custom/trained). | ||
| """ | ||
| use_15 = _acestep_download_available() | ||
| # Known DiT: always include; installed = on disk (1.5 layout or v1 legacy) | ||
| dit = [] | ||
| for m in DIT_MODELS: | ||
| installed = _model_installed_15(m["id"], "dit") if use_15 else (m["id"] == "turbo" and _default_dit_installed()) | ||
| dit.append({**m, "installed": installed}) | ||
| # Known LM | ||
| lm = [] | ||
| for m in LM_MODELS: | ||
| installed = _model_installed_15(m["id"], "lm") if use_15 else (m["id"] == "none") | ||
| lm.append({**m, "installed": installed}) | ||
| # All model dirs found under checkpoints (custom trained, v1, 1.5, etc.) | ||
| discovered = _discover_model_dirs() | ||
| return jsonify({ | ||
| "dit_models": dit, | ||
| "lm_models": lm, | ||
| "discovered_models": discovered, | ||
| "acestep_download_available": use_15, | ||
| "checkpoints_path": str(_checkpoint_root()), | ||
| }) |
There was a problem hiding this comment.
New /api/ace-step/* routes are introduced here, but tests/test_new_ui_api.py currently has no coverage for listing models, starting a download, and checking status/cancel behavior. Adding at least contract-level tests (status codes + response shape) would help prevent regressions.
| except Exception: | ||
| pass | ||
| if _JOB_PROGRESS_CALLBACK is not None: | ||
| try: | ||
| _JOB_PROGRESS_CALLBACK(frac, stage, steps_current, steps_total, eta_seconds) | ||
| except Exception: | ||
| pass |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| pass | |
| if _JOB_PROGRESS_CALLBACK is not None: | |
| try: | |
| _JOB_PROGRESS_CALLBACK(frac, stage, steps_current, steps_total, eta_seconds) | |
| except Exception: | |
| pass | |
| except Exception as exc: | |
| print( | |
| f"[generate_ace] ERROR: progress callback raised an exception: {exc}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| if _JOB_PROGRESS_CALLBACK is not None: | |
| try: | |
| _JOB_PROGRESS_CALLBACK(frac, stage, steps_current, steps_total, eta_seconds) | |
| except Exception as exc: | |
| print( | |
| f"[generate_ace] ERROR: job progress callback raised an exception: {exc}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) |
| if _JOB_PROGRESS_CALLBACK is not None: | ||
| try: | ||
| _JOB_PROGRESS_CALLBACK(frac, stage, steps_current, steps_total, eta_seconds) | ||
| except Exception: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| eta_sec = fd.get("remaining") | ||
| if eta_sec is not None and not isinstance(eta_sec, (int, float)): | ||
| eta_sec = None | ||
| except Exception: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | ||
| pass |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| pass | |
| except Exception as exc: | |
| logger.exception("Error in progress callback during download: %s", exc) |
Extend Settings