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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.1
dev
15 changes: 13 additions & 2 deletions cdmf_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,9 @@ def generate():
out_dir = (
request.form.get("out_dir", DEFAULT_OUT_DIR).strip() or DEFAULT_OUT_DIR
)
basename = request.form.get("basename", "Candy Dreams").strip() or "Candy Dreams"
basename = request.form.get("basename", "").strip()
if not basename:
raise ValueError("Base filename is required and cannot be empty.")
seed_vibe = request.form.get("seed_vibe", "any").strip() or "any"

preset_id = request.form.get("preset_id", "").strip()
Expand All @@ -417,13 +419,15 @@ def generate():
# Determine reference-audio path
uploaded_ref = request.files.get("ref_audio_file")
src_audio_path: Optional[str] = None
ref_audio_filename: Optional[str] = None
if uploaded_ref and uploaded_ref.filename:
try:
filename = secure_filename(uploaded_ref.filename)
except Exception:
filename = uploaded_ref.filename or ""
if not filename:
filename = f"ref_{int(time.time() * 1000)}.wav"
ref_audio_filename = filename # Save original filename

name_root, ext = os.path.splitext(filename)
ext = (ext or "").lower()
Expand Down Expand Up @@ -580,7 +584,14 @@ def generate():
"lora_name_or_path", lora_name_or_path
)
entry["lora_weight"] = summary.get("lora_weight", lora_weight)
entry["generator"] = "ace_step"
entry["generator"] = "gen"
# Save input file as full path when available
if src_audio_path:
entry["input_file"] = src_audio_path
entry["input_file_path"] = src_audio_path
entry["src_audio_path"] = src_audio_path # Keep for backward compatibility
elif ref_audio_filename:
entry["input_file"] = ref_audio_filename # Fallback: filename only (legacy)

meta[wav_path.name] = entry
cdmf_tracks.save_track_meta(meta)
Expand Down
32 changes: 20 additions & 12 deletions cdmf_midi_generation_bp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from werkzeug.utils import secure_filename

import cdmf_tracks
from cdmf_paths import DEFAULT_OUT_DIR, APP_VERSION
from cdmf_paths import DEFAULT_OUT_DIR, APP_VERSION, get_next_available_output_path
from cdmf_midi_generation import get_midi_generator

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -88,31 +88,37 @@ def midi_generate():
melodia_trick = request.form.get("melodia_trick", "true").lower() == "true"
midi_tempo = float(request.form.get("midi_tempo", DEFAULT_MIDI_TEMPO))

# Get output filename
# Get output filename (required)
output_filename = request.form.get("output_filename", "").strip()
if not output_filename:
# Generate default filename from input
input_basename = Path(filename).stem
output_filename = f"{input_basename}_midi"
return jsonify({
"error": True,
"message": "Output filename is required and cannot be empty."
}), 400

# Ensure .mid extension
if not output_filename.lower().endswith('.mid'):
output_filename = f"{output_filename}.mid"
# Ensure .mid extension for stem
if output_filename.lower().endswith('.mid'):
stem = Path(output_filename).stem
else:
stem = output_filename

# Get output directory (same as music generation)
out_dir = request.form.get("out_dir", DEFAULT_OUT_DIR)
out_dir_path = Path(out_dir)
out_dir_path.mkdir(parents=True, exist_ok=True)

# Resolve path without overwriting existing files (-1, -2, …)
output_path = get_next_available_output_path(out_dir_path, stem, ".mid")
output_filename = output_path.name

# Save uploaded input file temporarily
import tempfile
temp_dir = Path(tempfile.mkdtemp(prefix="aceforge_midi_temp_"))
temp_input_path = temp_dir / filename
input_file.save(str(temp_input_path))

try:
# Generate output path
output_path = out_dir_path / output_filename
# output_path already set above (next-available, no overwrite)

# Perform MIDI generation
logger.info(f"[MIDI Generation] Starting: input={filename}, output={output_path}")
Expand Down Expand Up @@ -153,8 +159,9 @@ def midi_generate():
if "favorite" not in entry:
entry["favorite"] = False
entry["created"] = time.time()
entry["generator"] = "midi_generation"
entry["generator"] = "midi"
entry["basename"] = Path(midi_filename).stem
# original_file already saved below
entry["onset_threshold"] = onset_threshold
entry["frame_threshold"] = frame_threshold
entry["minimum_note_length_ms"] = minimum_note_length_ms
Expand All @@ -164,7 +171,8 @@ def midi_generate():
entry["melodia_trick"] = melodia_trick
entry["midi_tempo"] = midi_tempo
entry["out_dir"] = str(out_dir_path)
entry["original_file"] = filename
entry["original_file"] = str(temp_input_path)
entry["input_file"] = str(temp_input_path) # Full path for consistency
track_meta[midi_filename] = entry

cdmf_tracks.save_track_meta(track_meta)
Expand Down
81 changes: 69 additions & 12 deletions cdmf_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,33 @@ def _get_default_output_dir() -> Path:

