Skip to content

Cleanup, new lint rules, decreased complexity - #425

Draft
kozlov721 wants to merge 4 commits into
mainfrom
ci/complexipy
Draft

Cleanup, new lint rules, decreased complexity#425
kozlov721 wants to merge 4 commits into
mainfrom
ci/complexipy

Conversation

@kozlov721

@kozlov721 kozlov721 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Purpose

Specification

  • Added complexipy to pre-commit
  • Simplified too complex functions/methods, removed duplicate and dead code
  • No functional change, only restructuring, breaking some long functions to smaller parts etc.

Dependencies & Potential Impact

None / not applicable

Deployment Plan

None / not applicable

Testing & Validation

None / not applicable

AI Usage

Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]

Submitted code was reviewed by a human: YES/NO

The author is taking the responsibility for the contribution: YES/NO

Summary by CodeRabbit

  • Improvements
    • Improved visualization handling (keypoints rendering, image alignment/resizing, and video inference output).
    • Improved training robustness with clearer “no train batches” error details, plus more consistent checkpoint/metric/scheduler behavior.
    • Improved configuration, annotation generation, and text metadata decoding.
    • Improved model export, conversion, tuning, and quantization workflows.
  • Documentation
    • Refreshed README and examples formatting, including updated API/customization and code block layout.
  • Chores
    • Updated pre-commit/linting configuration and cleaned up duplicate component exports.

@kozlov721
kozlov721 requested a review from a team as a code owner July 26, 2026 20:55
@kozlov721
kozlov721 requested review from conorsim, klemen1999 and tersekmatija and removed request for a team July 26, 2026 20:55
@github-actions github-actions Bot added documentation Improvements or additions to documentation CLI Changes affecting the CLI labels Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request decomposes model configuration, training, export, inference, data loading, visualization, annotation, and utility logic into focused helpers. It also updates lint hooks, documentation examples, and integration-test setup while preserving existing workflows.

Changes

Cross-cutting refactor

