#include "runtime.hpp" #include "cli.hpp" #include "audio.hpp" #include "controller.hpp" #include "backend_manager.hpp" #include "config.hpp" #include "overlay.hpp" #include "text_input.hpp" #include "worker.hpp" #include "instance_lock.hpp" #include "focus_guard.hpp" #include #include #include #include #include #include #include #include #include namespace frameyap { namespace { volatile sig_atomic_t interrupted = 0; void signal_stop(int) { interrupted = 1; } class NativeDeliveryLease final : public DeliveryLease { public: explicit NativeDeliveryLease(const std::string& socket) : input_(socket) {} void text(const std::string& literal) override { input_.text(literal); } void enter() override { input_.enter(); } private: TextInput input_; }; class NativeAudio final : public ControllerAudio { public: bool open() const override { return audio_.open(); } void prepare() override { audio_.prepare(); } void start() override { audio_.start(); } bool poll() override { return audio_.poll(); } std::vector finish() override { return audio_.finish(); } void cancel() override { audio_.cancel(); } void close() override { audio_.close(); } int seconds() const override { return audio_.seconds(); } private: Audio audio_; }; class NativeWorker final : public ControllerWorker { public: explicit NativeWorker(const Options& options) : options_(options) {} void configure(std::string backend, std::string model, std::string manifest, std::string root) { backend_ = std::move(backend); model_ = std::move(model); manifest_ = std::move(manifest); root_ = std::move(root); } void start(bool advanced_debug) override { const std::string script = backend_.empty() ? options_.worker : root_ + "/python/frameyap/backend_worker.py"; worker_.start(options_.python, script, backend_.empty() ? options_.model : model_, options_.threads, advanced_debug, backend_, manifest_, root_); } bool ready() const override { return worker_.ready(); } void submit(uint64_t id, const std::vector& pcm) override { worker_.submit(id, pcm); } std::optional poll() override { return worker_.poll(); } void stop() override { worker_.stop(); } private: const Options& options_; Worker worker_; std::string backend_, model_, manifest_, root_; }; // Opt-in (Advanced debug) native delivery diagnostics. Metadata only: never // transcript text. Owner-private file next to worker-debug.log. class DeliveryTrace { public: ~DeliveryTrace() { set_enabled(false); } void set_enabled(bool enabled) { if (enabled == enabled_) return; enabled_ = enabled; if (fd_ >= 0) { ::close(fd_); fd_ = -1; } if (enabled_) { try { fd_ = open_private_debug_log("delivery-debug.log", "delivery-debug.previous.log"); } catch (...) { fd_ = -1; } written_ = 0; } } void operator()(const std::string& line) { if (fd_ < 0 || written_ > 1024 * 1024) return; const auto ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - start_).count(); const auto out = std::to_string(ms) + "ms " + line + "\n"; if (::write(fd_, out.data(), out.size()) > 0) written_ += out.size(); } private: bool enabled_ = false; int fd_ = -1; size_t written_ = 0; std::chrono::steady_clock::time_point start_ = std::chrono::steady_clock::now(); }; class NativeFocus final : public ControllerFocus { public: explicit NativeFocus(DeliveryTrace& trace) : trace_(trace) {} bool arm() override { if (guard_.arm()) { trace_("focus armed"); return true; } trace_(std::string("focus arm failed: ") + guard_.failure()); return false; } bool valid() override { if (guard_.valid()) return true; trace_(std::string("focus invalid: ") + guard_.failure()); return false; } private: FocusGuard guard_; DeliveryTrace& trace_; }; } // namespace int run(const Options& options) { InstanceLock lock; // Authorized sends retain the IME between per-action leases. Release it on // every exit path, after any active delivery stack has unwound; never between // text and its explicitly requested Enter. This is not a delivery receipt. struct ReleaseInput { ~ReleaseInput() { try { TextInput::release_idle(); } catch (...) {} } } release_input; interrupted = 0; auto old_int = std::signal(SIGINT, signal_stop); auto old_term = std::signal(SIGTERM, signal_stop); struct Restore { decltype(old_int) a, b; ~Restore() { std::signal(SIGINT, a); std::signal(SIGTERM, b); } } restore{old_int, old_term}; Overlay overlay(options.assets, options.font, options.mount); DeliveryTrace trace; trace.set_enabled(overlay.advanced_debug()); NativeWorker worker(options); NativeAudio audio; const DeliveryFactory acquire = [&]() -> std::unique_ptr { auto input = std::make_unique(options.socket); if (interrupted) throw std::runtime_error("Input cancelled before delivery"); return input; }; PacedDelivery paced(acquire, std::chrono::steady_clock::now, [] { TextInput::release_idle(); }, [] { return !interrupted; }); paced.set_trace([&trace](const std::string& line) { trace(line); }); Controller controller(audio, worker, acquire, [&trace] { return std::make_unique(trace); }, overlay.quick_inputs(), overlay.auto_insert(), overlay.advanced_debug(), overlay.close_mic_when_idle(), &paced); namespace fs = std::filesystem; const auto root = asset_root(options.assets); const auto manifest = options.manifest_dir.empty() ? root / "assets/backends" : fs::path(options.manifest_dir); const auto service = root / "scripts/backend-service.py"; const bool generic = (options.worker.empty() || is_managed_worker(options.assets, options.worker)) && fs::is_regular_file(root / "python/frameyap/backend_worker.py") && fs::is_regular_file(service) && fs::is_directory(manifest); const char* data = std::getenv("XDG_DATA_HOME"); const char* home = std::getenv("HOME"); const fs::path store = !options.model_store.empty() ? fs::path(options.model_store) : data && fs::path(data).is_absolute() ? fs::path(data) / "frameyap/models" : home && fs::path(home).is_absolute() ? fs::path(home) / ".local/share/frameyap/models" : root / "models"; const auto installer = fs::is_regular_file(root / "bin/install.sh") ? root / "bin/install.sh" : root / "install.sh"; std::unique_ptr models; std::string selected = options.backend.empty() ? load_config(default_config_path()).backend : options.backend; size_t revision = 0; std::string selection_note; if (generic) { models = std::make_unique("python3", service.string(), manifest.string(), store.string(), fs::is_regular_file(installer) ? installer.string() : "", options.model); models->refresh(); controller.backend_changed(false, "Checking installed models offline. Recording unavailable until verified."); } else controller.initialize(); bool previous_debug = overlay.advanced_debug(); while (!controller.quitting() && !interrupted) { if (models) { models->poll(); if (models->revision() != revision) { revision = models->revision(); auto it = std::find_if(models->entries().begin(), models->entries().end(), [&](const auto& entry) { return entry.id == selected; }); const bool verified = models->checked() && it != models->entries().end() && it->state == "installed_verified" && !models->busy(); if (verified) worker.configure(selected, models->model_path(selected), manifest.string(), root.string()); controller.backend_changed(verified, it == models->entries().end() ? "Selected backend unavailable. Choose a listed model in Settings." : "Selected model not installed/verified. Select Install in Settings."); } } controller.tick(); // Drawing precedes polling actions: Enter is disabled until Ready is visible. auto panel = controller.panel(); if (models) { panel.selected_backend = selected; panel.model_note = selection_note.empty() ? models->note() : selection_note; panel.model_busy = models->busy(); for (const auto& entry : models->entries()) panel.models.push_back({entry.id, entry.name, entry.id == selected && controller.state() == State::Warming ? "loading" : entry.id == selected && controller.state() == State::Ready ? "ready" : entry.id == selected && controller.state() == State::Error && entry.state == "installed_verified" ? "failed" : entry.state, entry.source, entry.license, entry.license_text, entry.attribution, entry.bytes, entry.state == "installed_verified", entry.manifest_sha256}); } overlay.draw(panel); auto actions = overlay.poll(); const auto model_actions = overlay.take_model_actions(); // A model-row click revokes the current clip/review even when it targets // the selected row, is rejected while busy, or carries stale consent. // Do not dispatch the Cancel/EndRecord generated when the overlay resets // held PTT: it could turn an unavailable backend back into Ready. for (const auto& change : model_actions) { if (!models) continue; try { if (change.install) { if (change.id == selected) { models->install(change.id, change.manifest_sha256); selection_note.clear(); } } else if (change.id != selected && !models->busy()) { const auto& entries = models->entries(); auto it = std::find_if(entries.begin(), entries.end(), [&](const auto& entry) { return entry.id == change.id; }); if (it == entries.end()) continue; selected = change.id; selection_note = save_backend(default_config_path(), selected) ? "" : "Backend preference not saved; selection lasts only this session."; } } catch (const std::exception&) { selection_note = "Model metadata changed or installer unavailable. Recheck and confirm again."; } } if (models && !model_actions.empty()) { const auto& entries = models->entries(); auto it = std::find_if(entries.begin(), entries.end(), [&](const auto& entry) { return entry.id == selected; }); const bool verified = models->checked() && !models->busy() && it != entries.end() && it->state == "installed_verified"; if (verified) worker.configure(selected, it->model_path, manifest.string(), root.string()); controller.backend_changed(verified, verified ? "" : "Selected model unavailable or being checked. Recording disabled until offline verification."); } bool debug_changed = overlay.advanced_debug() != previous_debug; trace.set_enabled(overlay.advanced_debug()); controller.settings(overlay.auto_insert(), overlay.advanced_debug(), overlay.close_mic_when_idle()); // Debug consent and model actions cancel work before any stale input. if (debug_changed || !model_actions.empty()) { for (auto action : actions) if (action == UiAction::Quit) controller.action(action); } else { for (auto action : actions) { controller.action(action); if (controller.quitting() || interrupted) break; } } previous_debug = overlay.advanced_debug(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } controller.shutdown(); if (models) models->cancel(); return 0; } } // namespace frameyap