| 714 | } |
| 715 | std::ofstream out(output); |
| 716 | out << srt; |
| 717 | context.values[id + ".srt_path"] = output.string(); |
| 718 | std::cout << "workflow_step=" << id << "\n"; |
| 719 | std::cout << "srt_out=" << output.string() << "\n"; |
| 720 | } |
| 721 | |
| 722 | void run_chunked_model_step( |
| 723 | const engine::runtime::ModelRegistry & registry, |
| 724 | const engine::io::json::Value & step, |
| 725 | const WorkflowRunOptions & options, |
| 726 | WorkflowContext & context) { |
| 727 | const std::string id = workflow_string(step, "id"); |
| 728 | const auto request_value = step.find("request"); |
| 729 | if (request_value == nullptr || request_value->is_null()) { |
| 730 | throw std::runtime_error("chunked_model step requires request object: " + id); |
| 731 | } |
| 732 | const std::string audio_key = workflow_string_or(step, "audio_key", "audio"); |
| 733 | engine::audio::WavData input_wav; |
| 734 | if (workflow_has_field(*request_value, "audio_data")) { |
| 735 | auto input_audio_data = workflow_buffer(*request_value, "audio_data"); |
| 736 | input_wav = engine::audio::read_wav_f32(input_audio_data); |
| 737 | } else { |
| 738 | const auto input_audio_path = resolve_workflow_path(workflow_string(*request_value, audio_key), context); |
| 739 | input_wav = engine::audio::read_wav_f32(input_audio_path); |
| 740 | } |
| 741 | if (input_wav.channels <= 0 || input_wav.sample_rate <= 0 || input_wav.samples.empty()) { |
| 742 | throw std::runtime_error("chunked_model input audio is invalid: " + id); |
| 743 | } |
| 744 | const int64_t total_frames = static_cast<int64_t>(input_wav.samples.size() / static_cast<size_t>(input_wav.channels)); |
| 745 | const double chunk_seconds = workflow_optional_number(step, "chunk_seconds").value_or(30.0); |
| 746 | const double overlap_seconds = workflow_optional_number(step, "overlap_seconds").value_or(0.0); |
| 747 | if (!(chunk_seconds > 0.0) || overlap_seconds != 0.0) { |
| 748 | throw std::runtime_error("chunked_model requires chunk_seconds > 0 and overlap_seconds == 0"); |
| 749 | } |
| 750 | const int64_t chunk_frames = static_cast<int64_t>(chunk_seconds * static_cast<double>(input_wav.sample_rate)); |
| 751 | const int64_t step_frames = chunk_frames; |
| 752 | if (chunk_frames <= 0 || step_frames <= 0) { |
| 753 | throw std::runtime_error("chunked_model computed invalid chunk frame count"); |
| 754 | } |
| 755 | std::optional<engine::audio::WavData> timing_wav; |
| 756 | if (const auto timing_audio = workflow_optional_string(step, "timing_audio")) { |
| 757 | timing_wav = engine::audio::read_wav_f32(resolve_workflow_path(*timing_audio, context)); |
| 758 | if (timing_wav->sample_rate != input_wav.sample_rate || timing_wav->channels != input_wav.channels) { |
| 759 | throw std::runtime_error("chunked_model timing_audio must match the source sample rate and channel count"); |
| 760 | } |
| 761 | const int64_t timing_frames = |
| 762 | static_cast<int64_t>(timing_wav->samples.size() / static_cast<size_t>(timing_wav->channels)); |
| 763 | if (timing_frames < total_frames) { |
| 764 | throw std::runtime_error("chunked_model timing_audio is shorter than the source audio"); |
| 765 | } |
| 766 | } |
| 767 | const bool use_timing_repaint_window = workflow_optional_bool(step, "set_repaint_window_from_timing", false); |
| 768 | if (use_timing_repaint_window && !timing_wav.has_value()) { |
| 769 | throw std::runtime_error("chunked_model set_repaint_window_from_timing requires timing_audio"); |
| 770 | } |
| 771 | const float timing_threshold_dbfs = |
| 772 | static_cast<float>(workflow_optional_number(step, "timing_threshold_dbfs").value_or(-40.0)); |
| 773 | const double timing_window_seconds = workflow_optional_number(step, "timing_window_seconds").value_or(0.1); |
no test coverage detected