貼り紙やメモを撮ってWi‑Fi接続!QR生成までサクッとやってくれるAIエージェントを作った
概要
AI Agent Hackathon with Google Cloudが盛り上がっていますね!!
ところで、飲食店やホテルなど、さまざまな施設で掲示されるWi‑Fi接続情報。手入力だと面倒なうえに、入力ミスで接続トラブルが発生することも…。
喫茶店とかうどん屋さんとかにあるこういう貼り紙(あくまで手作りの一例です)
そこで今回作ったアプリは、スマホのブラウザで動き、Wi‑FiのSSIDやパスワードが記載された書類やポスターの写真をアップロードするだけで、LLMが即座に接続用のQRコードを生成しちゃうという画期的なAIエージェント。
名付けて「VTQ Wi‑Fi Reader」(Vision to QRの略)です!
後述しますが、Difyというローコードツールを使って作っています。この記事では、開発の背景から実装まで、初心者でも分かりやすく解説していきます。最後にはDSLファイルでインポート可能なプロジェクトファイルも公開しているので、ぜひ使ってみてください!
Difyとは?
今回の開発では、アプリ開発の基盤として「Dify」を使っています。
Difyは、プログラミングの知識がなくても直感的な操作だけでAIアプリが作れる、オープンソースのノーコードプラットフォームなんです。画面上でブロックを組み合わせるだけで、RAGや画像認識など、LLMを使った複雑な処理を簡単に実現できちゃうので、エンジニア以外の方にもおすすめ。
Difyの使い方や特徴、料金体系については、以下の外部記事も参考にしてみてくださいね。
プロジェクトについて
アプリの目的と背景
みなさんもご存知の通り、Wi‑Fi情報って紙に書かれて掲示されることが多いんですが、実際に接続するときには手入力が必要。これが面倒だったり、入力ミスで困ったりすることがたくさんあります。
そこで生まれたのが、スマホで撮影したWi‑Fi情報の写真からOCRでSSIDとパスワードを抽出し、その情報をもとにQRコードを作るAIエージェント「VTQ Wi‑Fi Reader」です。これで、手入力の煩わしさを解決したいと考えました。
対象ユーザー
このアプリは、以下のような方々におすすめします。
- 飲食店やホテルでのWi-Fi接続でポスターやカードに書かれた情報の手入力が煩わしい方
- 複数人のイベントや会議などで、グループ内へすぐにWi‑Fi情報を共有したい方。
課題
- 面倒な手入力を減らす
- 入力ミスが原因で接続できないことを防ぐ
- グループ内でのWi-Fi共有を簡単にする
ソリューション
写真から自動でQRコード生成!「VTQ Wi‑Fi Reader」は、さまざまな形で書かれたWi-Fi情報を掲示物から撮影し、そこからWi‑Fi設定のための情報を読み取って接続用のQRコードを生成します。
自分の入力の手間が減る&一緒に旅行に来ている友達にもQRコードを共有すれば、人気者になれるかも!?
設計
ワークフローの構成
※ワークフロー図
-
入力ブロック
- ファイル入力の機能を使い、画像の入力を受け取っています
-
読み取りブロック
- LLMブロックを使い、後述のプロンプトで画像の内容を読み取り、Wi-Fi接続のQRコードの変換元になる文字列を生成します
-
結果判定ブロック
- 条件分岐ブロックを使い、前のブロックの出力がWi-Fi接続QRコード用のフォーマットになっているかバリデートします。OKなら次に進み、ダメならエラーブロックに処理を流します
-
QR生成ブロック
- SSID、PW等が含まれる文字列を受け取り、QRコードに変換します
-
出力ブロック
- QR生成ブロックが生成したQRコードの画像をユーザーに表示します
-
テンプレートブロック
- エラー時の日本語の案内文を設定しています(例:"QRコード生成でエラーが発生しました。もう一度やり直してみてください。")
-
エラーブロック
- 読み取り、結果判定、QR生成の過程でエラーが起こった場合、このブロックに処理が分岐します。
設計時の思想と設計のポイント
設計において意識したり、工夫したりしたポイントを補足的に紹介します。
Gemini 1.5 flash モデルを使った高速化
画像から文字情報を抽出するために、Gemini APIでGeminiのLLMを使っています。画像処理が可能かつ高速な処理ができる「Gemini 1.5 flash」モデルを使っています。これにより、撮影された画像からSSIDやパスワードを素早いレスポンスで抽出できるようになっています。
Few Shot プロンプトとパラメータ設定による動作の安定化
出力例を3つ使ったFew Shot プロンプトと、低めのtemperature設定で、入力画像に対して安定した結果が得られるように工夫しました。これにより、どんな画像でも一貫した結果を出せるのがポイントです。
▼システムプロンプト
あなたは優秀なAIアシスタントです。
▼ユーザープロンプト
写真に含まれるSSID、パスワードの情報を読み取り、以下の形式で出力してください。SSIDはIDやid、パスワードはpassword、PASS、PW、など別の呼び方で書かれている場合もありますので可能な限り意味を汲み取って判定してください。
## 制約
以下の出力形式を守り、前置きや注釈は絶対に入れないでください。
## 出力形式
WIFI:S:{SSID};T:WPA;R:1;P:{パスワード};;
## 出力例
### 例1
WIFI:S:abcdefg;T:WPA;R:1;P:password;;
### 例2
WIFI:S:aiueo;T:WPA;R:1;P:kakikukeko;;
### 例3
WIFI:S:tanoshiwifi;T:WPA;R:1;P:tanoshipass;;
エラーハンドリング
Dify v0.14.2で新しく加わったエラーハンドリング機能を活用して、各処理で発生する可能性のあるエラーをしっかりキャッチするようにしました!
エラーの際には即座にエラーメッセージが表示され、再試行がスムーズに行えるユーザーに優しい設計になっています。
補足
QR生成にはDify組み込みの QR Code Generator が使えました
Difyに搭載されているQR Code Generatorを利用すれば、追加の開発なしで文字列をQRコードに変換できます。
上記のプロンプトで示している形式の文字列をQRコードに変換することで、自動的にWi-Fi接続するQRコードの完成となります。
参考:Wi-Fi QRコード仕様のメモ
インフラには GCP Compute Engine を使っています
システム全体は、Google Cloud PlatformのCompute Engine上に構築しています。OSS(オープンソースソフトウェア)を自前で運用することで、カスタマイズもしやすく、コスパも良いです。DIfyを GCE上に構築する詳細の手順については、下記の別記事を参考にしてみてください!
GCEへのDify構築とアプリ公開まで | 政党比較アプリ「elecnecta」
DifyワークフローのDSLを公開
DifyのワークフローはDSLというyml形式でインポート・エクスポートできるようになっています。
そのまま読み込んで使えるDSLを下記に共有するので、Difyの開発環境がある方はぜひご活用ください!
※任意のymlファイルに下記のデータをコピーしてインポートしてください。
app:
description: ''
icon: 🎛️
icon_background: '#EFF1F5'
mode: workflow
name: VTQ Wi‑Fi Reader
use_icon_as_answer_icon: false
kind: app
version: 0.1.5
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
allowed_file_extensions:
- .JPG
- .JPEG
- .PNG
- .GIF
- .WEBP
- .SVG
allowed_file_types:
- image
allowed_file_upload_methods:
- local_file
enabled: false
fileUploadConfig:
audio_file_size_limit: 50
batch_count_limit: 5
file_size_limit: 15
image_file_size_limit: 10
video_file_size_limit: 100
workflow_file_upload_limit: 10
image:
enabled: false
number_limits: 3
transfer_methods:
- local_file
- remote_url
number_limits: 1
opening_statement: ''
retriever_resource:
enabled: true
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
language: ''
voice: ''
graph:
edges:
- data:
isInIteration: false
sourceType: tool
targetType: end
id: 1739148258480-source-1739148598849-target
source: '1739148258480'
sourceHandle: source
target: '1739148598849'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: start
targetType: llm
id: 1739147741488-source-1739227949027-target
source: '1739147741488'
sourceHandle: source
target: '1739227949027'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: llm
targetType: if-else
id: 1739227949027-source-1739228623530-target
source: '1739227949027'
sourceHandle: source
target: '1739228623530'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: if-else
targetType: tool
id: 1739228623530-true-1739148258480-target
source: '1739228623530'
sourceHandle: 'true'
target: '1739148258480'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: if-else
targetType: template-transform
id: 1739228623530-false-1739260060776-target
source: '1739228623530'
sourceHandle: 'false'
target: '1739260060776'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: template-transform
targetType: end
id: 1739260060776-source-1739260006592-target
source: '1739260060776'
sourceHandle: source
target: '1739260006592'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: tool
targetType: template-transform
id: 1739148258480-fail-branch-1739260177287-target
source: '1739148258480'
sourceHandle: fail-branch
target: '1739260177287'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: template-transform
targetType: end
id: 1739260177287-source-1739228459344-target
source: '1739260177287'
sourceHandle: source
target: '1739228459344'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: template-transform
targetType: end
id: 17392602416500-source-1739260265322-target
source: '17392602416500'
sourceHandle: source
target: '1739260265322'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: llm
targetType: template-transform
id: 1739227949027-fail-branch-17392602416500-target
source: '1739227949027'
sourceHandle: fail-branch
target: '17392602416500'
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: 入力
type: start
variables:
- allowed_file_extensions: []
allowed_file_types:
- image
allowed_file_upload_methods:
- local_file
label: image
max_length: 48
options: []
required: true
type: file
variable: image
height: 90
id: '1739147741488'
position:
x: 30
y: 267
positionAbsolute:
x: 30
y: 267
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
error_strategy: fail-branch
provider_id: qrcode
provider_name: qrcode
provider_type: builtin
selected: false
title: QR生成
tool_configurations:
border: 2
error_correction: M
tool_label: Generate QR Code
tool_name: qrcode_generator
tool_parameters:
content:
type: mixed
value: '{{#1739227949027.text#}}'
type: tool
height: 152
id: '1739148258480'
position:
x: 947.7142857142856
y: 267
positionAbsolute:
x: 947.7142857142856
y: 267
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
outputs:
- value_selector:
- '1739233262907'
- text
variable: text
- value_selector:
- '1739148258480'
- files
variable: files
selected: false
title: 出力
type: end
height: 116
id: '1739148598849'
position:
x: 1237.4285714285716
y: 267
positionAbsolute:
x: 1237.4285714285716
y: 267
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: false
variable_selector: []
desc: ''
error_strategy: fail-branch
model:
completion_params:
temperature: 0.4
mode: chat
name: gemini-1.5-flash-002
provider: google
prompt_template:
- id: b6bfe8c8-4e07-4eb4-85a6-8fec04b2ac29
role: system
text: あなたは優秀なAIアシスタントです。
- id: b3de08ed-b179-43f7-821c-d19171d49e29
role: user
text: '写真に含まれるSSID、パスワードの情報を読み取り、以下の形式で出力してください。SSIDはIDやid、パスワードはpassword、PASS、PW、など別の呼び方で書かれている場合もありますので可能な限り意味を汲み取って判定してください。
## 制約
以下の出力形式を守り、前置きや注釈は絶対に入れないでください。
## 出力形式
WIFI:S:{SSID};T:WPA;R:1;P:{パスワード};;
## 出力例
### 例1
WIFI:S:abcdefg;T:WPA;R:1;P:password;;
### 例2
WIFI:S:aiueo;T:WPA;R:1;P:kakikukeko;;
### 例3
WIFI:S:tanoshiwifi;T:WPA;R:1;P:tanoshipass;;'
retry_config:
max_retries: 3
retry_enabled: false
retry_interval: 100
selected: false
title: 読み取り
type: llm
variables: []
vision:
configs:
detail: high
variable_selector:
- '1739147741488'
- image
enabled: true
height: 134
id: '1739227949027'
position:
x: 334
y: 267
positionAbsolute:
x: 334
y: 267
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
outputs:
- value_selector:
- '1739260177287'
- output
variable: output
selected: false
title: QR生成エラー
type: end
height: 90
id: '1739228459344'
position:
x: 1237.4285714285716
y: 461.59142750250976
positionAbsolute:
x: 1237.4285714285716
y: 461.59142750250976
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
cases:
- case_id: 'true'
conditions:
- comparison_operator: start with
id: f6ad7a10-f630-4317-a1db-70df00f05031
value: 'WIFI:'
varType: string
variable_selector:
- '1739227949027'
- text
- comparison_operator: contains
id: ba3eedef-d1bc-4f01-85d2-39a0d5fccc10
value: ;;
varType: string
variable_selector:
- '1739227949027'
- text
id: 'true'
logical_operator: and
desc: ''
selected: false
title: 結果判定
type: if-else
height: 152
id: '1739228623530'
position:
x: 639.4285714285714
y: 267
positionAbsolute:
x: 639.4285714285714
y: 267
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
outputs:
- value_selector:
- '1739260060776'
- output
variable: output
selected: false
title: フォーマットエラー
type: end
height: 90
id: '1739260006592'
position:
x: 1237.4285714285716
y: 577.0258225588977
positionAbsolute:
x: 1237.4285714285716
y: 577.0258225588977
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
selected: false
template: 無効な文字列が読み込まれました。もう一度やり直してみてください。
title: エラー2
type: template-transform
variables: []
height: 54
id: '1739260060776'
position:
x: 947.7142857142856
y: 577.0258225588977
positionAbsolute:
x: 947.7142857142856
y: 577.0258225588977
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
selected: false
template: 'QRコード生成でエラーが発生しました。もう一度やり直してみてください。
'
title: エラー3
type: template-transform
variables: []
height: 54
id: '1739260177287'
position:
x: 947.7142857142856
y: 461.59142750250976
positionAbsolute:
x: 947.7142857142856
y: 461.59142750250976
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
selected: false
template: 読み取り時にエラーが発生しました。もう一度やり直してみてください。
title: エラー1
type: template-transform
variables: []
height: 54
id: '17392602416500'
position:
x: 947.7142857142856
y: 693.5209932534674
positionAbsolute:
x: 947.7142857142856
y: 693.5209932534674
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
outputs:
- value_selector:
- '17392602416500'
- output
variable: output
selected: false
title: 終了 4
type: end
height: 90
id: '1739260265322'
position:
x: 1237.4285714285716
y: 693.5209932534674
positionAbsolute:
x: 1237.4285714285716
y: 693.5209932534674
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: 17.477273307867563
y: -168.46961960478973
zoom: 0.9972185713510199
※DIfyとGemini APIの連携をしていない場合、LLMプロバイダの設定を別途行う必要があります。
まとめと今後の展望
今回は「VTQ Wi‑Fi Reader」を通して、スマホで手軽にWi‑Fi接続QRコードを発行するアプリの開発事例と、その技術的なポイントを解説しました。
今後はフロントエンドの改善をしていきたいと思っています。
今回はDify組み込みのフロントエンド表示機能を使いましたが、DifyにはバックエンドAPIとしてワークフローを公開する機能があるので、よりシンプルかつユーザーフレンドリーなUIの実装に挑戦したいですね。例えば、アプリ内でカメラを起動して画像撮影するとか・・・
Discussion