Skip to content

Commit b162f29

Browse files
Copilotlmangani
andcommitted
Scan models dir for GGUF files; ACESTEP_MODELS as filter/gate
Co-authored-by: lmangani <1423657+lmangani@users.noreply.github.com> Agent-Logs-Url: https://github.com/audiohacking/acestep-cpp-api/sessions/8c812542-7f5b-4523-9ab9-8b884ad75b21
1 parent a84fd05 commit b162f29

6 files changed

Lines changed: 239 additions & 39 deletions

File tree

README.md

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,38 @@ Per-request `lm_model_path` and **`ACESTEP_MODEL_MAP`** values use the same reso
5252

5353
## Multi-model support (GET /v1/models + per-request `model`)
5454

55-
Register multiple DiT models via `ACESTEP_MODEL_MAP` (JSON). The available model list is **derived automatically** from the map keys — no separate list variable is needed. Select a model per-request using the `model` field in `/release_task`.
55+
`GET /v1/models` **automatically scans `ACESTEP_MODELS_DIR`** for `.gguf` files and returns them as the available model list. No extra configuration is required.
5656

5757
```bash
5858
export ACESTEP_MODELS_DIR="$HOME/models/acestep"
59-
export ACESTEP_MODEL_MAP='{"acestep-v15-turbo":"acestep-v15-turbo-Q8_0.gguf","acestep-v15-turbo-shift3":"acestep-v15-turbo-shift3-Q8_0.gguf"}'
60-
# Optional: set which model name is default (first map key is used if omitted)
61-
# export ACESTEP_DEFAULT_MODEL=acestep-v15-turbo
59+
# /v1/models will list every .gguf file found there, e.g.:
60+
# ["acestep-v15-turbo-Q8_0.gguf", "acestep-v15-turbo-shift3-Q8_0.gguf"]
6261
```
6362

63+
Use the discovered filename as the `model` value per-request:
64+
6465
```bash
65-
# List models
66-
curl http://localhost:8001/v1/models
66+
curl http://localhost:8001/v1/models # discover available names
6767

68-
# Generate with a specific model
6968
curl -X POST http://localhost:8001/release_task \
7069
-H 'Content-Type: application/json' \
71-
-d '{"prompt": "jazz piano trio", "model": "acestep-v15-turbo-shift3"}'
70+
-d '{"prompt": "jazz piano trio", "model": "acestep-v15-turbo-shift3-Q8_0.gguf"}'
71+
```
72+
73+
**Optional: logical names via `ACESTEP_MODEL_MAP`** — map friendly names to GGUF filenames:
74+
75+
```bash
76+
export ACESTEP_MODELS_DIR="$HOME/models/acestep"
77+
export ACESTEP_MODEL_MAP='{"acestep-v15-turbo":"acestep-v15-turbo-Q8_0.gguf","acestep-v15-turbo-shift3":"acestep-v15-turbo-shift3-Q8_0.gguf"}'
78+
# Now use the short names: "model": "acestep-v15-turbo"
79+
```
80+
81+
**Optional: `ACESTEP_MODELS` as a filter/gate** — restrict the list to a subset:
82+
83+
```bash
84+
export ACESTEP_MODELS_DIR="$HOME/models/acestep"
85+
export ACESTEP_MODELS="acestep-v15-turbo-Q8_0.gguf,acestep-v15-turbo-shift3-Q8_0.gguf"
86+
# Only those two filenames appear in /v1/models even if more .gguf files exist
7287
```
7388

7489
Generation parameters (`inference_steps`, `guidance_scale`, `bpm`, etc.) are **always per-request** and are never fixed by environment variables.

docs/API.md

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -415,18 +415,18 @@ curl -X POST http://localhost:8001/create_random_sample \
415415

416416
- **URL**: `GET /v1/models`
417417

418-
Returns the DiT models registered on this server. The list is derived automatically from `ACESTEP_MODEL_MAP` (no separate list variable required).
418+
Returns the DiT models available on this server. The list is discovered automatically by scanning `ACESTEP_MODELS_DIR` for `.gguf` files. `ACESTEP_MODEL_MAP` (if set) overrides discovery with explicit logical names. `ACESTEP_MODELS` acts as a filter/gate on the discovered list.
419419

