スマホで撮影した名刺画像をSalesforceにアップロードする機能を自作しました

* Google Formで名刺画像を受信 → SheetsにURL自動保存
* GASトリガーで Vision OCR → GPT-4 Turbo が会社名・氏名などを JSON で抽出
* 抽出結果を Sheets に展開し、そのまま Web-to-Lead で Salesforce に登録
* Salesforce API 不要だから Essentials や無償エディションでも動く
* 月100枚程度なら API コストは数百円、5名以下の零細でも即導入可

GoogleForm上で名刺を撮影し、送信するとSalesforceに名刺情報が対応する項目に書き分けてレコードとして登録する機能を構築しました。
Salesforce側のAPIは使用していないので、API無しのエディションでも可能です。

### 自作した背景
名刺をスマートフォンで撮影しSalesforceにアップロードする機能はAppExchange製品にはいくつかあります。
以前は某社が提供している無料のAppExchangeがありましたが現在は廃止されています。
そして有料の製品も一般的には良心的な価格設定ではあるものの、弊所のような5名以下でしかも名刺交換するのはほぼ１人だけという小規模事業者には現実的ではない金額です。

とはいえ仕事柄名刺交換は多いので悩んでいたところ、名刺管理アプリを自作された方の記事を拝見しました。
https://qiita.com/watanabe-tsubasa/items/12dc7ba9a6de55e8afd9

某社に抹殺されないか心配しつつ記事を読み進めると、なんとGoogleスプレッドシートにデータ保管する仕組みではないですか。
これであれば、以前私が書いた以下記事と同様にGoogleform→スプシ→SalesforceというGASだけ連携でイケるかも…！と思い、実装してみました。これで私も抹殺予備軍です。
https://qiita.com/Lucy_kgsmec/items/f96a01ced2b8a6ee81ee


## 全体像

```mermaid
sequenceDiagram
  actor User as ユーザー
  participant Form as Google Form
  participant Drive as Google Drive
  participant Sheets as Google Sheets
  participant GAS as GAS
  participant Vision as Vision API
  participant GPT as ChatGPT API
  participant SF as Salesforce
  actor Admin as システム管理者

  User->>Form: 名刺写真アップロード
  Form->>Drive: ファイル保存
  Form->>Sheets: URL書込
  Sheets->>GAS: onFormSubmit

  GAS->>Vision: OCR依頼
  Vision-->>Drive: OCR読み込み
  Vision-->>GAS: OCR文字列
  GAS->>Sheets: OCR文字書込
  Sheets->>GAS: onEdit

  GAS->>GPT: 構造化依頼
  GPT-->>GAS: 構造化データ
  GAS->>Sheets: 構造化データ書込
  Sheets->>GAS: onEdit
  GAS->>SF: Web-to-Lead POST
  SF-->>Admin: (Salesforce標準のweb-to-lead 失敗通知)
```

### GoogleFormの作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017914/49d3057e-3579-4e7f-b2e3-3d6e2fd77130.png)

名刺画像を入れられるように画像を追加してください。

### スプレッドシートの作成
Googleformからスプレッドシートを作成します（回答→スプレッドシートを表示）
必要な列を足していきます。
Salesforceのリード項目に対応させるように適宜調整ください。

| 列番号 | 列名                           |
|:------:|:------------------------------|
| A      | タイムスタンプ                 |
| B      | OCR結果全文                    |
| C      | 名刺画像                       |
| D      | 会社名                         |
| E      | 姓                             |
| F      | 名                             |
| G      | ミドルネーム                   |
| H      | 役職                           |
| I      | メールアドレス(個人)           |
| J      | 電話(会社)                     |
| K      | 電話(携帯)                     |
| L      | 郵便番号                       |
| M      | 名刺交換時のメモ               |
| N      | 列 13                         |
| O      | 都道府県                       |
| P      | 市区郡                         |
| Q      | 番地                           |
| R      | web                            |
| S      | その他名刺に記載された内容     |

### Google CLoud Vision APIのAPIキーを取る
公式をご覧ください
https://cloud.google.com/vision/docs/ocr?hl=ja
### OpenAIのAPIキーを取る
公式をご覧ください
https://platform.openai.com/docs/overview

### GASを書く
ここはほぼChat GPTさんに書いてもらいました。
大枠こうやったらできるだろ、みたいなところだけ考えましたらあとはお任せしてしまいました。
ここからは作成した本人のGPTさんにコードを解説してもらっています。

#### 1. フォーム回答の取得・画像ダウンロード
```js
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("フォームの回答 1");
var lastRow = sheet.getLastRow();
var imageUrl = sheet.getRange(lastRow, 3).getValue();
```
「フォームの回答」シートを取得
→ Google Form の回答を格納しているシートを開きます。

最新行（lastRow）を参照
→ 投稿されたばかりの回答だけを対象にします。

C列から画像のURLを取得
→ Google Drive に保存されたファイルの URL が here に入っています。

