Skip to content
Open
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
5 changes: 4 additions & 1 deletion apps/cam2v/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ application named `cam2v-<model-config-name>`.
| `Q` / `E` | Strafe left / right |
| `I` / `K` | Pitch up / down |

Losing browser focus clears held keys.
Losing browser focus clears held keys. The HUD wraps each held key in brackets.
Model adapters may replace the displayed groups while keeping the same UI loop;
Waypoint shows its raw-action controls and `R` reset command this way.

## Usage

Concrete launch commands live with each model adapter:

- [Lingbot](../../integrations_v2/lingbot/apps/cam2v/README.md)
- [HY-WorldPlay](../../integrations_v2/hy_worldplay/apps/cam2v/README.md)
- [Waypoint](../../integrations_v2/waypoint/apps/cam2v/README.md)

The general command shape is:

Expand Down
4 changes: 4 additions & 0 deletions apps/cam2v/cam2v/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
CameraControlInput,
)
from .ui import (
Cam2VControlGroup,
Cam2VControlKey,
Cam2VSlangPyUILoop,
Cam2VUIState,
Cam2VUIStatus,
Expand All @@ -29,6 +31,8 @@
"Cam2VApplication",
"Cam2VApplicationDefaults",
"Cam2VConditioning",
"Cam2VControlGroup",
"Cam2VControlKey",
"Cam2VGenerateStep",
"Cam2VInputResolver",
"Cam2VModelLoop",
Expand Down
148 changes: 122 additions & 26 deletions apps/cam2v/cam2v/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,64 @@
)
from flashdreams.runtime_v2.user_input_events import UserInputEvents

_CAMERA_KEY_ORDER = ("w", "s", "q", "e", "a", "d", "j", "l", "i", "k")
"""Stable order used when active camera controls are displayed."""

_CAMERA_KEYS = frozenset(_CAMERA_KEY_ORDER)
"""Keyboard controls recognized by the shared camera pose integrator."""

RECENT_MODEL_FPS_WINDOW_SECONDS = 2.0
"""Trailing AR-step completion window displayed in the model status panel."""


@dataclass(frozen=True, slots=True)
class Cam2VControlKey:
"""One keyboard key displayed in a Cam2V control group."""

key: str
"""Canonical runtime key used to track held state."""

label: str
"""Short user-facing label rendered in the overlay."""


@dataclass(frozen=True, slots=True)
class Cam2VControlGroup:
"""One action and its associated keyboard controls."""

action: str
"""User-facing action description."""

keys: tuple[Cam2VControlKey, ...]
"""Keys that trigger the action."""


DEFAULT_CAM2V_CONTROL_GROUPS = (
Cam2VControlGroup(
action="Move forward / backward",
keys=(Cam2VControlKey("w", "W"), Cam2VControlKey("s", "S")),
),
Cam2VControlGroup(
action="Strafe left / right",
keys=(Cam2VControlKey("q", "Q"), Cam2VControlKey("e", "E")),
),
Cam2VControlGroup(
action="Yaw left / right",
keys=(
Cam2VControlKey("a", "A"),
Cam2VControlKey("d", "D"),
Cam2VControlKey("j", "J"),
Cam2VControlKey("l", "L"),
),
),
Cam2VControlGroup(
action="Pitch up / down",
keys=(Cam2VControlKey("i", "I"), Cam2VControlKey("k", "K")),
),
)
"""Default keyboard groups for the shared camera pose integrator."""

DEFAULT_CAM2V_UI_INSTRUCTIONS = (
"Held controls are shown in brackets.",
"Click the video before using keyboard controls.",
)
"""Default hints rendered below the Cam2V controls."""


@dataclass(frozen=True, slots=True)
class Cam2VUIStatus:
"""Latest model-generation status copied to the UI loop."""
Expand Down Expand Up @@ -71,12 +119,19 @@ class Cam2VUIState:
warmup_blocks: int
"""Leading blocks excluded from recent model throughput."""

control_groups: tuple[Cam2VControlGroup, ...] = DEFAULT_CAM2V_CONTROL_GROUPS
"""Action-oriented key groups displayed by the overlay."""

instructions: tuple[str, ...] = DEFAULT_CAM2V_UI_INSTRUCTIONS
"""Short usage hints displayed below the controls."""

show_status: bool = True
"""Whether model throughput lines are included above the controls."""

held_keys: set[str] = field(default_factory=set)
"""Camera-control keys currently held by the client."""
"""Keyboard control keys currently held by the client."""

_keyboard_state: KeyboardState = field(
default_factory=lambda: KeyboardState(supported_keys=_CAMERA_KEYS),
)
_keyboard_state: KeyboardState = field(init=False)
"""UI-thread-owned source-aware keyboard state."""

status: Cam2VUIStatus | None = None
Expand All @@ -91,8 +146,14 @@ class Cam2VUIState:
status_widgets: list[Any] = field(default_factory=list, init=False, repr=False)
"""Retained SlangPy text widgets for model status."""

control_widgets: list[Any] = field(default_factory=list, init=False, repr=False)
"""Retained SlangPy text widgets for action-oriented key groups."""

active_keys_widget: Any | None = field(default=None, init=False, repr=False)
"""Retained SlangPy text widget for active camera controls."""
"""Retained SlangPy text widget for active keyboard controls."""

def __post_init__(self) -> None:
self._keyboard_state = self._new_keyboard_state()

def update_status(self, status: Cam2VUIStatus) -> None:
"""Replace the displayed model-generation status."""
Expand All @@ -101,10 +162,16 @@ def update_status(self, status: Cam2VUIStatus) -> None:
def reset(self) -> None:
"""Clear transient controls and model status for a new generation."""
self.held_keys.clear()
self._keyboard_state = KeyboardState(supported_keys=_CAMERA_KEYS)
self._keyboard_state = self._new_keyboard_state()
self.status = None
self.frames_presented = 0

def _new_keyboard_state(self) -> KeyboardState:
keys = frozenset(
control.key for group in self.control_groups for control in group.keys
)
return KeyboardState(supported_keys=keys)


