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