Skip to content

Sleep Synthesizer#54

Open
max-rosenblattl wants to merge 3 commits intomainfrom
KarlDeck/sleep-synthesizer
Open

Sleep Synthesizer#54
max-rosenblattl wants to merge 3 commits intomainfrom
KarlDeck/sleep-synthesizer

Conversation

@max-rosenblattl
Copy link
Copy Markdown
Collaborator

Sleep Synthesizer

♻️ Current situation & Problem

The cross-channel extractor framework lacked support for sleep-specific annotations and was not designed for pluggable synthesizers.

⚙️ Release Notes

  • Add "In Bed but not Asleep" sleep annotation
  • Refactor CrossChannelExtractor into a driver with pluggable synthesizers

📚 Documentation

Split from #49 — the stationary and cardio synthesizers will follow in a separate PR.

✅ Testing

tbd

Code of Conduct & Contributing Guidelines

By creating and submitting this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

This PR introduces a cross-channel annotation framework enabling multi-signal analysis. Added CrossChannelExtractor class with SleepSynthesizer implementation to detect "in bed but not sleeping" periods. Extended templates with cross-channel sleep annotations. Updated explorer UI with target cycling and window visualization.

Changes

Cohort / File(s) Summary
Extractor Framework
extractors/__init__.py, extractors/cross_channel.py
Added CrossChannelExtractor class supporting synthesizer-based multi-channel annotations; expanded VALID_CAPTION_TYPES to include "cross_channel".
Synthesizer Framework
synthesizers/__init__.py, synthesizers/sleep.py
Introduced abstract CrossChannelSynthesizer interface and contiguous_windows() utility; implemented SleepSynthesizer to detect contiguous in-bed-awake periods via channel masking and template formatting.
Template Assets
templates/templates.json
Added cross_channel.sleep template group with 5 parameterized strings for in-bed-awake interval descriptions.
Integration Points
captionizer.py, visualizer.py
Added CrossChannelExtractor with SleepSynthesizer to Annotator in __main__ entrypoints.
Explorer UI Enhancement
explorer.py
Added hit-target cycling via keyboard shortcuts ([ / ]); replaced detector-only navigation with target-based jumping supporting InBedAwake synthetic target; introduced cross-channel window rendering (axvspan) for in-bed-not-sleeping intervals.

Sequence Diagram

