1010#include " wav_writer.h"
1111
1212#include < gflags/gflags.h>
13+ #include < nlohmann/json.hpp>
1314
15+ #include < unistd.h>
16+ #include < cerrno>
17+ #include < chrono>
1418#include < cmath>
19+ #include < cstdio>
20+ #include < cstring>
1521#include < filesystem>
1622#include < iostream>
1723#include < limits>
24+ #include < optional>
25+ #include < set>
1826#include < stdexcept>
1927#include < string>
2028
@@ -32,16 +40,254 @@ DEFINE_string(language, "en", "Published Supertonic language tag.");
3240DEFINE_double (speed, 1.05 , " Positive duration speed divisor." );
3341DEFINE_uint64 (seed, 42 , " Portable xorshift64/Box-Muller latent seed." );
3442DEFINE_string (output, " supertonic.wav" , " Output mono PCM16 WAV path." );
43+ DEFINE_bool (
44+ server_jsonl,
45+ false ,
46+ " Keep the model resident and serve JSONL requests on stdin." );
47+ DEFINE_string (
48+ warmup_text,
49+ " Warmup." ,
50+ " Text synthesized and discarded before the JSONL ready event." );
3551
3652namespace {
3753
54+ using Json = nlohmann::ordered_json;
55+
56+ constexpr size_t kMaxJsonLineBytes = 64 * 1024 ;
57+
3858void require_file (const std::filesystem::path& path, const char * flag) {
3959 if (!std::filesystem::is_regular_file (path)) {
4060 throw std::invalid_argument (
4161 std::string (flag) + " does not name a readable file: " + path.string ());
4262 }
4363}
4464
65+ void write_json_line (const Json& value) {
66+ std::cout << value.dump () << ' \n ' ;
67+ std::cout.flush ();
68+ if (!std::cout) {
69+ throw std::runtime_error (" failed to write JSONL response" );
70+ }
71+ }
72+
73+ void write_error (std::optional<uint64_t > id, const std::string& message) {
74+ write_json_line ({
75+ {" type" , " error" },
76+ {" id" , id.has_value () ? Json (*id) : Json (nullptr )},
77+ {" message" , message},
78+ });
79+ }
80+
81+ double elapsed_seconds (std::chrono::steady_clock::time_point started) {
82+ const double elapsed =
83+ std::chrono::duration<double >(std::chrono::steady_clock::now () - started)
84+ .count ();
85+ if (!std::isfinite (elapsed) || elapsed < 0.0 ) {
86+ throw std::runtime_error (" elapsed time is not finite and nonnegative" );
87+ }
88+ return elapsed;
89+ }
90+
91+ std::optional<std::string> validate_output_path (
92+ const std::filesystem::path& output) {
93+ std::error_code error;
94+ if (std::filesystem::exists (output, error)) {
95+ return " output already exists: " + output.string ();
96+ }
97+ if (error) {
98+ return " failed to inspect output path: " + output.string ();
99+ }
100+ const std::filesystem::path parent = output.parent_path ().empty ()
101+ ? std::filesystem::path (" ." )
102+ : output.parent_path ();
103+ if (!std::filesystem::is_directory (parent, error)) {
104+ return " output parent is not an existing directory: " + parent.string ();
105+ }
106+ if (error) {
107+ return " failed to inspect output parent: " + parent.string ();
108+ }
109+ return std::nullopt ;
110+ }
111+
112+ enum class LineReadResult {
113+ Line,
114+ End,
115+ TooLong,
116+ };
117+
118+ LineReadResult read_json_line (std::istream& input, std::string& line) {
119+ line.clear ();
120+ char character;
121+ while (input.get (character)) {
122+ if (character == ' \n ' ) {
123+ return LineReadResult::Line;
124+ }
125+ if (line.size () == kMaxJsonLineBytes ) {
126+ while (input.get (character) && character != ' \n ' ) {
127+ }
128+ return LineReadResult::TooLong;
129+ }
130+ line.push_back (character);
131+ }
132+ return line.empty () ? LineReadResult::End : LineReadResult::Line;
133+ }
134+
135+ void write_wav_without_overwrite (
136+ const std::filesystem::path& output,
137+ const std::vector<float >& waveform,
138+ int sample_rate) {
139+ const std::filesystem::path parent = output.parent_path ().empty ()
140+ ? std::filesystem::path (" ." )
141+ : output.parent_path ();
142+ std::string temporary_template =
143+ (parent / (" ." + output.filename ().string () + " .XXXXXX" )).string ();
144+ int temporary_fd = mkstemp (temporary_template.data ());
145+ if (temporary_fd < 0 ) {
146+ throw std::runtime_error (
147+ " failed to create temporary WAV: " + output.string () + " : " +
148+ std::strerror (errno));
149+ }
150+ const std::filesystem::path temporary (temporary_template);
151+ std::error_code cleanup_error;
152+ try {
153+ const std::string fd_path = " /dev/fd/" + std::to_string (temporary_fd);
154+ if (!supertonic::write_pcm16_wav (fd_path, waveform, sample_rate)) {
155+ throw std::runtime_error (
156+ " failed to write output WAV: " + output.string ());
157+ }
158+ if (close (temporary_fd) != 0 ) {
159+ const int close_error = errno;
160+ temporary_fd = -1 ;
161+ throw std::runtime_error (
162+ " failed to close output WAV: " + output.string () + " : " +
163+ std::strerror (close_error));
164+ }
165+ temporary_fd = -1 ;
166+ if (renamex_np (temporary.c_str (), output.c_str (), RENAME_EXCL ) != 0 ) {
167+ const int rename_error = errno;
168+ if (rename_error == EEXIST ) {
169+ throw std::invalid_argument (
170+ " output already exists: " + output.string ());
171+ }
172+ throw std::runtime_error (
173+ " failed to finalize output WAV: " + output.string () + " : " +
174+ std::strerror (rename_error));
175+ }
176+ } catch (...) {
177+ if (temporary_fd >= 0 ) {
178+ close (temporary_fd);
179+ }
180+ std::filesystem::remove (temporary, cleanup_error);
181+ throw ;
182+ }
183+ }
184+
185+ int run_jsonl_server (
186+ supertonic::SupertonicRunner& runner,
187+ const supertonic::SynthesisOptions& defaults,
188+ double load_seconds) {
189+ supertonic::SynthesisOptions warmup = defaults;
190+ warmup.text = FLAGS_warmup_text;
191+ const auto warmup_started = std::chrono::steady_clock::now ();
192+ (void )runner.synthesize (warmup);
193+ write_json_line ({
194+ {" type" , " ready" },
195+ {" protocol_version" , 1 },
196+ {" sample_rate" , runner.metadata ().sample_rate },
197+ {" load_seconds" , load_seconds},
198+ {" warmup_seconds" , elapsed_seconds (warmup_started)},
199+ });
200+
201+ uint64_t last_request_id = 0 ;
202+ std::string line;
203+ while (true ) {
204+ const LineReadResult line_result = read_json_line (std::cin, line);
205+ if (line_result == LineReadResult::End) {
206+ return 0 ;
207+ }
208+ if (line_result == LineReadResult::TooLong) {
209+ write_error (std::nullopt , " JSON request exceeds 65536 bytes" );
210+ continue ;
211+ }
212+ std::optional<uint64_t > id;
213+ try {
214+ const Json request = Json::parse (line);
215+ if (!request.is_object () || !request.contains (" type" ) ||
216+ !request[" type" ].is_string ()) {
217+ throw std::invalid_argument (" request must be an object with type" );
218+ }
219+ const std::string type = request[" type" ].get <std::string>();
220+ if (type == " shutdown" ) {
221+ if (request.size () != 1 ) {
222+ throw std::invalid_argument (" shutdown request has unexpected fields" );
223+ }
224+ write_json_line ({{" type" , " stopped" }});
225+ return 0 ;
226+ }
227+ if (type != " synthesize" ) {
228+ throw std::invalid_argument (" unknown request type: " + type);
229+ }
230+ if (!request.contains (" id" ) || !request[" id" ].is_number_unsigned () ||
231+ request[" id" ].get <uint64_t >() == 0 ) {
232+ throw std::invalid_argument (" id must be a positive integer" );
233+ }
234+ id = request[" id" ].get <uint64_t >();
235+ static const std::set<std::string> expected{
236+ " type" , " id" , " text" , " output" };
237+ std::set<std::string> actual;
238+ for (auto item = request.begin (); item != request.end (); ++item) {
239+ actual.insert (item.key ());
240+ }
241+ if (actual != expected) {
242+ throw std::invalid_argument (" synthesize request has unexpected fields" );
243+ }
244+ if (!request[" text" ].is_string () ||
245+ request[" text" ].get_ref <const std::string&>().empty ()) {
246+ throw std::invalid_argument (" text must be a nonempty string" );
247+ }
248+ if (!request[" output" ].is_string () ||
249+ request[" output" ].get_ref <const std::string&>().empty ()) {
250+ throw std::invalid_argument (" output must be a nonempty string" );
251+ }
252+ if (*id <= last_request_id) {
253+ throw std::invalid_argument (
254+ " id must increase monotonically above " +
255+ std::to_string (last_request_id));
256+ }
257+ last_request_id = *id;
258+
259+ const std::filesystem::path output = request[" output" ].get <std::string>();
260+ if (const auto error = validate_output_path (output)) {
261+ throw std::invalid_argument (*error);
262+ }
263+ supertonic::SynthesisOptions options = defaults;
264+ options.text = request[" text" ].get <std::string>();
265+ const auto result = runner.synthesize (options);
266+ write_wav_without_overwrite (
267+ output,
268+ result.waveform ,
269+ static_cast <int >(runner.metadata ().sample_rate ));
270+ const double audio_seconds = static_cast <double >(result.waveform .size ()) /
271+ runner.metadata ().sample_rate ;
272+ write_json_line ({
273+ {" type" , " result" },
274+ {" id" , *id},
275+ {" output" , output.string ()},
276+ {" samples" , result.waveform .size ()},
277+ {" audio_seconds" , audio_seconds},
278+ {" synthesis_seconds" , result.elapsed_seconds },
279+ {" rtf" ,
280+ audio_seconds > 0.0 ? result.elapsed_seconds / audio_seconds : 0.0 },
281+ });
282+ } catch (const Json::parse_error&) {
283+ write_error (std::nullopt , " malformed JSON request" );
284+ } catch (const std::exception& error) {
285+ std::cerr << " Supertonic JSONL request failed: " << error.what () << ' \n ' ;
286+ write_error (id, error.what ());
287+ }
288+ }
289+ }
290+
45291} // namespace
46292
47293int main (int argc, char ** argv) {
@@ -50,9 +296,14 @@ int main(int argc, char** argv) {
50296 gflags::ParseCommandLineFlags (&argc, &argv, true );
51297 try {
52298 if (FLAGS_pte.empty () || FLAGS_asset_dir.empty () ||
53- FLAGS_voice_style.empty () || FLAGS_text.empty ()) {
299+ FLAGS_voice_style.empty () ||
300+ (!FLAGS_server_jsonl && FLAGS_text.empty ())) {
54301 throw std::invalid_argument (
55- " --pte, --asset_dir, --voice_style, and --text are required" );
302+ " --pte, --asset_dir, and --voice_style are required; --text is "
303+ " required unless --server_jsonl is set" );
304+ }
305+ if (FLAGS_server_jsonl && FLAGS_warmup_text.empty ()) {
306+ throw std::invalid_argument (" --warmup_text must not be empty" );
56307 }
57308 const std::filesystem::path pte (FLAGS_pte);
58309 const std::filesystem::path indexer =
@@ -70,14 +321,20 @@ int main(int argc, char** argv) {
70321 }
71322 require_file (style, " --voice_style" );
72323
324+ const auto load_started = std::chrono::steady_clock::now ();
73325 supertonic::SupertonicRunner runner (pte.string (), indexer.string ());
326+ const double load_seconds = elapsed_seconds (load_started);
74327
75328 supertonic::SynthesisOptions options;
76329 options.text = FLAGS_text;
77330 options.language = FLAGS_language;
78331 options.voice_style_paths = {style};
79332 options.speed = static_cast <float >(FLAGS_speed);
80333 options.seed = FLAGS_seed;
334+ if (FLAGS_server_jsonl) {
335+ return run_jsonl_server (runner, options, load_seconds);
336+ }
337+
81338 auto result = runner.synthesize (options);
82339 if (!supertonic::write_pcm16_wav (
83340 FLAGS_output,
0 commit comments