{"id":482105,"date":"2026-06-02T13:18:36","date_gmt":"2026-06-02T13:18:36","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=482105"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=482105","title":{"rendered":"\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0434\u043e\u043c\u0430\u0448\u043d\u0438\u0439 \u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442 \u043d\u0430 Raspberry Pi 5 \u0447.2"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<h2>\u0428\u0430\u0433 4: \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u00ab\u0410\u0443\u0434\u0438\u043e\u00bb<\/h2>\n<p><strong>\u0426\u0435\u043b\u044c \u0448\u0430\u0433\u0430:<\/strong> \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u043e \u0437\u0432\u0443\u043a\u043e\u043c, \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043c\u043e\u0434\u0443\u043b\u044c <code>core\/audio_<\/code><a href=\"http:\/\/manager.py\" rel=\"noopener noreferrer nofollow\"><code>manager.py<\/code><\/a> \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0430\u0443\u0434\u0438\u043e-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0432\u0438\u0434\u0436\u0435\u0442 <code>ui\/audio_<\/code><a href=\"http:\/\/tab.py\" rel=\"noopener noreferrer nofollow\"><code>tab.py<\/code><\/a> \u0441 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u043c\u0438 \u0441\u043f\u0438\u0441\u043a\u0430\u043c\u0438 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u043e\u0432 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u043e\u0432, \u043a\u043d\u043e\u043f\u043a\u0430\u043c\u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435\u043c \u0432\u044b\u0431\u043e\u0440\u0430 \u0432 <code>config.json<\/code>. <\/p>\n<h2>4.1. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0445 \u0438 Python-\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439<\/h2>\n<p> \u26a0\ufe0f <strong>\u041f\u0435\u0440\u0435\u0434 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435\u043c \u044d\u0442\u0438\u0445 \u043a\u043e\u043c\u0430\u043d\u0434 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e \u0441\u0430\u043c\u043e\u0435 \u041f\u041e \u0438\u0437 \u0442\u0440\u0435\u0445 \u0431\u0443\u043a\u0432.<\/strong> <\/p>\n<pre><code># 1. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0443\u044e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 PortAudio# \u041e\u043d\u0430 \u043d\u0443\u0436\u043d\u0430 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b sounddevice \u043d\u0430 Linux\/Raspberry Pi# Install PortAudio system library# It's required for sounddevice to work on Linux\/Raspberry Pisudo apt install libportaudio2 -y# 2. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u043c \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 (\u0435\u0441\u043b\u0438 \u0435\u0449\u0451 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u043e)# Activate environment (if not already activated)cd ~\/Zahar &amp;&amp; source venv\/bin\/activate# 3. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c Python-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u043e \u0437\u0432\u0443\u043a\u043e\u043c# Install Python libraries for audio work# sounddevice - \u0440\u0430\u0431\u043e\u0442\u0430 \u0441 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430\u043c\u0438 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430\u043c\u0438 \/ working with mics and speakers# numpy - \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0430\u0443\u0434\u0438\u043e\u0434\u0430\u043d\u043d\u044b\u0445 \/ audio data processingpip install sounddevice numpy<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h2>4.2. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u044f core\/audio_manager.py<\/h2>\n<p>\u042d\u0442\u043e \u00ab\u043c\u043e\u0437\u0433\u00bb \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0430\u0443\u0434\u0438\u043e. \u041c\u043e\u0434\u0443\u043b\u044c \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430:<\/p>\n<ul>\n<li>\n<p>\u0421\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0441\u0435\u0445 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u043e\u0432 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u043e\u0432<\/p>\n<\/li>\n<li>\n<p>\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e<\/p>\n<\/li>\n<li>\n<p>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430 (\u0437\u0430\u043f\u0438\u0441\u044c 3 \u0441\u0435\u043a\u0443\u043d\u0434 + \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435)<\/p>\n<\/li>\n<li>\n<p>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 (\u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0442\u043e\u043d\u0430 440 \u0413\u0446)<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u0441\u0435 \u0442\u0435\u0441\u0442\u044b \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u043e\u0442\u043e\u043a\u0430\u0445, \u0447\u0442\u043e\u0431\u044b \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043d\u0435 \u0437\u0430\u0432\u0438\u0441\u0430\u043b.<\/p>\n<details class=\"spoiler\">\n<summary>\u0421\u043a\u0440\u044b\u0442\u044b\u0439 \u0442\u0435\u043a\u0441\u0442<\/summary>\n<div class=\"spoiler__content\">\n<pre><code>cat &gt; ~\/Zahar\/core\/audio_manager.py &lt;&lt; 'EOF'# ============================================================# \u041c\u043e\u0434\u0443\u043b\u044c \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0430\u0443\u0434\u0438\u043e-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438# Audio devices management module# ============================================================# \u041e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0437\u0430\u043f\u0438\u0441\u044c \u0438 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0437\u0432\u0443\u043a\u0430.# Responsible for device scanning, recording and playing sound.import sounddevice as sdimport numpy as npimport threadingfrom typing import List, Tuple, Optional, Callableclass AudioManager:    \"\"\"    \u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0430\u0443\u0434\u0438\u043e-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.    Audio devices manager.    \u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430\u043c\u0438 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430\u043c\u0438.    Provides methods for working with microphones and speakers.    \"\"\"        def __init__(self):        # \u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u0434\u0438\u0441\u043a\u0440\u0435\u0442\u0438\u0437\u0430\u0446\u0438\u0438 (16kHz \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0434\u043b\u044f \u0440\u0435\u0447\u0438)        # Sample rate (16kHz is enough for speech)        self.sample_rate = 16000        # \u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043a\u0430\u043d\u0430\u043b\u043e\u0432 (1 = \u043c\u043e\u043d\u043e)        # Number of channels (1 = mono)        self.channels = 1        def get_input_devices(self) -&gt; List[Tuple[int, str]]:        \"\"\"        \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0432\u0432\u043e\u0434\u0430 (\u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u043e\u0432).        Returns list of input devices (microphones).                \u0424\u043e\u0440\u043c\u0430\u0442 \/ Format: [(id, \"\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\"), ...]        \"\"\"        devices = sd.query_devices()        result = []        for i, dev in enumerate(devices):            # \u0424\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0432\u0445\u043e\u0434\u043d\u044b\u043c\u0438 \u043a\u0430\u043d\u0430\u043b\u0430\u043c\u0438            # Filter only devices with input channels            if dev['max_input_channels'] &gt; 0:                result.append((i, dev['name']))        return result        def get_output_devices(self) -&gt; List[Tuple[int, str]]:        \"\"\"        \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0432\u044b\u0432\u043e\u0434\u0430 (\u0434\u0438\u043d\u0430\u043c\u0438\u043a\u043e\u0432).        Returns list of output devices (speakers).                \u0424\u043e\u0440\u043c\u0430\u0442 \/ Format: [(id, \"\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\"), ...]        \"\"\"        devices = sd.query_devices()        result = []        for i, dev in enumerate(devices):            # \u0424\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0432\u044b\u0445\u043e\u0434\u043d\u044b\u043c\u0438 \u043a\u0430\u043d\u0430\u043b\u0430\u043c\u0438            # Filter only devices with output channels            if dev['max_output_channels'] &gt; 0:                result.append((i, dev['name']))        return result        def get_default_input_id(self) -&gt; Optional[int]:        \"\"\"        \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432\u0432\u043e\u0434\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e.        Returns default input device ID.        \"\"\"        try:            # sd.default.device \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u0440\u0442\u0435\u0436 (input_id, output_id)            # sd.default.device returns tuple (input_id, output_id)            default_input = sd.default.device[0]            # \u0415\u0441\u043b\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c None            # If system didn't detect device, return None            if default_input is None or default_input &lt; 0:                return None            return int(default_input)        except Exception:            return None        def get_default_output_id(self) -&gt; Optional[int]:        \"\"\"        \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e.        Returns default output device ID.        \"\"\"        try:            default_output = sd.default.device[1]            if default_output is None or default_output &lt; 0:                return None            return int(default_output)        except Exception:            return None        def test_microphone(        self,         input_device_id: int,         output_device_id: int,        duration: float = 3.0,        on_status: Optional[Callable[[str], None]] = None    ) -&gt; threading.Thread:        \"\"\"        \u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u0442 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d: \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 3 \u0441\u0435\u043a\u0443\u043d\u0434\u044b \u0437\u0432\u0443\u043a\u0430 \u0438 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442 \u0435\u0433\u043e.        Tests microphone: records 3 seconds of sound and plays it back.                \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \/ Parameters:            input_device_id - ID \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430 \/ microphone ID            output_device_id - ID \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 \u0434\u043b\u044f \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f \/ speaker ID for playback            duration - \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u0438 \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445 \/ recording duration in seconds            on_status - \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0434\u043b\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u0432 \/ callback for statuses                        \u0421\u0442\u0430\u0442\u0443\u0441\u044b \/ Statuses: \"recording\", \"playing\", \"done\", \"error: ...\"                \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \/ Returns:            threading.Thread - \u043f\u043e\u0442\u043e\u043a \u0441 \u0442\u0435\u0441\u0442\u043e\u043c \/ thread with test        \"\"\"        def _run_test():            try:                # \u0421\u0442\u0430\u0442\u0443\u0441: \u0437\u0430\u043f\u0438\u0441\u044c                # Status: recording                if on_status:                    on_status(\"recording\")                                # \u0417\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c \u0437\u0432\u0443\u043a \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430                # Record sound from selected microphone                audio_data = sd.rec(                    int(duration * self.sample_rate),                    samplerate=self.sample_rate,                    channels=self.channels,                    device=input_device_id,                    dtype='float32'                )                # \u0416\u0434\u0435\u043c \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f \u0437\u0430\u043f\u0438\u0441\u0438                # Wait for recording to finish                sd.wait()                                # \u0421\u0442\u0430\u0442\u0443\u0441: \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435                # Status: playing                if on_status:                    on_status(\"playing\")                                # \u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u043e\u0435 \u0447\u0435\u0440\u0435\u0437 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0434\u0438\u043d\u0430\u043c\u0438\u043a                # Play back recorded audio through selected speaker                sd.play(                    audio_data,                    samplerate=self.sample_rate,                    device=output_device_id                )                sd.wait()                                # \u0421\u0442\u0430\u0442\u0443\u0441: \u0433\u043e\u0442\u043e\u0432\u043e                # Status: done                if on_status:                    on_status(\"done\")                                except Exception as e:                # \u0421\u0442\u0430\u0442\u0443\u0441: \u043e\u0448\u0438\u0431\u043a\u0430                # Status: error                if on_status:                    on_status(f\"error: {e}\")                # \u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0442\u0435\u0441\u0442 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c \u043f\u043e\u0442\u043e\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c GUI        # Run test in separate thread to not block GUI        # daemon=True - \u043f\u043e\u0442\u043e\u043a \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043e\u0439        # daemon=True - thread will terminate with the program        thread = threading.Thread(target=_run_test, daemon=True)        thread.start()        return thread        def test_speaker(        self,         output_device_id: int,        on_status: Optional[Callable[[str], None]] = None    ) -&gt; threading.Thread:        \"\"\"        \u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u0442 \u0434\u0438\u043d\u0430\u043c\u0438\u043a: \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0439 \u0442\u043e\u043d (440 \u0413\u0446, 1 \u0441\u0435\u043a\u0443\u043d\u0434\u0430).        Tests speaker: plays a test tone (440 Hz, 1 second).                \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \/ Parameters:            output_device_id - ID \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 \/ speaker ID            on_status - \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0434\u043b\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u0432 \/ callback for statuses        \"\"\"        def _run_test():            try:                if on_status:                    on_status(\"playing\")                                # \u0413\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u043c \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0439 \u0442\u043e\u043d: \u0441\u0438\u043d\u0443\u0441\u043e\u0438\u0434\u0430 440 \u0413\u0446 (\u043d\u043e\u0442\u0430 \"\u041b\u044f\")                # Generate test tone: 440 Hz sine wave (note \"A\")                duration = 1.0                t = np.linspace(0, duration, int(self.sample_rate * duration), False)                # 0.3 - \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u044c (30% \u043e\u0442 \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c\u0430, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u043e\u0433\u043b\u0443\u0448\u0438\u0442\u044c)                # 0.3 - volume (30% of max to avoid being too loud)                tone = 0.3 * np.sin(2 * np.pi * 440 * t)                                # \u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u043c \u0442\u043e\u043d \u0447\u0435\u0440\u0435\u0437 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0434\u0438\u043d\u0430\u043c\u0438\u043a                # Play tone through selected speaker                sd.play(                    tone.astype(np.float32),                    samplerate=self.sample_rate,                    device=output_device_id                )                sd.wait()                                if on_status:                    on_status(\"done\")                                except Exception as e:                if on_status:                    on_status(f\"error: {e}\")                thread = threading.Thread(target=_run_test, daemon=True)        thread.start()        return threadEOF<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<\/details>\n<h2>4.3. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 ui\/audio_tab.py<\/h2>\n<p>\u042d\u0442\u043e \u0432\u0438\u0434\u0436\u0435\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u043a\u043d\u043e\u043f\u043a\u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0432\u044b\u0431\u043e\u0440 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. <\/p>\n<details class=\"spoiler\">\n<summary>\u0421\u043a\u0440\u044b\u0442\u044b\u0439 \u0442\u0435\u043a\u0441\u0442<\/summary>\n<div class=\"spoiler__content\">\n<pre><code>cat &gt; ~\/Zahar\/ui\/audio_tab.py &lt;&lt; 'EOF'# ============================================================# \u0412\u043a\u043b\u0430\u0434\u043a\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0443\u0434\u0438\u043e-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432# Audio devices settings tab# ============================================================# \u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043a\u0438 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u043e\u0432 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u043e\u0432, \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0445 \u0432\u044b\u0431\u0440\u0430\u0442\u044c,# \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0432\u044b\u0431\u043e\u0440 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.# Displays lists of microphones and speakers, allows selection,# testing and saving choice to configuration.import customtkinter as ctkfrom typing import Dict, List, Tuple, Callablefrom core.audio_manager import AudioManagerclass AudioTab(ctk.CTkFrame):    \"\"\"    \u0412\u0438\u0434\u0436\u0435\u0442 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0430\u0443\u0434\u0438\u043e.    Audio settings tab widget.    \"\"\"        def __init__(        self,         parent,         translations: Dict[str, Dict[str, str]],        get_lang: Callable[[], str],        config: Dict,        save_config: Callable[[Dict], None]    ):        # \u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0444\u0440\u0435\u0439\u043c        # Initialize parent frame        super().__init__(parent, fg_color=\"transparent\")                # \u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0430\u0443\u0434\u0438\u043e (\u043b\u043e\u0433\u0438\u043a\u0430 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438)        # Audio manager (device logic)        self.audio_manager = AudioManager()                # \u0421\u0441\u044b\u043b\u043a\u0438 \u043d\u0430 \u043e\u0431\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f        # References to common application objects        self.translations = translations        self.get_lang = get_lang        self.config = config        self.save_config = save_config                # \u0421\u043b\u043e\u0432\u0430\u0440\u0438 \u0434\u043b\u044f \u043c\u0430\u043f\u043f\u0438\u043d\u0433\u0430 \"\u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -&gt; ID\"        # Dictionaries for mapping \"device name -&gt; ID\"        # \u042d\u0442\u043e \u043d\u0443\u0436\u043d\u043e, \u0442\u0430\u043a \u043a\u0430\u043a CTkOptionMenu \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441\u043e \u0441\u0442\u0440\u043e\u043a\u0430\u043c\u0438        # Needed because CTkOptionMenu works with strings        self.input_devices: List[Tuple[int, str]] = []        self.output_devices: List[Tuple[int, str]] = []                # \u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441        # Build the interface        self._build_ui()                # \u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0438 \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f        # Load device list and set saved values        self.refresh_devices()        def _t(self, key: str, default: str = \"\") -&gt; str:        \"\"\"        \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0432\u043e\u0434 \u0434\u043b\u044f \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u044f\u0437\u044b\u043a\u0430.        Gets translation for current language.        \"\"\"        lang = self.get_lang()        return self.translations.get(lang, {}).get(key, default)        def _build_ui(self):        \"\"\"        \u0421\u043e\u0431\u0438\u0440\u0430\u0435\u0442 \u0432\u0438\u0437\u0443\u0430\u043b\u044c\u043d\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0432\u043a\u043b\u0430\u0434\u043a\u0438.        Builds tab visual elements.        \"\"\"        # ==========================================        # \u0417\u0410\u0413\u041e\u041b\u041e\u0412\u041e\u041a \u0412\u041a\u041b\u0410\u0414\u041a\u0418        # TAB HEADER        # ==========================================        self.title_label = ctk.CTkLabel(            self,            text=self._t(\"audio_header\", \"\u0410\u0443\u0434\u0438\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\"),            font=ctk.CTkFont(size=24, weight=\"bold\")        )        self.title_label.pack(pady=(20, 5), padx=20, anchor=\"w\")                self.subtitle_label = ctk.CTkLabel(            self,            text=self._t(\"audio_subtitle\", \"\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a \u0434\u043b\u044f \u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u0430\"),            font=ctk.CTkFont(size=14),            text_color=\"gray\"        )        self.subtitle_label.pack(pady=(0, 20), padx=20, anchor=\"w\")                # ==========================================        # \u0421\u0415\u041a\u0426\u0418\u042f \u041c\u0418\u041a\u0420\u041e\u0424\u041e\u041d\u0410        # MICROPHONE SECTION        # ==========================================        # \u0420\u0430\u043c\u043a\u0430 \u0434\u043b\u044f \u0433\u0440\u0443\u043f\u043f\u044b \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430        # Frame for microphone elements group        self.mic_frame = ctk.CTkFrame(self, fg_color=(\"gray85\", \"gray20\"))        self.mic_frame.pack(fill=\"x\", padx=20, pady=10)                # \u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0441\u0435\u043a\u0446\u0438\u0438        # Section header        self.mic_label = ctk.CTkLabel(            self.mic_frame,            text=self._t(\"microphone\", \"\u041c\u0438\u043a\u0440\u043e\u0444\u043e\u043d (\u0432\u0445\u043e\u0434)\"),            font=ctk.CTkFont(size=16, weight=\"bold\")        )        self.mic_label.pack(pady=(15, 10), padx=15, anchor=\"w\")                # \u0412\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u043e\u0432        # Microphone dropdown        # \u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u044b \u043f\u043e\u0437\u0436\u0435 \u0432 refresh_devices()        # Values will be filled later in refresh_devices()        self.mic_menu = ctk.CTkOptionMenu(            self.mic_frame,            values=[\"...\"],            width=500,            height=40,            font=ctk.CTkFont(size=14)        )        self.mic_menu.pack(pady=5, padx=15, fill=\"x\")                # \u041a\u043d\u043e\u043f\u043a\u0430 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430        # Microphone test button        self.mic_test_btn = ctk.CTkButton(            self.mic_frame,            text=self._t(\"test_microphone\", \"\u0422\u0435\u0441\u0442 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430 (3 \u0441\u0435\u043a)\"),            command=self._on_test_mic,            height=40,            font=ctk.CTkFont(size=14),            fg_color=(\"gray60\", \"gray40\")        )        self.mic_test_btn.pack(pady=(10, 15), padx=15, fill=\"x\")                # ==========================================        # \u0421\u0415\u041a\u0426\u0418\u042f \u0414\u0418\u041d\u0410\u041c\u0418\u041a\u0410        # SPEAKER SECTION        # ==========================================        self.speaker_frame = ctk.CTkFrame(self, fg_color=(\"gray85\", \"gray20\"))        self.speaker_frame.pack(fill=\"x\", padx=20, pady=10)                self.speaker_label = ctk.CTkLabel(            self.speaker_frame,            text=self._t(\"speaker\", \"\u0414\u0438\u043d\u0430\u043c\u0438\u043a (\u0432\u044b\u0445\u043e\u0434)\"),            font=ctk.CTkFont(size=16, weight=\"bold\")        )        self.speaker_label.pack(pady=(15, 10), padx=15, anchor=\"w\")                # \u0412\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u043e\u0432        # Speaker dropdown        self.speaker_menu = ctk.CTkOptionMenu(            self.speaker_frame,            values=[\"...\"],            width=500,            height=40,            font=ctk.CTkFont(size=14)        )        self.speaker_menu.pack(pady=5, padx=15, fill=\"x\")                # \u041a\u043d\u043e\u043f\u043a\u0430 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430        # Speaker test button        self.speaker_test_btn = ctk.CTkButton(            self.speaker_frame,            text=self._t(\"test_speaker\", \"\u0422\u0435\u0441\u0442 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 (\u0442\u043e\u043d 440 \u0413\u0446)\"),            command=self._on_test_speaker,            height=40,            font=ctk.CTkFont(size=14),            fg_color=(\"gray60\", \"gray40\")        )        self.speaker_test_btn.pack(pady=(10, 15), padx=15, fill=\"x\")                # ==========================================        # \u0421\u0422\u0410\u0422\u0423\u0421 \u0418 \u041a\u041d\u041e\u041f\u041a\u0418 \u0414\u0415\u0419\u0421\u0422\u0412\u0418\u0419        # STATUS AND ACTION BUTTONS        # ==========================================        # \u041c\u0435\u0442\u043a\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430 (\u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f)        # Status label (shows test result)        self.status_label = ctk.CTkLabel(            self,            text=\"\",            font=ctk.CTkFont(size=14),            text_color=\"gray\"        )        self.status_label.pack(pady=10, padx=20)                # \u0424\u0440\u0435\u0439\u043c \u0434\u043b\u044f \u043a\u043d\u043e\u043f\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439        # Frame for action buttons        self.buttons_frame = ctk.CTkFrame(self, fg_color=\"transparent\")        self.buttons_frame.pack(fill=\"x\", padx=20, pady=10)                # \u041a\u043d\u043e\u043f\u043a\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432        # Device list refresh button        self.refresh_btn = ctk.CTkButton(            self.buttons_frame,            text=self._t(\"refresh_devices\", \"\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a\"),            command=self.refresh_devices,            height=40,            font=ctk.CTkFont(size=14),            fg_color=(\"gray60\", \"gray40\"),            width=180        )        self.refresh_btn.pack(side=\"left\", padx=5)                # \u041a\u043d\u043e\u043f\u043a\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a        # Save settings button        self.save_btn = ctk.CTkButton(            self.buttons_frame,            text=self._t(\"save\", \"\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c\"),            command=self._on_save,            height=40,            font=ctk.CTkFont(size=14),            width=180        )        self.save_btn.pack(side=\"right\", padx=5)        def refresh_devices(self):        \"\"\"        \u041f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0438 \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0432\u044b\u0431\u043e\u0440.        Reloads device list and sets current selection.        \"\"\"        # \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043e\u0442 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430        # Get device lists from manager        self.input_devices = self.audio_manager.get_input_devices()        self.output_devices = self.audio_manager.get_output_devices()                # \u0424\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0438 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u0445 \u043c\u0435\u043d\u044e        # Form name lists for dropdown menus        input_names = [name for _, name in self.input_devices]        output_names = [name for _, name in self.output_devices]                # \u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e, \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0443        # If no devices found, show placeholder        if not input_names:            input_names = [self._t(\"no_devices\", \"\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b\")]        if not output_names:            output_names = [self._t(\"no_devices\", \"\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b\")]                # \u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u0445 \u043c\u0435\u043d\u044e        # Update values in dropdown menus        self.mic_menu.configure(values=input_names)        self.speaker_menu.configure(values=output_names)                # ==========================================        # \u0423\u0421\u0422\u0410\u041d\u041e\u0412\u041a\u0410 \u0421\u041e\u0425\u0420\u0410\u041d\u0415\u041d\u041d\u042b\u0425 \u0417\u041d\u0410\u0427\u0415\u041d\u0418\u0419        # SET SAVED VALUES        # ==========================================        # \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0430 (\u0435\u0441\u043b\u0438 \u0431\u044b\u043b\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b)        # Get device IDs from config (if were saved)        saved_input_id = self.config.get(\"audio_input_id\")        saved_output_id = self.config.get(\"audio_output_id\")                # \u0412\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0439 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d        # Select saved microphone        if saved_input_id is not None:            saved_name = next(                (name for dev_id, name in self.input_devices if dev_id == saved_input_id),                None            )            if saved_name:                self.mic_menu.set(saved_name)            else:                self.mic_menu.set(input_names[0])        else:            # \u0415\u0441\u043b\u0438 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0435 \u043d\u0435\u0442 - \u0431\u0435\u0440\u0435\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e            # If not in config - take default device            default_id = self.audio_manager.get_default_input_id()            default_name = next(                (name for dev_id, name in self.input_devices if dev_id == default_id),                None            )            self.mic_menu.set(default_name if default_name else input_names[0])                # \u0412\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0439 \u0434\u0438\u043d\u0430\u043c\u0438\u043a        # Select saved speaker        if saved_output_id is not None:            saved_name = next(                (name for dev_id, name in self.output_devices if dev_id == saved_output_id),                None            )            if saved_name:                self.speaker_menu.set(saved_name)            else:                self.speaker_menu.set(output_names[0])        else:            default_id = self.audio_manager.get_default_output_id()            default_name = next(                (name for dev_id, name in self.output_devices if dev_id == default_id),                None            )            self.speaker_menu.set(default_name if default_name else output_names[0])                self._set_status(self._t(\"devices_loaded\", \"\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u044b\"), \"gray\")        def _get_selected_input_id(self) -&gt; int:        \"\"\"\u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 ID \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430. \/ Returns selected microphone ID.\"\"\"        current_name = self.mic_menu.get()        for dev_id, name in self.input_devices:            if name == current_name:                return dev_id        # \u0415\u0441\u043b\u0438 \u043d\u0435 \u043d\u0430\u0448\u043b\u0438 - \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u043f\u0435\u0440\u0432\u044b\u0439 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439        # If not found - return first available        return self.input_devices[0][0] if self.input_devices else 0        def _get_selected_output_id(self) -&gt; int:        \"\"\"\u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 ID \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430. \/ Returns selected speaker ID.\"\"\"        current_name = self.speaker_menu.get()        for dev_id, name in self.output_devices:            if name == current_name:                return dev_id        return self.output_devices[0][0] if self.output_devices else 0        def _on_test_mic(self):        \"\"\"\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043a\u043d\u043e\u043f\u043a\u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430. \/ Microphone test button handler.\"\"\"        input_id = self._get_selected_input_id()        output_id = self._get_selected_output_id()                # \u0411\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u043c \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0432\u0440\u0435\u043c\u044f \u0442\u0435\u0441\u0442\u0430        # Disable button during test        self.mic_test_btn.configure(state=\"disabled\")                def on_status(status: str):            # \u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0441\u0442\u0430\u0442\u0443\u0441 \u0432 \u0433\u043b\u0430\u0432\u043d\u043e\u043c \u043f\u043e\u0442\u043e\u043a\u0435 GUI            # Update status in main GUI thread            if status == \"recording\":                self._set_status(                    self._t(\"status_recording\", \"\u0417\u0430\u043f\u0438\u0441\u044c 3 \u0441\u0435\u043a\u0443\u043d\u0434\u044b... \u0413\u043e\u0432\u043e\u0440\u0438\u0442\u0435 \u0432 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\"),                    \"orange\"                )            elif status == \"playing\":                self._set_status(                    self._t(\"status_playing\", \"\u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u0438...\"),                    \"orange\"                )            elif status == \"done\":                self._set_status(                    self._t(\"status_mic_ok\", \"\u041c\u0438\u043a\u0440\u043e\u0444\u043e\u043d \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442! \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u043b\u0438 \u0443\u0441\u043b\u044b\u0448\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0437\u0430\u043f\u0438\u0441\u044c.\"),                    \"green\"                )                # \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u0440\u0430\u0431\u043e\u0447\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043f\u043e\u0442\u043e\u043a                # Re-enable button via main thread                self.after(0, lambda: self.mic_test_btn.configure(state=\"normal\"))            elif status.startswith(\"error\"):                self._set_status(                    f\"{self._t('status_error', '\u041e\u0448\u0438\u0431\u043a\u0430')}: {status}\",                    \"red\"                )                self.after(0, lambda: self.mic_test_btn.configure(state=\"normal\"))                # \u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0442\u0435\u0441\u0442 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c \u043f\u043e\u0442\u043e\u043a\u0435        # Run test in separate thread        self.audio_manager.test_microphone(            input_device_id=input_id,            output_device_id=output_id,            on_status=on_status        )        def _on_test_speaker(self):        \"\"\"\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043a\u043d\u043e\u043f\u043a\u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430. \/ Speaker test button handler.\"\"\"        output_id = self._get_selected_output_id()                self.speaker_test_btn.configure(state=\"disabled\")                def on_status(status: str):            if status == \"playing\":                self._set_status(                    self._t(\"status_tone\", \"\u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0442\u043e\u043d\u0430...\"),                    \"orange\"                )            elif status == \"done\":                self._set_status(                    self._t(\"status_speaker_ok\", \"\u0414\u0438\u043d\u0430\u043c\u0438\u043a \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442! \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u043b\u0438 \u0443\u0441\u043b\u044b\u0448\u0430\u0442\u044c \u0442\u043e\u043d.\"),                    \"green\"                )                self.after(0, lambda: self.speaker_test_btn.configure(state=\"normal\"))            elif status.startswith(\"error\"):                self._set_status(                    f\"{self._t('status_error', '\u041e\u0448\u0438\u0431\u043a\u0430')}: {status}\",                    \"red\"                )                self.after(0, lambda: self.speaker_test_btn.configure(state=\"normal\"))                self.audio_manager.test_speaker(            output_device_id=output_id,            on_status=on_status        )        def _on_save(self):        \"\"\"\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043a\u043d\u043e\u043f\u043a\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f. \/ Save button handler.\"\"\"        # \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c ID \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432        # Get IDs of selected devices        input_id = self._get_selected_input_id()        output_id = self._get_selected_output_id()        input_name = self.mic_menu.get()        output_name = self.speaker_menu.get()                # \u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u043c \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e        # Save to configuration        self.config[\"audio_input_id\"] = input_id        self.config[\"audio_output_id\"] = output_id        self.config[\"audio_input_name\"] = input_name        self.config[\"audio_output_name\"] = output_name        self.save_config(self.config)                self._set_status(self._t(\"settings_saved\", \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b!\"), \"green\")        def _set_status(self, text: str, color: str):        \"\"\"        \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0442\u0435\u043a\u0441\u0442 \u0438 \u0446\u0432\u0435\u0442 \u043c\u0435\u0442\u043a\u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u0430.        Sets status label text and color.        \u0412\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 self.after() \u0434\u043b\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0438\u0437 \u043f\u043e\u0442\u043e\u043a\u043e\u0432.        Called via self.after() for thread safety.        \"\"\"        def _update():            self.status_label.configure(text=text, text_color=color)        self.after(0, _update)        def update_language(self):        \"\"\"        \u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0432\u0441\u0435 \u043d\u0430\u0434\u043f\u0438\u0441\u0438 \u043f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435 \u044f\u0437\u044b\u043a\u0430.        Updates all labels on language change.        \"\"\"        self.title_label.configure(text=self._t(\"audio_header\", \"\u0410\u0443\u0434\u0438\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\"))        self.subtitle_label.configure(text=self._t(\"audio_subtitle\", \"\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a \u0434\u043b\u044f \u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u0430\"))        self.mic_label.configure(text=self._t(\"microphone\", \"\u041c\u0438\u043a\u0440\u043e\u0444\u043e\u043d (\u0432\u0445\u043e\u0434)\"))        self.speaker_label.configure(text=self._t(\"speaker\", \"\u0414\u0438\u043d\u0430\u043c\u0438\u043a (\u0432\u044b\u0445\u043e\u0434)\"))        self.mic_test_btn.configure(text=self._t(\"test_microphone\", \"\u0422\u0435\u0441\u0442 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430 (3 \u0441\u0435\u043a)\"))        self.speaker_test_btn.configure(text=self._t(\"test_speaker\", \"\u0422\u0435\u0441\u0442 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 (\u0442\u043e\u043d 440 \u0413\u0446)\"))        self.refresh_btn.configure(text=self._t(\"refresh_devices\", \"\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a\"))        self.save_btn.configure(text=self._t(\"save\", \"\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c\"))EOF<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<\/details>\n<h2>\u041a\u0430\u043a \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0443\u0441\u043f\u0435\u0445 \u0428\u0430\u0433\u0430 4 <\/h2>\n<p><strong>\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u044b<\/strong> <\/p>\n<pre><code>cd ~\/Zahar &amp;&amp; source venv\/bin\/activatepip list | grep -E \"sounddevice|numpy\"<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0414\u043e\u043b\u0436\u043d\u044b \u0443\u0432\u0438\u0434\u0435\u0442\u044c <code>sounddevice<\/code> \u0438 <code>numpy<\/code> \u0432 \u0441\u043f\u0438\u0441\u043a\u0435 <\/p>\n<p><strong>\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u043e\u0432 <\/strong><\/p>\n<pre><code>ls -l ~\/Zahar\/core\/audio_manager.py ~\/Zahar\/ui\/audio_tab.py<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041e\u0431\u0430 \u0444\u0430\u0439\u043b\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c. <\/p>\n<h2>4.3. \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u00ab\u0410\u0443\u0434\u0438\u043e\u00bb \u0432 main.py<\/h2>\n<p>\u041f\u0440\u043e\u0441\u0442\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u044d\u0442\u0443 \u043a\u043e\u043c\u0430\u043d\u0434\u0443, \u0438 <a href=\"http:\/\/main.py\" rel=\"noopener noreferrer nofollow\"><code>main.py<\/code><\/a> \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0448\u0435\u0442\u0441\u044f \u0441 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u043e\u0439 \u043d\u043e\u0432\u043e\u0439 \u0432\u043a\u043b\u0430\u0434\u043a\u0438: <\/p>\n<details class=\"spoiler\">\n<summary>\u0421\u043a\u0440\u044b\u0442\u044b\u0439 \u0442\u0435\u043a\u0441\u0442<\/summary>\n<div class=\"spoiler__content\">\n<pre><code>cat &gt; ~\/Zahar\/main.py &lt;&lt; 'EOF'# ============================================================# \u0413\u043b\u0430\u0432\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f ZAHAR# Main application file ZAHAR# ============================================================# \u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442 \u0441 \u043c\u043e\u0434\u0443\u043b\u044c\u043d\u043e\u0439 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u043e\u0439.# Local assistant with modular architecture.# \u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \/ Import librariesimport customtkinter as ctkimport osimport sysimport atexit# \u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043a\u043e\u0440\u0435\u043d\u044c \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432 \u043f\u0443\u0442\u044c \u0434\u043b\u044f \u0438\u043c\u043f\u043e\u0440\u0442\u0430 \/ Add project root to pathsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))from core.password_manager import PasswordManagerfrom ui.dashboard_tab import DashboardTabfrom ui.audio_tab import AudioTab# ==========================================# \u041d\u0410\u0421\u0422\u0420\u041e\u0419\u041a\u0418 \u0422\u0415\u041c\u042b \/ APPEARANCE SETTINGS# ==========================================ctk.set_appearance_mode(\"dark\")ctk.set_default_color_theme(\"blue\")# ==========================================# \u0421\u0418\u0421\u0422\u0415\u041c\u0410 \u041b\u041e\u041a\u0410\u041b\u0418\u0417\u0410\u0426\u0418\u0418 \/ LOCALIZATION SYSTEM# ==========================================TRANSLATIONS = {    \"ru\": {        \"window_title\": \"ZAHAR - \u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442\",        \"sidebar_title\": \"ZAHAR\",        \"lang_btn_enter\": \"\u0412\u0432\u0435\u0441\u0442\u0438 \u043f\u0430\u0440\u043e\u043b\u044c\",        \"lang_btn_clear\": \"\u041e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u043a\u044d\u0448\",        \"pwd_ok\": \"[OK] \u041f\u0430\u0440\u043e\u043b\u044c \u0437\u0430\u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\",        \"pwd_req\": \"[!] \u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0432\u043e\u0434\",        \"timer_prefix\": \"\u041a\u044d\u0448:\",                # === \u041a\u043d\u043e\u043f\u043a\u0438 \u043c\u0435\u043d\u044e \/ Menu buttons ===        \"btn_dashboard\": \"\u0414\u0430\u0448\u0431\u043e\u0440\u0434\",        \"btn_audio\": \"\u0410\u0443\u0434\u0438\u043e\",        \"btn_clone\": \"\u041a\u043b\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0414\u0438\u0441\u043a\u0438\",        \"btn_display\": \"\u0414\u0438\u0441\u043f\u043b\u0435\u0439\",        \"btn_stt\": \"\u0420\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u0435 (STT)\",        \"btn_tts\": \"\u0421\u0438\u043d\u0442\u0435\u0437 \u0440\u0435\u0447\u0438 (TTS)\",        \"btn_llm\": \"LLM \u041c\u043e\u0434\u0435\u043b\u044c\",        \"btn_structure\": \"\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430\",        \"btn_ha\": \"Home Assistant\",                # === \u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0438 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430 \/ Content placeholders ===        \"content_clone\": \"\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0438\u0441\u043a\u0430\u043c\u0438 \u0438 \u043a\u043b\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\\n(\u041c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u0424\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u041a\u043b\u043e\u043d)\",        \"content_display\": \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0414\u0438\u0441\u043f\u043b\u0435\u044f\\n(\u042d\u043a\u0440\u0430\u043d\u043d\u0430\u044f \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u0430, \u042f\u0440\u043a\u043e\u0441\u0442\u044c)\",        \"content_stt\": \"\u0412\u044b\u0431\u043e\u0440 \u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043c\u043e\u0434\u0435\u043b\u0438 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u044f \u0440\u0435\u0447\u0438\\n(Whisper \/ Vosk)\",        \"content_tts\": \"\u0412\u044b\u0431\u043e\u0440 \u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0433\u043e\u043b\u043e\u0441\u0430\\n(Piper Voices)\",        \"content_llm\": \"\u0412\u044b\u0431\u043e\u0440 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u044f\u0437\u044b\u043a\u043e\u0432\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438\\n(Ollama \/ Llama.cpp)\",        \"content_structure\": \"\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434\",        \"content_ha\": \"\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Home Assistant\\n(\u041e\u043f\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e)\",                # === \u041a\u043b\u044e\u0447\u0438 \u0434\u043b\u044f \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u0414\u0430\u0448\u0431\u043e\u0440\u0434 \/ Keys for Dashboard tab ===        \"dashboard_header\": \"\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438\",        \"dashboard_subtitle\": \"\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438\",        \"cpu_temp\": \"\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 CPU\",        \"ram\": \"\u041e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u0430\u044f \u043f\u0430\u043c\u044f\u0442\u044c\",        \"disk\": \"\u0414\u0438\u0441\u043a (\/)\",        \"network\": \"\u0421\u0435\u0442\u044c\",        \"uptime\": \"\u0412\u0440\u0435\u043c\u044f \u0440\u0430\u0431\u043e\u0442\u044b\",        \"status\": \"\u0421\u0442\u0430\u0442\u0443\u0441\",                # === \u041a\u043b\u044e\u0447\u0438 \u0434\u043b\u044f \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u0410\u0443\u0434\u0438\u043e \/ Keys for Audio tab ===        \"audio_header\": \"\u0410\u0443\u0434\u0438\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\",        \"audio_subtitle\": \"\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a \u0434\u043b\u044f \u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u0430\",        \"microphone\": \"\u041c\u0438\u043a\u0440\u043e\u0444\u043e\u043d (\u0432\u0445\u043e\u0434)\",        \"speaker\": \"\u0414\u0438\u043d\u0430\u043c\u0438\u043a (\u0432\u044b\u0445\u043e\u0434)\",        \"test_microphone\": \"\u0422\u0435\u0441\u0442 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430 (3 \u0441\u0435\u043a)\",        \"test_speaker\": \"\u0422\u0435\u0441\u0442 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 (\u0442\u043e\u043d 440 \u0413\u0446)\",        \"refresh_devices\": \"\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a\",        \"save\": \"\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c\",        \"no_devices\": \"\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b\",        \"devices_loaded\": \"\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u044b\",        \"status_recording\": \"\u0417\u0430\u043f\u0438\u0441\u044c 3 \u0441\u0435\u043a\u0443\u043d\u0434\u044b... \u0413\u043e\u0432\u043e\u0440\u0438\u0442\u0435 \u0432 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\",        \"status_playing\": \"\u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u0438...\",        \"status_mic_ok\": \"\u041c\u0438\u043a\u0440\u043e\u0444\u043e\u043d \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442! \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u043b\u0438 \u0443\u0441\u043b\u044b\u0448\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0437\u0430\u043f\u0438\u0441\u044c.\",        \"status_tone\": \"\u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0442\u043e\u043d\u0430...\",        \"status_speaker_ok\": \"\u0414\u0438\u043d\u0430\u043c\u0438\u043a \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442! \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u043b\u0438 \u0443\u0441\u043b\u044b\u0448\u0430\u0442\u044c \u0442\u043e\u043d.\",        \"status_error\": \"\u041e\u0448\u0438\u0431\u043a\u0430\",        \"settings_saved\": \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b!\"    },    \"en\": {        \"window_title\": \"ZAHAR - Local Assistant\",        \"sidebar_title\": \"ZAHAR\",        \"lang_btn_enter\": \"Enter Password\",        \"lang_btn_clear\": \"Clear Cache\",        \"pwd_ok\": \"[OK] Password cached\",        \"pwd_req\": \"[!] Input required\",        \"timer_prefix\": \"Cache:\",                \"btn_dashboard\": \"Dashboard\",        \"btn_audio\": \"Audio\",        \"btn_clone\": \"Clone &amp; Disks\",        \"btn_display\": \"Display\",        \"btn_stt\": \"Speech Recognition (STT)\",        \"btn_tts\": \"Speech Synthesis (TTS)\",        \"btn_llm\": \"LLM Model\",        \"btn_structure\": \"Structure\",        \"btn_ha\": \"Home Assistant\",                \"content_clone\": \"Disk Management and Cloning\\n(Mount, Format, Clone)\",        \"content_display\": \"Display Settings\\n(On-screen keyboard, Brightness)\",        \"content_stt\": \"Speech recognition model selection and test\\n(Whisper \/ Vosk)\",        \"content_tts\": \"Voice selection and test\\n(Piper Voices)\",        \"content_llm\": \"Local language model selection\\n(Ollama \/ Llama.cpp)\",        \"content_structure\": \"Project structure and source code\",        \"content_ha\": \"Home Assistant Integration\\n(Optional)\",                # === Keys for Dashboard tab ===        \"dashboard_header\": \"System Metrics\",        \"dashboard_subtitle\": \"Real-time monitoring\",        \"cpu_temp\": \"CPU Temperature\",        \"ram\": \"Random Access Memory\",        \"disk\": \"Disk (\/)\",        \"network\": \"Network\",        \"uptime\": \"Uptime\",        \"status\": \"Status\",                # === Keys for Audio tab ===        \"audio_header\": \"Audio Devices\",        \"audio_subtitle\": \"Select microphone and speaker for the assistant\",        \"microphone\": \"Microphone (input)\",        \"speaker\": \"Speaker (output)\",        \"test_microphone\": \"Test microphone (3 sec)\",        \"test_speaker\": \"Test speaker (440 Hz tone)\",        \"refresh_devices\": \"Refresh list\",        \"save\": \"Save\",        \"no_devices\": \"No devices found\",        \"devices_loaded\": \"Devices loaded\",        \"status_recording\": \"Recording 3 seconds... Speak into microphone\",        \"status_playing\": \"Playing back recording...\",        \"status_mic_ok\": \"Microphone works! You should have heard your recording.\",        \"status_tone\": \"Playing test tone...\",        \"status_speaker_ok\": \"Speaker works! You should have heard the tone.\",        \"status_error\": \"Error\",        \"settings_saved\": \"Settings saved!\"    }}CONFIG_FILE = os.path.join(os.path.dirname(__file__), \"config.json\")def load_config():    \"\"\"\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \/ Loads configuration from file\"\"\"    import json    if os.path.exists(CONFIG_FILE):        try:            with open(CONFIG_FILE, 'r', encoding='utf-8') as f:                return json.load(f)        except Exception:            return {}    return {}def save_config(config):    \"\"\"\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0432 \u0444\u0430\u0439\u043b \/ Saves configuration to file\"\"\"    import json    try:        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:            json.dump(config, f, ensure_ascii=False, indent=2)    except Exception as e:        print(f\"Config save error: {e}\")class ZaharApp(ctk.CTk):    \"\"\"    \u0413\u043b\u0430\u0432\u043d\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f ZAHAR.    Main class of ZAHAR application.    \"\"\"        def __init__(self):        super().__init__()        # \u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430 \u043f\u0430\u0440\u043e\u043b\u0435\u0439        # Load config and initialize password manager        self.config = load_config()        self.current_lang = self.config.get(\"language\", \"ru\")        self.pwd_manager = PasswordManager(self)                # \u0413\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043e\u0447\u0438\u0441\u0442\u043a\u0430 \u043a\u044d\u0448\u0430 \u043f\u0440\u0438 \u0430\u0432\u0430\u0440\u0438\u0439\u043d\u043e\u043c \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0438 (Ctrl+C \u0432 \u0442\u0435\u0440\u043c\u0438\u043d\u0430\u043b\u0435)        # Guaranteed cache clear on abrupt termination (Ctrl+C in terminal)        atexit.register(self.pwd_manager.clear)        # ==========================================        # \u0411\u0410\u0417\u041e\u0412\u042b\u0415 \u041d\u0410\u0421\u0422\u0420\u041e\u0419\u041a\u0418 \u041e\u041a\u041d\u0410 \/ BASIC WINDOW SETTINGS        # ==========================================        self.title(TRANSLATIONS[self.current_lang][\"window_title\"])        self.geometry(\"950x650\")        self.minsize(850, 550)                # \u0421\u0435\u0442\u043a\u0430: 2 \u0441\u0442\u0440\u043e\u043a\u0438 (\u0432\u0435\u0440\u0445\u043d\u044f\u044f \u043f\u0430\u043d\u0435\u043b\u044c \u0438 \u043a\u043e\u043d\u0442\u0435\u043d\u0442), 2 \u043a\u043e\u043b\u043e\u043d\u043a\u0438 (\u0441\u0430\u0439\u0434\u0431\u0430\u0440 \u0438 \u043a\u043e\u043d\u0442\u0435\u043d\u0442)        # Grid: 2 rows (top panel and content), 2 columns (sidebar and content)        self.grid_rowconfigure(0, weight=0) # \u0412\u0435\u0440\u0445\u043d\u044f\u044f \u043f\u0430\u043d\u0435\u043b\u044c \u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \/ Top panel is fixed        self.grid_rowconfigure(1, weight=1) # \u041a\u043e\u043d\u0442\u0435\u043d\u0442 \u0440\u0430\u0441\u0442\u044f\u0433\u0438\u0432\u0430\u0435\u0442\u0441\u044f \/ Content stretches        self.grid_columnconfigure(0, weight=0) # \u0421\u0430\u0439\u0434\u0431\u0430\u0440 \u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d \/ Sidebar is fixed        self.grid_columnconfigure(1, weight=1) # \u041a\u043e\u043d\u0442\u0435\u043d\u0442 \u0440\u0430\u0441\u0442\u044f\u0433\u0438\u0432\u0430\u0435\u0442\u0441\u044f \/ Content stretches        # ==========================================        # \u0412\u0415\u0420\u0425\u041d\u042f\u042f \u041f\u0410\u041d\u0415\u041b\u042c (HEADER) \/ TOP PANEL (HEADER)        # ==========================================        self.header_frame = ctk.CTkFrame(            self, height=50, corner_radius=0,             fg_color=(\"gray85\", \"gray15\")        )        self.header_frame.grid(row=0, column=0, columnspan=2, sticky=\"ew\")        # 1. \u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u044f\u0437\u044b\u043a\u0430 \/ Language switcher        ctk.CTkLabel(            self.header_frame, text=\"Lang:\",             font=ctk.CTkFont(size=12)        ).pack(side=\"left\", padx=(15, 5), pady=10)                self.lang_switcher = ctk.CTkSegmentedButton(            self.header_frame, values=[\"RU\", \"EN\"],             command=self._switch_lang,             font=ctk.CTkFont(size=11), width=80        )        self.lang_switcher.set(self.current_lang.upper())        self.lang_switcher.pack(side=\"left\", padx=5, pady=10)        ctk.CTkLabel(            self.header_frame, text=\"|\",             font=ctk.CTkFont(size=12), text_color=\"gray\"        ).pack(side=\"left\", padx=10, pady=10)        # 2. \u041a\u043d\u043e\u043f\u043a\u0430 \u0432\u0432\u043e\u0434\u0430 \u043f\u0430\u0440\u043e\u043b\u044f \/ Enter password button        self.btn_enter_pwd = ctk.CTkButton(            self.header_frame,             text=TRANSLATIONS[self.current_lang][\"lang_btn_enter\"],             command=self._enter_pwd,             width=130, height=30, font=ctk.CTkFont(size=12)        )        self.btn_enter_pwd.pack(side=\"left\", padx=5, pady=10)        # 3. \u041a\u043d\u043e\u043f\u043a\u0430 \u043e\u0447\u0438\u0441\u0442\u043a\u0438 \u043a\u044d\u0448\u0430 \/ Clear cache button        self.btn_clear_pwd = ctk.CTkButton(            self.header_frame,             text=TRANSLATIONS[self.current_lang][\"lang_btn_clear\"],             command=self._clear_pwd,             width=130, height=30, font=ctk.CTkFont(size=12),            state=\"disabled\", fg_color=\"gray\"        )        self.btn_clear_pwd.pack(side=\"left\", padx=5, pady=10)        # 4. \u0422\u0410\u0419\u041c\u0415\u0420 \u041e\u0411\u0420\u0410\u0422\u041d\u041e\u0413\u041e \u041e\u0422\u0421\u0427\u0415\u0422\u0410 (\u0412\u0421\u0415\u0413\u0414\u0410 \u0412\u0418\u0414\u0415\u041d) \/ COUNTDOWN TIMER (ALWAYS VISIBLE)        self.lbl_timer = ctk.CTkLabel(            self.header_frame,             text=f\"{TRANSLATIONS[self.current_lang]['timer_prefix']} 00:00\",             font=ctk.CTkFont(size=12, weight=\"bold\"),            text_color=\"gray\"        )        self.lbl_timer.pack(side=\"left\", padx=20, pady=10)        # 5. \u0421\u0442\u0430\u0442\u0443\u0441 \u043f\u0430\u0440\u043e\u043b\u044f (\u0441\u043f\u0440\u0430\u0432\u0430) \/ Password status (right)        self.lbl_pwd_status = ctk.CTkLabel(            self.header_frame, text=\"\",             font=ctk.CTkFont(size=12, weight=\"bold\")        )        self.lbl_pwd_status.pack(side=\"right\", padx=20, pady=10)                # \u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u0432 \/ Initialize statuses        self._update_pwd_status()        self._update_timer_loop() # \u0417\u0430\u043f\u0443\u0441\u043a \u0446\u0438\u043a\u043b\u0430 \u0442\u0430\u0439\u043c\u0435\u0440\u0430 \/ Start timer loop        # ==========================================        # \u0411\u041e\u041a\u041e\u0412\u0410\u042f \u041f\u0410\u041d\u0415\u041b\u042c \/ SIDEBAR        # ==========================================        self.sidebar_frame = ctk.CTkFrame(            self, width=220, corner_radius=0,             fg_color=(\"gray90\", \"gray10\")        )        self.sidebar_frame.grid(row=1, column=0, sticky=\"nsew\")        self.sidebar_frame.grid_columnconfigure(0, weight=1)        # \u041b\u043e\u0433\u043e\u0442\u0438\u043f \/ Logo        self.logo_label = ctk.CTkLabel(            self.sidebar_frame,             text=TRANSLATIONS[self.current_lang][\"sidebar_title\"],             font=ctk.CTkFont(size=26, weight=\"bold\")        )        self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))        # \u041f\u0420\u041e\u041a\u0420\u0423\u0427\u0418\u0412\u0410\u0415\u041c\u042b\u0419 \u041a\u041e\u041d\u0422\u0415\u0419\u041d\u0415\u0420 \u0414\u041b\u042f \u041a\u041d\u041e\u041f\u041e\u041a \/ SCROLLABLE FRAME FOR BUTTONS        self.scrollable_frame = ctk.CTkScrollableFrame(            self.sidebar_frame, width=190, fg_color=\"transparent\"        )        self.scrollable_frame.grid(row=1, column=0, sticky=\"nsew\", padx=10, pady=10)        self.sidebar_frame.grid_rowconfigure(1, weight=1)        self.scrollable_frame.grid_columnconfigure(0, weight=1)        # \u0421\u043f\u0438\u0441\u043e\u043a \u043a\u043d\u043e\u043f\u043e\u043a \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \/ Navigation buttons list        nav_items = [            (\"btn_dashboard\", 0, self.show_dashboard),            (\"btn_audio\", 1, self.show_audio),            (\"btn_clone\", 2, self.show_clone),            (\"btn_display\", 3, self.show_display),            (\"btn_stt\", 4, self.show_stt),            (\"btn_tts\", 5, self.show_tts),            (\"btn_llm\", 6, self.show_llm),            (\"btn_structure\", 7, self.show_structure),            (\"btn_ha\", 8, self.show_ha)        ]        self.nav_btns = {}        for key, row, cmd in nav_items:            is_optional = (key == \"btn_ha\")            btn = ctk.CTkButton(                self.scrollable_frame,                 text=TRANSLATIONS[self.current_lang][key],                 command=cmd,                 height=38, width=170, font=ctk.CTkFont(size=13),                fg_color=(\"gray60\", \"gray30\") if is_optional else None,                hover_color=(\"gray50\", \"gray40\") if is_optional else None            )            btn.grid(row=row, column=0, pady=3, padx=5, sticky=\"ew\")            self.nav_btns[key] = btn        # ==========================================        # \u041e\u0421\u041d\u041e\u0412\u041d\u0410\u042f \u041e\u0411\u041b\u0410\u0421\u0422\u042c \u041a\u041e\u041d\u0422\u0415\u041d\u0422\u0410 \/ MAIN CONTENT AREA        # ==========================================        self.content_frame = ctk.CTkFrame(            self, corner_radius=0, fg_color=\"transparent\"        )        self.content_frame.grid(row=1, column=1, sticky=\"nsew\")        # ==========================================        # \u0421\u041e\u0417\u0414\u0410\u041d\u0418\u0415 \u0412\u041a\u041b\u0410\u0414\u041e\u041a \/ CREATING TABS        # ==========================================        # \u0412\u043a\u043b\u0430\u0434\u043a\u0430 \u0434\u0430\u0448\u0431\u043e\u0440\u0434\u0430 (\u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u0434\u0438\u043d \u0440\u0430\u0437, \u043f\u043e\u0442\u043e\u043c \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f\/\u0441\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f)        # Dashboard tab (created once, then just shown\/hidden)        self.dashboard_tab = DashboardTab(            parent=self.content_frame,            translations=TRANSLATIONS,            get_lang=lambda: self.current_lang        )                # \u0412\u043a\u043b\u0430\u0434\u043a\u0430 \u0430\u0443\u0434\u0438\u043e (\u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u0434\u0438\u043d \u0440\u0430\u0437)        # Audio tab (created once)        # \u041f\u0435\u0440\u0435\u0434\u0430\u0435\u043c config \u0438 save_config \u0434\u043b\u044f \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0432\u044b\u0431\u043e\u0440\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432        # Pass config and save_config to save device selection        self.audio_tab = AudioTab(            parent=self.content_frame,            translations=TRANSLATIONS,            get_lang=lambda: self.current_lang,            config=self.config,            save_config=save_config        )                # \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f \u043e\u043a\u043d\u0430 \u0434\u043b\u044f \u043e\u0447\u0438\u0441\u0442\u043a\u0438 \u043a\u044d\u0448\u0430        # Window close handler to clear cache        self.protocol(\"WM_DELETE_WINDOW\", self.on_close)        # \u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \/ Show initial page        self.current_tab = \"dashboard\"        self.show_dashboard()    # ==========================================    # \u0423\u041f\u0420\u0410\u0412\u041b\u0415\u041d\u0418\u0415 \u041f\u0410\u0420\u041e\u041b\u042f\u041c\u0418 \u0418 \u0422\u0410\u0419\u041c\u0415\u0420\u041e\u041c \/ PASSWORD &amp; TIMER MANAGEMENT    # ==========================================    def _update_timer_loop(self):        \"\"\"\u0426\u0438\u043a\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0442\u0430\u0439\u043c\u0435\u0440\u0430 \u043a\u0430\u0436\u0434\u0443\u044e \u0441\u0435\u043a\u0443\u043d\u0434\u0443 \/ Timer update loop every second\"\"\"        remaining = self.pwd_manager.get_remaining_time()        prefix = TRANSLATIONS[self.current_lang]['timer_prefix']                if remaining &gt; 0:            mins = remaining \/\/ 60            secs = remaining % 60            time_str = f\"{mins:02d}:{secs:02d}\"            self.lbl_timer.configure(                text=f\"{prefix} {time_str}\",                text_color=\"#4caf50\" # \u0417\u0435\u043b\u0435\u043d\u044b\u0439 \u043f\u0440\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u043a\u044d\u0448\u0435 \/ Green when cached            )            # \u041f\u043b\u0430\u043d\u0438\u0440\u0443\u0435\u043c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0432\u044b\u0437\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 1 \u0441\u0435\u043a\u0443\u043d\u0434\u0443            # Schedule next call in 1 second            self.after(1000, self._update_timer_loop)        else:            # \u0412\u0440\u0435\u043c\u044f \u0432\u044b\u0448\u043b\u043e \u0438\u043b\u0438 \u043a\u044d\u0448 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u0435\u043d. \u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c 00:00 \u0441\u0435\u0440\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c.            # Time is up or cache inactive. Show 00:00 in gray.            self.lbl_timer.configure(text=f\"{prefix} 00:00\", text_color=\"gray\")            self._update_pwd_status()            # \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u043c \u0446\u0438\u043a\u043b, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0439 \u043d\u043e\u0432\u044b\u0439 \u0432\u0432\u043e\u0434 \u043f\u0430\u0440\u043e\u043b\u044f            # Continue loop to track possible new password entry            self.after(1000, self._update_timer_loop)    def _update_pwd_status(self):        \"\"\"\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0442\u0435\u043a\u0441\u0442 \u0438 \u0446\u0432\u0435\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u043f\u0430\u0440\u043e\u043b\u044f \/ Updates password status text and color\"\"\"        self.pwd_manager._check_cache()        if self.pwd_manager.is_cached:            txt = TRANSLATIONS[self.current_lang][\"pwd_ok\"]            col = \"#4caf50\"            self.btn_clear_pwd.configure(state=\"normal\", fg_color=(\"#3a7ebf\", \"#1f538d\"))        else:            txt = TRANSLATIONS[self.current_lang][\"pwd_req\"]            col = \"#ff9800\"            self.btn_clear_pwd.configure(state=\"disabled\", fg_color=\"gray\")                self.lbl_pwd_status.configure(text=txt, text_color=col)    def _enter_pwd(self):        \"\"\"\u0412\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u0434\u0438\u0430\u043b\u043e\u0433 \u0432\u0432\u043e\u0434\u0430 \u043f\u0430\u0440\u043e\u043b\u044f \/ Calls password input dialog\"\"\"        self.pwd_manager.ensure_password(on_success=self._update_pwd_status)    def _clear_pwd(self):        \"\"\"\u041e\u0447\u0438\u0449\u0430\u0435\u0442 \u043a\u044d\u0448 \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 UI \/ Clears cache and updates UI\"\"\"        self.pwd_manager.clear()        self._update_pwd_status()    def on_close(self):        \"\"\"\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \/ Application close handler\"\"\"        # \u041a\u0420\u0418\u0422\u0418\u0427\u0415\u0421\u041a\u0418 \u0412\u0410\u0416\u041d\u041e: \u041e\u0447\u0438\u0449\u0430\u0435\u043c \u043a\u044d\u0448 sudo \u043f\u0440\u0438 \u043b\u044e\u0431\u043e\u043c \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u0438        # CRITICAL: Clear sudo cache on any close        self.pwd_manager.clear()        self.destroy()    # ==========================================    # \u0423\u041f\u0420\u0410\u0412\u041b\u0415\u041d\u0418\u0415 \u0418\u041d\u0422\u0415\u0420\u0424\u0415\u0419\u0421\u041e\u041c \/ INTERFACE MANAGEMENT    # ==========================================    def _switch_lang(self, lang_code):        \"\"\"\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u044f\u0437\u044b\u043a \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \/ Switches interface language\"\"\"        new_lang = \"ru\" if lang_code == \"RU\" else \"en\"        if new_lang != self.current_lang:            self.current_lang = new_lang            self.config[\"language\"] = new_lang            save_config(self.config)                        # \u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0438 \u043a\u043d\u043e\u043f\u043a\u0438 \/ Update title and buttons            self.title(TRANSLATIONS[self.current_lang][\"window_title\"])            self.logo_label.configure(text=TRANSLATIONS[self.current_lang][\"sidebar_title\"])            self.btn_enter_pwd.configure(text=TRANSLATIONS[self.current_lang][\"lang_btn_enter\"])            self.btn_clear_pwd.configure(text=TRANSLATIONS[self.current_lang][\"lang_btn_clear\"])                        for key, btn in self.nav_btns.items():                btn.configure(text=TRANSLATIONS[self.current_lang][key])                        # \u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0441\u0442\u0430\u0442\u0443\u0441 \u043f\u0430\u0440\u043e\u043b\u044f (\u0442\u0435\u043a\u0441\u0442 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u0441\u044f)            # Update password status (text will change)            self._update_pwd_status()                        # \u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u044f\u0437\u044b\u043a \u043d\u0430 \u0432\u0441\u0435\u0445 \u0432\u043a\u043b\u0430\u0434\u043a\u0430\u0445            # Update language on all tabs            self.dashboard_tab.update_language()            self.audio_tab.update_language()                        # \u041f\u0435\u0440\u0435\u0440\u0438\u0441\u043e\u0432\u044b\u0432\u0430\u0435\u043c \u0442\u0435\u043a\u0443\u0449\u0443\u044e \u0432\u043a\u043b\u0430\u0434\u043a\u0443            # Redraw current tab            getattr(self, f\"show_{self.current_tab}\")()    def _hide_all_tabs(self):        \"\"\"        \u0421\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0432\u0441\u0435 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u043f\u0435\u0440\u0435\u0434 \u043f\u043e\u043a\u0430\u0437\u043e\u043c \u043d\u043e\u0432\u043e\u0439.        Hides all tabs before showing new one.        \"\"\"        # \u0421\u043a\u0440\u044b\u0432\u0430\u0435\u043c \u0433\u043e\u0442\u043e\u0432\u044b\u0435 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 (\u043d\u0435 \u0443\u0434\u0430\u043b\u044f\u0435\u043c, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u043d\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u044b \u0437\u0430\u0440\u0430\u043d\u0435\u0435)        # Hide ready tabs (don't destroy, as they are created in advance)        self.dashboard_tab.pack_forget()        self.audio_tab.pack_forget()                # \u0423\u0434\u0430\u043b\u044f\u0435\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0438 \u0434\u043b\u044f \u0434\u0440\u0443\u0433\u0438\u0445 \u0432\u043a\u043b\u0430\u0434\u043e\u043a        # Remove temporary placeholders for other tabs        for widget in self.content_frame.winfo_children():            if widget not in (self.dashboard_tab, self.audio_tab):                widget.destroy()    def _show_placeholder(self, tab_name, content_key):        \"\"\"\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0443 \u0434\u043b\u044f \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \/ Shows placeholder for tab\"\"\"        self.current_tab = tab_name        self._hide_all_tabs()                lbl = ctk.CTkLabel(            self.content_frame,             text=TRANSLATIONS[self.current_lang][content_key],             font=ctk.CTkFont(size=20), justify=\"center\"        )        lbl.pack(expand=True)    def show_dashboard(self):        \"\"\"\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0432\u043a\u043b\u0430\u0434\u043a\u0443 \u0434\u0430\u0448\u0431\u043e\u0440\u0434\u0430. \/ Shows dashboard tab.\"\"\"        self.current_tab = \"dashboard\"        self._hide_all_tabs()        self.dashboard_tab.pack(fill=\"both\", expand=True, padx=0, pady=0)    def show_audio(self):        \"\"\"\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0432\u043a\u043b\u0430\u0434\u043a\u0443 \u0430\u0443\u0434\u0438\u043e. \/ Shows audio tab.\"\"\"        self.current_tab = \"audio\"        self._hide_all_tabs()        self.audio_tab.pack(fill=\"both\", expand=True, padx=0, pady=0)    def show_clone(self): self._show_placeholder(\"clone\", \"content_clone\")    def show_display(self): self._show_placeholder(\"display\", \"content_display\")    def show_stt(self): self._show_placeholder(\"stt\", \"content_stt\")    def show_tts(self): self._show_placeholder(\"tts\", \"content_tts\")    def show_llm(self): self._show_placeholder(\"llm\", \"content_llm\")    def show_structure(self): self._show_placeholder(\"structure\", \"content_structure\")    def show_ha(self): self._show_placeholder(\"ha\", \"content_ha\")# ==========================================# \u0422\u041e\u0427\u041a\u0410 \u0412\u0425\u041e\u0422\u0410 \u0412 \u041f\u0420\u0418\u041b\u041e\u0416\u0415\u041d\u0418\u0415 \/ APPLICATION ENTRY POINT# ==========================================if __name__ == \"__main__\":    app = ZaharApp()    app.mainloop()EOF<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<\/details>\n<h2>\u041a\u0430\u043a \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0443\u0441\u043f\u0435\u0445 \u0428\u0430\u0433\u0430 4<\/h2>\n<p><strong>\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435<\/strong> <\/p>\n<pre><code>zaharrun<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><strong>\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0432\u043a\u043b\u0430\u0434\u043a\u0443 \u00ab\u0410\u0443\u0434\u0438\u043e\u00bb \/ Check the &#171;Audio&#187; tab:<\/strong><\/p>\n<ul>\n<li>\n<p>\u041a\u043b\u0438\u043a\u043d\u0438\u0442\u0435 \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0443 <strong>\u00ab\u0410\u0443\u0434\u0438\u043e\u00bb<\/strong> \u0432 \u0441\u0430\u0439\u0434\u0431\u0430\u0440\u0435<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u043b\u0436\u043d\u044b \u0443\u0432\u0438\u0434\u0435\u0442\u044c:<\/p>\n<ul>\n<li>\n<p>\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u00ab\u0410\u0443\u0434\u0438\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u00bb \/ Header &#171;\u0410\u0443\u0434\u0438\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430&#187;<\/p>\n<\/li>\n<li>\n<p>\u0421\u0435\u043a\u0446\u0438\u044e <strong>\u00ab\u041c\u0438\u043a\u0440\u043e\u0444\u043e\u043d (\u0432\u0445\u043e\u0434)\u00bb<\/strong> \u0441 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u043c \u0441\u043f\u0438\u0441\u043a\u043e\u043c<\/p>\n<\/li>\n<li>\n<p>\u041a\u043d\u043e\u043f\u043a\u0443 <strong>\u00ab\u0422\u0435\u0441\u0442 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430 (3 \u0441\u0435\u043a)\u00bb<\/strong><\/p>\n<\/li>\n<li>\n<p>\u0421\u0435\u043a\u0446\u0438\u044e <strong>\u00ab\u0414\u0438\u043d\u0430\u043c\u0438\u043a (\u0432\u044b\u0445\u043e\u0434)\u00bb<\/strong> \u0441 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u043c \u0441\u043f\u0438\u0441\u043a\u043e\u043c<\/p>\n<\/li>\n<li>\n<p>\u041a\u043d\u043e\u043f\u043a\u0443 <strong>\u00ab\u0422\u0435\u0441\u0442 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 (\u0442\u043e\u043d 440 \u0413\u0446)\u00bb<\/strong><\/p>\n<\/li>\n<li>\n<p>\u041a\u043d\u043e\u043f\u043a\u0438 <strong>\u00ab\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a\u00bb<\/strong> \u0438 <strong>\u00ab\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c\u00bb<\/strong><\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1042730\/\">https:\/\/habr.com\/ru\/articles\/1042730\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u0428\u0430\u0433 4: \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u00ab\u0410\u0443\u0434\u0438\u043e\u00bb\u0426\u0435\u043b\u044c \u0448\u0430\u0433\u0430: \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u043e \u0437\u0432\u0443\u043a\u043e\u043c, \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043c\u043e\u0434\u0443\u043b\u044c core\/audio_manager.py \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0430\u0443\u0434\u0438\u043e-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0432\u0438\u0434\u0436\u0435\u0442 ui\/audio_tab.py \u0441 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u043c\u0438 \u0441\u043f\u0438\u0441\u043a\u0430\u043c\u0438 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u043e\u0432 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u043e\u0432, \u043a\u043d\u043e\u043f\u043a\u0430\u043c\u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435\u043c \u0432\u044b\u0431\u043e\u0440\u0430 \u0432 config.json. 4.1. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0445 \u0438 Python-\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 \u26a0\ufe0f \u041f\u0435\u0440\u0435\u0434 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435\u043c \u044d\u0442\u0438\u0445 \u043a\u043e\u043c\u0430\u043d\u0434 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e \u0441\u0430\u043c\u043e\u0435 \u041f\u041e \u0438\u0437 \u0442\u0440\u0435\u0445 \u0431\u0443\u043a\u0432. # 1. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0443\u044e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 PortAudio# \u041e\u043d\u0430 \u043d\u0443\u0436\u043d\u0430 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b sounddevice \u043d\u0430 Linux\/Raspberry Pi# Install PortAudio system library# It&#8217;s required for sounddevice to work on Linux\/Raspberry Pisudo apt install libportaudio2 -y# 2. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u043c \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 (\u0435\u0441\u043b\u0438 \u0435\u0449\u0451 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u043e)# Activate environment (if not already activated)cd ~\/Zahar &amp;&amp; source venv\/bin\/activate# 3. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c Python-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u043e \u0437\u0432\u0443\u043a\u043e\u043c# Install Python libraries for audio work# sounddevice &#8212; \u0440\u0430\u0431\u043e\u0442\u0430 \u0441 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430\u043c\u0438 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430\u043c\u0438 \/ working with mics and speakers# numpy &#8212; \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0430\u0443\u0434\u0438\u043e\u0434\u0430\u043d\u043d\u044b\u0445 \/ audio data processingpip install sounddevice numpy4.2. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u044f core\/audio_manager.py\u042d\u0442\u043e \u00ab\u043c\u043e\u0437\u0433\u00bb \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0430\u0443\u0434\u0438\u043e. \u041c\u043e\u0434\u0443\u043b\u044c \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430:\u0421\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0441\u0435\u0445 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u043e\u0432 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u043e\u0432\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430 (\u0437\u0430\u043f\u0438\u0441\u044c 3 \u0441\u0435\u043a\u0443\u043d\u0434 + \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435)\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 (\u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0442\u043e\u043d\u0430 440 \u0413\u0446)\u0412\u0441\u0435 \u0442\u0435\u0441\u0442\u044b \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u043e\u0442\u043e\u043a\u0430\u0445, \u0447\u0442\u043e\u0431\u044b \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043d\u0435 \u0437\u0430\u0432\u0438\u0441\u0430\u043b.\u0421\u043a\u0440\u044b\u0442\u044b\u0439 \u0442\u0435\u043a\u0441\u0442cat &gt; ~\/Zahar\/core\/audio_manager.py &lt;&lt; &#8216;EOF&#8217;# ============================================================# \u041c\u043e\u0434\u0443\u043b\u044c \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0430\u0443\u0434\u0438\u043e-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438# Audio devices management module# ============================================================# \u041e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0437\u0430\u043f\u0438\u0441\u044c \u0438 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0437\u0432\u0443\u043a\u0430.# Responsible for device scanning, recording and playing sound.import sounddevice as sdimport numpy as npimport threadingfrom typing import List, Tuple, Optional, Callableclass AudioManager:    &#171;&#187;&#187;    \u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0430\u0443\u0434\u0438\u043e-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.    Audio devices manager.    \u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430\u043c\u0438 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430\u043c\u0438.    Provides methods for working with microphones and speakers.    &#171;&#187;&#187;        def __init__(self):        # \u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u0434\u0438\u0441\u043a\u0440\u0435\u0442\u0438\u0437\u0430\u0446\u0438\u0438 (16kHz \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0434\u043b\u044f \u0440\u0435\u0447\u0438)        # Sample rate (16kHz is enough for speech)        self.sample_rate = 16000        # \u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043a\u0430\u043d\u0430\u043b\u043e\u0432 (1 = \u043c\u043e\u043d\u043e)        # Number of channels (1 = mono)        self.channels = 1        def get_input_devices(self) -&gt; List[Tuple[int, str]]:        &#171;&#187;&#187;        \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0432\u0432\u043e\u0434\u0430 (\u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u043e\u0432).        Returns list of input devices (microphones).                \u0424\u043e\u0440\u043c\u0430\u0442 \/ Format: [(id, &#171;\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430&#187;), &#8230;]        &#171;&#187;&#187;        devices = sd.query_devices()        result = []        for i, dev in enumerate(devices):            # \u0424\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0432\u0445\u043e\u0434\u043d\u044b\u043c\u0438 \u043a\u0430\u043d\u0430\u043b\u0430\u043c\u0438            # Filter only devices with input channels            if dev[&#8216;max_input_channels&#8217;] &gt; 0:                result.append((i, dev[&#8216;name&#8217;]))        return result        def get_output_devices(self) -&gt; List[Tuple[int, str]]:        &#171;&#187;&#187;        \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0432\u044b\u0432\u043e\u0434\u0430 (\u0434\u0438\u043d\u0430\u043c\u0438\u043a\u043e\u0432).        Returns list of output devices (speakers).                \u0424\u043e\u0440\u043c\u0430\u0442 \/ Format: [(id, &#171;\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430&#187;), &#8230;]        &#171;&#187;&#187;        devices = sd.query_devices()        result = []        for i, dev in enumerate(devices):            # \u0424\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0432\u044b\u0445\u043e\u0434\u043d\u044b\u043c\u0438 \u043a\u0430\u043d\u0430\u043b\u0430\u043c\u0438            # Filter only devices with output channels            if dev[&#8216;max_output_channels&#8217;] &gt; 0:                result.append((i, dev[&#8216;name&#8217;]))        return result        def get_default_input_id(self) -&gt; Optional[int]:        &#171;&#187;&#187;        \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432\u0432\u043e\u0434\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e.        Returns default input device ID.        &#171;&#187;&#187;        try:            # sd.default.device \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u0440\u0442\u0435\u0436 (input_id, output_id)            # sd.default.device returns tuple (input_id, output_id)            default_input = sd.default.device[0]            # \u0415\u0441\u043b\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c None            # If system didn&#8217;t detect device, return None            if default_input is None or default_input &lt; 0:                return None            return int(default_input)        except Exception:            return None        def get_default_output_id(self) -&gt; Optional[int]:        &#171;&#187;&#187;        \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e.        Returns default output device ID.        &#171;&#187;&#187;        try:            default_output = sd.default.device[1]            if default_output is None or default_output &lt; 0:                return None            return int(default_output)        except Exception:            return None        def test_microphone(        self,         input_device_id: int,         output_device_id: int,        duration: float = 3.0,        on_status: Optional[Callable[[str], None]] = None    ) -&gt; threading.Thread:        &#171;&#187;&#187;        \u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u0442 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d: \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 3 \u0441\u0435\u043a\u0443\u043d\u0434\u044b \u0437\u0432\u0443\u043a\u0430 \u0438 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442 \u0435\u0433\u043e.        Tests microphone: records 3 seconds of sound and plays it back.                \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \/ Parameters:            input_device_id &#8212; ID \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430 \/ microphone ID            output_device_id &#8212; ID \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 \u0434\u043b\u044f \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f \/ speaker ID for playback            duration &#8212; \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u0438 \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445 \/ recording duration in seconds            on_status &#8212; \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0434\u043b\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u0432 \/ callback for statuses                        \u0421\u0442\u0430\u0442\u0443\u0441\u044b \/ Statuses: &#171;recording&#187;, &#171;playing&#187;, &#171;done&#187;, &#171;error: &#8230;&#187;                \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \/ Returns:            threading.Thread &#8212; \u043f\u043e\u0442\u043e\u043a \u0441 \u0442\u0435\u0441\u0442\u043e\u043c \/ thread with test        &#171;&#187;&#187;        def _run_test():            try:                # \u0421\u0442\u0430\u0442\u0443\u0441: \u0437\u0430\u043f\u0438\u0441\u044c                # Status: recording                if on_status:                    on_status(&#171;recording&#187;)                                # \u0417\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c \u0437\u0432\u0443\u043a \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u0430                # Record sound from selected microphone                audio_data = sd.rec(                    int(duration * self.sample_rate),                    samplerate=self.sample_rate,                    channels=self.channels,                    device=input_device_id,                    dtype=&#8217;float32&#8242;                )                # \u0416\u0434\u0435\u043c \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f \u0437\u0430\u043f\u0438\u0441\u0438                # Wait for recording to finish                sd.wait()                                # \u0421\u0442\u0430\u0442\u0443\u0441: \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435                # Status: playing                if on_status:                    on_status(&#171;playing&#187;)                                # \u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u043e\u0435 \u0447\u0435\u0440\u0435\u0437 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0434\u0438\u043d\u0430\u043c\u0438\u043a                # Play back recorded audio through selected speaker                sd.play(                    audio_data,                    samplerate=self.sample_rate,                    device=output_device_id                )                sd.wait()                                # \u0421\u0442\u0430\u0442\u0443\u0441: \u0433\u043e\u0442\u043e\u0432\u043e                # Status: done                if on_status:                    on_status(&#171;done&#187;)                                except Exception as e:                # \u0421\u0442\u0430\u0442\u0443\u0441: \u043e\u0448\u0438\u0431\u043a\u0430                # Status: error                if on_status:                    on_status(f&#187;error: {e}&#187;)                # \u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0442\u0435\u0441\u0442 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c \u043f\u043e\u0442\u043e\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c GUI        # Run test in separate thread to not block GUI        # daemon=True &#8212; \u043f\u043e\u0442\u043e\u043a \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043e\u0439        # daemon=True &#8212; thread will terminate with the program        thread = threading.Thread(target=_run_test, daemon=True)        thread.start()        return thread        def test_speaker(        self,         output_device_id: int,        on_status: Optional[Callable[[str], None]] = None    ) -&gt; threading.Thread:        &#171;&#187;&#187;        \u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u0442 \u0434\u0438\u043d\u0430\u043c\u0438\u043a: \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0439 \u0442\u043e\u043d (440 \u0413\u0446, 1 \u0441\u0435\u043a\u0443\u043d\u0434\u0430).        Tests speaker: plays a test tone (440 Hz, 1 second).                \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \/ Parameters:            output_device_id &#8212; ID \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0430 \/ speaker ID            on_status &#8212; \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0434\u043b\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u0432 \/ callback for statuses        &#171;&#187;&#187;        def _run_test():            try:                if on_status:                    on_status(&#171;playing&#187;)                                # \u0413\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u043c \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0439 \u0442\u043e\u043d: \u0441\u0438\u043d\u0443\u0441\u043e\u0438\u0434\u0430 440 \u0413\u0446 (\u043d\u043e\u0442\u0430 &#171;\u041b\u044f&#187;)                # Generate test tone: 440 Hz sine wave (note &#171;A&#187;)                duration = 1.0                t = np.linspace(0, duration, int(self.sample_rate * duration), False)                # 0.3 &#8212; \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u044c (30% \u043e\u0442 \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c\u0430, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u043e\u0433\u043b\u0443\u0448\u0438\u0442\u044c)                # 0.3 &#8212; volume (30% of max to avoid being too loud)                tone = 0.3 * np.sin(2 * np.pi * 440 * t)                                # \u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u043c \u0442\u043e\u043d \u0447\u0435\u0440\u0435\u0437 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0434\u0438\u043d\u0430\u043c\u0438\u043a                # Play tone through selected speaker                sd.play(                    tone.astype(np.float32),                    samplerate=self.sample_rate,                    device=output_device_id                )                sd.wait()                                if on_status:                    on_status(&#171;done&#187;)                                except Exception as e:                if on_status:                    on_status(f&#187;error: {e}&#187;)                thread = threading.Thread(target=_run_test, daemon=True)        thread.start()        return threadEOF4.3. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 ui\/audio_tab.py\u042d\u0442\u043e \u0432\u0438\u0434\u0436\u0435\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u043a\u043d\u043e\u043f\u043a\u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0432\u044b\u0431\u043e\u0440 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u0421\u043a\u0440\u044b\u0442\u044b\u0439 \u0442\u0435\u043a\u0441\u0442cat &gt; ~\/Zahar\/ui\/audio_tab.py &lt;&lt; &#8216;EOF&#8217;# ============================================================# \u0412\u043a\u043b\u0430\u0434\u043a\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0443\u0434\u0438\u043e-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432# Audio devices settings tab# ============================================================# \u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043a\u0438 \u043c\u0438\u043a\u0440\u043e\u0444\u043e\u043d\u043e\u0432 \u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u043e\u0432, \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0445 \u0432\u044b\u0431\u0440\u0430\u0442\u044c,# \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0432\u044b\u0431\u043e\u0440 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.# Displays lists of microphones and speakers, allows selection,# testing and saving choice to configuration.import customtkinter as ctkfrom typing import Dict, List, Tuple, Callablefrom core.audio_manager import AudioManagerclass AudioTab(ctk.CTkFrame):    &#171;&#187;&#187;    \u0412\u0438\u0434\u0436\u0435\u0442 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-482105","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/482105","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=482105"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/482105\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=482105"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=482105"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=482105"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}