docs(readme): rewrite as implementation blueprint (latency budget, dual eye tracks, milestones)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
190
README.md
190
README.md
@@ -1,89 +1,129 @@
|
||||
# watch_sceen_ai
|
||||
|
||||
디스코드 화면공유를 **AI가 실시간으로 보면서 음성으로 함께 대화하는** 봇.
|
||||
디스코드 화면공유를 **AI가 실시간으로 보면서, 사용자와 음성으로 대화하는** 봇.
|
||||
|
||||
사용자가 디스코드에서 화면공유(Go Live)를 켜면, AI가 그 화면을 실시간으로 이해하고,
|
||||
사용자의 음성 질문에 최대한 빠르게 답한다. 필요하면 화면이 크게 바뀔 때 먼저 말을 걸기도 한다.
|
||||
사용자가 디스코드에서 화면공유(Go Live)를 켜면 AI가 그 화면을 실시간으로 이해하고,
|
||||
사용자가 음성으로 물으면 최대한 빠르게 답한다. 화면이 크게 바뀌면 먼저 말을 걸 수도 있다(proactive).
|
||||
|
||||
- 실행 위치: 이 리눅스 호스트(.9, RTX 5050)
|
||||
- 상태: 1단계(눈: 화면공유 스트림 수신) 진행 중 — 관문 1(E2EE 합류) 통과
|
||||
- 실행 위치: 이 리눅스 호스트(.9, RTX 5050 8GB / ffmpeg / node v22 있음)
|
||||
- 현재 상태: 1단계(눈: 화면공유 수신) 진행 중 — E2EE 합류 관문 통과, 실제 프레임 추출 PoC가 다음 목표
|
||||
|
||||
## 전체 구조
|
||||
---
|
||||
|
||||
두 개의 async 루프가 동시에 돈다.
|
||||
## 1. 설계 원칙 (이 프로젝트를 관통하는 3가지)
|
||||
|
||||
1. **지연이 전부다.** 음성 대화형이라 "정확하지만 느린" 답은 실패다. 전 구간을
|
||||
스트리밍으로 잇고, 목표는 사용자가 말 끝낸 뒤 **첫 소리까지 ~1.5초 이내**.
|
||||
2. **싼 것부터, 필요할 때만 비싸게.** 매 프레임을 클라우드로 보내면 비용·지연이 폭발한다.
|
||||
로컬에서 화면 변화 감지 + 1차 이해를 하고, 정말 어려운 프레임만 클라우드로 올린다.
|
||||
3. **부품은 교체 가능해야 한다.** 눈/귀/두뇌/입이 전부 `wsai/interfaces.py`의 Protocol이라,
|
||||
어느 하나(예: 화면 수신 방식)가 막히거나 바뀌어도 나머지는 그대로 돌아간다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 전체 구조
|
||||
|
||||
두 개의 async 루프가 동시에 돈다. 둘을 잇는 건 "지금 화면이 뭔지" 하나를 담는 공유 상태다.
|
||||
|
||||
```
|
||||
[인지 루프] 화면공유 수신 ──frames──▶ 비전 이해 ──▶ [최신 화면 맥락 저장소]
|
||||
│
|
||||
[대화 루프] STT(음성) ──utterances──▶ Brain(LLM) ◀────────┘
|
||||
│
|
||||
▼
|
||||
TTS(음성 응답)
|
||||
[인지 루프] FrameSource ──Frame──▶ 변화감지 ──▶ VisionBackend ──ScreenObservation──▶┐
|
||||
▼
|
||||
[SharedScreenContext]
|
||||
│ (최신 1건)
|
||||
[대화 루프] SpeechToText ──Utterance──▶ Brain(LLM) ◀───────────────────┘
|
||||
/ TextChannel │ (+대화이력)
|
||||
▼
|
||||
TextToSpeech / TextChannel
|
||||
```
|
||||
|
||||
- 인지 루프: 화면 캡처 → 비전 이해 → 최신 화면 맥락 저장
|
||||
- 대화 루프: 음성/텍스트 입력 → 두뇌(LLM, 화면맥락 + 대화이력) → 음성/텍스트 출력
|
||||
- 인지 루프: 프레임 수신 → (이전과 유사하면 버림) → 비전 이해 → 최신 화면 맥락 갱신.
|
||||
- 대화 루프: 사용자 발화 → 두뇌(최신 화면 맥락 + 대화이력) → 음성/텍스트 응답.
|
||||
- 두 루프는 비동기로 독립. 대화 루프는 항상 "가장 최근에 이해된 화면"을 읽을 뿐,
|
||||
프레임 하나하나를 기다리지 않는다.
|
||||
|
||||
모든 컴포넌트는 `wsai/interfaces.py`의 Protocol이라, 백엔드(mock / 로컬GPU / 클라우드 등)를
|
||||
config에서 바꿔 끼워도 오케스트레이터는 그대로다.
|
||||
데이터 타입/인터페이스는 `wsai/interfaces.py`에 이미 정의돼 있다:
|
||||
`Frame`, `ScreenObservation`, `Utterance`, `Reply` /
|
||||
`FrameSource`, `VisionBackend`, `SpeechToText`, `TextToSpeech`, `Brain`, `TextChannel`.
|
||||
|
||||
## 채택한 방식 (2026-08-09 확정)
|
||||
---
|
||||
|
||||
### 눈 — 화면공유 스트림 직접 수신
|
||||
스크린샷·브라우저 캡처는 **거부**. 실제 화면공유 스트림 자체를 진짜 클라이언트처럼 수신한다.
|
||||
## 3. 눈 — 화면공유를 어떻게 받아오나
|
||||
|
||||
- 공식 디스코드 봇 API는 비디오(화면공유) 수신을 완전 차단 → 봇으로는 불가.
|
||||
- 그래서 **셀프봇(유저 토큰)** 으로 프로토콜 레벨에서 비디오 RTP를 직접 수신·디코딩한다.
|
||||
- 수신 파이프: 셀프봇 음성채널 join → 상대 Go Live 스트림 "watch" 구독(비디오 SSRC 획득)
|
||||
→ 암호화 비디오 RTP 수신 → DAVE(E2EE) 복호화 → 전송암호(aead_aes256_gcm_rtpsize) 복호
|
||||
이게 이 프로젝트의 가장 어려운 부분이다. 두 트랙을 **병행**한다.
|
||||
|
||||
### 트랙 A (주력) — 화면공유 스트림 직접 수신
|
||||
스크린샷·브라우저 캡처가 아니라, 실제 화면공유 스트림 자체를 클라이언트처럼 수신한다.
|
||||
|
||||
- 공식 디스코드 봇 API는 비디오 수신을 지원하지 않으므로, **유저 토큰(셀프봇)** 으로
|
||||
프로토콜 레벨에서 비디오 RTP를 받는다. 전용 버너 계정만 사용한다.
|
||||
- 파이프: 셀프봇 음성채널 join → 상대 Go Live 스트림 "watch" 구독(비디오 SSRC 획득)
|
||||
→ 암호화 비디오 RTP 수신 → **DAVE(MLS E2EE) 복호** → 전송암호(aead_aes256_gcm_rtpsize) 복호
|
||||
→ VP8/H264 depayload → ffmpeg 디코드 → 프레임.
|
||||
- 라이브러리: `discord.js-selfbot` 계열(werift-rtp), DAVE 복호화는 `@snazzah/davey`(Rust NAPI).
|
||||
- 계정: 전용 버너 계정 "클로드봇"(tkrmagid_bot). 토큰은 `.env`(gitignore).
|
||||
- 라이브러리: `discord.js-selfbot` 계열(werift-rtp), DAVE는 `@snazzah/davey`(Rust NAPI).
|
||||
- 이 트랙은 Node로 프레임까지 뽑아 로컬 소켓/파이프로 파이썬 파이프라인에 넘긴다
|
||||
(`FrameSource` 구현 하나로 감싼다).
|
||||
|
||||
### 귀 — STT
|
||||
`faster-whisper` 스트리밍/부분 전사로 사용자의 음성을 빠르게 텍스트화.
|
||||
솔직한 리스크: (1) 완성형 "수신→프레임" 라이브러리가 없어 직접 조립해야 한다.
|
||||
(2) 셀프봇은 디스코드 ToS 회색지대라 계정 밴 위험이 있다(→ 버너 전용).
|
||||
(3) 셀프봇/프로토콜이 바뀌면 깨질 수 있다.
|
||||
|
||||
### 두뇌 — 하이브리드 비전 + LLM
|
||||
이 호스트 RTX 5050(8GB)으로 소형 로컬 VLM(moondream2 / Qwen2-VL-2B)이 매 프레임 1차 이해 +
|
||||
화면 변화 감지를 값싸게 처리하고, 어려운 화면만 Claude OAuth(Haiku 계열)로 에스컬레이션.
|
||||
### 트랙 B (폴백/개발용) — 로컬 렌더 화면 캡처
|
||||
디스코드 클라이언트가 화면공유를 "보고 있는" 상태의 화면을 로컬에서 캡처한다.
|
||||
트랙 A가 막히거나 불안정할 때의 안전판이자, 위쪽(비전/대화) 개발을 트랙 A 완성 전에
|
||||
진행하기 위한 실용 소스다. `poc/`의 Xvfb 실험과 `wsai/backends/capture_mss.py`가 여기 해당.
|
||||
|
||||
### 입 — TTS
|
||||
MeloTTS 등(이 호스트에 기존 자산 있음)으로 음성 응답. 말 끊기 인터럽트 지원, 지연 최소화.
|
||||
두 트랙 모두 결과물은 동일한 `Frame` 스트림이라, 눈을 바꿔도 나머지는 그대로다.
|
||||
|
||||
## 진행 상황
|
||||
---
|
||||
|
||||
### 완료
|
||||
- 파이프라인 스켈레톤(`wsai/`) — 의존성/키 없이 mock 모드로 전체 흐름 검증됨.
|
||||
- 캡처 방식 결정 확정: 셀프봇 프로토콜 레벨 비디오 RTP 수신.
|
||||
- 버너 계정 + 토큰 확보, ToS/밴 위험 수용 확인.
|
||||
- **관문 1 통과 (DAVE/MLS E2EE 합류)** — `dave/gate.mjs` 실측 검증:
|
||||
- 셀프봇 유저토큰 메인 GW IDENTIFY → 음성채널 join → 보이스 GW v8 IDENTIFY(`max_dave_protocol_version=1`).
|
||||
- close 4017 없음. `SESSION_DESCRIPTION`에서 `dave_protocol_version=1` 협상됨(채널 E2EE 활성).
|
||||
- `@snazzah/davey`로 MLS 멤버십 핸드셰이크 완주(op25~op29) → MLS session ready=true, voicePrivacyCode 산출.
|
||||
- 결론: 셀프봇이 E2EE 보이스 그룹에 정식 멤버로 합류 가능 → 직접 수신 방식 실현 가능 확정.
|
||||
## 4. 두뇌 — 하이브리드 비전 + LLM
|
||||
|
||||
### 다음 (1단계 마무리)
|
||||
- 상대 Go Live 스트림 구독으로 비디오 SSRC 확보 → UDP SRTP 비디오 패킷 수신 → RTP 파싱
|
||||
→ `daveSession.decrypt` E2EE 복호 → 전송암호 복호 → VP8/H264 depayload → ffmpeg 디코드
|
||||
→ 실제 공유화면 프레임 추출 PoC. (검증하려면 실제 Go Live 중인 소스 필요)
|
||||
- 프레임 변화 감지(동일 화면 반복 전송 방지) + 소스 인터페이스.
|
||||
- 로컬 소형 VLM(예: moondream2 / Qwen2-VL-2B)이 RTX 5050에서 매 프레임 1차 이해.
|
||||
- **변화 감지**로 이전과 거의 같은 화면은 새로 설명하지 않는다(지연·비용 절감).
|
||||
- 화면이 크게 바뀌었거나, 로컬 VLM 신뢰도가 낮거나, 사용자가 화면에 대해 구체적으로
|
||||
물으면 → Claude(OAuth, Haiku 계열)로 에스컬레이션.
|
||||
- 대화 응답 생성(Brain)은 Claude가 담당하되, 항상 "최신 화면 맥락 1건 + 대화이력"만 넣어
|
||||
토큰을 작게 유지한다.
|
||||
|
||||
### 이후 단계
|
||||
- 3단계 · 두뇌: 로컬 VLM 1차 이해 + 변화 감지, Claude(Haiku) 에스컬레이션, 화면 맥락 저장소.
|
||||
- 4단계 · 음성 대화: STT + 대화 브레인 + TTS + 인터럽트/지연 최적화.
|
||||
- 5단계 · 통합·운영: 인지/대화 루프 동시 실행, proactive(화면 크게 바뀌면 먼저 말 걸기), 끊김 복구, 비용 모니터링, 서비스화.
|
||||
---
|
||||
|
||||
## 실행
|
||||
## 5. 귀 / 입 — 음성
|
||||
|
||||
이 호스트에는 `python` 별칭이 없으므로 워크스페이스 venv를 사용한다(`.venv/bin/python`, 없으면 `python3`).
|
||||
- 귀(STT): `faster-whisper` 스트리밍/부분 전사. 마이크는 `sounddevice`.
|
||||
- 입(TTS): MeloTTS 등(이 호스트에 기존 자산 있음). 문장 단위로 스트리밍 재생.
|
||||
- 인터럽트: 사용자가 말하기 시작하면 재생 중인 TTS를 즉시 멈춘다(barge-in).
|
||||
|
||||
### 지연 예산(목표)
|
||||
사용자 발화 종료 → 첫 소리까지:
|
||||
|
||||
| 구간 | 목표 |
|
||||
|------|------|
|
||||
| STT 종료 판정(endpointing) | ~200 ms |
|
||||
| Brain 첫 토큰(화면 맥락은 이미 준비됨) | ~500 ms |
|
||||
| TTS 첫 문장 합성·재생 시작 | ~400 ms |
|
||||
| 여유 | ~400 ms |
|
||||
| **합계** | **~1.5 s** |
|
||||
|
||||
화면 이해는 대화 요청과 분리된 인지 루프에서 미리 끝나 있으므로, 응답 시점에
|
||||
비전 지연이 끼어들지 않는 게 핵심이다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 실행
|
||||
|
||||
이 호스트에는 `python` 별칭이 없으므로 워크스페이스 venv를 쓴다(`.venv/bin/python`).
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m wsai # mock 데모 (의존성·키 불필요, 몇 프레임 돌고 종료)
|
||||
.venv/bin/python -m wsai --env # WSAI_* 환경변수로 조립
|
||||
.venv/bin/python -m pip install pytest && .venv/bin/python -m pytest -q # 스모크 테스트(pytest 미설치 시 먼저 설치)
|
||||
.venv/bin/python -m wsai --env # WSAI_* 환경변수로 백엔드 조립
|
||||
.venv/bin/python -m pip install pytest && .venv/bin/python -m pytest -q # 스모크 테스트
|
||||
```
|
||||
|
||||
## 파일 / 디렉터리
|
||||
백엔드별 추가 설치는 `requirements.txt` 주석 참고(mss/pillow, anthropic, faster-whisper 등).
|
||||
|
||||
---
|
||||
|
||||
## 7. 파일 / 디렉터리
|
||||
|
||||
| 경로 | 역할 |
|
||||
|------|------|
|
||||
@@ -92,15 +132,35 @@ MeloTTS 등(이 호스트에 기존 자산 있음)으로 음성 응답. 말 끊
|
||||
| `wsai/state.py` | 최신 화면 맥락 공유 저장소 |
|
||||
| `wsai/config.py` / `factory.py` | 설정 → 백엔드 조립 |
|
||||
| `wsai/backends/mock.py` | 무의존성 mock 전 계열 |
|
||||
| `wsai/backends/capture_mss.py` | 로컬 화면 캡처(폴백용) |
|
||||
| `wsai/backends/capture_mss.py` | 로컬 화면 캡처(트랙 B) |
|
||||
| `wsai/backends/claude.py` | Claude 비전 + 두뇌 |
|
||||
| `dave/gate.mjs` | DAVE/MLS E2EE 합류 관문 검증(관문 1 통과 증거) |
|
||||
| `poc/` | Xvfb+Chromium 프레임 캡처 실험 — **폐기**(참고 폴백으로만 보존) |
|
||||
| `dave/gate.mjs` | DAVE/MLS E2EE 합류 관문 검증(통과 증거) |
|
||||
| `poc/` | Xvfb+Chromium 캡처 실험(트랙 B 참고) |
|
||||
| `PLAN.md` | 단계별 착수 계획(확정본) |
|
||||
| `tests/test_pipeline.py` | 파이프라인 스모크 테스트 |
|
||||
|
||||
## 리스크 메모
|
||||
---
|
||||
|
||||
- 셀프봇(유저 토큰) 자동화는 디스코드 ToS 회색지대 → 계정 밴 위험. 반드시 버리는 전용 계정만 사용.
|
||||
- 완성형 "수신→프레임" 라이브러리는 없음(직접 조립 필요). 셀프봇 라이브러리는 프로토콜 변경 시 깨질 수 있음.
|
||||
- 리스크를 완전히 없애려면 대안: (a) 디스코드 클라이언트 렌더 화면 로컬 캡처, (b) 디스코드 대신 WebRTC/OBS/Janus/mediasoup 기반 수신 구조. 다만 둘 다 "실제 화면공유 스트림을 진짜로 수신"하려던 원래 요구와는 타협이 있음.
|
||||
- 로컬 GPU 8GB VRAM 제약 → 소형 VLM만. 큰 이해는 클라우드(Claude).
|
||||
## 8. 구현 순서 (검증 가능한 체크포인트)
|
||||
|
||||
- [x] 파이프라인 스켈레톤 — mock 모드로 전 흐름 동작.
|
||||
- [x] E2EE 합류 관문 — 셀프봇이 DAVE/MLS 보이스 그룹에 정식 멤버로 합류 확인(`dave/gate.mjs`).
|
||||
- [ ] **M1 · 진짜 프레임 뽑기(눈).** 트랙 A로 Go Live 스트림 구독 → 비디오 RTP 수신 → 복호 →
|
||||
ffmpeg 디코드 → 실제 공유화면 1프레임을 PNG로 저장.
|
||||
· 검증: 실제로 공유 중인 화면이 이미지로 저장된다. (Go Live 켠 테스트 통화 필요)
|
||||
- [ ] **M2 · 변화 감지 + 로컬 VLM.** 프레임 스트림에서 유사 프레임 버리고, 로컬 VLM이 설명 생성.
|
||||
· 검증: 화면을 바꾸면 새 설명이 나오고, 가만히 두면 새 호출이 안 나간다.
|
||||
- [ ] **M3 · 화면 Q&A(텍스트).** 텍스트 입력으로 "지금 화면 뭐야?"에 최신 맥락 기반 답변.
|
||||
· 검증: 실제 화면 내용을 반영한 답이 나온다.
|
||||
- [ ] **M4 · 음성 왕복.** STT→Brain→TTS 스트리밍 + barge-in. 지연 예산 측정.
|
||||
· 검증: 말로 묻고 말로 답을 듣는다, 첫 소리까지 ~1.5s 근처.
|
||||
- [ ] **M5 · 통합·운영.** 두 루프 동시 실행, proactive, 끊김 복구, 비용 모니터링, 서비스화.
|
||||
|
||||
---
|
||||
|
||||
## 9. 결정이 필요한 것 (사용자 확인 대기)
|
||||
|
||||
- M1을 트랙 A로 바로 갈지, 트랙 B(로컬 캡처)로 위층(M2~M4)을 먼저 완성한 뒤 눈만 교체할지.
|
||||
(트랙 B 선행이 리스크가 낮고 개발이 빠름 — 눈이 막혀도 나머지가 진행됨)
|
||||
- 로컬 VLM 모델 선택(moondream2 vs Qwen2-VL-2B) — VRAM 8GB 내에서 STT/TTS와 공존 가능한지.
|
||||
- Claude 예산·모델(Haiku 고정 vs 난이도별 상향).
|
||||
|
||||
Reference in New Issue
Block a user