sequenceDiagram
    participant Recording
    participant CrossChannelExtractor
    participant SleepSynthesizer
    participant ChannelConfig
    participant Annotation

    Recording->>CrossChannelExtractor: extract(row)
    CrossChannelExtractor->>SleepSynthesizer: for each synthesizer
    SleepSynthesizer->>Recording: locate sleep:inbed and sleep:asleep channels
    SleepSynthesizer->>SleepSynthesizer: create boolean mask (inbed=1 AND asleep≠1)
    SleepSynthesizer->>SleepSynthesizer: detect contiguous windows from mask
    SleepSynthesizer->>ChannelConfig: load templates from config
    loop for each window
        SleepSynthesizer->>SleepSynthesizer: select template by seed rotation
        SleepSynthesizer->>Annotation: create with window, caption_type, text
    end
    SleepSynthesizer-->>CrossChannelExtractor: return list[Annotation]
    CrossChannelExtractor-->>Recording: aggregated annotations
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • Add reviewer and model components #28: Modifies captionizer.py __main__ entrypoint by altering the Annotator constructor and Captionizer.run() invocation; same integration point affected by this PR's extractor addition.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Sleep Synthesizer' accurately reflects the main change—adding a sleep-specific synthesizer with 'In Bed but not Asleep' annotations and refactoring CrossChannelExtractor to support pluggable synthesizers.
Description check ✅ Passed The description clearly explains the problem (lack of sleep annotations and pluggable synthesizers), the solution (adding SleepSynthesizer and refactoring), and provides context (split from #49, testing status).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 KarlDeck/sleep-synthesizer

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
.gitignore (1)

6-6: Consider whether to scope the pattern to root-level only.

The pattern data/ will ignore any directory named data anywhere in the repository tree. If the intent is to ignore only the root-level data/ directory, use /data/ instead. If ignoring all data/ directories throughout the repository is intentional, the current pattern is correct.

📝 Proposed alternative if root-level scoping is preferred
-data/
+/data/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 6, The .gitignore currently uses the pattern "data/"
which ignores any directory named data anywhere in the repo; decide whether you
want root-only scoping and, if so, change the pattern to "/data/" instead of
"data/". Update the file by replacing the "data/" entry with "/data/" to limit
ignoring to the repository root, or leave "data/" unchanged if you intentionally
want to ignore every data/ directory recursively.
synthesizers/sleep.py (1)

35-36: Cache the parsed templates instead of rereading JSON per row.

SleepSynthesizer.synthesize() is on the per-row annotation path, so reopening and reparsing templates.json on every call is avoidable overhead. Loading this once in __init__ or memoizing by templates_path would keep dataset runs and explorer scans cheaper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@synthesizers/sleep.py` around lines 35 - 36, The synthesizer currently
re-reads and re-parses templates.json inside SleepSynthesizer.synthesize() which
is called per-row; move the JSON load into the SleepSynthesizer.__init__ (or
implement a small memoization keyed by config.templates_path) so that templates
are parsed once and stored on the instance (e.g., self.templates) instead of
reassigning local variable templates in synthesize(); update synthesize() to
reference self.templates and keep the existing time_unit logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@visualizer.py`:
- Around line 125-129: The demo wires CrossChannelExtractor (with
SleepSynthesizer) into Annotator but plot_row() currently only renders
'statistical', 'structural', and 'semantic' caption types so the new
'cross_channel' captions are dropped; either update plot_row() to handle and
render 'cross_channel' captions (add 'cross_channel' to the caption types it
iterates/labels and format the label text accordingly) or remove
CrossChannelExtractor from the Annotator initialization to keep the demo
consistent (modify the Annotator call where
CrossChannelExtractor(MHC_CHANNEL_CONFIG, synthesizers=[SleepSynthesizer()]) is
added).

---

Nitpick comments:
In @.gitignore:
- Line 6: The .gitignore currently uses the pattern "data/" which ignores any
directory named data anywhere in the repo; decide whether you want root-only
scoping and, if so, change the pattern to "/data/" instead of "data/". Update
the file by replacing the "data/" entry with "/data/" to limit ignoring to the
repository root, or leave "data/" unchanged if you intentionally want to ignore
every data/ directory recursively.

In `@synthesizers/sleep.py`:
- Around line 35-36: The synthesizer currently re-reads and re-parses
templates.json inside SleepSynthesizer.synthesize() which is called per-row;
move the JSON load into the SleepSynthesizer.__init__ (or implement a small
memoization keyed by config.templates_path) so that templates are parsed once
and stored on the instance (e.g., self.templates) instead of reassigning local
variable templates in synthesize(); update synthesize() to reference
self.templates and keep the existing time_unit logic unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db229781-fe5f-42c8-abb1-bba471e8ade3

📥 Commits

Reviewing files that changed from the base of the PR and between b268ec4 and e457736.

📒 Files selected for processing (9)
  • .gitignore
  • captionizer.py
  • explorer.py
  • extractors/__init__.py
  • extractors/cross_channel.py
  • synthesizers/__init__.py
  • synthesizers/sleep.py
  • templates/templates.json
  • visualizer.py

Comment thread visualizer.py
Comment on lines +125 to +129
annotator = Annotator([
StatisticalExtractor(MHC_CHANNEL_CONFIG),
StructuralExtractor(MHC_CHANNEL_CONFIG),
CrossChannelExtractor(MHC_CHANNEL_CONFIG, synthesizers=[SleepSynthesizer()]),
])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This wiring doesn't surface the new sleep annotations yet.

CrossChannelExtractor now runs here, but plot_row() still only renders statistical, structural, and semantic caption types into the subplot labels. The new cross_channel captions are generated and then dropped, so this example path never shows the feature it just enabled. Either render cross_channel captions as well or leave the extractor out of this demo entrypoint for now.

Possible follow-up in plot_row()
-        for ann_type in ("statistical", "structural", "semantic"):
+        for ann_type in ("statistical", "structural", "semantic", "cross_channel"):
             if ann_type in caps:
                 parts.append(textwrap.fill(" ".join(caps[ann_type]), width=80))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@visualizer.py` around lines 125 - 129, The demo wires CrossChannelExtractor
(with SleepSynthesizer) into Annotator but plot_row() currently only renders
'statistical', 'structural', and 'semantic' caption types so the new
'cross_channel' captions are dropped; either update plot_row() to handle and
render 'cross_channel' captions (add 'cross_channel' to the caption types it
iterates/labels and format the label text accordingly) or remove
CrossChannelExtractor from the Annotator initialization to keep the demo
consistent (modify the Annotator call where
CrossChannelExtractor(MHC_CHANNEL_CONFIG, synthesizers=[SleepSynthesizer()]) is
added).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
explorer.py (1)

211-217: Consider using unpacking for list construction.

Per static analysis, unpacking is more idiomatic than concatenation here.

♻️ Suggested change
     def _available_hit_target_names(self) -> list[str]:
         names = {
             detector.__class__.__name__
             for detectors in self.channel_config.detectors.values()
             for detector in detectors
         }
-        return sorted(names) + [IN_BED_AWAKE_TARGET]
+        return [*sorted(names), IN_BED_AWAKE_TARGET]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@explorer.py` around lines 211 - 217, The method _available_hit_target_names
builds a sorted list then concatenates a single extra item; replace the list
concatenation with sequence unpacking to be more idiomatic: construct the set
from self.channel_config.detectors, sort it, and return a new list using
unpacking to append IN_BED_AWAKE_TARGET (e.g., return [*sorted(names),
IN_BED_AWAKE_TARGET]) so references are to _available_hit_target_names,
self.channel_config.detectors, names, and IN_BED_AWAKE_TARGET.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@explorer.py`:
- Around line 757-760: The help text still references a non-existent control
"target< / target>"; update the help string (the block containing "target< /
target> changes the jump target", "n / p jump to next or previous hit for that
target", etc.) to remove the bogus control and instead instruct users to change
the jump target by clicking the detector name buttons (e.g., "InBedAwake") or by
pressing "[" and "]" keys, keeping the related "n / p" and "hit buttons"
guidance intact; locate and edit the help text literal in explorer.py to replace
the incorrect line with the corrected wording.

---

Nitpick comments:
In `@explorer.py`:
- Around line 211-217: The method _available_hit_target_names builds a sorted
list then concatenates a single extra item; replace the list concatenation with
sequence unpacking to be more idiomatic: construct the set from
self.channel_config.detectors, sort it, and return a new list using unpacking to
append IN_BED_AWAKE_TARGET (e.g., return [*sorted(names), IN_BED_AWAKE_TARGET])
so references are to _available_hit_target_names, self.channel_config.detectors,
names, and IN_BED_AWAKE_TARGET.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd817f1e-9cd0-4cc9-b554-9c45264c6924

📥 Commits

Reviewing files that changed from the base of the PR and between e457736 and 1741d55.

📒 Files selected for processing (2)
  • explorer.py
  • templates/templates.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • templates/templates.json

Comment thread explorer.py
Comment on lines +757 to +760
"target< / target> changes the jump target",
"n / p jump to next or previous hit for that target",
"hit< / hit> buttons do the same",
"[ / ] also changes the jump target",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Help text references non-existent controls.

Line 757 mentions target< / target> but these controls don't exist in the UI. The actual ways to change the jump target are:

  • Click the detector name buttons (e.g., "InBedAwake")
  • Press [ or ] keys

Consider removing or correcting this line to avoid user confusion.

📝 Suggested fix
                     "up/down changes row",
                     "left/right changes signal",
-                    "target< / target> changes the jump target",
+                    "click detector buttons to change the jump target",
                     "n / p jump to next or previous hit for that target",
                     "hit< / hit> buttons do the same",
                     "[ / ] also changes the jump target",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@explorer.py` around lines 757 - 760, The help text still references a
non-existent control "target< / target>"; update the help string (the block
containing "target< / target> changes the jump target", "n / p jump to next or
previous hit for that target", etc.) to remove the bogus control and instead
instruct users to change the jump target by clicking the detector name buttons
(e.g., "InBedAwake") or by pressing "[" and "]" keys, keeping the related "n /
p" and "hit buttons" guidance intact; locate and edit the help text literal in
explorer.py to replace the incorrect line with the corrected wording.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants