1#![allow(dead_code)]
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct OfflineEditConfig {
14 pub proxy_codec: String,
16 pub resolution: (u32, u32),
18 pub bitrate_kbps: u32,
20 pub audio_channels: u8,
22}
23
24impl OfflineEditConfig {
25 #[must_use]
27 pub fn avid_dnxhd_proxy() -> Self {
28 Self {
29 proxy_codec: "dnxhd".to_string(),
30 resolution: (1920, 1080),
31 bitrate_kbps: 36_000,
32 audio_channels: 2,
33 }
34 }
35
36 #[must_use]
38 pub fn prores_proxy() -> Self {
39 Self {
40 proxy_codec: "prores_proxy".to_string(),
41 resolution: (1920, 1080),
42 bitrate_kbps: 45_000,
43 audio_channels: 2,
44 }
45 }
46
47 #[must_use]
49 pub fn h264_proxy() -> Self {
50 Self {
51 proxy_codec: "h264".to_string(),
52 resolution: (1280, 720),
53 bitrate_kbps: 8_000,
54 audio_channels: 2,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct OnlineRelink {
62 pub proxy_id: String,
64 pub master_id: String,
66 pub offset_frames: i64,
68 pub confidence: f32,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74pub enum RelinkStrategy {
75 ExactMatch,
77 FuzzyMatch,
79 ManualApproval,
81}
82
83impl RelinkStrategy {
84 #[must_use]
86 pub fn min_confidence(self) -> f32 {
87 match self {
88 Self::ExactMatch => 1.0,
89 Self::FuzzyMatch => 0.75,
90 Self::ManualApproval => 0.0,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct EditEvent {
98 pub clip_id: String,
100 pub in_point: u64,
102 pub out_point: u64,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct MasterFile {
109 pub id: String,
111 pub path: String,
113 pub duration_frames: u64,
115}
116
117pub struct OfflineEditRelinker {
119 pub strategy: RelinkStrategy,
121}
122
123impl OfflineEditRelinker {
124 #[must_use]
126 pub fn new(strategy: RelinkStrategy) -> Self {
127 Self { strategy }
128 }
129
130 #[must_use]
132 pub fn default_fuzzy() -> Self {
133 Self::new(RelinkStrategy::FuzzyMatch)
134 }
135
136 #[must_use]
143 pub fn relink(
144 &self,
145 proxy_edit: &[EditEvent],
146 master_files: &[MasterFile],
147 ) -> Vec<OnlineRelink> {
148 let min_conf = self.strategy.min_confidence();
149 let mut results = Vec::new();
150
151 for event in proxy_edit {
152 if let Some(master) = master_files.iter().find(|m| m.id == event.clip_id) {
154 let link = OnlineRelink {
155 proxy_id: event.clip_id.clone(),
156 master_id: master.id.clone(),
157 offset_frames: 0,
158 confidence: 1.0,
159 };
160 if link.confidence >= min_conf {
161 results.push(link);
162 continue;
163 }
164 }
165
166 let best = master_files.iter().find_map(|m| {
168 let conf = compute_fuzzy_confidence(&event.clip_id, &m.id);
169 if conf >= min_conf {
170 Some((m, conf))
171 } else {
172 None
173 }
174 });
175
176 if let Some((master, conf)) = best {
177 results.push(OnlineRelink {
178 proxy_id: event.clip_id.clone(),
179 master_id: master.id.clone(),
180 offset_frames: 0,
181 confidence: conf,
182 });
183 }
184 }
185
186 results
187 }
188}
189
190fn compute_fuzzy_confidence(proxy_id: &str, master_id: &str) -> f32 {
192 if proxy_id.is_empty() && master_id.is_empty() {
194 return 0.0;
195 }
196 if proxy_id == master_id {
197 return 1.0;
198 }
199 let common = proxy_id
203 .chars()
204 .zip(master_id.chars())
205 .take_while(|(a, b)| a == b)
206 .count();
207 let min_len = proxy_id.len().min(master_id.len());
208 if min_len == 0 {
209 return 0.0;
210 }
211 let ratio = common as f32 / min_len as f32;
212 (ratio.sqrt() * 0.95).min(0.95)
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct RelinkReport {
219 pub total_clips: u32,
221 pub relinked: u32,
223 pub failed: u32,
225 pub confidence_avg: f32,
227}
228
229impl RelinkReport {
230 #[must_use]
232 pub fn from_results(events: &[EditEvent], relinks: &[OnlineRelink]) -> Self {
233 let total_clips = events.len() as u32;
234 let relinked = relinks.len() as u32;
235 let failed = total_clips.saturating_sub(relinked);
236 let confidence_avg = if relinked == 0 {
237 0.0
238 } else {
239 relinks.iter().map(|r| r.confidence).sum::<f32>() / relinked as f32
240 };
241 Self {
242 total_clips,
243 relinked,
244 failed,
245 confidence_avg,
246 }
247 }
248
249 #[must_use]
251 pub fn success_rate(&self) -> f32 {
252 if self.total_clips == 0 {
253 return 1.0;
254 }
255 self.relinked as f32 / self.total_clips as f32
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_avid_dnxhd_proxy_preset() {
265 let cfg = OfflineEditConfig::avid_dnxhd_proxy();
266 assert_eq!(cfg.proxy_codec, "dnxhd");
267 assert_eq!(cfg.resolution, (1920, 1080));
268 assert_eq!(cfg.bitrate_kbps, 36_000);
269 assert_eq!(cfg.audio_channels, 2);
270 }
271
272 #[test]
273 fn test_prores_proxy_preset() {
274 let cfg = OfflineEditConfig::prores_proxy();
275 assert_eq!(cfg.proxy_codec, "prores_proxy");
276 assert_eq!(cfg.resolution, (1920, 1080));
277 assert_eq!(cfg.bitrate_kbps, 45_000);
278 }
279
280 #[test]
281 fn test_h264_proxy_preset() {
282 let cfg = OfflineEditConfig::h264_proxy();
283 assert_eq!(cfg.proxy_codec, "h264");
284 assert_eq!(cfg.resolution, (1280, 720));
285 assert_eq!(cfg.bitrate_kbps, 8_000);
286 }
287
288 #[test]
289 fn test_relink_strategy_min_confidence() {
290 assert!((RelinkStrategy::ExactMatch.min_confidence() - 1.0).abs() < f32::EPSILON);
291 assert!(RelinkStrategy::FuzzyMatch.min_confidence() < 1.0);
292 assert!((RelinkStrategy::ManualApproval.min_confidence() - 0.0).abs() < f32::EPSILON);
293 }
294
295 #[test]
296 fn test_exact_relink() {
297 let relinker = OfflineEditRelinker::new(RelinkStrategy::ExactMatch);
298 let events = vec![EditEvent {
299 clip_id: "clip_001".to_string(),
300 in_point: 0,
301 out_point: 100,
302 }];
303 let masters = vec![MasterFile {
304 id: "clip_001".to_string(),
305 path: "/media/clip_001.mov".to_string(),
306 duration_frames: 200,
307 }];
308 let relinks = relinker.relink(&events, &masters);
309 assert_eq!(relinks.len(), 1);
310 assert!((relinks[0].confidence - 1.0).abs() < f32::EPSILON);
311 assert_eq!(relinks[0].master_id, "clip_001");
312 }
313
314 #[test]
315 fn test_exact_relink_no_match() {
316 let relinker = OfflineEditRelinker::new(RelinkStrategy::ExactMatch);
317 let events = vec![EditEvent {
318 clip_id: "clip_999".to_string(),
319 in_point: 0,
320 out_point: 50,
321 }];
322 let masters = vec![MasterFile {
323 id: "clip_001".to_string(),
324 path: "/media/clip_001.mov".to_string(),
325 duration_frames: 200,
326 }];
327 let relinks = relinker.relink(&events, &masters);
328 assert_eq!(relinks.len(), 0);
330 }
331
332 #[test]
333 fn test_fuzzy_relink_partial_match() {
334 let relinker = OfflineEditRelinker::new(RelinkStrategy::FuzzyMatch);
335 let events = vec![EditEvent {
336 clip_id: "clip_001_proxy".to_string(),
337 in_point: 0,
338 out_point: 100,
339 }];
340 let masters = vec![MasterFile {
341 id: "clip_001_master".to_string(),
342 path: "/media/clip_001_master.mov".to_string(),
343 duration_frames: 200,
344 }];
345 let relinks = relinker.relink(&events, &masters);
346 assert_eq!(relinks.len(), 1);
348 assert!(relinks[0].confidence >= 0.75);
349 }
350
351 #[test]
352 fn test_manual_approval_strategy_accepts_all() {
353 let relinker = OfflineEditRelinker::new(RelinkStrategy::ManualApproval);
354 let events = vec![EditEvent {
355 clip_id: "xyz".to_string(),
356 in_point: 0,
357 out_point: 10,
358 }];
359 let masters = vec![MasterFile {
360 id: "abc".to_string(),
361 path: "/media/abc.mov".to_string(),
362 duration_frames: 100,
363 }];
364 let relinks = relinker.relink(&events, &masters);
366 assert_eq!(relinks.len(), 1);
368 }
369
370 #[test]
371 fn test_relink_multiple_events() {
372 let relinker = OfflineEditRelinker::new(RelinkStrategy::ExactMatch);
373 let events = vec![
374 EditEvent {
375 clip_id: "a".to_string(),
376 in_point: 0,
377 out_point: 10,
378 },
379 EditEvent {
380 clip_id: "b".to_string(),
381 in_point: 10,
382 out_point: 20,
383 },
384 EditEvent {
385 clip_id: "c".to_string(),
386 in_point: 20,
387 out_point: 30,
388 },
389 ];
390 let masters = vec![
391 MasterFile {
392 id: "a".to_string(),
393 path: "/a.mov".to_string(),
394 duration_frames: 50,
395 },
396 MasterFile {
397 id: "b".to_string(),
398 path: "/b.mov".to_string(),
399 duration_frames: 50,
400 },
401 ];
402 let relinks = relinker.relink(&events, &masters);
403 assert_eq!(relinks.len(), 2); }
405
406 #[test]
407 fn test_relink_report_success_rate() {
408 let events = vec![
409 EditEvent {
410 clip_id: "a".to_string(),
411 in_point: 0,
412 out_point: 10,
413 },
414 EditEvent {
415 clip_id: "b".to_string(),
416 in_point: 10,
417 out_point: 20,
418 },
419 ];
420 let relinks = vec![OnlineRelink {
421 proxy_id: "a".to_string(),
422 master_id: "a".to_string(),
423 offset_frames: 0,
424 confidence: 1.0,
425 }];
426 let report = RelinkReport::from_results(&events, &relinks);
427 assert_eq!(report.total_clips, 2);
428 assert_eq!(report.relinked, 1);
429 assert_eq!(report.failed, 1);
430 assert!((report.success_rate() - 0.5).abs() < f32::EPSILON);
431 }
432
433 #[test]
434 fn test_relink_report_empty() {
435 let report = RelinkReport::from_results(&[], &[]);
436 assert_eq!(report.total_clips, 0);
437 assert!((report.success_rate() - 1.0).abs() < f32::EPSILON);
438 }
439
440 #[test]
441 fn test_relink_report_confidence_avg() {
442 let events = vec![
443 EditEvent {
444 clip_id: "a".to_string(),
445 in_point: 0,
446 out_point: 5,
447 },
448 EditEvent {
449 clip_id: "b".to_string(),
450 in_point: 5,
451 out_point: 10,
452 },
453 ];
454 let relinks = vec![
455 OnlineRelink {
456 proxy_id: "a".to_string(),
457 master_id: "a".to_string(),
458 offset_frames: 0,
459 confidence: 1.0,
460 },
461 OnlineRelink {
462 proxy_id: "b".to_string(),
463 master_id: "b".to_string(),
464 offset_frames: 0,
465 confidence: 0.5,
466 },
467 ];
468 let report = RelinkReport::from_results(&events, &relinks);
469 assert!((report.confidence_avg - 0.75).abs() < f32::EPSILON);
470 }
471
472 #[test]
473 fn test_compute_fuzzy_confidence_identical() {
474 assert!((compute_fuzzy_confidence("abc", "abc") - 1.0).abs() < f32::EPSILON);
475 }
476
477 #[test]
478 fn test_compute_fuzzy_confidence_partial() {
479 let c = compute_fuzzy_confidence("abcXXX", "abcYYY");
480 assert!(c > 0.0 && c < 1.0);
482 }
483
484 #[test]
485 fn test_compute_fuzzy_confidence_empty() {
486 assert!((compute_fuzzy_confidence("", "") - 0.0).abs() < f32::EPSILON);
487 }
488}