Layer / File(s) Summary
Tooling and documentation updates
.pre-commit-config.yaml, pyproject.toml, README.md, examples/README.md
Pre-commit revisions, a complexity hook, lint ignores, and documentation examples are updated.
Configuration and model setup
luxonis_train/config/*, luxonis_train/core/core.py, luxonis_train/upgrade.py
Configuration validation, migration, predefined-model setup, loader initialization, metadata resolution, and checkpoint loading are reorganized into helpers.
Export, conversion, tuning, and inference workflows
luxonis_train/core/core.py, luxonis_train/core/utils/{export_utils,infer_utils,tune_utils}.py
Export artifacts, HubAI conversion, ONNX initializer handling, video inference, tuning, and quantization are split into helper-driven flows.
Runtime data and node plumbing
luxonis_train/loaders/*, luxonis_train/nodes/*, luxonis_train/__main__.py
Loader collation, dataset validation, node input resolution, visualization retrieval, backbone construction, neck construction, and export declarations are refactored.
Attached modules, visualization, and callbacks
luxonis_train/attached_modules/*, luxonis_train/callbacks/*
Parameter mapping, metric-state registration, rendering, resizing, EMA restoration, no-batch diagnostics, and progress output use dedicated helpers.
Lightning execution and logging
luxonis_train/lightning/*
Forward execution, checkpoint loading, logging, MLflow key generation, optimizer construction, and balanced-image logging are decomposed.
Annotation and numerical utility pipelines
luxonis_train/utils/*, luxonis_train/core/utils/annotate_utils.py
Annotation generation, NMS, IoU loss, batching, text decoding, and dataset-record handling are modularized.
Integration test helpers
tests/integration/*
Repeated ONNX normalization, input generation, and predefined-model inference test logic is centralized.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • luxonis/luxonis-train#366: The AIMET quantization refactor and Lightning/export restructuring overlap with the quantization integration introduced there.

Suggested labels: enhancement, tests

Suggested reviewers: klemen1999, tersekmatija, conorsim

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is broadly related, but it is too generic to clearly describe the main changes in the PR. Use a more specific title like “Add complexipy pre-commit checks and refactor complex functions”.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/complexipy
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch ci/complexipy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
luxonis_train/config/config.py (1)

146-156: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

last_body_index can be -1 when the first node is a head.

At Line 147, if node 0 has "Head" in its name, last_body_index becomes -1, and every subsequent node without explicit inputs resolves names[-1], which is the node's own just-appended name → a self-referencing edge (later rejected by the acyclic check with a confusing message). Guard against i == 0.

🐛 Proposed fix
-            if "Head" in name and last_body_index is None:
+            if "Head" in name and last_body_index is None and i > 0:
                 last_body_index = i - 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/config/config.py` around lines 146 - 156, Guard the
last_body_index assignment in the node-processing logic so a head at index 0
does not set it to -1. Update the condition around the "Head" check to require i
> 0, preserving the existing previous-node resolution for later head nodes and
preventing subsequent nodes from using names[-1] as their predecessor.
🧹 Nitpick comments (13)
pyproject.toml (1)

113-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Scope the PLR0917 ignore narrowly.

Ignoring this rule globally can hide future functions with excessive positional arguments—the kind of complexity regression this PR is intended to expose. If existing APIs require exceptions, use per-file or line-level ignores with a brief rationale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 113, Replace the global PLR0917 ignore in the Ruff
configuration with narrowly scoped per-file or line-level exceptions only where
existing APIs require them, and add a brief rationale for each exception. Ensure
new functions remain subject to PLR0917.
luxonis_train/lightning/luxonis_lightning.py (3)

1325-1334: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse _output_order in forward.

forward (Lines 256-263) builds the identical sorted (node_name, output_name, i) list inline; now that the helper exists, calling it there removes the duplicate ordering logic and keeps export/forward ordering guaranteed in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/lightning/luxonis_lightning.py` around lines 1325 - 1334,
Update forward to call the existing _output_order helper instead of constructing
the sorted (node_name, output_name, index) list inline. Remove the duplicated
ordering logic while preserving forward’s current output ordering and downstream
behavior.

1212-1215: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead loop: for _ in sorted(val_eval_epochs) adds the same key repeatedly.

The val metric key doesn't depend on the epoch, so the loop only reduces to "add if val_eval_epochs is non-empty". Making that explicit removes the pointless sort/iteration and preserves the empty-set semantics.

♻️ Proposed simplification
         else:
-            for _ in sorted(val_eval_epochs):
-                metric_keys.add(f"val/metric/{formatted_node_name}/{sub_name}")
+            if val_eval_epochs:
+                metric_keys.add(f"val/metric/{formatted_node_name}/{sub_name}")
             metric_keys.add(f"test/metric/{formatted_node_name}/{sub_name}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/lightning/luxonis_lightning.py` around lines 1212 - 1215, In
the metric-key construction branch, replace the epoch loop around
val_eval_epochs with a single conditional that adds the val metric key only when
val_eval_epochs is non-empty. Keep the test metric key added unconditionally in
the existing else branch, and remove the unnecessary sorting and iteration.

363-423: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Loose Any annotations introduced by the helper extraction. The extracted helpers dropped the concrete types that are already known at the call sites (NodeWrapper from Nodes.traverse()/Nodes.items(), and list[str] order lists per _get_node_order_mapping), so the new boundaries are untyped.

  • luxonis_train/lightning/luxonis_lightning.py#L363-L423: annotate the node parameter of _collect_node_results, _collect_losses, _update_metrics, and _collect_visualizations as NodeWrapper (already imported at Line 40).
  • luxonis_train/lightning/luxonis_lightning.py#L689-L691: type _prepare_checkpoint's return as tuple[dict[str, Any], Version, list[str] | None, list[str]] and propagate list[str] | None / list[str] to the old_order/new_order parameters of _load_node_checkpoint and _load_with_order_mapping.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/lightning/luxonis_lightning.py` around lines 363 - 423, Replace
the loose node annotations in _collect_node_results, _collect_losses,
_update_metrics, and _collect_visualizations with NodeWrapper. In
luxonis_train/lightning/luxonis_lightning.py:363-423, update only these helper
parameters; in luxonis_train/lightning/luxonis_lightning.py:689-691, annotate
_prepare_checkpoint as returning tuple[dict[str, Any], Version, list[str] |
None, list[str]] and use list[str] | None for old_order and list[str] for
new_order in _load_node_checkpoint and _load_with_order_mapping.
luxonis_train/config/config.py (1)

284-311: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Renamed aliases are not registered in names.

Line 311 adds the original name rather than the (possibly rewritten) module.alias, so a later module whose name equals a generated alias won't be detected as a duplicate. Tracking module.alias or module.name keeps the set consistent with what is actually emitted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/config/config.py` around lines 284 - 311, Update
_make_node_module_names_unique so the names set records the module’s final
emitted identifier after any alias generation or renaming, using module.alias or
module.name rather than the original name variable. Preserve the existing
duplicate detection and warning behavior.
luxonis_train/core/core.py (1)

397-411: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Weight normalization/download duplicated with _normalize_weights_and_load_ckpt.

__init__ already normalized the dict and downloaded the checkpoint; this re-does both (a second download via LuxonisFileSystem.download). Passing the already-loaded ckpt through would avoid the redundant network/disk work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/core/core.py` around lines 397 - 411, Update
_load_initial_weights to reuse the already normalized and downloaded checkpoint
produced by _normalize_weights_and_load_ckpt instead of rewrapping dicts or
calling LuxonisFileSystem.download again. Preserve the existing config-weight
warning and pass the reused checkpoint directly to
lightning_module.load_checkpoint.
luxonis_train/core/utils/annotate_utils.py (2)

102-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type head more precisely.

head: object loses all type information at the call site. The argument is always lt_module.nodes[head_name].module, so annotate it as the node/module base type (e.g. BaseNode or nn.Module) while keeping the isinstance(head, lxt.BaseHead) narrowing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/core/utils/annotate_utils.py` around lines 102 - 109, Update
the head parameter annotation in _annotated_records from object to the
appropriate node/module base type used by lt_module.nodes[head_name].module,
such as BaseNode or nn.Module, while preserving the existing isinstance(head,
lxt.BaseHead) narrowing and behavior.

113-118: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Narrow the suppress scope to record construction only.

yield inside with suppress(Exception) also swallows exceptions thrown back into the generator by the consumer (e.g. failures inside LuxonisDataset.add), which can silently drop errors unrelated to invalid predictions. Build the record inside the guard and yield outside it.

♻️ Proposed refactor
-        # Skip predictions that are invalid, e.g. outside the clipping range.
-        with suppress(Exception):
-            yield DatasetRecord(**record)
+        # Skip predictions that are invalid, e.g. outside the clipping range.
+        try:
+            dataset_record = DatasetRecord(**record)
+        except Exception:  # noqa: BLE001
+            continue
+        yield dataset_record
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/core/utils/annotate_utils.py` around lines 113 - 118, Update
the record conversion path after the DatasetRecord check so the
suppress(Exception) context covers only DatasetRecord(**record) construction;
store the constructed record, exit the suppression scope, then yield it outside
the guard. Preserve skipping invalid predictions while allowing consumer-thrown
exceptions from the generator yield to propagate.
luxonis_train/utils/annotation.py (2)

127-180: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

instance_segmentation and segmentation share the norm_masks key.

If a head requires both labels, the second branch overwrites the first and both annotators emit the same masks. Use distinct keys (norm_instance_masks / norm_semantic_masks) to make the helper collision-free.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/utils/annotation.py` around lines 127 - 180, Update
_prepare_transformed so instance_segmentation stores transformed masks under
norm_instance_masks and segmentation stores them under norm_semantic_masks,
preventing either branch from overwriting the other when both labels are
required. Keep the existing transformation logic unchanged.

301-314: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Non-deterministic annotation ordering.

required_labels is a set, so _emit_annotations emits per-task records in arbitrary order across runs. Iterating _ANNOTATORS and filtering by membership gives a stable order at no cost.

♻️ Proposed refactor
-    for task in required_labels:
-        annotator = _ANNOTATORS.get(task)
-        if annotator is not None:
-            yield from annotator(
-                head, img_path, preds_for_image, transformed, i
-            )
+    for task, annotator in _ANNOTATORS.items():
+        if task in required_labels:
+            yield from annotator(
+                head, img_path, preds_for_image, transformed, i
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/utils/annotation.py` around lines 301 - 314, Update
_emit_annotations to iterate through _ANNOTATORS in its defined order and emit
annotations only for tasks present in required_labels, instead of iterating
required_labels directly. Preserve the existing annotator lookup and yield
behavior for matching tasks.
luxonis_train/utils/general.py (1)

365-388: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tighten the helper return types and the batch_size falsy check.

Both helpers return Iterator[Any], which drops the Iterator[tuple[Tensor, ...]] | Iterator[Tensor] contract the public overloads advertise. Also, batch_size or int(bboxes[:, 0].max()) + 1 silently falls back for batch_size == 0; an explicit is None check matches _empty_batch_instances.

♻️ Proposed refactor
-def _batched_instances(
-    bboxes: Tensor, args: tuple[Tensor, ...], batch_size: int | None
-) -> Iterator[Any]:
-    n_batches = batch_size or int(bboxes[:, 0].max()) + 1
+def _batched_instances(
+    bboxes: Tensor, args: tuple[Tensor, ...], batch_size: int | None
+) -> Iterator[tuple[Tensor, ...] | Tensor]:
+    n_batches = (
+        batch_size
+        if batch_size is not None
+        else int(bboxes[:, 0].max()) + 1
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/utils/general.py` around lines 365 - 388, Update
_empty_batch_instances and _batched_instances to return Iterator[tuple[Tensor,
...]] | Iterator[Tensor], matching the public overload contract instead of
Iterator[Any]. In _batched_instances, replace the falsy batch_size fallback with
an explicit None check so batch_size == 0 produces zero batches while only None
derives the count from bboxes.
luxonis_train/utils/boundingbox.py (2)

343-384: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Recompute of x[candidate_mask_i] in the additional-fields path.

Line 375 re-slices x[candidate_mask_i] even though the same slice was already produced at Line 359. Hold the original slice in a local before _apply_objectness mutates/replaces curr_out and reuse it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/utils/boundingbox.py` around lines 343 - 384, In
_nms_single_image, preserve the initial x[candidate_mask_i] slice in a local
variable before _apply_objectness updates curr_out, then reuse that variable
when selecting additional fields instead of re-slicing x[candidate_mask_i].

299-299: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

keep_mask is allocated on CPU while bboxes/curr_out may be on CUDA.

torch.zeros(bboxes.size(0)).bool() has no device=, so bboxes[keep_mask] mixes devices. Add device=bboxes.device while the code is being extracted.

🛡️ Proposed fix
-    keep_mask = torch.zeros(bboxes.size(0)).bool()
+    keep_mask = torch.zeros(bboxes.size(0), dtype=torch.bool, device=bboxes.device)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/utils/boundingbox.py` at line 299, Update the keep_mask
allocation in the bounding-box filtering logic to create the boolean tensor on
bboxes.device, ensuring it matches bboxes and curr_out when they are on CUDA.
Preserve the existing size and boolean dtype.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@luxonis_train/attached_modules/base_attached_module.py`:
- Around line 223-260: Update _validate_parameter_types to handle signature
parameters absent from kwargs, preserving callee defaults instead of indexing
unconditionally; skip validation for missing keys while continuing to validate
every present value, so _add_parameter’s non-empty-default path cannot raise
KeyError.

In `@luxonis_train/attached_modules/visualizers/utils.py`:
- Around line 432-436: Update the ValueError message in the resize_along
validation to interpolate the actual resize_along value by making the string an
f-string. Preserve the existing validation condition and valid-options text.

In `@luxonis_train/callbacks/fail_on_no_train_batches.py`:
- Around line 55-62: Update _loader_details to retrieve
trainer.fit_loop._combined_loader via getattr with a None fallback, then
preserve the existing flattened-list handling while safely treating a missing
combined loader as unavailable. Ensure this diagnostic path cannot raise
AttributeError and still allows the caller to produce the actionable
zero-training-batches RuntimeError.

In `@luxonis_train/config/config.py`:
- Around line 1027-1032: Update the accumulate_grad_batches calculation in the
trainer configuration to clamp the computed value to at least 1, including when
trainer.batch_size exceeds 64. Preserve the existing assignment and logging
behavior while ensuring downstream loss weights and scheduling never receive
zero.

In `@luxonis_train/core/core.py`:
- Line 182: Update the torch.load call in the checkpoint-loading flow to pass
weights_only=True, preserving subsequent access to the plain-data config and
dataset_metadata fields; if that payload cannot support weights_only, correct
the suppression to # nosemgrep and include a justification.
- Around line 634-641: Update the YAML export flow around
_upload_export_artifact so the file is fully closed before uploading: retain the
generated path while inside the with open block, then invoke the upload
afterward. Pass the intended artifact name rather than f.name as the upload
name, avoiding the absolute file path.
- Around line 167-183: Update _normalize_weights_and_load_ckpt to report the
requested weights value in the safe_download failure message instead of
downloaded, and add an explicit fallback for unsupported weights types that
raises a clear validation error rather than returning None.

In `@luxonis_train/lightning/luxonis_lightning.py`:
- Around line 942-955: Update _prepare_balanced_labels to prune the segmentation
background only when the corresponding classification key exists in labels_copy;
skip segmentation-only entries without raising a missing-key error, while
preserving the existing slicing behavior for present classification labels.

In `@luxonis_train/nodes/base_node.py`:
- Around line 725-741: Add a catch-all branch to the attach_index pattern match
in get_attached so unsupported values raise ValueError instead of falling
through and returning None. Preserve the existing handling for "all", integer
indices, slices, and None, and include the invalid attach_index value in the
error context.
- Around line 619-622: Update the index calculation near input_name and the
name-in-xyz check so the regex-derived idx assignment runs only when the xyz
branch did not assign an index. Preserve indices 0, 1, and 2 for x, y, and z
respectively, while retaining the existing regex fallback for other parameter
names.

In `@luxonis_train/upgrade.py`:
- Around line 130-135: Update the config format check in the
configuration-loading function to compare Path.suffix against the dotted JSON
extension, ensuring .json files use json.loads while other files continue
through yaml.safe_load. Apply the same correction to the config handling in the
__main__.py config command.

In `@luxonis_train/utils/annotation.py`:
- Around line 80-82: Update default_annotate so an image with all-empty
predictions yields its bare annotation and then continues processing the
remaining image paths in the batch. Replace the early generator termination
after _is_all_empty(preds_for_image, required_labels) with iteration
continuation, preserving the existing annotation output.
- Around line 259-288: Update _annotate_classification and _annotate_text to use
the already-selected per-image values in transformed without indexing them by i
again. Compute the class from transformed["pred_classes"].argmax(), and read the
decoded text from transformed["pred_text"] as the list[tuple[str, float]]
result, preserving the existing annotation structure.

In `@luxonis_train/utils/boundingbox.py`:
- Around line 581-591: Update the fallback mask in _bbox_weight to create
torch.ones_like(target_scores.sum(-1)) with dtype=torch.bool, ensuring
torch.masked_select receives a boolean mask when mask_positive is absent.

In `@tests/integration/test_export_unique_identifiers.py`:
- Around line 20-36: Update the test input generation around dtype_map and the
loop over onnx_model.graph.input so supported ONNX element types receive
compatible generated values; for any unsupported type, raise a clear error
instead of using the current np.float32 fallback. Preserve the existing
dynamic-dimension handling and initializer exclusion.

In `@tests/integration/test_predefined_models.py`:
- Around line 48-49: Update the output assertion in _assert_infer_output so it
runs for the "loader" subtest, matching the value passed by the loop, while
preserving the existing dataset-case behavior as appropriate.

---

Outside diff comments:
In `@luxonis_train/config/config.py`:
- Around line 146-156: Guard the last_body_index assignment in the
node-processing logic so a head at index 0 does not set it to -1. Update the
condition around the "Head" check to require i > 0, preserving the existing
previous-node resolution for later head nodes and preventing subsequent nodes
from using names[-1] as their predecessor.

---

Nitpick comments:
In `@luxonis_train/config/config.py`:
- Around line 284-311: Update _make_node_module_names_unique so the names set
records the module’s final emitted identifier after any alias generation or
renaming, using module.alias or module.name rather than the original name
variable. Preserve the existing duplicate detection and warning behavior.

In `@luxonis_train/core/core.py`:
- Around line 397-411: Update _load_initial_weights to reuse the already
normalized and downloaded checkpoint produced by
_normalize_weights_and_load_ckpt instead of rewrapping dicts or calling
LuxonisFileSystem.download again. Preserve the existing config-weight warning
and pass the reused checkpoint directly to lightning_module.load_checkpoint.

In `@luxonis_train/core/utils/annotate_utils.py`:
- Around line 102-109: Update the head parameter annotation in
_annotated_records from object to the appropriate node/module base type used by
lt_module.nodes[head_name].module, such as BaseNode or nn.Module, while
preserving the existing isinstance(head, lxt.BaseHead) narrowing and behavior.
- Around line 113-118: Update the record conversion path after the DatasetRecord
check so the suppress(Exception) context covers only DatasetRecord(**record)
construction; store the constructed record, exit the suppression scope, then
yield it outside the guard. Preserve skipping invalid predictions while allowing
consumer-thrown exceptions from the generator yield to propagate.

In `@luxonis_train/lightning/luxonis_lightning.py`:
- Around line 1325-1334: Update forward to call the existing _output_order
helper instead of constructing the sorted (node_name, output_name, index) list
inline. Remove the duplicated ordering logic while preserving forward’s current
output ordering and downstream behavior.
- Around line 1212-1215: In the metric-key construction branch, replace the
epoch loop around val_eval_epochs with a single conditional that adds the val
metric key only when val_eval_epochs is non-empty. Keep the test metric key
added unconditionally in the existing else branch, and remove the unnecessary
sorting and iteration.
- Around line 363-423: Replace the loose node annotations in
_collect_node_results, _collect_losses, _update_metrics, and
_collect_visualizations with NodeWrapper. In
luxonis_train/lightning/luxonis_lightning.py:363-423, update only these helper
parameters; in luxonis_train/lightning/luxonis_lightning.py:689-691, annotate
_prepare_checkpoint as returning tuple[dict[str, Any], Version, list[str] |
None, list[str]] and use list[str] | None for old_order and list[str] for
new_order in _load_node_checkpoint and _load_with_order_mapping.

In `@luxonis_train/utils/annotation.py`:
- Around line 127-180: Update _prepare_transformed so instance_segmentation
stores transformed masks under norm_instance_masks and segmentation stores them
under norm_semantic_masks, preventing either branch from overwriting the other
when both labels are required. Keep the existing transformation logic unchanged.
- Around line 301-314: Update _emit_annotations to iterate through _ANNOTATORS
in its defined order and emit annotations only for tasks present in
required_labels, instead of iterating required_labels directly. Preserve the
existing annotator lookup and yield behavior for matching tasks.

In `@luxonis_train/utils/boundingbox.py`:
- Around line 343-384: In _nms_single_image, preserve the initial
x[candidate_mask_i] slice in a local variable before _apply_objectness updates
curr_out, then reuse that variable when selecting additional fields instead of
re-slicing x[candidate_mask_i].
- Line 299: Update the keep_mask allocation in the bounding-box filtering logic
to create the boolean tensor on bboxes.device, ensuring it matches bboxes and
curr_out when they are on CUDA. Preserve the existing size and boolean dtype.

In `@luxonis_train/utils/general.py`:
- Around line 365-388: Update _empty_batch_instances and _batched_instances to
return Iterator[tuple[Tensor, ...]] | Iterator[Tensor], matching the public
overload contract instead of Iterator[Any]. In _batched_instances, replace the
falsy batch_size fallback with an explicit None check so batch_size == 0
produces zero batches while only None derives the count from bboxes.

In `@pyproject.toml`:
- Line 113: Replace the global PLR0917 ignore in the Ruff configuration with
narrowly scoped per-file or line-level exceptions only where existing APIs
require them, and add a brief rationale for each exception. Ensure new functions
remain subject to PLR0917.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98373d48-1b44-4875-9d8a-ecbdd7a21733

📥 Commits

Reviewing files that changed from the base of the PR and between 8d9d3ae and 90529f6.

📒 Files selected for processing (37)
  • .pre-commit-config.yaml
  • README.md
  • examples/README.md
  • luxonis_train/__main__.py
  • luxonis_train/attached_modules/base_attached_module.py
  • luxonis_train/attached_modules/metrics/base_metric.py
  • luxonis_train/attached_modules/visualizers/fomo_visualizer.py
  • luxonis_train/attached_modules/visualizers/utils.py
  • luxonis_train/callbacks/ema.py
  • luxonis_train/callbacks/fail_on_no_train_batches.py
  • luxonis_train/callbacks/luxonis_progress_bar.py
  • luxonis_train/config/config.py
  • luxonis_train/config/predefined_models/base_predefined_model.py
  • luxonis_train/core/core.py
  • luxonis_train/core/utils/annotate_utils.py
  • luxonis_train/core/utils/export_utils.py
  • luxonis_train/core/utils/infer_utils.py
  • luxonis_train/core/utils/tune_utils.py
  • luxonis_train/lightning/luxonis_lightning.py
  • luxonis_train/lightning/utils.py
  • luxonis_train/loaders/base_loader.py
  • luxonis_train/loaders/dummy_loader.py
  • luxonis_train/loaders/luxonis_loader_torch.py
  • luxonis_train/nodes/backbones/rexnetv1.py
  • luxonis_train/nodes/base_node.py
  • luxonis_train/nodes/blocks/__init__.py
  • luxonis_train/nodes/blocks/blocks.py
  • luxonis_train/nodes/blocks/unet.py
  • luxonis_train/nodes/necks/reppan_neck/reppan_neck.py
  • luxonis_train/upgrade.py
  • luxonis_train/utils/annotation.py
  • luxonis_train/utils/boundingbox.py
  • luxonis_train/utils/general.py
  • pyproject.toml
  • tests/integration/test_bump_opset_version.py
  • tests/integration/test_export_unique_identifiers.py
  • tests/integration/test_predefined_models.py
💤 Files with no reviewable changes (1)
  • luxonis_train/nodes/blocks/init.py

Comment thread luxonis_train/attached_modules/base_attached_module.py
Comment thread luxonis_train/attached_modules/visualizers/utils.py
Comment thread luxonis_train/callbacks/fail_on_no_train_batches.py
Comment thread luxonis_train/config/config.py Outdated
Comment thread luxonis_train/core/core.py
Comment thread luxonis_train/utils/annotation.py Outdated
Comment on lines +259 to +288
def _annotate_classification(
head: "lxt.nodes.BaseHead",
img_path: Path,
preds_for_image: dict[str, Tensor],
transformed: dict[str, Any],
i: int,
) -> DatasetIterator:
yield {
"file": str(img_path),
"task_name": head.task_name,
"annotation": {
"class": head.classes.inverse[
int(transformed["pred_classes"][i].argmax())
],
},
}


def _annotate_text(
head: "lxt.nodes.BaseHead",
img_path: Path,
preds_for_image: dict[str, Tensor],
transformed: dict[str, Any],
i: int,
) -> DatasetIterator:
yield {
"file": str(img_path),
"task_name": head.task_name,
"annotation": {"metadata": {"text": transformed["pred_text"][i][0]}},
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --type=py -C5 'pred_classes|pred_text' luxonis_train/
rg -n --type=py -C3 'def decoder|self\.decoder' luxonis_train/

Repository: luxonis/luxonis-train

Length of output: 22477


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== annotation.py relevant sections =="
sed -n '130,290p' luxonis_train/utils/annotation.py

echo
echo "== ocr_ctc_head.py relevant sections =="
sed -n '1,180p' luxonis_train/nodes/heads/ocr_ctc_head.py

echo
echo "== search _build_preds_for_image definition and call sites =="
rg -n --type=py -C8 '_build_preds_for_image|_ANNOTATORS|_annotate_classification|_annotate_text' luxonis_train/utils/annotation.py

Repository: luxonis/luxonis-train

Length of output: 12356


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== OCRDecoder definition =="
rg -n --type=py -C20 'class OCRDecoder|def _.*decode|ctc|text' luxonis_train -g '*.py' | sed -n '1,220p'

echo
echo "== behavioral probe for shapes/index semantics =="
python3 - <<'PY'
import numpy as np

i = 2
pred_classes = np.arange(5 * i + 3).reshape(-1)  # shape [13]; i=2 selects one logit
print("classification shape", pred_classes.shape)
print("current classification annotation idx", int(pred_classes[i].argmax()), "from logit", pred_classes[i])
print("intended classification annotation idx", int(pred_classes.argmax()), "from logits")

decoded_texts = ["abc", "def", "ghi", "jkl"]  # decoded output already has batch length 4
print("text shape", np.array(decoded_texts).shape)
print("current text annotation", decoded_texts[i][0] if i < len(decoded_texts) else "<out-of-bounds>")
print("intended text annotation", decoded_texts[0].split()[0] if False else None)
PY

Repository: luxonis/luxonis-train

Length of output: 19527


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate OCRDecoder =="
rg -n 'OCRDecoder|class .*Decoder|decode\(self' luxonis_train -g '*.py'

echo
echo "== inspect candidate files =="
files=$(rg -l 'OCRDecoder|decode\(self' luxonis_train -g '*.py')
for f in $files; do
  echo "--- $f ---"
  wc -l "$f"
  rg -n -C25 'class .*Decoder|def decode|decode\(self|class OCRDecoder|CTC|text' "$f"
done

Repository: luxonis/luxonis-train

Length of output: 4881


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== annotation.py full outline/top =="
sed -n '1,130p' luxonis_train/utils/annotation.py

echo
echo "== luxonis_train/utils/ocr.py =="
cat -n luxonis_train/utils/ocr.py

Repository: luxonis/luxonis-train

Length of output: 9525


Use the per-image quantities in the annotation helpers.

_build_preds_for_image() already selects head_output[task][i], so transformed["pred_classes"] is the classification logits for one image and transformed["pred_text"] is a list[tuple[str, float]], not a per-row tensor. Indexing again with i produces the wrong classification class and the wrong decoded text for every image except the first, or can crash when the decoded OCR result contains fewer decoded steps than the batch index.

🐛 Suggested direction
-        "class": head.classes.inverse[
-            int(transformed["pred_classes"][i].argmax())
-        ],
+        "class": head.classes.inverse[
+            int(transformed["pred_classes"].argmax())
+        ],
-        "annotation": {"metadata": {"text": transformed["pred_text"][i][0]}},
+        "annotation": {"metadata": {"text": transformed["pred_text"][0]}},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _annotate_classification(
head: "lxt.nodes.BaseHead",
img_path: Path,
preds_for_image: dict[str, Tensor],
transformed: dict[str, Any],
i: int,
) -> DatasetIterator:
yield {
"file": str(img_path),
"task_name": head.task_name,
"annotation": {
"class": head.classes.inverse[
int(transformed["pred_classes"][i].argmax())
],
},
}
def _annotate_text(
head: "lxt.nodes.BaseHead",
img_path: Path,
preds_for_image: dict[str, Tensor],
transformed: dict[str, Any],
i: int,
) -> DatasetIterator:
yield {
"file": str(img_path),
"task_name": head.task_name,
"annotation": {"metadata": {"text": transformed["pred_text"][i][0]}},
}
def _annotate_classification(
head: "lxt.nodes.BaseHead",
img_path: Path,
preds_for_image: dict[str, Tensor],
transformed: dict[str, Any],
i: int,
) -> DatasetIterator:
yield {
"file": str(img_path),
"task_name": head.task_name,
"annotation": {
"class": head.classes.inverse[
int(transformed["pred_classes"].argmax())
],
},
}
def _annotate_text(
head: "lxt.nodes.BaseHead",
img_path: Path,
preds_for_image: dict[str, Tensor],
transformed: dict[str, Any],
i: int,
) -> DatasetIterator:
yield {
"file": str(img_path),
"task_name": head.task_name,
"annotation": {"metadata": {"text": transformed["pred_text"][0]}},
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/utils/annotation.py` around lines 259 - 288, Update
_annotate_classification and _annotate_text to use the already-selected
per-image values in transformed without indexing them by i again. Compute the
class from transformed["pred_classes"].argmax(), and read the decoded text from
transformed["pred_text"] as the list[tuple[str, float]] result, preserving the
existing annotation structure.

Comment thread luxonis_train/utils/boundingbox.py
Comment thread tests/integration/test_export_unique_identifiers.py
Comment thread tests/integration/test_predefined_models.py Outdated
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 31.38528% with 951 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.93%. Comparing base (8d9d3ae) to head (c174e05).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
luxonis_train/core/core.py 16.31% 236 Missing ⚠️
luxonis_train/lightning/luxonis_lightning.py 19.30% 163 Missing ⚠️
luxonis_train/core/utils/export_utils.py 13.88% 62 Missing ⚠️
luxonis_train/nodes/base_node.py 41.30% 54 Missing ⚠️
luxonis_train/utils/annotation.py 21.31% 48 Missing ⚠️
...uxonis_train/callbacks/fail_on_no_train_batches.py 14.00% 43 Missing ⚠️
luxonis_train/utils/boundingbox.py 41.09% 43 Missing ⚠️
...nis_train/attached_modules/base_attached_module.py 12.50% 35 Missing ⚠️
luxonis_train/lightning/utils.py 12.50% 35 Missing ⚠️
...uxonis_train/attached_modules/visualizers/utils.py 9.09% 30 Missing ⚠️
... and 16 more

❗ There is a different number of reports uploaded between BASE (8d9d3ae) and HEAD (c174e05). Click for more details.

HEAD has 4 uploads less than BASE
Flag BASE (8d9d3ae) HEAD (c174e05)
4 0
Additional details and impacted files
@@             Coverage Diff             @@
##             main     #425       +/-   ##
===========================================
- Coverage   93.70%   61.93%   -31.78%     
===========================================
  Files         269      268        -1     
  Lines       13178    13490      +312     
===========================================
- Hits        12349     8355     -3994     
- Misses        829     5135     +4306     
Flag Coverage Δ
unit 61.93% <31.38%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
luxonis_train/core/core.py (2)

707-713: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Serialize input_model as a string before writing YAML.

_build_modelconverter_config stores the exported Path directly in the config and passes it to yaml.safe_dump; PyYAML cannot represent Path, so this raises RepresenterError and prevents the .yaml export artifact from being written.

🐛 Proposed fix
-            "input_model": onnx_save_path,
+            "input_model": str(onnx_save_path),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/core/core.py` around lines 707 - 713, Update
_build_modelconverter_config so the input_model value is converted to a string
before the returned configuration is passed to YAML serialization, while
preserving the existing path value and all other configuration fields.

1708-1721: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Load the requested checkpoint before reparameterizing the quantization model.

GeneralReparametrizableBlock.reparametrize() fuses branches into a new fused_branch, so load_checkpoint() after reparameterization will skip the original parameter keys and potentially leave quantization initialized from older weights. Apply requested weights first, then reparametrize the fused checkpoint.

🐛 Proposed fix
-        model.reparametrize().eval()
-
         if weights is not None:
             model.load_checkpoint(weights)
+        model.reparametrize()
+        model.eval()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@luxonis_train/core/core.py` around lines 1708 - 1721, Update
_build_quant_model to load the requested checkpoint on the selected model before
calling reparametrize(). Keep eval() after reparameterization, ensuring
checkpoint keys for the original branches are restored before they are fused and
the quantization model uses the requested weights.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@luxonis_train/core/core.py`:
- Around line 634-642: Update the _upload_export_artifact call in the YAML
export flow to pass yaml_path.name as the artifact name instead of
str(yaml_path), ensuring the remote key contains only the filename and not the
local filesystem path.

---

Outside diff comments:
In `@luxonis_train/core/core.py`:
- Around line 707-713: Update _build_modelconverter_config so the input_model
value is converted to a string before the returned configuration is passed to
YAML serialization, while preserving the existing path value and all other
configuration fields.
- Around line 1708-1721: Update _build_quant_model to load the requested
checkpoint on the selected model before calling reparametrize(). Keep eval()
after reparameterization, ensuring checkpoint keys for the original branches are
restored before they are fused and the quantization model uses the requested
weights.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bdb54088-2d34-426f-b185-e1472f64bfe6

📥 Commits

Reviewing files that changed from the base of the PR and between 90529f6 and 6336907.

📒 Files selected for processing (10)
  • luxonis_train/attached_modules/base_attached_module.py
  • luxonis_train/attached_modules/visualizers/utils.py
  • luxonis_train/config/config.py
  • luxonis_train/core/core.py
  • luxonis_train/lightning/luxonis_lightning.py
  • luxonis_train/nodes/base_node.py
  • luxonis_train/upgrade.py
  • luxonis_train/utils/annotation.py
  • luxonis_train/utils/boundingbox.py
  • tests/integration/test_predefined_models.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • luxonis_train/attached_modules/base_attached_module.py
  • luxonis_train/attached_modules/visualizers/utils.py
  • tests/integration/test_predefined_models.py
  • luxonis_train/upgrade.py
  • luxonis_train/nodes/base_node.py
  • luxonis_train/lightning/luxonis_lightning.py
  • luxonis_train/utils/boundingbox.py
  • luxonis_train/config/config.py

Comment thread luxonis_train/core/core.py
@kozlov721
kozlov721 marked this pull request as draft July 29, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLI Changes affecting the CLI documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant