
WhatYouSay는 WhatsApp 음성 메시지를 텍스트 답장으로 바꿔 같은 대화에 올리는 자체 호스팅 서버다. 전사문은 저장하지 않는다는 원칙으로 만들어졌다.
WhatYouSay는 Node.js 서버로, WhatsApp 계정을 연결 기기로 연동한다. 음성 메시지가 오면 Whisper로 전사하고 LLM으로 문장을 다듬는다. 선택적으로 번역하거나 요약한 뒤 원래 메시지 바로 아래에 답장으로 게시한다. 메시지를 듣기 위해 하던 일을 멈출 필요가 없다는 것이 제작자가 설명하는 용도다.
이 프로젝트는 같은 기능을 제공하는 호스팅 서비스 WhatScribe에서 착안했으며, WhatYouSay는 별도로 운영하는 독립 구현체다. 평가와 Whisper가 음성 메시지를 중간에서 자르는 문제를 어떻게 다뤘는지는 수정 전에 평가부터 작성하기에서 설명한다.
기능과 이용 조건
WhatYouSay는 Groq의 Whisper large-v3로 음성 메시지를 전사한다. 잘림과 언어가 바뀌는 문제를 막기 위해 두 번 처리하는 방식을 사용한다. 영어·독일어·네덜란드어별 규칙으로 문장부호와 문단을 정리하고 명백한 오청을 고친다. 정리 과정에서 원문이 지나치게 바뀌면 정리 전 Whisper 텍스트를 사용한다.
긴 메시지에는 선택적으로 짧은 요약을 붙일 수 있고, 지정한 언어로 번역할 수도 있다. 사용자는 보낸 메시지와 받은 메시지 중 무엇을 처리할지, 1대1 대화와 그룹 대화 중 어디에 적용할지 정하고 특정 연락처나 그룹을 제외할 수 있다. 웹 대시보드가 열려 있는 동안에는 전사문을 실시간으로 볼 수 있으며, 로그인은 매직 링크로 이뤄진다. 선택 기능으로 연결된 Telegram 그룹의 음성 메시지를 처리하는 봇과 운영자 알림도 제공한다.
이용하려면 Node.js 22.13 이상, Groq API 키, 로그인 이메일 발송을 위한 Resend 계정과 인증된 발신 도메인, HTTPS를 설정할 도메인, QR 코드를 스캔할 WhatsApp 계정이 필요하다. Node.js의 기본 제공 node:sqlite를 사용한다. 제작자에 따르면 개인 이용에는 Groq 무료 요금제로 충분하다.
WhatYouSay는 Baileys라는 비공식 WhatsApp Web 클라이언트를 통해 연결한다. 비공식 클라이언트는 WhatsApp 서비스 약관을 위반하며, WhatsApp이 이를 사용하는 전화번호를 제한하거나 차단할 수 있다. 실행자는 자신의 번호로 그 위험을 감수해야 한다. 이 프로젝트는 WhatsApp, 메타, WhatScribe와 제휴 관계가 없다.
개인정보 처리와 접근 제어
전사문과 요약은 데이터베이스에 기록하지 않는다. 처리한 메시지마다 발신자, 길이, 언어, 토큰 수, WhatsApp 메시지 ID, 시각만 메타데이터 행에 남고 전사문 열은 NULL로 유지된다. 매일 실행되는 작업은 90일이 지난 발신자의 이름을 지우고 WhatsApp ID를 키가 적용된 해시로 바꾼다. WhatsApp 세션 자격 증명과 Signal 키는 DB_ENCRYPTION_KEY를 사용해 저장 시 암호화한다.
다만 처리 과정에서 서버 밖으로 나가는 정보가 있다. 음성과 텍스트는 전사 및 텍스트 처리 업체인 Groq로 전송된다. 답장은 WhatsApp 대화방에 올라가므로 그 대화방에 있는 모든 사람이 읽을 수 있다. 대시보드가 열려 있으면 실시간 연결을 통해 전사문을 받으며, 수신자 기기가 재전송을 요청할 때 WhatsApp이 다시 보낼 수 있도록 전송한 답장을 잠시 메모리 캐시에 보관한다. 대시보드는 libphonenumber-js 스크립트를 unpkg.com에서 불러온다.
다른 사람이 보낸 음성 메시지를 전사하면 그 사람의 음성이 제3자 AI API를 거친다. 프로젝트는 이 사실을 상대방에게 알리라고 안내한다.
기본 설정은 초대 전용이다. 운영 환경에서는 쉼표로 구분한 ALLOWED_EMAILS에 포함된 이메일 주소와 ADMIN_EMAIL만 로그인 링크를 요청할 수 있다. ALLOWED_EMAILS=*로 설정하면 누구나 가입할 수 있다. 이 경우 낯선 사람이 자신의 WhatsApp을 연결해 운영자의 Groq 할당량을 사용할 수 있으며, 모든 전사 답장에 인스턴스 URL이 붙어 있어 서비스를 찾기도 쉽다. 이메일 주소를 허용 목록에서 빼면 새 로그인은 막히지만 기존 세션은 유지된다. 기존 세션을 끝내려면 /admin에서 계정을 삭제해야 한다. 운영 환경이 아닌 npm run dev에서는 ALLOWED_EMAILS를 설정하지 않으면 로컬 테스트를 위해 누구나 접근할 수 있다.
로컬에서 실행하기
git clone https://github.com/waldo-van-der-code/whatyousay-oss.git whatyousay
cd whatyousay
npm ci # do not use --ignore-scripts: some dependencies need their install scripts
cp env.example .env # fill in GROQ_API_KEY at minimum
echo "LOG_MAGIC_LINKS=true" >> .env
echo "ALLOW_INSECURE_COOKIES=true" >> .env
echo "BASE_URL=http://localhost:3000" >> .env
npm run dev
http://localhost:3000을 열고 이메일 주소를 입력한 다음 콘솔에 출력된 매직 링크를 복사한다. LOG_MAGIC_LINKS는 이메일을 보내기 전에 링크를 출력한다. 따라서 Resend 키가 없으면 페이지에 “이메일 발송 실패”가 표시되지만 콘솔에 출력된 링크는 사용할 수 있다. 대시보드에 QR 코드가 표시되면 휴대전화에서 WhatsApp → 설정 → 연결된 기기 → 기기 연결을 선택하고 스캔한다.
Oracle Cloud 서버에 배포하기
다음 절차는 Ubuntu, nginx, Let's Encrypt 인증서, pm2를 사용하는 Oracle Cloud “Always Free” ARM 인스턴스(Ampere A1)를 설정한다. 다른 Ubuntu 서버에서도 Oracle 전용 방화벽 설정만 건너뛰면 같은 방식으로 배포할 수 있다.
인스턴스 만들기
Oracle Cloud 콘솔에서 컴퓨트 인스턴스를 만들 때 Canonical Ubuntu 24.04 이미지와 VM.Standard.A1.Flex(Ampere ARM) 셰이프를 선택한다. 1 OCPU와 6 GB RAM이면 충분하다고 안내한다. SSH 공개 키를 추가한다. 인스턴스에 공인 IP를 할당한 뒤 도메인의 DNS A 레코드가 해당 IP를 가리키도록 설정한다. 예를 들어 whatyousay.example.com을 사용할 수 있다.
80·443 포트 열기
Oracle은 두 곳에서 트래픽을 필터링하므로 모두 열어야 한다. 콘솔에서 인스턴스의 서브넷 → Security List → Add Ingress Rules로 이동해 출발지 0.0.0.0/0, TCP, 대상 포트 80과 443을 추가한다.
인스턴스의 Oracle Ubuntu 이미지에는 SSH 외의 연결을 거부하는 iptables 규칙이 기본으로 들어 있다.
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT
sudo netfilter-persistent save
Node.js, pm2, nginx 설치하기
sudo apt update && sudo apt install -y git nginx sqlite3
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.bashrc
nvm install 22
npm install -g pm2
WhatYouSay 설치하기
cd ~
git clone https://github.com/waldo-van-der-code/whatyousay-oss.git whatyousay
cd whatyousay
npm ci
npm run build # compiles src/ to dist/, which production runs
cp env.example .env
chmod 600 .env
nano .env
최소한 GROQ_API_KEY, RESEND_API_KEY, EMAIL_FROM, DB_ENCRYPTION_KEY, BASE_URL, ALLOWED_EMAILS를 입력한다. BASE_URL은 https://를 포함한 도메인이어야 하며, 운영 환경에서는 이 값이 없으면 서버가 시작되지 않는다. ALLOWED_EMAILS에는 로그인할 수 있는 사람을 설정한다. DB_ENCRYPTION_KEY는 다음 명령으로 생성한다.
openssl rand -hex 32
이 키는 백업과 별도의 안전한 장소에 보관해야 한다. 키가 없으면 연결된 WhatsApp 세션을 모두 다시 QR 코드로 스캔해야 한다.
pm2로 실행하기
pm2 start ecosystem.config.js
pm2 save
pm2 startup systemd # run the command it prints, so pm2 starts on boot
curl -sI http://127.0.0.1:3000/login | head -1 # expect HTTP/1.1 200 OK
앱은 127.0.0.1:3000에서만 요청을 받으며, 외부 연결은 nginx가 맡는다.
nginx 설정하기
/etc/nginx/sites-available/whatyousay 파일을 만든다.
server {
listen 80;
server_name whatyousay.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# The dashboard keeps a live Server-Sent Events connection open.
proxy_read_timeout 86400;
proxy_buffering off;
}
}
앱은 로그인 요청 제한에 사용할 방문자 IP를 X-Real-IP에서 가져온다. Cloudflare를 앞에 두면 nginx에는 방문자 대신 Cloudflare 주소가 전달된다. 방문자 IP를 복원하려면 nginx의 real_ip 모듈을 사용하고 Cloudflare IP 목록에 있는 모든 범위를 추가한다. 다음 설정을 server 블록 안에 넣는다.
real_ip_header CF-Connecting-IP;
set_real_ip_from 173.245.48.0/20; # one line per Cloudflare IPv4 and IPv6 range
설정을 활성화하고 HTTPS를 추가한다.
sudo ln -s /etc/nginx/sites-available/whatyousay /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d whatyousay.example.com
WhatsApp 연결하기
도메인을 열고 이메일로 받은 매직 링크로 로그인한 뒤 WhatsApp → 설정 → 연결된 기기에서 QR 코드를 스캔한다. 몇 초보다 긴 음성 메시지를 직접 보내 답장이 도착하는지 확인한다. 사용자 목록과 계정 삭제 기능이 있는 /admin을 사용하려면 .env의 ADMIN_EMAIL에 로그인 이메일을 설정한다.
업데이트와 백업
업데이트할 때는 저장소를 가져오고 의존성을 설치한 뒤 빌드하고 pm2를 재시작한다.
cd ~/whatyousay
git pull
npm ci
npm run build
pm2 restart whatyousay
start.sh는 시작할 때마다 .env를 읽으므로 재시작하면 환경 변수 변경도 적용된다. 재시작은 처리 중인 음성 메시지를 최대 45초까지 기다린다.
SQLite 데이터베이스 백업 명령은 다음과 같다.
sqlite3 ~/whatyousay/whatyousay.db ".backup '/path/to/backup/whatyousay-$(date +%F).db'"
데이터베이스에 쓰는 다른 스크립트를 실행하기 전에는 서버를 중지해야 한다. SQLite 파일에 두 프로세스가 동시에 쓰면 기록이 조용히 유실될 수 있다.
pm2 stop whatyousay
설정과 구조
모든 설정은 .env에 있으며, env.example에 모든 변수가 설명돼 있다.
| 변수 | 필수 여부 | 용도 |
|---|---|---|
GROQ_API_KEY | 예 | Whisper 전사와 LLM 처리 단계 |
RESEND_API_KEY, EMAIL_FROM | 운영 환경에서 예 | 매직 링크 로그인 이메일 |
DB_ENCRYPTION_KEY | 운영 환경에서 예 | 저장된 WhatsApp 자격 증명 암호화. 값이 없으면 운영 환경에서 시작하지 않음 |
BASE_URL | 운영 환경에서 예 | 로그인 링크, 답장 하단, Telegram 웹훅에 사용하는 공개 URL |
ALLOWED_EMAILS | 운영 환경에서 예 | 로그인 허용 이메일 목록. 쉼표로 구분하거나 *로 가입을 개방 |
ADMIN_EMAIL | 아니요 | 해당 이메일의 로그인을 항상 허용하고 /admin을 활성화 |
GROQ_LLM_MODELS | 아니요 | LLM 단계에서 사용할 순서가 있는 대체 모델 목록. 기본값은 openai/gpt-oss-120b,openai/gpt-oss-20b |
TELEGRAM_* | 아니요 | Telegram 전사 봇과 운영자 알림 |
Groq는 모델을 때때로 지원 종료한다. LLM 모델이 사라지면 짧은 메시지는 계속 작동하지만 긴 메시지는 정리되지 않은 채 전송된다. Telegram 알림을 설정했다면 운영 알림도 발생한다. 후속 모델을 GROQ_LLM_MODELS의 첫 번째 항목으로 지정하고 서버를 재시작한다.
발신자 가명 처리 작업은 매일 Europe/Berlin 시간 기준 04:15에 실행되며 src/retention.ts에 있다.
앱은 SQLite(node:sqlite)를 사용하는 장시간 실행 Node.js·Express 프로세스 하나로 구성된다. WhatsApp 소켓, 실시간 대시보드 연결, 답장 캐시는 메모리에 둔다. 따라서 인스턴스는 정확히 하나만 실행해야 하며 클러스터 모드나 서버리스 환경에서는 사용할 수 없다.
| 경로 | 내용 |
|---|---|
src/whatsapp.ts | Baileys 연결, 메시지 처리, 재시도 확인 |
src/pipeline.ts | Whisper 처리 단계, 정리, 번역, 요약, 답장 형식 지정 |
src/transcript-format.ts | 정리 과정의 보호 장치와 언어별 충실도 검사 |
src/routes.ts, src/auth.ts | 웹 경로, 매직 링크 로그인, 세션, 설정 |
src/retention.ts | 매일 실행되는 발신자 가명 처리 |
src/crypto.ts | 저장된 WhatsApp 자격 증명 암호화 |
src/telegram-bot.ts, src/notify.ts | Telegram 봇과 운영자 알림 |
views/, public/ | 대시보드, 로그인, 관리자 페이지 |
evals/ | 평가 테스트 |
테스트
타입 검사와 오프라인 평가는 API 키나 네트워크 없이 실행할 수 있다.
npm run check # typecheck + offline evals (no API keys, no network)
npx playwright install chromium
npm run test:eval # browser tests; start the server separately first
브라우저 테스트는 테스트 사용자를 데이터베이스에 직접 쓴다. 운영 환경에서는 실행하지 말고 임시 데이터베이스를 사용하는 개발 체크아웃에서만 실행한다. 테스트 헬퍼는 .env를 읽지 않으므로 서버와 테스트를 시작하기 전에 셸에서 DB_PATH를 내보낸다.
export DB_PATH=/tmp/whatyousay-test.db
npm run dev # in one terminal
npm run test:eval # in another, with the same DB_PATH exported
브라우저 테스트 두 개(auth.spec.ts의 B01과 로그인 성공 화면 테스트)는 Resend의 @resend.dev 테스트 주소로 실제 이메일을 보낸다. 따라서 RESEND_API_KEY가 필요하다.
evals/pipeline-evals.ts, evals/whisper-ab.ts, evals/whisper-settings-matrix.ts, evals/benchmark-vs-chatgpt.ts는 Groq를 호출한다. 자체 녹음 파일을 evals/fixtures/<name>/audio.opus에 넣어 실행하며, expected.json, settings.json, reference.txt는 선택 사항이다. 해당 폴더는 Git에서 제외된다. 다른 사람의 음성 메시지를 커밋해서는 안 된다.
라이선스
프로젝트는 MIT 라이선스를 따른다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요