cockscomblog?

cockscomb on hatena blog

3DプリンタでMagSafe充電器スタンドをつくる

この秋にリリースされるiOS 17では、充電中のiPhoneに情報を一目でわかるように表示する「スタンバイ」機能が搭載されるそうだ。iPhone 14 Proの常時表示ディスプレイと組み合わせると便利そうだ。

これを活用するには充電中のiPhoneを一定の角度に保つ充電スタンドが必要になる。MagSafe充電器 ワイヤレスなら、充電器ごとに設定を記憶してくれるようだから、MagSafe充電器タイプが望ましい。市場には、MagSafe充電器が一体になったスタンドや、単体のMagSafe充電器と組み合わせて使う製品がある。今回は3Dプリンタを買ったことだから、メイカー精神を発揮してみる。

試作1号

MagSafe充電器の大きさをノギスで測ると、直径は55.9 mm、厚みは5.5 mmある。そこから例によって、Fusion 360モデリングする。MagSafe充電器が少し高い位置に一定の角度で固定されていれば用が足りるので、70度に傾けた枠に愚直に支柱をつける。iPhoneの分だけ重心が手前にくるから、台座も手前に伸ばしてある。

これを3Dプリンタで出力する。ツリー状のサポートをつけている。

一見するとうまくできているが、オーバーハング(せり出し)部分で精度が悪化し、荒れてしまった。内側もその影響を受けて、MagSafe充電器に干渉し、うまく収まらない。パーツを分割して出力するとよいのだろうか。

使用感も確かめてみると、最低限スタンドとしての役には立つ。iPhoneが浮かんで見えるのは格好がよい。ただ、支柱部分の剛性が足りていないのか、iPhoneの重みで僅かなたわみが生まれ、振動してしまう。iPhoneを操作するたびに振動するので、使い心地はよくない。

試作2号

全体的な剛性を確保するために、支柱で支える構造をやめた。ケーブルを裏側にまわすついでに、ひろく穴を空けてMagSafe充電器の充電中の熱が後ろへ逃げるようにしている。

3Dプリンタでの出力時には、MagSafe充電器まわりの精度が出やすいように、倒した状態にしている。

出力してみると、精度は十分で、剛性もある。使用感にも問題がない。

素材が軽いので、iPhoneを近づけると磁力でスタンドの方が動いてしまう。錘をつけると改善されるかもしれないが、困るわけではないので、このままでもよいだろうか。

感想

CADの操作に慣れて、少し複雑な形でもモデリングできるようになった。ただしCADでどんなに自由にかたちを作っても、それでうまくいくとは限らない。素材の特性や加工する装置にあわせて設計しなければ、まさに机上の空論だ。

今回は最低限度、MagSafe充電器スタンドとして使えるものができた。ヒンジで角度を調整できるとか、改良したい箇所もあるが、そもそもヒンジをどう作ったらいいのか、見当がつかない。いくらでも学ぶことがある。

3Dプリンタで何かを作るプロセスは、頭の中にあるものを現実で試し、失敗や成功の経験をするもので、刺激的でおもしろい。これは相当におもしろい。


3Dプリンタを買った

Bambu LabのP1Sという3Dプリンタを買った。少し前に出たばかりの機種で、同じメーカーのP1Pという機種にエンクロージャ(覆い)がついたようなモデルだ。

家庭用の3Dプリンタは安いものなら数万円で買えるが、これは12万円弱で、価格帯としてはミドルレンジにあたるのだろうか。会社の表彰制度で社長賞を頂戴して気が大きくなっていたので、ついに買った。何かを買うときに、少しいいものを選びがちな性分で、安物買いを恐れている。入念にインターネットで検索をして、FDM(熱溶解積層)法で、Core XY方式であり、エンクロージャで覆われている、不具合の少なそうな機種を選んだ。

3Dプリンタを選ぶのは相当に難しいことだ。何を基準に選んだらいいのかも知らないし、進歩が激しい分野だから、状況が変化しやすい。

もしも家電量販店で販売されているような製品であれば、家電量販店の売り場にしばらくいれば、だんだんと事情が掴めてくるものだ。商品のひとつひとつに主だった特徴が書かれていて、例えばパソコンなら、CPUが何で、メモリが何GBで、ストレージがどうで、インターフェースが……、というのがまとめられている。それがドラム式洗濯機であっても、乾燥機能がヒートポンプ式なのかヒーター式なのか、洗濯の容量、洗剤自動投入機能がついているか、一目でわかる。つまりそういったことが製品を差別化していて、それに気を配ればよい、ということだ。

そういうポイントを知らない状態で3Dプリンタを選ぶのは困難だから、かなりの時間をかけて、何となくの知識を得た。

この過程でわかったことだが、3Dプリンタを愛好する人たちの多くにはメイカー文化が根付いていて、3Dプリンタそれ自体もその対象となっている。つまり3Dプリンタそのものについて、自分たちで手を加えやすいものが好まれている。これにはフリーソフトウェア運動的な側面もあって、プリント可能な3Dプリンタのパーツのモデルが多く公開されている。

それはさておいて、何となくの知識でBambu Lab P1Sを注文し、3日後には届いた。

P1Sを設置するのは、付属する印刷物を読めば難しくない。Bambu Labが公開しているYouTubeの動画でも予習してあったので、特に苦も無く済んだ。電源が国内で一般的な2ピンではなく3ピンタイプなので、変換してやる必要はあるが、これも事前にアダプタを用意していたので問題ない。

設置後に早速、Benchyと呼ばれる船のモデルをプリントしてみた。3Dプリンタベンチマークとされる船は、だいたい20分弱できれいに出力された。これは3Dプリンタとしてはかなり早い部類で、Bambu Labのセールスポイントだ。

そこから、Autodesk Fusion 360の個人利用版を使って、CADを学んでいる。スケッチから立体を作るフローがわかってきて、単純な形状なら作れるようになった。今朝は毎月頼んでいるレターのスタンドをプリントしてみた。プリントにかかった時間は30分くらい。まあまあかな。

使い途の当てもなく3Dプリンタを買ったが、自分でモデリングしたかたちをプリントできたとき、えもいわれぬ感慨を得た。コンピュータの中にしか存在しなかったものが、実際に触れるものとしてできあがると、嬉しいものだ。ましてそれが家にいながらにしてとなれば格別である。

Apple Vision Pro所感

Apple Vision Proの発表を見ての感想です。

空間コンピュータ

予想通り、ハードウェア面でもソフトウェア面でも圧倒的な完成度の製品を出してきた、という印象だ。ディスプレイからオーディオ、センサーに至るまで、可能な限りが詰め込まれている。新たなカテゴリをプラットフォームとして切り拓く、硬い意志が感じられる。

製品に「Pro」と名付けつつも、同種の製品カテゴリではかなりコンシューマ向けに寄せられてもいる。映画とか、ゲームとか。この辺りはMacintoshを生み出したAppleらしいところでもある。とはいえ価格帯からは「Pro」カテゴリになるし、将来的により廉価なモデルを発売する余地を確保しているのだろう。

Appleは本日、デジタルコンテンツを現実の世界とシームレスに融合しながら、実世界や周囲の人とのつながりを保つことができる革新的な空間コンピュータ、Apple Vision Proを発表しました。

visionOSは、思っていたよりもずっとコンピューティングにフォーカスされていた印象がある。Apple自身も空間コンピューティングと表現しているが、道具として屋内で普通に使うものとして設計されていて、奇を衒ったところがない。極めてインドア的な、普段からコンピュータを四六時中使っているようなオフィスワーカー向けの製品のように感じられ、つまりこれはMacintoshなんだな、と理解した。

Apple Silicon

Apple Siliconもしっかりと活かされている。性能と効率のバランス、そして既存のアプリとの互換性を考えれば、M2チップが採用されたのは理解しやすい。空間コンピューティングで複数のアプリを同時に実行するには、最低限の性能かもしれない。できればM3を期待したかったところだが、コストや量産する上でのトレードオフがあるのだろう。

M2チップは単体で機能するための比類ないパフォーマンスを提供し、新たに開発されたR1チップは、12のカメラ、5つのセンサー、6つのマイクロフォンからの入力を処理し、コンテンツがユーザーの目の前に現れるような感覚を生み出します。

白眉はR1チップで、空間上にリアリティのあるユーザーインターフェースを描画するための専用チップになっている。R1に多くの処理を肩代わりさせられることで、M2はその性能のほとんどをコンピューティングに使えるのだろう。プレスリリースには次のように記載されていて、これは要するにフレームレートが90fpsということか。

R1は瞬きの8倍高速な12ミリ秒で新しいイメージをディスプレイにデータストリームとして伝送します。

