Skip to main content

oximedia_workflow/
patterns.rs

1//! Common workflow patterns and templates.
2
3use crate::task::{Task, TaskPriority, TaskType};
4use crate::workflow::Workflow;
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::time::Duration;
8
9/// Watch folder auto-transcode pattern.
10///
11/// Automatically transcodes files that arrive in a watched folder.
12#[allow(dead_code)]
13#[must_use]
14pub fn watch_folder_transcode(
15    input_folder: PathBuf,
16    output_folder: PathBuf,
17    preset: String,
18) -> Workflow {
19    let mut workflow = Workflow::new("watch-folder-transcode")
20        .with_description("Auto-transcode files from watch folder");
21
22    // This would be integrated with file watcher in practice
23    let transcode_task = Task::new(
24        "transcode",
25        TaskType::Transcode {
26            input: input_folder.join("input.mp4"),
27            output: output_folder.join("output.mp4"),
28            preset,
29            params: HashMap::new(),
30        },
31    )
32    .with_priority(TaskPriority::High);
33
34    workflow.add_task(transcode_task);
35    workflow
36}
37
38/// Multi-pass encoding workflow.
39///
40/// Source → Low-res proxy → QC → High-res → Delivery
41#[allow(dead_code)]
42#[must_use]
43pub fn multi_pass_encoding(
44    source: PathBuf,
45    proxy_output: PathBuf,
46    final_output: PathBuf,
47    qc_profile: String,
48) -> Workflow {
49    let mut workflow = Workflow::new("multi-pass-encoding")
50        .with_description("Multi-pass encoding with QC validation");
51
52    // Step 1: Create low-res proxy
53    let proxy_task = Task::new(
54        "create-proxy",
55        TaskType::Transcode {
56            input: source.clone(),
57            output: proxy_output.clone(),
58            preset: "proxy-lowres".to_string(),
59            params: HashMap::new(),
60        },
61    )
62    .with_priority(TaskPriority::High);
63
64    let proxy_id = workflow.add_task(proxy_task);
65
66    // Step 2: QC validation on proxy
67    let qc_task = Task::new(
68        "qc-validation",
69        TaskType::QualityControl {
70            input: proxy_output.clone(),
71            profile: qc_profile,
72            rules: vec!["video_quality".to_string(), "audio_levels".to_string()],
73        },
74    );
75
76    let qc_id = workflow.add_task(qc_task);
77    workflow.add_edge(proxy_id, qc_id).unwrap_or(());
78
79    // Step 3: High-res encoding (if QC passes)
80    let final_task = Task::new(
81        "final-encode",
82        TaskType::Transcode {
83            input: source,
84            output: final_output,
85            preset: "high-quality".to_string(),
86            params: HashMap::new(),
87        },
88    )
89    .with_priority(TaskPriority::Critical)
90    .with_timeout(Duration::from_secs(7200)); // 2 hours
91
92    let final_id = workflow.add_task(final_task);
93    workflow.add_edge(qc_id, final_id).unwrap_or(());
94
95    workflow
96}
97
98/// Validation pipeline pattern.
99///
100/// Upload → Format check → QC → Archive → Notify
101#[allow(dead_code)]
102#[must_use]
103pub fn validation_pipeline(
104    input_file: PathBuf,
105    archive_destination: String,
106    notification_email: Vec<String>,
107) -> Workflow {
108    let mut workflow = Workflow::new("validation-pipeline")
109        .with_description("Complete validation and archival pipeline");
110
111    // Step 1: Format validation
112    let validation_task = Task::new(
113        "format-validation",
114        TaskType::Analysis {
115            input: input_file.clone(),
116            analyses: vec![
117                crate::task::AnalysisType::VideoQuality,
118                crate::task::AnalysisType::AudioLevels,
119            ],
120            output: None,
121        },
122    );
123
124    let validation_id = workflow.add_task(validation_task);
125
126    // Step 2: QC checks
127    let qc_task = Task::new(
128        "quality-check",
129        TaskType::QualityControl {
130            input: input_file.clone(),
131            profile: "broadcast".to_string(),
132            rules: vec![
133                "video_bitrate".to_string(),
134                "audio_channels".to_string(),
135                "format_compliance".to_string(),
136            ],
137        },
138    );
139
140    let qc_id = workflow.add_task(qc_task);
141    workflow.add_edge(validation_id, qc_id).unwrap_or(());
142
143    // Step 3: Archive transfer
144    let archive_task = Task::new(
145        "archive",
146        TaskType::Transfer {
147            source: input_file.to_string_lossy().to_string(),
148            destination: archive_destination,
149            protocol: crate::task::TransferProtocol::S3,
150            options: HashMap::new(),
151        },
152    )
153    .with_timeout(Duration::from_secs(3600));
154
155    let archive_id = workflow.add_task(archive_task);
156    workflow.add_edge(qc_id, archive_id).unwrap_or(());
157
158    // Step 4: Notification
159    let notify_task = Task::new(
160        "notify-completion",
161        TaskType::Notification {
162            channel: crate::task::NotificationChannel::Email {
163                to: notification_email,
164                subject: "Validation Pipeline Completed".to_string(),
165            },
166            message: "File successfully validated and archived".to_string(),
167            metadata: HashMap::new(),
168        },
169    );
170
171    let notify_id = workflow.add_task(notify_task);
172    workflow.add_edge(archive_id, notify_id).unwrap_or(());
173
174    workflow
175}
176
177/// Distribution workflow pattern.
178///
179/// Encode → Multiple outputs (web, mobile, broadcast) → Upload
180#[allow(dead_code)]
181#[must_use]
182pub fn distribution_workflow(
183    source: PathBuf,
184    output_dir: PathBuf,
185    upload_destinations: Vec<String>,
186) -> Workflow {
187    let mut workflow = Workflow::new("distribution-workflow")
188        .with_description("Multi-format distribution pipeline");
189
190    let source_task = Task::new(
191        "source-analysis",
192        TaskType::Analysis {
193            input: source.clone(),
194            analyses: vec![crate::task::AnalysisType::VideoQuality],
195            output: None,
196        },
197    );
198
199    let source_id = workflow.add_task(source_task);
200
201    // Create multiple format outputs
202    let formats = vec![
203        ("web-hd", "1080p-web"),
204        ("web-sd", "720p-web"),
205        ("mobile", "480p-mobile"),
206        ("broadcast", "broadcast-hd"),
207    ];
208
209    let mut encode_ids = Vec::new();
210
211    for (name, preset) in formats {
212        let encode_task = Task::new(
213            format!("encode-{name}"),
214            TaskType::Transcode {
215                input: source.clone(),
216                output: output_dir.join(format!("{name}.mp4")),
217                preset: preset.to_string(),
218                params: HashMap::new(),
219            },
220        )
221        .with_priority(TaskPriority::High);
222
223        let encode_id = workflow.add_task(encode_task);
224        workflow.add_edge(source_id, encode_id).unwrap_or(());
225        encode_ids.push(encode_id);
226    }
227
228    // Upload all outputs
229    for (idx, destination) in upload_destinations.iter().enumerate() {
230        let upload_task = Task::new(
231            format!("upload-{idx}"),
232            TaskType::Transfer {
233                source: output_dir.to_string_lossy().to_string(),
234                destination: destination.clone(),
235                protocol: crate::task::TransferProtocol::S3,
236                options: HashMap::new(),
237            },
238        );
239
240        let upload_id = workflow.add_task(upload_task);
241
242        // Upload depends on all encodes
243        for &encode_id in &encode_ids {
244            workflow.add_edge(encode_id, upload_id).unwrap_or(());
245        }
246    }
247
248    workflow
249}
250
251/// Archive workflow pattern.
252///
253/// Ingest → Validate → Create proxies → Deep archive
254#[allow(dead_code)]
255#[must_use]
256pub fn archive_workflow(
257    source: PathBuf,
258    proxy_dir: PathBuf,
259    archive_destination: String,
260) -> Workflow {
261    let mut workflow = Workflow::new("archive-workflow")
262        .with_description("Complete archival workflow with proxies");
263
264    // Step 1: Ingest and validate
265    let ingest_task = Task::new(
266        "ingest-validate",
267        TaskType::Analysis {
268            input: source.clone(),
269            analyses: vec![
270                crate::task::AnalysisType::VideoQuality,
271                crate::task::AnalysisType::AudioLevels,
272                crate::task::AnalysisType::Color,
273            ],
274            output: Some(proxy_dir.join("analysis.json")),
275        },
276    );
277
278    let ingest_id = workflow.add_task(ingest_task);
279
280    // Step 2: Create multiple proxy formats
281    let proxy_formats = vec![
282        ("proxy-high", "proxy-1080p"),
283        ("proxy-medium", "proxy-720p"),
284        ("proxy-low", "proxy-480p"),
285        ("thumbnail", "thumbnail-grid"),
286    ];
287
288    let mut proxy_ids = Vec::new();
289
290    for (name, preset) in proxy_formats {
291        let proxy_task = Task::new(
292            format!("create-{name}"),
293            TaskType::Transcode {
294                input: source.clone(),
295                output: proxy_dir.join(format!("{name}.mp4")),
296                preset: preset.to_string(),
297                params: HashMap::new(),
298            },
299        );
300
301        let proxy_id = workflow.add_task(proxy_task);
302        workflow.add_edge(ingest_id, proxy_id).unwrap_or(());
303        proxy_ids.push(proxy_id);
304    }
305
306    // Step 3: Deep archive original
307    let archive_task = Task::new(
308        "deep-archive",
309        TaskType::Transfer {
310            source: source.to_string_lossy().to_string(),
311            destination: archive_destination,
312            protocol: crate::task::TransferProtocol::S3,
313            options: {
314                let mut opts = HashMap::new();
315                opts.insert("storage_class".to_string(), "GLACIER".to_string());
316                opts
317            },
318        },
319    )
320    .with_timeout(Duration::from_secs(7200));
321
322    let archive_id = workflow.add_task(archive_task);
323
324    // Archive depends on all proxies being created
325    for proxy_id in proxy_ids {
326        workflow.add_edge(proxy_id, archive_id).unwrap_or(());
327    }
328
329    workflow
330}
331
332/// Parallel processing pattern.
333///
334/// Creates a workflow with parallel independent tasks.
335#[allow(dead_code)]
336#[must_use]
337pub fn parallel_processing(inputs: Vec<PathBuf>, output_dir: PathBuf, preset: String) -> Workflow {
338    let mut workflow =
339        Workflow::new("parallel-processing").with_description("Process multiple files in parallel");
340
341    for (idx, input) in inputs.iter().enumerate() {
342        let task = Task::new(
343            format!("process-{idx}"),
344            TaskType::Transcode {
345                input: input.clone(),
346                output: output_dir.join(format!("output_{idx}.mp4")),
347                preset: preset.clone(),
348                params: HashMap::new(),
349            },
350        )
351        .with_priority(TaskPriority::Normal);
352
353        workflow.add_task(task);
354    }
355
356    workflow
357}
358
359/// Sequential processing pattern.
360///
361/// Creates a linear workflow where each task depends on the previous one.
362#[allow(dead_code)]
363#[must_use]
364pub fn sequential_processing(input: PathBuf, output_dir: PathBuf, stages: Vec<String>) -> Workflow {
365    let mut workflow = Workflow::new("sequential-processing")
366        .with_description("Sequential multi-stage processing");
367
368    let mut previous_id = None;
369    let mut current_input = input;
370
371    for (idx, stage) in stages.iter().enumerate() {
372        let output = output_dir.join(format!("stage_{idx}.mp4"));
373
374        let task = Task::new(
375            format!("stage-{idx}"),
376            TaskType::Transcode {
377                input: current_input.clone(),
378                output: output.clone(),
379                preset: stage.clone(),
380                params: HashMap::new(),
381            },
382        );
383
384        let task_id = workflow.add_task(task);
385
386        if let Some(prev_id) = previous_id {
387            workflow.add_edge(prev_id, task_id).unwrap_or(());
388        }
389
390        previous_id = Some(task_id);
391        current_input = output;
392    }
393
394    workflow
395}
396
397/// Conditional branching pattern.
398///
399/// Executes different paths based on conditions.
400#[allow(dead_code)]
401#[must_use]
402pub fn conditional_workflow(input: PathBuf, output_hq: PathBuf, output_lq: PathBuf) -> Workflow {
403    let mut workflow = Workflow::new("conditional-workflow")
404        .with_description("Conditional processing based on input properties");
405
406    // Analysis task to determine quality
407    let analysis_task = Task::new(
408        "analyze-input",
409        TaskType::Analysis {
410            input: input.clone(),
411            analyses: vec![crate::task::AnalysisType::VideoQuality],
412            output: None,
413        },
414    );
415
416    let analysis_id = workflow.add_task(analysis_task);
417
418    // High quality path
419    let hq_task = Task::new(
420        "high-quality-encode",
421        TaskType::Transcode {
422            input: input.clone(),
423            output: output_hq,
424            preset: "ultra-hq".to_string(),
425            params: HashMap::new(),
426        },
427    )
428    .with_condition("input.bitrate > 10000000"); // Condition example
429
430    let hq_id = workflow.add_task(hq_task);
431    workflow.add_edge(analysis_id, hq_id).unwrap_or(());
432
433    // Low quality path
434    let lq_task = Task::new(
435        "standard-quality-encode",
436        TaskType::Transcode {
437            input,
438            output: output_lq,
439            preset: "standard".to_string(),
440            params: HashMap::new(),
441        },
442    )
443    .with_condition("input.bitrate <= 10000000");
444
445    let lq_id = workflow.add_task(lq_task);
446    workflow.add_edge(analysis_id, lq_id).unwrap_or(());
447
448    workflow
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn test_watch_folder_transcode() {
457        let workflow = watch_folder_transcode(
458            PathBuf::from("/input"),
459            PathBuf::from("/output"),
460            "h264".to_string(),
461        );
462
463        assert_eq!(workflow.name, "watch-folder-transcode");
464        assert_eq!(workflow.tasks.len(), 1);
465    }
466
467    #[test]
468    fn test_multi_pass_encoding() {
469        let workflow = multi_pass_encoding(
470            PathBuf::from("/source.mp4"),
471            PathBuf::from("/proxy.mp4"),
472            PathBuf::from("/final.mp4"),
473            "broadcast".to_string(),
474        );
475
476        assert_eq!(workflow.name, "multi-pass-encoding");
477        assert_eq!(workflow.tasks.len(), 3);
478        assert_eq!(workflow.edges.len(), 2);
479    }
480
481    #[test]
482    fn test_validation_pipeline() {
483        let workflow = validation_pipeline(
484            PathBuf::from("/input.mp4"),
485            "s3://bucket/archive".to_string(),
486            vec!["admin@example.com".to_string()],
487        );
488
489        assert_eq!(workflow.name, "validation-pipeline");
490        assert_eq!(workflow.tasks.len(), 4);
491        assert!(workflow.validate().is_ok());
492    }
493
494    #[test]
495    fn test_distribution_workflow() {
496        let workflow = distribution_workflow(
497            PathBuf::from("/source.mp4"),
498            PathBuf::from("/output"),
499            vec!["s3://cdn1".to_string(), "s3://cdn2".to_string()],
500        );
501
502        assert_eq!(workflow.name, "distribution-workflow");
503        assert!(workflow.tasks.len() >= 5); // source + 4 formats + uploads
504    }
505
506    #[test]
507    fn test_archive_workflow() {
508        let workflow = archive_workflow(
509            PathBuf::from("/source.mp4"),
510            PathBuf::from("/proxies"),
511            "s3://archive".to_string(),
512        );
513
514        assert_eq!(workflow.name, "archive-workflow");
515        assert!(workflow.tasks.len() >= 5); // ingest + proxies + archive
516    }
517
518    #[test]
519    fn test_parallel_processing() {
520        let inputs = vec![
521            PathBuf::from("/input1.mp4"),
522            PathBuf::from("/input2.mp4"),
523            PathBuf::from("/input3.mp4"),
524        ];
525
526        let workflow = parallel_processing(inputs, PathBuf::from("/output"), "h264".to_string());
527
528        assert_eq!(workflow.tasks.len(), 3);
529        assert_eq!(workflow.edges.len(), 0); // No dependencies - parallel
530    }
531
532    #[test]
533    fn test_sequential_processing() {
534        let stages = vec![
535            "denoise".to_string(),
536            "color-correct".to_string(),
537            "stabilize".to_string(),
538        ];
539
540        let workflow = sequential_processing(
541            PathBuf::from("/input.mp4"),
542            PathBuf::from("/output"),
543            stages,
544        );
545
546        assert_eq!(workflow.tasks.len(), 3);
547        assert_eq!(workflow.edges.len(), 2); // Sequential dependencies
548    }
549
550    #[test]
551    fn test_conditional_workflow() {
552        let workflow = conditional_workflow(
553            PathBuf::from("/input.mp4"),
554            PathBuf::from("/output_hq.mp4"),
555            PathBuf::from("/output_lq.mp4"),
556        );
557
558        assert_eq!(workflow.tasks.len(), 3); // analysis + 2 conditional paths
559        assert!(workflow.validate().is_ok());
560    }
561}