#2353·labelme

fix(download): 32-bit Signal truncates byte counts for model files >2 GiB (corrupt progress display)

Author: wkentaroCreated Jul 15, 2026Updated Aug 25, 2026
Labelsready-for-humantype:bug

This was generated by AI during a kaizen pass.

Summary

The model-download progress dialog (labelme/_widgets/download.py) marshals byte counts through a Signal(int, ...), which Qt binds to a native 32-bit signed int. For a single downloaded file larger than 2 GiB (2**31 - 1 bytes) the byte count silently wraps around, so the progress dialog shows garbage byte counts and a wrong/negative progress bar. The download itself still succeeds (the actual transfer is done by gdown inside osam, independent of the Qt signal); only the progress display is corrupted.

This is reachable in practice: large single-file models such as SAM ViT-H (~2.5 GB) are selectable, and bytes_total is reported per file by gdown, not summed, so a single file can exceed the limit.

Reproduction

download.py:22 declares:

python
progress = Signal(int, int, str, int, int)   # bytes_so_far / bytes_total are the last two

Confirmed against the shipped binding (PySide6 6.11.1):

signal: [('int', -1794967296), ('qint64', 2500000000)]
setRange-max -> RAISED OverflowError
setValue     -> RAISED OverflowError
  • Signal(int).emit(2_500_000_000) delivers -1794967296 (wrapped mod 2**32, reinterpreted signed).
  • QProgressDialog.setRange(0, 2_500_000_000) and setValue(2_500_000_000) both raise OverflowError (also 32-bit).

Those wrapped values then flow into _on_progress (download.py:100-120): dialog.setRange(0, bytes_total) / dialog.setValue(bytes_so_far) get a negative/garbage range, and _format_bytes(bytes_so_far) / _format_bytes(bytes_total) renders nonsense like 672 MB / -1712 MB.

Why this needs a human decision (not a blind kaizen PR)

The fix is necessarily two-part, and the second part is a display-behavior decision:

  1. Signal transport — widen the two byte-count fields to 64-bit, e.g. Signal(int, int, str, "qint64", "qint64"), so _format_bytes shows accurate totals. This part is cheaply unit-testable (emit large values through _DownloadThread.run() with a fake pull(), assert the received values are not truncated).

  2. QProgressDialog is itself 32-bit. Once the signal carries the true 2.5e9, setRange(0, bytes_total) / setValue(bytes_so_far) would then raise OverflowError (verified above) — i.e. fixing only the signal converts today's cosmetic bug into a real exception in the slot. So the sink must scale the values into 32-bit range before handing them to the dialog. That scaling scheme has more than one reasonable answer:

    • fixed resolution (e.g. setRange(0, 1000), setValue(round(1000 * so_far / total))),
    • unit scaling (bytes // 1024, KB granularity), or
    • percentage (0..100).

Additionally, the end-to-end behavior can only be truly verified by triggering a real multi-GB download, which is impractical in CI — so an unattended kaizen PR cannot /verify it.

Affected files

  • labelme/_widgets/download.pyprogress Signal declaration (line 22) and the _on_progress sink (lines 100-120).

Suggested resolution

Pick a scaling scheme for the 32-bit QProgressDialog (fixed resolution is the least surprising and keeps the byte-count label driven by the raw 64-bit values), widen the Signal to qint64, and add a unit test asserting the transported byte counts survive round-trip above 2 GiB.