diff --git a/VERSION b/VERSION index 085135e..38f8e88 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.1 +dev diff --git a/cdmf_generation.py b/cdmf_generation.py index a0eeed9..5e07049 100644 --- a/cdmf_generation.py +++ b/cdmf_generation.py @@ -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() @@ -417,6 +419,7 @@ 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) @@ -424,6 +427,7 @@ def generate(): 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() @@ -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) diff --git a/cdmf_midi_generation_bp.py b/cdmf_midi_generation_bp.py index ccc5b5e..dead4c9 100644 --- a/cdmf_midi_generation_bp.py +++ b/cdmf_midi_generation_bp.py @@ -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__) @@ -88,22 +88,29 @@ 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_")) @@ -111,8 +118,7 @@ def midi_generate(): 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}") @@ -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 @@ -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) diff --git a/cdmf_paths.py b/cdmf_paths.py index caa9f03..240027d 100644 --- a/cdmf_paths.py +++ b/cdmf_paths.py @@ -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 @@ -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() diff --git a/cdmf_stem_splitting_bp.py b/cdmf_stem_splitting_bp.py index e7c175b..f731881 100644 --- a/cdmf_stem_splitting_bp.py +++ b/cdmf_stem_splitting_bp.py @@ -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 @@ -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) diff --git a/cdmf_template.py b/cdmf_template.py index 657cdfe..892624c 100644 --- a/cdmf_template.py +++ b/cdmf_template.py @@ -30,7 +30,7 @@
- {{ version or 'v0.1' }} + {{ version or 'dev' }} @@ -142,7 +142,8 @@
- + +
Required. Used as the base name for the generated track file (e.g. My Track.wav).
@@ -793,15 +794,16 @@
- + + value="voice_clone_output" + required>
- 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, …).
@@ -963,6 +965,19 @@
+
+ + +
+ Optional. If set, this prefix is added to generated stem filenames (e.g. myprefix_song_stems_vocals.wav). +
+
+
+ value="" + required>
- 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, …).
diff --git a/cdmf_voice_cloning_bp.py b/cdmf_voice_cloning_bp.py index c01aff6..7629b8c 100644 --- a/cdmf_voice_cloning_bp.py +++ b/cdmf_voice_cloning_bp.py @@ -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__) @@ -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" @@ -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)) @@ -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}") @@ -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 diff --git a/generate_ace.py b/generate_ace.py index 5e92a2d..79bd344 100644 --- a/generate_ace.py +++ b/generate_ace.py @@ -466,23 +466,9 @@ def _choose_effective_seed(seed: int) -> int: def _next_available_output_path(out_dir: Path, basename: str, ext: str = ".wav") -> Path: - out_dir = Path(out_dir) - out_dir.mkdir(parents=True, exist_ok=True) - - stem = Path(basename).stem - if not ext.startswith("."): - ext = "." + ext - - candidate = out_dir / f"{stem}{ext}" - if not candidate.exists(): - return candidate - - idx = 2 - while True: - candidate = out_dir / f"{stem}{idx}{ext}" - if not candidate.exists(): - return candidate - idx += 1 + """Use shared helper to avoid overwriting existing files (-1, -2, -3, ...).""" + stem = Path(basename).stem if basename else "output" + return cdmf_paths.get_next_available_output_path(out_dir, stem, ext) def _apply_vibe_to_tags(prompt: str, seed_vibe: str) -> str: diff --git a/static/scripts/cdmf.css b/static/scripts/cdmf.css index 7b87dc7..2f499ae 100644 --- a/static/scripts/cdmf.css +++ b/static/scripts/cdmf.css @@ -699,3 +699,4 @@ p { #consoleToggleIcon.expanded { transform: rotate(180deg); } + diff --git a/static/scripts/cdmf_generation_ui.js b/static/scripts/cdmf_generation_ui.js index ff3b29e..da219c9 100644 --- a/static/scripts/cdmf_generation_ui.js +++ b/static/scripts/cdmf_generation_ui.js @@ -429,6 +429,17 @@ return false; } + // Base filename is required + const basenameEl = document.getElementById("basename"); + if (basenameEl && !(basenameEl.value || "").trim()) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + alert("Base filename is required. Please enter a name for the generated track."); + basenameEl.focus(); + return false; + } + // Sync Audio2Audio flag with the file selector try { const form = ev && ev.target ? ev.target : document; diff --git a/static/scripts/cdmf_midi_generation_ui.js b/static/scripts/cdmf_midi_generation_ui.js index 71c6695..66f5bf0 100644 --- a/static/scripts/cdmf_midi_generation_ui.js +++ b/static/scripts/cdmf_midi_generation_ui.js @@ -23,6 +23,13 @@ CDMF.onSubmitMidiGen = function (event) { event.preventDefault(); + var outputFilenameEl = document.getElementById("midi_gen_output_filename"); + if (outputFilenameEl && !(outputFilenameEl.value || "").trim()) { + alert("Output filename is required. Please enter a name for the MIDI file."); + outputFilenameEl.focus(); + return false; + } + // If basic-pitch model is not ready, trigger download (like ACE-Step "Download Models") var statusEl = document.getElementById("midiGenModelStatusNotice"); var downloadBtn = document.getElementById("midiGenDownloadModelsBtn"); @@ -174,6 +181,16 @@ } setVal("midi_gen_output_filename", settings.basename); + + // Restore input file (show basename from full path) + var inputFileName = settings.original_file || settings.input_file; + if (inputFileName && typeof inputFileName === "string") { + var inputFileEl = document.getElementById("midi_gen_input_file"); + if (inputFileEl && typeof CDMF.restoreFileInput === "function") { + CDMF.restoreFileInput(inputFileEl, inputFileName); + } + } + setNumPair("midi_gen_onset_threshold", "midi_gen_onset_threshold_range", settings.onset_threshold); setNumPair("midi_gen_frame_threshold", "midi_gen_frame_threshold_range", settings.frame_threshold); setNumPair("midi_gen_minimum_note_length_ms", "midi_gen_minimum_note_length_ms_range", settings.minimum_note_length_ms); diff --git a/static/scripts/cdmf_presets_ui.js b/static/scripts/cdmf_presets_ui.js index b515a44..dcf973a 100644 --- a/static/scripts/cdmf_presets_ui.js +++ b/static/scripts/cdmf_presets_ui.js @@ -169,6 +169,62 @@ }; } + // --------------------------------------------------------------------------- + // Helper: Restore file input from path using DataTransfer API + // Only restores if file is accessible, otherwise does nothing + // --------------------------------------------------------------------------- + + CDMF.restoreFileInput = function(fileInput, filePath) { + if (!fileInput || !filePath || typeof filePath !== "string") { + return; + } + + // Extract basename from path (handle both / and \ separators) + var basename = filePath.split(/[/\\]/).pop() || filePath; + + // Try to fetch the file and restore it + // For files in output directory, try to serve via /music/ + // For other paths, try direct fetch (may fail due to CORS/file:// restrictions) + var url = filePath; + + // If path looks like it's in the output directory, try /music/ + // This is a heuristic - we check if the path ends with the basename + if (filePath.includes(basename)) { + // Try serving via /music endpoint if it's in output directory + url = "/music/" + encodeURIComponent(basename); + } + + // Try to fetch and restore the file + fetch(url) + .then(function(response) { + if (!response.ok) { + throw new Error("File not accessible"); + } + return response.blob(); + }) + .then(function(blob) { + // Create a File object from the blob + var file = new File([blob], basename, { + type: blob.type || "application/octet-stream", + lastModified: new Date() + }); + + // Use DataTransfer to set the file + var dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + fileInput.files = dataTransfer.files; + + // Set data-file attribute for Safari compatibility + if (fileInput.webkitEntries && fileInput.webkitEntries.length) { + fileInput.setAttribute("data-file", basename); + } + }) + .catch(function() { + // File doesn't exist or isn't accessible - do nothing + // (e.g., deleted temp files from uploads) + }); + }; + // --------------------------------------------------------------------------- // Apply settings → form (used by presets + tracks) // --------------------------------------------------------------------------- @@ -177,7 +233,7 @@ if (!settings) return; // Voice Clone tracks: switch to Voice Clone tab and fill that form - if (settings.generator === "voice_clone") { + if (settings.generator === "tts" || settings.generator === "voice_clone") { if (typeof CDMF.applyVoiceCloneSettingsToForm === "function") { CDMF.applyVoiceCloneSettingsToForm(settings); return; @@ -185,13 +241,21 @@ } // Stem Split tracks: switch to Stem Splitting tab and fill that form - if (settings.generator === "stem_split") { + if (settings.generator === "stem" || settings.generator === "stem_split") { if (typeof CDMF.applyStemSplitSettingsToForm === "function") { CDMF.applyStemSplitSettingsToForm(settings); return; } } + // MIDI Generation tracks: switch to MIDI Generation tab and fill that form + if (settings.generator === "midi" || settings.generator === "midi_generation") { + if (typeof CDMF.applyMidiGenSettingsToForm === "function") { + CDMF.applyMidiGenSettingsToForm(settings); + return; + } + } + const promptField = document.getElementById("prompt"); const lyricsField = document.getElementById("lyrics"); const instrumentalCheckbox = document.getElementById("instrumental"); @@ -354,8 +418,18 @@ if (refAudioStrengthField && settings.ref_audio_strength != null) { refAudioStrengthField.value = String(settings.ref_audio_strength); } - if (srcAudioPathField && typeof settings.src_audio_path === "string") { - srcAudioPathField.value = settings.src_audio_path; + if (srcAudioPathField) { + // Restore from input_file_path if available (new field), otherwise src_audio_path + if (settings.input_file_path && typeof settings.input_file_path === "string") { + srcAudioPathField.value = settings.input_file_path; + } else if (settings.src_audio_path && typeof settings.src_audio_path === "string") { + srcAudioPathField.value = settings.src_audio_path; + } + // Show indicator if input_file exists but file input can't be set + if (settings.input_file && typeof settings.input_file === "string" && refAudioFileField) { + // Can't set file input directly, but we can show a helper message + // The src_audio_path field above should handle the path case + } } if (loraNameField && typeof settings.lora_name_or_path === "string") { loraNameField.value = settings.lora_name_or_path; diff --git a/static/scripts/cdmf_stem_splitting_ui.js b/static/scripts/cdmf_stem_splitting_ui.js index 91aea65..36a5369 100644 --- a/static/scripts/cdmf_stem_splitting_ui.js +++ b/static/scripts/cdmf_stem_splitting_ui.js @@ -383,6 +383,16 @@ setVal("stem_split_mode", settings.mode || ""); setVal("stem_split_export_format", settings.export_format); setVal("stem_split_out_dir", settings.out_dir); + setVal("stem_split_base_filename", settings.base_filename || ""); + + // Restore input file (show basename from full path) + var inputFileName = settings.original_file || settings.input_file; + if (inputFileName && typeof inputFileName === "string") { + var inputFileEl = document.getElementById("stem_split_input_file"); + if (inputFileEl && typeof CDMF.restoreFileInput === "function") { + CDMF.restoreFileInput(inputFileEl, inputFileName); + } + } }; // Update mode-specific UI when stem_count or mode changes diff --git a/static/scripts/cdmf_voice_cloning_ui.js b/static/scripts/cdmf_voice_cloning_ui.js index b2096be..a225cc2 100644 --- a/static/scripts/cdmf_voice_cloning_ui.js +++ b/static/scripts/cdmf_voice_cloning_ui.js @@ -9,7 +9,14 @@ CDMF.onSubmitVoiceClone = function (event) { event.preventDefault(); - + + var outputFilenameEl = document.getElementById("voice_clone_output_filename"); + if (outputFilenameEl && !(outputFilenameEl.value || "").trim()) { + alert("Output filename is required. Please enter a name for the output file."); + outputFilenameEl.focus(); + return; + } + var form = event.target; var formData = new FormData(form); @@ -148,6 +155,14 @@ setVal("voice_clone_output_filename", settings.basename); setVal("voice_clone_language", settings.language); setVal("voice_clone_device", settings.device_preference); + + // Restore input file (show basename from full path) + if (settings.input_file && typeof settings.input_file === "string") { + var inputFileEl = document.getElementById("speaker_wav"); + if (inputFileEl && typeof CDMF.restoreFileInput === "function") { + CDMF.restoreFileInput(inputFileEl, settings.input_file); + } + } setNumPair("voice_clone_temperature", "voice_clone_temperature_range", settings.temperature); setNumPair("voice_clone_length_penalty", "voice_clone_length_penalty_range", settings.length_penalty); setNumPair("voice_clone_repetition_penalty", "voice_clone_repetition_penalty_range", settings.repetition_penalty);