fgerlits commented on code in PR #2258:
URL: https://github.com/apache/nifi-minifi-cpp/pull/2258#discussion_r4060487812


##########
minifi_rust/extensions/minifi_tensor/features/resources/.gitignore:
##########


Review Comment:
   could these resources go in the build directory?



##########
behave_framework/pyproject.toml:
##########
@@ -9,7 +9,8 @@ dependencies = [
     "PyYAML==6.0.3",
     "humanfriendly==10.0",
     "cryptography==50.0.0",
-    "pyjks==20.0.0"
+    "pyjks==20.0.0",
+    "certifi==2026.7.22"

Review Comment:
   `certifi` is just a list of root certificates, right? if so, then newer is 
better:
   ```suggestion
       "certifi>=2026.7.22"
   ```



##########
minifi_rust/extensions/minifi_tensor/src/low_level_processors/classify_output/classify_output_def.rs:
##########
@@ -0,0 +1,164 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use super::{ClassifyOutput, ScoreActivation};
+use minifi_native::{
+    OutputAttribute, ProcessorDefinition, ProcessorInputRequirement, Property, 
PropertyDefinition,
+    Relationship, property_definitions,
+};
+use std::path::PathBuf;
+
+pub(crate) const TOP_K: Property<usize> = Property::new(
+    "Top K",
+    "Number of highest-scoring classes to include in the output JSON, in 
descending \
+                  order of confidence. Values above the total class count are 
clamped. Set to 1 \
+                  for pure top-1 classification.",
+)
+.with_default("5");
+
+pub(crate) const SCORE_OUTPUT_INDEX: Property<usize> = Property::new(
+    "Score output index",
+    "Zero-based index of the model output tensor that holds classification 
scores. \
+                  The processor slices the concatenated payload from 
InvokeTractModel according \
+                  to the 'tensor.N.bytes' attributes. Almost always 0 for 
single-head \
+                  classifiers.",
+)
+.with_default("0");
+
+pub(crate) const SCORE_ACTIVATION: Property<ScoreActivation> = Property::new(
+    "Score activation",
+    "Activation applied to the raw score vector before ranking. \
+                  Softmax = mutually-exclusive classes (ImageNet-trained 
ResNet/MobileNet/\
+                  EfficientNet raw logits). \
+                  Sigmoid = independent classes (multi-label classifiers). \
+                  None = the model already emits probabilities/scores; rank 
the raw values.",
+)
+.with_default(ScoreActivation::Softmax.into_str());
+
+pub(crate) const CONFIDENCE_THRESHOLD: Property<f32> = Property::new(
+    "Confidence Threshold",
+    "Minimum confidence a class must reach to be included in the output JSON. \
+                  Applied AFTER activation, so the units match the chosen 
activation \
+                  (0.0..=1.0 for Softmax/Sigmoid, model-native for None). Set 
to 0.0 to always \
+                  emit exactly Top K predictions.",
+)
+.with_default("0.0")
+.supports_expression_language();
+
+pub(crate) const LABELS_FILE_PATH: Property<Option<PathBuf>> = Property::new(
+    "Labels file path",
+    "Optional path to a newline-separated labels file (line N = name of class 
N). \
+                  Loaded once at service enable time. When set, each 
prediction in the output \
+                  JSON gains a 'class_name' field and the 'class.top1.name' 
flow file attribute \
+                  is populated. Leave empty to emit numeric class IDs only.",
+);
+
+pub(crate) const LABEL_INDEX_OFFSET: Property<usize> = Property::new(
+    "Label index offset",
+    "Offset added to the model's class ID when looking up a name in the labels 
file. \
+                  Defaults to 0 (labels file line N = class N). Set to 1 for 
label files that \
+                  start with a dummy/background entry — e.g. the ONNX 
MobileNetV2 model emits \
+                  1000 class scores while 'imagenet_slim_labels.txt' has 1001 
lines (line 0 = \
+                  'dummy'), so class ID 653 maps to line 654 = 'military 
uniform'.",
+)
+.with_default("0");
+
+pub(super) const SUCCESS: Relationship = Relationship {
+    name: "success",
+    description: "Classification completed. The flow file content is a JSON 
array of the Top K \
+                  predictions (possibly fewer if the confidence threshold 
filtered some out).",
+};
+
+pub(super) const FAILURE: Relationship = Relationship {
+    name: "failure",
+    description: "The upstream output attributes were missing/invalid or the 
score tensor could \
+                  not be interpreted as f32 values.",
+};
+
+const MIME_TYPE_ATTR: OutputAttribute = OutputAttribute {
+    name: "mime.type",
+    relationships: &["success"],
+    description: "Always 'application/json' — the output payload is a JSON 
array of prediction \
+                  objects with fields class_id, confidence, and optional 
class_name.",
+};
+
+const CLASS_COUNT_ATTR: OutputAttribute = OutputAttribute {
+    name: "class.count",
+    relationships: &["success"],
+    description: "Number of predictions retained after Top K selection and 
confidence filtering.",
+};
+
+const CLASS_TOP1_ID_ATTR: OutputAttribute = OutputAttribute {
+    name: "class.top1.id",
+    relationships: &["success"],
+    description: "Numeric class ID of the highest-confidence prediction, when 
at least one \
+                  prediction cleared the confidence threshold.",
+};
+
+const CLASS_TOP1_CONFIDENCE_ATTR: OutputAttribute = OutputAttribute {
+    name: "class.top1.confidence",
+    relationships: &["success"],
+    description: "Confidence (post-activation) of the highest-confidence 
prediction, when at \
+                  least one prediction cleared the confidence threshold.",
+};
+
+const CLASS_TOP1_NAME_ATTR: OutputAttribute = OutputAttribute {
+    name: "class.top1.name",
+    relationships: &["success"],
+    description: "Label of the highest-confidence prediction. Only present 
when 'Labels file \
+                  path' was configured and at least one prediction cleared the 
threshold.",
+};
+
+pub(crate) const OUTPUT_ATTRIBUTE_NAME: Property<Option<String>> = 
Property::new(
+    "Output attribute name",
+    "Specify the attribute to use as output, if not provided, the content is 
overridden instead.",
+)
+.supports_expression_language();

