backend(audio-cpp): use upstream's 'spk' task name and pin the preference order

The SpeakerRecognition short name was 'spkrec', which audio.cpp neither prints
nor parses; a name copied out of audio.cpp was rejected and a pinned 'spkrec'
would not survive the engine boundary. Emit 'spk', keep 'spkrec' as an
input-only alias, and correct the known-tasks lists.

Three assertions were vacuous because their fixtures advertised a single task,
so reversing a preference order or dropping the RPC name and the attempted
pairs from the capability error all passed. Give them fixtures that can tell
the orderings apart.

Assisted-by: Claude:claude-opus-5 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto
2026-07-25 23:32:18 +00:00
parent 56268eb6c3
commit 5e05ba2ebe
3 changed files with 72 additions and 7 deletions

View File

@@ -10,8 +10,11 @@ struct NamedTask {
const char *name;
};
// Short names match audiocpp_cli's --task values (docs/usage.md). Two kinds
// have no CLI name upstream; they get stable names here so pinning still works.
// Short names are exactly the strings audio.cpp prints and parses in
// framework/runtime/session.cpp, so a name pinned here survives conversion at
// the engine boundary and a name copied out of audio.cpp is accepted here. All
// thirteen have an upstream name; only "spk" is absent from the --task table in
// docs/usage.md.
const NamedTask kTaskNames[] = {
{Task::Vad, "vad"},
{Task::Asr, "asr"},
@@ -24,10 +27,20 @@ const NamedTask kTaskNames[] = {
{Task::SpeechToSpeech, "s2s"},
{Task::Alignment, "align"},
{Task::VoiceDesign, "vdes"},
{Task::SpeakerRecognition, "spkrec"},
{Task::SpeakerRecognition, "spk"},
{Task::Svc, "svc"},
};
// Accepted on input but never emitted. "spkrec" was this backend's own earlier
// name for the kind; upstream only ever knew "spk".
const NamedTask kTaskAliases[] = {
{Task::SpeakerRecognition, "spkrec"},
};
// First match wins, which is safe because a Capabilities value holds at most one
// entry per task: it mirrors upstream runtime::TaskCapability (model.h), which
// pairs one kind with a modes vector, and no loader's supported_tasks list
// repeats a kind.
bool family_supports(const Capabilities &caps, Task task, Mode mode) {
for (const auto &capability : caps.tasks) {
if (capability.task != task) {
@@ -85,6 +98,11 @@ std::vector<Task> task_candidates(Rpc rpc, const RequestShape &shape) {
case Rpc::SoundGeneration:
return {Task::AudioGeneration};
case Rpc::AudioTransform:
// Svc is listed for completeness but is unreachable by auto-routing, by
// design: the only families advertising it (seed_vc, vevo2) also
// advertise VoiceConversion, which always wins, and no request signal
// means "this input is singing". Singing voice conversion therefore
// requires an explicit task:svc pin.
return {Task::SourceSeparation, Task::VoiceConversion, Task::Svc,
Task::SpeechToSpeech};
}
@@ -153,6 +171,12 @@ bool parse_task_name(const std::string &value, Task &out) {
return true;
}
}
for (const auto &entry : kTaskAliases) {
if (value == entry.name) {
out = entry.task;
return true;
}
}
return false;
}
@@ -184,7 +208,7 @@ Route resolve_route(Rpc rpc, const RequestShape &shape,
if (!parse_task_name(shape.pinned_task, pinned)) {
route.error = "audio-cpp: unknown task option '" + shape.pinned_task +
"'. Known tasks: gen, tts, clon, vc, svc, s2s, asr, "
"align, vad, diar, sep, vdes, spkrec";
"align, vad, diar, sep, vdes, spk";
return route;
}
// A pinned task is honoured exactly. Silently rerouting would make the

View File

@@ -75,8 +75,9 @@ struct Route {
Route resolve_route(Rpc rpc, const RequestShape &shape, const Capabilities &caps);
// Canonical audio.cpp CLI short names: gen, tts, clon, vc, svc, s2s, asr,
// align, vad, diar, sep, vdes, spkrec.
// Canonical audio.cpp short names: gen, tts, clon, vc, svc, s2s, asr, align,
// vad, diar, sep, vdes, spk. parse_task_name additionally accepts "spkrec" as
// a legacy alias; task_name only ever emits "spk".
const char *task_name(Task task);
const char *mode_name(Mode mode);
const char *rpc_name(Rpc rpc);

View File

@@ -119,6 +119,10 @@ static void test_tts_stream_requires_streaming() {
"error names the family");
check(bad.error.find("tts/offline") != std::string::npos,
"error lists what the family does support");
check(bad.error.find("TTSStream") != std::string::npos,
"error names the RPC that was refused");
check(bad.error.find("tts/streaming") != std::string::npos,
"error lists the (task, mode) pairs that were tried");
}
static void test_transcription_stream_falls_back_to_offline() {
@@ -178,7 +182,16 @@ static void test_prompt_does_not_hijack_asr() {
shape.has_prompt_text = true;
const auto r = resolve_route(Rpc::AudioTranscription, shape, nemotron());
check(r.ok, "ASR with a prompt routes");
check(r.task == Task::Asr, "Asr is preferred over Alignment");
// nemotron advertises Asr alone, so the assertion has to be made against a
// family that advertises both: otherwise "Asr is preferred" only restates
// that Asr is the only option, and reversing the preference order passes.
Capabilities both{"asr_with_aligner",
{{Task::Asr, {Mode::Offline}},
{Task::Alignment, {Mode::Offline}}}};
const auto pref = resolve_route(Rpc::AudioTranscription, shape, both);
check(pref.ok, "a family offering both routes");
check(pref.task == Task::Asr, "Asr is preferred over Alignment");
}
static void test_audio_transform_prefers_separation() {
@@ -186,6 +199,16 @@ static void test_audio_transform_prefers_separation() {
resolve_route(Rpc::AudioTransform, RequestShape{}, htdemucs());
check(sep.ok && sep.task == Task::SourceSeparation, "separation routes");
// htdemucs advertises separation alone, so the check above cannot fail on
// ordering. This family advertises both, which is what pins the preference.
Capabilities sep_and_vc{"sep_and_vc",
{{Task::SourceSeparation, {Mode::Offline}},
{Task::VoiceConversion, {Mode::Offline}}}};
const auto pref =
resolve_route(Rpc::AudioTransform, RequestShape{}, sep_and_vc);
check(pref.ok && pref.task == Task::SourceSeparation,
"separation is preferred over voice conversion");
Capabilities miocodec{"miocodec",
{{Task::VoiceConversion, {Mode::Offline}},
{Task::SpeechToSpeech, {Mode::Offline}}}};
@@ -250,6 +273,23 @@ static void test_names_round_trip() {
}
check(std::string(mode_name(Mode::Offline)) == "offline", "offline name");
check(std::string(mode_name(Mode::Streaming)) == "streaming", "streaming name");
// The emitted name must be the one audio.cpp itself prints and parses
// (framework/runtime/session.cpp), because `task:` is user-facing: a name
// copied out of audio.cpp has to be accepted here, and a name pinned here
// has to survive conversion at the engine boundary.
check(std::string(task_name(Task::SpeakerRecognition)) == "spk",
"speaker recognition emits upstream's name 'spk'");
Task pinned = Task::Vad;
check(parse_task_name("spk", pinned) && pinned == Task::SpeakerRecognition,
"'spk' parses to SpeakerRecognition");
// Accepted as a legacy alias so configs written against the earlier name
// keep working, but never emitted.
Task alias = Task::Vad;
check(parse_task_name("spkrec", alias) && alias == Task::SpeakerRecognition,
"'spkrec' is still accepted as an alias");
}
static void test_describe_capabilities() {