From 9b893da80fc83fbc87a38fb7dd23bae9c33cfcfc Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 25 Jul 2026 23:32:18 +0000 Subject: [PATCH] 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 --- backend/cpp/audio-cpp/capability_routing.cpp | 32 ++++++++++++-- backend/cpp/audio-cpp/capability_routing.h | 5 ++- .../cpp/audio-cpp/capability_routing_test.cpp | 42 ++++++++++++++++++- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/backend/cpp/audio-cpp/capability_routing.cpp b/backend/cpp/audio-cpp/capability_routing.cpp index 3e9414025..e80663aa7 100644 --- a/backend/cpp/audio-cpp/capability_routing.cpp +++ b/backend/cpp/audio-cpp/capability_routing.cpp @@ -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_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 diff --git a/backend/cpp/audio-cpp/capability_routing.h b/backend/cpp/audio-cpp/capability_routing.h index e5ebb4dd6..e486b7cc5 100644 --- a/backend/cpp/audio-cpp/capability_routing.h +++ b/backend/cpp/audio-cpp/capability_routing.h @@ -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); diff --git a/backend/cpp/audio-cpp/capability_routing_test.cpp b/backend/cpp/audio-cpp/capability_routing_test.cpp index 439c3b23e..02eaeef10 100644 --- a/backend/cpp/audio-cpp/capability_routing_test.cpp +++ b/backend/cpp/audio-cpp/capability_routing_test.cpp @@ -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() {