VR的な感覚では、片目あたり4K以上とされる解像度だと、グラフィクスを処理するのに非常に大きな性能が必要に感じられる。しかし実際には、Apple Vision Proの空間コンピューティングでは、単にウインドウを描画する程度の性能でいいはずだ。このウインドウの描画を行うのがM2チップなんだろう。R1チップは描画されたウインドウを空間上にプロジェクションして、カメラの入力と合成する。こちらは片目あたり4Kのグラフィクスを扱うことになる。

ディスプレイ

micro-OLEDで2,300万ピクセルということで、例えば4,150×2,750ピクセルくらいのディスプレイが2枚くらいだろうか。

Appleシリコンのチップにもとづいて開発された画期的な超高解像度ディスプレイシステムを備えたApple Vision Proは、micro-OLEDテクノロジーにより、広色域とハイダイナミックレンジを備えた切手サイズの2つのディスプレイに、合計2,300万ものピクセルを詰め込んでいます。

これが実際にどれくらいきれいに見えるのかは、視野角と関係するから、この情報だけではわからない。例えばPS VR2の水平視野角は110度で、これと同じくらいだとすれば、角画素密度(ピクセル/度)はおおよそ38になり、PS VR2の2倍になる。これくらいの角画素密度があるとよほど小さな文字でなければ普通に読めると思う。

オーディオ

WWDC20で、iOS 14からAirPods Proなどで空間オーディオをサポートすることが発表された。それから3年、空間オーディオが利用できるハードウェアもソフトウェアも増え、そして今回Apple Vision Proでも空間オーディオが大きく取り上げられている。

Apple Vision Proでの体験の中核となるのは、サウンドがユーザーを取り巻く環境から聞こえるような感覚を生み出し、その空間に合わせてサウンドを調整する、先進的な空間オーディオシステムです。

Apple Vision Proの開発期間を考えれば、空間オーディオはそもそもApple Visionプラットフォームの副産物である可能性も考えられる。

空間コンピューティングというものを考えたときに、音がどこから聞こえるかは空間を認識する重要な手掛かりになる。

API

visionOSのアプリは、iOSやiPadOS向けのアプリがほとんどそのまま動くほか、UIKitやSwiftUIで開発できるようだ。UIKitが異なるフォームファクタのOSに使われるのはこれが初めてではなく、例えばtvOSでも利用されている。

SwiftUIはもともとAppleのプラットフォーム間でクロスプラットフォーム指向であり、visionOS向けアプリ開発に推奨されることに驚きはない。開発者にとっても扱いやすい。

総じて、visionOS向けのアプリ開発iOSアプリの開発と同じくらいには容易そうだ。既存のアプリも含めて、ローンチ時から多くのアプリが提供されるだろう。新規のプラットフォームで最初からiOSのエコシステムに相乗りできるのは恩恵が大きそうだ。特に昨今のiPadOSは、プロ向けのアプリを推進してきたところがあって、空間コンピューティングとは相性がよさそうだ。

ゲーミング

Apple Vision Proは、明確にVRのゲームプラットフォームではなさそうだ。ハンドコントローラがなく、ジェスチャのみでVRのゲームを遊ぶのはまだ難しいだろう。そもそもデモに登場したゲームは、PlayStationのコントローラを持って遊んでいる。


いったんこれくらいです。

WWDC23与太話

WWDC23で発表されそうなことを考える遊びを行う。

Swift 5.9

Swift 5.9は大規模なアップデートになる。特にMacroとVariadic Genericsは、ライブラリの提供するAPIに影響しそうだ。

Macro

Macroは今後のSwiftプログラミング体験を大きく変えていく。自分でMacroを定義することもあるかもしれないが、SwiftSyntaxを駆使するMacroは単純なSwiftコードを書くより難解で、再利用性が高くない場面では正当化しにくい。裏を返せば、Macroが役立つ場面は、多くの人の課題を解決する場面ということだ。

便利なMacroを提供するライブラリは増えるだろうし、それはAppleのライブラリも例外ではない。例えばFoundationフレームワークに(Optional Unwrapなしに)文字列からURLを構築するMacroが(要するに疑いようなく便利なMacroが)追加されたりするかもしれない。

Macroはコンパイル時の機能だから、特別何もしなくてもバックデプロイできるのも嬉しい。

Variadic Generics

Variadic Genericsは、型パラメータを可変長にできる機能である。swift-async-algorithmsなどでも見られるように、複数のパラメータを取るジェネリックな関数ではこれまで、パラメータの数が異なるオーバーロードが複数定義されてきた。Variadic Genericsがあれば、そういうことをせずに済む。これもAppleのライブラリ側に実装される可能性がある。

Observation

この他、まだ議論中のObservationにも注目したい。

Cocoaの時代からSwiftUIに至るまで、データの変更を監視するオブザーバーパターンは重要なイディオムだった。KVOやCombineのような仕組みがそれを支えていた。

これをSwiftの言語機能を利用して実現することが議論されている。KVOはObjective-Cランタイムと紐づいた機能であり、CombineはSwift Concurrency以前の仕組みである。そしてこれらは今日のAppleプラットフォームにおけるGUI開発を根本から支えている。したがって、Swift自体にオブザーバーパターンの仕組みが導入されることは、GUIフレームワークがよりSwiftらしくなるのに必要なステップである。

いずれの新機能もSwiftによる開発体験を大きく改善するもので、WWDC23ではこれらが大きく取り上げられるだろうし、Appleフレームワークにもこれらが活かされていると期待される。

Xcode

Xcodeについてはいろいろと思うところがある人が多いと思う。優れたGUIアプリケーションであるとも思うし、年々改善されてはいるものの、不安定さも感じる。同種のソフトウェアと比較すると拡張性に乏しく、レガシーを引きずっている部分もある。

とはいえ毎年丁寧に改善が行われていて、内部的にはここ数年で大きく変化しているようだ。その成果の一つはおそらくSwift Playgroudsで、1年半ほど前のSwift Playgrouds 4.0のリリースから、ついにアプリを開発できるようになった。内部的にはSwift Packageの亜種のような形でプロジェクトが保存されていて、従来のXcode Projectとは異なる。

WWDC23では、この新しい形式のプロジェクト(を発展させたもの)がXcodeでも利用されるようになると期待したい。従来のXcode Projectは複雑なXMLで、慣れていないとコンフリクトの解消も難しい。また同時に、Swift PlaygroundsではなくXcodeそのものがiPadで動作することも期待したい。これは何年も期待されていることだが、先日のiPad向けFinal Cut ProとLogic Proのリリースで、急に現実味を帯びてきている。

加えて、Xcodeプラグインシステムにも改善を望みたいところだ。単純に、拡張できる場所が広がって、例えばGitHub Copilotの拡張機能が普通に導入できると嬉しい。またプラグインをSwift Packageの形で配布したり導入したりできると便利だと思う。

XR

WWDC23の目玉は、当然XR対応のヘッドセットになると思う。もはや誰も疑っていない。

Appleが近年力を入れてきた技術は、ARKitのようなソフトウェア面も、LiDARセンサのようなハードウェア面も、どれもXRヘッドセットに紐づけて考えられる。もちろんMetaのような強力なライバルが先行している分野でもあるが、それに対してAppleの得意とするUIや完成度の面で一定のアドバンテージを出してくるのではないか。

気になるのはもっぱら価格や発売時期で、特に北米以外での発売が遅れたりしないかが気がかりだ。

SwiftUI

SwiftUIは毎年着実に改善されている。WWDC22ではナビゲーションやレイアウト周りが大きく改善され、その他にもひろく手が入っていた。UIKitとの連携も改善された。それまでは不足しているメジャーな機能が足されていく印象だったが、改善のサイクルに入りつつあるのを感じた。

とはいえ、SwiftUIでは実現が困難なこともまだたくさんある。カスタマイズ性が不足しているところは無数にあるが、特にナビゲーションのカスタマイズできなさは気になる。

また、潜在的に宣言的UIフレームワークのSingle Source of Truth的な発想と相性がよくないと思われる、WebViewのようなコンポーネントをどうするかが未決着だ。WebViewは内部に多くの状態を抱えており、多くの状態は外部から自由に操作できない。例えば「読み込み中」かどうかを外部からコントロールできない。こういったいった制御下に置きにくいビューについて、例えばReactのrefのように、何らかの仕組みが欲しい。

逆にそういった不足を補う方向性ではなく、よくあるユースケースをカバーした便利な機能が追加されることも期待したい。例えば、URLのパスベースでナビゲーションを駆動する仕組みは、SwiftUIそのものに含まれていてもおかしくない。またあるいはiPadのためのLogic Proで見られるような、プロ向けのアプリのためのUIが追加されるとおもしろい。

OS

macOS/iOS/iPadOSについて、あまりメジャーな変化が起きるとは思いにくい。例えばmacOSとiPadOSの統合のようなことは、iPadのためのFinal Cut ProやLogic Proが出た以上、想像できない。

