Bind Frame right X to normal-priority hold-to-talk

This commit is contained in:
baketnk committed 2026-09-24 13:40:27 -04:00
1 parent b5499eccdf
commit 6b1d20858a
10 files changed
+85 -23

No files matched your search

+3 -1
View File
@@ -138,9 +138,11 @@ if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
endif()
endif()
if(Python3_FOUND)
add_test(NAME frameyap.action_bindings COMMAND "${Python3_EXECUTABLE}" -m unittest discover -s tests -p test_action_bindings.py)
add_test(NAME frameyap.python_worker COMMAND "${Python3_EXECUTABLE}" -m unittest discover -s tests -p test_worker.py)
add_test(NAME frameyap.installer COMMAND "${Python3_EXECUTABLE}" -m unittest discover -s tests -p test_installer.py)
set_tests_properties(frameyap.python_worker frameyap.installer PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" TIMEOUT 60)
set_tests_properties(frameyap.action_bindings frameyap.python_worker frameyap.installer
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" TIMEOUT 60)
if(TARGET frameyap_worker)
add_executable(frameyap_worker_test tests/worker_test.cpp)
target_link_libraries(frameyap_worker_test PRIVATE frameyap_worker)
+5 -4
View File
@@ -2,7 +2,7 @@
Standalone, on-device voice typing POC for Steam Frame. **MIT licensed.**
Implemented: native OpenVR overlay, remappable grip controls, bounded SDL3 capture,
Implemented: native OpenVR overlay, remappable controller actions, bounded SDL3 capture,
persistent local Parakeet Redux worker, preview/explicit insertion through Gamescope,
and an idempotent user-local installer. No Steam store AppID, sudo, desktop ASR
server, cloud fallback or unrelated application dependency.
@@ -19,9 +19,10 @@ See [third-party notes](docs/third-party.md). No GitHub release is published yet
## Controls
- **Right grip:** short tap, then hold the second squeeze to record; release to
transcribe. Remappable through SteamVR bindings; separate hold-to-talk action too.
- **Left grip:** double-tap to explicitly send Enter. Never inferred from speech.
- **Right X (default Frame binding):** hold to record; release to
transcribe. This PTT action is remappable through SteamVR bindings; delivery
in the dashboard still requires an on-device test.
- **Left grip (when active):** double-tap to explicitly send Enter. Never inferred from speech.
- **Overlay:** Record/Stop, Cancel, paginated preview, Insert, Enter and Quit.
Review and settings share one Inconsolata/neon-framed surface. World-space by
default; settings offer left wrist, right wrist and head mounting, plus recenter.
+1 -1
View File
@@ -19,7 +19,7 @@
"/actions/frameyap": "FrameYap controls",
"/actions/frameyap/in/left_grip": "Double tap left grip: Enter (enabled only)",
"/actions/frameyap/in/right_grip": "Tap right grip briefly, then squeeze and hold to record; release to finish",
"/actions/frameyap/in/ptt": "Hold to record",
"/actions/frameyap/in/ptt": "Hold right X to record (default; remappable)",
"/actions/frameyap/in/cancel": "Cancel recording or discard",
"/actions/frameyap/in/insert": "Insert reviewed text",
"/actions/frameyap/in/enter": "Explicit Enter"
+7 -2
View File
@@ -1,7 +1,7 @@
{
"controller_type": "frame_controller",
"description": "Right grip: tap then hold to dictate. Left grip: double tap Enter. Remap other actions in SteamVR.",
"name": "FrameYap Steam Frame grips",
"description": "Hold right X to dictate; release to review. Grip gestures remain remappable in SteamVR.",
"name": "FrameYap Steam Frame controls",
"bindings": {
"/actions/frameyap": {
"sources": [
@@ -14,6 +14,11 @@
"path": "/user/hand/right/input/grip",
"mode": "button",
"inputs": { "click": { "output": "/actions/frameyap/in/right_grip" } }
},
{
"path": "/user/hand/right/input/x",
"mode": "button",
"inputs": { "click": { "output": "/actions/frameyap/in/ptt" } }
}
]
}
+5 -3
View File
@@ -92,9 +92,11 @@ texture replacements; it does not establish that ordinary state-changing UI
updates are flicker-free.
`assets/actions.json` names six actions: left/right grip, PTT, cancel, insert,
Enter. `bindings_frame_controller.json` uses the observed Frame profile's grip
click paths; both bound actions were reported tracked/active in the device check.
This does not prove physical gesture delivery. `bindings_knuckles.json` is an
Enter. `bindings_frame_controller.json` maps right X click to hold-to-talk PTT;
the grip bindings remain for optional remapping/diagnosis. Both grip actions
were **inactive** during the dashboard controls check despite a loaded binding
file; this does not prove whether right X will deliver until an on-device test.
`bindings_knuckles.json` is an
additional **Index/knuckles example only**. Collisions with scene actions require
separate on-device validation. Left grip double tap
(releases <=250 ms, second press within 350 ms) requests explicit Enter only
+12 -8
View File
@@ -7,10 +7,13 @@ initialize OpenVR, open a microphone, run ASR, download files or inject input.
- Opt-in OpenVR RGBA overlay with world-space default and selectable wrist/head
mounts, status, recording timer, paginated UTF-8 preview and explicit controls.
- Remappable SteamVR actions. Default Steam Frame grip bindings use the observed
`frame_controller` profile. Right: short tap, then hold the second squeeze to
record; release to transcribe. Left: two short taps request explicit Enter.
First squeeze <=250 ms; second squeeze begins <=350 ms after first release.
- Remappable SteamVR actions. The default Steam Frame binding maps right X
(hold to record, release to transcribe) to the existing PTT action using the
observed `frame_controller` profile. Grip bindings remain, but both grip
actions were inactive in the observed dashboard check; do not rely on them.
If left grip becomes active, two short taps request explicit Enter.
For grip gestures, first squeeze <=250 ms; second squeeze begins <=350 ms
after first release.
Activity/tracking loss cancels a held recording and requires neutral rearm.
- SDL3 default recording device, mono float32 conversion at 16 kHz, 200 ms minimum,
20 second maximum. Microphone is closed outside actual capture. Other apps may
@@ -90,10 +93,11 @@ That is a producer workflow, not an end-user compiler requirement.
The first acquires/releases an IME without text/actions. The second displays a
five-second inert panel. The third displays a 30-second diagnostic panel and
reports pointer actions, SteamVR action activity, tracking and read-only legacy
grip state, **without microphone or input injection**. Mount clicks in check
modes do not save a preference. A SteamVR error code or inactive grip means the
gesture cannot be accepted; raw grip reads do not authorize a fallback binding.
reports pointer actions, SteamVR grip and PTT action activity, tracking and
read-only legacy grip state, **without microphone or input injection**. Mount
clicks in check modes do not save a preference. A SteamVR error code or inactive
action means the gesture cannot be accepted; raw grip reads do not authorize a
fallback binding.
Action handles alone are not proof that gestures were delivered. Checks must be explicitly launched
while the user expects the panel. Normal CLI/help/version remain inert.
+3 -2
View File
@@ -24,8 +24,9 @@ void help() {
"Mount: world (first-launch default), left-wrist, right-wrist, head.\n"
"Settings save the mount; --mount overrides it for this launch. --head is an alias.\n"
"Font defaults to bundled Inconsolata in the assets directory.\n"
"Right grip: double-tap, hold second squeeze to speak, release to review.\n"
"Left grip: double-tap for explicit Enter. Bindings are remappable.\n"
"Right X: hold to speak, release to review (default Frame binding).\n"
"Grip gestures are remappable but may be unavailable in the dashboard.\n"
"Left grip: double-tap for explicit Enter when active.\n"
"Review first: Insert approves current focus; never automatic Enter.\n"
"No device access unless an explicit runtime/check/registration mode is used.\n";
#ifndef FRAMEYAP_NATIVE
+9
View File
@@ -341,6 +341,15 @@ std::string Overlay::controls_status() {
" origin=" + (origin_ok ? "Y" : "N") +
" raw=" + (raw_ok ? ((raw.ulButtonPressed & vr::ButtonMaskFromId(vr::k_EButton_Grip)) ? "down" : "up") : "n/a");
}
// The default Frame binding maps right X to the named hold-to-talk action.
// Report the action's own activity separately from the stricter pose gate;
// remapped bindings may intentionally have a different origin.
vr::InputDigitalActionData_t ptt{};
const auto ptt_error = impl_->input->GetDigitalActionData(impl_->actions[2], &ptt, sizeof(ptt), vr::k_ulInvalidInputValueHandle);
const auto [accepted, down] = impl_->digital(2);
result += "\nPTT (default right X): err=" + std::to_string(int(ptt_error)) +
" active=" + (ptt.bActive ? "Y" : "N") + " down=" + (ptt.bState ? "Y" : "N") +
" accepted=" + (accepted ? "Y" : "N") + " gated-down=" + (down ? "Y" : "N");
return result;
}
std::string Overlay::pointer_status() const {
+2 -2
View File
@@ -24,7 +24,7 @@ void signal_stop(int) { interrupted = 1; }
std::string state_label(State state) {
switch (state) {
case State::Warming: return "Warming - on-device Redux CPU";
case State::Ready: return "Ready - double-tap + hold right grip";
case State::Ready: return "Ready - hold X or click Record";
case State::Recording: return "RECORDING";
case State::Transcribing: return "Transcribing - on device";
case State::Review: return "Review - Insert approves CURRENT focus";
@@ -66,7 +66,7 @@ int run(const Options& options) {
if (session.state() == State::Error) session.cancel();
if (!session.record()) return;
audio.start();
detail = "Release grip to finish. Cancel discards. Maximum 20 seconds.";
detail = "Release to finish. Cancel discards. Maximum 20 seconds.";
};
try { warm(); }
catch (const std::exception& e) { session.fail(); detail = e.what(); }
+38
View File
@@ -0,0 +1,38 @@
"""Offline contract for the default Frame controller shortcut (no OpenVR)."""
import json
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
class ActionBindingTests(unittest.TestCase):
def test_right_x_is_hold_to_talk(self):
manifest = json.loads((ROOT / "assets/actions.json").read_text())
binding = json.loads((ROOT / "assets/bindings_frame_controller.json").read_text())
self.assertIn(
{"controller_type": "frame_controller", "binding_url": "bindings_frame_controller.json"},
manifest["default_bindings"],
)
self.assertIn(
{"name": "/actions/frameyap/in/ptt", "type": "boolean"},
manifest["actions"],
)
sources = binding["bindings"]["/actions/frameyap"]["sources"]
right_x = [source for source in sources if source["path"] == "/user/hand/right/input/x"]
self.assertEqual(right_x, [{
"path": "/user/hand/right/input/x",
"mode": "button",
"inputs": {"click": {"output": "/actions/frameyap/in/ptt"}},
}])
# Do not map a second PTT source or replace the dashboard's pointer trigger.
self.assertEqual(sum(
source["inputs"].get("click", {}).get("output") == "/actions/frameyap/in/ptt"
for source in sources
), 1)
self.assertFalse(any(source["path"].endswith("/input/trigger") for source in sources))
if __name__ == "__main__":
unittest.main()