1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone)]
9pub struct NarrationStyle {
10 pub role: String,
12 pub persona: String,
14 pub length: String,
16 pub tone: String,
18 pub structure: String,
20}
21
22impl Default for NarrationStyle {
23 fn default() -> Self {
24 Self {
25 role: "news broadcast scriptwriter".into(),
26 persona: "a news anchor".into(),
27 length: "2-4 paragraphs, 30-90 seconds when read aloud".into(),
28 tone: "Conversational and engaging".into(),
29 structure: "Start with the key headline/finding, then provide context".into(),
30 }
31 }
32}
33
34impl NarrationStyle {
35 pub fn role(mut self, role: impl Into<String>) -> Self {
36 self.role = role.into();
37 self
38 }
39
40 pub fn persona(mut self, persona: impl Into<String>) -> Self {
41 self.persona = persona.into();
42 self
43 }
44
45 pub fn length(mut self, length: impl Into<String>) -> Self {
46 self.length = length.into();
47 self
48 }
49
50 pub fn tone(mut self, tone: impl Into<String>) -> Self {
51 self.tone = tone.into();
52 self
53 }
54
55 pub fn structure(mut self, structure: impl Into<String>) -> Self {
56 self.structure = structure.into();
57 self
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct CaptionSegment {
67 pub text: String,
69 pub start_ms: u64,
71 pub duration_ms: u64,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(tag = "type", content = "value")]
78pub enum MediaSource {
79 Url(String),
81 FilePath(String),
83 Bytes(Vec<u8>),
85}
86
87impl MediaSource {
88 pub fn display_short(&self) -> String {
90 match self {
91 MediaSource::Url(u) => {
92 if u.len() > 80 {
93 let truncated: String = u.chars().take(80).collect();
94 format!("{truncated}…")
95 } else {
96 u.clone()
97 }
98 }
99 MediaSource::FilePath(p) => p.clone(),
100 MediaSource::Bytes(b) => format!("<{} bytes>", b.len()),
101 }
102 }
103}
104
105impl From<&str> for MediaSource {
106 fn from(s: &str) -> Self {
107 if s.starts_with("http://") || s.starts_with("https://") {
108 MediaSource::Url(s.to_string())
109 } else {
110 MediaSource::FilePath(s.to_string())
111 }
112 }
113}
114
115impl From<String> for MediaSource {
116 fn from(s: String) -> Self {
117 if s.starts_with("http://") || s.starts_with("https://") {
118 MediaSource::Url(s)
119 } else {
120 MediaSource::FilePath(s)
121 }
122 }
123}
124
125impl From<Vec<u8>> for MediaSource {
126 fn from(data: Vec<u8>) -> Self {
127 MediaSource::Bytes(data)
128 }
129}
130
131#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
133#[serde(rename_all = "snake_case")]
134#[non_exhaustive]
135pub enum MediaKind {
136 #[default]
137 Image,
138 Video,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct MediaSegment {
146 pub source: MediaSource,
148 pub start_ms: f64,
150 pub end_ms: f64,
152 #[serde(default)]
154 pub kind: MediaKind,
155}
156
157#[derive(Debug, Clone)]
159pub struct TtsResult {
160 pub audio: Vec<u8>,
162 pub captions: Vec<CaptionSegment>,
164}
165
166#[derive(Debug, Clone)]
168pub struct KeywordResult {
169 pub keywords: Vec<String>,
171}
172
173#[derive(Debug, Clone)]
175pub struct ContentOutput {
176 pub narration: String,
178 pub audio: Vec<u8>,
180 pub captions: Vec<CaptionSegment>,
182 pub media_segments: Vec<MediaSegment>,
184 pub audio_path: Option<String>,
186 pub video_path: Option<String>,
188}
189
190#[derive(Debug, Clone)]
206pub struct AudioTrack {
207 pub path: String,
209 pub volume: f32,
211 pub start_ms: Option<u64>,
213 pub end_ms: Option<u64>,
215 pub loop_track: bool,
217}
218
219impl AudioTrack {
220 pub fn new(path: impl Into<String>) -> Self {
221 Self {
222 path: path.into(),
223 volume: 0.3,
224 start_ms: None,
225 end_ms: None,
226 loop_track: true,
227 }
228 }
229
230 pub fn volume(mut self, volume: f32) -> Self {
231 self.volume = volume.clamp(0.0, 1.0);
232 self
233 }
234
235 pub fn start_at(mut self, ms: u64) -> Self {
236 self.start_ms = Some(ms);
237 self
238 }
239
240 pub fn end_at(mut self, ms: u64) -> Self {
241 self.end_ms = Some(ms);
242 self
243 }
244
245 pub fn no_loop(mut self) -> Self {
246 self.loop_track = false;
247 self
248 }
249}
250
251#[derive(Debug, Clone)]
253#[non_exhaustive]
254pub enum ContentSource {
255 Text(String),
257 ArticleUrl { url: String, title: Option<String> },
259 SearchQuery(String),
261}
262
263#[derive(Debug, Clone)]
277pub struct MediaAsset {
278 pub source: MediaSource,
280 pub description: String,
283 pub kind: MediaKind,
285}
286
287impl MediaAsset {
288 pub fn image(source: impl Into<MediaSource>, description: impl Into<String>) -> Self {
290 Self {
291 source: source.into(),
292 description: description.into(),
293 kind: MediaKind::Image,
294 }
295 }
296
297 pub fn video(source: impl Into<MediaSource>, description: impl Into<String>) -> Self {
299 Self {
300 source: source.into(),
301 description: description.into(),
302 kind: MediaKind::Video,
303 }
304 }
305
306 pub fn image_bytes(data: Vec<u8>, description: impl Into<String>) -> Self {
308 Self {
309 source: MediaSource::Bytes(data),
310 description: description.into(),
311 kind: MediaKind::Image,
312 }
313 }
314
315 pub fn video_bytes(data: Vec<u8>, description: impl Into<String>) -> Self {
317 Self {
318 source: MediaSource::Bytes(data),
319 description: description.into(),
320 kind: MediaKind::Video,
321 }
322 }
323}
324
325#[derive(Debug, Clone, Copy, Default)]
327#[non_exhaustive]
328pub enum MediaFallback {
329 #[default]
332 StockSearch,
333 Skip,
335}
336
337#[derive(Debug, Clone)]
339pub struct TimedChunk {
340 pub text: String,
342 pub start_ms: f64,
344 pub end_ms: f64,
346}
347
348#[derive(Debug, Clone)]
350#[non_exhaustive]
351pub enum PipelineProgress {
352 NarrationStarted,
353 NarrationComplete { narration_len: usize },
354 TextTransformStarted,
355 TextTransformComplete { narration_len: usize },
356 TtsStarted,
357 TtsComplete { audio_bytes: usize, caption_count: usize },
358 MediaSearchStarted { chunk_count: usize },
359 MediaSegmentFound { index: usize, kind: MediaKind },
360 MediaSearchComplete { segment_count: usize },
361 AudioStorageStarted,
362 AudioStored { path: String },
363 RenderStarted,
364 RenderComplete { path: String },
365}