Mac のためのローカル LLM 環境 MLX-LM のススメ

前回の記事では、Qwen3 の MLX 版と Ollama (GGUF) 版の速度比較を行いました。結論として、MLX 版の LLM のほうが速いということがわかりました。

その後も主に MLX-LM で LLM を使っているのですが特に不具合も無く、なんなら慣れてしまえばとてもヨイということがわかってきました。というわけで本記事でやり方を一通り共有します。現在 Ollama を使用中で MLX-LM はまだ触っていない、という方が対象になるかと思うので、所々で Ollama との比較を入れていきます。

フロントエンドとしては、ボクは Dify をメインで使っていますが、よりお手軽な Open WebUI での使い方にも触れます。

書いていたら大作になってしまったので、気になるところだけでも覗いてみてください。

↑前回の MLX-LM vs Ollama 的記事

なぜ MLX-LM を使うのか

MLX-LM を使う理由は、MLX は Apple が開発している機械学習フレームワークなので Apple ハードウェア (M1~M4 等の Apple シリコンシリーズ) に最適化されており、単純に LLM の実行速度が Ollama (GGUF モデル) より速いからです。前回の記事で調べてみてはっきりとわかりました (量子化の違いもあり性能差はあるのでしょうが、それすら MLX のほうが上という調査結果もあります)。

実は Dify で MLX-LM を使い始めた当初、システム推論モデルとして Ollama のモデルを使用していました。すると、最初のチャットの後サマリ (タイトル?) の生成に Ollama のモデルが使われ、メモリの使用量が高止まりするような状況が頻発しました。それで MLX-LM はまだ実用には向かないと勝手に思い込んでいたのですが、Dify のシステムモデル設定で推論モデルも MLX の小さなモデルに変更したところ、チャットサマリ生成後もメモリプレッシャーがキレイに下がることがわかりました。MLX-LM だけを使用することで無駄にメモリが占有される問題は解消です。

また、サーバの起動やモデルのダウンロードで必要な長めのコマンドも仮想環境専用のaliasを登録することで解消できて、運用の手間が大幅に下がったことも大きいです (使い慣れている Ollama ではまだ MLX のモデルが使えないので仕方なくなんとかした、とも言えますけど) サーバ自体の起動も速いので、一度落としてあげ直すのも苦痛じゃないです。

MLX で LLM を動かすだけなら LM Studio という選択肢もあります。モデルの検索からダウンロード、テキストのチャット、ビジョンモデルに画像を認識させる、OpenAPI コンパチの API サーバを立ち上げる、等など様々な機能が利用できます。が、全部盛り過ぎてアプリケーション自体が重いのと、モデルを読み込むとその分メモリを占有し続けるのが個人的には気に入らないです。ネット上では、ボクはあまり気にしていませんが、プロプライエタリ (クローズドソース) だからダメだ、なんて論調もありますね。逆に「自分は LM Studio が好き、LM Studio で MLX のモデルを使う」という方はこれ以上読む必要はありません。LM Studio は使わないという人向けの内容です。

MLX-LM モデルの量子化について

新しめの MLX-LM には Learned Quantization (学習済み量子化?) という機能が導入されています。これまでの、全体を画一的に 8-bit や 4-bit に量子化するのではなく、より効率的に量子化を行うことで、結果としてモデルのサイズを小さくしたり、性能の劣化を小さくしたり、推論速度を上げたり、ということができるようです。Hugging Face ではDWQAWQDynamic等とモデル名に付いているものがこれらのテクニックを使って量子化されている事を示しています。詳細はこちら (公式):

https://github.com/ml-explore/mlx-lm/blob/main/mlx_lm/LEARNED_QUANTS.md

ボクも 32GB RAM の M2 max で google/gemma-3-12b-it の Dynamic-quant を数回チャレンジしてみたのですが、おそらくメモリ不足で macOS がクラッシュしてしまい、諦めました (量子化作業にはpip install datasetsが必要でした)。上の公式以外では詳細について書かれている記事などもほぼ見当たらず、今後に期待ですね。

モデルの選定

Mac の GPU に割り当てる VRAM 容量を増やしたり、モデルに最適な量子化が行われていたりしても、それらはより大きなパラメータサイズを使えるようになるほどの効果は期待しづらいです (32B を 70B にとか 4-bit を 8-bit に等はキツい)。なので、これまで Ollama で使っていたモデルの同レベルの量子化バージョンが、より速く低劣化で動き、より大きなコンテキスト長が使えるというのが MLX-LM モデルの大きなメリットになると思います。

モデルを選定するには、慣れないうちは LM Studio で MLX のみにチェックを入れて使えそう (Full GPU Offload Possible) なモデルを見つけて Model (例: /deepseek/deepseek-r1-0528-qwen3-8b) をコピーし、後述するコマンドでダウンロード、というのが良いと思います。慣れてきたら Hugging Face で “mlx gemma-3” 等と検索するのが早くなると思います。

下の記事ではより詳細に自分の RAM (ユニファイドメモリ) のサイズに合わせたモデルの見つけ方を説明しています。(英語ページが開いてしまったら、右の「日本語」をクリックしてください)

今回は MLX-LM に変換・量子化されたモデルを対象とした記事ですが、そもそもの LLM の性能差などを調べるのは、各種リーダーボードを見るのが良いでしょう。ボクは最近もっぱら↓のサイトで性能差を見ています。

https://artificialanalysis.ai (オープン、クローズド、複数選んで比較できます。新しいモデルが追加されるのも早い)

試した環境

  • Mac Studio M2 Max 32GB GPU (24,576 GB を VRAM に割り当て済み。OS 標準以上の容量を GPU に割り振る方法はこちら)
  •  macOS: Sequoia 15.5
  • Python 仮想環境: pipenv version 2025.0.3 (なぜ pipenv なのか、みたいな話はこちら)
  • Python: 3.12.11 (特に意味は無し。brew install [email protected]でインストール)
  • MLX-LM: 0.25.2 (pip install mlx-lmでインストール)
  • Open WebUI: 0.6.15 (pip install open-webuiでインストール)
  • Dify: 1.4.2 (LAN にいる別の Mac mini M1 にインストール。やりかたはこちら)
  • Ollama: 0.9.2 preview (Ollama 新アプリのプレビュー版。比較用に。詳しくはこちら)
  • LLM: Qwen/Qwen3-32B-MLX-4bit (17.42 GB / メインで使う LLM)
  • LLM: mlx-community/gemma-3-12b-it-4bit (8.07 GB / Dify のシステム推論モデルとして使用)

RAM が 32GB より小さい場合は LLM も性能がそれなりのものしか使えないので、正直実用的なローカル LLM 環境を作るのはキツいと思います。48GB 以上あれば Dify 含めて全て同一 Mac で動かせると思います。

仮想環境を作る

Python の仮想環境は、最低限 MLX-LM 実行用に一つ必要です。Open WebUI を新たにpipで導入する場合には専用にもう一つ作ったほうが良いと思います。お好みの仮想環境ツール+上記pipコマンドで作ってください。

もし新規で Dify をインストールする場合は Docker が必要となりますので、公式過去記事を参考に構築してください (CPU >= 2コア、RAM >=4GB の割り当てが必要)。

(蛇足) ボクはあまり人気が無いらしいpipenvを使ってます。仮想環境内だけで有効になるaliasを使って長くなりがちなコマンドを簡単に実行しています。特にこだわりや縛りの無い方はお試しあれ。(英語ページが開いてしまったら、右の「日本語」をクリックしてください)

pipenv内専用のaliasについてもう少し触れておくと、仮想環境のルートディレクトリに置いた.zshrc.localファイルに下記のように書き込んでおけば、pipenv shellで環境に入ったときだけmlxsvで MLX-LM の API サーバを実行でき、モデルのダウンロードはdownloadの後に Hugging Face のモデルを指定するだけで実行できるので便利です (例: download mlx-community/gemma-3-12b-it-4bit)。詳細は上記記事をご覧ください。

alias mlxsv='mlx_lm.server --host 0.0.0.0 --port 8585 --log-level DEBUG'
alias download='HF_HUB_ENABLE_HF_TRANSFER=1 huggingface-cli download'

MLX-LM 仮想環境でモデルをダウンロード&動作確認

mlx_lm.generateコマンドを使ったモデルのダウンロード方法をよく見ますが、やっているのはollama runコマンドでモデルをダウンロードしてチャット開始するのと近く、ダウンロード後にテキストの生成が行われます (チャットでは無く、生成のみ)。ollama pullのようにシンプルにモデルをダウンロードするだけであれば、Hugging Face のコマンドをインストールして使用するのが良いでしょう。というわけで、まずは MLX-LM 用に作った仮想環境に入ってから Huggng Face 関連コマンドをインストールします。

pip install -U huggingface_hub hf_transfer

次に、普通にやるより速いらしい以下の方法でモデルをダウンロードします (上記のaliasを設定済みであればdownload Qwen/Qwen3-32B-MLX-4bitで OK です)。モデルはお好みでどうぞ。

HF_HUB_ENABLE_HF_TRANSFER=1 huggingface-cli download Qwen/Qwen3-32B-MLX-4bit

動作確認はmlx_lm.chatコマンドでターミナルから行えます。下記例では最大トークン数をデフォルトより増やしています (Qwen3 のような thinking/reasoning モデルだと考えているうちに最大トークンに達してしまう)。

mlx_lm.chat --model Qwen/Qwen3-32B-MLX-4bit --max-tokens 8192
MLX-LM と Ollama との CLI チャット機能比較: mlx_lm.chatコマンドはあまりイケてません。ollama runコマンドのようにチャットを始めてから設定を変更したりはできませんし、いくつか改行するつもりでエンターキーを叩くと無言のプロンプトが LLM に送られて生成が始まりますし、LLM のテキスト生成を止めようと Ctrl + C するとコマンド自体が停止します (ズコー)。よって、ollama runの様な使い勝手は期待してはいけません。

次のコマンドでは Dify のシステム推論モデルとして設定する mlx-community/gemma-3-12b-it-4bit もダウンロードしています。Dify を使わない方は不要です。ファイルサイズは 8.1GB なので、上で落とした Qwen/Qwen3-32B-MLX-4bit の半分以下の時間で完了すると思います。

HF_HUB_ENABLE_HF_TRANSFER=1 huggingface-cli download mlx-community/gemma-3-12b-it-4bit

ダウンロードされたモデルを一覧表示するのは以下のコマンドです:

mlx_lm.manage --scan

ところがこのコマンドでは最初にダウンロードしたQwenリポジトリのモデルは表示されません。mlx-communityリポジトリのモデルは表示されます。API サーバを実行すればブラウザからは確認できるので、その方法は後ほど説明します。また、モデル名を指定すれば使用することも可能です。

チャットで使うならこう:

mlx_lm.chat --model Qwen/Qwen3-32B-MLX-4bit

削除するならこう:

mlx_lm.manage --delete --pattern Qwen/Qwen3-32B-MLX-4bit

モデルは/.cache/huggingface/hub/以下に保存されているため、ファインダーから削除しても問題ありません。

ところで先ほどのmlx_lm.manage --scanでモデルの実サイズは表示されるものの、他の情報は特に確認できません。Ollama では ollama show <modelname>でコンテキスト長や量子化方法等を確認できますが、代わりになる方法は MLX-LM にはありません。必要な場合は Hugging Face のモデルカードを確認するか、LM Studio がインストールしてあれば My Models で確認するか、といったところです。ただしコマンドでダウンロードしたモデルの名前は LM Studio では正しく表示できないので、ダウンロードしたタイミングなどで見分けましょう。モデルの詳細 (メタデータ) を確認する機能はぜひ MLX-LM に追加して欲しいところですよね (LM Studio は MLX-LM を内蔵しているので、同じ事ができると思うんですけど)。

OpenAI API コンパチのサーバを実行する

公式の実行方法 (↓ のリンク) をみるとモデル名を渡しているのでそのモデルしか使えないのかと思っていたのですが、サーバの起動時にモデル名を渡す必要はありません。起動後はクライアントで指定したモデルが利用できます。

https://github.com/ml-explore/mlx-lm/blob/main/mlx_lm/SERVER.md

上のaliasのとこにも書きましたが、ボクが MLX-LM の API サーバを実行するコマンドは以下の通りです。オプションがどれも不要であれば、mlx_lm.serverだけで大丈夫です。

mlx_lm.server --host 0.0.0.0 --port 8585 --log-level DEBUG
  • --host 0.0.0.0 こうするとの他のホストからもアクセスできます (ボクは Dify が別の Mac で動いているので必須)
  • --port 8585 デフォルトの8080 Open WebUI のデフォルトと被るので変えています
  • --log-level DEBUG プロンプトと速度 (tokens-per-sec = トークン/秒) やメモリの最大使用量が表示されます

コマンドを実行するとほどなく以下の様な画面になり、LLM が使用できるようになります。

% mlx_lm.server --host 0.0.0.0 --port 8585 --log-level DEBUG
/Users/handsome/Documents/Python/mlx-lm/.venv/lib/python3.12/site-packages/mlx_lm/server.py:880: UserWarning: mlx_lm.server is not recommended for production as it only implements basic security checks.
warnings.warn(
2025-06-28 18:56:23,071 - INFO - Starting httpd at 0.0.0.0 on port 8585...

UserWarningには、基本的なセキュリティチェックしか行っていないので本番環境での使用は推奨しない、と書かれています。閉じた環境で使う分には問題無いでしょう。

では簡単に、接続できるのかを試しておきましょう。ウェブブラウザで以下の URL を開くと、MLX-LM から利用できるモデルが表示されます。

http://localhost:8585/v1/models

表示例 (Qwen も見えてますね):

{"object": "list", "data": [{"id": "mlx-community/gemma-3-12b-it-4bit", "object": "model", "created": 1751104699}, {"id": "qwen/qwen3-1.7b", "object": "model", "created": 1751104699}, {"id": "Qwen/Qwen3-32B-MLX-4bit", "object": "model", "created": 1751104699}, {"id": "mlx-community/Qwen2.5-Coder-32B-Instruct-4bit", "object": "model", "created": 1751104699}, {"id": "mlx-community/QwQ-32b-4bit-DWQ", "object": "model", "created": 1751104699}]}

こうなれば、OpenAI API コンパチブルサーバに接続できるクライアントから MLX-LM の LLM を利用できるようになります。

サーバの停止とアップデート

アクティビティモニタでメモリメモリプレッシャーを見ていると、まれに黄色く高止まりすることがあります。そんなときは Ctrl + C で一度 MLX-LM サーバを止めて再度走らせるのが安心ですが、高止まりの原因が MLX-LM であれば次のチャットの後には平常に戻ることがほとんどです。他に GPU ヘビーなアプリを使っていなければ、雑に扱っても割と平気です。

アップデートに関しては Ollama のようなアイコンで知らせてくれたり、自動でダウンロードしてくれるような機能はありません。必要に応じてコマンドを叩く必要があります。

pip list|grep mlx # インストール済みバージョンの確認
pip install -U mlx-lm

https://pypi.org/project/mlx-lm (pip パッケージの情報)

もしアップデート後に不具合が出たら、上のコマンドで表示されたインストール済みバージョンに戻しましょう。例えばバージョン 0.25.1 に戻すならこんな感じです:

pip install mlx-lm==0.25.1

Open WebUI から接続する

Open WebUI を実行する

別の Python 仮想環境に Open WebUI をインストールした場合は、以下のコマンドでクライアントを実行できます (公式ではpipよりuvを強力に推していましたが、ボクは Open WebUI をメインで使わないのでなじみのあるpip使っちゃいました)。Docker で構築した人は飛ばしてください。

open-webui serve

オプションで--host (デフォルト: 0.0.0.0)、--port (デフォルト: 8080) の指定も可能です。

(ボクはコマンドを忘れがちなので、.zshrc_localalias sv='open-webui serve'と書いてきて、svで起動できるようにしています)

しばし待ち、ターミナルにロゴといくつかのINFOが表示されたらアクセスできるハズです (ロゴが収まりきらなかったのでコードブロックで貼り付けました)。


 ██████╗ ██████╗ ███████╗███╗   ██╗    ██╗    ██╗███████╗██████╗ ██╗   ██╗██╗
██╔═══██╗██╔══██╗██╔════╝████╗  ██║    ██║    ██║██╔════╝██╔══██╗██║   ██║██║
██║   ██║██████╔╝█████╗  ██╔██╗ ██║    ██║ █╗ ██║█████╗  ██████╔╝██║   ██║██║
██║   ██║██╔═══╝ ██╔══╝  ██║╚██╗██║    ██║███╗██║██╔══╝  ██╔══██╗██║   ██║██║
╚██████╔╝██║     ███████╗██║ ╚████║    ╚███╔███╔╝███████╗██████╔╝╚██████╔╝██║
 ╚═════╝ ╚═╝     ╚══════╝╚═╝  ╚═══╝     ╚══╝╚══╝ ╚══════╝╚═════╝  ╚═════╝ ╚═╝


v0.6.15 - building the best AI user interface.

https://github.com/open-webui/open-webui

Fetching 30 files: 100%|█████████████████████████████████████████████████████████████████████████████████████| 30/30 [00:00<00:00, 7708.23it/s]
INFO:     Started server process [84450]
INFO:     Waiting for application startup.
2025-06-28 19:08:50.198 | INFO     | open_webui.utils.logger:start_logger:140 - GLOBAL_LOG_LEVEL: INFO - {}
2025-06-28 19:08:50.198 | INFO     | open_webui.main:lifespan:514 - Installing external dependencies of functions and tools... - {}
2025-06-28 19:08:50.370 | INFO     | open_webui.utils.plugin:install_frontmatter_requirements:241 - No requirements found in frontmatter. - {}

以下の様な URL をブラウザで開きましょう (デフォルトでホストが0.0.0.0なので、自宅の Wi-Fi であれば iPhone 等から Mac の IP アドレスを指定してアクセスできます)。

http://localhost:8080

最初に管理者アカウントの作成があるんじゃないかと思いますので、終わらせてから進めてください。

OpenAI API として追加する

右上のアイコンから管理者パネルを開きます。

設定から接続を選び、OpenAI API接続の管理にあるプラスボタンをクリックします (下のスクリーンショットは設定済みの状態)。

Connection Type の右の「外部」をクリックして「ローカル」に変更し、URL に今回の例では「http://localhost:8585/v1」を入力し、保存します。

接続の下の「モデル」をクリックすると、ダウンロード済みのモデルが表示されると思います。もし表示されなければ、一度 Open WebUI のターミナルでサーバを Ctrl + C で止めて、再度実行してみてください。

Alibaba の回し者ではないです

ついでにやっておくべきオススメ設定

ここでモデルが見えれば新しいチャットの右にあるドロップダウンメニューから選んで使えるハズですが、その他いくつかやっておくべき設定を紹介します。

モデルの詳細設定をする

管理者パネル > 設定 > モデルで、モデル名をクリックするとデフォルトの設定を変更できます。システムプロンプトに「常に日本語で回答してください」と入れたり、高度なパラメータを表示して max_tokens を最大値にしておくと良いでしょう (デフォルトだと 128トークンしかない)。下にある資格のチェックボックスは、よくわからなければ全て外してしまいましょう。最後に「保存して更新」をクリックするのをお忘れ無く。

コンテキスト長は Ollama を Dify から使う場合などは注意して設定しないと大きな生成速度の低下を招きますが (参考記事)、Open WebUI だと max_tokens や num_ctx (Ollama) をどれだけ大きくしても?影響ないみたいです。どうやっているのかは未確認。

余計な仕事をさせない

管理者パネル > 設定 > インターフェースで、Follow Up Generation とオートコンプリート生成をオフにして保存します。いらないでしょ?

チャットタイトルについて

上と同じインターフェース画面で、タイトル生成についての設定があります。この生成処理にも LLM が使われるので、全く不要ならオフにする事もできます。有効にしておく場合、Qwen3 ではここでも思考プロセスが動いてしまうため、タイトル生成プロンプトに/no_thinkとだけいれて保存しましょう。こうすると、何の工夫も無くチャットに最初に入力した文章そのままがタイトルになり、余計な GPU の使用を防げます (デフォルトのタイトル生成プロンプトを見ると対策をしようとしているみたいですが、現状はうまく機能していません)。

Safari ユーザは日本語確定のエンターでメッセージが送信されるのを防ぐ

別記事にその方法を書いています。この方法はどうやら localhost に対しては使えないようなので、Mac には固定 IP アドレスを振り、// @include http://192.168.1.100:8080/*の様な形で対象を指定する必要があります。

Dify から接続する

OpenAI-API-compatible を使えるようにする

Dify のバージョン 1以上で使うには、まず OpenAI-API-compatible をプラグインからインストールします。

モデルを追加する

次に、右上の自分のアカウントアイコン > 設定 > モデルプロバイダーを開き、上で追加した OpenAI-API-compatible の「モデル追加」をクリックします。

Qwen/Qwen3-32B-MLX-4bit を追加するなら、こんな感じです。

  • Model Name: Qwen/Qwen3-32B-MLX-4bit
  • Model display name: (自分にわかりやすいように。例: MLX – Qwen/Qwen3-32B-MLX-4bit)
  • API Key: 不要
  • API endpoint URL: http://localhost:8585/v1 とか、別ホストなら http://192.168.1.100:8585/v1 とか
  • Completion mode: Chat
  • Model context size: 32768
  • Upper bound for max tokens: 32768
  • その他もろもろ: Not Support またはよしなに
  • Delimiter for streaming results: \n\n

チャットのタイトルを生成するモデルを選ぶ

また、Dify ではチャットタイトルを作るのはシステム推論モデル固定なため、小さめで thinking/reasoning ではない MLX-LM のモデルを設定しておきます。ここでは先ほどダウンロードしておいた Gemma 3 を上同様の要領で OpenAI-API-compatible モデルとして追加した後、指定しています (Model context size は 40960)。

あとはそれなりに

ここまでできたら、後は作ったアプリのモデルとして使用してみましょう。数字を鵜呑みにして良いのかわりませんが、いくつかチャットを行った後でアプリの「監視」メニューを見てみると、MLX-LM モデルのトークン出力速度が Ollama モデルより速いことが確認できます。

最後に

長々と書きましたが、使うほどに速さを実感しています。Ollama や LM Studio のようなユーザーフレンドリーさはありませんが、CLI での扱い方に慣れてしまえば MLX をサポートしない Ollama には戻れなくなると思います。ボクはディスク容量削減のため、Ollama からほとんどのモデルを削除してしまいました。

今回記事を書きながら Open WebUI をじっくり使ってみました。チャットだけなら十分ですね。タイトルの自動生成キャンセル技は速度を稼げるので地味に便利です。OpenAI API 接続だと tokens-per-sec が表示されないのは残念ですけど。RAG や MCP の利用もできるようなので、もっと使い込んでみようと思っています。

あとはやっぱり Qwen3 の性能の高さですよね。フロントエンド側でのサポートも進んでいて、QwQ だと丸見えになる思考が非表示になるのも地味にうれしいところです。政治的な話や中国にまつわる話を避ければおかしなところは感じないですし、最終的な回答に中国語が混ざる事も無く、当面はこれ一本でよさそうだと思っています。

Image by Stable Diffusion (Mochi Diffusion)

「リンゴTシャツを着てラマに乗る女性」いいんじゃないっすかコレ?もう一つステップを上げると破綻したので、これがベスト。

Date:
2025年6月29日 22:15:00

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
a lady with an apple t-shirt riding on a lama

Exclude from Image:

Seed:
391522385

Steps:
27

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

Python の pipenv 環境で専用の alias や export を読み込む

本記事の内容をより正確に書くと「zsh (bash) が読み込まれたときに、カレントディレクトリにあるaliasexportの書かれた設定ファイルを自動で読み込ませる」方法です。Python の仮想環境pipenvは新しいシェルを読み込むので、結果としてalias等を自動的に設定することができます。pipenv環境から抜けると仮想環境内の設定が無効になるため、素や他の環境に影響を与えません。簡単に実現できますが、pipenvを使っている人が少ないのかズバリの情報が見つからなかったのでまとめました。

環境

シェル:zsh (bashでもできるらしいですが未確認です)

Python 仮想環境:pipenv

手順

macOS でpipenvを使うには、まずbrew install pipenvでインストールします。簡単に仮想環境を作る手順はこんな感じです:

mkdir my_project # プロジェクトディレクトリを作る
cd my_project # プロジェクトディレクトリに入る
pipenv --python 3.11 # Python 3.11 が入った仮想環境を作る
pipenv shell # 仮想環境に入る。出るときは exit または ctrl + D

~/.zshrc に一文追加

自分のホームディレクトリにある.zshrcに以下を追加します。コメント文は無くて構いません。

# カレントディレクトリに .zshrc.local ファイルが存在する場合は読み込む
[[ -f .zshrc.local ]] && source .zshrc.local

内容としては、&&の左の部分が条件式で、カレントディレクトリに.zshrc.localファイルが存在しているか調べています。真であれば右のsourceコマンドが実行され.zshrc.localファイルを読み込みます (試してませんが、書式としてはbashでも同じ方法でイケるらしいです)。(2025/07/02 訂正) Bash は上の書式が使えないので、以下の様にしてください。仮想環境内のファイルは.bashrc.localとしています。

if [ -f .bashrc.local ]; then
    source .bashrc.local
fi

pipenv のルートディレクトリに .zshrc.local ファイルを書く

その仮想環境内でのみ有効にしたいaliasexport、その他.zshrcに書けることはもちろん何でも書けます。とりあえず簡単なサンプルは以下の通りです:

alias t='time'
export HW="Hello, World!"

仮想環境に入り、試す

以下、実行例です:

$ pipenv shell # 仮想環境に入る
$ t # alias で登録した time コマンド
(time コマンドの実行結果が表示される)

$ echo $HW
Hello, World!
(export で登録した文字列が表示される)

仮想環境から出て、試す

以下、実行例です:

$ exit # または ctrl + D で仮想環境を抜ける
$ t
zsh: command not found: t
(t というコマンドはない)

$echo $HW

(空行が表示される)

注意点

GitHub 等に公開するプロジェクトでは.gitignore.zshrc.localを忘れずに追加しましょう。

venv でもほぼ同様のことをする

Python の標準的仮想環境ツールvenvでは新たにシェルを読み込まれません。よって、別の方法で同様の事を実現します。

bin/activateの最終行に以下を追加します。~/.zshrcに書いたものと同じです。

# カレントディレクトリに .zshrc.local ファイルが存在する場合は読み込む
[[ -f .zshrc.local ]] && source .zshrc.local

pipenvのやり方よりひと手間増えますが、これで一応同じ様な事ができます。

venv での違い、注意点

上記の方法では、シェルの再読み込みはせずに.zshrc.localを読み込んでいるので、deactivatevenv環境を抜けた後もエイリアスや環境変数が有効になっています。同一のターミナルで仮想環境を抜けた後も別の作業を続けることがよくあるという方は普段の環境変数などが上書きされている可能性があるので注意が必要です (ターミナルを閉じるのが手っ取り早い)。

なぜこんなことが必要だったのか

ボクは最近、mlx-lm.serverでサーバを立てて MLX 版 LLM を使うのですが、Ollama と違ってメモリが解放されない (メモリプレッシャーが高止まり状態になる) ことがちょいちょいあります。仕方が無いのでつど ctrl + C で止めて再度コマンドからサーバを立てるのですが、他のターミナルでコマンドを叩いていたりするとカーソルキーの上ですぐに呼び出せずストレスを感じていました。そこで、pipenvの環境内でのみ有効なaliasを作れないかと思った次第です。

ネット上では想像したほど簡単にその方法が見つからず、ローカルの QwQ や Qwen3、ChatGPT にも相談しながら最終的には自分で解決方法にたどり着きました。それぞれの LLM に評価をお願いしたところ「すばらしい!」と褒めてくれたので、うれしくてブログにまとめました。わはは!

Image by Stable Diffusion (Mochi Diffusion)

この記事にどのような画像が合うのかイメージか浮かばず、とりあえずいろんな自転車のあるショールームを描いてもらいました。依頼内容も画像もこれが正解なのかいまだにわかっていませんけど。

Date:
2025年6月14日 19:47:15

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
showroom with different types of bycicles

Exclude from Image:

Seed:
1251791658

Steps:
20

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

【Mac 専用】OpenVoice で音声クローン

音声クローン技術の OpenVoice を Mac にインストールして使ってみました。作業を始めてから実際に使えるようになるまで結構大変だったので、やり方をまとめます。また、OpenVoice V2 は日本語ネイティブ対応ということですが、TTS (Text-To-Speech) に使われている MeloTTS の日本語能力がよろしくないので、macOS に標準搭載されている say コマンドで生成した音声を元にクローン音声を生成するスクリプトを書きました。そちらはなかなかの成果が出たので、合わせて紹介します。

OpenVoice (オフィシャル GitHub): https://github.com/myshell-ai/OpenVoice/

Weel さんの紹介記事 (こちらで知りました): https://weel.co.jp/media/tech/openvoice

注意点

OpenVoice v2 は MIT ライセンスで公開されているので、ソースコードも生成された音声も商用利用可能です。ただし、クローン元となる音声を利用する権利を有していない場合は、商用か否かにかかわらず生成された音声を公開する権利はありません。現在の法律がどうであれ、AI の進歩と共に法整備も進んでいくはずです。声やしゃべりで生計を立てている方々もいる以上、許可無く誰かの音声を使用することは何らかの形で罰せられる可能性があります。そのことを理解した上で本記事を読み進めてください。また、ご自身の音声データを公開する場合においても、ライセンスの明記は行うようにしましょう。

免責事項:
このサイトで提供される情報や方法を使用して行われた行為については、私たちは一切の責任を負いません。特に、他人の無断使用または違法な手段によって得られた音声データを使用して音声クローンを作成し、公開することは完全に利用者の自己責任であり、その結果として生じるあらゆる問題やトラブルについても当方は何ら関知せず、一切の責任を負わないものとします。

法律により保護された著作権やパブリシティ権等が侵害される可能性のある行為を行う際には、利用者は事前に適切な法的措置を講じ、自己のリスクにおいて行動するものとし、当方はその結果生じる一切の損害について責任を負わないものとします。

なお、この免責事項は日本国の法律に基づいて解釈されるものとし、適用されるべき法域については当事者の本拠地が所在する国または地域の法律ではなく、日本国の法律を選択的に適用することに同意するものとします。

以上のご理解とご了承をいただきますようお願い申し上げます。

環境

動作確認ができた環境

macOS: 14.5
ffmpeg version 7.0.1
pip version 24.2

環境構築手順

入っていなければ、ffmpeg をインストール (もちろん brew が必要です):

brew update
brew install ffmpeg

ディレクトリを作って OpenVoice リポジトリをクローン。ディレクトリ名はお好きにどうぞ:

mkdir OpenVoice
cd OpenVoice
git clone https://github.com/myshell-ai/OpenVoice.git .

仮想環境を構築して入る。ボクは得意のpipenvですが、これもお好きなのをお使いください。Python は 3.9 必須らしいです:

pipenv --python 3.9
pipenv shell

pipを最新にし、torchchardet、そして OpenVoice をインストール:

python -m pip install --upgrade pip
pip install torch chardet
pip install -e .

OpenVoice V2 をダウンロードして展開:

※ チェックポイントの最新版 (unzip の対象) は、https://github.com/myshell-ai/OpenVoice/blob/main/docs/USAGE.md の Download the checkpoint from here and extract it to the checkpoints_v2 folder. で確認のこと!

wget https://myshell-public-repo-host.s3.amazonaws.com/openvoice/checkpoints_v2_0417.zip
unzip checkpoints_v2_0417.zip
rm checkpoints_v2_0417.zip

以下に従ってファイル openvoice/se_extractor.py を編集 (22行目のcudacpuにし、float16float32に変更):

[[[IMPORTANT]]] modify OpenVoice source for Apple Silicon Mac: https://github.com/reid-prismatic/OpenVoice-Scribe/commit/f681f5bcbc18df3f356953928a78ba6dcff9de99

def split_audio_whisper(audio_path, audio_name, target_dir='processed'):
    global model
    if model is None:
        model = WhisperModel(model_size, device="cpu", compute_type="float32")
    audio = AudioSegment.from_file(audio_path)
    max_len = len(audio)

(ここは必要な場合のみ) OpenVoice V2 オフィシャルでは、音声生成 (TTS、Text-To-Speech) に MeloTTS を使用しているため、別途インストールする手順が示されています。個人的に日本語の能力は不十分だと思いますが、気になる方、オフィシャルの方法でまずは進めたい方は以下も実施してください:

git clone https://github.com/myshell-ai/MeloTTS
cd MeloTTS
pip install -e .
python -m unidic download

(MeloTTS のテストをしたい場合のみ) 以下にテスト用のサンプルコードを貼っておきます (★ MeloTTS テストコード ★をクリックするとコードが開きます)。python japanese.py で実行し、生成された jp.wav をファインダで QuickLook もしくは Terminal からafplay jp.wav で再生できます。テキストの文面は上で紹介した Weel さんの記事の一部ですが、アルファベット表記の “OpenVoice” がガン無視されたり、音引きが部分的にスルーされたり、読点で溜めが無かったり、なんとなく中国語っぽい癖があったり、という感じで、実用するにはきびしいです。ちなみに初回実行時はファイルのダウンロードが発生するので時間がかかります。

★ MeloTTS テストコード ★
from melo.api import TTS
import time

# Speed is adjustable
speed = 1.0
device = 'cpu' # 'mps' にしても動くが、速くはならない

start = time.time()

text = """
OpenVoiceは、正確なトーンカラーのクローニング、柔軟な声のスタイル制御、ゼロショット多言語クローニングを可能にする音声クローニング技術です。

OpenVoiceを使用すると、リファレンス スピーカーの音色を複製するだけでなく、感情、アクセント、リズム、ポーズ、イントネーションなどの音声スタイルをきめ細かく制御できます。

これを使用するとこのような音声が生成できます。
"""
model = TTS(language='JP', device=device)
speaker_ids = model.hps.data.spk2id

output_path = 'jp.wav'
model.tts_to_file(text, speaker_ids['JP'], output_path, speed=speed)

print(f'\nかかった時間: {round(time.time()-start, 2)} 秒\n')

OpenVoice を使う

クローンしたい音声を準備

環境の準備ができたら、クローンしたい音声ファイルを準備します。なるべく高品質で、他の人の声や雑音、BGM 等が入っていない、10秒以上の音声がよさそうです。参考まで、ffmpegで MP4 の映像ファイルから MP3 の音声を書き出すなら、こんな感じで行えます:

ffmpeg -i input.mp4 -vn -acodec libmp3lame myvoice.mp3

OpenVoice/resources フォルダに入れておきます (ファイル名 myvoice.mp3はサンプルコードで使用しているので、そのまま使いましょう):

cp myvoice.mp3 resources

まずはオフィシャルのテストコードで試す (MeloTTS 必須)

オフィシャルのサンプルコード (demo_part3.ipynb GitHub で見るならこちら) を適当にいじって日本語だけ生成するようにし、かかった時間を表示するようにしたものがこちらです。上の手順で MeloTTS をダウンロードした場合はこれでクローンのテストが行えます。OpenVoice ディレクトリ直下においてpython test.pyで実行します:

import os
import torch
from openvoice import se_extractor
from openvoice.api import ToneColorConverter
from melo.api import TTS
import time

home_dir = './'
ckpt_converter = home_dir + 'checkpoints_v2/converter'
device = "cuda:0" if torch.cuda.is_available() else "cpu"
output_dir = 'outputs'

start = time.time()

tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')

os.makedirs(output_dir, exist_ok=True)

reference_speaker = home_dir + 'resources/myvoice.mp3' # This is the voice you want to clone
target_se, audio_name = se_extractor.get_se(reference_speaker, tone_color_converter, vad=False)

texts = {
    'JP': "彼は毎朝ジョギングをして体を健康に保っています。"
}

src_path = f'{output_dir}/tmp.wav'

# Speed is adjustable
speed = 1.0

for language, text in texts.items():
    model = TTS(language=language, device=device)
    speaker_ids = model.hps.data.spk2id

    for speaker_key in speaker_ids.keys():
        speaker_id = speaker_ids[speaker_key]
        speaker_key = speaker_key.lower().replace('_', '-')

        source_se = torch.load(f'checkpoints_v2/base_speakers/ses/{speaker_key}.pth', map_location=device)
        model.tts_to_file(text, speaker_id, src_path, speed=speed)
        save_path = f'{output_dir}/output_v2_{speaker_key}.wav'

        # Run the tone color converter
        encode_message = "@MyShell"
        tone_color_converter.convert(
            audio_src_path=src_path,
            src_se=source_se,
            tgt_se=target_se,
            output_path=save_path,
            message=encode_message)
 
print(f'\nかかった時間: {round(time.time()-start, 2)} 秒\n')

生成された音声は outputs に保存されます。afplay outputs/output_v2_jp.wav で再生しましょう。サンプルの文章がちょうど良いのか悪くない感じがするものの、文章をいろいろ変えて試すと結構きびしいことがわかってきます。でもこれって、OpenVoice の問題じゃないんです。

OpenVoice はボイスチェンジャーである

outputs ディレクトリには output_v2_jp.wav 以外に、tmp.wav ファイルがあるのがわかります。実はこれは MeloTTS で作られた、ベースとなる音声ファイルです。OpenVoice はその音声を、ユーザが用意した音声の特徴で置き換える、つまり乱暴に言うとボイスチェンジャー的なことをしています。音楽で言えばピアノで弾いた曲の音色をギターに変えるようなことをしているので、ベースのアクセントを踏襲しますし、他の言語でもしゃべれてしまう、というわけです。

ええぃ、MeloTTS はキツい!Siri さん、お願いします!

我らが macOS にはいにしえ (Mac OS 9?) より音声合成技術が備わっております。Siri さんなんかは iPhone の普及も相まって、なかなかに鍛わっています。面倒なインストール作業も必要なく、ファイルへの書き出しにも対応しています。というわけでボクはあまり使えない MeloTTS を捨て、音声合成部分に macOS 標準搭載のsayコマンドを使用することにしました。簡単なオプションも追加したスクリプトを書いたので紹介します。名付けて clonesay (クローンせい!) です。やかましいですね。

python clonesay.py でとりあえず動きます (-hを付けると、利用できるオプションの一覧を表示します)。macOS で指定されている音声で、テスト用に入れているデフォルトのテキストを音声合成し、myvoice.mp3 の声色にクローンして outputs/output_jp.mp3 として書き出してくれます。実行中にはごちゃごちゃと英文が表示されますが、最終的に「かかった時間」が表示されればおそらく問題無いはずです。

import os
import torch
from openvoice import se_extractor
from openvoice.api import ToneColorConverter
import time
import subprocess
import argparse

# macOS の 'say' コマンドを使用して、ベースとなる音声ファイルを生成
def text_to_speech(text, output_file):
    command = ['say', '-o', output_file, text]
    subprocess.run(command)

# 引数を解析するための設定
parser = argparse.ArgumentParser(description='OpenVoice を利用して音声のクローニングを行います。事前にクローンしたい音声ファイルを resources/myvoice.mp3 \
                                 として保存してから実行してください。macOS の "say" コマンドを利用しています。\
                                 https://github.com/myshell-ai/OpenVoice/')
parser.add_argument('-i', '--input', type=str, required=False, help='しゃべらせたいテキストを直接入力します。例 -i "こんにちは、おげんきですか"')
parser.add_argument('-f', '--file', type=str, required=False, help='しゃべらせたいテキストが入力されたファイルを指定します。同時に指定した場合は -i が優先されます。')
parser.add_argument('-s', '--source', type=str, required=False, help='テキストから音声を生成せずに、指定した既存の音声ファイルを元にクローンを作成します。\
                    同時に指定した場合は -i、-f が優先されます。')
parser.add_argument('-o', '--output', type=str, required=False, help='出力するファイル名を指定します。利用できるフォーマット: mp3(デフォルト), aiff, wav.')
parser.add_argument('-r', '--reference', type=str, required=False, help='クローン元となる音声ファイルを指定します。\
                    初回指定時には必要なデータが processed ディレクトリに作成されます。')
parser.add_argument('-p', '--play', action='store_true', required=False, help='生成された音声を自動再生します。')
args = parser.parse_args()

home_dir = './'
device = 'cpu'
ckpt_converter = home_dir + 'checkpoints_v2/converter'
tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')
output_dir = 'outputs'
language = 'jp'
src_path = f'{output_dir}/tmp.aiff'

os.makedirs(output_dir, exist_ok=True)
start = time.time()

# クローン元となる音声ファイルの指定
if args.reference:
    reference_speaker = args.reference
else:
    reference_speaker = home_dir + 'resources/myvoice.mp3' # クローン元となる音声ファイル

target_se, audio_name = se_extractor.get_se(reference_speaker, tone_color_converter, vad=False)

# 発声するテキストを指定もしくは、既存の音声ファイルを指定
if args.input:
    input_text = args.input
#elif os.path.isfile(args.file) and os.path.getsize(args.file) > 0:
elif args.file and os.path.isfile(args.file):
    with open(args.file, 'r', encoding='utf-8') as file:
        input_text = file.read()
elif args.source:
    src_path = args.source
    input_text = ''
else:
    input_text = """
    しゃべらせたい文章を、ダッシュアイで指定してください。その他利用可能なオプションは、ダッシュエイチで確認できます。
    """

# 発声するテキストの元となる音声ファイルを生成
if input_text != '':
    text_to_speech(input_text, src_path)

source_se = torch.load(f'checkpoints_v2/base_speakers/ses/{language}.pth', map_location=device)
save_path = f'{output_dir}/output_{language}.mp3' # デフォルトの出力ファイル

# 出力ファイル名の指定がある場合
if args.output:
    output_extension = os.path.splitext(args.output)[1].lower()
    if output_extension in {'.mp3', '.aiff', '.wav'}:
        save_path = args.output
    else:
        save_path = os.path.splitext(args.output)[0] + '.aiff'
else:
    save_path = f'{output_dir}/output_{language}.mp3'

# OpenVoice processing part
# Run the tone color converter
encode_message = "@MyShell"
tone_color_converter.convert(
    audio_src_path=src_path,
    src_se=source_se,
    tgt_se=target_se,
    output_path=save_path,
    message=encode_message)
 
print(f'\nかかった時間: {round(time.time()-start, 2)} 秒\n')

# Play the generated MP3 file using the default media player
if args.play and os.path.isfile(save_path):
    # Use afplay for macOS, which is a simple command-line audio player
    print(f'ファイル: {save_path}')
    if input_text !="":
        print(input_text)
    subprocess.run(['afplay', save_path])
else:
    print("The output file does not exist.")

簡単にいろいろ試すなら、-iオプションでテキストを入力し、-pで生成後に再生するようにするのが良いと思います。例えばこんな感じ:

python clonesay.py -p -i "本日皆さんに集まっていただいたのは、\
新しいエーアイ活用の方法を議論するのが目的です。どうぞ、よろしくお願いします。"

(“AI” は “エーアイ” にするなど、工夫は必要です)

macOS の音声を変えてみる

多分 macOS デフォルトの音声は Siri (声 2) ですが、さすがに男性の声に寄せるのはきびしいと思います。そのような場合には、システム設定で変更しましょう。コマンドでのやり方はみつかりませんでした (声の管理… から、追加もできるようです)。

システム設定… → アクセシビリティ → 読み上げコンテンツ → システムの声

感想

AI による音声のクローニング手法はいくつかあるようで、以前 GPT-SoVITS というのをいじったときには結構衝撃的な結果でした。数秒の音声からかなり高い再現度だったからです。今回はたまたま Weel さんで見つけた OpenVoice を試したわけですが、正直クローニングのクオリティとしては劣っていると感じています。ベースの音声 (Siri さん) に寄りすぎだな、と。ただ、今後ベースの TTS (Text-To-Speech) の性能がとんでもなく高く、自分がクローンしたい声質に近いものが現れたときには TTS だけ乗り換えれば良いわけで、将来性はあるのかもしれない、とも思っています。また、OpenVoice は TTS に頼らない技術です。なので、ボクのサンプルスクリプトの-sオプションでリアルの人物がしゃべった音声ファイルをソースとして指定すれば、ナチュラルな結果が得られるでしょう。何か使い道はありそうですよね。mac ユーザの方は遊んでみてください。

Image by Stable Diffusion (Mochi Diffusion)

今回の画像は何のひねりも無いプロンプトで描いてもらいました。ステップ数が高ければキレイになったりリアルになったりするわけでも無いんですよね。ランダムでしばらく回し、めぼしい Seed 値が見つかった後は何度か Steps の増減を繰り返し、指と女性の顔が一番ブキミじゃなかった、マシだった、という一枚に決めました。

Date:
2024年9月23日 23:14:52

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
human voice clone technology

Exclude from Image:

Seed:
85304328

Steps:
25

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
All

Flet でビルドしたクライアントサイドウェブアプリをデプロイ

デスクトップ用に書いた Flet アプリのサイズや表示位置などをいじり、同じコード&操作感でウェブアプリとして使えるようにしました。今回のビルド方法はクライアントサイドで動くので (公式で言うところの、Static website) 、WordPress 等のホスティングサーバがあれば公開できるはずです。ついでに Google AdSense の広告も表示させています。

下準備

環境の構築方法などは以前の投稿に書いてあるので今回は省きます。以下の記事を参考に構築してください。

Python のフレームワーク Flet で macOS 用デスクトップアプリをビルド

サーバサイドでデプロイする場合

本記事はクライアントサイドなので、要するに HTML と JavaScript で動く公開方法です。サーバサイド、特に Apache ウェブサーバへデプロイする場合は以下の記事をご覧ください。Nginx ウェブサーバへのデプロイは公式サイトを見てもらうのが良いです。

Flet のウェブアプリを Apache ウェブサーバのリバースプロキシで動かす

ウェブアプリとしてビルドする

以前の macOS アプリビルドの記事の、ビルド実行 (手順 13) 直前まで終わっている事を前提に進めます。ウェブアプリに指定できる項目はあまりないようなので、簡単に以下のコマンドでビルドします。ビルドにかかる時間は、macOS 用にビルドした時と特に変わりないようです (本記事では、上記記事で紹介している fletpassgen を使用しています)。

flet build web

ローカル環境で動作確認

完成したファイル群は、build/web にまとめられています。まずは、ローカルのブラウザで動作することを確認しましょう。以下を実行し、ブラウザで http://localhost:8000 を開きます。一通りいじってみて問題無ければ、いよいよサーバへデプロイします。

python -m http.server --directory build/web
# 終了するときは Ctrl + C
Chrome で開いたところ

サーバにアップロードする前の作業

ディレクトリを指定

今回の例では、https://blog.peddals.com/fletpassgen にアクセスしたときにウェブアプリが開くようにします。そのために、index.html を一箇所書き換えます (ビルドするときにオプション --base-dir "/fletpassgen/" を追加していればこの手順は省けますが、ローカルでテストできません。なので、ビルド後に書き換えましょう)。お使いのエディタで build/web/index.html を開き、以下のように書き換えます。ディレクトリ名前後のスラッシュ (/) は必須です。

  <base href="/fletpassgen/">

フォルダを固める

フォルダ名自体も上記ディレクトリ名に変更し、一つのファイルに固めてアップロードしやすくします。以下実行後、fletpassgen.tar.gz が作られます。

cd build
mv web fletpassgen
tar cvzf fletpassgen.tar.gz fletpassgen

サーバにアップロード&その後の作業

固めたファイルをアップロード

ホスティングサーバへ fletpassgen.tar.gz をアップロードします。Terminal.app から実行する場合は以下のようなコマンドになります。指定するユーザ名、ホスト名、ディレクトリ名は適宜変更してください。

scp fletpassgen.tar.gz username@hostname:~/public_html

圧縮ファイルを展開

その後サーバでアップロードしたファイルを展開します。ssh が使える場合はこんな感じです。最後のコマンドは、不要になった圧縮ファイルを削除しています。

ssh username@hostname
cd ~/public_html
tar xvf fletpassgen.tar.gz
rm fletpassgen.tar.gz

Web ブラウザでアクセス

これまでの作業で、Flet のクライアントサイド (static) ウェブアプリがアップロードできました。実際にアップしたものがこちらからアクセスできます: https://blog.peddals.com/fletpassgen/

最初に数秒アイコンが表示され、その後ローカルでテストしたものと同じウェブアプリが表示されれば成功です。

うまくいかない時は

この方法でビルドした場合、ファイル容量が大きくなります。今回使ったサンプルで、展開後のトータルサイズは 28MB あります。なので、最初にアクセスしたときにはブラウザに必要なファイルを読み込むために多少の時間がかかります。まずはとにかく待ってみましょう。

いくら待ってもアイコンすら表示されない場合は、index.html 内のディレクトリ名の設定、実際に展開したディレクトリ名、ディレクトリやファイルのユーザ・グループ・アクセス権それぞれを見直しましょう。既存のファイルやディレクトリを参考に修正してみてください。

Tips と追加情報

デスクトップアプリと同じコードを使う

こぢんまりとしたデスクトップアプリは、ウィンドウサイズを指定し、リサイズを許可しない事で適当にレイアウトしてもどうにかなったりします (当初 fletwebapp がそうでした)。もちろん作るアプリ次第ですが、ウェブアプリにしたときにデザインが崩れないようにするには、page.window_width= で指定した幅を、ft.Containerwidth= プロパティでも指定するのが良いと思います。

サイズが大きいので注意

上にも書きましたが、こんな簡単なアプリ (Python コード単体で 約 3.9KB) でも、ビルドすると Python 自体や Flet、その他必要なファイルが追加されて約 28MB になってしまいます。デプロイの際には注意が必要です。

一度開けばネットワークが切れても動く

サーバサイドのデプロイではサーバとの接続が切れたときにも動作するように設計する必要がありますが、ブラウザにダウンロードするこの方法では、ネットが切れても動きます。用途によってはこの方法で十分でしょう。

Safari はコピーボタンが動かない

いつか修正されると思いますが、Chrome では動作する Copy ボタンが今回のデプロイ方法では動作しません。コード自体には、Safari だと Copy ボタンを表示しない設定を入れていますが、クライアントサイド (static website) デプロイではブラウザのエージェントを確認することができないためボタンを消すこともできませんでした。サーバサイドでのデプロイであれば、コピーはできないものの、ボタンが消えるハズです (そのうちテストします)。

おまけ: Google AdSense の広告を追加する

こちらのサイトを大いに参考にさせていただきました。ありがとうございます。

Flutterで作ったWebアプリでGoogleAdSenseの広告を表示する。

AdSense の HTML コードを取得

上記サイトを参考にコードを作成し、以下の部分をどこかに保存しておきます。

Google AdSence > 広告 > 広告ユニットごと > ディスプレイ広告 > 名前を付けて、作成

             data-ad-client="xxxxxxxx"
             data-ad-slot="yyyyyyyy"

index.html に style を追加

Flet ウェブアプリのディレクトリ (本記事では fletpassgen) 内にある index.html に以下 CSS の設定を追加します。追加場所は </style> タグのすぐ上あたりが良いでしょう。行番号は、Flet 0.19.0 の場合の参考としてください。

    footer{
        width: 100%;
        height: 100px;
        text-align: center;
        padding: 0;
        position: absolute;
        bottom: 0;
        z-index: 100;
    }

index.html に <footer></footer> ブロックを追加

最終行 </body></html> の上に、以下を追加します。ハイライトした data-ad-clientdata-ad-slot にはそれぞれ、先ほど AdSense からコピーした内容を貼り付けます。行番号に関しては同じく参考まで。

  <footer>
    <style>
    .example_responsive_1 { width: 320px; height: 100px; }
    @media(min-width: 500px) { .example_responsive_1 { width: 468px; height: 60px; } }
    @media(min-width: 800px) { .example_responsive_1 { width: 728px; height: 90px; } } 
    </style>
        <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
        <!-- Home_Page -->
        <ins class="adsbygoogle"
             style="display:inline-block"
             data-ad-client="AdSence でコピーした内容を貼る"
             data-ad-slot="AdSence でコピーした内容を貼る">
        </ins>
        <script>
    (adsbygoogle = window.adsbygoogle || []).push({});
    </script>
  </footer>

変更内容を保存し、ウェブアプリの画面最下部に広告が表示されていれば成功です。

Image by Stable Diffusion

Date:
2024年1月29日 0:04:44

Model:
realisticVision-v20_split-einsum

Size:
512 x 512

Include in Image:
masterpiece, best quality, retro future, successful upload of application

Exclude from Image:

Seed:
3400661084

Steps:
20

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & Neural Engine

mac でラズパイ起動用 SD カードや USB メモリをまるごとバックアップする簡単な Python スクリプト

ラズパイで使用している SSD が突然読み込めなくなったので、おおよそ1年前にバックアップしてあったディスクイメージを別の SSD にリストアし、諸々1年分を作り直しました。当然すごく面倒だったので、外付けメディアのバックアップを取る極シンプルな mac 用 Python スクリプトを書きました。ターゲットのデバイスを指定すると、バックアップの後イジェクトまでやってくれます。

いちおう、環境

  • macOS: Sonoma 14.2
  • Python: 3.11.6
  • 追加パッケージ等: 不要

普通にやるならこんな手順

mac に記憶デバイスを接続して一覧を表示、ターゲットとなるデバイス番号を確認、デバイスをアンマウントして、バックアップ開始、終了したらイジェクトして、記憶デバイスを物理的に取り外す、というのが一連の手順になります。Ubuntu がインストールされた 128GB SSD をバックアップしたときの例はこんな感じです ($ 以降が入力するコマンド)。

$ diskutil list external
... 省略 ...
/dev/disk4 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *128.0 GB   disk4
   1:             Windows_FAT_32 system-boot             268.4 MB   disk4s1
   2:                      Linux                         127.8 GB   disk4s2
... 省略 ...
$ sudo diskutil unmountDisk /dev/disk4
Password:
Unmount of all volumes on disk4 was successful
$ sudo dd if=/dev/rdisk4 of=RPi3BP_128GB_20231218-1.img bs=6M status=progress
127999672320 bytes (128 GB, 119 GiB) copied, 2054 s, 62.3 MB/s
20350+1 records in
20350+1 records out
128035674112 bytes (128 GB, 119 GiB) copied, 2054.98 s, 62.3 MB/s
$ sudo diskutil eject /dev/disk4
Password:
Disk /dev/disk4 ejected

最初のコマンドは接続されている記憶デバイスの一覧を表示するのですが、external を付けて外付けディスクのみ表示させています。複数表示される場合は、表示される SIZE の値を頼りにするか、接続前に一度 diskutil list external を実行し、メディアを接続してから再度 diskutil list external で追加されたメディアの番号を確認します。

dd コマンドの説明:

  • if は、入力ファイルを指定。上の例の /dev/rdisk4 は、シーケンシャルにアクセスされるデバイスを指定しているので、/dev/disk4 とやるより速い
  • of は、出力ファイルを指定。上の例は、コマンドを実行したディレクトリに .img の拡張子を付けて書き出し
  • bs で、一度に処理するブロックサイズを指定。m1 mac mini (16GB RAM) でいくつか値を変えてテストした結果、6M (6メガバイト) が一番安定して速い速度で処理できた
  • status=progress を付けると、実行中に進捗状況が表示される (2025/06/19: 誤って process となっていたところを全てprogressに訂正しました)

mac 専用 Python スクリプト (とは言ってもほぼシステムコマンドを逐次実行)

import subprocess
import sys
import os
from datetime import datetime

if not os.getuid()==0:
    sys.exit(">> 管理者権限が必要です。'sudo python3 clibackup.py' の形式で再度実行し、パスワードを入力してください。")

subprocess.run(["diskutil", "list", "external"])
dnum = input(">> バックアップする記憶デバイスの番号を数字で入力してください('q' を入力すると終了)。\n>> (例: /dev/disk9 の場合は 9 を入力)\n>> /dev/disk?: ")
if dnum == 'q':
    print('終了しました。')
    sys.exit()
print(f">> ターゲット /dev/disk{dnum} をマウント解除します。")
subprocess.run(["diskutil", "umountDisk", "/dev/rdisk"+dnum])

timestamp = datetime.now().strftime("%Y%m%d%H%M")
#cmd = ["dd", "if=/dev/rdisk"+dnum, f"of=/Volumes/External HDD/SD_Card_Backup/backup_{timestamp}.img", "bs=6M", "status=progress"]
cmd = ["dd", "if=/dev/rdisk"+dnum, f"of=backup_{timestamp}.img", "bs=6M", "status=progress"]

print("\n>> 以下のコマンドを実行してバックアップします:")
for i in range(len(cmd)):
    print(cmd[i], end = " ")
print("\n")

process = subprocess.run(cmd)

print(f"\n>> バックアップが完了しました。デバイス /dev/disk{dnum} をイジェクトします。")
subprocess.run(["diskutil", "eject", "/dev/disk"+dnum])

保存先ディレクトリを指定する場合は、18行目の様に of= の後にパスを入れてください。ダブルクォーテーションでくくってあるので、スペースがあってもエスケープは不要です。作られるディスクイメージのファイル名は、年月日時分を含んだ backup_YYYYmmddHHMM.img になります。実行時の「分」までファイル名に含めているため、既存のファイルを上書きすることは無いでしょう。エラー処理の一切はしていないのであしからず。

使い方

sudo python3 clibackup.py で実行します (sudo を忘れると、メッセージを表示して終了します)。パスワードを入力すると、マウントされている外付け記憶デバイスの一覧が表示されるので、バックアップしたい USB ドライブなり SD カードなりのディスク番号を入力するとバックアップがスタートします。もし対象がわからない場合、一度接続していない状態で本スクリプトを実行し q で終了、その後メディアを接続して再度スクリプトを実行すると、前回は表示されていなかった /dev/disk があるはずなので、その番号を指定します。バックアップが完了すると自動でメディアをイジェクトするので、そのまま mac から取り外してかまいません。

実行例

Raspbian が入った 16GB の micro SD カード (SanDisk Ultra Class 10 A1 MicroSD HC I) を、mac mini の内蔵 SSD にバックアップしたときの実行例です。速度は 34MB/s、大体 8分ほどで完了しています。使用する記憶デバイスや接続の仕方次第で速度は結構違います (本記事頭で使用している SSD は、62.3MB/s)。自分は 2つのパーティションに分けた外付け 4TB HDD があるので、/dev/disk6~8 も表示されていますが、これらも環境によって変わります。

% sudo python clibackup.py 
Password:
/dev/disk4 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *15.9 GB    disk4
   1:             Windows_FAT_16 RECOVERY                1.7 GB     disk4s1
   2:                      Linux                         33.6 MB    disk4s5
   3:             Windows_FAT_32 boot                    72.4 MB    disk4s6
   4:                      Linux                         14.1 GB    disk4s7

/dev/disk6 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *4.0 TB     disk6
   1:                 Apple_APFS Container disk8         2.0 TB     disk6s1
   2:                 Apple_APFS Container disk7         2.0 TB     disk6s2

/dev/disk7 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +2.0 TB     disk7
                                 Physical Store disk6s2
   1:                APFS Volume External HDD            1.4 TB     disk7s1

/dev/disk8 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +2.0 TB     disk8
                                 Physical Store disk6s1
   1:                APFS Volume handsome's mac mini...  673.7 GB   disk8s1
   2:                APFS Volume Buffalo HDD             1.0 TB     disk8s2

>> バックアップする記憶デバイスの番号を数字で入力してください('q' を入力すると終了)。
>> (例: /dev/disk9 の場合は 9 を入力)
>> /dev/disk?: 4
>> ターゲット /dev/disk4 をマウント解除します。
Unmount of all volumes on disk4 was successful

>> 以下のコマンドを実行してバックアップします:
dd if=/dev/rdisk4 of=backup_202312231503.img bs=6M status=progress 

  15904800768 bytes (16 GB, 15 GiB) transferred 474.073s, 34 MB/s   
2532+1 records in
2532+1 records out
15931539456 bytes transferred in 474.873141 secs (33549043 bytes/sec)

>> バックアップが完了しました。デバイス /dev/disk4 をイジェクトします。
Disk /dev/disk4 ejected

記憶デバイスへの書き込み

スクリプトを書いていないので、手動でいくつかコマンドを打ってください。手順は本記事最初のサンプルと同様ですが、入出力が逆になります。dd コマンドでは、if= (入力ファイル) にディスクイメージ、of= (出力ファイル) に USB や SD カードなどの /dev/rdiskディスク番号 を指定します。出力先を間違うと取り返しがつかないことになり得るので、注意してください (サンプル通りに実行して大事なデータが消えてしまっても当方では責任とれません)。以下は、/dev/disk4 に SD カードがマウントされているときの例です。メディアを抜いた状態で diskutil list external を一度実行し、メディアをさしてから再度実行すれば、増えた /dev/disk が該当の記憶デバイスになります。

(記憶デバイスを接続していない状態で実行)
$ diskutil list external

(記憶デバイスを接続して再度実行)
$ diskutil list external
... 省略 ...
/dev/disk4 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *15.9 GB    disk4
   1:             Windows_FAT_16 RECOVERY                1.7 GB     disk4s1
   2:                      Linux                         33.6 MB    disk4s5
   3:             Windows_FAT_32 boot                    72.4 MB    disk4s6
   4:                      Linux                         14.1 GB    disk4s7
... 省略 ...
$ sudo diskutil unmountDisk /dev/disk4
Password:
Unmount of all volumes on disk4 was successful
$ sudo dd if=backup_202312231503.img of=/dev/rdisk4 bs=6M status=progress

Image by Stable Diffusion

Date:
2023年12月23日 16:42:10

Model:
fruity-mix_split-einsum_compiled

Size:
512 x 512

Include in Image:
comicbook-style, cloned sheep standing side by side

Exclude from Image:

Seed:
3723203146

Steps:
25

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & Neural Engine

Flet のウェブアプリを Apache ウェブサーバのリバースプロキシで動かす

Python でサクッとデスクトップアプリが作れる Flet ですが、同じコードに少しの変更をするだけでブラウザで動かせるようになります。今回の記事は、公式サイトにはない、Apache ウェブサーバで Flet アプリを動かす (セルフホスティングする) 方法です。

まず簡単に Flet とは

GUI やウェブフロントエンドの知識・経験がほとんど無くても、お手軽にデスクトップアプリやウェブアプリが作れる Python のフレームワークです。本家 (?) は Flutter という、Google さんが Dart という言語向けに開発しているモバイルアプリ用フレームワークです。それを Python から利用できるようにしたのが Flet と考えて良さそうです。実際、Flet 出書いたコードから吐き出されるエラーをネットで検索すると、Flutter に関するポストが多くヒットします。本記事では Flet も Flutter も深く触れませんので、詳細は他のサイトを漁ってください。

この記事で説明すること

キモは、Apache の TCP ポート指定を利用したリバースプロキシで Flet のウェブアプリを公開する、というところです。公開と言ってもボクの環境では、LAN にある mac からブラウザでアクセスできるようにするまでですが、パブリックに公開している Apache ウェブサーバでも同様の方法で公開できます。Flet の公式サイトでは、こちらの Self Hosting で NGINX ウェブサーバを使用した公開方法が紹介されていますが、それの Apache 版ということです。本記事のタイトルそのものズバリを説明しているサイトが見つからなかったので、まとめました。

環境

  • Ubuntu 20.04 LTS
  • Apache 2.4.41

ざっくりとした手順

  1. Ubuntu サーバに、Flet アプリの実行に必要なパッケージをインストール
  2. Python の仮想環境を作り、Flet をインストール
  3. Flet アプリを準備
  4. リバースプロキシに必要なモジュールを Apache で有効化
  5. Apache のコンフィグファイルを書く
  6. 自動的に起動する設定を書く

細かい手順

Ubuntu サーバに、Flet アプリの実行必要なパッケージをインストール

公式サイトの通り、Linux で Flet を動かすためには GStreamer のインストールが必要です。ここはサクッと入れてしまいましょう。

sudo apt-get update
sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio

Python の仮想環境を作り、Flet をインストール

ボクは pipenv を使っているので、こんな感じで仮想環境を作ります。手慣れた仮想環境とバージョンで、ここもサクッとどうぞ。サポートされている Python のバージョンは 3.8 以上です。

pipenv --python 3.11
pipenv shell
pip install flet

Flet アプリを準備

とりあえず、公式からそれっぽいものを持ってきましょう。インタラクションが確認できるので、ボクはこちらの Counter app を使用しました。とりあえず counter.py として保存し、最後の行を下のように編集します。

ft.app(target=main, view=None, port=8501)

簡単に説明すると、view=None でウィンドウやブラウザによる表示を行わず、ポート 8501 で待ち受けるよう指示をしています。ポートは同一サーバ上で重複が無ければ何でもかまいません。GUI とブラウザがインストールしてある環境であれば、python3 counter.py で実行すると、http://localhost:8501 にアクセスすればウェブアプリが開くと思います。次の手順以降で、外部にウェブアプリとして公開します。

リバースプロキシに必要なモジュールを Apache で有効化

Apache でリバースプロキシを行うには、いくつか必要なモジュールを追加する必要があります。Flet の場合、web socket も利用するため、wstunnel も必要です。以下は、モジュール追加、Apache の再起動、ステータス確認を実施しています。

sudo a2enmod proxy proxy_http proxy_wstunnel headers
sudo systemctl restart apache2
sudo systemctl status apache2

Apache のコンフィグファイルを書く

この例では、クライアント (PC 等のブラウザ) から flet.dev.peddals.com にアクセスすると Flet ウェブアプリが開く構成にしています。また、別記事に書いた様にこのドメインへの接続は HTTPS 接続になるので、Apache はポート 443 で待ち受け、内部的に 8501 ポートへリバースプロキシしています。このあたりはご自身の環境に合わせて指定してください。

13-14行目の wss:// の部分はひょっとしたら環境によっては必要ないかもしれません。

<VirtualHost *:443>
	ServerName flet.dev.peddals.com

	SSLEngine on
	SSLCertificateFile /etc/letsencrypt/live/dev.peddals.com/fullchain.pem	
	SSLCertificateKeyFile /etc/letsencrypt/live/dev.peddals.com/privkey.pem

	ProxyRequests Off
	ProxyPreserveHost On

	ProxyPass /ws ws://localhost:8501/ws
	ProxyPassReverse /ws ws://localhost:8501/ws
	ProxyPass /ws wss://localhost:8501/ws
	ProxyPassReverse /ws wss://localhost:8501/ws
	ProxyPass / http://localhost:8501/
	ProxyPassReverse / http://localhost:8501/

	ErrorLog ${APACHE_LOG_DIR}/flet.error.log
	CustomLog ${APACHE_LOG_DIR}/flet.log combined

</VirtualHost>

Apache に設定を読み込ませます。

sudo apachectl configtest
sudo systemctl reload apache2
sudo systemctl status apache2

この状態で一度 python3 counter.py で実行し、別のクライアント PC からサイトへアクセスし、動作するか確認してみましょう。wss:// の行を削除して読み込みが終わらない様でしたら追加してください。

自動的に起動する設定を書く

ここは公式のやり方を参考に編集します。自分の環境に合わせたものを貼っておきます。これを fletcounter.service として、counter.py と同じディレクトリに保存しています。

[Unit]
Description=Flet Counter Service
After=network.target

[Service]
User=handsome
Group=handsome
WorkingDirectory=/home/handsome/codes/flet
Environment="PATH=/home/handsome/.local/share/virtualenvs/flet-xuR7EMBP/bin/"
ExecStart=/home/handsome/.local/share/virtualenvs/flet-xuR7EMBP/bin/python3 /home/handsome/codes/flet/counter.py

[Install]
WantedBy=multi-user.target

いじる部分 (いじった内容) は以下の通りです:

  • Description= はご自由に
  • User=Group= には自分のユーザ名 (whoami)
  • WorkingDirectory= には、counter.py のあるディレクトリのパス
  • Environment="PATH= には、python3 のあるディレクトリのパス (which python3 の出力の bin/ まで)
  • ExecStart= の最初の引数は which python3 の出力全て、次の引数には counter.py のフルパス

そして最後にサービスとして起動、有効化します。これも公式のやり方に従います。シンボリックリンクの元ファイルは、上記のファイルを指定します。

cd /etc/systemd/system
sudo ln -s /home/handsome/codes/flet/fletcounter.service
sudo systemctl start fletcounter
sudo systemctl enable fletcounter
sudo systemctl status fletcounter

以上で設定はおしまいです。クライアント PC からアクセスし、カウンターが表示されれば OK です。可能であればサーバを再起動し、起動後にもカウンターが表示されることを確認しましょう。

ハマったところ

自分の環境で当初発生していた、読み込みが一生終わらない状態を解決するのにすごく時間がかかりました。原因は、ProxyPassProxyPassReverse にそれぞれ ws://http:// の両プロトコルだけしか指定していないことでした (公式の NGINX のリバースプロキシのコンフィグにも wss:// は無い) 。wss がウェブソケットのセキュア版 (http に対する https) と気づけなかったら諦めていたと思います。– なんて言いつつ、その後ラズパイの SSD が死に、リバースプロキシの設定をやり直したところ、wss:// の 2行が無くても問題なく動くようになっていました。ナゾ。

Image by Stable Diffusion

Date:
2023年11月25日 23:02:10

Model:
realisticVision-v20_split-einsum

Size:
512 x 512

Include in Image:
cartoon, clolorful,
modern ladies working at post office classifying letters

Exclude from Image:

Seed:
4084494267

Steps:
23

Guidance Scale:
11.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & Neural Engine

© Peddals.com