高校生がAI作品に特化した画像投稿サービス「Aivy」を個人開発した話 | SolidJS & Supabase & Cloudflare
こんにちは
だだっこぱんだと言います。だだっこではないです。
今回は初めてしっかりした個人開発をしたので、技術的な部分についていろいろとお話しします。
つくったもの
Aivy というAI作品に特化した画像投稿サイトを作りました🍃
👇よかったらこれ拡散してくれるとうれしいですの
ScreenShot
ホーム画面
投稿ページ
ユーザーページ
検索
投稿管理
なにこれ
AI作品に特化した画像投稿コミュニティサービスです。
ただ画像を投稿するだけでなく、プロンプトやモデル等のAI画像生成に使う情報を一緒に投稿できます。
この記事では
この記事では技術面にフォーカスしてお話しします。
どうして作ったの? とかその辺の話は下記リンクのNoteにまとめてありますので合わせてご覧ください。
開発体制
メンバー
- ぼく(高校生)
開発フロー
完成まではゴリゴリローカルでした。タスク分けとか何も管理しませんでした。ある程度完成してからGithubのリポジトリ作り直してしっかりコミット分けたりするようにしました。
個人開発だとどうしても適当になっちゃいますね;
Github
こういうアプリケーションってOSSにするのはよろしくないんですかね?OSSにしていろんな人に機能追加とかしてもらえたら最高だなぁとか思ったのですが、認証とかその辺扱ってるのであまりよろしくないのかなとも思ってプライベートリポジトリで管理してます。
もしその辺知見ある方いらしたら教えていただけるとありがたいです。
※2022/11/14追記
OSS化
こちらでコード公開しますた
使用技術
今回使った技術はこれらになります。
- SolidJs - 仮想DOMを使わないReactみたいな(?)
- SolidStart - NextjsのSolidJs版(?)みたいなアプリケーションフレームワーク 詳しくはこちら
- Supabase - 認証とデータベース
- Vercel - ホスティング
- CloudFlare - DDOS保護とかドメインとか
- Cloudflare Images - 画像の保存/配信
- Cloudflare Zaraz - GoogleAnalyticsを使うため
はじめはCloudflareWorkersを使おうと思っていたのですが、SolidStartがなんかまだ対応してないみたいなんですよね。(前回の記事で嘘つきましたすいません。)
なので仕方なくVercelで...
SolidJs - SolidStart
これらを選んだ理由は単純に僕が好きだからです。仮想DOMを使わないのでパフォーマンスも良いですし、個人的にはcreateSignal
がとても好きです。これのおかげでReactivity(?)みたいなのの仕組みがだいぶ理解できました。
デメリットとしてはまだできたばかりのフレームワークなのでバグがちょくちょく見つかります。開発中にいろいろとバグが見つかっては修正されるみたいなのがよくありました。
特にCloudflareWorkersが使えなかったのがすごい辛い...
速く対応してほしいですの
CSS全部書いた
SolidJsはまだできたばかり。SSR対応のUIライブラリはかなり少ないです。
なので1からsolid-styled-components
でcssを書きました。割と大変でした。
これのおかげでかなりcssの理解が深まった
SSRは最小限に
SolidStartのSSRはNextjsみたいに簡単に扱えないのでなるべく使わない方針で行きました。
使ったのはユーザーページや投稿ページ等のOGPの設定が必要なページのみにとどめました。
そのほかは全部CSRです。(CSRと言ってもある程度の部分はサーバー側でレンダリングされるのですが)
Supabase
supabaseはもうほんとに強いですね。無料でここまでいろいろできるのはほんとにありがたいです。
認証
とりあえずTwitterアカウントでログインできるようにしています。
Supabaseのデメリットは他のプロバイダとのリンクができないところですね。メールアドレスが同じアカウントでないと新しいアカウントが作られてしまいます。
データベース
投稿の情報やプロフィール、いいね、フォロー、タグ...などを保存してます。500MBは多分超えないでしょう...
超えたら僕の月3000円のお小遣いが飛び散ります。
設計が正しいのかわからない
投稿、いいね、フォロー関係、コメント... みたいな単位でテーブルを分けているのですが、なかなかこういう開発をしたことがなく正しいのかどうなのかが全然わからんのです。
特に、現在投稿の情報にもいいね数を保存していて、いいねのInsert時にTriggerを使って投稿の情報のいいね数を増やしてます。これはベストな実装だったのだろうか...?
CloudflareImages
今回大活躍のサービスです。
とりあえず強みを紹介します。
安い(多分)
10万枚の画像保存につき月額5ドル、10万枚の画像配信につき月額1ドルです。
supabaseのストレージとか使おうとすると1GBじゃさすがに足りないのでPro($25)にする必要があります。
他のサービスは配信容量での従量課金だったりするので、高校生の僕はあんまり安心して使えないです。
CloudflareImagesは全て枚数計算です。しかも10万枚の配信で1ドル!
これならまだ安心して使えますね!
Flexible variantsが超強い
簡単に説明すると画像のリサイズとかブラー処理とかをサーバー側で配信時にやってくれる奴です。これのおかげで4Kとかの大きなサイズの画像を利用者に投げつけなくて済みます。
https://aivy.run/cdn-cgi/images/<ACCOUNT_HASH>/<IMAGE_ID>/w=400,h=800,blur=50
👆みたいなURLで画像を取得すると、横幅400ピクセル高さ800ピクセル、ブラーのかかった画像が取得できます。
できることはドキュメント見れば大体わかります。
DirectCreatorUpload
普通は画像をアップロードする際に一度サーバーを介す必要があります。APIキーをブラウザに晒すのはまずいですからね。しかしそうすると帯域制限とかがかなり気になりますね...
そんな時はDirectCreatorUpload!アップロード用のURLだけ取得してブラウザから直接アップロードしちゃいましょ!そうすれば大きなサイズの画像もアップロードできます!
ちなみにv2だとカスタムIDが使えませんでした。なぞいのでコミュニティで聞こうとしたらアカウントが止められました。さらに謎いです🤔
1週間後くらいに復活したので良かったですが...
Cloudflare Zaraz
コードかかなくてもGoogleAnalyticsとかが使えるやつです。
ダッシュボードで追加してID入力すれば自動でhtmlにコードを挿入してくれます。
結構便利です。
Exifの読み込み
AI画像と言えばプロンプト等の情報ですよね。もちろん一緒に投稿できます。
ついでに画像のexif情報からプロンプト等の情報を読み込み用にしました。
exifr というライブラリを使ってブラウザ上でexifを読み込んでいます。
NovelAIはDescription
にプロンプトが、Comment
にステップ等の情報が保存されています。比較的扱いやすかったです。
一方Automatic1111 Webuiの方はparameters
に全て詰まったテキストが入っている感じでした、正規表現とかいろいろ使わないといけなくて少し大変でした。
👇書いたコードです。
import type { ImageInformation } from '~/types/images'
const parseAutomatic1111Exif = (parameters: string) => {
const result: Partial<ImageInformation> = {}
const prompt = parameters.split(/\nNegative prompt:/)[0]
const negative_prompt = parameters.split(/\nNegative prompt:/)[1]?.split(/Steps: \d+/)[0]
const others = parameters.split(/\n/g).slice(-1)[0]
result.prompt = prompt || ''
result.negative_prompt = (negative_prompt || '').replace(/\n$/, '')
if (others) {
for (const prop of others.split(/, /g)) {
const [key, value] = prop.split(': ')
switch (key) {
case 'Steps':
result.steps = value || ''
break
case 'Sampler':
result.sampler = value || ''
break
case 'CFG scale':
result.cfg_scale = value || ''
break
case 'Seed':
result.seed = value || ''
break
}
}
}
return result
}
const parseNovelAIExif = (exif: Record<string, any>) => {
const comment = JSON.parse(exif['Comment'])
const prompt = exif['Description']
const result: Partial<ImageInformation> = {
prompt,
negative_prompt: comment['uc'],
steps: `${comment['steps']}`,
sampler: `${comment['sampler']}`,
cfg_scale: `${comment['scale']}`,
seed: `${comment['seed']}`,
model: `${exif['Software']}`,
}
return result
}
export const parseExif = (exif: any) => {
if (exif?.['parameters']) return parseAutomatic1111Exif(exif['parameters'] as string)
else if (exif?.['Software'] === 'NovelAI') return parseNovelAIExif(exif)
}
検索
検索は一番大事だと思って結構しっかり書きました。
シンプルにタイトルやプロンプトを入力しても良いですし、twitterのように(username: だだっこぱんだ)
のように各プロパティを指定して検索できます。
👇書いたコード
class Api {
// ...
public async search(builder: PostgrestFilterBuilder<any, any, any>, query: string) {
// eslint-disable-next-line no-irregular-whitespace
query = query.replace(/ /g, ' ')
const options = (query.match(/\(.+?\)/g) || []).map((v) => v.replace(/^\(|\)$/g, ''))
const words = query
.replace(/\(.+?\)/g, '')
.split(' ')
.filter((v) => !!v)
for (const option of options) {
const [key, value] = option.split(':') as [string, string]
const trimmed = value.trimStart()
switch (key.trim()) {
case 'username':
builder.ilike('profiles.username', `${trimmed}%`)
break
case 'id':
builder.eq('profiles.id', trimmed)
break
case 'tag':
builder.contains('tags', [trimmed.split(' ')])
break
case 'prompt':
builder.ilike('information->0->>prompt', `%${trimmed}%`)
break
case 'negative_prompt':
builder.ilike('information->0->>negative_prompt', `%${trimmed}%`)
break
}
}
for (const v of words) {
const word = v.trimStart()
builder.or(
`title.ilike.%${word}%,description.ilike.%${word}%,information->0->>prompt.ilike.%${word}%,tags.cs.{${word}}`,
)
}
}
// ...
}
ただこれ一つ問題があります。
現在information->0->>prompt
のようにしてプロンプトから検索しています。
配列にしているのには理由があり、今後複数枚投稿に対応していたいからです。
しかし、json配列から検索する方法がないのです。
rpcを使えばできなくもないようですが、sqlを書けない僕には荷が重い...
なるべくjsで完結させたい...
なにかご存じの方いらしたら教えてほしいです
反省点
デザインが...
いろんな人のデザインを参考にしていたのですが、なんかただのパクリみたいになってる気がしてちょっと申し訳なくなりました。
特にZennのデザインをめちゃくちゃ参考にしました。
時間かかった
始めは1週間程度でできるかなとか思ったのですが割と時間かかっちゃいました。もう少しコーディング速くなりたいです。かといって適当にやるのは良くないですけどね。
学んだこと
- データベースの知識 (Postgresql)
- デザインのむずかしさ。
- お金があまりなくても開発はできる。無料サービスに感謝。
今後
いずれCloudflareWorkersに移行したいです。solid-startのコード書こうかなとも思ってます。
Workersが使えればキャッシュ関係もいじれるのでうまいことISRのようなものも実装したいですね。
サービスに関する今後はNoteの方に書きます。
良ければぜひ使ってください
使ってもらえるかが一番の不安要素なので良かったらぜひ1枚だけでも投稿してみてください。
画像生成AIはほんとに簡単に触れます!使ったことない方もこの機会にぜひ一度!
Discussion
めっちゃ素敵なサービスですね!何より高校生でここまでのものを作っているのがすごいです!
オープンソース化も是非するべきだと思います!認証認可周りの設定がちゃんとできていて、隠すべきキーなどがきちんと隠されている状態であればオープンソース化しても問題ないはずなのですが、心配であれば全然レビューに協力しますよ!
なるほど!であればOSS化も進めていきたいと思います!
レビューの件TwitterにDM送らせていただきました。ご確認のほどよろしくお願いいたします。
面白い記事でした!
モノとしては全く違うものなんですが、投稿サイトを個人で作っている所だったので
構成など参考にさせていただきます
ありがとうございます!!
開発の参考になれば幸いです...