Skip to content

Mocap Slice D (#873): MocapRecorder + CLI 'qtmesh mocap --face' + MCP capture_face_from_video - #882

Closed
fernandotonon wants to merge 1 commit into
feat/mocap-slice-c-facecap-872from
feat/mocap-slice-d-recorder-cli-873
Closed

Mocap Slice D (#873): MocapRecorder + CLI 'qtmesh mocap --face' + MCP capture_face_from_video#882
fernandotonon wants to merge 1 commit into
feat/mocap-slice-c-facecap-872from
feat/mocap-slice-d-recorder-cli-873

Conversation

@fernandotonon

Copy link
Copy Markdown
Owner

Part of epic #869. Closes #873. Stacked on #881 (Slice C) — merge order: #880#881 → this.

What this adds

The first user-visible milestone: one command turns a face video into a playing, exportable morph animation.

  • MocapRecorderFaceSample stream → weight keyframes on a named morph clip through the Anim: Slice B — Mesh / vertex animation (Alembic + Ogre VAT_POSE clips) #519 pipeline (dope sheet / timeline / glTF export all work downstream for free). Epsilon run-length suppression (constant channels → 2 keys), jump pre-anchoring, first/last always keyed, face-lost gaps > 0.5 s held at both edges. Head pose neutral-calibrates on the first confident frame and lands on the Head bone (<clip>_Head skeletal clip, canonicalIndexForBone role) or as node TRS for static meshes.
  • MorphAnimationManager::writeWeightKeyOn — small refactor exposing the entity-explicit keyframe writer (setMorphWeightKeyframe now delegates to it); the shared-track pose-reference subtlety stays in one place.
  • RecordMocapClipCommand — a whole take is ONE undo step (snapshot pattern; pre-existing clips restored keyframe-for-keyframe, covered by test).
  • CLI qtmesh mocap (separate MocapCLI.cpp, the SceneLightsCLI precedent): mapping table printed (unmatched channels reported, never dropped), --frames-dir headless path, --json report, clear errors for every failure mode, "rebuild with -DENABLE_MOCAP" on non-mocap builds.
  • MCP capture_face_from_video (heavy): live-scene entity, single undoable clip, optional export, full report JSON.
  • Drive-by fix: CLIPipeline::writeCliError output was silently lost at _exit() (unflushed static QTextStream) — affected every existing subcommand's error text.

Verification (run locally on macOS with the Slice A models)

  • Static OBJ + 6-frame sequence (portrait → rotated portrait): framesProcessed 6, framesNoFace 0, headTarget "node", 5 head keys, valid glb exported.
  • Minimal glTF with jawOpen/mouthSmileLeft targets: 2 channels matched, 9 weight keys, exported glb contains the FaceCap morph-weights animation ("path":"weights" — the Anim: Slice B — Mesh / vertex animation (Alembic + Ogre VAT_POSE clips) #519 round-trip).
  • 43 headless unit tests pass locally; the new MocapRecorder_test.cpp suite (keyframe times/values, epsilon suppression, gap holds, unmatched reporting, single-undo round-trip, head-target resolution) needs Ogre → runs in the Linux CI lane.
  • Known upstream gap (noted, not introduced here): Assimp's glTF exporter doesn't write extras.targetNames, so a REimported mesh shows Shape_N target names — a --map sidecar re-binds them.

Real-webcam-video + ARKit-avatar verification is the Slice F/G QA pass (needs a hosted model + a Ready Player Me head; the pipeline pieces are each proven above).

🤖 Generated with Claude Code

…ace_from_video

Slice D of epic #869 — the first user-visible milestone: video file ->
animated mesh, end-to-end.

- MocapRecorder (src/Mocap/): FaceSample stream -> morph weight keyframes on
  a named clip via the #519 pipeline (new MorphAnimationManager::
  writeWeightKeyOn public static — the entity-explicit core of
  setMorphWeightKeyframe, which now delegates to it), with epsilon run-length
  suppression, first/last anchoring, jump pre-anchors and >0.5s face-lost
  gaps held at both edges. Head pose is calibrated on the take's first
  confident frame and keyed as rotation deltas on the Head bone
  (canonicalIndexForBone role 5) in '<clip>_Head', or as node-TRS deltas via
  NodeAnimationManager for static meshes.
- RecordMocapClipCommand (src/commands/): one undo step per take — first redo
  snapshots the pre-existing weight/head clips keyframe-for-keyframe, undo
  restores them exactly (verified by test).
- CLI 'qtmesh mocap' (src/Mocap/MocapCLI.cpp, the SceneLightsCLI pattern):
  import mesh -> mapping table (matched/unmatched printed, never silently
  dropped) -> FileFrameSource or --frames-dir image sequence ->
  FaceCapPredictor -> One-Euro -> recordFace -> optional re-export; --json
  emits the FaceRecordReport. Non-MOCAP builds print the standard rebuild
  hint. Registered in the dispatcher, subcommand list and usage text.
- MCP capture_face_from_video: heavy tool, live-scene entity (selected or
  entity_name), single undoable clip, optional output_path export, report
  JSON; clean error on non-MOCAP builds.
- Fixes a latent CLI bug: writeCliError text was silently lost at _exit()
  (unflushed static QTextStream) — every subcommand's stderr errors printed
  nothing; now flushed.
- Sentry ai.assist.mocap_face breadcrumbs; gamification
  noteOperation("mocap_face", {frames, keyframes}) on CLI + MCP.

Verified end-to-end on macOS with the Slice A models: a 6-frame image
sequence (portrait -> rotated portrait) onto (a) a static OBJ — 5 head keys
on the node path, glb exported — and (b) a minimal glTF with jawOpen/
mouthSmileLeft targets — 2 channels matched, 9 weight keys, exported glb
carries the 'FaceCap' morph-weights animation (path:"weights"). Known
upstream gap noted: the Assimp glTF exporter doesn't emit extras.targetNames,
so reimported targets alias to Shape_N.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f83cd70-be78-4d2c-9366-a3b9229cec9f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mocap-slice-d-recorder-cli-873

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c5bbf9d12

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const QString clip = options.clipName + QStringLiteral("_Head");
if (nam) {
if (options.replaceExisting)
nam->deleteClip(clip); // no-op when absent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve replaced node head clips for undo

When recording head pose for a static mesh and a scene-level node animation named <clipName>_Head already exists (for example, a user-authored FaceCap_Head clip), this deletes that clip before creating the mocap clip. Unlike the mesh and skeletal clips, RecordMocapClipCommand never snapshots scene-level node clips and its undo path only deletes the newly recorded clip, so undoing the single mocap step permanently loses the user's pre-existing node animation. Please snapshot/restore the node clip or reject the name collision before deletion.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in #909: recordFace no longer deletes a pre-existing scene-level node clip. It rejects the name collision (new report.headError, surfaced in CLI text + JSON) so undo can never lose a user clip it didn't snapshot; a fresh clip name proceeds normally.

@fernandotonon

Copy link
Copy Markdown
Owner Author

Consolidated into #909 (single epic PR targeting master). Review findings from this PR are addressed there — see the latest commit.

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.

1 participant