てけとーぶろぐ。

ソフトウェアの開発と、お絵かきと、雑記と。

nurie-app.siteと生成AIでデジタルぬりえをつくる

nurie-app.site はデジタルぬりえを塗ったり作ったりできるサイトです。

nurie-app.site

この nurie-app.site と画像生成AIを使って無料で簡単にデジタルぬりえをつくってみましょう。

元画像の生成

色々な生成AIがありますが今回はMicrosoft Copilotを使います。

copilot.microsoft.com

例えばクマのぬりえが作りたかったら以下のメッセージをCopilotに送ればPNG画像がつくれます。

Create a simple, black-and-white line drawing of a cute, Japanese-cartoon-style bear for a children’s coloring page. The bear should have thick lines, and a friendly expression. The image should be easy to color, with low detail and no blackout areas.

もちろん「bear」を別の単語に変えたりしてアレンジしてもよし、 入力するたびに違う絵となるのでいい感じの絵ができるまで何度か試すのもありです。

さてこんな画像ができました。

クマ

SVG画像に変換

nurie-app.siteではぬりえをつくるのにSVG形式の画像ファイルを使います。

Adobe ExpressのSVG変換機能を使うとJPGやPNG画像をSVG画像に変換できますので変換します。

www.adobe.com

ぬりえで塗りたい部分の塗りつぶし

こうして出来上がるSVG画像は線以外の部分は透明になっています。 これに対してnurie-app.siteでぬりえをつくるのに使うSVG画像は ぬりえで塗りたい部分が塗りつぶされている必要があります。 そこでその塗りつぶしをします。

無料でSVGファイルを編集できるアプリということで今回はInkscapeを使います。

表示の設定

InkscapeでSVGファイルを開きます。

はじめに透明部分を塗りつぶしたことがわかるように 透明部分が市松模様で表示されるよう設定します。

メニューの「ファイル」→「ドキュメントのプロパティ」とクリックします。 表示されたドキュメントのプロパティダイアログで 「表示」タブの「表示」の中にある「市松模様」にチェックを入れ、ダイアログを閉じます。

塗りつぶし

ぬりえで塗りたい部分を塗りつぶしていきます。

画面左端に並んでいるツールのボタンの中の 「バケツ(塗りつぶし)ツール」を選択します。

画面下に並んでいる色の中の白を選択します。

塗りつぶしたときに輪郭線と塗りつぶしの間に隙間ができないように 画面上に並んでいるバケツツールの設定で以下のように設定します。

ぬりえで塗りたい部分をクリックすると塗りつぶしのパスが作成され 塗りつぶされるので、次々とクリックして塗りつぶしていきます。

パスを最背面に移動

輪郭線と塗りつぶしの間に隙間がない代わりに 塗りつぶしのパスが輪郭線に若干覆いかぶさっている状態なので 塗りつぶしのパスを最背面に持っていきます。

画面左端に並んでいるツールのボタンの中の 「選択ツール」を選択します。

最背面に持っていきたい塗りつぶしのパスをクリックして選択します。 キーボードのEndキーを押すとパスが最背面に移動されます。 同様にすべての塗りつぶしのパスを最背面に持っていきます。

保存

できたらSVGファイルとして保存します。

メニューの「ファイル」→「名前を付けて保存」とクリックし 表示されたファイルの保存先の選択ダイアログで 「ファイルの種類」を「プレーンSVG (*.svg)」として保存します。

デジタルぬりえの作成

SVGファイルができてしまえばあとは簡単です。

https://nurie-app.site にアクセスします。

下の方にある「ぬりえを作る」ボタンをクリックするとぬりえを作る画面が表示されます。

右上の「参照」ボタンをクリックして作成したSVGファイルを選択すると画像が表示されます。

ぬりえに使うパレットも設定してみましょう。 「パレット」の右の方にある「選択」ボタンをクリックして 表示されたダイアログで「16色」を選択し「OK」ボタンをクリックします。 ぬりえに使うパレットが16色のパレットとなります。

あとは右上の「保存」ボタンをクリックすれば出来上がりです。

ぬりえを作る画面

右上の「シェア」ボタンをクリックして表示されるURLからぬりえを楽しみましょう。

ぬりえを塗る画面

補足など

  • SVG画像の塗りつぶしについては 有料のアプリですがAdobe Illustratorã‚„Affinity Designerを使うと 隙間ができないように工夫せずともピッタリ塗りつぶせます。

  • 画像生成AIで、塗りつぶした状態の画像を生成してしまって、 その画像をSVGConverterã‚„Vectorizer.AIでSVG画像に変換すれば 手作業での塗りつぶしなくnurie-appにそのまま使えるSVG画像が作成できそうにも思いますが 自分の方ではいい感じの画像生成ができていません。

  • 今回は子ども向けのようなぬりえでしたが、 大人向けの緻密なものやステンドグラスのようなものなど、 あると塗りごたえがありそうです。

nurie-app.site

デジタルぬりえを塗ったり、作ったりできるサイト nurie-app.site を開発中です。

nurie-app.site

もう少し実装したい機能もあったりしますけど、 それはそれとして、近々生成AIを使ってnurie-app.site 用のぬりえを作る手順を紹介する予定です。

記事を待たずともぬりえの作成、大歓迎です。

軽い荷物で出先でPC(2)

軽い荷物で出先でPC(1) - てけとーぶろぐ。 の続きです。

背の高いスマホスタンドがあればスマホを快適にPCとして使えるのではないか。 しかし持ち歩きやすく背が高い(20cm以上持ち上げたい)セットしやすいスマホスタンドというのがどうも見当たらないので作ってみるという話ですね。

試作してみました。

ドン

スマホスタンド1

バン

スマホスタンド2

バァ〜〜ン

スマホスタンド3

使ってみると、画面が遠いなぁというのはなくなりましたが TeamViewerでのPC画面については解像度を落としたり、OSやアプリの設定でUIの表示サイズを大きくしてやらないと見づらいのは変わらずでした。

考えてみれば当たり前の話で、結局スマホ用のアプリやサイトぐらいの画面密度が見やすいということですね。

その辺を割り切ってしまえばそれなりに快適です。 これが求めていたPC環境かといえば違うのですけど…。 スマホアプリでキーボード・マウスを使うのにもよさそうです。

スタンドについて気になる点が3つあります。

折りたたんでも厚みがある

完全にヒンジの厚みですね。 薄いヒンジを探せばよさそうですけど、それだとそれなりにお金がかかりそう。

それなりに立てないとスマホの重みで閉じてしまう

立てて高さを取るのが目的なので問題ないといえば問題ないのですけど ちょっと高さを控えたくても支持できないですね。ヒンジを固くしたりすればいいのでしょうけど。

衝撃が加わるとスマホが揺れる

スタンドに長さがあるので、机に衝撃が加わったりするとスマホが少しゆらゆらしますね。 まぁ許容範囲内です。

ちなみに材料はこんな感じです。

MDFボードはカッターナイフで切ったり、ピンバイスで穴空けたりできるんですね。 おかげで手動の工具だけでできました。

誰か薄く折り畳める感じで製品化しませんか〜。

軽い荷物で出先でPC(1)

軽い荷物で出先でPCを使いたい。

だまってLIFEBOOKなどの軽量ノートPCを買えばいいのですけど もっと軽くできないだろうか、スマホは持ち歩いているのだから これを活かせばもっと軽い荷物でいけるのではないか…と試行錯誤しています。

というわけで、この企画では以下を重視します。

以下の記事を書いたときの経験でスマホをPCとして快適に使うにはリモートデスクトップなどを使って別PCを操作するしかないことがわかっています。

Fire HD 10 タブレット(≒Androidタブレット)をPC風に使う(ソフトウェア編) - てけとーぶろぐ。

Android上でLinuxを動かすようなものは現状実用的ではありません。

操作する別PCはどうするのか?あれこれ探した結果、自宅でRaspberry Pi 5を動かしておいて、そこにTeamViewerで接続するのが良さそうです。

Raspberry Pi 5はそこまで高くなく、常時起動でも電気代もあまりかかりません。性能もそれなりに快適です。Windowsを使いたい場合はミニPCが代わりになるでしょうか。

TeamViewerは個人向け無料ライセンスがありますし、グローバルIPがなくても出先から自宅のRaspberry Piに接続できます。

これであれはBluetoothキーボードとマウスさえ持っていけばOKか? …と思ったのですが、実際にやってみると6.5インチのスマホを 以下のようなスタンドで立て掛けたのでは画面が遠く小さく、操作しづらいことがわかりました。

もうひと工夫必要そうなので案を考えました。

追加案1: 折り畳みスマホ

今流行りの折り畳みスマホなら大きな画面でバッチリ? しかし自分の場合、10インチのタブレットを机の上にキックスタンドで立ててみても画面の遠さ、小ささを感じました。今出回っている折りたたみスマホは10インチ未満なので今ひとつかもしれません。

追加案2: 軽量モバイルモニター

例えば以下のモニターは13.5インチで380g。

Pixel 8a だとかは外部映像出力できるし、おそらくモニターをスマホにつなぐだけで別途電源なしでも使えます。 しかし、いくら軽いとはいえ、モニターが加わると大分ノートPCの重さに近づいてしまうし、モニターとの接続や設置も煩わしいだろうしで今ひとつかもしれません。

追加案3: 背の高いスマホスタンド

実はスマホの画面が高い位置にあって顔から近ければ操作しづらさは軽減できるのではないでしょうか? 背の高いスマホスタンドを使ってそのようにすれば…。 追加案1と組み合わせればなおよしかもしれない。 しかし持ち歩きやすく背が高い(20cm以上持ち上げたい)セットしやすいスマホスタンドというのがどうも見当たりません。 もしかして自分で作れるのではないかと思い試作中です…。

音声認識ストップウォッチ(2)

引き続き「スタート」「ストップ」などとマイクに向かって発声することで操作できるストップウォッチ。

日本語対応や、音声の再生を追加してだいぶ使えるようになってきた。

speechrecogstopwatch.site

音声認識ストップウォッチ(1)

「start」「stop」などとマイクに向かって発声することで操作できるストップウォッチを作りました。

speechrecogstopwatch.site

料理やトレーニングなど、ちょっと手を離せない状態で時間を測りたいときにどうぞ。

まだ英語にしか対応していないので英語風の発音でないと反応してくれません…。

あと自分で使ってみて、こうだったらいいな、が何点かあったので、使いつつ手を入れていこうと思います。

Python + Flet + Gemini API でAIチャットアプリを作る

前回はコンソールアプリとしてAIチャットを作りました。

kurima.hatenablog.com

このAIチャットにFletを使ってGUIをつけてみます。

FletはPythonで書けるFlutterと言えば伝わるでしょうか?

自分は何年も前からPythonでデスクトップアプリを組むには何を使ったらいいのかと悩みあれこれ眺めたり少し触ったりしてきました。

やっぱりTkinterなのかなぁなどと思いつつも、整った画面を作るのにノウハウが必要なところに違和感を感じていました。かといって広く使われていないものも使う気がしないといった状態でした。

そんなときにFletを見つけ、やっと腰を据えて使えるものが来たかー?と感じました。

ちなみにStreamlitも、こりゃすごい、と思ったのですけど、自由度が少し足りないんですよね…。そこを犠牲にして思い切り使いやすくしているというものだと思うので仕方ないと思います。

Fletやらの感想はこのくらいにしてアプリを作っていきます。プロジェクトの作成、APIキーの取得、プログラムからのAPIキーの参照については前回と同じなので詳しくは前回をご参照ください

プロジェクトの作成

プロジェクトを作成します。

$ rye init gemini-chat
$ cd gemini-chat
$ rye sync
パッケージのインストール

Gemini API 用の Python SDK が含まれる google-generativeai パッケージ、GUIの作成に使うfletパッケージをインストールします。

$ rye add google-generativeai
$ rye add flet
$ rye sync
Pythonファイルの作成

GUIのチャットアプリを作るわけですけど、ちょうどFletのチュートリアルにチャットアプリがありましたのでこちらをベースとして使わせてもらいたいと思います。

flet.dev

以下のPythonファイルを作成します。 gemini-chat/src/gemini_chat/main.py

そしてまずは以下にあるチュートリアルの最終的なコードをそのままコピーしてmain.pyの内容とします。

github.com

.env ファイルの作成

gemini-chat/.env を以下の内容で作成します。("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"の部分は取得したAPIキー) 作成の理由は前回を参照してください。

GOOGLE_API_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
.gitignore ファイル編集

gemini-chat/.gitignore に以下を追記します。 編集の理由は前回を参照してください。

# env
.env
pyproject.toml ファイルの編集

.envファイルから環境変数に値をセットしてプログラムの実行をするような スクリプトを gemini-chat/pyproject.toml に定義します。 pyproject.tomlに以下を追記します。 Fletを使ったアプリなのでfletコマンドで実行します。

[tool.rye.scripts]
run-main = { cmd = "flet run src/gemini_chat/main.py", env-file = ".env" }
プログラムの実行

以下のコマンドでプログラムを実行します。

$ rye run run-main

プログラムを実行するとFletのチュートリアルのチャットが表示されます。 これに少し手を加えてAIチャットにしましょう。

ユーザー名入力処理の削除

AIチャットでユーザー名の入力を求められるのは煩わしいのでユーザー名入力の処理を削除して固定のユーザー名となるようにしましょう。

ユーザー名入力のダイアログの「Join chat」ボタンをクリックしたときの処理である main() の中の join_chat_click() を削除します。具体的には以下のコードを削除します。

    def join_chat_click(e):
        if not join_user_name.value:
            join_user_name.error_text = "Name cannot be blank!"
            join_user_name.update()
        else:
            page.session.set("user_name", join_user_name.value)
            page.dialog.open = False
            new_message.prefix = ft.Text(f"{join_user_name.value}: ")
            page.pubsub.send_all(
                Message(
                    user_name=join_user_name.value,
                    text=f"{join_user_name.value} has joined the chat.",
                    message_type="login_message",
                )
            )
            page.update()

ユーザー名入力のダイアログの表示の処理を削除します。具体的には main() の中盤にある以下のコードを削除します。

    # A dialog asking for a user display name
    join_user_name = ft.TextField(
        label="Enter your name to join the chat",
        autofocus=True,
        on_submit=join_chat_click,
    )
    page.dialog = ft.AlertDialog(
        open=True,
        modal=True,
        title=ft.Text("Welcome!"),
        content=ft.Column([join_user_name], width=300, height=70, tight=True),
        actions=[ft.ElevatedButton(text="Join chat", on_click=join_chat_click)],
        actions_alignment=ft.MainAxisAlignment.END,
    )

固定のユーザー名を設定する処理を追加します。main() の最後に以下のコードを追加します。

    user_name = "You"
    page.session.set("user_name", user_name)
    new_message.prefix = ft.Text(f"{user_name}: ")
    page.pubsub.send_all(
        Message(
            user_name=user_name,
            text=f"{user_name} has joined the chat.",
            message_type="login_message",
        )
    )
    page.update()
Gemini APIのChatSession作成処理の追加

Gemini APIのChatSessionを作成する処理を追加します。main() の最後に以下のコードを追加します。

エラーメッセージの表示にアラートダイアログを使うようにしただけでChatSessionの作成自体は前回と同じです。

    GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
    chat_session = None

    if GOOGLE_API_KEY is not None:
        genai.configure(api_key=GOOGLE_API_KEY)

        model = genai.GenerativeModel("gemini-1.5-flash")

        chat_session = model.start_chat(history=[])
    else:

        def handle_close(e):
            print("handle_close()")
            page.dialog.open = False
            page.update()

        page.dialog = ft.AlertDialog(
            open=True,
            modal=True,
            title=ft.Text("エラー"),
            content=ft.Text("環境変数 GOOGLE_API_KEY がセットされていません"),
            actions=[
                ft.TextButton("OK", on_click=handle_close),
            ],
            actions_alignment=ft.MainAxisAlignment.END,
        )

main.py の先頭に import も追加します。

import google.generativeai as genai
import os
AIによる返答処理の追加

AIによる返答処理を追加します。 ユーザーからのメッセージを受けてAIからのメッセージを作成・送信し、AIからのメッセージを受けた場合もユーザーからメッセージを受けた場合と同じように処理します。 そのために main() の中の on_message() を編集します。以下の on_message() を

    def on_message(message: Message):
        if message.message_type == "chat_message":
            m = ChatMessage(message)
        elif message.message_type == "login_message":
            m = ft.Text(message.text, italic=True, color=ft.colors.BLACK45, size=12)
        chat.controls.append(m)
        page.update()

以下のように変更します。

先頭の if に "ai_chat_message" の場合が加わり、末尾にAIからのメッセージ作成・送信が加わっています。

    def on_message(message: Message):
        if (
            message.message_type == "chat_message"
            or message.message_type == "ai_chat_message"
        ):
            m = ChatMessage(message)
        elif message.message_type == "login_message":
            m = ft.Text(message.text, italic=True, color=ft.colors.BLACK45, size=12)

        chat.controls.append(m)
        page.update()

        if message.message_type == "chat_message" and chat_session is not None:
            response = chat_session.send_message(message.text)
            page.pubsub.send_all(
                Message(
                    "AI",
                    response.text,
                    message_type="ai_chat_message",
                )
            )
プログラムの再実行

再度以下のコマンドでプログラムを実行します。

$ rye run run-main

このようにAIとチャットできます。

スクリーンショット

