| 53 | } |
| 54 | if (const auto * voice_ref = value.find("voice_ref")) { |
| 55 | preset.voice_ref = resolve_path(base, voice_ref->as_string()); |
| 56 | } |
| 57 | if (const auto * reference_text = value.find("reference_text")) { |
| 58 | preset.reference_text = reference_text->as_string(); |
| 59 | } |
| 60 | if (!preset.voice_id.has_value() && !preset.voice_ref.has_value() && !preset.reference_text.has_value()) { |
| 61 | throw std::runtime_error(context + " must set voice_id, voice_ref, or reference_text"); |
| 62 | } |
| 63 | return preset; |
| 64 | } |
| 65 | |
| 66 | // Every live-ingest bound uses 0 to mean "disabled", matching busy_timeout_ms, so |
| 67 | // only a negative value is malformed. Rejected at parse time rather than clamped: |
| 68 | // a negative deadline is a typo, and silently treating it as "no bound" would |
| 69 | // remove a guard the operator believed they had set. |
| 70 | // |
| 71 | // Read and validated by hand rather than through optional_i32/optional_i64, both |
| 72 | // of which are wrong here in two ways. They return the supplied fallback for a |
| 73 | // present-but-wrong-typed field, so `"max_body_bytes": "oops"` in a MODEL override |
| 74 | // would silently record the compiled default as a deliberate override and widen a |
| 75 | // stricter server policy. And optional_i32 narrows to int before anything checks |
| 76 | // the range, so on a 32-bit int a value of 4294967296 becomes 0 — which here means |
| 77 | // "disabled", quietly removing the bound the operator was trying to set. |
| 78 | double live_ingest_number( |
| 79 | const engine::io::json::Value & value, |
| 80 | const char * key, |
| 81 | const std::string & context) { |
| 82 | const auto * field = value.find(key); |
| 83 | if (field == nullptr || !field->is_number()) { |
| 84 | throw std::runtime_error(context + " " + key + " must be a number"); |
| 85 | } |
| 86 | const double parsed = field->as_number(); |
| 87 | constexpr double kMaxSafeJsonInteger = 9007199254740991.0; // 2^53 - 1 |
| 88 | if (std::floor(parsed) != parsed) { |
| 89 | throw std::runtime_error(context + " " + key + " must be an integer"); |
| 90 | } |
| 91 | if (parsed < 0.0) { |
| 92 | throw std::runtime_error(context + " " + key + " must be >= 0 (0 disables the bound)"); |
| 93 | } |
| 94 | if (parsed > kMaxSafeJsonInteger) { |
| 95 | throw std::runtime_error(context + " " + key + " must be <= 2^53 - 1"); |
| 96 | } |
| 97 | return parsed; |
| 98 | } |
| 99 | |
| 100 | int live_ingest_ms( |
| 101 | const engine::io::json::Value & value, |
| 102 | const char * key, |
| 103 | int fallback, |
| 104 | const std::string & context) { |
| 105 | if (value.find(key) == nullptr) { |
| 106 | return fallback; |
| 107 | } |
| 108 | const double parsed = live_ingest_number(value, key, context); |
| 109 | if (parsed > static_cast<double>(std::numeric_limits<int>::max())) { |
| 110 | throw std::runtime_error( |
| 111 | context + " " + key + " must be <= " + std::to_string(std::numeric_limits<int>::max()) + " ms"); |
| 112 | } |