Skip to content

Commit 53c4c93

Browse files
authored
Merge pull request #40 from audiohacking/input-fields
Input fields
2 parents 39c144d + c121eb9 commit 53c4c93

14 files changed

Lines changed: 291 additions & 65 deletions

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.1
1+
dev

cdmf_generation.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,9 @@ def generate():
400400
out_dir = (
401401
request.form.get("out_dir", DEFAULT_OUT_DIR).strip() or DEFAULT_OUT_DIR
402402
)
403-
basename = request.form.get("basename", "Candy Dreams").strip() or "Candy Dreams"
403+
basename = request.form.get("basename", "").strip()
404+
if not basename:
405+
raise ValueError("Base filename is required and cannot be empty.")
404406
seed_vibe = request.form.get("seed_vibe", "any").strip() or "any"
405407

406408
preset_id = request.form.get("preset_id", "").strip()
@@ -417,13 +419,15 @@ def generate():
417419
# Determine reference-audio path
418420
uploaded_ref = request.files.get("ref_audio_file")
419421
src_audio_path: Optional[str] = None
422+
ref_audio_filename: Optional[str] = None
420423
if uploaded_ref and uploaded_ref.filename:
421424
try:
422425
filename = secure_filename(uploaded_ref.filename)
423426
except Exception:
424427
filename = uploaded_ref.filename or ""
425428
if not filename:
426429
filename = f"ref_{int(time.time() * 1000)}.wav"
430+
ref_audio_filename = filename # Save original filename
427431

428432
name_root, ext = os.path.splitext(filename)
429433
ext = (ext or "").lower()
@@ -580,7 +584,14 @@ def generate():
580584
"lora_name_or_path", lora_name_or_path
581585
)
582586
entry["lora_weight"] = summary.get("lora_weight", lora_weight)
583-
entry["generator"] = "ace_step"
587+
entry["generator"] = "gen"
588+
# Save input file as full path when available
589+
if src_audio_path:
590+
entry["input_file"] = src_audio_path
591+
entry["input_file_path"] = src_audio_path
592+
entry["src_audio_path"] = src_audio_path # Keep for backward compatibility
593+
elif ref_audio_filename:
594+
entry["input_file"] = ref_audio_filename # Fallback: filename only (legacy)
584595

585596
meta[wav_path.name] = entry
586597
cdmf_tracks.save_track_meta(meta)

cdmf_midi_generation_bp.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from werkzeug.utils import secure_filename
1414

1515
import cdmf_tracks
16-
from cdmf_paths import DEFAULT_OUT_DIR, APP_VERSION
16+
from cdmf_paths import DEFAULT_OUT_DIR, APP_VERSION, get_next_available_output_path
1717
from cdmf_midi_generation import get_midi_generator
1818

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

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

98-
# Ensure .mid extension
99-
if not output_filename.lower().endswith('.mid'):
100-
output_filename = f"{output_filename}.mid"
99+
# Ensure .mid extension for stem
100+
if output_filename.lower().endswith('.mid'):
101+
stem = Path(output_filename).stem
102+
else:
103+
stem = output_filename
101104

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

110+
# Resolve path without overwriting existing files (-1, -2, …)
111+
output_path = get_next_available_output_path(out_dir_path, stem, ".mid")
112+
output_filename = output_path.name
113+
107114
# Save uploaded input file temporarily
108115
import tempfile
109116
temp_dir = Path(tempfile.mkdtemp(prefix="aceforge_midi_temp_"))
110117
temp_input_path = temp_dir / filename
111118
input_file.save(str(temp_input_path))
112119

113120
try:
114-
# Generate output path
115-
output_path = out_dir_path / output_filename
121+
# output_path already set above (next-available, no overwrite)
116122

