@@ -33,6 +33,8 @@ use ultralytics_inference::visualizer::skeleton::{
3333} ;
3434use ultralytics_inference:: { InferenceConfig , Task } ;
3535
36+ mod onnx_meta;
37+
3638/// Default inference image size used when a model does not record `imgsz` in its
3739/// metadata. Mirrors the native crate's fallback.
3840const DEFAULT_IMGSZ : usize = 640 ;
@@ -154,7 +156,7 @@ async fn ensure_backend(ort_base_url: Option<String>, webgpu: bool) -> Result<()
154156 ort_web:: api ( feature) . await
155157 }
156158 }
157- . map_err ( |e| JsError :: new ( & format ! ( "failed to initialize ort-web backend: {e}" ) ) ) ?;
159+ . map_err ( err_ctx ( "failed to initialize ort-web backend" ) ) ?;
158160 ort:: set_api ( api) ;
159161 BACKEND . with ( |b| * b. borrow_mut ( ) = Some ( requested) ) ;
160162 Ok ( ( ) )
@@ -167,7 +169,7 @@ async fn ensure_backend(ort_base_url: Option<String>, webgpu: bool) -> Result<()
167169/// from the model protobuf, rebuild the `key: value` YAML the native path uses,
168170/// and hand it to the shared parser.
169171fn build_metadata ( model_bytes : & [ u8 ] ) -> Result < ModelMetadata , JsError > {
170- let props = parse_metadata_props ( model_bytes) ;
172+ let props = onnx_meta :: parse_metadata_props ( model_bytes) ;
171173 if props. is_empty ( ) {
172174 return Err ( JsError :: new (
173175 "no metadata found in ONNX model. Export it with Ultralytics \
@@ -180,92 +182,7 @@ fn build_metadata(model_bytes: &[u8]) -> Result<ModelMetadata, JsError> {
180182 . map ( |( k, v) | format ! ( "{k}: {v}" ) )
181183 . collect :: < Vec < _ > > ( )
182184 . join ( "\n " ) ;
183- ModelMetadata :: from_yaml_str ( & combined)
184- . map_err ( |e| JsError :: new ( & format ! ( "failed to parse model metadata: {e}" ) ) )
185- }
186-
187- /// Read a protobuf base-128 varint at `pos`, advancing it. Returns `None` on a
188- /// truncated/oversized value.
189- fn read_varint ( buf : & [ u8 ] , pos : & mut usize ) -> Option < u64 > {
190- let mut result = 0u64 ;
191- let mut shift = 0u32 ;
192- loop {
193- let byte = * buf. get ( * pos) ?;
194- * pos += 1 ;
195- result |= u64:: from ( byte & 0x7f ) << shift;
196- if byte & 0x80 == 0 {
197- return Some ( result) ;
198- }
199- shift += 7 ;
200- if shift >= 64 {
201- return None ;
202- }
203- }
204- }
205-
206- /// Read the next protobuf field at `pos`, advancing it. Returns the field number
207- /// and, for length-delimited fields (wire type 2), the payload bytes; varint and
208- /// fixed-width fields are skipped and yield `None` payload. Returns `None` at the
209- /// end of the buffer or on a malformed field.
210- fn read_field < ' a > ( buf : & ' a [ u8 ] , pos : & mut usize ) -> Option < ( u64 , Option < & ' a [ u8 ] > ) > {
211- let tag = read_varint ( buf, pos) ?;
212- let payload = match tag & 7 {
213- 0 => {
214- read_varint ( buf, pos) ?;
215- None
216- }
217- 1 => {
218- * pos += 8 ;
219- None
220- }
221- 5 => {
222- * pos += 4 ;
223- None
224- }
225- 2 => {
226- let len = read_varint ( buf, pos) ? as usize ;
227- let sub = buf. get ( * pos..* pos + len) ?;
228- * pos += len;
229- Some ( sub)
230- }
231- _ => return None ,
232- } ;
233- Some ( ( tag >> 3 , payload) )
234- }
235-
236- /// Extract `ModelProto.metadata_props` (field 14, repeated
237- /// `StringStringEntryProto`) into a key/value map. Other fields (including the
238- /// large graph) are skipped without being decoded.
239- fn parse_metadata_props ( buf : & [ u8 ] ) -> HashMap < String , String > {
240- let mut map = HashMap :: new ( ) ;
241- let mut pos = 0 ;
242- while let Some ( ( field, payload) ) = read_field ( buf, & mut pos) {
243- if field == 14
244- && let Some ( sub) = payload
245- && let Some ( ( key, value) ) = parse_string_string_entry ( sub)
246- {
247- map. insert ( key, value) ;
248- }
249- }
250- map
251- }
252-
253- /// Parse a `StringStringEntryProto` (field 1 = key, field 2 = value).
254- fn parse_string_string_entry ( buf : & [ u8 ] ) -> Option < ( String , String ) > {
255- let mut pos = 0 ;
256- let mut key = None ;
257- let mut value = None ;
258- while let Some ( ( field, payload) ) = read_field ( buf, & mut pos) {
259- if let Some ( sub) = payload {
260- let text = String :: from_utf8_lossy ( sub) . into_owned ( ) ;
261- match field {
262- 1 => key = Some ( text) ,
263- 2 => value = Some ( text) ,
264- _ => { }
265- }
266- }
267- }
268- Some ( ( key?, value. unwrap_or_default ( ) ) )
185+ ModelMetadata :: from_yaml_str ( & combined) . map_err ( err_ctx ( "failed to parse model metadata" ) )
269186}
270187
271188/// A loaded YOLO model ready for inference in the browser.
@@ -322,7 +239,7 @@ impl YoloModel {
322239 #[ wasm_bindgen( getter) ]
323240 #[ must_use]
324241 pub fn task ( & self ) -> String {
325- format ! ( "{:?}" , self . metadata. task) . to_lowercase ( )
242+ self . metadata . task . as_str ( ) . to_owned ( )
326243 }
327244
328245 /// The active device: `"webgpu"` or `"cpu"` (the fallback when WebGPU is
@@ -339,8 +256,7 @@ impl YoloModel {
339256 /// Returns a JS error only if serialization fails (not expected).
340257 #[ wasm_bindgen( getter) ]
341258 pub fn names ( & self ) -> Result < JsValue , JsError > {
342- serde_wasm_bindgen:: to_value ( & * self . metadata . names )
343- . map_err ( |e| JsError :: new ( & format ! ( "failed to serialize names: {e}" ) ) )
259+ to_js ( & * self . metadata . names , "names" )
344260 }
345261
346262 /// Run inference on a single encoded image (JPEG or PNG bytes).
@@ -359,8 +275,7 @@ impl YoloModel {
359275 iou : f32 ,
360276 classes : Option < Vec < u32 > > ,
361277 ) -> Result < JsValue , JsError > {
362- let dynimg = image:: load_from_memory ( & image)
363- . map_err ( |e| JsError :: new ( & format ! ( "failed to decode image: {e}" ) ) ) ?;
278+ let dynimg = image:: load_from_memory ( & image) . map_err ( err_ctx ( "failed to decode image" ) ) ?;
364279 self . run ( dynimg, conf, iou, classes) . await
365280 }
366281
@@ -411,7 +326,7 @@ impl YoloModel {
411326 builder
412327 . commit_from_memory ( bytes)
413328 . await
414- . map_err ( |e| JsError :: new ( & format ! ( "failed to load model from bytes: {e}" ) ) )
329+ . map_err ( err_ctx ( "failed to load model from bytes" ) )
415330 }
416331
417332 /// Finish constructing a model from a committed session and its parsed
@@ -456,7 +371,7 @@ impl YoloModel {
456371 let rgb = dynimg. to_rgb8 ( ) ;
457372 let ( w, h) = rgb. dimensions ( ) ;
458373 let orig_img = Array3 :: from_shape_vec ( ( h as usize , w as usize , 3 ) , rgb. into_raw ( ) )
459- . map_err ( |e| JsError :: new ( & format ! ( "failed to build image array: {e}" ) ) ) ?;
374+ . map_err ( err_ctx ( "failed to build image array" ) ) ?;
460375
461376 // Classification uses center-crop (like Ultralytics); all other tasks
462377 // use letterbox. Both share the f32 NCHW output.
@@ -474,11 +389,11 @@ impl YoloModel {
474389 . session
475390 . run_async ( ort:: inputs![ input. view( ) ] , & run_options)
476391 . await
477- . map_err ( |e| JsError :: new ( & format ! ( "inference failed: {e}" ) ) ) ?;
392+ . map_err ( err_ctx ( "inference failed" ) ) ?;
478393 // Outputs live in the ONNX Runtime wasm context; copy them back to Rust.
479394 sync_outputs ( & mut outputs)
480395 . await
481- . map_err ( |e| JsError :: new ( & format ! ( "failed to sync outputs: {e}" ) ) ) ?;
396+ . map_err ( err_ctx ( "failed to sync outputs" ) ) ?;
482397
483398 // Borrow each output's data directly (no copy, important for the large
484399 // semantic logits, ~160 MB) and feed it to the shared postprocessor while
@@ -515,8 +430,7 @@ impl YoloModel {
515430 self . metadata . kpt_shape ,
516431 ) ;
517432 let payload = JsResults :: from_results ( & results, self . metadata . task ) ;
518- serde_wasm_bindgen:: to_value ( & payload)
519- . map_err ( |e| JsError :: new ( & format ! ( "failed to serialize results: {e}" ) ) )
433+ to_js ( & payload, "results" )
520434 }
521435}
522436
@@ -526,6 +440,18 @@ fn map_ort<M>(e: ort::Error<M>) -> JsError {
526440 JsError :: new ( & format ! ( "ort error: {e}" ) )
527441}
528442
443+ /// Build a `.map_err` closure that prefixes any displayable error with `context`,
444+ /// so call sites read `.map_err(err_ctx("inference failed"))`.
445+ fn err_ctx < E : std:: fmt:: Display > ( context : & ' static str ) -> impl FnOnce ( E ) -> JsError {
446+ move |e| JsError :: new ( & format ! ( "{context}: {e}" ) )
447+ }
448+
449+ /// Serialize a value to a `JsValue`, mapping a failure to a JS error naming `what`.
450+ fn to_js < T : Serialize > ( value : & T , what : & str ) -> Result < JsValue , JsError > {
451+ serde_wasm_bindgen:: to_value ( value)
452+ . map_err ( |e| JsError :: new ( & format ! ( "failed to serialize {what}: {e}" ) ) )
453+ }
454+
529455/// One detected box, mirroring `Boxes` in the Ultralytics API (pixel `xyxy`).
530456/// `color` is the Ultralytics palette color for the class (`#rrggbb`).
531457#[ derive( Serialize ) ]
@@ -676,7 +602,7 @@ impl JsResults {
676602 } ) ;
677603
678604 Self {
679- task : format ! ( "{ task:?}" ) . to_lowercase ( ) ,
605+ task : task. as_str ( ) . to_owned ( ) ,
680606 width : r. orig_shape . 1 ,
681607 height : r. orig_shape . 0 ,
682608 boxes,
@@ -784,8 +710,7 @@ pub fn pose_palette() -> Result<JsValue, JsError> {
784710 . map ( |& i| Color :: from_pose_index ( i) . to_hex ( ) )
785711 . collect ( ) ,
786712 } ;
787- serde_wasm_bindgen:: to_value ( & scheme)
788- . map_err ( |e| JsError :: new ( & format ! ( "failed to serialize pose palette: {e}" ) ) )
713+ to_js ( & scheme, "pose palette" )
789714}
790715
791716/// Install a panic hook that logs Rust panics to the browser console.
0 commit comments