mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 05:00:04 +02:00
Keep Redux loaded across capture failures and diagnose grip actions
This commit is contained in:
1 parent
18a66b021c
commit
9b4bf9f047
9 files changed
+126
-36
No files matched your search
+13
-7
@@ -5,8 +5,8 @@ initialize OpenVR, open a microphone, run ASR, download files or inject input.
|
|||||||
|
|
||||||
## Implemented
|
## Implemented
|
||||||
|
|
||||||
- Opt-in OpenVR RGBA overlay with head/left-hand placement, status, recording timer,
|
- Opt-in OpenVR RGBA overlay with world-space default and selectable wrist/head
|
||||||
paginated UTF-8 preview, Record/Cancel/Insert/Enter/Quit controls.
|
mounts, status, recording timer, paginated UTF-8 preview and explicit controls.
|
||||||
- Remappable SteamVR actions. Default Steam Frame grip bindings use the observed
|
- Remappable SteamVR actions. Default Steam Frame grip bindings use the observed
|
||||||
`frame_controller` profile. Right: short tap, then hold the second squeeze to
|
`frame_controller` profile. Right: short tap, then hold the second squeeze to
|
||||||
record; release to transcribe. Left: two short taps request explicit Enter.
|
record; release to transcribe. Left: two short taps request explicit Enter.
|
||||||
@@ -17,7 +17,10 @@ initialize OpenVR, open a microphone, run ASR, download files or inject input.
|
|||||||
still transmit your voice: this app does **not** mute VRChat or any other app.
|
still transmit your voice: this app does **not** mute VRChat or any other app.
|
||||||
- Persistent local Redux worker, correlated bounded pipes, private tmpfs clips,
|
- Persistent local Redux worker, correlated bounded pipes, private tmpfs clips,
|
||||||
cancellation/reaping and deadlines; exact pinned model SHA-256 verification.
|
cancellation/reaping and deadlines; exact pinned model SHA-256 verification.
|
||||||
Model imports are lazy and loading is offline. No cloud/desktop fallback.
|
Model imports are lazy and loading is offline. Normal repeats, request-local
|
||||||
|
transcription failures and microphone failures retain the loaded model;
|
||||||
|
microphone device streams close outside capture. A cancelled in-flight request
|
||||||
|
or broken worker protocol may require reloading. No cloud/desktop fallback.
|
||||||
- Gamescope IME v2 generated bindings, per-action short-lived lease, unavailable
|
- Gamescope IME v2 generated bindings, per-action short-lived lease, unavailable
|
||||||
handling, UTF-8/control validation and explicit separate Submit action for Enter.
|
handling, UTF-8/control validation and explicit separate Submit action for Enter.
|
||||||
- Idempotent user-local release-archive installer: SHA-256, safe extraction,
|
- Idempotent user-local release-archive installer: SHA-256, safe extraction,
|
||||||
@@ -81,14 +84,17 @@ That is a producer workflow, not an end-user compiler requirement.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
./build-native/frameyap --check-input --socket gamescope-0
|
./build-native/frameyap --check-input --socket gamescope-0
|
||||||
./build-native/frameyap --check-overlay --assets "$PWD/assets" --font /path/font.ttf --head
|
./build-native/frameyap --check-overlay --assets "$PWD/assets" --mount world
|
||||||
./build-native/frameyap --check-controls --assets "$PWD/assets" --font /path/font.ttf --head
|
./build-native/frameyap --check-controls --assets "$PWD/assets" --mount world
|
||||||
```
|
```
|
||||||
|
|
||||||
The first acquires/releases an IME without text/actions. The second displays a
|
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
|
five-second inert panel. The third displays a 30-second diagnostic panel and
|
||||||
reports gestures, **without microphone or input injection**. Active action handles
|
reports pointer actions, SteamVR action activity, tracking and read-only legacy
|
||||||
are not proof that gestures were delivered. Checks must be explicitly launched
|
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.
|
||||||
|
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.
|
while the user expects the panel. Normal CLI/help/version remain inert.
|
||||||
|
|
||||||
The ARM64 OpenVR loader's default `/data/work/openvrpaths.vrpath` failed on the
|
The ARM64 OpenVR loader's default `/data/work/openvrpaths.vrpath` failed on the
|
||||||
|
|||||||
+11
-5
@@ -8,13 +8,19 @@
|
|||||||
|
|
||||||
namespace frameyap {
|
namespace frameyap {
|
||||||
namespace { void check(bool result) { if (!result) throw std::runtime_error(std::string("Microphone: ") + SDL_GetError()); } }
|
namespace { void check(bool result) { if (!result) throw std::runtime_error(std::string("Microphone: ") + SDL_GetError()); } }
|
||||||
Audio::~Audio() { cancel(); }
|
Audio::~Audio() {
|
||||||
|
cancel();
|
||||||
|
if (initialized_) SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
}
|
||||||
void Audio::start() {
|
void Audio::start() {
|
||||||
if (stream_) throw std::runtime_error("Already recording");
|
if (stream_) throw std::runtime_error("Already recording");
|
||||||
check(SDL_InitSubSystem(SDL_INIT_AUDIO)); initialized_ = true;
|
// Keep SDL's audio backend initialized across utterances, but own the
|
||||||
|
// recording device only while actually capturing. Reinitializing PipeWire
|
||||||
|
// after every clip can fail even though the local model remains healthy.
|
||||||
|
if (!initialized_) { check(SDL_InitSubSystem(SDL_INIT_AUDIO)); initialized_ = true; }
|
||||||
SDL_AudioSpec spec{SDL_AUDIO_F32, 1, 16000};
|
SDL_AudioSpec spec{SDL_AUDIO_F32, 1, 16000};
|
||||||
stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, nullptr, nullptr);
|
stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, nullptr, nullptr);
|
||||||
if (!stream_) { cancel(); check(false); }
|
if (!stream_) throw std::runtime_error(std::string("Microphone: ") + SDL_GetError());
|
||||||
pcm_.clear(); pcm_.reserve(320000);
|
pcm_.clear(); pcm_.reserve(320000);
|
||||||
started_ = last_data_ = std::chrono::steady_clock::now();
|
started_ = last_data_ = std::chrono::steady_clock::now();
|
||||||
try { check(SDL_ResumeAudioStreamDevice(stream_)); }
|
try { check(SDL_ResumeAudioStreamDevice(stream_)); }
|
||||||
@@ -51,20 +57,20 @@ bool Audio::poll() {
|
|||||||
}
|
}
|
||||||
std::vector<float> Audio::finish() {
|
std::vector<float> Audio::finish() {
|
||||||
if (!stream_) return {};
|
if (!stream_) return {};
|
||||||
|
try {
|
||||||
check(SDL_PauseAudioStreamDevice(stream_));
|
check(SDL_PauseAudioStreamDevice(stream_));
|
||||||
check(SDL_FlushAudioStream(stream_));
|
check(SDL_FlushAudioStream(stream_));
|
||||||
drain();
|
drain();
|
||||||
auto result = std::move(pcm_);
|
auto result = std::move(pcm_);
|
||||||
cancel();
|
cancel();
|
||||||
return result;
|
return result;
|
||||||
|
} catch (...) { cancel(); throw; }
|
||||||
}
|
}
|
||||||
void Audio::cancel() {
|
void Audio::cancel() {
|
||||||
if (stream_) SDL_DestroyAudioStream(stream_);
|
if (stream_) SDL_DestroyAudioStream(stream_);
|
||||||
stream_ = nullptr;
|
stream_ = nullptr;
|
||||||
// Best effort clearing of the owned buffer; no persistent recording/logging.
|
// Best effort clearing of the owned buffer; no persistent recording/logging.
|
||||||
std::fill(pcm_.begin(), pcm_.end(), 0.0f); pcm_.clear();
|
std::fill(pcm_.begin(), pcm_.end(), 0.0f); pcm_.clear();
|
||||||
if (initialized_) SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
||||||
initialized_ = false;
|
|
||||||
}
|
}
|
||||||
int Audio::seconds() const {
|
int Audio::seconds() const {
|
||||||
return stream_ ? int(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - started_).count()) : 0;
|
return stream_ ? int(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - started_).count()) : 0;
|
||||||
|
|||||||
+19
-4
@@ -76,7 +76,7 @@ int main(int argc, char** argv) {
|
|||||||
if (options.assets.empty()) throw std::runtime_error("--assets required");
|
if (options.assets.empty()) throw std::runtime_error("--assets required");
|
||||||
if (mode == "--check-overlay" || mode == "--check-controls") {
|
if (mode == "--check-overlay" || mode == "--check-controls") {
|
||||||
frameyap::InstanceLock lock;
|
frameyap::InstanceLock lock;
|
||||||
frameyap::Overlay overlay(options.assets, options.font, options.mount);
|
frameyap::Overlay overlay(options.assets, options.font, options.mount, false);
|
||||||
overlay.draw({"FrameYap five-second visual check", "No audio captured. No text or Enter delivered.", "Controls inactive during this check.", false, false});
|
overlay.draw({"FrameYap five-second visual check", "No audio captured. No text or Enter delivered.", "Controls inactive during this check.", false, false});
|
||||||
for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
||||||
if (vr::VRSystem()->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) continue;
|
if (vr::VRSystem()->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) continue;
|
||||||
@@ -89,17 +89,32 @@ int main(int argc, char** argv) {
|
|||||||
const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||||
std::string status;
|
std::string status;
|
||||||
bool held = false;
|
bool held = false;
|
||||||
|
std::string last_action = "Press panel controls or tap then hold right grip. No mic or typing.";
|
||||||
|
auto action_name = [](frameyap::UiAction action) {
|
||||||
|
switch (action) {
|
||||||
|
case frameyap::UiAction::BeginRecord: return "BeginRecord";
|
||||||
|
case frameyap::UiAction::EndRecord: return "EndRecord";
|
||||||
|
case frameyap::UiAction::Record: return "Record";
|
||||||
|
case frameyap::UiAction::Cancel: return "Cancel";
|
||||||
|
case frameyap::UiAction::Insert: return "Insert";
|
||||||
|
case frameyap::UiAction::Enter: return "Enter";
|
||||||
|
case frameyap::UiAction::Quit: return "Quit";
|
||||||
|
case frameyap::UiAction::Toggle: return "Toggle";
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
};
|
||||||
while (std::chrono::steady_clock::now() < end) {
|
while (std::chrono::steady_clock::now() < end) {
|
||||||
overlay.draw({"Controls check ONLY - no mic or typing", "Tap then hold right grip; double-tap left grip.",
|
|
||||||
held ? "HOLD gesture recognized. Release to finish." : "Try grips or click buttons. Auto-closes after 30 seconds.", true, held});
|
|
||||||
for (auto action : overlay.poll()) {
|
for (auto action : overlay.poll()) {
|
||||||
if (action == frameyap::UiAction::Quit) return 0;
|
if (action == frameyap::UiAction::Quit) return 0;
|
||||||
if (action == frameyap::UiAction::BeginRecord) held = true;
|
if (action == frameyap::UiAction::BeginRecord) held = true;
|
||||||
if (action == frameyap::UiAction::EndRecord || action == frameyap::UiAction::Cancel) held = false;
|
if (action == frameyap::UiAction::EndRecord || action == frameyap::UiAction::Cancel) held = false;
|
||||||
std::cout << "Control action=" << static_cast<int>(action) << " (diagnostic only)" << std::endl;
|
last_action = std::string(action_name(action)) + " received; diagnostic only.";
|
||||||
|
std::cout << last_action << std::endl;
|
||||||
}
|
}
|
||||||
auto next = overlay.controls_status();
|
auto next = overlay.controls_status();
|
||||||
if (next != status) { status = next; std::cout << status << std::endl; }
|
if (next != status) { status = next; std::cout << status << std::endl; }
|
||||||
|
overlay.draw({"Controls ONLY - " + std::string(held ? "HOLD recognized" : "no microphone or typing"),
|
||||||
|
status, last_action, true, held});
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+38
-13
@@ -64,11 +64,14 @@ struct Overlay::Impl {
|
|||||||
std::array<vr::TrackedDevicePose_t, vr::k_unMaxTrackedDeviceCount> poses{};
|
std::array<vr::TrackedDevicePose_t, vr::k_unMaxTrackedDeviceCount> poses{};
|
||||||
bool grip_capture = false, ptt_capture = false;
|
bool grip_capture = false, ptt_capture = false;
|
||||||
bool focus = true;
|
bool focus = true;
|
||||||
|
bool persist_mount = true;
|
||||||
|
vr::EVRInputError action_update_error = vr::VRInputError_None;
|
||||||
|
|
||||||
Impl(const std::string& assets, const std::string& font, std::optional<Mount> requested)
|
Impl(const std::string& assets, const std::string& font, std::optional<Mount> requested, bool persist)
|
||||||
: settings_path(default_mount_settings_path()),
|
: settings_path(default_mount_settings_path()),
|
||||||
mount(requested ? *requested : load_mount(settings_path)),
|
mount(requested ? *requested : load_mount(settings_path)),
|
||||||
surface(font.empty() ? (std::filesystem::path(assets) / "fonts/Inconsolata-Regular.ttf").string() : font, mount) {
|
surface(font.empty() ? (std::filesystem::path(assets) / "fonts/Inconsolata-Regular.ttf").string() : font, mount),
|
||||||
|
persist_mount(persist) {
|
||||||
const auto action_path = absolute_file(std::filesystem::path(assets) / "actions.json");
|
const auto action_path = absolute_file(std::filesystem::path(assets) / "actions.json");
|
||||||
absolute_file(std::filesystem::path(assets) / "bindings_knuckles.json");
|
absolute_file(std::filesystem::path(assets) / "bindings_knuckles.json");
|
||||||
try {
|
try {
|
||||||
@@ -234,10 +237,11 @@ struct Overlay::Impl {
|
|||||||
focus = true; reset_input(result); break;
|
focus = true; reset_input(result); break;
|
||||||
case vr::VREvent_OverlayGamepadFocusLost:
|
case vr::VREvent_OverlayGamepadFocusLost:
|
||||||
case vr::VREvent_OverlayFocusChanged:
|
case vr::VREvent_OverlayFocusChanged:
|
||||||
// Pointer/gamepad overlay focus is not OS keyboard focus or action
|
// Dashboard laser/gamepad focus can change between the two grip
|
||||||
// activity. Cancel the old gesture, then require neutral rearm;
|
// squeezes. It is not action activity or controller tracking loss:
|
||||||
// do not permanently latch global grip actions off.
|
// invalidate pointer presses, but leave a physical grip gesture
|
||||||
reset_input(result); break;
|
// armed. Hidden overlays and true OpenVR input capture still reset.
|
||||||
|
surface.reset_pointers(); break;
|
||||||
case vr::VREvent_MouseMove:
|
case vr::VREvent_MouseMove:
|
||||||
surface.pointer_move(event.data.mouse.cursorIndex, event.data.mouse.x, H - event.data.mouse.y);
|
surface.pointer_move(event.data.mouse.cursorIndex, event.data.mouse.x, H - event.data.mouse.y);
|
||||||
break;
|
break;
|
||||||
@@ -251,7 +255,7 @@ struct Overlay::Impl {
|
|||||||
if (event_result.action) result.push_back(*event_result.action);
|
if (event_result.action) result.push_back(*event_result.action);
|
||||||
if (event_result.mount) {
|
if (event_result.mount) {
|
||||||
mount = *event_result.mount;
|
mount = *event_result.mount;
|
||||||
save_failed = !save_mount(settings_path, mount);
|
save_failed = persist_mount && !save_mount(settings_path, mount);
|
||||||
world_ready = false;
|
world_ready = false;
|
||||||
applied_mount.reset();
|
applied_mount.reset();
|
||||||
}
|
}
|
||||||
@@ -266,7 +270,8 @@ struct Overlay::Impl {
|
|||||||
set.ulActionSet = action_set;
|
set.ulActionSet = action_set;
|
||||||
set.ulRestrictedToDevice = vr::k_ulInvalidInputValueHandle;
|
set.ulRestrictedToDevice = vr::k_ulInvalidInputValueHandle;
|
||||||
set.nPriority = 0; // normal priority; no experimental scene-input overrides
|
set.nPriority = 0; // normal priority; no experimental scene-input overrides
|
||||||
if (input->UpdateActionState(&set, sizeof(set), 1) != vr::VRInputError_None) {
|
action_update_error = input->UpdateActionState(&set, sizeof(set), 1);
|
||||||
|
if (action_update_error != vr::VRInputError_None) {
|
||||||
reset_input(result); return result;
|
reset_input(result); return result;
|
||||||
}
|
}
|
||||||
auto [la, ld] = digital(0);
|
auto [la, ld] = digital(0);
|
||||||
@@ -300,15 +305,35 @@ struct Overlay::Impl {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Overlay::Overlay(const std::string& assets, const std::string& font, std::optional<Mount> mount)
|
Overlay::Overlay(const std::string& assets, const std::string& font, std::optional<Mount> mount, bool persist_mount)
|
||||||
: impl_(std::make_unique<Impl>(assets, font, mount)) {}
|
: impl_(std::make_unique<Impl>(assets, font, mount, persist_mount)) {}
|
||||||
Overlay::~Overlay() = default;
|
Overlay::~Overlay() = default;
|
||||||
std::vector<UiAction> Overlay::poll() { return impl_->poll(); }
|
std::vector<UiAction> Overlay::poll() { return impl_->poll(); }
|
||||||
void Overlay::draw(const Panel& panel) { impl_->draw(panel); }
|
void Overlay::draw(const Panel& panel) { impl_->draw(panel); }
|
||||||
std::string Overlay::controls_status() {
|
std::string Overlay::controls_status() {
|
||||||
auto left = impl_->digital(0), right = impl_->digital(1);
|
// Diagnostic only: distinguish SteamVR binding/activity from our stricter
|
||||||
return std::string("Grip actions tracked/active: left=") + (left.first ? "yes" : "no") +
|
// pose/role gate. Raw legacy state is read-only and may be unavailable.
|
||||||
" right=" + (right.first ? "yes" : "no");
|
std::string result = "SteamVR update=" + std::to_string(int(impl_->action_update_error));
|
||||||
|
for (size_t i = 0; i < 2; ++i) {
|
||||||
|
const auto role = i == 0 ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand;
|
||||||
|
const auto device = impl_->system->GetTrackedDeviceIndexForControllerRole(role);
|
||||||
|
const bool tracked = device < impl_->poses.size() && impl_->poses[device].bPoseIsValid &&
|
||||||
|
impl_->system->IsTrackedDeviceConnected(device);
|
||||||
|
vr::InputDigitalActionData_t data{};
|
||||||
|
const auto error = impl_->input->GetDigitalActionData(impl_->actions[i], &data, sizeof(data), vr::k_ulInvalidInputValueHandle);
|
||||||
|
vr::InputOriginInfo_t origin{};
|
||||||
|
const bool origin_ok = error == vr::VRInputError_None && data.activeOrigin != vr::k_ulInvalidInputValueHandle &&
|
||||||
|
impl_->input->GetOriginTrackedDeviceInfo(data.activeOrigin, &origin, sizeof(origin)) == vr::VRInputError_None &&
|
||||||
|
origin.trackedDeviceIndex == device;
|
||||||
|
vr::VRControllerState_t raw{};
|
||||||
|
const bool raw_ok = device < impl_->poses.size() && impl_->system->GetControllerState(device, &raw, sizeof(raw));
|
||||||
|
result += std::string(i == 0 ? "\nLeft:" : "\nRight:") +
|
||||||
|
" err=" + std::to_string(int(error)) + " bound=" + (data.bActive ? "Y" : "N") +
|
||||||
|
" down=" + (data.bState ? "Y" : "N") + " pose=" + (tracked ? "Y" : "N") +
|
||||||
|
" origin=" + (origin_ok ? "Y" : "N") +
|
||||||
|
" raw=" + (raw_ok ? ((raw.ulButtonPressed & vr::ButtonMaskFromId(vr::k_EButton_Grip)) ? "down" : "up") : "n/a");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int registration(const std::string& manifest, bool remove, bool autostart) {
|
int registration(const std::string& manifest, bool remove, bool autostart) {
|
||||||
|
|||||||
+2
-1
@@ -17,7 +17,8 @@ struct Panel {
|
|||||||
};
|
};
|
||||||
class Overlay {
|
class Overlay {
|
||||||
public:
|
public:
|
||||||
Overlay(const std::string& assets, const std::string& font = {}, std::optional<Mount> mount = {});
|
Overlay(const std::string& assets, const std::string& font = {}, std::optional<Mount> mount = {},
|
||||||
|
bool persist_mount = true);
|
||||||
~Overlay();
|
~Overlay();
|
||||||
Overlay(const Overlay&) = delete;
|
Overlay(const Overlay&) = delete;
|
||||||
Overlay& operator=(const Overlay&) = delete;
|
Overlay& operator=(const Overlay&) = delete;
|
||||||
|
|||||||
+23
-6
@@ -74,20 +74,33 @@ int run(const Options& options) {
|
|||||||
try {
|
try {
|
||||||
if (auto reply = worker.poll()) {
|
if (auto reply = worker.poll()) {
|
||||||
if (reply->id == session.id() && session.state() == State::Transcribing) {
|
if (reply->id == session.id() && session.state() == State::Transcribing) {
|
||||||
if (!reply->error.empty()) throw std::runtime_error(reply->error);
|
if (!reply->error.empty()) {
|
||||||
|
// E is a request-local error. The child still owns its
|
||||||
|
// loaded model and can accept the next utterance.
|
||||||
|
session.fail(); detail = reply->error + "; model ready. Record to retry.";
|
||||||
|
} else {
|
||||||
session.reply(reply->id, reply->text);
|
session.reply(reply->id, reply->text);
|
||||||
detail = session.text().empty() ? "No speech recognized; try again." : "Focus your destination, then Insert. Cancel discards.";
|
detail = session.text().empty() ? "No speech recognized; try again." : "Focus your destination, then Insert. Cancel discards.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (worker.ready()) session.ready();
|
if (worker.ready()) session.ready();
|
||||||
if (audio.recording() && audio.poll()) stop_record();
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
audio.cancel(); worker.stop();
|
audio.cancel(); worker.stop();
|
||||||
if (session.state() != State::Review && session.state() != State::Queued) session.fail();
|
if (session.state() != State::Review && session.state() != State::Queued) session.fail();
|
||||||
detail = e.what(); // Preserve an already-correlated preview if the worker dies.
|
detail = e.what(); // Preserve an already-correlated preview if the worker dies.
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
if (audio.recording() && audio.poll()) stop_record();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
// A microphone failure is not a model failure. Release the device,
|
||||||
|
// retain the loaded worker, and allow Record to retry capture.
|
||||||
|
audio.cancel(); session.fail(); detail = e.what();
|
||||||
|
}
|
||||||
// Drawing precedes input polling: Enter is disabled until Ready is visible.
|
// Drawing precedes input polling: Enter is disabled until Ready is visible.
|
||||||
Panel panel{state_label(session.state()), session.text(), detail,
|
const auto status = session.state() == State::Error && worker.ready()
|
||||||
|
? "Retry available - local model still loaded" : state_label(session.state());
|
||||||
|
Panel panel{status, session.text(), detail,
|
||||||
session.state() != State::Error && session.state() != State::Warming && session.state() != State::Transcribing,
|
session.state() != State::Error && session.state() != State::Warming && session.state() != State::Transcribing,
|
||||||
session.state() == State::Recording,
|
session.state() == State::Recording,
|
||||||
session.state() != State::Warming && session.state() != State::Transcribing && session.state() != State::Review};
|
session.state() != State::Warming && session.state() != State::Transcribing && session.state() != State::Review};
|
||||||
@@ -106,8 +119,10 @@ int run(const Options& options) {
|
|||||||
case UiAction::Cancel: {
|
case UiAction::Cancel: {
|
||||||
auto state = session.state();
|
auto state = session.state();
|
||||||
audio.cancel(); session.cancel(); detail = "Discarded. Microphone closed.";
|
audio.cancel(); session.cancel(); detail = "Discarded. Microphone closed.";
|
||||||
if (state == State::Warming || state == State::Transcribing || state == State::Error) {
|
if (state == State::Warming || state == State::Transcribing) {
|
||||||
worker.stop(); session.fail(); detail = "Cancelled. Record to reload local worker.";
|
worker.stop(); session.fail(); detail = "Cancelled. Record to reload local worker.";
|
||||||
|
} else if (state == State::Error && !worker.ready()) {
|
||||||
|
session.fail(); detail = "Worker unavailable. Record to reload local worker.";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -131,9 +146,11 @@ int run(const Options& options) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
// Input lease failures preserve preview; capture failures release mic.
|
// Input lease failures preserve preview. Capture failures close
|
||||||
|
// only the microphone; submit failures stop their own child if
|
||||||
|
// the IPC stream was partially written.
|
||||||
if (session.state() == State::Recording || session.state() == State::Transcribing) {
|
if (session.state() == State::Recording || session.state() == State::Transcribing) {
|
||||||
audio.cancel(); worker.stop(); session.fail();
|
audio.cancel(); session.fail();
|
||||||
}
|
}
|
||||||
detail = e.what();
|
detail = e.what();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,5 +27,11 @@ int main() {
|
|||||||
CHECK(s.record()); CHECK(s.finish(16000)); CHECK(s.reply(s.id(), ""));
|
CHECK(s.record()); CHECK(s.finish(16000)); CHECK(s.reply(s.id(), ""));
|
||||||
CHECK(s.state() == State::Ready); CHECK(!s.take_insert());
|
CHECK(s.state() == State::Ready); CHECK(!s.take_insert());
|
||||||
CHECK(s.record()); CHECK(s.finish(16000)); s.fail(); CHECK(!s.reply(s.id(), "bad"));
|
CHECK(s.record()); CHECK(s.finish(16000)); s.fail(); CHECK(!s.reply(s.id(), "bad"));
|
||||||
|
// Request-local model or microphone errors can leave the worker loaded:
|
||||||
|
// discard the failed session and record again without a new warmup state.
|
||||||
|
s.cancel(); CHECK(s.state() == State::Ready);
|
||||||
|
CHECK(s.record()); CHECK(s.finish(16000)); CHECK(s.reply(s.id(), "again"));
|
||||||
|
CHECK(s.take_insert() == "again"); CHECK(s.record());
|
||||||
|
s.fail(); s.cancel(); CHECK(s.record());
|
||||||
std::cout << "core checks passed\n";
|
std::cout << "core checks passed\n";
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,7 @@ def fake_child():
|
|||||||
time.sleep(130)
|
time.sleep(130)
|
||||||
return
|
return
|
||||||
worker.send_frame(1, b"Y")
|
worker.send_frame(1, b"Y")
|
||||||
|
requests = 0
|
||||||
while True:
|
while True:
|
||||||
msg = worker.read_frame(0)
|
msg = worker.read_frame(0)
|
||||||
if msg is None:
|
if msg is None:
|
||||||
@@ -52,6 +53,7 @@ def fake_child():
|
|||||||
time.sleep(70)
|
time.sleep(70)
|
||||||
continue
|
continue
|
||||||
assert len(msg) == 9 and msg[:1] == b"T"
|
assert len(msg) == 9 and msg[:1] == b"T"
|
||||||
|
requests += 1
|
||||||
clip = Path(args.clip_dir) / "clip.raw"
|
clip = Path(args.clip_dir) / "clip.raw"
|
||||||
assert clip.stat().st_size == 3200 * 4
|
assert clip.stat().st_size == 3200 * 4
|
||||||
ident = msg[1:9]
|
ident = msg[1:9]
|
||||||
@@ -60,6 +62,9 @@ def fake_child():
|
|||||||
if args.model == "oversized-frame":
|
if args.model == "oversized-frame":
|
||||||
os.write(1, struct.pack("<I", 65537))
|
os.write(1, struct.pack("<I", 65537))
|
||||||
continue
|
continue
|
||||||
|
if args.model == "request-error" and requests == 1:
|
||||||
|
worker.send_frame(1, b"E", ident + b"transcription failed")
|
||||||
|
continue
|
||||||
text = b"a" * 4097 if args.model == "long" else "héllo 世界".encode()
|
text = b"a" * 4097 if args.model == "long" else "héllo 世界".encode()
|
||||||
worker.send_frame(1, b"R", ident + text)
|
worker.send_frame(1, b"R", ident + text)
|
||||||
if args.model == "duplicate":
|
if args.model == "duplicate":
|
||||||
|
|||||||
@@ -55,6 +55,15 @@ int main(int argc, char** argv) {
|
|||||||
(void)entry;
|
(void)entry;
|
||||||
throw std::runtime_error("private directory leaked");
|
throw std::runtime_error("private directory leaked");
|
||||||
}
|
}
|
||||||
|
worker.start(argv[1], argv[2], "request-error", 2);
|
||||||
|
until([&] { worker.poll(); return worker.ready(); });
|
||||||
|
worker.submit(200, clip);
|
||||||
|
until([&] { reply = worker.poll(); return reply.has_value(); });
|
||||||
|
assert(reply->id == 200 && reply->error == "transcription failed" && worker.ready());
|
||||||
|
worker.submit(201, clip); // A request-local failure must not unload Redux.
|
||||||
|
until([&] { reply = worker.poll(); return reply.has_value(); });
|
||||||
|
assert(reply->id == 201 && reply->text == "héllo 世界" && worker.ready());
|
||||||
|
worker.stop();
|
||||||
worker.start(argv[1], argv[2], "fail", 2);
|
worker.start(argv[1], argv[2], "fail", 2);
|
||||||
bool failed = false;
|
bool failed = false;
|
||||||
until([&] {
|
until([&] {
|
||||||
|
|||||||
Reference in new issue
Block a user