Review Comment:
   please move this up in this file, so all the properties are together



##########
minifi_rust/extensions/minifi_tensor/src/low_level_processors/classify_output.rs:
##########
@@ -0,0 +1,476 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::utils::score_activation::{ScoreActivation, SoftmaxTerms};
+use crate::utils::tensor_helpers::{deserialize_tensors, tensor_as_f32, 
tensor_shape};
+use classify_output_def::SUCCESS;
+pub(crate) use classify_output_def::{
+    CLASSIFY_OUTPUT_ATTRIBUTES, CONFIDENCE_THRESHOLD, LABEL_INDEX_OFFSET, 
LABELS_FILE_PATH,
+    OUTPUT_ATTRIBUTE_NAME, SCORE_ACTIVATION, SCORE_OUTPUT_INDEX, TOP_K,
+};
+use minifi_native::macros::ComponentIdentifier;
+use minifi_native::{
+    Content, FlowFileTransform, GetAttribute, GetId, GetProperty, InputStream, 
Logger, MinifiError,
+    ProcessError, RouteErrorExt, Schedule, TransformedFlowFile, warn,
+};
+use serde::Serialize;
+use std::path::Path;
+use tract::Tensor;
+
+mod classify_output_def;
+
+#[derive(Serialize, Clone, Debug, PartialEq)]
+struct Prediction {
+    class_id: usize,
+    confidence: f32,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    class_name: Option<String>,
+}
+
+fn load_labels(path: &Path) -> Result<Vec<String>, MinifiError> {
+    let content = std::fs::read_to_string(path).map_err(|e| {
+        MinifiError::custom(format!("Failed to read labels file '{:?}': {}", 
path, e))
+    })?;
+    Ok(content
+        .lines()
+        .map(|line| line.trim().to_string())
+        .collect())
+}
+
+fn top_k(mut scored: Vec<(usize, f32)>, k: usize) -> Vec<(usize, f32)> {
+    scored.sort_by(|&(ai, a), &(bi, b)| b.total_cmp(&a).then(ai.cmp(&bi)));
+    scored.truncate(k);
+    scored
+}
+
+#[derive(ComponentIdentifier)]
+pub(crate) struct ClassifyOutput {
+    top_k: usize,
+    score_output_index: usize,
+    score_activation: ScoreActivation,
+    confidence_threshold: f32,
+    labels: Vec<String>,
+    label_index_offset: usize,
+}
+
+impl Schedule for ClassifyOutput {
+    fn schedule<Ctx: GetProperty, L: Logger>(
+        context: &Ctx,
+        _logger: &L,
+    ) -> Result<Self, MinifiError>
+    where
+        Self: Sized,
+    {
+        let top_k = context.get_property(&TOP_K)?;
+        if top_k == 0 {
+            return Err(MinifiError::validation("Top K must be >= 1"));
+        }
+        let score_output_index = context.get_property(&SCORE_OUTPUT_INDEX)?;
+        let score_activation = context.get_property(&SCORE_ACTIVATION)?;
+        let confidence_threshold = 
context.get_property(&CONFIDENCE_THRESHOLD)?;
+
+        let labels = match context.get_property(&LABELS_FILE_PATH)? {
+            Some(path) => load_labels(&path)?,
+            _ => Vec::new(),

Review Comment:
   Nitpicking, but why isn't `labels` an `Option<Vec<String>>`, set to `None` 
if the property is unset? I think the code would read better that way, and 
`label_for` could throw an error if the index is not found.



##########
minifi_rust/extensions/minifi_tensor/src/low_level_processors/mod.rs:
##########


Review Comment:
   How does `#[cfg(feature = "low-level-processors")]` work? Specifically, why 
do we have it only on `invoke_tract_model` here, but on all 4 low-level 
processors in `lib.rs`?



##########
minifi_rust/extensions/minifi_tensor/src/low_level_processors/classify_output.rs:
##########


Review Comment:
   There are no unit tests for `ScoreActivation::Sigmoid`, only for `None` and 
`Softmax`.



##########
minifi_rust/extensions/minifi_tensor/features/detection.feature:
##########
@@ -0,0 +1,131 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+@SUPPORTS_WINDOWS
+Feature: Face detection with UltraFace (SSD)

Review Comment:
   Can we write this abbreviation out, please? I had to google it, and it isn't 
even easy to google, because most of the pages which come up are about 
"solid-state drive".
   ```suggestion
   Feature: Face detection with UltraFace Single Shot MultiBox Detector (SSD)
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to