Skip to content

Commit d41e996

Browse files
committed
fix(supertonic): bind graph inputs by tensor name
The converter permutes a graph's input slots away from the signature order, and differently per toolchain version: the published base graphs bind [text_mask, text_ids, style_dp] where a current litert-torch export binds [text_ids, style_dp, text_mask]. The hard-coded slot order therefore broke any re-exported graph at run time ("Failed to register input tensor buffer"), including the L=128 bucket. Resolve the slot per role from the tensor names (`serving_default_args_N`) when a model is loaded, for the duration predictor, the text encoder and every vector_estimator bucket, and keep the published order as the fallback. Output for the published bundle is unchanged (verified byte-identical).
1 parent e1a5889 commit d41e996

2 files changed

Lines changed: 75 additions & 11 deletions

File tree

include/speech_core/models/litert_supertonic_tts.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "speech_core/models/litert_engine.h"
55
#include "speech_core/models/supertonic_tokenizer.h"
66

7+
#include <array>
78
#include <atomic>
89
#include <cstdint>
910
#include <memory>
@@ -111,12 +112,23 @@ class LiteRTSupertonicTts : public TTSInterface {
111112
};
112113
Prepared prepare_chunk(const std::string& text, const std::string& language, bool continuation);
113114

115+
// Physical input slot per signature role. The converter permutes a graph's input slots away
116+
// from the signature order (and differently per toolchain version), so slots are resolved from
117+
// the tensor names (`serving_default_args_N`) at load time; see input_slots() in the .cpp.
118+
// duration_predictor roles: 0 text_ids, 1 style_dp, 2 text_mask
119+
// text_encoder roles: 0 text_ids, 1 style_ttl, 2 text_mask
120+
// vector_estimator roles: 0 noisy, 1 text_emb, 2 style_ttl, 3 latent_mask, 4 text_mask,
121+
// 5 current_step, 6 total_step
122+
using Slots3 = std::array<int, 3>;
123+
using Slots7 = std::array<int, 7>;
124+
114125
// One fixed-L export of the two L-dependent graphs. Handles are raw (freed in destroy_graphs()).
115126
struct LatentBucket {
116127
int frames = 0; // L
117128
std::string vector_path, vocoder_path;
118129
LiteRtModel vector_model = nullptr; LiteRtCompiledModel vector_compiled = nullptr;
119130
LiteRtModel vocoder_model = nullptr; LiteRtCompiledModel vocoder_compiled = nullptr;
131+
Slots7 vector_slots{};
120132
bool failed = false; // load attempted and failed; skipped from then on
121133
bool loaded() const { return vector_compiled && vocoder_compiled; }
122134
};
@@ -134,6 +146,8 @@ class LiteRTSupertonicTts : public TTSInterface {
134146

135147
LiteRtModel duration_model_ = nullptr; LiteRtCompiledModel duration_compiled_ = nullptr;
136148
LiteRtModel encoder_model_ = nullptr; LiteRtCompiledModel encoder_compiled_ = nullptr;
149+
Slots3 duration_slots_{};
150+
Slots3 encoder_slots_{};
137151
std::vector<LatentBucket> buckets_; // ascending frames; [0] = base, loaded in the ctor
138152
bool hw_accel_ = false;
139153

src/models/litert/litert_supertonic_tts.cpp

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,48 @@ void load_style(const std::string& path, std::vector<float>& ttl, std::vector<fl
9292
dp = extract_style(text, "style_dp");
9393
}
9494

95+
// Physical input slot per signature role, parsed from the tensor names of the model's main
96+
// subgraph (`serving_default_args_N[:0]`, N = signature position). ai_edge_torch / litert_torch
97+
// permute the slots away from the signature order, and differently per toolchain version: the
98+
// published base graphs bind [text_mask, text_ids, style_dp] where a current re-export binds
99+
// [text_ids, style_dp, text_mask]. When a name cannot be parsed, or the slots do not form a
100+
// permutation, `legacy` (the published bundle's introspected order) is used.
101+
template <size_t N>
102+
std::array<int, N> input_slots(LiteRtModel model, const std::array<int, N>& legacy, const char* what) {
103+
std::array<int, N> slots;
104+
slots.fill(-1);
105+
LiteRtParamIndex main = 0;
106+
LiteRtSubgraph sg = nullptr;
107+
LiteRtParamIndex n = 0;
108+
if (LiteRtGetMainModelSubgraphIndex(model, &main) == kLiteRtStatusOk &&
109+
LiteRtGetModelSubgraph(model, main, &sg) == kLiteRtStatusOk &&
110+
LiteRtGetNumSubgraphInputs(sg, &n) == kLiteRtStatusOk && n == N) {
111+
for (LiteRtParamIndex i = 0; i < n; ++i) {
112+
LiteRtTensor t = nullptr;
113+
const char* nm = nullptr;
114+
if (LiteRtGetSubgraphInput(sg, i, &t) != kLiteRtStatusOk || !t ||
115+
LiteRtGetTensorName(t, &nm) != kLiteRtStatusOk || !nm) continue;
116+
const std::string name(nm);
117+
const size_t pos = name.find("args_");
118+
if (pos == std::string::npos) continue;
119+
const int role = std::atoi(name.c_str() + pos + 5);
120+
if (role >= 0 && role < static_cast<int>(N) && slots[static_cast<size_t>(role)] < 0)
121+
slots[static_cast<size_t>(role)] = static_cast<int>(i);
122+
}
123+
}
124+
for (int s : slots) {
125+
if (s < 0) {
126+
LOGI("Supertonic: %s input names not parseable; using the published slot order", what);
127+
return legacy;
128+
}
129+
}
130+
return slots;
131+
}
132+
133+
constexpr std::array<int, 3> kLegacyDurationSlots = {1, 2, 0}; // [text_mask, text_ids, style_dp]
134+
constexpr std::array<int, 3> kLegacyEncoderSlots = {1, 2, 0}; // [text_mask, text_ids, style_ttl]
135+
constexpr std::array<int, 7> kLegacyVectorSlots = {3, 6, 1, 2, 5, 0, 4}; // [cur, ttl, lmask, noisy, tot, tmask, emb]
136+
95137
// Near-silence gate for the seam trims below (-46 dBFS; the vocoder's silence floor is well under it).
96138
constexpr float kSeamSilenceGate = 0.005f;
97139

@@ -135,6 +177,8 @@ LiteRTSupertonicTts::LiteRTSupertonicTts(const std::string& duration_path,
135177
auto& engine = LiteRTEngine::get();
136178
engine.load(duration_path, hw_accel, &duration_model_, &duration_compiled_);
137179
engine.load(text_encoder_path, hw_accel, &encoder_model_, &encoder_compiled_);
180+
duration_slots_ = input_slots<3>(duration_model_, kLegacyDurationSlots, "duration_predictor");
181+
encoder_slots_ = input_slots<3>(encoder_model_, kLegacyEncoderSlots, "text_encoder");
138182

139183
// Base bucket: the graphs given explicitly, loaded now. Larger buckets are optional
140184
// `*_L{N}.tflite` siblings, discovered here and loaded on first use (see bucket_for()).
@@ -188,6 +232,7 @@ void LiteRTSupertonicTts::load_bucket(LatentBucket& b) {
188232
auto& engine = LiteRTEngine::get();
189233
engine.load(b.vector_path, hw_accel_, &b.vector_model, &b.vector_compiled);
190234
engine.load(b.vocoder_path, hw_accel_, &b.vocoder_model, &b.vocoder_compiled);
235+
b.vector_slots = input_slots<7>(b.vector_model, kLegacyVectorSlots, "vector_estimator");
191236
}
192237

193238
// Register `vector_estimator_L<N>.tflite` + `vocoder_L<N>.tflite` pairs found next to the base
@@ -391,13 +436,15 @@ LiteRTSupertonicTts::Prepared LiteRTSupertonicTts::prepare_chunk(const std::stri
391436
LiteRtHostBuffer in_dp (env, t_dp, voice.style_dp.size() * sizeof(float), voice.style_dp.data());
392437

393438
// --- 1) duration_predictor → duration[1] ---
394-
// LiteRtRunCompiledModel binds ins[] by the graph's tensor-INDEX order, which the ai_edge_torch
395-
// export permutes away from the (args_0=ids, args_1=style_dp, args_2=text_mask) declaration.
396-
// Introspected order of the published duration_predictor.tflite: [text_mask, text_ids, style_dp].
439+
// LiteRtRunCompiledModel binds ins[] by the graph's input-slot order, which the converter
440+
// permutes away from the (args_0=ids, args_1=style_dp, args_2=text_mask) signature; the slot
441+
// per role was resolved from the tensor names at load (input_slots()).
397442
float duration = 0.0f;
398443
{
399444
LiteRtHostBuffer out(env, t_dur, sizeof(float));
400-
LiteRtTensorBuffer ins[3] = { in_mask.raw(), in_ids.raw(), in_dp.raw() };
445+
const LiteRtTensorBuffer by_role[3] = { in_ids.raw(), in_dp.raw(), in_mask.raw() };
446+
LiteRtTensorBuffer ins[3] = {};
447+
for (size_t r = 0; r < 3; ++r) ins[duration_slots_[r]] = by_role[r];
401448
LiteRtTensorBuffer outs[1] = { out.raw() };
402449
litert_check(LiteRtRunCompiledModel(duration_compiled_, 0, 3, ins, 1, outs),
403450
"duration_predictor Run");
@@ -431,12 +478,13 @@ std::vector<float> LiteRTSupertonicTts::synth_prepared(const Prepared& p, size_t
431478
LiteRtHostBuffer in_mask(env, t_mask, p.tok.mask.size() * sizeof(float), p.tok.mask.data());
432479
LiteRtHostBuffer in_ttl (env, t_ttl, voice.style_ttl.size() * sizeof(float), voice.style_ttl.data());
433480

434-
// --- 2) text_encoder → text_emb[1,256,T] ---
435-
// Introspected tensor-index order of text_encoder.tflite: [text_mask, text_ids, style_ttl].
481+
// --- 2) text_encoder → text_emb[1,256,T] --- (roles: text_ids, style_ttl, text_mask)
436482
std::vector<float> text_emb(static_cast<size_t>(256) * kTextT);
437483
{
438484
LiteRtHostBuffer out(env, t_emb, text_emb.size() * sizeof(float));
439-
LiteRtTensorBuffer ins[3] = { in_mask.raw(), in_ids.raw(), in_ttl.raw() };
485+
const LiteRtTensorBuffer by_role[3] = { in_ids.raw(), in_ttl.raw(), in_mask.raw() };
486+
LiteRtTensorBuffer ins[3] = {};
487+
for (size_t r = 0; r < 3; ++r) ins[encoder_slots_[r]] = by_role[r];
440488
LiteRtTensorBuffer outs[1] = { out.raw() };
441489
litert_check(LiteRtRunCompiledModel(encoder_compiled_, 0, 3, ins, 1, outs),
442490
"text_encoder Run");
@@ -492,10 +540,12 @@ std::vector<float> LiteRTSupertonicTts::synth_prepared(const Prepared& p, size_t
492540
LiteRtHostBuffer in_tot (env, t_step, sizeof(float), &total_step_f);
493541
LiteRtHostBuffer out (env, t_lat, xt.size() * sizeof(float));
494542

495-
// Introspected tensor-index order of vector_estimator.tflite:
496-
// [current_step, style_ttl, latent_mask, noisy, total_step, text_mask, text_emb].
497-
LiteRtTensorBuffer ins[7] = { in_cur.raw(), in_ttl.raw(), in_lmask.raw(),
498-
in_noisy.raw(), in_tot.raw(), in_mask.raw(), in_emb.raw() };
543+
// Roles: noisy, text_emb, style_ttl, latent_mask, text_mask, current_step, total_step —
544+
// placed into the bucket's physical slots (resolved from tensor names at load).
545+
const LiteRtTensorBuffer by_role[7] = { in_noisy.raw(), in_emb.raw(), in_ttl.raw(), in_lmask.raw(),
546+
in_mask.raw(), in_cur.raw(), in_tot.raw() };
547+
LiteRtTensorBuffer ins[7] = {};
548+
for (size_t r = 0; r < 7; ++r) ins[bucket.vector_slots[r]] = by_role[r];
499549
LiteRtTensorBuffer outs[1] = { out.raw() };
500550
litert_check(LiteRtRunCompiledModel(bucket.vector_compiled, 0, 7, ins, 1, outs),
501551
"vector_estimator Run");

0 commit comments

Comments
 (0)