OpenClaw๋ฅผ โ๋ํํ ๋น์โ์์ ํ ๋จ๊ณ ์ฌ๋ ค, ์ค์ ๋ก ์ ํ๊น์ง ๊ฑฐ๋ ์คํํ ๋น์๋ก ๋ง๋๋ ๊ฐ์ด๋์ ๋๋ค. ์ด ๋ฌธ์๋ ๋ณต์กํ ์๋ฐฉํฅ ํตํ๋ด๋ณด๋ค, ๋จผ์ ์ค๋ฌด์์ ๋ฐ๋ก ์ฐ๋ ์ ํ ์๋ฆผ ์๋ํ์ ์ง์คํฉ๋๋ค.
์๋ด: ๋ณธ๋ฌธ์ ์์ฑํ AI๋ฅผ ํ์ฉํด ์ ๋ฆฌํ์ผ๋ฉฐ, ํ ํฐ/์ ํ๋ฒํธ ๋ฑ ๋ฏผ๊ฐ์ ๋ณด๋ ์์์์ ๋ง์คํนํ์ต๋๋ค.
flowchart LR A[OpenClaw ์ด๋ฒคํธ ๋ฐ์] --> B[์ ํ ๋ธ๋ฆฌ์ง API ํธ์ถ] B --> C[Twilio Voice API] C --> D[์์ ์ ์ ํ ์ฐ๊ฒฐ] D --> E[TTS ์๋ฆผ ์ฌ์]
0) ์ด ๋ฐฉ์์ด ์ข์ ์ด์
- ๋น ๋ฆ: 10~20๋ถ์ด๋ฉด ์ฒซ ํตํ ํ ์คํธ ๊ฐ๋ฅ
- ์์ ์ : ์ค์๊ฐ ์์ฑ ๋ํ๋ณด๋ค ์ฅ์ ํฌ์ธํธ๊ฐ ์ ์
- ํ์ฅ ์ฌ์: ๋์ค์ ์น์ธ/์๊ฐ์ ํ/๋ค์ค ์์ ์๋ก ํ์ฅ ๊ฐ๋ฅ
1) ์ค๋น๋ฌผ
- Twilio ๊ณ์ + Voice ๋ฐ์ ๋ฒํธ 1๊ฐ
- OpenClaw๊ฐ ๋์ ์ค์ธ ์๋ฒ 1๋
- ๋ธ๋ฆฌ์ง ์๋ฒ(๊ฐ์ ์๋ฒ ๊ฐ๋ฅ)
- ํ๊ฒฝ๋ณ์ 4๊ฐ
TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENTWILIO_FROM_NUMBER(์:+1...)CALL_BRIDGE_TOKEN(๋ธ๋ฆฌ์ง ํธ์ถ์ฉ ๋น๋ฐ ํ ํฐ)
1-1) ๋ฐ์ ๋ฒํธ๋ ์ด๋ป๊ฒ ๋ฐ๋? (Twilio์์ ๋ฐ๊ธ ๊ฐ๋ฅ)
๋ค, Twilio์์ ๋ฐ์ ๋ฒํธ๋ฅผ ์ง์ ๋ฐ๊ธ(๊ตฌ๋งค)ํ ์ ์์ต๋๋ค.
๋น ๋ฅธ ์์:
- Twilio ๊ณ์ ์์ฑ(Trial ๊ฐ๋ฅ)
- Console์์ Phone Number ๊ฒ์/๊ตฌ๋งค
Voice๊ฐ๋ฅ ๋ฒํธ์ธ์ง ํ์ธ ํ ๋ฐ๊ธ- ๋ฐ๊ธ๋ ๋ฒํธ๋ฅผ
TWILIO_FROM_NUMBER์ ์ค์
์ค๋ฌด ํ:
- Trial ๊ณ์ ์ ๊ธฐ๋ฅ ์ ํ์ด ์์ ์ ์์ด, ํ ์คํธ ๋์ ๋ฒํธ ๊ฒ์ฆ(Verified Caller ID)์ด ํ์ํ ์ ์์ต๋๋ค.
- ๊ตญ๊ฐ/๋ฒํธ ์ ํ(์ง์ญ๋ฒํธยท์์ ์ ๋ถ๋ด)์ ๋ฐ๋ผ ๊ท์ ๋ฌธ์(์ฃผ์/์ ๋ถ) ์ ์ถ์ด ํ์ํ ์ ์์ต๋๋ค.
์ฐธ๊ณ ๋งํฌ:
- Twilio Phone Numbers: https://www.twilio.com/en-us/phone-numbers
- Twilio Free Trial ์์: https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account
- ๊ท์ ์ปดํ๋ผ์ด์ธ์ค ์์: https://www.twilio.com/docs/phone-numbers/regulatory/getting-started
2) ์ต์ ๋ธ๋ฆฌ์ง API ๋ง๋ค๊ธฐ (FastAPI ์์)
์๋ ์์๋ OpenClaw/์คํฌ๋ฆฝํธ์์ POST๋ฅผ ๋ฐ์ผ๋ฉด Twilio๋ก ์ ํ๋ฅผ ๊ฑฐ๋ ์ต์ ๊ตฌํ์ ๋๋ค.
# file: call_bridge.py
import os
from fastapi import FastAPI, Header, HTTPException
from twilio.rest import Client
app = FastAPI()
client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
ALLOWLIST = {x.strip() for x in os.environ.get("CALL_ALLOWLIST", "").split(",") if x.strip()}
BRIDGE_TOKEN = os.environ["CALL_BRIDGE_TOKEN"]
@app.post("/call")
def make_call(payload: dict, authorization: str = Header(default="")):
if authorization != f"Bearer {BRIDGE_TOKEN}":
raise HTTPException(status_code=401, detail="unauthorized")
to = payload.get("to", "").strip()
msg = payload.get("message", "์๋ฆผ์ด ๋์ฐฉํ์ต๋๋ค.").strip()
if not to:
raise HTTPException(status_code=400, detail="to required")
if ALLOWLIST and to not in ALLOWLIST:
raise HTTPException(status_code=403, detail="number not allowed")
twiml = f"<Response><Say language='ko-KR'>{msg}</Say></Response>"
call = client.calls.create(
to=to,
from_=os.environ["TWILIO_FROM_NUMBER"],
twiml=twiml,
)
return {"ok": True, "callSid": call.sid}์คํ:
uvicorn call_bridge:app --host 0.0.0.0 --port 87873) OpenClaw์์ ์ ํ ํธ๋ฆฌ๊ฑฐํ๊ธฐ
๊ฐ์ฅ ๋จ์ํ ๋ฐฉ์์ OpenClaw ์์ ํ๋ฆ(์คํฌ๋ฆฝํธ/cron/์๋ ์คํ)์์ ๋ธ๋ฆฌ์ง API๋ฅผ ํธ์ถํ๋ ๊ฒ์ ๋๋ค.
curl -X POST http://127.0.0.1:8787/call \
-H "Authorization: Bearer $CALL_BRIDGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "+82xxxxxxxxxx",
"message": "๋์ฐ๋, 30๋ถ ๋ค ๋ฏธํ
์์์
๋๋ค."
}'4) ์ค๋ฌด ์์ ์ฅ์น (ํ์)
- ๋ฒํธ allowlist: ์ง์ ๋ฒํธ๋ง ๋ฐ์ ํ์ฉ
- ์๊ฐ ์ ํ: ์ฌ์ผ(์: 22:00~08:00) ๋ฐ์ ๊ธ์ง
- ์ฟจ๋ค์ด: ๊ฐ์ ๋ฒํธ ์ฐ์ ๋ฐ์ ์ต์ ๊ฐ๊ฒฉ(์: 10๋ถ)
- ์คํจ fallback: ํตํ ์คํจ ์ Telegram/๋ฌธ์ ์๋ฆผ ์ ํ
5) ํ ์คํธ ์ฒดํฌ๋ฆฌ์คํธ
- ๋ธ๋ฆฌ์ง API ํฌ์ค์ฒดํฌ(200) ํ์ธ
- ํ์ฉ ๋ฒํธ๋ก ํ ์คํธ ์ฝ ์ฑ๊ณต
- allowlist ๋ฏธ๋ฑ๋ก ๋ฒํธ ์ฐจ๋จ ํ์ธ
- ์ผ๊ฐ ์ฐจ๋จ/์ฟจ๋ค์ด ๋์ ํ์ธ
- ์คํจ ์ ๋์ฒด ์๋ฆผ(ํ ๋ ๊ทธ๋จ) ํ์ธ
6) ๋ค์ ํ์ฅ
- ๋จ์ TTS ์๋ฆผ โ โ์น์ธ ํ์/ํ์ธ ํ์โ ์์ฑ ๋ฉ๋ด(ํคํจ๋ ์ ๋ ฅ)
- ๋จ์ผ ๋ฒํธ โ ํ ๋จ์ ๋ผ์ฐํ
- ์ด๋ฒคํธ๋ณ ์ฐ์ ์์(๊ธด๊ธ๋ง ์ ํ, ์ผ๋ฐ์ ๋ฉ์์ง)
ํ ์ค ๊ฒฐ๋ก
Twilio ์ฐ๋์ OpenClaw๋ฅผ โ๋ง ์ํ๋ AIโ์์ ์ค์ ๋ก ๊นจ์์ฃผ๋ ์คํํ ๋น์๋ก ๋ฐ๊พธ๋ ๊ฐ์ฅ ์ฒด๊ฐ ํฐ ํ์ฅ์ ๋๋ค.