UIの改善は一定の規模で行われるようには想像できる。1年前に出たばかりのStage Managerなんかはいかにも改善されそうだ。ウィジェットもさらなる改善があっていい。もっとインタラクティブウィジェットがあると便利だし、macOSやiPadOSで、もっとウィジェットが使いやすくてもいいと思う。

XRヘッドセットとの関係を考えると、ウィジェットはXRでも使えると便利そうだ。あるいは通知についても、XRヘッドセットに合わせて改良される可能性がある。

App Intentsは進展があることを期待したい。アプリの機能をシステムに公開する仕組みであるApp Intentsは、ショートカットアプリやSiriとの連携に利用できる。これがもっと拡張されて連携可能な箇所が増えるとおもしろい。アプリ間でも連携したい。

Swift Chartsも描画できるチャートの種類が増えると嬉しい。例えばPieチャートとか。

あとは、iPhone 14 Proの常時表示ディスプレイの用途が広がると良いと思う。Appleの地図アプリでは常時点灯ディスプレイにナビゲーションを表示し続けているが、これはサードパーティには開放されていない機能だ。

Web技術

Web技術に関連して、iOS 16ではWeb Pushがついに実装されたりした。Appleは別にWeb技術に関心がないわけではなく、ユーザーの利益を重視してやっていると思う。その流れでは、やはりSafariの更新をOSの更新と分離して、もっと高頻度かつ古いOSに向けてもアップデートされてほしい。

セキュリティ面では、Passkeysに関して、ブラウザ拡張がPasskeysを扱えるようになったりするんじゃないか。

サイドローディング

またご時世的に、アプリのサイドローディングが話題である。Appleも法律には抗えないから、これはいずれできるようになりそう。macOSと同じようにNotalizationくらいはするだろう。WWDC23で発表されるかはわからない。サイドローディングまでいかずとも、ChromiumGeckoのようなWebKit以外のブラウザエンジンくらいは解放されるような気はする。

ハードウェア

M2ベースのプロセッサを搭載したMacが発表されることはありそう。TSMCのN3世代の製品(M3?)にはまだ早いんじゃないか。

発表されそうにないこと

大規模言語モデルを活用したSiriみたいなものはまだ出てこない気がする。Apple機械学習そのものや自然言語処理については着実に利用を広げているが、全体的にオンデバイスを好む傾向にある。例えばHomePodのSiriに今日の予定を聞くと、近くのiPhoneのカレンダー情報を取得して返答する。そういうポリシーだから、大規模言語モデルの活用は遅くなりそうだ。とはいえ機械学習を活用した機能自体はもう少し増えていくような気はする。

以上です

いろいろ考えていくと、何にしても幅広くいろいろ発表されそうだし、楽しみですね。

huggingface/transformersでストリーミング

AIにテキストを生成させるサービスでは、テキスト生成の速度が遅いとユーザー体験がよくない。ChatGPTは、生成されたテキストが一度に表示されるのではなく、少しずつ表示されるUIになっている。このことで、テキスト生成の完了まで待たずに結果を確かめられる。

テキスト生成の結果を少しずつ表示するためには、生成途中のテキストがストリーミングで返される必要がある。OpenAIのAPIにはstreamオプションがあり、有効にするとserver-sent eventsで結果を返してくれる。

huggingface/transformersのStreamer

OpenAI APIではなく、自分たちでhuggingface/transformersを動かしている場合、ストリーミングは困難だった。transformersにはストリーミングをサポートするAPIがなかったためである。

ところが、最近になってこれをサポートする実装が入ってきた。Hugging FaceのJoão Ganteさんが精力的に取り組んでいるようだ。

ただし今日(2023年4月10日)時点ではまだリリースされておらず、おそらくv4.28.0以降でリリースされるものと思われる。今回はソースからtransformersをインストールすることで、ストリーミングを試してみる。

transformersのストリーミング機能は、ドキュメントのUtilities for Generationで紹介されており、TextStreamerあるいはTextIteratorStreamermodel.generateに渡す形で利用する。TextStreamerは標準出力に書き出すもので、TextIteratorStreamerイテレータとして利用するものだ。

サーバーからストリーミングする

実際にFastAPIを使ったサーバーでTextIteratorStreamerを使う。まずは次のように、非同期ジェネレータとして関数generateを定義する。

import asyncio
from threading import Thread
from typing import AsyncIterator

from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    TextIteratorStreamer,
)

tokenizer = AutoTokenizer.from_pretrained("rinna/japanese-gpt2-medium", use_fast=False)
tokenizer.do_lower_case = True

model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt2-medium")


async def generate(text: str) -> AsyncIterator[str]:
    inputs = tokenizer(text, add_special_tokens=False, return_tensors="pt")

    streamer = TextIteratorStreamer(tokenizer)
    generation_kwargs = dict(
        **inputs,
        streamer=streamer,
        max_new_tokens=100,
        do_sample=True,
        top_k=50,
        top_p=0.95,
        temperature=0.9,
        pad_token_id=tokenizer.pad_token_id,
        bos_token_id=tokenizer.bos_token_id,
        eos_token_id=tokenizer.eos_token_id,
        bad_words_ids=[[tokenizer.bos_token_id]],
        num_return_sequences=1,
    )
    thread = Thread(target=model.generate, kwargs=generation_kwargs)
    thread.start()

    for output in streamer:
        if not output:
            continue
        await asyncio.sleep(0)
        yield output

TextIteratorStreamermodel.generateに渡している。ドキュメントに倣ってmodel.generateは別スレッドで実行し、TextIteratorStreamerイテレータとして回してyieldしている。TextIteratorStreamerは生成されたテキストを内部である程度バッファリングして、きりのよいところで返してくれる。ややワークアラウンドのようにも見えるが、ループの中でasyncio.sleepを行って、他の処理をブロッキングしないようにする。

FastAPIのエンドポイントからは次のようにStreamingResponseを返す。

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

from server.generator import generate

app = FastAPI()


class GenerateInput(BaseModel):
    text: str


@app.post("/generate")
async def generate_post(generate_input: GenerateInput):
    return StreamingResponse(generate(generate_input.text), media_type="text/plain")

StreamingResponseは非同期イテレータを受け取るので、これで十分機能する。この実装では単純にtext/plainのレスポンスがストリームで返るので、クライアントが特別に対応していない場合でも互換性がある。より構造化されたレスポンスを返したい場合は、server-sent eventsとして返す方がよさそうだ。

フロントエンドでストリーミングされたデータを表示する。

フロントエンドでもストリーミングで表示するには少し工夫が要る。次のように非同期ジェネレータを書く。

async function* generate(text: string): AsyncGenerator<string> {
  const res = await fetch('https://example.com/generate', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ text }),
  })
  const reader = res.body?.getReader()
  if (!reader) {
    return
  }
  const decoder = new TextDecoder()
  while (true) {
    const { value, done } = await reader.read()
    if (done) {
      break
    }
    yield decoder.decode(value)
  }
}

普通ならawait res.text()とするところで、本文の読み込みストリームを取得する。そこから逐次的に値を読み取って、TextDecoderでデコードしつつyieldしていく。

Reactなら、簡略化した例だが、次のように使う。

'use client'

import React from 'react'

async function* generate(text: string): AsyncGenerator<string> {
  (省略)
}

export function Generator() {
  const [input, setInput] = React.useState('')
  const onChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
    setInput(event.currentTarget.value)
  }
  const [output, setOutput] = React.useState('')
  const onSubmit: React.FormEventHandler<HTMLFormElement> = async (event) => {
    event.preventDefault()
    setOutput('')
    for await (const chunk of generate(input)) {
      setOutput((prev) => prev + chunk)
    }
  }
  return (
    <div>
      <form onSubmit={onSubmit}>
        <input onChange={onChange} />
        <button type="submit">
          Generate
        </button>
      </form>
      <div>{output}</div>
    </div>
  )
}

for await...ofで非同期ジェネレータから情報を読み出している。

Time to First Byte (TTFB)

ストリーミングの様子は、Webインスペクタのネットワークタブでも確かめられる。ストリーミングしないと「待機中」のまま長く待たされるが、ストリーミングすると「待機中」はすぐに終わり、「ダウンロード」が続く。つまりTTFBが改善されると言い換えられる。

ストリーミングしない場合は待機中が続く

ストリーミングすると待機中が短くなる

いかがでしたか

huggingface/transformersのStreamerによって、簡単にテキスト生成結果をストリーミングできることがわかった。まだリリースされていない機能で、リリースまでにAPIが変わるかもしれないが、リリースされるのが楽しみだ。

XCFrameworkをSwift PackageとしてGitHubでリリースする

以前、XCFrameworkをバイナリターゲットとしてSwift Packageに埋め込んだライブラリを作った。

これを容易に利用するためには、ビルド済みのXCFrameworkをどこかにアップロードして、それを参照するSwift Packageを公開するとよい。XCFrameworkはGitHubリリースに成果物として置いておくことにする。