DEFAULT_OUT_DIR = str(_get_default_output_dir())


def get_next_available_output_path(out_dir: Path | str, base_stem: str, ext: str = ".wav") -> Path:
"""
Return a path under out_dir for the given base name and extension that does not
yet exist. If the exact path exists, appends -1, -2, -3, etc. to avoid overwriting.
base_stem should not include the extension (e.g. "My Track" not "My Track.wav").
"""
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
if not ext.startswith("."):
ext = "." + ext
stem = (base_stem or "").strip()
if not stem:
stem = "output"
# Sanitize: remove path separators
stem = stem.replace("/", "_").replace("\\", "_").replace(":", "_")
candidate = out_dir / f"{stem}{ext}"
if not candidate.exists():
return candidate
idx = 1
while True:
candidate = out_dir / f"{stem}-{idx}{ext}"
if not candidate.exists():
return candidate
idx += 1


# Presets / tracks metadata / user presets
# Keep these in APP_DIR as they're bundled with the application
# User presets go in user data directory
Expand Down Expand Up @@ -189,19 +216,49 @@ def _get_custom_lora_root() -> Path:
def get_app_version() -> str:
"""
Read the application version from VERSION file.
Falls back to 'v0.1' if file doesn't exist or can't be read.
Falls back to 'dev' if file doesn't exist or can't be read.
The VERSION file is updated by GitHub Actions during release builds.

For frozen apps (PyInstaller), checks multiple locations:
1. sys._MEIPASS (PyInstaller temp extraction directory) - primary location
2. APP_DIR (executable directory) - fallback
3. Bundle root (for macOS app bundles) - fallback
"""
version_file = APP_DIR / "VERSION"
if version_file.exists():
try:
with version_file.open("r", encoding="utf-8") as f:
version = f.read().strip()
if version:
return version
except Exception as e:
print(f"[AceForge] Warning: Failed to read VERSION file: {e}", flush=True)
# Default fallback
return "v0.1"
# Try multiple locations for frozen apps
candidates = []

if getattr(sys, "frozen", False):
# For frozen apps, check sys._MEIPASS first (PyInstaller extraction dir)
# This is where PyInstaller extracts bundled files during execution
if hasattr(sys, "_MEIPASS"):
candidates.append(Path(sys._MEIPASS) / "VERSION")
# Also check executable directory (MacOS folder)
candidates.append(APP_DIR / "VERSION")
# For macOS app bundles, also check bundle root (Contents/)
if sys.platform == "darwin":
# sys.executable is Contents/MacOS/AceForge_bin
# APP_DIR is Contents/MacOS/
# Bundle root (Contents/) is APP_DIR.parent
bundle_root = APP_DIR.parent
candidates.append(bundle_root / "VERSION")
else:
# For development, just check APP_DIR (project root)
candidates.append(APP_DIR / "VERSION")

# Try each candidate location
for version_file in candidates:
if version_file.exists():
try:
with version_file.open("r", encoding="utf-8") as f:
version = f.read().strip()
if version:
print(f"[AceForge] Loaded version '{version}' from {version_file}", flush=True)
return version
except Exception as e:
print(f"[AceForge] Warning: Failed to read VERSION file from {version_file}: {e}", flush=True)

# Default fallback for development builds
print(f"[AceForge] No VERSION file found, using default 'dev'", flush=True)
return "dev"

APP_VERSION = get_app_version()
15 changes: 13 additions & 2 deletions cdmf_stem_splitting_bp.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ def stem_split():
input_basename = Path(filename).stem
# Sanitize basename
input_basename = input_basename.replace("/", "_").replace("\\", "_").replace(":", "_")
# Optional base filename prefix from form
base_filename = request.form.get("base_filename", "").strip()
if base_filename:
prefix = base_filename.replace("/", "_").replace("\\", "_").replace(":", "_")
input_basename = f"{prefix}_{input_basename}"

# Save uploaded input file temporarily
# Use a temp directory to avoid cluttering the output directory
Expand Down Expand Up @@ -151,15 +156,21 @@ def stem_split():
entry["favorite"] = False
entry["seconds"] = dur
entry["created"] = time.time()
entry["generator"] = "stem_split"
entry["generator"] = "stem"
entry["basename"] = Path(stem_filename).stem
# original_file already saved below
entry["stem_name"] = stem_name
entry["stem_count"] = stem_count
entry["mode"] = mode or ""
entry["export_format"] = export_format
entry["device_preference"] = device_preference
entry["out_dir"] = str(out_dir_path)
entry["original_file"] = filename
entry["original_file"] = str(temp_input_path)
entry["input_file"] = str(temp_input_path) # Full path for consistency
# Save base_filename if provided
base_filename = request.form.get("base_filename", "").strip()
if base_filename:
entry["base_filename"] = base_filename
track_meta[stem_filename] = entry

