1use crate::egemma2::{EmbeddingGemma2, TextInput};
34use crate::egemma2_audio::{self as ea, AudioTower};
35use crate::egemma2_vision::{VisionInput, VisionProcessor, VisionTower};
36use crate::media::RgbFrame;
37use crate::pool::Pool;
38use cortiq_core::CmfModel;
39use serde_json::Value;
40use std::sync::{Arc, OnceLock};
41
42pub const IMAGE_PLACEHOLDER: &str = "<|image|>";
43pub const VIDEO_PLACEHOLDER: &str = "<|video|>";
44pub const AUDIO_PLACEHOLDER: &str = ea::PLACEHOLDER;
45
46#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct SpecialIds {
49 pub image: u32,
50 pub video: u32,
51 pub boi: u32,
52 pub eoi: u32,
53 pub audio: u32,
54 pub boa: u32,
55 pub eoa: u32,
56}
57
58impl Default for SpecialIds {
59 fn default() -> Self {
60 SpecialIds {
61 image: 258_880,
62 video: 258_884,
63 boi: 255_999,
64 eoi: 258_882,
65 audio: ea::AUDIO_TOKEN,
66 boa: ea::BOA_TOKEN,
67 eoa: ea::EOA_TOKEN,
68 }
69 }
70}
71
72impl SpecialIds {
73 pub fn from_config(cfg: &Value) -> Self {
74 let d = SpecialIds::default();
75 let g = |k: &str, v: u32| cfg.get(k).and_then(|x| x.as_u64()).map_or(v, |x| x as u32);
76 SpecialIds {
77 image: g("image_token_id", d.image),
78 video: g("video_token_id", d.video),
79 boi: g("boi_token_id", d.boi),
80 eoi: g("eoi_token_id", d.eoi),
81 audio: g("audio_token_id", d.audio),
82 boa: g("boa_token_id", d.boa),
83 eoa: g("eoa_token_id", g("eoa_token_index", d.eoa)),
85 }
86 }
87}
88
89#[derive(Clone, Debug)]
91pub enum Media {
92 Image(VisionInput),
93 Video(Vec<VisionInput>),
95 Audio(Vec<f32>),
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
101enum Kind {
102 Image,
103 Video,
104 Audio,
105}
106
107impl Kind {
108 const ALL: [Kind; 3] = [Kind::Image, Kind::Video, Kind::Audio];
109
110 fn placeholder(self) -> &'static str {
111 match self {
112 Kind::Image => IMAGE_PLACEHOLDER,
113 Kind::Video => VIDEO_PLACEHOLDER,
114 Kind::Audio => AUDIO_PLACEHOLDER,
115 }
116 }
117}
118
119impl Media {
120 fn kind(&self) -> Kind {
121 match self {
122 Media::Image(_) => Kind::Image,
123 Media::Video(_) => Kind::Video,
124 Media::Audio(_) => Kind::Audio,
125 }
126 }
127
128 fn vision_rows(&self) -> usize {
130 match self {
131 Media::Image(_) => 1,
132 Media::Video(f) => f.len(),
133 Media::Audio(_) => 0,
134 }
135 }
136}
137
138#[derive(Clone, Debug, Default)]
140pub struct MixedInput {
141 pub text: String,
142 pub media: Vec<Media>,
143 pub prompt_name: Option<String>,
144 pub title: Option<String>,
145 pub prompt: Option<String>,
146}
147
148impl MixedInput {
149 pub fn text(t: TextInput) -> Self {
150 MixedInput {
151 text: t.text,
152 media: Vec::new(),
153 prompt_name: t.prompt_name,
154 title: t.title,
155 prompt: t.prompt,
156 }
157 }
158
159 pub fn image(img: VisionInput) -> Self {
160 MixedInput {
161 media: vec![Media::Image(img)],
162 ..Default::default()
163 }
164 }
165
166 pub fn video(frames: Vec<VisionInput>) -> Self {
167 MixedInput {
168 media: vec![Media::Video(frames)],
169 ..Default::default()
170 }
171 }
172
173 pub fn audio(wave: Vec<f32>) -> Self {
175 MixedInput {
176 media: vec![Media::Audio(wave)],
177 ..Default::default()
178 }
179 }
180}
181
182pub fn layout_text(text: &str, media: &[Media]) -> Result<String, String> {
188 let found: Vec<usize> = Kind::ALL
189 .iter()
190 .map(|k| text.matches(k.placeholder()).count())
191 .collect();
192 let want: Vec<usize> = Kind::ALL
193 .iter()
194 .map(|&k| media.iter().filter(|m| m.kind() == k).count())
195 .collect();
196 if found.iter().all(|&n| n == 0) {
197 let mut kinds: Vec<Kind> = media.iter().map(Media::kind).collect();
198 kinds.sort();
199 let mut parts: Vec<&str> = kinds.iter().map(|k| k.placeholder()).collect();
200 if !text.is_empty() {
201 parts.push(text);
202 }
203 return Ok(parts.join(" "));
204 }
205 if found != want {
206 return Err(format!(
207 "the text holds {} {IMAGE_PLACEHOLDER}, {} {VIDEO_PLACEHOLDER} and {} \
208 {AUDIO_PLACEHOLDER} but {} image(s), {} video(s) and {} audio clip(s) were given",
209 found[0], found[1], found[2], want[0], want[1], want[2]
210 ));
211 }
212 Ok(text.to_string())
213}
214
215fn placeholder_order(text: &str) -> Vec<Kind> {
217 let mut out = Vec::new();
218 let mut rest = text;
219 loop {
220 let next = Kind::ALL
221 .iter()
222 .filter_map(|&k| rest.find(k.placeholder()).map(|at| (at, k)))
223 .min();
224 let Some((at, k)) = next else { break };
225 out.push(k);
226 rest = &rest[at + k.placeholder().len()..];
227 }
228 out
229}
230
231#[derive(Clone, Debug, Default, PartialEq)]
235pub struct Placement {
236 pub vision: Vec<usize>,
237 pub audio: Vec<usize>,
238}
239
240pub struct MediaEncoder {
243 pub text: EmbeddingGemma2,
244 pub proc: VisionProcessor,
245 pub ids: SpecialIds,
246 model: Arc<CmfModel>,
247 pool: Option<Arc<Pool>>,
248 vision: OnceLock<Result<VisionTower, String>>,
249 audio: OnceLock<Result<AudioTower, String>>,
250}
251
252impl MediaEncoder {
253 pub fn load(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
254 let text = EmbeddingGemma2::load(model, pool.clone())?;
255 let prov = model.header.provenance.clone().unwrap_or(Value::Null);
256 let eg = &prov["embedding_gemma2"];
257 Ok(MediaEncoder {
258 text,
259 proc: VisionProcessor::from_config(&eg["processor"]),
260 ids: SpecialIds::from_config(&eg["config"]),
261 model: model.clone(),
262 pool,
263 vision: OnceLock::new(),
264 audio: OnceLock::new(),
265 })
266 }
267
268 pub fn has_vision(&self) -> bool {
270 VisionTower::present(&self.model)
271 }
272
273 pub fn vision(&self) -> Result<&VisionTower, String> {
275 self.vision
276 .get_or_init(|| VisionTower::load(&self.model, self.pool.clone()))
277 .as_ref()
278 .map_err(|e| e.clone())
279 }
280
281 pub fn warm_vision(&self) -> Result<(), String> {
283 self.vision().map(|_| ())
284 }
285
286 pub fn has_audio(&self) -> bool {
288 ea::has_audio(&self.model)
289 }
290
291 pub fn audio(&self) -> Result<&AudioTower, String> {
293 self.audio
294 .get_or_init(|| {
295 if !self.has_audio() {
296 return Err("this file carries no audio tower (a text-only pack?)".into());
297 }
298 AudioTower::load(&self.model, self.pool.clone())
299 })
300 .as_ref()
301 .map_err(|e| e.clone())
302 }
303
304 pub fn warm_audio(&self) -> Result<(), String> {
306 self.audio().map(|_| ())
307 }
308
309 pub fn warm_for(&self, inputs: &[MixedInput]) -> Result<(), String> {
312 let media = || inputs.iter().flat_map(|x| x.media.iter());
313 if media().any(|m| m.vision_rows() > 0) {
314 self.warm_vision()?;
315 }
316 if media().any(|m| matches!(m, Media::Audio(_))) {
317 self.warm_audio()?;
318 }
319 Ok(())
320 }
321
322 pub fn prepare_image(&self, img: &RgbFrame, budget: Option<usize>) -> Result<Media, String> {
324 Ok(Media::Image(self.proc.prepare_image(
325 img,
326 budget.unwrap_or(self.proc.image_budget),
327 )?))
328 }
329
330 pub fn prepare_frames(
332 &self,
333 frames: &[RgbFrame],
334 budget: Option<usize>,
335 ) -> Result<Media, String> {
336 Ok(Media::Video(self.proc.prepare_frames(
337 frames,
338 budget.unwrap_or(self.proc.video_budget),
339 )?))
340 }
341
342 pub fn input_ids(&self, input: &MixedInput) -> Result<Vec<u32>, String> {
344 Ok(self.layout(input)?.0)
345 }
346
347 pub fn layout(&self, input: &MixedInput) -> Result<(Vec<u32>, Placement), String> {
350 if input.media.is_empty() {
351 let ids = self.text.input_ids(&TextInput {
352 text: input.text.clone(),
353 prompt_name: input.prompt_name.clone(),
354 title: input.title.clone(),
355 prompt: input.prompt.clone(),
356 })?;
357 return Ok((ids, Placement::default()));
358 }
359 let k = self.proc.pool_k;
360 let laid = layout_text(&input.text, &input.media)?;
361 let full = if input.prompt_name.is_some() || input.title.is_some() || input.prompt.is_some()
362 {
363 self.text.prompts().format(
364 &laid,
365 input.prompt_name.as_deref(),
366 input.title.as_deref(),
367 input.prompt.as_deref(),
368 )?
369 } else {
370 laid
371 };
372 let mut first = Vec::with_capacity(input.media.len());
374 let (mut r, mut a) = (0usize, 0usize);
375 for m in &input.media {
376 match m {
377 Media::Audio(_) => {
378 first.push(a);
379 a += 1;
380 }
381 _ => {
382 first.push(r);
383 r += m.vision_rows();
384 }
385 }
386 }
387 let mut of_kind: Vec<_> = Kind::ALL
389 .iter()
390 .map(|&kd| {
391 (0..input.media.len())
392 .filter(move |&i| input.media[i].kind() == kd)
393 .collect::<Vec<_>>()
394 .into_iter()
395 })
396 .collect();
397 let mut seq_media: Vec<usize> = Vec::new();
398 for kd in placeholder_order(&full) {
399 let m = of_kind[kd as usize].next();
400 seq_media.push(m.ok_or("a placeholder without a medium")?);
401 }
402 if of_kind.iter_mut().any(|it| it.next().is_some()) {
403 return Err("a medium without a placeholder".into());
404 }
405 let raw = self.text.tokenizer().encode(&full);
406 let mut ids = Vec::with_capacity(raw.len() + 512);
407 let mut place = Placement::default();
408 ids.push(self.text.bos);
409 let mut next = seq_media.iter();
410 for &t in &raw {
411 if t == self.ids.image || t == self.ids.video || t == self.ids.audio {
412 let m = *next.next().ok_or("more placeholder tokens than media")?;
413 let want = match input.media[m].kind() {
414 Kind::Image => self.ids.image,
415 Kind::Video => self.ids.video,
416 Kind::Audio => self.ids.audio,
417 };
418 if t != want {
419 return Err("a placeholder did not tokenize to its token".into());
420 }
421 match &input.media[m] {
422 Media::Image(x) => {
423 ids.push(self.ids.boi);
424 ids.extend(std::iter::repeat_n(self.ids.image, x.n_soft(k)));
425 ids.push(self.ids.eoi);
426 place.vision.push(first[m]);
427 }
428 Media::Video(frames) => {
429 for (j, f) in frames.iter().enumerate() {
430 ids.push(self.ids.boi);
431 ids.extend(std::iter::repeat_n(self.ids.video, f.n_soft(k)));
432 ids.push(self.ids.eoi);
433 place.vision.push(first[m] + j);
434 }
435 }
436 Media::Audio(w) => {
437 ids.push(self.ids.boa);
438 ids.extend(std::iter::repeat_n(self.ids.audio, ea::num_tokens(w.len())));
439 ids.push(self.ids.eoa);
440 place.audio.push(first[m]);
441 }
442 }
443 } else {
444 ids.push(t);
445 }
446 }
447 if next.next().is_some() {
448 return Err("a placeholder did not tokenize to its token".into());
449 }
450 ids.push(self.text.eos);
451 if ids.len() > self.text.max_tokens {
452 return Err(format!(
453 "{} tokens > the model's {} token context (media are not truncated: \
454 lower the image budget, the number of frames or the audio length)",
455 ids.len(),
456 self.text.max_tokens
457 ));
458 }
459 Ok((ids, place))
460 }
461
462 pub fn embed(&self, inputs: &[MixedInput]) -> Result<Vec<Vec<f32>>, String> {
464 let laid: Vec<(Vec<u32>, Placement)> = inputs
465 .iter()
466 .enumerate()
467 .map(|(i, x)| self.layout(x).map_err(|e| format!("input {i}: {e}")))
468 .collect::<Result<_, _>>()?;
469 let seqs: Vec<Vec<u32>> = laid.iter().map(|(s, _)| s.clone()).collect();
470 if inputs.iter().all(|x| x.media.is_empty()) {
471 return self.text.embed_ids(&seqs);
472 }
473 let mut flat: Vec<VisionInput> = Vec::new();
476 let mut clips: Vec<&[f32]> = Vec::new();
477 let mut base = Vec::with_capacity(inputs.len());
478 for x in inputs {
479 base.push((flat.len(), clips.len()));
480 for m in &x.media {
481 match m {
482 Media::Image(v) => flat.push(v.clone()),
483 Media::Video(f) => flat.extend(f.iter().cloned()),
484 Media::Audio(w) => clips.push(w),
485 }
486 }
487 }
488 let d = self.text.hidden();
489 let soft = if flat.is_empty() {
490 Vec::new()
491 } else {
492 self.vision()?.encode(&flat)?
493 };
494 drop(flat);
495 let asoft = if clips.is_empty() {
496 Vec::new()
497 } else {
498 let tower = self.audio()?;
499 if tower.out_dim != d {
500 return Err(format!(
501 "audio soft tokens are {}-d, the text encoder is {d}-d",
502 tower.out_dim
503 ));
504 }
505 tower.soft_tokens(&clips)
506 };
507 let (image, video, audio) = (self.ids.image, self.ids.video, self.ids.audio);
508 let mut out: Vec<Option<Vec<f32>>> = vec![None; inputs.len()];
509 let mut start = 0usize;
511 while start < inputs.len() {
512 let mut end = start;
513 let mut toks = 0usize;
514 while end < inputs.len()
515 && (end == start || toks + seqs[end].len() <= self.text.max_tokens)
516 {
517 toks += seqs[end].len();
518 end += 1;
519 }
520 let mut x = self.text.embed_rows(&seqs[start..end]);
521 let mut row = 0usize;
522 for i in start..end {
523 let (ids, place) = &laid[i];
524 let (vb, ab) = base[i];
525 let mut vsrc = place
526 .vision
527 .iter()
528 .flat_map(|&j| soft[vb + j].chunks_exact(d));
529 let mut asrc = place
530 .audio
531 .iter()
532 .flat_map(|&j| asoft[ab + j].chunks_exact(d));
533 for &t in ids {
534 let src = if t == image || t == video {
535 Some(vsrc.next().ok_or("fewer soft tokens than placeholders")?)
536 } else if t == audio {
537 Some(asrc.next().ok_or("fewer audio tokens than placeholders")?)
538 } else {
539 None
540 };
541 if let Some(s) = src {
542 x[row * d..(row + 1) * d].copy_from_slice(s);
543 }
544 row += 1;
545 }
546 if vsrc.next().is_some() || asrc.next().is_some() {
547 return Err("more soft tokens than placeholders".into());
548 }
549 }
550 let lens: Vec<usize> = seqs[start..end].iter().map(|s| s.len()).collect();
551 for (j, v) in self.text.embed_merged(x, &lens).into_iter().enumerate() {
552 out[start + j] = Some(v);
553 }
554 start = end;
555 }
556 Ok(out.into_iter().map(|v| v.unwrap()).collect())
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 fn img(pw: usize, ph: usize) -> Media {
565 Media::Image(VisionInput {
566 pw,
567 ph,
568 pixels: Vec::new(),
569 })
570 }
571
572 fn vid(frames: usize) -> Media {
573 Media::Video(
574 (0..frames)
575 .map(|_| VisionInput {
576 pw: 45,
577 ph: 24,
578 pixels: Vec::new(),
579 })
580 .collect(),
581 )
582 }
583
584 #[test]
585 fn layout_places_media_without_placeholders_first() {
586 assert_eq!(layout_text("", &[img(3, 3)]).unwrap(), "<|image|>");
587 assert_eq!(
588 layout_text("", &[img(3, 3), img(3, 3)]).unwrap(),
589 "<|image|> <|image|>"
590 );
591 assert_eq!(
592 layout_text("a fox", &[img(3, 3), vid(2)]).unwrap(),
593 "<|image|> <|video|> a fox"
594 );
595 assert_eq!(
596 layout_text("A <|image|> b", &[img(3, 3)]).unwrap(),
597 "A <|image|> b"
598 );
599 assert!(layout_text("A <|image|> <|image|>", &[img(3, 3)]).is_err());
600 assert!(layout_text("A <|video|>", &[img(3, 3)]).is_err());
601 let clip = Media::Audio(vec![0.0; 1600]);
603 assert_eq!(
604 layout_text("", &[clip.clone(), vid(1), img(3, 3)]).unwrap(),
605 "<|image|> <|video|> <|audio|>"
606 );
607 assert_eq!(
608 layout_text("said: <|audio|>", std::slice::from_ref(&clip)).unwrap(),
609 "said: <|audio|>"
610 );
611 assert!(layout_text("said: <|audio|> <|audio|>", &[clip]).is_err());
612 }
613
614 #[test]
615 fn placeholder_order_interleaves_kinds() {
616 assert_eq!(
617 placeholder_order("x <|video|> y <|image|><|audio|><|image|> <|video|>"),
618 vec![
619 Kind::Video,
620 Kind::Image,
621 Kind::Audio,
622 Kind::Image,
623 Kind::Video
624 ]
625 );
626 assert!(placeholder_order("plain").is_empty());
627 }
628}