@@ -169,6 +169,53 @@ export async function checkSpaceHealth(): Promise<boolean> {
169169 }
170170}
171171
172+ // ---------------------------------------------------------------------------
173+ // Model name → full path resolution
174+ // ---------------------------------------------------------------------------
175+
176+ /**
177+ * Resolves a DiT model name sent by the frontend into an absolute file path.
178+ *
179+ * The UI only knows model names (e.g. "acestep-v15-turbo-Q8_0" or
180+ * "acestep-v15-turbo-shift3"). The backend owns the model directory and is
181+ * solely responsible for turning that name into a real path:
182+ *
183+ * 1. No name supplied → use the default from config (auto-detected or env).
184+ * 2. Already an absolute path → pass through unchanged.
185+ * 3. Exact filename match: look for "<name>.gguf" in the models dir.
186+ * 4. Prefix match: find any gguf whose name starts with "<name>-", preferring
187+ * Q8_0 → Q6_K → Q5_K_M → Q4_K_M → BF16.
188+ * 5. Nothing found → fall back to the configured default.
189+ */
190+ function resolveParamDitModel ( name : string | undefined ) : string {
191+ if ( ! name ) return config . acestep . ditModel ;
192+ if ( path . isAbsolute ( name ) ) return name ;
193+
194+ const modelsDir = config . models . dir ;
195+ if ( existsSync ( modelsDir ) ) {
196+ // Exact filename match (e.g. "acestep-v15-turbo-Q8_0" → "…Q8_0.gguf")
197+ const exact = path . join ( modelsDir , `${ name } .gguf` ) ;
198+ if ( existsSync ( exact ) ) return exact ;
199+
200+ // Prefix match for variant names without quantization suffix
201+ try {
202+ const files = readdirSync ( modelsDir ) . filter (
203+ f => f . endsWith ( '.gguf' ) && ! f . endsWith ( '.part' ) && f . startsWith ( `${ name } -` ) ,
204+ ) ;
205+ if ( files . length > 0 ) {
206+ const quants = [ 'Q8_0' , 'Q6_K' , 'Q5_K_M' , 'Q4_K_M' , 'BF16' ] ;
207+ for ( const q of quants ) {
208+ const match = files . find ( f => f === `${ name } -${ q } .gguf` ) ;
209+ if ( match ) return path . join ( modelsDir , match ) ;
210+ }
211+ return path . join ( modelsDir , files [ 0 ] ) ;
212+ }
213+ } catch { /* ignore read errors */ }
214+ }
215+
216+ return config . acestep . ditModel ;
217+ }
218+
172219// ---------------------------------------------------------------------------
173220// Audio path resolution (for reference/source audio inputs)
174221// ---------------------------------------------------------------------------
@@ -482,7 +529,7 @@ async function runViaSpawn(
482529
483530 const ditVaeBin = config . acestep . ditVaeBin ! ;
484531 const textEncoderModel = config . acestep . textEncoderModel ;
485- const ditModel = params . ditModel ? params . ditModel : config . acestep . ditModel ;
532+ const ditModel = resolveParamDitModel ( params . ditModel ) ;
486533 const vaeModel = config . acestep . vaeModel ;
487534
488535 if ( ! textEncoderModel ) throw new Error ( 'Text-encoder model not found — run models.sh first' ) ;
@@ -622,7 +669,8 @@ function buildHttpRequest(params: GenerationParams): Record<string, unknown> {
622669
623670 if ( params . referenceAudioUrl ) body . reference_audio = resolveAudioPath ( params . referenceAudioUrl ) ;
624671 if ( params . sourceAudioUrl ) body . src_audio = resolveAudioPath ( params . sourceAudioUrl ) ;
625- if ( params . ditModel ) body . dit_model = params . ditModel ;
672+ const resolvedDitModel = resolveParamDitModel ( params . ditModel ) ;
673+ if ( resolvedDitModel ) body . dit_model = resolvedDitModel ;
626674
627675 // Pass LoRA state as request fields
628676 if ( loraState . loaded && loraState . active && loraState . path ) {
0 commit comments