それっぽくなってきました。

最終的な全体のコードを載せておきます。

import flet as ft
import google.generativeai as genai
import os


class Message:
    def __init__(self, user_name: str, text: str, message_type: str):
        self.user_name = user_name
        self.text = text
        self.message_type = message_type


class ChatMessage(ft.Row):
    def __init__(self, message: Message):
        super().__init__()
        self.vertical_alignment = ft.CrossAxisAlignment.START
        self.controls = [
            ft.CircleAvatar(
                content=ft.Text(self.get_initials(message.user_name)),
                color=ft.colors.WHITE,
                bgcolor=self.get_avatar_color(message.user_name),
            ),
            ft.Column(
                [
                    ft.Text(message.user_name, weight="bold"),
                    ft.Text(message.text, selectable=True),
                ],
                tight=True,
                spacing=5,
            ),
        ]

    def get_initials(self, user_name: str):
        if user_name:
            return user_name[:1].capitalize()
        else:
            return "Unknown"  # or any default value you prefer

    def get_avatar_color(self, user_name: str):
        colors_lookup = [
            ft.colors.AMBER,
            ft.colors.BLUE,
            ft.colors.BROWN,
            ft.colors.CYAN,
            ft.colors.GREEN,
            ft.colors.INDIGO,
            ft.colors.LIME,
            ft.colors.ORANGE,
            ft.colors.PINK,
            ft.colors.PURPLE,
            ft.colors.RED,
            ft.colors.TEAL,
            ft.colors.YELLOW,
        ]
        return colors_lookup[hash(user_name) % len(colors_lookup)]


def main(page: ft.Page):
    page.horizontal_alignment = ft.CrossAxisAlignment.STRETCH
    page.title = "Flet Chat"

    def send_message_click(e):
        if new_message.value != "":
            page.pubsub.send_all(
                Message(
                    page.session.get("user_name"),
                    new_message.value,
                    message_type="chat_message",
                )
            )
            new_message.value = ""
            new_message.focus()
            page.update()

    def on_message(message: Message):
        if (
            message.message_type == "chat_message"
            or message.message_type == "ai_chat_message"
        ):
            m = ChatMessage(message)
        elif message.message_type == "login_message":
            m = ft.Text(message.text, italic=True, color=ft.colors.BLACK45, size=12)

        chat.controls.append(m)
        page.update()

        if message.message_type == "chat_message" and chat_session is not None:
            response = chat_session.send_message(message.text)
            page.pubsub.send_all(
                Message(
                    "AI",
                    response.text,
                    message_type="ai_chat_message",
                )
            )

    page.pubsub.subscribe(on_message)

    # Chat messages
    chat = ft.ListView(
        expand=True,
        spacing=10,
        auto_scroll=True,
    )

    # A new message entry form
    new_message = ft.TextField(
        hint_text="Write a message...",
        autofocus=True,
        shift_enter=True,
        min_lines=1,
        max_lines=5,
        filled=True,
        expand=True,
        on_submit=send_message_click,
    )

    # Add everything to the page
    page.add(
        ft.Container(
            content=chat,
            border=ft.border.all(1, ft.colors.OUTLINE),
            border_radius=5,
            padding=10,
            expand=True,
        ),
        ft.Row(
            [
                new_message,
                ft.IconButton(
                    icon=ft.icons.SEND_ROUNDED,
                    tooltip="Send message",
                    on_click=send_message_click,
                ),
            ]
        ),
    )

    user_name = "You"
    page.session.set("user_name", user_name)
    new_message.prefix = ft.Text(f"{user_name}: ")
    page.pubsub.send_all(
        Message(
            user_name=user_name,
            text=f"{user_name} has joined the chat.",
            message_type="login_message",
        )
    )
    page.update()

    GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
    chat_session = None

    if GOOGLE_API_KEY is not None:
        genai.configure(api_key=GOOGLE_API_KEY)

        model = genai.GenerativeModel("gemini-1.5-flash")

        chat_session = model.start_chat(history=[])
    else:

        def handle_close(e):
            print("handle_close()")
            page.dialog.open = False
            page.update()

        page.dialog = ft.AlertDialog(
            open=True,
            modal=True,
            title=ft.Text("エラー"),
            content=ft.Text("環境変数 GOOGLE_API_KEY がセットされていません"),
            actions=[
                ft.TextButton("OK", on_click=handle_close),
            ],
            actions_alignment=ft.MainAxisAlignment.END,
        )


ft.app(target=main)