117123
# Perform MIDI generation
118124
logger.info(f"[MIDI Generation] Starting: input={filename}, output={output_path}")
@@ -153,8 +159,9 @@ def midi_generate():
153159
if "favorite" not in entry:
154160
entry["favorite"] = False
155161
entry["created"] = time.time()
156-
entry["generator"] = "midi_generation"
162+
entry["generator"] = "midi"
157163
entry["basename"] = Path(midi_filename).stem
164+
# original_file already saved below
158165
entry["onset_threshold"] = onset_threshold
159166
entry["frame_threshold"] = frame_threshold
160167
entry["minimum_note_length_ms"] = minimum_note_length_ms
@@ -164,7 +171,8 @@ def midi_generate():
164171
entry["melodia_trick"] = melodia_trick
165172
entry["midi_tempo"] = midi_tempo
166173
entry["out_dir"] = str(out_dir_path)
167-
entry["original_file"] = filename
174+
entry["original_file"] = str(temp_input_path)
175+
entry["input_file"] = str(temp_input_path) # Full path for consistency
168176
track_meta[midi_filename] = entry
169177

170178
cdmf_tracks.save_track_meta(track_meta)

cdmf_paths.py

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,33 @@ def _get_default_output_dir() -> Path:
128128

129129
DEFAULT_OUT_DIR = str(_get_default_output_dir())
130130

131+
132+
def get_next_available_output_path(out_dir: Path | str, base_stem: str, ext: str = ".wav") -> Path:
133+
"""
134+
Return a path under out_dir for the given base name and extension that does not
135+
yet exist. If the exact path exists, appends -1, -2, -3, etc. to avoid overwriting.
136+
base_stem should not include the extension (e.g. "My Track" not "My Track.wav").
137+
"""
138+
out_dir = Path(out_dir)
139+
out_dir.mkdir(parents=True, exist_ok=True)
140+
if not ext.startswith("."):
141+
ext = "." + ext
142+
stem = (base_stem or "").strip()
143+
if not stem:
144+
stem = "output"
145+
# Sanitize: remove path separators
146+
stem = stem.replace("/", "_").replace("\\", "_").replace(":", "_")
147+
candidate = out_dir / f"{stem}{ext}"
148+
if not candidate.exists():
149+
return candidate
150+
idx = 1
151+
while True:
152+
candidate = out_dir / f"{stem}-{idx}{ext}"
153+
if not candidate.exists():
154+
return candidate
155+
idx += 1
156+
157+
131158
# Presets / tracks metadata / user presets
132159
# Keep these in APP_DIR as they're bundled with the application
133160
# User presets go in user data directory
@@ -189,19 +216,49 @@ def _get_custom_lora_root() -> Path:
189216
def get_app_version() -> str:
190217
"""
191218
Read the application version from VERSION file.
192-
Falls back to 'v0.1' if file doesn't exist or can't be read.
219+
Falls back to 'dev' if file doesn't exist or can't be read.
193220
The VERSION file is updated by GitHub Actions during release builds.
221+
222+
For frozen apps (PyInstaller), checks multiple locations:
223+
1. sys._MEIPASS (PyInstaller temp extraction directory) - primary location
224+
2. APP_DIR (executable directory) - fallback
225+
3. Bundle root (for macOS app bundles) - fallback
194226
"""
195-
version_file = APP_DIR / "VERSION"
196-
if version_file.exists():
197-
try:
198-
with version_file.open("r", encoding="utf-8") as f:
199-
version = f.read().strip()
200-
if version:
201-
return version
202-
except Exception as e:
203-
print(f"[AceForge] Warning: Failed to read VERSION file: {e}", flush=True)
204-
# Default fallback
205-
return "v0.1"
227+
# Try multiple locations for frozen apps
228+
candidates = []
229+
230+
if getattr(sys, "frozen", False):
231+
# For frozen apps, check sys._MEIPASS first (PyInstaller extraction dir)
232+
# This is where PyInstaller extracts bundled files during execution
233+
if hasattr(sys, "_MEIPASS"):
234+
candidates.append(Path(sys._MEIPASS) / "VERSION")
235+
# Also check executable directory (MacOS folder)
236+
candidates.append(APP_DIR / "VERSION")
237+
# For macOS app bundles, also check bundle root (Contents/)
238+
if sys.platform == "darwin":
239+
# sys.executable is Contents/MacOS/AceForge_bin
240+
# APP_DIR is Contents/MacOS/
241+
# Bundle root (Contents/) is APP_DIR.parent
242+
bundle_root = APP_DIR.parent
243+
candidates.append(bundle_root / "VERSION")
244+
else:
245+
# For development, just check APP_DIR (project root)
246+
candidates.append(APP_DIR / "VERSION")
247+
248+
# Try each candidate location
249+
for version_file in candidates:
250+
if version_file.exists():
251+
try:
252+
with version_file.open("r", encoding="utf-8") as f:
253+
version = f.read().strip()
254+
if version:
255+
print(f"[AceForge] Loaded version '{version}' from {version_file}", flush=True)
256+
return version
257+
except Exception as e:
258+
print(f"[AceForge] Warning: Failed to read VERSION file from {version_file}: {e}", flush=True)
259+
260+
# Default fallback for development builds
261+
print(f"[AceForge] No VERSION file found, using default 'dev'", flush=True)
262+
return "dev"
206263

207264
APP_VERSION = get_app_version()

cdmf_stem_splitting_bp.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ def stem_split():
8484
input_basename = Path(filename).stem
8585
# Sanitize basename
8686
input_basename = input_basename.replace("/", "_").replace("\\", "_").replace(":", "_")
87+
# Optional base filename prefix from form
88+
base_filename = request.form.get("base_filename", "").strip()
89+
if base_filename:
90+
prefix = base_filename.replace("/", "_").replace("\\", "_").replace(":", "_")
91+
input_basename = f"{prefix}_{input_basename}"
8792

8893
# Save uploaded input file temporarily
8994
# Use a temp directory to avoid cluttering the output directory
@@ -151,15 +156,21 @@ def stem_split():
151156
entry["favorite"] = False
152157
entry["seconds"] = dur
153158
entry["created"] = time.time()
154-
entry["generator"] = "stem_split"
159+
entry["generator"] = "stem"
155160
entry["basename"] = Path(stem_filename).stem
161+
# original_file already saved below
156162
entry["stem_name"] = stem_name
157163
entry["stem_count"] = stem_count
158164
entry["mode"] = mode or ""
159165
entry["export_format"] = export_format
160166
entry["device_preference"] = device_preference
161167
entry["out_dir"] = str(out_dir_path)
162-
entry["original_file"] = filename
168+
entry["original_file"] = str(temp_input_path)
169+
entry["input_file"] = str(temp_input_path) # Full path for consistency
170+
# Save base_filename if provided
171+
base_filename = request.form.get("base_filename", "").strip()
172+
if base_filename:
173+
entry["base_filename"] = base_filename
163174
track_meta[stem_filename] = entry
164175

165176
cdmf_tracks.save_track_meta(track_meta)