420420
### 8.2 Response
421421

422422
```json
423423
{
424424
"data": {
425425
"models": [
426-
{ "name": "acestep-v15-turbo", "is_default": true },
427-
{ "name": "acestep-v15-turbo-shift3", "is_default": false }
426+
{ "name": "acestep-v15-turbo-Q8_0.gguf", "is_default": true },
427+
{ "name": "acestep-v15-turbo-shift3-Q8_0.gguf", "is_default": false }
428428
],
429-
"default_model": "acestep-v15-turbo"
429+
"default_model": "acestep-v15-turbo-Q8_0.gguf"
430430
},
431431
"code": 200,
432432
"error": null,
@@ -441,16 +441,49 @@ Returns the DiT models registered on this server. The list is derived automatica
441441
curl http://localhost:8001/v1/models
442442
```
443443

444-
### 8.4 Registering Multiple Models
444+
### 8.4 Model discovery order
445445

446-
Set `ACESTEP_MODEL_MAP` to a JSON object mapping logical names to GGUF file paths (bare filenames resolve under `ACESTEP_MODELS_DIR`):
446+
1. **`ACESTEP_MODEL_MAP`** (explicit) — JSON map of `{"logical-name": "file.gguf", …}`. The logical names are exposed as the model names. Use this when you want human-friendly names instead of raw filenames.
447+
2. **`ACESTEP_MODELS_DIR` scan** (automatic) — `.gguf` files found in the models directory are listed by their filename (e.g. `acestep-v15-turbo-Q8_0.gguf`). Sorted alphabetically.
448+
3. **Fallback**`[defaultModel]` when no directory is set and no map is configured.
449+
450+
`ACESTEP_MODELS` (comma-separated names) acts as a **filter/gate** on whichever source is discovered (map keys or scanned filenames). Only names present in the filter are returned.
451+
452+
### 8.5 Selecting a model per-request
453+
454+
Use the `model` field in `/release_task` with a name from the list:
455+
456+
```bash
457+
# Auto-discover — just set the models dir
458+
export ACESTEP_MODELS_DIR="$HOME/models/acestep"
459+
460+
# List what was found
461+
curl http://localhost:8001/v1/models
462+
# → ["acestep-v15-turbo-Q8_0.gguf", "acestep-v15-turbo-shift3-Q8_0.gguf", ...]
463+
464+
# Select one per-request
465+
curl -X POST http://localhost:8001/release_task \
466+
-H 'Content-Type: application/json' \
467+
-d '{"prompt": "jazz piano trio", "model": "acestep-v15-turbo-shift3-Q8_0.gguf"}'
468+
```
469+
470+
Or use `ACESTEP_MODEL_MAP` for logical names:
447471

448472
```bash
449473
export ACESTEP_MODELS_DIR="$HOME/models/acestep"
450474
export ACESTEP_MODEL_MAP='{"acestep-v15-turbo":"acestep-v15-turbo-Q8_0.gguf","acestep-v15-turbo-shift3":"acestep-v15-turbo-shift3-Q8_0.gguf"}'
475+
476+
curl -X POST http://localhost:8001/release_task \
477+
-H 'Content-Type: application/json' \
478+
-d '{"prompt": "jazz piano trio", "model": "acestep-v15-turbo-shift3"}'
451479
```
452480

453-
The `/v1/models` response will list exactly those names. Select one per-request via the `model` field in `/release_task`.
481+
Or gate the list to a subset:
482+
483+
```bash
484+
export ACESTEP_MODELS_DIR="$HOME/models/acestep"
485+
export ACESTEP_MODELS="acestep-v15-turbo-Q8_0.gguf,acestep-v15-turbo-shift3-Q8_0.gguf"
486+
```
454487

455488
---
456489

@@ -561,11 +594,15 @@ Only **paths** and server-level settings are configured via environment variable
561594

562595
| Variable | Default | Description |
563596
|----------|---------|-------------|
564-
| `ACESTEP_MODEL_MAP` | `{}` | JSON map of `{"name": "path.gguf", …}` — drives both the `/v1/models` list **and** per-request `model` parameter validation |
565-
| `ACESTEP_DEFAULT_MODEL` | first key of map, or `"acestep-v15-turbo"` | Name used when no `model` is specified |
566-
| `ACESTEP_MODELS` | *(derived from map)* | Override the `/v1/models` list explicitly (comma-separated names) |
567-
568-
> **Note**: `ACESTEP_MODEL_MAP` is the canonical source for multi-model setup. `ACESTEP_MODELS` and `ACESTEP_DEFAULT_MODEL` are optional overrides.
597+
| `ACESTEP_MODEL_MAP` | `{}` | JSON map of `{"name": "file.gguf", …}` — explicit name→path mapping. Drives both `/v1/models` and per-request `model` validation. Takes precedence over directory scan. |
598+
| `ACESTEP_DEFAULT_MODEL` | first map key / first scanned file / `"acestep-v15-turbo"` | Name used when no `model` is specified per-request |
599+
| `ACESTEP_MODELS` | *(all discovered)* | Comma-separated **filter/gate** applied to the discovered list (map keys or scanned filenames). Only names in this list are returned by `/v1/models`. |
600+
601+
> **Recommended minimal setup** (no `ACESTEP_MODEL_MAP` needed):
602+
> ```bash
603+
> export ACESTEP_MODELS_DIR="$HOME/models/acestep"
604+
> # /v1/models will automatically list every .gguf file in that directory
605+
> ```
569606
570607
### Queue / Storage
571608

src/config.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** Env-based config. Binaries: https://github.com/audiohacking/acestep.cpp/releases/tag/v0.0.3 */
2-
import { resolveModelFile, resolveModelMapPaths, resolveAcestepBinDir } from "./paths";
2+
import { resolveModelFile, resolveModelMapPaths, resolveAcestepBinDir, listGgufFiles } from "./paths";
33

