Bizcard
Business card scanner + Google Contacts manager. Auto-detects business card images, extracts contact info via OCR (imageModel), confirms with user, saves to Google Contacts with configurable name format and card photo. Trigger: image that looks like a business card, or keywords "명함", "bizcard", "연락처 저장". Settings: /bizcard config
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install skilldb:project820~bizcardcURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/skilldb%3Aproject820~bizcard/file -o bizcard.mdGit 仓库获取源码
git clone https://github.com/openclaw/skills/commit/e886ffefad46b3e62b464d87c5c218177e3302d4# Bizcard — 명함 스캐너 + 연락처 관리
명함 이미지를 받으면 자동 감지 → 전처리 → OCR → 사용자 확인 → Google Contacts 저장까지 처리한다.
## Pipeline Overview
```
이미지 수신 → 명함 자동 감지
→ Gemini Flash OCR → 필드 파싱 → Name 포맷 적용
→ 사용자 확인 → 중복 감지 → Nano Banana Pro 보정 → Google Contacts 저장 + 사진 첨부
```
---
## 1. 자동 감지 (Trigger Detection)
### 키워드 매칭 (API 호출 없음)
메시지에 다음 키워드가 포함되면 즉시 명함 처리 모드 진입:
- 한국어: "명함", "연락처 저장", "연락처 추가"
- 영어: "bizcard", "business card", "save contact"
### 이미지 분석 (키워드 없이 이미지만 올라왔을 때)
imageModel에 다음을 요청:
```
이 이미지가 명함(business card)인지 판단해.
명함이면 YES, 아니면 NO만 답해.
명함 = 사람의 이름, 회사명, 연락처 정보가 인쇄된 카드 형태.
음식, 풍경, 스크린샷, 메모, 영수증 등은 NO.
```
- YES → 명함 처리 진행
- NO → 무시 (다른 스킬에 넘기거나 일반 응답)
---
## 2. 이미지 전처리 (ImageMagick)
### 2-1. 품질 평가
imageModel에 요청:
```
이 명함 이미지의 품질을 평가해:
- 텍스트가 선명하게 읽히는가? (CLEAR / BLURRY)
- 기울어져 있는가? (STRAIGHT / TILTED)
- 전체적으로 전처리가 필요한가? (NEEDS_PROCESSING / SKIP)
JSON으로 답해: {"clarity": "...", "tilt": "...", "preprocessing": "..."}
```
### 2-2. 전처리 실행 (NEEDS_PROCESSING일 때만)
```bash
# 요청별 고유 디렉터리 생성
BIZCARD_TMP=$(mktemp -d /tmp/bizcard-XXXXXXXX)
# 1. Deskew (기울기 보정)
magick "$BIZCARD_TMP/raw.jpg" -deskew 40% "$BIZCARD_TMP/deskew.jpg"
# 2. 콘트라스트 개선 + 선명화
magick "$BIZCARD_TMP/deskew.jpg" -normalize -sharpen 0x1 "$BIZCARD_TMP/enhanced.jpg"
```
전처리된 이미지로 OCR 진행.
### 2-3. SKIP일 때
원본 그대로 OCR 진행. 전처리 건너뜀.
---
## 3. OCR 필드 추출
imageModel(Gemini Flash)에 다음 JSON 구조로 추출 요청:
```
이 명함 이미지에서 연락처 정보를 추출해. 다음 JSON 형식으로 답해.
읽을 수 없는 필드는 null로 남겨.
{
"name_ko": "한글 이름",
"name_en": "English name",
"company_ko": "한글 회사명",
"company_en": "English company name",
"title_ko": "한글 직함",
"title_en": "English title",
"department": "부서",
"mobile": ["개인 휴대폰 배열"],
"email": ["이메일 배열"],
"locations": [
{
"label": "본사",
"phone": ["063-000-0000"],
"fax": ["063-000-0001"],
"address_ko": "서울특별시 강남구 테헤란로 123",
"address_en": null
},
{
"label": "영업부",
"phone": ["02-000-0000"],
"fax": ["02-000-0001"],
"address_ko": "경기도 성남시 분당구 판교로 456",
"address_en": null
}
],
"website": ["웹사이트 배열"],
"notes": "기타 (SNS, 자격증 등)",
"language": "명함의 주 언어 (ko / en / ja / zh / other)"
}
```
---
## 4. 전화번호 정규화
한국 번호를 국제 형식으로 변환:
| 원본 | 변환 |
|------|------|
| `010-1234-5678` | `+82-10-1234-5678` |
| `02-1234-5678` | `+82-2-1234-5678` |
| `031-123-4567` | `+82-31-123-4567` |
규칙:
- `0`으로 시작하는 한국 번호 → 앞의 `0`을 `+82-`로 교체
- `+`로 시작하는 해외 번호 → 원본 유지
- 숫자 사이 `-` 또는 공백은 `-`로 통일
---
## 5. 이름 처리 규칙
### 5-1. 한국식 명함 (config: `koreanStyleName=true`, 기본값)
**한국에서는 성과 이름을 분리하지 않는다.** 비즈니스에서 "홍길동 대표", "김갑돌 과장"처럼 풀네임이 하나의 단위다.
People API 저장 시:
- `familyName` → **비움** (빈 문자열)
- `givenName` → **풀네임** (예: `홍길동`)
- `unstructuredName` → config 포맷 적용된 이름 (예: `#홍길동 과장`)
### 5-2. 외국 명함 (config: `koreanStyleName=false`이거나, OCR language가 ko가 아닌 경우)
외국인은 first name / last name 분리가 기본:
- `givenName` → first name (예: `John`)
- `familyName` → last name (예: `Smith`)
- `unstructuredName` → config 포맷 적용
### 5-3. Korean Reading (config: `koreanReading=true`)
외국어 명함일 때, 이름과 회사명을 한국어로 독음(transliteration)하여 기록:
| 원본 | 독음 |
|------|------|
| John Smith | 존 스미스 |
| Google LLC | 구글 |
| Toyota Motor | 토요타 모터 |
| François Dupont | 프랑수아 뒤퐁 |
**적용 방법:**
- imageModel에 "이 이름/회사명을 한국어 외래어 표기법으로 독음해줘" 요청
- **이름 독음 → People API `phoneticName` 필드에 저장** (검색 가능!)
- 회사명 독음 → `biographies`에 기록
**People API 저장:**
```
names[].phoneticGivenName = "퀘 훙 웨인" ← 풀네임 독음을 하나로
names[].phoneticFamilyName = "" ← 비움
```
**규칙:** 독음은 성/이름 분리하지 않는다. 풀네임 독음을 `phoneticGivenName`에 통째로 넣는다.
예시: `Kweh Hoong Wayne` → phoneticGivenName=`퀘 훙 웨인`
예시: `François Dupont` → phoneticGivenName=`프랑수아 뒤퐁`
**효과:** Google Contacts에서 "퀘 훙 웨인"으로 검색해도 해당 연락처를 찾을 수 있다.
`koreanReading=false`이면 독음 생략.
---
## 6. Name 포맷 적용
config 설정을 읽어 `unstructuredName` (displayName)을 생성한다.
### 적용 순서
```
1. 기본 이름: "홍길동"
2. hashtag=true → "#홍길동"
3. appendTitle=true → "#홍길동 과장"
4. appendCompany=true → "#홍길동 과장 (ABC주식회사)"
```
### 조합 예시
| hashtag | appendTitle | appendCompany | 결과 |
|:---:|:---:|:---:|------|
| off | off | off | `홍길동` |
| on | off | off | `#홍길동` |
| off | on | off | `홍길동 과장` |
| off | off | on | `홍길동 (ABC주식회사)` |
| on | on | on | `#홍길동 과장 (ABC주식회사)` |
### People API 저장 시 (한국식)
- `names[].unstructuredName` → 포맷된 displayName (예: `#홍길동 과장`)
- `names[].givenName` → 풀네임 (예: `홍길동`)
- `names[].familyName` → 비움
### People API 저장 시 (외국식)
- `names[].unstructuredName` → 포맷된 displayName (예: `#John Smith, VP`)
- `names[].givenName` → first name (예: `John`)
- `names[].familyName` → last name (예: `Smith`)
---
## 7. 사용자 확인 플로우
OCR + 포맷 결과를 **아래 템플릿 그대로** 출력한다. 포맷 변경 금지.
**한국 명함 템플릿 (이 포맷 그대로 사용):**
```
📇 명함 인식 결과
👤 #홍길동 과장 (ABC주식회사)
🏢 ABC주식회사 / 영업부
💼 과장
📱 +82-10-1234-5678
📧 gdhong@example.co.kr
🌐 www.example.co.kr
📍 본사:
📞 +82-63-450-3500 / 📠 +82-63-450-3517
서울특별시 강남구 테헤란로 123
📍 영업부:
📞 +82-2-597-8071~3 / 📠 +82-2-586-4388
경기도 성남시 분당구 판교로 456
🖼️ 명함 사진 → 연락처 프로필 사진으로 저장
1. 저장
2. 수정
3. 취소
```
**거점이 1개뿐이면** 📍 label 없이 한 줄로:
```
📞 +82-2-9876-5432
📠 +82-2-9876-5433
📍 서울시 강남구 테헤란로 123
```
**외국 명함 템플릿 (koreanReading=true, 이 포맷 그대로 사용):**
```
📇 명함 인식 결과
👤 John Smith (존 스미스)
🏢 Google LLC (구글)
💼 VP of Engineering
📱 +1-555-123-4567
📧 jsmith@example.com
🖼️ 명함 사진 → 연락처 프로필 사진으로 저장
1. 저장
2. 수정
3. 취소
```
**규칙:**
- 이모지 순서 고정: 👤🏢💼📱📞📠📧🌐📍🖼️
- 없는 필드는 해당 줄 자체를 생략 (빈 줄 금지)
- 부가 설명이나 "20년차 전문가" 같은 OCR 잡데이터를 이름에 넣지 마라. 타이틀 필드에만 표시.
- 이름 라인(👤)에는 config 포맷만 적용: `#이름 직함 (회사명)`
- **직함은 💼 줄에 별도 표시.** 👤 줄의 직함은 config appendTitle 적용 시에만, 짧게. ALL CAPS 금지.
- ✅ `👤 #Kweh Hoong Wayne` + `💼 Technical Service Manager`
- ❌ `👤 #Kweh Hoong Wayne TECHNICAL SERVICE MANAGER`
- 하단은 반드시 `1. 저장 / 2. 수정 / 3. 취소` 번호 선택. "저장할까?" 같은 서술형 금지.
- **복수 거점:** 명함에 주소가 2개 이상이면 **거점(location) 단위로 그룹핑**하여 출력.
각 거점은 📍 label + 해당 거점의 📞/📠/주소를 묶어서 표시.
명함에서 전화/팩스/주소가 시각적으로 분리되어 있으면 반드시 별도 거점으로 분리. 절대 병합하지 마라.
- 거점 label(본사/공장/영업부 등)은 명함에서 추출. 없으면 "거점1", "거점2"로 표시.
### 응답 처리
| 입력 | 동작 |
|------|------|
| `1` | 저장 진행 |
| `2` | "뭘 수정할까?" 물은 뒤, 수정 후 다시 1/2/3 선택 |
| `3` | 취소 |
**번호 외 텍스트 입력도 허용:** "ㅇㅇ", "ok" → 1번 처리, "취소" → 3번 처리.
하지만 **번호 입력을 우선 유도**한다.
---
## 8. 명함 이미지 보정 + 사진 업로드 (항상 실행)
`cardAsPhoto=true`이면 **무조건 실행**한다. 모든 명함을 일관되게 보정한다.
말로만 "적용했어" 하지 말고 **실제로 exec 도구로 명령어를 실행**해라.
### 8-1. 원본 이미지를 /tmp에 복사
```bash
BIZCARD_TMP=$(mktemp -d /tmp/bizcard-XXXXXXXX)
cp /path/to/원본명함이미지.jpg "$BIZCARD_TMP/raw.jpg"
```
### 8-2. Nano Banana Pro로 이미지 보정 (핵심!)
원본 이미지를 Nano Banana Pro에 보내서 **배경 제거 + 정면 보정 + 1:1 정사각형**으로 변환.
```bash
python3 <<'PYEOF'
import urllib.request, json, base64, os
with open(os.environ["BIZCARD_TMP"] + "/raw.jpg", "rb") as f:
img_b64 = base64.b64encode(f.read()).decode()
api_key = os.environ.get("NANO_BANANA_API_KEY", "")
if not api_key:
print("ERROR: NANO_BANANA_API_KEY not set")
exit(1)
url = f"https://generativelanguage.googleapis.com/v1beta/models/nano-banana-pro-preview:generateContent?key={api_key}"
payload = {
"contents": [{
"parts": [
{"inlineData": {"mimeType": "image/jpeg", "data": img_b64}},
{"text": "이 사진에서 명함 카드만 잘라내서 정면으로 보정한 이미지를 생성해줘. 배경 완전 제거, 명함만 남기고, 흰색 여백을 추가해서 1:1 정사각형 비율로 만들어줘. 명함 내용은 전부 보존해."}
]
}],
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"]
}
}
data = json.dumps(payload).encode()
req = urllib.request.Request(url, data=data, method="POST")
req.add_header("Content-Type", "application/json")
resp = urllib.request.urlopen(req, timeout=60)
result = json.loads(resp.read())
for part in result["candidates"][0]["content"]["parts"]:
if "inlineData" in part:
img_data = base64.b64decode(part["inlineData"]["data"])
with open(os.environ["BIZCARD_TMP"] + "/clean.jpg", "wb") as f:
f.write(img_data)
print(f"OK: clean.jpg ({len(img_data)} bytes)")
elif "text" in part:
print(f"Text: {part['text'][:100]}")
PYEOF
```
**결과:** `$BIZCARD_TMP/clean.jpg` — 1024x1024, 배경 제거, 정면 보정, 1:1 정사각형.
### 8-3. 보정된 이미지를 G