Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ jobs:

- name: Display Python version
run: python --version


- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build new UI (React SPA) with Bun
run: ./scripts/build_ui.sh

- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -67,6 +74,20 @@ jobs:
pip install "coqpit" "trainer>=0.0.32" "pysbd>=0.3.4" "inflect>=5.6.0" "unidecode>=1.3.2"
pip install "TTS==0.21.2"
python -c "from TTS.api import TTS" || (echo "::error::TTS import failed. Voice cloning will not work." && exit 1)

- name: Install Demucs for stem splitting
run: |
pip install "demucs==4.0.1"
python -c "import demucs.separate" || (echo "::warning::Demucs import failed. Stem splitting may not work." && true)

- name: Install basic-pitch for MIDI generation
run: |
pip install "basic-pitch>=0.4.0"
python -c "from basic_pitch.inference import predict" || (echo "::warning::basic-pitch import failed. MIDI generation may not work." && true)

- name: Slim bundle (remove Sudachi if present)
run: |
pip uninstall -y SudachiDict-core SudachiPy sudachidict-core sudachipy 2>/dev/null || true

- name: Install PyInstaller
run: |
Expand Down Expand Up @@ -150,4 +171,5 @@ jobs:
with:
files: |
AceForge-macOS.dmg
AceForge-macOS.zip
checksums.txt
68 changes: 68 additions & 0 deletions .github/workflows/new-ui-api-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# New UI API integration tests: real Flask app, no mocks.
# Asserts API contract (paths and JSON shape) for ace-step-ui compatibility.

name: New UI API Tests

on:
push:
branches: [main, develop, experimental-ui]
pull_request:
branches: [main, develop, experimental-ui]
workflow_dispatch:

permissions:
contents: read

jobs:
api-tests:
name: New UI API integration tests
runs-on: macos-latest
timeout-minutes: 25

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements_ace_macos.txt
pip install pytest

- name: Install optional deps (no-deps)
run: |
pip install "audio-separator==0.40.0" --no-deps || true
pip install "py3langid==0.3.0" --no-deps || true
pip install "git+https://github.com/ace-step/ACE-Step.git" --no-deps || true

- name: Run New UI API integration tests
run: |
cd "$GITHUB_WORKSPACE"
python -m pytest tests/test_new_ui_api.py -v --tb=short

build-ui:
name: Build new UI (React SPA) with Bun
runs-on: macos-latest
timeout-minutes: 10

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Build UI
run: |
cd ui
bun install --frozen-lockfile 2>/dev/null || bun install
bun run build
test -f dist/index.html
test -d dist/assets || true
26 changes: 26 additions & 0 deletions .github/workflows/test-bundled-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,32 @@ jobs:
./build/macos/codesign.sh dist/AceForge.app
env:
MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY || '-' }}

- name: Test training entry point (--train) from bundled app
run: |
BUNDLED_BIN="./dist/AceForge.app/Contents/MacOS/AceForge_bin"
if [ ! -f "$BUNDLED_BIN" ]; then
echo "::error::Bundled binary not found at $BUNDLED_BIN"
exit 1
fi
echo "Running bundled app with --train --help (must exit 0 and print trainer options)..."
OUTPUT=$("$BUNDLED_BIN" --train --help 2>&1) || EXIT=$?
EXIT=${EXIT:-0}
if [ "$EXIT" -ne 0 ]; then
echo "::error::Binary with --train --help exited with code $EXIT"
echo "$OUTPUT"
exit 1
fi
for opt in "--dataset_path" "--exp_name" "--epochs" "--max_steps"; do
if echo "$OUTPUT" | grep -q -- "$opt"; then
echo "✓ Trainer option $opt present"
else
echo "::error::Trainer help missing option: $opt"
echo "$OUTPUT"
exit 1
fi
done
echo "✓ Training-from-bundle entry point (--train) works correctly"

- name: Download ACE-Step models (if not cached)
if: steps.cache-models.outputs.cache-hit != 'true'
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ release/
*.app
*.spec.bak

# ---- New UI (React/Vite) ----
ui/node_modules/
ui/dist/

# ---- Minification / tooling caches ----
node_modules/
npm-debug.log*
Expand Down
6 changes: 5 additions & 1 deletion CDMF.spec
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ static_dir = spec_root / 'static'
training_config_dir = spec_root / 'training_config'
ace_models_dir = spec_root / 'ace_models'
icon_path = spec_root / 'build' / 'macos' / 'AceForge.icns'
ui_dist_dir = spec_root / 'ui' / 'dist'

