use rusqlite::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize};
use crate::store::StoreError;
pub mod gate;
pub mod producers;
pub use gate::{GateReason, GateThresholds, MediaSkip};
pub const MEDIA_PRODUCER_PREFIX: &str = "media";
pub const MEDIA_SCHEMA: &str = "roteiro.media/v1";
pub const MAX_AUDIO_BYTES: usize = 50 * 1024 * 1024;
pub const MAX_IMAGE_BYTES: usize = 20 * 1024 * 1024;
pub const MAX_PROMPT: usize = 4096;
pub const MAX_MODEL_ID: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MediaError {
#[error(
"invalid model id {0:?} (expected 1 to {MAX_MODEL_ID} characters of lowercase [a-z0-9._-])"
)]
InvalidModelId(String),
#[error("invalid producer {field}: {reason}")]
InvalidProducer {
field: &'static str,
reason: String,
},
#[error(
"this build cannot generate {kind} content: rebuild with `--features {feature}` \
(generated media content is opt-in, so the default build has no producer)"
)]
NoProducer {
kind: &'static str,
feature: &'static str,
},
#[error("model `{model}` is not installed: run `roteiro model pull {model}`")]
ModelMissing {
model: String,
},
#[cfg(feature = "models")]
#[error(transparent)]
ModelConfig(#[from] crate::model_choice::ModelChoiceError),
#[error("corrupt media record: {0}")]
Corrupt(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MediaKind {
Audio,
Vision,
}
impl MediaKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Audio => "audio",
Self::Vision => "vision",
}
}
#[must_use]
pub fn from_token(s: &str) -> Option<Self> {
match s {
"audio" => Some(Self::Audio),
"vision" => Some(Self::Vision),
_ => None,
}
}
#[must_use]
pub fn accepts_path(self, path: &str) -> bool {
match self {
Self::Audio => is_audio(path),
Self::Vision => is_image(path),
}
}
#[must_use]
pub fn max_bytes(self) -> usize {
match self {
Self::Audio => MAX_AUDIO_BYTES,
Self::Vision => MAX_IMAGE_BYTES,
}
}
#[must_use]
pub fn feature(self) -> &'static str {
match self {
Self::Audio => "audio-transcribe",
Self::Vision => "image-vision",
}
}
#[must_use]
pub const fn model(self) -> &'static str {
match self {
Self::Audio => "voxtral-mini-3b",
Self::Vision => "smolvlm-500m-gguf",
}
}
#[cfg(feature = "models")]
#[must_use]
pub fn task(self) -> crate::model_choice::ModelTask {
match self {
Self::Audio => crate::model_choice::ModelTask::Transcribe,
Self::Vision => crate::model_choice::ModelTask::Describe,
}
}
#[must_use]
pub fn compiled_in(self) -> bool {
match self {
Self::Audio => cfg!(feature = "audio-transcribe"),
Self::Vision => cfg!(feature = "image-vision"),
}
}
}
impl std::fmt::Display for MediaKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[must_use]
pub fn is_audio(path: &str) -> bool {
matches!(
crate::extract::extension(path).as_deref(),
Some("wav" | "mp3" | "flac")
)
}
#[must_use]
pub fn is_image(path: &str) -> bool {
matches!(
crate::extract::extension(path).as_deref(),
Some("png" | "jpg" | "jpeg")
)
}
fn is_model_id_char(c: char) -> bool {
c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '.' | '_' | '-')
}
#[must_use]
pub fn is_valid_model_id(id: &str) -> bool {
!id.is_empty() && id.len() <= MAX_MODEL_ID && id.chars().all(is_model_id_char)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Producer {
pub kind: MediaKind,
pub model: String,
pub model_digest: String,
pub quantisation: String,
pub mmproj_digest: String,
pub prompt: String,
pub temperature: f64,
pub max_tokens: u32,
}
impl Producer {
pub fn validate(&self) -> Result<(), MediaError> {
if !is_valid_model_id(&self.model) {
return Err(MediaError::InvalidModelId(self.model.clone()));
}
let non_empty = |field: &'static str, value: &str| {
if value.is_empty() {
Err(MediaError::InvalidProducer {
field,
reason: "it is empty".to_owned(),
})
} else {
Ok(())
}
};
non_empty("model_digest", &self.model_digest)?;
non_empty("quantisation", &self.quantisation)?;
non_empty("mmproj_digest", &self.mmproj_digest)?;
non_empty("prompt", &self.prompt)?;
if self.prompt.len() > MAX_PROMPT {
return Err(MediaError::InvalidProducer {
field: "prompt",
reason: format!(
"it is {} bytes, over the {MAX_PROMPT}-byte limit",
self.prompt.len()
),
});
}
if !self.temperature.is_finite() {
return Err(MediaError::InvalidProducer {
field: "temperature",
reason: format!("{} is not a finite number", self.temperature),
});
}
Ok(())
}
#[must_use]
pub fn id(&self) -> ProducerId {
use std::fmt::Write as _;
let mut canonical = String::new();
for part in [
self.kind.as_str(),
self.model.as_str(),
self.model_digest.as_str(),
self.quantisation.as_str(),
self.mmproj_digest.as_str(),
self.prompt.as_str(),
] {
let _ = write!(canonical, "{}:{part}", part.len());
}
let _ = write!(canonical, "t{:?}m{}", self.temperature, self.max_tokens);
ProducerId(format!(
"{MEDIA_PRODUCER_PREFIX}:{}:{}:{:016x}",
self.kind.as_str(),
self.model,
fnv1a(canonical.as_bytes())
))
}
}
fn fnv1a(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for b in bytes {
hash ^= u64::from(*b);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ProducerId(String);
impl ProducerId {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ProducerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GeneratedContent {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "lowercase")]
pub enum MediaOutcome {
Generated(GeneratedContent),
Skipped(MediaSkip),
}
impl MediaOutcome {
#[must_use]
pub fn text(&self) -> Option<&str> {
match self {
Self::Generated(content) => Some(content.text.as_str()),
Self::Skipped(_) => None,
}
}
#[must_use]
pub fn skip(&self) -> Option<MediaSkip> {
match self {
Self::Generated(_) => None,
Self::Skipped(skip) => Some(*skip),
}
}
#[must_use]
pub fn is_skipped(&self) -> bool {
matches!(self, Self::Skipped(_))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MediaRecord {
pub blob_id: String,
pub path: String,
pub producer_id: ProducerId,
pub producer: Producer,
pub tool_version: String,
pub generation: u32,
pub produced_at: String,
#[serde(flatten)]
pub outcome: MediaOutcome,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct MediaFilter<'a> {
pub producer: Option<&'a str>,
pub kind: Option<MediaKind>,
pub blob_id: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct MediaWrite<'a> {
pub blob_id: &'a str,
pub path: &'a str,
pub producer: &'a Producer,
pub tool_version: &'a str,
pub outcome: &'a MediaOutcome,
pub replace: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProducerSummary {
pub producer_id: ProducerId,
pub kind: MediaKind,
pub model: String,
pub quantisation: String,
pub records: u64,
pub skipped: u64,
pub latest: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SkipEntry {
pub blob_id: String,
pub path: String,
pub kind: MediaKind,
pub producer_id: ProducerId,
#[serde(flatten)]
pub skip: MediaSkip,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MediaStatus {
pub schema: &'static str,
pub records: u64,
pub producers: Vec<ProducerSummary>,
pub candidates: Vec<CandidateCount>,
pub available_producers: Vec<ProducerSummaryAvailable>,
pub skipped: Vec<SkipEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CandidateCount {
pub kind: MediaKind,
pub blobs: u64,
pub described: u64,
pub skipped: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProducerSummaryAvailable {
pub producer_id: ProducerId,
pub kind: MediaKind,
pub model: String,
pub current: bool,
}
pub trait MediaProducer {
fn producer(&self) -> &Producer;
fn generate(&self, path: &str, bytes: &[u8]) -> Option<GeneratedContent>;
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MediaBuildOptions {
pub audio: bool,
pub vision: bool,
pub force: bool,
pub thresholds: GateThresholds,
}
impl Default for MediaBuildOptions {
fn default() -> Self {
Self {
audio: true,
vision: true,
force: false,
thresholds: GateThresholds::default(),
}
}
}
impl MediaBuildOptions {
#[must_use]
pub fn wants(self, kind: MediaKind) -> bool {
match kind {
MediaKind::Audio => self.audio,
MediaKind::Vision => self.vision,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct MediaBuildReport {
#[serde(default = "media_schema")]
pub schema: &'static str,
pub candidates: usize,
pub generated: usize,
pub skipped_existing: usize,
#[serde(default)]
pub gated: usize,
pub empty: usize,
pub producers: Vec<ProducerId>,
}
fn media_schema() -> &'static str {
MEDIA_SCHEMA
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MediaBlob {
pub blob_id: String,
pub path: String,
pub kind: MediaKind,
}
pub fn media_blobs(repo: &crate::Repo) -> Result<Vec<MediaBlob>, crate::GitError> {
let mut blobs = repo.walk_blobs()?;
blobs.sort_by(|a, b| a.path.cmp(&b.path));
let mut seen: std::collections::BTreeSet<(MediaKind, String)> =
std::collections::BTreeSet::new();
let mut out = Vec::new();
for blob in blobs {
for kind in [MediaKind::Audio, MediaKind::Vision] {
if !kind.accepts_path(&blob.path) {
continue;
}
if seen.contains(&(kind, blob.oid.clone())) {
continue;
}
let bytes = repo.read_blob(&blob.oid)?;
if bytes.len() > kind.max_bytes() {
continue;
}
seen.insert((kind, blob.oid.clone()));
out.push(MediaBlob {
blob_id: blob.oid.clone(),
path: blob.path.clone(),
kind,
});
}
}
out.sort_by(|a, b| (a.kind, &a.blob_id).cmp(&(b.kind, &b.blob_id)));
Ok(out)
}
pub fn build_media<F>(
store: &mut crate::Store,
blobs: &[MediaBlob],
producers: &[&dyn MediaProducer],
opts: MediaBuildOptions,
mut read: F,
) -> Result<MediaBuildReport, StoreError>
where
F: FnMut(&MediaBlob) -> Option<Vec<u8>>,
{
let tool_version = env!("CARGO_PKG_VERSION");
let mut report = MediaBuildReport {
schema: MEDIA_SCHEMA,
..MediaBuildReport::default()
};
let mut ids: Vec<ProducerId> = producers.iter().map(|p| p.producer().id()).collect();
ids.sort();
ids.dedup();
report.producers = ids;
for producer in producers {
let identity = producer.producer();
let id = identity.id();
for blob in blobs {
if blob.kind != identity.kind || !opts.wants(blob.kind) {
continue;
}
report.candidates += 1;
if !opts.force && store.has_media_record(&blob.blob_id, id.as_str())? {
report.skipped_existing += 1;
continue;
}
let Some(bytes) = read(blob) else {
report.empty += 1;
continue;
};
let gated = if opts.force {
None
} else {
gate::evaluate(blob.kind, &bytes, opts.thresholds)
};
if let Some(skip) = gated {
store.record_media_content(&MediaWrite {
blob_id: &blob.blob_id,
path: &blob.path,
producer: identity,
tool_version,
outcome: &MediaOutcome::Skipped(skip),
replace: false,
})?;
report.gated += 1;
continue;
}
let Some(content) = producer.generate(&blob.path, &bytes) else {
report.empty += 1;
continue;
};
if content.text.trim().is_empty() {
report.empty += 1;
continue;
}
let written = store.record_media_content(&MediaWrite {
blob_id: &blob.blob_id,
path: &blob.path,
producer: identity,
tool_version,
outcome: &MediaOutcome::Generated(content),
replace: opts.force,
})?;
if written {
report.generated += 1;
} else {
report.skipped_existing += 1;
}
}
}
Ok(report)
}
pub fn status(store: &crate::Store, blobs: &[MediaBlob]) -> Result<MediaStatus, StoreError> {
let mut candidates = Vec::new();
for kind in [MediaKind::Audio, MediaKind::Vision] {
let described_ids = store.described_media_blobs(kind)?;
let gated_ids = store.gated_media_blobs(kind)?;
let in_tree: std::collections::BTreeSet<&str> = blobs
.iter()
.filter(|b| b.kind == kind)
.map(|b| b.blob_id.as_str())
.collect();
let described = in_tree
.iter()
.filter(|id| described_ids.contains(**id))
.count();
let skipped = in_tree
.iter()
.filter(|id| gated_ids.contains(**id) && !described_ids.contains(**id))
.count();
candidates.push(CandidateCount {
kind,
blobs: u64::try_from(in_tree.len()).unwrap_or(u64::MAX),
described: u64::try_from(described).unwrap_or(u64::MAX),
skipped: u64::try_from(skipped).unwrap_or(u64::MAX),
});
}
let stored = store.media_producer_summaries()?;
let mut available_producers: Vec<ProducerSummaryAvailable> = producers::available()
.into_iter()
.map(|p| {
let producer_id = p.id();
ProducerSummaryAvailable {
current: stored.iter().any(|s| s.producer_id == producer_id),
producer_id,
kind: p.kind,
model: p.model,
}
})
.collect();
available_producers.sort_by(|a, b| a.producer_id.cmp(&b.producer_id));
let skipped = store
.media_records(&MediaFilter::default())?
.into_iter()
.filter_map(|record| {
record.outcome.skip().map(|skip| SkipEntry {
blob_id: record.blob_id,
path: record.path,
kind: record.producer.kind,
producer_id: record.producer_id,
skip,
})
})
.collect();
Ok(MediaStatus {
schema: MEDIA_SCHEMA,
records: store.media_content_count()?,
producers: stored,
candidates,
available_producers,
skipped,
})
}
const RECORD_COLS: &str = "m.blob_id, m.path, m.kind, m.producer, m.model, m.model_digest, \
m.quantisation, m.mmproj_digest, m.prompt, m.temperature, m.max_tokens, \
m.tool_version, m.generation, m.produced_at, m.text, m.confidence, \
m.skip_reason, m.skip_value, m.skip_threshold";
pub(crate) fn record(conn: &Connection, write: &MediaWrite<'_>) -> Result<bool, StoreError> {
let id = write.producer.id();
let existing: Option<i64> = conn
.query_row(
"SELECT id FROM media_content WHERE blob_id = ?1 AND producer = ?2",
params![write.blob_id, id.as_str()],
|r| r.get(0),
)
.optional()?;
let previous: i64 = conn.query_row(
"SELECT COALESCE(MAX(generation), 0) FROM media_content WHERE blob_id = ?1",
[write.blob_id],
|r| r.get(0),
)?;
match (existing, write.replace) {
(Some(_), false) => return Ok(false),
(Some(row), true) => {
conn.execute("DELETE FROM media_content WHERE id = ?1", [row])?;
}
(None, _) => {}
}
let generation = u32::try_from(previous + 1).unwrap_or(u32::MAX);
let (text, confidence, skip) = match write.outcome {
MediaOutcome::Generated(content) => {
(content.text.as_str(), content.confidence, None::<MediaSkip>)
}
MediaOutcome::Skipped(skip) => ("", None, Some(*skip)),
};
conn.execute(
"INSERT INTO media_content (
blob_id, path, kind, producer, model, model_digest, quantisation, mmproj_digest,
prompt, temperature, max_tokens, tool_version, generation, text, confidence,
skip_reason, skip_value, skip_threshold
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
params![
write.blob_id,
write.path,
write.producer.kind.as_str(),
id.as_str(),
write.producer.model,
write.producer.model_digest,
write.producer.quantisation,
write.producer.mmproj_digest,
write.producer.prompt,
write.producer.temperature,
write.producer.max_tokens,
write.tool_version,
generation,
text,
confidence,
skip.map(|s| s.reason.as_str()),
skip.map(|s| s.value),
skip.map(|s| s.threshold),
],
)?;
Ok(true)
}
pub(crate) fn exists(conn: &Connection, blob_id: &str, producer: &str) -> Result<bool, StoreError> {
let n: i64 = conn.query_row(
"SELECT COUNT(*) FROM media_content WHERE blob_id = ?1 AND producer = ?2",
params![blob_id, producer],
|r| r.get(0),
)?;
Ok(n > 0)
}
pub(crate) fn records(
conn: &Connection,
filter: &MediaFilter<'_>,
) -> Result<Vec<MediaRecord>, StoreError> {
let mut where_parts: Vec<&str> = Vec::new();
let mut bound: Vec<String> = Vec::new();
if let Some(producer) = filter.producer {
where_parts.push("m.producer = ?");
bound.push(producer.to_owned());
}
if let Some(kind) = filter.kind {
where_parts.push("m.kind = ?");
bound.push(kind.as_str().to_owned());
}
if let Some(blob) = filter.blob_id {
where_parts.push("m.blob_id = ?");
bound.push(blob.to_owned());
}
let clause = if where_parts.is_empty() {
String::new()
} else {
format!(" WHERE {}", where_parts.join(" AND "))
};
let sql =
format!("SELECT {RECORD_COLS} FROM media_content m{clause} ORDER BY m.producer, m.blob_id");
let mut stmt = conn.prepare(&sql)?;
let mut rows = stmt.query(rusqlite::params_from_iter(bound))?;
let mut out = Vec::new();
while let Some(row) = rows.next()? {
out.push(record_from_row(row)?);
}
Ok(out)
}
pub(crate) fn delete(conn: &Connection, producer: Option<&str>) -> Result<usize, StoreError> {
let removed = match producer {
Some(id) => conn.execute("DELETE FROM media_content WHERE producer = ?1", [id])?,
None => conn.execute("DELETE FROM media_content", [])?,
};
Ok(removed)
}
pub(crate) fn count(conn: &Connection) -> Result<u64, StoreError> {
let n: i64 = conn.query_row("SELECT COUNT(*) FROM media_content", [], |r| r.get(0))?;
Ok(u64::try_from(n).unwrap_or(0))
}
pub(crate) fn producer_summaries(conn: &Connection) -> Result<Vec<ProducerSummary>, StoreError> {
let mut stmt = conn.prepare(
"SELECT producer, kind, model, quantisation, COUNT(*),
SUM(skip_reason IS NOT NULL), MAX(produced_at)
FROM media_content GROUP BY producer, kind, model, quantisation ORDER BY producer",
)?;
let mut rows = stmt.query([])?;
let mut out = Vec::new();
while let Some(row) = rows.next()? {
let kind_token: String = row.get(1)?;
let kind = MediaKind::from_token(&kind_token)
.ok_or_else(|| StoreError::Corrupt(format!("unknown media kind: {kind_token}")))?;
let records: i64 = row.get(4)?;
let skipped: i64 = row.get(5)?;
out.push(ProducerSummary {
producer_id: ProducerId(row.get(0)?),
kind,
model: row.get(2)?,
quantisation: row.get(3)?,
records: u64::try_from(records).unwrap_or(0),
skipped: u64::try_from(skipped).unwrap_or(0),
latest: row.get(6)?,
});
}
Ok(out)
}
pub(crate) fn described_blobs(
conn: &Connection,
kind: MediaKind,
) -> Result<std::collections::BTreeSet<String>, StoreError> {
blob_ids(
conn,
"SELECT DISTINCT blob_id FROM media_content
WHERE kind = ?1 AND skip_reason IS NULL ORDER BY blob_id",
kind,
)
}
pub(crate) fn gated_blobs(
conn: &Connection,
kind: MediaKind,
) -> Result<std::collections::BTreeSet<String>, StoreError> {
blob_ids(
conn,
"SELECT DISTINCT blob_id FROM media_content
WHERE kind = ?1 AND skip_reason IS NOT NULL ORDER BY blob_id",
kind,
)
}
fn blob_ids(
conn: &Connection,
sql: &str,
kind: MediaKind,
) -> Result<std::collections::BTreeSet<String>, StoreError> {
let mut stmt = conn.prepare(sql)?;
let mut rows = stmt.query([kind.as_str()])?;
let mut out = std::collections::BTreeSet::new();
while let Some(row) = rows.next()? {
out.insert(row.get::<_, String>(0)?);
}
Ok(out)
}
fn record_from_row(row: &rusqlite::Row<'_>) -> Result<MediaRecord, StoreError> {
let kind_token: String = row.get(2)?;
let kind = MediaKind::from_token(&kind_token)
.ok_or_else(|| StoreError::Corrupt(format!("unknown media kind: {kind_token}")))?;
let generation: i64 = row.get(12)?;
let skip_reason: Option<String> = row.get(16)?;
let outcome = match skip_reason {
Some(token) => {
let reason = GateReason::from_token(&token).ok_or_else(|| {
StoreError::Corrupt(format!("unknown media skip reason: {token}"))
})?;
MediaOutcome::Skipped(MediaSkip {
reason,
value: row.get(17)?,
threshold: row.get(18)?,
})
}
None => MediaOutcome::Generated(GeneratedContent {
text: row.get(14)?,
confidence: row.get(15)?,
}),
};
Ok(MediaRecord {
blob_id: row.get(0)?,
path: row.get(1)?,
producer_id: ProducerId(row.get(3)?),
producer: Producer {
kind,
model: row.get(4)?,
model_digest: row.get(5)?,
quantisation: row.get(6)?,
mmproj_digest: row.get(7)?,
prompt: row.get(8)?,
temperature: row.get(9)?,
max_tokens: row.get(10)?,
},
tool_version: row.get(11)?,
generation: u32::try_from(generation).unwrap_or(u32::MAX),
produced_at: row.get(13)?,
outcome,
})
}
#[cfg(test)]
mod tests {
use super::{
GeneratedContent, MAX_MODEL_ID, MAX_PROMPT, MediaError, MediaKind, Producer,
is_valid_model_id,
};
fn producer() -> Producer {
Producer {
kind: MediaKind::Audio,
model: "voxtral-mini-3b".to_owned(),
model_digest: "4705be8e".to_owned(),
quantisation: "Q4_K_M".to_owned(),
mmproj_digest: "4f24c4ef".to_owned(),
prompt: "Transcribe this audio recording.".to_owned(),
temperature: 0.0,
max_tokens: 512,
}
}
#[test]
fn a_producer_id_names_its_modality_and_model() {
let id = producer().id();
assert!(
id.as_str().starts_with("media:audio:voxtral-mini-3b:"),
"got {id}"
);
assert_eq!(producer().id(), producer().id());
}
#[test]
fn every_identity_field_changes_the_producer_id() {
type Mutation = (&'static str, fn(&mut Producer));
let base = producer().id();
let mutate: [Mutation; 7] = [
("kind", |p| p.kind = MediaKind::Vision),
("model", |p| p.model = "smolvlm-500m-gguf".to_owned()),
("model_digest", |p| p.model_digest = "deadbeef".to_owned()),
("quantisation", |p| p.quantisation = "Q8_0".to_owned()),
("mmproj_digest", |p| p.mmproj_digest = "cafebabe".to_owned()),
("prompt", |p| p.prompt = "Describe this.".to_owned()),
("temperature", |p| p.temperature = 0.2),
];
for (field, apply) in mutate {
let mut p = producer();
apply(&mut p);
assert_ne!(p.id(), base, "changing {field} must change the producer id");
}
let mut p = producer();
p.max_tokens = 256;
assert_ne!(
p.id(),
base,
"changing max_tokens must change the producer id"
);
}
#[test]
fn adjacent_fields_cannot_be_confused() {
let mut a = producer();
a.model_digest = "ab".to_owned();
a.quantisation = "cd".to_owned();
let mut b = producer();
b.model_digest = "abc".to_owned();
b.quantisation = "d".to_owned();
assert_ne!(a.id(), b.id());
}
#[test]
fn model_ids_accept_the_registry_names_and_reject_separators() {
assert!(is_valid_model_id("voxtral-mini-3b"));
assert!(is_valid_model_id("smolvlm-500m-gguf"));
assert!(!is_valid_model_id(""));
assert!(!is_valid_model_id("a:b"));
assert!(!is_valid_model_id("Voxtral"));
assert!(is_valid_model_id(&"a".repeat(MAX_MODEL_ID)));
assert!(!is_valid_model_id(&"a".repeat(MAX_MODEL_ID + 1)));
}
#[test]
fn validation_names_the_field_it_refused() {
assert!(producer().validate().is_ok());
let mut bad = producer();
bad.model = "Voxtral".to_owned();
assert_eq!(
bad.validate(),
Err(MediaError::InvalidModelId("Voxtral".to_owned()))
);
for (field, apply) in [
(
"model_digest",
(|p: &mut Producer| p.model_digest.clear()) as fn(&mut Producer),
),
("quantisation", |p: &mut Producer| p.quantisation.clear()),
("mmproj_digest", |p: &mut Producer| p.mmproj_digest.clear()),
("prompt", |p: &mut Producer| p.prompt.clear()),
] {
let mut p = producer();
apply(&mut p);
let err = p.validate().expect_err("empty field must be refused");
assert!(
err.to_string().contains(field),
"the rejection must name {field}: {err}"
);
}
let mut long = producer();
long.prompt = "x".repeat(MAX_PROMPT + 1);
assert!(
long.validate()
.expect_err("over-long prompt")
.to_string()
.contains("over the")
);
let mut nan = producer();
nan.temperature = f64::NAN;
assert!(
nan.validate()
.expect_err("NaN temperature")
.to_string()
.contains("finite")
);
}
#[test]
fn media_kind_tokens_round_trip() {
for kind in [MediaKind::Audio, MediaKind::Vision] {
assert_eq!(MediaKind::from_token(kind.as_str()), Some(kind));
}
assert_eq!(MediaKind::from_token("ocr"), None);
assert_eq!(MediaKind::from_token("nope"), None);
}
#[test]
fn modalities_accept_only_their_own_extensions() {
assert!(MediaKind::Audio.accepts_path("a/clip.wav"));
assert!(MediaKind::Audio.accepts_path("a/clip.MP3"));
assert!(MediaKind::Audio.accepts_path("a/clip.flac"));
assert!(!MediaKind::Audio.accepts_path("a/clip.ogg"));
assert!(!MediaKind::Audio.accepts_path("a/clip.wav.bak"));
assert!(MediaKind::Vision.accepts_path("a/x.png"));
assert!(MediaKind::Vision.accepts_path("a/x.jpeg"));
assert!(!MediaKind::Vision.accepts_path("a/x.gif"));
assert!(!MediaKind::Audio.accepts_path("a/x.md"));
assert!(!MediaKind::Vision.accepts_path("a/x.md"));
}
#[test]
fn generated_content_omits_an_absent_confidence() {
let bare = GeneratedContent {
text: "hello".to_owned(),
confidence: None,
};
assert_eq!(
serde_json::to_string(&bare).expect("serialize"),
r#"{"text":"hello"}"#
);
}
}