Spaces:
Running
Running
RemiFabre commited on
Commit Β·
35d5e6c
1
Parent(s): c0e8ee8
Remove denoise feature from UI and backend
Browse filesThe motor-noise denoising feature didn't work well and added significant
complexity. Removed the /api/moves/{move_id}/denoise endpoint, the
_perform_denoise and _apply_noise_profile methods, the denoise toggle
from the experimental panel, and the denoise button from the move list.
Existing .denoised.wav files are still recognized and used for playback
(backward compatibility).
- marionette/main.py +2 -180
- marionette/static/index.html +2 -12
- marionette/static/main.js +3 -53
- tests/test_api.py +5 -6
marionette/main.py
CHANGED
|
@@ -67,7 +67,7 @@ HF_DATASETS_API_URL = "https://huggingface.co/api/datasets"
|
|
| 67 |
DATASET_DATA_SUBDIR = "data"
|
| 68 |
NOISE_SUFFIX = ".noise.wav"
|
| 69 |
DENOISED_SUFFIX = ".denoised.wav"
|
| 70 |
-
DEFAULT_FEATURES = {"
|
| 71 |
|
| 72 |
logger = logging.getLogger(__name__)
|
| 73 |
|
|
@@ -183,7 +183,6 @@ class UpdateMotionModelPayload(BaseModel):
|
|
| 183 |
|
| 184 |
|
| 185 |
class UpdateExperimentsPayload(BaseModel):
|
| 186 |
-
denoise: bool | None = Field(default=None, description="Enable or disable denoise feature")
|
| 187 |
motion_models: bool | None = Field(default=None, description="Enable or disable motion model feature")
|
| 188 |
duration_seconds: float | None = Field(default=None, gt=0.5, le=300.0)
|
| 189 |
|
|
@@ -215,7 +214,6 @@ class Marionette(ReachyMiniApp):
|
|
| 215 |
self._recordings: dict[str, RecordingMetadata] = {}
|
| 216 |
self._pending_recording: RecordingRequest | None = None
|
| 217 |
self._pending_playback: str | None = None
|
| 218 |
-
self._pending_denoise: str | None = None
|
| 219 |
self._uploaded_audio: dict[str, Path] = {} # upload_id -> temp file path
|
| 220 |
self._playback_cancel_event = threading.Event()
|
| 221 |
self._recording_cancel_event = threading.Event()
|
|
@@ -331,7 +329,6 @@ class Marionette(ReachyMiniApp):
|
|
| 331 |
self._mode not in {"idle", "queued"}
|
| 332 |
or self._pending_recording
|
| 333 |
or self._pending_playback
|
| 334 |
-
or self._pending_denoise
|
| 335 |
):
|
| 336 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 337 |
self._preferred_duration = float(request.duration)
|
|
@@ -359,7 +356,6 @@ class Marionette(ReachyMiniApp):
|
|
| 359 |
self._mode != "idle"
|
| 360 |
or self._pending_recording
|
| 361 |
or self._pending_playback
|
| 362 |
-
or self._pending_denoise
|
| 363 |
):
|
| 364 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 365 |
self._pending_playback = payload.move_id
|
|
@@ -390,27 +386,6 @@ class Marionette(ReachyMiniApp):
|
|
| 390 |
self._refresh_recordings()
|
| 391 |
return {"status": "deleted", "move_id": move_id}
|
| 392 |
|
| 393 |
-
@self.settings_app.post("/api/moves/{move_id}/denoise")
|
| 394 |
-
def denoise_move(move_id: str) -> dict[str, Any]:
|
| 395 |
-
if not self._features.get("denoise"):
|
| 396 |
-
raise HTTPException(status_code=400, detail="Denoise feature is disabled.")
|
| 397 |
-
if move_id not in self._recordings:
|
| 398 |
-
raise HTTPException(status_code=404, detail=f"Move {move_id} not found.")
|
| 399 |
-
if not self._audio_available:
|
| 400 |
-
raise HTTPException(status_code=400, detail="Audio backend unavailable for denoise.")
|
| 401 |
-
with self._state_lock:
|
| 402 |
-
if (
|
| 403 |
-
self._mode not in {"idle", "queued"}
|
| 404 |
-
or self._pending_recording
|
| 405 |
-
or self._pending_playback
|
| 406 |
-
or self._pending_denoise
|
| 407 |
-
):
|
| 408 |
-
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 409 |
-
self._pending_denoise = move_id
|
| 410 |
-
self._mode = "queued"
|
| 411 |
-
self._message = f"Denoise queued for {move_id}"
|
| 412 |
-
return {"status": "queued", "move_id": move_id}
|
| 413 |
-
|
| 414 |
@self.settings_app.get("/api/datasets")
|
| 415 |
def list_datasets() -> dict[str, Any]:
|
| 416 |
return self._datasets_payload()
|
|
@@ -422,7 +397,6 @@ class Marionette(ReachyMiniApp):
|
|
| 422 |
self._mode not in {"idle", "queued"}
|
| 423 |
or self._pending_recording
|
| 424 |
or self._pending_playback
|
| 425 |
-
or self._pending_denoise
|
| 426 |
):
|
| 427 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 428 |
entry = self._create_dataset(payload.name, payload.label)
|
|
@@ -437,7 +411,6 @@ class Marionette(ReachyMiniApp):
|
|
| 437 |
self._mode not in {"idle", "queued"}
|
| 438 |
or self._pending_recording
|
| 439 |
or self._pending_playback
|
| 440 |
-
or self._pending_denoise
|
| 441 |
):
|
| 442 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 443 |
self._select_dataset(payload.dataset_id)
|
|
@@ -451,7 +424,6 @@ class Marionette(ReachyMiniApp):
|
|
| 451 |
self._mode not in {"idle", "queued"}
|
| 452 |
or self._pending_recording
|
| 453 |
or self._pending_playback
|
| 454 |
-
or self._pending_denoise
|
| 455 |
):
|
| 456 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 457 |
result = self._sync_dataset(payload.hf_username, payload.move_ids, payload.dataset_slug)
|
|
@@ -464,7 +436,6 @@ class Marionette(ReachyMiniApp):
|
|
| 464 |
self._mode not in {"idle", "queued"}
|
| 465 |
or self._pending_recording
|
| 466 |
or self._pending_playback
|
| 467 |
-
or self._pending_denoise
|
| 468 |
):
|
| 469 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 470 |
self._set_dataset_root(payload.path)
|
|
@@ -598,10 +569,6 @@ class Marionette(ReachyMiniApp):
|
|
| 598 |
job_type = "play"
|
| 599 |
payload = self._pending_playback
|
| 600 |
self._pending_playback = None
|
| 601 |
-
elif self._pending_denoise is not None:
|
| 602 |
-
job_type = "denoise"
|
| 603 |
-
payload = self._pending_denoise
|
| 604 |
-
self._pending_denoise = None
|
| 605 |
|
| 606 |
if job_type == "record" and isinstance(payload, RecordingRequest):
|
| 607 |
self._perform_recording(reachy_mini, stop_event, payload)
|
|
@@ -610,9 +577,6 @@ class Marionette(ReachyMiniApp):
|
|
| 610 |
self._perform_playback(reachy_mini, payload)
|
| 611 |
self._set_idle_state()
|
| 612 |
self._align_head_and_release(reachy_mini)
|
| 613 |
-
elif job_type == "denoise" and isinstance(payload, str):
|
| 614 |
-
self._perform_denoise(reachy_mini, payload)
|
| 615 |
-
self._set_idle_state()
|
| 616 |
else:
|
| 617 |
stop_event.wait(0.05)
|
| 618 |
finally:
|
|
@@ -716,9 +680,8 @@ class Marionette(ReachyMiniApp):
|
|
| 716 |
self._set_state(mode="error", message=f"Move {move_id} missing.", active_move=None)
|
| 717 |
return
|
| 718 |
|
| 719 |
-
prefer_denoised = bool(self._features.get("denoise") and meta.has_denoised_audio)
|
| 720 |
try:
|
| 721 |
-
move = self._load_move(meta.json_path
|
| 722 |
except Exception as exc: # pragma: no cover - filesystem failure
|
| 723 |
self._set_state(
|
| 724 |
mode="error",
|
|
@@ -765,145 +728,6 @@ class Marionette(ReachyMiniApp):
|
|
| 765 |
active_move=None,
|
| 766 |
)
|
| 767 |
|
| 768 |
-
def _perform_denoise(self, reachy_mini: ReachyMini, move_id: str) -> None:
|
| 769 |
-
if not self._audio_available:
|
| 770 |
-
self._set_state(
|
| 771 |
-
mode="error",
|
| 772 |
-
message="Audio backend unavailable for motor noise capture.",
|
| 773 |
-
active_move=None,
|
| 774 |
-
)
|
| 775 |
-
return
|
| 776 |
-
|
| 777 |
-
meta = self._recordings.get(move_id)
|
| 778 |
-
if not meta:
|
| 779 |
-
self._set_state(mode="error", message=f"Move {move_id} missing.", active_move=None)
|
| 780 |
-
return
|
| 781 |
-
if not meta.has_audio:
|
| 782 |
-
self._set_state(
|
| 783 |
-
mode="error",
|
| 784 |
-
message=f"No audio track to denoise for {meta.label}.",
|
| 785 |
-
active_move=None,
|
| 786 |
-
)
|
| 787 |
-
return
|
| 788 |
-
|
| 789 |
-
try:
|
| 790 |
-
move = self._load_move(meta.json_path, prefer_denoised=False)
|
| 791 |
-
except Exception as exc: # pragma: no cover - filesystem failure
|
| 792 |
-
self._set_state(
|
| 793 |
-
mode="error",
|
| 794 |
-
message=f"Failed to load {move_id}: {exc}",
|
| 795 |
-
active_move=None,
|
| 796 |
-
)
|
| 797 |
-
return
|
| 798 |
-
|
| 799 |
-
audio_frames: list[np.ndarray] = []
|
| 800 |
-
audio_samplerate = AUDIO_SAMPLE_RATE
|
| 801 |
-
audio_active = False
|
| 802 |
-
|
| 803 |
-
def _pull_audio_frames() -> None:
|
| 804 |
-
if not audio_active:
|
| 805 |
-
return
|
| 806 |
-
while True:
|
| 807 |
-
sample = reachy_mini.media.get_audio_sample()
|
| 808 |
-
if sample is None:
|
| 809 |
-
break
|
| 810 |
-
audio_frames.append(sample)
|
| 811 |
-
|
| 812 |
-
try:
|
| 813 |
-
reachy_mini.media.start_recording()
|
| 814 |
-
reported = reachy_mini.media.get_input_audio_samplerate()
|
| 815 |
-
if reported:
|
| 816 |
-
audio_samplerate = int(reported)
|
| 817 |
-
audio_active = True
|
| 818 |
-
except Exception as exc:
|
| 819 |
-
self._set_state(
|
| 820 |
-
mode="error",
|
| 821 |
-
message=f"Unable to start audio capture: {exc}",
|
| 822 |
-
active_move=None,
|
| 823 |
-
)
|
| 824 |
-
return
|
| 825 |
-
|
| 826 |
-
self._set_state(
|
| 827 |
-
mode="denoise",
|
| 828 |
-
message=f"Capturing motor noise for {meta.label}",
|
| 829 |
-
active_move=meta.move_id,
|
| 830 |
-
)
|
| 831 |
-
|
| 832 |
-
success = False
|
| 833 |
-
self._safe_enable_motors(reachy_mini)
|
| 834 |
-
try:
|
| 835 |
-
success = self._stream_playback(
|
| 836 |
-
reachy_mini,
|
| 837 |
-
move,
|
| 838 |
-
play_sound=False,
|
| 839 |
-
sample_hook=_pull_audio_frames,
|
| 840 |
-
)
|
| 841 |
-
finally:
|
| 842 |
-
self._goto_sleep_and_release(reachy_mini)
|
| 843 |
-
if audio_active:
|
| 844 |
-
try:
|
| 845 |
-
reachy_mini.media.stop_recording()
|
| 846 |
-
except Exception:
|
| 847 |
-
pass
|
| 848 |
-
_pull_audio_frames()
|
| 849 |
-
|
| 850 |
-
if not success:
|
| 851 |
-
self._set_state(
|
| 852 |
-
mode="error",
|
| 853 |
-
message=f"Motor noise capture canceled for {meta.label}",
|
| 854 |
-
active_move=None,
|
| 855 |
-
)
|
| 856 |
-
return
|
| 857 |
-
|
| 858 |
-
if not audio_frames:
|
| 859 |
-
self._set_state(
|
| 860 |
-
mode="error",
|
| 861 |
-
message="No audio captured during motor noise replay.",
|
| 862 |
-
active_move=None,
|
| 863 |
-
)
|
| 864 |
-
return
|
| 865 |
-
|
| 866 |
-
noise_path = meta.json_path.with_suffix(NOISE_SUFFIX)
|
| 867 |
-
denoised_path = meta.json_path.with_suffix(DENOISED_SUFFIX)
|
| 868 |
-
audio_data = np.concatenate(audio_frames, axis=0)
|
| 869 |
-
sf.write(str(noise_path), audio_data, audio_samplerate)
|
| 870 |
-
try:
|
| 871 |
-
self._apply_noise_profile(meta.json_path, noise_path, denoised_path)
|
| 872 |
-
except Exception as exc:
|
| 873 |
-
logger.warning("Noise subtraction failed for %s: %s", meta.move_id, exc)
|
| 874 |
-
self._refresh_recordings()
|
| 875 |
-
self._set_state(
|
| 876 |
-
mode="idle",
|
| 877 |
-
message=f"Denoised {meta.label}",
|
| 878 |
-
active_move=None,
|
| 879 |
-
)
|
| 880 |
-
|
| 881 |
-
def _apply_noise_profile(self, json_path: Path, noise_path: Path, target_path: Path) -> None:
|
| 882 |
-
original_audio = json_path.with_suffix(".wav")
|
| 883 |
-
if not original_audio.exists():
|
| 884 |
-
logger.info("No original audio present for %s; skipping denoise.", json_path)
|
| 885 |
-
return
|
| 886 |
-
original, orig_rate = sf.read(str(original_audio))
|
| 887 |
-
noise, noise_rate = sf.read(str(noise_path))
|
| 888 |
-
if orig_rate != noise_rate:
|
| 889 |
-
logger.warning(
|
| 890 |
-
"Noise sample rate mismatch for %s (original=%sHz, noise=%sHz).",
|
| 891 |
-
json_path.name,
|
| 892 |
-
orig_rate,
|
| 893 |
-
noise_rate,
|
| 894 |
-
)
|
| 895 |
-
return
|
| 896 |
-
overlap = min(len(original), len(noise))
|
| 897 |
-
if overlap == 0:
|
| 898 |
-
logger.warning("Noise sample empty for %s", json_path.name)
|
| 899 |
-
return
|
| 900 |
-
denoised = np.array(original, copy=True)
|
| 901 |
-
denoised[:overlap] = denoised[:overlap] - noise[:overlap]
|
| 902 |
-
denoised = np.clip(denoised, -1.0, 1.0)
|
| 903 |
-
if len(original) > overlap:
|
| 904 |
-
denoised[overlap:] = original[overlap:]
|
| 905 |
-
sf.write(str(target_path), denoised, orig_rate)
|
| 906 |
-
|
| 907 |
# ββββββββ Capture helpers βββββββββββββββββββββββββββββββββββββββββββ
|
| 908 |
def _capture_motion(
|
| 909 |
self,
|
|
@@ -1814,7 +1638,6 @@ class Marionette(ReachyMiniApp):
|
|
| 1814 |
"recording_stats": self._recording_stats,
|
| 1815 |
"pending_recording": self._pending_recording.label if self._pending_recording else None,
|
| 1816 |
"pending_playback": self._pending_playback,
|
| 1817 |
-
"pending_denoise": self._pending_denoise,
|
| 1818 |
"moves": moves,
|
| 1819 |
"config": {
|
| 1820 |
"default_duration": DEFAULT_DURATION,
|
|
@@ -1826,7 +1649,6 @@ class Marionette(ReachyMiniApp):
|
|
| 1826 |
"dataset_root_path": str(self._dataset_root),
|
| 1827 |
"features": dict(self._features),
|
| 1828 |
"feature_support": {
|
| 1829 |
-
"denoise": self._audio_available,
|
| 1830 |
"motion_models": True,
|
| 1831 |
},
|
| 1832 |
"motion_models": self._motion_model_registry.to_payload()
|
|
|
|
| 67 |
DATASET_DATA_SUBDIR = "data"
|
| 68 |
NOISE_SUFFIX = ".noise.wav"
|
| 69 |
DENOISED_SUFFIX = ".denoised.wav"
|
| 70 |
+
DEFAULT_FEATURES = {"motion_models": False}
|
| 71 |
|
| 72 |
logger = logging.getLogger(__name__)
|
| 73 |
|
|
|
|
| 183 |
|
| 184 |
|
| 185 |
class UpdateExperimentsPayload(BaseModel):
|
|
|
|
| 186 |
motion_models: bool | None = Field(default=None, description="Enable or disable motion model feature")
|
| 187 |
duration_seconds: float | None = Field(default=None, gt=0.5, le=300.0)
|
| 188 |
|
|
|
|
| 214 |
self._recordings: dict[str, RecordingMetadata] = {}
|
| 215 |
self._pending_recording: RecordingRequest | None = None
|
| 216 |
self._pending_playback: str | None = None
|
|
|
|
| 217 |
self._uploaded_audio: dict[str, Path] = {} # upload_id -> temp file path
|
| 218 |
self._playback_cancel_event = threading.Event()
|
| 219 |
self._recording_cancel_event = threading.Event()
|
|
|
|
| 329 |
self._mode not in {"idle", "queued"}
|
| 330 |
or self._pending_recording
|
| 331 |
or self._pending_playback
|
|
|
|
| 332 |
):
|
| 333 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 334 |
self._preferred_duration = float(request.duration)
|
|
|
|
| 356 |
self._mode != "idle"
|
| 357 |
or self._pending_recording
|
| 358 |
or self._pending_playback
|
|
|
|
| 359 |
):
|
| 360 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 361 |
self._pending_playback = payload.move_id
|
|
|
|
| 386 |
self._refresh_recordings()
|
| 387 |
return {"status": "deleted", "move_id": move_id}
|
| 388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
@self.settings_app.get("/api/datasets")
|
| 390 |
def list_datasets() -> dict[str, Any]:
|
| 391 |
return self._datasets_payload()
|
|
|
|
| 397 |
self._mode not in {"idle", "queued"}
|
| 398 |
or self._pending_recording
|
| 399 |
or self._pending_playback
|
|
|
|
| 400 |
):
|
| 401 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 402 |
entry = self._create_dataset(payload.name, payload.label)
|
|
|
|
| 411 |
self._mode not in {"idle", "queued"}
|
| 412 |
or self._pending_recording
|
| 413 |
or self._pending_playback
|
|
|
|
| 414 |
):
|
| 415 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 416 |
self._select_dataset(payload.dataset_id)
|
|
|
|
| 424 |
self._mode not in {"idle", "queued"}
|
| 425 |
or self._pending_recording
|
| 426 |
or self._pending_playback
|
|
|
|
| 427 |
):
|
| 428 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 429 |
result = self._sync_dataset(payload.hf_username, payload.move_ids, payload.dataset_slug)
|
|
|
|
| 436 |
self._mode not in {"idle", "queued"}
|
| 437 |
or self._pending_recording
|
| 438 |
or self._pending_playback
|
|
|
|
| 439 |
):
|
| 440 |
raise HTTPException(status_code=409, detail="Robot is busy.")
|
| 441 |
self._set_dataset_root(payload.path)
|
|
|
|
| 569 |
job_type = "play"
|
| 570 |
payload = self._pending_playback
|
| 571 |
self._pending_playback = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
|
| 573 |
if job_type == "record" and isinstance(payload, RecordingRequest):
|
| 574 |
self._perform_recording(reachy_mini, stop_event, payload)
|
|
|
|
| 577 |
self._perform_playback(reachy_mini, payload)
|
| 578 |
self._set_idle_state()
|
| 579 |
self._align_head_and_release(reachy_mini)
|
|
|
|
|
|
|
|
|
|
| 580 |
else:
|
| 581 |
stop_event.wait(0.05)
|
| 582 |
finally:
|
|
|
|
| 680 |
self._set_state(mode="error", message=f"Move {move_id} missing.", active_move=None)
|
| 681 |
return
|
| 682 |
|
|
|
|
| 683 |
try:
|
| 684 |
+
move = self._load_move(meta.json_path)
|
| 685 |
except Exception as exc: # pragma: no cover - filesystem failure
|
| 686 |
self._set_state(
|
| 687 |
mode="error",
|
|
|
|
| 728 |
active_move=None,
|
| 729 |
)
|
| 730 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
# ββββββββ Capture helpers βββββββββββββββββββββββββββββββββββββββββββ
|
| 732 |
def _capture_motion(
|
| 733 |
self,
|
|
|
|
| 1638 |
"recording_stats": self._recording_stats,
|
| 1639 |
"pending_recording": self._pending_recording.label if self._pending_recording else None,
|
| 1640 |
"pending_playback": self._pending_playback,
|
|
|
|
| 1641 |
"moves": moves,
|
| 1642 |
"config": {
|
| 1643 |
"default_duration": DEFAULT_DURATION,
|
|
|
|
| 1649 |
"dataset_root_path": str(self._dataset_root),
|
| 1650 |
"features": dict(self._features),
|
| 1651 |
"feature_support": {
|
|
|
|
| 1652 |
"motion_models": True,
|
| 1653 |
},
|
| 1654 |
"motion_models": self._motion_model_registry.to_payload()
|
marionette/static/index.html
CHANGED
|
@@ -211,18 +211,8 @@
|
|
| 211 |
<details id="experimental-details">
|
| 212 |
<summary>Experimental</summary>
|
| 213 |
<div class="experimental-content">
|
| 214 |
-
<p class=
|
| 215 |
-
<div class=
|
| 216 |
-
<label class="toggle">
|
| 217 |
-
<input id="feature-denoise" type="checkbox" />
|
| 218 |
-
<span>Enable motor-noise denoising for move audio</span>
|
| 219 |
-
</label>
|
| 220 |
-
<p class="hint small">
|
| 221 |
-
Adds a per-move βDenoiseβ button that records the motor hum separately and subtracts it from the original
|
| 222 |
-
track.
|
| 223 |
-
</p>
|
| 224 |
-
</div>
|
| 225 |
-
<div class="field-group">
|
| 226 |
<label class="toggle">
|
| 227 |
<input id="feature-motion-models" type="checkbox" />
|
| 228 |
<span>Enable motion compensation models</span>
|
|
|
|
| 211 |
<details id="experimental-details">
|
| 212 |
<summary>Experimental</summary>
|
| 213 |
<div class="experimental-content">
|
| 214 |
+
<p class=βhintβ>Toggle prototype features. These may change or disappear without notice.</p>
|
| 215 |
+
<div class=βfield-groupβ>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
<label class="toggle">
|
| 217 |
<input id="feature-motion-models" type="checkbox" />
|
| 218 |
<span>Enable motion compensation models</span>
|
marionette/static/main.js
CHANGED
|
@@ -59,7 +59,6 @@ const motionModelSelect = document.getElementById("motion-model-select");
|
|
| 59 |
const motionModelHint = document.getElementById("motion-model-hint");
|
| 60 |
const experimentalStatus = document.getElementById("experimental-status");
|
| 61 |
const featureToggles = {
|
| 62 |
-
denoise: document.getElementById("feature-denoise"),
|
| 63 |
motionModels: document.getElementById("feature-motion-models"),
|
| 64 |
};
|
| 65 |
|
|
@@ -258,10 +257,6 @@ function renderMoves(moves, mode, config) {
|
|
| 258 |
updateSyncButtonState();
|
| 259 |
return;
|
| 260 |
}
|
| 261 |
-
const features = (config && config.features) || {};
|
| 262 |
-
const support = (config && config.feature_support) || {};
|
| 263 |
-
const denoiseEnabled = !!features.denoise && support.denoise !== false;
|
| 264 |
-
|
| 265 |
for (const move of moves) {
|
| 266 |
const li = document.createElement("li");
|
| 267 |
const selection = document.createElement("div");
|
|
@@ -292,16 +287,9 @@ function renderMoves(moves, mode, config) {
|
|
| 292 |
const info = document.createElement("div");
|
| 293 |
info.className = "move-info";
|
| 294 |
const date = new Date(move.created_at * 1000).toLocaleString();
|
| 295 |
-
const badges = [];
|
| 296 |
-
if (move.has_denoised_audio) {
|
| 297 |
-
badges.push("denoised");
|
| 298 |
-
} else if (move.has_noise_profile) {
|
| 299 |
-
badges.push("noise profile");
|
| 300 |
-
}
|
| 301 |
-
const badgeText = badges.length ? ` β’ ${badges.join(", ")}` : "";
|
| 302 |
info.textContent = `${formatDuration(move.duration)} β’ ${
|
| 303 |
move.has_audio ? "π audio" : "π€ silent"
|
| 304 |
-
} β’ ${date}
|
| 305 |
meta.appendChild(info);
|
| 306 |
|
| 307 |
const actions = document.createElement("div");
|
|
@@ -331,15 +319,6 @@ function renderMoves(moves, mode, config) {
|
|
| 331 |
deleteBtn.addEventListener("click", () => deleteMove(move.id));
|
| 332 |
actions.appendChild(deleteBtn);
|
| 333 |
|
| 334 |
-
if (denoiseEnabled && move.has_audio) {
|
| 335 |
-
const denoiseBtn = document.createElement("button");
|
| 336 |
-
denoiseBtn.className = "play-btn";
|
| 337 |
-
denoiseBtn.textContent = move.has_denoised_audio ? "Rebuild noise" : "Denoise";
|
| 338 |
-
denoiseBtn.disabled = busy;
|
| 339 |
-
denoiseBtn.addEventListener("click", () => queueDenoise(move.id));
|
| 340 |
-
actions.appendChild(denoiseBtn);
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
li.appendChild(selection);
|
| 344 |
li.appendChild(actions);
|
| 345 |
movesList.appendChild(li);
|
|
@@ -425,22 +404,6 @@ async function queuePlayback(moveId) {
|
|
| 425 |
}
|
| 426 |
}
|
| 427 |
|
| 428 |
-
async function queueDenoise(moveId) {
|
| 429 |
-
if (!moveId) return;
|
| 430 |
-
try {
|
| 431 |
-
const resp = await fetch(`/api/moves/${encodeURIComponent(moveId)}/denoise`, {
|
| 432 |
-
method: "POST",
|
| 433 |
-
headers: { "Content-Type": "application/json" },
|
| 434 |
-
});
|
| 435 |
-
if (!resp.ok) throw new Error(await resp.text());
|
| 436 |
-
primaryStatus.textContent = `Denoise queued for ${moveId}.`;
|
| 437 |
-
await fetchState();
|
| 438 |
-
} catch (error) {
|
| 439 |
-
console.error("Denoise error:", error);
|
| 440 |
-
primaryStatus.textContent = `Unable to denoise ${moveId}.`;
|
| 441 |
-
}
|
| 442 |
-
}
|
| 443 |
-
|
| 444 |
recordForm.addEventListener("submit", queueRecording);
|
| 445 |
refreshBtn.addEventListener("click", fetchState);
|
| 446 |
if (datasetSelect) {
|
|
@@ -563,11 +526,6 @@ if (motionModelSelect) {
|
|
| 563 |
updateMotionModel(value);
|
| 564 |
});
|
| 565 |
}
|
| 566 |
-
if (featureToggles.denoise) {
|
| 567 |
-
featureToggles.denoise.addEventListener("change", (event) => {
|
| 568 |
-
updateExperimentalFeatures({ denoise: event.target.checked });
|
| 569 |
-
});
|
| 570 |
-
}
|
| 571 |
if (featureToggles.motionModels) {
|
| 572 |
featureToggles.motionModels.addEventListener("change", (event) => {
|
| 573 |
updateExperimentalFeatures({ motion_models: event.target.checked });
|
|
@@ -750,8 +708,8 @@ function updatePhaseFromState(state) {
|
|
| 750 |
phaseStartAt = null;
|
| 751 |
phaseEndAt = null;
|
| 752 |
}
|
| 753 |
-
// For "playing"
|
| 754 |
-
if (state.mode === "playing"
|
| 755 |
phaseMode = state.mode;
|
| 756 |
phaseStartAt = null;
|
| 757 |
phaseEndAt = null;
|
|
@@ -1109,14 +1067,6 @@ function updateExperimentalUI(config) {
|
|
| 1109 |
experimentalPanel.hidden = false;
|
| 1110 |
const features = config.features || {};
|
| 1111 |
const support = config.feature_support || {};
|
| 1112 |
-
if (featureToggles.denoise) {
|
| 1113 |
-
featureToggles.denoise.checked = !!features.denoise;
|
| 1114 |
-
const supported = support.denoise !== false;
|
| 1115 |
-
featureToggles.denoise.disabled = !supported;
|
| 1116 |
-
featureToggles.denoise.title = supported
|
| 1117 |
-
? "Capture and subtract motor noise."
|
| 1118 |
-
: "Audio backend unavailable, cannot capture motor noise.";
|
| 1119 |
-
}
|
| 1120 |
if (featureToggles.motionModels) {
|
| 1121 |
featureToggles.motionModels.checked = !!features.motion_models;
|
| 1122 |
}
|
|
|
|
| 59 |
const motionModelHint = document.getElementById("motion-model-hint");
|
| 60 |
const experimentalStatus = document.getElementById("experimental-status");
|
| 61 |
const featureToggles = {
|
|
|
|
| 62 |
motionModels: document.getElementById("feature-motion-models"),
|
| 63 |
};
|
| 64 |
|
|
|
|
| 257 |
updateSyncButtonState();
|
| 258 |
return;
|
| 259 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
for (const move of moves) {
|
| 261 |
const li = document.createElement("li");
|
| 262 |
const selection = document.createElement("div");
|
|
|
|
| 287 |
const info = document.createElement("div");
|
| 288 |
info.className = "move-info";
|
| 289 |
const date = new Date(move.created_at * 1000).toLocaleString();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
info.textContent = `${formatDuration(move.duration)} β’ ${
|
| 291 |
move.has_audio ? "π audio" : "π€ silent"
|
| 292 |
+
} β’ ${date}`;
|
| 293 |
meta.appendChild(info);
|
| 294 |
|
| 295 |
const actions = document.createElement("div");
|
|
|
|
| 319 |
deleteBtn.addEventListener("click", () => deleteMove(move.id));
|
| 320 |
actions.appendChild(deleteBtn);
|
| 321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
li.appendChild(selection);
|
| 323 |
li.appendChild(actions);
|
| 324 |
movesList.appendChild(li);
|
|
|
|
| 404 |
}
|
| 405 |
}
|
| 406 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
recordForm.addEventListener("submit", queueRecording);
|
| 408 |
refreshBtn.addEventListener("click", fetchState);
|
| 409 |
if (datasetSelect) {
|
|
|
|
| 526 |
updateMotionModel(value);
|
| 527 |
});
|
| 528 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
if (featureToggles.motionModels) {
|
| 530 |
featureToggles.motionModels.addEventListener("change", (event) => {
|
| 531 |
updateExperimentalFeatures({ motion_models: event.target.checked });
|
|
|
|
| 708 |
phaseStartAt = null;
|
| 709 |
phaseEndAt = null;
|
| 710 |
}
|
| 711 |
+
// For "playing", keep the phase cleared
|
| 712 |
+
if (state.mode === "playing") {
|
| 713 |
phaseMode = state.mode;
|
| 714 |
phaseStartAt = null;
|
| 715 |
phaseEndAt = null;
|
|
|
|
| 1067 |
experimentalPanel.hidden = false;
|
| 1068 |
const features = config.features || {};
|
| 1069 |
const support = config.feature_support || {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
if (featureToggles.motionModels) {
|
| 1071 |
featureToggles.motionModels.checked = !!features.motion_models;
|
| 1072 |
}
|
tests/test_api.py
CHANGED
|
@@ -72,7 +72,7 @@ class TestStateEndpoint:
|
|
| 72 |
"phase_start_at", "phase_end_at",
|
| 73 |
"countdown_ends_at",
|
| 74 |
"recording_started_at", "recording_duration", "recording_stats",
|
| 75 |
-
"pending_recording", "pending_playback",
|
| 76 |
"moves", "config", "datasets",
|
| 77 |
}
|
| 78 |
assert required_keys.issubset(data.keys())
|
|
@@ -105,7 +105,6 @@ class TestStateEndpoint:
|
|
| 105 |
data = client.get("/api/state").json()
|
| 106 |
assert data["pending_recording"] is None
|
| 107 |
assert data["pending_playback"] is None
|
| 108 |
-
assert data["pending_denoise"] is None
|
| 109 |
|
| 110 |
|
| 111 |
# ββββββββ Recording endpoint tests ββββββββββββββββββββββββββββββββββββ
|
|
@@ -422,10 +421,10 @@ class TestMovesRefresh:
|
|
| 422 |
|
| 423 |
|
| 424 |
class TestExperiments:
|
| 425 |
-
def
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
assert
|
| 429 |
|
| 430 |
def test_toggle_motion_models(self, client: TestClient):
|
| 431 |
resp = client.post("/api/experiments", json={"motion_models": True})
|
|
|
|
| 72 |
"phase_start_at", "phase_end_at",
|
| 73 |
"countdown_ends_at",
|
| 74 |
"recording_started_at", "recording_duration", "recording_stats",
|
| 75 |
+
"pending_recording", "pending_playback",
|
| 76 |
"moves", "config", "datasets",
|
| 77 |
}
|
| 78 |
assert required_keys.issubset(data.keys())
|
|
|
|
| 105 |
data = client.get("/api/state").json()
|
| 106 |
assert data["pending_recording"] is None
|
| 107 |
assert data["pending_playback"] is None
|
|
|
|
| 108 |
|
| 109 |
|
| 110 |
# ββββββββ Recording endpoint tests ββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 421 |
|
| 422 |
|
| 423 |
class TestExperiments:
|
| 424 |
+
def test_denoise_feature_removed(self, client: TestClient):
|
| 425 |
+
"""Denoise feature was removed; the features dict should not contain it."""
|
| 426 |
+
data = client.get("/api/state").json()
|
| 427 |
+
assert "denoise" not in data["config"]["features"]
|
| 428 |
|
| 429 |
def test_toggle_motion_models(self, client: TestClient):
|
| 430 |
resp = client.post("/api/experiments", json={"motion_models": True})
|