44
function parseModelMap(raw: string): Record<string, string> {
55
if (!raw.trim()) return {};
@@ -85,32 +85,74 @@ export const config = {
8585
* List of available model names shown by GET /v1/models.
8686
*
8787
* Resolution order:
88-
* 1. `ACESTEP_MODELS` (comma-separated) — explicit override.
89-
* 2. Keys of `ACESTEP_MODEL_MAP` — derived automatically when a map is configured.
90-
* 3. `[defaultModel]` — single-model fallback so the endpoint always returns something.
88+
* 1. `ACESTEP_MODEL_MAP` keys — when an explicit name→path map is configured.
89+
* 2. `.gguf` files found in `modelsDir` — discovered at runtime.
90+
* 3. Fallback: `ACESTEP_MODELS` list as-is (no dir to scan), or `[defaultModel]`.
91+
*
92+
* `ACESTEP_MODELS` (comma-separated) acts as a **filter/gate** on the discovered list
93+
* (steps 1 & 2). When set, only names present in that list are returned.
9194
*/
9295
get modelsList(): string[] {
93-
const explicit = process.env.ACESTEP_MODELS?.trim();
94-
if (explicit) return explicit.split(",").map((s) => s.trim()).filter(Boolean);
96+
const filterRaw = process.env.ACESTEP_MODELS?.trim();
97+
const allowed = filterRaw ? new Set(filterRaw.split(",").map((s) => s.trim()).filter(Boolean)) : null;
98+
99+
// 1. Explicit MODEL_MAP: use map keys
95100
const mapKeys = Object.keys(getModelMapRaw());
96-
if (mapKeys.length > 0) return mapKeys;
101+
if (mapKeys.length > 0) {
102+
return allowed ? mapKeys.filter((k) => allowed.has(k)) : mapKeys;
103+
}
104+
105+
// 2. Scan models directory for .gguf files
106+
const dir = this.modelsDir;
107+
if (dir) {
108+
const scanned = listGgufFiles(dir);
109+
if (scanned.length > 0) {
110+
return allowed ? scanned.filter((n) => allowed.has(n)) : scanned;
111+
}
112+
}
113+
114+
// 3. Fallback: use ACESTEP_MODELS list directly, or [defaultModel]
115+
if (allowed) return [...allowed];
97116
const def = this.defaultModel;
98117
return def ? [def] : [];
99118
},
100119

120+
/**
121+
* Model name → resolved file path map derived from scanning `modelsDir`.
122+
* Used by `resolveDitPath` so per-request `model` accepts discovered filenames.
123+
* Only populated when `ACESTEP_MODEL_MAP` is not set.
124+
*/
125+
get scannedModelMap(): Record<string, string> {
126+
if (Object.keys(getModelMapRaw()).length > 0) return {};
127+
const dir = this.modelsDir;
128+
if (!dir) return {};
129+
const files = listGgufFiles(dir);
130+
const out: Record<string, string> = {};
131+
for (const f of files) {
132+
out[f] = resolveModelFile(f);
133+
}
134+
return out;
135+
},
136+
101137
/**
102138
* The default model name (used when no `model` is specified per request).
103139
*
104140
* Resolution order:
105141
* 1. `ACESTEP_DEFAULT_MODEL` — explicit override.
106142
* 2. First key of `ACESTEP_MODEL_MAP` — when a map is configured.
107-
* 3. `"acestep-v15-turbo"` — hardcoded fallback label.
143+
* 3. First `.gguf` file in `modelsDir` — when the directory is scanned.
144+
* 4. `"acestep-v15-turbo"` — hardcoded fallback label.
108145
*/
109146
get defaultModel(): string {
110147
const explicit = process.env.ACESTEP_DEFAULT_MODEL?.trim();
111148
if (explicit) return explicit;
112149
const mapKeys = Object.keys(getModelMapRaw());
113-
if (mapKeys.length > 0) return mapKeys[0] as string;
150+
if (mapKeys.length > 0) return mapKeys[0];
151+
const dir = this.modelsDir;
152+
if (dir) {
153+
const scanned = listGgufFiles(dir);
154+
if (scanned.length > 0) return scanned[0];
155+
}
114156
return "acestep-v15-turbo";
115157
},
116158

src/paths.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync } from "fs";
1+
import { existsSync, readdirSync } from "fs";
22
import { dirname, join, resolve, isAbsolute } from "path";
33

44
/**
@@ -74,3 +74,18 @@ export function resolveReferenceAudioPath(pathOrName: string): string {
7474
if (isAbsolute(p)) return p;
7575
return join(resolve(getResourceRoot()), p);
7676
}
77+
78+
/**
79+
* Returns basenames of `.gguf` files found in `dir`, sorted alphabetically.
80+
* Returns `[]` if `dir` is empty, does not exist, or cannot be read.
81+
*/
82+
export function listGgufFiles(dir: string): string[] {
83+
if (!dir) return [];
84+
try {
85+
return readdirSync(dir)
86+
.filter((f) => f.toLowerCase().endsWith(".gguf"))
87+
.sort();
88+
} catch {
89+
return [];
90+
}
91+
}

src/worker.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,11 @@ export function resolveLmPath(body: Record<string, unknown>): string {
106106

107107
export function resolveDitPath(body: Record<string, unknown>): string {
108108
const modelName = typeof body.model === "string" ? body.model.trim() : "";
109-
if (modelName && config.modelMap[modelName]) return config.modelMap[modelName];
110-
if (modelName && !config.modelMap[modelName]) {
111-
throw new Error(`Unknown model "${modelName}". Set ACESTEP_MODEL_MAP JSON or use a configured name.`);
109+
if (modelName) {
110+
if (config.modelMap[modelName]) return config.modelMap[modelName];
111+
const scanned = config.scannedModelMap;
112+
if (scanned[modelName]) return scanned[modelName];
113+
throw new Error(`Unknown model "${modelName}". Use GET /v1/models to list available models.`);
112114
}
113115
if (!config.ditModelPath) throw new Error("ACESTEP_DIT_MODEL or ACESTEP_CONFIG_PATH not set");
114116
return config.ditModelPath;

0 commit comments

Comments
 (0)