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
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ jobs:
run: ruff format --check musicalgestures/ || true

- name: Mypy type check
# Non-blocking: the public API is typed (params + returns + py.typed), but the internals
# still have a backlog of errors. Settings (follow_imports/exclude) come from pyproject.toml.
run: mypy musicalgestures/ || true
# Blocking since 2026-08-23. The internal backlog that kept this advisory is gone: it ran
# from 248 errors to zero over #350. Settings (follow_imports/exclude) come from
# pyproject.toml, and the version is pinned above, because the count this reports moves
# with the checker as well as with the code.
run: mypy musicalgestures/
12 changes: 9 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
behind it are gone. Closes #370.

### Changed
- Internal typing, issue #350: `mypy musicalgestures/` is at 17 errors in 9 files, from 90 when
this work started. Retiring `motion_mp()` accounts for nine of the reduction, since typing code
that cannot run would have been effort spent hiding a breakage.
- **The mypy step in CI blocks merges now.** `mypy musicalgestures/` reports no issues across all
69 source files, from 248 when issue #350 was opened, so the `|| true` that made it advisory is
gone. The checker version is pinned alongside it, because the count this reports moves with the
checker as well as with the code: on identical source, mypy 1.20.1 reported 169 errors where
2.3.1 reported 90. Closes #350.
- The last of that backlog: two frame rates typed as integers, which they are not; two GPU objects
created under a flag and used under the same flag, which a checker will not correlate; three
output names resolved in one branch and used in another; and two functions carrying no return
annotation at all.

## [1.13.0] — 2026-08-23

Expand Down
2 changes: 1 addition & 1 deletion musicalgestures/_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
class _StrEnumBase(str, Enum): # type: ignore[no-redef]
"""Backward-compatible StrEnum for Python 3.10."""
def __str__(self) -> str:
return self.value
return str(self.value)


class _MgEnum(_StrEnumBase):
Expand Down
7 changes: 6 additions & 1 deletion musicalgestures/_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ def dense(
if ii == 0:
out.stdin.write(rgb.astype(np.uint8))
else:
# frame 0 always sets prev_rgb below, and this arm only
# runs from frame 1 on, so it is set by the time we read it
assert prev_rgb is not None
out.stdin.write(prev_rgb.astype(np.uint8))
else:
out.stdin.write(rgb.astype(np.uint8))
Expand Down Expand Up @@ -287,6 +290,7 @@ def dense(
else:
out.stdin.close()
out.wait()
assert target_name is not None # resolve_filename ran earlier in this branch
destination_video = target_name

if self.has_audio:
Expand All @@ -300,7 +304,7 @@ def dense(

return self._parent().flow_dense_video

def get_acceleration(self, velocity: list, fps: int):
def get_acceleration(self, velocity: list, fps: float):

acceleration = np.zeros(len(velocity))
velocity = np.abs(velocity)
Expand Down Expand Up @@ -450,6 +454,7 @@ def sparse(

# calculate optical flow
if _use_gpu:
assert lk_gpu is not None # created under the same flag above
gpu_frame_gray.upload(frame_gray)
gpu_p1, gpu_st, _gpu_err = lk_gpu.calc(gpu_old_gray, gpu_frame_gray, gpu_p0, None)
# GPU returns 1xN; flatten to (N,2)/(N,) for uniform selection
Expand Down
2 changes: 1 addition & 1 deletion musicalgestures/_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def history_cv2(self, filename: str | None = None, history_length: int = 10, wei

ii = 0
history: list = []
weights_map = [1 for weight in range(history_length+1)]
weights_map: list[float] = [1 for weight in range(history_length+1)]

if type(weights) in [int, float]:
assert isinstance(weights, (int, float))
Expand Down
1 change: 1 addition & 0 deletions musicalgestures/_motionvideo.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ def mg_motion(
os.remove(source_audio)

# Save generated musicalgestures video as the video of the parent MgVideo
assert target_name_video is not None # resolved under `if save_video` above
self.motion_video = musicalgestures.MgVideo(filename=target_name_video, returned_by_process=True)
return self.motion_video

Expand Down
5 changes: 3 additions & 2 deletions musicalgestures/_pose.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ def save_single_file(of, width, height, model, data, data_format, target_name_da

if save_video:
# save result as pose_video for parent MgVideo
assert target_name_video is not None # resolved under `if save_video` above
self.pose_video = musicalgestures.MgVideo(target_name_video, color=self.color, returned_by_process=True)
self.pose_video.average_pose = average_image
self.pose_video.trajectories = trajectories_image
Expand Down Expand Up @@ -788,7 +789,7 @@ def _rerender_pose_from_cache(self: "musicalgestures.MgVideo", style='both', ove
trajectory_labels=False,
marker_history=0, target_name_video=None,
target_name_data=None, target_name_average=None,
target_name_trajectories=None, overwrite=True):
target_name_trajectories=None, overwrite=True) -> "musicalgestures.MgVideo":
"""Re-render the pose outputs from cached keypoints (no network inference)."""
c = self._pose_keypoints
data, names, connections = c['data'], c['names'], c['connections']
Expand Down Expand Up @@ -1147,7 +1148,7 @@ def _pose_mediapipe(
target_name_data=None,
target_name_average=None,
target_name_trajectories=None,
overwrite=True):
overwrite=True) -> "musicalgestures.MgVideo":
"""
Internal helper: run MediaPipe Pose on a video and render/save the output.
Called by :func:`pose` when ``model='mediapipe'`` (or when GPU is requested and the
Expand Down
1 change: 1 addition & 0 deletions musicalgestures/_pose_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ def predict_frame(self, frame: np.ndarray) -> PoseEstimatorResult:

rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
assert self._landmarker is not None, "the landmarker was not initialised"
detection_result = self._landmarker.detect(mp_image)

n = len(MEDIAPIPE_LANDMARK_NAMES)
Expand Down
1 change: 1 addition & 0 deletions musicalgestures/_posetools.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ def _read_result(lms, wlms):
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# stdout=PIPE guarantees a stream; the annotation is Optional for the general case
assert proc.stdout is not None, "ffmpeg was started without a readable output stream"
assert proc.stderr is not None, "ffmpeg was started without a readable error stream"
# Drain FFmpeg's stderr on a background thread as it is produced, rather
# than only reading it in the `finally` block below: the stdout-reading
# loop can run far longer than the OS pipe buffer takes to fill (a few
Expand Down
1 change: 1 addition & 0 deletions musicalgestures/_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def colab_display(video_to_display, video_width, video_height):
horizontal_keys = ('horizontal', 'mgh', 'vgh', 'mgy', 'vgy')
orientation = 'horizontal' if k in horizontal_keys else 'vertical'
label = orientation.capitalize()
kinds: tuple[str, ...]
if k in ('mgh', 'mgv', 'mgx', 'mgy'):
kinds = ('motiongram',)
elif k in ('vgh', 'vgv', 'vgx', 'vgy'):
Expand Down
5 changes: 4 additions & 1 deletion musicalgestures/_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ def __enter__(self) -> "MgVideoReader":
def __exit__(self, *_) -> None:
if self._process is not None:
try:
self._process.stdout.close()
if self._process.stdout is not None:
self._process.stdout.close()
self._process.wait(timeout=5)
except Exception:
self._process.kill()
Expand All @@ -177,6 +178,8 @@ def __iter__(self) -> Generator[tuple[np.ndarray, float], None, None]:
frame_bytes = self._height * self._width * channels
fps = self._fps

# opened with stdout=PIPE in __enter__, which the guard above confirms ran
assert self._process.stdout is not None
while True:
raw = self._process.stdout.read(frame_bytes)
if len(raw) < frame_bytes:
Expand Down
Loading