cdmf_tracks.save_track_meta(track_meta)
Expand Down
32 changes: 24 additions & 8 deletions cdmf_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<span class="cd-alpha">{{ version or 'v0.1' }}</span>
<span class="cd-alpha">{{ version or 'dev' }}</span>
<button type="button" class="btn danger exit" id="btnExitApp" title="Exit AceForge">
<span class="icon">🚪</span><span class="label">Exit</span>
</button>
Expand Down Expand Up @@ -142,7 +142,8 @@
<div id="coreKnobs">
<div class="row">
<label for="basename">Base filename</label>
<input id="basename" name="basename" type="text" value="{{ basename or 'Candy Dreams' }}">
<input id="basename" name="basename" type="text" value="{{ basename or 'Candy Dreams' }}" required>
<div class="small">Required. Used as the base name for the generated track file (e.g. My Track.wav).</div>
</div>

<div class="row" id="autoPromptLyricsRow">
Expand Down Expand Up @@ -793,15 +794,16 @@
</div>

<div class="row">
<label for="voice_clone_output_filename">Output Filename</label>
<label for="voice_clone_output_filename">Output filename</label>
<input
id="voice_clone_output_filename"
name="output_filename"
type="text"
placeholder="voice_clone_output"
value="voice_clone_output">
value="voice_clone_output"
required>
<div class="small">
Output filename (without extension). Will be saved as .wav in the output directory.
Required. Output filename (without extension). Saved as MP3 in the output directory. If the file already exists, a number is appended (-1, -2, …).
</div>
</div>

Expand Down Expand Up @@ -963,6 +965,19 @@
</div>
</div>

<div class="row">
<label for="stem_split_base_filename">Base filename (optional)</label>
<input
id="stem_split_base_filename"
name="base_filename"
type="text"
placeholder=""
value="">
<div class="small">
Optional. If set, this prefix is added to generated stem filenames (e.g. <em>myprefix</em>_song_stems_vocals.wav).
</div>
</div>

<div class="row">
<label for="stem_split_stem_count">Number of Stems</label>
<select id="stem_split_stem_count" name="stem_count">
Expand Down Expand Up @@ -1064,15 +1079,16 @@
</div>

<div class="row">
<label for="midi_gen_output_filename">Output Filename</label>
<label for="midi_gen_output_filename">Output filename</label>
<input
id="midi_gen_output_filename"
name="output_filename"
type="text"
placeholder="output_midi"
value="">
value=""
required>
<div class="small">
Output filename (without extension). Will be saved as .mid in the output directory. If left empty, uses input filename + "_midi".
Required. Output filename (without extension). Saved as .mid in the output directory. If the file already exists, a number is appended (-1, -2, …).
</div>
</div>

Expand Down
21 changes: 15 additions & 6 deletions cdmf_voice_cloning_bp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from werkzeug.utils import secure_filename

import cdmf_tracks
from cdmf_paths import DEFAULT_OUT_DIR, APP_VERSION
from cdmf_paths import DEFAULT_OUT_DIR, APP_VERSION, get_next_available_output_path
from cdmf_voice_cloning import get_voice_cloner

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -72,10 +72,13 @@ def voice_clone():
"message": "Invalid file format. Please use MP3, WAV, M4A, or FLAC."
}), 400

# Get output filename (default MP3 256k for cloned voices)
# Get output filename (required)
output_filename = request.form.get("output_filename", "").strip()
if not output_filename:
output_filename = "voice_clone_output"
return jsonify({
"error": True,
"message": "Output filename is required and cannot be empty."
}), 400
if not output_filename.lower().endswith((".wav", ".mp3")):
output_filename += ".mp3"

Expand All @@ -84,6 +87,12 @@ def voice_clone():
out_dir_path = Path(out_dir)
out_dir_path.mkdir(parents=True, exist_ok=True)

# Resolve stem and extension for next-available path
stem = Path(output_filename).stem
ext = Path(output_filename).suffix or ".mp3"
output_path = get_next_available_output_path(out_dir_path, stem, ext)
output_filename = output_path.name

# Save uploaded reference audio temporarily
temp_ref_path = out_dir_path / f"_temp_ref_{filename}"
speaker_file.save(str(temp_ref_path))
Expand All @@ -100,8 +109,7 @@ def voice_clone():
speed = float(request.form.get("speed", DEFAULT_SPEED))
enable_text_splitting = request.form.get("enable_text_splitting", "true").lower() == "true"

# Generate output path
output_path = out_dir_path / output_filename
# output_path already set above (next-available, no overwrite)

# Perform voice cloning
logger.info(f"[Voice Cloning] Starting: text='{text[:50]}...', language={language}, output={output_path}")
Expand Down Expand Up @@ -143,8 +151,9 @@ def voice_clone():
entry["favorite"] = False
entry["seconds"] = dur
entry["created"] = time.time()
entry["generator"] = "voice_clone"
entry["generator"] = "tts"
entry["basename"] = Path(final_name).stem
entry["input_file"] = str(temp_ref_path) # Full path to reference audio
entry["text"] = text
entry["language"] = language
entry["temperature"] = temperature
Expand Down
Loading