class Cam2VSlangPyUILoop(SlangPyUILoop[Cam2VUIState]):
"""Draw Cam2V controls and model throughput over generated video."""
Expand Down Expand Up @@ -142,27 +209,38 @@ def _ensure_widgets(
return
state.window = ui.Window(
ui.screen,
"Camera controls",
"Controls",
position=(16, 16),
size=(360, 280),
size=(400, 340 if state.show_status else 220),
)
state.status_widgets = [
ui.Text(state.window, line)
for line in _status_lines(state, sampled_at=sampled_at)
if state.show_status:
state.status_widgets = [
ui.Text(state.window, line)
for line in _status_lines(state, sampled_at=sampled_at)
]
state.control_widgets = [
ui.Text(state.window, _control_group_text(group, state.held_keys))
for group in state.control_groups
]
ui.Text(state.window, "Move: W/S Strafe: Q/E")
ui.Text(state.window, "Yaw: A/D or J/L Pitch: I/K")
state.active_keys_widget = ui.Text(state.window, _active_keys_text(state))
ui.Text(state.window, "Click the video before using keyboard controls.")
for instruction in state.instructions:
ui.Text(state.window, instruction)


def _refresh_widgets(state: Cam2VUIState, *, sampled_at: float) -> None:
for widget, line in zip(
state.status_widgets,
_status_lines(state, sampled_at=sampled_at),
if state.status_widgets:
for widget, line in zip(
state.status_widgets,
_status_lines(state, sampled_at=sampled_at),
strict=True,
):
widget.text = line
for widget, group in zip(
state.control_widgets,
state.control_groups,
strict=True,
):
widget.text = line
widget.text = _control_group_text(group, state.held_keys)
if state.active_keys_widget is not None:
state.active_keys_widget.text = _active_keys_text(state)

Expand Down Expand Up @@ -207,15 +285,31 @@ def _status_lines(


def _active_keys_text(state: Cam2VUIState) -> str:
active = [key.upper() for key in _CAMERA_KEY_ORDER if key in state.held_keys]
active = [
control.label
for group in state.control_groups
for control in group.keys
if control.key in state.held_keys
]
return f"Active keys: {', '.join(active) if active else 'none'}"


def _control_group_text(
group: Cam2VControlGroup,
held_keys: set[str],
) -> str:
labels = [
f"[{control.label}]" if control.key in held_keys else control.label
for control in group.keys
]
return f"{group.action}: {' / '.join(labels)}"


def _apply_ui_input(state: Cam2VUIState, events: UserInputEvents) -> None:
for event in events.get_events():
if isinstance(event, FocusUserInputEvent) and not event.focused:
state.held_keys.clear()
state._keyboard_state = KeyboardState(supported_keys=_CAMERA_KEYS)
state._keyboard_state = state._new_keyboard_state()
continue
if not isinstance(event, KeyboardUserInputEvent):
continue
Expand All @@ -229,6 +323,8 @@ def _apply_ui_input(state: Cam2VUIState, events: UserInputEvents) -> None:


__all__ = [
"Cam2VControlGroup",
"Cam2VControlKey",
"Cam2VSlangPyUILoop",
"Cam2VUIState",
"Cam2VUIStatus",
Expand Down
1 change: 1 addition & 0 deletions apps/cam2v/tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ def render(
assert any(line.startswith("Recent model rate (2 s):") for line in displayed)
assert state.active_keys_widget is not None
assert state.active_keys_widget.text == "Active keys: W"
assert state.control_widgets[0].text == "Move forward / backward: [W] / S"

ui_loop.step(
1,
Expand Down
30 changes: 18 additions & 12 deletions docs/source/models/waypoint.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Support summary
* - Surface
- FlashDreams support
* - Application slug
- waypoint-1-5-1b through flashdreams-run-v2
- cam2v-waypoint through flashdreams-run-v2
* - Input modalities
- RGB/RGBA seed image; keyboard and mouse buttons; relative mouse motion;
ternary scroll-wheel direction
Expand Down Expand Up @@ -104,10 +104,10 @@ From the FlashDreams repository root:

.. code-block:: bash

uv sync --package flashdreams-waypoint-v2 --inexact
uv sync --package flashdreams-waypoint --inexact

The V2 application package depends on the sibling flashdreams-waypoint model
package, so both are installed together.
The package contains the model implementation, configuration, tests, and Cam2V
application binding in the V2 integration layout.

Running the model
-----------------
Expand All @@ -117,24 +117,30 @@ and bundled control timeline:

.. code-block:: bash

uv run --no-sync flashdreams-run-v2 waypoint-1-5-1b \
uv run --no-sync flashdreams-run-v2 cam2v-waypoint \
--presentation-mode on_demand \
--output-path waypoint.mp4 --stats-path waypoint.metrics.json \
-- --example-data --actions 40 --seed 464 --profile

Run the same application interactively in a browser:

.. code-block:: bash

uv run --no-sync flashdreams-run-v2 waypoint-1-5-1b \
--mode webrtc --host 127.0.0.1 --port 8766 \
-- --seed-image seed.png --seed 464
uv run --no-sync flashdreams-run-v2 cam2v-waypoint \
--mode webrtc --presentation-mode continuous \
--host 127.0.0.1 --port 8766 \
-- --image-path seed.png --seed 464

Open http://127.0.0.1:8766/. Arguments before the separator configure the V2
runtime; arguments after it configure Waypoint. To inspect all model arguments:
Open http://127.0.0.1:8766/. The control HUD highlights held keys; press ``R``
to reset the rollout to the starting image. Continuous presentation keeps the
interactive HUD responsive; the application default remains on-demand so
finite replays contain each generated frame exactly once. Arguments before the
separator configure the V2 runtime; arguments after it configure Waypoint. To
inspect all model arguments:

.. code-block:: bash

uv run --no-sync flashdreams-run-v2 waypoint-1-5-1b -- --help
uv run --no-sync flashdreams-run-v2 cam2v-waypoint -- --help

Model and integration architecture
----------------------------------
Expand Down Expand Up @@ -170,7 +176,7 @@ reports both rather than relabeling the upstream model.
The package-level design review contains component, class, use-case, and
sequence diagrams:

.. button-link:: https://github.com/NVIDIA/flashdreams/blob/main/integrations/waypoint/README.md
.. button-link:: https://github.com/NVIDIA/flashdreams/blob/main/integrations_v2/waypoint/README.md
:color: secondary
:outline:

Expand Down
Loading
Loading