入力した文字列を月の絵文字で表現する「StarryKids」というWebサービスを作りました
/ 12 min read
Updated:Table of Contents
はじめに
昔「月文字ジェネレータ」という Web サービスがありました。これは、入力したテキストを月の絵文字に変換して返すという Web サービスでした。Heroku の無料枠が閉じた影響でこのサービスも終了したようでした(確か…)。
そこで、業務で使っている Rails と Vue の勉強も兼ねて自分でも作ってみようと思い、StarryKids(すたーりーきっず)という Web サービスを作りました。なんとなく感のいいかたなら気づいたかもですが、K-POP アイドルの StrayKids(すとれいきっず)からインスパイアされた && 月の絵文字を複数並べて作るので、星空みたいだなと思って名付けました。
現時点ではこんな感じの UI になっています。
現状のUI
機能としてはいくつか制限もあります(Rails のここでバリデーションをしています)。
- 月のサイズ(縦横の絵文字数)は 10 ~ 100(おすすめは 15)
- 変換できる文字はひらがな・カタカナ・半角全角英数字
- 一度に変換できる文字数は 20 文字まで
になっています。
コードは、Tatsumi0000/starry-kidsにあります。実際にローカルで動かしたい方は、Docker を準備すれば簡単にセットアップできるようになっています(多分)。
git clone https://github.com/Tatsumi0000/starry-kids.gitcd webmake setupdocker compose up作成したプロジェクトの概要図と処理の流れは以下のとおりです。
StarryKidsの概要図
技術選定
使用する言語やフレームワークは、バックエンドに Rails、フロントエンドに Vue を採用しており、Rails は Json を返すだけにしています。Vue だけでも作れそうではあるのですが最終的にモバイルアプリも作りたかったのでバックエンド側を作るようにしています。テストには Rspec と Vitest を採用しています。
Vue では TypeScript と、UI フレームワークとしてVuetify、ビルドツールとしてViteを採用しています。おそらく Vue で開発をしようと思うと、まぁこれを使うよね〜というツールを採用してみました。
開発環境ではバックエンドとフロントエンドの Dockerfile を作り docker compose 上で開発できるようにしています。RMagick を使う関係上、ImageMagick が必須になってきます。いちいちローカルにセットアップするのも面倒なので簡単にセットアップできる Docker を採用しました。
プロジェクト自体はモノレポ構成で開発を進めました。今後モバイル(iOS/Android)も開発したかったので、webというディレクトリをトップに作りその中にそれぞれbackendとfrontendを作って開発を進めるようにしています。
ライブラリアップデートもできるだけ自動化したかったので、dependabot を使って自動で PR ができるようにしています。たくさん作られても困るので週末に 5 件までの PR を作るようにしています。
実装の工夫点
テキストを月の絵文字で表現するアルゴリズムについては、月で絵を描きたいだと?それなら python に任せなさい 🌝を参考にしました。ただ今回実装する言語が Python ではなく、Ruby(Rails)だったので一部自分で考える必要がありました。
解説記事では Numpy を使っていましたが代わりに、numo-narrayを採用しました。これが一番 Numpy っぽい書き心地かなと思い採用しました。
解説記事では画像を月の絵文字で表現するという方法でした。今回は変換したいテキストの画像を生成することで解説記事と同じような環境を再現しました。具体的な方法として、予め用意した空の画像ファイルとフォントファイルを使って、受け取ったテキストを RMagick で画像生成するようにしました。フォントファイルはOFL ライセンスで公開されているものがあったのでそちらを使わせていただきました。
画像のグレースケール化に関しては、グレースケール画像のうんちくを参考に自分で各配列の RGB 値をもとに、CIE XYZ の Yの公式 ($V’ = 0.2126R’ + 0.7152G’ + 0.0722*B’$)を使って求めています。これは、人の眼の視覚特性を等色実験で調べて制定した規格らしく、一番人間に自然そうと思い採用しました。コード的にはここの部分です。
DB は使ってないのですが今後使うことを見越して一応セットアップをしています。public で開発しているのでそのまま本番 DB の認証情報をアップするのはよくありません。そこで、Rails の Credentials を使って暗号化して運用しています。マネージドクラウド側に環境変数として復号キーをセットして本番運用しています。
最初の方に少しだけ触れましたが、一応 Model で変換できる文字列に対してバリデーションをかけています。Rails のバリデーションは実際に DB を使わなくてもバリデーション機能だけを使うことができるのでそちらを使っています。ActiveModel::Modelをincludeして、validatesでバリデーションのルールを適用しています(バリデーションの正規表現は ChatGPT に聞きました)。
include ActiveModel::Model
validates :text, presence: truevalidates :text, length: { minimum: 1, maximum: 20 }validates :text, format: { with: /\A[ぁ-んァ-ンa-zA-Z0-9ー0-9A-Za-z ]+\z/, message: '変換できるのは全角半角英数字、ひらがなカタカナのみです。' }validates :size, numericality: { only_integer: true, greater_than_or_equal_to: 10, less_than_or_equal_to: 100 }ディレクトリ構成や CI/CD
CI/CD は GitHub Actions を使っています。Rails では RMagick を使っている箇所もテストしたかったので、予め RMagick 入りのイメージをGitHub Registryに上げてそれを使って CI するようにしています。またイメージはいちいち手動で push すると面倒なので Dockerfile に差分があると自動で CD するようにしています。コード的にはここらへんです。container.imageで使いたいイメージを指定するだけです。
jobs: backend-ci: runs-on: ubuntu-latest defaults: run: working-directory: web/backend
container: image: ghcr.io/tatsumi0000/starry-kids/backend:latest一応フロントエンドのイメージもレジストリに上げてはいるのですが使ってないです…(普通にactions/setup-node@v3を使ってます)
CI/CD のタイミングで関係のないディレクトリは clone したくない(例えば Rails の CI をしたいのに Vue のディレクトリはいらないみたいな)ので、sparse-checkout という機能を使って clone するようにしています。これは指定したディレクトリのみを持ってくる機能です。GitHub Actions では以下のような形で使うことができます。
- name: Checkout code uses: actions/checkout@v4 with: sparse-checkout: | web/frontend本番では Rails を動かすためにロリポップ!マネージドクラウドを採用しました。お金的に安い && 簡単に動かせそう(CD できそう)だったので採用しました。GitHub Pages も安い && 簡単に動かせそうだったので採用しました。安さは大正義です!!
CD については、main ブランチに差分が発生すると デプロイするようにしています。GitHub Pages へのデプロイは特に工夫点もないので割愛します…
マネージドクラウドに関しては、マネージドクラウドと GitHub Actions を利用してデプロイを自動化しようぜを参考にしました。この記事と違う点としては今回はモノレポ構成なのでbackendディレクトリで git を初期化し、マネージドクラウドに force push してデプロイしています。毎回 force push すると履歴が残らないのですが、GitHub には履歴が残るのでまぁいっかという判断でこのようにしています。
最後に
Rails と Vue で月文字に変換する Web サービス「StarryKids」を作りました。この開発を通して Vue と Rails の勘所がなんとなくですがわかってきました。
今後はアイコンの作成や UI の改良、バリデーションに引っかかったときのエラー処理の追加、モバイルアプリの開発、画像から月の絵文字に変換する機能を作りたいなと思っています。