Xperia Z3 Tablet Compactを、話しかけると絵を描くフォトフレームにした
目次

前回はXperia Z3 Tablet Compact(SGP611)にLineageOS for microG 18.1を入れて、Wi-Fiにつながるところまで確認した。
今回はその続きで、自宅サーバーの画像や部屋のCO₂の値を常時表示し、話しかけると絵を描いて出すフォトフレームに仕立てた。
実験中に実機を不用意に操作すると、何が起きたのか分からなくなるので、実機には触らずに進めた。
アプリの導入から設定の変更、画面の撮影まで、USBでつないだPCから、Android端末を操作するADBのコマンドで済ませている。
作業した環境と構成
| 項目 | 内容 |
|---|---|
| 端末 | Xperia Z3 Tablet Compact SGP611(Wi-Fiモデル) |
| OS | LineageOS for microG 18.1(Android 11) |
| 画面 | 1200×1920、density 320 |
| PC | Windows 11 Home |
| 表示サーバー | Python(標準ライブラリとPillow) |
| アプリのビルド | Android Studio付属のJDK 21.0.6、SDK Platform 34、Build Tools 36.0.0 |
サーバーは3台を組み合わせていて、このうち表示サーバーを今回新しく作った。
| サーバー | 役割 |
|---|---|
| 音声チャットサーバー | StackChanと話すためのサーバー。StackChanが測ったCO₂・温度・湿度も受け取る |
| 画像生成サーバー | genserver。ComfyUIのAnimaなどで画像を作る |
| 表示サーバー | 今回作成。PCで動かし、タブレットへの画面の配信と、ほかのサーバーとの通信の中継を受け持つ |
音声チャットサーバーと画像生成サーバーはTailscaleの仮想ネットワークの中にあるが、タブレットはTailscaleに入れていない。
そこで同じLANにある表示サーバーを間に置き、必要な通信だけを取り次がせた。
flowchart TD
A["StackChan<br/>CO₂センサー"] -->|測定値を送る| B["音声チャットサーバー"]
C["表示サーバー(PC)"] -->|測定値を取得| B
C -->|生成を頼む| D["画像生成サーバー"]
E["SGP611<br/>常設アプリ"] -->|画面・録音・返事| C
C -->|会話を中継| B
サンプルのデータで画面を作る
まずは実データにつながず、サンプルの写真と値で画面の構成を決めた。
横向きでは左に写真、右に時計とCO₂・温度・湿度、サーバーの状態を並べ、縦向きでは情報欄を写真の下に回している。
サンプルの値を出している間は、画面に「サンプル値・実測ではありません」と表示しておいた。
表示サーバーがタブレットに配信するのは、画面のHTML、写真の一覧、各項目の値をまとめたJSON、写真の4つ。
写真は撮影時の向きの情報(EXIF)に合わせて回転し、長辺1920px以下のWebPにしてから送る。
最初は標準ブラウザのJellyで開いたが、横向きだとアドレスバーに高さを取られて、サーバー欄が下で切れた。
文字サイズを画面の高さに合わせて縮めれば全項目は収まるものの、アドレスバーとタイトルは消せない。
置きっぱなしのフォトフレームには邪魔なので、全画面で表示するアプリを作ることにした。
全画面のWebViewアプリ
アプリは保存したURLをWebViewで全画面に開くだけの作りにした。
ステータスバーとナビゲーションバーを隠し、表示している間は画面が消えないようにしてある。
外部ライブラリを使わないのでGradleは通さず、Android SDKのaapt2・d8・zipalign・apksignerとJDKのjavacを、Pythonのスクリプトから直接呼んでビルドした。
最初のビルドはjavacで失敗した。ラムダ式に必要な LambdaMetafactory がandroid.jarに入っていなかったためで、ラムダ式を無名クラスに書き換えて通している。
再起動したら表示に戻す
タブレットを再起動したときにそのまま表示へ戻るよう、アプリをホームアプリとして登録した。
再起動まわりでは次の3つでつまずき、どれもコマンドかアプリ側の設定で片付いた。
| 起きたこと | 対処 |
|---|---|
pm set-home-activity で登録すると、再起動時にホームアプリの選択ダイアログで止まる | cmd role でホームの役割をアプリに割り当てる |
| 端末の設定で横向きにしても、再起動で縦に戻る | アプリ側で横向きに固定する |
| 初回起動時に「全画面表示 終了するには、上から下にスワイプします」の案内が出る | settings put secure immersive_mode_confirmations confirmed で確認済みにする |
ホームの役割の割り当てには次のコマンドを使った。これを実行してからは再起動してもタップなしで表示に戻るようになった。
adb -d shell cmd role add-role-holder android.app.role.HOME io.github.hide3tu.kiosk 0
サーバーが止まっているときの待機画面
次に、表示サーバーを止めたままタブレットを再起動して、サーバーにつながらないときの動きを確かめた。
アプリが「表示できた」と判定する方法は、2回変えている。
| 試した順 | 判定の方法 | 結果 |
|---|---|---|
| 1回目 | ページの読み込み完了で成功とみなす | キャッシュに残ったHTMLとCSSだけが出て、JavaScriptが動かないまま止まった |
| 2回目 | 画面のファイルをキャッシュしない指定をサーバー側で付け、読み込み完了の10秒後にJavaScriptが動いているかを確かめる | 接続待ちのまま読み込みが進まず、完了も失敗もアプリに通知されないので、真っ黒のままだった |
| 3回目 | 読み込み開始から20秒後に、JavaScriptが動いているかを確かめる | 動いていなければ、アプリに入れておいた待機画面に切り替わった |
待機画面に切り替わった後は、5秒後、10秒後、30秒後、以降は60秒ごとに読み込み直す。
そのため、あとからPCで表示サーバーを起動すれば、タブレットに触らなくても元の表示に戻る。

常時表示したときの発熱
置きっぱなしで点けておく端末なので、温度を先に測った。
人が前に来たときだけ画面を明るくする案もあったため、フロントカメラで動きを見る機能を試作し、カメラなしとカメラありを30分ずつ動かして比べた。
動きの検知ではフロントカメラを最小のプレビューサイズ(176×144)と最低のフレームレートで開く。
1秒に1回、画面を16×12のマスに分けてマスごとの明るさを平均し、前回から大きく変わったマスが6つ以上あれば、動きとしてログに出す。画像は保存も送信もしない。
測った条件はAC給電・電池100%・自動輝度で、サンプルの画面を出したまま、誰も端末に触れていない。
| 項目 | カメラなし(30分) | カメラあり(30分) |
|---|---|---|
| 電池温度 | 30.2〜30.3℃で横ばい | 30.3℃から31.5℃まで上がり続けた |
| CPUの温度センサーの最高値 | 42〜49℃、平均46.1℃ | 43〜52℃、平均47.3℃ |
| CPU使用率(4コアで400%) | 約27% | 約85% |
| 消灯・アプリの異常終了 | 0件 | 0件 |
判定は1秒に1回でも、カメラを開いている間はカメラの処理系が動き続けるので、CPU使用率は約3倍になった。
内訳はmediaserverが23%、system_serverが15%、カメラの常駐プログラムが11%、センサーの処理が7%で、アプリ本体は5%しか使っていない。
カメラありのほうは30分の時点でまだ温度が上がっていたので、計測を2時間に延ばした。
電池温度はカメラを入れてから約40分で31.5℃前後に落ち着き、最高でも31.6℃だった。
後半には29.0℃まで下がったが、アプリとカメラの動作は変わらず、輝度はむしろ上がっていた。室温を測っていないので、下がった理由はわからない。
照度センサーと間欠カメラ
温度は問題なさそうだったが、カメラを開いたままではCPU使用率が約85%に張り付く。
部屋にいる間は照明を点けて、寝るときに消すという使い方なので、照度センサーで部屋の明るさを測り、明るい間だけカメラをときどき開く方式に変えた。
設定した動作は次のとおり。照度の単位はlx(ルクス)。
| 状況 | 動作 |
|---|---|
| 照度が10lx以下の状態が2分続いた | 画面を消し、カメラも止める |
| 照度が20lx以上に戻った | 画面を点ける |
| 部屋が明るい間 | 30秒ごとにフロントカメラを約4秒だけ開いて動きを調べる |
| 一定時間動きがない | 最低輝度にする |
| 給電中で部屋が明るいのに画面が消えた | 3秒後に点け直す |
| 電池温度が38℃以上 | カメラを止める。35℃以下で戻す |
| 電池温度が40℃以上 | 最低輝度にする。35℃以下で戻す |
画面を消すのにはAndroidのデバイス管理の画面ロック(lockNow)を使った。
端末のロック画面は前回無効にしてあるので、点け直したときにロック画面は出ない。
照明を変えずに試すため、しきい値を一時的に1000lxにして、室内(約131lx)を暗いと判定させた。
2分後に画面は消えたが、しきい値を10lxと20lxに戻しても点かなかった。
Androidの光センサーは、値が変わったときにしか通知を送らない。部屋の明るさが変わらない限り、アプリは判定をやり直さないので、しきい値を変えたときにも最後に受け取った値で判定し直すように直した。
カメラに誰も映っていなかった
この修正を入れてしばらくすると、画面が暗くなっていた。
照度は131lxのままで、10分間カメラに動きが映らなかったため、最低輝度に落としていた。
その日は誰も端末の前に来ていないので、検知0回は実際のとおりだった。
ただ、このときの置き場所ではカメラに人が映らなかったので、動きがないときの減光は外した。
電源ボタンを押した扱いで消えた
アプリを入れた直後の13時41分ごろにも、一度画面が消えている。
dumpsys power で電源の状態を見ると、消灯の理由は power_button になっていた。電源ボタンは押しておらず、カバーも付けていない。
getevent -pl で入力デバイスを調べても、電源キーの信号を出せるのは本体の電源ボタン(qpnp_pon)だけだった。
カバーの磁気センサーもあるが、そちらで消えたなら理由は lid_switch になる。
その後は端末上で getevent を動かし続けて電源ボタンの信号を記録したものの、発熱の計測中も含めて再発せず、原因はわからずじまいだった。
上の表にある「給電中に画面が消えたら3秒後に点け直す」処理は、この件への保険として入れたもの。
CO₂・温度・湿度をStackChanの実測値にする
StackChanはCO₂センサー(SCD41)の値を、5分おきか値が大きく変わったときに音声チャットサーバーへ送っている。
表示サーバーでその値を音声チャットサーバーから取ってきて、画面に出すようにした。
15分より古い値には「古い値」と添え、値が取れないときは0ではなく -- と「未取得」を出す。
サーバーの状態の欄は取得元を決めず、「未設定」と出すだけにした。

生成した画像を表示する
続いて、画像生成サーバーに絵を描かせ、写真と同じフォルダに保存して表示する仕組みを作った。
生成は頼んだときだけで、定期的には作らない。
PCから「夕焼けの海辺の小さな街並み、風景、人物なし」と頼むと、69秒で画像ができて画面に出た。
左下には生成に使ったプロンプトが出る。右側のCO₂などの値はこの時点ではまだサンプルのままだ。

かなちゃんの絵がくすむ
画像生成サーバーには、かなちゃんを描けるように追加学習したLoRAを使うオプションがある。
「晴れた日の公園で、桜の木の下でピクニックをしている、笑顔、明るい日差し」と頼むと、48秒でかなちゃんの絵が出た。

ただ、全体に色が少しくすんでいたので、Anima向けのプロンプトに変えて、もっと動きのある絵をぱきっと出せないか試した。
画像生成サーバーに残っている過去のプロンプトを見ると、Animaでは masterpiece, best quality, high quality, newest, year 2025, year 2024 の品質タグを先頭に置いて、英語のタグと文章を続けていた。
一方、桜の絵は日本語のプロンプトを画像生成サーバーに英訳させたうえ、8ステップで描く速いモデルを使っていた。そこを次のように変えた。
| 項目 | 桜の絵 | 変更後 |
|---|---|---|
| プロンプト | 日本語。画像生成サーバーが英訳する | 英語のタグ。翻訳を通さないオプションを付ける |
| モデル | anima_turbo_lora(8ステップ) | anima_quality(25ステップ) |
| 色と線の指定 | なし | vivid colors, high contrast, sharp lineart, cel shading など |
| 生成にかかった時間 | 48秒 | 220秒 |
この設定で跳び上がって腕を広げる場面を頼むと、青空と桜の色が濃くなり、線もはっきりした。
ただ、下からあおる構図(from below)とスカートがひるがえる指定(skirt flutter)が重なって、居間に置くには際どい絵になったので、この絵は載せていない。
1枚表示とスライドショー
最初は写真を60秒ごとに入れ替え、画像ができたら5分だけ表示してスライドショーに戻す作りだった。
とはいえ生成AIの画像をそう連発して作るわけではないので、入れ替えて眺めるときと、1枚だけ出しておくときを切り替えられるようにした。
| モード | 動き |
|---|---|
| 1枚表示(初期値) | 選んだ1枚を出し続ける。画像を生成し終えると、このモードに切り替わってその画像を出す |
| スライドショー | 写真を60秒ごとに入れ替える |
モードは表示サーバーに保存するので、サーバーやタブレットを再起動しても変わらない。
PCのスリープで止まった
作業した日の17時37分にPCがスリープし、そこで動いていた表示サーバーも止まった。
タブレットは待機画面に切り替わったが、照明を消したときの照度と消灯を一晩記録するつもりだった計測は、そこで途切れた。
表示サーバーをPCで動かしている以上、PCが止まれば表示も止まる。夜間の照度と消灯の確認は、この記事の作業の間にはやり直さなかった。
タブレットで話す
次に、タブレットのマイクとスピーカーで音声チャットサーバーと話せるかを試した。
表示サーバーは音声チャットサーバーの決まったパスだけを中継し、録音の送信、返事の取得、返事の音声のダウンロードを通す。
長期記憶の操作のようなほかの機能は通さない。
アプリには試験用の入口を入れてあり、PCから am start を送ると録音や再生が始まる。
adb -d shell am start -n io.github.hide3tu.kiosk/.MainActivity --es voicetest chat
声を出して録るまでに、音声まわりで3か所つまずいた。
| 起きたこと | 原因 | 対処 |
|---|---|---|
| 起動するたびに音量が最大に戻る | 大きく聞こえていたのは音楽ではなく、効果音や充電音だったとみられる | 効果音をすべて切る |
| 録音開始の「ピッ」が鳴らない | ToneGeneratorの音が、この端末では出ない | AudioTrackで作った正弦波を鳴らす |
| 内蔵マイクの入力が小さい | 音声認識向けの録音モードでは、自動の音量調整がかからない | 増幅してから送る |
音量が最大に戻る
試験の前から、起動するたびに音量が最大になっていた。電源を切る前に下げたはずなのに、また大きな音が鳴る。
ADBで音楽と着信音の音量を下げてから再起動すると、値はそのまま残っていた。
ところが音声チャットサーバーの待ち時間用の音声(フィラー)を流すと、声は小さいのに、効果音のような音だけが大きく鳴る。
設定を見ると、タッチ音、ロック画面の音、充電を始めたときの音、ダイヤル音が有効になっていた。
本体の音量ボタンで変わるのは音楽の音量だけで、着信・通知の音量は変わらない。
電源を切る前に下げたのは音楽の音量で、大きく聞こえていたのは効果音や充電音だったと考えられる。
効果音をすべて切り、音楽の音量を25段階の9にした。
合図の音が鳴っていなかった
録音の始まりがわかるよう、最初はAndroidのToneGeneratorで「ピッ」と鳴らしてから6秒録音する作りにした。
ところがこの端末ではToneGeneratorの音が出ず、合図なしで録音が始まっていた。
録ったデータはほぼ無音で、ピークは-26.7dBFS。dBFSはデジタル音声で出せる最大の音を0とする単位で、マイナスの数字が大きいほど小さい音になる。
無音なのに、文字起こしの結果には二十四節気や祝日、「かなちゃん」などの単語が並んでいた。
音声チャットサーバーは、聞き取りの手がかりになるヒント語を文字起こしに渡している。声が入っていないと、そのヒント語がそのまま出てくるとみられる。StackChanの音声チャットを作っていたときに無音を送ったのと同じ出方だ。
合図をAudioTrackで作った880Hz・0.3秒の正弦波に変えると、今度は「ぷー」と鳴った。
あわせて録音を15秒に延ばし、画面に「録音中」の表示も出して録り直すと、「今何時?」を正しく聞き取れた。
ただ、内蔵マイクの入力はかなり小さく、ピークは-23.8dBFSだった。
音声認識向けの録音モードでは自動の音量調整がかからないので、ピークが-6dBFSになるよう、最大30倍まで増幅してから送るようにした。
「録音中」の表示は約3.5秒で消える小さな通知(トースト)で見落としたため、話し始めるときは合図の音を頼りにした。
天気を聞いたら答えなかった
文字起こしができたので、返事を文ごとの音声で順に再生するところまでつないだ。録音は8秒にしている。
「今日の東京の天気は。」と聞くと、送信から10.3秒で1文目が流れ始め、19.7秒で返事が終わった。
ただ、返ってきたのは「ごめん、今日の東京の天気はわからないんだよね。窓の外を見てみるか、スマホで調べてみてね。」という答えだった。
StackChanで天気を聞いたときと同じで、タブレットではなく音声チャットサーバーの検索に問題がある。
音声チャットサーバーのPCで動いているClaudeに調べてもらうと、Web検索自体は動いていたものの、ModelScopeの検索で当日の東京の天気が見つかっていなかった。
そこで音声チャットサーバー側で、ModelScopeの検索をやめ、ddgsというPythonのライブラリで検索するツールをQwenに渡すようにした。ddgsはDuckDuckGoなど複数の検索サービスの結果をまとめて取ってくる。
天気のほうは、気象庁の予報のJSONを取ってきてプロンプトに入れる形に作り直した。
作り直した後に同じ質問をすると、今度は天気を答えた。
今日の東京はくもりで、所により夜に雨が降る見込みだよ。気温は20度から27度くらいで、夕方までは雨の可能性もあるから傘があると安心だね。
このとき取得した気象庁の東京地方の予報のJSONは19日17時発表の分で、このURLの中身は発表のたびに上書きされる。
それと比べると、20日の「くもり 所により夕方まで雨」と気温20℃・27℃は合っていたが、「所により夜に雨」は19日の天気の文だった。
聞いたのは午前1時台で、次の発表の前だったため予報の先頭に前日の分が残っていて、前日と当日の文が混ざったとみられる。
これを受けて音声チャットサーバー側で、期間の終わった予報を消してから渡すように直した。
続けて「ヤフーニュースから最新のニュース一覧取ってきて」と頼むと、「ヤフーニュースの最新記事は取得できないので、代わりに最新のニュースを検索して伝えるね。」の1文で返事が終わり、ニュースは1件も返ってこなかった。
こちらも調べてもらうと、Qwenはサイトを開けないと断る途中で「検索して伝えるね」と言い、検索のツールを呼ばないまま返事を終えていた。
検索まわりの作り直しは別の記事にまとめた。
声で絵を頼む
最後に、声で絵を頼めるようにした。
絵を描いてほしいという頼みかどうかの判断とプロンプト作りは音声チャットサーバーのQwenに任せ、画像生成と表示の切り替えは表示サーバーが受け持つ。
| 担当 | やること |
|---|---|
| 音声チャットサーバー | タブレットから来た会話のときだけ、Qwenに絵を描くツールを渡す。ツールが呼ばれたら、英語のプロンプト、かなちゃんを描くかどうか、速いモデルとくっきりしたモデルのどちらで描くかを返事に付ける |
| 表示サーバー | タブレットからの会話を中継するときに印を付ける。返事に描く指示があれば画像生成サーバーに回し、できたら1枚表示に切り替える |
画面のないStackChanからの会話では、このツールを渡さない。「描いて」に「描くね」と答えたのにどこにも絵が出ない、という状態を避けるためだ。
ツールの説明には、さきほどくっきりした絵が出たときのプロンプトの書き方を入れた。かなちゃんを描くときはQwenに見た目を書かせず、画像生成サーバーが足す説明に任せている。
表示中の絵を直す頼みにも応えられるようにした。
1枚表示で生成した絵を出しているときは、表示サーバーがその絵のプロンプトも一緒に送る。「もっと明るくして」と頼むと、Qwenはそのプロンプトを直して描き直す。
スライドショーの間は、どの絵が出ているかを表示サーバーが把握していないので、プロンプトは送らず、新しく描く扱いにした。
音声チャットサーバー側の変更は、やってほしいことを文書にまとめて渡し、同じClaudeに実装してもらった。
録音の開始をPCから送り、タブレットに「朝の公園をランニングしてるかなちゃんを描いて。」と話すと、「朝の公園をランニングするかなちゃんを描き始めるね、できたら画面に出すよ。」と返ってきた。
Qwenが作ったプロンプトは 1girl, solo, running, dynamic pose, morning park のようにAnima向けの書き方に沿っていて、かなちゃんの見た目は書かれていなかった。
生成を頼んでから40秒で絵ができ、話し終えてから約50秒で画面が切り替わった。
最初は左下の説明に英語のプロンプトがそのまま出ていた。タブレットの画面が古いスクリプトのままだったのが原因で、読み込み直すと聞き取った文が出た。

描き直しのほうは、音声チャットサーバー側の試験で、「もっと明るくして」に対して元のプロンプトに明るい空や日差しのタグを足したプロンプトが返るところまで確かめた。この時点では全体がおおまかに動くことを先にして、タブレットから話しかけての描き直しは、全体を通して確かめるときに回した。