@@ -128,6 +128,33 @@ def _get_default_output_dir() -> Path:
128128
129129DEFAULT_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:
189216def 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
207264APP_VERSION = get_app_version ()
0 commit comments