Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/nvisy-cli/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use std::time::Duration;

use anyhow::Context;
use clap::{Args, Parser};
use nvisy_engine::pipeline::RuntimeConfig;
use nvisy_engine::core::RuntimeConfig;
use serde::Deserialize;

pub use self::server::ServerConfig;
Expand Down
18 changes: 10 additions & 8 deletions crates/nvisy-codec/src/handler/audio/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,26 +44,28 @@ const TARGET: &str = "nvisy_codec::handler::audio::duration";
/// first track lacks a timebase or a known duration, or when the
/// computed duration would overflow `i64` microseconds.
pub(super) fn probe_duration_us(bytes: &Bytes, extension_hint: &str) -> Result<i64, Error> {
let mss = MediaSourceStream::new(
Box::new(Cursor::new(bytes.clone())),
Default::default(),
);
let mss = MediaSourceStream::new(Box::new(Cursor::new(bytes.clone())), Default::default());

let mut hint = Hint::new();
hint.with_extension(extension_hint);

let reader = get_probe()
.probe(&hint, mss, FormatOptions::default(), MetadataOptions::default())
.probe(
&hint,
mss,
FormatOptions::default(),
MetadataOptions::default(),
)
.map_err(|e| Error::validation(format!("audio probe failed: {e}"), TARGET))?;

let track = reader
.tracks()
.first()
.ok_or_else(|| Error::validation("audio probe returned no tracks", TARGET))?;

let time_base = track.time_base.ok_or_else(|| {
Error::validation("audio track is missing a timebase", TARGET)
})?;
let time_base = track
.time_base
.ok_or_else(|| Error::validation("audio track is missing a timebase", TARGET))?;
let duration = track.duration.ok_or_else(|| {
Error::validation("audio track is missing a container-level duration", TARGET)
})?;
Expand Down
43 changes: 22 additions & 21 deletions crates/nvisy-codec/src/handler/audio/mp3_codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
//! redaction of MP3 streams. Callers wanting bit-perfect preservation
//! of unredacted regions should not round-trip.

use std::io::Cursor;

use bytes::Bytes;
use mp3lame_encoder::{Builder, FlushNoGap, InterleavedPcm, MonoPcm};
use nvisy_core::Error;
Expand All @@ -28,8 +30,6 @@ use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::default::{get_codecs, get_probe};

use std::io::Cursor;

const TARGET: &str = "nvisy_codec::handler::audio::mp3_codec";

/// Decoded MP3 contents in interleaved `f32` PCM form.
Expand All @@ -48,15 +48,17 @@ pub(super) struct DecodedMp3 {
/// only supports mono and stereo, and silent downmixing would
/// quietly edit the *unredacted* audio).
pub(super) fn probe_channels(bytes: &Bytes) -> Result<u16, Error> {
let mss = MediaSourceStream::new(
Box::new(Cursor::new(bytes.clone())),
Default::default(),
);
let mss = MediaSourceStream::new(Box::new(Cursor::new(bytes.clone())), Default::default());
let mut hint = Hint::new();
hint.with_extension("mp3");

let reader = get_probe()
.probe(&hint, mss, FormatOptions::default(), MetadataOptions::default())
.probe(
&hint,
mss,
FormatOptions::default(),
MetadataOptions::default(),
)
.map_err(|e| Error::validation(format!("MP3 probe failed: {e}"), TARGET))?;

let track = reader
Expand Down Expand Up @@ -87,15 +89,17 @@ pub(super) fn probe_channels(bytes: &Bytes) -> Result<u16, Error> {
/// [`super::redact::apply`] helper and for handing back to
/// [`encode_from_pcm`].
pub(super) fn decode_to_pcm(bytes: &Bytes) -> Result<DecodedMp3, Error> {
let mss = MediaSourceStream::new(
Box::new(Cursor::new(bytes.clone())),
Default::default(),
);
let mss = MediaSourceStream::new(Box::new(Cursor::new(bytes.clone())), Default::default());
let mut hint = Hint::new();
hint.with_extension("mp3");

let mut reader = get_probe()
.probe(&hint, mss, FormatOptions::default(), MetadataOptions::default())
.probe(
&hint,
mss,
FormatOptions::default(),
MetadataOptions::default(),
)
.map_err(|e| Error::validation(format!("MP3 probe failed: {e}"), TARGET))?;

let track = reader
Expand All @@ -110,9 +114,9 @@ pub(super) fn decode_to_pcm(bytes: &Bytes) -> Result<DecodedMp3, Error> {
.ok_or_else(|| Error::validation("MP3 track is missing audio codec params", TARGET))?
.clone();

let sample_rate = audio_params.sample_rate.ok_or_else(|| {
Error::validation("MP3 track is missing a sample rate", TARGET)
})?;
let sample_rate = audio_params
.sample_rate
.ok_or_else(|| Error::validation("MP3 track is missing a sample rate", TARGET))?;
let channels = audio_params
.channels
.as_ref()
Expand Down Expand Up @@ -163,10 +167,7 @@ pub(super) fn decode_to_pcm(bytes: &Bytes) -> Result<DecodedMp3, Error> {
continue;
}
Err(e) => {
return Err(Error::validation(
format!("MP3 decode failed: {e}"),
TARGET,
));
return Err(Error::validation(format!("MP3 decode failed: {e}"), TARGET));
}
}
}
Expand Down Expand Up @@ -250,8 +251,8 @@ pub(super) fn encode_from_pcm(
) -> Result<Vec<u8>, Error> {
let bitrate = snap_bitrate(target_bitrate_bps);

let mut encoder = Builder::new()
.ok_or_else(|| Error::validation("LAME builder failed", TARGET))?;
let mut encoder =
Builder::new().ok_or_else(|| Error::validation("LAME builder failed", TARGET))?;
encoder
.set_sample_rate(sample_rate)
.map_err(|e| Error::validation(format!("LAME sample-rate rejected: {e:?}"), TARGET))?;
Expand Down
8 changes: 5 additions & 3 deletions crates/nvisy-codec/src/handler/audio/mp3_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ use nvisy_core::modality::{Audio, AudioData, AudioLocation};
use nvisy_core::primitive::TimeSpan;
use nvisy_core::redaction::Redactions;

use super::Mp3Loader;
use super::duration::probe_duration_us;
use super::mp3_codec::{decode_to_pcm, encode_from_pcm};
use super::redact;
use super::{Mp3Loader, redact};
use crate::content::{ContentData, ContentSource};
use crate::{Chunk, Format, FormatId, Handler};

Expand Down Expand Up @@ -229,7 +228,10 @@ mod tests {
// boundary where smear is biggest.
let start = sr * 3 / 4;
let end = sr * 5 / 4;
let mean_abs: f32 = decoded.samples[start..end].iter().map(|s| s.abs()).sum::<f32>()
let mean_abs: f32 = decoded.samples[start..end]
.iter()
.map(|s| s.abs())
.sum::<f32>()
/ (end - start) as f32;
assert!(
mean_abs < 0.05,
Expand Down
7 changes: 3 additions & 4 deletions crates/nvisy-codec/src/handler/audio/mp3_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,19 @@ mod tests {

#[tokio::test]
async fn accepts_stereo_mp3() {
let loader = Mp3Loader;
let bytes = fixture_stereo_mp3();
let content = ContentData::new(ContentSource::new(), bytes);
loader.decode(content).await.expect("stereo MP3 should load");
let handler = Mp3Loader.decode(content).await;
handler.expect("stereo MP3 should load");
}

#[tokio::test]
async fn rejects_garbage_bytes() {
let loader = Mp3Loader;
let content = ContentData::new(
ContentSource::new(),
Bytes::from_static(b"definitely not an mp3"),
);
let err = loader.decode(content).await.unwrap_err();
let err = Mp3Loader.decode(content).await.unwrap_err();
assert!(err.to_string().contains("MP3 probe failed"));
}
}
7 changes: 4 additions & 3 deletions crates/nvisy-core/src/context/enhancer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ mod tests {
use super::*;
use crate::context::Context;
use crate::entity::{
EntityKind, ModelProvenance, PatternProvenance, TrailProvenance, TrailStepKind,
EntityLabelRef, ModelProvenance, PatternProvenance, TrailProvenance, TrailStepKind,
builtins,
};
use crate::modality::{Text, TextLocation};

Expand All @@ -240,7 +241,7 @@ mod tests {
format!("pattern `{name}` matched"),
);
Entity::builder()
.with_entity_kind(EntityKind::GovernmentId)
.with_label(EntityLabelRef::from(builtins::GOVERNMENT_ID.name.clone()))
.with_trail(vec![step])
.with_confidence(confidence)
.with_location(TextLocation::new(span.start, span.end))
Expand All @@ -258,7 +259,7 @@ mod tests {
format!("model `{name}` matched"),
);
Entity::builder()
.with_entity_kind(EntityKind::PersonName)
.with_label(EntityLabelRef::from(builtins::PERSON_NAME.name.clone()))
.with_trail(vec![step])
.with_confidence(confidence)
.with_location(TextLocation::new(span.start, span.end))
Expand Down
37 changes: 19 additions & 18 deletions crates/nvisy-core/src/entity/annotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use super::{AnnotationProvenance, Entity, EntityKind, TrailProvenance, TrailStep};
use super::{AnnotationProvenance, Entity, EntityLabelRef, TrailProvenance, TrailStep, builtins};
use crate::modality::{Modality, Overlap};
use crate::primitive::Confidence;

Expand Down Expand Up @@ -82,16 +82,14 @@ pub enum AnnotationStrength {
pub enum AnnotationKind<M: Modality> {
/// Pre-identified region the user wants treated as sensitive.
Inclusion {
/// Specific entity kind. `None` when the user wants the
/// region treated as sensitive without committing to a
/// kind — synthesised entities fall back to
/// [`EntityKind::Unresolved`]. The broad
/// [`EntityCategory`] is derived via
/// [`EntityKind::category`].
/// Label to attach to the synthesised entity. `None`
/// when the user wants the region treated as sensitive
/// without committing to a kind — synthesised entities
/// then fall back to [`builtins::UNRESOLVED`].
///
/// [`EntityCategory`]: super::EntityCategory
#[serde(skip_serializing_if = "Option::is_none")]
entity_kind: Option<EntityKind>,
/// [`builtins::UNRESOLVED`]: super::builtins::UNRESOLVED
#[serde(default, skip_serializing_if = "Option::is_none")]
label: Option<EntityLabelRef>,
/// Modality-specific location this inclusion targets.
target: M::Location,
/// Whether this is an advisory [`Hint`] (LLM may reject) or
Expand Down Expand Up @@ -137,16 +135,20 @@ impl<M: Modality> Annotation<M> {
/// [`Hint`]: AnnotationStrength::Hint
pub fn to_inclusion_entity(&self) -> Option<Entity<M>> {
let AnnotationKind::Inclusion {
entity_kind,
label,
target,
strength: AnnotationStrength::Assert,
} = &self.kind
else {
return None;
};

let label_ref = label
.clone()
.unwrap_or_else(|| EntityLabelRef::from(builtins::UNRESOLVED.name.clone()));

let entity = Entity::builder()
.with_entity_kind(entity_kind.unwrap_or(EntityKind::Unresolved))
.with_label(label_ref)
.with_trail(vec![TrailStep::recognition(
"annotation",
Confidence::MAX,
Expand Down Expand Up @@ -212,15 +214,15 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::entity::EntityCategory;
use crate::entity::builtins;
use crate::modality::{Image, ImageLocation, Text, TextLocation};
use crate::primitive::BoundingBox;

fn inclusion(start: usize, end: usize, strength: AnnotationStrength) -> Annotation<Text> {
Annotation {
name: None,
kind: AnnotationKind::Inclusion {
entity_kind: Some(EntityKind::PersonName),
label: Some(EntityLabelRef::from(builtins::PERSON_NAME.name.clone())),
target: TextLocation::new(start, end),
strength,
},
Expand Down Expand Up @@ -263,14 +265,13 @@ mod tests {
let ann: Annotation<Text> = Annotation {
name: None,
kind: AnnotationKind::Inclusion {
entity_kind: None,
label: None,
target: TextLocation::new(0, 10),
strength: AnnotationStrength::Assert,
},
};
let entity = ann.to_inclusion_entity().unwrap();
assert_eq!(entity.category(), EntityCategory::Unresolved);
assert_eq!(entity.entity_kind, EntityKind::Unresolved);
assert_eq!(entity.label.as_str(), builtins::UNRESOLVED.name.as_str());
}

#[test]
Expand All @@ -285,7 +286,7 @@ mod tests {
let ann: Annotation<Image> = Annotation {
name: Some("face".into()),
kind: AnnotationKind::Inclusion {
entity_kind: Some(EntityKind::PersonName),
label: Some(EntityLabelRef::from(builtins::FACE.name.clone())),
target: ImageLocation::new(bbox),
strength: AnnotationStrength::Assert,
},
Expand Down
72 changes: 0 additions & 72 deletions crates/nvisy-core/src/entity/category.rs

This file was deleted.

Loading