ということで、前回は概ねこれを手作業でやったのだけど、手作業したくないのでGitHub Actionsにした。

リリースが自動化されているソフトウェアは信頼性が高い。

GitHub Actionsでバイナリターゲットを含むSwift Packageをリリース

自動化にあたって手順を書き出してみると、次のようになる。ややこしい。

  1. XCFrameworkをビルドしてアーカイブする
  2. XCFrameworkのチェックサムを計算
  3. バージョン番号を決める
  4. Package.swiftを書き換える
  5. Package.swiftをコミット
  6. タグを打つ
  7. GitHubでリリースを作成

実際のActionはこうなる。

順を追ってやっていく。

XCFrameworkをビルドしてアーカイブする

これは普通にやったらいい。今回はgomobileを使っているのでちょっと変な感じではある。Makefileに書いてある。

.PHONY: install-tools
install-tools:
    go install golang.org/x/mobile/cmd/gomobile@latest
    gomobile init

.PHONY: xcframework zip
xcframework: install-tools
    gomobile bind -target=ios,iossimulator,macos,maccatalyst -iosversion 14 -prefix GOJQ -o Frameworks/GOJQBinding.xcframework github.com/cockscomb/swift-gojq/binding

zip: xcframework
    zip -X -r Frameworks/GOJQBinding.xcframework.zip Frameworks/GOJQBinding.xcframework/

XCFrameworkのチェックサムを計算

Swift Packageのバイナリターゲットにはチェックサムが必要なので計算する。swift package compute-checksum するだけ。

- name: Compute checksum
  id: checksum
  run: |
    echo "checksum=$(swift package compute-checksum Frameworks/GOJQBinding.xcframework.zip)" >> $GITHUB_OUTPUT

バージョン番号を決める

mathieudutour/github-tag-action を使った。major とか minor とか patch とかで一つ前のバージョン番号から決めることにして、workflow_dispatchinput で選べる感じにしている。

on:
  workflow_dispatch:
    inputs:
      bump:
        description: 'Bump version'
        required: true
        default: 'patch'
        type: choice
        options:
        - major
        - minor
        - patch
- name: Calculate next version
  id: next_version
  uses: mathieudutour/github-tag-action@v6.1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    default_bump: ${{ github.event.inputs.bump }}
    tag_prefix: ''
    dry_run: true

バージョンを先に決めておかないとリリースのURLが決まらないので、先にバージョンを決める。dry_run があって便利。

Package.swiftを書き換える

ここがいちばんトリッキー。リリースのURLとチェックサムPackage.swift に書き込まないといけない。Package.swift のテンプレートを用意してレンダリングするか、すでにある Package.swift を書き換えるかだが、テンプレートを管理するのが面倒なので後者を選ぶ。

丁寧にSwift Package Pluginを作ってある。Swift Syntaxで Package.swift を書き換える。

Swift SyntaxによるPackage.swiftの書き換え

Swift Syntaxでは、SyntaxRewriter を継承して適当な visit(_:) メソッドを実装すると、ASTを辿って必要な箇所を書き換えられる。Sources/swift-package-checksum-rewriter/BinaryTargetSourceRewriter.swift として実装している。書き換え前後で余計な差分が出にくいように Trivia というやつで改行や空白を丁寧に調整している。(それもあってSwiftSyntaxBuilderがあまり思うように使えていない。)

これを executableTarget にしておいて、Swift Package Pluginからは実行ファイルとして呼び出す。だからプラグインとしての実装は次の通りで、ただのラッパーになった。

import Foundation
import PackagePlugin

@main
struct RewriteChecksumCommandPlugin: CommandPlugin {
    func performCommand(context: PluginContext, arguments: [String]) async throws {
        let tool = try context.tool(named: "swift-package-checksum-rewriter")
        let process = try Process.run(URL(fileURLWithPath: tool.path.string), arguments: arguments)
        process.waitUntilExit()
    }
}

使い方

これを使うには Package.swift に依存を追加する。

     dependencies: [
        .package(url: "https://github.com/cockscomb/swift-package-checksum-rewriter", from: "0.1.0"),
    ],

あとは swift package コマンドから呼び出すだけ。--allow-writing-to-package-directory するのがコツ。

