Sploitus

Exploit for live-copilot-poc

githubexploit · 2026-08-21

Exploit Code

README270 lines
## https://sploitus.com/exploit?id=9975179A-6D96-51D2-8868-964637C544EB
# Live Copilot PoC

Универсальный live-суфлёр: слушает разговор (микрофон + системный звук), даёт
контекстные подсказки, умеет гуглить и разбирать скриншоты. PoC для Рината —
проверка технологии и рынка, не продакшн.

## Запуск (venv нужно пересоздать — сам venv не сохранён, только код)

```bash
python3 -m venv ~/Desktop/live_copilot_poc/venv
source ~/Desktop/live_copilot_poc/venv/bin/activate
pip install speechmatics-rt sounddevice numpy requests pywebview pyobjc-framework-Cocoa pypdf python-docx pynput
cd ~/Desktop/live_copilot_poc
python3 live_copilot_poc.py
```

Важно: нужен именно Homebrew Python 3.12 (`/opt/homebrew/bin/python3.12`) —
системный Python на macOS тащит древний Tk/иногда ломает pywebview. Если venv
создаётся не тем python — указать явно: `/opt/homebrew/bin/python3.12 -m venv ...`

## Нужные ключи (у Артёма лежат в `~/.credentials/`, уже настроены)

- `speechmatics_api_key.env` — потоковая транскрипция (оба канала, два
  параллельных сеанса). Бесплатно: 100 кредитов без карты (регистрация через
  Google-логин)
- `groq_api_key.env` — LLM-подсказки (openai/gpt-oss-120b)
- `tavily_api_key.env` — веб-поиск (модель сама решает гуглить или нет через tool-calling)
- `openrouter_api_key.env` — vision-модель для разбора скриншотов
  (`nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free` — единственная из
  протестированных, что давала адекватные ответы; ~15-20 сек на ответ)

(`assemblyai` в коде больше не используется — заменён на Speechmatics, см.
"Известные ограничения" ниже про причину)

## Запуск на чужой машине (для Рината)

Ключи в коде НЕ зашиты — они читаются из файлов на диске (`~/.credentials/`),
которых у тебя нет. Без своих ключей приложение не стартует. Порядок:

**1. Заведи свои ключи (все — бесплатно, без карты):**

