diff --git a/examples/whisper.linux/.gitignore b/examples/whisper.linux/.gitignore new file mode 100644 index 000000000..75c61823b --- /dev/null +++ b/examples/whisper.linux/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.pytest_cache/ diff --git a/examples/whisper.linux/README.md b/examples/whisper.linux/README.md new file mode 100644 index 000000000..b9d69a08a --- /dev/null +++ b/examples/whisper.linux/README.md @@ -0,0 +1,154 @@ +# whisper.linux + +Voice typing for Linux desktop using [whisper.cpp](https://github.com/ggerganov/whisper.cpp). + +Transcribes speech from microphone and injects text at the cursor position. +Supports X11 and Wayland, hotkey and continuous listening modes, voice commands. + +## Requirements + +- Python 3.10+ +- PyQt5: `pip install PyQt5` +- Built `whisper-cli` from whisper.cpp (see root README) +- A GGML model file (e.g. `ggml-base.bin`) +- `xdotool` (X11/XWayland) or `wtype` (Wayland) for text injection +- `xclip` (X11) or `wl-copy` (Wayland) for clipboard fallback + +## Quick Start + +```bash +cd examples/whisper.linux + +# Start +./whisper-linux + +# Start with debug logging +./whisper-linux --debug + +# Toggle recording on a running instance +./whisper-linux --toggle + +# Stop +pkill -f whisper-linux +``` + +## CLI Options + +``` +--toggle Send toggle signal to running instance +--language LANG Override language (ru, en, auto) +--model PATH Override model path +--input-mode MODE hotkey or listen +--output-mode MODE batch or stream +--stream Shortcut for --output-mode stream +--wake-word WORD Override wake word (for listen mode) +--wake-model PATH Lighter model for wake word detection +--debug Enable debug logging +``` + +## Input / Output Modes + +Two independent axes control behavior: + +| | batch | stream | +|---|---|---| +| **hotkey** | Record all, transcribe, inject at once | Each speech segment transcribed and injected live | +| **listen** | Wake word activates, text accumulated, injected on stop | Wake word activates, each segment injected live | + +Default: `hotkey` + `batch` (press hotkey to record, press again to transcribe). + +## Voice Commands + +When `voice_commands = True` (default), spoken command words trigger key presses +instead of being typed literally. Editable via tray menu: Settings > Edit voice commands. + +| Word (EN) | Word (RU) | Action | +|---|---|---| +| enter | энтер, ввод | Press Enter | +| backspace | бэкспейс, назад | Delete previous word | +| tab | таб, табуляция | Press Tab | +| escape, stop | эскейп, стоп | Press Escape | + +Commands use fuzzy matching (threshold 0.75), so slight mispronunciations are tolerated. + +**Backspace** has special behavior: if there are buffered words not yet injected, +it removes the last word from the buffer. If the buffer is empty, it sends +`Ctrl+BackSpace` to delete the previous word in the editor. + +## Keyboard Shortcut + +In hotkey mode, you toggle recording via `--toggle`. Set up a global keyboard shortcut +to trigger it from anywhere: + +**GNOME** (Settings → Keyboard → Custom Shortcuts → Add): + +| Field | Value | +|---|---| +| Name | whisper-linux | +| Command | `/path/to/whisper.linux/whisper-linux --toggle` | +| Shortcut | `Super+V` or any key you prefer | + +**Or via command line (GNOME):** + +```bash +# Replace /path/to/whisper.linux with the actual path +TOGGLE_CMD="/path/to/whisper.linux/whisper-linux --toggle" + +gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings \ + "['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/whisper-linux/']" + +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/whisper-linux/name "'whisper-linux'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/whisper-linux/command "'$TOGGLE_CMD'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/whisper-linux/binding "'F8'" +``` + +**KDE:** System Settings → Shortcuts → Custom Shortcuts → Add. + +## Configuration + +Config file: `~/.config/whisper-linux/config.ini` + +All settings are configurable via the system tray menu (right-click the tray icon). + +## Autostart + +Copy the desktop file to autostart: + +```bash +cp whisper-linux.desktop ~/.config/autostart/ +``` + +## Project Structure + +``` +whisper.linux/ + whisper-linux # Launcher script (unique process name) + app/ # Python package + __init__.py # Re-exports public API + __main__.py # Entry point + config.py # Config, AppState, constants, helpers + audio.py # AudioRecorder, AudioStream, SimpleVAD + transcriber.py # Transcriber, WakeWordDetector + injector.py # TextInjector (xdotool/wtype/clipboard) + commands.py # VoiceCommands (Enter, Backspace, etc.) + tray.py # TrayIcon, system tray menu, settings + app.py # WhisperLinuxApp, state machine, CLI + tests/ + conftest.py # Fixtures + test_whisper_linux.py # Tests (all mocked, no hardware needed) + run_tests.sh # Run all tests with one command + whisper-linux.desktop # Desktop entry for autostart +``` + +## Running Tests + +```bash +# All tests +./run_tests.sh + +# With options +./run_tests.sh --debug -x -k "test_toggle" + +# Or directly +python3 -m pytest tests/ -v +``` diff --git a/examples/whisper.linux/app/__init__.py b/examples/whisper.linux/app/__init__.py new file mode 100644 index 000000000..5f2e583de --- /dev/null +++ b/examples/whisper.linux/app/__init__.py @@ -0,0 +1,21 @@ +"""whisper.linux — Voice typing for Linux desktop.""" + +# Re-export public API for backward compatibility (tests import from whisper_linux) +import os +import subprocess +import threading +import time + +from .config import ( + Config, AppState, CONFIG_DIR, CONFIG_FILE, PID_FILE, + AVAILABLE_MODELS, DEFAULT_VOICE_COMMANDS, _REPO_ROOT, + _find_executable, _find_model, _list_models, _list_audio_devices, + _WHISPER_CLI_NAMES, _WHISPER_SEARCH_DIRS, _MODEL_SEARCH_DIRS, + LOG_FORMAT, log, +) +from .audio import AudioRecorder, AudioStream, SimpleVAD, _write_wav +from .transcriber import Transcriber, WakeWordDetector, _is_hallucination +from .injector import TextInjector +from .commands import VoiceCommands +from .tray import TrayIcon, _create_icon +from .app import WhisperLinuxApp, send_toggle, main diff --git a/examples/whisper.linux/app/__main__.py b/examples/whisper.linux/app/__main__.py new file mode 100644 index 000000000..00a3f881f --- /dev/null +++ b/examples/whisper.linux/app/__main__.py @@ -0,0 +1,4 @@ +"""Entry point for `python3 -m whisper_linux`.""" +from .app import main + +main() diff --git a/examples/whisper.linux/app/app.py b/examples/whisper.linux/app/app.py new file mode 100644 index 000000000..9d06e7189 --- /dev/null +++ b/examples/whisper.linux/app/app.py @@ -0,0 +1,750 @@ +"""Main application logic, state machine, and CLI for whisper.linux.""" + +import argparse +import logging +import os +import queue +import signal +import subprocess +import sys +import tempfile +import threading +import time +from pathlib import Path +from typing import Optional + +from . import config as _cfg +from .config import Config, AppState, log +from .audio import AudioRecorder, AudioStream, SimpleVAD, _write_wav +from .transcriber import Transcriber, WakeWordDetector +from .injector import TextInjector +from .commands import VoiceCommands +from .tray import TrayIcon + + +class WhisperLinuxApp: + """Main application: state machine, threading, PID file, signal handling.""" + + def __init__(self, config: Optional[Config] = None): + self.config = config or Config() + self.state = AppState.IDLE + self.recorder = AudioRecorder(self.config) + self.transcriber = Transcriber(self.config) + self.injector = TextInjector(self.config) + self._tray: Optional[TrayIcon] = None + self._qt_app = None + self._prev_window_id: Optional[str] = None + self._audio_stream: Optional[AudioStream] = None + self._vad: Optional[SimpleVAD] = None + self._wake_detector: Optional[WakeWordDetector] = None + self._silence_timer: Optional[threading.Timer] = None + self._segment_queue: Optional[queue.Queue] = None + self._segment_worker: Optional[threading.Thread] = None + self._accumulated_texts: list = [] + self._voice_commands = VoiceCommands(self.config.voice_commands_map) + self._mute_until: float = 0 + + def _inject_text(self, text: str): + """Inject text, processing voice commands if enabled.""" + if self.config.voice_commands: + self._voice_commands.process( + text, self.injector.inject, self.injector.send_key, + ) + else: + self.injector.inject(text) + + # -- PID file management -- + + @staticmethod + def write_pid(): + _cfg.PID_FILE.write_text(str(os.getpid())) + + @staticmethod + def read_pid() -> Optional[int]: + if _cfg.PID_FILE.exists(): + try: + pid = int(_cfg.PID_FILE.read_text().strip()) + os.kill(pid, 0) + return pid + except (ValueError, ProcessLookupError, PermissionError): + return None + return None + + @staticmethod + def remove_pid(): + _cfg.PID_FILE.unlink(missing_ok=True) + + # -- Signal handling -- + + def _setup_signals(self): + signal.signal(signal.SIGUSR1, self._on_sigusr1) + signal.signal(signal.SIGTERM, self._on_sigterm) + signal.signal(signal.SIGINT, self._on_sigterm) + + def _on_sigusr1(self, signum, frame): + log.info("Received SIGUSR1 \u2014 toggling") + if self._qt_app: + from PyQt5.QtCore import QTimer + QTimer.singleShot(0, self.toggle) + + def _on_sigterm(self, signum, frame): + log.info("Received %s \u2014 quitting", signal.Signals(signum).name) + self.quit() + + # -- State machine -- + + def toggle(self): + log.info("Toggle: state=%s, input=%s, output=%s", + self.state.value, self.config.input_mode, self.config.output_mode) + if self.config.input_mode == "listen": + # In listen mode, hotkey acts as manual dictation trigger + if self.state == AppState.IDLE: + self._start_listening() + elif self.state == AppState.LISTENING: + # Hotkey starts dictation (like wake word) + self._save_active_window() + self._accumulated_texts.clear() + self._cancel_silence_timer() + self.state = AppState.DICTATING + self._set_state(AppState.DICTATING) + self._play_start_signal() + self._reset_silence_timer() + log.info("Hotkey: LISTENING → DICTATING") + elif self.state == AppState.DICTATING: + # Hotkey stops dictation back to listening + self._cancel_silence_timer() + self._flush_accumulated_text() + self.state = AppState.LISTENING + self._set_state(AppState.LISTENING) + self._play_end_signal() + log.info("Hotkey: DICTATING → LISTENING") + else: + self._toggle_listen() + else: + self._toggle_hotkey() + + def _toggle_hotkey(self): + if self.state == AppState.IDLE: + if self.config.output_mode == "batch": + self._start_recording() + else: + self._start_hotkey_stream() + elif self.state == AppState.RECORDING: + self._stop_recording() + elif self.state == AppState.DICTATING: + self._stop_hotkey_stream() + + def _toggle_listen(self): + if self.state == AppState.IDLE: + self._start_listening() + elif self.state in (AppState.LISTENING, AppState.DICTATING): + self._stop_listening() + + def _force_idle(self): + if self.state == AppState.RECORDING: + self.recorder.stop() + elif self.state in (AppState.LISTENING, AppState.DICTATING): + if self.config.input_mode == "hotkey" and self.state == AppState.DICTATING: + self._stop_hotkey_stream() + return + self._stop_listening() + self._set_state(AppState.IDLE) + + # -- Streaming mode -- + + def _start_listening(self): + self._wake_detector = WakeWordDetector(self.config.wake_word) + self._vad = SimpleVAD(self.config, on_speech_end=self._on_speech_end, + on_speech_start=self._on_speech_start) + self._audio_stream = AudioStream(self.config) + self._segment_queue = queue.Queue() + self._segment_worker = threading.Thread( + target=self._segment_worker_loop, daemon=True, + ) + self._segment_worker.start() + try: + self._audio_stream.start(on_data=self._on_audio_data) + self._set_state(AppState.LISTENING) + log.info("Streaming: LISTENING (waiting for wake word '%s')", self.config.wake_word) + except Exception as e: + log.error("Failed to start audio stream: %s", e) + if self._tray: + self._tray.notify("Error", f"Stream failed: {e}") + if self._segment_queue: + self._segment_queue.put(None) + self._segment_worker = None + self._segment_queue = None + + def _stop_listening(self): + self._cancel_silence_timer() + self._flush_accumulated_text() + if self._audio_stream: + self._audio_stream.stop() + self._audio_stream = None + self._vad = None + self._wake_detector = None + if self._segment_queue: + self._segment_queue.put(None) + if self._segment_worker: + self._segment_worker.join(timeout=5) + self._segment_worker = None + self._segment_queue = None + self._set_state(AppState.IDLE) + log.info("Streaming: stopped") + + def _start_hotkey_stream(self): + self._save_active_window() + self._wake_detector = None + self._vad = SimpleVAD(self.config, on_speech_end=self._on_speech_end, + on_speech_start=self._on_speech_start) + self._audio_stream = AudioStream(self.config) + self._accumulated_texts.clear() + self._segment_queue = queue.Queue() + self._segment_worker = threading.Thread( + target=self._segment_worker_loop, daemon=True, + ) + self._segment_worker.start() + try: + self._audio_stream.start(on_data=self._on_audio_data) + self._set_state(AppState.DICTATING) + self._play_start_signal() + log.info("Hotkey+Stream: DICTATING started") + except Exception as e: + log.error("Failed to start audio stream: %s", e) + if self._tray: + self._tray.notify("Error", f"Stream failed: {e}") + if self._segment_queue: + self._segment_queue.put(None) + self._segment_worker = None + self._segment_queue = None + + def _stop_hotkey_stream(self): + self._cancel_silence_timer() + self._flush_accumulated_text() + if self._audio_stream: + self._audio_stream.stop() + self._audio_stream = None + self._vad = None + if self._segment_queue: + self._segment_queue.put(None) + if self._segment_worker: + self._segment_worker.join(timeout=5) + self._segment_worker = None + self._segment_queue = None + self._set_state(AppState.IDLE) + self._play_end_signal() + log.info("Hotkey+Stream: stopped") + + def _flush_accumulated_text(self): + if self._accumulated_texts: + all_text = " ".join(self._accumulated_texts) + self._restore_active_window() + self._inject_text(all_text) + self._accumulated_texts.clear() + + def _on_audio_data(self, chunk: bytes): + if self._mute_until and time.time() < self._mute_until: + return + if self._vad: + self._vad.feed(chunk) + + def _on_speech_start(self): + if self.state == AppState.DICTATING: + self._cancel_silence_timer() + log.debug("Silence timer cancelled (speech started)") + + def _on_speech_end(self, pcm_data: bytes): + if self.state == AppState.DICTATING: + self._cancel_silence_timer() + if self._segment_queue: + self._segment_queue.put(pcm_data) + + def _segment_worker_loop(self): + while True: + try: + pcm_data = self._segment_queue.get(timeout=1.0) + except queue.Empty: + if not self._segment_queue: + break + continue + if pcm_data is None: + break + self._process_speech_segment(pcm_data) + + def _process_speech_segment(self, pcm_data: bytes): + duration_s = len(pcm_data) / (16000 * 2) + fd, wav_path = tempfile.mkstemp(suffix=".wav", prefix="whisper-stream-") + os.close(fd) + try: + _write_wav(wav_path, pcm_data) + model = None + if self.state == AppState.LISTENING and self.config.wake_model: + model = self.config.wake_model + text = self.transcriber.transcribe(wav_path, model=model, + duration_s=duration_s) + if not text: + return + + log.info("Stream segment: %r (state=%s)", text[:80], self.state.value) + + if self._wake_detector and self._wake_detector.contains_wake_word(text): + remaining = self._wake_detector.strip_wake_word(text) + + if self.state == AppState.LISTENING: + self._save_active_window() + self._accumulated_texts.clear() + self.state = AppState.DICTATING + self._marshal_set_state(AppState.DICTATING) + self._play_start_signal() + # Start silence timer immediately so dictation auto-stops + # even if user says nothing after wake word + if self.config.input_mode == "listen": + self._reset_silence_timer() + log.info("Streaming: DICTATING (wake word detected)") + self._marshal_notify("whisper.linux", "Dictation started") + elif self.state == AppState.DICTATING: + self._cancel_silence_timer() + if remaining: + if self.config.output_mode == "stream": + self._restore_active_window() + self._inject_text(remaining) + else: + self._accumulated_texts.append(remaining) + self._flush_accumulated_text() + self.state = AppState.LISTENING + self._marshal_set_state(AppState.LISTENING) + self._play_end_signal() + log.info("Streaming: LISTENING (wake word \u2192 stop dictation)") + self._marshal_notify("whisper.linux", "Dictation paused") + return + + if self.state == AppState.DICTATING: + if self.config.output_mode == "stream": + self._restore_active_window() + self._inject_text(text) + else: + self._accumulated_texts.append(text) + if self.config.input_mode == "listen": + self._reset_silence_timer() + log.debug("Silence timer reset after segment (%.1fs)", + self.config.silence_timeout) + if self.config.notification: + preview = text[:80] + ("..." if len(text) > 80 else "") + self._marshal_notify("whisper.linux", preview) + + except Exception as e: + log.error("Stream segment processing failed: %s", e) + finally: + if os.path.exists(wav_path): + os.unlink(wav_path) + + def _marshal_set_state(self, state: AppState): + if self._tray: + self._tray._call_helper.call(lambda: self._set_state(state)) + else: + self._set_state(state) + + def _marshal_notify(self, title: str, message: str): + if not self._tray: + return + self._tray._call_helper.call(lambda: self._tray.notify(title, message)) + + def _reset_silence_timer(self): + self._cancel_silence_timer() + self._silence_timer = threading.Timer( + self.config.silence_timeout, + self._on_silence_timeout, + ) + self._silence_timer.daemon = True + self._silence_timer.start() + + def _cancel_silence_timer(self): + if self._silence_timer: + self._silence_timer.cancel() + self._silence_timer = None + + def _on_silence_timeout(self): + if self.state == AppState.DICTATING: + if self._vad and self._vad._in_speech: + log.debug("Silence timeout skipped (speech active), restarting") + self._reset_silence_timer() + return + log.info("Streaming: silence timeout \u2192 LISTENING") + self._flush_accumulated_text() + self.state = AppState.LISTENING + self._marshal_set_state(AppState.LISTENING) + self._marshal_notify("whisper.linux", "Dictation paused (silence)") + self._play_end_signal() + + def _play_sound(self, sounds: tuple): + """Play the first available sound file.""" + players = ("pw-play", "paplay", "canberra-gtk-play") + for sound in sounds: + if not os.path.isfile(sound): + continue + for player in players: + try: + subprocess.Popen( + [player, sound], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + log.debug("Sound played via %s: %s", player, sound) + return + except FileNotFoundError: + continue + log.debug("No sound player available") + + def _play_start_signal(self): + if not self.config.end_signal: + return + self._mute_until = time.time() + 0.6 + if self._vad: + self._vad.reset() + self._play_sound(( + "/usr/share/sounds/freedesktop/stereo/message-new-instant.oga", + "/usr/share/sounds/freedesktop/stereo/message.oga", + "/usr/share/sounds/freedesktop/stereo/bell.oga", + )) + + def _play_end_signal(self): + if not self.config.end_signal: + return + self._mute_until = time.time() + 0.6 + if self._vad: + self._vad.reset() + self._play_sound(( + "/usr/share/sounds/freedesktop/stereo/complete.oga", + "/usr/share/sounds/freedesktop/stereo/bell.oga", + "/usr/share/sounds/freedesktop/stereo/message.oga", + )) + + def _set_state(self, state: AppState): + self.state = state + if self._tray: + self._tray.set_state(state) + + def _save_active_window(self): + try: + result = subprocess.run( + ["xdotool", "getactivewindow"], + capture_output=True, text=True, timeout=2, + ) + if result.returncode == 0 and result.stdout.strip(): + self._prev_window_id = result.stdout.strip() + log.debug("Saved active window: %s", self._prev_window_id) + except Exception: + self._prev_window_id = None + + def _restore_active_window(self): + if not self._prev_window_id: + return + try: + subprocess.run( + ["xdotool", "windowactivate", "--sync", self._prev_window_id], + check=True, timeout=3, + ) + time.sleep(0.15) + log.debug("Restored focus to window: %s", self._prev_window_id) + except Exception as e: + log.debug("Could not restore window focus: %s", e) + + def _start_recording(self): + self._save_active_window() + try: + self.recorder.start() + self._set_state(AppState.RECORDING) + except Exception as e: + log.error("Failed to start recording: %s", e) + if self._tray: + self._tray.notify("Error", f"Recording failed: {e}") + + def _stop_recording(self): + wav_path = self.recorder.stop() + if not wav_path: + self._set_state(AppState.IDLE) + return + self._set_state(AppState.PROCESSING) + t = threading.Thread(target=self._transcribe_and_inject, args=(wav_path,), daemon=True) + t.start() + + def _transcribe_and_inject(self, wav_path: str): + try: + text = self.transcriber.transcribe(wav_path) + if text: + self._restore_active_window() + self._inject_text(text) + if self._tray and self.config.notification: + preview = text[:80] + ("..." if len(text) > 80 else "") + self._tray.notify("whisper.linux", preview) + else: + log.info("Empty transcription") + if self._tray: + self._tray.notify("whisper.linux", "(no speech detected)") + except Exception as e: + log.error("Transcription/injection failed: %s", e) + if self._tray: + self._tray.notify("Error", str(e)[:100]) + finally: + if os.path.exists(wav_path): + os.unlink(wav_path) + self._set_state(AppState.IDLE) + self._play_end_signal() + + def quit(self): + log.info("Shutting down") + self._cancel_silence_timer() + if self._audio_stream: + self._audio_stream.stop() + if self._segment_queue: + self._segment_queue.put(None) + if self._segment_worker: + self._segment_worker.join(timeout=3) + self.recorder.cleanup() + self.remove_pid() + if self._qt_app: + self._qt_app.quit() + + # -- Global hotkey -- + + def _start_hotkey_listener(self): + """Start a global keyboard hotkey listener using Xlib XRecord.""" + hotkey_str = self.config.hotkey + if not hotkey_str: + log.info("No hotkey configured, skipping listener") + return + + try: + from Xlib import X, XK, display + from Xlib.ext import record + from Xlib.protocol import rq + except ImportError: + log.warning("python-xlib not installed — global hotkey disabled") + return + + # Parse hotkey string into X11 keycodes + keysym_map = { + "ctrl": XK.XK_Control_L, "ctrl_l": XK.XK_Control_L, + "ctrl_r": XK.XK_Control_R, + "alt": XK.XK_Alt_L, "alt_l": XK.XK_Alt_L, "alt_r": XK.XK_Alt_R, + "shift": XK.XK_Shift_L, "shift_l": XK.XK_Shift_L, + "shift_r": XK.XK_Shift_R, + "super": XK.XK_Super_L, "super_l": XK.XK_Super_L, + "super_r": XK.XK_Super_R, + } + + d = display.Display() + combo_keycodes = set() + parts = [p.strip() for p in hotkey_str.split("+")] + for p in parts: + low = p.lower() + if low in keysym_map: + kc = d.keysym_to_keycode(keysym_map[low]) + combo_keycodes.add(kc) + elif len(p) == 1: + kc = d.keysym_to_keycode(XK.string_to_keysym(p)) + if kc: + combo_keycodes.add(kc) + else: + log.warning("Unknown hotkey part: %r", p) + else: + log.warning("Unknown hotkey part: %r", p) + + if not combo_keycodes: + log.warning("Could not parse hotkey: %s", hotkey_str) + d.close() + return + + log.debug("Hotkey keycodes: %s", combo_keycodes) + pressed = set() + hotkey_fired = [False] # prevent repeats while held + + def callback(reply): + if reply.category != record.FromServer: + return + if reply.client_swapped: + return + data = reply.data + while data: + event, data = rq.EventField(None).parse_binary_value( + data, record_display.display, None, None) + keycode = event.detail + if event.type == X.KeyPress: + pressed.add(keycode) + if combo_keycodes.issubset(pressed) and not hotkey_fired[0]: + hotkey_fired[0] = True + log.info("Hotkey pressed: %s", hotkey_str) + if self._tray: + self._tray._call_helper.call(self.toggle) + elif event.type == X.KeyRelease: + pressed.discard(keycode) + if not combo_keycodes.issubset(pressed): + hotkey_fired[0] = False + + record_display = display.Display() + ctx = record_display.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': (X.KeyPress, X.KeyRelease), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }] + ) + + def listener_thread(): + try: + record_display.record_enable_context(ctx, callback) + except Exception as e: + log.error("Hotkey listener error: %s", e) + + self._hotkey_record_display = record_display + self._hotkey_record_ctx = ctx + t = threading.Thread(target=listener_thread, daemon=True) + t.start() + d.close() + log.info("Global hotkey listener started (Xlib XRecord): %s", hotkey_str) + + # -- Main entry -- + + def run(self): + from PyQt5.QtWidgets import QApplication + + self._qt_app = QApplication(sys.argv) + self._qt_app.setQuitOnLastWindowClosed(False) + + self.write_pid() + self._setup_signals() + + from PyQt5.QtCore import QTimer + signal_timer = QTimer() + signal_timer.timeout.connect(lambda: None) + signal_timer.start(200) + + self._tray = TrayIcon(self) + self._set_state(AppState.IDLE) + self._start_hotkey_listener() + + log.info("whisper.linux started (pid %d)", os.getpid()) + log.info(" whisper-cli : %s", self.config.whisper_cli) + log.info(" model : %s", self.config.model) + log.info(" models_dir : %s", self.config.models_dir) + log.info(" language : %s", self.config.language) + log.info(" threads : %d", self.config.threads) + log.info(" gpu_device : %d", self.config.gpu_device) + log.info(" audio_device: %s", self.config.audio_device) + log.info(" display : %s", self.config.display_server) + log.info(" paste_keys : %s", self.config.paste_keys) + log.info(" clipboard : %s", self.config.use_clipboard_fallback) + log.info(" input_mode : %s", self.config.input_mode) + log.info(" output_mode : %s", self.config.output_mode) + log.info(" hotkey : %s", self.config.hotkey) + log.info(" wake_word : %s", self.config.wake_word) + log.info(" wake_model : %s", self.config.wake_model or "(same as main)") + log.info(" voice_cmds : %s", self.config.voice_commands) + + if self.config.input_mode == "listen": + from PyQt5.QtCore import QTimer as _QT + _QT.singleShot(0, self.toggle) + + try: + sys.exit(self._qt_app.exec_()) + finally: + self.remove_pid() + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def send_toggle(): + pid = WhisperLinuxApp.read_pid() + if pid is None: + print("whisper-linux is not running.", file=sys.stderr) + sys.exit(1) + os.kill(pid, signal.SIGUSR1) + log.info("Sent SIGUSR1 to pid %d", pid) + + +def main(): + parser = argparse.ArgumentParser( + description="whisper.linux \u2014 Voice typing for Linux desktop", + ) + parser.add_argument("--toggle", action="store_true", + help="Toggle recording on a running instance (sends SIGUSR1)") + parser.add_argument("--language", "-l", default=None, + help="Override transcription language (e.g. ru, en, auto)") + parser.add_argument("--model", "-m", default=None, + help="Override model path") + parser.add_argument("--input-mode", choices=["hotkey", "listen"], default=None, + help="Override input mode (hotkey or listen)") + parser.add_argument("--output-mode", choices=["batch", "stream"], default=None, + help="Override output mode (batch or stream)") + parser.add_argument("--stream", action="store_true", + help="Shortcut for --output-mode stream") + parser.add_argument("--wake-word", default=None, + help="Override wake word for listen mode") + parser.add_argument("--wake-model", default=None, + help="Override wake model (lighter model for wake word detection)") + parser.add_argument("--debug", action="store_true", + help="Enable debug logging") + args = parser.parse_args() + + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + if args.toggle: + send_toggle() + return + + existing_pid = WhisperLinuxApp.read_pid() + if existing_pid is not None: + print(f"whisper-linux is already running (pid {existing_pid}).", file=sys.stderr) + print("Use --toggle to start/stop recording, or kill the existing instance.", file=sys.stderr) + sys.exit(1) + + config = Config() + if args.language: + config.language = args.language + if args.model: + config.model = args.model + if args.stream: + config.output_mode = "stream" + if args.input_mode: + config.input_mode = args.input_mode + if args.output_mode: + config.output_mode = args.output_mode + if args.wake_word: + config.wake_word = args.wake_word + if args.wake_model: + config.wake_model = args.wake_model + + if not config.model or not os.path.isfile(config.model): + if config.model: + log.warning("Model not found: %s — searching for alternatives", config.model) + from .config import _find_model + fallback = _find_model(config.model_search_dirs) + if fallback: + log.info("Using model: %s", fallback) + config.model = fallback + config.save() + else: + print("Error: No model found. Run install.sh or set model path in config.", file=sys.stderr) + sys.exit(1) + + if config.wake_model and not os.path.isfile(config.wake_model): + log.warning("Wake model not found: %s — using main model", config.wake_model) + config.wake_model = "" + + app = WhisperLinuxApp(config) + app.run() + + +if __name__ == "__main__": + main() diff --git a/examples/whisper.linux/app/audio.py b/examples/whisper.linux/app/audio.py new file mode 100644 index 000000000..7e76df2d6 --- /dev/null +++ b/examples/whisper.linux/app/audio.py @@ -0,0 +1,288 @@ +"""Audio recording, streaming, and VAD for whisper.linux.""" + +import array +import math +import os +import signal +import struct +import subprocess +import tempfile +import threading +import time +from typing import Optional + +from .config import Config, log + + +# --------------------------------------------------------------------------- +# AudioRecorder +# --------------------------------------------------------------------------- + +class AudioRecorder: + """Records audio to a temporary WAV file.""" + + def __init__(self, config: Config): + self._config = config + self._proc: Optional[subprocess.Popen] = None + self._wav_path: Optional[str] = None + + @property + def wav_path(self) -> Optional[str]: + return self._wav_path + + @property + def is_recording(self) -> bool: + return self._proc is not None and self._proc.poll() is None + + def _build_record_cmd(self, path: str) -> list: + audio_device = self._config.audio_device + if audio_device.startswith("pw") or audio_device == "auto": + pw = self._find_pw_record() + if pw: + return [pw, "--format", "s16", "--rate", "16000", "--channels", "1", path] + if audio_device and audio_device not in ("auto", "default"): + return ["arecord", "-D", audio_device, "-f", "S16_LE", "-r", "16000", + "-c", "1", "-t", "wav", path] + pw = self._find_pw_record() + if pw: + return [pw, "--format", "s16", "--rate", "16000", "--channels", "1", path] + return ["arecord", "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "wav", path] + + @staticmethod + def _find_pw_record(): + for p in ("/usr/bin/pw-record", "/usr/local/bin/pw-record"): + if os.path.isfile(p) and os.access(p, os.X_OK): + return p + return None + + def start(self) -> str: + if self.is_recording: + raise RuntimeError("Already recording") + fd, path = tempfile.mkstemp(suffix=".wav", prefix="whisper-linux-") + os.close(fd) + self._wav_path = path + cmd = self._build_record_cmd(path) + self._proc = subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + time.sleep(0.2) + if self._proc.poll() is not None: + stderr = self._proc.stderr.read().decode(errors="replace").strip() + self._proc = None + os.unlink(path) + self._wav_path = None + raise RuntimeError(f"Recording failed to start ({cmd[0]}): {stderr}") + log.info("Recording started → %s (pid %d, cmd=%s)", path, self._proc.pid, cmd[0]) + return path + + def stop(self) -> Optional[str]: + if self._proc is None: + return None + if self._proc.poll() is None: + self._proc.send_signal(signal.SIGINT) + try: + self._proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self._proc.kill() + self._proc.wait() + log.info("Recording stopped → %s", self._wav_path) + self._proc = None + return self._wav_path + + def cleanup(self): + self.stop() + if self._wav_path and os.path.exists(self._wav_path): + os.unlink(self._wav_path) + self._wav_path = None + + +# --------------------------------------------------------------------------- +# _write_wav +# --------------------------------------------------------------------------- + +def _write_wav(path: str, pcm_data: bytes, sample_rate: int = 16000, channels: int = 1, bits: int = 16): + """Write raw PCM s16le data to a WAV file with RIFF header.""" + data_size = len(pcm_data) + byte_rate = sample_rate * channels * bits // 8 + block_align = channels * bits // 8 + with open(path, "wb") as f: + f.write(b"RIFF") + f.write(struct.pack(" float: + if len(frame_bytes) < 2: + return 0.0 + samples = array.array("h") + samples.frombytes(frame_bytes[:len(frame_bytes) - len(frame_bytes) % 2]) + if not samples: + return 0.0 + return math.sqrt(sum(s * s for s in samples) / len(samples)) + + def feed(self, chunk: bytes): + self._buffer.extend(chunk) + while len(self._buffer) >= self._frame_bytes: + frame = bytes(self._buffer[:self._frame_bytes]) + del self._buffer[:self._frame_bytes] + rms = self._rms(frame) + + if rms >= self._threshold: + if not self._in_speech: + self._in_speech = True + self._silent_count = 0 + log.debug("VAD: speech start (RMS=%.0f)", rms) + if self.on_speech_start: + self.on_speech_start() + self._speech_buffer.extend(frame) + self._silent_count = 0 + elif self._in_speech: + self._speech_buffer.extend(frame) + self._silent_count += 1 + if self._silent_count >= self._silence_frames: + self._emit_speech() + + max_bytes = int(self._max_speech_s * self._sample_rate * 2) + if self._in_speech and len(self._speech_buffer) >= max_bytes: + self._emit_speech() + + def _emit_speech(self): + pcm = bytes(self._speech_buffer) + duration_ms = len(pcm) * 1000 // (self._sample_rate * 2) + self._speech_buffer.clear() + self._in_speech = False + self._silent_count = 0 + if duration_ms >= self._min_speech_ms: + log.debug("VAD: speech end (%dms)", duration_ms) + if self.on_speech_end: + self.on_speech_end(pcm) + else: + log.debug("VAD: speech too short (%dms), discarded", duration_ms) + + +# --------------------------------------------------------------------------- +# AudioStream +# --------------------------------------------------------------------------- + +class AudioStream: + """Streams raw PCM from pw-record to a callback in a reader thread.""" + + SAMPLE_RATE = 16000 + BYTES_PER_SEC = 32000 + CHUNK_MS = 100 + CHUNK_BYTES = BYTES_PER_SEC * CHUNK_MS // 1000 + + def __init__(self, config: Config): + self._config = config + self._proc: Optional[subprocess.Popen] = None + self._thread: Optional[threading.Thread] = None + self._on_data = None + self._running = False + + @property + def is_running(self) -> bool: + return self._running and self._proc is not None and self._proc.poll() is None + + def _build_stream_cmd(self) -> list: + audio_device = self._config.audio_device + pw = AudioRecorder._find_pw_record() + if pw: + return [pw, "--format", "s16", "--rate", "16000", "--channels", "1", "-"] + cmd = ["arecord", "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw"] + if audio_device and audio_device not in ("auto", "default"): + cmd.extend(["-D", audio_device]) + return cmd + + def start(self, on_data): + if self._running: + raise RuntimeError("AudioStream already running") + self._on_data = on_data + self._running = True + cmd = self._build_stream_cmd() + self._proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + time.sleep(0.2) + if self._proc.poll() is not None: + stderr = self._proc.stderr.read().decode(errors="replace").strip() + self._running = False + self._proc = None + raise RuntimeError(f"AudioStream failed to start ({cmd[0]}): {stderr}") + self._thread = threading.Thread(target=self._reader_loop, daemon=True) + self._thread.start() + log.info("AudioStream started (pid %d, cmd=%s)", self._proc.pid, cmd[0]) + + def stop(self): + self._running = False + if self._proc and self._proc.poll() is None: + self._proc.send_signal(signal.SIGINT) + try: + self._proc.wait(timeout=3) + except subprocess.TimeoutExpired: + self._proc.kill() + self._proc.wait() + if self._thread: + self._thread.join(timeout=3) + self._thread = None + self._proc = None + log.info("AudioStream stopped") + + def _reader_loop(self): + try: + while self._running and self._proc and self._proc.poll() is None: + data = self._proc.stdout.read(self.CHUNK_BYTES) + if not data: + break + if self._on_data and self._running: + self._on_data(data) + except Exception as e: + log.error("AudioStream reader error: %s", e) + finally: + self._running = False diff --git a/examples/whisper.linux/app/commands.py b/examples/whisper.linux/app/commands.py new file mode 100644 index 000000000..1ab38cc10 --- /dev/null +++ b/examples/whisper.linux/app/commands.py @@ -0,0 +1,81 @@ +"""Voice command processing for whisper.linux. + +Detects command words in transcribed text (e.g. "Enter", "Backspace") +and executes corresponding key presses instead of typing them literally. +""" + +import difflib +from typing import Callable, Optional + +from .config import DEFAULT_VOICE_COMMANDS, log + + +class VoiceCommands: + """Processes transcribed text for voice commands (Enter, Backspace, etc.).""" + + DEFAULT_COMMANDS = DEFAULT_VOICE_COMMANDS + FUZZY_THRESHOLD = 0.75 + + def __init__(self, commands: dict = None): + self._commands = commands if commands is not None else dict(self.DEFAULT_COMMANDS) + + def process(self, text: str, inject_fn: Callable, send_key_fn: Callable) -> bool: + """Process text, calling inject_fn for text and send_key_fn for key presses. + + For "backspace", removes the previous word from the buffer. If the buffer + is empty, sends Ctrl+BackSpace to delete the word in the editor. + + Returns True if any commands were found. + """ + words = text.split() + if not words: + return False + + buffer = [] + had_commands = False + + for word in words: + clean = word.strip(".,!?;:-\"'()[]").lower() + action = self._match_command(clean) + if action: + had_commands = True + if action == "backspace": + if buffer: + removed = buffer.pop() + log.info("Voice command: backspace (removed '%s' from buffer)", removed) + else: + send_key_fn("ctrl+BackSpace") + log.info("Voice command: backspace (Ctrl+BackSpace sent)") + else: + # Flush text buffer first, then send key + if buffer: + inject_fn(" ".join(buffer)) + buffer.clear() + key = action[4:] # strip "key:" prefix + send_key_fn(key) + log.info("Voice command: %s -> %s", clean, key) + else: + buffer.append(word) + + # Flush remaining text + if buffer: + inject_fn(" ".join(buffer)) + + return had_commands + + def _match_command(self, word: str) -> Optional[str]: + """Match a word to a command (exact, then fuzzy).""" + if not word: + return None + # Exact match + if word in self._commands: + return self._commands[word] + # Fuzzy match — pick best above threshold + best_ratio = 0.0 + best_action = None + for cmd_word, action in self._commands.items(): + ratio = difflib.SequenceMatcher(None, cmd_word, word).ratio() + if ratio >= self.FUZZY_THRESHOLD and ratio > best_ratio: + best_ratio = ratio + best_action = action + return best_action diff --git a/examples/whisper.linux/app/config.py b/examples/whisper.linux/app/config.py new file mode 100644 index 000000000..9b9e5af7d --- /dev/null +++ b/examples/whisper.linux/app/config.py @@ -0,0 +1,364 @@ +"""Configuration, constants, and helpers for whisper.linux.""" + +import configparser +import enum +import logging +import os +import re +import subprocess +from dataclasses import dataclass +from pathlib import Path + +LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" +logging.basicConfig(level=logging.INFO, format=LOG_FORMAT) +log = logging.getLogger("whisper-linux") + +# --------------------------------------------------------------------------- +# AppState +# --------------------------------------------------------------------------- + +class AppState(enum.Enum): + IDLE = "idle" + RECORDING = "recording" + PROCESSING = "processing" + LISTENING = "listening" # stream: waiting for wake word + DICTATING = "dictating" # stream: actively typing text + + +# --------------------------------------------------------------------------- +# Paths and constants +# --------------------------------------------------------------------------- + +_REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent + +CONFIG_DIR = Path.home() / ".config" / "whisper-linux" +CONFIG_FILE = CONFIG_DIR / "config.ini" +PID_FILE = Path("/tmp") / "whisper-linux.pid" + +_WHISPER_CLI_NAMES = ["whisper-cli", "main"] +_WHISPER_SEARCH_DIRS = [ + _REPO_ROOT / "build" / "bin", + _REPO_ROOT / "build", + Path.home() / ".local" / "bin", + Path("/usr/local/bin"), + Path("/usr/bin"), +] + +_MODEL_SEARCH_DIRS = [ + _REPO_ROOT / "models", + Path.home() / ".local" / "share" / "whisper-linux" / "models", +] + +DEFAULT_VOICE_COMMANDS = { + "enter": "key:Return", + "энтер": "key:Return", + "ввод": "key:Return", + "backspace": "backspace", + "бэкспейс": "backspace", + "бекспейс": "backspace", + "назад": "backspace", + "tab": "key:Tab", + "таб": "key:Tab", + "табуляция": "key:Tab", + "escape": "key:Escape", + "эскейп": "key:Escape", + "стоп": "key:Escape", +} + +AVAILABLE_MODELS = [ + ("tiny", "~75 MB"), + ("base", "~142 MB"), + ("small", "~466 MB"), + ("medium", "~1.5 GB"), + ("medium-q5_0", "~539 MB"), + ("large-v2", "~3.1 GB"), + ("large-v3", "~3.1 GB"), + ("large-v3-q5_0", "~1.1 GB"), + ("large-v3-turbo", "~1.5 GB"), + ("large-v3-turbo-q5_0", "~574 MB"), + ("large-v3-turbo-q8_0", "~834 MB"), +] + + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +def _find_executable(names, search_dirs): + """Find first matching executable in search dirs, then PATH.""" + for d in search_dirs: + for name in names: + p = d / name + if p.is_file() and os.access(p, os.X_OK): + return str(p) + for name in names: + for pdir in os.environ.get("PATH", "").split(os.pathsep): + p = Path(pdir) / name + if p.is_file() and os.access(p, os.X_OK): + return str(p) + return None + + +def _find_model(search_dirs, preferred="ggml-base.bin"): + """Find first .bin model file.""" + for d in search_dirs: + p = d / preferred + if p.is_file(): + return str(p) + for d in search_dirs: + if d.is_dir(): + for f in sorted(d.glob("ggml-*.bin")): + return str(f) + return None + + +def _list_models(search_dirs): + """Return list of (name, path) for all available ggml models, sorted by size.""" + seen = set() + models = [] + for d in search_dirs: + if d.is_dir(): + for f in sorted(d.glob("ggml-*.bin")): + if f.name not in seen: + seen.add(f.name) + name = f.stem.replace("ggml-", "") + models.append((name, str(f))) + models.sort(key=lambda x: Path(x[1]).stat().st_size) + return models + + +def _list_audio_devices(): + """Return list of (label, device_id) for audio input devices.""" + devices = [] + if os.path.isfile("/usr/bin/pw-record"): + devices.append(("Auto (PipeWire)", "auto")) + else: + devices.append(("Auto", "auto")) + try: + result = subprocess.run( + ["arecord", "-l"], capture_output=True, text=True, timeout=5, + ) + for line in result.stdout.splitlines(): + m = re.match(r"card\s+(\d+):\s*(\w+).*device\s+(\d+):\s*(.*?)(?:\[|$)", line) + if m: + card, card_name, dev, dev_name = m.groups() + device_id = f"plughw:{card},{dev}" + label = dev_name.strip() if dev_name.strip() else device_id + devices.append((label, device_id)) + except Exception: + pass + return devices + + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- + +@dataclass +class Config: + whisper_cli: str = "" + model: str = "" + models_dir: str = "" + language: str = "ru" + threads: int = 4 + gpu_device: int = -1 + display_server: str = "" + audio_device: str = "" + paste_keys: str = "shift+Insert" + use_clipboard_fallback: bool = False + notification: bool = True + input_mode: str = "hotkey" + output_mode: str = "batch" + wake_word: str = "марфуша" + wake_model: str = "" + silence_timeout: float = 3.0 + vad_threshold: int = 300 + min_speech_ms: int = 500 + max_speech_s: float = 30.0 + end_signal: bool = True + hotkey: str = "ctrl+Super_L" + voice_commands: bool = True + + def __post_init__(self): + self._gpu_devices = [] + self.voice_commands_map: dict = dict(DEFAULT_VOICE_COMMANDS) + self._load() + + def _load(self): + """Load from config.ini, auto-detect missing values.""" + cp = configparser.ConfigParser() + if CONFIG_FILE.exists(): + cp.read(CONFIG_FILE) + + sec = "whisper-linux" + if cp.has_section(sec): + self.whisper_cli = cp.get(sec, "whisper_cli", fallback=self.whisper_cli) + self.model = cp.get(sec, "model", fallback=self.model) + self.models_dir = cp.get(sec, "models_dir", fallback=self.models_dir) + self.language = cp.get(sec, "language", fallback=self.language) + self.threads = cp.getint(sec, "threads", fallback=self.threads) + self.gpu_device = cp.getint(sec, "gpu_device", fallback=self.gpu_device) + self.display_server = cp.get(sec, "display_server", fallback=self.display_server) + self.audio_device = cp.get(sec, "audio_device", fallback=self.audio_device) + self.paste_keys = cp.get(sec, "paste_keys", fallback=self.paste_keys) + self.use_clipboard_fallback = cp.getboolean( + sec, "use_clipboard_fallback", fallback=self.use_clipboard_fallback + ) + self.notification = cp.getboolean(sec, "notification", fallback=self.notification) + old_mode = cp.get(sec, "mode", fallback=None) + if old_mode and not cp.has_option(sec, "input_mode"): + if old_mode == "stream": + self.input_mode = "listen" + self.output_mode = "stream" + else: + self.input_mode = "hotkey" + self.output_mode = "batch" + self.input_mode = cp.get(sec, "input_mode", fallback=self.input_mode) + self.output_mode = cp.get(sec, "output_mode", fallback=self.output_mode) + self.wake_word = cp.get(sec, "wake_word", fallback=self.wake_word) + self.wake_model = cp.get(sec, "wake_model", fallback=self.wake_model) + self.silence_timeout = cp.getfloat(sec, "silence_timeout", fallback=self.silence_timeout) + self.vad_threshold = cp.getint(sec, "vad_threshold", fallback=self.vad_threshold) + self.min_speech_ms = cp.getint(sec, "min_speech_ms", fallback=self.min_speech_ms) + self.max_speech_s = cp.getfloat(sec, "max_speech_s", fallback=self.max_speech_s) + self.end_signal = cp.getboolean(sec, "end_signal", fallback=self.end_signal) + self.hotkey = cp.get(sec, "hotkey", fallback=self.hotkey) + self.voice_commands = cp.getboolean(sec, "voice_commands", fallback=self.voice_commands) + + vc_sec = "voice-commands" + if cp.has_section(vc_sec): + saved = dict(cp.items(vc_sec)) + # Merge: keep saved commands, add new defaults that aren't overridden + merged = dict(DEFAULT_VOICE_COMMANDS) + merged.update(saved) + self.voice_commands_map = merged + + if not self.whisper_cli: + self.whisper_cli = _find_executable(_WHISPER_CLI_NAMES, _WHISPER_SEARCH_DIRS) or "whisper-cli" + if not self.models_dir: + self.models_dir = self._detect_models_dir() + if not self.model: + self.model = _find_model(self.model_search_dirs) or "" + if not self.display_server: + self.display_server = self._detect_display_server() + if not self.audio_device: + self.audio_device = self._detect_audio_device() + detected_gpu = self._detect_gpu_device() + if self.gpu_device < 0: + self.gpu_device = detected_gpu + + @staticmethod + def _detect_display_server(): + xdg = os.environ.get("XDG_SESSION_TYPE", "").lower() + if "wayland" in xdg: + return "wayland" + if "x11" in xdg: + return "x11" + if os.environ.get("WAYLAND_DISPLAY"): + return "wayland" + if os.environ.get("DISPLAY"): + return "x11" + return "x11" + + @staticmethod + def _detect_audio_device(): + if os.path.isfile("/usr/bin/pw-record"): + log.info("PipeWire detected (pw-record available), using auto audio device") + return "auto" + try: + result = subprocess.run( + ["arecord", "-l"], capture_output=True, text=True, timeout=5, + ) + for line in result.stdout.splitlines(): + m = re.match(r"card\s+(\d+):.*device\s+(\d+):", line) + if m: + card, dev = m.group(1), m.group(2) + device = f"plughw:{card},{dev}" + log.info("Auto-detected ALSA audio device: %s (%s)", + device, line.strip()) + return device + except Exception as e: + log.warning("Failed to detect audio device: %s", e) + return "default" + + @staticmethod + def _detect_models_dir(): + for d in _MODEL_SEARCH_DIRS: + if d.is_dir() and any(d.glob("ggml-*.bin")): + return str(d) + return str(_MODEL_SEARCH_DIRS[0]) + + @property + def model_search_dirs(self): + dirs = [] + if self.models_dir: + dirs.append(Path(self.models_dir)) + for d in _MODEL_SEARCH_DIRS: + if d not in dirs: + dirs.append(d) + return dirs + + def _detect_gpu_device(self): + cli = self.whisper_cli or _find_executable(_WHISPER_CLI_NAMES, _WHISPER_SEARCH_DIRS) + model = self.model or _find_model(self.model_search_dirs) + if not cli or not model: + return 0 + try: + result = subprocess.run( + [cli, "-m", model, "-f", "/dev/null"], + capture_output=True, text=True, timeout=10, + ) + devices = {} + for line in result.stderr.splitlines(): + m = re.match(r"ggml_vulkan:\s+(\d+)\s*=\s*(.+?)(?:\s*\|.*)?$", line) + if m: + idx, name = int(m.group(1)), m.group(2).strip() + devices[idx] = name + log.info("GPU device %d: %s", idx, name) + self._gpu_devices = [(name, idx) for idx, name in sorted(devices.items())] + for idx, name in devices.items(): + if "NVIDIA" in name.upper(): + log.info("Auto-selected NVIDIA GPU device: %d", idx) + return idx + for idx, name in devices.items(): + if "AMD" in name.upper() or "RADEON" in name.upper(): + log.info("Auto-selected AMD GPU device: %d", idx) + return idx + except Exception as e: + log.debug("GPU device detection failed: %s", e) + return 0 + + def save(self): + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + cp = configparser.ConfigParser() + sec = "whisper-linux" + cp.add_section(sec) + cp.set(sec, "whisper_cli", self.whisper_cli) + cp.set(sec, "model", self.model) + cp.set(sec, "models_dir", self.models_dir) + cp.set(sec, "language", self.language) + cp.set(sec, "threads", str(self.threads)) + cp.set(sec, "gpu_device", str(self.gpu_device)) + cp.set(sec, "display_server", self.display_server) + cp.set(sec, "audio_device", self.audio_device) + cp.set(sec, "paste_keys", self.paste_keys) + cp.set(sec, "use_clipboard_fallback", str(self.use_clipboard_fallback)) + cp.set(sec, "notification", str(self.notification)) + cp.set(sec, "input_mode", self.input_mode) + cp.set(sec, "output_mode", self.output_mode) + cp.set(sec, "wake_word", self.wake_word) + cp.set(sec, "wake_model", self.wake_model) + cp.set(sec, "silence_timeout", str(self.silence_timeout)) + cp.set(sec, "vad_threshold", str(self.vad_threshold)) + cp.set(sec, "min_speech_ms", str(self.min_speech_ms)) + cp.set(sec, "max_speech_s", str(self.max_speech_s)) + cp.set(sec, "end_signal", str(self.end_signal)) + cp.set(sec, "hotkey", self.hotkey) + cp.set(sec, "voice_commands", str(self.voice_commands)) + vc_sec = "voice-commands" + cp.add_section(vc_sec) + for word, action in self.voice_commands_map.items(): + cp.set(vc_sec, word, action) + with open(CONFIG_FILE, "w") as f: + cp.write(f) + log.info("Config saved to %s", CONFIG_FILE) diff --git a/examples/whisper.linux/app/injector.py b/examples/whisper.linux/app/injector.py new file mode 100644 index 000000000..dc0f77791 --- /dev/null +++ b/examples/whisper.linux/app/injector.py @@ -0,0 +1,165 @@ +"""Text injection (xdotool/ydotool/clipboard) for whisper.linux.""" + +import os +import subprocess +import time + +from .config import Config, log + + +class TextInjector: + """Injects text at cursor position using xdotool, ydotool, or clipboard.""" + + # X11 keysym → evdev name (for ydotool which uses evdev names) + _YDOTOOL_KEY_MAP = { + "Return": "Enter", + "Escape": "Esc", + } + + def __init__(self, config: Config): + self.config = config + + def inject(self, text: str): + if not text: + return + ds = self.config.display_server + if ds == "wayland": + self._inject_wayland(text) + else: + self._inject_x11(text) + + def _inject_x11(self, text: str): + if self.config.use_clipboard_fallback: + self._inject_clipboard_x11(text) + return + try: + subprocess.run( + ["xdotool", "type", "--delay", "12", "--clearmodifiers", "--", text], + check=True, timeout=10, + ) + except (subprocess.CalledProcessError, FileNotFoundError): + log.warning("xdotool type failed, falling back to clipboard") + self._inject_clipboard_x11(text) + + def _inject_clipboard_x11(self, text: str): + encoded = text.encode("utf-8") + # Set both CLIPBOARD and PRIMARY so shift+Insert works everywhere + subprocess.run( + ["xclip", "-selection", "clipboard"], + input=encoded, check=True, timeout=5, + ) + subprocess.run( + ["xclip", "-selection", "primary"], + input=encoded, check=True, timeout=5, + ) + time.sleep(0.05) + paste_keys = self.config.paste_keys + subprocess.run( + ["xdotool", "key", "--clearmodifiers", paste_keys], + check=True, timeout=5, + ) + + def _inject_wayland(self, text: str): + try: + subprocess.run(["wtype", "--", text], check=True, timeout=10) + return + except (subprocess.CalledProcessError, FileNotFoundError): + pass + + try: + subprocess.run( + ["wl-copy", "--", text], check=True, timeout=5, + ) + subprocess.run( + ["wl-copy", "--primary", "--", text], check=True, timeout=5, + ) + except (subprocess.CalledProcessError, FileNotFoundError): + log.warning("wl-copy failed, falling back to xclip (XWayland only)") + self._inject_x11(text) + return + + paste_keys = self.config.paste_keys + log.info("Clipboard set via wl-copy, sending %s", paste_keys) + time.sleep(0.3) + + try: + subprocess.run( + ["ydotool", "key", "--delay", "100", paste_keys], + check=True, timeout=5, capture_output=True, + ) + log.info("Paste sent via ydotool (%s)", paste_keys) + return + except (subprocess.CalledProcessError, FileNotFoundError, RuntimeError) as e: + log.debug("ydotool failed: %s", e) + + try: + subprocess.run( + ["xdotool", "key", "--clearmodifiers", paste_keys], + check=True, timeout=5, + ) + log.info("Paste sent via xdotool (%s)", paste_keys) + return + except (subprocess.CalledProcessError, FileNotFoundError): + pass + + log.warning("Text copied to clipboard (Ctrl+V to paste). " + "For auto-paste: sudo chmod 0660 /dev/uinput && " + "sudo chown root:$USER /dev/uinput") + + def send_key(self, key: str): + """Send a key press (e.g. 'Return', 'Tab', 'ctrl+BackSpace').""" + ds = self.config.display_server + if ds == "wayland": + self._send_key_wayland(key) + else: + self._send_key_x11(key) + + def _send_key_x11(self, key: str): + try: + subprocess.run( + ["xdotool", "key", "--clearmodifiers", key], + check=True, timeout=5, + ) + except (subprocess.CalledProcessError, FileNotFoundError) as e: + log.warning("xdotool key '%s' failed: %s", key, e) + + def _send_key_wayland(self, key: str): + # wtype supports XKB key names natively on Wayland + try: + if "+" in key: + parts = key.split("+") + cmd = ["wtype"] + for mod in parts[:-1]: + cmd.extend(["-M", mod.lower()]) + cmd.extend(["-k", parts[-1]]) + else: + cmd = ["wtype", "-k", key] + subprocess.run(cmd, check=True, timeout=5) + return + except (subprocess.CalledProcessError, FileNotFoundError): + pass + # ydotool uses evdev key names (Enter, not Return; Esc, not Escape) + try: + if "+" in key: + parts = key.split("+") + evdev_parts = [self._YDOTOOL_KEY_MAP.get(p, p) for p in parts] + evdev_key = "+".join(evdev_parts) + else: + evdev_key = self._YDOTOOL_KEY_MAP.get(key, key) + subprocess.run( + ["ydotool", "key", "--delay", "50", evdev_key], + check=True, timeout=5, capture_output=True, + ) + return + except (subprocess.CalledProcessError, FileNotFoundError): + pass + # xdotool via XWayland + try: + subprocess.run( + ["xdotool", "key", "--clearmodifiers", key], + check=True, timeout=5, + ) + return + except (subprocess.CalledProcessError, FileNotFoundError): + pass + log.warning("Failed to send key '%s' — no working key sender", key) diff --git a/examples/whisper.linux/app/transcriber.py b/examples/whisper.linux/app/transcriber.py new file mode 100644 index 000000000..c211bcafe --- /dev/null +++ b/examples/whisper.linux/app/transcriber.py @@ -0,0 +1,292 @@ +"""Transcriber and WakeWordDetector for whisper.linux.""" + +import difflib +import os +import re +import subprocess + +from .config import Config, log + + +# --------------------------------------------------------------------------- +# Hallucination filter — whisper generates these from training data on silence +# --------------------------------------------------------------------------- + +# Known hallucination phrases (training data leaks) +_HALLUCINATION_PATTERNS = [ + r"субтитр", + r"редактор\s+субтитр", + r"перевод\w*\s+субтитр", + r"подредактир", + r"продолжение\s+следует", + r"спасибо\s+за\s+(просмотр|подписк|внимание)", + r"подписывайтесь", + r"не\s+забудьте\s+подписаться", + r"ставьте\s+лайк", + r"с\s+вами\s+был[аио]?\s", + r"а[пп]+ортизатор", + r"добро\s+пожаловать", + r"до\s+новых\s+встреч", + r"до\s+свидания.*друзья", + r"subtitles?\s+(by|made|edited|created)", + r"thanks?\s+for\s+watching", + r"subscribe\s+(to|and)", + r"please\s+(like|subscribe)", + r"don'?t\s+forget\s+to\s+subscribe", + r"copyright\s+\d{4}", +] + +_HALLUCINATION_RE = re.compile( + "|".join(_HALLUCINATION_PATTERNS), re.IGNORECASE, +) + +# Speech rate limits — normal speech is ~2-3 words/sec, ~12-15 chars/sec. +# Hallucinations on short audio produce way more text than physically possible. +_MAX_WORDS_PER_SEC = 5 # generous upper bound +_MAX_CHARS_PER_SEC = 25 # generous upper bound + + +def _is_hallucination(text: str, duration_s: float = 0) -> bool: + """Return True if text looks like a whisper hallucination. + + Uses two layers: + 1. Pattern matching — known hallucination phrases. + 2. Speech rate check — if audio duration is known, rejects text that + is impossibly long for the given duration (e.g. 5 words from 0.5s). + """ + # Layer 1: known patterns + if _HALLUCINATION_RE.search(text): + return True + + # Layer 2: speech rate sanity check (only when duration is known) + if duration_s > 0: + words = text.split() + word_count = len(words) + char_count = len(text.replace(" ", "")) + + max_words = max(2, duration_s * _MAX_WORDS_PER_SEC) + max_chars = max(10, duration_s * _MAX_CHARS_PER_SEC) + + if word_count > max_words: + log.debug("Hallucination (words): %d words in %.1fs (max %.0f)", + word_count, duration_s, max_words) + return True + if char_count > max_chars: + log.debug("Hallucination (chars): %d chars in %.1fs (max %.0f)", + char_count, duration_s, max_chars) + return True + + return False + + +# --------------------------------------------------------------------------- +# WakeWordDetector +# --------------------------------------------------------------------------- + +class WakeWordDetector: + """Checks transcription text for the wake word using exact + fuzzy matching.""" + + FUZZY_THRESHOLD = 0.7 + + def __init__(self, wake_word: str): + self._wake_word = wake_word.lower().strip() + self._variants = self._build_variants(self._wake_word) + + @staticmethod + def _build_variants(word): + variants = {word} + for suffix in ("а", "я", "ша", "жа"): + if word.endswith(suffix) and len(word) > len(suffix) + 2: + variants.add(word[:-len(suffix)]) + return variants + + def contains_wake_word(self, text: str) -> bool: + text_lower = text.lower().strip() + if self._wake_word in text_lower: + return True + for word in text_lower.split(): + word = word.strip(".,!?;:-\"'()[]") + if word and self._is_fuzzy_match(word): + return True + return False + + def strip_wake_word(self, text: str) -> str: + import re + pattern = re.compile(re.escape(self._wake_word), re.IGNORECASE) + if pattern.search(text): + result = pattern.sub("", text) + result = re.sub(r"\s+", " ", result).strip() + return result.strip(".,!?;:- ") + words = text.split() + kept = [] + for w in words: + clean = w.strip(".,!?;:-\"'()[]").lower() + if clean and self._is_fuzzy_match(clean): + continue + kept.append(w) + result = " ".join(kept).strip() + return result.strip(".,!?;:- ") + + def _is_fuzzy_match(self, word: str) -> bool: + ratio = difflib.SequenceMatcher(None, self._wake_word, word).ratio() + if ratio >= self.FUZZY_THRESHOLD: + return True + for variant in self._variants: + ratio = difflib.SequenceMatcher(None, variant, word).ratio() + if ratio >= self.FUZZY_THRESHOLD: + return True + return False + + +# --------------------------------------------------------------------------- +# Transcriber +# --------------------------------------------------------------------------- + +PARAKEET_PREFIX = "parakeet:" +PARAKEET_VENV_PYTHON = ( + "/mnt/82A23910A2390A65/Trade/EducationAndHack/VOICE/whisper.cpp/" + "examples/whisper.youtube/.venv-parakeet/bin/python" +) + + +class Transcriber: + """Runs whisper-cli or Parakeet (NeMo) and returns transcribed text.""" + + def __init__(self, config: Config): + self.config = config + self._parakeet_proc = None + self._parakeet_model = None + + def transcribe(self, wav_path: str, model: str = None, + duration_s: float = 0) -> str: + if not os.path.isfile(wav_path): + raise FileNotFoundError(f"WAV file not found: {wav_path}") + + target = model or self.config.model + if target.startswith(PARAKEET_PREFIX): + return self._transcribe_parakeet(wav_path, target, duration_s) + return self._transcribe_whisper(wav_path, target, duration_s) + + def _transcribe_whisper(self, wav_path, model, duration_s): + cmd = [ + self.config.whisper_cli, + "-m", model, + "-f", wav_path, + "-nt", + "-np", + "-t", str(self.config.threads), + "-l", self.config.language, + "-dev", str(self.config.gpu_device), + ] + log.info("Transcribing whisper (%.1fs): %s", duration_s, " ".join(cmd)) + + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=300, + ) + + if result.returncode != 0: + log.error("whisper-cli stderr: %s", result.stderr) + raise RuntimeError(f"whisper-cli failed (rc={result.returncode}): {result.stderr[:200]}") + + text = result.stdout.strip() + text = text.replace("[BLANK_AUDIO]", "").strip() + if text and _is_hallucination(text, duration_s): + log.info("Hallucination filtered (%.1fs): %r", duration_s, text[:100]) + return "" + log.info("Transcription: %r", text[:100]) + return text + + _PARAKEET_WORKER_SCRIPT = ( + "import sys, json, os\n" + "os.environ.setdefault('HF_HUB_DISABLE_TELEMETRY', '1')\n" + "import nemo.collections.asr as nemo_asr\n" + "import torch\n" + "model_name = sys.argv[1]\n" + "asr = nemo_asr.models.ASRModel.from_pretrained(model_name)\n" + "if torch.cuda.is_available():\n" + " asr = asr.cuda()\n" + "asr.eval()\n" + "sys.stdout.write('__PARAKEET_READY__\\n')\n" + "sys.stdout.flush()\n" + "for line in sys.stdin:\n" + " wav = line.strip()\n" + " if not wav: continue\n" + " try:\n" + " out = asr.transcribe([wav], timestamps=False, verbose=False)\n" + " text = out[0].text or ''\n" + " sys.stdout.write('__PARAKEET_RESULT__' + json.dumps({'text': text}) + '\\n')\n" + " except Exception as e:\n" + " sys.stdout.write('__PARAKEET_RESULT__' + json.dumps({'error': str(e)}) + '\\n')\n" + " sys.stdout.flush()\n" + ) + + def _ensure_parakeet_worker(self, model_name): + """Spawn persistent NeMo worker if not running or model changed.""" + if (self._parakeet_proc is not None + and self._parakeet_proc.poll() is None + and self._parakeet_model == model_name): + return + + if self._parakeet_proc is not None and self._parakeet_proc.poll() is None: + log.info("Stopping parakeet worker (model change)") + self._parakeet_proc.terminate() + try: + self._parakeet_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self._parakeet_proc.kill() + + log.info("Starting parakeet worker for model: %s", model_name) + self._parakeet_proc = subprocess.Popen( + [PARAKEET_VENV_PYTHON, "-c", self._PARAKEET_WORKER_SCRIPT, model_name], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, bufsize=1, + ) + self._parakeet_model = model_name + + # wait for ready signal + while True: + line = self._parakeet_proc.stdout.readline() + if not line: + err = self._parakeet_proc.stderr.read() + raise RuntimeError(f"parakeet worker died at startup: {err[-500:]}") + if line.strip() == "__PARAKEET_READY__": + log.info("Parakeet worker ready") + return + + def _transcribe_parakeet(self, wav_path, model, duration_s): + model_name = model[len(PARAKEET_PREFIX):] + log.info("Transcribing parakeet (%.1fs): model=%s file=%s", + duration_s, model_name, wav_path) + + self._ensure_parakeet_worker(model_name) + self._parakeet_proc.stdin.write(wav_path + "\n") + self._parakeet_proc.stdin.flush() + + line = self._parakeet_proc.stdout.readline() + if not line: + err = self._parakeet_proc.stderr.read() + raise RuntimeError(f"parakeet worker died: {err[-500:]}") + + import json + if not line.startswith("__PARAKEET_RESULT__"): + raise RuntimeError(f"unexpected parakeet output: {line[:200]}") + data = json.loads(line[len("__PARAKEET_RESULT__"):]) + if "error" in data: + raise RuntimeError(f"parakeet error: {data['error']}") + text = data["text"].strip() + + if text and _is_hallucination(text, duration_s): + log.info("Hallucination filtered (%.1fs): %r", duration_s, text[:100]) + return "" + log.info("Transcription: %r", text[:100]) + return text + + def shutdown(self): + """Cleanly stop parakeet worker if running.""" + if self._parakeet_proc is not None and self._parakeet_proc.poll() is None: + log.info("Shutting down parakeet worker") + self._parakeet_proc.terminate() + try: + self._parakeet_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self._parakeet_proc.kill() diff --git a/examples/whisper.linux/app/tray.py b/examples/whisper.linux/app/tray.py new file mode 100644 index 000000000..4d0e5a523 --- /dev/null +++ b/examples/whisper.linux/app/tray.py @@ -0,0 +1,780 @@ +"""System tray icon and menu for whisper.linux.""" + +import os +import subprocess +import threading +from pathlib import Path +from typing import TYPE_CHECKING + +from .config import ( + Config, AppState, AVAILABLE_MODELS, DEFAULT_VOICE_COMMANDS, _REPO_ROOT, + _list_models, _list_audio_devices, log, +) + +if TYPE_CHECKING: + from .app import WhisperLinuxApp + + +def _create_icon(color, size=64): + """Create a microphone tray icon programmatically via QPainter.""" + from PyQt5.QtCore import Qt, QRect, QPoint + from PyQt5.QtGui import QPixmap, QPainter, QColor, QBrush, QPen, QIcon + + pixmap = QPixmap(size, size) + pixmap.fill(Qt.transparent) + p = QPainter(pixmap) + p.setRenderHint(QPainter.Antialiasing) + + c = QColor(color) + p.setBrush(QBrush(c)) + p.setPen(QPen(c.darker(130), 2)) + + # Mic body (rounded rect) + bw, bh = size // 3, size // 2 + bx = (size - bw) // 2 + by = size // 8 + p.drawRoundedRect(bx, by, bw, bh, bw // 3, bw // 3) + + # Arc below mic + p.setBrush(Qt.NoBrush) + p.setPen(QPen(c, max(2, size // 16))) + arc_w = int(bw * 1.6) + arc_h = size // 4 + arc_x = (size - arc_w) // 2 + arc_y = by + bh - arc_h // 2 + p.drawArc(arc_x, arc_y, arc_w, arc_h, 0, -180 * 16) + + # Stem + mid_x = size // 2 + stem_top = arc_y + arc_h + stem_bot = stem_top + size // 6 + p.drawLine(mid_x, stem_top, mid_x, stem_bot) + + # Base + base_w = size // 3 + p.drawLine(mid_x - base_w // 2, stem_bot, mid_x + base_w // 2, stem_bot) + + p.end() + return QIcon(pixmap) + + +class _CallHelper: + """Helper to marshal calls from background threads to Qt main thread.""" + + def __init__(self): + from PyQt5.QtCore import QObject, pyqtSignal + + class _Obj(QObject): + sig = pyqtSignal(object) + + self._obj = _Obj() + self._obj.sig.connect(self._run) + + @staticmethod + def _run(func): + func() + + def call(self, func): + self._obj.sig.emit(func) + + +class TrayIcon: + """System tray icon with right-click menu.""" + + COLORS = { + AppState.IDLE: "#888888", + AppState.RECORDING: "#e53935", + AppState.PROCESSING: "#ffb300", + AppState.LISTENING: "#2196F3", + AppState.DICTATING: "#4CAF50", + } + + # Minimum expected sizes (bytes) for downloaded models + _MODEL_MIN_SIZES = { + "tiny": 70_000_000, + "base": 130_000_000, + "small": 450_000_000, + "medium": 1_400_000_000, + "large-v1": 2_900_000_000, + "large-v2": 2_900_000_000, + "large-v3": 2_900_000_000, + "large-v3-turbo": 1_500_000_000, + } + + # States that blink the tray icon + _BLINK_STATES = {AppState.RECORDING, AppState.DICTATING} + _BLINK_INTERVAL_MS = 500 + + def __init__(self, app_ref: "WhisperLinuxApp"): + from PyQt5.QtWidgets import QSystemTrayIcon + from PyQt5.QtCore import QTimer + + self._app_ref = app_ref + self._icons = {state: _create_icon(color) for state, color in self.COLORS.items()} + self._bright_icon = _create_icon("#FFFFFF") + self._downloading = set() + self._kept_actions = [] + self._call_helper = _CallHelper() + self._blink_on = True + self._current_state = AppState.IDLE + + self._blink_timer = QTimer() + self._blink_timer.timeout.connect(self._on_blink) + + self.tray = QSystemTrayIcon() + self.tray.setIcon(self._icons[AppState.IDLE]) + self.tray.setToolTip("whisper.linux \u2014 Voice Typing") + self.tray.activated.connect(self._on_activated) + + self._build_menu() + self.tray.show() + + # -- Menu construction -- + + def _build_menu(self): + from PyQt5.QtWidgets import QMenu, QAction, QActionGroup + + self.menu = QMenu() + + self.action_toggle = QAction("Start Recording", self.menu) + self.action_toggle.triggered.connect(lambda: self._app_ref.toggle()) + self.menu.addAction(self.action_toggle) + + self.menu.addSeparator() + + # Language + lang_menu = self.menu.addMenu("Language") + self._lang_group = QActionGroup(lang_menu) + self._lang_group.setExclusive(True) + for code, label in [("ru", "Russian"), ("en", "English"), ("auto", "Auto-detect")]: + a = QAction(label, lang_menu, checkable=True) + a.setData(code) + if code == self._app_ref.config.language: + a.setChecked(True) + a.triggered.connect(self._on_language_changed) + self._lang_group.addAction(a) + lang_menu.addAction(a) + + # Model + self._model_menu = self.menu.addMenu("Model") + self._model_group = QActionGroup(self._model_menu) + self._model_group.setExclusive(True) + self._rebuild_model_menu() + + # Settings + self._build_settings_menu() + + self.menu.addSeparator() + + quit_action = QAction("Quit", self.menu) + quit_action.triggered.connect(self._app_ref.quit) + self.menu.addAction(quit_action) + + self.tray.setContextMenu(self.menu) + + def _rebuild_model_menu(self): + from PyQt5.QtWidgets import QAction + + menu = self._model_menu + menu.clear() + self._kept_actions.clear() + for a in self._model_group.actions(): + self._model_group.removeAction(a) + + current_model_raw = self._app_ref.config.model or "" + current_model = (current_model_raw if current_model_raw.startswith("parakeet:") + else Path(current_model_raw).name) + downloaded = _list_models(self._app_ref.config.model_search_dirs) + downloaded_names = {name for name, _ in downloaded} + + if not downloaded and current_model_raw and not current_model_raw.startswith("parakeet:"): + name = Path(current_model_raw).stem.replace("ggml-", "") + downloaded = [(name, current_model_raw)] + downloaded_names = {name} + + for name, path in downloaded: + size_mb = Path(path).stat().st_size / (1024 * 1024) + label = f"{name} ({size_mb:.0f} MB)" + a = QAction(label, menu, checkable=True) + a.setData(path) + if Path(path).name == current_model: + a.setChecked(True) + a.triggered.connect(self._on_model_changed) + self._model_group.addAction(a) + menu.addAction(a) + + # Parakeet V3 \u2014 separate engine (NVIDIA NeMo) + menu.addSeparator() + parakeet_id = "parakeet:nvidia/parakeet-tdt-0.6b-v3" + parakeet_label = "Parakeet V3 (NVIDIA, GPU, ~600 MB)" + if self._parakeet_available(): + a = QAction(parakeet_label, menu, checkable=True) + a.setData(parakeet_id) + if current_model_raw == parakeet_id: + a.setChecked(True) + a.triggered.connect(self._on_model_changed) + self._model_group.addAction(a) + menu.addAction(a) + else: + a = QAction(parakeet_label + " (venv missing)", menu) + a.setEnabled(False) + menu.addAction(a) + self._kept_actions.append(a) + + available = [(n, s) for n, s in AVAILABLE_MODELS if n not in downloaded_names] + if available: + menu.addSeparator() + + for name, size in available: + if name in self._downloading: + a = QAction(f"{name} {size} \u23f3", menu) + a.setEnabled(False) + else: + a = QAction(f"{name} {size} \u2193 Download", menu) + a.setData(name) + a.triggered.connect(lambda checked, n=name: self._on_download_model(n)) + menu.addAction(a) + self._kept_actions.append(a) + + @staticmethod + def _parakeet_available(): + from .transcriber import PARAKEET_VENV_PYTHON + return os.path.isfile(PARAKEET_VENV_PYTHON) and os.access(PARAKEET_VENV_PYTHON, os.X_OK) + + def _rebuild_wake_model_menu(self): + from PyQt5.QtWidgets import QAction + + menu = self._wake_model_menu + menu.clear() + for a in self._wake_model_group.actions(): + self._wake_model_group.removeAction(a) + + config = self._app_ref.config + same_label = "Same as main model" + if config.model and Path(config.model).is_file(): + main_mb = Path(config.model).stat().st_size / (1024 * 1024) + same_label = f"Same as main model ({main_mb:.0f} MB)" + a = QAction(same_label, menu, checkable=True) + a.setData("") + if not config.wake_model: + a.setChecked(True) + a.triggered.connect(self._on_wake_model_changed) + self._wake_model_group.addAction(a) + menu.addAction(a) + for name, path in _list_models(config.model_search_dirs): + size_mb = Path(path).stat().st_size / (1024 * 1024) + label = f"{name} ({size_mb:.0f} MB)" + a = QAction(label, menu, checkable=True) + a.setData(path) + if path == config.wake_model: + a.setChecked(True) + a.triggered.connect(self._on_wake_model_changed) + self._wake_model_group.addAction(a) + menu.addAction(a) + + def _build_settings_menu(self): + from PyQt5.QtWidgets import QAction, QActionGroup + + settings_menu = self.menu.addMenu("Settings") + config = self._app_ref.config + + # Hotkey (always active) + hotkey_label = self._format_hotkey_label(config.hotkey) + self._hotkey_action = QAction(f"Hotkey: {hotkey_label} (change...)", settings_menu) + self._hotkey_action.triggered.connect(self._on_hotkey_change) + settings_menu.addAction(self._hotkey_action) + + # Wake word listening toggle + self._wake_listen_action = QAction("Wake word listening", settings_menu, checkable=True) + self._wake_listen_action.setChecked(config.input_mode == "listen") + self._wake_listen_action.triggered.connect(self._on_wake_listen_toggled) + settings_menu.addAction(self._wake_listen_action) + + # Output mode + output_menu = settings_menu.addMenu("Output mode") + self._output_mode_group = QActionGroup(output_menu) + self._output_mode_group.setExclusive(True) + for val, label in [("batch", "Batch (all at once)"), ("stream", "Streaming (per segment)")]: + a = QAction(label, output_menu, checkable=True) + a.setData(val) + if val == config.output_mode: + a.setChecked(True) + a.triggered.connect(self._on_output_mode_changed) + self._output_mode_group.addAction(a) + output_menu.addAction(a) + + settings_menu.addSeparator() + + # Threads + threads_menu = settings_menu.addMenu("Threads") + self._threads_group = QActionGroup(threads_menu) + self._threads_group.setExclusive(True) + for n in [1, 2, 4, 8, 16]: + a = QAction(str(n), threads_menu, checkable=True) + a.setData(n) + if n == config.threads: + a.setChecked(True) + a.triggered.connect(self._on_threads_changed) + self._threads_group.addAction(a) + threads_menu.addAction(a) + + # GPU + gpu_menu = settings_menu.addMenu("GPU") + self._gpu_group = QActionGroup(gpu_menu) + self._gpu_group.setExclusive(True) + gpu_devices = getattr(config, '_gpu_devices', []) + if gpu_devices: + for name, idx in gpu_devices: + label = f"{idx}: {name}" + a = QAction(label, gpu_menu, checkable=True) + a.setData(idx) + if idx == config.gpu_device: + a.setChecked(True) + a.triggered.connect(self._on_gpu_changed) + self._gpu_group.addAction(a) + gpu_menu.addAction(a) + else: + a = QAction(f"Device {config.gpu_device}", gpu_menu, checkable=True) + a.setData(config.gpu_device) + a.setChecked(True) + self._gpu_group.addAction(a) + gpu_menu.addAction(a) + + # Audio + audio_menu = settings_menu.addMenu("Audio") + self._audio_group = QActionGroup(audio_menu) + self._audio_group.setExclusive(True) + audio_devices = _list_audio_devices() + for label, device_id in audio_devices: + a = QAction(label, audio_menu, checkable=True) + a.setData(device_id) + if device_id == config.audio_device: + a.setChecked(True) + a.triggered.connect(self._on_audio_changed) + self._audio_group.addAction(a) + audio_menu.addAction(a) + + # Paste mode + paste_menu = settings_menu.addMenu("Paste mode") + self._paste_group = QActionGroup(paste_menu) + self._paste_group.setExclusive(True) + for keys, label in [("shift+Insert", "Shift+Insert (universal)"), + ("ctrl+v", "Ctrl+V (regular apps)"), + ("ctrl+shift+v", "Ctrl+Shift+V (terminals)")]: + a = QAction(label, paste_menu, checkable=True) + a.setData(keys) + if keys == config.paste_keys: + a.setChecked(True) + a.triggered.connect(self._on_paste_keys_changed) + self._paste_group.addAction(a) + paste_menu.addAction(a) + + settings_menu.addSeparator() + + # Models directory + models_dir = config.models_dir or "auto" + self._models_dir_action = QAction(f"Models: {models_dir}", settings_menu) + self._models_dir_action.triggered.connect(self._on_models_dir_change) + settings_menu.addAction(self._models_dir_action) + + settings_menu.addSeparator() + + # Wake word + self._wake_word_action = QAction(f"Wake word: {config.wake_word}", settings_menu) + self._wake_word_action.triggered.connect(self._on_wake_word_change) + settings_menu.addAction(self._wake_word_action) + + # Wake model + self._wake_model_menu = settings_menu.addMenu("Wake model") + self._wake_model_group = QActionGroup(self._wake_model_menu) + self._wake_model_group.setExclusive(True) + self._rebuild_wake_model_menu() + + settings_menu.addSeparator() + + # Silence timeout (inline spinbox) + from PyQt5.QtWidgets import QWidgetAction, QWidget, QHBoxLayout, QLabel, QDoubleSpinBox + silence_widget = QWidget() + silence_layout = QHBoxLayout(silence_widget) + silence_layout.setContentsMargins(8, 2, 8, 2) + silence_label = QLabel("Silence timeout:") + self._silence_spin = QDoubleSpinBox() + self._silence_spin.setRange(0.5, 300.0) + self._silence_spin.setSingleStep(0.5) + self._silence_spin.setDecimals(1) + self._silence_spin.setSuffix("s") + self._silence_spin.setValue(config.silence_timeout) + self._silence_spin.valueChanged.connect(self._on_silence_timeout_spin) + silence_layout.addWidget(silence_label) + silence_layout.addWidget(self._silence_spin) + self._silence_widget_action = QWidgetAction(settings_menu) + self._silence_widget_action.setDefaultWidget(silence_widget) + settings_menu.addAction(self._silence_widget_action) + + # End signal toggle + self._end_signal_action = QAction("End signal (beep)", settings_menu, checkable=True) + self._end_signal_action.setChecked(config.end_signal) + self._end_signal_action.triggered.connect(self._on_end_signal_toggled) + settings_menu.addAction(self._end_signal_action) + + # Voice commands toggle + self._voice_cmd_action = QAction("Voice commands", settings_menu, checkable=True) + self._voice_cmd_action.setChecked(config.voice_commands) + self._voice_cmd_action.triggered.connect(self._on_voice_commands_toggled) + settings_menu.addAction(self._voice_cmd_action) + + # Voice commands editor + self._edit_cmds_action = QAction("Edit voice commands...", settings_menu) + self._edit_cmds_action.triggered.connect(self._on_edit_voice_commands) + settings_menu.addAction(self._edit_cmds_action) + + @staticmethod + def _format_hotkey_label(hotkey: str) -> str: + """Convert internal hotkey string to a human-readable label.""" + parts = hotkey.split("+") + pretty = [] + for p in parts: + low = p.lower() + if low in ("super_l", "super_r", "super"): + pretty.append("Super") + elif low == "ctrl": + pretty.append("Ctrl") + elif low == "alt": + pretty.append("Alt") + elif low == "shift": + pretty.append("Shift") + else: + pretty.append(p) + return "+".join(pretty) + + # -- Event handlers -- + + def _on_activated(self, reason): + from PyQt5.QtWidgets import QSystemTrayIcon + if reason == QSystemTrayIcon.Trigger: + self._app_ref.toggle() + + def _on_language_changed(self): + action = self._lang_group.checkedAction() + if action: + self._app_ref.config.language = action.data() + self._app_ref.config.save() + log.info("Language changed to: %s", action.data()) + + def _on_model_changed(self): + action = self._model_group.checkedAction() + if action: + self._app_ref.config.model = action.data() + self._app_ref.config.save() + self._rebuild_wake_model_menu() + log.info("Model changed to: %s", action.data()) + + def _on_download_model(self, name): + if name in self._downloading: + return + self._downloading.add(name) + self._rebuild_model_menu() + log.info("Download requested: model '%s'", name) + self.notify("whisper.linux", f"Downloading {name}...") + t = threading.Thread(target=self._do_download_model, args=(name,), daemon=True) + t.start() + + def _marshal_call(self, func): + """Schedule a callable on the Qt main thread (thread-safe).""" + self._call_helper.call(func) + + def _do_download_model(self, name): + script = _REPO_ROOT / "models" / "download-ggml-model.sh" + if not script.is_file(): + self._downloading.discard(name) + log.error("Download script not found: %s", script) + self._marshal_call( + lambda: self.notify("Error", "download-ggml-model.sh not found")) + return + models_dir = self._app_ref.config.models_dir + Path(models_dir).mkdir(parents=True, exist_ok=True) + model_path = Path(models_dir) / f"ggml-{name}.bin" + + # Remove incomplete downloads so the script doesn't skip them + min_size = self._MODEL_MIN_SIZES.get(name, 0) + if model_path.is_file() and min_size and model_path.stat().st_size < min_size: + log.warning("Removing incomplete model %s (%d bytes, expected >= %d)", + model_path, model_path.stat().st_size, min_size) + model_path.unlink() + + log.info("Download started: model '%s' \u2192 %s", name, models_dir) + try: + result = subprocess.run( + ["bash", str(script), name, models_dir], + capture_output=True, text=True, timeout=3600, + ) + if result.returncode != 0: + log.error("Download script failed (rc=%d): %s", + result.returncode, result.stderr[:200]) + if model_path.is_file(): + size = model_path.stat().st_size + if min_size and size < min_size: + log.error("Downloaded model too small (%d bytes, expected >= %d), removing", + size, min_size) + model_path.unlink() + self._downloading.discard(name) + msg = f"Download incomplete ({size // 1_000_000}MB), please retry" + self._marshal_call(lambda: self.notify("Error", msg)) + return + self._downloading.discard(name) + size_mb = size / (1024 * 1024) + log.info("Download complete: model '%s' (%d MB)", name, size_mb) + msg = f"Model '{name}' ready ({size_mb:.0f} MB)" + self._marshal_call(lambda: ( + self._rebuild_model_menu(), + self._rebuild_wake_model_menu(), + self.notify("whisper.linux", msg), + )) + else: + self._downloading.discard(name) + err = result.stderr[:100] + log.error("Download failed: model file not found after script: %s", model_path) + self._marshal_call( + lambda: self.notify("Error", f"Download failed: {err}")) + except subprocess.TimeoutExpired: + self._downloading.discard(name) + log.error("Download timed out for model '%s' (1 hour limit)", name) + self._marshal_call( + lambda: self.notify("Error", f"Download timed out for '{name}' (1 hour limit)")) + except Exception as e: + self._downloading.discard(name) + log.error("Download failed for model '%s': %s", name, e) + err_str = str(e) + self._marshal_call( + lambda: self.notify("Error", f"Download failed: {err_str}")) + + def _on_threads_changed(self): + action = self._threads_group.checkedAction() + if action: + self._app_ref.config.threads = action.data() + self._app_ref.config.save() + log.info("Threads changed to: %d", action.data()) + + def _on_gpu_changed(self): + action = self._gpu_group.checkedAction() + if action: + self._app_ref.config.gpu_device = action.data() + self._app_ref.config.save() + log.info("GPU device changed to: %d", action.data()) + + def _on_audio_changed(self): + action = self._audio_group.checkedAction() + if action: + self._app_ref.config.audio_device = action.data() + self._app_ref.config.save() + log.info("Audio device changed to: %s", action.data()) + + def _on_paste_keys_changed(self): + action = self._paste_group.checkedAction() + if action: + self._app_ref.config.paste_keys = action.data() + self._app_ref.config.save() + log.info("Paste mode changed to: %s", action.data()) + + def _on_models_dir_change(self): + from PyQt5.QtWidgets import QFileDialog + current = self._app_ref.config.models_dir or str(Path.home()) + new_dir = QFileDialog.getExistingDirectory(None, "Models directory", current) + if not new_dir: + return + self._app_ref.config.models_dir = new_dir + self._app_ref.config.save() + self._models_dir_action.setText(f"Models: {new_dir}") + self._rebuild_model_menu() + self._rebuild_wake_model_menu() + log.info("Models dir changed to: %s", new_dir) + + def _on_wake_listen_toggled(self): + enabled = self._wake_listen_action.isChecked() + old_val = self._app_ref.config.input_mode + new_val = "listen" if enabled else "hotkey" + if old_val != new_val and self._app_ref.state != AppState.IDLE: + self._app_ref._force_idle() + self._app_ref.config.input_mode = new_val + self._app_ref.config.save() + log.info("Wake word listening: %s (input_mode=%s)", enabled, new_val) + if enabled and self._app_ref.state == AppState.IDLE: + self._app_ref.toggle() + + def _on_output_mode_changed(self): + action = self._output_mode_group.checkedAction() + if action: + old_val = self._app_ref.config.output_mode + new_val = action.data() + if old_val != new_val and self._app_ref.state != AppState.IDLE: + self._app_ref._force_idle() + self._app_ref.config.output_mode = new_val + self._app_ref.config.save() + log.info("Output mode changed to: %s", new_val) + + def _on_hotkey_change(self): + from PyQt5.QtWidgets import QInputDialog + current = self._app_ref.config.hotkey + text, ok = QInputDialog.getText( + None, "Hotkey", "Enter hotkey (e.g. ctrl+Super_L):", text=current, + ) + if ok and text.strip(): + self._app_ref.config.hotkey = text.strip() + self._app_ref.config.save() + label = self._format_hotkey_label(text.strip()) + self._hotkey_action.setText(f"Hotkey: {label} (change...)") + log.info("Hotkey changed to: %s", text.strip()) + + def _on_wake_word_change(self): + from PyQt5.QtWidgets import QInputDialog + current = self._app_ref.config.wake_word + text, ok = QInputDialog.getText(None, "Wake Word", "Enter wake word:", text=current) + if ok and text.strip(): + self._app_ref.config.wake_word = text.strip() + self._app_ref.config.save() + self._wake_word_action.setText(f"Wake word: {text.strip()}") + log.info("Wake word changed to: %s", text.strip()) + + def _on_wake_model_changed(self): + action = self._wake_model_group.checkedAction() + if action: + self._app_ref.config.wake_model = action.data() + self._app_ref.config.save() + log.info("Wake model changed to: %s", action.data() or "(same as main)") + + def _on_silence_timeout_spin(self, value): + self._app_ref.config.silence_timeout = value + self._app_ref.config.save() + log.info("Silence timeout changed to: %.1fs", value) + + def _on_end_signal_toggled(self): + self._app_ref.config.end_signal = self._end_signal_action.isChecked() + self._app_ref.config.save() + log.info("End signal: %s", self._app_ref.config.end_signal) + + def _on_voice_commands_toggled(self): + self._app_ref.config.voice_commands = self._voice_cmd_action.isChecked() + self._app_ref.config.save() + log.info("Voice commands: %s", self._app_ref.config.voice_commands) + + def _on_edit_voice_commands(self): + from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, + QTableWidgetItem, QPushButton, QHeaderView, QAbstractItemView, + ) + from PyQt5.QtCore import Qt + + config = self._app_ref.config + cmds = dict(config.voice_commands_map) + + dlg = QDialog() + dlg.setWindowTitle("Voice Commands") + dlg.setMinimumSize(450, 400) + layout = QVBoxLayout(dlg) + + table = QTableWidget(len(cmds), 2) + table.setHorizontalHeaderLabels(["Word", "Action"]) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + for row, (word, action) in enumerate(cmds.items()): + table.setItem(row, 0, QTableWidgetItem(word)) + table.setItem(row, 1, QTableWidgetItem(action)) + layout.addWidget(table) + + btn_layout = QHBoxLayout() + add_btn = QPushButton("Add") + remove_btn = QPushButton("Remove") + reset_btn = QPushButton("Reset defaults") + btn_layout.addWidget(add_btn) + btn_layout.addWidget(remove_btn) + btn_layout.addStretch() + btn_layout.addWidget(reset_btn) + layout.addLayout(btn_layout) + + ok_layout = QHBoxLayout() + ok_btn = QPushButton("OK") + cancel_btn = QPushButton("Cancel") + ok_layout.addStretch() + ok_layout.addWidget(ok_btn) + ok_layout.addWidget(cancel_btn) + layout.addLayout(ok_layout) + + def add_row(): + row = table.rowCount() + table.insertRow(row) + table.setItem(row, 0, QTableWidgetItem("")) + table.setItem(row, 1, QTableWidgetItem("key:Return")) + table.editItem(table.item(row, 0)) + + def remove_row(): + rows = sorted({idx.row() for idx in table.selectedIndexes()}, reverse=True) + for row in rows: + table.removeRow(row) + + def reset_defaults(): + table.setRowCount(0) + for word, action in DEFAULT_VOICE_COMMANDS.items(): + row = table.rowCount() + table.insertRow(row) + table.setItem(row, 0, QTableWidgetItem(word)) + table.setItem(row, 1, QTableWidgetItem(action)) + + add_btn.clicked.connect(add_row) + remove_btn.clicked.connect(remove_row) + reset_btn.clicked.connect(reset_defaults) + ok_btn.clicked.connect(dlg.accept) + cancel_btn.clicked.connect(dlg.reject) + + if dlg.exec_() == QDialog.Accepted: + new_cmds = {} + for row in range(table.rowCount()): + w = (table.item(row, 0).text() or "").strip().lower() + a = (table.item(row, 1).text() or "").strip() + if w and a: + new_cmds[w] = a + config.voice_commands_map = new_cmds + config.save() + # Update the live VoiceCommands instance + self._app_ref._voice_commands._commands = new_cmds + log.info("Voice commands updated: %d entries", len(new_cmds)) + + # -- State & notifications -- + + def _on_blink(self): + self._blink_on = not self._blink_on + state = self._current_state + if self._blink_on: + self.tray.setIcon(self._icons[state]) + else: + self.tray.setIcon(self._bright_icon) + + def set_state(self, state: AppState): + self._current_state = state + self.tray.setIcon(self._icons[state]) + self._blink_on = True + + if state in self._BLINK_STATES: + if not self._blink_timer.isActive(): + self._blink_timer.start(self._BLINK_INTERVAL_MS) + else: + self._blink_timer.stop() + + if state == AppState.IDLE: + self.action_toggle.setText("Start Recording") + self.tray.setToolTip("whisper.linux \u2014 Idle") + elif state == AppState.RECORDING: + self.action_toggle.setText("Stop Recording") + self.tray.setToolTip("whisper.linux \u2014 Recording...") + elif state == AppState.PROCESSING: + self.action_toggle.setText("Processing...") + self.tray.setToolTip("whisper.linux \u2014 Transcribing...") + elif state == AppState.LISTENING: + self.action_toggle.setText("Stop Listening") + self.tray.setToolTip("whisper.linux \u2014 Listening (say wake word)...") + elif state == AppState.DICTATING: + self.action_toggle.setText("Stop Dictating") + self.tray.setToolTip("whisper.linux \u2014 Dictating...") + + def notify(self, title: str, message: str): + from PyQt5.QtWidgets import QSystemTrayIcon + self.tray.showMessage(title, message, QSystemTrayIcon.Information, 3000) diff --git a/examples/whisper.linux/install.sh b/examples/whisper.linux/install.sh new file mode 100755 index 000000000..c27e72f84 --- /dev/null +++ b/examples/whisper.linux/install.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# install.sh — Install dependencies and set up whisper.linux +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +MODEL_NAME="${1:-base}" +MODEL_FILE="ggml-${MODEL_NAME}.bin" +CONFIG_DIR="$HOME/.config/whisper-linux" + +echo "=== whisper.linux installer ===" +echo "" + +# --- 1. Install system dependencies --- +echo "[1/4] Installing system dependencies..." +sudo apt-get update -qq +sudo apt-get install -y -qq \ + python3-pyqt5 \ + xdotool \ + xclip \ + wl-clipboard \ + ydotool \ + alsa-utils \ + build-essential \ + cmake + +# Install Vulkan dev deps for GPU acceleration (optional, non-fatal) +sudo apt-get install -y -qq libvulkan-dev glslc 2>/dev/null || true + +echo " ✓ System dependencies installed" + +# --- 2. Build whisper-cli --- +echo "[2/4] Building whisper-cli..." +BUILD_DIR="$REPO_ROOT/build" +mkdir -p "$BUILD_DIR" + +# Enable Vulkan GPU acceleration if available +CMAKE_EXTRA="" +if pkg-config --exists vulkan 2>/dev/null || [ -f /usr/include/vulkan/vulkan.h ]; then + CMAKE_EXTRA="-DGGML_VULKAN=ON" + echo " → Vulkan detected, building with GPU support" +else + echo " → Vulkan not found, building CPU-only" +fi + +cmake -S "$REPO_ROOT" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release $CMAKE_EXTRA +cmake --build "$BUILD_DIR" -j "$(nproc)" + +WHISPER_CLI="$BUILD_DIR/bin/whisper-cli" +if [ ! -f "$WHISPER_CLI" ]; then + # Fallback: some builds put it directly in build/ + WHISPER_CLI="$BUILD_DIR/whisper-cli" +fi + +if [ ! -x "$WHISPER_CLI" ]; then + echo " ✗ Failed to build whisper-cli" + exit 1 +fi +echo " ✓ whisper-cli built: $WHISPER_CLI" + +# --- 3. Download model --- +echo "[3/4] Downloading model ($MODEL_NAME)..." +MODEL_DIR="$REPO_ROOT/models" +MODEL_PATH="$MODEL_DIR/$MODEL_FILE" + +if [ -f "$MODEL_PATH" ]; then + echo " ✓ Model already exists: $MODEL_PATH" +else + bash "$REPO_ROOT/models/download-ggml-model.sh" "$MODEL_NAME" + if [ ! -f "$MODEL_PATH" ]; then + echo " ✗ Model download failed" + exit 1 + fi + echo " ✓ Model downloaded: $MODEL_PATH" +fi + +# --- 4. Create config --- +echo "[4/4] Creating config..." +mkdir -p "$CONFIG_DIR" +CONFIG_FILE="$CONFIG_DIR/config.ini" + +if [ -f "$CONFIG_FILE" ]; then + echo " → Config exists: $CONFIG_FILE" + # Update model and whisper_cli paths to match current build + if grep -q "^model\s*=" "$CONFIG_FILE"; then + sed -i "s|^model\s*=.*|model = $MODEL_PATH|" "$CONFIG_FILE" + fi + if grep -q "^whisper_cli\s*=" "$CONFIG_FILE"; then + sed -i "s|^whisper_cli\s*=.*|whisper_cli = $WHISPER_CLI|" "$CONFIG_FILE" + fi + echo " ✓ Config updated (model=$MODEL_PATH)" +else + cat > "$CONFIG_FILE" < /dev/null + sudo udevadm control --reload-rules + sudo udevadm trigger /dev/uinput + fi + # Immediate fix for current session + sudo chmod 0660 /dev/uinput + sudo chown "root:$(id -gn)" /dev/uinput + echo " ✓ /dev/uinput access configured" +fi + +# --- Optional: install desktop file for autostart --- +AUTOSTART_DIR="$HOME/.config/autostart" +DESKTOP_FILE="$SCRIPT_DIR/whisper-linux.desktop" +if [ -f "$DESKTOP_FILE" ]; then + mkdir -p "$AUTOSTART_DIR" + # Update Exec path in desktop file + sed "s|Exec=.*|Exec=python3 -m app|" \ + "$DESKTOP_FILE" > "$AUTOSTART_DIR/whisper-linux.desktop" + echo " ✓ Autostart desktop file installed" +fi + +# --- Set up keyboard shortcut (GNOME) --- +LAUNCHER="$SCRIPT_DIR/whisper-linux" +TOGGLE_CMD="$LAUNCHER --toggle" +echo "" +echo "=== Installation complete ===" +echo "" +echo "Usage:" +echo " $LAUNCHER # Start (tray icon)" +echo " $LAUNCHER --toggle # Toggle recording" +echo " $LAUNCHER --debug # Start with debug logging" +echo " pkill -f whisper-linux # Stop" +echo "" +echo "=== Keyboard shortcut setup ===" +echo " GNOME: Settings → Keyboard → Custom Shortcuts → Add:" +echo " Name: whisper-linux" +echo " Command: $TOGGLE_CMD" +echo " Shortcut: Super+V (or any key you like)" +echo "" +echo " Or run this command to set it up automatically:" +echo " gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings \"['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/whisper-linux/']\"" +echo " gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/whisper-linux/ name 'whisper-linux'" +echo " gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/whisper-linux/ command '$TOGGLE_CMD'" +echo " gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/whisper-linux/ binding 'F8'" diff --git a/examples/whisper.linux/run_tests.sh b/examples/whisper.linux/run_tests.sh new file mode 100755 index 000000000..9681577b2 --- /dev/null +++ b/examples/whisper.linux/run_tests.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run all whisper.linux tests. +# Usage: ./run_tests.sh [-v] [--debug] [pytest-options...] + +cd "$(dirname "$0")" +python3 -m pytest tests/ -v "$@" diff --git a/examples/whisper.linux/tests/conftest.py b/examples/whisper.linux/tests/conftest.py new file mode 100644 index 000000000..76a3a0cb5 --- /dev/null +++ b/examples/whisper.linux/tests/conftest.py @@ -0,0 +1,133 @@ +"""Pytest fixtures for whisper.linux tests.""" + +import os +import sys +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +# Add project root to path so we can import the 'app' package +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + + +@pytest.fixture +def tmp_config_dir(tmp_path): + """Temporary config directory.""" + config_dir = tmp_path / ".config" / "whisper-linux" + config_dir.mkdir(parents=True) + return config_dir + + +@pytest.fixture +def tmp_config_file(tmp_config_dir): + """Temporary config.ini with valid paths.""" + config_file = tmp_config_dir / "config.ini" + config_file.write_text( + "[whisper-linux]\n" + "whisper_cli = /usr/bin/whisper-cli\n" + "model = /tmp/ggml-base.bin\n" + "models_dir = /tmp\n" + "language = ru\n" + "threads = 4\n" + "display_server = x11\n" + "audio_device = plughw:1,0\n" + "paste_keys = shift+Insert\n" + "use_clipboard_fallback = False\n" + "notification = True\n" + ) + return config_file + + +@pytest.fixture +def mock_config(tmp_config_file, monkeypatch): + """Config that reads from temp config file.""" + import app as wl + + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_file.parent) + monkeypatch.setattr(wl.config, "CONFIG_FILE", tmp_config_file) + return wl.Config() + + +@pytest.fixture +def mock_config_wayland(mock_config): + """Config set to Wayland.""" + mock_config.display_server = "wayland" + return mock_config + + +@pytest.fixture +def tmp_wav_file(): + """Create a temporary WAV file for testing.""" + fd, path = tempfile.mkstemp(suffix=".wav", prefix="test-whisper-") + os.write(fd, b"RIFF" + b"\x00" * 40) # Minimal WAV header stub + os.close(fd) + yield path + if os.path.exists(path): + os.unlink(path) + + +@pytest.fixture +def tmp_pid_file(tmp_path, monkeypatch): + """Temporary PID file.""" + import app as wl + + pid_file = tmp_path / "whisper-linux.pid" + monkeypatch.setattr(wl.config, "PID_FILE", pid_file) + return pid_file + + +@pytest.fixture +def mock_config_stream(mock_config): + """Config set to listen+stream mode (old 'stream' mode).""" + mock_config.input_mode = "listen" + mock_config.output_mode = "stream" + mock_config.wake_word = "дуняша" + mock_config.silence_timeout = 3.0 + mock_config.vad_threshold = 300 + mock_config.min_speech_ms = 500 + mock_config.max_speech_s = 30.0 + return mock_config + + +@pytest.fixture +def mock_config_hotkey_stream(mock_config): + """Config set to hotkey+stream mode.""" + mock_config.input_mode = "hotkey" + mock_config.output_mode = "stream" + mock_config.wake_word = "дуняша" + mock_config.silence_timeout = 3.0 + mock_config.vad_threshold = 300 + mock_config.min_speech_ms = 500 + mock_config.max_speech_s = 30.0 + return mock_config + + +@pytest.fixture +def mock_config_listen_batch(mock_config): + """Config set to listen+batch mode.""" + mock_config.input_mode = "listen" + mock_config.output_mode = "batch" + mock_config.wake_word = "дуняша" + mock_config.silence_timeout = 3.0 + mock_config.vad_threshold = 300 + mock_config.min_speech_ms = 500 + mock_config.max_speech_s = 30.0 + return mock_config + + +@pytest.fixture +def silence_pcm(): + """Return 1 second of silence (s16le mono 16kHz).""" + return b"\x00\x00" * 16000 + + +@pytest.fixture +def speech_pcm(): + """Return 1 second of loud 'speech' (s16le mono 16kHz, high amplitude sine).""" + import array, math + samples = array.array("h") + for i in range(16000): + samples.append(int(10000 * math.sin(2 * math.pi * 440 * i / 16000))) + return samples.tobytes() diff --git a/examples/whisper.linux/tests/test_whisper_linux.py b/examples/whisper.linux/tests/test_whisper_linux.py new file mode 100644 index 000000000..b53a4ff73 --- /dev/null +++ b/examples/whisper.linux/tests/test_whisper_linux.py @@ -0,0 +1,2511 @@ +"""Unit tests for whisper.linux — all mocked, no hardware needed.""" + +import os +import signal +import subprocess +import tempfile +import threading +from pathlib import Path +from unittest.mock import MagicMock, patch, call + +import pytest + +import app as wl + + +# =================================================================== +# AppState +# =================================================================== + +class TestAppState: + def test_states_exist(self): + assert wl.AppState.IDLE.value == "idle" + assert wl.AppState.RECORDING.value == "recording" + assert wl.AppState.PROCESSING.value == "processing" + assert wl.AppState.LISTENING.value == "listening" + assert wl.AppState.DICTATING.value == "dictating" + + def test_all_states(self): + assert len(wl.AppState) == 5 + + +# =================================================================== +# Config +# =================================================================== + +class TestConfig: + def test_load_from_file(self, mock_config): + assert mock_config.whisper_cli == "/usr/bin/whisper-cli" + assert mock_config.model == "/tmp/ggml-base.bin" + assert mock_config.language == "ru" + assert mock_config.threads == 4 + assert mock_config.display_server == "x11" + assert mock_config.use_clipboard_fallback is False + assert mock_config.notification is True + + def test_auto_detect_display_x11(self, monkeypatch): + monkeypatch.setenv("XDG_SESSION_TYPE", "x11") + monkeypatch.delenv("WAYLAND_DISPLAY", raising=False) + assert wl.Config._detect_display_server() == "x11" + + def test_auto_detect_display_wayland(self, monkeypatch): + monkeypatch.setenv("XDG_SESSION_TYPE", "wayland") + assert wl.Config._detect_display_server() == "wayland" + + def test_auto_detect_display_wayland_env(self, monkeypatch): + monkeypatch.setenv("XDG_SESSION_TYPE", "") + monkeypatch.setenv("WAYLAND_DISPLAY", "wayland-0") + assert wl.Config._detect_display_server() == "wayland" + + def test_auto_detect_display_fallback(self, monkeypatch): + monkeypatch.setenv("XDG_SESSION_TYPE", "") + monkeypatch.delenv("WAYLAND_DISPLAY", raising=False) + monkeypatch.setenv("DISPLAY", ":0") + assert wl.Config._detect_display_server() == "x11" + + def test_save_config(self, mock_config, tmp_config_file): + mock_config.language = "en" + mock_config.save() + text = tmp_config_file.read_text() + assert "language = en" in text + + def test_default_config_no_file(self, tmp_path, monkeypatch): + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_path / "nonexistent") + monkeypatch.setattr(wl.config, "CONFIG_FILE", tmp_path / "nonexistent" / "config.ini") + monkeypatch.setattr(wl.config, "_find_executable", lambda *a: "/mock/whisper-cli") + monkeypatch.setattr(wl.config, "_find_model", lambda *a, **kw: "/mock/model.bin") + monkeypatch.setenv("XDG_SESSION_TYPE", "x11") + config = wl.Config() + assert config.whisper_cli == "/mock/whisper-cli" + assert config.model == "/mock/model.bin" + + def test_gpu_devices_cache_populated(self, mock_config): + """Config has _gpu_devices attribute (list) after init.""" + assert hasattr(mock_config, '_gpu_devices') + assert isinstance(mock_config._gpu_devices, list) + + +# =================================================================== +# find_executable / find_model helpers +# =================================================================== + +class TestHelpers: + def test_find_executable_found(self, tmp_path): + exe = tmp_path / "whisper-cli" + exe.write_text("#!/bin/sh\n") + exe.chmod(0o755) + result = wl._find_executable(["whisper-cli"], [tmp_path]) + assert result == str(exe) + + def test_find_executable_not_found(self, tmp_path): + result = wl._find_executable(["nonexistent-tool-xyz"], [tmp_path]) + assert result is None + + def test_find_model_preferred(self, tmp_path): + model = tmp_path / "ggml-base.bin" + model.write_bytes(b"\x00" * 100) + result = wl._find_model([tmp_path]) + assert result == str(model) + + def test_find_model_any_bin(self, tmp_path): + model = tmp_path / "ggml-small.bin" + model.write_bytes(b"\x00" * 100) + result = wl._find_model([tmp_path], preferred="ggml-base.bin") + assert result == str(model) + + def test_find_model_none(self, tmp_path): + result = wl._find_model([tmp_path / "empty"]) + assert result is None + + def test_available_models_constant(self): + assert len(wl.AVAILABLE_MODELS) == 5 + names = [n for n, _ in wl.AVAILABLE_MODELS] + assert "tiny" in names + assert "base" in names + assert "large-v3" in names + + @patch("app.subprocess.run") + @patch("app.os.path.isfile", return_value=True) + def test_list_audio_devices_pipewire(self, mock_isfile, mock_run): + mock_run.return_value = MagicMock( + stdout="card 0: PCH [HDA Intel PCH], device 0: ALC892 Analog [ALC892 Analog]\n", + stderr="", + ) + devices = wl._list_audio_devices() + assert devices[0] == ("Auto (PipeWire)", "auto") + assert len(devices) >= 2 + assert devices[1][1] == "plughw:0,0" + + @patch("app.subprocess.run", side_effect=FileNotFoundError()) + @patch("app.os.path.isfile", return_value=False) + def test_list_audio_devices_no_pipewire(self, mock_isfile, mock_run): + devices = wl._list_audio_devices() + assert devices[0] == ("Auto", "auto") + assert len(devices) == 1 + + +# =================================================================== +# AudioRecorder +# =================================================================== + +class TestAudioRecorder: + @patch("app.time.sleep") + @patch("app.subprocess.Popen") + def test_start_recording(self, mock_popen, mock_sleep): + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_popen.return_value = mock_proc + + rec = wl.AudioRecorder(MagicMock(audio_device="plughw:1,0")) + path = rec.start() + + assert path.endswith(".wav") + assert rec.is_recording is True + mock_popen.assert_called_once() + args = mock_popen.call_args[0][0] + assert args[0] == "arecord" + assert "-D" in args and "plughw:1,0" in args + assert "-r" in args and "16000" in args + + @patch("app.time.sleep") + @patch("app.subprocess.Popen") + def test_stop_recording(self, mock_popen, mock_sleep): + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_popen.return_value = mock_proc + + rec = wl.AudioRecorder(MagicMock(audio_device="plughw:1,0")) + rec.start() + wav = rec.stop() + + assert wav is not None + assert wav.endswith(".wav") + mock_proc.send_signal.assert_called_once_with(signal.SIGINT) + assert rec.is_recording is False + + @patch("app.time.sleep") + @patch("app.subprocess.Popen") + def test_start_while_recording_raises(self, mock_popen, mock_sleep): + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_popen.return_value = mock_proc + + rec = wl.AudioRecorder(MagicMock(audio_device="plughw:1,0")) + rec.start() + with pytest.raises(RuntimeError, match="Already recording"): + rec.start() + + def test_stop_without_start(self): + rec = wl.AudioRecorder(MagicMock(audio_device="plughw:1,0")) + assert rec.stop() is None + + @patch("app.time.sleep") + @patch("app.subprocess.Popen") + def test_cleanup_removes_file(self, mock_popen, mock_sleep): + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_popen.return_value = mock_proc + + rec = wl.AudioRecorder(MagicMock(audio_device="plughw:1,0")) + path = rec.start() + + # Create the file so cleanup can delete it + Path(path).write_bytes(b"\x00") + assert os.path.exists(path) + + mock_proc.poll.return_value = 0 # Already stopped + rec.cleanup() + assert not os.path.exists(path) + + @patch("app.subprocess.Popen") + def test_start_fails_on_bad_device(self, mock_popen): + mock_proc = MagicMock() + mock_proc.poll.return_value = 1 # Exited immediately + mock_proc.stderr.read.return_value = b"audio open error: No such file or directory" + mock_popen.return_value = mock_proc + + rec = wl.AudioRecorder(MagicMock(audio_device="hw:99,0")) + with pytest.raises(RuntimeError, match="Recording failed to start"): + rec.start() + + +# =================================================================== +# Transcriber +# =================================================================== + +class TestTranscriber: + @patch("app.subprocess.run") + def test_transcribe_success(self, mock_run, mock_config, tmp_wav_file): + mock_run.return_value = MagicMock( + returncode=0, + stdout=" Привет, мир! \n", + stderr="", + ) + + tr = wl.Transcriber(mock_config) + result = tr.transcribe(tmp_wav_file) + + assert result == "Привет, мир!" + mock_run.assert_called_once() + cmd = mock_run.call_args[0][0] + assert cmd[0] == "/usr/bin/whisper-cli" + assert "-m" in cmd + assert "-nt" in cmd + assert "-np" in cmd + + @patch("app.subprocess.run") + def test_transcribe_removes_blank_audio(self, mock_run, mock_config, tmp_wav_file): + mock_run.return_value = MagicMock( + returncode=0, + stdout="[BLANK_AUDIO] some text [BLANK_AUDIO]", + stderr="", + ) + + tr = wl.Transcriber(mock_config) + result = tr.transcribe(tmp_wav_file) + assert result == "some text" + + @patch("app.subprocess.run") + def test_transcribe_failure(self, mock_run, mock_config, tmp_wav_file): + mock_run.return_value = MagicMock( + returncode=1, + stdout="", + stderr="Error loading model", + ) + + tr = wl.Transcriber(mock_config) + with pytest.raises(RuntimeError, match="whisper-cli failed"): + tr.transcribe(tmp_wav_file) + + def test_transcribe_missing_file(self, mock_config): + tr = wl.Transcriber(mock_config) + with pytest.raises(FileNotFoundError): + tr.transcribe("/tmp/nonexistent-file-xyz.wav") + + @patch("app.subprocess.run") + def test_transcribe_filters_hallucination_ru(self, mock_run, mock_config, tmp_wav_file): + mock_run.return_value = MagicMock( + returncode=0, + stdout="Аппортизатор субтитров И.Б.С.С вами был Игорь Негода.", + stderr="", + ) + tr = wl.Transcriber(mock_config) + result = tr.transcribe(tmp_wav_file) + assert result == "" + + @patch("app.subprocess.run") + def test_transcribe_filters_hallucination_thanks(self, mock_run, mock_config, tmp_wav_file): + mock_run.return_value = MagicMock( + returncode=0, + stdout="Спасибо за просмотр!", + stderr="", + ) + tr = wl.Transcriber(mock_config) + result = tr.transcribe(tmp_wav_file) + assert result == "" + + @patch("app.subprocess.run") + def test_transcribe_filters_hallucination_en(self, mock_run, mock_config, tmp_wav_file): + mock_run.return_value = MagicMock( + returncode=0, + stdout="Subtitles by the Amara.org community", + stderr="", + ) + tr = wl.Transcriber(mock_config) + result = tr.transcribe(tmp_wav_file) + assert result == "" + + @patch("app.subprocess.run") + def test_transcribe_keeps_normal_text(self, mock_run, mock_config, tmp_wav_file): + mock_run.return_value = MagicMock( + returncode=0, + stdout="Привет, как дела?", + stderr="", + ) + tr = wl.Transcriber(mock_config) + result = tr.transcribe(tmp_wav_file) + assert result == "Привет, как дела?" + + +class TestHallucinationFilter: + def test_russian_subtitles(self): + assert wl._is_hallucination("Субтитры сделал DimaTorzworker") + assert wl._is_hallucination("Редактор субтитров А.Б.В.") + assert wl._is_hallucination("Продолжение следует...") + + def test_russian_credits(self): + assert wl._is_hallucination("Спасибо за просмотр!") + assert wl._is_hallucination("Подписывайтесь на канал") + assert wl._is_hallucination("С вами был Игорь Негода.") + assert wl._is_hallucination("Аппортизатор субтитров И.Б.С.С") + assert wl._is_hallucination("Добро пожаловать в КПС!") + + def test_english_hallucinations(self): + assert wl._is_hallucination("Thanks for watching!") + assert wl._is_hallucination("Subtitles by the Amara.org community") + assert wl._is_hallucination("Please subscribe and like") + + def test_normal_text_not_filtered(self): + assert not wl._is_hallucination("Привет, как дела?") + assert not wl._is_hallucination("Hello world") + assert not wl._is_hallucination("Запустить сервер") + assert not wl._is_hallucination("Открой файл конфигурации") + + def test_speech_rate_too_many_words(self): + """5 words from 0.5s of audio is impossible — hallucination.""" + assert wl._is_hallucination("Один два три четыре пять", duration_s=0.5) + + def test_speech_rate_too_many_chars(self): + """Long word from 0.5s audio — too many characters.""" + assert wl._is_hallucination("Константинопольский", duration_s=0.5) + + def test_speech_rate_normal(self): + """1 word from 0.6s is perfectly normal.""" + assert not wl._is_hallucination("Привет", duration_s=0.6) + assert not wl._is_hallucination("Энтер", duration_s=0.5) + assert not wl._is_hallucination("Таб", duration_s=0.5) + assert not wl._is_hallucination("Табуляция", duration_s=0.5) + + def test_speech_rate_longer_audio(self): + """5 words from 3s is fine (~1.7 wps).""" + assert not wl._is_hallucination("Привет как дела у тебя", duration_s=3.0) + + def test_speech_rate_no_duration(self): + """Without duration, only pattern matching applies.""" + assert not wl._is_hallucination("Один два три четыре пять") + assert wl._is_hallucination("Субтитры от переводчиков") + + +# =================================================================== +# TextInjector +# =================================================================== + +class TestTextInjector: + @patch("app.subprocess.run") + def test_inject_x11_xdotool(self, mock_run, mock_config): + mock_run.return_value = MagicMock(returncode=0) + + inj = wl.TextInjector(mock_config) + inj.inject("Hello world") + + mock_run.assert_called_once() + cmd = mock_run.call_args[0][0] + assert cmd[0] == "xdotool" + assert "type" in cmd + assert "Hello world" in cmd + + @patch("app.subprocess.run") + def test_inject_x11_clipboard_fallback_explicit(self, mock_run, mock_config): + mock_config.use_clipboard_fallback = True + mock_run.return_value = MagicMock(returncode=0) + + inj = wl.TextInjector(mock_config) + inj.inject("hello") + + assert mock_run.call_count == 3 + # First call: xclip clipboard + assert mock_run.call_args_list[0][0][0][0] == "xclip" + # Second call: xclip primary + assert mock_run.call_args_list[1][0][0][0] == "xclip" + # Third call: xdotool key paste + assert mock_run.call_args_list[2][0][0][0] == "xdotool" + + @patch("app.subprocess.run") + def test_inject_x11_non_ascii_uses_direct_type(self, mock_run, mock_config): + """Non-ASCII text (Cyrillic) uses xdotool type directly (no clipboard).""" + mock_run.return_value = MagicMock(returncode=0) + + inj = wl.TextInjector(mock_config) + inj.inject("Привет мир") + + assert mock_run.call_count == 1 + cmd = mock_run.call_args_list[0][0][0] + assert cmd[0] == "xdotool" + assert "type" in cmd + + @patch("app.subprocess.run") + def test_inject_x11_xdotool_fails_falls_back(self, mock_run, mock_config): + # First call (xdotool type) fails, then clipboard calls succeed + mock_run.side_effect = [ + subprocess.CalledProcessError(1, "xdotool"), + MagicMock(returncode=0), # xclip clipboard + MagicMock(returncode=0), # xclip primary + MagicMock(returncode=0), # xdotool key + ] + + inj = wl.TextInjector(mock_config) + inj.inject("test") + + assert mock_run.call_count == 4 + + @patch("app.subprocess.run") + def test_inject_wayland_wtype(self, mock_run, mock_config_wayland): + mock_run.return_value = MagicMock(returncode=0) + + inj = wl.TextInjector(mock_config_wayland) + inj.inject("test") + + cmd = mock_run.call_args[0][0] + assert cmd[0] == "wtype" + + @patch("app.time.sleep") + @patch("app.subprocess.run") + def test_inject_wayland_wlcopy_ydotool(self, mock_run, mock_sleep, mock_config_wayland): + """wtype fails → wl-copy + wl-copy --primary + ydotool Shift+Insert.""" + mock_run.side_effect = [ + FileNotFoundError(), # wtype not found + MagicMock(returncode=0), # wl-copy (CLIPBOARD) + MagicMock(returncode=0), # wl-copy --primary (PRIMARY) + MagicMock(returncode=0), # ydotool key shift+Insert + ] + + inj = wl.TextInjector(mock_config_wayland) + inj.inject("test") + + assert mock_run.call_count == 4 + assert mock_run.call_args_list[1][0][0][0] == "wl-copy" + assert "--primary" in mock_run.call_args_list[2][0][0] + cmd = mock_run.call_args_list[3][0][0] + assert cmd[0] == "ydotool" + assert "shift+Insert" in cmd + + @patch("app.time.sleep") + @patch("app.subprocess.run") + def test_inject_wayland_custom_paste_keys(self, mock_run, mock_sleep, mock_config_wayland): + """Config paste_keys overrides the paste shortcut sent via ydotool.""" + mock_config_wayland.paste_keys = "ctrl+shift+v" + mock_run.side_effect = [ + FileNotFoundError(), # wtype not found + MagicMock(returncode=0), # wl-copy + MagicMock(returncode=0), # wl-copy --primary + MagicMock(returncode=0), # ydotool key ctrl+shift+v + ] + + inj = wl.TextInjector(mock_config_wayland) + inj.inject("test") + + assert mock_run.call_count == 4 + cmd = mock_run.call_args_list[3][0][0] + assert cmd[0] == "ydotool" + assert "ctrl+shift+v" in cmd + + @patch("app.time.sleep") + @patch("app.subprocess.run") + def test_inject_wayland_wlcopy_xdotool(self, mock_run, mock_sleep, mock_config_wayland): + """wtype fails, ydotool fails → wl-copy + xdotool key.""" + mock_run.side_effect = [ + FileNotFoundError(), # wtype + MagicMock(returncode=0), # wl-copy + MagicMock(returncode=0), # wl-copy --primary + FileNotFoundError(), # ydotool fails + MagicMock(returncode=0), # xdotool key + ] + + inj = wl.TextInjector(mock_config_wayland) + inj.inject("test") + + assert mock_run.call_count == 5 + assert mock_run.call_args_list[1][0][0][0] == "wl-copy" + assert mock_run.call_args_list[4][0][0][0] == "xdotool" + + @patch("app.time.sleep") + @patch("app.subprocess.run") + def test_inject_wayland_xclip_fallback(self, mock_run, mock_sleep, mock_config_wayland): + """When wtype and wl-copy both missing, falls back to xclip via XWayland.""" + mock_run.side_effect = [ + FileNotFoundError(), # wtype + FileNotFoundError(), # wl-copy + MagicMock(returncode=0), # xdotool type (X11 fallback, text is ASCII) + ] + + inj = wl.TextInjector(mock_config_wayland) + inj.inject("test") + + assert mock_run.call_count == 3 + cmd = mock_run.call_args_list[2][0][0] + assert cmd[0] == "xdotool" + + def test_inject_empty_text(self, mock_config): + inj = wl.TextInjector(mock_config) + # Should return without doing anything + inj.inject("") + inj.inject(None) + + +# =================================================================== +# PID file management +# =================================================================== + +class TestPidFile: + def test_write_and_read_pid(self, tmp_pid_file): + wl.WhisperLinuxApp.write_pid() + pid = wl.WhisperLinuxApp.read_pid() + assert pid == os.getpid() + + def test_read_pid_no_file(self, tmp_pid_file): + assert wl.WhisperLinuxApp.read_pid() is None + + def test_read_pid_stale(self, tmp_pid_file): + # Write a PID that doesn't exist + tmp_pid_file.write_text("9999999") + assert wl.WhisperLinuxApp.read_pid() is None + + def test_read_pid_invalid(self, tmp_pid_file): + tmp_pid_file.write_text("not-a-number") + assert wl.WhisperLinuxApp.read_pid() is None + + def test_remove_pid(self, tmp_pid_file): + wl.WhisperLinuxApp.write_pid() + assert tmp_pid_file.exists() + wl.WhisperLinuxApp.remove_pid() + assert not tmp_pid_file.exists() + + def test_remove_pid_nonexistent(self, tmp_pid_file): + # Should not raise + wl.WhisperLinuxApp.remove_pid() + + +# =================================================================== +# WhisperLinuxApp — state machine +# =================================================================== + +class TestWhisperLinuxApp: + def _make_app(self, mock_config): + app = wl.WhisperLinuxApp(mock_config) + app.recorder = MagicMock(spec=wl.AudioRecorder) + app.transcriber = MagicMock(spec=wl.Transcriber) + app.injector = MagicMock(spec=wl.TextInjector) + return app + + def test_initial_state(self, mock_config): + app = self._make_app(mock_config) + assert app.state == wl.AppState.IDLE + + def test_toggle_starts_recording(self, mock_config): + app = self._make_app(mock_config) + app.toggle() + app.recorder.start.assert_called_once() + assert app.state == wl.AppState.RECORDING + + def test_toggle_stops_recording(self, mock_config): + app = self._make_app(mock_config) + app.recorder.stop.return_value = "/tmp/test.wav" + # Prevent the transcription thread from running + app.transcriber.transcribe.return_value = "" + + app.toggle() # IDLE → RECORDING + assert app.state == wl.AppState.RECORDING + + with patch("app.threading.Thread") as mock_thread: + mock_thread.return_value = MagicMock() + app.toggle() # RECORDING → PROCESSING + app.recorder.stop.assert_called_once() + assert app.state == wl.AppState.PROCESSING + mock_thread.assert_called_once() + + def test_toggle_while_processing_does_nothing(self, mock_config): + app = self._make_app(mock_config) + app.state = wl.AppState.PROCESSING + app.toggle() + # Should not call start or stop + app.recorder.start.assert_not_called() + app.recorder.stop.assert_not_called() + + def test_transcribe_and_inject(self, mock_config): + app = self._make_app(mock_config) + app.transcriber.transcribe.return_value = "Привет мир" + + app._transcribe_and_inject("/tmp/test.wav") + + app.transcriber.transcribe.assert_called_once_with("/tmp/test.wav") + app.injector.inject.assert_called_once_with("Привет мир") + assert app.state == wl.AppState.IDLE + + def test_transcribe_empty_result(self, mock_config): + app = self._make_app(mock_config) + app.transcriber.transcribe.return_value = "" + + app._transcribe_and_inject("/tmp/test.wav") + + app.injector.inject.assert_not_called() + assert app.state == wl.AppState.IDLE + + def test_transcribe_error_returns_to_idle(self, mock_config): + app = self._make_app(mock_config) + app.transcriber.transcribe.side_effect = RuntimeError("model failed") + + app._transcribe_and_inject("/tmp/test.wav") + + assert app.state == wl.AppState.IDLE + + def test_quit_cleans_up(self, mock_config, tmp_pid_file): + app = self._make_app(mock_config) + wl.WhisperLinuxApp.write_pid() + assert tmp_pid_file.exists() + + app.quit() + app.recorder.cleanup.assert_called_once() + assert not tmp_pid_file.exists() + + def test_start_recording_failure(self, mock_config): + app = self._make_app(mock_config) + app.recorder.start.side_effect = OSError("no arecord") + + app.toggle() # Should not crash + assert app.state == wl.AppState.IDLE + + def test_stop_recording_no_wav(self, mock_config): + app = self._make_app(mock_config) + app.recorder.stop.return_value = None + + app.state = wl.AppState.RECORDING + app.toggle() + assert app.state == wl.AppState.IDLE + + +# =================================================================== +# send_toggle +# =================================================================== + +class TestSendToggle: + def test_send_toggle_no_running_instance(self, tmp_pid_file): + with pytest.raises(SystemExit): + wl.send_toggle() + + @patch("app.os.kill") + def test_send_toggle_success(self, mock_kill, tmp_pid_file): + tmp_pid_file.write_text(str(os.getpid())) + wl.send_toggle() + # read_pid calls os.kill(pid, 0) to check existence, then send_toggle sends SIGUSR1 + assert mock_kill.call_count == 2 + mock_kill.assert_any_call(os.getpid(), 0) + mock_kill.assert_any_call(os.getpid(), signal.SIGUSR1) + + +# =================================================================== +# CLI argument parsing +# =================================================================== + +class TestCLI: + @patch("app.app.send_toggle") + def test_toggle_flag(self, mock_toggle, monkeypatch): + monkeypatch.setattr("sys.argv", ["app.py", "--toggle"]) + wl.main() + mock_toggle.assert_called_once() + + @patch("app.app.WhisperLinuxApp") + def test_language_override(self, mock_app_cls, monkeypatch, tmp_pid_file, tmp_config_file): + monkeypatch.setattr("sys.argv", ["app.py", "--language", "en"]) + monkeypatch.setattr(wl.config, "CONFIG_FILE", tmp_config_file) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_file.parent) + mock_app = MagicMock() + mock_app_cls.return_value = mock_app + mock_app_cls.read_pid.return_value = None # No running instance + + wl.main() + + # Check that Config was created with language override + config_arg = mock_app_cls.call_args[0][0] + assert config_arg.language == "en" + + def test_already_running(self, tmp_pid_file, monkeypatch, capsys): + tmp_pid_file.write_text(str(os.getpid())) + monkeypatch.setattr("sys.argv", ["app.py"]) + with pytest.raises(SystemExit): + wl.main() + captured = capsys.readouterr() + assert "already running" in captured.err + + +# =================================================================== +# TrayIcon — model download and settings handlers +# =================================================================== + +class TestTrayIconHandlers: + """Test TrayIcon event handlers using mocked app_ref.""" + + def _make_tray_mocked(self, mock_config, tmp_path): + """Create a TrayIcon with fully mocked Qt and app_ref.""" + app_ref = MagicMock() + app_ref.config = mock_config + # Create a model file so _rebuild_model_menu can stat it + model_file = tmp_path / "ggml-base.bin" + model_file.write_bytes(b"\x00" * 1000) + mock_config.model = str(model_file) + + tray = wl.TrayIcon.__new__(wl.TrayIcon) + tray._app_ref = app_ref + tray._downloading = set() + tray._kept_actions = [] + # _marshal_call: execute immediately in tests (no Qt event loop) + tray._marshal_call = lambda func: func() + return tray, app_ref + + @patch("app.subprocess.run") + def test_download_model_no_auto_switch(self, mock_run, mock_config, tmp_path): + """After download completes, model is NOT auto-switched.""" + tray, app_ref = self._make_tray_mocked(mock_config, tmp_path) + tray.notify = MagicMock() + tray._rebuild_model_menu = MagicMock() + original_model = mock_config.model + + # Point models_dir to tmp_path so the downloaded file can be checked + mock_config.models_dir = str(tmp_path) + # Create the "downloaded" model file with a realistic size + model_file = tmp_path / "ggml-tiny.bin" + model_file.write_bytes(b"\x00" * 75_000_000) + + mock_run.return_value = MagicMock(returncode=0, stderr="") + + with patch.object(Path, "is_file", return_value=True): + tray._do_download_model("tiny") + + # Model should NOT have been changed — user decides manually + assert mock_config.model == original_model + + def test_on_threads_changed(self, mock_config): + """Threads handler updates config and saves.""" + mock_config.save = MagicMock() + app_ref = MagicMock() + app_ref.config = mock_config + + tray = wl.TrayIcon.__new__(wl.TrayIcon) + tray._app_ref = app_ref + + mock_group = MagicMock() + mock_action = MagicMock() + mock_action.data.return_value = 8 + mock_group.checkedAction.return_value = mock_action + tray._threads_group = mock_group + + tray._on_threads_changed() + assert mock_config.threads == 8 + mock_config.save.assert_called_once() + + def test_on_gpu_changed(self, mock_config): + """GPU handler updates config and saves.""" + mock_config.save = MagicMock() + app_ref = MagicMock() + app_ref.config = mock_config + + tray = wl.TrayIcon.__new__(wl.TrayIcon) + tray._app_ref = app_ref + + mock_group = MagicMock() + mock_action = MagicMock() + mock_action.data.return_value = 1 + mock_group.checkedAction.return_value = mock_action + tray._gpu_group = mock_group + + tray._on_gpu_changed() + assert mock_config.gpu_device == 1 + mock_config.save.assert_called_once() + + def test_on_audio_changed(self, mock_config): + """Audio handler updates config and saves.""" + mock_config.save = MagicMock() + app_ref = MagicMock() + app_ref.config = mock_config + + tray = wl.TrayIcon.__new__(wl.TrayIcon) + tray._app_ref = app_ref + + mock_group = MagicMock() + mock_action = MagicMock() + mock_action.data.return_value = "plughw:1,0" + mock_group.checkedAction.return_value = mock_action + tray._audio_group = mock_group + + tray._on_audio_changed() + assert mock_config.audio_device == "plughw:1,0" + mock_config.save.assert_called_once() + + def test_on_paste_keys_changed(self, mock_config): + """Paste keys handler updates config and saves.""" + mock_config.save = MagicMock() + app_ref = MagicMock() + app_ref.config = mock_config + + tray = wl.TrayIcon.__new__(wl.TrayIcon) + tray._app_ref = app_ref + + mock_group = MagicMock() + mock_action = MagicMock() + mock_action.data.return_value = "ctrl+shift+v" + mock_group.checkedAction.return_value = mock_action + tray._paste_group = mock_group + + tray._on_paste_keys_changed() + assert mock_config.paste_keys == "ctrl+shift+v" + mock_config.save.assert_called_once() + + def test_download_marks_downloading(self, mock_config, tmp_path): + """_on_download_model adds name to _downloading set.""" + tray, app_ref = self._make_tray_mocked(mock_config, tmp_path) + tray.notify = MagicMock() + tray._rebuild_model_menu = MagicMock() + + with patch("app.threading.Thread") as mock_thread: + mock_thread.return_value = MagicMock() + tray._on_download_model("small") + + assert "small" in tray._downloading + tray._rebuild_model_menu.assert_called_once() + + def test_download_duplicate_ignored(self, mock_config, tmp_path): + """Second download of same model is ignored.""" + tray, app_ref = self._make_tray_mocked(mock_config, tmp_path) + tray.notify = MagicMock() + tray._rebuild_model_menu = MagicMock() + tray._downloading.add("small") + + with patch("app.threading.Thread") as mock_thread: + tray._on_download_model("small") + mock_thread.assert_not_called() + + +# =================================================================== +# _write_wav helper +# =================================================================== + +class TestWriteWav: + def test_write_wav_creates_valid_file(self, tmp_path): + path = str(tmp_path / "test.wav") + pcm = b"\x00\x01" * 1600 # 100ms of s16le mono + wl._write_wav(path, pcm) + + data = Path(path).read_bytes() + assert data[:4] == b"RIFF" + assert data[8:12] == b"WAVE" + assert data[12:16] == b"fmt " + assert data[36:40] == b"data" + + def test_write_wav_correct_size(self, tmp_path): + path = str(tmp_path / "test.wav") + pcm = b"\x00" * 3200 # 100ms + wl._write_wav(path, pcm) + + import struct + data = Path(path).read_bytes() + # RIFF size = 36 + data_size + riff_size = struct.unpack_from(" 0 + + def test_short_speech_discarded(self): + """Speech shorter than min_speech_ms is not emitted.""" + import array, math + callback = MagicMock() + cfg = self._make_config() + cfg.min_speech_ms = 2000 # 2 seconds minimum + vad = wl.SimpleVAD(cfg, on_speech_end=callback) + # 100ms of loud audio + samples = array.array("h") + for i in range(1600): + samples.append(int(10000 * math.sin(2 * math.pi * 440 * i / 16000))) + short_speech = samples.tobytes() + silence = b"\x00\x00" * 16000 + vad.feed(short_speech) + vad.feed(silence) + callback.assert_not_called() + + def test_reset_clears_state(self, speech_pcm): + callback = MagicMock() + vad = wl.SimpleVAD(self._make_config(), on_speech_end=callback) + vad.feed(speech_pcm) + vad.reset() + assert not vad._in_speech + assert len(vad._speech_buffer) == 0 + + def test_rms_silence(self): + rms = wl.SimpleVAD._rms(b"\x00\x00" * 100) + assert rms == 0.0 + + def test_rms_loud(self): + import array + samples = array.array("h", [10000] * 100) + rms = wl.SimpleVAD._rms(samples.tobytes()) + assert rms == 10000.0 + + def test_rms_empty(self): + assert wl.SimpleVAD._rms(b"") == 0.0 + assert wl.SimpleVAD._rms(b"\x00") == 0.0 + + def test_speech_start_callback(self, speech_pcm): + start_cb = MagicMock() + end_cb = MagicMock() + vad = wl.SimpleVAD(self._make_config(), on_speech_end=end_cb, + on_speech_start=start_cb) + vad.feed(speech_pcm) + start_cb.assert_called_once() + + def test_max_speech_enforced(self): + """Speech exceeding max_speech_s is force-emitted.""" + import array, math + callback = MagicMock() + cfg = self._make_config() + cfg.max_speech_s = 0.5 # 500ms max + vad = wl.SimpleVAD(cfg, on_speech_end=callback) + # Feed 1 second of speech (exceeds 500ms max) + samples = array.array("h") + for i in range(16000): + samples.append(int(10000 * math.sin(2 * math.pi * 440 * i / 16000))) + vad.feed(samples.tobytes()) + callback.assert_called_once() + + +# =================================================================== +# WakeWordDetector +# =================================================================== + +class TestWakeWordDetector: + def test_exact_match(self): + d = wl.WakeWordDetector("дуняша") + assert d.contains_wake_word("дуняша привет") is True + + def test_exact_match_case_insensitive(self): + d = wl.WakeWordDetector("дуняша") + assert d.contains_wake_word("Дуняша, привет") is True + + def test_no_match(self): + d = wl.WakeWordDetector("дуняша") + assert d.contains_wake_word("привет мир") is False + + def test_fuzzy_match(self): + d = wl.WakeWordDetector("дуняша") + # Close misspelling + assert d.contains_wake_word("дуняшя") is True + + def test_strip_wake_word(self): + d = wl.WakeWordDetector("дуняша") + result = d.strip_wake_word("дуняша привет мир") + assert "дуняша" not in result.lower() + assert "привет" in result + + def test_strip_wake_word_only(self): + d = wl.WakeWordDetector("дуняша") + result = d.strip_wake_word("дуняша") + assert result == "" + + def test_strip_wake_word_with_punctuation(self): + d = wl.WakeWordDetector("дуняша") + result = d.strip_wake_word("Дуняша, запиши") + assert "запиши" in result + assert "дуняша" not in result.lower() + + def test_english_wake_word(self): + d = wl.WakeWordDetector("computer") + assert d.contains_wake_word("Hey computer, write this") is True + assert d.contains_wake_word("Hey cortana, write this") is False + + def test_empty_text(self): + d = wl.WakeWordDetector("дуняша") + assert d.contains_wake_word("") is False + assert d.strip_wake_word("") == "" + + +# =================================================================== +# AudioStream +# =================================================================== + +class TestAudioStream: + @patch("app.time.sleep") + @patch("app.subprocess.Popen") + @patch("app.AudioRecorder._find_pw_record", return_value="/usr/bin/pw-record") + def test_start_stop(self, mock_pw, mock_popen, mock_sleep): + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_proc.stdout.read.return_value = b"" # Will end reader loop + mock_popen.return_value = mock_proc + + stream = wl.AudioStream(MagicMock(audio_device="auto")) + callback = MagicMock() + stream.start(callback) + + assert stream.is_running or True # process may have ended from mock + stream.stop() + mock_proc.send_signal.assert_called_with(signal.SIGINT) + + @patch("app.time.sleep") + @patch("app.subprocess.Popen") + @patch("app.AudioRecorder._find_pw_record", return_value="/usr/bin/pw-record") + def test_start_while_running_raises(self, mock_pw, mock_popen, mock_sleep): + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + # Block the reader thread so it doesn't clear _running + mock_proc.stdout.read.side_effect = lambda *a, **kw: threading.Event().wait(0.5) or b"" + mock_popen.return_value = mock_proc + + stream = wl.AudioStream(MagicMock(audio_device="auto")) + stream.start(MagicMock()) + with pytest.raises(RuntimeError, match="already running"): + stream.start(MagicMock()) + stream.stop() + + @patch("app.subprocess.Popen") + @patch("app.AudioRecorder._find_pw_record", return_value="/usr/bin/pw-record") + def test_start_fails(self, mock_pw, mock_popen): + mock_proc = MagicMock() + mock_proc.poll.return_value = 1 + mock_proc.stderr.read.return_value = b"Connection refused" + mock_popen.return_value = mock_proc + + stream = wl.AudioStream(MagicMock(audio_device="auto")) + with pytest.raises(RuntimeError, match="AudioStream failed"): + stream.start(MagicMock()) + + @patch("app.AudioRecorder._find_pw_record", return_value=None) + def test_fallback_to_arecord(self, mock_pw): + stream = wl.AudioStream(MagicMock(audio_device="plughw:1,0")) + cmd = stream._build_stream_cmd() + assert cmd[0] == "arecord" + assert "-D" in cmd + assert "plughw:1,0" in cmd + + +# =================================================================== +# Config — streaming fields +# =================================================================== + +class TestConfigStreaming: + def test_default_mode_fields(self, mock_config): + assert mock_config.input_mode == "hotkey" + assert mock_config.output_mode == "batch" + assert mock_config.wake_word == "марфуша" + assert mock_config.silence_timeout == 3.0 + assert mock_config.vad_threshold == 300 + assert mock_config.min_speech_ms == 500 + assert mock_config.max_speech_s == 30.0 + + def test_mode_fields_from_file(self, tmp_config_dir, monkeypatch): + config_file = tmp_config_dir / "config.ini" + config_file.write_text( + "[whisper-linux]\n" + "whisper_cli = /usr/bin/whisper-cli\n" + "model = /tmp/ggml-base.bin\n" + "models_dir = /tmp\n" + "language = ru\n" + "threads = 4\n" + "display_server = x11\n" + "audio_device = plughw:1,0\n" + "input_mode = listen\n" + "output_mode = stream\n" + "wake_word = hello\n" + "silence_timeout = 5.0\n" + "vad_threshold = 500\n" + "min_speech_ms = 200\n" + "max_speech_s = 60.0\n" + ) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_dir) + monkeypatch.setattr(wl.config, "CONFIG_FILE", config_file) + config = wl.Config() + assert config.input_mode == "listen" + assert config.output_mode == "stream" + assert config.wake_word == "hello" + assert config.silence_timeout == 5.0 + assert config.vad_threshold == 500 + assert config.min_speech_ms == 200 + assert config.max_speech_s == 60.0 + + def test_backward_compat_old_mode_stream(self, tmp_config_dir, monkeypatch): + """Old config with mode=stream maps to input_mode=listen, output_mode=stream.""" + config_file = tmp_config_dir / "config.ini" + config_file.write_text( + "[whisper-linux]\n" + "whisper_cli = /usr/bin/whisper-cli\n" + "model = /tmp/ggml-base.bin\n" + "models_dir = /tmp\n" + "display_server = x11\n" + "audio_device = plughw:1,0\n" + "mode = stream\n" + ) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_dir) + monkeypatch.setattr(wl.config, "CONFIG_FILE", config_file) + config = wl.Config() + assert config.input_mode == "listen" + assert config.output_mode == "stream" + + def test_backward_compat_old_mode_manual(self, tmp_config_dir, monkeypatch): + """Old config with mode=manual maps to input_mode=hotkey, output_mode=batch.""" + config_file = tmp_config_dir / "config.ini" + config_file.write_text( + "[whisper-linux]\n" + "whisper_cli = /usr/bin/whisper-cli\n" + "model = /tmp/ggml-base.bin\n" + "models_dir = /tmp\n" + "display_server = x11\n" + "audio_device = plughw:1,0\n" + "mode = manual\n" + ) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_dir) + monkeypatch.setattr(wl.config, "CONFIG_FILE", config_file) + config = wl.Config() + assert config.input_mode == "hotkey" + assert config.output_mode == "batch" + + def test_new_fields_override_old_mode(self, tmp_config_dir, monkeypatch): + """New input_mode/output_mode take precedence over old mode field.""" + config_file = tmp_config_dir / "config.ini" + config_file.write_text( + "[whisper-linux]\n" + "whisper_cli = /usr/bin/whisper-cli\n" + "model = /tmp/ggml-base.bin\n" + "models_dir = /tmp\n" + "display_server = x11\n" + "audio_device = plughw:1,0\n" + "mode = stream\n" + "input_mode = hotkey\n" + "output_mode = batch\n" + ) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_dir) + monkeypatch.setattr(wl.config, "CONFIG_FILE", config_file) + config = wl.Config() + assert config.input_mode == "hotkey" + assert config.output_mode == "batch" + + def test_save_mode_fields(self, mock_config, tmp_config_file): + mock_config.input_mode = "listen" + mock_config.output_mode = "stream" + mock_config.wake_word = "ok dunyasha" + mock_config.save() + text = tmp_config_file.read_text() + assert "input_mode = listen" in text + assert "output_mode = stream" in text + assert "wake_word = ok dunyasha" in text + # Old "mode" field should NOT be saved + assert "\nmode = " not in text + + +# =================================================================== +# WhisperLinuxApp — streaming state machine +# =================================================================== + +class TestWhisperLinuxAppStreaming: + def _make_app(self, config): + app = wl.WhisperLinuxApp(config) + app.recorder = MagicMock(spec=wl.AudioRecorder) + app.transcriber = MagicMock(spec=wl.Transcriber) + app.injector = MagicMock(spec=wl.TextInjector) + return app + + def test_toggle_hotkey_batch_mode(self, mock_config): + """In hotkey+batch mode, toggle starts recording.""" + mock_config.input_mode = "hotkey" + mock_config.output_mode = "batch" + app = self._make_app(mock_config) + app.toggle() + app.recorder.start.assert_called_once() + assert app.state == wl.AppState.RECORDING + + @patch("app.app.AudioStream") + def test_toggle_listen_starts_listening(self, mock_stream_cls, mock_config_stream): + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app.toggle() + + assert app.state == wl.AppState.LISTENING + mock_stream.start.assert_called_once() + + @patch("app.app.AudioStream") + def test_toggle_listen_hotkey_starts_dictating(self, mock_stream_cls, mock_config_stream): + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app.toggle() # IDLE → LISTENING + assert app.state == wl.AppState.LISTENING + app.toggle() # LISTENING → DICTATING (hotkey trigger) + assert app.state == wl.AppState.DICTATING + + @patch("app.app.AudioStream") + def test_toggle_listen_stops_dictating(self, mock_stream_cls, mock_config_stream): + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app.toggle() # IDLE → LISTENING + app.state = wl.AppState.DICTATING # simulate wake word + app.toggle() # DICTATING → LISTENING + assert app.state == wl.AppState.LISTENING + + @patch("app.app.AudioStream") + def test_process_segment_wake_word_starts_dictating(self, mock_stream_cls, mock_config_stream): + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.LISTENING + app.transcriber.transcribe.return_value = "дуняша привет" + + app._process_speech_segment(b"\x00" * 32000) + + assert app.state == wl.AppState.DICTATING + # Wake word segment never injects text — it's purely a toggle + app.injector.inject.assert_not_called() + + @patch("app.app.AudioStream") + def test_process_segment_wake_word_stops_dictating(self, mock_stream_cls, mock_config_stream): + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.DICTATING + app.transcriber.transcribe.return_value = "дуняша" + + app._process_speech_segment(b"\x00" * 32000) + + assert app.state == wl.AppState.LISTENING + # Wake word alone — nothing to inject + app.injector.inject.assert_not_called() + + @patch("app.app.AudioStream") + def test_process_segment_wake_word_injects_preceding_text(self, mock_stream_cls, mock_config_stream): + """Text before wake word is injected when stopping dictation.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.DICTATING + app.transcriber.transcribe.return_value = "еще пять раз дуняша." + + app._process_speech_segment(b"\x00" * 32000) + + assert app.state == wl.AppState.LISTENING + app.injector.inject.assert_called_once_with("еще пять раз") + + @patch("app.app.AudioStream") + def test_process_segment_injects_text_when_dictating_stream(self, mock_stream_cls, mock_config_stream): + """In stream output mode, text is injected immediately.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.DICTATING + app.transcriber.transcribe.return_value = "привет мир" + + app._process_speech_segment(b"\x00" * 32000) + + app.injector.inject.assert_called_once_with("привет мир") + assert app.state == wl.AppState.DICTATING + + @patch("app.app.AudioStream") + def test_process_segment_no_inject_when_listening(self, mock_stream_cls, mock_config_stream): + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.LISTENING + app.transcriber.transcribe.return_value = "привет мир" + + app._process_speech_segment(b"\x00" * 32000) + + app.injector.inject.assert_not_called() + + @patch("app.app.AudioStream") + def test_state_set_immediately_not_via_marshal(self, mock_stream_cls, mock_config_stream): + """State is set immediately in _process_speech_segment, not deferred via Qt.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.LISTENING + app.transcriber.transcribe.return_value = "дуняша" + + app._process_speech_segment(b"\x00" * 32000) + + # State is DICTATING immediately after processing (not deferred) + assert app.state == wl.AppState.DICTATING + + @patch("app.app.AudioStream") + def test_sequential_segments_correct_state(self, mock_stream_cls, mock_config_stream): + """Simulate: wake word → text → wake word. Text must be injected.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.LISTENING + + # Segment 1: wake word → DICTATING + app.transcriber.transcribe.return_value = "дуняша" + app._process_speech_segment(b"\x00" * 32000) + assert app.state == wl.AppState.DICTATING + + # Segment 2: actual text → should be injected + app.transcriber.transcribe.return_value = "привет мир" + app._process_speech_segment(b"\x00" * 32000) + app.injector.inject.assert_called_once_with("привет мир") + assert app.state == wl.AppState.DICTATING + + # Segment 3: wake word → LISTENING + app.transcriber.transcribe.return_value = "дуняша" + app._process_speech_segment(b"\x00" * 32000) + assert app.state == wl.AppState.LISTENING + # Only one inject call (the text, not the wake words) + app.injector.inject.assert_called_once() + + def test_silence_timer_returns_to_listening(self, mock_config_stream): + app = self._make_app(mock_config_stream) + app.state = wl.AppState.DICTATING + app._on_silence_timeout() + assert app.state == wl.AppState.LISTENING + + def test_silence_timer_ignored_when_not_dictating(self, mock_config_stream): + app = self._make_app(mock_config_stream) + app.state = wl.AppState.IDLE + app._on_silence_timeout() + assert app.state == wl.AppState.IDLE + + def test_speech_start_cancels_silence_timer(self, mock_config_stream): + """When VAD detects speech start, silence timer is cancelled.""" + app = self._make_app(mock_config_stream) + app.state = wl.AppState.DICTATING + app._reset_silence_timer() + assert app._silence_timer is not None + app._on_speech_start() + assert app._silence_timer is None + + def test_speech_start_ignored_when_not_dictating(self, mock_config_stream): + """on_speech_start does not cancel timer when not in DICTATING.""" + app = self._make_app(mock_config_stream) + app.state = wl.AppState.LISTENING + app._on_speech_start() + # No crash, no side effects + assert app._silence_timer is None + + @patch("app.app.AudioStream") + def test_timer_starts_on_wake_word_and_after_injection(self, mock_stream_cls, mock_config_stream): + """Timer is started both when wake word detected AND after text injection.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.LISTENING + + # Wake word → DICTATING: timer SHOULD be set immediately + app.transcriber.transcribe.return_value = "дуняша" + app._process_speech_segment(b"\x00" * 32000) + assert app.state == wl.AppState.DICTATING + assert app._silence_timer is not None + app._cancel_silence_timer() + + # Text injection → timer SHOULD be reset + app.transcriber.transcribe.return_value = "привет мир" + app._process_speech_segment(b"\x00" * 32000) + assert app._silence_timer is not None + app._cancel_silence_timer() # cleanup + + @patch("app.app.AudioStream") + def test_speech_end_cancels_timer_before_queue(self, mock_stream_cls, mock_config_stream): + """_on_speech_end cancels timer when dictating (not resets it).""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app.state = wl.AppState.DICTATING + app._segment_queue = MagicMock() + app._reset_silence_timer() + assert app._silence_timer is not None + + app._on_speech_end(b"\x00" * 32000) + assert app._silence_timer is None + app._segment_queue.put.assert_called_once() + + @patch("app.app.AudioStream") + def test_force_idle_from_listening(self, mock_stream_cls, mock_config_stream): + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app.toggle() # → LISTENING + app._force_idle() + assert app.state == wl.AppState.IDLE + + def test_force_idle_from_recording(self, mock_config): + app = self._make_app(mock_config) + app.state = wl.AppState.RECORDING + app._force_idle() + app.recorder.stop.assert_called_once() + assert app.state == wl.AppState.IDLE + + @patch("app.app.AudioStream") + def test_quit_cleans_up_stream(self, mock_stream_cls, mock_config_stream, tmp_pid_file): + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._audio_stream = mock_stream + wl.WhisperLinuxApp.write_pid() + app.quit() + + mock_stream.stop.assert_called_once() + app.recorder.cleanup.assert_called_once() + + +# =================================================================== +# WhisperLinuxApp — hotkey+stream mode +# =================================================================== + +class TestHotkeyStream: + def _make_app(self, config): + app = wl.WhisperLinuxApp(config) + app.recorder = MagicMock(spec=wl.AudioRecorder) + app.transcriber = MagicMock(spec=wl.Transcriber) + app.injector = MagicMock(spec=wl.TextInjector) + return app + + @patch("app.app.AudioStream") + def test_toggle_starts_dictating(self, mock_stream_cls, mock_config_hotkey_stream): + """Hotkey+stream: toggle goes directly to DICTATING.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_hotkey_stream) + app.toggle() + + assert app.state == wl.AppState.DICTATING + mock_stream.start.assert_called_once() + assert app._wake_detector is None # no wake word in hotkey mode + + @patch("app.app.AudioStream") + def test_toggle_stops_dictating(self, mock_stream_cls, mock_config_hotkey_stream): + """Hotkey+stream: second toggle stops dictating.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_hotkey_stream) + app.toggle() # IDLE → DICTATING + assert app.state == wl.AppState.DICTATING + app.toggle() # DICTATING → IDLE + assert app.state == wl.AppState.IDLE + + @patch("app.app.AudioStream") + def test_text_injected_immediately(self, mock_stream_cls, mock_config_hotkey_stream): + """Hotkey+stream: text is injected per segment.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_hotkey_stream) + app.state = wl.AppState.DICTATING + app.transcriber.transcribe.return_value = "привет мир" + + app._process_speech_segment(b"\x00" * 32000) + + app.injector.inject.assert_called_once_with("привет мир") + + @patch("app.app.AudioStream") + def test_no_silence_timer_in_hotkey_mode(self, mock_stream_cls, mock_config_hotkey_stream): + """Hotkey+stream: no silence timer (user presses hotkey to stop).""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_hotkey_stream) + app.state = wl.AppState.DICTATING + app.transcriber.transcribe.return_value = "привет мир" + + app._process_speech_segment(b"\x00" * 32000) + + assert app._silence_timer is None + + @patch("app.app.AudioStream") + def test_start_signal_on_hotkey_stream(self, mock_stream_cls, mock_config_hotkey_stream): + """Hotkey+stream: start signal plays when dictation begins.""" + mock_config_hotkey_stream.end_signal = True + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_hotkey_stream) + app._play_start_signal = MagicMock() + app.toggle() + + assert app.state == wl.AppState.DICTATING + app._play_start_signal.assert_called_once() + + @patch("app.app.AudioStream") + def test_end_signal_on_hotkey_stream_stop(self, mock_stream_cls, mock_config_hotkey_stream): + """Hotkey+stream: end signal plays when dictation stops.""" + mock_config_hotkey_stream.end_signal = True + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_hotkey_stream) + app._play_start_signal = MagicMock() + app._play_end_signal = MagicMock() + app.toggle() # start + app.toggle() # stop + + assert app.state == wl.AppState.IDLE + app._play_end_signal.assert_called_once() + + @patch("app.app.AudioStream") + def test_no_signal_when_disabled(self, mock_stream_cls, mock_config_hotkey_stream): + """Hotkey+stream: no signals when end_signal is False.""" + mock_config_hotkey_stream.end_signal = False + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_hotkey_stream) + app._play_start_signal = MagicMock() + app._play_end_signal = MagicMock() + app.toggle() # start + app.toggle() # stop + app._play_start_signal.assert_called_once() # method called but returns early + app._play_end_signal.assert_called_once() # method called but returns early + + +# =================================================================== +# WhisperLinuxApp — batch mode end signal +# =================================================================== + +class TestBatchEndSignal: + def _make_app(self, config): + app = wl.WhisperLinuxApp(config) + app.recorder = MagicMock(spec=wl.AudioRecorder) + app.transcriber = MagicMock(spec=wl.Transcriber) + app.injector = MagicMock(spec=wl.TextInjector) + return app + + def test_end_signal_after_transcription(self, mock_config): + """Batch mode: end signal plays after transcription completes.""" + mock_config.end_signal = True + app = self._make_app(mock_config) + app.transcriber.transcribe.return_value = "Привет мир" + app._play_end_signal = MagicMock() + + app._transcribe_and_inject("/tmp/test.wav") + + assert app.state == wl.AppState.IDLE + app._play_end_signal.assert_called_once() + + def test_end_signal_after_empty_transcription(self, mock_config): + """Batch mode: end signal plays even on empty transcription.""" + mock_config.end_signal = True + app = self._make_app(mock_config) + app.transcriber.transcribe.return_value = "" + app._play_end_signal = MagicMock() + + app._transcribe_and_inject("/tmp/test.wav") + + assert app.state == wl.AppState.IDLE + app._play_end_signal.assert_called_once() + + +# =================================================================== +# WhisperLinuxApp — listen+batch mode +# =================================================================== + +class TestListenBatch: + def _make_app(self, config): + app = wl.WhisperLinuxApp(config) + app.recorder = MagicMock(spec=wl.AudioRecorder) + app.transcriber = MagicMock(spec=wl.Transcriber) + app.injector = MagicMock(spec=wl.TextInjector) + return app + + @patch("app.app.AudioStream") + def test_toggle_starts_listening(self, mock_stream_cls, mock_config_listen_batch): + """Listen+batch: toggle starts LISTENING.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_listen_batch) + app.toggle() + + assert app.state == wl.AppState.LISTENING + + @patch("app.app.AudioStream") + def test_text_accumulated_not_injected(self, mock_stream_cls, mock_config_listen_batch): + """Listen+batch: text is accumulated, not injected per segment.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_listen_batch) + app._wake_detector = wl.WakeWordDetector(mock_config_listen_batch.wake_word) + app.state = wl.AppState.DICTATING + app.transcriber.transcribe.return_value = "привет мир" + + app._process_speech_segment(b"\x00" * 32000) + + # Text NOT injected immediately + app.injector.inject.assert_not_called() + assert app._accumulated_texts == ["привет мир"] + + @patch("app.app.AudioStream") + def test_accumulated_text_flushed_on_wake_word_stop(self, mock_stream_cls, mock_config_listen_batch): + """Listen+batch: accumulated text flushed when wake word stops dictation.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_listen_batch) + app._wake_detector = wl.WakeWordDetector(mock_config_listen_batch.wake_word) + app.state = wl.AppState.DICTATING + app._accumulated_texts = ["привет", "мир"] + app.transcriber.transcribe.return_value = "дуняша" + + app._process_speech_segment(b"\x00" * 32000) + + assert app.state == wl.AppState.LISTENING + app.injector.inject.assert_called_once_with("привет мир") + assert app._accumulated_texts == [] + + @patch("app.app.AudioStream") + def test_text_before_wake_word_accumulated_in_batch(self, mock_stream_cls, mock_config_listen_batch): + """Listen+batch: text before wake word is added to accumulated, then flushed.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_listen_batch) + app._wake_detector = wl.WakeWordDetector(mock_config_listen_batch.wake_word) + app.state = wl.AppState.DICTATING + app._accumulated_texts = ["первая часть"] + app.transcriber.transcribe.return_value = "вторая часть дуняша" + + app._process_speech_segment(b"\x00" * 32000) + + assert app.state == wl.AppState.LISTENING + app.injector.inject.assert_called_once_with("первая часть вторая часть") + + @patch("app.app.AudioStream") + def test_accumulated_text_flushed_on_silence_timeout(self, mock_stream_cls, mock_config_listen_batch): + """Listen+batch: accumulated text flushed on silence timeout.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_listen_batch) + app.state = wl.AppState.DICTATING + app._accumulated_texts = ["один", "два", "три"] + + app._on_silence_timeout() + + assert app.state == wl.AppState.LISTENING + app.injector.inject.assert_called_once_with("один два три") + assert app._accumulated_texts == [] + + @patch("app.app.AudioStream") + def test_multiple_segments_accumulated(self, mock_stream_cls, mock_config_listen_batch): + """Listen+batch: multiple segments accumulate text.""" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_listen_batch) + app._wake_detector = wl.WakeWordDetector(mock_config_listen_batch.wake_word) + app.state = wl.AppState.DICTATING + + app.transcriber.transcribe.return_value = "первый" + app._process_speech_segment(b"\x00" * 32000) + app.transcriber.transcribe.return_value = "второй" + app._process_speech_segment(b"\x00" * 32000) + app.transcriber.transcribe.return_value = "третий" + app._process_speech_segment(b"\x00" * 32000) + + app.injector.inject.assert_not_called() + assert app._accumulated_texts == ["первый", "второй", "третий"] + + +# =================================================================== +# CLI — streaming arguments +# =================================================================== + +class TestCLIStreaming: + @patch("app.app.WhisperLinuxApp") + def test_stream_flag(self, mock_app_cls, monkeypatch, tmp_pid_file, tmp_config_file): + monkeypatch.setattr("sys.argv", ["app.py", "--stream"]) + monkeypatch.setattr(wl.config, "CONFIG_FILE", tmp_config_file) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_file.parent) + mock_app = MagicMock() + mock_app_cls.return_value = mock_app + mock_app_cls.read_pid.return_value = None + + wl.main() + + config_arg = mock_app_cls.call_args[0][0] + assert config_arg.output_mode == "stream" + + @patch("app.app.WhisperLinuxApp") + def test_input_mode_flag(self, mock_app_cls, monkeypatch, tmp_pid_file, tmp_config_file): + monkeypatch.setattr("sys.argv", ["app.py", "--input-mode", "listen"]) + monkeypatch.setattr(wl.config, "CONFIG_FILE", tmp_config_file) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_file.parent) + mock_app = MagicMock() + mock_app_cls.return_value = mock_app + mock_app_cls.read_pid.return_value = None + + wl.main() + + config_arg = mock_app_cls.call_args[0][0] + assert config_arg.input_mode == "listen" + + @patch("app.app.WhisperLinuxApp") + def test_output_mode_flag(self, mock_app_cls, monkeypatch, tmp_pid_file, tmp_config_file): + monkeypatch.setattr("sys.argv", ["app.py", "--output-mode", "stream"]) + monkeypatch.setattr(wl.config, "CONFIG_FILE", tmp_config_file) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_file.parent) + mock_app = MagicMock() + mock_app_cls.return_value = mock_app + mock_app_cls.read_pid.return_value = None + + wl.main() + + config_arg = mock_app_cls.call_args[0][0] + assert config_arg.output_mode == "stream" + + @patch("app.app.WhisperLinuxApp") + def test_wake_word_flag(self, mock_app_cls, monkeypatch, tmp_pid_file, tmp_config_file): + monkeypatch.setattr("sys.argv", ["app.py", "--wake-word", "компьютер"]) + monkeypatch.setattr(wl.config, "CONFIG_FILE", tmp_config_file) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_file.parent) + mock_app = MagicMock() + mock_app_cls.return_value = mock_app + mock_app_cls.read_pid.return_value = None + + wl.main() + + config_arg = mock_app_cls.call_args[0][0] + assert config_arg.wake_word == "компьютер" + + +# =================================================================== +# End signal player +# =================================================================== + +class TestEndSignal: + def _make_app(self, mock_config): + app = wl.WhisperLinuxApp(mock_config) + app.recorder = MagicMock(spec=wl.AudioRecorder) + app.transcriber = MagicMock(spec=wl.Transcriber) + app.injector = MagicMock(spec=wl.TextInjector) + return app + + @patch("app.subprocess.Popen") + @patch("app.os.path.isfile", return_value=True) + def test_play_end_signal_pw_play(self, mock_isfile, mock_popen, mock_config): + """pw-play is tried first when sound file exists.""" + mock_config.end_signal = True + app = self._make_app(mock_config) + app._play_end_signal() + mock_popen.assert_called_once() + cmd = mock_popen.call_args[0][0] + assert cmd[0] == "pw-play" + + @patch("app.subprocess.Popen") + @patch("app.os.path.isfile", return_value=True) + def test_play_end_signal_fallback_paplay(self, mock_isfile, mock_popen, mock_config): + """Falls back to paplay when pw-play fails.""" + mock_config.end_signal = True + mock_popen.side_effect = [FileNotFoundError(), MagicMock()] + app = self._make_app(mock_config) + app._play_end_signal() + assert mock_popen.call_count == 2 + assert mock_popen.call_args_list[1][0][0][0] == "paplay" + + @patch("app.subprocess.Popen") + @patch("app.os.path.isfile", return_value=True) + def test_play_end_signal_fallback_canberra(self, mock_isfile, mock_popen, mock_config): + """Falls back to canberra-gtk-play when pw-play and paplay fail.""" + mock_config.end_signal = True + mock_popen.side_effect = [FileNotFoundError(), FileNotFoundError(), MagicMock()] + app = self._make_app(mock_config) + app._play_end_signal() + assert mock_popen.call_count == 3 + assert mock_popen.call_args_list[2][0][0][0] == "canberra-gtk-play" + + def test_play_end_signal_disabled(self, mock_config): + """Does nothing when end_signal is False.""" + mock_config.end_signal = False + app = self._make_app(mock_config) + with patch("app.subprocess.Popen") as mock_popen: + app._play_end_signal() + mock_popen.assert_not_called() + + +# =================================================================== +# TrayIcon — silence timeout text input handler +# =================================================================== + +class TestSilenceTimeoutHandler: + def test_on_silence_timeout_spin(self, mock_config): + """Silence timeout spinbox handler updates config and saves.""" + mock_config.save = MagicMock() + app_ref = MagicMock() + app_ref.config = mock_config + + tray = wl.TrayIcon.__new__(wl.TrayIcon) + tray._app_ref = app_ref + + tray._on_silence_timeout_spin(5.0) + + assert mock_config.silence_timeout == 5.0 + mock_config.save.assert_called_once() + + +# =================================================================== +# TrayIcon — wake model handler +# =================================================================== + +class TestWakeModelHandler: + def test_on_wake_model_changed(self, mock_config): + """Wake model handler updates config and saves.""" + mock_config.save = MagicMock() + app_ref = MagicMock() + app_ref.config = mock_config + + tray = wl.TrayIcon.__new__(wl.TrayIcon) + tray._app_ref = app_ref + + mock_group = MagicMock() + mock_action = MagicMock() + mock_action.data.return_value = "/tmp/ggml-tiny.bin" + mock_group.checkedAction.return_value = mock_action + tray._wake_model_group = mock_group + + tray._on_wake_model_changed() + assert mock_config.wake_model == "/tmp/ggml-tiny.bin" + mock_config.save.assert_called_once() + + def test_on_wake_model_changed_same_as_main(self, mock_config): + """Setting wake model to empty string means 'same as main'.""" + mock_config.save = MagicMock() + app_ref = MagicMock() + app_ref.config = mock_config + + tray = wl.TrayIcon.__new__(wl.TrayIcon) + tray._app_ref = app_ref + + mock_group = MagicMock() + mock_action = MagicMock() + mock_action.data.return_value = "" + mock_group.checkedAction.return_value = mock_action + tray._wake_model_group = mock_group + + tray._on_wake_model_changed() + assert mock_config.wake_model == "" + mock_config.save.assert_called_once() + + +# =================================================================== +# WakeWordDetector — fuzzy strip +# =================================================================== + +class TestWakeWordFuzzyStrip: + def test_strip_fuzzy_match(self): + """Fuzzy match (e.g. 'Морфуша' for 'марфуша') is stripped.""" + d = wl.WakeWordDetector("марфуша") + result = d.strip_wake_word("Морфуша текст после") + assert "морфуша" not in result.lower() + assert "морфуша" not in result.lower() + assert "текст после" in result + + def test_strip_fuzzy_match_case_variant(self): + """Fuzzy match with different casing is stripped.""" + d = wl.WakeWordDetector("марфуша") + result = d.strip_wake_word("МАРФУША привет") + assert "марфуша" not in result.lower() + assert "привет" in result + + def test_strip_preserves_surrounding_text(self): + """Text before and after fuzzy-matched wake word is preserved.""" + d = wl.WakeWordDetector("марфуша") + result = d.strip_wake_word("раз два Морфуша три четыре") + assert "раз" in result + assert "два" in result + assert "три" in result + assert "четыре" in result + + def test_is_fuzzy_match_above_threshold(self): + """Words close enough to wake word match.""" + d = wl.WakeWordDetector("марфуша") + assert d._is_fuzzy_match("морфуша") is True + + def test_is_fuzzy_match_below_threshold(self): + """Completely different words do not match.""" + d = wl.WakeWordDetector("марфуша") + assert d._is_fuzzy_match("привет") is False + + +# =================================================================== +# Silence timeout — VAD speech-active check +# =================================================================== + +class TestSilenceTimeoutVAD: + def _make_app(self, config): + app = wl.WhisperLinuxApp(config) + app.recorder = MagicMock(spec=wl.AudioRecorder) + app.transcriber = MagicMock(spec=wl.Transcriber) + app.injector = MagicMock(spec=wl.TextInjector) + return app + + def test_timeout_skipped_when_speech_active(self, mock_config_stream): + """Silence timeout restarts timer if VAD has active speech.""" + app = self._make_app(mock_config_stream) + app.state = wl.AppState.DICTATING + app._vad = MagicMock() + app._vad._in_speech = True + + app._on_silence_timeout() + + # Should NOT transition to LISTENING + assert app.state == wl.AppState.DICTATING + # Timer should have been restarted + assert app._silence_timer is not None + app._cancel_silence_timer() + + def test_timeout_fires_when_no_speech(self, mock_config_stream): + """Silence timeout fires normally when VAD has no active speech.""" + app = self._make_app(mock_config_stream) + app.state = wl.AppState.DICTATING + app._vad = MagicMock() + app._vad._in_speech = False + + app._on_silence_timeout() + + assert app.state == wl.AppState.LISTENING + + def test_timeout_fires_when_no_vad(self, mock_config_stream): + """Silence timeout fires normally when no VAD exists.""" + app = self._make_app(mock_config_stream) + app.state = wl.AppState.DICTATING + app._vad = None + + app._on_silence_timeout() + + assert app.state == wl.AppState.LISTENING + + +# =================================================================== +# Transcriber — wake model parameter +# =================================================================== + +class TestTranscriberWakeModel: + @patch("app.subprocess.run") + def test_transcribe_with_custom_model(self, mock_run, mock_config, tmp_wav_file): + """Transcriber uses provided model parameter instead of config.model.""" + mock_run.return_value = MagicMock( + returncode=0, + stdout="test text", + stderr="", + ) + + tr = wl.Transcriber(mock_config) + tr.transcribe(tmp_wav_file, model="/tmp/ggml-tiny.bin") + + cmd = mock_run.call_args[0][0] + assert "-m" in cmd + m_idx = cmd.index("-m") + assert cmd[m_idx + 1] == "/tmp/ggml-tiny.bin" + + @patch("app.subprocess.run") + def test_transcribe_default_model(self, mock_run, mock_config, tmp_wav_file): + """Transcriber uses config.model when no model parameter given.""" + mock_run.return_value = MagicMock( + returncode=0, + stdout="test text", + stderr="", + ) + + tr = wl.Transcriber(mock_config) + tr.transcribe(tmp_wav_file) + + cmd = mock_run.call_args[0][0] + m_idx = cmd.index("-m") + assert cmd[m_idx + 1] == mock_config.model + + +# =================================================================== +# Process segment — wake model used in LISTENING state +# =================================================================== + +class TestProcessSegmentWakeModel: + def _make_app(self, config): + app = wl.WhisperLinuxApp(config) + app.recorder = MagicMock(spec=wl.AudioRecorder) + app.transcriber = MagicMock(spec=wl.Transcriber) + app.injector = MagicMock(spec=wl.TextInjector) + return app + + @patch("app.app.AudioStream") + def test_wake_model_used_when_listening(self, mock_stream_cls, mock_config_stream): + """In LISTENING state with wake_model set, lighter model is used.""" + mock_config_stream.wake_model = "/tmp/ggml-tiny.bin" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.LISTENING + app.transcriber.transcribe.return_value = "привет мир" + + app._process_speech_segment(b"\x00" * 32000) + + app.transcriber.transcribe.assert_called_once() + _, kwargs = app.transcriber.transcribe.call_args + assert kwargs.get("model") == "/tmp/ggml-tiny.bin" + + @patch("app.app.AudioStream") + def test_main_model_used_when_dictating(self, mock_stream_cls, mock_config_stream): + """In DICTATING state, main model is used even if wake_model is set.""" + mock_config_stream.wake_model = "/tmp/ggml-tiny.bin" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.DICTATING + app.transcriber.transcribe.return_value = "привет мир" + + app._process_speech_segment(b"\x00" * 32000) + + app.transcriber.transcribe.assert_called_once() + _, kwargs = app.transcriber.transcribe.call_args + assert kwargs.get("model") is None + + @patch("app.app.AudioStream") + def test_no_wake_model_uses_default(self, mock_stream_cls, mock_config_stream): + """When wake_model is empty, default model is used even in LISTENING.""" + mock_config_stream.wake_model = "" + mock_stream = MagicMock() + mock_stream_cls.return_value = mock_stream + + app = self._make_app(mock_config_stream) + app._wake_detector = wl.WakeWordDetector(mock_config_stream.wake_word) + app.state = wl.AppState.LISTENING + app.transcriber.transcribe.return_value = "привет мир" + + app._process_speech_segment(b"\x00" * 32000) + + _, kwargs = app.transcriber.transcribe.call_args + assert kwargs.get("model") is None + + +# =================================================================== +# CLI — wake model flag +# =================================================================== + +class TestCLIWakeModel: + @patch("app.app.WhisperLinuxApp") + def test_wake_model_flag(self, mock_app_cls, monkeypatch, tmp_pid_file, tmp_config_file, tmp_path): + model_file = tmp_path / "ggml-base.bin" + model_file.write_bytes(b"\x00" * 100) + wake_file = tmp_path / "ggml-tiny.bin" + wake_file.write_bytes(b"\x00" * 100) + tmp_config_file.write_text( + "[whisper-linux]\n" + f"whisper_cli = /usr/bin/whisper-cli\n" + f"model = {model_file}\n" + f"models_dir = {tmp_path}\n" + ) + monkeypatch.setattr("sys.argv", ["app.py", "--wake-model", str(wake_file)]) + monkeypatch.setattr(wl.config, "CONFIG_FILE", tmp_config_file) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_file.parent) + mock_app = MagicMock() + mock_app_cls.return_value = mock_app + mock_app_cls.read_pid.return_value = None + + wl.main() + + config_arg = mock_app_cls.call_args[0][0] + assert config_arg.wake_model == str(wake_file) + + +# =================================================================== +# Config — wake_model save/load +# =================================================================== + +class TestConfigWakeModel: + def test_wake_model_default_empty(self, mock_config): + """Default wake_model is empty string.""" + assert mock_config.wake_model == "" + + def test_save_and_load_wake_model(self, mock_config, tmp_config_file, monkeypatch): + """wake_model is persisted in config file.""" + mock_config.wake_model = "/tmp/ggml-tiny.bin" + mock_config.save() + text = tmp_config_file.read_text() + assert "wake_model = /tmp/ggml-tiny.bin" in text + + def test_load_wake_model_from_file(self, tmp_config_dir, monkeypatch): + config_file = tmp_config_dir / "config.ini" + config_file.write_text( + "[whisper-linux]\n" + "whisper_cli = /usr/bin/whisper-cli\n" + "model = /tmp/ggml-base.bin\n" + "models_dir = /tmp\n" + "display_server = x11\n" + "audio_device = plughw:1,0\n" + "wake_model = /tmp/ggml-tiny.bin\n" + ) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_dir) + monkeypatch.setattr(wl.config, "CONFIG_FILE", config_file) + config = wl.Config() + assert config.wake_model == "/tmp/ggml-tiny.bin" + + def test_voice_commands_default_true(self, mock_config): + assert mock_config.voice_commands is True + + def test_save_voice_commands(self, mock_config, tmp_config_file): + mock_config.voice_commands = False + mock_config.save() + text = tmp_config_file.read_text() + assert "voice_commands = False" in text + + def test_load_voice_commands_from_file(self, tmp_config_dir, monkeypatch): + config_file = tmp_config_dir / "config.ini" + config_file.write_text( + "[whisper-linux]\n" + "whisper_cli = /usr/bin/whisper-cli\n" + "model = /tmp/ggml-base.bin\n" + "models_dir = /tmp\n" + "display_server = x11\n" + "audio_device = plughw:1,0\n" + "voice_commands = False\n" + ) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_dir) + monkeypatch.setattr(wl.config, "CONFIG_FILE", config_file) + config = wl.Config() + assert config.voice_commands is False + + +# =================================================================== +# VoiceCommands +# =================================================================== + +class TestVoiceCommands: + def test_no_commands_just_text(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + result = vc.process("hello world", inject, send_key) + assert result is False + inject.assert_called_once_with("hello world") + send_key.assert_not_called() + + def test_enter_command(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + result = vc.process("hello enter world", inject, send_key) + assert result is True + assert inject.call_args_list == [call("hello"), call("world")] + send_key.assert_called_once_with("Return") + + def test_enter_russian(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + vc.process("текст энтер ещё", inject, send_key) + assert inject.call_args_list == [call("текст"), call("ещё")] + send_key.assert_called_once_with("Return") + + def test_backspace_removes_from_buffer(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + result = vc.process("hello world backspace more", inject, send_key) + assert result is True + # "hello world" buffered, backspace removes "world", "more" added + # Final buffer: "hello more" + inject.assert_called_once_with("hello more") + send_key.assert_not_called() + + def test_backspace_empty_buffer_sends_ctrl_backspace(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + result = vc.process("backspace", inject, send_key) + assert result is True + inject.assert_not_called() + send_key.assert_called_once_with("ctrl+BackSpace") + + def test_backspace_russian(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + vc.process("слово назад", inject, send_key) + # "слово" buffered, "назад" = backspace removes "слово" + inject.assert_not_called() + send_key.assert_not_called() + + def test_tab_command(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + vc.process("tab", inject, send_key) + send_key.assert_called_once_with("Tab") + + def test_escape_command(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + vc.process("escape", inject, send_key) + send_key.assert_called_once_with("Escape") + + def test_fuzzy_match(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + # "entr" is close enough to "enter" (ratio > 0.75) + vc.process("entr", inject, send_key) + send_key.assert_called_once_with("Return") + + def test_fuzzy_no_match(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + # "xyz" is not close to any command + result = vc.process("xyz", inject, send_key) + assert result is False + inject.assert_called_once_with("xyz") + send_key.assert_not_called() + + def test_command_with_punctuation(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + vc.process("hello, enter.", inject, send_key) + inject.assert_called_once_with("hello,") + send_key.assert_called_once_with("Return") + + def test_empty_text(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + result = vc.process("", inject, send_key) + assert result is False + inject.assert_not_called() + send_key.assert_not_called() + + def test_multiple_commands(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + vc.process("hello enter enter world", inject, send_key) + assert inject.call_args_list == [call("hello"), call("world")] + assert send_key.call_args_list == [call("Return"), call("Return")] + + def test_command_at_end(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + vc.process("hello world enter", inject, send_key) + inject.assert_called_once_with("hello world") + send_key.assert_called_once_with("Return") + + def test_custom_commands(self): + vc = wl.VoiceCommands(commands={"stop": "key:Escape"}) + inject = MagicMock() + send_key = MagicMock() + vc.process("stop", inject, send_key) + send_key.assert_called_once_with("Escape") + + def test_ввод_command(self): + vc = wl.VoiceCommands() + inject = MagicMock() + send_key = MagicMock() + vc.process("текст ввод", inject, send_key) + inject.assert_called_once_with("текст") + send_key.assert_called_once_with("Return") + + +# =================================================================== +# TextInjector.send_key +# =================================================================== + +class TestTextInjectorSendKey: + @patch("app.subprocess.run") + def test_send_key_x11(self, mock_run, mock_config): + mock_run.return_value = MagicMock(returncode=0) + inj = wl.TextInjector(mock_config) + inj.send_key("Return") + mock_run.assert_called_once() + cmd = mock_run.call_args[0][0] + assert cmd[0] == "xdotool" + assert "key" in cmd + assert "Return" in cmd + + @patch("app.subprocess.run") + def test_send_key_wayland_wtype(self, mock_run, mock_config_wayland): + mock_run.return_value = MagicMock(returncode=0) + inj = wl.TextInjector(mock_config_wayland) + inj.send_key("Return") + mock_run.assert_called_once() + cmd = mock_run.call_args[0][0] + assert cmd[0] == "wtype" + assert "Return" in cmd + + @patch("app.subprocess.run") + def test_send_key_wayland_modifier_combo(self, mock_run, mock_config_wayland): + mock_run.return_value = MagicMock(returncode=0) + inj = wl.TextInjector(mock_config_wayland) + inj.send_key("ctrl+BackSpace") + mock_run.assert_called_once() + cmd = mock_run.call_args[0][0] + assert cmd == ["wtype", "-M", "ctrl", "-k", "BackSpace"] + + @patch("app.subprocess.run") + def test_send_key_wayland_fallback_ydotool(self, mock_run, mock_config_wayland): + mock_run.side_effect = [ + FileNotFoundError("wtype"), + MagicMock(returncode=0), # ydotool + ] + inj = wl.TextInjector(mock_config_wayland) + inj.send_key("Tab") + assert mock_run.call_count == 2 + cmd = mock_run.call_args_list[1][0][0] + assert cmd[0] == "ydotool" + assert "Tab" in cmd + + @patch("app.subprocess.run") + def test_send_key_wayland_ydotool_evdev_map(self, mock_run, mock_config_wayland): + """ydotool translates X11 keysym Return → evdev Enter.""" + mock_run.side_effect = [ + FileNotFoundError("wtype"), + MagicMock(returncode=0), # ydotool + ] + inj = wl.TextInjector(mock_config_wayland) + inj.send_key("Return") + assert mock_run.call_count == 2 + cmd = mock_run.call_args_list[1][0][0] + assert cmd == ["ydotool", "key", "--delay", "50", "Enter"] + + @patch("app.subprocess.run") + def test_send_key_wayland_fallback_xdotool(self, mock_run, mock_config_wayland): + mock_run.side_effect = [ + FileNotFoundError("wtype"), + FileNotFoundError("ydotool"), + MagicMock(returncode=0), # xdotool + ] + inj = wl.TextInjector(mock_config_wayland) + inj.send_key("Tab") + assert mock_run.call_count == 3 + cmd = mock_run.call_args_list[2][0][0] + assert cmd[0] == "xdotool" + assert "Tab" in cmd + + @patch("app.subprocess.run") + def test_send_key_x11_failure(self, mock_run, mock_config): + mock_run.side_effect = FileNotFoundError("xdotool") + inj = wl.TextInjector(mock_config) + # Should not raise + inj.send_key("Return") + + +# =================================================================== +# Voice commands map config +# =================================================================== + +class TestVoiceCommandsMap: + def test_default_map_loaded(self, mock_config): + assert "enter" in mock_config.voice_commands_map + assert mock_config.voice_commands_map["enter"] == "key:Return" + assert "backspace" in mock_config.voice_commands_map + + def test_save_and_load_custom_map(self, mock_config, tmp_config_file): + mock_config.voice_commands_map = {"go": "key:Return", "стоп": "key:Escape"} + mock_config.save() + text = tmp_config_file.read_text() + assert "[voice-commands]" in text + assert "go = key:Return" in text + assert "стоп = key:Escape" in text + + def test_load_custom_map_from_file(self, tmp_config_dir, monkeypatch): + config_file = tmp_config_dir / "config.ini" + config_file.write_text( + "[whisper-linux]\n" + "whisper_cli = /usr/bin/whisper-cli\n" + "model = /tmp/ggml-base.bin\n" + "models_dir = /tmp\n" + "display_server = x11\n" + "audio_device = plughw:1,0\n" + "\n" + "[voice-commands]\n" + "go = key:Return\n" + "стоп = key:Escape\n" + ) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_dir) + monkeypatch.setattr(wl.config, "CONFIG_FILE", config_file) + config = wl.Config() + # Custom entries override/extend defaults + assert config.voice_commands_map["go"] == "key:Return" + assert config.voice_commands_map["стоп"] == "key:Escape" + # Defaults still present + assert "enter" in config.voice_commands_map + assert "табуляция" in config.voice_commands_map + + def test_voice_commands_uses_config_map(self, mock_config): + mock_config.voice_commands_map = {"go": "key:Return"} + from app.commands import VoiceCommands + vc = VoiceCommands(mock_config.voice_commands_map) + inject = MagicMock() + send_key = MagicMock() + vc.process("go", inject, send_key) + send_key.assert_called_once_with("Return") + + def test_empty_voice_commands_section(self, tmp_config_dir, monkeypatch): + config_file = tmp_config_dir / "config.ini" + config_file.write_text( + "[whisper-linux]\n" + "whisper_cli = /usr/bin/whisper-cli\n" + "model = /tmp/ggml-base.bin\n" + "models_dir = /tmp\n" + "display_server = x11\n" + "audio_device = plughw:1,0\n" + "\n" + "[voice-commands]\n" + ) + monkeypatch.setattr(wl.config, "CONFIG_DIR", tmp_config_dir) + monkeypatch.setattr(wl.config, "CONFIG_FILE", config_file) + config = wl.Config() + # Empty section still gets defaults merged in + assert "enter" in config.voice_commands_map + assert "табуляция" in config.voice_commands_map diff --git a/examples/whisper.linux/whisper-linux b/examples/whisper.linux/whisper-linux new file mode 100755 index 000000000..1fef7b73c --- /dev/null +++ b/examples/whisper.linux/whisper-linux @@ -0,0 +1,6 @@ +#!/bin/bash +# whisper-linux — Voice typing for Linux desktop +# Unique process name for easy pkill -f whisper-linux +pkill -f whisper_linux; +cd "$(dirname "$0")" +exec python3 -m app "$@" diff --git a/examples/whisper.linux/whisper-linux.desktop b/examples/whisper.linux/whisper-linux.desktop new file mode 100644 index 000000000..0806e1a77 --- /dev/null +++ b/examples/whisper.linux/whisper-linux.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=whisper.linux +Comment=Voice typing for Linux desktop +Exec=~/whisper.linux/whisper-linux +Icon=audio-input-microphone +Terminal=false +Categories=Utility;Audio; +StartupNotify=false +X-GNOME-Autostart-enabled=true diff --git a/examples/whisper.youtube/.gitignore b/examples/whisper.youtube/.gitignore new file mode 100644 index 000000000..57c7d8104 --- /dev/null +++ b/examples/whisper.youtube/.gitignore @@ -0,0 +1,4 @@ +videos/ +.venv-parakeet/ +__pycache__/ +*.pyc diff --git a/examples/whisper.youtube/parakeet b/examples/whisper.youtube/parakeet new file mode 100755 index 000000000..f65d083cd --- /dev/null +++ b/examples/whisper.youtube/parakeet @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Parakeet V3 transcription launcher. +# Usage: +# ./parakeet +# ./parakeet +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# numba cannot write its cache on the mounted filesystem under this dir +# (no inode-stable locator); redirect cache to /tmp. +export NUMBA_CACHE_DIR="${NUMBA_CACHE_DIR:-/tmp/numba-cache-$USER}" +mkdir -p "$NUMBA_CACHE_DIR" +exec "$DIR/.venv-parakeet/bin/python" "$DIR/parakeet_transcribe.py" "$@" diff --git a/examples/whisper.youtube/parakeet_mic.py b/examples/whisper.youtube/parakeet_mic.py new file mode 100644 index 000000000..607f42d2d --- /dev/null +++ b/examples/whisper.youtube/parakeet_mic.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Live microphone streaming with Parakeet-tdt-0.6b-v3. + +Captures audio from the mic (pw-record or arecord), runs a sliding-window +transcription, and prints text progressively as new audio arrives. +""" + +import argparse +import os +import shutil +import signal +import subprocess +import sys +import tempfile +import time + +import numpy as np + +MODEL_NAME = "nvidia/parakeet-tdt-0.6b-v3" +SR = 16000 +DEFAULT_WINDOW_S = 8.0 +DEFAULT_SHIFT_S = 2.5 + + +def _norm(w: str) -> str: + return "".join(ch for ch in w.lower() if ch.isalnum()) + + +def _dedup_overlap(prev_text: str, curr_text: str) -> str: + prev_words = prev_text.split() + curr_words = curr_text.split() + prev_norm = [_norm(w) for w in prev_words] + curr_norm = [_norm(w) for w in curr_words] + max_n = min(len(prev_norm), len(curr_norm)) + for n in range(max_n, 0, -1): + if prev_norm[-n:] == curr_norm[:n]: + return " ".join(curr_words[n:]) + return curr_text + + +def _find_recorder(): + for p in ("pw-record", "/usr/bin/pw-record", "/usr/local/bin/pw-record"): + if shutil.which(p) if not os.path.isabs(p) else os.path.isfile(p): + return p + return shutil.which("arecord") + + +def _build_record_cmd(): + rec = _find_recorder() + if not rec: + raise RuntimeError("Neither pw-record nor arecord found") + if "pw-record" in rec: + return [rec, "--format", "s16", "--rate", str(SR), "--channels", "1", "-"] + return [rec, "-f", "S16_LE", "-r", str(SR), "-c", "1", "-t", "raw"] + + +def load_model(): + import nemo.collections.asr as nemo_asr + from nemo.utils import logging as nemo_logging + import torch + import logging + + nemo_logging.set_verbosity(nemo_logging.ERROR) + logging.getLogger("nemo_logger").setLevel(logging.ERROR) + print(f"Loading {MODEL_NAME} ...", flush=True) + asr = nemo_asr.models.ASRModel.from_pretrained(MODEL_NAME) + asr = asr.cuda() if torch.cuda.is_available() else asr + nemo_logging.set_verbosity(nemo_logging.ERROR) + return asr + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--window", type=float, default=DEFAULT_WINDOW_S, + help=f"sliding window length in seconds (default: {DEFAULT_WINDOW_S})") + parser.add_argument("--shift", type=float, default=DEFAULT_SHIFT_S, + help=f"window shift / step in seconds (default: {DEFAULT_SHIFT_S})") + args = parser.parse_args() + + if args.shift <= 0 or args.window <= 0: + sys.exit("window and shift must be positive") + if args.shift > args.window: + sys.exit("shift must be <= window") + + import soundfile as sf + + asr = load_model() + cmd = _build_record_cmd() + + win_samples = int(args.window * SR) + shift_samples = int(args.shift * SR) + shift_bytes = shift_samples * 2 # int16 mono + + tmp = tempfile.NamedTemporaryFile(suffix=".wav", prefix="parakeet-mic-", delete=False) + tmp_wav = tmp.name + tmp.close() + + print(f"Recorder: {cmd[0]}", flush=True) + print(f"Window: {args.window:.1f}s, shift: {args.shift:.1f}s " + f"(overlap: {args.window - args.shift:.1f}s)", flush=True) + print("--- speak into mic; Ctrl-C to stop ---\n", flush=True) + + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + + def _shutdown(*_): + if proc.poll() is None: + proc.send_signal(signal.SIGINT) + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + if os.path.exists(tmp_wav): + os.unlink(tmp_wav) + print("\n--- stopped ---", flush=True) + sys.exit(0) + + signal.signal(signal.SIGINT, _shutdown) + signal.signal(signal.SIGTERM, _shutdown) + + buffer = np.zeros(0, dtype=np.int16) + prev_text = "" + + try: + while True: + new_bytes = proc.stdout.read(shift_bytes) + if not new_bytes: + break + new_samples = np.frombuffer(new_bytes, dtype=np.int16) + buffer = np.concatenate([buffer, new_samples]) + if len(buffer) > win_samples: + buffer = buffer[-win_samples:] + if len(buffer) < shift_samples: + continue + + sf.write(tmp_wav, buffer, SR) + t0 = time.monotonic() + out = asr.transcribe([tmp_wav], timestamps=False, verbose=False) + elapsed = time.monotonic() - t0 + current = out[0].text.strip() + if not current: + continue + + new_part = _dedup_overlap(prev_text, current) if prev_text else current + if new_part.strip(): + print(new_part, end=" ", flush=True) + prev_text = current + finally: + _shutdown() + + +if __name__ == "__main__": + main() diff --git a/examples/whisper.youtube/parakeet_transcribe.py b/examples/whisper.youtube/parakeet_transcribe.py new file mode 100644 index 000000000..55b2a2255 --- /dev/null +++ b/examples/whisper.youtube/parakeet_transcribe.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Transcribe audio with NVIDIA Parakeet-tdt-0.6b-v3 (multilingual). + +Modes: + batch — transcribe the whole audio at once, print at the end (default). + stream — sliding window with LocalAgreement: progressive text output as + new audio arrives. Each window of WINDOW seconds is re-transcribed + every SHIFT seconds; tokens agreed between two consecutive windows + are "confirmed" and printed; the rest stays uncertain until the + next iteration confirms (or invalidates) it. +""" + +import argparse +import os +import subprocess +import sys +import tempfile + +MODEL_NAME = "nvidia/parakeet-tdt-0.6b-v3" + +# Sliding-window streaming defaults (in seconds). +# Matches whisper.cpp/examples/stream defaults. +DEFAULT_WINDOW_S = 10.0 +DEFAULT_SHIFT_S = 3.0 + + +def download_audio(url: str, out_path: str): + subprocess.run([ + "yt-dlp", "-x", "--audio-format", "wav", + "-o", out_path, url, + ], check=True) + + +def convert_wav(src: str, dst: str): + subprocess.run([ + "ffmpeg", "-y", "-i", src, + "-ar", "16000", "-ac", "1", "-f", "wav", dst, + ], check=True, capture_output=True) + + +def load_model(long_audio: bool = True): + import nemo.collections.asr as nemo_asr + import torch + + print(f"Loading {MODEL_NAME} ...", flush=True) + asr = nemo_asr.models.ASRModel.from_pretrained(MODEL_NAME) + asr = asr.cuda() if torch.cuda.is_available() else asr + if long_audio: + asr.change_attention_model( + self_attention_model="rel_pos_local_attn", + att_context_size=[256, 256], + ) + return asr + + +def transcribe_batch(asr, wav_path: str) -> str: + print(f"Transcribing {wav_path} ...", flush=True) + out = asr.transcribe([wav_path], timestamps=False) + return out[0].text + + +def _norm(w: str) -> str: + """Normalize a word for dedup comparison: lower-case, strip punctuation.""" + return "".join(ch for ch in w.lower() if ch.isalnum()) + + +def _dedup_overlap(prev_text: str, curr_text: str) -> str: + """Return the new portion of curr_text by removing the trailing words of + prev_text that match the leading words of curr_text (overlap region). + Comparison is case- and punctuation-insensitive.""" + prev_words = prev_text.split() + curr_words = curr_text.split() + prev_norm = [_norm(w) for w in prev_words] + curr_norm = [_norm(w) for w in curr_words] + max_n = min(len(prev_norm), len(curr_norm)) + for n in range(max_n, 0, -1): + if prev_norm[-n:] == curr_norm[:n]: + return " ".join(curr_words[n:]) + return curr_text + + +def transcribe_sliding(asr, wav_path: str, window_s: float, shift_s: float) -> str: + """Sliding-window streaming: each window of WINDOW seconds is transcribed + every SHIFT seconds; words overlapping with the previous window are + de-duplicated and only the new tail is printed.""" + import soundfile as sf + + if shift_s <= 0 or window_s <= 0: + raise ValueError("window and shift must be positive") + if shift_s > window_s: + raise ValueError("shift must be <= window") + + audio, sr = sf.read(wav_path) + if sr != 16000: + raise ValueError(f"Expected 16kHz WAV, got {sr}Hz") + + n_samples = len(audio) + win = int(window_s * sr) + shift = int(shift_s * sr) + overlap_s = window_s - shift_s + + print( + f"Sliding window: {window_s:.1f}s window, {shift_s:.1f}s shift " + f"({overlap_s:.1f}s overlap), audio {n_samples / sr:.1f}s", + flush=True, + ) + print("--- streaming transcription ---", flush=True) + + tmp = tempfile.NamedTemporaryFile(suffix=".wav", prefix="parakeet-slide-", + delete=False) + tmp_wav = tmp.name + tmp.close() + + full_text_parts = [] + prev_text = "" + pos = 0 + + try: + while pos < n_samples: + end = min(pos + win, n_samples) + chunk = audio[pos:end] + sf.write(tmp_wav, chunk, sr) + + out = asr.transcribe([tmp_wav], timestamps=False, verbose=False) + current = out[0].text.strip() + + new_part = _dedup_overlap(prev_text, current) if prev_text else current + if new_part: + print(new_part, end=" ", flush=True) + full_text_parts.append(new_part) + + prev_text = current + pos += shift + + print(flush=True) + return " ".join(full_text_parts).strip() + finally: + if os.path.exists(tmp_wav): + os.unlink(tmp_wav) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("source", help="WAV file path or YouTube URL") + parser.add_argument("--stream", action="store_true", + help="streaming mode (sliding window + LocalAgreement)") + parser.add_argument("--window", type=float, default=DEFAULT_WINDOW_S, + help=f"sliding window length in seconds (default: {DEFAULT_WINDOW_S})") + parser.add_argument("--shift", type=float, default=DEFAULT_SHIFT_S, + help=f"window shift in seconds (default: {DEFAULT_SHIFT_S}); " + "overlap = window - shift") + args = parser.parse_args() + + src = args.source + + if src.startswith(("http://", "https://", "youtu", "www.")): + tmp = tempfile.mkdtemp(prefix="parakeet-") + raw = os.path.join(tmp, "audio.%(ext)s") + wav = os.path.join(tmp, "audio_16k.wav") + print(f"Downloading {src} ...", flush=True) + download_audio(src, raw) + downloaded = [f for f in os.listdir(tmp) if f.startswith("audio.")] + print("Converting to 16kHz WAV ...", flush=True) + convert_wav(os.path.join(tmp, downloaded[0]), wav) + wav_path = wav + else: + wav_path = src + + asr = load_model(long_audio=not args.stream) + + if args.stream: + text = transcribe_sliding(asr, wav_path, args.window, args.shift) + else: + text = transcribe_batch(asr, wav_path) + print("\n=== Transcription ===") + print(text) + + +if __name__ == "__main__": + main() diff --git a/examples/whisper.youtube/transcriptions/q1CluXct_VI.txt b/examples/whisper.youtube/transcriptions/q1CluXct_VI.txt new file mode 100644 index 000000000..c26ceb273 --- /dev/null +++ b/examples/whisper.youtube/transcriptions/q1CluXct_VI.txt @@ -0,0 +1 @@ +Мирзакарим Нарбеков. Опыт дурака или ключ к прозрению. Как избавиться от очков. Реклама книги, которую вы держите в руках. Убедительная просьба. Людям с обрезанным чувством юмора, кастрированным оптимизмом, гипертрофированной обидчивостью, лживым, то есть этику. Тем, кто знает, что все люди сволочи. Кого уволили с работы или собираются уволить. Кто стареет или думает о старости. Кто разведен или намеревается разводиться. У кого полностью отсутствует сексуальность. А также страдающим метеоризмом умникам и особенно очкарикам, которые на носу таскают модную сантехнику для глаз. Эту книгу брать в руки, читать и тем более заниматься по ней не рекомендую. И хотя все эти проблемы мы в книге решаем, но это не для вас. Мирзакарем Санакулович Нарбеков. Доктор психологии, доктор педагогики, доктор философии в медицине, профессор, действительный член и член-корреспондент ряда российских и зарубежных академий, автор многих запатентованных изобретений и открытий в науке. Но об этом потом. Об авторе. На занятиях Мирзакарем Санакулович Нарбеков часто повторяет. Я обычный человек. Такой же, как все. Ничем не лучше и, надеюсь, ничем не хуже вас. Всего, чего я добился в жизни, вы тоже можете достичь. Важно только этого захотеть. А добился он действительно многого. Сегодня Нарбеков доктор психологии, доктор педагогики, доктор философии в медицине, профессор, действительный член и член-корреспондент ряда российских и зарубежных академий, автор многих запатентованных изобретений и открытий в науке. Впрочем, все эти звания сам он называет собачьими регалиями, потому что не ради признания он работает. Мирзакарем Санакулович – истинный ученый-исследователь. Круг его интересов очень широкий. Многие удивляются, как все это может сочетаться в одном человеке. Художник, композитор, писатель, кинорежиссер, артист, спортсмен, тренер. Имеющий черный пояс по карате – третий дан. И черный пояс по сам Чондо – девятый дан. Но самое главное – автор книги, специалист по суфийской медицине и практике, одной из самых древних среди всех существующих. Суфизм, учение мудрости, относится к классической философской традиции Востока. Он дал миру алгебру, письменность. От него берут начало три основные религии. Суфи – это люди. Но своих священных городов, храмов, иерархии, атрибутики у них нет. Храм Бога находится в сердце. Они не отвергают никакую религию, но рассуждать о Боге у них считается фарисейством. Любой может говорить о добре. Эти люди словам предпочитают действия. Поступками, делами и самой жизнью они сеют любовь и охраняют божественный свет во всем, что их окружает. Являются воинами света, хранителями добра, любви, гармонии, красоты и мудрости. Внешне они ничем не отличаются от обычных людей. Живут привычной жизнью, работают, воспитывают детей, заботятся о близких. Их главное правило – сердце с Богом, а руки в труде. В каждой сфере деятельности Мирзакарим Санакулович имеет своих учеников, которые продолжают его дело. Это дает ему возможность не стоять на месте, а двигаться дальше, осваивать новые рубежи. Он требовательный, жесткий, решительный, когда речь идет о достижении задуманного. Каждому человеку он дает шанс раскрыть и проявить свою божественную природу. Для вас, уважаемый читатель, эта возможность предоставляется через книгу. Древние считали небо символом единства и вечности. Когда люди смотрят на звезды, даже если находятся далеко друг от друга, в этот момент они вместе. Пусть эти страницы будут маленьким символом, объединяющим нас с вами на пути к здоровью и успеху. И каждый раз, когда вы будете брать книгу в руки, знайте – мы вместе. Литературный редактор. От Писаки, то есть от меня. Уважаемый читатель, вы держите в руках книгу, которая написана для вас с целью помочь восстановить зрение, пробудить скрытые в душе способности и реализовать себя как личность. Но будьте готовы к тому, что с первых же страниц книга будет вас шокировать, вызывать бурю отрицательных эмоций, недовольства, недоброжелательность, гнев, обиду, агрессию в адрес автора. Более того, скажу вам, что если это будет так, то считаю свою задачу выполненной. Заранее знаю, вы скажете, что книги так не пишут. То, что допускается в устной речи, недопустимо в печатном издании. Что все эти оскорбления и грубости вульгарны и не делают чести автору. И что этого оправдать не может. Что все то же самое можно изложить деликатным языком, интеллигентно объяснить, тогда все и так поймут и так далее. Я бы и сам так думал 20 с лишним лет назад, когда был зависим от врачей, процедур, лекарств, был нереализованным в жизни человеком без будущего, помышляющим даже о самоубийстве. Да, был и такой момент. А чего вы хотите от инвалида первой группы, привязанного к аппарату по искусственной очистке крови? Так и так, смертник. И это в 20-то лет. Официальная медицина такое заболевание не лечит. Больного, который доживает свой век, только наблюдают, поддерживают имеющимися в арсенале средствами и смотрят, как он медленно угасает. Не страшно ли это? Я слишком ценю жизнь, чтобы кланяться и расшаркиваться в тот момент, когда просто нет времени, когда нужно быстро помочь вам освободиться от таков, удерживающих в болоте болезней и нереализованности. Вы суетитесь, что-то делаете, куда-то постоянно спешите, но даже не позволяете себе на минуточку остановиться и задуматься. А для чего все это? Куда я иду и что после меня останется? Хотя в глубине души, где-то очень глубоко, нет-нет, да что-то отзовется болью, что как-то все не так. Но нет времени. Надо, надо постоянно куда-то бежать. Весь вопрос куда и для чего? На самом-то деле мы топчемся на месте, ходим по кругу и сами того не замечаем. А все усилия приходят в исходную точку. Именно для того, чтобы вы прекратили это бессмысленное хождение по заколдованному кругу проблем и неудач, я специально выбиваю у вас почву из-под ног. В этом заключается суть метода. Моя задача вытащить наружу это зло, глубоко сидящее и разъедающее вас изнутри. Вы ходите с этим нарывом в душе, а его нужно прорвать и выпустить гной наружу, очистить. Тогда откроются способности созидать, мечтать и осуществлять задуманное, откроются пути. Больше всего мне не хочется, чтобы вы после прочтения книги остались равнодушными и тем более в тех же очках, что и сейчас. Сознательно задуманный в книге перепад эмоций это хирургический инструмент. Существует и простая формула. Характер и судьба равны болезни. Получается, что если у человека геморрой, то характер какой? Это проверено временем на практическом опыте, в том числе и личном. Значит, чтобы изменить к лучшему свое здоровье и жизнь в целом, нужно изменить характер, а просто уговорами этого не добьешься. Характер переплавляется изнутри при вашем непосредственном участии, а я только играю роль катализатора. Поэтому приготовьтесь к самым неожиданным и нетрадиционным в печатном издании выпадам в ваш адрес. Внутри вас заключен огромный алмаз. Остается только его найти, дать огранку и расположить так, чтобы он заиграл всеми своими гранями. Тогда вы сможете взглянуть на все другими глазами, открыть в жизни новые горизонты, и вы сможете осознать, что вы на этом пути, готов служить вам. Жизнь не такая плохая и сложная штука. Она такая, какая есть. С искренним уважением и от всей души, Мирзакарим Нарбеков. Вместо предисловия или гильотину на свою голову заказывали? Внимание! Важная информация в книге изложена с применением техники ускоренного обучения. Чтобы исключить ваши возможные обиды в адрес автора, убедительно прошу, познакомьтесь, пожалуйста, с главой «Жаркое из метода ускоренного обучения и способ его употребления». В ваших руках малюсенький учебник, и гениальность его заключается в том, что он написан для ленивых, то есть именно для вас. Улучшить зрение очень легко и просто. От вас потребуется капелька желания, чуть-чуть шевеления извилинами и совсем немного труда. Если это вам не покажется много, то все, цель достигнута. А минимальное количество требуемых для успеха мозгов, по нашим расчетам, 50 граммов. Надеюсь, это у вас есть. Хотя надежда еще не факт. На сегодняшний день готовится к выпуску такая книга для специалистов. Талмуд примерно из 600 страниц. Там все разложено по полочкам. По психофизиологическим и причинно-следственным факторам. Нужна ли она вам? Считай, что нет. В данный момент задача заключается в том, чтобы восстановить ваше зрение. Учебник написан для тех, кто в жизни признает себя человеком с большой буквы и собственными усилиями и трудом стремится улучшить или восстановить зрение. Поэтому здесь главным козырем будет искренность, а также краткость, ясность, популярность, доступность в изложении, то есть по-домашнему, местами с использованием в ваш адрес лексикона сильно выпивших граждан из провинции. Так что приготовьтесь. Техника ускоренного обучения предполагает грубость и доходчивое объяснение в двух словах на 500 страницах. Не пугайтесь, чрезмерно грубить не буду. Просто назову вас тем, кем вы являетесь, и не пугайтесь, чрезмерно грубить не буду. И у вас, дорогой читатель, есть эта чаша. В погоне за очередным желанием вы кинули в нее молодость, здоровье, красоту, счастье. Она поглотила душевный покой, радость жизни. Она не дала воплотить ваши истинные, сокровенные мечты. В эту чашу вы бросили всю свою жизнь. Впереди, быть может, осталась одна треть. И если не закроете эту чашу сейчас, она затянет и оставшуюся вашу жизнь. Неужели вы не видите этого? Каждый раз, когда вы говорите «мало», вы тренируете в себе отрицательное отношение к себе и ко всему достигнутому. Обесцениваете все прожитые годы, всю жизнь. Придираясь к себе, вы продолжаете сравнивать себя с другими не с лучшей стороны. Заниженная самооценка взялась из чаши желаний. Это основа основ несчастья, неудовлетворенности, неудач. Загляните в себя. Если бы вы даже восстановили зрение на 100% за один день, вы все равно были бы недовольны. Не так ли? Измените пока не поздно отношение к себе, к окружающему миру и к жизни. Найдите внутри себя что-то истинное, ценное, подлинное, вечное. И развивайте. Вот сейчас поднимите правую руку. Погладьте себя по голове хотя бы за то, что вы, читая эту книгу, предпринимаете попытку заняться своим здоровьем, восстановить зрение. Даже если у вас что-то не получается, это только поначалу. Научитесь прощать себе мелкие промахи и ошибки. Создайте к себе благодарность просто за то, что вы есть. Всегда можно найти, за что себя похвалить. Желая что-то для себя изменить, оставаясь таким же, вы тешите себя иллюзиями. Извините, но бесполезно смазывать язву, если внутри находится ржавый гвоздь. Все равно рано будет раскрываться. Созидание личности Вы думаете, только у вас есть проблемы? Их у всех предостаточно. Не зря говорят, каждому дому по кому. Но главное, есть такое правило, если не можешь изменить ситуацию, измени отношение к ней. Все просто. Созидание личности Вы думаете, только у вас есть проблемы? Их у всех предостаточно. Не зря говорят, каждому дому по кому. Но главное, есть такое правило, если не можешь изменить ситуацию, измени отношение к ней. Все просто. Созидание личности Вы думаете, только у вас есть проблемы? Их у всех предостаточно. Не зря говорят, каждому дому по кому. Но главное, есть такое правило, если не можешь изменить ситуацию, измени отношение к ней. Созидание личности Вы думаете, только у вас есть проблемы? Их у всех предостаточно. Не зря говорят, каждому дому по кому. Но главное, есть такое правило, если не можешь изменить ситуацию, измени отношение к ней. Вспомните, какая у него прохладная, влажная, нежная, сочная мякоть. Мысленно широко откройте рот и откусите большой кусок. Рот закройте, я попросил открыть мысленно. Ешьте лимон как яблоко. Холодный, кислый, душистый сок растекается по всему рту. Проглотите его и продолжайте жевать лимон с кожурой, без сахара. Откусите еще кусок. Замечательно. Доели до конца? Получилось? Молодец! Теперь посмотрим, как организм реагировал на этот воображаемый лимон. Слюна во рту появилась? Да? Тогда вопрос. Вы давили на свои железы? Говорили им, а ну-ка вырабатывайте слюну. Вы говорили своему желудку, а ну-ка выделяй желудочный сок. Вы этого не делали, а просто представили, что едите лимон. И все. Остальную работу сделал без вашего участия организм. Желудок уже приготовился переваривать несуществующий лимон. И все. Остальную работу сделал без вашего участия организм. Желудок уже приготовился переваривать несуществующий лимон. И все. Остальную работу сделал без вашего участия организм. Желудок уже приготовился переваривать несуществующий лимон. Вспомните огромный спелый бенгальский орех, откройте рот и откусите от него кусище. Да побольше! Жуйте, жуйте, хоть до конца света. Ну как? Получилось? Точно знаю, какой вопрос возник у вас в голове. Что такое бенгальский орех? Скажу как мужчина, емко и точно. А хрен его знает. Если бы знал, то обязательно описал бы. Это словосочетание сам придумал. Если вы понимаете цель «я хочу быть здоровым», ваш организм точно так же реагирует с недоумением, как вы сейчас на бенгальский орех, и спрашивает «а что это такое?». Хозяин, объясни, что ты хочешь. Опиши мне понятным языком. Почему на лимон организм реагировал? А потому что, когда вы думали о лимоне, в первую очередь вы представили объем, вес, цвет, запах, не так ли? Значит, разница оказывается в том, что больной человек, говоря, что хочет быть здоровым, ставит себе лимон. Свой организм в тупик. Получается, он хочет того, не знает чего. Любое ваше высказывание «я хочу быть здоровым» организмом воспринимается как «моя твоя не понимает». Он просит «ну что ты хочешь?» «Ну покажи мне, что тебе сделать?» Слово «здоровье» для него абракадабра, потому что оно относится ко второй сигнальной системе, которая в отношении выздоровления действует очень слабо. А вот на слова «таракан у вас в трусиках» сразу мурашки побежали по телу. Побежали или нет? Если нет, тогда поймайте одного жирненького и отпустите в штаны. Запомните. Вот так. Но, скорее всего, вы без эксперимента прекрасно все прочувствовали. Это потому что тараканов видели, штаны носите, сразу реально представили образ и возникла реакция. Вот здесь нашел еще одну закономерность застревания больного в недуге. От благого намерения, от благого стремления быть здоровым, вы попадаете в никуда. Внимание! Когда вы приступите к работе над зрением, поставьте ясную, четкую и конкретную задачу. Организм сразу начнет воплощать ее. Искусственно создавая образ выздоровления, создавая радость за то, что вы выздоровеете, вы осознанно начинаете управлять организмом, потому что задаете ему конкретную задачу. И он вынужден, не увиливая в сторону, подчиняться и выполнять. В образе лимона вы о слюне и не думали. Реакция получилась сама собой. Организм отзывается на образ. Вывод таков. Мысль и образ материальны. Значит, с помощью образа вы берете под свое управление весь организм. Это главная тронная заповедь. А сейчас еще одно подготовительное упражнение. Образ пяти пальцев. Поставьте перед лицом ладонь. И выберите один из пальцев на руке. Смотрите на него внимательно, без отрыва. Начните с любовью его изучать. Просматривайте изнутри и снаружи. Передайте свои мысли этому пальцу через глаза, через душу, через дух. Вложите в это упражнение самые светлые и искренние чувства. Не забывайте об улыбке. Что вы замечаете? Какова чувствительность этого пальца по сравнению с остальными? Вы заметили, что с каждой секундой он по ощущениям все больше отличается от других. Чем именно? Что вы там ощущаете? Тепло? Тяжесть? Покалывание? Набухание? Пульсацию? Все это правильно. Могут быть и другие индивидуальные ощущения. Некоторые слушатели курсов говорят, я чувствую только этот палец, остальные как будто не существуют. Обострение внимания к какому-то участку тела вызывает подключение дополнительных рецепторов. Ваше внимание меняет чувствительность там, куда вы его направляете. Таким образом, вы на тот участок оказываете влияние, причем как положительное, так и отрицательное, в зависимости от того, какие эмоции и мысли туда вкладываете. Тот же самый механизм подключается, когда вы работаете с глазами. Это так называемая идеомоторная реакция организма на мысли. Вы сейчас чувствуете свою спину? Неожиданный вопрос? Конечно, да. А если там какая-нибудь букашка ползает? Обостряется, и оттуда вы получаете полную информацию от прикосновения ножек этой букашки. Значит, обострение внимания в области какого-то участка тела вызывает в мозгу повышенную информативность этой зоны. А проявляется это в виде повышенной чувствительности. Или другой пример. Я не надоел вам своими примерами? Хорошо. Вы купались когда-нибудь в речке или в море? Конечно, да. Тогда, закрыв глаза, вспомните полдень жаркого дня. Синее небо, вода искрится под лучами яркого солнца, летают белоснежные чайки, вдалеке видны паруса, слышна музыка, дети радостно визжат на берегу. Вы заходите в воду до уровня плеч. Волны набегают одна за другой, ласкают тело. Постарайтесь представить поярче, и ваше тело будет отвечать. Какое наслаждение! Легкий ветерок обдувает влажное лицо. Закройте глаза и представьте, как вы стоите в воде. Вот прошел катер, приближается большая волна, вы ждете ее с замиранием сердца. Следующая волна. Вы балдеете, а крокодил-то, в упоении ожидая встречи с вами, медленно-медленно приближается к вам под водой, готовя свои губы-зубы для лобзания вашей попочки. Что вы ощущаете? Покачивание и мороз по коже в жаркий полдень. Это и есть идиомоторный ответ. Каждая клеточка тела реагирует на ваши мысли. Значит, уверенность в своих силах будет отражаться всюду, во всем теле, в каждой клеточке. Когда вы верите, что вы человек с большой буквы, когда вы знаете, что вы самая красивая женщина в мире, что вы мужчина что надо, тогда и внешность, и поведение, и результаты, любых ваших усилий будут соответствовать внутреннему состоянию. Мысли материальные, дорогой мой, не забывайте об этом. Древние говорили, прежде чем подумать, подумай. Примерим образ молодости. Он вам к лицу. Дорогой читатель, вы согласны, что в настоящее время люди стали хуже, злее, агрессивнее, завистливее, чем, скажем, пять лет назад. Да? Тогда вот вам ответ. Если вокруг все плохие, ищите причины в себе. Какой вы, таковы и окружающие люди, таков и мир вокруг вас. Подобие узнает подобие. Понаблюдайте сами. Только сильный человек никогда не обижается, никогда не нападает. Посмотрите в природе, какая собака громче гавкает и на всех кидается. Маленькая жучка, которая сама хвост поджимает, и хочет забиться в угол. Живет по принципу, лучший метод обороны — нападение. И человек, когда себя ни в грош не ставит, когда отношение к себе как к слабому существу, он становится похожим на шавку. Гавкает на всех, бросается, все вокруг такие сволочи. Где слабый дух, там вперед выходит агрессия. Так что давайте не будем показывать свою слабость и начнем создавать внутри образ совершенства. Согласны? Тогда ответьте на вопрос. Чем отличается мышление взрослого человека от мышления ребенка? Ответили? Дети мыслят образами, а взрослые словами. Поэтому сейчас нашими учителями будут дети. Нам с вами предстоит путешествовать в детство, путешествовать в юность. Вы готовы? Вам будет тяжеловато, потому что у нас с вами такое исковерканное восприятие. Такие мы с вами повзрослевшие. Как старый трухлявый пень на морозе. Особенно трудным это упражнение покажется бухгалтерам, экономистам, инженерам и тем, у кого желудочно-кишечный тракт имеет очень большой вес. Сейчас мы будем создавать свой идеальный образ, где внешне вы сам идеал. Внутренне сама гармония, где вообще отсутствует болезнь. Где лицо молодое и доброжелательное, а не старое и мымрообразное. А щеки из-за спины. Сияет румянцем. Постарайтесь приблизиться к эталону естественности. Дети фантазируют, думают путем образа, а взрослые думают через слова. Когда мы говорим «огурец», ребенок сразу представляет упругий, зелененький, весь в пупырышках, ароматный огурчик. А взрослый дает определение, словесную формулировку. Огурец – это огородное растение из семейства тыквенных с продолговатым зеленым плодом. А потом старается вникнуть и представить предмет, по его описанию. Разница есть? Образ здоровья, образ юности – это заменитель слова «здоровье». Бенгальский орех помните? Вот. Слова «здоровье» и «бенгальский орех» одинаковые по своей сути. Значит, сейчас будем фантазировать, создавать объект нашего подражания, эталон, идеал, на который потом будем равняться внешне и внутренне. Постарайтесь делать это как раньше, в детстве. Помните, пожалуйста, как вы в детстве танцевали, как играли. Или понаблюдайте, как дети играют. Для меня, например, это было откровением. Однажды дома я сидел, работал за своим письменным столом, а дети – моя дочка и ее подруга из соседнего дома – вдвоем играли в прятки. В комнате есть только одно кресло и пара одеял в углу. Знаете, как они ищут друг друга? Одна закрыла голову, все остальное торчит, а вторая ходит, ищет, но не видит. Понимаете? Она вошла, глаза смотрит, но не видит. Как вы думаете, она притворяется, что не замечает? А через некоторое время, когда пора наступает, находит. А та, которую находит, так пугается, аж виск-писк стоит. Ведь так хорошо спряталась, заткнув свою голову под одеяло. Я на них посмотрел, у них мимика была настоящего испуга, настоящей радости, все по-настоящему. Почему я вдруг об этом вспомнил и почему заговорил о детях вообще? Тогда малюсенькое объяснение, откуда возникло упражнение, которое мы с вами сейчас будем выполнять. Однажды мой друг из газеты позвонил и сказал, «Слушай, поедем на море? У тебя есть время?» Я говорю, «Есть». Едет целая группа психологов, я как журналист. «Хочешь, я тебя включу в список? Ты ведь психолог? Представляешь, в Крым на 35 дней за казенные деньги. Море, вино, шашлык, девочки, кустотерапия». Я согласился. Халява кому не нравится, скажите, пожалуйста. Это сладкое слово «халява». Поехал. И оказался в санатории для детей-инвалидов-сирот дошкольного возраста, страдающих сахарным диабетом. В первый день у меня был такой шок! Какое там море! Какие девочки! Какая кустотерапия! Я начал интересоваться, для какой цели мы приехали. Зачем здесь понадобились психологи? На одном берегу расположены три одинаковых санатория. Все лекарства они получают из одного аптечного управления. Продукты питания берут с одного продовольственного склада. Воздух, море – все одно и то же. Почему только в одном из трех происходит выздоровление детей от диабета, а в двух других – нет? Вот из-за этого туда неоднократно направлялись комиссия за комиссией, и в конечном итоге сказали, только психологи могут найти причины выздоровления детей в этом санатории. И я, случайно оказавшись на халяву среди психологов, обрек свою голову на страдания. Московские психологи две недели поработали, отписку сделали, отдохнули, уехали. А я там застрял на три месяца, потому что мне надо было докопаться до истины. Кроме всего прочего, получилось так, что на моих ушах, на моей шее я не могла найти причины выздоровления детей. Нашел. Благодаря этому вы тоже будете выздоравливать. Примерно через месяц-полтора наблюдений я начал замечать, что игры этих детей чем-то отличаются от других. Именно в детском восприятии, именно в детской искренности, именно в детском воображении кроется секрет. Так каким же путем они выздоравливают? Как срабатывает детское воображение? Прежде чем узнать это, мы должны знать, как относятся к болезни дети. И как же? Да никак! Они просто через короткое время, дней через десять, привыкают к болезни, как к своим штанишкам. У них ко всему происходит адаптация. Единственное, к чему дети не могут привыкнуть, к чему у них есть постоянная тяга от природы, это инстинктивность. Инстинктивная потребность в родительской любви и защите. Мать природа так устроила, что ребенок должен быть около взрослых. Внимание, ключ! Какая особенность есть у детей? Они всегда, вечно голодные до внимания, ласки, любви, особенно маленькие дети. Сколько вы ребенка не ласкаете, ему все равно мало. Через две минуты опять голодный около тебя стоит. А представьте ситуацию. Отец пришел домой, телек подключил, ребенок подходит. Ну что, как дела? Как день прошел? Иди сюда, поцелуй папочку, чмок. А теперь иди к мамочке, иди, не мешай папочке. Иди к бабушке. Все, ребенок не получил того, в чем испытывал колоссальную потребность. А мамаша тут же под затыльник. Не лезь в горячую духовку, обожжешься. А бабушка вместо того, чтобы приласкать, сидит и воспитывает. Хорошие девочки вот такими должны быть. Хорошие мальчики так себя не ведут. И вдруг, когда ребенок заболевает, происходит чудо. Отец забыл о своем телевизоре, на задних лапках прыгает, готов выполнить любое желание. Мамочка, которая не дает в горячую духовку голову пихать и шлепает всякий раз по попочке, кружится, ухаживает, переживает. Бабушка сказки читает, песенки поет. И дедушка откуда-то появился пыхтя. Все над головой. Происходит запись. Утоление голода и жажды любви связано с болезнью. Я спрашивал малышей. Моя милая, скажи, пожалуйста, когда ты очень хочешь, чтобы тебя погладили по головушке или что-то хорошее, и сказали, что ты делаешь? Двое из троих отвечали. Говорю, что голова болит или животик болит. Получается, что ребенок как бы притворяется. Вы согласны? Но! Ребенок не может притворяться, потому что его мысли моментально материализуются в теле, и они начинают болеть по-настоящему. Те дети, которые находятся с родителями, оказывается, болезнь воспринимают как источник ласки, заботы, внимания, любви. А у детей-сирот тоже огромная генетическая тяга к ласке. Природная потребность быть под защитой. Эти малыши подходят к работникам санатория, воспитателям, врачам и спрашивают. А почему у нее есть родители, а у меня нет? Воспитатели же не могут сказать, у тебя родителей вообще нет. И отвечают, у тебя тоже есть. А где мои родители? Когда они приходят, не забирают меня. А когда моя бабушка приедет? Когда дедушка? Им говорят, ты сейчас болеешь. Вот когда выздоровеешь, они придут и заберут тебя. Работающие там, оказывается, сами не знали, что их обман сработает на выздоровление. Они-то знали, что болезнь неизлечима. И дети, чтобы удовлетворить свое ненасытное желание быть нужными, любимыми, интуитивно начинают искать пути. И внутренний зов способен уничтожить любую страшную болезнь. Двух-трехлетний ребенок начинает интересоваться. А что такое болезнь? А что такое выздоравливать? Ему объясняют. У тебя в крови много сахара, понимаешь? Ты сахар не ешь. Малыши начинают очень быстро вникать, что от источника любви, защиты, покоя его отделяет какой-то бармалей, который называется болезнью. Я подружился там с цыпленком, трехлетней девочкой, и спросил у нее. Вот скажи, пожалуйста, что означает твоя болезнь? Она объяснила. У меня внутри много-много кусочков сахара друг за другом ходят. Вот из-за этого мои родители ко мне не приезжают. А когда ты очень тоскуешь по родителям, что ты делаешь, чтобы они скорее пришли? Она, держа в своем кулачке мой мизинец, привела меня во двор. А там стоят штук 70 разноцветных пластмассовых ванночек. Утром работник санатория заполняет их из шланга морской водой. Санторий под солнцем нагревается, и к обеду детей в этот лягушатник запускают. Они там парахтаются. Девочка залезла в ванночку и начала там плескаться, приговаривая какие-то слова. С трудом понял. Она, оказывается, бесконечно говорила «я сахар, я сахар, я сахар». Я спрашиваю. А зачем ты говоришь «я сахар»? До сих пор вспоминаются глаза в пол лица, будто смотрит на идиота. Как может такой взрослый человек не понять самого понятного? Сахар-то в воде исчезает. Воображение у детей работает буквально. И они каждый по-своему играют. А эта игра, оказывается, их исцеляет. Только потом, когда я рассказал об этом воспитателям, они в один голос воскликнули. Ах, вот почему многие наши малыши второй раз в эту же самую воду не забираются. Они видят, оказывается, что враг там растворился и ждут, когда эта вода уйдет. Понимаете? Общаясь друг с другом, они быстро передают опыт выздоровления, технологию добывания своих родителей. Я узнал у них несколько способов. Одним из них поделюсь с вами. Итак, сегодня мы будем фантазировать как дети. Постарайтесь дать волю своему воображению увидеть себя, ощутить, почувствовать в этой фантазии все, что вы считаете свойственно гармоничному человеку, совершенному человеку. Договорились? Будем... подать в детство. Зачем нам это? Вот обратите внимание. Мы с вами общаемся, я вам что-то объясняю, доказываю, а вы сомневаетесь, не верите, делаете вид, что думаете о чем-то. А я, когда раньше с детьми работал, заметил такую особенность. Сто очкариков из ста быстро восстанавливают зрение. Они приходят на занятия. Их внимания хватает максимум на пять минут. Уже через пять минут начинает отвлекаться, ерзать, потом общаться друг с другом, разговаривать. Они только делают вид, что слушают. А на самом деле вообще ничего не понимают, не вникают, им до лампочки. О чем бы я ни говорил, они запоминают только последнюю пару фраз. Все остальное не помнят. Потому что им умные доказательства не нужны. Весь парадокс в том, что они не понимают, о чем идет речь. Они ничего не спрашивают, не уточняют. Когда я говорю, дети, домой? Они с радостью исчезают. Но тогда почему зрение у всех восстанавливается? Из двухсот детей у двухсот зрение восстанавливается. Не было такого случая, чтобы результат был у 199. У всех. Из ста глухих детей у всех начинают открываться ушки. Они даже не понимают, о чем я говорю, но у всех стопроцентный результат. Как-то на занятии мы им сказали. Знаете, дети, наши взрослые слушатели, ваши мамочки, папочки, дедушки, бабушки, свои шрамы убирают. Вы над шрамами еще будете работать, делать специальные упражнения. На следующий день приходит ребенок. А у меня шрам исчез. Им только один раз сказали, что взрослые сами убирают шрамы, и все. Этого оказалось достаточно. Никакого упражнения с ними не делали. Но шрам исчез. Что это такое? Просто дети думают образно. Желания, стремления детей всегда воплощаются как бы сами собой. У них вторая сигнальная система сильно не развита, слава Богу. Вторая сигнальная система – это слова, речь, посредник в нашем восприятии окружающего мира. Разговорная речь – самая легкая, самая простая, самая примитивная форма общения. В этом примитиве у нас, взрослых, и происходит застревание. Потому что, когда вам говорю, что надо делать, как надо делать, я вам говорю через противогаз. А вы смотрите, нюхаете через противогаз и говорите, не понял, ты мне еще раз объясни. Вторая сигнальная система – это, образно говоря, противогаз, через который мы хотим улавливать запах. Попробуйте вспомнить себя в детстве. Я, например, в прошлом году встретился со своим детством, когда был в родительском доме, зашел на чердак. Просто вдруг вспомнил, что здесь у меня была комната, огромная комната, где я ходил лет 30 тому назад. Это был мой дворец. Я пришел туда, а мой тайник найти не могу. Кроме толстого слоя пыли – ничего. И вдруг внизу, где шифер уходит к углу, там увидел знакомое очертание. Подошел, опустился на корточки и вот так зашел туда. Оказывается, рост у меня был такой, что я там ходил с гордо поднятой головой. Я ходил в то место, где всегда хранил свое богатство. Когда открыл, там были кусочки от фафоровых чайников, побитые пробки от лимонада, денежки, чайная фольга, фантики от Ириса Золотой Ключик и обертки от шоколадных конфет Чайка, которые получил в подарок на Новый год. Я ходил с растопыренными карманами, потому что все это носил с собой. Помню, у меня была игрушка, какой не было ни у кого больше. Это был настоящий автомобиль. Я мог ходить везде, и по батареям, и по подоконникам, где угодно. А на самом деле это просто кусок от мотоциклетного аккумулятора. Понимаете, насколько богато детское воображение? Вспомните себя, где остались нераскрытыми ваши сокровища, где зарыты клады. Значит, когда мы сейчас будем фантазировать, попытайтесь фантазировать, как в детстве. В этой фантазии постарайтесь увидеть, ощутить и запомнить себя именно таким, каким вы намерены быть, увидеть, как выглядят ваши ноги, руки, торс, лицо, глаза. Каково там внутри, в душе? Ощущение легкости, гармонии, безмятежности, счастья. Сначала мы выдумываем, создаем образ, как художник, а потом запоминаем бархатность кожи, упругость тела, покой в душе и так далее. Теперь мы попробуем работать не только с глазами, но и со всем организмом. Начнем фантазировать. Представьте, что вы бессмертный человек. Вы могли бы людям открыть свою тайну? Вы могли бы кому-нибудь рассказать о своем бессмертии? Нет. Вас разорвут на тысячи кусочков, чтобы в лабораторных условиях изучать, исследовать и в конечном итоге найти способ, как самим стать бессмертными. Они начнут бесконечно экспериментировать. И вы не сможете помешать этой толпе людоедов. И вот из-за этого вы вынуждены, находясь среди людей, выглядеть как все. Как все стареть, а потом каждый раз уходить, менять место жительства. А потом возвращаться на старое место, как сын, внук того, кем вы были тогда. Теперь о том, как вы оказались бессмертным. Более 10 тысяч лет назад на ваше племя напал неприятель, и были уничтожены все. И все, кто уцелел в этой резне, израненным уходили от преследования. Враг шел по следу, и уже спиной вы чувствовали его горячее дыхание. Стрела в одну ногу, стрела в другую ногу. Вы в ужасе заползли в горную пещеру и, стараясь укрыться от приближающихся шагов, прислонились к скале. Здесь оказалось какое-то ослабленное место на границе с параллельным измерением. Ваш внутренний ужас запустил какие-то механизмы, и вдруг, протиснувшись в узкую щель, вы провалились туда, упали в соседнюю пещеру, и там просидели несколько дней. Ваши раны как-то неестественно быстро зажили, силы восстановились, и когда вы окрепли и вышли на поверхность, то заметили, что все вокруг как будто в зеркальном отражении. Солнце встает с другой стороны, и время идет в обратном направлении. Затем холмом вы искали свое селение, но не нашли. Кроме буйной травы, ничего, никаких следов пребывания человека. Вы опять возвратились в пещеру и стали вспоминать, как попали сюда, как нашли щель на изломе двух миров. И с того момента началась ваша жизнь. В этом мире вы стареете, как все, болеете, страдаете, переживаете все, что свойственно обычным людям. Но в один прекрасный момент, когда вам становится невмоготу, вы встаете в горы, открываете дверь в другое измерение и уходите в тот мир, где время течет вспять. За короткий период вы начинаете обновляться, потому что там действует закон компенсации. Но вся беда заключается в том, что тот мир открыть только для вас. Никто другой не может пройти туда. И так как человек не выдерживает одиночества, вы вынуждены вновь и вновь возвращаться к людям. Вы видели, как строились пирамиды, как люди растаскивали чистую, голубовато-белую глазурь, покрывавшую эти пирамиды. Вы видели, как разрушался Вавилон, вы были в рядах Чингисхана. И там есть дом, который вы построили собственными усилиями тысячи-тысячи лет тому назад. Двухэтажный маленький домик среди слияния четырех стихий. Там пыль и тление отсутствуют. Все хранится в нетронутом виде. Все находится в первозданном виде. Этот ковер подарил вам сам император, а рыцарские доспехи принадлежали отцу Генриха IV. На втором этаже ваша спальня. Окна открываются на четыре стороны. Представьте. С одной стороны океан, с другой — бескрайние поля, уходящие за горизонт. С третьей — благоухающий лес с залитыми солнцем цветочными полянами. С четвертой — горы. С четвертой — вечером. Помылись и легли в постель. Начнем с момента пробуждения. Окна настежь открыты. За ночь регенерация в вашем организме уже начала происходить. Белый тончайший тюль колышется у окна. Глазки закрываем. Океан. Утро. Вы в постели. Окна открыты настежь. Легкий прохладный ветерок. Неся в себе запах земли и запах цветов. Запах гор. Запах хвойных лесов. Влажные тросы. Запах океана. Осторожно с любовью гладит ваши волосы, ваше лицо. И шепчет вам. Доброе утро, радость моя. Доброе утро, любовь моя. С возвращением. Открой глазки. Тебя ждет волшебный день. Создайте образ. Представьте. Вы мысленно потягиваетесь. Нежно. С наслаждением. Открываете глаза и видите, как поднимается солнышко. Постепенно пробуждаясь, как ребенок, поднимаете свою головушку с подушек облаков. Вокруг птички ликуют, сходя от радости с ума, потому что каждое ваше появление в этом мире вызывает укрепление всего живого, укрепление всего, что там есть. Вы действуете на этот мир омолаживающе, и он на вас точно так же. Вы встаете. Представьте. Босиком ступаете по прохладному полу. Спускаетесь по лестнице, выходите на крыльцо и… О, Боже! До чего чисто! До чего свежо! До чего прекрасно! Ушли вчерашнее утомление и усталость. Еще раз мысленно, с хрустом, потягиваетесь. Перед вами песчаный берег океана, прозрачная гладь воды. Вы берете полотенце и по еле заметной тропинке, наступая на влажную от росы траву, вдыхая свежесть утра, ароматы цветов, идете в сторону водопада, где бушует стихия. И это не простой водопад. Возникнув из маленького родника в расщелине скалы, пробивая себе дорогу, он вобрал силу скалы и превратился в мощный поток. Он очистилась и обрела волшебную мощь. Приближаясь к своему водопаду, вы подходите к подножию скалы и любуетесь игрой солнца в хрустальных брызгах воды. С наслаждением вдыхаете влажность и чистоту воздуха. Растворяетесь в нем. Представьте, в каждой капельке солнечный свет, в каждой капельке радуга. Этот водопад несет в своих ладошках радужное освещение. Ожерелье чистоты, молодости, свежести, бодрости, красоты. Вы мысленно сбрасываете с плеч одежду, а вместе с ней все прошлое. Возраст, тяготы жизни, обиды, сомнения, напряжение. Все, что накопилось за эти годы. Солнышко и ветерок начинают ласкать ваше тело. Мысленно, сделав глубокий вдох, уходите под водопад. Представьте. На вас обрушивается каскад радости, каскад бодрости, счастья, свежести, юности. От прикосновения волшебных капель вибрирует все тело, а кожа становится гладкой, упругой, бархатной, как у младенца. Почувствуйте, как вода струится по коже, смывая все поверхностное, наносное, ненастоящее. С каждой секундой вы чувствуете все большую и большую легкость, бодрость, чистоту. Вы наслаждаетесь тем, что происходит с вами, и... О, чудо! Вдруг вода начинает проходить насквозь, унося все, что чуждо здоровью, все, что чуждо спокойствию, все, что чуждо гармонии. Вы видите, как густая черная слизь ваших потерь, обид, разочарований, поражений, болезней стекает под ноги и исчезает в волшебной воде, и вы чувствуете, как вода струится навсегда. Вы ощущаете, как с каждой секундой обретаете все большую легкость, свежесть, бодрость, величественную красоту и вместе с тем силу, уверенность и царственное спокойствие. Вы выходите из-под водопада. Мысленно посмотрите на свое тело. Ноги, руки, спину, живот, грудь, шею, лицо. Посмотрите на себя со стороны. Вы видите, как вода струится навсегда. Вы видите, как вода струится навсегда. Вы видите, как вода струится навсегда. Вы видите, как вода струится навсегда. Вдохновение и силы. Потому что вам еще предстоит вернуться на поле битвы, где добро и зло находятся в вечном бою за душу человека. А теперь всматриваемся в небеса. Вы ясно видите птиц, парящих высоко-высоко. Создайте желание мысленно взлететь. Представьте, как вы улетаете ввысь, чтобы впитать чистоту небес. Представьте, как вы улетаете ввысь, чтобы впитать чистоту небес. Представьте, как вы улетаете ввысь, чтобы впитать чистоту небес. Представьте, как вы улетаете ввысь, чтобы впитать чистоту небес. Фантазируем. Создаем образ полета. Впереди белые, серые облака и черные грозовые тучи, но одновременно светит солнце. Молния еще далеко, но гроза приближается. Первые капельки. Впитываем солнечный свет. Впитываем чистоту. Впитываем солнечный свет. Впитываем свежесть. Впитываем образ красоты, образ юности, образ зоркости. Представьте, как ваше тело с каждой минутой, с каждой секундой приобретает одну за другой именно те черты, о которых вы мечтали. Фантазируем полет. Приближаемся к белым облакам, несущим в себе сказку детства, сказку юности, несущим в себе первую любовь, чистую, возвышенную, несущим в себе звездные ночи и проходим насквозь. Вы впускаете в себя великий гимн юности, беззаботности, свободы. Ливень приближается. Он уже виден. Далеко впереди радуга. Вот воображаемые поля. И вы вбираете в себя запах влажной соломы, аромат знакомых и незнакомых цветов. И вы в полете. Вы свободны. Вы великих влюбленных. Небо и земля в едином экстазе любви. И от этого экстаза в небе разбушевалась гроза. Ливень, молнии и гром. Представьте образ. Вспомните звуки грозы и постарайтесь услышать их сейчас. Молния на какое-то мгновение распахивает окно в удивительный соседний мир, и вы видите четкие картины. Представьте, что находитесь в небе. Вы, разбушевавшиеся стихии, и вбираете в себя силу молнии. Ливень смывает с вас все прошлое и наполняет весенней свежестью и чистотой. Раскинув в полете руки и ноги, вы втягиваете в себя волшебную силу стихии, пушующую ураган любви, ураган созидания, ураган творения, долгожданную чистоту, долгожданную бодрость, свежесть, легкость, щедрость, доброту. Каждая волосинка, каждая клеточка, каждый сосудик накапливают эту мощь. Волну за волной, по нарастанию. Запомните внутренние ощущения свободы. Запомните. В этом воображаемом полете впитайте все, что вы считаете совершенным. Браво! Умница! Молодец! Фантазируйте и запоминайте. Вы видите себя совершенным. Гениальный художник. Продолжаем дальше фантазировать. Представьте, что вы возвращаетесь к своему домику. Стихия остается далеко позади. Вы опускаетесь у крыльца. Создайте ощущения, создайте чувства. Мысленно входите в дом, подходите к зеркалу и смотритесь в него. Вы видите себя сильным, молодым, здоровым, зорким человеком. Полюбуйтесь собой. А теперь танец. В этом воображаемом танце постарайтесь быть принцессой, королевой, повелителем, повелительницей, кем хотите. Представьте себе, вы видите огромный зал, блестящий паркет, колонны, а за колоннами море и звездная лунная ночь. Вы можете посчитать звезды, каждую из которых четко различаете на небосводе. Музыканты в белых одеяниях смотрят на вас лукавыми и добрыми глазами. Они будут играть только для вас. Вы появляетесь в бальном наряде, и все присутствующие обращают свои восхищенные взоры на вас. Музыканты как будто ждали этого момента, и при вашем появлении начинает играть величественный вальс. Вы мысленно танцуете с любимым человеком, не отрывая друг от друга взора, полного нежности, любви и счастья, надежд, мечтаний, стремлений. Включаем торжественный вальс и мысленно танцуем. Включаем торжественный вальс и мысленно, стоя с закрытыми глазами, начинаем танцевать. Кружимся в воображаемом танце. Дайте волю своей фантазии. Видим себя изнутри и со стороны. Запоминаем. Запоминаем все. И водопад, и полет, и танец. А главное, запоминаем внутреннее состояние, состояние души. А теперь танго. Настроились. На лице легкая улыбка. Плечи расправлены, как крылья. В глазах блеск. Задор, озорство, молодость, счастье, любовь. Браво. Молодец. А теперь поднимаем обе руки над головой, потягиваемся, открываем глаза. Можете сказать мяу. Сказали? Слава Богу, что хоть не разучились говорить на нормальном языке. А теперь сравните, пожалуйста, свой сегодняшний облик с образом из вашей фантазии. Есть разница? Да. Вот эта разница и есть тот объем работы, который необходимо выполнить в ближайшие полтора-два месяца. И запомните, любовь, уважение к себе, любое внимание к себе не могут остаться без результата. Каждым из нас скрыта сила. Многие из нас не знают своих внутренних возможностей. Но в каждом они есть. Надо только уметь разбудить их, заставить работать на себя. То, что вы достигли в жизни во всех отношениях, есть лишь малая частица заложенного вас потенциала. Говорю это как специалист с многолетним опытом работы. Подтверждение тому — экстремальные ситуации. Вот несколько примеров. Как вы думаете, откуда взялись силы у 70-летней женщины, чтобы с 40-летним сыном на спине пройти 13 километров через снежные сугробы и ни разу не опустить его на землю? А это факт. Когда люди стали пробираться на снегоходе к месту аварии, чтобы спасти остальных, оставшихся под трактором, они увидели на протяжении всего пути следы только одной пары ног. Как смогла женщина в Санкт-Петербурге на уровне 7-го этажа удерживать одной рукой своего двухлетнего ребенка, сватившись за кирпич карниза только указательным и средним пальцами другой руки, пока не подоспела помощь? И когда ее снимали, то она не могла разжать пальцы. Несколько часов ее успокаивали, чтобы она отпустила руку своего ребенка. Вот какая сверхъестественная сила. Но эти силы находятся в спящем состоянии, потому что для обслуживания желудочно-кишечного тракта, большого ума не надо. Вот почему задействованными оказываются лишь 3-4% мозга. А человек может очень много читать мысли, предвидеть будущее и тому подобное. В вас они тоже есть, дорогой читатель. Мы должны найти доступ к этим силам. Но одним потребительским желанием, типа «жрать хочу», эта дверь не открывается, потому что ведет она в волшебный мир, где будут исполняться ваши желания. Это мир, где вы, являетесь повелителем. Двери в этот мир открываются тремя ключами. Силой воли, душой и разумом. Надо слить все воедино. Вы готовы? Тогда поехали. Истошный вопль и шага. Октава. Что это такое? Это название специального упражнения, направленного на пробуждение той скрытой силы, которая есть в каждом от природы и которая будет вас вести к выздоровлению. В чем суть упражнения? За 20 лет работы я много раз объяснял Октаву, но ни разу не был удовлетворен тем, как это получалось. Почему? Потому что только безумный возьмется объяснять, что такое счастье, что такое нежность, что такое любовь. Скажите, пожалуйста, вы могли бы передать словами самые высокие и яркие душевные переживания? Попробуйте. Затрудняетесь? Вот и для меня это очень тяжело. И все-таки я попробую, потому что ничего другого мне не остается. Что же такое Октава? Это особое упражнение для тренировки духа. Какие чувства и ощущения можно вызывать, сейчас постараюсь объяснить. Представьте себе, вы идете по дороге, а вам навстречу бежит годовалый или двухлетний малыш-топотушка. Вот сейчас он подбежит к вам, вы его возьмете на руки, он обнимет вас своими ручонками, прижмется, поцелует теплыми, пухлыми, влажными губками. Вы ощутите родниковое дыхание у лица, и все вокруг для вас перестанет существовать. Вы почувствуете себя по отношению к этой крошке сильным, могущественным покровителем, и вместе с этим душа наполнится чистотой, нежностью, блаженством, любовью. Наполнится или нет? Да. Или вы видите, впереди вас по дороге идет ваш сын. Вы смотрите на него, и сердце переполняется гордостью. Это мой сын. Именно такие чувства и переживания служат основой, внутренней опорой лучших ваших стремлений, на которой будет строиться ваша цель. И в то же время октава – это движущая сила к осуществлению цели. Октава – это искусство управлять своим телом, приказывать ему, заставлять, вынуждать его постепенно внешне и внутренне перевоплощаться в тот облик, в который вы желаете. Это утверждение, это внутреннее ощущение, что будет именно так, как вы этого захотите. Это уважение в душе уверенности, силы, могущества. Древние говорили, что наверху, то и внизу. Несколько перефразировав эту мысль, можно сказать, что внутри, то и снаружи. Внутреннее созидание дает пользу. Уважаемый мужчина, скажите, вы думаете, что вы мужчина? Вы верите, что вы мужчина? Вы надеетесь, что вы мужчина? Или того лучше, сомневаетесь, что вы мужчина? Или вы знаете, что вы мужчина? Какой ответ выбираете? Аналогичный вопрос задам женщинам. Будете ли вы раздумывать и сомневаться, что вы женщина, а не мужчина? Нет? Вот уловите, какое ощущение возникает внутри, когда вы точно знаете, что это так и никак иначе. Как будто бы вы добрый волшебник, уверенный в силе своего волшебства. Вы просто даете повеление и уходите. Ваше повеление такой силы, что все задуманное сразу исполняется. Такое же утверждение нужно искусственно создавать внутри, в отношении выздоровления. Октава – это сочетание уверенности, решительности, силы, мощи, могущества, твердости и нежности, любви, доброты, трепетности, чувства полета и радости. Это самое уверенное знание, что будет так, как вы этого захотите. Откуда же черпать эти ощущения? Из пережитого опыта. Расскажу вам одну историю. Встретились три столетних старика. Сели и, гоняя чаек, стали обсуждать, какой музыкальный инструмент лучше. Один говорит, лучше флейты не бывает. Второй, у тебя нет никакого вкуса. От флейты у меня понос. А вот скрипка обращается к третьему. Друзья мои, твоя скрипка помогает тебе совершать мужские подвиги? А тебе твоя флейта помогает? Оба на него набросились. Ты что, мозги, что ли, съел? Мы говорим о музыке, а не о прошедших временах. Я уже 20 лет на женщин не смотрю. Он – 30 лет. А ты что? Я же говорю, у меня все в порядке. Твоя старушка умерла 60 лет тому назад, а твоя 15 лет назад. Моя жена до сих пор красится. Я вчера купил ей духи, тройной одеколон. Они снова на него накинулись. Ну давай, расскажи, что это за музыка? Приведу сегодня внука с магнитофоном, пусть запишет. Может, у нас тоже что-то получится? О, ночью эта музыка слышна в нашем селении. Открываешь окна и слушаешь. Но в последнее время что-то редко ее слышно. А что за музыка? Когда я слышу эту божественную музыку… Ну не томи, что за музыка? Это когда ночью осел орет. Когда среди ночи кричит ишак, я себя чувствую 18-летним юношей. Поняли оба старика. Их было трое старейших в селе. Теперь осталось двое, потому что третий сошел с ума. Смотрите на меня так. Помните, когда я первый раз пошел к своей старушке на свидание? Помним, мы на стреме стояли. Ночью я перелез через забор. И когда шел в темноте, наступил на хвост их ишака. Этот поганец заорал. А помните, какой злой был у нее отец? Да! Я постучал в окно, взял ее на руки и понес в сторону сада. Ах, какая была лунная звездная ночь! Под цветущей яблонью путь наградил меня медом из ее уст. Я почувствовал от ее волос, как божественно пахнет в раю. Но от крика этого поганого ишака все ослы в нашем селе проснулись и начали орать «И-я! И-я! И-я!». Я был готов их растерзать, потому что тогда они испортили все наше свидание. А сейчас, по ночам, мы открываем окна, прикладываем руки к ушам и сидим, ждем. Когда начинают кричать ишаки, у нас первая брачная ночь. Значит, октава — это что? У каждого есть свой образ, свой ишачий крик. Это жизнеутверждающее стремление к свету, добру, любви, к тому, с чем вы ассоциируете свою силу, свое величие, юность, здоровье. Всякое действие, которое ведет нас к созиданию, творению. Когда вы от всей души говорите «Все! Хватит быть рабом болезни!» Рабом нужды — это тоже октава. А октаву можно найти во всем. И в радостном крике ребенка, и в песне, спетой от души, и в добром слове, сказанном с чувством, и в светлых переживаниях, и... У каждого найдется что-то свое, сокровенное. Во время упражнения вы сначала внутри искусственно вызываете гамму чувств, а потом постепенно добавляете интенсивность, затем еще, еще и еще. Чтобы во всем теле, и особенно в области нездорового органа, звучал гимн созидания. Чтобы каждая клеточка тела получила резонанс. Внутреннее стремление, утверждение должно быть властелином в теле, чтобы подавить, уничтожить, стереть в порошок все другие мысли и чувства, чуждые созиданию, чуждые творению. Ваша октава должна разнести в пух и прах всю неуверенность, сомнения и страхи, уничтожить их навсегда. Постарайтесь почувствовать свою значимость, силу, мощь, красоту, утверждать это в себе и шаг за шагом продвигаться к цели. Каждый день старайтесь это ощущение восполнять новыми оттенками, новой гаммой чувств. По мере тренировок вы будете становиться более спокойным, сильным, уверенным в себе человеком, защитой, поддержкой и опорой для людей, которые вас окружают. Вы станете для них оазисом в пустыне, будете нести свет, добро и любовь. Когда вы начнете работать с глазами, то ощущение, утверждение, команду, приказ, повеление глазам быть такими, какими вы хотите, чтобы они были, направляете туда. Там создаете радость и получаете ответную реакцию. Это внутреннее слияние с тем образом молодости, который вы только что создали. Но как всегда соблюдайте технику безопасности. Следите за тем, чтобы сердце работало спокойно, голова была ясная, дыхание ровное. Внутри снисходительное спокойствие и абсолютное знание того, что будет так, как вы этого хотите. Вы должны знать, что любовь, которая вы получаете, это любовь, которая вы получаете, это любовь, которая вы получаете, это любовь, которая вы получаете, создайте это утверждение. Плечи поправили, на лице гордая улыбка и поехали. Мысленный взор направляем в глаза и вызываем там ответную реакцию. Вспомните образ лимона или образ пяти пальцев, как организм реагировал. Сейчас функцию этого пальца будут выполнять глаза. Внутреннее стремление должно быть очень сильным. Это внутреннее утверждение, типа, я есть воля, я есть сила, я есть любовь, я есть прощение, я есть могущество, я есть юность, я есть молодость, я есть здоровье, я есть мудрость, я есть жизнерадостность, я есть все прекрасное, все здоровое. Зависит от меня. Все в моих руках. Это стремление должно господствовать и уничтожить всякое сомнение, которое само собой фигушки уйдет. Не ждите, чтобы оно покинуло вас, сказав на прощание, ну что ж, до свидания, ты меня выгоняешь, я ухожу. Оно зубами, когтями будет держаться в вашем сознании. Вы просто скажите, тьфу на тебя, и усильте констатацию юности, здоровья и бодрости. Окей? Заставьте себя ощутить личностью, человеком с большой буквы. Создайте внутренние чувства, что лучше вас никого в мире нет. Надеваем на стальную руку мягкую бархатную перчатку и берем все управление под контроль. Получилось? Отлично. Это упражнение применяется всегда во время коррекции зрения или работы с любым другим нездоровым органом. Время суставной гимнастики и упражнений для глаз. А также стирая белье, убирая квартиру, трясясь в общественном транспорте и так далее. А особенно после неприятного разговора с начальником или в другие подобные жизненные моменты. Это упражнение нужно делать и на улице, среди людей. Они будут давить на вас, и сами того не подозревая пытаться превратить в себе подобных, то есть в однородную массу. Замечаете ли вы, что отличаетесь от них? Да? Чем? У них у всех сплошные штампованные рожи, рыло и морды с овковой лопаткой. Почему так? Потому что так принято, все так ходят. А вы — личность. Очень важное замечание. Когда вы выполняете упражнение, а голова забита посторонними мыслями, все ваши усилия будут пустой тратой времени. Весь труд пойдет коту под хвост. Заниматься надо в состоянии октавы. Только тогда вы достигнете успеха. Скажем, у меня проблемы с коленной чашечкой. Я мысленный взор направляю туда и представляю свой образ здоровья такой силы, с которой я летал, танцевал в образе молодости и посылаю его в коленную чашечку. А затем сознательно увеличиваю, наращиваю положительные ощущения, и улавливаю там физический отклик. Октава для вас — есть что-то старое и одновременно что-то новое. Искусство усилием духа созидать, искусство перевоплощаться в того, кем вы намерены быть. Внешне это никак не проявляется, лишь на лице у вас должна быть легкая улыбочка и спокойствие. Спина ровная, голова ясная и спокойная. Дыхание светлое, дыхание спокойное. Сердце бьется ровно. Октава работает внутренне. История одного из моих слушателей. Сегодня это известный в Средней Азии композитор. А всего несколько лет назад, когда у него зрение было минус 18 диоптрий, ему подписали приговор. Рекомендуем вам ежедневно упражняться. Своими глазами запоминать расположение предметов. Когда ослепнете, вам легче будет адаптироваться к своему состоянию. Другими словами, ему предложили постепенно готовиться, привыкать к будущей слепоте. Тогда он, оказывается, решил покончить с собой. Страшно мужчине в полном расцвете сил оказаться на издевении у своей жены. Но поднять на себя руку все-таки не хватило сил. Все его душевные переживания запечатлелись в его сердце. Восстановление зрения будет идти волнообразно. Внимание! Механизм восстановления зрения такой. В течение часа-двух идет улучшение, улучшение, улучшение, потом полчаса-час ухудшение, затем снова улучшение и так далее. Такие колебания у всех бывают разные. У кого-то эти периоды могут измеряться секундами. 5-6 секунд прояснение, потом 2-3 секунды спад, который затем опять переходит в подъем. У кого-то подъем может продолжаться 10 минут, а потом 2-3 минуты спад. У кого-то колебания качества зрения будут выражаться в часах, у кого-то в днях. Но общая тенденция такова. Всегда период улучшения зрения более продолжительный, чем период ухудшения. Всегда держите хвост пистолетом. В целом, несмотря на такие колебания, зрение постоянно становится лучше. Кривая улучшения зрения всегда идет вверх. Главное продолжать работу. С каждым днем процесс восстановления зрения будет набирать силу. Не забывайте, существует закон инерции. Если вы маховик запустили, то нужно постоянно поддерживать его движение, иначе инерция падает и маховик останавливается. Внимание, опасность. Одна дама рассказала, у меня было плюс 7 диоптрий. 5 диоптрий убрала, а 2 ну никак не могу. Почему? Потому что она начала за здравие, а закончила за упокой. Она стала успокаиваться. Делать упражнения машинально, ради галочки. Эмоций уже стало недостаточно. Автоматизм – это враг любого доброго начинания. Всякое успокоение долой. Сначала доведите дело до конца. Договорились? Отлично. Прямите мышечный корсет. Улыбку натянуть не забудьте. Искусственно вызывайте внутреннюю радость за то, что сейчас зрение начнет улучшаться. Все делаем вверх тормашками. Потом повышаем эмоции до тех пор, пока зрение не пойдет. Прояснение начинает появляться подобно тому, как вы объективом наводите резкость или ручкой радиоприемника и ищите нужную частоту. Легкую вспышку, которую вы можете сделать, чтобы улучшение заметили? Очень хорошо. Тут же зафиксируйте для себя это состояние. Есть! Удалось поймать нужную волну. Явный проблеск улучшения у вас обязательно появится примерно за 30 секунд работы с таблицей. Зрение пошло. Поздравляю! Ваша задача теперь прислушаться к себе. Уловить ощущения и запомнить, что вы сделали. Как изменили внутреннее состояние для того, чтобы зрение стало вам подчиняться. В следующий раз при очередном подходе вы это внутреннее состояние будете вызывать сознательно и стараться удерживать настрой как можно дольше. Поскольку вы обучаетесь самостоятельно, нужно, чтобы кто-то или что-то выполнял функцию секундомера. Сделайте запись счета от 1 до 30 на магнитофонную пленку. Смотри ниже в главе 3 подхода по 30 секунд за один прием. В следующий раз, когда первые проблески улучшения возникнут, обратите внимание на внутреннее состояние и запомните, на какой отметке счета они появились. Например, сначала зрение пойдет на счете 20. В следующий раз попытайтесь запустить на счете 15, потом 10. Дальше, после 30 секунд внутренний настрой вы продолжаете держать. А зрение пойдет на счете 20. В следующий раз будет все улучшаться и улучшаться. В следующий раз нужно уже получить легкое дополнительное улучшение, например, на счете 5. Процесс идет волнообразно. Вспышки четкости то появляются, то пропадают, но с каждым днем, с каждой тренировкой они будут появляться раньше и держаться дольше. Постепенно отдельные знаки и фрагменты сольются в целую рабочую строку. Если вы создадите свой уникальный инструмент для коррекции зрения, для восстановления зрения достаточно заниматься два раза в день по 10 минут. Как вызвать улучшение зрения, запомнили? Как его удержать, запомнили? Что нужно всякий раз фиксировать состояние «есть, удалось поймать нужную волну», запомнили? Молодец! Отлично! А теперь опять техника безопасности. Бывают интересные ситуации. В набережных Челнах один мужчина был очень старательный. На протяжении пяти занятий он постоянно поднимал руку. Улучшение шло полным ходом, а потом остановилось. Спрашиваю, почему стоите? Что случилось? А я, говорит, не вижу последнюю строчку. Вы с какой строки начали? Со второй сверху. Он, оказывается, на расстоянии вытянутой руки еле различал вторую сверху строку таблицы и всего за пять занятий дошел до самой нижней. Это категорически запрещается. Он нарушил технику безопасности и процесс восстановления зрения остановился. Вот так. Будьте внимательны. Не напрягайте глаза. Разгоняться нужно постепенно. В этом узнать один секрет. Принцип восстановления зрения и устранения любого другого заболевания один и тот же. Вы используете основной инструмент – ваш созидательный дух. Создавая нужные эмоции, вы через кровь заставляете свой организм выздоравливать, то есть начинаете управлять этим процессом. Этапы восстановления зрения. Всю работу мы разделяем на этапы. Первый, маленький, два-три дня, который называем запуск зрения. Потом сразу же начинается второй – восстановление зрения. Этот этап у каждого сугубо индивидуален. У кого две диоптрии меньше, восемь дней, мы уже об этом говорили. Переходите к третьему этапу – адаптации к нормальному зрению. Очень важно именно в этот момент не прекращать работу. Некоторые делают ошибку, считая, что очки сняли, и все, чего еще желать. Но нам надо, чтобы возник условный рефлекс к нормальному зрению, чтобы оно стало привычным для организма и зафиксировалось мозгом. Условный рефлекс возникает в период от 20 до 40 суток. Представьте ситуацию. Вы начали работать. Появились первые результаты. Образно говоря, приходит инвентаризационная комиссия с амбарной книгой и говорит, у вас записано глаза две штуки, катаракта, предъявите. Проверили, все на месте. Отлично, пошли дальше. Вы стали работать, и катаракта начала рассасываться. При следующей проверке – так, у нас записано глаза две штуки, катаракта. Сверяют. Глаза здоровы. А где же катаракта? А ну-ка вернуть ее на место. Она у нас числится под номером 7612008. Организм начинает сопротивляться и отстаивать свой результат. На физическом плане это проявляется в виде временного обострения, ухудшения состояния. Самое главное – не останавливаться. Если вы продолжаете заниматься, в следующий раз результат принимается мозгом на баланс. Глаза две штуки – здоровые. Это происходит примерно на сороковые сутки. Теперь уже если какая-то проблема с глазами возникает, мозг тут же ее уберет, потому что в инвентаризационной ведомости зафиксирована норма. Вот так возникает условный рефлекс к хорошему зрению. Нормально видящие глаза уже находятся, образно говоря, в балансе вашего мозга с инвентарным номером. По этой системе мне приходилось работать в обществе слепых. Возможно, вы совсем не можете обходиться без них. Не расстраивайтесь. Вы не одиноки. Однажды на занятиях я задал этот вопрос. Подняли руку, а одна дама сказала, я даже во сне вижу себя в очках. Как же быть? Есть хороший выход. Сегодня-завтра присмотрите декоративные очки без диоптрий, например, солнцезащитные, но только со светло-зелеными стеклами. Или поменяйте стекла в своих очках на простые. Вы видели, как трудно дети порой отучаются от пустышки? Когда на носу вам чего-то будет не хватать, это вызовет дискомфорт. Возьмите пустышку, чтобы чувствовать себя нормально. Есть наркоман, есть алкаш, а есть очкоман. Сам придумал. Уже сегодня вы надеваете очки с меньшими диоптриями, то есть более слабые. Через день, два, три, те очки вам будут уже как раз. Когда они вам станут в пору, надо будет надеть еще более слабые. Три-четыре дня, и наступит момент, когда и в этих очках вы будете видеть нормально. Тогда наденете на нос пустышку, чтобы легче было преодолеть физическое неудобство. Понятно? Внимание! На снижение одной диоптрии дается от четырех до шести дней. Быстрее снижать диоптрий запрещается, потому что стремительная и напряженная работа может привести к торможению процесса восстановления зрения. Природа не любит торопливости. Смешно вытягивать молодые всходы с той целью, чтобы они быстрее выросли. Если вы носили очки, то несколько дополнительных дней вам погоды не сделают. Так что наберитесь терпения, работайте не спеша, обращая внимание на суть. Если у вас минус двенадцать или плюс двенадцать, или астигматизм, катаракта и так далее, абсолютно не имеет значения. Вы все будете работать по одной схеме и по одному параметру сдавать зачет по нормализации зрения. Средний показатель для отличников таков. Одна диоптрия за два дня. Для сачков одна диоптрия за шесть дней, значит в среднем одна за четыре дня. То есть восстанавливать зрение по 0,25 диоптрии в день. У вас появились сомнения типа «А вдруг я не успею? Вдруг я буду отставать?» Меня они не волнуют, дорогой читатель. Мне глубоко начихать на них. Вы будете сдавать нормативы как миленькие. У студента есть три задачи. Сдавать зачет, экзамен. Отставание наказуемо. Итак, если у вас две диоптрии, в вашем распоряжении всего восемь дней. То есть полный курс восстановления нормального зрения восемь дней. И после этого переходите к следующему обязательному этапу адаптации, если, конечно, хотите сохранить наработанный результат. Три подхода по 30 секунд за один прием. Коррекция зрения. Наконец-то практика. Дождались. Уважаемый читатель, сколько вам в душе лет? Если больше 16, значит вы усопший. Давайте договоримся, что нам с вами в душе 16 лет. Плечи поправьте. Вот сейчас попрыгайте на месте на одной ножке. Выкиньте из головы всякую глупость, типа «Я бабушка» или «Я начальник». Или «Я ученый». Теперь создайте такое беззаботное, беспечное, безмятежное настроение. Уберите с лица очень умное выражение, как будто вы сидите на унитазе, и у вас запор. Улыбочка. Осаночка победителя. Очки в сторону. Примите выражение лица пятилетнего ребенка, который впервые оказался на новогодней елке. Похлопайте ресничками. Внутренним взором пройдите по всему телу. Какая у вас осанка? Мимика. Создайте внутри трепетное ожидание того, что сейчас будете видеть чуточку лучше. И радость от этого пропустите волной через себя. Еще. Еще чуть-чуть добавьте эмоций. Молодец. Теперь берем в руки таблицу на свое расстояние и влюбленными глазами смотрим на рабочую строчку. Повышаем настроение. В душе удовольствие от предвкушения результата. Вам когда-нибудь приходилось объективом наводить резкость? Вспомните ощущение, когда расплывчатое изображение начинает проясняться и постепенно становится четким. Кайф. Вот с таким трепетным ожиданием яркости и четкости изображения в течение 30 секунд двумя глазами легко и плавно скользим всей рабочей строчке вправо и влево, вправо и влево, ни на чем не задерживая взгляд, не останавливаясь, не всматриваясь в отдельные элементы. И каждые 2-3 секунды еще поднимаем настроение. Глазами делаем легкие движения, а главное что? Вызываем радость. Самое главное, вы создаете состояние радости и волной пропускаете ее через глаза. Вызывайте благодарность в свой адрес, уверенность в успехе. То есть вы не за строчкой следите. На строчку смотрите, а следите за внутренним состоянием. Дайте глазам своеобразный аванс, и они начнут подстраиваться. Если будете пассивно стоять и ждать, когда зрение начнет улучшаться, получите огромный кукиш. Кроме разочарования ничего не будет. Потребительского подхода быть не должно. Если вам нужно не считать и не отвлекаться, включите магнитофонную запись с счета до 39 раз. Будем делать 3 подхода, 3 раза по 30 секунд каждый. Если вы эмоции подняли на достаточный уровень, то проблески улучшения зрения не заставят себя долго ждать. Как только первые мимолетные вспышки яркости и четкости изображения начнут проявляться, с этого момента старайтесь удерживать настроение, на том же уровне и следующие 30 секунд продолжайте работать на том же подъеме. Получилось? Отлично. Создайте внутри волну благодарности и уважения к себе. Прилив радости и гордости за то, что вы работаете над собой. Только очень прошу, не забывайте моргать, чтобы глаза на лоб не выскочили. Если пока работа идет без изменений, тогда добавляем новую гамму ощущений, и так до первых проблесков улучшения. Следите за внутренним настроем. Не забудьте, вы пятилетний ребенок, который открыто и с любопытством смотрит на мир удивленными глазами. Войдите в роль. Станьте ребенком. Или просто вспомните в красках самые прекрасные и яркие эпизоды из жизни. Первую любовь, первое кормление, купание ребенка, нежность его прикосновения, красивый закат или восход. И так первые 30 секунд поработали. Легко поморгайте глазками. Следующие 30 секунд работаем аналогично. Не забывайте моргать. И последний раз в этом подходе работаем 30 секунд. После этого расслабляем глаза с помощью упражнения «Начихать то наплевать то». Оно приведено ниже. Проверьте осанку и мимику. Проверьте мышечный корсет. Вы уже начали успокаиваться, понимаете? Вы сами себе начали рыть яму. Этого нельзя допускать. Раньше времени успокаиваться нельзя. Но и брать быстрый темп тоже нельзя. Помните пример с моим слушателем из набережных Челнов? Найдите золотую середину. Главное – качество работы при соблюдении техники безопасности. Второй подход выполняется поочередно каждым главным. Причем для каждого глаза выбирается своя рабочая строчка, как это было описано выше. Первые 30 секунд одним глазом, вторые 30 секунд другим. Далее обязательно делаем расслабляющие упражнения. И только после этого, завершающий третий раз, работаем двумя глазами. И опять даем глазам отдых. Глаза открываем спокойно и медленно, стараясь не расплескать покой. Берем в руки таблицу и… Получили сюрприз? Молодчина! Не теряя настроя, с новыми силами приступаем к третьему, заключительному подходу, три раза по 30 секунд. Теперь повернитесь так, чтобы свет на таблицу падал под другим углом. Поехали заново. Грудь, улыбка, настрой. Выполняем третий подход три раза по 30 секунд. Отмечайте для себя дополнительные улучшения. Каждый раз делайте маленький шажок вперед. Получите запись счета. Продолжаем работать. Откладываем таблицу в сторону и делаем расслабляющие упражнения. Создайте в душе благодарность к себе за то, что вы сделали еще один шаг на пути к здоровью и совершенству. Упражнения на расслабление. Поморгайте легко, легко, без напряжения, как мотылек крылышками. А теперь упражнения с научным названием «Начихатто-наплеватто». Сначала поставьте приятную, расслабляющую музыку. Разогрейте ладони, чтобы усилить приток энергии к ним. Для этого опустите руки до уровня солнечного сплетения, так энергия течет лучше. Продолжая растирать, подносим руки к глазам. Сделайте ладони чашечкой. Кладем кисти одну на другую, пальцы плотно соединены, скрещены на лбу, а основание мизинцев, соединенные в одной точке, размещается строго на переносице, в том месте, где обычно находится душка очков. Поправьте их так, чтобы свет не проникал внутрь и в то же время ресницы не касались ладоней. Только после этого закройте глаза веками. Прямо в глазные яблоки. Для того, чтобы энергия поступала беспрепятственно, голова должна находиться в одной плоскости с позвоночником. Отпустите глазные яблоки назад. Расслабьте веки, лицо. Челюсти разжаты, язык не прижат, плечи опущены, руки без напряжения. Локти разведите в стороны. Мышцы всего туловища расслаблены. Воздушность, спокойствие, бездумности, пустоты. Вам на все, на чиха то, на плева то. Рассматриваем темноту или любую картинку, которая возникает в сознании. Можно мысленно рассматривать движущиеся объекты, причем на таком расстоянии, на котором физически вы видите плохо, а на воображаемом уровне очень четко, даже на таком удалении. Очень полезно во время расслабления мысленно просматривать свою рабочую строчку в таблице для коррекции зрения и представлять, что вы ее видите ясно и четко. Внимание переведите в пупок. Создайте внутри ощущение полного покоя и расслабленности. Мысли ваши о любви к себе, своим глазам, о любви к самой жизни. После этого переведите в пупок. Внимание переведите в область печени, правое подреберье, и направьте туда всю нежность, какую только можете ощутить. Почувствуйте физический отклик в теле, как в образе пяти пальцев. Затем аналогично направьте внимание в область почек, на спине чуть выше поясницы. И также мысленно пошлите им любовь, нежность, и от всей души пожелайте им здоровья. Уловите ответ в теле и создайте к себе благодарность. Слеза не открывайте. Настройтесь на полный отдых. Представьте, что дуновение ветерка коснулось вас, и вы легко-легко покачиваетесь из стороны в сторону и расслабляетесь. Отпустите мышцы икр, бедер, ягодиц, поясницы, шеи, лица. Все, кроме мышц мочевого пузыря. Представьте, утро. Солнце поднимается из-за горизонта. Вы на холме. Внизу речка. Летний ливень. Листики на деревьях блестят. Ветерок со стороны реки гладит их, а солнечный свет проникает насквозь так, что листочки даже сами излучают свет. В душе и теле безмятежность. А теперь спокойно откройте глаза. Кстати, никогда резко не распахивайте глаза. Нельзя также прищуриваться или таращить глаза. Все это создает напряжение, которое недопустимо. Если вы, сохраняя внутренний покой, возьмете в руки таблицу, вас ожидает сюрприз. Вы заметили еще дополнительный скачок улучшения? Браво! Я горжусь вами. Значит, вы все сделали правильно. Во время расслабления и отдыха зрение продолжает улучшаться. Как настроение? Улыбку не потеряли? Поднимаем эмоции еще на одну ступеньку. На душе хорошо-хорошо. Легко-легко. И вы ощущаете тепло в глазах. Сохраните это чувство в течение всего дня. А если вдруг потеряете его, то срочно сделайте упражнение еще раз. Третье. Направление энергии к глазам. Сначала растираем ладони, как это описано в упражнении на чихатто-наплеватто. Корпус прямой. Голова на одной линии с позвоночником. Глаза закрыты. Указательные и средние пальцы обеих рук направлены перпендикулярно к глазам. Расстояние от кончиков пальцев до глаз буквально 1-2 мм, но пальцами век не касаемся. Чувствуем, как энергия струится в глаза и заполняет их изнутри. Теперь аналогично средние фаланги больших пальцев, для этого пальцы нужно согнуть, подносим к закрытым векам и точно так же наполняем глаза энергией. И разведены в стороны. Голову не наклоняем. Взбучка. Читает тот, у кого зрение не идет. А теперь я хочу вылить немножко грязи на вашу умную голову, которая держит вас в болезнях и проблемах. И если во время тренировки у вас в голове возникают сомнения и мысли, типа «поможет, не поможет», «поможет, не поможет», «все», вы обречены на провал в прорубь, где как болтались, так и будете болтаться. Я сам в свое время 6 лет болтыхался. Итак, любые упражнения, которые вы выполняете, это оболочка, фантик, а суть совсем в другом. Представьте себе, вы приходите в магазин и вместо конфет берете только фантики. Есть польза от них. Шоколад остался в магазине, а вы фантик взяли, и пошли. Зато красиво блестит. Упражнение – это только форма, пустой сосуд. Вы ищете упражнения, способы, методы, средства, то есть сосуд, оболочку, фантик, забывая позаботиться о главном – о содержании, о внутреннем состоянии. А есть ли содержимое серная кислота? Меняется ли суть кислоты в зависимости от того, куда вы ее нальете – в фрустальный фужер или в консервную банку из мусорного кислота? Поменяется ли? Нет. А тело тоже является сосудом. Скажите, пожалуйста, тело корректируется чем? Физическими упражнениями. Вы упражнения делаете, то есть сосуд приводите в порядок, а внутреннее содержимое, та самая кислота, остается и продолжает его разъедать. Значит, суть вашего характера от упражнений, таблеток, процедур не изменится. Он однажды уже разрушил здоровье, и каждый раз все равно сделает то же самое. Вот так. Поэтому, если вы собираетесь обрести здоровое тело, нужно характер менять в лучшую сторону, в сторону созидания. Свое безалаберное отношение к здоровью вы, конечно, оправдываете тем, что отдали себя семье, науке, работе и так далее. Получится длинный список, не так ли? А попробуйте-ка матушке-природе привести эти свои аргументы и доводы. Ей все равно, кому или чему вы отдались. Законы единые и обязательные для исполнения. Не хочешь соблюдать – болей и самоуничтожайся, освобождая место для здоровых людей. Выбор всегда есть. Значит, если не будем менять подход в отношении себя, грош нам с вами цена. Зрение начнет восстанавливаться семимильными шагами только с того момента, когда вы признаете себя человеком с большой буквы, сильной личностью. Результат будет липнуть к вам, как банный лист. Всякая тратата в свой адрес, любая форма самобичевания и сомнений в своих силах воспринимается как оскорбление личности. Вы смотрите на таблицу потребительски, как в окошко кассы. Прежде чем подойти и протянуть руку, вы должны потрудиться. Тогда еще раз. Когда вы смотрите на таблицу, прежде всего принимаете мышечный корсет, гордое торжествующее выражение повелителя, повелительницы. Лицо расплывается в улыбке. Уголки губ уходят за уши, правда не больше, чем на один метр. Лицо выражает искусственно созданную радость. Вы держите этот ненормальный для вашего состояния мышечный корсет, внутри формируете, высасываете из пальца приподнятое настроение. Чему вы улыбаетесь? Тому, что сейчас начнете видеть чуточку лучше. Сознательно создайте ожидание положительного результата. И все. Я еще не встретил человека, у которого по этой формуле зрение бы не пошло, к вашему сведению. В свое время я был тренером по рукопашному бою. Один парень тренировался с каким-то остервенением. Я выгонял его из спортзала, а он не уходил. Техникой он владел прекрасно и в тренировочном бою не пропускал ни одного удара. Но у него была проблема. Неуверенность в себе. Нет человека, в котором не было бы страха. Страх, самосохранение существует у всех. А у него этот страх превратился в неуверенность в своих силах. В контактном бою он всегда проигрывал. Я говорил ему, не техника побеждает, а дух. Но ничего не менялось. Однажды ко мне приехал чемпион Казахстана по карате среди работников МВД. Я его попросил показать моим ребятам технику и так вскользь заметил, что среди них есть один очень хорошо подготовленный технический. С ним нужно быть осторожным. Я посеял сомнения. А потом, когда пришли ребята, сказал, завтра к нам приезжает один выскочка, такой трепач. Он выпендривается, что самый сильный боец. Надо его проучить. Я знаю, что любой из вас от него мокрого места не оставит. И указывая на одного новичка, который даже ногу еще не мог поднять, добавил, вот ты можешь его размазать. А потом, обращаясь к моему трусу, я сказал, ты техничный, немножко попугай его. Во время встречи мой парень сделал из этого чемпиона такую котлету. Когда же я ему назвал фамилию чемпиона, которого он победил, у него чуть выкидыш не случился. В 1994 году в Хиросиме была Олимпиада среди азиатских стран. Три парня, получившие призовые места, были из Ташкента. Мой ученик был первый. Он победил всех и китайцев, и тайваньцев, потому что однажды получил урок. И он был победителем самого себя. Ясно? Корону надели. Итак, берем управление процессом выздоровления в свои руки. Даже если вы родились в очках, меня это не волнует. Вы все равно восстановите зрение. Техника безопасности. Первое. С первых дней занятий негативных выводов о себе и своих способностях не делайте. Зрением без очков, даже с закрытыми глазами. Третье. Подберите более слабые очки, чем те, которые обычно носите, и сегодня же наденьте их. По мере улучшения зрения эти очки тоже станут вам велики. Меняйте их до тех пор, пока потребность в них вообще не отпадет. Если вы останетесь в старых очках, тогда не мечтайте об успехе. В первые дни работы с таблицей улучшение начнется еле заметно. Осторожно, со скрипом. И все достигнутое пока будет оставаться очень зыбким. Если вы наденете старые очки хотя бы на пять минут, давняя привычка десятилетиями носить на носу велосипед тут же мгновенно вспомнится. И потом вернуться к наработанному результату будет значительно тяжелее. Строить труднее, чем ломать. Труд, с помощью которого вы достигнете цели, очень легко разрушить. Если вы хотите обходиться без очков, это относится к тем, у кого зрение полторы-две диоптрии, то попрощайтесь с ними навсегда уже сегодня. Больше не надевайте их никогда и нигде. Очки — это костыли для глаз. Во время коррекции зрения нельзя прищуриваться и таращить глаза. Работать нужно без перенапряжения. Шестое. Не спешите. Одна строчка таблицы на два-три дня работы. 0,25 диоптрии в сутки. Седьмое. Не успокаивайтесь на достигнутом результате, а продолжайте работать. Как только успокоитесь, остановитесь, расслабитесь, так зрение перестанет улучшаться. Помните? Маховик, инерция, остановка. Как преодолеть хроническое возвращение к недугу? Если после выздоровления болезни возникают вновь, то это происходит в основном между вторым и третьим этапами. Больной выздоравливает, но в памяти у него эта болезнь остается. Например, утром по привычке он начинает искать очки и вдруг вспоминает, я же без очков хорошо вижу, но при этом почему-то ощущает дискомфорт. Привычка начинает давить. И этот период неудобства надо преодолеть. Хроническое возвращение к недугу — это старая программа, все еще работающая в голове. И еще. Человек расположен к плохому. Чтобы предостеречь вас от ошибок, расскажу случай из практики. У нас в группе занимался мальчик с обширным бельмом на глазу. Ребенок работал над собой, и в результате от бельма осталась лишь еле заметная ниточка. Он пошел обследоваться. И одно слово окулиста ребенка сделало слепым. Врача попросили только посмотреть, а он говорит, мальчик не может видеть, я в прошлый раз его осматривал, это невозможно. Врач, который говорит больному, что тот неизлечим, это преступник, возомнивший себя Господом Богом. Однажды ко мне на стажировку приехал коллега из Республиканского центра микрохирургии глаза. Хороший врач. Он всегда с собой носил толстенную книгу. Когда я посмотрел, это оказался справочник глазных болезней, 1928 года. Я спросил его, зачем тебе такая старая книга? Он ответил, лучше ее нет. Это значит, все достижения науки за эти годы его не интересуют, он не знает о них. Берегите себя. В каждой профессии найдется случайный человек, семья не без уродов. Одна паршивая овца поганит все стадо. И как быть с тем, что человек расположен к плохому? Все ваши поступки могут быть перечеркнуты одним плохим словом. И все ваши созидательные стремления и усилия сведены к исходной точке. Еще Иисус Христос говорил, что человек расположен больше к плохому. Пациент впитывает негативную информацию, как губка. Например, врач, прекрасный специалист, компетентный и внимательный, ведет тяжело больного. Он назначает лечение, контролирует его ход и постоянно подбадривает пациента. Без сомнения, эффект будет значительный. И вдруг какая-нибудь санитарка войдет и бросит между делом. Сколько лет работаю здесь, я никогда не видела, чтобы от этой болезни вылечивались. Именно эти слова западут в душу человеку, посеют сомнения и сведут на нет все усилия врача. Остерегайтесь умников, которые начинают высказывать свое мнение. Не надо общаться с другими очкариками, особенно до окончательного завершения работы. Третий этап – адаптация. Люди терпеть не могут тех, кто не похож на них. Помните про толпу и личность? А слова других людей имеют очень сильное влияние. Будьте сами себе головой. Представьте, вы решили заниматься восстановлением зрения. В вашем окружении находятся еще несколько очкариков, которые тоже хотят иметь хорошее зрение. Но когда узнают, что надо тренироваться каждый день по 15-20 минут, это приводит их в ужас. Они тут же начинают нападать. «Да не может быть! Это ерунда! Я не верю! Разные вещи пишут всякие шарлатаны!» Почему? Потому что подобным отрицанием эти люди защищают свою вопиющую невежественную лень. Так что, мой читатель, берегите себя. Берегите экологическую чистоту своего сознания. Остерегайтесь. Вот где опасность. Если вы помните, в процессе работы период улучшения зрения сменяется небольшим. А так как человек расположен к плохому, значит, вероятнее всего, что ухудшение сопровождается более сильной эмоциональной реакцией, чем улучшение. Но если помнить и подходить к этому сознательно, то можно избежать попадания в ловушку, поставленную своим характером. Будем вырабатывать новую привычку. Ну как? У вас есть сомнения, что ваше зрение начнет улучшаться? Послушайте. Теория. Научный изыскание на соискание ученой степени доктора хрен знает каких там наук, но по теме «Механизм возникновения второго дыхания во время убегания от преследования отряда сексуально-маниакальных мышей». Теория. Теория обоснования применения механизма для лечения застревания глаз в унитазиках, то есть в очках. Этапы приспособления организма к новым условиям. В нашем организме есть участки, исполняющие роль аккумулятора, собирающего и удерживающего энергетический импульс. Его можно сравнить со сжатой пружиной или рогаткой в боевой готовности. Внезапный шорох, испуг, рука дрогнула и камень тянулся. Тело приняло соответствующую позу, изменился мышечный корсет. Первая адаптация происходит через молниеносный нервный импульс. Он подобен взрыву. Взрывной импульс действует короткое время. Запас прочности организма в этом случае маленький. Примером могут служить бегуны в спринте, которые на коротких дистанциях выкладываются полностью. В этом случае, когда организм бросает со своих складов НЗ, происходит второе приспособление организма к окружающей среде. Это можно сравнить с бегунами на средней дистанции. Они изначально бегут медленнее, чем в спринте, и силы расходуют постепенно. У них интенсивность воздействия небольшая, но более длительная. Когда гормоны уже успели поработать, в нашем организме происходит всеобщая мобилизация. Поднимается весь народ, и происходит это через кровь. Тогда открывается, как мы говорим, второе дыхание. Организм уже готов к более длительным повышенным нагрузкам. Это сравнимо со спортивной ходьбой. Оказывается, не последнюю роль в адаптации организма к новым условиям играют эритроциты крови. Это для вас новость? Попались! Ну хорошо, тогда еще раз о том же самом домашнем языком. То, что эритроциты осуществляют газообмен, это знает даже школьник. Но они еще работают почтальонным курьером по передаче долгосрочных приказов об изменениях на всех уровнях, исходящих от центральной нервной системы. И это, конечно, влияет на первичный мозг, гипоталамус, который связан с эмоциональным центром. Он отвечает за простейшие функции организма, по типу «мне тепло», «мне холодно», «мне грустно», «мне весело», «жрать хочу». Эти элементарные приказы передаются всему организму через кровь от мозга к периферии. Поэтому если человек огорчается или радуется, то об этом сразу же узнают все клеточки организма. Мысль влияет на эмоциональный центр. И адаптация всех внутренних процессов происходит через кровь. Значит, эмоции тоже передаются через кровь. А теперь вопрос на засыпку. Когда у вас что-то болит, как вы поступаете в этом случае? Запомните, пожалуйста, свой ответ. Мой наставник говорил, ты должен относиться к нездоровому органу как к ребенку, которому несколько месяцев от роду. А я-то был холостой. С умным видом я что-то делал. Но только с годами и опытом я испытал всю глубину чувства, которая требовалась тогда моим почкам. Ребенок самостоятельно может только дышать, сосать, глотать, поддерживать температуру тела, переваривать молочко, писать какать. В остальном он полностью зависит от вас. Любое недовольство, жалобу или просьбу он может выразить только плачем. Нездоровый орган – это тот же беспомощный ребенок, который полностью зависит от вас. Если что-то не так, то свое состояние он может донести через дискомфорт, недомогание, боль. Ребенок начал плакать. Что будете делать? Вот теперь повторите тот свой ответ, который вы запомнили. Значит, ребенок плачет, а вы... Давайте проведем анализ вашего ответа и сравним свое отношение к нездоровому органу. С отношением к грудному ребенку. Если у вас что-то заболело, вы идете в аптеку за болеутоляющим. Ребенок начал хныкать. Боль появилась. А нас это раздражает, не дает покоя. Берем анальгин и даем ребенку. Боль приглушили, крика не слышно, покой в доме наступил. Вы знаете, что это нехорошо, но продолжаете делать. Так проще, быстрее. А ребенок-то находится в полной зависимости от вас. Он и так долго терпел. Но когда его силы иссякли, он закричал. Просто вынужден был обратиться за помощью. А вы ему говорите, замолчи. А он все равно плачет. Вы чувствуете абсурдность подхода к себе? Я, например, когда ребенок плачет, в первую очередь проверяю, не мокрый ли. А может быть он лежит неудобно, или пеленка где-то давит. Может быть он голодный, или у него животик болит. Или, или, или. Сначала нахожу причину дискомфорта, а потом ее устраняю. И ребенок тут же успокаивается. Значит, что бы вы ни делали, как бы ни работали с нездоровым органом, подход всегда один. Как к грудному ребенку. Потому что он – это часть вас самих, ваше будущее. И зависит оно тоже от вас. Если вы хотите вылечить свои глаза, или любой другой нездоровый орган, то необходимо поступать по законам природы. А не вопреки им. Найти и устранить причину плача. Дать то, чего не хватает. У нашего дальнего родственника, живущего по соседству, много лет не было детей. В 40 лет родился сын. Жена и мать так его лелеяли, что даже летом тепло одевали. В конце концов, ребенок заболел и умер. Родился второй ребенок, но тоже умер. Дошло чуть ли не до развода. Моя мама сказала, я воспитала семерых детей и 35 внуков. Следующего ребенка я возьму под контроль. Родился третий ребенок. Моя мама ухаживала за ним. Она голенького малыша подставляла солнечным лучам, ветерку. А они протестовали и говорили ей, вы что делаете, ребенок простудится. Она сберегла этого ребенка от слепой, нерадивой и, как показал опыт, гибельной любви родителей. Сейчас этому мальчику 15 лет. До 5 лет он и зимой, и летом ходил голый, потому что на него невозможно было ничего надеть. Он тут же одежду снимал. В нашем доме дети до 5 лет не одеваются, а мы живем в горах. Ко всему наш организм привыкает. Значит, что нужно дать этому ребенку? Надо закалять организм и тот нездоровый орган. Постоянно не кутать, так как в береженый глаз всегда шишка попадает. Дать полноценное питание. Во время выполнения упражнения, когда вы будете ребенка успокаивать, подходите с лаской, любовью, нежностью, в общем, с чувством. Наставник мне говорил, создай отцовскую любовь. Теперь я понимаю, в чем-то он ошибался. Он слишком долго жил и забыл, что чувствует еще холостой юноша. Я понимал и вызывал какие-то чувства, ощущения, и дело у меня продвигалось. Очень медленно. Потом, когда я впервые ощутил отцовское чувство, это было что-то неописуемое. Я, конечно, готовился к тому, что скоро стану отцом, но все равно это событие застало меня врасплох. Однажды, когда в очередной раз вернулся из командировки, мне говорят, поздравляем с сыном. Прислушиваюсь к сердцу, там ничего. Подумал, странно, что же такое отцовское чувство? Мне дали какой-то сверток. Я смотрел в живной уголок, сначала даже испугался. Вижу, лежит какой-то красненький, сморщенный маленький старичок. Первое мое чувство было, неужели все дети рождаются такими? Пришли домой. Прислушиваюсь, чувств никаких. Только заметил, что на меня никто не обращает внимания. Все, и молодая мамаша, и родители стоят вокруг этого крикуна. Ну, думаю, отцовские чувства появятся на следующий день. А вместо этого добавились новые запахи. Через две недели мне снова нужно было уезжать в командировку. Вернулся уже через три месяца. Плач превратился в рев. Запах от подгузников стойкий. Сна по ночам нет. В общем, кроме раздражения, ничего. И вот однажды я приезжаю домой, иду к сыну, и вдруг обнаруживаю, что из манежа, где его оставили, он исчез. Оказывается, к этому времени он уже начал самостоятельно передвигаться, где ползком, где как. А там нигде нет. Тогда заглянул в чулан. А там как раз стояли еще незакрытые банки с вареньем. В тот день домашние занимались заготовками. Я посмотрел и понял. Сын был здесь. Две трехлитровые банки лежат на боку в огромной луже варенья. Перья, собранные для перины, разметаны по всему полу, и свежий след ведет под кровать. А оттуда, хитро блестя, смотрят на меня два уголька глаз. Все остальное в варенье, пухе, перьях, пыли и мусоре, собранных, кажется, со всех чуланов нашего квартала. Я взял его на руки и понес купать. В тот момент, когда я начал его мыть, у меня возникло новое, необъяснимое, но очень приятное чувство. Это одновременная трепетная нежность и вместе с тем ликующая гордость. Наверное, никто не сможет описать словами родительское чувство. И когда я прижал его к себе, ощутил на груди его коленочки, у меня на душе стало так хорошо, легко и покойно, что я вдруг почувствовал, что улетаю. Возникло такое ощущение, как будто за спиной выросли крылья. Это мой сын! Наверняка вы, дорогой читатель, хоть раз в жизни испытали нечто подобное. И когда вы смотрите на своего ребенка, на его рост, вы представляете, каким он станет. Вы тревожитесь за его будущее и вместе с тем гордитесь. Значит, еще раз. С глазами или любым другим нездоровым органом нужно работать с такой нежностью и заботой, с какой вы ухаживаете за больным ребенком. И в то же время, когда вы свой внутренний взор будете направлять туда, нужно с гордостью представлять будущее, как будущее своего ребенка, что завтра ваши глаза будут видеть чуточку лучше, чем сегодня. И так каждый день. Сегодня вы им даете своеобразный аванс, а завтра непременно получите такую благодарность, о которой даже не смели мечтать. Вот в этом и заключается суть. Вначале чувство, а потом результат. С каждым днем все лучше, лучше и лучше. Упражнение для глаз. На Востоке существует древний способ диагностики заболеваний по движению глаз не по радужной оболочке, а именно по движению глаз. Например, специалист просит вас нарисовать глазами окружность и смотрит, как вы это делаете. Оказывается, в зависимости от заболевания глаза начинают где-то срезать углы, линия получается неровной. Это еще раз подтверждает, что все в нашем организме взаимосвязано и взаимозависимо. Но сам человек не может проследить за правильностью движения глаз, поэтому попросите кого-нибудь из родственников, помочь вам. Выполняя упражнения для глаз правильно, мы не только тренируем мышцы, но и опосредованно работаем с нездоровыми органами. Итак, следите за тем, чтобы во время выполнения упражнений движения глаз четко рисовали указанные линии. Приступаем. С чего мы начинаем любую работу над собой? Вы помните? А ну-ка расправьте плечи. Вначале примите мышечный корсет. Что для этого нужно? Правильно. Выпрямить спину и растянуть улыбку до ушей. Затем внутри искусственно вызвать положительные эмоции. Как? Вы это уже знаете. Сделали? А теперь можно начинать выполнять упражнения. Только не забывайте моргать. Первое. Взгляд вверх-вниз. Голову держите прямо. Не запрокидывайте. Взгляд направлен вверх, в потолок, а мысленно продолжаем движение глаз под череп на макушку, как будто вы туда посмотрели. А теперь глаза вниз, а внимание в область щитовидной железы, как будто вы заглянули туда, где наше горло. Выполняем 8-10 раз в каждом направлении. Почему важно при выполнении этих, казалось, давно известных упражнений продолжать движение глаз на мысленном уровне? Еще в древние времена на Востоке было известно, что в области макушки находится огромный пучок энергетических каналов, которые связаны с желчными протоками. Поэтому мысленно продолжая движение глаз, например, за ухо, мы тем самым влияем на желчные протоки и печень. Глаза — это окна печени, и я не случайно говорю об этом. За внешней простотой всех упражнений на восстановление зрения кроется глубинный смысл, уходящий корнями в древность. Но, как и во всем, здесь тоже нужно помнить о технике безопасности. Любое перенапряжение в работе с глазами приводит к противоположному результату. Вот почему здесь еще раз хочу обратить ваше внимание на описанные выше расслабляющие упражнения для глаз, которые благотворно влияют и на весь организм в целом. Но сначала поработаем. Второе упражнение — ходики. Посмотрели влево, глаза смотрят на стену, а внимание ушло за левое ухо. Посмотрели вправо, глаза смотрят на другую стену, а внимание ушло за правое ухо. Выполняем 8-10 раз в каждом направлении. Третье, следующее упражнение. Посмотрели влево и перевели взгляд прямо, затем посмотрели вправо и перевели взгляд прямо. Выполняем 8-10 раз в каждом направлении. Четвертое, циферблат. Голова остается неподвижной. Представьте перед собой большой циферблат золотого цвета. Этот цвет способствует восстановлению зрения. Медленно введите взгляд, отмечая цифры 12 вверху и 6 внизу на воображаемом циферблате. Сначала в одну сторону, потом в другую. Внимание! Отмечать цифры 12 и 6 необходимо, чтобы линия круга получалась ровной и движительной. Движение было плавным. Выполняем 8-10 раз по часовой стрелке и против. Спокойно моргайте, не переутомляйте глаза. А теперь то же упражнение, но лицо обращено к небу. Глаза открыты. Выполняем 8-10 раз по часовой стрелке и против. Теперь шею выпрямляем, глаза закрываем. То же самое упражнение выполняем с закрытыми глазами. Того, что когда откроем глаза, мы будем хорошо видеть. Создайте такое трепетное ожидание результата и в то же время состояние снисходительного спокойствия, что будет именно так, как вы хотите. Пятое упражнение. Бабочка. Глазки открыты. Непременные условия выполнения упражнения. Голова неподвижна. Работаем только глазами. Возможного размера в пределах лица. Но мышцы глазных яблок при этом не перенапрягайте. Следите за состоянием. Взгляд переводим в такой последовательности. В верхний левый угол. В нижний левый. В верхний правый угол. В нижний правый. А теперь наоборот, в другом направлении. В нижний левый угол. В верхний левый. В нижний правый. В верхний правый. В каждом направлении. А сейчас расслабьте глаза. Поморгайте часто-часто. Легко-легко. Примерно так, как машет крылышками мотылек. Во время выполнения разминки для глаз не щурьтесь. Не открывайте глаза очень широко. Все это создает напряжение, которое противопоказано. Шестое упражнение. Восьмерка. Непременные условия выполнения этого упражнения А теперь глазами плавно опишите горизонтальную восьмерку или знак бесконечности максимального размера в пределах лица. В одну сторону несколько раз, а затем в другую. Поморгайте часто-часто. Легко-легко. Седьмое. Тоже упражнение, только теперь рисуем глазками вертикальную восьмерку. До перенапряжения ни в коем случае не доводим. Восьмое. Упражнение на развитие бокового зрения. Здесь работают косые мышцы глаз. Особое замечание. Это упражнение следует выполнять в спокойной обстановке. Никто и ничто не должно вас напугать. Посмотрите на свой кончик носа, скосив глаза. После этого смотрим вперед, а внимание в стороны, отмечая какие-нибудь предметы боковым зрением. И так попеременно. На кончик носа, вперед, а внимание в стороны. На переносицу, вперед, а внимание в стороны. На точку между бровями. Повторяем 8-10 раз все упражнение. Упражнение делайте медленно, но с радостью и с чувством благодарности к себе. Улыбка на месте? Отлично! Проверьте внутреннее состояние. Опять поморгайте глазами, похлопайте ресницами и сделайте упражнение на чихат-то, на плеват-то. Особо обращаю ваше внимание, на то, что упражнение разминки для глаз следует выполнять в той последовательности, в которой они описаны, по степени увеличения сложности. Запомните, большое напряжение глаз приводит к ухудшению зрения. Поэтому следите за нагрузкой, по своим ощущениям, и чаще, чем чаще, тем лучше, практикуйте расслабляющие упражнения. Рассмотрим психиатро-фигеологический фундамент, который приводит к ухудшению зрения. Глаза – это показатель того, как мы смотрим на мир. Если зрение ухудшается или возникают разного рода проблемы с глазами, то очень возможно, что человек внутренне от чего-то отгораживается в своей жизни, или на глубинном подсознательном уровне его что-то не устраивает в окружающем мире. Это может быть связано с любыми событиями в прошлом, настоящем или будущем. Если вы предлагаете что-то сейчас, или чего-то опасаетесь в ближайшее время, это может сказаться на состоянии здоровья глаз. Внутренние несогласия с самим собой, ощущение себя не от мира сего, нежелание кого-то простить, состояние подавленности, застарелые обиды, неспособность с радостью смотреть вперед, боязнь заглянуть действительностью в глаза и тому подобное – все это скрытые психологические причины ухудшения зрения. В общем, с чем связано возникновение ухудшения зрения у детей? Если такие проблемы появляются, сразу можно сказать, что дома, где малыш живет, не все в порядке. Когда дети бессильно изменить ситуацию, они в прямом смысле не желают на что-то смотреть и неосознанно стараются рассеять внимание, сгладить контуры, чтобы окружающее не давило на них. Другая причина – если кто-то в семье, кого ребенок больше всех любит, носит очки. Между ними находятся хрящевые межпозвонковые диски. Позвонки выполняют опорную функцию, а диски служат амортизаторами осевой нагрузки, придают позвоночнику гибкость и подвижность. Все понятно? Тогда продолжаем. От спинного мозга отходят нервные корешки, дающие ветви к каждому участку тела и ко всем внутренним органам. Межпозвонковый диск состоит из фиброзного кольца и студенистого ядра. От спинного мозга отходят нервные корешки, дающие ветви к каждому участку тела и ко всем внутренним органам. Межпозвонковый диск состоит из фиброзного кольца и студенистого ядра. От спинного мозга отходят нервные корешки, дающие ветви к каждому участку тела и ко всем внутренним органам. Межпозвонковый диск состоит из фиброзного кольца и студенистого ядра. От спинного мозга отходят нервные корешки, дающие ветви к каждому участку тела и студенистого ядра. От спинного мозга отходят нервные корешки, дающие ветви к каждому участку тела и студенистого ядра. От спинного мозга отходят нервные корешки, дающие ветви к каждому участку тела и студенистого ядра. От спинного мозга отходят нервные корешки, дающие ветви к каждому участку тела и студенистого ядра. Вся беда заключается в том, что ни одна из них не открывает пути его полного излечения. Это говорит о многом, вы согласны? Когда мы точно знаем природу заболевания, мы в состоянии его вылечить или вовсе не допустить. А так мы всегда лечим, к великому сожалению, только следствие. Раз единого взгляда на эту проблему нет, я тоже воткну в этот базар свое очень умное соображение. Теория от Нарбекова. Знаете ли вы, каков химический состав соли, который откладывается в суставах и прочих местах, и особенно у вас в тканях мозга? Не отрицаю, у меня тоже, раз я столько лет общаюсь с вами. Если говорить простым языком, это мочевина. Теперь вы понимаете всю глубину народной мудрости? Остеохондроз ударил в голову. После такого удара глаза вылезают из орбит, и вследствие этого возникает дальнозоркость. Позвоночник имеет каркас из мышц, который обладает природной способностью растягиваться и также отсыхать, если хозяин дурак. Это только одна из закономерностей появления дальнозоркости. Но на самом деле и другие заболевания, как вы уже, наверное, поняли, в той же мере связаны с плохим состоянием позвоночника. Даже самые незначительные нарушения обмена веществ в тканях мозга приносят очень много проблем. После упражнения на восстановление гибкости позвоночника мы увеличиваем его эластичность. За счет этого межпозвонковые диски принимают свою нормальную форму. А в результате восстановления обмена веществ нормализуется и состав костной ткани, что приводит к восстановлению ее опорной функции. Подключение к работе молчащих капилляров восстанавливает нормальную циркуляцию крови, доставку необходимых веществ к клеткам и отток отработанных веществ. Постепенно за счет восстановления нормального тонуса мышц восстанавливается правильное положение позвоночника, а крепшие мышцы и связки уже могут его удерживать в нем. Постепенно восстанавливается нормальная конфигурация и тонус межпозвонкового диска. Нормальная ширина межпозвонковых щелей и сдавливание нервных корешков и сосудов уже не происходит. И таким образом возобновляется работа соответствующих органов и систем. Именно поэтому необходимо каждый день выполнять суставную гимнастику. Тогда наша почти детективная история закончится очень даже хорошо. Суставная гимнастика с начинкой. Суставная гимнастика с начинкой. Можете ли вы сейчас сходу сесть на шпагат? Не можете. А если начнете тренироваться, то соответствующие мышцы будут постепенно растягиваться, и с каждым днем вам будет все легче и легче садиться все глубже и глубже. В один прекрасный день, хоп, даже если вам 90 лет, вы сядете на шпагат. Внимание! Закостенелый неподвижный позвоночник, гиппопотамовская походка – это все явления вашего образа жизни. Это признаки лени. И в 90-летнем возрасте способность восстановления хрящей и межпозвонковых дисков остается хорошей. Для этого надо только захотеть и немного поработать. Пошевелить чем? Суставами, а не извилинами. Если наше тело не работает, то и зарплату не получает. Например, если вы правую руку положите в карман, и полгода будете ее просто носить в кармане, то мышцы начнут атрофироваться, а мышцы другой руки накачиваться, так как на нее падает двойная нагрузка. Это означает, что наше тело каждый день проводит анализ и приспосабливается к условиям, которые мы сами можем сделать оптимальными. Значит, то, что мы сейчас будем делать, это не физзарядка. Это специально подобранные упражнения, направленные на нормализацию функции позвоночника. Это означает устранение всевозможных недугов, как перечисленных выше, так и не обозначенных здесь вовсе. Не забудьте измерить свой рост. Вы будете расти в среднем на 1-3 см и более за счет восстановления нормального расстояния между позвонками. Главное не сами упражнения, а ваши внутренние состояния, в котором вы их выполняете. 90% внимания нужно направить на созидание внутреннего настроя, и только 10% на технику выполнения. Настрой вызываем искусственно, усилием воли. Запомните, само собой из воздуха ничего не возникает. На сегодняшний день, если ваш настрой будет плохим, это то самое болото, в котором утонет любой труд. Поэтому с чего начнем? Поправили осанку, улыбку натянули и вперед. Наступило время проверки домашнего задания. Достаньте список с положительными и отрицательными чертами характера. Так, вы его еще не написали? Вам полагается хорошая взбучка. Так и быть, даю вам последний шанс. Составьте его сейчас. Не сегодня, а сейчас. Смотри приложение номер один. Массаж ушных раковин. Каждое движение здесь и дальше следует выполнять не менее 8-10 раз, сначала в одном направлении, а затем в другом. Так сделано сознательно, с той целью, чтобы механический подсчет количества повторов не поглотил все ваше внимание. Главное, куда нужно направить мысли и душевные силы. Вы уже знаете, на внутренние состояния, на формирование тех положительных сторон характера, которые, на ваш взгляд, развиты недостаточно. Ни в коем случае нельзя опускаться до автоматизма. Это путь в никуда. На поверхности ушных раковин расположено более тысячи биологически активных точек, поэтому массируя их, мы опосредованно воздействуем на весь организм. Настрой. Убираем волосы, снимаем серьги, слуховые аппараты, если есть, беремся за уши у основания ушной раковины, большой палец за ухом. Так. А теперь посмотрите в зеркало. Видите страшное выражение лица, как у людоеда? Что, главное уже забыли? Главное, настрой, елки-палки. Опять у вас хищническое выражение. Ну-ка, осаночка, улыбочка. Приготовились. Поехали. Упражнение номер один. С юмором тянем ушные раковины вниз и одновременно назад за плечо, причем так, чтобы оттянулась внутренняя часть уха. В каждое движение вкладываем радость. Потом аналогичные движения вверх и чуть назад. Только прошу вас, соблюдайте технику безопасности. Уши не оторвите. Упражнение номер два. А теперь возьмитесь за середину ушной раковины. Тянем в стороны и чуть назад от наружного слухового прохода. С каждым новым движением оттягивайте уши как можно дальше. Дальше. Дальше. Упражнение номер три. Теперь, не меняя положение рук, делаем круговые движения. Уши горят. Очень хорошо. Плечи поправили. Создаем искусственную радость, гордость за свою работу над собой. Внимание направьте в область ушей. Выполняйте упражнение с чувством. Ваши отношения к себе не будут. Упражнение номер четыре. Меняем захват ушной раковины. Ладонь основанием большого пальца плотно прижимаем к ушам таким образом, чтобы внутри возникло ощущение вакуума. Ладони удобно развернуть так, чтобы пальцы были направлены назад. Выполняем круговые движения в обе стороны. Во время упражнения создайте такое ощущение, что вы всем окружаете. И все ваши повеления подлежат исполнению. Помните октаву. Настрой. Создайте чувство, что вы сильный человек. Удерживайте внутренний образ. Упражнение номер пять. Осторожно. У кого повреждена или отсутствует барабанная перепонка, это упражнение. Выполнять нельзя. Не меняя положение рук, еще плотнее прижимаем ладони к ушам и резко отрываем их, чтобы они не были наружу. Упражнение номер семь. Упражнение номер семь. Кисти. Каждое движение повторяем 8-10 раз. Упражнение номер один. Сжимаем, разжимаем кулаки. Несколько раз, вы уже знаете примерно сколько. Ритмично, как можно быстрее. Упражнение выполняется в двух вариантах. Сначала акцент делаем на сжимании пальцев в кулак. Хватательные движения. А затем на разжимания. Бросательные. Причем пальцы нужно выпрямлять полностью. Создайте ощущение того, что вы человек. С большой буквы. Самый красивый и самый волевой. Внутренне прочувствуйте этот образ. Улыбка на лице. Упражнение номер два. Каждым пальцем по очереди выполняем движение, как будто вы кого-то с любовью щелкаете по лбу. Упражнение номер три. Последовательно складываем пальцы в кулак. Сначала несколько раз от мизинца к большому, а потом от большого к мизинцу. Встряхиваем кисти рук. Расслабляем мышцы. Упражнение номер четыре. Акцентируем внимание на лучезапясном суставе. Руки вытянуты вперед, параллельно полу. Кисти направлены вниз, в сторону туловища. Кончики пальцев тянем к себе. Делаем несколько пружинящих движений. Чередование напряжения и легкого расслабления. Мысленным взором просматриваем область напряжения, то есть лучезапясный сустав. Аналогично выполняем упражнение в противоположном направлении. Кисти рук направлены вверх. Усилием воли вызываем еще одну порцию самоуважения. Создаем и впитываем в себя. Плечи поправили. Мышечный корсет. Настрой. Улыбочка. Упражнение номер пять. Руки вытянуты вперед, параллельно полу. Кисти направлены ладонями вниз, в сторону туловища. Также параллельно полу. Разводим ладони в сторону мизинца. Это исходное положение. Выполняем несколько пружинящих движений, мелких колебаний, кистями рук к мизинцу. Затем исходное положение меняем. Теперь обе ладони сводим к большому пальцу и повторяем упражнение. Упражнение номер шесть. Исходное положение тоже. Создаем гордость. Теперь вы направляете на создание настроя. Улыбочка, радость и стома по всему телу. Теперь кисти, сжатые в кулак, вращаем по кругу максимального диаметра. Сначала в одну, потом в другую. Локтевые суставы. Упражнение номер семь. Плечи параллельны полу, зафиксированы. Руки согнуты в локтях. Предплечья свободно висят. Кисти рук в легком кулачке. Совершаем вращательные движения предплечьями вокруг локтевых суставов. Сначала несколько раз в одну, потом в другую сторону. Следите за тем, чтобы плечи не двигались. Мысленно надели корону на голову. Настрой. Добавьте какое-нибудь новое ощущение и усилием воли увеличьте его. Продолжаем. Плечевые суставы. Упражнение номер восемь. Выпрямленную руку, свободно опущенную вдоль туловища, кисть в легком кулачке вращаем во фронтальной плоскости перед собой. В кисти появится ощущение тяжести и набухания. От приливающей крови она станет красной. Скорость вращения постепенно увеличиваем. Тренируем поочередно оба плечевых сустава. Каждую руку вращаем сначала по часовой стрелке, затем против. Какое чувство вы внутри вызываете? Упражнение номер девять. Голова прямо, плечи тянем вперед навстречу друг другу. Чувствуем приятное напряжение. Даем легкое расслабление и опять с новым усилием дополнительное напряжение. Снова расслабление и так далее. Упражнение номер десять. Не меняя исходного положения, плечи тянем назад. Лопатки наезжают одна на другую. Упражнение по нескольку раз. Упражнение номер одиннадцать. После этого плечи тянем вверх, дотягиваемся ими до ушей, слегка отпускаем напряжение и снова тянемся к ушам. Аналогично чередуем увеличивающееся с каждым разом напряжение с легким расслаблением, опуская плечи как можно ниже. Затем снова плечи тянем вверх и заканчиваем упражнение. Упражнение номер двенадцать. Круговые движения плечами вперед, а затем назад осуществляем по такому же принципу. Амплитуда максимальная. Технику упражнения освоили? А теперь добавьте эмоцию! Создайте во всем теле весну! Молодец! Упражнение номер тринадцать. Голова прямо, руки выпрямлены вдоль туловища, разворачиваем их ладонями наружу, как будто ввинчиваем руки в пол. Проверьте. В работу включаются лучезапясный, локтевой и плечевой суставы. После того, как дошли до упора, даем дополнительное напряжение, подтягиваем винт и легкое расслабление. Еще дополнительное, чуть больше по силе, напряжение, затягиваем ту же и расслабление. Делаем несколько таких движений, а затем разворачиваем руки в противоположную сторону и выполняем упражнение, аналогично. Внимание! Во всех упражнениях чередование дополнительного напряжения с легким расслаблением нужно выполнять по принципу подтягивания винта для мужчин и по принципу выжимания белья вручную для женщин, но безболезненно. И еще очень важное правило. Как нужно дышать? Запомните правило. На любое напряжение всегда приходится выдох. А на расслабление вдох. И никогда дыхание не задерживайте. Упражнение номер 14. Встряхиваем руки, расслабляем мышцы. Упражнение номер 15. Руки перед грудью сцеплены в замок. Корпус прямой, положение зафиксировано, двигаются только голова и плечи, все остальное неподвижно. Взгляд направляем вправо, затем в ту же сторону поворачиваем голову. Правая рука начинает вправо тянуть левую. Доходим до упора и еще поднатуживаемся, стараясь продолжить движение. Затем, не меняя положение, отпускаем напряжение и снова прилагаем дополнительное усилие. После нескольких таких напряжений и расслаблений плавно переходим влево, теперь левая рука тянет правую, и выполняем упражнение аналогично. Что вы создаете в душе? Стопы. Упражнение номер 16. Акцентируем внимание на голеностопном суставе. Упражнение выполняем сначала одной стопой, а потом другой. Ногу слегка сгибаем в колени, стопу держим на весу, это исходное положение. Оттягивая носок от себя, совершаем небольшие пружинищие движения. Вперед, носок на себя, при этом нога выпрямлена. Упражнение номер 17. Исходное положение такое же, как и в предыдущем упражнении. Стопу поверните внутрь так, что если вы поставите ногу на пол, она внешним краем коснется его поверхности. При этом возникает напряжение в наружной стороне голеностопного сустава. Делаем несколько пружинищих движений стопой внутрь, и с каждым разом развернуть ее больше, больше, больше. Какую черту характера вы тренируете сейчас? Не забывайте об этом. Упражнение номер 18. Стопа повернута наружу, исходное положение противоположное описанному в предыдущем пункте. Упражнение выполняется аналогично. Упражнение номер 19. Стопой медленно совершаем круговые движения, по несколько раз в каждую сторону. Движения такие, как будто большим пальцем ноги рисуем на стене круг максимально возможного радиуса. Обратите внимание на то, что нога при этом абсолютно неподвижна, работает только стопа. Механическое выполнение упражнения это путь в никуда, помните? Так куда вы идете? Коленные суставы. Упражнение номер 20. Нога согнута в колене, бедро параллельно полу, рука фиксирует бедро, голень расслаблена. Совершаем вращательные движения голенью по и против часовой стрелки, несколько раз в каждом направлении поочередно, каждой ногой. Стоим ровно, плечи расправлены. Пропустите по всему телу чувство, что вы человек, личность. Упражнение номер 21. Ноги чуть шире плеч, стопы параллельно, носки слегка повернуты внутрь, ладони на коленных чашечках. Спина ровная, смотрим вперед, голову не опускаем. Совершаем круговые движения коленями, сначала несколько раз внутрь, затем наружу. Руки помогают вращению. В конце каждого движения колени разгибаются полностью. Упражнение номер 22. Ноги вместе, ладони на коленях, спина ровная, смотрим вперед, голову не опускаем. Совершаем круговые движения коленями, описываем коленями круги в одну сторону, разгибая колени в конце каждого движения. В противоположную сторону аналогично. Упражнение номер 23. Ноги вместе, в коленях выпрямлены, спина ровная. С движениями рук надавливаем на коленные чашечки, стараясь еще лучше их распрямить. Выполняем несколько таких движений. Смотрим вперед. Тазобедренные суставы. Упражнение номер 24. Поднимаем согнутую в колени одну ногу, бедро параллельно полу, корпус неподвижный. Отводим бедро до отказа в сторону и добавляем усилие, стремясь к коленям. Стремясь отвести бедро еще дальше. Делаем несколько таких пружинистых движений. То же самое выполняем другой ногой в другую сторону. Создайте ощущение внутренней силы. Упражнение номер 25. Исходное положение аналогично предыдущему. Отводим бедро вправо до упора и возвращаем вперед. Делаем это с дополнительным махом бедра вверх-вниз. И повторяем упражнение другой ногой. Напоминаю про общий принцип. Чередуем напряжение и расслабление при минимальной амплитуде движений. Упражнение номер 26. Исходное положение тоже. Отводим правое бедро вправо как можно дальше. Это исходное положение. Коленной чашечкой рисуем на стене кружочки по несколько раз в каждом направлении. Аналогично проделываем левой ногой. Упражнение номер 27. Ходим на выпрямленных ногах. Сначала опираясь на всю стопу, затем на пятки, на носки, на внутреннюю сторону стоп и на внешнюю сторону стоп. Можно даже ускорить шаг. Колени не сгибаем. Плечи не раскачиваем. Напряжение чувствуем в тазобедренных суставах и в области крестца. Вызовите ощущение того, что вы человек жизнерадостный. Упражнение номер 28. Работаем с каждым отделом позвоночника. Шейным, верхнегрудным, нижнегрудным, поясничным. Перед началом упражнений на позвоночник делаем глубокий вдох через нос и медленный выдох через рот. Если вы поели чеснок, то вы можете продолжать упражнение. Если вы поели чеснок, лук или накануне хорошо выпили, то можете дыхательные упражнения делать напротив тещи. Выдох должен быть по продолжительности минимум в 2-3 раза дольше вдоха. С каждым вдохом впитываем физически ощущаемое чувство молодости, свежести, красоты. Упражнение номер 29. Работаем с каждым отделом позвоночника. Вызовите ощущение того, что вы человек жизнерадостный. Упражнение номер 30. Работаем с каждым отделом позвоночника. Если вы поели чеснок, лук или накануне хорошо выпили, то вы можете продолжать упражнение. Вы можете продолжать упражнение номер 31. Работаем с каждым отделом позвоночника. Вы можете продолжать упражнение номер 32. В каждом движении нужно искусственно создавать чувства, ощущения, переживания какой-либо положительной черты характера из вашего списка. Тогда со временем они укоренятся внутри и станут вашей сутью. Итак, приступаем. Создайте внутри радостное состояние, и в каком бы темпе вы ни работали, внутри должно быть спокойствие. С чего начинает возникать спокойствие? Помните? С мышечного корсета. Плечи поправьте. Осанка. Легкая улыбка. Расслабьте все. Веки, лицо, шею, плечи, грудь, живот, ягодицы, ноги. А мышцы мочевого пузыря пока нежелательно. Вдох – безмятежность. Выдох – покой. Вдох – умиротворенность. Выдох – спокойствие. Шина. Выдох – уравновешенность. Дальше продолжайте дышать самостоятельно. Настроились? Прекрасно. Тогда поехали. Шейный отдел. Работа с шейным отделом позвоночника нормализует внутричерепное давление, улучшает зрение, слух, память, повышает работоспособность. Со временем восстанавливается вестибулярный аппарат, улучшается состояние щитовидной железы, становится нормальным сон, устраняется онемение рук и в целом улучшается питание мозга. Упражнение номер один. Корпус прямой, подбородок опущен на грудь. Подбородком скользим вниз по грудине, пытаясь дотянуться до пупка. Как до пупка дойдете, можете обратно идти. Чередуем напряжение и легкое расслабление. С каждым новым напряжением стараемся продолжить движение, добавляя усилий. И снова легкое расслабление. Выполняем несколько таких движений. До боли не доводите. В области шеи должно возникать чувство приятного напряжения. А в теле искусственно создайте волну уверенности в своих силах и постарайтесь как можно дольше удержать ее. Внимание! Если это упражнение выполнять очень тяжело или у вас есть проблемы в шейном отделе позвоночника, то это упражнение очень важно. Упражнение номер два. Корпус прямой, голову не запрокидываем, а слегка отклоняем назад. Тянемся подбородком вверх. Затем движение на секунду останавливаем. Немного отпускаем напряжение, но не расслабляемся. И снова тянемся подбородком ввысь. Делаем несколько таких движений, не забывая про технику безопасности. Упражнение номер три. Позвоночник постоянно прямой. Плечи во время выполнения упражнения абсолютно неподвижны. Голову наклоняем вправо, не поворачиваем. И без особых усилий пытаемся коснуться ухом плеча. Не смущайтесь, если не достигнете цели сразу. И не переусердствуйте. Со временем вы будете делать это свободно. Затем наклоняем голову к левому плечу. Над какой чертой характера вы сейчас работаете? Не упускайте возможности. За одну работу получить несколько вознаграждений. Упражнение номер четыре. Стоим ровно. Голова прямо, смотрим перед собой. Вокруг носа, как вокруг неподвижной опоры, начинаем поворачивать голову. Подбородок при этом идет вправо вверх, а макушка влево вниз. Глаза смотрят вверх. Повторяем в противоположную сторону. Вспомните, как это делает маленький щенок, когда видит что-то интересное, или реагирует на ваши слова. Это упражнение выполняем в трех вариантах. Голова ровно, смотрим перед собой. Голова опущена, смотрим в пол. Голова слегка отклонена назад, смотрим в потолок. Будьте осторожны. Упражнение номер пять. Корпус прямой. Стоим ровно, голова на одной линии с позвоночником. Медленно уводим взгляд вправо, следом поворачиваем голову, и до упора. Это исходное положение. Стараясь увидеть, что находится за спиной, каждый раз дополнительными усилиями пытайтесь увеличить угол поворота. Голову не запрокидываем. Проверьте подбородок около плеча. Делаем несколько таких движений в одну сторону, затем это же упражнение в другую сторону. Перенапряжение недопустимо. Дышать не забывайте. Упражнение номер шесть. Круговые движения головой объединяет в одно все предыдущие упражнения для шейного отдела позвоночника. Голова перекатывается медленно и свободно, не перенапрягая мышцы шеи. Подбородок тянем к грудине, затем ухо стремится к надплечью, затылок к спине, другое ухо к надплечью, подбородок к грудине. Несколько раз в одну сторону, а потом в другую. Выполняйте его с особой осторожностью и вниманием. Следите за своими ощущениями. Если у вас есть проблемы в шейном отделе позвоночника, то движения выполняем по такой схеме. Ухом тянемся к правому плечу, подбородок направлен вниз, затем голова плавно перекатывается к левому плечу и обратно. То есть делаем неполный круг головой, без наклона назад. Работа с верхнегрудным и нижнегрудным отделами позвоночника улучшает состояние сердечно-сосудистой и дыхательной системы. Убирает боль при межреберной невралгии, улучшает состояние органов брюшной полости, почек, поджелудочной железы и также устраняет онемение ног. Верхнегрудной отдел позвоночника. Упражнение номер один. Стоим ровно, спина прямая, никаких наклонов, поясница неподвижна. Замок. Подбородок прижат к груди. Руками устремляемся вниз, а задней поверхностью шеи вверх. Плечи навстречу друг другу. Подбородок не отрывая от груди, тянем к пупку. Дыхание не задерживаем. Верхняя часть позвоночника принимает форму дуги. Представьте, что вы стали похожи на ежика, который несет на спине съестные запасы. Повторяем это движение несколько раз. Упражнение номер два. Выполняем упражнение аналогично предыдущему в противоположную сторону. Выпрямленные руки, сцепленные сзади, тянем вниз. Лопатки стараемся свести. Плечи не поднимаем. Голову держим прямо, не запрокидываем. В этом положении прогибаем верхнюю часть спины. Грудь становится колесом. Грудиной стремимся ввысь. Будьте осторожны. Не переусердствуйте. Упражнение номер три. Позвоночник прямой. Поясница неподвижна. Руки согнуты в локтях. Одно плечо поднимаем, другое опускаем, как чаши весов с разным грузом. Голова наклоняется в сторону, вниз, идущего плеча. Глаза смотрят вверх. В верхнегрудном отделе позвоночника чувствуем приятное напряжение и растяжение. Не меняя положение, чередуем напряжение с легким расслаблением и с каждым разом стараемся чуть больше изогнуть позвоночник. Никаких наклонов, следите за этим. То же самое выполняем в другом направлении. Вы дышите? Хорошо. Дыхание не задерживайте. Какую черту характера вы формируете этим упражнением? Идем дальше. Упражнение номер четыре. Внимание! Техника безопасности. У вас межпозвонковая грыжа? Вам противопоказано упражнение на сжатие позвоночника. Вам следует выполнять только вторую часть упражнения на растяжение. Позвоночник прямой, таз или копчик подаем вперед и фиксируем его в этом положении. Голова неподвижна, руки вдоль туловища. Опуская плечи, тянемся руками в пол. Чувствуем напряжение в верхнегрудном, отделе позвоночника и с каждым повтором после незначительного расслабления добавляем небольшое усилие. Представьте, как будто вам на плечи положили тяжелый мешок. Позвоночник под его весом становится как сжатая пружина. Держим, держим этот груз, добавляем усилие. Помогите себе движением плеч вниз. Просмотрите мысленно весь позвоночник сверху донизу, и равномерно распределите нагрузку. Следите за тем, чтобы нагрузка не была чрезмерной. Теперь сбрасываем мешок. Ощущение легкости полета. Плечи поднимаем максимально вверх, макушкой тянемся к потолку, позвоночник растягивается. Несколько раз чередуем движение плечами вверх с легким расслаблением. Представляем, как все позвонки расправляются и встают на свои места. Упражнение номер пять. Выполняем круговые движения плечами, объединяя при этом предыдущие упражнения. Плечевые суставы вращаем сначала вперед, а затем то же самое проделываем в обратную сторону. Активно работает верхняя часть позвоночника. Упражнение номер шесть. Внимание! Позвоночник – это ось поворота. Ноги на ширине плеч, стопы приклеены к полу параллельно друг к другу, носки слегка направлены внутрь. Кисти рук на плечах, локти разведены в стороны, смотрим прямо перед собой. Последовательно поворачиваем глаза, голову, плечи, грудь. Плечи подвижны. Правый локоть уходит вправо и тянет за собой левую руку. Если живот и бедра следуют за движением вправо, это ошибка. Постарайтесь, не меняя позы, вернуть их в исходное положение. Напряжение возникает в плечевом поясе и в верхнегрудном отделе позвоночника. После того, как дошли до упора, стараемся повернуться еще дальше. Упражнение номер один. Работаем так же, как в упражнении номер один для верхнегрудного отдела позвоночника. Но прорабатываем позвоночник от шеи до поясницы. Копчик подаем вперед и фиксируем это положение, то есть таз неподвижен. Руками как будто обхватываем что-то большое и круглое. Голову наклоняем вниз. Позвоночник от основания черепа до поясницы прогибается дугой. Добавьте напряжение. Слегка отпустите и снова добавьте напряжение. Проверьте, никаких наклонов. Будьте внимательны. В этом положении подвигайте руками. Почувствуйте, как играют, перекатываются мышцы спины. Упражнение номер два. Движение обратное предыдущему. Макушкой тянемся вверх и слегка назад, но голову не запрокидываем. Руки развернуты ладонями вверх и отведены назад. Лопатки сводим. В пояснице не прогибаемся. Упражнение номер три. Стопы вместе. Правую руку сгибаем за головой. Локоть направлен в потолок. Взгляд тоже устремляем в потолок. Левое плечо идет вниз. Растягивая правый бок, подмышкой тянемся вверх, выполняя чередование напряжения с незначительным расслаблением. Амплитуда колебаний небольшая. Позвоночник принимает форму дуги. Наклонов нет. Меняем руку. Делаем то же самое несколько раз вправо. Упражнение номер четыре. Плечами делаем медленные движения по кругу с максимальной амплитудой. В движении принимает участие не только плечи, но и голова, и весь позвоночник до копчика. Разучим это упражнение. Стоим ровно. Ноги шире плеч. Колени слегка согнуты. Перед собой плечи поднимаем к ушам. Голову наклоняем вниз, а плечи направляем навстречу друг другу. Позвоночник принимает форму дуги. Будьте внимательны, это не наклон. Плечи постепенно идут вниз, голову выравниваем. Плечи назад. Голова осторожно отклоняется назад. Позвоночник прогибается вперед. Распределим нагрузку по всему позвоночнику до копчика. Зрительно вспомните, как вращаются колеса паровоза. Выполняем упражнение несколько раз вперед, а затем в противоположную сторону, назад. Делаем аналогично. Упражнение номер пять. Корпус прямой. Ноги вместе. Подбородок прижимаем к грудине. Кулаки над поясницей в области почек. Стараемся как можно ближе свести локти. После этого выполняем несколько пружинищих движений локтями навстречу друг другу. Позвоночник прогибается вперед, как будто от затылка до копчика натягиваем тетиву лука. Локти, стрелы. Аналогично упражнение делаем вперед, только теперь ноги на ширине плеч. Колени слегка согнуты и, выгибая позвоночник, стараемся носом дотянуться до коленей. Позвоночник выгибается как лук. Следите за тем, чтобы наклонов в пояснике не было. Упражнение номер шесть. Ноги шире плеч. Стопы приклеены к полу параллельно друг другу. Носки слегка внутрь. Кисти рук на плечах. Локти разведены в стороны. Таз и бедра зафиксированы. Смотрим прямо перед собой. Уводим глаза. Затем плавно и последовательно поворачиваем голову, плечевой пояс, грудь, живот вправо. Скручиваем верхнюю часть позвоночника от плеч до того места, где теоретически должна быть талия. В этом положении выполняем несколько пружинящих движений, так что каждое следующее усилие должно привести к небольшому увеличению угла поворота. Проверьте. Таз, бедра, стопы должны оставаться неподвижными. В другую сторону аналогично. Пояснично-крестцовый отдел позвоночника. В результате работы с этим отделом позвоночника улучшается состояние мочи половой системы, уменьшается застой крови в органах малого таза, снимается боль при радикулите, ишиасе и других заболеваниях и восстанавливается сексуальность. Внимание! Если у вас есть грыжи в пояснично-крестцовой области позвоночника, то все упражнения выполняем очень аккуратно. С минимальной амплитудой. Нагрузку распределяйте, пожалуйста, равномерно по всему позвоночнику. Упражнение номер один. Ноги на ширине плеч, полусогнутый в коленях, таз вперед, верхняя часть туловища неподвижна. Копчиком тянемся снизу вверх, пытаясь лобком дотянуться до лба, не наоборот. При этом возникающее напряжение чередуем с легким расслаблением. Это несколько раз. Следите за тем, чтобы не было наклонов. Позвоночник прогибается назад дугой. Упражнение номер два. Копчик и таз назад. Ноги на ширине плеч, слегка согнуты в коленях, носки чуть повернуты внутрь, верхняя часть туловища неподвижна. Голова ровно. Копчиком тянемся к затылку. Делаем несколько пружинящих движений, чередуя напряжение и расслабление. Ощущения возникают в пояснично-крестцовой зоне. Возникающую тяжесть снимаем упражнением номер один. Упражнение номер три. Ноги на ширине плеч, колени можно слегка согнуть, корпус прямой, наклонен вперед примерно под углом 45 градусов. Стараясь копчиком дотянуться до затылка, не наоборот, прогибаемся в пояснице. Голову не запрокидываем. Делаем 8-10 таких движений. Затем в этом положении несколько раз переносим вес тела с одной ноги на другую. Напряжение в копчике снимаем упражнением номер один. Упражнение номер четыре. Колени слегка согнуты, прямой корпус слегка отклонен назад, голова прямо. Копчиком устремляемся к затылку, при этом попочку откидываем назад, пузо уходит вперед. В этом положении опускаемся все ниже и ниже, попеременно перенося вес тела с одной ноги на другую. Повторяем упражнение несколько раз. Снимаем напряжение в пояснице. Упражнение номер пять. Круговые движения бедрами сначала 8-10 раз в одну, затем столько же в другую сторону. Верхняя часть корпуса неподвижна, ноги согнуты в коленях. Упражнение номер шесть. Корпус прямой, бедро перемещаем вправо и вперед, то есть вес тела переносим вправо. Это исходное положение. Делаем несколько пружинящих движений бедром в сторону, как бы проталкивая его дальше вправо. Затем исходное положение фиксируем и растягиваем левый бок. Левая ладонь на затылке, взгляд направлен вниз и в сторону, корпус наклоняем вправо. После этого, не меняя наклона, вес тела переносим на левую ногу и еще больше растягиваем левый бок. Аналогично выполняем упражнение левым бедром и растягиваем правый бок. Тянемся ладонью к потолку и делаем легкий наклон влево. Упражнение номер семь. Ноги на ширине плеч, носки чуть-чуть повернуты внутрь, правая рука направлена вертикально вверх, левая опущена. Ладонью стремимся коснуться потолка. С каждым разом все больше и больше растягиваем и слегка прогибаем позвоночник. Те же движения повторяем левой рукой. Упражнение номер восемь. Расслабляем все тело, проводим массаж капилляров. Отрываем пятки от пола и начинаем встряхивать мышцы голеней, бедер, ягодиц, живота, груди, рук, шеи. Внимание! Голова в упражнении не участвует, так как это противопоказано тем, у кого глаукома, отслоение сетчатки, высокое давление и есть проблемы с почками. Ну и в обратном порядке. Представьте, как это делает щенок, отряхиваясь после купания. А теперь можно отдохнуть и подышать, как мы делали перед началом комплекса упражнений на позвоночник. Напомню, позвоночник – ось для всех движений. Голова находится на одной линии с позвоночником. Нагрузку распределяйте равномерно по всему позвоночному столбу. Движения плавные, болевых ощущений не допускайте. Дыхание не задерживайте. Упражнение номер один. Ноги шире плеч, стопы приклеены к полу параллельно друг к другу, носки слегка внутрь. Колени чуть согнуты, руки на надплечьях. Начинаем плавный, медленный, последовательный поворот корпуса вправо. Глаза, голова, плечи, грудь, живот, бедра, таз, ноги – все, кроме стоп. Это исходное положение. Затем добавляем усилие, создаем напряжение, разворачиваемся еще дальше. И снова напряжение. И так несколько раз. При каждом напряжении делаем медленный выдох, после чего возвращаемся в исходное положение. Внимание! При появлении боли уменьшите, пожалуйста, нагрузку. Упражнение номер два. Ноги шире плеч, стопы приклеены к полу параллельно друг к другу, носки слегка внутрь, корпус наклонен вперед под углом 45 градусов. Спина прямая, руки на надплечьях. Начинаем поворот туловища вокруг позвоночника вправо. Глаза, голова, шея, плечи, грудь разворачиваются к потолку, при этом локоть правой руки смотрит вверх. Чередование напряжения и легкого расслабления позволяет постепенно увеличить угол поворота. Выполнив несколько таких чередований, плавно и медленно возвращаемся в исходное положение. Только после этого вы можете выпрямить корпус. Также делаем упражнение влево. Упражнение номер три. Ноги шире плеч, стопы приклеены к полу параллельно друг к другу, носки слегка внутрь. Спина прямая, отклонена назад, голова на одной линии с позвоночником, подбородок направлен к груди, руки на надплечьях. Упражнение номер четыре. Ноги шире плеч, стопы приклеены к полу параллельно друг к другу, носки слегка внутрь. Голова на одной линии с позвоночником, подбородок направлен к груди, руки на надплечьях. Упражнение номер четыре. Ноги шире плеч, стопы приклеены к груди, руки на надплечьях. Упражнение номер четыре. Ноги шире плеч, стопы приклеены к груди, руки на надплечьях. Но это еще не все. Не меняя положение туловища, раскручиваемся влево в обратной последовательности. При этом левый локоть становится ведущим, он идет вверх, назад и вниз, а правый, соответственно, вверх. Смотрим через левое плечо на правую пятку. Чтобы вернуться в исходное положение, совершаем обратный поворот туловища вправо. В баране рог не закрутились? Молодцы! Упражнение номер пять. Чтобы правильно выполнить это упражнение, необходимо в описании предыдущей скрутки везде поменять слово «вправо» на «лево» и наоборот. Будьте, пожалуйста, внимательны и осторожны. Если вы все-таки не уверены, что делаете упражнение правильно, то возьмите какой-нибудь дорогой и хрупкий предмет и выполните скрутку около стены, прижимая и удерживая этот предмет головой. Но как? Ничего не разбилось? Очень хорошо! Значит, вы все сделали правильно. Если с первого раза не получилось, тогда предпринимайте попытку до тех пор, пока не научитесь. И еще. Эти упражнения можно выполнять с палкой на плечах, но не из-под палки. Упражнение номер шесть. Сделайте несколько спокойных глубоких вдохов и выдохов, как мы это делали перед началом упражнений. Делаем вдох носом и одновременно через стороны поднимаем руки. Задерживаем дыхание, руки через стороны опускаем, пока не дышим. Затем ртом делаем выдох, а руки поднимаем над головой спереди. Задерживаем дыхание, руки опускаем через стороны и делаем довыдох. И настраивающее дыхание. Вдох и выдох с движениями рук не совмещаем. Спокойно открываем глаза. И сохраняем это состояние в течение всего дня. Разминка окончена. Вы довольны своей работой? Как ваш внутренний настрой? Хороший? Прекрасно! Теперь в течение дня, удерживая этот настрой, наблюдайте за собой со стороны. Ах, как вы изменились! Как похорошели! Комплекс обязательных упражнений. Напоминаю, что все упражнения выполняются без очков. Как стать влюбленным марсианином с глазами на стебельках. Это упражнение можно выполнять как сидя, так и стоя у окна. Сразу решите для себя, как вы будете это делать. На стекло, чуть ниже уровня глаз, наклейте маленькую картинку размером 3х3 см или 4х4 см. Рисунок должен быть веселый, четко нарисованный. За окном выберите ближайший предмет, который вы видите нечетко. Внимание! Как только этот предмет увидели четко, выбираем другой предмет на таком расстоянии, чтобы он был виден нечетко. А вот расстояние между картинкой на окне и глазами 20-25 см не меняем. Аккомодация глаз. Продолжительность работы 10 минут. Не думайте о времени. Лучше всего поставить легкую, спокойную, приятную музыку. В течение 3-5 секунд рассматриваем рисунок на картинке, а затем взгляд переводим на выбранный объект за окном, причем смотрим на него поверх приклеенной на стекло картинки. Затем также 3-5 секунд разглядываем этот объект и снова плавно переводим взгляд на картинку. Это упражнение следует выполнять 2 раза в день в светлое время суток. Но промежуток времени между подходами должен составлять не менее 2 часов. Настрой есть? Отлично! Держите, держите его как можно дольше. Только следите за тем, чтобы глаза не вылезли из орбит. Если напряжение возникло, чтобы расслабить мышцы, легко-легко поморгайте, похлопайте ресницами по щекам. Следует начинать и заканчивать упражнением начихатто-наплеватто, описанным чуть ниже. Умеете ли вы смотреть на солнце? Пожалуйста, без шуток, вопрос очень серьезный. Еще с древних времен на Востоке люди умели лечить любое глазное заболевание с помощью солнца. Но как? Полезно смотреть на солнце двумя глазами до слез, не моргая, но при строгом соблюдении определения. Запомните, смотреть на солнце двумя глазами можно только на восходе или на закате, когда на линии горизонта видна только половина диска. Это особенно важно. Если оно уже полностью поднялось над горизонтом или еще не опустилось наполовину за горизонт, то глядеть на него опасно. Очень опасно смотреть на него, когда оно в зените. Ни в коем случае нельзя смотреть на солнце неподвижно. Все птицы, оказывается, живут и умирают со стопроцентным зрением, потому что они первыми встречают рассвет, а днем на солнце не смотрят. В природе все гармонично. Первое. Встаньте в тень, так, чтобы одна половина лица оказалась в тени, а другая на освещенном солнцем участке. Глаза при этом закрыты. Для устойчивости расставьте ноги шире плеч. Начните делать легкие повороты туловищем, чтобы лицо оказывалось то на солнце, то в тени. Дыхание спокойное. При повороте головы влево вы мысленно говорите «Солнце приходит». При повороте головы вправо вы мысленно говорите «Солнце уходит». Это очень важно, чтобы не цепляться взглядом за солнце, потому что даже глазами, закрытыми веками, нельзя смотреть на яркое солнце. Продолжительность упражнения увеличивайте до пяти минут постепенно, начиная с одной. После этого обязательно выполняем начихатто-наплеватто. Расслабление выполняем как минимум в два раза дольше, чем делали само упражнение. Работа обязательно должна чередоваться с продолжительным отдыхом. Чем дольше, тем лучше. Второе. Теперь оставим тень и выйдем на солнце. Работа начинается в течение дня до 11 часов и после 15 часов. Ноги поставьте удобно и начните делать легкие повороты корпуса на месте, вокруг оси. Очки не забыли снять. Глаза закрыты. Голова слегка приподнята вверх, так чтобы солнце светило вам прямо на веки. Совершаем большие повороты туловища вправо, при этом левую пятку легко отрываем от земли. Голова следует за движениями корпуса. Обычно при повороте влево отрываем правую пятку. Сильно не раскачивайтесь. Это упражнение должно вызывать расслабление во всем теле. Во время поворотов мысленно говорите фразы «Солнце проходит мимо меня влево, затем вправо, снова влево, и так снова и снова, но все время в направлении, противоположном моему повороту». Помните, в конце предыдущего упражнения мы объясняли, для чего нужно так думать во время работы? Опять расслабление начихато-наплевато. Третье. Особое примечание. Если солнце в зените, то выполнять упражнение нельзя. Продолжаем стоять на освещенном солнцем участке. Ноги поставьте устойчиво. Один глаз накройте ладонью, он остается открытым. Выполняем повороты, часто-часто моргая обоими глазами. Под ладонью глаз тоже моргает. Лицо подставляем солнцу и, выполняя большие повороты и часто моргая обоими глазами, просматриваем небо. Затем с другим глазом аналогично. Закрываем оба глаза веками и выполняем это же упражнение. Это упражнение заканчивается длительным расслаблением начихато-наплевато. Ну как? Сделал вам рекламу начихато-наплевато? Прочтите еще раз. Умеете ли вы дышать глазами? Прежде чем вы приступите к упражнению, внимательно прочитайте его описание. Напяльте образ молодости. Сядьте на стул, устройтесь поудобнее, руки на бедрах, спина прямая. Все тело и мышцы лица расслаблены. В теле, в душе покой. Дышим только носом. А теперь приступаем. Понаблюдайте, как вы дышите через нос. На вдохе в носу и носоглотке ощущается прохлада, а на выдохе тепло. Во всяком случае, вдыхаемый воздух прохладнее, чем тот, который мы выдыхаем. Вот это незначительное колебание температуры нам надо научиться улавливать глазами. Теперь представьте, что вы делаете вдох и выдох через нос, а внимание переносите в область глаз. Попытайтесь глазами уловить движение воздуха, колебание температуры. Можно выполнять это упражнение открытыми, можно закрытыми глазами. Делайте это так, как вам легче. Такое дыхание дает отличные результаты, особенно когда вы представляете свои глаза здоровыми. Дышать глазами можно сколько угодно и где угодно. Так что дышите на здоровье. Как факир Вася Иванов перемещает ощущения. Будьте осторожны, мыслью перемещая ощущения. Помните, как мы перед лицом держали пять пальцев и без отрыва смотрели на один из них? Этот палец моментально начинал откликаться, отличаться более высоким уровнем чувствительности. Он становился теплее, появлялась тяжесть, покалывание. Почему? Потому что от нашего внимания сосуды расширились или сузились. Иначе говоря, с этим пальцем мы установили более высокий контакт. Связь типа «Сокол, Сокол, это Беркут, ты меня слышишь?» Если ответа нет, скажите еще громче «Сокол, Сокол, твою мать, ты меня слышишь?» И рано или поздно Сокол ответит «Беркут, Беркут, я Сокол, слышу тебя хорошо». И ответ имеет, куда вы переносите внимание. Механизм установления связи один. Применим он и к глазам. Особо хочу подчеркнуть лишь то, что фокус внимания имеет ограниченный радиус действия. Вот вам маленький эксперимент. Подойдите к зеркалу и посмотрите на грудь. Теперь, не переводя взгляда, периферическим зрением постарайтесь разглядеть лицо. Вопрос. Лицо видно так же четко, как и грудь? Нет. Поле зрения и фокус внимания имеют ограниченный радиус. Поэтому, когда вы направляете внимание в какую-нибудь зону, именно там происходят изменения. Усиливается приток крови, расширяются мелкие сосуды и возникает ощущение тепла или появляется пульсация. Сосуды расширились и рецепторы проснулись. Когда вы чувствуете прохладу, это показатель того, что сосуды сузились. Прохладу также необходимо вызывать, как и тепло. Она фиксирует результат. Мы будем мысленно направлять ощущение тепла, покалывания, холода от затылка к глазным яблокам и обратно. Это упражнение несложное, но с некоторыми тонкостями, обязательными для исполнения. Изучите его хорошенько, прежде чем приступать. Сядьте удобно, примите мышечный корсет, закройте глаза. Начнем с ощущения тепла. Представьте себе, что вы сидите спиной к солнцу, и его лучи мягко касаются затылка. Затылок приятно нагревается. Продолжаем накапливать тепло. Внимание, до ощущения жара ни в коем случае не доводим. Нельзя так же, чтобы тепло разливалось внутри. Тепло собираем в комочек в области затылка. Затем отпускаем, рассеиваем, теряем его. Еще раз. Собираем и рассеиваем ощущение тепла. Достаточно сделать так 2-3 раза. После этого собираем тепло в комочек и медленно, на мысленном уровне, начинаем тянуть его по прямой от затылка к глазным яблокам и обратно в затылок. Внимание, техника безопасности. Комочек ощущения глазных яблок не касается. Делаем несколько таких проходов в одну и другую сторону. После того, как вы сделали упражнение с закрытыми глазами, глазки открываем и выполняем его аналогично несколько раз с тем же комочком тепла. Ну как, освоили? Если ответ положительный, то только в этом случае вы можете приступить к изучению упражнения с ощущением холода. Теперь как создать ощущение холода. Держим внимание в области затылка и просто стараемся улучшить его. Дополнительно вызываем чувство легкости. Ну как, получилось? Чмок-чмок. Значит, все зависит от вашего внимания. Теперь, когда вы научились улавливать прохладу, собираем в области затылка комочек прохлады и делаем упражнение аналогично предыдущему. Маленькое дополнение, обязательное для применения. Постарайтесь добавить к ощущению тепла и, соответственно, прохлады, покалывание. Как его вызвать? Вспомните, например, как, попадая с мороза в теплое помещение, начинаете оттаивать. Что вы при этом чувствуете? Вспомните эти ощущения, а потом мысленно перенесите их в область затылка и продолжайте работать описанным выше способом. Теперь переходим ко второй части этого упражнения. Задачу немного усложним. Комочки тепла с покалыванием или холода с покалыванием спирали от затылка к глазным яблокам и обратно. Направление движения произвольное, по или против часовой стрелки. Напомню, все комочки ощущений, которые мы передвигаем от затылка, глазных яблок не касаются. Контрольный вопрос. У вас получилось упражнение? Если да, так держать. А если вы сомневаетесь или говорите, что у вас получилось слабовато или не получилось вовсе, то скажу одно. Во время выполнения упражнений уберите недовольство собой в любой форме проявления. Такие неудачи являются отражением вашего характера. Продолжайте тренировку, и результат постепенно будет накапливаться. Учитесь чувствовать к себе признательность за все, что бы вы ни делали. Вот сейчас почешите свое ухо. А теперь даже за это искусственно создайте благодарность в свой адрес. Счастье складывается из мелочей, из маленьких незначительных радостей. Огромное счастье, как кирпич с неба не падает, а кропотливо созидается. Рекомендации для самостоятельной работы. Дорогой читатель, теперь вы вооружены знаниями. Осталось тренироваться до полной адаптации организма к нормальному зрению. Правда, при этом необходимо соблюдать все правила по технике безопасности и делать упражнения с внутренним настроем на успех. Вспомните, как в детстве вы ждали любимый праздник, считали дни, часы до его наступления. В душе нетерпеливое и трепетное ожидание, истома от предвкушения чего-то светлого и радостного. Вы представляли в ярких красках, и до наступления самого торжества много раз проживали его. А потом, при каждом воспоминании, как становилось тепло и радостно на душе. Помните? Создайте внутри такой праздник по собственному желанию. Опасайтесь выполнять упражнения с неохотой, как бы делая себе одолжение. Так вы не получите никакой пользы и даже можете навредить себе, потому что мысли материальны. Что посеете, то и пожнете. Утро. Внутри легкость и радость от того, что наступил новый день. Открылись новые возможности сделать много хорошего для себя и окружающих. На первых порах вам придется приложить колоссальные усилия к тому, чтобы играть роль счастливого и здорового человека. Вырабатывайте привычку быть счастливым и здоровым. С каждым днем вам все меньше и меньше придется заставлять себя потому что постепенно эта роль станет вашей внутренней сутью. Что тренируется, то развивается. Все упражнения делайте по октаве в образе молодости. Слейтесь с этим образом воедино. Каждое утро натощак выпивайте стакан горячей, но не обжигающей кипяченой воды. Представляете, как эта вода проходит по клеткам и сосудам всего организма, очищая их. Делая упругими и эластичными. Два раза в день утром и вечером принимайте душ. Смывайте токсины и шлаки, чтобы предотвратить их повторное всасывание через поры кожи. Для усиления эффекта можно после душа протирать все тело горячим, мокрым, махровым полотенцем. После утренних процедур легко перекусите, голодным не занимайтесь, но знайте меру, не переедайте, иначе будет трудно выполнять упражнения. Первое. Суставная гимнастика с начинкой. Первое время, примерно неделю, на ее выполнение будет уходить около 35 минут. Когда все упражнения освоите хорошо, это время не должно превышать 15-20 минут. Главное помнить, что 90% своих усилий вы должны направить на искусственное созидание тех качеств, которые хотите в себе воплотить. Механическое выполнение упражнений – путь в никуда. Второе. Упражнения, как стать влюбленным марсианином с глазами на стебельках, два раза в день по 10 минут. Третье. Упражнения для глаз. Можно выполнять несколько раз в течение дня, это очень полезно, но один раз в день 10 минут обязательно. Лучше всего перед началом работы с таблицей. Смотри приложения 2 и 3. Четвертое. Упражнения на расслабление. Направление энергии к глазам на чихатто-наплеватто. Чем чаще, тем лучше, но минимум 10 минут в день. Расслабление способствует более быстрому восстановлению зрения и очень полезно для всей нервной системы. Пятое. Работа с таблицей. Коррекция зрения. Два раза в день, утром и во второй половине дня. Чтобы у вас была возможность перед коррекцией зрения по таблице выполнить упражнения для глаз и на расслабление. Направление энергии к глазам и на чихатто-наплеватто. Не забудьте вымыть руки, это серьезно. Выполните три подхода по 30 секунд за один прием. Принимаем мышечный корсет. Поднимаем настроение. Искусственно создаем в душе радостное ожидание результата. Выполняем первый подход, это первые полторы минуты. Разложенные на 30 секунд. Работаем двумя глазами. Затем выполняем упражнение на расслабление. На чихатто, на все, на плеватто. Эмоции повышаем до тех пор, пока вы не заметите, работая с таблицей, первые проблески улучшения зрения. Удерживаем настрой на таком уровне. Второй подход. Выполняем поочередно каждым глазом по 30 секунд. После этого опять обязательно на чихатто-наплеватто. И последние 30 секунд снова работаем двумя глазами. Снова даем глазкам и всей нервной системе расслабление и отдых. На чихатто-наплеватто. Третий подход. Поворачиваемся так, чтобы свет на таблицу падал под другим углом. И снова продолжаем работать двумя глазами, как это делали в первом подходе. Делаем упражнение на расслабление и создаем в душе благодарность в свой адрес. Шестое. Работа с солнцем. Каждое утро до 11 часов, максимум 10 минут. Седьмое. Дыхание через глаза. Выполняйте так часто, как это возможно. Восьмое. Передвижение ощущений тепла, покалывания, холода от затылка до глазных яблок выполняется только два раза в неделю. Не более 10 минут. Работу по восстановлению зрения можно распределить на весь день, выбирая для занятий удобное время, но при соблюдении техники безопасности, особых условий и других замечаний. Поэтому, прежде чем составлять индивидуальный план занятий, еще раз внимательно прочитайте все упражнения. И еще маленький совет. Когда вы начинаете планировать работу над собой, как будто с неба разные мешающие обстоятельства. Вы уже знаете, что это происки лени. Обманите ее. Жестко не привязывайте себя ко времени и месту занятий. Что это значит? Многие упражнения легко выполнять по ходу, занимаясь домашними делами или в дороге. Так можно с пользой для себя употребить время вынужденного ожидания или пребывания в пути. Дышание через глаза и особые упражнения на чихатто-наплеватто. Принимая душ, представляете себя под водопадом из образа молодости. Не нужно искать специального времени и условий, чтобы носить мышечный корсет. Тем более, что вы в нем должны спать. Это, конечно, шутка, но не лишенная доли интереса. В общем, идея понятна, да? Так что действуйте. Вы же хотите хорошо видеть без костылей, унитазов, гоночного велосипеда и прочего снаряжения не только по вторникам и субботам с 10 до 18 часов, а всегда? Не так ли? Поэтому работайте над собой постоянно, меняйтесь сами, и мир вокруг вас тоже изменится. С вами всей душой, автор. Заключение. Дорогой собеседник, книга закончилась, но путь к самопознанию только начинается. Вы познакомились, а может даже и освоили, лишь первую ступень лестницы, ведущей в мир ваших неограниченных возможностей. Вы решали вопросы, связанные с гематомией, пороем между ушами, другими словами, проблемы тела. Если вы успешно преодолели этот рубеж, то оказались на пороге новых открытий себя как личности. У вас есть к этому стремление и тяга, да? Отлично, все дороги к этому открыты. Но только одно предостережение. Мы живем в мире зазеркалья. В мире, где фальшь, обман, воровство, подлость, лицемерие, предательство, подняты до уровня добра. Где ложные ценности управляют людьми. А честность, порядочность, доброта высмеиваются и чуть ли не вменяются в вину. Но выбор все-таки всегда остается за каждым. Те люди, которые ради тщеславия, наживы, жажды власти идут на все, свой выбор уже сделали. Не всегда смешно и одновременно грустно, когда какая-нибудь Марья Ивановна ведет занятие по йоге, Иван Иванович объявляет себя специалистом по культуре Востока и читает лекции, а Петр Петрович обучает людей восточным единоборствам. Такие люди воспринимают во всем, с чем соприкасаются, только внешнюю форму. То, что лежит на поверхности, что более доступно. А суть? Суть их тоже, может быть, интересует, но только суть в их понимании. Для глубинного осмысления у них не хватает терпения или духовного опыта, а скорее того и другого, а вместе с этим и стремление докопаться до нее. Проще схватить, что ближе лежит, и вперед, размахивая флагом, нести в массы то, что смогли постичь своим умом, а не душой. Не будем осуждать таких людей. В этом проявляется их слабость. Только слабый человек сам не способен ничего создать, а начинает менять, улучшать что-то уже существующее. Не позволяйте обманывать и дальше уводить себя в мир зазеркалья. Учитесь отделять зерна от плевел. Как вы думаете, почему Амар Хайям и Фердауси считаются мировыми классиками? Ведь читая Амара Хайяма, приходишь к мнению, что он вечный забулдыга и бабник. Не так ли? На самом деле, это только внешняя оболочка, за которой большинство людей не видят ничего другого. Блистательный астроном, выдающийся математик, великий лекарь, яркий писатель, величайший философ. Он спрятал истину, как сам говорил, за семью печатями и сорока замками, чтобы злобное стадо людей не использовало ее во имя зла. Внешняя форма скрывает истину от случайных людей, не готовых без искажения принять ее. От тех, у кого примитивное мышление, то есть мышление на уровне желудочно-кишечного тракта. Точно знаю, мой собеседник, что это к вам не относится. Вы прочитали книгу по учебно-оздоровительной системе. На основе ее планируется выпуск серии книг, связанной с другими органами и системами организма человека. Вы пропустили через себя огромный поток информации и тем самым проделали немалую работу над собой. Значит, вы уже другой человек. Впереди много дорог, но вы уже знаете, чтобы чего-то добиться, главное действовать. Тогда в час добрый. А вот и готовый список положительных и отрицательных черт характера. Подарок от слушателей курсов. Он, конечно, очень приблизительный и далеко не полный. Аккуратность, активность, альтруизм, артистичность, бескорыстие, бесстрашие, благородство, вежливость, великодушие, вера, верность, внимательность, воля, выдержка, галантность, гостеприимство, гуманность, дальновидность, доброжелательность, добросовестность, доброта, дружелюбие, естественность, женственность, жизнелюбие, жизнерадостность, заботливость, изобретательность, интеллигентность, искренность, коммуникабельность, кропотливость, любезность, любознательность, мудрость, мужество. Надежность, настойчивость, нежность, независимость, обаятельность, общительность, обязательность, озорство, опрятность, оптимизм, остроумие, отважность, отзывчивость, открытость, понятливость, постоянство, правдивость, приветливость, принципиальность, пунктуальность, работоспособность, раскованность, раскрепощенность, расторопность, решительность, романтичность. Самостоятельность, сдержанность, сердечность, серьезность, собранность, сострадание, сочувствие, спокойствие, способность быть сильным, способность мечтать, способность созидать, стабильность, строгость, тактичность, творчество, терпимость, точность, трудолюбие, увлеченность, удачливость, удовлетворенность, уживчивость, улыбчивость, умение быть благодарным. Умение любить, умение молчать, умение понимать, умение прощать, умение слушать, умение фантазировать, умеренность, упорство, уравновешенность, усидчивость, уступчивость, утонченность, хозяйственность, целеустремленность, целостность, честность, чистоплотность, чувственность, чувствительность, чувство долга, чувство собственного достоинства, чувство юмора, щедрость, элегантность, на сегодня. diff --git a/examples/whisper.youtube/yt_transcribe.py b/examples/whisper.youtube/yt_transcribe.py new file mode 100644 index 000000000..6e5237edc --- /dev/null +++ b/examples/whisper.youtube/yt_transcribe.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Download audio from YouTube and transcribe with local whisper-cli.""" + +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent +WHISPER_CLI = REPO_ROOT / "build" / "bin" / "whisper-cli" +MODEL = REPO_ROOT / "models" / "ggml-large-v3.bin" + + +def download_audio(url: str, out_path: str): + """Download audio from YouTube URL using yt-dlp.""" + subprocess.run([ + "yt-dlp", "-x", "--audio-format", "wav", + "-o", out_path, url, + ], check=True) + + +def convert_wav(src: str, dst: str): + """Convert audio to 16kHz mono WAV for whisper.""" + subprocess.run([ + "ffmpeg", "-y", "-i", src, + "-ar", "16000", "-ac", "1", "-f", "wav", dst, + ], check=True, capture_output=True) + + +def transcribe(wav_path: str) -> str: + """Run whisper-cli on a WAV file and return text.""" + result = subprocess.run([ + str(WHISPER_CLI), "-m", str(MODEL), + "-f", wav_path, "-nt", "-np", "-l", "auto", + ], capture_output=True, text=True) + if result.returncode != 0: + print(f"whisper-cli error: {result.stderr[:500]}", file=sys.stderr) + sys.exit(1) + return result.stdout.strip().replace("[BLANK_AUDIO]", "").strip() + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + url = sys.argv[1] + + if not WHISPER_CLI.exists(): + print(f"whisper-cli not found: {WHISPER_CLI}", file=sys.stderr) + sys.exit(1) + if not MODEL.exists(): + print(f"Model not found: {MODEL}\nRun: bash models/download-ggml-model.sh base", file=sys.stderr) + sys.exit(1) + + with tempfile.TemporaryDirectory(prefix="yt-whisper-") as tmpdir: + raw_audio = os.path.join(tmpdir, "audio.%(ext)s") + wav_16k = os.path.join(tmpdir, "audio_16k.wav") + + print(f"Downloading audio from {url} ...") + download_audio(url, raw_audio) + + # find the downloaded file (yt-dlp replaces %(ext)s) + downloaded = [f for f in os.listdir(tmpdir) if f.startswith("audio.")] + if not downloaded: + print("Download failed: no audio file found", file=sys.stderr) + sys.exit(1) + raw_path = os.path.join(tmpdir, downloaded[0]) + + print("Converting to 16kHz WAV ...") + convert_wav(raw_path, wav_16k) + + print("Transcribing ...") + text = transcribe(wav_16k) + + print("\n=== Transcription ===") + print(text) + + +if __name__ == "__main__": + main()