Skip to content

Commit cd91110

Browse files
authored
Merge pull request #74 from lapy/feature/machine-readable-loader-catalog
Add machine-readable loader and package catalog exports
2 parents 7554ee6 + 589fa06 commit cd91110

36 files changed

Lines changed: 628 additions & 9 deletions

File tree

‎app/cli/main.cpp‎

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
#include "engine/framework/audio/chunking.h"
1111
#include "engine/framework/audio/conversion.h"
1212
#include "engine/framework/debug/trace.h"
13+
#include "engine/framework/io/json.h"
1314
#include "engine/framework/runtime/registry.h"
15+
#include "engine/framework/runtime/session.h"
1416

1517
#include <algorithm>
1618
#include <cmath>
@@ -141,7 +143,7 @@ void print_task_list_help() {
141143
<< " --mode streaming uses the selected model's default streaming policy\n"
142144
<< " Utility:\n"
143145
<< " --inspect\n"
144-
<< " --list-loaders\n"
146+
<< " --list-loaders [--json]\n"
145147
<< "\n"
146148
<< " Tasks:\n"
147149
<< " vad voice activity detection\n"
@@ -564,10 +566,67 @@ int audiocpp_cli_main(int argc, char ** argv) {
564566
return 0;
565567
}
566568
if (has_arg(argc, argv, "--list-loaders")) {
567-
const auto families = registry.families();
568-
std::cout << "registered_loaders=" << registry.size() << "\n";
569-
for (const auto & family : families) {
570-
std::cout << family << "\n";
569+
const auto advertisements = registry.advertise_loaders();
570+
if (has_arg(argc, argv, "--json")) {
571+
engine::io::json::Value::Object loaders_object;
572+
for (const auto & row : advertisements) {
573+
engine::io::json::Value::Object tasks_object;
574+
for (const auto & task_cap : row.capabilities.supported_tasks) {
575+
engine::io::json::Value::Array modes;
576+
for (const auto mode : task_cap.modes) {
577+
modes.push_back(engine::io::json::Value::make_string(engine::runtime::to_string(mode)));
578+
}
579+
tasks_object.emplace(
580+
engine::runtime::to_string(task_cap.task),
581+
engine::io::json::Value::make_array(std::move(modes)));
582+
}
583+
engine::io::json::Value::Array endpoints;
584+
for (const auto & endpoint : row.api_endpoints) {
585+
endpoints.push_back(engine::io::json::Value::make_string(endpoint));
586+
}
587+
engine::io::json::Value::Object loader_object;
588+
loader_object.emplace("tasks", engine::io::json::Value::make_object(std::move(tasks_object)));
589+
loader_object.emplace(
590+
"instructions_policy",
591+
engine::io::json::Value::make_string(row.instructions_policy));
592+
loader_object.emplace(
593+
"api_endpoints",
594+
engine::io::json::Value::make_array(std::move(endpoints)));
595+
loaders_object.emplace(
596+
row.family,
597+
engine::io::json::Value::make_object(std::move(loader_object)));
598+
}
599+
engine::io::json::Value::Object root;
600+
root.emplace("schema_version", engine::io::json::Value::make_number(1));
601+
root.emplace("loaders", engine::io::json::Value::make_object(std::move(loaders_object)));
602+
std::cout << engine::io::json::stringify(engine::io::json::Value::make_object(std::move(root)))
603+
<< "\n";
604+
} else {
605+
std::cout << "registered_loaders=" << advertisements.size() << "\n";
606+
for (const auto & row : advertisements) {
607+
std::cout << row.family;
608+
if (!row.capabilities.supported_tasks.empty()) {
609+
std::cout << ":";
610+
for (size_t i = 0; i < row.capabilities.supported_tasks.size(); ++i) {
611+
const auto & task_cap = row.capabilities.supported_tasks[i];
612+
if (i > 0) {
613+
std::cout << ",";
614+
}
615+
std::cout << " " << engine::runtime::to_string(task_cap.task);
616+
if (!task_cap.modes.empty()) {
617+
std::cout << " (";
618+
for (size_t m = 0; m < task_cap.modes.size(); ++m) {
619+
if (m > 0) {
620+
std::cout << "|";
621+
}
622+
std::cout << engine::runtime::to_string(task_cap.modes[m]);
623+
}
624+
std::cout << ")";
625+
}
626+
}
627+
}
628+
std::cout << "\n";
629+
}
571630
}
572631
return 0;
573632
}

‎include/engine/framework/runtime/model.h‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ class ILoadedVoiceModel {
105105
const SessionOptions & options) const = 0;
106106
};
107107

108+
struct LoaderAdvertisement {
109+
std::string family;
110+
CapabilitySet capabilities;
111+
std::string instructions_policy;
112+
std::vector<std::string> api_endpoints;
113+
};
114+
115+
/** Map advertised capabilities to the HTTP surfaces they normally use. */
116+
std::vector<std::string> default_api_endpoints_for_capabilities(const CapabilitySet & capabilities);
117+
108118
class IVoiceModelLoader {
109119
public:
110120
virtual ~IVoiceModelLoader() = default;
@@ -113,6 +123,15 @@ class IVoiceModelLoader {
113123
virtual bool can_load(const ModelLoadRequest & request) const = 0;
114124
virtual ModelInspection inspect(const ModelLoadRequest & request) const = 0;
115125
virtual std::unique_ptr<ILoadedVoiceModel> load(const ModelLoadRequest & request) const = 0;
126+
127+
/**
128+
* Path-free loader catalog for ``--list-loaders --json``.
129+
* Override ``advertised_capabilities`` (and policy when non-default) on each loader.
130+
*/
131+
virtual CapabilitySet advertised_capabilities() const;
132+
virtual std::string advertised_instructions_policy() const;
133+
virtual std::vector<std::string> advertised_api_endpoints() const;
134+
LoaderAdvertisement advertise() const;
116135
};
117136

118137
} // namespace engine::runtime

‎include/engine/framework/runtime/registry.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class ModelRegistry {
2222
size_t size() const noexcept;
2323
std::vector<std::string> families() const;
2424
bool supports_family(const std::string & family) const noexcept;
25+
/** Path-free loader catalog for ``--list-loaders --json``. */
26+
std::vector<LoaderAdvertisement> advertise_loaders() const;
2527
ModelInspection inspect(const ModelLoadRequest & request) const;
2628
ModelInspection inspect(const std::filesystem::path & model_path) const;
2729
std::unique_ptr<ILoadedVoiceModel> load(const ModelLoadRequest & request) const;

‎src/framework/runtime/model.cpp‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,65 @@ const NamedAsset * select_named_asset(
9696
return &assets.front();
9797
}
9898

99+
std::vector<std::string> default_api_endpoints_for_capabilities(const CapabilitySet & capabilities) {
100+
bool has_asr = false;
101+
bool has_speech = false;
102+
bool has_other = false;
103+
for (const auto & task : capabilities.supported_tasks) {
104+
switch (task.task) {
105+
case VoiceTaskKind::Asr:
106+
has_asr = true;
107+
break;
108+
case VoiceTaskKind::Tts:
109+
case VoiceTaskKind::VoiceCloning:
110+
case VoiceTaskKind::VoiceDesign:
111+
has_speech = true;
112+
break;
113+
default:
114+
has_other = true;
115+
break;
116+
}
117+
}
118+
if (has_asr && !has_speech && !has_other) {
119+
return {"/v1/audio/transcriptions"};
120+
}
121+
if (has_speech && !has_other) {
122+
return {"/v1/audio/speech"};
123+
}
124+
if (has_speech && has_other) {
125+
return {"/v1/tasks/run", "/v1/audio/speech"};
126+
}
127+
return {"/v1/tasks/run"};
128+
}
129+
130+
CapabilitySet IVoiceModelLoader::advertised_capabilities() const {
131+
// Path-free catalog entry. Each loader overrides with the same task/mode
132+
// contract it exposes via inspect/load (without requiring a model path).
133+
return {};
134+
}
135+
136+
std::string IVoiceModelLoader::advertised_instructions_policy() const {
137+
// Generic default from advertised tasks. Loaders with a different contract override.
138+
for (const auto & task : advertised_capabilities().supported_tasks) {
139+
if (task.task == VoiceTaskKind::Tts || task.task == VoiceTaskKind::VoiceDesign
140+
|| task.task == VoiceTaskKind::VoiceCloning) {
141+
return "openai_instruct";
142+
}
143+
}
144+
return "none";
145+
}
146+
147+
std::vector<std::string> IVoiceModelLoader::advertised_api_endpoints() const {
148+
return default_api_endpoints_for_capabilities(advertised_capabilities());
149+
}
150+
151+
LoaderAdvertisement IVoiceModelLoader::advertise() const {
152+
LoaderAdvertisement row;
153+
row.family = family();
154+
row.capabilities = advertised_capabilities();
155+
row.instructions_policy = advertised_instructions_policy();
156+
row.api_endpoints = advertised_api_endpoints();
157+
return row;
158+
}
159+
99160
} // namespace engine::runtime

‎src/framework/runtime/registry.cpp‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@ bool ModelRegistry::supports_family(const std::string & family) const noexcept {
120120
return false;
121121
}
122122

123+
std::vector<LoaderAdvertisement> ModelRegistry::advertise_loaders() const {
124+
std::vector<LoaderAdvertisement> out;
125+
out.reserve(loaders_.size());
126+
for (const auto & loader : loaders_) {
127+
if (loader == nullptr) {
128+
continue;
129+
}
130+
out.push_back(loader->advertise());
131+
}
132+
std::sort(out.begin(), out.end(), [](const LoaderAdvertisement & a, const LoaderAdvertisement & b) {
133+
return a.family < b.family;
134+
});
135+
return out;
136+
}
137+
123138
ModelInspection ModelRegistry::inspect(const ModelLoadRequest & request) const {
124139
engine::assets::ScopedModelPackageSpecOverride spec_override(request.model_spec_override, request.model_path);
125140
validate_request(request);

‎src/models/ace_step/loader.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ class AceStepLoader final : public runtime::IVoiceModelLoader {
9090
return "ace_step";
9191
}
9292

93+
runtime::CapabilitySet advertised_capabilities() const override {
94+
runtime::CapabilitySet out;
95+
out.supported_tasks = {
96+
{runtime::VoiceTaskKind::AudioGeneration, {runtime::RunMode::Offline}},
97+
};
98+
return out;
99+
}
100+
93101
bool can_load(const runtime::ModelLoadRequest & request) const override {
94102
try {
95103
const auto root = resolve_model_root(request.model_path);

‎src/models/chatterbox/loader.cpp‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ class ChatterboxLoader final : public runtime::IVoiceModelLoader {
6666
return "chatterbox";
6767
}
6868

69+
runtime::CapabilitySet advertised_capabilities() const override {
70+
runtime::CapabilitySet out;
71+
out.supported_tasks = {
72+
{runtime::VoiceTaskKind::VoiceCloning, {runtime::RunMode::Offline}},
73+
{runtime::VoiceTaskKind::VoiceConversion, {runtime::RunMode::Offline}},
74+
};
75+
out.supports_speaker_reference = true;
76+
out.supports_style_condition = true;
77+
return out;
78+
}
79+
6980
bool can_load(const runtime::ModelLoadRequest & request) const override {
7081
try {
7182
const auto root = resolve_model_root(request.model_path);

‎src/models/citrinet_asr/session.cpp‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ class CitrinetASRLoader final : public runtime::IVoiceModelLoader {
3737
return "citrinet_asr";
3838
}
3939

40+
runtime::CapabilitySet advertised_capabilities() const override {
41+
runtime::CapabilitySet out;
42+
out.supported_tasks = {
43+
{runtime::VoiceTaskKind::Asr, {runtime::RunMode::Offline}},
44+
};
45+
out.supports_timestamps = true;
46+
return out;
47+
}
48+
4049
bool can_load(const runtime::ModelLoadRequest & request) const override {
4150
if (request.family_hint.has_value() && *request.family_hint != family()) {
4251
return false;

‎src/models/demucs/loader.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ class HTDemucsLoader final : public runtime::IVoiceModelLoader {
4040
return "htdemucs";
4141
}
4242

43+
runtime::CapabilitySet advertised_capabilities() const override {
44+
runtime::CapabilitySet out;
45+
out.supported_tasks = {
46+
{runtime::VoiceTaskKind::SourceSeparation, {runtime::RunMode::Offline}},
47+
};
48+
return out;
49+
}
50+
4351
bool can_load(const runtime::ModelLoadRequest & request) const override {
4452
try {
4553
const auto package_spec = assets::default_model_package_spec_path(family());

‎src/models/heartmula/loader.cpp‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ class HeartMuLaLoader final : public runtime::IVoiceModelLoader {
8484
return "heartmula";
8585
}
8686

87+
runtime::CapabilitySet advertised_capabilities() const override {
88+
runtime::CapabilitySet out;
89+
out.supported_tasks = {
90+
{runtime::VoiceTaskKind::AudioGeneration, {runtime::RunMode::Offline}},
91+
};
92+
out.supports_style_condition = true;
93+
return out;
94+
}
95+
8796
bool can_load(const runtime::ModelLoadRequest & request) const override {
8897
try {
8998
const auto root = resolve_model_root(request.model_path);

0 commit comments

Comments
 (0)