```js
var fileId = imageUrl.split("id=")[1];
var file = DriveApp.getFileById(fileId);
var blob = file.getBlob();
```
ファイルIDの抽出
→ URL の ...?id=ファイルID 部分を切り出し、Drive から Blob（バイナリ）を取得します。

#### 2.Google Cloud Vision で OCR
```js
var visionApiUrl = "https://vision.googleapis.com/v1/images:annotate?key=" + visionApiKey;
var requestBody = {
  requests: [{
    image: { content: Utilities.base64Encode(blob.getBytes()) },
    features: [{ type: "TEXT_DETECTION" }]
  }]
};
var response = UrlFetchApp.fetch(visionApiUrl, {
  method: "post",
  contentType: "application/json",
  payload: JSON.stringify(requestBody)
});
var extractedText = JSON.parse(response.getContentText())
                     .responses[0].textAnnotations[0].description;
sheet.getRange(lastRow, 2).setValue(extractedText);

```
APIエンドポイントへ POST
→ 画像を base64 化して Vision API に渡し、テキスト検出を依頼します。

OCR結果の取得
→ textAnnotations[0].description に認識文字列全文が格納されるので B列に書き込み。

待機 (Utilities.sleep)
→ シートの反映を安定させるために数秒待っています。

### 3.ChatGPTで構造化解析
```js
var prompt = "以下のテキストは名刺のOCR結果です…\n\n" + extractedText;
var requestBody = {
  model: "gpt-4-turbo",
  messages: [
    { role: "system", content: "あなたは名刺を解析するAIアシスタントです。" },
    { role: "user", content: prompt }
  ],
  temperature: 0.5
};
var jsonResponse = JSON.parse(UrlFetchApp.fetch(openAiUrl, {
  method: "post",
  contentType: "application/json",
  headers: { Authorization: "Bearer " + openAiApiKey },
  payload: JSON.stringify(requestBody)
}).getContentText());
var extractedInfo = jsonResponse.choices[0].message.content;

```
プロンプトで名刺の各項目を指定
「会社名」「姓」「名」「役職」「メールアドレス」…といったフィールドをリクエストします。

ChatGPTの返答を取得
→ 期待通りにキーと値のペアが改行区切りで返ってくる想定。

```js
function extractField(lines, key) {
  var line = lines.find(line => line.includes(key));
  if (!line) return "";
  var parts = line.split(/[:＝→]/);
  return parts.length > 1 ? parts[1].trim() : "";
}

```
パース用ユーティリティ
→ 各行を key （例：「会社名(CompanyName)」）で検索し、: や = などで分割して右側の値だけを抜き出す仕組みです。

最後に、抽出した各フィールドを D～S 列にそれぞれ書き込みます。

### 4. Salesforce への Web-to-Lead 送信
```js
function sendToSalesforce(sheet, lastRow) {
  var payload = {
    oid: "Salesforceの組織Idを入れてください",
    first_name: sheet.getRange(lastRow, 6).getValue(),
    last_name: sheet.getRange(lastRow, 5).getValue(),
    company: sheet.getRange(lastRow, 4).getValue(),
    email: sheet.getRange(lastRow, 9).getValue(),
    phone: sheet.getRange(lastRow, 11).getValue(),
    street: sheet.getRange(lastRow, 15).getValue(),
    city: sheet.getRange(lastRow, 14).getValue(),
    state: sheet.getRange(lastRow, 13).getValue(),
    zip: sheet.getRange(lastRow, 12).getValue(),
    description: sheet.getRange(lastRow, 17).getValue()
  };
  UrlFetchApp.fetch("https://webto.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8", {
    method: "post",
    payload: payload
  });
}

```
Web-to-Lead エンドポイント に必要なパラメータをすべて詰め込み、POST するだけ。
web-to-leadフォームHTMLジェネレーターを使うとわかりやすいと思います。
コード内の番号はスプレッドシートの列の順番で対応付けします。
これで名刺情報が Salesforce のリードとして自動登録されます。

#### ChatGPTさんのまとめ
* onFormSubmit → 最新回答行を取得
* Vision API → OCR → B列に書込
* onEdit（OCR完了後想定） → ChatGPT へ投げる → 各フィールドを D～S 列に書込
* Web-to-Lead → Salesforce にリードを登録

※コード内ではわかりやすさ優先でAPIキーを露出していますが、実際にはスクリプトプロパティに入れてScriptProperties.getProperty() に置き換えてください。

…と、ここまでGPTさんでした。

### 品質について
読み込みの品質については先の記事の方の通りです。私は充分だと感じています。
また、Salesforce側で発生したエラーについてはスプシに書き戻すのではなく、Salesforceの標準のweb to leadのエラー処理メールで確認出来るようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017914/e1fc759b-f762-472f-9c4d-0488004521a9.png)

### セキュリティについて
OpenAIはAPI経由の場合には学習に使わないと言っているので信じることにしています。
むしろGoogleとOpenAIとSalesforceしか使っていないので、どこかの会社が管理しているより安心かもしれません。