- name: Rewrite Package.swift
  run: |
    swift package \
      --allow-writing-to-package-directory \
      rewrite-package-binary-target \
      --url=https://github.com/cockscomb/swift-gojq/releases/download/${{ steps.next_version.outputs.new_tag }}/GOJQBinding.xcframework.zip \
      --checksum=${{ steps.checksum.outputs.checksum }} \
      Package.swift \
      GOJQBinding

ここが一番面倒だった。

Package.swiftをコミット

ここはどうやってもいい。今回は stefanzweifel/git-auto-commit-action を使った。簡単。

- uses: stefanzweifel/git-auto-commit-action@v4
  with:
    commit_message: Bump version to ${{ steps.next_version.outputs.new_tag }}

タグを打つ

さっきは dry_run したけど今回が本番。

- name: Bump version
  id: bump_version
  uses: mathieudutour/github-tag-action@v6.1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    default_bump: ${{ github.event.inputs.bump }}
    tag_prefix: ''

GitHubでリリースを作成

actions/upload-release-assetアーカイブされているので、案内に従って softprops/action-gh-release を使う。これも簡単。

- name: Release
  uses: softprops/action-gh-release@v1
  with:
    tag_name: ${{ steps.next_version.outputs.new_tag }}
    body: ${{ steps.next_version.outputs.changelog }}
    files: Frameworks/GOJQBinding.xcframework.zip

完成

結構ややこしい感じだけど、これでバイナリターゲットを含むSwift Packageを自動的にリリースできるようになった。

ちょっとやったらできるだろうと思って作業し始めたけど、Package.swiftの書き換えがややこしくて思ったより大変だった。

React Server ComponentsとGraphQLは競合するか

Next.jsのapp directoryについて話していて、GraphQLを使う場面ではServer Componentsの魅力がいくらか落ちるよな、と思った。裏を返せば、Server Componentsが活用されるような時代ではGraphQLの重要度が下がるかもしれない。

現にServer ComponentsのRFCの「Credits and Prior Artを見ると次のように書いてある。

  • Relay’s data-driven dependencies, which allow the server to dynamically choose which Client Component to use.
  • GraphQL, for demonstrating one approach to avoiding multiple round trips when loading data for client-side apps.

GraphQLやRelayは「Prior Art」だ。

GraphQLが強力な理由に、データのグラフがそのまま扱えることや、フラグメントによってコンポーネントが必要とするデータを宣言的に表現できることがある。これらによって解決された課題は、Server Componentsでも解決できる。Server Componentsはサーバーで構築されるから、Web APIを複数回にわたって呼び出す際のRTTが十分に短くなるし、それぞれのコンポーネントで必要なデータを個別に取得してもそれなりにいい感じになる。

ということで、Server Componentsを前提とすると、GraphQLによって得られていた利益(の一部)が他の手段でも叶えられることになる。またはGraphQLを前提とすると、Server Componentsによって得られるはずの利益が多少は目減りする。

もちろんServer Componentsによって達成されることは他にもあるし、GraphQLにも他にメリットがある。

GraphQLは引き続き有力

GraphQLの重要なメリットに、オーバーフェッチングを防ぐことがある。またエコシステムの発展のおかげで、ツーリングも優れている。ネイティブアプリからも利用でき、ひとつのAPIで多様なクライアントをサポートできる。GraphQLはWeb APIを構築する際に、引き続き有力な選択肢である。

またGraphQLの側も@defer@streamディレクティブによって、Next.jsのStreamingと同じ課題を解決しようとしている。ふたつの技術がある意味では競合するという例でもある。

しかし、RelayやApollo Clientの強力なキャッシュ機構を今のServer Componentsと組み合わせてうまく動かすのはややこしいだろう。と言いつつ、先のページのFAQに「Does this replace Apollo, Relay, or other GraphQL clients for React?というのがあり、そこでは「No」という回答に加えて、次のようにある。

For example, internally we use Relay and GraphQL in conjunction with Server Components.

つまり詳細はわからないがFacebookでは何らかうまくやっているようである。ただし直感的には、urqlとか、ややライトウェイトなクライアントが取り回しやすい、みたいなことが起きるんじゃないかという気はする。もちろんRelayやApollo Clientの側もさらに進歩して、適応していくだろう。

Server Componentsの時代

ということでServer Componentsの時代では、これまでのクライアントサイドレンダリング(+1ページ目はサーバーサイドレンダリング)のような、クライアント側でやっていくスタイルから変化し、GraphQLの意味付けも変わっていく予感がある。より単純なクライアントが好まれるようになったり、そもそもGraphQLではなくRESTあるいはRPCスタイルのWeb APIが流行るかもしれない。