| Файл | Где взять |
|---|---|
| `~/.credentials/speechmatics_api_key.env` | [portal.speechmatics.com](https://portal.speechmatics.com) — регистрация (можно через Google), 100 бесплатных кредитов |
| `~/.credentials/groq_api_key.env` | [console.groq.com](https://console.groq.com) — регистрация, бесплатный тир |
| `~/.credentials/tavily_api_key.env` | [tavily.com](https://tavily.com) — регистрация, бесплатный тир |
| `~/.credentials/openrouter_api_key.env` | [openrouter.ai](https://openrouter.ai) — регистрация, используемая модель бесплатная |

Каждый файл — одна строка в формате `КЛЮЧ=значение`, например:
```
speechmatics_api_key.env:  SPEECHMATICS_API_KEY=твой_ключ
groq_api_key.env:          GROQ_API_KEY=твой_ключ
tavily_api_key.env:        TAVILY_API_KEY=твой_ключ
openrouter_api_key.env:    OPENROUTER_API_KEY=твой_ключ
```

**2. Склонируй репозиторий:**
```bash
git clone https://github.com/boldyrev-tema/live-copilot-poc.git
cd live-copilot-poc
```

**3. Собери venv и поставь зависимости** (нужен именно Homebrew Python 3.12,
см. предупреждение выше) — команды из раздела "Запуск" выше, только с путём
до своей папки.

**4. Запусти `python3 live_copilot_poc.py`.** macOS спросит разрешения при
первом запуске — дай все три, иначе конкретная часть не заработает:
- **Микрофон** — без него не слышит твой голос
- **Screen Recording** (Экранная запись) — без него не работает захват
  системного звука (реплики собеседника) через `SystemAudioDump`, и не
  работают обе кнопки скриншота
- **Input Monitoring** (Мониторинг ввода) — без него не сработает глобальный
  хоткей `Cmd+Shift+J`

Если после выдачи разрешения всё равно не работает — обычно помогает
перезапустить сам процесс (macOS иногда не подхватывает разрешение "на лету").

**5. Проверка, что всё живое:** скажи что-нибудь в микрофон — реплика должна
появиться в ленте с меткой "Ты". Дай кому-то на созвоне (или включи видео с
вопросом) — реплика "Собеседника" тоже должна появиться, а на вопрос — сама
всплыть подсказка. Кодовая фраза для форс-ответа — "хороший вопрос".

## Архитектура

- **Микрофон** ("Ты") + **системный звук** ("Собеседник", через бинарник
  `cheating-daddy/src/assets/SystemAudioDump` — украден у open-source клона
  Cluely, работает без Electron, просто пишет сырой PCM в stdout)
- **Транскрипция потоковая, не батчевая**: оба канала льются непрерывным PCM16
  в `assemblyai.streaming.v3.ChannelStreamer` — он сводит их в одну
  websocket-сессию AssemblyAI, сохраняя разметку по каналу (`mic`/`system` →
  "Ты"/"Собеседник"), и сам определяет конец реплики (end-of-turn) вместо
  наших прежних фиксированных 6-сек окон. Раньше был батч Groq Whisper раз в
  6 сек — задержка ~10-15 сек; разведка технологии lockedinai.com показала, что
  их sub-1-сек скорость держится именно на потоковом STT (AssemblyAI/
  Speechmatics), не на модели — перешли на то же самое. Системный звук у
  SystemAudioDump идёт на 24кГц стерео, ресемплится до 16кГц моно через
  `audioop.ratecv` перед отправкой в сессию.
- Полный автотриггер, без ручной кнопки: как только реплика "Собеседника"
  закрывается (`end_of_turn`) и похожа на вопрос — сама спрашивает LLM.
  Тумблер "🔍 Поиск" в UI управляет только тем, включать ли Tavily-поиск в
  этот автовызов (ручной кнопки "Спросить" больше нет — во время реального
  собеседования кликать неудобно/невозможно; решение подтверждено референсом
  lockedinai.com, см. `pywebview.api.set_search`)
- **Фильтр светской беседы, в два слоя** (разведка показала: у `cheating-daddy`
  вообще нет отдельного классификатора "вопрос/не вопрос" — вся фильтрация
  держится на самой модели через промпт; у нас то же самое плюс лёгкий
  пре-фильтр сверху):
  1. `is_smalltalk()` — грубая проверка на явные фразы ("как дела", "спасибо"
     и т.п., см. `SMALLTALK_PHRASES`) отсекает самые очевидные случаи ещё до
     вызова LLM, экономя запрос
  2. Что не поймал пре-фильтр — ловит сама модель: в `SYSTEM_PROMPT` есть
     инструкция вывести ровно токен `SKIP_TOKEN` ("НЕТ_ОТВЕТА"), если реплика
     светская. Проверено живым вызовом Groq на фразе, нарочно обходящей
     пре-фильтр ("Ну и как у вас в целом сегодня настроение, бодрячком?") —
     модель корректно вернула `НЕТ_ОТВЕТА`, подсказка не показывается
  - Важно: индустрия эту задачу тоже не решает идеально — по отзывам о Cluely,
    рекомендуемая практика "не смотреть в суфлёр во время светской беседы"
    подразумевает, что человек сам игнорирует часть подсказок глазами. Наш
    фильтр снижает число ложных срабатываний, но не гарантирует ноль
- **Два ручных оверрайда** на случай, когда автодетект пропустил реальный
  вопрос (кнопку "Спросить" убрали, но иногда всё равно нужно форсировать):
  1. **Глобальный хоткей `Cmd+Shift+J`** (`pynput.keyboard.GlobalHotKeys`,
     константа `HOTKEY_COMBO`) — работает даже когда окно не в фокусе,
     беззвучно и мгновенно. Сделан по образцу реальных конкурентов: у Cluely
     это `Ctrl+Enter`, у LockedIn AI — тоже хоткей ("simple keyboard
     shortcuts"), выяснено из их собственных туториалов на YouTube (субтитры
     вытащены через yt-dlp, не с чужих слов). Требует разрешения Input
     Monitoring в Системных настройках — в этой среде оно уже было выдано
     с прошлых тестов PoC, при первом запуске на чистой системе macOS
     запросит его сам
  2. **Голосовая кодовая фраза** — скажи "хороший вопрос" вслух (ловится на
     ТВОЁМ канале микрофона, не собеседника) — звучит естественно как обычная
     реакция на вопрос, не подозрительно для собеседника. Фраза задаётся
     константой `HOTWORD` — легко поменять. Оставлена как запасной путь
     параллельно с хоткеем, не заменена им
- LLM-подсказка: контекст пользователя (файл/заметки) + живой транскрипт →
  `openai/gpt-oss-120b` с опциональным tool-calling поиском через Tavily
- Скриншот — два режима, оба ручные (это отдельный сценарий: не ответ на
  вопрос собеседника, а разбор того, что на экране, по запросу):
  1. "📷 Скрин" — `screencapture -x -m`, без взаимодействия. **С `-m`**: без
     него при двух мониторах `screencapture` молча создаёт ОТДЕЛЬНЫЙ файл на
     каждый экран (не один общий снимок, см. `man screencapture`), а наш код
     читал бы только один путь — второй монитор терялся бы молча, плюс его
     файл никто не удалял бы. `-m` ограничивает съёмку главным монитором —
     предсказуемо, но если нужный контент на ВТОРОМ (не главном) мониторе,
     эта кнопка его не возьмёт
  2. "🖼️ Область" — `screencapture -i`, тянешь рамку или кликаешь окно на
     ЛЮБОМ мониторе (это уже решает проблему пункта 1 для мульти-монитора).
     Esc отменяет (код учитывает — если файл не создался, ставит статус
     "скриншот отменён"). Точнее и меньше шума для vision-модели, чем весь
     стол, но требует ручного выделения
  - Проверено: наше окно "Live Copilot" НЕ попадает в собственный
    `screencapture` — `NSWindowSharingNone` защищает и от локального захвата,
    не только от Zoom/screen-share (подтверждено реальным скриншотом экрана)
- Невидимость от захвата экрана: `NSWindowSharingNone` через PyObjC —
  подтверждено РЕАЛЬНЫМ тестом в Zoom (два отдельных устройства — свой Mac
  показывал звонок другому человеку). Механизм — свойство самого окна на
  уровне window server, не должен зависеть от того, какой физический монитор
  Zoom транслирует. Но конкретно сценарий "два монитора на одном Mac, Zoom
  шарит именно тот экран, где сидит окно копилота" отдельно не тестировался —
  ожидаемо должно работать так же, но это не то же самое, что проверено
- Интерфейс: pywebview (HTML/CSS/JS), не Tkinter — тот не умеет скругления/блюр

## Известные ограничения (честно, для разговора с Ринатом)

- **КРИТИЧНО, ещё не решено: потоковая модель AssemblyAI (`universal-streaming-
  multilingual`) даёт бессвязный текст на реальной русской речи в наших живых
  тестах** (21 авг, тест на видео "Самые частые вопросы на собеседовании",
  Noukash). Полный разбор: захватил живой звук через отладочный дамп прямо из
  работающего приложения, прогнал ЭТОТ ЖЕ файл через batch Groq Whisper без
  привязки к языку — получил чистый связный русский текст ("отличный ответ или
  именно потому что у вас такая структура команды компании..."). То есть сама
  запись звука (SystemAudioDump → даунмикс → `audioop.ratecv`) подтверждена
  полностью исправной. Проблема именно в потоковом движке AssemblyAI — он
  выдаёт кашу из случайных языков (итальянский/арабский/иврит/польский/
  португальский вперемешку) на этом же самом звуке, причём ни жёсткий
  `language_codes=["ru"]`, ни `language_detection=True` не спасают (с жёстким
  ru чуть ближе к правде, но всё равно не то). Попробовал `whisper_rt` и
  `universal-3-5-pro` как альтернативные `speech_model` — оба не вернули НИ
  ОДНОГО turn-события за 15 сек теста (без ошибок, просто тишина в ответ) —
  не проверено до конца, не хватило времени копать дальше в этом заходе.
  **Вывод: вся идея "перейти на потоковый STT ради задержки" (см. блок про
  lockedinai.com выше) пока не оправдалась на практике** — старый батч-Whisper
  был медленнее, но реально работал корректно на этом же контенте. Нужно либо
  разобраться с параметрами AssemblyAI (`end_of_turn_confidence_threshold`,
  `min_turn_silence`, другой `speech_model`), либо попробовать Speechmatics
  (второй кандидат из разведки lockedinai.com, не пробовали вообще), либо
  вернуться к Whisper, но с более короткими окнами вместо 6 сек
- **Задержка LLM-подсказки ещё не переизмерена** после перехода на потоковый
  STT — сама транскрипция теперь должна закрывать реплику почти сразу
  (end-of-turn у AssemblyAI, а не ожидание 6 сек), но итоговое время
  "речь → подсказка на экране" зависит ещё и от вызова Groq LLM (~1-2 сек
  обычно) — нужен свежий реальный замер с живым разговором, старая цифра
  10-15 сек относилась к старому батч-конвейеру и больше не актуальна
- **Живой тест на реальной русской речи проведён (21 авг) — и обнаружил пункт
  выше как основную проблему.** Разбивка по каналам (mic/system) технически
  сработала (`channel=system`/`channel=mic` в событиях приходили верно), но
  качество самого текста от AssemblyAI оказалось непригодным
- **Бесплатный тир Groq** легко упирается в лимиты при активном тестировании
  (и обычный chat, и особенно tool-calling запросы — у них РАЗНЫЕ, более узкие
  лимиты на tools)
- **Контекст — не RAG**, просто весь текст файла вставляется в промпт целиком
  каждый раз. Работает для одного резюме, не масштабируется на большую базу
  документов (для этого нужен настоящий RAG, как у Plaud — см. артефакт
  "Не из воздуха")
- **Не проверено до конца**: во время последнего теста задержки (ещё на старом
  Whisper-конвейере) микрофон поймал связный посторонний разговор (про
  школу/учёбу), которого никто не произносил в микрофон намеренно — стоит
  перепроверить на новом конвейере, реальный ли это фоновый разговор рядом с
  компьютером или что-то более странное

## Живой тест 21 авг (видео "Самые частые вопросы на собеседовании") — найденные и исправленные баги

- **Автопрокрутка ленты мешала читать историю** — лента (`#feed`, транскрипт-лог)
  дёргалась к последнему сообщению на КАЖДОЕ новое, даже если специально
  прокрутил вверх почитать прошлые ответы. Исправлено: `isNearBottom()`
  проверяет, был ли уже внизу ДО добавления нового элемента — прокручивает
  только если да, иначе оставляет прокрутку как есть.
- **Фильтр "светской беседы" резал реальные короткие вопросы** — на вопрос
  викторины "Такие морские свиньи? Киты, тюлени или моржи?" модель ошибочно
  вернула `SKIP_TOKEN`, посчитав его светским. Причина: инструкция в промпте
  не различала "вопрос с '?'" от "содержательный вопрос". Переписано на два
  явных случая с примерами в обе стороны (социальный ритуал vs содержательный
  вопрос) — проверено 4 регрессионных кейса, все проходят.
- **Бесплатная vision-модель регулярно недоступна** — `nvidia/nemotron-3-nano-
  omni-30b-a3b-reasoning:free` упала с "Worker local total request limit
  reached (16/16)". Проверил 3 альтернативы (другая NVIDIA VL-модель, два
  Gemma) — ВСЕ тоже упали (NVIDIA idle timeout, Google AI Studio 429) — это
  системная проблема бесплатных vision-моделей на OpenRouter, не проблема
  конкретной модели. Исправлено частично: `ask_about_screenshot` теперь
  перебирает список из 4 моделей по очереди, пока одна не ответит, вместо
  гарантированного отказа на первой же — повышает шанс получить ответ, но не
  гарантирует его (если провайдер весь пул перегружен, все 4 могут упасть).

## Что дальше — пути развития (не сделано)

Живьём проверено и работает (21 авг): STT на Speechmatics даёт связный русский,
автотриггер по вопросу собеседника, кодовая фраза "хороший вопрос" (в два
шага — фраза, потом отдельно свой вопрос), хоткей `Cmd+Shift+J`, сохранение
транскрипта, подстановка контекста (резюме) в ответы. Ниже — что осталось.

- **Поиск не гарантирован, даже когда разрешён.** Тест: вопрос "какая последняя
  версия Python" с `use_search=True` дал тот же (устаревший) ответ, что и без
  поиска — модель решила не гуглить. Два пути: (а) усилить формулировку в
  `SYSTEM_PROMPT` конкретными триггерами ("если в вопросе даты/версии/слова
  «последний/новый/актуальный» — используй поиск"), дёшево, не 100% гарантия;
  (б) заставлять модель искать всегда — надёжнее, но лишний round-trip на
  КАЖДЫЙ вопрос, включая те, что явно не требуют поиска (противоречит цели
  минимизировать задержку). Для реальных собеседований это не критично — там
  обычно спрашивают про опыт (работает контекст) или про компанию (промпт уже
  явно называет искать в этом случае), а не "какая последняя версия X".
- **`setVisibleOnAllWorkspaces`/`setSkipTaskbar`** для "поверх всех столов" —
  не сделано.
- **Экран согласия перед записью собеседника** — юридически/этически важно
  при реальном использовании на живом человеке, сейчас его нет.
- **Тонкая настройка чувствительности `end_of_utterance_silence_trigger`**
  (сейчас 0.5с) — возможно, стоит подстроить под реальный темп речи, если
  реплики режутся слишком рано/поздно.
- **Vision-модели для скриншота остаются ненадёжными** — все проверенные
  бесплатные варианты периодически падают по лимитам провайдера (см. выше).
  Реальное решение — платная модель, если это станет критично для регулярного
  использования.

## Связанные материалы

- Артефакт "Не из воздуха" — разбор 19 конкурентов, как они добиваются
  специфичности ответов: https://claude.ai/code/artifact/c10898d1-232c-4b7d-8d24-262a05d6694e
- Артефакт "Бесплатный слой" — разбор 9 open-source клонов Cluely