# Collect _lzma binary explicitly (critical for py3langid in frozen apps)
# PyInstaller should auto-detect it, but we ensure it's included
Expand Down Expand Up @@ -216,7 +217,8 @@ a = Analysis(
('presets.json', '.'),
# Include VERSION file (placed in MacOS directory for frozen apps)
('VERSION', '.'),
] + _py3langid_data + _acestep_lyrics_data + _tokenizers_data + _basic_pitch_data + _tts_data + _tts_vocoder_configs + _trainer_data + _gruut_data + _jamo_data + _demucs_data,
# Include new React UI (built by build_local.sh / scripts/build_ui.sh)
] + ([(str(ui_dist_dir), 'ui/dist')] if ui_dist_dir.is_dir() else []) + _py3langid_data + _acestep_lyrics_data + _tokenizers_data + _basic_pitch_data + _tts_data + _tts_vocoder_configs + _trainer_data + _gruut_data + _jamo_data + _demucs_data,
hiddenimports=[
'diffusers',
'diffusers.loaders',
Expand All @@ -242,6 +244,8 @@ a = Analysis(
'huggingface_hub',
# ACE-Step wrapper module (imported with try/except in generate_ace.py)
'cdmf_pipeline_ace_step',
# Trainer CLI parser (--train --help path; avoids loading full cdmf_trainer in frozen app)
'cdmf_trainer_parser',
# Lyrics prompt model (lazily imported in cdmf_generation.py)
'lyrics_prompt_model',
# ACE-Step package and all its submodules (critical for frozen app)
Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ AceForge is a **local-first AI music workstation for macOS Silicon** powered by

> Status: **ALPHA**

<img width="779" height="588" alt="image" src="https://github.com/user-attachments/assets/bd520fcb-7c79-450e-8613-6e30a252a8ca" />
<img width="700" alt="AceForge-UI" src="https://github.com/user-attachments/assets/e987b9fe-dee7-43a4-9371-9608baad6a20" />


## Features
Expand All @@ -28,18 +28,16 @@ AceForge is a **local-first AI music workstation for macOS Silicon** powered by

### Minimum

- macOS 12.0 (Monterey) or later
- Apple Silicon (M1/M2/M3) or Intel Mac with AMD GPU
- Apple Silicon (M1/M2/M3)
- 16 GB unified memory (for Apple Silicon) or 16 GB RAM
- ~10–12 GB VRAM/unified memory (more = more headroom)
- ~8–16 GB VRAM/unified memory (more = more headroom)
- SSD with tens of GB free (models + audio + datasets)

### Recommended

- Apple Silicon M1 Pro/Max/Ultra, M2 Pro/Max/Ultra, or M3 Pro/Max
- Apple Silicon M4 Pro or M3 Pro/Max
- 32 GB+ unified memory
- Fast SSD
- Comfort reading terminal logs when something goes wrong

## Install and run

Expand Down Expand Up @@ -202,5 +200,6 @@ Issues and PRs welcome. If you’re changing anything related to training, model

## License

This project’s **source code** is licensed under the **Apache License 2.0**. See `LICENSE`.
AceForge is licensed under the **Apache License 2.0**. See `LICENSE`.

THe UI is forked and extended from [Ace-Step UI](https://github.com/fspecii/ace-step-ui) (MIT)
14 changes: 14 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,20 @@ The **Advanced** tab exposes more ACE-Step internals:
- Browse for a LoRA folder under `custom_lora`.
- Set the LoRA weight (0–10).

### 5.5 Cover and Audio→Audio: key parameters

In **Cover** and **Audio→Audio** modes you transform an existing track. The following parameters directly control how much the output follows the **source audio** vs your **Style** and **Lyrics**:

| Parameter | What it controls | Effect |
|-----------|------------------|--------|
| **Style of Music** (caption) | Target style for the output | Describes the *target* genre, mood, instruments. Strongly influences the result when Cover Strength is lower. |
| **Lyrics** | Target lyrics for the output | The *target* lyric content and structure. Uncheck **Instrumental** to use them; otherwise the model gets an instrumental token. |
| **Cover Strength** (Source influence) | Balance: source vs your text | **1.0** = output follows the source closely (structure + character). **Lower (e.g. 0.5–0.7)** = more influence from your Style and Lyrics. **0.2** = loose style transfer. |
| **Instrumental** | Whether lyrics are used | When checked, lyrics are ignored and the model receives an instrumental token. Uncheck to apply your Lyrics. |
| **Guidance scale** | How strongly the model follows text | Higher = stronger adherence to your Style/Lyrics (and to the source when combined with high Cover Strength). |

**Summary:** For covers that reflect your own style and lyrics, set **Style** and **Lyrics** as desired, uncheck **Instrumental** if you use lyrics, and lower **Cover Strength** (e.g. 0.5–0.7) so your text has more influence. The (i) tooltips in the Create panel repeat this for quick reference.

If you're new to ACE-Step, you can ignore the Advanced tab entirely. The defaults were
chosen to be safe and high quality out of the box.

Expand Down
41 changes: 38 additions & 3 deletions aceforge_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,19 @@ def _patched_getsource(obj):
except Exception as e:
print(f"[AceForge] WARNING: lzma initialization: {e}", flush=True)

# When frozen and launched with --train, run the LoRA trainer in this process and exit (no GUI).
# This allows Training to work from the app bundle; the parent app spawns us with --train + args.
# For --train --help we only load the parser (no heavy deps) so the bundle test can pass.
if getattr(sys, "frozen", False) and "--train" in sys.argv:
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a != "--train"]
if "--help" in sys.argv or "-h" in sys.argv:
from cdmf_trainer_parser import _make_parser
_make_parser().print_help()
sys.exit(0)
from cdmf_trainer import run_from_argv
run_from_argv()
sys.exit(0)

# Import pywebview FIRST and patch it BEFORE importing music_forge_ui
# This ensures that even if music_forge_ui tries to use webview, it will be protected
import webview
Expand Down Expand Up @@ -428,7 +441,7 @@ def on_window_closed():
# Create pywebview window pointing to Flask server
# The singleton wrapper ensures this can only be called once
window = webview.create_window(
title="AceForge - AI Music Generation",
title="AceForge",
url=SERVER_URL,
width=1400,
height=900,
Expand Down Expand Up @@ -458,9 +471,31 @@ def on_window_closed():
import atexit
atexit.register(cleanup_resources)

# Apply zoom from preferences (default 80%); takes effect on next launch if changed in Settings
try:
from cdmf_paths import load_config
_cfg = load_config()
_z = int(_cfg.get("ui_zoom") or 80)
_z = max(50, min(150, _z))
except Exception:
_z = 80
_WEBVIEW_ZOOM = f"{_z}%"
_WEBVIEW_ZOOM_JS = f'document.documentElement.style.zoom = "{_WEBVIEW_ZOOM}";'

def _apply_webview_zoom(win):
time.sleep(1.8) # allow initial page load
try:
if hasattr(win, 'run_js'):
win.run_js(_WEBVIEW_ZOOM_JS)
else:
win.evaluate_js(_WEBVIEW_ZOOM_JS)
print(f"[AceForge] Webview zoom set to {_WEBVIEW_ZOOM}", flush=True)
except Exception as e:
print(f"[AceForge] Could not set webview zoom: {e}", flush=True)

# Start the GUI event loop (only once - this is a blocking call)
# The singleton wrapper ensures this can only be called once globally
webview.start(debug=False)
# Pass _apply_webview_zoom so it runs in a separate thread after window is ready
webview.start(_apply_webview_zoom, window, debug=False)

# This should not be reached (on_window_closed exits), but just in case
cleanup_resources()
Expand Down
25 changes: 25 additions & 0 deletions api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# AceForge New UI API compatibility layer.
# Blueprints match Express routes from ace-step-ui for the ported React front end.
# No auth (local-only); all persistence via cdmf_paths global app settings.

from api.auth import bp as auth_bp
from api.songs import bp as songs_bp
from api.generate import bp as generate_bp
from api.playlists import bp as playlists_bp
from api.users import bp as users_bp
from api.contact import bp as contact_bp
from api.reference_tracks import bp as reference_tracks_bp
from api.search import bp as search_bp
from api.preferences import bp as preferences_bp

__all__ = [
"auth_bp",
"songs_bp",
"generate_bp",
"playlists_bp",
"users_bp",
"contact_bp",
"reference_tracks_bp",
"search_bp",
"preferences_bp",
]
Loading