Flet で作った、音声と字幕 (SRT) の同時再生・字幕編集アプリ『字幕極楽丸』のご紹介

OpenAI の音声文字起こし AI である Whisper を試したところ、その性能の高さに非常に興奮しました。無料なうえローカルで完結し、M1 mac mini 16GB RAM なら音声ファイル自体の 90% 位の時間でかなり使える文字起こしをしてくれます。すごい時代です。で、精度を高める方法を調べていろいろ試したものの、やはり 100% にはなりません。だったら逆に、人間がテキストを簡単に修正ができるアプリがあれば良いだろうとの発想から、音声を聞きながら Whisper が書き出した文章を編集できるアプリ、その名も『字幕極楽丸』を Flet で作りました。今回はとりあえずアプリの紹介として、使い方やインストール方法を説明します。コードの内容に関しては別記事にするつもりですが、(適当な英語の) コメントを入れてあるので、気になる方は GitHub を覗いてみてください。

アプリの紹介

Whisper 等の文字起こし (speech-to-text) ツールで書き出した字幕ファイルを、音声ファイルと同期しながら再生し、必要に応じてテキストを編集できるというのがこのアプリです (と言っても、現状は flet build macos コマンドで実行ファイルとして書き出すと Numpy の読み込みができないためにクラッシュします。また、いくつか既知の不具合もあるため、一応の完成型といういう状態です)。英名は全くひねらず『Speech + Subtitles Player』、略して SPSP (または、SPS Player)。和名は『字幕極楽丸』です。はい、ファミカセ『地獄極楽丸』インスパイアー系の命名となっています (名前だけ)。ま、どうでも良い話ですね。ロゴにはカッコイイフリーフォント (フロップデザインさんの、源界明朝) を使わせていただきました。ありがとうございます。

アプリの使い方

(実行方法は下記「アプリの実行方法」参照) Open Speech File ボタンで音声ファイルを読み込むと、同名の字幕ファイル (拡張子 .srt) もしくはテキスト (拡張子 .txt) が同じフォルダにあれば自動的に読み込み、タイムスタンプと字幕テキストを表示します。Play ボタンで音声を再生し、テキストは音声に合わせてスクロールします。タイムスタンプをクリックすると、そこへ頭出しします。テキスト部分をクリックすると修正が行えます。編集内容は Save ボタンで同一ファイルに上書きし、SRT と TXT ボタンはそれぞれ別ファイルとして書き出します。1.5x と Auto scroll のスイッチはそれぞれ、1.5 倍速再生、音声に合わせたテキストの自動スクロールをオン・オフできます (自動スクロールはタイムスタンプのある SRT 形式のみ可能)。現在既知の問題として、Open/Export as のボタンをクリックするとダイアログが開かず、アプリ全体が動作しなくなることが確認できています。編集を行っている際には頻繁に Save をクリックするようにしてください。

TXT で書き出した場合はタイムスタンプが含まれない文章のみのため、アプリ内でのオートスクロールができませんが、議事録やレポートなど本アプリ以外の様々な用途に利用できます。SRT は字幕フォーマットとしてよく使われる形式 (Wikipedia) で、音声の元データが映像であれば、DaVinci Resolve (フリーでも利用できる映像編集アプリ) 等で字幕データとして動画に取り込めます。

アプリの対象ユーザ/ユースケース

映像に字幕を埋め込みたい方が主な対象ユーザになると思います。また、Whisper 含め文字起こし AI の精度の検証を行うエンジニアや、コールセンタで通話内容をレポートにまとめるオペレータと言った方々には有用だと思います。他には、ミーティングの議事録を AI に出力させてから清書をするとか、外国語の学習にも便利に使ってもらえるでしょう (精度の違いを無視すれば、Whisper ではかなりの数の言語がサポートされています: Supported languages)。

アプリの実行方法

アプリアプリ言ってますが、現状ダブルクリックで開くアプリケーションとして書き出せないため、下記方法でコマンドラインからの実行が推奨です。大きな違いは無いはずですが、Windows や Linux の方は、すみませんがよしなにお願いします。コードは GitHub に置いてあります。

https://github.com/tokyohandsome/Speech-plus-Subtitles-Player

コードをクローンし、Python の仮想環境を作り、Flet と Numpy をインストール

Python は 3.8 以降であれば大丈夫のハズです (制作環境の Python は 3.11.7)。以下例では仮想環境の作成に pipenv を使用していますが、何でもかまいません。

git clone https://github.com/tokyohandsome/Speech-plus-Subtitles-Player.git
cd Speech-plus-Subtitles-Player
pipenv --python 3.11
pipenv shell
pip install flet
pip install numpy

アプリを実行

環境ができたら、以下コマンドで字幕極楽丸を実行できます。

python main.py

音声ファイルを選択

起動後、Open Speech File ボタンをクリックして、MP3 や WAV 等の音声ファイルを選択します。macOS では初回に書類フォルダへのアクセス権を与えるか聞かれると思いますので、許可してあげてください。音声ファイルと同じフォルダに、同じファイル名で拡張子が .srt (もしくは .txt) となっているファイルがあると、自動的に読み込まれます (スクショで言うところの、kishida.m4a を開くと kishida.srt も読み込まれる)。音声ファイルを読み込んだ後ならば、手動で字幕ファイルを読み込むこともできます。

既知の不具合

クリティカルな問題は無いと思いますが、心配な方は SRT や TXT のバックアップを別の場所に保存した上でご利用ください。

  • 字幕のボタンが多くなると、ウィンドウの移動やリサイズの際にカクつきます
  • flet build macos --include-packages flet_audio 等としてアプリケーションとして書き出しても、実行時にクラッシュします  (Flet version == 0.21.2)。オートスクロールが不要であれば、import numpy as np をコメントしてもらえれば、書き出した実行ファイルも動きます
  • Open や Export のボタンをクリックすると、ダイアログが開かず、アプリを閉じる以外できなくなることがあります (原因調査中)。セーブは頻繁に行ってください
  • macOS で再生できる MP3 のサンプルレートは 44.1KHz までのようです。それより高いサンプルレートの場合は Audacity 等で変換してください
  • SRT は本来、字幕部分が複数行あっても良いみたいですが、本アプリでは 2行以上あることを想定していません。Whisper で書き出した SRT ファイルは問題無いはずです

おまけ

細かいことは書きませんが、Youtube 等のオンライン動画を音声ファイルとしてダウンロードする方法と、Apple が macOS 用に最適化した Whisper で音声ファイルを SRT に書き出す方法を貼っておきます。

オンラインの動画を m4a 音声ファイルとしてダウンロード

pip install yt_dlp
python -m yt_dlp -f 140 "動画ページのリンク"

Whisper で音声ファイルを SRT に書き出す (文字起こしする) Python スクリプト

macOS であれば、MLX 版 Whisper が動く環境を作り、Hugging Face から whisper-large-v3-mlx (jsonnpz) をダウンロードして mlx_models フォルダに展開します。その後、mlx-examples/whisper フォルダに以下のファイル speech2srt.py を作ります。5-6行目のフォルダ名とファイル名はそれぞれ書き換えてください。日本語以外の場合は、音声の言語に合わせて language='ja', の部分を変更してください (日本語の音声を ‘en’ で指定すると英語訳した字幕が書き出され、英語音声を ‘ja’ 指定すると日本語訳されたものが書き出されます。が、残念ながら翻訳の精度は非常に低いのでやめた方が良いでしょう)。

import whisper
import time
import os

base_dir = "音声ファイルのあるフォルダ名"
speech_file_name = "読み込みたい音声ファイル名"

start_time = time.time()
speech_file = base_dir + speech_file_name
model = "mlx_models/whisper-large-v3-mlx" 

result = whisper.transcribe(
                            speech_file, 
                            language='ja', 
                            #language='en', 
                            path_or_hf_repo=model, 
                            verbose=True,
                            #fp16=True,
                            word_timestamps=True,
                            condition_on_previous_text=False,
                            #response_format='srt',
                            append_punctuations="\"'.。,,!!??::”)]}、",
                            #append_punctuations="。!?、",
                            #initial_prompt='です。ます。した。',
                            temperature=(0.0, 0.2, 0.4, 0.6, 0.8, 1.0),
                            )

end_time = time.time()
elapsed_time = round(end_time - start_time, 1)

print('############################')
print(f"処理にかかった時間: {elapsed_time}秒")
print('############################')

def ms_to_srt_time(milliseconds):
    seconds = int(milliseconds / 1000)
    h = seconds // 3600
    m = (seconds - h * 3600) // 60
    s = seconds - h * 3600 - m * 60
    n = round(milliseconds % 1000)
    return f"{h:02}:{m:02}:{s:02},{n:03}"

subs = []
sub = []
for i in range(len(result["segments"])):
    start_time = ms_to_srt_time(result["segments"][i]["start"]*1000)
    end_time = ms_to_srt_time(result["segments"][i]["end"]*1000)
    text = result["segments"][i]["text"]

    sub = [str(i+1), start_time+' --> '+end_time, text+'\n']
    subs.append(sub)

text_file = base_dir + os.path.splitext(os.path.basename(speech_file_name))[0] + ".srt"

# Overwrites file if exists.
with open(text_file, 'w') as txt:
    for i in subs:
        for j in range(len(i)):
            txt.write('%s\n' % i[j])

後は Python で実行すれば、音声ファイルと同じフォルダに SRT ファイルが作られます。同名ファイルがあると上書きするので注意してください。

python speech2srt.py

Whisper は GPU で動きます。参考まで、Mac Studio (M2 Max 30 コア GPU) だと、大体音声の長さの 1/6 位で書き出しが完了します。

Image by Stable Diffusion

Date:
2024年3月24日 23:45:42

Model:
realisticVision-v20_split-einsum

Size:
512 x 512

Include in Image:
realistic, masterpiece, best quality, retro future, office ladies transcribing audio from record player

Exclude from Image:

Seed:
2389164678

Steps:
20

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & Neural Engine

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

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

2023年末にリリースされた Flet のバージョン 0.18.0 では、以前からあった pack より良いビルド方法 build が導入されました。自分も macOS 用にビルドしてみたのですが、なぜか一つの環境では Hello, Flet とだけウィンドウに表示されるアプリがビルドされ、いろいろ試すも解決できず。そしてつい数日前の 2024/1/16 に、主に flet build 関連の bug fix がなされたバージョン 0.19.0 がリリースされたので、macOS 用デスクトップアプリをビルドしてみました。

リリースのあった公式 discord: https://discord.com/channels/981374556059086931/981375816489402408

公式 GitHub の change log: https://github.com/flet-dev/flet/blob/main/CHANGELOG.md

公式 flet build の解説ページ (英語): https://flet.dev/docs/guides/python/packaging-app-for-distribution

まずはビルド前の Python スクリプトを簡単にご紹介

パスワード生成アプリを作りました。今回は Flet のビルドの紹介をメインとするので、アプリ自体の細かい説明は省きます。が、簡単に何ができるかというと、好きな文字数で、好きな特殊文字を含んだ、ある程度強度のあるパスワードを生成することができます。生成されたパスワードはコピーボタンでクリップボードにコピーできます。Safari が作ってくれる強力なパスワードが使えないサイトや、もっと強度を強くしたい、等の用途で活躍します。パスワード強度をチェックしてくれるサイト (bitwarden 等) で強度を調べると解析されるまで数世紀かかる等と結果が出て面白いです。

コードは GitHub に置いてあります: https://github.com/tokyohandsome/passgen.py/blob/main/fletpassgen.py

今回の Flet アプリ。Generate で生成し、Copy でクリップボードにコピー。文字数と特殊文字はキーボードから入力可能

環境

  • macOS: 14.2.1
  • 仮想環境 (pipenv): 2023.10.24
  • Python: 3.11.6
  • 追加モジュール: flet (バージョン: 0.19.0) 自動的に flet-core と flet-runtime も入っている
  • Rosetta
  • Xcode: 15.2
  • Git: 2.29.2
  • cocoapods: 1.14.3_1
  • Flutter: 3.16.7

手順

  1. 仮想環境を作る
  2. flet をインストール (バージョン 0.18.0 以上、今回は最新の 0.19.0)
  3. コードを書く、もしくは持ってくる
  4. 動作確認
  5. Flutter とその他必要なものを一式インストール
  6. アプリ名のフォルダを作り、以降はその中で作業
  7. assets フォルダを作り、アイコン (icon.png) を格納
  8. 手順 3. のコードを main.py とリネームしてコピー
  9. main.py の最後、メイン関数を呼び出す行を ft.app(main) だけにする
  10. requirements.txt に追加モジュールを書く
  11. ビルドテンプレートを GitHub からクローン
  12. ビルドテンプレートのコピーライト表記などを編集
  13. ビルド実行

部分的に細かく (手順 1~4)

仮想環境はお好きなのを使って良いと思います。Python は 3.8 以降、flet は特にバージョン指定せず pip install flet で良いでしょう。自分で書いた Flet アプリのコードが無ければ、上記のボクの GitHub からコピーしたものを適当な名前 (例: fletpassgen.py) で保存してください。python3 fletpassgen.py で動作することを確認したら、デスクトップアプリビルドに必要な諸々をインストールします。

Flutter とその他必要なものを一式インストール (手順 5)

ビルドには元祖である Flutter や開発言語の Dart とその他必要なもの一式をインストールする必要があります。基本的には一度済ませれば何度も行う必要はありません。以下は Apple Silicon (M1、M2、M3 シリーズ) の場合です。Flutter のサイトを参考に全て準備しましょう。

1. Rosetta: 以下を実行してインストール

sudo softwareupdate --install-rosetta --agree-to-license

2. Xcode 15: このページ右上の Download から Xcode 15 をダウンロードし、インストール

3. Cocoapods: 以下を実行してインストール

brew install cocoapods

4. Git: 以下を実行してインストール

brew install git

5. Flutter: ページ中頃のあたりの手順に従い、自分の CPU 用の Flutter SDK をダウンロードし、適当なフォルダ (例では ~/development/) に移動して .zip ファイルを展開、最後に環境変数 PATH にパスを追加 (以下の handsome を自分のユーザ名に変えて、~/.zshrc に追加し、source ~/.zshrc で読み込み)

export PATH="/Users/handsome/development/Flutter/flutter/bin:$PATH"

ビルドの準備 (手順 6~12)

Flet アプリとしての動作が確認できたら、最終的なアプリ名のフォルダを作り、中に入って準備を進めます。今回は fletpassgen という名前のアプリにします。

mkdir fletpassgen
cd fletpassgen
mkdir assets
open assets

最後の open assets で Finder でフォルダを開くので、アプリのアイコンに使用したい 512×512 ピクセルの画像ファイルを icon.png に従った名前で保存します (フォーマットは他に .bmp.jpg.webp が対応)。今回は使いませんが、その他アプリで使用する音声やテキストなどのファイルも assets フォルダに保存します。

アイコン画像について余談: 画像ファイルが無くても Flet のアイコンが使われるので、ビルドはできます。ボクは画像生成 AI 、Stable Diffusion の mac 用クライアントアプリ Mochi Diffusion を使ってアイコンを生成しました。参考までに本記事の最後にモデルやプロンプトを貼っておきます。

次に、Flet アプリのコードを main.py としてコピーしてきます。

cp ../fletpassgen.py main.py

Python コード最後の main 関数呼び出し部分が ft.app(main) じゃない場合は変更します。

#if __name__ == "__main__":ft.app(target=main) だったりしたら変更
ft.app(main)

requirements.txt には追加が必要なモジュールを記載するのですが、pip freeze > requirements.txt で書き出すと様々なエラーでビルドが止まり、ハマりました。今回のように標準のモジュールしか使用していない場合は、flet だけあれば大丈夫です。iOS や Android 用にビルドする場合も注意が必要なので、詳細は公式ページを見てください。

flet

コマンド + i でコピーライト表記が正しく表示されるように、テンプレートを公式の GitHub からクローンして cookiecutter.json の中身を書き換えます。以下例ではエディタに vi (vim) を使用していますが、お好みでどうぞ。

git clone https://github.com/flet-dev/flet-build-template
vi flet-build-template/cookiecutter.json

ハイライトした、7-9行目だけいじってます。

{
    "out_dir": "",
    "python_module_name": "main",
    "project_name": "",
    "project_description": "",
    "product_name": "{{ cookiecutter.project_name }}",
    "org_name": "com.peddals",
    "company_name": "Peddals.com",
    "copyright": "Copyright (c) 2024 Peddals.com",
    "sep": "/",
    "kotlin_dir": "{{ cookiecutter.org_name.replace('.', cookiecutter.sep) }}{{ cookiecutter.sep }}{{ cookiecutter.project_name }}{{ cookiecutter.sep }}",
    "hide_loading_animation": true,
    "team_id": "",
    "base_url": "/",
    "route_url_strategy": "path",
    "web_renderer": "canvaskit",
    "use_color_emoji": "false"
}

ビルド実行 (手順 13)

何もかも気にせずとりあえずビルドするなら、flet build macos でかまいません。バージョンを指定し、テンプレートをローカルに作ったものから読み込ませるなら、もうちょっと長く以下のように実行します (バージョン --build-version は指定しなければ 1.0.0 になるので、このサンプルではあえて 1.0.1 にしています)。(以下、Flet 作者の Feodor による説明を元に訂正 2024/01/25) ビルドテンプレートの指定は公式の説明に理解が及ばず、当初以下のように書いていましたが、--template の後に相対パス指定になります。修正前の方法でもビルドできますが、あるべき書き方にコマンドも修正しました。テンプレートの指定は相対パス (relative path) と公式ドキュメントにありますがうまくいかないので `pwd` を頭に付けて絶対パスとして指定しています。また、テンプレート指定のオプション自体が公式だと --template_dir (アンダースコア) になっていますが、0.18.0 と 0.19.0 では --template-dir (ハイフン) が正解でした。

flet build macos --build-version "1.0.1" --template flet-build-template

やや待って Success! が表示されれば完了です。build/macos/ の下に、アプリ fletpassgen.app が作られています。ボクの書いた fletpassgen.py を M1 mac mini でビルドすると、大体 3分 10秒くらいで完了しました。基本的に高効率コアが 6-8 割の使用量で推移し、所々パフォーマンス (高性能) コアのスパイクが見える感じです (他にもアプリが動いているので、はっきりとしたことはわかりませんけど)。

Creating Flutter bootstrap project...OK
Customizing app icons and splash images...OK
Generating app icons...OK
Packaging Python app...OK
Building macOS bundle...OK
Copying build to build/macos directory...OK
Success!
カラーだとこんな感じ

完成したアプリは Universal

アイコンはもちろん、バージョンとコピーライト表記も反映されている

ビルドされたアプリはアプリケーションフォルダにコピーしてダブルクリックで開けます。プライバシーとセキュリティで実行許可を与える必要はありません。Universal バイナリなので、試していませんが Intel mac でも動きそうです。アプリにした後の動作自体は CLI から実行したときと変わりありません。ウィンドウが開くときに若干もたつきがあり、Flet デフォルトのウィンドウが開いてから、指定したウィンドウにリサイズしてコンテンツを表示する、という動きも同じです。自分のコードの最適化か、Flet のアップデートで解決するかもしれません。

感想

実に良いです。これまで tkinter や、そのラッパー pysimplegui を使って GUI アプリを幾つか作りましたが、Flet は断然作りやすく、デザインはモダンで、今回紹介したようにビルドも簡単です。今後試したいこととしては、mac で Windows 用にビルドはできないようなのでParallels を使用したビルドを試したり、iOS やウェブ (サーバサイドでは無く、スタティック) 用にビルドすることも試したいと思います。一気にやれることが増え、やりたいことも増えてすごく楽しいですね。mac をお持ちの方は、ぜひお試しあれ (もちろん Windows や Linux の方も)。

Image by Stable Diffusion

Date:
2024年1月15日 23:05:04

Model:
realisticVision-v20_split-einsum

Size:
512 x 512

Include in Image:
masterpiece, best quality, retro future, cyber, disco computer, password generator

Exclude from Image:

Seed:
3224310018

Steps:
20

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & Neural Engine

(解決済み) macOS 「書類」以下のフォルダが Finder の移動メニューやターミナル.app から直接開けなかった

いつからかわからないんですが、mac の書類フォルダ (~/Documents) 以下のフォルダが Finder で直接開けなくなってしまっていました。原因はわからずじまいなのですが、解決できたので共有します。

きっかけ

現在の macOS のバージョンは Sonoma 14.2.1 です。実は以前から、ひょっとしたら Big Sur 11.0 あたりからターミナルで open ~/Documents/Python/hoge とかやってもウィンドウが開かないなと気になってはいたのですが、そんなに実害も無いしまあいいか、と放置していました。ところが今日、GitHub Desktop をいじっていた時に症状が現れました。同アプリで Show in Finder ボタンをクリックすると、本来開くべき書類フォルダ数階層下のフォルダでは無く、かわりに自分のホームフォルダがヘンな感じで開いたのです。これはやっぱりおかしい、解決しておかないと面倒なことになりそうだぞ、と言うことで調べ始めました。

↑これクリックで↓これが現れた (自分のホームディレクトリ)
本来は書類フォルダのもっと下の方にあるフォルダが開いてくれないとおかしい

症状

ターミナルアプリで ~/Documents フォルダ以下の様々なフォルダを open コマンドで開いても、同じく自分のホームしか開いてくれません。開いた Finder のウィンドウで書類フォルダをクリックすれば、内部のフォルダは全て開けます。ミュージック (~/Music) やダウンロード (~/Downloads) などの内部にあるフォルダも同様の手順でターミナルから開けます。書類フォルダの中にあるフォルダだけ、直接 Finder で開けないのです。Finder の「移動」メニューから「最近使ったフォルダ」で書類フォルダ以下のフォルダを指定したときも同じ動作です。右クリックから「新規タブで開く」を選んでも同じ。とにかく Finder が、書類フォルダ自体とその配下のフォルダを直接開くことができず、仕方なくホームフォルダを開いている感じでした。

どうやって解決したか

いろいろ試しましたが、最終的には Finder の表示方法をリストに変更することで解決したようです (元々は、カラム表示がダメだった雰囲気)。手順をもう少し細かく書くと、まず書類フォルダを開き、ウィンドウ上部にある表示からリストを選びます。

↑か↓

その後、アクションメニューから「表示オプションを表示」します。

コマンド + J でも OK

開いた小さいウィンドウの「常にリスト表示で開く」にチェックを入れ、一番下の「デフォルトとして使用」をクリックし、閉じます。これで、書類フォルダや配下のフォルダが Finder のリスト表示で開くようになりました。

勝利宣言

Finder は、小さな親切か大きなお世話かわかりませんが、あるウィンドウで表示方法を変更すると、次に新しく開いたウィンドウも表示方法が踏襲されたりします (条件はよくわからず)。なので、不具合を再現してみようと、上記設定をした後に表示方法をカラムにしたりギャラリーにしたり閉じたり開いたりを繰り返していたところ、最終的に不具合はぱったり発生しなくなりました。カラム表示でもサブフォルダが開くんです。よって、原因不明ながら、上記手順で解決、と言ってしまおうと思います。

他に試してダメだったこと (参考まで)

Image by Stable Diffusion

Date:
2024年1月3日 17:56:25

Model:
realisticVision-v20_split-einsum

Size:
512 x 512

Include in Image:
comicbook-style, gray hair guy looking for a missing folder in a book library

Exclude from Image:

Seed:
2520942867

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=process
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=process を付けると、実行中に進捗状況が表示される

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=process

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

LAN 内の開発用 Ubuntu に Let’s Encrypt の無料 SSL 証明書をインストール

HSTS preload の申請が通ったら、開発用サイトも HTTPS 接続必須となった (それは、そう)

会社で使っているドメインのセキュリティ向上のため HSTS preload の申請を行ったら良い感じでした。なので、個人所有のドメイン peddals.com も申請をしました。サブドメインを含め (includeSubDomains) 常に HTTPS で読み込む設定にしたため、クローズドな環境 (自宅 LAN 内の Raspberry Pi 3 model B) で http のみで運用していた開発サイトがアクセスできなくなりました。いくつか解決策を検討し、今回は Let’s Encrypt の無料 SSL 証明書を入れて対応してみたので、その手順を共有します。

開発環境と、その他必要だったもの

  • ラズパイ (Raspberry Pi 3 model B)
  • Ubuntu 20.04 LTS
  • Apache 2.4.41
  • ドメイン名
  • DNS サーバへのアクセス (TXT レコードの追加権限)

ざっくりとした手順

  1. Certbot をインストール
  2. certbot コマンドの実行 (DNS チャレンジ)
  3. 出力された値を DNS に TXT レコードとして記入
  4. Let’s Encrypt 証明書がインストールされる
  5. Apache で SSL を有効にする
  6. 開発サイトの Apache 設定ファイルのポートを 443 に変更し、SSL 証明書の内容を追記

細かい手順

証明書の取得 (CLI + DNS レコード追加)

まずは証明書のインストールに必要な certbot をインストールします (sudo bash で管理者権限を持ってから実行しています)。念のため、$# 以降が入力したコマンドで、それ以外は出力サンプルです。

$ sudo bash
# apt-get update
# apt-get install certbot

自分の場合、*.dev.peddals.com を対象にしたワイルドカード証明書をインストールしたいので、そのドメイン名を加えて以下コマンドで申請しました (別記事に書くかもしれませんが、mac では DNSmasq を動かしており、*.dev.peddals.com へのアクセスは全てラズパイに飛ばしています)。この例ではトップレベルドメインも含んでいますが、必要なわけではありません。

# certbot certonly --manual --preferred-challenges dns-01 -m mail@example.com -d '*.dev.peddals.com' -d peddals.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

今回はなぜかここで止まって進まなかったので一度 ctrl + C で止め、再度同じ certbot コマンドを実行しています。問題なく進んだので原因は調べていません、あしからず。

メールアドレスの共有 (上の質問) は強制では無かったと思いますが、IP アドレスの登録 (下の質問) は必須なので Y を入力しましょう。

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for dev.peddals.com
dns-01 challenge for peddals.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.dev.peddals.com with the following value:

(ここにコードが表示される)

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

ここで一度止まるので、指示の通りに DNS に TXT レコードとして追加します。こんな感じです。

ホスト名にドメイン名が入らないように注意

追加したら CLI に戻ってエンターキーを押します。自分の例ではトップレベルドメインも -d で追加していたので、もう一度 DNS レコード記入の指示が出ました。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.peddals.com with the following value:

(ここにコードが表示される)

Before continuing, verify the record is deployed.
(This must be set up in addition to the previous challenges; do not remove,
replace, or undo the previous challenge tasks yet. Note that you might be
asked to create multiple distinct TXT records with the same name. This is
permitted by DNS standards.)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

同じ要領で DNS に追加し、エンターキーを押します。うまくいけば以下のように表示されて、証明書の取得が完了です。

Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/dev.peddals.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/dev.peddals.com/privkey.pem
   Your cert will expire on 2024-02-10. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

秘密鍵や証明書の保存場所は IMPORTANT NOTES に書いてあります (忘れたら /etc/letsencrypt/live/ を見ましょう)。証明書の期限は 90日で、つど再取得の必要があります。再取得後も Apache の設定ファイルを書き直さなくて良いように、それぞれ実ファイルへのリンクになっています。

# ll /etc/letsencrypt/live/dev.peddals.com/
total 12
drwxr-xr-x 2 root root 4096 Nov 12 19:00 ./
drwx------ 3 root root 4096 Nov 12 19:00 ../
-rw-r--r-- 1 root root  692 Nov 12 19:00 README
lrwxrwxrwx 1 root root   39 Nov 12 19:00 cert.pem -> ../../archive/dev.peddals.com/cert1.pem
lrwxrwxrwx 1 root root   40 Nov 12 19:00 chain.pem -> ../../archive/dev.peddals.com/chain1.pem
lrwxrwxrwx 1 root root   44 Nov 12 19:00 fullchain.pem -> ../../archive/dev.peddals.com/fullchain1.pem
lrwxrwxrwx 1 root root   42 Nov 12 19:00 privkey.pem -> ../../archive/dev.peddals.com/privkey1.pem

Apache とサイトの設定ファイル

Apache で SSL が使えるようにします。最後の 2行で Apache を再起動 & 動いているか確認しています。

# a2enmod ssl
Considering dependency setenvif for ssl:
Module setenvif already enabled
Considering dependency mime for ssl:
Module mime already enabled
Considering dependency socache_shmcb for ssl:
Enabling module socache_shmcb.
Enabling module ssl.
See /usr/share/doc/apache2/README.Debian.gz on how to configure SSL and create self-signed certificates.
To activate the new configuration, you need to run:
  systemctl restart apache2
# systemctl restart apache2
# systemctl status apache2

サイトのコンフィグファイルに変更を加えます。ここでは変更、追加の部分のみ記載しています。先頭の TCP ポートを 443 に変更し、SSL の有効化、証明書と秘密鍵の指定を追加しています。最低限ブラウザでサイトが開くようにするための設定内容です。HSTS preload により全てのホストとの接続が HTTPS になるため、ポート 80 の設定ファイルに 443 へのリダイレクトを仕込むことも不要です。

<VirtualHost *:443>
	SSLEngine on
	SSLCertificateFile /etc/letsencrypt/live/dev.peddals.com/fullchain.pem	
	SSLCertificateKeyFile /etc/letsencrypt/live/dev.peddals.com/privkey.pem

設定ファイルに間違いが無いかテストし、Apache に設定を読み直させます。

# apachectl configtest
Syntax OK
# systemctl reload apache2

ブラウザで確認

最後に、ウェブブラウザで HTTPS で開ければ無事完成です。自動的に HTTPS 通信になるので、https:// を入力する必要はありません。

Safari でアクセス
もちろん Chrome でも開ける

Let’s Encrypt 証明書は 90日で期限が切れる

証明書の有効期限は 90日なので、なるべく切れる前に更新しましょう。期限切れまで 30日であれば更新できるようです。まぁ、開発用なのでとりあえずはつど手動で更新しましょうか。--dry-run オプションを付けてエラーが出ていなければ、オプション無しでもう一度実行して更新します。

# certbot renew --dry-run
# certbot renew

更新が必要ないときに実行すると、有効期限が切れる日付を表示してくれます。

# certbot renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/dev.peddals.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

The following certs are not due for renewal yet:
  /etc/letsencrypt/live/dev.peddals.com/fullchain.pem expires on 2024-02-10 (skipped)
No renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

自動化は crontab コマンドへの登録で実現できますが、今回は触れません。

ところで、HSTS の書き方・置き場所

本サイトはエックスサーバーに置いているので、ドキュメントルートに .htaccess を作り、そこに以下の一行を追加しています。includeSubDomains で全てのサブドメインを対象とし、preload でサイトへのアクセスは HTTPS に固定しています。

Header set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

まとめ

HSTS での設定以外にでも、開発環境に SSL 証明書をインストールしたい理由はいろいろあると思います。無料とはいえ本物の証明書が使えるので、この方法は試す価値大アリです。

Image by Stable Diffusion

Date:
2023年11月12日 17:59:44

Model:
realisticVision-v20_split-einsum

Size:
512 x 512

Include in Image:
cartoon, let’s encrypt, apache, success

Exclude from Image:

Seed:
356871963

Steps:
30

Guidance Scale:
11.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & Neural Engine

StaSh の SSH を動かす方法 Pythonista 3 (ver. 3.4)

Pythonista 3.4 の StaSh で ssh を動かせたので、やったことをまとめます。

おことわり

Pythonista 3 のシェル環境 StaSh で ssh を使用するメリットについては触れません。ssh 接続をしている間は iPhone 本体が通常より熱を持つため、特にその必要が無い限り Termius 等の無料の SSH クライアント専用アプリを使用するのが良いと思います。本記事はむしろ、以前のバージョンの Pythonista 3 と StaSh で動いていたコマンドを、ver. 3.4 で動かす時の参考にしてもらえればと思います。また、再検証はしておらず、残しておいたメモを元に構成しているため、漏れや間違いがある可能性も否めません。

Pythonista 3.4 では、Python 2.7 のコードをそのまま実行することができなくなりました。StaSh に含まれるコマンドは全てが Python 3 に対応しているわけでは無く、ssh コマンド (ssh.py) もその一つです。以前の Pythonista (ver. 3.3 以前) では StaSh 自体を Python 2 環境で実行すれば ssh が実行可能でしたが、今後は Python 2 用コードは Python 3 に変換してあげる必要があります。Pythonista ではツールが用意されていて、スパナアイコンから Python 2 to 3 をタップすると自動変換してくれます。ただ、ssh コマンドは手直しが必要でした。

Python 2 のコードを開いた状態で Python 2 to 3 をタップすると変換してくれる

バージョン情報等

$ version
StaSh v0.8.0
Python 3.10.4 (CPython)
UI stash.system.shui.pythonista_ui
root: ~/Documents/site-packages/stash
core.py: 2023-05-05 18:00:40
SELFUPDATE_TARGET: master
Pythonista 3.4 (340012)
iOS 16.4.1 (64-bit iPhone10,2)
Platform iOS-16.4.1-iPhone10,2-64bit
BIN_PATH:
  ~/Documents/bin
  ~/Documents/stash_extensions/bin
  ~/Documents/site-packages/stash/bin

StaSh は、記事作成時に最新の dev バージョンです (StaSh インストール方法は別記事参照)

やったことひとまとめ

つど必要に応じて (変更箇所を反映させる等) Pythonista の再起動を実施しています。秘密鍵を使用した SSH の設定と、接続時の行数の指定例は過去記事をご参照下さい

  1. ~/Documents/site-packages/stash/bin/ssh.py を ~/Documents/stash_extensions/bin/ssh3.py としてコピー
  2. ssh3.py に対して「Python 2 to 3」を実行 (変更内容はそのまま)
  3. ssh3.py の関数 vk_tapped の内部 vk.name を全て int として処理するように書き換え (変更内容は下記)
  4. ~/Documents/site-packages/stash/system/shscreens.py に対して「Python 2 to 3」を実行 (変更内容はそのまま)
  5. shscreens.py の 541 行目と 576 行目それぞれにある /// に変更。変更後はこうなります: idx_line, idx_column = idx // (ncolumns + 1), idx % (ncolumns + 1)
  6. StaSh で ssh3 を実行

vk_tapped の変更後はこちら。Python 3 のコードに変換後の行数で、242~262 の部分になります。内容としては、vk.name を全て vk にし、'k_tab' 等となっていた部分を対応した数値に置き換えています。この変更により、StaSh のバーチャルキーボードで Tab や Up、CC 等をタップしたときに、SSH 接続先にも正しいキーコード (それぞれタブ、カーソル上、Control + C) が送られるようになります。

    def vk_tapped(self, vk):
        if vk == 7:
            self.send('\t')
        elif vk == 0:
            self.kc_pressed('C', CTRL_KEY_FLAG)
        elif vk == 1:
            self.kc_pressed('D', CTRL_KEY_FLAG)
        elif vk == 6:
            self.kc_pressed('U', CTRL_KEY_FLAG)
        elif vk == 9:
            self.kc_pressed('Z', CTRL_KEY_FLAG)
        elif vk == 2:
            self.kc_pressed('UIKeyInputUpArrow', 0)
        elif vk == 3:
            self.kc_pressed('UIKeyInputDownArrow', 0)

        elif vk == 10:
            if _stash.terminal.is_editing:
                _stash.terminal.end_editing()
            else:
                _stash.terminal.begin_editing()

出ていたエラーと解決のためのヒント

実際に ssh が動くようになるまでに行った手順としては、上のステップ 3 が最後になります。自分が最初にぶちあたったのは、ステップ 4の shscreens.py を Python 3 に変換した後で、エラーはこちらです:

system/shscreens.py", line 578, in load_pyte_screen
    c = pyte_screen.buffer[idx_line][idx_column]
TypeError: list indices must be integers or slices, not float

小数 (float) になっているのが問題と言うことなので、元の idx_line と idx_column を int() で整数にしてから呼び出されるようにしても解決せず、以下ページがヒント (とういか答え) となりました。

My guess would be that stash uses / division for line/column indices. On Python 3 / always produces a float. The fix is simple: replace it with flooring division //.

https://github.com/selectel/pyte/issues/123

ステップ 3 の変更を加える事になった原因のエラーはこちらです:

  File "stash_extensions/bin/ssh3.py", line 230, in vk_tapped
    if vk.name == 'k_tab':
AttributeError: 'int' object has no attribute 'name'

これは StaSh 自体のコードに関わる内容だと思うんですが、よくわかりません。ここをいじらなくても、SSH セッション自体は張れます。この関数の目的は、押された vk (=virtual key) に対応したキーコードを SSH 接続先に送るということです。なので、関数直下に print(vk) を差し込み、SSH 接続した状態で Tab やら CC やらを押して Console に表示される数字に name の値を置き換えていきました。美しい解決方法を探ったのですが諦めました。

さて、そもそも今回 ssh コマンドを動かしてやろうという動機付けになったのは、StaSh の説明の中にあった一文、特に色を付けた部分でした。

StaSh works with both Pythonista 2 and 3, though not all commands support python3. (訳: StaSh は Pythonista 2 でも 3 でも動きますが、全てのコマンドが python3 対応にはなっていません)

https://github.com/ywangd/stash

Image by Stable Diffusion

ここからは本文に関係ありません。今回のアイキャッチ画像は、Mac 用 Stable Diffusion クライアントの Mochi Diffusion で出力した画像に、後から文字を描き足したモノです。 Stable Diffusion を使った場合は、情報を記載するようにします。

Date:
2023年5月6日 22:31:24

Model:
realisticVision-v20_split-einsum

Size:
512 x 512

Include in Image:
cartoon, a young man waring glasses, super happy

Exclude from Image:


Seed:
3826992198

Steps:
20

Guidance Scale:
11.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & Neural Engine

3年以上の時を経て、Pythonista 3 がバージョン 3.4 にアップデート (Python 3.10 サポート)

もう終わったのかと思っていたけど、、、うれしかった!

iPad で実行。Stash の version コマンドの出力。Python 3.10.4 になってます

Python 3 は 3.10.4 にアップデート。Python 2 は完全廃止

iOS で Python コードを書いて実行できるアプリ、Pythonista 3。3年以上の時を経て Python 3.10 対応となってアップデートされました。

Pythonista 3 ←App Storeへのリンク

詳しい変更内容は、以下のリリースノートを見てください。Google 日本語翻訳のリンクも貼っておきます。

https://omz-software.com/pythonista/docs-3.4/py3/ios/new.html

プラス要素だけでは無く、本バージョンでは Python 2.7 が無くなりました。なので、過去記事で書いた 2.x 環境用で動くスクリプトは、そのままでは実行できなくなりました。StaSh では ssh コマンドも動きません。

StaSh の話が続きますが、StaSh は有志の方々によって開発されている OSS のため、対応が追いついていません。やっとなんとかインストール・実行できるようになりましたが、エイリアスや環境変数を書き込める .stashrc ファイルや、複数のコマンドをセミコロンでつなげて一度に実行しようとしても、最初のコマンドしか実行されない状況です。

StaSh のインストール

StaSh をインストールして正しく動作させるには、一度 Pythonista 3 を削除することが推奨されています (StaSh 自体やインストール方法などは日々アップデートされているようなので、定期的に Github をチェックすることをお勧めします)。現在のところ、自分で試した感じでは dev バージョンをインストールするのが良さそうです。Console に以下をコピペして実行してください。

url = 'https://raw.githubusercontent.com/ywangd/stash/dev/getstash.py'; import requests as r; exec(r.get(url).text.replace('master', 'dev'))

その後、一度 Pythonista 3 アプリを終了後、This iPhone (Documents フォルダ) にある launch_stash.py を実行すると StaSh が立ち上がります。

Django のインストールと実行 (最新版は動かない)

Django は、試したところ version 4.0 ならインストールとテストページの表示までできました。バージョンを指定しないと 4.2.1 が入るのですが、そのバージョンでは openssl_md5 のシグナチャが見つからないというようなエラーで django-admin が動きません。

pip install django==4.0

ちなみに、StaSh に表示されるインストールログや、pip show Django を見ると 4.2.1 がインストールされているように表示されるのですが、Console で確認 ( import django してから print(django.__version__) ) すると 4.0 です。pip のバージョン表示はこれまで通り信用できないようです。

ともあれ無事インストールできたら、Pythonista 3 を終了して再度立ち上げ、プロジェクトを作ります。

django-admin startproject mysite

右にスワイプして This iPhone > mysite と進み、manage.py をタップしてコードを開きます。▷を長押しして Arguments に以下をコピペし、Run しましょう。noreload の左にあるのは、マイナス二つです。

runserver --noreload

赤文字の英語で「CommandError: You must set settings. ALLOWED_HOSTS if DEBUG is False.」と出た場合は、ここではとりあえず気にせず Pythonista 3 を終了・起動、そしてもう一度 manage.py を Run しましょう。iOS がネットワークアクセスの可否を聞いてきたら許可してください。

うまくいけば Console に諸々の注意事項と共に URL http://127.0.0.1:8000/ が表示されるはずです。こちらをタップするなり Safari にコピペするなりすると、はい、おめでとうございます、ロケットページが表示されます。 (このサイトには過去バージョンの Pythonista 3 の記事がいくつかあるので、参考に見てみてください)。

とりあえずのまとめ

最近 a-Shell という Unix/Linux ライクなシェル環境で Python 3.11 がいじれる iOS アプリを見つけていたのですが、Python がバックグラウンドで動き続けない (Django や Flask アプリは、ブラウザとアプリを行き来しないとページが更新できない) ことにがっかり (← いじっているうちになぜか解決しました)。そんな矢先に届いたのが Pythonista 3 のアップデートでした。個人的に新しめの Python ならではの機能はあまり使っていないのですが、これを機に知識のアップデートもしていこうと思っています。StaSh がキャッチアップするのにまだかかりそうではありますが、新たな発見があれば書きためていこうと思います。

Image by Stable Diffusion

ステップ数が少ないからかなり怖い画像になってますが、Mochi Diffusion の max 値である 50にすると、変にまとまってしまって自分のうれしい気持ちを表現しきれないので、最初に出力した画像をアイキャッチにしました。ゾッとした方、ごめんなさい。

Date:
2023年5月6日 14:35:09

Model:
realisticVision-v20_split-einsum

Size:
512 x 512

Include in Image:
cartoon, people happy with a new release of software

Exclude from Image:


Seed:
3343127351

Steps:
20

Guidance Scale:
11.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & Neural Engine

ユニバーサルコントロールがいい感じ

Mac mini (M1) と MacBook Air (Intel 2019) を macOS 12.3.1 にし、iPad mini (第 5世代) を iPadOS 15.4.1 にアップデート。ユニバーサルコントロールを有効にして、ワンセットのマウスとキーボードで操作してみました。最初の感触としてはすごくいいです。3つの Apple デバイス+1つのモニタが、シームレスに操作できています。使い切れていない機器が再び使えるようになりそうですよ。

まずやることは、OS のアップデート、そしてユニバーサルコントロールの有効化

すでに macOS 12.3 と iOS 15.4 ではユニバーサルコントロールに対応していましたが、今回それぞれバグフィックスがされたバージョンをインストールしました。計測していませんが、macOS のアップデートは1時間くらい?iOS は 30分くらい?という感じです。どちらも再起動後、設定を有効にすれば使えるようになります。

そもそも完了しているとは思いますが、全てのデバイスが同じネットワーク内にいて、同じ Apple ID でサインインしている必要があります。その他の条件は Apple のページ (特にシステム条件) を確認してください。Mac と iPad それぞれの設定場所は以下の通りです:

macOS: アップルマーク > システム環境設定… > ディスプレイ > ユニバーサルコントロール … > 全部 (もしくは上から 2つ) にチェックを入れて、完了

接続された後、それぞれの位置を移動できます。左から、MBA、Mac Mini メイン、サブ、下に iPad mini。なんと、M1 搭載 Mac に標準の機能だけで 4画面!

iPadOS: 設定 > 一般 > AirPlay と Handoff > カーソルとキーボード (ベータ版) にチェック

iPad はここにチェックを入れるだけ

最初にできるのは横のつながりだけ?

Apple のデモを見ていたときには、あたかも置いてある方のデバイスのモニタにマウスカーソルが移動できるような雰囲気ではありましたが、さすがにそれぞれのデバイス同士で位置情報を交換しているわけでは無いです。ざっと試した感じでは、モニタの端にマウスカーソルを持って行ってから、さらに画面の外に持って行こうとすると、未接続のデバイスにカーソルが移動します。そうやって接続してから、Mac のディスプレイ設定で上のスクリーンショットのように気に入った場所に移動しましょう。下にも配置できます。iPad 画面の縦横も反映されるので、Sidecar より便利ですね。

MacBook Air (Intel) を生かせそうな予感

性能がかなり控えめで、たまーにリビングで Mac を使うときにしか稼働させることの無くなってしまった Intel CPU の MacBook Air (Retina, 13-inch, 2019) ですが、ユニバーサルコントロールのおかげで生き返りそうです。Mac mini と一緒に机で使うときは、3枚目のモニタとしてメールを開きっぱなしにしておけます。また、トラックパッドでページをめくれるので、リビングでは外付けモニタ的に iPad の Kindle で技術系書籍を開き、MBA でプログラムを書くなんて便利な使い方ができます。自分の用途では Sidecar より便利です (Sidecar は iPad を縦表示にできず、フルスクリーンでは使えない)。ただし、Kindle ではダブルクリックやドラッグで文字を選択することはできないようで、おそらく amazon 側の対応が必要な感じです。

ベータ版だからか、サポート外か?

ファイルのドラッグアンドドロップもできるということで、iPad のスクリーンショットをファイルに保存し、マウスで Mac のデスクトップに持ってこようとしましたができませんでした。できないどころか、Mac mini のディスプレイ設定から iPad mini が消え、MBA では見えているけど iPad mini の画面位置を移動できず、他のことをしていたらいつの間にか復活、という動作になりました。そもそも写真のドラッグアンドドロップは対応していないのかもしれません。使い続けていると、できそうでできないことが他にも見つかりそうです。少なくともデバイス間のテキストのコピペは問題なさそうです。

これは困る。iPad mini ではキーボード配列が日本語になってしまう

Mac mini M1 にインストールはできるものの、頻繁にクラッシュして使い物にならない Pythonista3 を iPad mini で起動していじってみました。使い慣れたキーボードで操作できるなんて最高です!と報告して本投稿を終了、と思ったのですが、なんだか様子がおかしい。どういうことかというと、キーボード配列が日本語 JIS 配列になってしまっていて、HHKB Pro 2 の英語キーボードではアンダースコアが入力できないのです。ググってみても、日本語キーボードが英語キーボードと認識されてしまうという情報は多いのですが、逆はみつかりませんでした。果たしてユニバーサルコントロールの問題なのか調べるために、iPad に USB-A コネクタを追加するアダプタでキーボードをつないだところ、結果は同じでした。日本語配列キーボードとして認識されてしまいます。

キーボード配列問題のワークアラウンド

本日確認できた唯一の解決方法は、iPad の使用言語を English にすることです。

iPad: 設定 > 一般 > 言語と地域 > iPad の使用言語 > English にして続ける

iPad のメニューが全て英語になるので、英語が苦手な方は注意

同じ言語と地域の画面の、言語を追加で英語を追加してもダメで、英語の優先順位を日本語より上にすると iPad の使用言語が English になり、上と同じ事に。なので、この記事を書いている現在では iPad 自体の使用言語を変えることがキーボードのレイアウトと合わせる唯一の方法のようです。いつかキーボードのレイアウト問題が解消するといいですね。

感想

Macbook + iPad のモバイル時の利便性や、M1 搭載 Mac でも擬似的にとはいえ 3画面 (以上) 使える使えるのはすごくいいですね。Mac 間では遅延や操作のしづらさもありません。無料の OS のアップデートだけで使えるので、複数の Mac や iPad を持っている方はいろいろ試してみたら良いと思います。ボクは以前購入した、iPad を Mac の外付けモニタにできる duet という 2,000円くらいしたアプリを削除する決心が付きました。最後にひとつ。どうやらユニバーサルコントロールで接続されていると、接続元 (?) がアクティブである間は接続先デバイスもアクティブであり続けるため、その分バッテリが減ってしまいます。モバイルで使用するときには、バッテリの使用状況に注意するようにしましょう。

© Peddals.com