66#include " engine/framework/runtime/registry.h"
77
88#include < algorithm>
9+ #include < chrono>
910#include < cmath>
1011#include < cstdint>
1112#include < sstream>
1213#include < stdexcept>
14+ #include < unordered_map>
1315#include < utility>
1416
1517namespace minitts ::server {
1618namespace {
1719
1820using engine::io::json::Value;
1921
22+ using Clock = std::chrono::steady_clock;
23+
2024std::string json_quote (std::string_view value) {
2125 return engine::io::json::stringify_string (value);
2226}
@@ -108,7 +112,49 @@ std::string base64_encode(const std::vector<uint8_t> & bytes) {
108112 return base64_encode (bytes.data (), bytes.size ());
109113}
110114
111- std::string task_result_json (const engine::runtime::TaskResult & result) {
115+ double elapsed_ms (Clock::time_point started) {
116+ return std::chrono::duration<double , std::milli>(Clock::now () - started).count ();
117+ }
118+
119+ double audio_duration_ms (const engine::runtime::AudioBuffer & audio) {
120+ if (audio.sample_rate <= 0 || audio.channels <= 0 ) {
121+ return 0.0 ;
122+ }
123+ return 1000.0 * static_cast <double >(audio.samples .size ()) /
124+ static_cast <double >(audio.sample_rate * audio.channels );
125+ }
126+
127+ double audio_rtf (double wall_ms, double duration_ms) {
128+ return duration_ms > 0.0 ? wall_ms / duration_ms : 0.0 ;
129+ }
130+
131+ std::string timing_json (double wall_ms) {
132+ std::ostringstream out;
133+ out << " {\" wall_ms\" :" << wall_ms << " }" ;
134+ return out.str ();
135+ }
136+
137+ std::string timing_json (double wall_ms, const engine::runtime::AudioBuffer & audio) {
138+ const double duration_ms = audio_duration_ms (audio);
139+ std::ostringstream out;
140+ out << " {\" wall_ms\" :" << wall_ms
141+ << " ,\" audio_duration_ms\" :" << duration_ms
142+ << " ,\" rtf\" :" << audio_rtf (wall_ms, duration_ms) << " }" ;
143+ return out.str ();
144+ }
145+
146+ std::unordered_map<std::string, std::string> timing_headers (
147+ double wall_ms,
148+ const engine::runtime::AudioBuffer & audio) {
149+ const double duration_ms = audio_duration_ms (audio);
150+ return {
151+ {" X-AudioCPP-Wall-Ms" , std::to_string (wall_ms)},
152+ {" X-AudioCPP-Audio-Duration-Ms" , std::to_string (duration_ms)},
153+ {" X-AudioCPP-RTF" , std::to_string (audio_rtf (wall_ms, duration_ms))},
154+ };
155+ }
156+
157+ std::string task_result_json (const engine::runtime::TaskResult & result, double wall_ms) {
112158 std::ostringstream out;
113159 out << " {" ;
114160 bool first = true ;
@@ -197,6 +243,14 @@ std::string task_result_json(const engine::runtime::TaskResult & result) {
197243 }
198244 out << " ]" ;
199245 }
246+ field (" timing" );
247+ if (result.audio_output .has_value ()) {
248+ out << timing_json (wall_ms, *result.audio_output );
249+ } else if (result.named_audio_outputs .size () == 1 ) {
250+ out << timing_json (wall_ms, result.named_audio_outputs .front ().audio );
251+ } else {
252+ out << timing_json (wall_ms);
253+ }
200254 out << " }" ;
201255 return out.str ();
202256}
@@ -364,37 +418,55 @@ ServerState::LoadedModel & ServerState::require_model(const Value & body) {
364418 return *models_.at (it->second );
365419}
366420
367- engine::runtime::TaskResult ServerState::run_model (
421+ struct ServerState ::TimedTaskResult {
422+ engine::runtime::TaskResult result;
423+ double wall_ms = 0.0 ;
424+ };
425+
426+ ServerState::TimedTaskResult ServerState::run_model (
368427 LoadedModel & model,
369428 const engine::runtime::TaskRequest & request) {
370429 std::lock_guard<std::mutex> lock (model.mutex );
371430 ensure_model_loaded_locked (model);
431+ const auto started = Clock::now ();
372432 model.session ->prepare (engine::runtime::build_preparation_request (request));
373- return model.offline ->run (request);
433+ auto result = model.offline ->run (request);
434+ return TimedTaskResult{std::move (result), elapsed_ms (started)};
374435}
375436
376437HttpResponse ServerState::handle_speech (const std::string & body_text) {
377438 const auto body = engine::io::json::parse (body_text);
378439 auto & model = require_model (body);
379440 const auto request = build_openai_speech_request (body, request_base_);
380- const auto result = run_model (model, request);
381- const auto wav = encode_pcm16_wav (select_audio_output (result));
441+ const auto timed_result = run_model (model, request);
442+ const auto & audio = select_audio_output (timed_result.result );
443+ const auto wav = encode_pcm16_wav (audio);
382444 const auto response_format = engine::io::json::optional_string (body, " response_format" , " wav" );
383445 if (response_format == " json" || response_format == " b64_json" ) {
384- return json_response (" {\" audio\" :" + json_quote (base64_encode (wav)) + " ,\" format\" :\" wav\" }" );
385- }
386- return HttpResponse{200 , " audio/wav" , std::string (reinterpret_cast <const char *>(wav.data ()), wav.size ()), {}};
446+ return json_response (
447+ " {\" audio\" :" + json_quote (base64_encode (wav)) +
448+ " ,\" format\" :\" wav\" ,\" timing\" :" + timing_json (timed_result.wall_ms , audio) + " }" );
449+ }
450+ return HttpResponse{
451+ 200 ,
452+ " audio/wav" ,
453+ std::string (reinterpret_cast <const char *>(wav.data ()), wav.size ()),
454+ timing_headers (timed_result.wall_ms , audio),
455+ };
387456}
388457
389458HttpResponse ServerState::handle_transcription (const std::string & body_text) {
390459 const auto body = engine::io::json::parse (body_text);
391460 auto & model = require_model (body);
392461 const auto request = build_openai_transcription_request (body, request_base_);
393- const auto result = run_model (model, request);
462+ const auto timed_result = run_model (model, request);
463+ const auto & result = timed_result.result ;
394464 if (!result.text_output .has_value ()) {
395465 throw std::runtime_error (" model result did not contain transcript text" );
396466 }
397- return json_response (" {\" text\" :" + json_quote (result.text_output ->text ) + " }" );
467+ return json_response (
468+ " {\" text\" :" + json_quote (result.text_output ->text ) +
469+ " ,\" timing\" :" + timing_json (timed_result.wall_ms ) + " }" );
398470}
399471
400472HttpResponse ServerState::handle_generic_run (const std::string & body_text) {
@@ -404,7 +476,8 @@ HttpResponse ServerState::handle_generic_run(const std::string & body_text) {
404476 const auto request = minitts::cli::build_request_from_json (
405477 request_json != nullptr ? *request_json : body,
406478 request_base_);
407- return json_response (task_result_json (run_model (model, request)));
479+ const auto timed_result = run_model (model, request);
480+ return json_response (task_result_json (timed_result.result , timed_result.wall_ms ));
408481}
409482
410483std::string ServerState::models_json () const {
0 commit comments