Skip to main content

honzo_chunks/extra/
sync.rs

1use honzo_core::HonzoError;
2use serde::{Deserialize, Serialize};
3
4pub const NAMESPACE: &str = super::SYNC_NAMESPACE;
5
6/// Represents the type of synchronization
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8#[repr(u8)]
9pub enum SyncType {
10    /// Audio synchronization (text-to-audio)
11    #[default]
12    Audio = 0,
13
14    /// Video synchronization (text-to-video)
15    Video = 1,
16
17    /// Animation synchronization
18    Animation = 2,
19
20    /// Page turn synchronization (for pagination)
21    Page = 3,
22
23    /// Custom synchronization type
24    Custom = 255,
25}
26
27/// Represents a synchronization cue point
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct SyncCue {
30    /// The type of synchronization
31    #[serde(default)]
32    pub sync_type: SyncType,
33
34    /// ID of the chunk this cue applies to
35    pub chunk_id: u32,
36
37    /// Byte offset within the chunk
38    pub offset: u32,
39
40    /// Timestamp in milliseconds (or page number for Page sync type)
41    pub timestamp_ms: u64,
42
43    /// Optional identifier for the sync media
44    #[serde(default)]
45    pub media_id: Option<String>,
46
47    /// Optional duration in milliseconds for this cue
48    #[serde(default)]
49    pub duration_ms: Option<u64>,
50
51    /// Optional custom data for the cue (e.g., JSON metadata)
52    #[serde(default)]
53    pub metadata: Option<SyncMetadata>,
54}
55
56/// Custom metadata for sync cues
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(untagged)]
59pub enum SyncMetadata {
60    /// String metadata
61    String(String),
62
63    /// Number metadata
64    Number(u64),
65
66    /// Boolean metadata
67    Boolean(bool),
68
69    /// Array of values
70    Array(Vec<SyncMetadata>),
71
72    /// Key-value pairs
73    Map(Vec<(String, SyncMetadata)>),
74}
75
76/// Represents a synchronization track (collection of cues for a specific media)
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct SyncTrack {
79    /// Unique identifier for this track
80    pub track_id: String,
81
82    /// Type of synchronization for this track
83    pub track_type: SyncType,
84
85    /// Optional media identifier
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub media_id: Option<String>,
88
89    /// Optional media duration in milliseconds
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub media_duration_ms: Option<u64>,
92
93    /// List of synchronization cues in this track
94    pub cues: Vec<SyncCue>,
95
96    /// Optional track metadata
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub metadata: Option<SyncMetadata>,
99}
100
101/// Represents a complete synchronization document
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct SyncDocument {
104    /// Format version
105    pub version: u8,
106
107    /// List of synchronization tracks
108    pub tracks: Vec<SyncTrack>,
109
110    /// Global metadata
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub metadata: Option<SyncMetadata>,
113}
114
115/// Validates a sync cue
116pub fn validate_cue(cue: &SyncCue) -> Result<(), HonzoError> {
117    if let Some(duration) = cue.duration_ms {
118        if duration == 0 {
119            return Err(HonzoError::InvalidSyncCue);
120        }
121    }
122
123    // For page syncs, validate page number range
124    if cue.sync_type == SyncType::Page && cue.timestamp_ms > 100000 {
125        // Arbitrary max page count (100,000 pages)
126        return Err(HonzoError::InvalidSyncCue);
127    }
128
129    Ok(())
130}
131
132/// Validates a sync track
133pub fn validate_track(track: &SyncTrack) -> Result<(), HonzoError> {
134    // Allow empty cues for now
135    // if track.cues.is_empty() {
136    //     return Err(HonzoError::Truncated);
137    // }
138
139    // Validate all cues in the track
140    for cue in &track.cues {
141        // Ensure cue type matches track type
142        if cue.sync_type != track.track_type && track.track_type != SyncType::Custom {
143            return Err(HonzoError::InvalidSyncCue);
144        }
145
146        validate_cue(cue)?;
147    }
148
149    Ok(())
150}
151
152/// Validates a sync document
153pub fn validate_document(doc: &SyncDocument) -> Result<(), HonzoError> {
154    if doc.version != 1 {
155        return Err(HonzoError::InvalidSyncCue);
156    }
157
158    // Allow empty tracks for now
159    // if doc.tracks.is_empty() {
160    //     return Err(HonzoError::Truncated);
161    // }
162
163    // Validate all tracks
164    for track in &doc.tracks {
165        validate_track(track)?;
166    }
167
168    Ok(())
169}
170
171/// Parses sync cues from binary data (legacy format)
172pub fn parse_sync(body: &[u8]) -> Result<Vec<SyncCue>, HonzoError> {
173    if body.is_empty() {
174        return Ok(Vec::new());
175    }
176
177    let cues: Vec<SyncCue> = rmp_serde::from_slice(body).map_err(|e| {
178        eprintln!("Failed to deserialize sync cues: {:?}", e);
179        HonzoError::Truncated
180    })?;
181
182    // Validate all cues
183    for cue in &cues {
184        if let Err(e) = validate_cue(cue) {
185            eprintln!("Invalid sync cue: {:?}", cue);
186            return Err(e);
187        }
188    }
189
190    Ok(cues)
191}
192
193/// Parses a sync document from binary data
194pub fn parse_sync_document(body: &[u8]) -> Result<SyncDocument, HonzoError> {
195    if body.is_empty() {
196        return Ok(SyncDocument {
197            version: 1,
198            tracks: Vec::new(),
199            metadata: None,
200        });
201    }
202
203    let doc: SyncDocument = rmp_serde::from_slice(body).map_err(|e| {
204        eprintln!("Failed to deserialize sync document: {:?}", e);
205        HonzoError::Truncated
206    })?;
207
208    if let Err(e) = validate_document(&doc) {
209        eprintln!("Invalid sync document: {:?}", doc);
210        return Err(e);
211    }
212
213    Ok(doc)
214}
215
216/// Builds binary data from sync cues (legacy format)
217pub fn build_sync(cues: &[SyncCue]) -> Result<Vec<u8>, HonzoError> {
218    if cues.is_empty() {
219        return Ok(Vec::new());
220    }
221
222    // Validate all cues before building
223    for cue in cues {
224        if let Err(e) = validate_cue(cue) {
225            eprintln!("Invalid sync cue during build: {:?}", cue);
226            return Err(e);
227        }
228    }
229
230    rmp_serde::to_vec_named(cues).map_err(|e| {
231        eprintln!("Failed to serialize sync cues: {:?}", e);
232        HonzoError::Truncated
233    })
234}
235
236/// Builds binary data from a sync document
237pub fn build_sync_document(doc: &SyncDocument) -> Result<Vec<u8>, HonzoError> {
238    if let Err(e) = validate_document(doc) {
239        eprintln!("Invalid sync document during build: {:?}", doc);
240        return Err(e);
241    }
242
243    rmp_serde::to_vec_named(doc).map_err(|e| {
244        eprintln!("Failed to serialize sync document: {:?}", e);
245        HonzoError::Truncated
246    })
247}
248
249/// Creates a new audio sync cue
250pub fn new_audio_cue(chunk_id: u32, offset: u32, timestamp_ms: u64) -> SyncCue {
251    SyncCue {
252        sync_type: SyncType::Audio,
253        chunk_id,
254        offset,
255        timestamp_ms,
256        media_id: None,
257        duration_ms: None,
258        metadata: None,
259    }
260}
261
262/// Creates a new video sync cue
263pub fn new_video_cue(chunk_id: u32, offset: u32, timestamp_ms: u64) -> SyncCue {
264    SyncCue {
265        sync_type: SyncType::Video,
266        chunk_id,
267        offset,
268        timestamp_ms,
269        media_id: None,
270        duration_ms: None,
271        metadata: None,
272    }
273}
274
275/// Creates a new page sync cue (for pagination)
276pub fn new_page_cue(chunk_id: u32, offset: u32, page_number: u32) -> SyncCue {
277    SyncCue {
278        sync_type: SyncType::Page,
279        chunk_id,
280        offset,
281        timestamp_ms: page_number as u64,
282        media_id: Some("page".to_string()),
283        duration_ms: None,
284        metadata: None,
285    }
286}
287
288/// Creates a new media segment cue with duration
289pub fn new_media_segment_cue(
290    sync_type: SyncType,
291    chunk_id: u32,
292    offset: u32,
293    timestamp_ms: u64,
294    duration_ms: u64,
295    media_id: &str,
296) -> SyncCue {
297    SyncCue {
298        sync_type,
299        chunk_id,
300        offset,
301        timestamp_ms,
302        media_id: Some(media_id.to_string()),
303        duration_ms: Some(duration_ms),
304        metadata: None,
305    }
306}
307
308/// Creates a new sync track
309pub fn new_sync_track(
310    track_id: &str,
311    track_type: SyncType,
312    media_id: Option<&str>,
313    media_duration_ms: Option<u64>,
314) -> SyncTrack {
315    SyncTrack {
316        track_id: track_id.to_string(),
317        track_type,
318        media_id: media_id.map(|s| s.to_string()),
319        media_duration_ms,
320        cues: Vec::new(),
321        metadata: None,
322    }
323}
324
325/// Creates a new sync document
326pub fn new_sync_document() -> SyncDocument {
327    SyncDocument {
328        version: 1,
329        tracks: Vec::new(),
330        metadata: None,
331    }
332}
333
334/// Converts sync cues to a more readable format for debugging
335pub fn sync_cues_to_debug_string(cues: &[SyncCue]) -> String {
336    cues.iter()
337        .map(|cue| {
338            format!(
339                "SyncCue {{ type: {:?}, chunk: {}, offset: {}, time: {}ms, media: {:?}, duration: {:?}, metadata: {:?} }}",
340                cue.sync_type,
341                cue.chunk_id,
342                cue.offset,
343                cue.timestamp_ms,
344                cue.media_id,
345                cue.duration_ms,
346                cue.metadata
347            )
348        })
349        .collect::<Vec<_>>()
350        .join("\n")
351}
352
353/// Filters sync cues by type
354pub fn filter_sync_cues(cues: &[SyncCue], sync_type: SyncType) -> Vec<SyncCue> {
355    cues.iter()
356        .filter(|cue| cue.sync_type == sync_type)
357        .cloned()
358        .collect()
359}
360
361/// Filters sync cues by media ID
362pub fn filter_sync_cues_by_media(cues: &[SyncCue], media_id: &str) -> Vec<SyncCue> {
363    cues.iter()
364        .filter(|cue| cue.media_id.as_deref() == Some(media_id))
365        .cloned()
366        .collect()
367}
368
369/// Finds the sync cue closest to a given timestamp
370pub fn find_closest_cue(cues: &[SyncCue], timestamp_ms: u64) -> Option<&SyncCue> {
371    cues.iter()
372        .min_by_key(|cue| cue.timestamp_ms.abs_diff(timestamp_ms))
373}
374
375/// Finds the sync cue for a specific page number
376pub fn find_page_cue(cues: &[SyncCue], page_number: u32) -> Option<&SyncCue> {
377    cues.iter()
378        .find(|cue| cue.sync_type == SyncType::Page && cue.timestamp_ms == page_number as u64)
379}
380
381/// Sorts sync cues by timestamp
382pub fn sort_sync_cues(cues: &mut [SyncCue]) {
383    cues.sort_by_key(|a| a.timestamp_ms);
384}
385
386/// Merges multiple sets of sync cues
387pub fn merge_sync_cues(cues_sets: &[&[SyncCue]]) -> Vec<SyncCue> {
388    let mut merged = Vec::new();
389    for cues in cues_sets {
390        merged.extend_from_slice(cues);
391    }
392    sort_sync_cues(&mut merged);
393    merged
394}
395
396/// Converts legacy sync cues to a sync document
397pub fn legacy_cues_to_document(cues: Vec<SyncCue>) -> SyncDocument {
398    let mut doc = new_sync_document();
399
400    // Group cues by type
401    let mut audio_cues = Vec::new();
402    let mut video_cues = Vec::new();
403    let mut page_cues = Vec::new();
404    let mut custom_cues = Vec::new();
405
406    for cue in cues {
407        match cue.sync_type {
408            SyncType::Audio => audio_cues.push(cue),
409            SyncType::Video => video_cues.push(cue),
410            SyncType::Page => page_cues.push(cue),
411            SyncType::Animation | SyncType::Custom => custom_cues.push(cue),
412        }
413    }
414
415    // Create tracks for each type
416    if !audio_cues.is_empty() {
417        let mut track = new_sync_track("audio", SyncType::Audio, None, None);
418        track.cues = audio_cues;
419        doc.tracks.push(track);
420    }
421
422    if !video_cues.is_empty() {
423        let mut track = new_sync_track("video", SyncType::Video, None, None);
424        track.cues = video_cues;
425        doc.tracks.push(track);
426    }
427
428    if !page_cues.is_empty() {
429        let mut track = new_sync_track("pages", SyncType::Page, Some("page"), None);
430        track.cues = page_cues;
431        doc.tracks.push(track);
432    }
433
434    if !custom_cues.is_empty() {
435        let mut track = new_sync_track("custom", SyncType::Custom, None, None);
436        track.cues = custom_cues;
437        doc.tracks.push(track);
438    }
439
440    doc
441}