Skip to main content

vecslide_core/
manifest.rs

1use serde::{Deserialize, Serialize};
2
3/// Top-level presentation descriptor. Maps to manifest.yaml inside a .vecslide archive.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub struct Presentation {
7    /// Format version for forward compatibility (e.g. "1.0").
8    #[serde(default = "default_format_version")]
9    pub format_version: String,
10    pub title: String,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub author: Option<String>,
13    /// Short description of the presentation content.
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub description: Option<String>,
16    /// Creation or recording date in ISO 8601 format (e.g. "2026-04-07").
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub date: Option<String>,
19    /// BCP-47 language tag for the presentation language (e.g. "it-IT", "en-US").
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub language: Option<String>,
22    /// Path of the audio file inside the ZIP archive (e.g. "audio/voce.opus").
23    /// `None` for "Light" mode (transcript-only, no recorded audio).
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub audio_track: Option<String>,
26    /// Single .typ source file with `----` separators for all slides.
27    /// Alternative to specifying svg_file/typst_file on each individual slide.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub typst_source: Option<String>,
30    pub slides: Vec<Slide>,
31    /// Editor-only data: highlights and comments. Excluded from the compiled HTML viewer.
32    #[serde(default, skip_serializing_if = "Vec::is_empty")]
33    pub annotations: Vec<Annotation>,
34    /// Synchronized or standalone transcript. Placed last in the manifest to keep
35    /// metadata and slides readable at a glance.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub transcript: Option<Transcript>,
38}
39
40fn default_format_version() -> String {
41    "1.0".into()
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub struct Slide {
47    /// Unique identifier used as the DOM element id (e.g. "slide_01").
48    pub id: String,
49    /// Absolute timestamp from audio start, in milliseconds.
50    pub time_start: u64,
51    /// Path of a pre-existing SVG file inside the ZIP archive (e.g. "vector_assets/01.svg").
52    /// Exactly one of svg_file, typst_file, or typst_inline must be set
53    /// (unless typst_source is set on the Presentation).
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub svg_file: Option<String>,
56    /// Path of an external .typ file inside the ZIP archive to compile to SVG.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub typst_file: Option<String>,
59    /// Inline Typst source to compile to SVG.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub typst_inline: Option<String>,
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub animations: Vec<Animation>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub pointer_trail: Option<PointerTrail>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub transition: Option<TransitionKind>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub struct Animation {
73    /// Id of the SVG element to animate (must match an `id` attribute in the SVG file).
74    pub element_id: String,
75    /// Absolute timestamp when the animation starts, in milliseconds.
76    pub time_start: u64,
77    /// Duration of the animation in milliseconds.
78    pub duration: u64,
79    /// Animation kind identifier (e.g. "fade-in", "draw-path", "slide-left").
80    pub kind: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub struct PointerTrail {
86    pub points: Vec<TrailPoint>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub struct TrailPoint {
92    /// Absolute timestamp in milliseconds.
93    pub time_ms: u64,
94    pub x: f32,
95    pub y: f32,
96}
97
98/// Editor-only annotation attached to a slide. Never included in the compiled HTML viewer.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub struct Annotation {
102    pub slide_id: String,
103    #[serde(flatten)]
104    pub kind: AnnotationType,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(tag = "type", rename_all = "snake_case")]
109pub enum AnnotationType {
110    Highlight {
111        rect: Rect,
112        color: String,
113        #[serde(skip_serializing_if = "Option::is_none")]
114        author_note: Option<String>,
115    },
116    Comment {
117        position: Point,
118        text: String,
119    },
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct Rect {
124    pub x: f32,
125    pub y: f32,
126    pub w: f32,
127    pub h: f32,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct Point {
132    pub x: f32,
133    pub y: f32,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum TransitionKind {
139    Cut,
140    Fade,
141    SlideLeft,
142    SlideRight,
143}
144
145// ── Transcript ────────────────────────────────────────────────────────────────
146
147/// Full transcript of the presentation, placed at the end of the manifest.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub struct Transcript {
151    pub mode: TranscriptMode,
152    /// BCP-47 language tag (e.g. "it-IT", "en-US").
153    pub language: String,
154    pub segments: Vec<TranscriptSegment>,
155}
156
157/// How the transcript is used by the viewer.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum TranscriptMode {
161    /// Highlights are driven by `audio.currentTime`. Requires `audio_track`.
162    Synchronized,
163    /// No audio: viewer uses Web Speech API (TTS) to read segments aloud.
164    Standalone,
165}
166
167/// One spoken phrase, aligned to a time range.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub struct TranscriptSegment {
171    /// Start of the segment in milliseconds (absolute from audio start).
172    pub start_ms: u64,
173    /// End of the segment in milliseconds.
174    pub end_ms: u64,
175    pub text: String,
176    /// ID of the slide visible during this segment (for click-to-slide navigation).
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub slide_ref: Option<String>,
179    /// Word-level timestamps for karaoke-style highlighting (optional).
180    #[serde(default, skip_serializing_if = "Vec::is_empty")]
181    pub words: Vec<TranscriptWord>,
182}
183
184/// Word-level timestamp inside a segment.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "snake_case")]
187pub struct TranscriptWord {
188    pub text: String,
189    pub start_ms: u64,
190    pub end_ms: u64,
191}