Bizcard — 명함 스캐너 + 연락처 관리
명함 이미지를 받으면 자동 감지 → 전처리 → OCR → 사용자 확인 → Google Contacts 저장까지 처리한다.
Pipeline Overview
CODEBLOCK0
1. 자동 감지 (Trigger Detection)
키워드 매칭 (API 호출 없음)
메시지에 다음 키워드가 포함되면 즉시 명함 처리 모드 진입:
- - 한국어: "명함", "연락처 저장", "연락처 추가"
- 영어: "bizcard", "business card", "save contact"
이미지 분석 (키워드 없이 이미지만 올라왔을 때)
imageModel에 다음을 요청:
CODEBLOCK1
- - YES → 명함 처리 진행
- NO → 무시 (다른 스킬에 넘기거나 일반 응답)
2. 이미지 전처리 (ImageMagick)
2-1. 품질 평가
imageModel에 요청:
CODEBLOCK2
2-2. 전처리 실행 (NEEDS_PROCESSING일 때만)
CODEBLOCK3
전처리된 이미지로 OCR 진행.
2-3. SKIP일 때
원본 그대로 OCR 진행. 전처리 건너뜀.
3. OCR 필드 추출
imageModel(Gemini Flash)에 다음 JSON 구조로 추출 요청:
CODEBLOCK4
4. 전화번호 정규화
한국 번호를 국제 형식으로 변환:
| 원본 | 변환 |
|---|
| INLINECODE0 | INLINECODE1 |
| INLINECODE2 |
+82-2-1234-5678 |
|
031-123-4567 |
+82-31-123-4567 |
규칙:
- -
0으로 시작하는 한국 번호 → 앞의 0을 +82-로 교체 - INLINECODE9 로 시작하는 해외 번호 → 원본 유지
- 숫자 사이
- 또는 공백은 -로 통일
5. 이름 처리 규칙
5-1. 한국식 명함 (config: koreanStyleName=true, 기본값)
한국에서는 성과 이름을 분리하지 않는다. 비즈니스에서 "홍길동 대표", "김갑돌 과장"처럼 풀네임이 하나의 단위다.
People API 저장 시:
- -
familyName → 비움 (빈 문자열) - INLINECODE14 → 풀네임 (예:
홍길동) - INLINECODE16 → config 포맷 적용된 이름 (예:
#홍길동 과장)
5-2. 외국 명함 (config: koreanStyleName=false이거나, OCR language가 ko가 아닌 경우)
외국인은 first name / last name 분리가 기본:
- -
givenName → first name (예: John) - INLINECODE21 → last name (예:
Smith) - INLINECODE23 → 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 저장:
CODEBLOCK5
규칙: 독음은 성/이름 분리하지 않는다. 풀네임 독음을 phoneticGivenName에 통째로 넣는다.
예시: Kweh Hoong Wayne → phoneticGivenName=퀘 훙 웨인
예시: François Dupont → phoneticGivenName= INLINECODE31
효과: Google Contacts에서 "퀘 훙 웨인"으로 검색해도 해당 연락처를 찾을 수 있다.
INLINECODE32 이면 독음 생략.
6. Name 포맷 적용
config 설정을 읽어 unstructuredName (displayName)을 생성한다.
적용 순서
CODEBLOCK6
조합 예시
| hashtag | appendTitle | appendCompany | 결과 |
|---|
| off | off | off | INLINECODE34 |
| on |
off | off |
#홍길동 |
| off | on | off |
홍길동 과장 |
| off | off | on |
홍길동 (ABC주식회사) |
| on | on | on |
#홍길동 과장 (ABC주식회사) |
People API 저장 시 (한국식)
- -
names[].unstructuredName → 포맷된 displayName (예: #홍길동 과장) - INLINECODE41 → 풀네임 (예:
홍길동) - INLINECODE43 → 비움
People API 저장 시 (외국식)
- -
names[].unstructuredName → 포맷된 displayName (예: #John Smith, VP) - INLINECODE46 → first name (예:
John) - INLINECODE48 → last name (예:
Smith)
7. 사용자 확인 플로우
OCR + 포맷 결과를 아래 템플릿 그대로 출력한다. 포맷 변경 금지.
한국 명함 템플릿 (이 포맷 그대로 사용):
CODEBLOCK7
거점이 1개뿐이면 📍 label 없이 한 줄로:
CODEBLOCK8
외국 명함 템플릿 (koreanReading=true, 이 포맷 그대로 사용):
CODEBLOCK9
규칙:
- - 이모지 순서 고정: 👤🏢💼📱📞📠📧🌐📍🖼️
- 없는 필드는 해당 줄 자체를 생략 (빈 줄 금지)
- 부가 설명이나 "20년차 전문가" 같은 OCR 잡데이터를 이름에 넣지 마라. 타이틀 필드에만 표시.
- 이름 라인(👤)에는 config 포맷만 적용: INLINECODE50
- 직함은 💼 줄에 별도 표시. 👤 줄의 직함은 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"로 표시.
응답 처리
| 입력 | 동작 |
|---|
| INLINECODE55 | 저장 진행 |
| INLINECODE56 |
"뭘 수정할까?" 물은 뒤, 수정 후 다시 1/2/3 선택 |
|
3 | 취소 |
번호 외 텍스트 입력도 허용: "ㅇㅇ", "ok" → 1번 처리, "취소" → 3번 처리.
하지만 번호 입력을 우선 유도한다.
8. 명함 이미지 보정 + 사진 업로드 (항상 실행)
INLINECODE58 이면 무조건 실행한다. 모든 명함을 일관되게 보정한다.
말로만 "적용했어" 하지 말고 실제로 exec 도구로 명령어를 실행해라.
8-1. 원본 이미지를 /tmp에 복사
CODEBLOCK10
8-2. Nano Banana Pro로 이미지 보정 (핵심!)
원본 이미지를 Nano Banana Pro에 보내서 배경 제거 + 정면 보정 + 1:1 정사각형으로 변환.
CODEBLOCK11
결과: $BIZCARD_TMP/clean.jpg — 1024x1024, 배경 제거, 정면 보정, 1:1 정사각형.
8-3. 보정된 이미지를 Google Contacts에 업로드
clean.jpg를 업로드한다. raw.jpg가 아니다.
CODEBLOCK12
8-4. 임시 파일 삭제
CODEBLOCK13
절대 금지:
- - 보정을 "실행했다"고 말만 하고 실제 exec를 안 하는 것
- clean.jpg 대신 raw.jpg(원본)를 업로드하는 것
- ImageMagick
-deskew나 -distort Perspective 사용 (Nano Banana Pro가 전부 처리)
Nano Banana Pro API 실패 시: 원본(raw.jpg)을 그대로 업로드하고 "이미지 보정 실패: 원본으로 저장" 알림.
9. 중복 감지 (자동 2필드 매칭)
저장 전 반드시 이름 + 휴대폰 번호 2개 필드로 기존 연락처를 자동 검색한다. 사용자가 요청하지 않아도 항상 실행.
검색 로직
CODEBLOCK14
번호 검색 시 정규화된 +82 포맷과 원본(010) 포맷 모두로 검색하여 누락을 방지한다.
매칭 판정
| 이름 일치 | 번호 일치 | 판정 | 동작 |
|---|
| ✅ | ✅ | 확정 중복 | 사용자에게 옵션 제시 |
| ✅ |
❌ |
동명이인 가능 | 사용자에게 알림 후 진행 |
| ❌ | ✅ |
번호 재사용 가능 | 사용자에게 알림 후 진행 |
| ❌ | ❌ |
신규 | 바로 저장 |
확정 중복 시 출력 (이름+번호 모두 일치)
아래 템플릿을 그대로 출력한다. 포맷 변경 금지.
CODEBLOCK15
부분 일치 시 출력 (이름만 또는 번호만 일치)
아래 템플릿을 그대로 출력한다. 포맷 변경 금지.
CODEBLOCK16
신규 (중복 없음)
중복 없으면 중복 관련 메시지 자체를 출력하지 않는다. 바로 사용자 확인으로 넘어간다.
10. Google Contacts 저장
Maton API Gateway (People API)를 사용. 전체 필드 + 커스텀 Name + 사진 지원.
연락처 생성
CODEBLOCK17
명함 사진을 연락처 사진으로 저장 (cardAsPhoto=true)
Section 8의 파이프라인을 따른다. 보정 + 업로드는 Section 8에서 일괄 처리.
파이프라인 종료 시 (성공, 취소, 에러 무관) $BIZCARD_TMP 디렉터리 전체 삭제 (rm -rf "$BIZCARD_TMP").
다른 세션의 파일을 건드리지 않는다.
11. 설정 관리 — /bizcard config
설정 조회
사용자가 /bizcard config 또는 "명함 설정"을 입력하면, 아래 템플릿을 그대로 복사해서 출력한다. 절대 다른 포맷으로 바꾸지 마라. on/off 값만 config.json 실제 상태에 맞춰 교체.
출력 템플릿 (이 포맷 그대로 사용):
CODEBLOCK18
이모지 규칙: on → 🟢, off → ❌. 값 바로 뒤에 이모지 붙인다.
규칙:
- - 번호, 항목명, 설명은 고정. 변경 금지.
- INLINECODE71 /
off 값만 config.json 실제 상태에 맞춰 출력. - 불릿 리스트(•)나 다른 포맷으로 변환 금지.
- 부가 설명이나 "원하면 바꿔줄게" 같은 멘트 추가 금지.
설정 변경 — 번호 토글
번호만 입력하면 현재 상태의 반대로 자동 전환된다. config.json 파일을 실제로 수정하고, 아래 응답 포맷 그대로 출력.
세션 규칙: /bizcard config 실행 후 다음 1회 입력만 설정 변경으로 처리한다. 그 이후의 숫자 입력은 일반 대화로 취급. 설정을 연속 변경하려면 /bizcard config를 다시 입력.
예시 1: 현재 3. Append company : off 상태에서 사용자가 3 입력
응답 (이 포맷 그대로):
CODEBLOCK19
예시 2: 현재 1. Hashtag : on 상태에서 사용자가 1 입력
응답 (이 포맷 그대로):
CODEBLOCK20
규칙: 변경된 항목만 한 줄 출력 + "All set now." 그 외 추가 멘트 금지.
특수 명령
| 코드 | 동작 | 설명 |
|---|
| INLINECODE79 | Reset | 기본값으로 초기화 |
| INLINECODE80 |
All on | 전체 설정 켜기 |
|
49 | All off | 전체 설정 끄기 |
입력: 49
응답:
CODEBLOCK21
입력: 00
응답:
CODEBLOCK22
번호-키 매핑 (고정)
| # | config key | 설명 |
|---|
| 1 | INLINECODE84 | 이름 앞에 # 추가 (카카오톡 자동추가 방지) |
| 2 |
appendTitle | 이름 뒤에 직함 추가 |
| 3 |
appendCompany | 이름 뒤에
(회사명) 추가 |
| 4 |
cardAsPhoto | 명함 이미지를 연락처 사진으로 저장 |
| 5 |
koreanReading | 외국 명함 이름/회사를 한국어 독음으로 기록 |
| 6 |
koreanStyleName | 한국 명함: familyName 비우고 givenName에 풀네임 |
기본값 (00 Reset 시 적용)
CODEBLOCK23
12. 첫 사용 온보딩
사용자가 처음 명함을 보낼 때 (bizcard-log.jsonl이 없거나 비어있을 때):
CODEBLOCK24
이후에는 온보딩 없이 바로 처리.
13. 메모리 로깅
저장 성공 시 memory/bizcard-log.jsonl에 최소한의 정보만 기록한다. 이메일, 전화번호 등 민감한 PII는 저장하지 않는다. 상세 정보는 Google Contacts에서 resourceName으로 조회.
CODEBLOCK25
PII 최소화 원칙: 로그에는 이름과 회사명(검색용)만 남기고, 전화번호/이메일/주소는 저장하지 않는다.
14. 검색 & 조회
| 명령 | 동작 |
|---|
| "명함 검색 홍길동" / "bizcard search 홍길동" | People API searchContacts 실행 |
| "최근 명함" / "recent bizcards" |
bizcard-log.jsonl에서 최근 10건 표시 |
15. 에러 처리
| 에러 상황 | 사용자 메시지 |
|---|
| OCR 실패 / 빈 결과 | "텍스트를 인식하지 못했어. 더 선명한 사진으로 다시 보내줘" |
| Maton API 실패 |
"Google Contacts 연결을 확인해줘. API 키나 OAuth가 만료됐을 수 있어" |
| 빈 명함 (정보 없음) | "연락처 정보를 못 찾았어. 명함이 맞는지 확인해줘" |
| 네트워크 오류 | "네트워크 연결을 확인해줘" |
| 사진 업로드 실패 (연락처는 저장됨) | "연락처는 저장됐는데 사진 업로드가 실패했어. 다시 시도할까?" |
| config.json 파싱 오류 | 기본값으로 자동 복구 후 "설정 파일이 손상돼서 기본값으로 복구했어" |
Config Reference
설정 파일: skills/bizcard/config.json
| # | key | 기본값 | 설명 |
|---|
| 1 | INLINECODE96 | INLINECODE97 | 이름 앞에 # 추가 (카카오톡 자동추가 방지) |
| 2 |
appendTitle |
true | 이름 뒤에 직함 추가 |
| 3 |
appendCompany |
false | 이름 뒤에
(회사명) 추가 |
| 4 |
cardAsPhoto |
true | 명함 이미지를 연락처 사진으로 저장 |
| 5 |
koreanReading |
true | 외국 명함 이름/회사를 한국어 독음으로 기록 |
| 6 |
koreanStyleName |
true | 한국 명함: familyName 비우고 givenName에 풀네임 |
Dependencies
| Tool | Purpose | Install |
|---|
| INLINECODE110 | Maton API Gateway auth (People API proxy) | maton.ai/settings |
| INLINECODE111 |
Google Gemini API key (used by Nano Banana Pro for image correction) |
aistudio.google.com |
References
Bizcard — 名片扫描器 + 联系人管理
接收名片图像后,自动完成检测 → 预处理 → OCR → 用户确认 → 保存至 Google Contacts。
流程概览
接收图像 → 自动检测名片
→ Gemini Flash OCR → 字段解析 → 名称格式应用
→ 用户确认 → 重复检测 → Nano Banana Pro 校正 → 保存至 Google Contacts + 附加照片
1. 自动检测(触发检测)
关键词匹配(无需 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. 倾斜校正
magick $BIZCARD
TMP/raw.jpg -deskew 40% $BIZCARDTMP/deskew.jpg
2. 对比度增强 + 锐化
magick $BIZCARD
TMP/deskew.jpg -normalize -sharpen 0x1 $BIZCARDTMP/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 语言不是 ko 时)
外国人默认区分名/姓:
- - givenName → 名(例如:John)
- familyName → 姓(例如:Smith)
- unstructuredName → 应用 config 格式
5-3. 韩语音读(config: koreanReading=true)
对于外语名片,将姓名和公司名音译为韩语并记录:
| 原始 | 音读 |
|---|
| 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. 名称格式应用
读取 config 设置,生成 unstructuredName(显示名称)。
应用顺序
- 1. 基本名称:홍길동
- hashtag=true → #홍길동
- appendTitle=true → #홍길동 科长
- 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 → 格式化后的显示名称(例如:#홍길동 科长)
- names[].givenName → 全名(例如:홍길동)
- names[].familyName → 留空
保存至 People API 时(外式)
- - names[].unstructuredName → 格式化后的显示名称(例如:#John Smith, VP)
- names[].givenName → 名(例如:John)
- names[].familyName → 姓(例如:Smith)
7. 用户确认流程
将 OCR + 格式结果按以下模板原样输出。禁止更改格式。
韩文名片模板(使用此格式):
📇 名片识别结果
👤 #홍길동 科长 (ABC株式会社)
🏢 ABC株式会社 / 营业部
💼 科长
📱 +82-10-1234-5678
📧 gdhong@example.co.kr
🌐 www.example.co.kr