Initial release
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// ace-lm.cpp: ACE-Step LLM CLI
|
||||
// Thin wrapper: parses args, scans the model registry, calls pipeline-lm,
|
||||
// writes output files. The model to use comes from request.lm_model, the
|
||||
// registry resolves it to a GGUF path under --models <dir>.
|
||||
|
||||
#include "model-registry.h"
|
||||
#include "model-store.h"
|
||||
#include "pipeline-lm.h"
|
||||
#include "request.h"
|
||||
#include "task-types.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static void usage(const char * prog) {
|
||||
AceLmParams d;
|
||||
ace_lm_default_params(&d);
|
||||
|
||||
fprintf(stderr, "acestep.cpp %s\n\n", ACE_VERSION);
|
||||
fprintf(stderr,
|
||||
"Usage: %s --models <dir> --request <json> [options]\n"
|
||||
"\n"
|
||||
"Required:\n"
|
||||
" --models <dir> Directory of GGUF model files\n"
|
||||
" --request <json> Input request JSON (carries lm_model)\n"
|
||||
"\n"
|
||||
"Debug:\n"
|
||||
" --max-seq <N> KV cache size (default: %d)\n"
|
||||
" --no-fsm Disable FSM constrained decoding\n"
|
||||
" --no-fa Disable flash attention\n"
|
||||
" --no-batch-cfg Split CFG into two separate forwards\n"
|
||||
" --clamp-fp16 Clamp hidden states to FP16 range\n"
|
||||
" --dump-logits <path> Dump prefill logits (binary f32)\n"
|
||||
" --dump-tokens <path> Dump prompt token IDs (CSV)\n",
|
||||
prog, d.max_seq);
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
AceLmParams params;
|
||||
ace_lm_default_params(¶ms);
|
||||
|
||||
const char * models_dir = NULL;
|
||||
const char * request_path = NULL;
|
||||
const char * dump_logits = NULL;
|
||||
const char * dump_tokens = NULL;
|
||||
|
||||
if (argc < 2) {
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--models") && i + 1 < argc) {
|
||||
models_dir = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--request") && i + 1 < argc) {
|
||||
request_path = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--max-seq") && i + 1 < argc) {
|
||||
params.max_seq = atoi(argv[++i]);
|
||||
} else if (!strcmp(argv[i], "--no-fsm")) {
|
||||
params.use_fsm = false;
|
||||
} else if (!strcmp(argv[i], "--no-fa")) {
|
||||
params.use_fa = false;
|
||||
} else if (!strcmp(argv[i], "--no-batch-cfg")) {
|
||||
params.use_batch_cfg = false;
|
||||
} else if (!strcmp(argv[i], "--clamp-fp16")) {
|
||||
params.clamp_fp16 = true;
|
||||
} else if (!strcmp(argv[i], "--dump-logits") && i + 1 < argc) {
|
||||
dump_logits = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--dump-tokens") && i + 1 < argc) {
|
||||
dump_tokens = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) {
|
||||
usage(argv[0]);
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!models_dir) {
|
||||
fprintf(stderr, "[CLI] ERROR: --models required\n");
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (!request_path) {
|
||||
fprintf(stderr, "[CLI] ERROR: --request required\n");
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parse input request
|
||||
AceRequest req;
|
||||
if (!request_parse(&req, request_path)) {
|
||||
return 1;
|
||||
}
|
||||
request_dump(&req, stderr);
|
||||
|
||||
// Scan the registry and resolve lm_model. Missing or empty lm_model falls
|
||||
// to the first LM in the registry, matching server default behavior.
|
||||
ModelRegistry registry;
|
||||
if (!registry_scan(®istry, models_dir)) {
|
||||
fprintf(stderr, "[Ace-LM] FATAL: cannot scan --models %s\n", models_dir);
|
||||
return 1;
|
||||
}
|
||||
if (registry.lm.empty()) {
|
||||
fprintf(stderr, "[Ace-LM] FATAL: no LM models found under %s\n", models_dir);
|
||||
return 1;
|
||||
}
|
||||
const ModelEntry * lm_entry =
|
||||
req.lm_model.empty() ? ®istry.lm[0] : registry_find(registry.lm, req.lm_model.c_str());
|
||||
if (!lm_entry) {
|
||||
fprintf(stderr, "[Ace-LM] FATAL: lm_model '%s' not found in registry\n", req.lm_model.c_str());
|
||||
return 1;
|
||||
}
|
||||
params.model_path = lm_entry->path.c_str();
|
||||
|
||||
// lm_batch_size from JSON (clamped to 1..9)
|
||||
int lm_batch_size = req.lm_batch_size;
|
||||
if (lm_batch_size < 1) {
|
||||
lm_batch_size = 1;
|
||||
} else if (lm_batch_size > 9) {
|
||||
fprintf(stderr, "[Ace-LM] WARNING: lm_batch_size %d clamped to 9\n", lm_batch_size);
|
||||
lm_batch_size = 9;
|
||||
}
|
||||
|
||||
// Resolve lm_mode string to integer mode used by ace_lm_generate.
|
||||
int mode;
|
||||
if (req.lm_mode == LM_MODE_NAME_GENERATE) {
|
||||
mode = LM_MODE_GENERATE;
|
||||
} else if (req.lm_mode == LM_MODE_NAME_INSPIRE) {
|
||||
mode = LM_MODE_INSPIRE;
|
||||
} else if (req.lm_mode == LM_MODE_NAME_FORMAT) {
|
||||
mode = LM_MODE_FORMAT;
|
||||
} else {
|
||||
fprintf(stderr, "[Ace-LM] FATAL: invalid lm_mode '%s' (use: generate, inspire, format)\n", req.lm_mode.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Load model (KV cache sized for request batch)
|
||||
params.max_batch = lm_batch_size;
|
||||
ModelStore * store = store_create(EVICT_STRICT);
|
||||
AceLm * ctx = ace_lm_load(store, ¶ms);
|
||||
if (!ctx) {
|
||||
store_free(store);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Generate
|
||||
std::vector<AceRequest> out(lm_batch_size);
|
||||
if (ace_lm_generate(ctx, &req, lm_batch_size, out.data(), dump_logits, dump_tokens, NULL, NULL, mode) != 0) {
|
||||
ace_lm_free(ctx);
|
||||
store_free(store);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Write output files: request.json -> request0.json, request1.json, ...
|
||||
std::string base(request_path);
|
||||
std::string ext = ".json";
|
||||
size_t dot = base.rfind('.');
|
||||
if (dot != std::string::npos) {
|
||||
ext = base.substr(dot);
|
||||
base = base.substr(0, dot);
|
||||
}
|
||||
for (int b = 0; b < lm_batch_size; b++) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s%d%s", base.c_str(), b, ext.c_str());
|
||||
request_write(&out[b], path);
|
||||
}
|
||||
|
||||
ace_lm_free(ctx);
|
||||
store_free(store);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# ace-midi-validate.py — reference dumps for the ace-midi GGML port (Phase 1/2)
|
||||
#
|
||||
# Runs inside the kept MuScriptor oracle venv (CPU torch, fp32 — deterministic):
|
||||
# server\data\muscriptor\venv\Scripts\python.exe engine\tools\ace-midi-validate.py \
|
||||
# --weights server\data\models\muscriptor\small\model.safetensors \
|
||||
# --out server\data\muscriptor\validation
|
||||
#
|
||||
# Dumps (little-endian f32 raw + manifest.json):
|
||||
# wav.bin [80000] deterministic synthetic 5 s chunk (sines + click train)
|
||||
# prefix.bin [T_prefix,dim] conditioning prefix embeddings in final sequence order
|
||||
# (exactly what LMModel.forward prepends before BOS)
|
||||
# logits_bos.bin [card] forward([[BOS]], conditions, first_step=True) last-step logits
|
||||
# tokens_ref.json greedy token stream for the chunk (max 256, EOS-stripped)
|
||||
#
|
||||
# See docs/plans/muscriptor-cpp-port.md §6 (validation plan).
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
from muscriptor.transcription_model import TranscriptionModel
|
||||
from muscriptor.modules.streaming import init_states
|
||||
|
||||
|
||||
def synth_wav(n: int = 80_000, sr: int = 16_000) -> torch.Tensor:
|
||||
"""Deterministic, spectrally busy test signal: three sines + a click train."""
|
||||
t = torch.arange(n, dtype=torch.float32) / sr
|
||||
wav = (
|
||||
0.40 * torch.sin(2 * math.pi * 220.0 * t)
|
||||
+ 0.25 * torch.sin(2 * math.pi * 554.37 * t) # C#5
|
||||
+ 0.15 * torch.sin(2 * math.pi * 1318.5 * t) # E6
|
||||
)
|
||||
# 4 Hz click train for onset structure
|
||||
clicks = ((torch.arange(n) % (sr // 4)) < 32).float() * 0.5
|
||||
return (wav + clicks).clamp(-1.0, 1.0)
|
||||
|
||||
|
||||
def synth_wav_multi(sr: int = 16_000) -> torch.Tensor:
|
||||
"""13 s / 3-chunk signal with content changes across chunk boundaries and
|
||||
sustained tones crossing them — exercises tie prologues + prelude forcing.
|
||||
Last chunk is partial (3 s) to exercise the mel length mask."""
|
||||
n = 13 * sr
|
||||
t = torch.arange(n, dtype=torch.float32) / sr
|
||||
wav = 0.35 * torch.sin(2 * math.pi * 220.0 * t) # sustained A3 throughout
|
||||
seg2 = (t >= 4.0) & (t < 9.5) # crosses the 5 s boundary
|
||||
wav = wav + 0.30 * torch.sin(2 * math.pi * 329.63 * t) * seg2.float()
|
||||
seg3 = t >= 8.0 # crosses the 10 s boundary
|
||||
wav = wav + 0.25 * torch.sin(2 * math.pi * 493.88 * t) * seg3.float()
|
||||
clicks = ((torch.arange(n) % (sr // 2)) < 32).float() * 0.4
|
||||
return (wav + clicks).clamp(-1.0, 1.0)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--weights", required=True)
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--max-tokens", type=int, default=256)
|
||||
args = ap.parse_args()
|
||||
|
||||
out = Path(args.out)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tm = TranscriptionModel.load_model(weights_path=Path(args.weights), device="cpu")
|
||||
model = tm._model
|
||||
model.eval()
|
||||
|
||||
wav = synth_wav()
|
||||
wav.numpy().tofile(out / "wav.bin")
|
||||
|
||||
conds = tm._build_conditions(wav.unsqueeze(0).to(tm._device))
|
||||
prepared = model.condition_provider.tokenize(conds)
|
||||
cond_tensors = model.condition_provider(prepared)
|
||||
|
||||
# Replicate LMModel.forward's prepend loop to get the prefix in final order
|
||||
dim = model.dim
|
||||
prefix = torch.zeros(1, 0, dim)
|
||||
for cond, _mask in cond_tensors.values():
|
||||
prefix = torch.cat([cond, prefix], dim=1)
|
||||
prefix[0].detach().float().numpy().tofile(out / "prefix.bin")
|
||||
|
||||
# Single forward: [BOS] with conditions prepended (prefill parity target)
|
||||
with torch.no_grad():
|
||||
seq = torch.tensor([[model.initial_token_id]], dtype=torch.long)
|
||||
state = init_states(model, batch_size=1, sequence_length=prefix.shape[1] + 16)
|
||||
logits = model(seq, cond_tensors, first_step=True, model_state=state)
|
||||
logits[0, -1].detach().float().numpy().tofile(out / "logits_bos.bin")
|
||||
|
||||
# Greedy token stream for the chunk (Phase 2 target)
|
||||
tokens: list[int] = []
|
||||
eos = tm._tokenizer.eos_id
|
||||
with torch.no_grad():
|
||||
for step in model.generate(
|
||||
conditions=conds,
|
||||
max_gen_len=args.max_tokens,
|
||||
use_sampling=False,
|
||||
early_stop_on_token=eos,
|
||||
):
|
||||
tok = int(step[0].item())
|
||||
if tok == eos:
|
||||
break
|
||||
tokens.append(tok)
|
||||
(out / "tokens_ref.json").write_text(json.dumps(tokens))
|
||||
|
||||
# ── Phase 3 references: multi-chunk events + MIDI (prelude forcing on) ──
|
||||
from muscriptor.events import NoteStartEvent, NoteEndEvent, ProgressEvent
|
||||
|
||||
wav15 = synth_wav_multi()
|
||||
wav15.numpy().tofile(out / "wav15.bin")
|
||||
|
||||
events_json = []
|
||||
collected = []
|
||||
for ev in tm.transcribe((wav15.unsqueeze(0), 16_000)):
|
||||
collected.append(ev)
|
||||
if isinstance(ev, NoteStartEvent):
|
||||
events_json.append({"type": "start", "index": ev.index, "pitch": ev.pitch,
|
||||
"time": round(ev.start_time, 6), "instrument": ev.instrument})
|
||||
elif isinstance(ev, NoteEndEvent):
|
||||
events_json.append({"type": "end", "index": ev.start_event_index,
|
||||
"time": round(ev.end_time, 6)})
|
||||
(out / "events_ref15.json").write_text(json.dumps(events_json))
|
||||
midi_bytes = tm.events_to_midi_bytes(iter(collected))
|
||||
(out / "ref15.mid").write_bytes(midi_bytes)
|
||||
|
||||
top = torch.topk(logits[0, -1], 5)
|
||||
manifest = {
|
||||
"dim": dim,
|
||||
"card": int(model.card),
|
||||
"bos_id": int(model.initial_token_id),
|
||||
"eos_id": int(eos),
|
||||
"prefix_len": int(prefix.shape[1]),
|
||||
"wav_samples": int(wav.shape[0]),
|
||||
"greedy_tokens": len(tokens),
|
||||
"wav15_samples": int(wav15.shape[0]),
|
||||
"events15": len(events_json),
|
||||
"ref15_mid_bytes": len(midi_bytes),
|
||||
"logits_top5_ids": top.indices.tolist(),
|
||||
"logits_top5_vals": [round(v, 6) for v in top.values.tolist()],
|
||||
}
|
||||
(out / "manifest.json").write_text(json.dumps(manifest, indent=2))
|
||||
print(json.dumps(manifest, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,627 @@
|
||||
// ace-synth.cpp: ACE-Step synthesis CLI
|
||||
// Thin wrapper: parses args, scans the model registry, calls pipeline-synth,
|
||||
// writes output files. Model selection (synth_model, adapter, output_format)
|
||||
// comes from the request JSON. The registry resolves names to GGUF paths
|
||||
// under --models <dir> and --adapters <dir>.
|
||||
|
||||
#include "audio-io.h"
|
||||
#include "backend.h"
|
||||
#include "ggml.h"
|
||||
#include "lua-plugin-registry.h"
|
||||
#include "model-registry.h"
|
||||
#include "model-store.h"
|
||||
#include "pipeline-synth.h"
|
||||
#include "request.h"
|
||||
#include "synth-batch-runner.h"
|
||||
#include "task-types.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// ─── Per-section mask broadcast self-test (HOTSTEP_BCAST_TEST) ───────────────
|
||||
// Verifies the exact tensor ops the per-section masking relies on, on the real
|
||||
// backend, with no models. Test 1: ggml_mul([out,S,N] f32, [1,S,1] f32) — the
|
||||
// per-frame mask broadcast over the feature (ne0) and batch (ne2) dims. Test 2:
|
||||
// the real chain mul_mat(BF16 delta, f32 x) -> mul(mask). Run: set the env var
|
||||
// and invoke ace-synth (it runs the test and exits).
|
||||
static int run_bcast_selftest() {
|
||||
BackendPair bp = backend_init("BcastTest");
|
||||
ggml_backend_t backend = bp.backend;
|
||||
fprintf(stderr, "[BcastTest] backend=%s\n", ggml_backend_name(backend));
|
||||
|
||||
const int64_t out = 8, S = 6, N = 2, in = 4;
|
||||
int fails = 0;
|
||||
|
||||
// Test 1: pure elementwise broadcast mul.
|
||||
{
|
||||
struct ggml_init_params p = { ggml_tensor_overhead() * 16 + ggml_graph_overhead() + 4096, NULL, true };
|
||||
struct ggml_context * ctx = ggml_init(p);
|
||||
struct ggml_tensor * a = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, out, S, N);
|
||||
struct ggml_tensor * mask = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 1, S, 1);
|
||||
ggml_set_input(a);
|
||||
ggml_set_input(mask);
|
||||
struct ggml_tensor * y = ggml_mul(ctx, a, mask);
|
||||
struct ggml_cgraph * gf = ggml_new_graph(ctx);
|
||||
ggml_build_forward_expand(gf, y);
|
||||
ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors(ctx, backend);
|
||||
std::vector<float> ad((size_t) (out * S * N), 1.0f), md((size_t) S);
|
||||
for (int s = 0; s < S; s++) md[s] = (float) (s + 1) * 0.25f; // distinct per frame
|
||||
ggml_backend_tensor_set(a, ad.data(), 0, ad.size() * sizeof(float));
|
||||
ggml_backend_tensor_set(mask, md.data(), 0, md.size() * sizeof(float));
|
||||
ggml_backend_graph_compute(backend, gf);
|
||||
std::vector<float> yd((size_t) (out * S * N));
|
||||
ggml_backend_tensor_get(y, yd.data(), 0, yd.size() * sizeof(float));
|
||||
int bad = 0;
|
||||
for (int n = 0; n < N; n++)
|
||||
for (int s = 0; s < S; s++)
|
||||
for (int f = 0; f < out; f++) {
|
||||
float got = yd[(size_t) n * S * out + (size_t) s * out + f];
|
||||
if (fabsf(got - md[s]) > 1e-4f) {
|
||||
if (bad < 6) fprintf(stderr, "[BcastTest] T1 MISMATCH f=%d s=%d n=%d exp=%.3f got=%.3f\n", f, s, n, md[s], got);
|
||||
bad++;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "[BcastTest] T1 ggml_mul[out,S,N]x[1,S,1]: %s (%d/%lld bad)\n",
|
||||
bad ? "FAIL" : "PASS", bad, (long long) (out * S * N));
|
||||
fails += bad ? 1 : 0;
|
||||
ggml_backend_buffer_free(buf);
|
||||
ggml_free(ctx);
|
||||
}
|
||||
|
||||
// Test 2: real chain — mul_mat(BF16 delta[in,out], f32 x[in,S,N]) then mask.
|
||||
{
|
||||
struct ggml_init_params p = { ggml_tensor_overhead() * 32 + ggml_graph_overhead() + 4096, NULL, true };
|
||||
struct ggml_context * ctx = ggml_init(p);
|
||||
struct ggml_tensor * d = ggml_new_tensor_2d(ctx, GGML_TYPE_BF16, in, out);
|
||||
struct ggml_tensor * x = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, in, S, N);
|
||||
struct ggml_tensor * mask = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 1, S, 1);
|
||||
ggml_set_input(d);
|
||||
ggml_set_input(x);
|
||||
ggml_set_input(mask);
|
||||
struct ggml_tensor * dy = ggml_mul_mat(ctx, d, x); // [out,S,N]
|
||||
struct ggml_tensor * dym = ggml_mul(ctx, dy, mask);
|
||||
ggml_set_output(dy);
|
||||
ggml_set_output(dym);
|
||||
struct ggml_cgraph * gf = ggml_new_graph(ctx);
|
||||
ggml_build_forward_expand(gf, dym);
|
||||
ggml_build_forward_expand(gf, dy);
|
||||
ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors(ctx, backend);
|
||||
std::vector<ggml_bf16_t> dd((size_t) (in * out));
|
||||
for (int i = 0; i < in * out; i++) dd[i] = ggml_fp32_to_bf16(0.5f); // all 0.5
|
||||
std::vector<float> xd((size_t) (in * S * N), 1.0f), md((size_t) S);
|
||||
for (int s = 0; s < S; s++) md[s] = (float) (s + 1) * 0.25f;
|
||||
ggml_backend_tensor_set(d, dd.data(), 0, dd.size() * sizeof(ggml_bf16_t));
|
||||
ggml_backend_tensor_set(x, xd.data(), 0, xd.size() * sizeof(float));
|
||||
ggml_backend_tensor_set(mask, md.data(), 0, md.size() * sizeof(float));
|
||||
ggml_backend_graph_compute(backend, gf);
|
||||
std::vector<float> dyd((size_t) (out * S * N)), dymd((size_t) (out * S * N));
|
||||
ggml_backend_tensor_get(dy, dyd.data(), 0, dyd.size() * sizeof(float));
|
||||
ggml_backend_tensor_get(dym, dymd.data(), 0, dymd.size() * sizeof(float));
|
||||
// Each dy element = sum_in(0.5 * 1.0) = in*0.5 = 2.0; masked = 2.0*md[s].
|
||||
int bad = 0;
|
||||
for (int n = 0; n < N; n++)
|
||||
for (int s = 0; s < S; s++)
|
||||
for (int f = 0; f < out; f++) {
|
||||
size_t idx = (size_t) n * S * out + (size_t) s * out + f;
|
||||
float exp = dyd[idx] * md[s];
|
||||
if (fabsf(dymd[idx] - exp) > 1e-3f) {
|
||||
if (bad < 6) fprintf(stderr, "[BcastTest] T2 MISMATCH f=%d s=%d n=%d dy=%.3f exp=%.3f got=%.3f\n", f, s, n, dyd[idx], exp, dymd[idx]);
|
||||
bad++;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "[BcastTest] T2 dy=%.3f (expect 2.0); mask chain: %s (%d/%lld bad)\n",
|
||||
dyd[0], bad ? "FAIL" : "PASS", bad, (long long) (out * S * N));
|
||||
fails += bad ? 1 : 0;
|
||||
ggml_backend_buffer_free(buf);
|
||||
ggml_free(ctx);
|
||||
}
|
||||
|
||||
fprintf(stderr, "[BcastTest] RESULT: %s\n", fails ? "FAIL — broadcast is the bug" : "PASS — broadcast is fine, look elsewhere");
|
||||
backend_release(bp.backend, bp.cpu_backend);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
|
||||
// ─── LoKr Kronecker-apply self-test (HOTSTEP_KRON_TEST) ──────────────────────
|
||||
// Phase-2 prototype for low-rank runtime adapters (docs/plans/lowrank-runtime-
|
||||
// adapters.md): verifies that (w1 ⊗ w2)@x can be computed on the real backend
|
||||
// from the factors alone — no materialized Kronecker delta — via
|
||||
// y[oa·c+oc, s] = Σ_ib w1[oa,ib] · ( Σ_id w2[oc,id] · x[ib·d+id, s] )
|
||||
// i.e. mul_mat(w2) → permute/cont → mul_mat(w1) → permute/cont. Pass 0 runs
|
||||
// F32 factors (validates the choreography exactly), pass 1 runs BF16 factors
|
||||
// (the production storage type). Compared against a host-side dense-kron
|
||||
// reference. Batched inputs [in,S,N] flatten to [in,S·N] first, so 2D covers
|
||||
// them. Run: set the env var and invoke ace-synth (runs the test and exits).
|
||||
static int run_kron_selftest() {
|
||||
BackendPair bp = backend_init("KronTest");
|
||||
ggml_backend_t backend = bp.backend;
|
||||
fprintf(stderr, "[KronTest] backend=%s\n", ggml_backend_name(backend));
|
||||
|
||||
// PyTorch shapes: w1 [a,b], w2 [c,d]; delta = kron(w1,w2) [out=a·c, in=b·d]
|
||||
const int64_t a = 3, b = 4, c = 5, d = 6, S = 7;
|
||||
const int64_t out = a * c, in = b * d;
|
||||
int fails = 0;
|
||||
|
||||
// deterministic fill (LCG) — same values every run and backend
|
||||
auto fill = [](std::vector<float> & v, uint32_t seed) {
|
||||
uint32_t s = seed;
|
||||
for (auto & f : v) {
|
||||
s = s * 1664525u + 1013904223u;
|
||||
f = ((float) (s >> 8) / (float) (1u << 24)) - 0.5f; // [-0.5, 0.5)
|
||||
}
|
||||
};
|
||||
std::vector<float> w1d((size_t) (a * b)), w2d((size_t) (c * d)), xd((size_t) (in * S));
|
||||
fill(w1d, 1);
|
||||
fill(w2d, 2);
|
||||
fill(xd, 3);
|
||||
|
||||
// Host reference: dense kron, then y = kron(w1,w2) @ x.
|
||||
// w1d/w2d are row-major PyTorch [out, in]: w1[oa,ib] = w1d[oa·b+ib].
|
||||
std::vector<float> yref((size_t) (out * S), 0.0f);
|
||||
for (int64_t oa = 0; oa < a; oa++)
|
||||
for (int64_t oc = 0; oc < c; oc++)
|
||||
for (int64_t s = 0; s < S; s++) {
|
||||
float acc = 0.0f;
|
||||
for (int64_t ib = 0; ib < b; ib++)
|
||||
for (int64_t id = 0; id < d; id++)
|
||||
acc += w1d[(size_t) (oa * b + ib)] * w2d[(size_t) (oc * d + id)]
|
||||
* xd[(size_t) ((ib * d + id) + in * s)];
|
||||
yref[(size_t) ((oa * c + oc) + out * s)] = acc;
|
||||
}
|
||||
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
const bool bf16 = (pass == 1);
|
||||
const ggml_type ftype = bf16 ? GGML_TYPE_BF16 : GGML_TYPE_F32;
|
||||
// F32 tol allows CUDA's TF32-accumulated cuBLAS matmul (~1e-3 rel), which
|
||||
// reorders/rounds vs the naive host reference; a wrong permute would be
|
||||
// off by whole values on most elements, not 1e-4 on a few.
|
||||
const float tol = bf16 ? 2e-2f : 1e-3f;
|
||||
|
||||
struct ggml_init_params p = { ggml_tensor_overhead() * 32 + ggml_graph_overhead() + 4096, NULL, true };
|
||||
struct ggml_context * ctx = ggml_init(p);
|
||||
// Row-major PyTorch [rows, cols] uploads directly as ggml [cols, rows]:
|
||||
// w1g [b, a] element (ib, oa) == w1[oa, ib]; same for w2g [d, c].
|
||||
struct ggml_tensor * w1g = ggml_new_tensor_2d(ctx, ftype, b, a);
|
||||
struct ggml_tensor * w2g = ggml_new_tensor_2d(ctx, ftype, d, c);
|
||||
struct ggml_tensor * x = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, in, S);
|
||||
ggml_set_input(w1g);
|
||||
ggml_set_input(w2g);
|
||||
ggml_set_input(x);
|
||||
|
||||
// x [in=b·d, S] viewed as [d, b·S]: column ib·d+id has id fastest — matches
|
||||
// the kron column convention, so the reshape is a free view.
|
||||
struct ggml_tensor * X2 = ggml_reshape_2d(ctx, x, d, b * S);
|
||||
struct ggml_tensor * T = ggml_mul_mat(ctx, w2g, X2); // [c, b·S] = T(oc; ib,s)
|
||||
struct ggml_tensor * T3 = ggml_reshape_3d(ctx, T, c, b, S);
|
||||
struct ggml_tensor * P = ggml_cont(ctx, ggml_permute(ctx, T3, 1, 0, 2, 3)); // [b, c, S]
|
||||
struct ggml_tensor * P2 = ggml_reshape_2d(ctx, P, b, c * S);
|
||||
struct ggml_tensor * Y = ggml_mul_mat(ctx, w1g, P2); // [a, c·S] = y(oa; oc,s)
|
||||
struct ggml_tensor * Y3 = ggml_reshape_3d(ctx, Y, a, c, S);
|
||||
struct ggml_tensor * YP = ggml_cont(ctx, ggml_permute(ctx, Y3, 1, 0, 2, 3)); // [c, a, S] → flat out=oa·c+oc
|
||||
ggml_set_output(YP);
|
||||
|
||||
struct ggml_cgraph * gf = ggml_new_graph(ctx);
|
||||
ggml_build_forward_expand(gf, YP);
|
||||
ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors(ctx, backend);
|
||||
|
||||
if (bf16) {
|
||||
std::vector<ggml_bf16_t> w1b((size_t) (a * b)), w2b((size_t) (c * d));
|
||||
ggml_fp32_to_bf16_row(w1d.data(), w1b.data(), a * b);
|
||||
ggml_fp32_to_bf16_row(w2d.data(), w2b.data(), c * d);
|
||||
ggml_backend_tensor_set(w1g, w1b.data(), 0, w1b.size() * sizeof(ggml_bf16_t));
|
||||
ggml_backend_tensor_set(w2g, w2b.data(), 0, w2b.size() * sizeof(ggml_bf16_t));
|
||||
} else {
|
||||
ggml_backend_tensor_set(w1g, w1d.data(), 0, w1d.size() * sizeof(float));
|
||||
ggml_backend_tensor_set(w2g, w2d.data(), 0, w2d.size() * sizeof(float));
|
||||
}
|
||||
ggml_backend_tensor_set(x, xd.data(), 0, xd.size() * sizeof(float));
|
||||
ggml_backend_graph_compute(backend, gf);
|
||||
|
||||
std::vector<float> yd((size_t) (out * S));
|
||||
ggml_backend_tensor_get(YP, yd.data(), 0, yd.size() * sizeof(float));
|
||||
|
||||
int bad = 0;
|
||||
float max_err = 0.0f;
|
||||
for (int64_t o = 0; o < out; o++)
|
||||
for (int64_t s = 0; s < S; s++) {
|
||||
float exp = yref[(size_t) (o + out * s)];
|
||||
float got = yd[(size_t) (o + out * s)];
|
||||
float err = fabsf(got - exp);
|
||||
if (err > max_err) max_err = err;
|
||||
if (err > tol) {
|
||||
if (bad < 6) fprintf(stderr, "[KronTest] %s MISMATCH o=%lld s=%lld exp=%.5f got=%.5f\n",
|
||||
bf16 ? "BF16" : "F32", (long long) o, (long long) s, exp, got);
|
||||
bad++;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "[KronTest] %s factors: %s (%d/%lld bad, max_err=%.3g, tol=%.3g)\n",
|
||||
bf16 ? "BF16" : "F32", bad ? "FAIL" : "PASS", bad, (long long) (out * S), max_err, tol);
|
||||
fails += bad ? 1 : 0;
|
||||
ggml_backend_buffer_free(buf);
|
||||
ggml_free(ctx);
|
||||
}
|
||||
|
||||
fprintf(stderr, "[KronTest] RESULT: %s\n",
|
||||
fails ? "FAIL — Kronecker apply choreography is wrong" : "PASS — LoKr factor apply is viable on this backend");
|
||||
backend_release(bp.backend, bp.cpu_backend);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
|
||||
static void usage(const char * prog) {
|
||||
AceSynthParams d;
|
||||
ace_synth_default_params(&d);
|
||||
|
||||
fprintf(stderr, "acestep.cpp %s\n\n", ACE_VERSION);
|
||||
fprintf(stderr,
|
||||
"Usage: %s --models <dir> --request <json...> [options]\n\n"
|
||||
"Required:\n"
|
||||
" --models <dir> Directory of GGUF model files\n"
|
||||
" --request <json...> One or more request JSONs (from ace-lm --request)\n\n"
|
||||
"Optional:\n"
|
||||
" --adapters <dir> Directory of adapter files (enables JSON adapter field)\n"
|
||||
" --src-audio <file> Source audio (WAV or MP3)\n"
|
||||
" --ref-audio <file> Timbre reference audio (WAV or MP3)\n\n"
|
||||
"Model selection comes from the request JSON: synth_model picks the DiT,\n"
|
||||
"adapter picks an adapter from --adapters, output_format picks the output\n"
|
||||
"extension. When synth_model is empty the first DiT in the registry is used;\n"
|
||||
"text-encoder and VAE are always the first in their registry bucket.\n\n"
|
||||
"Audio encoding:\n"
|
||||
" --mp3-bitrate <kbps> MP3 bitrate (default: 128)\n\n"
|
||||
"Memory control:\n"
|
||||
" --vae-chunk <N> Latent frames per tile (default: %d)\n"
|
||||
" --vae-overlap <N> Overlap frames per side (default: %d)\n\n"
|
||||
"Debug:\n"
|
||||
" --no-fa Disable flash attention\n"
|
||||
" --no-batch-cfg Split DiT CFG into two separate forwards\n"
|
||||
" --clamp-fp16 Clamp hidden states to FP16 range\n"
|
||||
" --dump <dir> Dump intermediate tensors\n",
|
||||
prog, d.vae_chunk, d.vae_overlap);
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
if (std::getenv("HOTSTEP_BCAST_TEST")) {
|
||||
return run_bcast_selftest();
|
||||
}
|
||||
if (std::getenv("HOTSTEP_KRON_TEST")) {
|
||||
return run_kron_selftest();
|
||||
}
|
||||
if (argc < 2) {
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Load solver/scheduler/guidance Lua plugins. The fork routes ALL
|
||||
// sampling through hot-step-sampler.h, which resolves solvers from the
|
||||
// plugin registry — without this init the standalone CLI had no solvers
|
||||
// at all ("unknown solver 'euler'") and crashed in the fallback path.
|
||||
// Same exe-relative resolution as hot-step-server.cpp.
|
||||
{
|
||||
std::filesystem::path exe_path = std::filesystem::canonical(argv[0]);
|
||||
std::filesystem::path exe_dir = exe_path.parent_path();
|
||||
std::string dir_name = exe_dir.filename().string();
|
||||
|
||||
std::filesystem::path engine_dir;
|
||||
if (dir_name == "Release" || dir_name == "Debug" ||
|
||||
dir_name == "RelWithDebInfo" || dir_name == "MinSizeRel") {
|
||||
engine_dir = exe_dir.parent_path().parent_path();
|
||||
} else if (dir_name == "build") {
|
||||
engine_dir = exe_dir.parent_path();
|
||||
} else {
|
||||
engine_dir = exe_dir;
|
||||
}
|
||||
std::filesystem::path project_dir = engine_dir.parent_path();
|
||||
PluginRegistry::instance().init(engine_dir.string(), project_dir.string());
|
||||
}
|
||||
|
||||
// Defaults live in ace_synth_default_params. CLI locals read from params
|
||||
// so there is exactly one place in the codebase that picks the numbers.
|
||||
AceSynthParams params;
|
||||
ace_synth_default_params(¶ms);
|
||||
|
||||
std::vector<const char *> request_paths;
|
||||
const char * models_dir = NULL;
|
||||
const char * adapters_dir = NULL;
|
||||
const char * src_audio_path = NULL;
|
||||
const char * ref_audio_path = NULL;
|
||||
const char * dump_dir = NULL;
|
||||
bool use_fa = true;
|
||||
bool use_batch_cfg = true;
|
||||
bool clamp_fp16 = false;
|
||||
int vae_chunk = params.vae_chunk;
|
||||
int vae_overlap = params.vae_overlap;
|
||||
int mp3_kbps = 128;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--request")) {
|
||||
// Collect all following non-option args
|
||||
while (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
request_paths.push_back(argv[++i]);
|
||||
}
|
||||
} else if (!strcmp(argv[i], "--models") && i + 1 < argc) {
|
||||
models_dir = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--adapters") && i + 1 < argc) {
|
||||
adapters_dir = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--src-audio") && i + 1 < argc) {
|
||||
src_audio_path = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--ref-audio") && i + 1 < argc) {
|
||||
ref_audio_path = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--dump") && i + 1 < argc) {
|
||||
dump_dir = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--no-fa")) {
|
||||
use_fa = false;
|
||||
} else if (!strcmp(argv[i], "--no-batch-cfg")) {
|
||||
use_batch_cfg = false;
|
||||
} else if (!strcmp(argv[i], "--clamp-fp16")) {
|
||||
clamp_fp16 = true;
|
||||
} else if (!strcmp(argv[i], "--vae-chunk") && i + 1 < argc) {
|
||||
vae_chunk = atoi(argv[++i]);
|
||||
} else if (!strcmp(argv[i], "--vae-overlap") && i + 1 < argc) {
|
||||
vae_overlap = atoi(argv[++i]);
|
||||
} else if (!strcmp(argv[i], "--mp3-bitrate") && i + 1 < argc) {
|
||||
mp3_kbps = atoi(argv[++i]);
|
||||
} else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) {
|
||||
usage(argv[0]);
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!models_dir) {
|
||||
fprintf(stderr, "[CLI] ERROR: --models required\n");
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (request_paths.empty()) {
|
||||
fprintf(stderr, "[CLI] ERROR: --request required\n");
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parse all requests first: the first request drives model selection.
|
||||
int batch_n = (int) request_paths.size();
|
||||
std::vector<AceRequest> reqs(batch_n);
|
||||
std::vector<std::string> basenames(batch_n);
|
||||
for (int ri = 0; ri < batch_n; ri++) {
|
||||
const char * rpath = request_paths[ri];
|
||||
if (!request_parse(&reqs[ri], rpath)) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: failed to parse %s\n", rpath);
|
||||
return 1;
|
||||
}
|
||||
request_dump(&reqs[ri], stderr);
|
||||
if (reqs[ri].caption.empty() && reqs[ri].task_type != TASK_LEGO && reqs[ri].task_type != TASK_EXTRACT &&
|
||||
reqs[ri].task_type != TASK_COMPLETE) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: caption is empty in %s\n", rpath);
|
||||
return 1;
|
||||
}
|
||||
// output basename: strip .json suffix
|
||||
basenames[ri] = rpath;
|
||||
size_t dot = basenames[ri].rfind(".json");
|
||||
if (dot != std::string::npos) {
|
||||
basenames[ri] = basenames[ri].substr(0, dot);
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "[Ace-Synth] Batch: %d request(s)\n", batch_n);
|
||||
|
||||
// Scan the registry and resolve model paths from the first request.
|
||||
ModelRegistry registry;
|
||||
if (!registry_scan(®istry, models_dir)) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: cannot scan --models %s\n", models_dir);
|
||||
return 1;
|
||||
}
|
||||
if (adapters_dir) {
|
||||
registry_scan_adapters(®istry, adapters_dir);
|
||||
}
|
||||
if (registry.dit.empty() || registry.text_enc.empty() || registry.vae.empty()) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: registry needs DiT, text-encoder and VAE models\n");
|
||||
return 1;
|
||||
}
|
||||
const ModelEntry * dit_entry =
|
||||
reqs[0].synth_model.empty() ? ®istry.dit[0] : registry_find(registry.dit, reqs[0].synth_model.c_str());
|
||||
if (!dit_entry) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: synth_model '%s' not found in registry\n", reqs[0].synth_model.c_str());
|
||||
return 1;
|
||||
}
|
||||
const AdapterEntry * adapter_entry = NULL;
|
||||
if (!reqs[0].adapter.empty()) {
|
||||
adapter_entry = registry_find_adapter(registry, reqs[0].adapter.c_str());
|
||||
if (!adapter_entry) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: adapter '%s' not found (use --adapters <dir>)\n",
|
||||
reqs[0].adapter.c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-adapter stack: the `adapters` array supersedes the single `adapter`
|
||||
// field. Fold the single field into a one-element stack so the load path is
|
||||
// uniform. The resolved stack drives merge/runtime loading via the sideband.
|
||||
g_hotstep_params.adapters.clear();
|
||||
{
|
||||
std::vector<AceAdapterRef> stack = reqs[0].adapters;
|
||||
if (stack.empty() && adapter_entry) {
|
||||
stack.push_back({ reqs[0].adapter, reqs[0].adapter_scale });
|
||||
}
|
||||
for (const auto & ar : stack) {
|
||||
const AdapterEntry * e = registry_find_adapter(registry, ar.name.c_str());
|
||||
std::string path;
|
||||
if (e) {
|
||||
path = e->path;
|
||||
} else {
|
||||
FILE * t = fopen(ar.name.c_str(), "rb");
|
||||
if (t) { fclose(t); path = ar.name; }
|
||||
}
|
||||
if (path.empty()) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: adapter '%s' not found (use --adapters <dir>)\n",
|
||||
ar.name.c_str());
|
||||
return 1;
|
||||
}
|
||||
g_hotstep_params.adapters.push_back({ path, ar.scale });
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve output_format to (is_mp3, wav_fmt).
|
||||
bool is_mp3 = true;
|
||||
WavFormat wav_fmt = WAV_S16;
|
||||
if (!audio_parse_format(reqs[0].output_format.c_str(), is_mp3, wav_fmt)) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: invalid output_format '%s' (use: mp3, wav16, wav24, wav32)\n",
|
||||
reqs[0].output_format.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fill params from registry lookups and CLI flags.
|
||||
params.text_encoder_path = registry.text_enc[0].path.c_str();
|
||||
params.dit_path = dit_entry->path.c_str();
|
||||
params.vae_path = registry.vae[0].path.c_str();
|
||||
params.adapter_path = g_hotstep_params.adapters.empty() ? NULL
|
||||
: g_hotstep_params.adapters[0].path.c_str();
|
||||
params.adapter_scale = g_hotstep_params.adapters.empty() ? 1.0f
|
||||
: g_hotstep_params.adapters[0].scale;
|
||||
params.use_fa = use_fa;
|
||||
params.use_batch_cfg = use_batch_cfg;
|
||||
params.clamp_fp16 = clamp_fp16;
|
||||
params.vae_chunk = vae_chunk;
|
||||
params.vae_overlap = vae_overlap;
|
||||
params.dump_dir = dump_dir;
|
||||
|
||||
// Local store with the default STRICT policy: at most one GPU module
|
||||
// resident at a time for this one-shot CLI. No module sharing across runs,
|
||||
// so EVICT_STRICT frees the DiT before the VAE loads, and so on.
|
||||
ModelStore * store = store_create(EVICT_STRICT);
|
||||
AceSynth * ctx = ace_synth_load(store, ¶ms);
|
||||
if (!ctx) {
|
||||
store_free(store);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Read source audio (cover/lego mode)
|
||||
float * src_interleaved = NULL;
|
||||
int src_len = 0;
|
||||
if (src_audio_path) {
|
||||
int T_audio = 0;
|
||||
float * planar = audio_read_48k(src_audio_path, &T_audio);
|
||||
if (!planar) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: cannot read --src-audio %s\n", src_audio_path);
|
||||
ace_synth_free(ctx);
|
||||
store_free(store);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "[Ace-Synth] Source audio: %.2fs @ 48kHz\n", (float) T_audio / 48000.0f);
|
||||
|
||||
src_interleaved = audio_planar_to_interleaved(planar, T_audio);
|
||||
free(planar);
|
||||
src_len = T_audio;
|
||||
}
|
||||
|
||||
// Read reference audio (timbre conditioning)
|
||||
float * ref_interleaved = NULL;
|
||||
int ref_len = 0;
|
||||
if (ref_audio_path) {
|
||||
int T_audio = 0;
|
||||
float * planar = audio_read_48k(ref_audio_path, &T_audio);
|
||||
if (!planar) {
|
||||
fprintf(stderr, "[Ace-Synth] FATAL: cannot read --ref-audio %s\n", ref_audio_path);
|
||||
free(src_interleaved);
|
||||
ace_synth_free(ctx);
|
||||
store_free(store);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "[Ace-Synth] Reference audio: %.2fs @ 48kHz\n", (float) T_audio / 48000.0f);
|
||||
ref_interleaved = audio_planar_to_interleaved(planar, T_audio);
|
||||
free(planar);
|
||||
ref_len = T_audio;
|
||||
}
|
||||
|
||||
// Generate every request in one DiT batch. synth_batch_size expands each
|
||||
// request into per-seed variants in groups[0]. Total clamped to DiT max 9.
|
||||
int total_alloc = 0;
|
||||
for (int ri = 0; ri < batch_n; ri++) {
|
||||
int sbs = reqs[ri].synth_batch_size;
|
||||
total_alloc += sbs < 1 ? 1 : (sbs > 9 ? 9 : sbs);
|
||||
}
|
||||
if (total_alloc > 9) {
|
||||
fprintf(stderr, "[Ace-Synth] Batch %d exceeds DiT max 9, clamping\n", total_alloc);
|
||||
total_alloc = 9;
|
||||
}
|
||||
std::vector<AceAudio> all_audio(total_alloc);
|
||||
std::vector<std::string> all_basenames(total_alloc);
|
||||
std::vector<int> all_synth_indices(total_alloc);
|
||||
std::vector<std::vector<AceRequest>> groups(1);
|
||||
groups[0].reserve(total_alloc);
|
||||
|
||||
int off = 0;
|
||||
for (int ri = 0; ri < batch_n && off < total_alloc; ri++) {
|
||||
int sbs = reqs[ri].synth_batch_size;
|
||||
if (sbs < 1) {
|
||||
sbs = 1;
|
||||
}
|
||||
if (sbs > 9) {
|
||||
sbs = 9;
|
||||
}
|
||||
if (off + sbs > total_alloc) {
|
||||
sbs = total_alloc - off;
|
||||
}
|
||||
|
||||
// resolve seed once per original request
|
||||
request_resolve_seed(&reqs[ri]);
|
||||
const long long base_seed = reqs[ri].seed;
|
||||
|
||||
for (int i = 0; i < sbs; i++) {
|
||||
AceRequest r = reqs[ri];
|
||||
r.seed = base_seed + i;
|
||||
groups[0].push_back(r);
|
||||
all_basenames[off + i] = basenames[ri];
|
||||
all_synth_indices[off + i] = i;
|
||||
}
|
||||
off += sbs;
|
||||
}
|
||||
|
||||
if (total_alloc > 1) {
|
||||
fprintf(stderr, "[Ace-Synth] Batch: %d track(s) from %d request(s)\n", total_alloc, batch_n);
|
||||
}
|
||||
|
||||
// Two-phase run: DiT resident for all groups, then VAE for all jobs.
|
||||
const int rc = synth_batch_run(ctx, groups, src_interleaved, src_len,
|
||||
nullptr, 0, // src_latents
|
||||
ref_interleaved, ref_len,
|
||||
nullptr, 0, // ref_latents
|
||||
all_audio.data());
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, "[Ace-Synth] ERROR: batch run failed\n");
|
||||
for (auto & a : all_audio) {
|
||||
ace_audio_free(&a);
|
||||
}
|
||||
free(src_interleaved);
|
||||
free(ref_interleaved);
|
||||
ace_synth_free(ctx);
|
||||
store_free(store);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Write output files
|
||||
for (int b = 0; b < (int) all_audio.size(); b++) {
|
||||
if (!all_audio[b].samples) {
|
||||
continue;
|
||||
}
|
||||
const char * ext = is_mp3 ? ".mp3" : ".wav";
|
||||
char out_path[1024];
|
||||
snprintf(out_path, sizeof(out_path), "%s%d%s", all_basenames[b].c_str(), all_synth_indices[b], ext);
|
||||
if (!audio_write(out_path, all_audio[b].samples, all_audio[b].n_samples, 48000, mp3_kbps, wav_fmt)) {
|
||||
fprintf(stderr, "[Ace-Synth Batch%d] FATAL: failed to write %s\n", b, out_path);
|
||||
}
|
||||
ace_audio_free(&all_audio[b]);
|
||||
}
|
||||
|
||||
free(src_interleaved);
|
||||
free(ref_interleaved);
|
||||
ace_synth_free(ctx);
|
||||
store_free(store);
|
||||
fprintf(stderr, "[Ace-Synth] All done\n");
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
// ace-understand.cpp: audio understanding CLI (thin wrapper)
|
||||
//
|
||||
// Audio -> VAE encode -> FSQ tokenize -> LM understand -> metadata + lyrics
|
||||
//
|
||||
// Output: request JSON with metadata + lyrics, reusable as ace-lm or ace-synth input.
|
||||
|
||||
#include "audio-io.h"
|
||||
#include "model-registry.h"
|
||||
#include "model-store.h"
|
||||
#include "pipeline-understand.h"
|
||||
#include "request.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
static void usage(const char * prog) {
|
||||
AceUnderstandParams d;
|
||||
ace_understand_default_params(&d);
|
||||
|
||||
fprintf(stderr, "acestep.cpp %s\n\n", ACE_VERSION);
|
||||
fprintf(stderr,
|
||||
"Usage: %s --models <dir> --src-audio <file> [--request <json>] [options]\n"
|
||||
"\n"
|
||||
"Required:\n"
|
||||
" --models <dir> Directory of GGUF model files\n"
|
||||
" --src-audio <file> Source audio (WAV or MP3, any sample rate)\n"
|
||||
"\n"
|
||||
"Optional:\n"
|
||||
" --request <json> Request JSON carrying model selection and\n"
|
||||
" sampling params (lm_model, synth_model,\n"
|
||||
" lm_temperature, lm_top_p, lm_top_k)\n"
|
||||
"\n"
|
||||
"When no --request is given, understand defaults apply\n"
|
||||
"(temperature 0.3, top_p disabled).\n"
|
||||
"\n"
|
||||
"Output:\n"
|
||||
" -o <json> Output JSON (default: stdout summary)\n"
|
||||
"\n"
|
||||
"Memory control:\n"
|
||||
" --vae-chunk <N> Latent frames per tile (default: %d)\n"
|
||||
" --vae-overlap <N> Overlap frames per side (default: %d)\n"
|
||||
"\n"
|
||||
"Debug:\n"
|
||||
" --max-seq <N> KV cache size (default: %d)\n"
|
||||
" --no-fsm Disable FSM constrained decoding\n"
|
||||
" --no-fa Disable flash attention\n"
|
||||
" --dump <dir> Dump tok_latents + tok_codes (skip LM)\n",
|
||||
prog, d.vae_chunk, d.vae_overlap, d.max_seq);
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
const char * models_dir = NULL;
|
||||
const char * src_audio_path = NULL;
|
||||
const char * request_path = NULL;
|
||||
const char * output_path = NULL;
|
||||
const char * dump_dir = NULL;
|
||||
|
||||
AceUnderstandParams params;
|
||||
ace_understand_default_params(¶ms);
|
||||
|
||||
if (argc < 2) {
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--models") && i + 1 < argc) {
|
||||
models_dir = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--src-audio") && i + 1 < argc) {
|
||||
src_audio_path = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--request") && i + 1 < argc) {
|
||||
request_path = argv[++i];
|
||||
} else if (!strcmp(argv[i], "-o") && i + 1 < argc) {
|
||||
output_path = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--dump") && i + 1 < argc) {
|
||||
dump_dir = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--max-seq") && i + 1 < argc) {
|
||||
params.max_seq = atoi(argv[++i]);
|
||||
} else if (!strcmp(argv[i], "--vae-chunk") && i + 1 < argc) {
|
||||
params.vae_chunk = atoi(argv[++i]);
|
||||
} else if (!strcmp(argv[i], "--vae-overlap") && i + 1 < argc) {
|
||||
params.vae_overlap = atoi(argv[++i]);
|
||||
} else if (!strcmp(argv[i], "--no-fsm")) {
|
||||
params.use_fsm = false;
|
||||
} else if (!strcmp(argv[i], "--no-fa")) {
|
||||
params.use_fa = false;
|
||||
} else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) {
|
||||
usage(argv[0]);
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!models_dir) {
|
||||
fprintf(stderr, "[CLI] ERROR: --models required\n");
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (!src_audio_path) {
|
||||
fprintf(stderr, "[CLI] ERROR: --src-audio required\n");
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parse request JSON (if provided). Sampling params come from JSON.
|
||||
// When no JSON, understand defaults apply (temperature=0.3 for transcription).
|
||||
AceRequest req;
|
||||
request_init(&req);
|
||||
req.lm_temperature = 0.3f; // understand default: lower than generation
|
||||
req.lm_top_p = 1.0f; // understand default: no nucleus sampling
|
||||
if (request_path) {
|
||||
if (!request_parse(&req, request_path)) {
|
||||
return 1;
|
||||
}
|
||||
request_dump(&req, stderr);
|
||||
}
|
||||
|
||||
// Scan the registry and resolve model paths. Empty lm_model / synth_model
|
||||
// fall to the first entry of their bucket, matching server default behavior.
|
||||
ModelRegistry registry;
|
||||
if (!registry_scan(®istry, models_dir)) {
|
||||
fprintf(stderr, "[Ace-Understand] FATAL: cannot scan --models %s\n", models_dir);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (registry.dit.empty() || registry.vae.empty()) {
|
||||
fprintf(stderr, "[Ace-Understand] FATAL: understand pipeline needs DiT and VAE models under %s\n", models_dir);
|
||||
return 1;
|
||||
}
|
||||
if (!dump_dir && registry.lm.empty()) {
|
||||
fprintf(stderr, "[Ace-Understand] FATAL: understand pipeline needs an LM model under %s\n", models_dir);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const ModelEntry * lm_entry =
|
||||
dump_dir ? NULL : (req.lm_model.empty() ? ®istry.lm[0] : registry_find(registry.lm, req.lm_model.c_str()));
|
||||
if (!dump_dir && !lm_entry) {
|
||||
fprintf(stderr, "[Ace-Understand] FATAL: lm_model '%s' not found in registry\n", req.lm_model.c_str());
|
||||
return 1;
|
||||
}
|
||||
const ModelEntry * dit_entry =
|
||||
req.synth_model.empty() ? ®istry.dit[0] : registry_find(registry.dit, req.synth_model.c_str());
|
||||
if (!dit_entry) {
|
||||
fprintf(stderr, "[Ace-Understand] FATAL: synth_model '%s' not found in registry\n", req.synth_model.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
params.model_path = lm_entry ? lm_entry->path.c_str() : NULL;
|
||||
params.dit_path = dit_entry->path.c_str();
|
||||
params.vae_path = registry.vae[0].path.c_str();
|
||||
params.dump_dir = dump_dir;
|
||||
|
||||
// load pipeline
|
||||
ModelStore * store = store_create(EVICT_STRICT);
|
||||
AceUnderstand * ctx = ace_understand_load(store, ¶ms);
|
||||
if (!ctx) {
|
||||
store_free(store);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Read and resample audio to 48kHz stereo
|
||||
int T_audio = 0;
|
||||
float * planar = audio_read_48k(src_audio_path, &T_audio);
|
||||
if (!planar) {
|
||||
fprintf(stderr, "[Ace-Understand] FATAL: cannot read %s\n", src_audio_path);
|
||||
ace_understand_free(ctx);
|
||||
store_free(store);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "[Ace-Understand] %.2fs @ 48kHz\n", (float) T_audio / 48000.0f);
|
||||
|
||||
float * src_interleaved = audio_planar_to_interleaved(planar, T_audio);
|
||||
free(planar);
|
||||
int src_len = T_audio;
|
||||
|
||||
// run understand pipeline
|
||||
AceRequest out;
|
||||
int rc = ace_understand_generate(ctx, src_interleaved, src_len,
|
||||
nullptr, 0, // src_latents (audio path)
|
||||
&req, &out,
|
||||
nullptr, nullptr, // latent_out, T_latent_out
|
||||
NULL, NULL);
|
||||
free(src_interleaved);
|
||||
ace_understand_free(ctx);
|
||||
store_free(store);
|
||||
|
||||
if (rc != 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// write output JSON
|
||||
if (output_path) {
|
||||
request_write(&out, output_path);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// bs-roformer-test.cpp: numeric validation of bs-roformer-ggml.h.
|
||||
//
|
||||
// Compares the GGML graph against PyTorch reference activations dumped by
|
||||
// scripts/dump_bs_roformer_goldens.py, stage by stage, so a divergence can be
|
||||
// localised to a layer instead of only showing up as bad audio.
|
||||
//
|
||||
// The goldens .npz is converted to a flat .bin by scripts/goldens_to_bin.py
|
||||
// (npz parsing in C++ is not worth the code).
|
||||
//
|
||||
// Usage:
|
||||
// bs-roformer-test <model.gguf> <goldens.bin> [--no-flash]
|
||||
//
|
||||
// Part of HOT-Step CPP. MIT license.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "bs-roformer-ggml.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Flat golden file layout (little-endian):
|
||||
// magic "BSRG" 4 bytes
|
||||
// int32 T, in_dim, depth, dim, n_bands, n_stems
|
||||
// f32 input [in_dim * T]
|
||||
// f32 band_split [dim * n_bands * T]
|
||||
// f32 layer_00 [dim * n_bands * T]
|
||||
// f32 layer_01 [dim * n_bands * T]
|
||||
// f32 layer_last [dim * n_bands * T]
|
||||
// f32 final_norm [dim * n_bands * T]
|
||||
// f32 mask [n_stems * in_dim * T]
|
||||
struct Goldens {
|
||||
int32_t T = 0, in_dim = 0, depth = 0, dim = 0, n_bands = 0, n_stems = 0;
|
||||
std::vector<float> input, band_split, layer_00, layer_01, layer_last, final_norm, mask;
|
||||
};
|
||||
|
||||
bool read_block(FILE * f, std::vector<float> & v, size_t n) {
|
||||
v.resize(n);
|
||||
return fread(v.data(), sizeof(float), n, f) == n;
|
||||
}
|
||||
|
||||
bool load_goldens(const char * path, Goldens & g) {
|
||||
FILE * f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "cannot open %s\n", path);
|
||||
return false;
|
||||
}
|
||||
char magic[4];
|
||||
if (fread(magic, 1, 4, f) != 4 || memcmp(magic, "BSRG", 4) != 0) {
|
||||
fprintf(stderr, "%s: bad magic\n", path);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
int32_t hdr[6];
|
||||
if (fread(hdr, sizeof(int32_t), 6, f) != 6) {
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
g.T = hdr[0]; g.in_dim = hdr[1]; g.depth = hdr[2];
|
||||
g.dim = hdr[3]; g.n_bands = hdr[4]; g.n_stems = hdr[5];
|
||||
|
||||
const size_t hidden = (size_t) g.dim * g.n_bands * g.T;
|
||||
bool ok = read_block(f, g.input, (size_t) g.in_dim * g.T)
|
||||
&& read_block(f, g.band_split, hidden)
|
||||
&& read_block(f, g.layer_00, hidden)
|
||||
&& read_block(f, g.layer_01, hidden)
|
||||
&& read_block(f, g.layer_last, hidden)
|
||||
&& read_block(f, g.final_norm, hidden)
|
||||
&& read_block(f, g.mask, (size_t) g.n_stems * g.in_dim * g.T);
|
||||
fclose(f);
|
||||
if (!ok) fprintf(stderr, "%s: truncated\n", path);
|
||||
return ok;
|
||||
}
|
||||
|
||||
struct Cmp {
|
||||
double max_abs = 0.0, mean_abs = 0.0, ref_peak = 0.0, got_peak = 0.0, rel = 0.0;
|
||||
size_t worst_idx = 0;
|
||||
bool finite = true;
|
||||
};
|
||||
|
||||
Cmp compare(const std::vector<float> & got, const std::vector<float> & ref) {
|
||||
Cmp c;
|
||||
const size_t n = ref.size() < got.size() ? ref.size() : got.size();
|
||||
double sum = 0.0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!std::isfinite(got[i])) c.finite = false;
|
||||
double d = std::fabs((double) got[i] - (double) ref[i]);
|
||||
if (d > c.max_abs) { c.max_abs = d; c.worst_idx = i; }
|
||||
sum += d;
|
||||
double a = std::fabs((double) ref[i]);
|
||||
if (a > c.ref_peak) c.ref_peak = a;
|
||||
double b = std::fabs((double) got[i]);
|
||||
if (b > c.got_peak) c.got_peak = b;
|
||||
}
|
||||
c.mean_abs = n ? sum / (double) n : 0.0;
|
||||
c.rel = c.ref_peak > 0.0 ? c.max_abs / c.ref_peak : c.max_abs;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Tolerances are per-stage and deliberately loose at the tail.
|
||||
//
|
||||
// F32 GGML vs F32 PyTorch across 16 layers of a residual stream accumulates
|
||||
// genuine reordering drift, and it is much worse for the instrumental
|
||||
// checkpoint than the vocal one: its activations reach absmax ~321 (vs ~32),
|
||||
// and final_norm then divides that down by ~90, so a ~1% relative difference
|
||||
// at layer_last lands intact on the mask.
|
||||
//
|
||||
// That is conditioning, not a bug — the same graph on the vocal weights
|
||||
// matches to 2e-3, and the flash and materialised-F32 paths agree with each
|
||||
// other to better than they each agree with PyTorch. The checkpoints were
|
||||
// trained with use_amp=true, so they are not precision-critical at 1e-3.
|
||||
// A 1% mask error is ~0.09 dB of amplitude — well below audibility.
|
||||
bool report(const char * label, const Cmp & c, double tol) {
|
||||
const bool pass = c.finite && c.rel <= tol;
|
||||
printf(" %-12s max %.3e mean %.3e ref_pk %.3e got_pk %.3e rel %.2e %s\n",
|
||||
label, c.max_abs, c.mean_abs, c.ref_peak, c.got_peak, c.rel,
|
||||
!c.finite ? "NON-FINITE" : (pass ? "ok" : "FAIL"));
|
||||
return pass;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
if (argc < 3) {
|
||||
fprintf(stderr,
|
||||
"usage: %s <model.gguf> <goldens.bin> [--no-flash]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
const char * gguf_path = argv[1];
|
||||
const char * gold_path = argv[2];
|
||||
bool no_flash = false;
|
||||
for (int i = 3; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--no-flash") == 0) no_flash = true;
|
||||
}
|
||||
|
||||
Goldens g;
|
||||
if (!load_goldens(gold_path, g)) return 1;
|
||||
printf("goldens: T=%d in_dim=%d depth=%d dim=%d bands=%d stems=%d\n",
|
||||
g.T, g.in_dim, g.depth, g.dim, g.n_bands, g.n_stems);
|
||||
|
||||
BsRoformer m;
|
||||
if (!bsr_load(&m, gguf_path)) return 1;
|
||||
m.use_flash_attn = !no_flash;
|
||||
printf("attention: %s\n\n", m.use_flash_attn ? "flash" : "materialised f32");
|
||||
|
||||
if (m.cfg.in_dim != g.in_dim || m.cfg.depth != g.depth ||
|
||||
m.cfg.dim != g.dim || m.cfg.n_bands != g.n_bands) {
|
||||
fprintf(stderr, "model/goldens mismatch\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const size_t hidden = (size_t) g.dim * g.n_bands * g.T;
|
||||
std::vector<float> out((size_t) g.n_stems * g.in_dim * g.T);
|
||||
bool all_ok = true;
|
||||
|
||||
struct Stage {
|
||||
int stage;
|
||||
const char * label;
|
||||
const std::vector<float> * ref;
|
||||
double tol;
|
||||
};
|
||||
const Stage stages[] = {
|
||||
{ 0, "band_split", &g.band_split, 2e-3 },
|
||||
{ 1, "layer_00", &g.layer_00, 5e-3 },
|
||||
{ 2, "layer_01", &g.layer_01, 5e-3 },
|
||||
{ g.depth, "layer_last", &g.layer_last, 2e-2 },
|
||||
{ g.depth + 1, "final_norm", &g.final_norm, 2e-2 },
|
||||
};
|
||||
|
||||
for (const Stage & s : stages) {
|
||||
m.debug_stage = s.stage;
|
||||
bsr_forward(&m, g.input.data(), g.T, out.data());
|
||||
if (m.debug_out.size() != hidden) {
|
||||
fprintf(stderr, " %-12s debug buffer %zu != %zu\n",
|
||||
s.label, m.debug_out.size(), hidden);
|
||||
all_ok = false;
|
||||
continue;
|
||||
}
|
||||
all_ok &= report(s.label, compare(m.debug_out, *s.ref), s.tol);
|
||||
}
|
||||
|
||||
m.debug_stage = -1;
|
||||
bsr_forward(&m, g.input.data(), g.T, out.data());
|
||||
// 8e-2 accommodates the instrumental checkpoint's conditioning (see above);
|
||||
// the vocal one lands at ~2e-3 against the same bar.
|
||||
all_ok &= report("mask", compare(out, g.mask), 8e-2);
|
||||
|
||||
bsr_free(&m);
|
||||
|
||||
// Regression guard: load/free/load again in the same process. The engine
|
||||
// runs the vocal then the instrumental checkpoint back to back, and
|
||||
// backend_init hands out a refcounted shared singleton — a bsr_free that
|
||||
// frees it outright leaves the second load holding a dangling backend and
|
||||
// segfaults. A single-model test cannot see that, so do it explicitly.
|
||||
{
|
||||
printf("\nreload check (shared-backend refcount)...\n");
|
||||
BsRoformer m2;
|
||||
if (!bsr_load(&m2, gguf_path)) {
|
||||
fprintf(stderr, " reload FAILED — backend teardown is wrong\n");
|
||||
return 1;
|
||||
}
|
||||
std::vector<float> out2((size_t) g.n_stems * g.in_dim * g.T);
|
||||
m2.debug_stage = -1;
|
||||
bsr_forward(&m2, g.input.data(), g.T, out2.data());
|
||||
all_ok &= report("mask(reload)", compare(out2, g.mask), 8e-2);
|
||||
bsr_free(&m2);
|
||||
}
|
||||
|
||||
printf("\n%s\n", all_ok ? "PASS — GGML matches the PyTorch reference"
|
||||
: "FAIL — see stages above");
|
||||
return all_ok ? 0 : 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,401 @@
|
||||
// mastering.cpp — Reference-based audio mastering CLI tool
|
||||
//
|
||||
// Usage: mastering --target input.wav --reference ref.wav --output mastered.wav
|
||||
// [--no-limiter] [--pcm24]
|
||||
//
|
||||
// Implements the matchering algorithm: spectral + RMS matching against a reference track.
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mastering.h"
|
||||
|
||||
// ─── Minimal WAV Reader/Writer ──────────────────────────────────────
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct WavHeader {
|
||||
char riff[4]; // "RIFF"
|
||||
uint32_t file_size; // file size - 8
|
||||
char wave[4]; // "WAVE"
|
||||
};
|
||||
struct WavChunkHdr {
|
||||
char id[4];
|
||||
uint32_t size;
|
||||
};
|
||||
struct WavFmt {
|
||||
uint16_t format; // 1=PCM, 3=IEEE float
|
||||
uint16_t channels;
|
||||
uint32_t sample_rate;
|
||||
uint32_t byte_rate;
|
||||
uint16_t block_align;
|
||||
uint16_t bits_per_sample;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct WavData {
|
||||
std::vector<float> L, R;
|
||||
int sample_rate;
|
||||
int channels;
|
||||
};
|
||||
|
||||
static bool wav_read(const char * path, WavData & out) {
|
||||
FILE * f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "[WAV] Cannot open: %s\n", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
WavHeader hdr;
|
||||
if (fread(&hdr, sizeof(hdr), 1, f) != 1
|
||||
|| memcmp(hdr.riff, "RIFF", 4) != 0
|
||||
|| memcmp(hdr.wave, "WAVE", 4) != 0) {
|
||||
fprintf(stderr, "[WAV] Invalid WAV header: %s\n", path);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
|
||||
WavFmt fmt = {};
|
||||
bool found_fmt = false, found_data = false;
|
||||
int data_size = 0;
|
||||
|
||||
while (!feof(f)) {
|
||||
WavChunkHdr chunk;
|
||||
if (fread(&chunk, sizeof(chunk), 1, f) != 1) break;
|
||||
|
||||
if (memcmp(chunk.id, "fmt ", 4) == 0) {
|
||||
int to_read = std::min((uint32_t) sizeof(fmt), chunk.size);
|
||||
if (fread(&fmt, to_read, 1, f) != 1) break;
|
||||
// Skip extra fmt bytes
|
||||
if (chunk.size > (uint32_t) to_read) {
|
||||
fseek(f, chunk.size - to_read, SEEK_CUR);
|
||||
}
|
||||
found_fmt = true;
|
||||
} else if (memcmp(chunk.id, "data", 4) == 0) {
|
||||
data_size = chunk.size;
|
||||
found_data = true;
|
||||
break; // data follows
|
||||
} else {
|
||||
// Skip unknown chunk
|
||||
fseek(f, chunk.size, SEEK_CUR);
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_fmt || !found_data) {
|
||||
fprintf(stderr, "[WAV] Missing fmt or data chunk: %s\n", path);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
|
||||
// WAVE_FORMAT_EXTENSIBLE (0xFFFE): the real format code is in the sub-format
|
||||
// GUID at the end of the extended fmt chunk. The first 2 bytes of the GUID
|
||||
// encode the actual format (1=PCM, 3=float). Common for 24/32-bit and
|
||||
// multi-channel audio from DAWs.
|
||||
if (fmt.format == 0xFFFE) {
|
||||
// The extended fmt chunk has: cbSize(2) + validBitsPerSample(2) +
|
||||
// channelMask(4) + subFormat GUID(16). We already read the base WavFmt
|
||||
// (16 bytes), and we skipped any extra bytes. We need to re-read the
|
||||
// extension. Seek back to re-read the extension portion.
|
||||
// Actually, we skipped extra fmt bytes — we need to read them before skip.
|
||||
// Let's fix: the fmt chunk was already fully consumed (read + skip).
|
||||
// We need to handle this in the fmt reading section above. For now,
|
||||
// the simplest fix: re-open and re-parse just the fmt extension.
|
||||
fprintf(stderr, "[WAV] WAVE_FORMAT_EXTENSIBLE detected, attempting re-parse: %s\n", path);
|
||||
fclose(f);
|
||||
|
||||
f = fopen(path, "rb");
|
||||
if (!f) return false;
|
||||
fseek(f, sizeof(WavHeader), SEEK_SET);
|
||||
|
||||
// Scan for fmt chunk again
|
||||
while (!feof(f)) {
|
||||
WavChunkHdr chunk2;
|
||||
if (fread(&chunk2, sizeof(chunk2), 1, f) != 1) break;
|
||||
if (memcmp(chunk2.id, "fmt ", 4) == 0) {
|
||||
// Read base fmt (16 bytes)
|
||||
WavFmt fmt2 = {};
|
||||
int base = std::min((uint32_t)sizeof(fmt2), chunk2.size);
|
||||
if (fread(&fmt2, base, 1, f) != 1) break;
|
||||
|
||||
// Read extension: cbSize(2), validBits(2), channelMask(4), subFormat(16)
|
||||
if (chunk2.size >= 40) { // 16 base + 2 cbSize + 2 validBits + 4 mask + 16 GUID
|
||||
uint16_t cb_size = 0;
|
||||
uint16_t valid_bits = 0;
|
||||
uint32_t channel_mask = 0;
|
||||
uint16_t sub_format = 0;
|
||||
|
||||
fread(&cb_size, 2, 1, f);
|
||||
fread(&valid_bits, 2, 1, f);
|
||||
fread(&channel_mask, 4, 1, f);
|
||||
fread(&sub_format, 2, 1, f); // first 2 bytes of GUID = real format
|
||||
|
||||
fmt.format = sub_format;
|
||||
if (valid_bits > 0) {
|
||||
fmt.bits_per_sample = valid_bits;
|
||||
}
|
||||
fprintf(stderr, "[WAV] EXTENSIBLE sub-format: %d (%s), valid bits: %d\n",
|
||||
sub_format, sub_format == 1 ? "PCM" : sub_format == 3 ? "float" : "unknown",
|
||||
valid_bits > 0 ? valid_bits : fmt.bits_per_sample);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
fseek(f, chunk2.size, SEEK_CUR);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
// Re-open and seek to data chunk
|
||||
f = fopen(path, "rb");
|
||||
if (!f) return false;
|
||||
fseek(f, sizeof(WavHeader), SEEK_SET);
|
||||
found_data = false;
|
||||
while (!feof(f)) {
|
||||
WavChunkHdr chunk2;
|
||||
if (fread(&chunk2, sizeof(chunk2), 1, f) != 1) break;
|
||||
if (memcmp(chunk2.id, "data", 4) == 0) {
|
||||
data_size = chunk2.size;
|
||||
found_data = true;
|
||||
break;
|
||||
} else {
|
||||
fseek(f, chunk2.size, SEEK_CUR);
|
||||
}
|
||||
}
|
||||
if (!found_data) {
|
||||
fprintf(stderr, "[WAV] Cannot find data chunk on re-parse: %s\n", path);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (fmt.format != 1 && fmt.format != 3) {
|
||||
fprintf(stderr, "[WAV] Unsupported format %d (need PCM=1 or float=3): %s\n",
|
||||
fmt.format, path);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fmt.channels < 1 || fmt.channels > 2) {
|
||||
fprintf(stderr, "[WAV] Unsupported channel count %d: %s\n", fmt.channels, path);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
|
||||
int bytes_per_sample = fmt.bits_per_sample / 8;
|
||||
int n_samples = data_size / (bytes_per_sample * fmt.channels);
|
||||
|
||||
out.sample_rate = fmt.sample_rate;
|
||||
out.channels = fmt.channels;
|
||||
out.L.resize(n_samples);
|
||||
out.R.resize(n_samples);
|
||||
|
||||
// Read raw data
|
||||
std::vector<uint8_t> raw(data_size);
|
||||
if (fread(raw.data(), 1, data_size, f) != (size_t) data_size) {
|
||||
fprintf(stderr, "[WAV] Truncated data: %s\n", path);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
// Convert to float
|
||||
for (int i = 0; i < n_samples; i++) {
|
||||
for (int ch = 0; ch < fmt.channels; ch++) {
|
||||
int offset = (i * fmt.channels + ch) * bytes_per_sample;
|
||||
float val = 0.0f;
|
||||
|
||||
if (fmt.format == 3) {
|
||||
// IEEE float
|
||||
if (bytes_per_sample == 4) {
|
||||
memcpy(&val, raw.data() + offset, 4);
|
||||
} else if (bytes_per_sample == 8) {
|
||||
double dval;
|
||||
memcpy(&dval, raw.data() + offset, 8);
|
||||
val = (float) dval;
|
||||
}
|
||||
} else {
|
||||
// PCM integer
|
||||
if (bytes_per_sample == 2) {
|
||||
int16_t ival;
|
||||
memcpy(&ival, raw.data() + offset, 2);
|
||||
val = ival / 32768.0f;
|
||||
} else if (bytes_per_sample == 3) {
|
||||
int32_t ival = 0;
|
||||
memcpy(&ival, raw.data() + offset, 3);
|
||||
if (ival & 0x800000) ival |= 0xFF000000; // sign extend
|
||||
val = ival / 8388608.0f;
|
||||
} else if (bytes_per_sample == 4) {
|
||||
int32_t ival;
|
||||
memcpy(&ival, raw.data() + offset, 4);
|
||||
val = (float) ((double) ival / 2147483648.0);
|
||||
}
|
||||
}
|
||||
|
||||
if (ch == 0) out.L[i] = val;
|
||||
else out.R[i] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// Mono → stereo
|
||||
if (fmt.channels == 1) {
|
||||
out.R = out.L;
|
||||
out.channels = 2;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[WAV] Read %s: %d samples, %d ch, %d Hz, %d-bit %s\n",
|
||||
path, n_samples, fmt.channels, fmt.sample_rate,
|
||||
fmt.bits_per_sample, fmt.format == 3 ? "float" : "PCM");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool wav_write(const char * path, const float * L, const float * R, int n,
|
||||
int sample_rate, int bits = 16) {
|
||||
FILE * f = fopen(path, "wb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "[WAV] Cannot create: %s\n", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
int channels = 2;
|
||||
int bytes_per_sample = bits / 8;
|
||||
int data_size = n * channels * bytes_per_sample;
|
||||
|
||||
WavHeader hdr;
|
||||
memcpy(hdr.riff, "RIFF", 4);
|
||||
hdr.file_size = 36 + data_size;
|
||||
memcpy(hdr.wave, "WAVE", 4);
|
||||
|
||||
WavChunkHdr fmt_chunk;
|
||||
memcpy(fmt_chunk.id, "fmt ", 4);
|
||||
fmt_chunk.size = 16;
|
||||
|
||||
WavFmt fmt;
|
||||
fmt.format = (bits == 32) ? 3 : 1; // float or PCM
|
||||
fmt.channels = channels;
|
||||
fmt.sample_rate = sample_rate;
|
||||
fmt.bits_per_sample = bits;
|
||||
fmt.block_align = channels * bytes_per_sample;
|
||||
fmt.byte_rate = sample_rate * fmt.block_align;
|
||||
|
||||
WavChunkHdr data_chunk;
|
||||
memcpy(data_chunk.id, "data", 4);
|
||||
data_chunk.size = data_size;
|
||||
|
||||
fwrite(&hdr, sizeof(hdr), 1, f);
|
||||
fwrite(&fmt_chunk, sizeof(fmt_chunk), 1, f);
|
||||
fwrite(&fmt, sizeof(fmt), 1, f);
|
||||
fwrite(&data_chunk, sizeof(data_chunk), 1, f);
|
||||
|
||||
// Write interleaved samples
|
||||
for (int i = 0; i < n; i++) {
|
||||
float l = std::clamp(L[i], -1.0f, 1.0f);
|
||||
float r = std::clamp(R[i], -1.0f, 1.0f);
|
||||
|
||||
if (bits == 16) {
|
||||
int16_t sl = (int16_t) (l * 32767.0f);
|
||||
int16_t sr = (int16_t) (r * 32767.0f);
|
||||
fwrite(&sl, 2, 1, f);
|
||||
fwrite(&sr, 2, 1, f);
|
||||
} else if (bits == 24) {
|
||||
int32_t sl = (int32_t) (l * 8388607.0f);
|
||||
int32_t sr_v = (int32_t) (r * 8388607.0f);
|
||||
fwrite(&sl, 3, 1, f);
|
||||
fwrite(&sr_v, 3, 1, f);
|
||||
} else if (bits == 32) {
|
||||
fwrite(&l, 4, 1, f);
|
||||
fwrite(&r, 4, 1, f);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
fprintf(stderr, "[WAV] Wrote %s: %d samples, %d Hz, %d-bit\n",
|
||||
path, n, sample_rate, bits);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── CLI ────────────────────────────────────────────────────────────
|
||||
|
||||
static void print_usage(const char * prog) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s --target input.wav --reference ref.wav --output mastered.wav\n"
|
||||
" [--pcm24] [--pcm32f]\n"
|
||||
"\n"
|
||||
"Reference-based audio mastering using the matchering algorithm.\n"
|
||||
"Matches the RMS level, frequency spectrum, and dynamic range\n"
|
||||
"of the target track to the reference track.\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" --target PATH Input audio file to master (WAV)\n"
|
||||
" --reference PATH Reference track to match against (WAV)\n"
|
||||
" --output PATH Output mastered file (WAV)\n"
|
||||
" --pcm24 Write 24-bit PCM output (default: 16-bit)\n"
|
||||
" --pcm32f Write 32-bit float output\n",
|
||||
prog);
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
const char * target_path = nullptr;
|
||||
const char * ref_path = nullptr;
|
||||
const char * output_path = nullptr;
|
||||
int output_bits = 16;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--target") == 0 && i + 1 < argc) {
|
||||
target_path = argv[++i];
|
||||
} else if (strcmp(argv[i], "--reference") == 0 && i + 1 < argc) {
|
||||
ref_path = argv[++i];
|
||||
} else if (strcmp(argv[i], "--output") == 0 && i + 1 < argc) {
|
||||
output_path = argv[++i];
|
||||
} else if (strcmp(argv[i], "--pcm24") == 0) {
|
||||
output_bits = 24;
|
||||
} else if (strcmp(argv[i], "--pcm32f") == 0) {
|
||||
output_bits = 32;
|
||||
} else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target_path || !ref_path || !output_path) {
|
||||
fprintf(stderr, "Error: --target, --reference, and --output are required\n\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto t_start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Read input files
|
||||
WavData target, reference;
|
||||
if (!wav_read(target_path, target)) return 1;
|
||||
if (!wav_read(ref_path, reference)) return 1;
|
||||
|
||||
// Run mastering
|
||||
auto result = mastering_process(
|
||||
target.L.data(), target.R.data(), (int) target.L.size(), target.sample_rate,
|
||||
reference.L.data(), reference.R.data(), (int) reference.L.size(), reference.sample_rate
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
fprintf(stderr, "[Mastering] FAILED: %s\n", result.error ? result.error : "unknown");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Write output
|
||||
if (!wav_write(output_path, result.L.data(), result.R.data(),
|
||||
(int) result.L.size(), target.sample_rate, output_bits)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto t_end = std::chrono::high_resolution_clock::now();
|
||||
double elapsed = std::chrono::duration<double>(t_end - t_start).count();
|
||||
fprintf(stderr, "[Mastering] Total time: %.2f seconds\n", elapsed);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// mdx23c-test.cpp: validation for mdx23c-ggml.h.
|
||||
//
|
||||
// Without a goldens file: loads the model, runs one forward at the trained
|
||||
// frame count and checks the output shape, finiteness and range. That alone
|
||||
// catches the shape/layout mistakes that are easy to make in a conv U-Net.
|
||||
//
|
||||
// With a goldens .bin (scripts/dump_mdx23c_goldens.py): also compares against
|
||||
// the PyTorch reference elementwise.
|
||||
//
|
||||
// Usage:
|
||||
// mdx23c-test <model.gguf> [goldens.bin]
|
||||
//
|
||||
// Part of HOT-Step CPP. MIT license.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "mdx23c-ggml.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Flat golden layout: magic "MDXG", int32 T, dim_f, cin, n_inst,
|
||||
// then f32 input [T*dim_f*cin], f32 output [n_inst*cin*dim_f*T].
|
||||
struct Goldens {
|
||||
int32_t T = 0, dim_f = 0, cin = 0, n_inst = 0;
|
||||
std::vector<float> input, out;
|
||||
};
|
||||
|
||||
bool load_goldens(const char * path, Goldens & g) {
|
||||
FILE * f = fopen(path, "rb");
|
||||
if (!f) { fprintf(stderr, "cannot open %s\n", path); return false; }
|
||||
char magic[4];
|
||||
int32_t hdr[4];
|
||||
if (fread(magic, 1, 4, f) != 4 || memcmp(magic, "MDXG", 4) != 0 ||
|
||||
fread(hdr, sizeof(int32_t), 4, f) != 4) {
|
||||
fprintf(stderr, "%s: bad header\n", path); fclose(f); return false;
|
||||
}
|
||||
g.T = hdr[0]; g.dim_f = hdr[1]; g.cin = hdr[2]; g.n_inst = hdr[3];
|
||||
g.input.resize((size_t) g.T * g.dim_f * g.cin);
|
||||
g.out.resize((size_t) g.n_inst * g.cin * g.dim_f * g.T);
|
||||
bool ok = fread(g.input.data(), sizeof(float), g.input.size(), f) == g.input.size()
|
||||
&& fread(g.out.data(), sizeof(float), g.out.size(), f) == g.out.size();
|
||||
fclose(f);
|
||||
if (!ok) fprintf(stderr, "%s: truncated\n", path);
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "usage: %s <model.gguf> [goldens.bin]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Mdx23c m;
|
||||
if (!mdx_load(&m, argv[1])) return 1;
|
||||
|
||||
const Mdx23cConfig & c = m.cfg;
|
||||
const int cin = c.n_audio_channels * 2;
|
||||
int T = c.chunk_size / c.hop_length + 1;
|
||||
|
||||
Goldens g;
|
||||
const bool have_goldens = (argc >= 3) && load_goldens(argv[2], g);
|
||||
if (argc >= 3 && !have_goldens) return 1;
|
||||
if (have_goldens) {
|
||||
if (g.dim_f != c.dim_f || g.cin != cin || g.n_inst != c.n_instruments) {
|
||||
fprintf(stderr, "goldens/model mismatch\n");
|
||||
return 1;
|
||||
}
|
||||
T = g.T;
|
||||
}
|
||||
|
||||
printf("T=%d dim_f=%d cin=%d instruments=%d\n", T, c.dim_f, cin, c.n_instruments);
|
||||
|
||||
std::vector<float> in((size_t) T * c.dim_f * cin);
|
||||
if (have_goldens) {
|
||||
memcpy(in.data(), g.input.data(), in.size() * sizeof(float));
|
||||
} else {
|
||||
unsigned s = 1234;
|
||||
for (size_t i = 0; i < in.size(); i++) {
|
||||
s = s * 1664525u + 1013904223u;
|
||||
in[i] = ((float) (s >> 8) / 8388608.0f - 1.0f) * 0.05f;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<float> out((size_t) c.n_instruments * cin * c.dim_f * T);
|
||||
mdx_forward(&m, in.data(), T, out.data());
|
||||
|
||||
double pk = 0.0;
|
||||
bool finite = true;
|
||||
for (float v : out) {
|
||||
if (!std::isfinite(v)) finite = false;
|
||||
double a = std::fabs((double) v);
|
||||
if (a > pk) pk = a;
|
||||
}
|
||||
printf("output %zu values, peak %.6f, %s\n", out.size(), pk,
|
||||
finite ? "all finite" : "NON-FINITE");
|
||||
if (!finite) return 1;
|
||||
|
||||
int rc = 0;
|
||||
if (have_goldens) {
|
||||
double max_abs = 0.0, sum = 0.0, ref_pk = 0.0;
|
||||
for (size_t i = 0; i < out.size(); i++) {
|
||||
double d = std::fabs((double) out[i] - (double) g.out[i]);
|
||||
if (d > max_abs) max_abs = d;
|
||||
sum += d;
|
||||
double a = std::fabs((double) g.out[i]);
|
||||
if (a > ref_pk) ref_pk = a;
|
||||
}
|
||||
const double mean = sum / (double) out.size();
|
||||
const double rel = ref_pk > 0.0 ? max_abs / ref_pk : max_abs;
|
||||
printf("vs PyTorch: max %.3e mean %.3e ref_pk %.3e rel %.2e %s\n",
|
||||
max_abs, mean, ref_pk, rel, rel <= 2e-2 ? "ok" : "FAIL");
|
||||
rc = rel <= 2e-2 ? 0 : 1;
|
||||
} else {
|
||||
printf("(no goldens supplied — shape/finiteness check only)\n");
|
||||
}
|
||||
|
||||
// Reload guard: backend_init hands out a refcounted shared singleton, so a
|
||||
// wrong teardown only shows up on the SECOND load in a process.
|
||||
{
|
||||
Mdx23c m2;
|
||||
if (!mdx_load(&m2, argv[1])) {
|
||||
fprintf(stderr, "reload FAILED — backend teardown is wrong\n");
|
||||
mdx_free(&m);
|
||||
return 1;
|
||||
}
|
||||
printf("reload ok\n");
|
||||
mdx_free(&m2);
|
||||
}
|
||||
|
||||
mdx_free(&m);
|
||||
printf("%s\n", rc == 0 ? "PASS" : "FAIL");
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// mp3-codec.cpp: MP3 encoder/decoder CLI.
|
||||
//
|
||||
// Encode: mp3-codec -i input.wav -o output.mp3 [-b 128]
|
||||
// Decode: mp3-codec -i input.mp3 -o output.wav
|
||||
//
|
||||
// Direction is auto-detected from output extension.
|
||||
// Encoder: acestep mp3enc (MIT). Decoder: minimp3 (CC0).
|
||||
|
||||
#include "audio-io.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
static bool ends_with(const char * str, const char * suffix) {
|
||||
int slen = (int) strlen(str);
|
||||
int xlen = (int) strlen(suffix);
|
||||
if (slen < xlen) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < xlen; i++) {
|
||||
char a = str[slen - xlen + i];
|
||||
char b = suffix[i];
|
||||
if (a >= 'A' && a <= 'Z') {
|
||||
a += 32;
|
||||
}
|
||||
if (b >= 'A' && b <= 'Z') {
|
||||
b += 32;
|
||||
}
|
||||
if (a != b) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
if (argc < 5) {
|
||||
fprintf(stderr, "acestep.cpp %s\n\n", ACE_VERSION);
|
||||
fprintf(stderr,
|
||||
"Usage: %s -i <input> -o <output> [options]\n"
|
||||
"\n"
|
||||
" -i <path> Input file (WAV or MP3)\n"
|
||||
" -o <path> Output file (WAV or MP3)\n"
|
||||
" -b <kbps> Bitrate for MP3 encoding (default: 128)\n"
|
||||
" --format <f> WAV format: wav16, wav24, wav32 (default: wav16)\n"
|
||||
"\n"
|
||||
"Mode is auto-detected from output extension.\n"
|
||||
"\n"
|
||||
"Examples:\n"
|
||||
" %s -i song.wav -o song.mp3\n"
|
||||
" %s -i song.wav -o song.mp3 -b 192\n"
|
||||
" %s -i song.mp3 -o song.wav\n"
|
||||
" %s -i song.mp3 -o song.wav --format wav32\n",
|
||||
argv[0], argv[0], argv[0], argv[0], argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char * input = NULL;
|
||||
const char * output = NULL;
|
||||
int bitrate = 128;
|
||||
WavFormat wav_fmt = WAV_S16;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-i") == 0 && i + 1 < argc) {
|
||||
input = argv[++i];
|
||||
} else if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
|
||||
output = argv[++i];
|
||||
} else if (strcmp(argv[i], "-b") == 0 && i + 1 < argc) {
|
||||
bitrate = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--format") == 0 && i + 1 < argc) {
|
||||
bool dummy_mp3;
|
||||
if (!audio_parse_format(argv[++i], dummy_mp3, wav_fmt)) {
|
||||
fprintf(stderr, "[MP3-Codec] Unknown format: %s\n", argv[i]);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "[MP3-Codec] Unknown option: %s\n", argv[i]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input || !output) {
|
||||
fprintf(stderr, "[MP3-Codec] Both -i and -o are required\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// read input (WAV or MP3, auto-detected)
|
||||
int T = 0, sr = 0;
|
||||
float * audio = audio_read(input, &T, &sr);
|
||||
if (!audio) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// write output (WAV or MP3, auto-detected from extension)
|
||||
bool ok;
|
||||
if (ends_with(output, ".mp3")) {
|
||||
ok = audio_write_mp3(output, audio, T, sr, bitrate);
|
||||
} else if (ends_with(output, ".wav")) {
|
||||
ok = audio_write_wav(output, audio, T, sr, wav_fmt);
|
||||
} else {
|
||||
fprintf(stderr, "[MP3-Codec] Cannot determine format from output extension\n");
|
||||
fprintf(stderr, " use .mp3 for encoding, .wav for decoding\n");
|
||||
free(audio);
|
||||
return 1;
|
||||
}
|
||||
|
||||
free(audio);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
// neural-codec.cpp: neural audio codec (Oobleck VAE)
|
||||
//
|
||||
// 2 modes:
|
||||
// encode: WAV -> latent file (f32, Q8, or Q4)
|
||||
// decode: latent file -> WAV (48kHz stereo)
|
||||
//
|
||||
// Three latent formats, decode auto-detects:
|
||||
//
|
||||
// f32 (default): flat [T, 64] f32, no header.
|
||||
// T = file_size / 256. 25Hz, ~6.4 KB/s, ~51 kbit/s.
|
||||
//
|
||||
// Q8 (--q8): symmetric per-frame int8 quantization.
|
||||
// header: "NAC8" magic (4B) + uint32 T_latent (4B)
|
||||
// frame: f16 scale (2B) + int8[64] (64B) = 66B
|
||||
// 25Hz, ~1.6 KB/s, ~13 kbit/s.
|
||||
//
|
||||
// Q4 (--q4): symmetric per-frame 4-bit quantization.
|
||||
// header: "NAC4" magic (4B) + uint32 T_latent (4B)
|
||||
// frame: f16 scale (2B) + nibbles[32] (32B) = 34B
|
||||
// 25Hz, ~850 B/s, ~6.8 kbit/s.
|
||||
//
|
||||
// Usage:
|
||||
// neural-codec --vae model.gguf --encode -i song.wav -o song.latent
|
||||
// neural-codec --vae model.gguf --encode --q8 -i song.wav -o song.nac8
|
||||
// neural-codec --vae model.gguf --encode --q4 -i song.wav -o song.nac4
|
||||
// neural-codec --vae model.gguf --decode -i song.nac4 -o song.wav
|
||||
|
||||
#include "audio-io.h"
|
||||
#include "vae-enc.h"
|
||||
#include "vae.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Q8 format constants
|
||||
static const char NAC8_MAGIC[4] = { 'N', 'A', 'C', '8' };
|
||||
static const int NAC8_HEADER = 8; // 4B magic + 4B T_latent
|
||||
static const int NAC8_FRAME = 66; // 2B f16 scale + 64B int8
|
||||
|
||||
// Write Q8 quantized latent
|
||||
static bool write_latent_q8(const char * path, const float * data, int T_latent) {
|
||||
FILE * f = fopen(path, "wb");
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fwrite(NAC8_MAGIC, 1, 4, f);
|
||||
uint32_t t = (uint32_t) T_latent;
|
||||
fwrite(&t, 4, 1, f);
|
||||
|
||||
for (int i = 0; i < T_latent; i++) {
|
||||
const float * frame = data + i * 64;
|
||||
|
||||
// find max abs for symmetric quant
|
||||
float amax = 0.0f;
|
||||
for (int j = 0; j < 64; j++) {
|
||||
float a = fabsf(frame[j]);
|
||||
if (a > amax) {
|
||||
amax = a;
|
||||
}
|
||||
}
|
||||
float scale = amax / 127.0f;
|
||||
ggml_fp16_t scale_f16 = ggml_fp32_to_fp16(scale);
|
||||
fwrite(&scale_f16, 2, 1, f);
|
||||
|
||||
// quantize
|
||||
int8_t q[64];
|
||||
float inv = (scale > 0.0f) ? 127.0f / amax : 0.0f;
|
||||
for (int j = 0; j < 64; j++) {
|
||||
int v = (int) roundf(frame[j] * inv);
|
||||
q[j] = (int8_t) (v < -127 ? -127 : (v > 127 ? 127 : v));
|
||||
}
|
||||
fwrite(q, 1, 64, f);
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
size_t bytes = NAC8_HEADER + (size_t) T_latent * NAC8_FRAME;
|
||||
float duration = (float) T_latent * 1920.0f / 48000.0f;
|
||||
float kbps = (float) bytes * 8.0f / (duration * 1000.0f);
|
||||
fprintf(stderr, "[Latent] Wrote %s: Q8, %d frames (%.2fs, %.1f KB, %.1f kbit/s)\n", path, T_latent, duration,
|
||||
(float) bytes / 1024.0f, kbps);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Q4 format constants
|
||||
static const char NAC4_MAGIC[4] = { 'N', 'A', 'C', '4' };
|
||||
static const int NAC4_HEADER = 8; // 4B magic + 4B T_latent
|
||||
static const int NAC4_FRAME = 34; // 2B f16 scale + 32B packed nibbles
|
||||
|
||||
// Write Q4 quantized latent
|
||||
// Symmetric 4-bit: range [-7, 7], scale = amax / 7.0
|
||||
// Packing: byte = (low & 0x0F) | (high << 4), two signed nibbles per byte
|
||||
static bool write_latent_q4(const char * path, const float * data, int T_latent) {
|
||||
FILE * f = fopen(path, "wb");
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fwrite(NAC4_MAGIC, 1, 4, f);
|
||||
uint32_t t = (uint32_t) T_latent;
|
||||
fwrite(&t, 4, 1, f);
|
||||
|
||||
for (int i = 0; i < T_latent; i++) {
|
||||
const float * frame = data + i * 64;
|
||||
|
||||
// find max abs for symmetric quant
|
||||
float amax = 0.0f;
|
||||
for (int j = 0; j < 64; j++) {
|
||||
float a = fabsf(frame[j]);
|
||||
if (a > amax) {
|
||||
amax = a;
|
||||
}
|
||||
}
|
||||
float scale = amax / 7.0f;
|
||||
ggml_fp16_t scale_f16 = ggml_fp32_to_fp16(scale);
|
||||
fwrite(&scale_f16, 2, 1, f);
|
||||
|
||||
// quantize and pack pairs into bytes
|
||||
float inv = (scale > 0.0f) ? 7.0f / amax : 0.0f;
|
||||
uint8_t packed[32];
|
||||
for (int j = 0; j < 32; j++) {
|
||||
int lo = (int) roundf(frame[j * 2 + 0] * inv);
|
||||
int hi = (int) roundf(frame[j * 2 + 1] * inv);
|
||||
lo = lo < -7 ? -7 : (lo > 7 ? 7 : lo);
|
||||
hi = hi < -7 ? -7 : (hi > 7 ? 7 : hi);
|
||||
packed[j] = (uint8_t) ((lo & 0x0F) | (hi << 4));
|
||||
}
|
||||
fwrite(packed, 1, 32, f);
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
size_t bytes = NAC4_HEADER + (size_t) T_latent * NAC4_FRAME;
|
||||
float duration = (float) T_latent * 1920.0f / 48000.0f;
|
||||
float kbps = (float) bytes * 8.0f / (duration * 1000.0f);
|
||||
fprintf(stderr, "[Latent] Wrote %s: Q4, %d frames (%.2fs, %.1f KB, %.1f kbit/s)\n", path, T_latent, duration,
|
||||
(float) bytes / 1024.0f, kbps);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Write f32 raw latent (no header)
|
||||
static bool write_latent_f32(const char * path, const float * data, int T_latent) {
|
||||
FILE * f = fopen(path, "wb");
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
size_t bytes = (size_t) T_latent * 64 * sizeof(float);
|
||||
fwrite(data, 1, bytes, f);
|
||||
fclose(f);
|
||||
float duration = (float) T_latent * 1920.0f / 48000.0f;
|
||||
fprintf(stderr, "[Latent] Wrote %s: f32, %d frames (%.2fs, %.1f KB, %.1f kbit/s)\n", path, T_latent, duration,
|
||||
(float) bytes / 1024.0f, (float) bytes * 8.0f / (duration * 1000.0f));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read latent, auto-detect format (NAC8 -> Q8, NAC4 -> Q4, else f32).
|
||||
// Returns [T_latent, 64] f32 (dequantized if quantized). Caller frees.
|
||||
static float * read_latent(const char * path, int * T_latent) {
|
||||
FILE * f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "[Latent] Cannot open %s\n", path);
|
||||
return NULL;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long fsize = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
// Check magic
|
||||
char magic[4] = {};
|
||||
if (fsize >= 8) {
|
||||
if (fread(magic, 1, 4, f) != 4) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (memcmp(magic, NAC8_MAGIC, 4) == 0) {
|
||||
// Q8 format
|
||||
uint32_t t;
|
||||
if (fread(&t, 4, 1, f) != 1) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
*T_latent = (int) t;
|
||||
|
||||
long expected = NAC8_HEADER + (long) t * NAC8_FRAME;
|
||||
if (fsize != expected) {
|
||||
fprintf(stderr, "[Latent] Q8 size mismatch: expected %ld, got %ld\n", expected, fsize);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
float * data = (float *) malloc((size_t) t * 64 * sizeof(float));
|
||||
for (int i = 0; i < (int) t; i++) {
|
||||
ggml_fp16_t scale_f16;
|
||||
if (fread(&scale_f16, 2, 1, f) != 1) {
|
||||
fclose(f);
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
float scale = ggml_fp16_to_fp32(scale_f16);
|
||||
|
||||
int8_t q[64];
|
||||
if (fread(q, 1, 64, f) != 64) {
|
||||
fclose(f);
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
float * frame = data + i * 64;
|
||||
for (int j = 0; j < 64; j++) {
|
||||
frame[j] = (float) q[j] * scale;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
float duration = (float) (*T_latent) * 1920.0f / 48000.0f;
|
||||
float kbps = (float) fsize * 8.0f / (duration * 1000.0f);
|
||||
fprintf(stderr, "[Latent] Read %s: Q8, %d frames (%.2fs, %.1f KB, %.1f kbit/s)\n", path, *T_latent, duration,
|
||||
(float) fsize / 1024.0f, kbps);
|
||||
return data;
|
||||
}
|
||||
|
||||
if (memcmp(magic, NAC4_MAGIC, 4) == 0) {
|
||||
// Q4 format
|
||||
uint32_t t;
|
||||
if (fread(&t, 4, 1, f) != 1) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
*T_latent = (int) t;
|
||||
|
||||
long expected = NAC4_HEADER + (long) t * NAC4_FRAME;
|
||||
if (fsize != expected) {
|
||||
fprintf(stderr, "[Latent] Q4 size mismatch: expected %ld, got %ld\n", expected, fsize);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
float * data = (float *) malloc((size_t) t * 64 * sizeof(float));
|
||||
for (int i = 0; i < (int) t; i++) {
|
||||
ggml_fp16_t scale_f16;
|
||||
if (fread(&scale_f16, 2, 1, f) != 1) {
|
||||
fclose(f);
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
float scale = ggml_fp16_to_fp32(scale_f16);
|
||||
|
||||
uint8_t packed[32];
|
||||
if (fread(packed, 1, 32, f) != 32) {
|
||||
fclose(f);
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// unpack signed nibbles
|
||||
float * frame = data + i * 64;
|
||||
for (int j = 0; j < 32; j++) {
|
||||
int lo = (int) (packed[j] & 0x0F);
|
||||
int hi = (int) (packed[j] >> 4);
|
||||
if (lo >= 8) {
|
||||
lo -= 16;
|
||||
}
|
||||
if (hi >= 8) {
|
||||
hi -= 16;
|
||||
}
|
||||
frame[j * 2 + 0] = (float) lo * scale;
|
||||
frame[j * 2 + 1] = (float) hi * scale;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
float duration = (float) (*T_latent) * 1920.0f / 48000.0f;
|
||||
float kbps = (float) fsize * 8.0f / (duration * 1000.0f);
|
||||
fprintf(stderr, "[Latent] Read %s: Q4, %d frames (%.2fs, %.1f KB, %.1f kbit/s)\n", path, *T_latent, duration,
|
||||
(float) fsize / 1024.0f, kbps);
|
||||
return data;
|
||||
}
|
||||
|
||||
// f32 format (no header, rewind)
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (fsize % (64 * (int) sizeof(float)) != 0) {
|
||||
fprintf(stderr, "[Latent] File size %ld not a multiple of %d (64 * f32)\n", fsize, (int) (64 * sizeof(float)));
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*T_latent = (int) (fsize / (64 * sizeof(float)));
|
||||
float * data = (float *) malloc(fsize);
|
||||
if (fread(data, 1, fsize, f) != (size_t) fsize) {
|
||||
fclose(f);
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
float duration = (float) (*T_latent) * 1920.0f / 48000.0f;
|
||||
fprintf(stderr, "[Latent] Read %s: f32, %d frames (%.2fs, %.1f KB, %.1f kbit/s)\n", path, *T_latent, duration,
|
||||
(float) fsize / 1024.0f, (float) fsize * 8.0f / (duration * 1000.0f));
|
||||
return data;
|
||||
}
|
||||
|
||||
static void print_usage(const char * prog) {
|
||||
fprintf(stderr, "acestep.cpp %s\n\n", ACE_VERSION);
|
||||
fprintf(stderr,
|
||||
"Usage: %s --vae <gguf> --encode|--decode -i <input> [-o <output>] [--q8|--q4]\n\n"
|
||||
"Required:\n"
|
||||
" --vae <path> VAE GGUF file\n"
|
||||
" --encode | --decode Encode audio to latent, or decode latent to WAV\n"
|
||||
" -i <path> Input (WAV/MP3 for encode, latent for decode)\n\n"
|
||||
"Output:\n"
|
||||
" -o <path> Output file (auto-named if omitted)\n"
|
||||
" --q8 Quantize latent to int8 (~13 kbit/s)\n"
|
||||
" --q4 Quantize latent to int4 (~6.8 kbit/s)\n"
|
||||
" --format <fmt> WAV format: wav16, wav24, wav32 (default: wav16)\n\n"
|
||||
"Output naming: song.wav -> song.latent (f32) or song.nac8 (Q8) or song.nac4 (Q4)\n"
|
||||
" song.latent -> song.wav\n\n"
|
||||
"Memory control:\n"
|
||||
" --vae-chunk <N> Latent frames per tile (default: 256)\n"
|
||||
" --vae-overlap <N> Overlap frames per side (default: 64)\n\n"
|
||||
"Latent formats (decode auto-detects):\n"
|
||||
" f32: flat [T, 64] f32, no header. ~51 kbit/s.\n"
|
||||
" NAC8: header + per-frame Q8. ~13 kbit/s.\n"
|
||||
" NAC4: header + per-frame Q4. ~6.8 kbit/s.\n",
|
||||
prog);
|
||||
}
|
||||
|
||||
static std::string auto_output(const char * input, const char * ext) {
|
||||
std::string s = input;
|
||||
size_t dot = s.rfind('.');
|
||||
if (dot != std::string::npos) {
|
||||
return s.substr(0, dot) + ext;
|
||||
}
|
||||
return s + ext;
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
const char * vae_path = NULL;
|
||||
const char * input_path = NULL;
|
||||
const char * output_path = NULL;
|
||||
int chunk_size = 256;
|
||||
int overlap = 64;
|
||||
int mode = -1; // 0 = encode, 1 = decode
|
||||
int quant = 0; // 0 = f32, 8 = q8, 4 = q4
|
||||
WavFormat wav_fmt = WAV_S16;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--vae") == 0 && i + 1 < argc) {
|
||||
vae_path = argv[++i];
|
||||
} else if (strcmp(argv[i], "-i") == 0 && i + 1 < argc) {
|
||||
input_path = argv[++i];
|
||||
} else if (strcmp(argv[i], "--input") == 0 && i + 1 < argc) {
|
||||
input_path = argv[++i];
|
||||
} else if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
|
||||
output_path = argv[++i];
|
||||
} else if (strcmp(argv[i], "--output") == 0 && i + 1 < argc) {
|
||||
output_path = argv[++i];
|
||||
} else if (strcmp(argv[i], "--format") == 0 && i + 1 < argc) {
|
||||
bool dummy_mp3;
|
||||
if (!audio_parse_format(argv[++i], dummy_mp3, wav_fmt)) {
|
||||
fprintf(stderr, "Unknown format: %s\n", argv[i]);
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
} else if (strcmp(argv[i], "--vae-chunk") == 0 && i + 1 < argc) {
|
||||
chunk_size = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--vae-overlap") == 0 && i + 1 < argc) {
|
||||
overlap = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--encode") == 0) {
|
||||
mode = 0;
|
||||
} else if (strcmp(argv[i], "--decode") == 0) {
|
||||
mode = 1;
|
||||
} else if (strcmp(argv[i], "--q8") == 0) {
|
||||
quant = 8;
|
||||
} else if (strcmp(argv[i], "--q4") == 0) {
|
||||
quant = 4;
|
||||
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Unknown arg: %s\n", argv[i]);
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!vae_path || !input_path || mode < 0) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Auto output names
|
||||
std::string out_str;
|
||||
if (!output_path) {
|
||||
if (mode == 0) {
|
||||
const char * ext = ".latent";
|
||||
if (quant == 8) {
|
||||
ext = ".nac8";
|
||||
}
|
||||
if (quant == 4) {
|
||||
ext = ".nac4";
|
||||
}
|
||||
out_str = auto_output(input_path, ext);
|
||||
} else {
|
||||
out_str = auto_output(input_path, ".wav");
|
||||
}
|
||||
output_path = out_str.c_str();
|
||||
}
|
||||
|
||||
const char * quant_str = "";
|
||||
if (mode == 0 && quant == 8) {
|
||||
quant_str = " (Q8)";
|
||||
}
|
||||
if (mode == 0 && quant == 4) {
|
||||
quant_str = " (Q4)";
|
||||
}
|
||||
fprintf(stderr, "\n[VAE] Mode: %s%s\n", mode == 0 ? "encode" : "decode", quant_str);
|
||||
fprintf(stderr, "[VAE] Input: %s\n", input_path);
|
||||
fprintf(stderr, "[VAE] Output: %s\n\n", output_path);
|
||||
|
||||
// ENCODE
|
||||
if (mode == 0) {
|
||||
int T_audio = 0;
|
||||
float * planar = audio_read_48k(input_path, &T_audio);
|
||||
if (!planar) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
float * audio = audio_planar_to_interleaved(planar, T_audio);
|
||||
free(planar);
|
||||
|
||||
VAEEncoder enc = {};
|
||||
vae_enc_load(&enc, vae_path);
|
||||
|
||||
int max_T = (T_audio / 1920) + 64;
|
||||
std::vector<float> latent((size_t) max_T * 64);
|
||||
|
||||
fprintf(stderr, "\n[VAE] Encoding %d samples (%.2fs)...\n", T_audio, (float) T_audio / 48000.0f);
|
||||
int T_latent = vae_enc_encode_tiled(&enc, audio, T_audio, latent.data(), max_T, chunk_size, overlap);
|
||||
free(audio);
|
||||
if (T_latent < 0) {
|
||||
vae_enc_free(&enc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (quant == 8) {
|
||||
write_latent_q8(output_path, latent.data(), T_latent);
|
||||
} else if (quant == 4) {
|
||||
write_latent_q4(output_path, latent.data(), T_latent);
|
||||
} else {
|
||||
write_latent_f32(output_path, latent.data(), T_latent);
|
||||
}
|
||||
|
||||
vae_enc_free(&enc);
|
||||
fprintf(stderr, "[VAE] Done.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// DECODE (auto-detects f32 vs Q8 vs Q4 from file content)
|
||||
{
|
||||
int T_latent = 0;
|
||||
float * latent = read_latent(input_path, &T_latent);
|
||||
if (!latent) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
VAEGGML dec = {};
|
||||
vae_ggml_load(&dec, vae_path);
|
||||
|
||||
int max_T = T_latent * 1920 + 4096;
|
||||
std::vector<float> audio((size_t) 2 * max_T, 0.0f);
|
||||
|
||||
fprintf(stderr, "\n[VAE] Decoding %d latent frames...\n", T_latent);
|
||||
int T_audio = vae_ggml_decode_tiled(&dec, latent, T_latent, audio.data(), max_T, chunk_size, overlap);
|
||||
free(latent);
|
||||
if (T_audio < 0) {
|
||||
vae_ggml_free(&dec);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (audio_write(output_path, audio.data(), T_audio, 48000, 0, wav_fmt)) {
|
||||
fprintf(stderr, "\n[VAE] Output: %s (%d samples, %.2fs @ 48kHz)\n", output_path, T_audio,
|
||||
(float) T_audio / 48000.0f);
|
||||
} else {
|
||||
fprintf(stderr, "[VAE] FATAL: failed to write %s\n", output_path);
|
||||
}
|
||||
|
||||
vae_ggml_free(&dec);
|
||||
fprintf(stderr, "[VAE] Done.\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,419 @@
|
||||
// quantize.cpp : GGUF requantizer for ACE-Step
|
||||
// Reads BF16 GGUF, writes quantized GGUF with mixed-precision K-quant policy.
|
||||
// Policy mirrors llama-quantize: important tensors (v_proj, down_proj) get
|
||||
// bumped in S/M variants, embed_tokens always Q6_K, norms promoted to F32.
|
||||
// Streaming write: one tensor at a time, low memory footprint for small configs.
|
||||
//
|
||||
// Usage: quantize <input.gguf> <output.gguf> <type>
|
||||
// Types: Q2_K Q3_K_S Q3_K_M Q3_K_L Q4_K_S Q4_K_M Q5_K_S Q5_K_M Q6_K Q8_0 NVFP4 MXFP4
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
# include <windows.h>
|
||||
# define strcasecmp _stricmp
|
||||
#else
|
||||
# include <fcntl.h>
|
||||
# include <sys/mman.h>
|
||||
# include <sys/stat.h>
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "ggml.h"
|
||||
#include "gguf.h"
|
||||
#include "version.h"
|
||||
|
||||
// Quant variant: base type + optional bump rules for important tensors
|
||||
struct QuantVariant {
|
||||
const char * name;
|
||||
enum ggml_type base;
|
||||
enum ggml_type bump; // type for "important" tensors (or COUNT = no bump)
|
||||
enum ggml_type embed; // type for embed_tokens (or COUNT = same as base)
|
||||
// bump_mode: 0=none, 1=first N layers, 2=first+last+every 3rd, 3=all important
|
||||
int bump_mode;
|
||||
int bump_n; // for mode 1: number of layers to bump
|
||||
};
|
||||
|
||||
static const QuantVariant VARIANTS[] = {
|
||||
// name base bump embed mode n
|
||||
{ "Q2_K", GGML_TYPE_Q2_K, GGML_TYPE_Q4_K, GGML_TYPE_Q6_K, 1, 4 },
|
||||
{ "Q3_K_S", GGML_TYPE_Q3_K, GGML_TYPE_COUNT, GGML_TYPE_Q6_K, 0, 0 },
|
||||
{ "Q3_K_M", GGML_TYPE_Q3_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, 2, 0 },
|
||||
{ "Q3_K_L", GGML_TYPE_Q3_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, 3, 0 },
|
||||
{ "Q4_K_S", GGML_TYPE_Q4_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, 1, 4 },
|
||||
{ "Q4_K_M", GGML_TYPE_Q4_K, GGML_TYPE_Q6_K, GGML_TYPE_Q6_K, 2, 0 },
|
||||
{ "Q5_K_S", GGML_TYPE_Q5_K, GGML_TYPE_COUNT, GGML_TYPE_Q6_K, 0, 0 },
|
||||
{ "Q5_K_M", GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, GGML_TYPE_Q6_K, 2, 0 },
|
||||
{ "Q6_K", GGML_TYPE_Q6_K, GGML_TYPE_COUNT, GGML_TYPE_Q6_K, 0, 0 },
|
||||
{ "Q8_0", GGML_TYPE_Q8_0, GGML_TYPE_COUNT, GGML_TYPE_Q8_0, 0, 0 },
|
||||
{ "NVFP4", GGML_TYPE_NVFP4, GGML_TYPE_COUNT, GGML_TYPE_Q8_0, 0, 0 },
|
||||
{ "MXFP4", GGML_TYPE_MXFP4, GGML_TYPE_COUNT, GGML_TYPE_Q8_0, 0, 0 },
|
||||
};
|
||||
|
||||
static const QuantVariant * find_variant(const char * s) {
|
||||
for (const auto & v : VARIANTS) {
|
||||
if (strcasecmp(s, v.name) == 0) {
|
||||
return &v;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Extract layer index from HF tensor name: model.layers.N.xxx -> N, else -1
|
||||
static int extract_layer(const char * name) {
|
||||
const char * p = strstr(name, "layers.");
|
||||
if (!p) {
|
||||
return -1;
|
||||
}
|
||||
return atoi(p + 7);
|
||||
}
|
||||
|
||||
// Important tensors for S/M: v_proj + down_proj
|
||||
static bool is_important_sm(const char * name) {
|
||||
return (strstr(name, "v_proj.weight") != nullptr) || (strstr(name, "down_proj.weight") != nullptr);
|
||||
}
|
||||
|
||||
// Important tensors for L: v_proj + down_proj + o_proj
|
||||
static bool is_important_l(const char * name) {
|
||||
return is_important_sm(name) || (strstr(name, "o_proj.weight") != nullptr);
|
||||
}
|
||||
|
||||
static bool is_embed(const char * name) {
|
||||
return strstr(name, "embed_tokens.weight") != nullptr;
|
||||
}
|
||||
|
||||
// Should this tensor be quantized at all?
|
||||
static bool should_quantize(const char * name, int n_dims, const char * arch) {
|
||||
if (strstr(arch, "vae")) {
|
||||
return false;
|
||||
}
|
||||
if (n_dims < 2) {
|
||||
return false;
|
||||
}
|
||||
if (strstr(arch, "text-enc") && strstr(name, "embed_tokens")) {
|
||||
return false;
|
||||
}
|
||||
if (strstr(name, "silence_latent")) {
|
||||
return false;
|
||||
}
|
||||
if (strstr(name, "scale_shift_table")) {
|
||||
return false;
|
||||
}
|
||||
if (strstr(name, "null_condition_emb")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decide target type for a single tensor given the variant + layer info
|
||||
static enum ggml_type pick_type(const char * name,
|
||||
int n_dims,
|
||||
const char * arch,
|
||||
const QuantVariant & v,
|
||||
int n_layers) {
|
||||
if (!should_quantize(name, n_dims, arch)) {
|
||||
return GGML_TYPE_COUNT;
|
||||
}
|
||||
|
||||
// embed_tokens in LM: use embed type
|
||||
if (is_embed(name) && !strstr(arch, "text-enc")) {
|
||||
return (v.embed != GGML_TYPE_COUNT) ? v.embed : v.base;
|
||||
}
|
||||
|
||||
// Important tensor bump logic
|
||||
bool important = (v.bump_mode == 3) ? is_important_l(name) : is_important_sm(name);
|
||||
|
||||
if (important && v.bump != GGML_TYPE_COUNT) {
|
||||
int layer = extract_layer(name);
|
||||
bool bumped = false;
|
||||
switch (v.bump_mode) {
|
||||
case 1: // first N layers only
|
||||
bumped = (layer >= 0 && layer < v.bump_n);
|
||||
break;
|
||||
case 2:
|
||||
{ // M variant: first few + last few + every 3rd
|
||||
int ql = n_layers;
|
||||
bumped = (layer >= 0) && (layer < ql / 9 || layer >= ql - ql / 7 || layer % 3 == 0);
|
||||
break;
|
||||
}
|
||||
case 3: // L variant: all important tensors (v+down+o_proj)
|
||||
bumped = true;
|
||||
break;
|
||||
}
|
||||
if (bumped) {
|
||||
return v.bump;
|
||||
}
|
||||
}
|
||||
|
||||
return v.base;
|
||||
}
|
||||
|
||||
// Promote 1D tensors (norms/biases) to F32 for precision
|
||||
static bool should_promote_f32(int n_dims) {
|
||||
return n_dims < 2;
|
||||
}
|
||||
|
||||
// Convert source data to F32
|
||||
static bool to_f32(const void * src, float * dst, int64_t n, enum ggml_type type) {
|
||||
switch (type) {
|
||||
case GGML_TYPE_BF16:
|
||||
ggml_bf16_to_fp32_row((const ggml_bf16_t *) src, dst, n);
|
||||
return true;
|
||||
case GGML_TYPE_F16:
|
||||
ggml_fp16_to_fp32_row((const ggml_fp16_t *) src, dst, n);
|
||||
return true;
|
||||
case GGML_TYPE_F32:
|
||||
memcpy(dst, src, (size_t) n * sizeof(float));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
if (argc != 4) {
|
||||
fprintf(stderr, "acestep.cpp %s\n\n", ACE_VERSION);
|
||||
fprintf(stderr, "Usage: %s <input.gguf> <output.gguf> <type>\n", argv[0]);
|
||||
fprintf(stderr, "Types:");
|
||||
for (const auto & v : VARIANTS) {
|
||||
fprintf(stderr, " %s", v.name);
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char * inp_path = argv[1];
|
||||
const char * out_path = argv[2];
|
||||
const QuantVariant * variant = find_variant(argv[3]);
|
||||
|
||||
if (!variant) {
|
||||
fprintf(stderr, "[Quantize] Unknown type: %s\n", argv[3]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[Quantize] %s -> %s (%s)\n", inp_path, out_path, variant->name);
|
||||
|
||||
// Mmap input file
|
||||
#ifdef _WIN32
|
||||
HANDLE fh = CreateFileA(inp_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (fh == INVALID_HANDLE_VALUE) {
|
||||
fprintf(stderr, "[Quantize] Failed to open %s\n", inp_path);
|
||||
return 1;
|
||||
}
|
||||
HANDLE mh = CreateFileMappingA(fh, NULL, PAGE_READONLY, 0, 0, NULL);
|
||||
if (!mh) {
|
||||
fprintf(stderr, "[Quantize] CreateFileMapping failed %s\n", inp_path);
|
||||
CloseHandle(fh);
|
||||
return 1;
|
||||
}
|
||||
void * mapping = MapViewOfFile(mh, FILE_MAP_READ, 0, 0, 0);
|
||||
if (!mapping) {
|
||||
fprintf(stderr, "[Quantize] MapViewOfFile failed %s\n", inp_path);
|
||||
CloseHandle(mh);
|
||||
CloseHandle(fh);
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
int fd = open(inp_path, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
perror("open");
|
||||
return 1;
|
||||
}
|
||||
struct stat st;
|
||||
fstat(fd, &st);
|
||||
size_t file_size = (size_t) st.st_size;
|
||||
void * mapping = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (mapping == MAP_FAILED) {
|
||||
perror("mmap");
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Parse input GGUF
|
||||
struct gguf_init_params params = { /*no_alloc=*/true, /*ctx=*/nullptr };
|
||||
struct ggml_context * meta = nullptr;
|
||||
params.ctx = &meta;
|
||||
|
||||
struct gguf_context * inp = gguf_init_from_file(inp_path, params);
|
||||
if (!inp) {
|
||||
fprintf(stderr, "[Quantize] Failed to read %s\n", inp_path);
|
||||
#ifdef _WIN32
|
||||
UnmapViewOfFile(mapping);
|
||||
CloseHandle(mh);
|
||||
CloseHandle(fh);
|
||||
#else
|
||||
munmap(mapping, file_size);
|
||||
close(fd);
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
|
||||
const size_t data_off = gguf_get_data_offset(inp);
|
||||
const int n_tensors = (int) gguf_get_n_tensors(inp);
|
||||
|
||||
// Read architecture
|
||||
char arch[64] = "unknown";
|
||||
{
|
||||
int64_t idx = gguf_find_key(inp, "general.architecture");
|
||||
if (idx >= 0) {
|
||||
const char * s = gguf_get_val_str(inp, (int) idx);
|
||||
snprintf(arch, sizeof(arch), "%s", s);
|
||||
}
|
||||
}
|
||||
|
||||
// Read block count for bump policy
|
||||
int n_layers = 0;
|
||||
{
|
||||
char key[128];
|
||||
snprintf(key, sizeof(key), "%s.block_count", arch);
|
||||
int64_t idx = gguf_find_key(inp, key);
|
||||
if (idx >= 0) {
|
||||
n_layers = (int) gguf_get_val_u32(inp, (int) idx);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "[Quantize] Arch=%s Layers=%d\n", arch, n_layers);
|
||||
|
||||
// Create output GGUF: copy KV metadata
|
||||
struct gguf_context * out = gguf_init_empty();
|
||||
gguf_set_kv(out, inp);
|
||||
gguf_set_val_u32(out, "general.quantization_version", 2);
|
||||
gguf_set_val_str(out, "general.file_type", variant->name);
|
||||
|
||||
// Plan: for each tensor, decide target type
|
||||
struct TensorPlan {
|
||||
enum ggml_type target;
|
||||
bool quantize;
|
||||
bool promote;
|
||||
};
|
||||
|
||||
std::vector<TensorPlan> plans((size_t) n_tensors);
|
||||
|
||||
for (int i = 0; i < n_tensors; i++) {
|
||||
const char * name = gguf_get_tensor_name(inp, i);
|
||||
struct ggml_tensor * t = ggml_get_tensor(meta, name);
|
||||
const int n_dims = ggml_n_dims(t);
|
||||
|
||||
gguf_add_tensor(out, t);
|
||||
plans[(size_t) i] = { GGML_TYPE_COUNT, false, false };
|
||||
|
||||
enum ggml_type target = pick_type(name, n_dims, arch, *variant, n_layers);
|
||||
|
||||
// Promote 1D norms/biases BF16/F16 -> F32
|
||||
if (target == GGML_TYPE_COUNT && should_promote_f32(n_dims) &&
|
||||
(t->type == GGML_TYPE_BF16 || t->type == GGML_TYPE_F16)) {
|
||||
gguf_set_tensor_type(out, name, GGML_TYPE_F32);
|
||||
plans[(size_t) i] = { GGML_TYPE_F32, false, true };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target == GGML_TYPE_COUNT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool can_convert = (t->type == GGML_TYPE_BF16 || t->type == GGML_TYPE_F16 || t->type == GGML_TYPE_F32);
|
||||
bool aligned = (t->ne[0] % ggml_blck_size(target) == 0);
|
||||
|
||||
if (can_convert && aligned) {
|
||||
gguf_set_tensor_type(out, name, target);
|
||||
plans[(size_t) i] = { target, true, false };
|
||||
}
|
||||
}
|
||||
|
||||
// Write metadata only (header + tensor info, no data)
|
||||
bool ok = gguf_write_to_file(out, out_path, true);
|
||||
if (!ok) {
|
||||
fprintf(stderr, "[Quantize] Failed to write metadata %s\n", out_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Stream tensor data one at a time (low memory)
|
||||
FILE * fout = fopen(out_path, "ab");
|
||||
if (!fout) {
|
||||
fprintf(stderr, "[Quantize] Failed to open %s for append\n", out_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const size_t alignment = gguf_get_alignment(out);
|
||||
int n_quantized = 0, n_promoted = 0;
|
||||
int64_t bytes_in = 0, bytes_out = 0;
|
||||
size_t data_pos = 0;
|
||||
|
||||
for (int i = 0; i < n_tensors; i++) {
|
||||
const char * name = gguf_get_tensor_name(inp, i);
|
||||
struct ggml_tensor * t = ggml_get_tensor(meta, name);
|
||||
const int64_t nel = ggml_nelements(t);
|
||||
const size_t src_size = ggml_nbytes(t);
|
||||
const size_t t_off = gguf_get_tensor_offset(inp, i);
|
||||
const void * src = (const uint8_t *) mapping + data_off + t_off;
|
||||
|
||||
bytes_in += (int64_t) src_size;
|
||||
|
||||
// Pad to alignment boundary
|
||||
size_t pad = (alignment - (data_pos % alignment)) % alignment;
|
||||
if (pad > 0) {
|
||||
uint8_t zeros[64] = {};
|
||||
fwrite(zeros, 1, pad, fout);
|
||||
data_pos += pad;
|
||||
}
|
||||
|
||||
const TensorPlan & plan = plans[(size_t) i];
|
||||
|
||||
if (plan.promote) {
|
||||
// BF16/F16 -> F32
|
||||
std::vector<float> f32((size_t) nel);
|
||||
to_f32(src, f32.data(), nel, t->type);
|
||||
size_t out_size = (size_t) nel * sizeof(float);
|
||||
fwrite(f32.data(), 1, out_size, fout);
|
||||
data_pos += out_size;
|
||||
bytes_out += (int64_t) out_size;
|
||||
n_promoted++;
|
||||
} else if (plan.quantize) {
|
||||
// Quantize: src -> f32 -> target
|
||||
std::vector<float> f32((size_t) nel);
|
||||
to_f32(src, f32.data(), nel, t->type);
|
||||
|
||||
const int64_t n_per_row = t->ne[0];
|
||||
const int64_t nrows = nel / n_per_row;
|
||||
const size_t qsize = ggml_row_size(plan.target, n_per_row) * (size_t) nrows;
|
||||
|
||||
std::vector<uint8_t> qbuf(qsize);
|
||||
ggml_quantize_chunk(plan.target, f32.data(), qbuf.data(), 0, nrows, n_per_row, nullptr);
|
||||
|
||||
fwrite(qbuf.data(), 1, qsize, fout);
|
||||
data_pos += qsize;
|
||||
bytes_out += (int64_t) qsize;
|
||||
n_quantized++;
|
||||
} else {
|
||||
// Keep as-is
|
||||
fwrite(src, 1, src_size, fout);
|
||||
data_pos += src_size;
|
||||
bytes_out += (int64_t) src_size;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fout);
|
||||
|
||||
fprintf(stderr, "[Quantize] Quantized %d/%d tensors, promoted %d to F32\n", n_quantized, n_tensors, n_promoted);
|
||||
fprintf(stderr, "[Quantize] %.1f GB -> %.1f GB (%.1fx)\n", (double) bytes_in / 1e9, (double) bytes_out / 1e9,
|
||||
bytes_out > 0 ? (double) bytes_in / (double) bytes_out : 0.0);
|
||||
fprintf(stderr, "[Quantize] Wrote %s\n", out_path);
|
||||
|
||||
gguf_free(out);
|
||||
gguf_free(inp);
|
||||
ggml_free(meta);
|
||||
#ifdef _WIN32
|
||||
UnmapViewOfFile(mapping);
|
||||
CloseHandle(mh);
|
||||
CloseHandle(fh);
|
||||
#else
|
||||
munmap(mapping, file_size);
|
||||
close(fd);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
// sa3-ggml-test.cpp: parity test CLI for the StableStep GGML SA3 modules.
|
||||
//
|
||||
// Runs GGML ports of the SA3 conditioning modules against golden vectors
|
||||
// dumped from the validated ONNX graphs (see tools/onnx-export/), and reports
|
||||
// cosine similarity + max abs diff per component.
|
||||
//
|
||||
// Usage:
|
||||
// sa3-ggml-test --models <dir-with-sa3-*.gguf> --goldens <dir-with-manifest.json>
|
||||
// [--component text_enc|seconds|same_enc|same_dec|dit|all]
|
||||
//
|
||||
// Components:
|
||||
// text_enc: sa3-text-enc-BF16.gguf, T5Gemma encoder + learned padding
|
||||
// substitution. Inputs input_ids [1,S] i64 + attention_mask [1,S]
|
||||
// u8, expected embeddings [1,S,768] f32.
|
||||
// seconds: sa3-dit-BF16.gguf (embedder tensors only), NumberConditioner
|
||||
// expo Fourier embedder. Input [1] f32, expected [1,768] f32.
|
||||
// same_enc: sa3-same-enc-F16.gguf, SAME-L encoder. Input audio
|
||||
// [1,2,524288] f32, expected latents [1,256,128] f32.
|
||||
// same_dec: sa3-same-dec-F16.gguf, SAME-L decoder. Input latents
|
||||
// [1,256,128] f32, expected audio [1,2,524288] f32.
|
||||
// dit: sa3-dit-BF16.gguf, DiffusionTransformer single forward. Inputs
|
||||
// x [1,256,T] f32, t [1] f32, cross_attn_cond [1,S,768] f32,
|
||||
// global_embed [1,768] f32, local_add_cond [1,257,T] f32,
|
||||
// padding_mask [1,T] u8; expected v [1,256,T] f32.
|
||||
//
|
||||
// Exit code 0 only if every run component passes cosine > 0.999.
|
||||
|
||||
#include "sa3-dit-ggml.h"
|
||||
#include "sa3-same-ggml.h"
|
||||
#include "sa3-t5gemma-enc.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static const double PASS_COSINE = 0.999;
|
||||
|
||||
static bool read_file(const std::string & path, std::vector<uint8_t> & out) {
|
||||
FILE * f = fopen(path.c_str(), "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "[Test] cannot open %s\n", path.c_str());
|
||||
return false;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
out.resize((size_t) sz);
|
||||
size_t rd = fread(out.data(), 1, (size_t) sz, f);
|
||||
fclose(f);
|
||||
if (rd != (size_t) sz) {
|
||||
fprintf(stderr, "[Test] short read on %s\n", path.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct Metrics {
|
||||
double cosine;
|
||||
double max_abs_diff;
|
||||
};
|
||||
|
||||
static Metrics compare(const float * a, const float * b, size_t n) {
|
||||
double dot = 0, na = 0, nb = 0, mad = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
dot += (double) a[i] * b[i];
|
||||
na += (double) a[i] * a[i];
|
||||
nb += (double) b[i] * b[i];
|
||||
double d = fabs((double) a[i] - b[i]);
|
||||
if (d > mad) {
|
||||
mad = d;
|
||||
}
|
||||
}
|
||||
Metrics m;
|
||||
m.cosine = (na > 0 && nb > 0) ? dot / (sqrt(na) * sqrt(nb)) : 0.0;
|
||||
m.max_abs_diff = mad;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Manifest helpers: get golden["<component>"]["inputs"/"outputs"]["<name>"]["file"]
|
||||
static std::string manifest_file(yyjson_val * root, const char * comp, const char * io, const char * name) {
|
||||
yyjson_val * c = yyjson_obj_get(root, comp);
|
||||
yyjson_val * g = c ? yyjson_obj_get(c, io) : NULL;
|
||||
yyjson_val * t = g ? yyjson_obj_get(g, name) : NULL;
|
||||
yyjson_val * f = t ? yyjson_obj_get(t, "file") : NULL;
|
||||
return (f && yyjson_is_str(f)) ? yyjson_get_str(f) : "";
|
||||
}
|
||||
|
||||
static int64_t manifest_shape_prod(yyjson_val * root, const char * comp, const char * io, const char * name) {
|
||||
yyjson_val * c = yyjson_obj_get(root, comp);
|
||||
yyjson_val * g = c ? yyjson_obj_get(c, io) : NULL;
|
||||
yyjson_val * t = g ? yyjson_obj_get(g, name) : NULL;
|
||||
yyjson_val * s = t ? yyjson_obj_get(t, "shape") : NULL;
|
||||
if (!s || !yyjson_is_arr(s)) {
|
||||
return 0;
|
||||
}
|
||||
int64_t prod = 1;
|
||||
size_t idx, max;
|
||||
yyjson_val * d;
|
||||
yyjson_arr_foreach(s, idx, max, d) {
|
||||
prod *= yyjson_get_int(d);
|
||||
}
|
||||
return prod;
|
||||
}
|
||||
|
||||
static bool run_text_enc(const std::string & models, const std::string & goldens, yyjson_val * root, Metrics * out) {
|
||||
std::string ids_f = manifest_file(root, "text_enc", "inputs", "input_ids");
|
||||
std::string mask_f = manifest_file(root, "text_enc", "inputs", "attention_mask");
|
||||
std::string exp_f = manifest_file(root, "text_enc", "outputs", "embeddings");
|
||||
if (ids_f.empty() || mask_f.empty() || exp_f.empty()) {
|
||||
fprintf(stderr, "[Test] text_enc: manifest missing entries\n");
|
||||
return false;
|
||||
}
|
||||
int64_t S = manifest_shape_prod(root, "text_enc", "inputs", "input_ids");
|
||||
int64_t n_out = manifest_shape_prod(root, "text_enc", "outputs", "embeddings");
|
||||
if (S <= 0 || n_out <= 0 || n_out % S != 0) {
|
||||
fprintf(stderr, "[Test] text_enc: bad shapes in manifest\n");
|
||||
return false;
|
||||
}
|
||||
int64_t H = n_out / S;
|
||||
|
||||
std::vector<uint8_t> ids_raw, mask_raw, exp_raw;
|
||||
if (!read_file(goldens + "/" + ids_f, ids_raw) || !read_file(goldens + "/" + mask_f, mask_raw) ||
|
||||
!read_file(goldens + "/" + exp_f, exp_raw)) {
|
||||
return false;
|
||||
}
|
||||
if (ids_raw.size() != (size_t) S * 8 || mask_raw.size() != (size_t) S || exp_raw.size() != (size_t) n_out * 4) {
|
||||
fprintf(stderr, "[Test] text_enc: golden file sizes do not match manifest shapes\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<int32_t> ids((size_t) S);
|
||||
const int64_t * ids64 = (const int64_t *) ids_raw.data();
|
||||
for (int64_t i = 0; i < S; i++) {
|
||||
ids[(size_t) i] = (int32_t) ids64[i];
|
||||
}
|
||||
|
||||
SA3T5GemmaEnc enc = {};
|
||||
if (!sa3_t5gemma_load(&enc, (models + "/sa3-text-enc-BF16.gguf").c_str())) {
|
||||
return false;
|
||||
}
|
||||
if ((int64_t) enc.cfg.hidden_size != H) {
|
||||
fprintf(stderr, "[Test] text_enc: model H=%d but golden H=%lld\n", enc.cfg.hidden_size, (long long) H);
|
||||
sa3_t5gemma_free(&enc);
|
||||
return false;
|
||||
}
|
||||
const char * env_layers = getenv("SA3_T5G_LAYERS");
|
||||
if (env_layers) {
|
||||
enc.debug_n_layers = atoi(env_layers);
|
||||
fprintf(stderr, "[Test] text_enc: DEBUG truncated to %d layers\n", enc.debug_n_layers);
|
||||
}
|
||||
|
||||
std::vector<float> got((size_t) n_out);
|
||||
sa3_t5gemma_forward(&enc, ids.data(), mask_raw.data(), (int) S, got.data());
|
||||
sa3_t5gemma_free(&enc);
|
||||
|
||||
*out = compare(got.data(), (const float *) exp_raw.data(), (size_t) n_out);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool run_seconds(const std::string & models, const std::string & goldens, yyjson_val * root, Metrics * out) {
|
||||
std::string in_f = manifest_file(root, "seconds", "inputs", "seconds");
|
||||
std::string exp_f = manifest_file(root, "seconds", "outputs", "embed");
|
||||
if (in_f.empty() || exp_f.empty()) {
|
||||
fprintf(stderr, "[Test] seconds: manifest missing entries\n");
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> in_raw, exp_raw;
|
||||
if (!read_file(goldens + "/" + in_f, in_raw) || !read_file(goldens + "/" + exp_f, exp_raw)) {
|
||||
return false;
|
||||
}
|
||||
if (in_raw.size() != 4) {
|
||||
fprintf(stderr, "[Test] seconds: bad input size\n");
|
||||
return false;
|
||||
}
|
||||
float seconds = *(const float *) in_raw.data();
|
||||
int64_t n_out = manifest_shape_prod(root, "seconds", "outputs", "embed");
|
||||
if (n_out <= 0 || exp_raw.size() != (size_t) n_out * 4) {
|
||||
fprintf(stderr, "[Test] seconds: golden size mismatch\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
SA3SecondsEmbedder emb;
|
||||
if (!sa3_seconds_embedder_load(&emb, (models + "/sa3-dit-BF16.gguf").c_str())) {
|
||||
return false;
|
||||
}
|
||||
if ((int64_t) emb.out_dim != n_out) {
|
||||
fprintf(stderr, "[Test] seconds: model out=%d but golden %lld\n", emb.out_dim, (long long) n_out);
|
||||
return false;
|
||||
}
|
||||
std::vector<float> got((size_t) n_out);
|
||||
sa3_seconds_embed(emb, seconds, got.data());
|
||||
|
||||
*out = compare(got.data(), (const float *) exp_raw.data(), (size_t) n_out);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Shared runner for the SAME autoencoder halves. is_encoder selects the
|
||||
// direction; the golden manifest supplies both tensors' shapes.
|
||||
static bool run_same(const std::string & models, const std::string & goldens, yyjson_val * root,
|
||||
bool is_encoder, Metrics * out) {
|
||||
const char * comp = is_encoder ? "same_enc" : "same_dec";
|
||||
const char * in_name = is_encoder ? "audio" : "latents";
|
||||
const char * out_name = is_encoder ? "latents" : "audio";
|
||||
|
||||
std::string in_f = manifest_file(root, comp, "inputs", in_name);
|
||||
std::string exp_f = manifest_file(root, comp, "outputs", out_name);
|
||||
if (in_f.empty() || exp_f.empty()) {
|
||||
fprintf(stderr, "[Test] %s: manifest missing entries\n", comp);
|
||||
return false;
|
||||
}
|
||||
int64_t n_in = manifest_shape_prod(root, comp, "inputs", in_name);
|
||||
int64_t n_out = manifest_shape_prod(root, comp, "outputs", out_name);
|
||||
int64_t n_lat = is_encoder ? n_out : n_in;
|
||||
if (n_in <= 0 || n_out <= 0 || n_lat % 256 != 0) {
|
||||
fprintf(stderr, "[Test] %s: bad shapes in manifest\n", comp);
|
||||
return false;
|
||||
}
|
||||
int n_latents = (int) (n_lat / 256); // latent_dim 256
|
||||
|
||||
std::vector<uint8_t> in_raw, exp_raw;
|
||||
if (!read_file(goldens + "/" + in_f, in_raw) || !read_file(goldens + "/" + exp_f, exp_raw)) {
|
||||
return false;
|
||||
}
|
||||
if (in_raw.size() != (size_t) n_in * 4 || exp_raw.size() != (size_t) n_out * 4) {
|
||||
fprintf(stderr, "[Test] %s: golden file sizes do not match manifest shapes\n", comp);
|
||||
return false;
|
||||
}
|
||||
|
||||
SA3Same same = {};
|
||||
// F16 is the current conversion (see convert-sa3.py); fall back to the
|
||||
// older BF16 name if that is what is on disk.
|
||||
std::string gguf = models + (is_encoder ? "/sa3-same-enc-F16.gguf" : "/sa3-same-dec-F16.gguf");
|
||||
{
|
||||
FILE * f = fopen(gguf.c_str(), "rb");
|
||||
if (f) {
|
||||
fclose(f);
|
||||
} else {
|
||||
gguf = models + (is_encoder ? "/sa3-same-enc-BF16.gguf" : "/sa3-same-dec-BF16.gguf");
|
||||
}
|
||||
}
|
||||
if (!sa3_same_load(&same, gguf.c_str(), is_encoder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Debug hooks: SA3_SAME_STAGE=<n> dumps the token sequence after n layers
|
||||
// (0 = folded input) to SA3_SAME_DUMP (default sa3_same_stage.bin).
|
||||
const char * env_stage = getenv("SA3_SAME_STAGE");
|
||||
if (env_stage) {
|
||||
same.debug_stage = atoi(env_stage);
|
||||
fprintf(stderr, "[Test] %s: DEBUG dumping stage %d\n", comp, same.debug_stage);
|
||||
}
|
||||
|
||||
std::vector<float> got((size_t) n_out);
|
||||
sa3_same_forward(&same, (const float *) in_raw.data(), got.data(), n_latents);
|
||||
|
||||
if (env_stage && !same.debug_out.empty()) {
|
||||
const char * dump = getenv("SA3_SAME_DUMP");
|
||||
std::string path = dump ? dump : "sa3_same_stage.bin";
|
||||
FILE * f = fopen(path.c_str(), "wb");
|
||||
if (f) {
|
||||
fwrite(same.debug_out.data(), sizeof(float), same.debug_out.size(), f);
|
||||
fclose(f);
|
||||
fprintf(stderr, "[Test] %s: stage tensor (%zu floats) -> %s\n", comp, same.debug_out.size(),
|
||||
path.c_str());
|
||||
}
|
||||
}
|
||||
sa3_same_free(&same);
|
||||
|
||||
*out = compare(got.data(), (const float *) exp_raw.data(), (size_t) n_out);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool run_dit(const std::string & models, const std::string & goldens, yyjson_val * root, Metrics * out) {
|
||||
const char * in_names[] = { "x", "t", "cross_attn_cond", "global_embed", "local_add_cond", "padding_mask" };
|
||||
std::vector<std::vector<uint8_t>> raw(7);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
std::string f = manifest_file(root, "dit", "inputs", in_names[i]);
|
||||
if (f.empty() || !read_file(goldens + "/" + f, raw[(size_t) i])) {
|
||||
fprintf(stderr, "[Test] dit: missing input '%s'\n", in_names[i]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::string exp_f = manifest_file(root, "dit", "outputs", "v");
|
||||
if (exp_f.empty() || !read_file(goldens + "/" + exp_f, raw[6])) {
|
||||
fprintf(stderr, "[Test] dit: missing output 'v'\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t n_x = manifest_shape_prod(root, "dit", "inputs", "x");
|
||||
int64_t n_cross = manifest_shape_prod(root, "dit", "inputs", "cross_attn_cond");
|
||||
int64_t n_glob = manifest_shape_prod(root, "dit", "inputs", "global_embed");
|
||||
int64_t n_local = manifest_shape_prod(root, "dit", "inputs", "local_add_cond");
|
||||
int64_t T = manifest_shape_prod(root, "dit", "inputs", "padding_mask");
|
||||
int64_t n_out = manifest_shape_prod(root, "dit", "outputs", "v");
|
||||
if (T <= 0 || n_x != 256 * T || n_out != n_x || n_glob != 768 || n_cross % 768 != 0 ||
|
||||
n_local != 257 * T) {
|
||||
fprintf(stderr, "[Test] dit: bad shapes in manifest\n");
|
||||
return false;
|
||||
}
|
||||
int64_t S_c = n_cross / 768;
|
||||
if (raw[0].size() != (size_t) n_x * 4 || raw[1].size() != 4 || raw[2].size() != (size_t) n_cross * 4 ||
|
||||
raw[3].size() != (size_t) n_glob * 4 || raw[4].size() != (size_t) n_local * 4 ||
|
||||
raw[5].size() != (size_t) T || raw[6].size() != (size_t) n_out * 4) {
|
||||
fprintf(stderr, "[Test] dit: golden file sizes do not match manifest shapes\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
SA3DiT dit = {};
|
||||
if (!sa3_dit_load(&dit, (models + "/sa3-dit-BF16.gguf").c_str())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Debug hook: SA3_DIT_STAGE=<n> dumps the token sequence after n layers
|
||||
// (0 = memory+projected input) to SA3_DIT_DUMP (default sa3_dit_stage.bin).
|
||||
const char * env_stage = getenv("SA3_DIT_STAGE");
|
||||
if (env_stage) {
|
||||
dit.debug_stage = atoi(env_stage);
|
||||
fprintf(stderr, "[Test] dit: DEBUG dumping stage %d\n", dit.debug_stage);
|
||||
}
|
||||
|
||||
std::vector<float> got((size_t) n_out);
|
||||
sa3_dit_forward(&dit, (const float *) raw[0].data(), *(const float *) raw[1].data(),
|
||||
(const float *) raw[2].data(), S_c, (const float *) raw[3].data(),
|
||||
(const float *) raw[4].data(), raw[5].data(), T, got.data());
|
||||
|
||||
if (env_stage && !dit.debug_out.empty()) {
|
||||
const char * dump = getenv("SA3_DIT_DUMP");
|
||||
std::string path = dump ? dump : "sa3_dit_stage.bin";
|
||||
FILE * f = fopen(path.c_str(), "wb");
|
||||
if (f) {
|
||||
fwrite(dit.debug_out.data(), sizeof(float), dit.debug_out.size(), f);
|
||||
fclose(f);
|
||||
fprintf(stderr, "[Test] dit: stage tensor (%zu floats) -> %s\n", dit.debug_out.size(),
|
||||
path.c_str());
|
||||
}
|
||||
}
|
||||
sa3_dit_free(&dit);
|
||||
|
||||
*out = compare(got.data(), (const float *) raw[6].data(), (size_t) n_out);
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
std::string models, goldens, component = "all";
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--models") && i + 1 < argc) {
|
||||
models = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--goldens") && i + 1 < argc) {
|
||||
goldens = argv[++i];
|
||||
} else if (!strcmp(argv[i], "--component") && i + 1 < argc) {
|
||||
component = argv[++i];
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"Usage: sa3-ggml-test --models <dir> --goldens <dir> [--component text_enc|seconds|same_enc|same_dec|dit|all]\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (models.empty() || goldens.empty()) {
|
||||
fprintf(stderr, "Usage: sa3-ggml-test --models <dir> --goldens <dir> [--component text_enc|seconds|same_enc|same_dec|dit|all]\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> manifest_raw;
|
||||
if (!read_file(goldens + "/manifest.json", manifest_raw)) {
|
||||
return 2;
|
||||
}
|
||||
yyjson_doc * doc = yyjson_read((const char *) manifest_raw.data(), manifest_raw.size(), 0);
|
||||
if (!doc) {
|
||||
fprintf(stderr, "[Test] cannot parse manifest.json\n");
|
||||
return 2;
|
||||
}
|
||||
yyjson_val * root = yyjson_doc_get_root(doc);
|
||||
|
||||
bool all_pass = true;
|
||||
bool any_run = false;
|
||||
|
||||
if (component == "all" || component == "text_enc") {
|
||||
Metrics m;
|
||||
any_run = true;
|
||||
if (run_text_enc(models, goldens, root, &m)) {
|
||||
bool pass = m.cosine > PASS_COSINE;
|
||||
printf("text_enc: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff,
|
||||
pass ? "PASS" : "FAIL");
|
||||
all_pass = all_pass && pass;
|
||||
} else {
|
||||
printf("text_enc: ERROR\n");
|
||||
all_pass = false;
|
||||
}
|
||||
}
|
||||
if (component == "all" || component == "seconds") {
|
||||
Metrics m;
|
||||
any_run = true;
|
||||
if (run_seconds(models, goldens, root, &m)) {
|
||||
bool pass = m.cosine > PASS_COSINE;
|
||||
printf("seconds: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff,
|
||||
pass ? "PASS" : "FAIL");
|
||||
all_pass = all_pass && pass;
|
||||
} else {
|
||||
printf("seconds: ERROR\n");
|
||||
all_pass = false;
|
||||
}
|
||||
}
|
||||
if (component == "all" || component == "same_enc") {
|
||||
Metrics m;
|
||||
any_run = true;
|
||||
if (run_same(models, goldens, root, true, &m)) {
|
||||
bool pass = m.cosine > PASS_COSINE;
|
||||
printf("same_enc: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff,
|
||||
pass ? "PASS" : "FAIL");
|
||||
all_pass = all_pass && pass;
|
||||
} else {
|
||||
printf("same_enc: ERROR\n");
|
||||
all_pass = false;
|
||||
}
|
||||
}
|
||||
if (component == "all" || component == "same_dec") {
|
||||
Metrics m;
|
||||
any_run = true;
|
||||
if (run_same(models, goldens, root, false, &m)) {
|
||||
bool pass = m.cosine > PASS_COSINE;
|
||||
printf("same_dec: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff,
|
||||
pass ? "PASS" : "FAIL");
|
||||
all_pass = all_pass && pass;
|
||||
} else {
|
||||
printf("same_dec: ERROR\n");
|
||||
all_pass = false;
|
||||
}
|
||||
}
|
||||
if (component == "all" || component == "dit") {
|
||||
Metrics m;
|
||||
any_run = true;
|
||||
if (run_dit(models, goldens, root, &m)) {
|
||||
bool pass = m.cosine > PASS_COSINE;
|
||||
printf("dit: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff,
|
||||
pass ? "PASS" : "FAIL");
|
||||
all_pass = all_pass && pass;
|
||||
} else {
|
||||
printf("dit: ERROR\n");
|
||||
all_pass = false;
|
||||
}
|
||||
}
|
||||
yyjson_doc_free(doc);
|
||||
|
||||
if (!any_run) {
|
||||
fprintf(stderr, "[Test] unknown component '%s'\n", component.c_str());
|
||||
return 2;
|
||||
}
|
||||
return all_pass ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
// synth-batch-runner.h: three-phase orchestration shared by the synth binaries
|
||||
//
|
||||
// Phase 1 (all groups) runs ace_synth_job_run_dit. Each call acquires the DiT,
|
||||
// runs the denoising loop AND LRC alignment (via ops_lrc_extract, while the DiT
|
||||
// is still held), then releases it. Phase 2 (all groups) runs
|
||||
// ace_synth_job_run_vae, which acquires the VAE decoder on entry and releases it
|
||||
// on exit. Phase 3 (LRC) simply copies the pre-computed alignment from
|
||||
// SynthState — no DiT acquisition needed.
|
||||
// Under EVICT_STRICT, at most one GPU module is resident at a time.
|
||||
|
||||
#include "pipeline-synth.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
// Run a batch of request groups through the synthesis phases.
|
||||
//
|
||||
// groups[g][i]: request i of group g. All requests in a group must share
|
||||
// the same T (same audio_codes or same duration), which the ops assume
|
||||
// when they stack per-batch tensors for a single DiT forward.
|
||||
// seed must be resolved (non-negative) on every request.
|
||||
// src_audio / ref_audio: interleaved stereo 48kHz buffers, NULL when not applicable.
|
||||
// src_latents / ref_latents: pre-encoded latents [T_latent * 64] f32 alternative
|
||||
// to the matching audio buffer. When non-NULL, the corresponding VAE encoder
|
||||
// pass is skipped for every group. The same buffers are shared across groups,
|
||||
// matching how src_audio and ref_audio are shared today.
|
||||
// audio_out[sum_g(groups[g].size())]: pre-allocated slots filled by phase 2.
|
||||
// On error, slots completed before the failure keep their audio; the rest
|
||||
// are left at {NULL, 0, 0}. Caller owns ace_audio_free.
|
||||
// latents_out: optional capture of one post-DiT latent per generated track,
|
||||
// indexed identically to audio_out. Each entry is [T_track * 64] f32 time-major,
|
||||
// T_track = entry.size() / 64. Pass NULL to skip the capture.
|
||||
// Returns 0 on success, -1 on any error or cancellation.
|
||||
static int synth_batch_run(AceSynth * ctx,
|
||||
std::vector<std::vector<AceRequest>> & groups,
|
||||
const float * src_audio,
|
||||
int src_len,
|
||||
const float * src_latents,
|
||||
int src_T_latent,
|
||||
const float * ref_audio,
|
||||
int ref_len,
|
||||
const float * ref_latents,
|
||||
int ref_T_latent,
|
||||
AceAudio * audio_out,
|
||||
std::string * lrc_out = nullptr,
|
||||
std::vector<std::vector<float>> * latents_out = nullptr,
|
||||
bool (*cancel)(void *) = nullptr,
|
||||
void * cancel_data = nullptr) {
|
||||
const int n_groups = (int) groups.size();
|
||||
std::vector<AceSynthJob *> jobs(n_groups, nullptr);
|
||||
std::vector<int> audio_off(n_groups, 0);
|
||||
|
||||
if (latents_out) {
|
||||
latents_out->clear();
|
||||
}
|
||||
|
||||
// Phase 1: denoising + inline LRC for each group. ops_dit_generate
|
||||
// acquires the DiT, runs the denoising loop, then calls ops_lrc_extract
|
||||
// while the DiT is still held — avoiding a redundant adapter merge+reload
|
||||
// under EVICT_STRICT. Results are cached in SynthState.lrc_results[].
|
||||
int off = 0;
|
||||
for (int g = 0; g < n_groups; g++) {
|
||||
const int gn = (int) groups[g].size();
|
||||
jobs[g] = ace_synth_job_run_dit(ctx, groups[g].data(), src_audio, src_len,
|
||||
src_latents, src_T_latent,
|
||||
ref_audio, ref_len,
|
||||
ref_latents, ref_T_latent,
|
||||
gn, cancel, cancel_data);
|
||||
if (!jobs[g]) {
|
||||
for (int j = 0; j < g; j++) {
|
||||
ace_synth_job_free(jobs[j]);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
audio_off[g] = off;
|
||||
off += gn;
|
||||
}
|
||||
|
||||
// Capture one post-DiT latent per track, time-major [T*64], indexed to
|
||||
// match audio_out. Latents live in jobs[g]->state.output until run_vae
|
||||
// frees the job; capture happens before phase 2.
|
||||
if (latents_out) {
|
||||
latents_out->resize((size_t) off);
|
||||
for (int g = 0; g < n_groups; g++) {
|
||||
const int gn = (int) groups[g].size();
|
||||
for (int i = 0; i < gn; i++) {
|
||||
int T = 0;
|
||||
const float * src = ace_synth_job_get_latent(jobs[g], i, &T);
|
||||
(*latents_out)[audio_off[g] + i].assign(src, src + (size_t) T * 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: VAE decode for each job. The decoder is acquired and released
|
||||
// by ops_vae_decode inside ace_synth_job_run_vae.
|
||||
for (int g = 0; g < n_groups; g++) {
|
||||
const int gn = (int) groups[g].size();
|
||||
const int rc =
|
||||
ace_synth_job_run_vae(ctx, jobs[g], audio_out + audio_off[g], cancel, cancel_data);
|
||||
if (rc != 0) {
|
||||
ace_synth_job_free(jobs[g]);
|
||||
jobs[g] = nullptr;
|
||||
for (int j = g + 1; j < n_groups; j++) {
|
||||
ace_synth_job_free(jobs[j]);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Phase 3: LRC — copy pre-computed alignment (no DiT acquisition)
|
||||
if (lrc_out && groups[g][0].get_lrc) {
|
||||
ace_synth_job_run_lrc(ctx, jobs[g], lrc_out + audio_off[g], gn);
|
||||
}
|
||||
|
||||
ace_synth_job_free(jobs[g]);
|
||||
jobs[g] = nullptr;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generate version.h with the current git commit hash and date.
|
||||
# Only rewrites the file if the content changed (avoids rebuild cascade).
|
||||
# Usage: cmake -DSRC_DIR=... -DOUTPUT=... -P version.cmake
|
||||
|
||||
execute_process(
|
||||
COMMAND git rev-parse --short HEAD
|
||||
WORKING_DIRECTORY "${SRC_DIR}"
|
||||
OUTPUT_VARIABLE GIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
RESULT_VARIABLE GIT_RESULT
|
||||
)
|
||||
if(NOT GIT_RESULT EQUAL 0)
|
||||
set(GIT_HASH "unknown")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND git show -s --format=%cs HEAD
|
||||
WORKING_DIRECTORY "${SRC_DIR}"
|
||||
OUTPUT_VARIABLE GIT_DATE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
RESULT_VARIABLE DATE_RESULT
|
||||
)
|
||||
if(NOT DATE_RESULT EQUAL 0)
|
||||
set(GIT_DATE "unknown")
|
||||
endif()
|
||||
|
||||
set(CONTENT "#pragma once\n#define ACE_VERSION \"${GIT_HASH} (${GIT_DATE})\"\n")
|
||||
|
||||
if(EXISTS "${OUTPUT}")
|
||||
file(READ "${OUTPUT}" EXISTING)
|
||||
if("${EXISTING}" STREQUAL "${CONTENT}")
|
||||
return()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
file(WRITE "${OUTPUT}" "${CONTENT}")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
package-lock.json
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Dice examples
|
||||
|
||||
JSON prompts loaded by the Dice button at build time.
|
||||
Any `.json` file in this directory or its subdirectories is included.
|
||||
|
||||
Add your own prompts in acestep.cpp format and rebuild the webui.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "a soft Bengali love song for a quiet evening",
|
||||
"instrumental": false,
|
||||
"vocal_language": "bn"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "an upbeat summer pop song with catchy hooks",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "epic orchestral cinematic music for a movie trailer",
|
||||
"instrumental": true,
|
||||
"vocal_language": "unknown"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "一首深情的中文抒情歌曲,适合夜晚独自聆听",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Japanese city pop with nostalgic 80s vibes",
|
||||
"instrumental": false,
|
||||
"vocal_language": "ja"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "lo-fi hip hop beats for studying and relaxing",
|
||||
"instrumental": true,
|
||||
"vocal_language": "unknown"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "energetic K-pop dance track with powerful vocals",
|
||||
"instrumental": false,
|
||||
"vocal_language": "ko"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "romantic Spanish guitar ballad with heartfelt lyrics",
|
||||
"instrumental": false,
|
||||
"vocal_language": "es"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "中国风电子舞曲,融合古典乐器与现代节拍",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "peaceful piano melody for meditation and relaxation",
|
||||
"instrumental": true,
|
||||
"vocal_language": "unknown"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Kraftvoller deutscher Metal-Anthem mit aggressivem Gesang und Double-Bass-Drums",
|
||||
"instrumental": false,
|
||||
"vocal_language": "de"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Energiegeladene Neue Deutsche Welle mit Retro-Synths und punkiger Attitüde",
|
||||
"instrumental": false,
|
||||
"vocal_language": "de"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Entspannter deutscher Hip-Hop mit lässigem Flow und jazzigen Samples",
|
||||
"instrumental": false,
|
||||
"vocal_language": "de"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Atmosphärische deutsche Elektronik mit Ambient-Texturen und sanftem Gesang",
|
||||
"instrumental": false,
|
||||
"vocal_language": "de"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Chanson française romantique avec accordéon et cordes nostalgiques",
|
||||
"instrumental": false,
|
||||
"vocal_language": "fr"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "French house groovy avec samples disco et ligne de basse funky",
|
||||
"instrumental": true,
|
||||
"vocal_language": "fr"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Rap français poétique avec samples jazz et flow smooth",
|
||||
"instrumental": false,
|
||||
"vocal_language": "fr"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Pop française rêveuse avec voix éthérées et synthés luxuriants",
|
||||
"instrumental": false,
|
||||
"vocal_language": "fr"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Style yé-yé rétro avec rythme entraînant et voix enjouées",
|
||||
"instrumental": false,
|
||||
"vocal_language": "fr"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Ballade française mélancolique avec piano et émotion",
|
||||
"instrumental": false,
|
||||
"vocal_language": "fr"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "a melancholic indie rock ballad with atmospheric guitars and dreamy reverb",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Électropop française énergique avec hooks accrocheurs",
|
||||
"instrumental": false,
|
||||
"vocal_language": "fr"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Britpop anthem with jangly guitars, catchy hooks, and witty lyrics about working-class life in modern Britain, featuring a driving rhythm section and melodic vocal harmonies",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Raw garage rock track with fuzzy distorted guitars, primal drumming, and aggressive vocals delivering rebellious lyrics about teenage angst and urban decay",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Psychedelic rock journey with swirling organ, phased guitars, and trippy effects, featuring dreamy vocals singing about cosmic exploration and altered consciousness",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Energetic surf rock instrumental with reverb-drenched twangy guitars, driving drums, and a catchy melody evoking sun-soaked beaches and crashing waves",
|
||||
"instrumental": true,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Flamboyant glam rock anthem with stomping beats, glittery guitar riffs, and theatrical vocals celebrating self-expression and rock and roll excess",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Experimental art rock composition with unconventional song structures, avant-garde instrumentation, and introspective vocals exploring themes of identity and alienation",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Complex math rock instrumental with intricate time signatures, angular guitar riffs, and precise polyrhythmic drumming creating a technically demanding sonic puzzle",
|
||||
"instrumental": true,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Abrasive noise rock track with dissonant guitar feedback, pounding bass, and shouted vocals expressing frustration and chaos in an industrial urban landscape",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Heavy stoner rock groove with thick fuzzy riffs, slow hypnotic tempo, and hazy vocals singing about desert highways and transcendental experiences",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "energetic EDM festival anthem with heavy bass drops and euphoric synth leads",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Soulful southern rock ballad with slide guitar, Hammond organ, and heartfelt vocals telling stories of small-town life and lost love in the American South",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Anthemic heartland rock song with ringing guitars, driving rhythm, and earnest vocals celebrating blue-collar values and the American working-class spirit",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Infectious power pop track with crunchy guitars, tight harmonies, and an irresistible chorus about summer romance and youthful optimism",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Shimmering jangle pop tune with chiming arpeggiated guitars, upbeat tempo, and breezy vocals singing about nostalgic memories and bittersweet longing",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Ornate baroque pop arrangement with harpsichord, string quartet, and lush orchestration, featuring elegant vocals weaving tales of aristocratic romance and melancholy",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Sophisticated chamber pop piece with piano, woodwinds, and sweeping strings, featuring intimate vocals exploring themes of existential wonder and quiet introspection",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Aggressive dubstep track with heavy wobble bass, distorted synths, and intense drops that shake the speakers",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "High-energy drum and bass anthem with rolling breakbeats, deep sub-bass, and euphoric vocal chops",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Classic house music with four-on-the-floor beats, soulful piano chords, and uplifting female vocals",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Dark industrial techno with pounding kicks, hypnotic synth loops, and relentless mechanical rhythms",
|
||||
"instrumental": true,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "smooth R&B love song with soulful vocals and silky piano chords",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Uplifting trance with soaring melodies, ethereal pads, and emotional breakdowns that build to euphoric climaxes",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Hardstyle banger with distorted kicks, reverse bass, and anthemic vocals that ignite the crowd",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Colorful future bass with lush synth chords, pitched vocal samples, and emotional melodic drops",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Sunny tropical house with steel drums, marimba melodies, and breezy summer vocals",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Smooth deep house groove with warm basslines, subtle percussion, and hypnotic late-night atmosphere",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Epic progressive house journey with layered synths, gradual builds, and massive festival-ready drops",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Vintage electro swing with jazzy brass samples, swinging rhythms, and playful retro vocals",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Funky breakbeat track with chopped drum breaks, funky basslines, and energetic vocal hooks",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Chilled downtempo with ambient textures, slow grooves, and dreamy atmospheric soundscapes",
|
||||
"instrumental": true,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Experimental IDM with complex polyrhythms, glitchy textures, and intricate sound design",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "uplifting country road trip anthem with acoustic guitar and harmonica",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Chaotic glitch hop with stuttering beats, digital artifacts, and fragmented vocal samples",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "轻快的国风电子舞曲,融合古筝与现代节拍",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "深情的粤语情歌,带有港式复古风情",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "热血沸腾的电竞主题曲,激昂的电子音效",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "空灵的禅意音乐,适合瑜伽冥想",
|
||||
"instrumental": true,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "欢快的儿童歌曲,充满童真童趣",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "激情四射的摇滚现场版,吉他solo燃爆全场",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "温馨的家庭主题曲,充满爱与温暖",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "神秘的武侠风格配乐,刀光剑影江湖梦",
|
||||
"instrumental": true,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "动感的健身房音乐,节奏强劲充满力量",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "dark trap beat with haunting melodies and 808 bass",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "古典戏曲风格流行歌曲,京剧元素与现代编曲融合",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "浪漫的校园民谣,青春回忆满满",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "悠扬的草原风情歌曲,马头琴与长调交织",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "时尚的都市R&B,慵懒性感的夜生活氛围",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "古风仙侠主题曲,飘逸空灵如临仙境",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "励志的毕业季歌曲,告别与祝福",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "深沉的蓝调爵士,午夜酒吧的忧郁情调",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "欢乐的新年贺岁歌曲,喜庆热闹迎新春",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "唯美的古典钢琴曲,如诗如画的江南水乡",
|
||||
"instrumental": true,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "热情的拉丁风格华语歌曲,异域风情与中文歌词碰撞",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "groovy funk jam with slap bass and wah-wah guitar riffs",
|
||||
"instrumental": false,
|
||||
"vocal_language": "en"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "磅礴大气的史诗级电影配乐,气势恢宏震撼人心",
|
||||
"instrumental": false,
|
||||
"vocal_language": "zh"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "疾走感のあるアニソンロック、熱いギターリフとパワフルなボーカル",
|
||||
"instrumental": false,
|
||||
"vocal_language": "ja"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user