cdmf_template.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
</span>
3131
</div>
3232
<div style="display:flex;align-items:center;gap:8px;">
33-
<span class="cd-alpha">{{ version or 'v0.1' }}</span>
33+
<span class="cd-alpha">{{ version or 'dev' }}</span>
3434
<button type="button" class="btn danger exit" id="btnExitApp" title="Exit AceForge">
3535
<span class="icon">🚪</span><span class="label">Exit</span>
3636
</button>
@@ -142,7 +142,8 @@
142142
<div id="coreKnobs">
143143
<div class="row">
144144
<label for="basename">Base filename</label>
145-
<input id="basename" name="basename" type="text" value="{{ basename or 'Candy Dreams' }}">
145+
<input id="basename" name="basename" type="text" value="{{ basename or 'Candy Dreams' }}" required>
146+
<div class="small">Required. Used as the base name for the generated track file (e.g. My Track.wav).</div>
146147
</div>
147148
148149
<div class="row" id="autoPromptLyricsRow">
@@ -793,15 +794,16 @@
793794
</div>
794795
795796
<div class="row">
796-
<label for="voice_clone_output_filename">Output Filename</label>
797+
<label for="voice_clone_output_filename">Output filename</label>
797798
<input
798799
id="voice_clone_output_filename"
799800
name="output_filename"
800801
type="text"
801802
placeholder="voice_clone_output"
802-
value="voice_clone_output">
803+
value="voice_clone_output"
804+
required>
803805
<div class="small">
804-
Output filename (without extension). Will be saved as .wav in the output directory.
806+
Required. Output filename (without extension). Saved as MP3 in the output directory. If the file already exists, a number is appended (-1, -2, …).
805807
</div>
806808
</div>
807809
@@ -963,6 +965,19 @@
963965
</div>
964966
</div>
965967
968+
<div class="row">
969+
<label for="stem_split_base_filename">Base filename (optional)</label>
970+
<input
971+
id="stem_split_base_filename"
972+
name="base_filename"
973+
type="text"
974+
placeholder=""
975+
value="">
976+
<div class="small">
977+
Optional. If set, this prefix is added to generated stem filenames (e.g. <em>myprefix</em>_song_stems_vocals.wav).
978+
</div>
979+
</div>
980+
966981
<div class="row">
967982
<label for="stem_split_stem_count">Number of Stems</label>
968983
<select id="stem_split_stem_count" name="stem_count">
@@ -1064,15 +1079,16 @@
10641079
</div>
10651080
10661081
<div class="row">
1067-
<label for="midi_gen_output_filename">Output Filename</label>
1082+
<label for="midi_gen_output_filename">Output filename</label>
10681083
<input
10691084
id="midi_gen_output_filename"
10701085
name="output_filename"
10711086
type="text"
10721087
placeholder="output_midi"
1073-
value="">
1088+
value=""
1089+
required>
10741090
<div class="small">
1075-
Output filename (without extension). Will be saved as .mid in the output directory. If left empty, uses input filename + "_midi".
1091+
Required. Output filename (without extension). Saved as .mid in the output directory. If the file already exists, a number is appended (-1, -2, …).
10761092
</div>
10771093
</div>
10781094

cdmf_voice_cloning_bp.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from werkzeug.utils import secure_filename
1414

1515
import cdmf_tracks
16-
from cdmf_paths import DEFAULT_OUT_DIR, APP_VERSION
16+
from cdmf_paths import DEFAULT_OUT_DIR, APP_VERSION, get_next_available_output_path
1717
from cdmf_voice_cloning import get_voice_cloner
1818

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

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

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

90+
# Resolve stem and extension for next-available path
91+
stem = Path(output_filename).stem
92+
ext = Path(output_filename).suffix or ".mp3"
93+
output_path = get_next_available_output_path(out_dir_path, stem, ext)
94+
output_filename = output_path.name
95+
8796
# Save uploaded reference audio temporarily
8897
temp_ref_path = out_dir_path / f"_temp_ref_{filename}"
8998
speaker_file.save(str(temp_ref_path))
@@ -100,8 +109,7 @@ def voice_clone():
100109
speed = float(request.form.get("speed", DEFAULT_SPEED))
101110
enable_text_splitting = request.form.get("enable_text_splitting", "true").lower() == "true"
102111

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

106114
# Perform voice cloning
107115
logger.info(f"[Voice Cloning] Starting: text='{text[:50]}...', language={language}, output={output_path}")
@@ -143,8 +151,9 @@ def voice_clone():
143151
entry["favorite"] = False
144152
entry["seconds"] = dur
145153
entry["created"] = time.time()
146-
entry["generator"] = "voice_clone"
154+
entry["generator"] = "tts"
147155
entry["basename"] = Path(final_name).stem
156+
entry["input_file"] = str(temp_ref_path) # Full path to reference audio
148157
entry["text"] = text
149158
entry["language"] = language
150159
entry["temperature"] = temperature

0 commit comments

Comments
 (0)