| 267 | context.values[key] = value; |
| 268 | } |
| 269 | for (auto & [_, value] : context.values) { |
| 270 | value = expand_value(value, context); |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | void run_batch_inputs_step( |
| 275 | const engine::io::json::Value & step, |
| 276 | WorkflowContext & context) { |
| 277 | const std::string id = workflow_string(step, "id"); |
| 278 | if (workflow_has_field(step, "foreach")) { |
| 279 | throw std::runtime_error("batch_inputs cannot use foreach: " + id); |
| 280 | } |
| 281 | const bool has_audio_dir = workflow_has_field(step, "audio_dir"); |
| 282 | const bool has_text_dir = workflow_has_field(step, "text_dir"); |
| 283 | if ((has_audio_dir ? 1 : 0) + (has_text_dir ? 1 : 0) != 1) { |
| 284 | throw std::runtime_error("batch_inputs requires exactly one of audio_dir or text_dir: " + id); |
| 285 | } |
| 286 | |
| 287 | std::vector<std::filesystem::path> paths; |
| 288 | std::vector<std::unordered_map<std::string, std::string>> items; |
| 289 | if (has_audio_dir) { |
| 290 | const auto dir = resolve_workflow_path(workflow_string(step, "audio_dir"), context); |
| 291 | if (!std::filesystem::is_directory(dir)) { |
| 292 | throw std::runtime_error("batch_inputs audio_dir must be an existing directory: " + dir.string()); |
| 293 | } |
| 294 | for (const auto & entry : std::filesystem::directory_iterator(dir)) { |
| 295 | if (entry.is_regular_file() && is_wav_path(entry.path())) { |
| 296 | paths.push_back(entry.path()); |
| 297 | } |
| 298 | } |
| 299 | std::sort(paths.begin(), paths.end()); |
| 300 | if (paths.empty()) { |
| 301 | throw std::runtime_error("batch_inputs audio_dir contains no .wav files: " + dir.string()); |
| 302 | } |
| 303 | items.reserve(paths.size()); |
| 304 | for (const auto & path : paths) { |
| 305 | items.push_back({ |
| 306 | {"id", safe_workflow_id(path.stem().string())}, |
| 307 | {"audio_path", path.string()}, |
| 308 | }); |
| 309 | } |
| 310 | } else { |
| 311 | const auto dir = resolve_workflow_path(workflow_string(step, "text_dir"), context); |
| 312 | if (!std::filesystem::is_directory(dir)) { |
| 313 | throw std::runtime_error("batch_inputs text_dir must be an existing directory: " + dir.string()); |
| 314 | } |
| 315 | for (const auto & entry : std::filesystem::directory_iterator(dir)) { |
| 316 | if (entry.is_regular_file() && is_text_batch_path(entry.path())) { |
| 317 | paths.push_back(entry.path()); |
| 318 | } |
| 319 | } |
| 320 | std::sort(paths.begin(), paths.end()); |
| 321 | if (paths.empty()) { |
| 322 | throw std::runtime_error("batch_inputs text_dir contains no .txt, .md, or .json files: " + dir.string()); |
| 323 | } |
| 324 | items.reserve(paths.size()); |
| 325 | for (const auto & path : paths) { |
| 326 | const auto text = read_batch_text_payload(path); |
no test coverage detected