Qiite

https://github.com/Digix00/58-Hackathon-202609

GitHub

TypeScript

LINEミニアプリ上で動作する、悩みを投稿するサービスです。

mimu

cocashy

Miyu Sone

推しアイデア

インクルーシブを実現するために子供から高齢者までが使用しているLINEを使用します。 追加のアカウント作成不要で手軽に始められ、レコメンド機能により受動的に他人の悩みに触れ、考えることができます。

作った背景

普段関わる人は学校や職場の人ばかりで別の立場の人の悩みは意外と知らないという課題があると考えます。 困っていても誰に言えばいいのかわからないという人もいると思います。 匿名で悩みを投稿し、立場の違う人あそれを読める場を幅広い年代が使うLINEミニアプリとして実装しました。

推し技術

Full-Stack TypeScriptで実装し、Hono RPCで型安全な通信を実現します。 Cloudflareに機能を乗せることで高速かつ、低コストに動作します。翻訳などの処理は、非同期でAIを呼び出しているためユーザー体験を損なわなずに利用できます。

プロジェクト詳細

Qiite — 悩みを聞いてもらうことから、はじめよう。

概要

「Qiite(キーテ)」は、匿名で寄せられた悩みを通して、自分とは違う立場の人の日常に触れるアプリです。

身近な困りごとを投稿したり、ノートをめくるように誰かの悩みを読んだり。実際の投稿を使ったクイズから、普段は意識していなかった立場について考えることもできます。

LINEミニアプリを入口に、音声入力、ひらがな・英語表示、AIによる悩みの分類を組み合わせました。

誰かの「困っている」を、別の誰かの「知らなかった」につなげることを目指しています。

推しアイデア

悩みを「届ける」と「知る」を、ひとつの場所に

自分の悩みを言葉にすることと、他人の悩みを想像すること。その両方を、日常の中で少しずつできるようにしました。

文章を書くのが難しいときは、話して入力する。 漢字が読みづらいときは、ひらがなに切り替える。 何から読めばよいか迷うときは、おすすめやクイズから触れてみる。

参加の仕方を複数用意することで、さまざまな人の声が集まる場所を目指しています。

作った背景

普段接する人は、同じ学校、同じ職場、同じ趣味のコミュニティなどに偏りがちです。

その中では当たり前に共有されている悩みも、別の場所で暮らす人にはほとんど知られていないかもしれません。

また、困っていることがあっても、「誰に伝えればよいかわからない」「人前では話しにくい」と感じることがあります。

そこで、身近な悩みを匿名で届け、それをさまざまな立場の人が読むための「目安箱」を作りました。

様々な年齢の人が使用しているLINEのプラットフォームの中で動くアプリにすることで、追加のアカウント作成なしで、気軽に始めることができます。

大切にしたのは、投稿するまでの使いやすさと、投稿された声に出会う体験です。

使い方

1. ノートをめくって、誰かの声を読む

公開された投稿は、通常のLINEミニアプリから読めます。

画面は、紙のノートをモチーフにしました。ページをめくりながら、一つひとつの悩みに触れられます。

年代や地域など、投稿に添えられた情報から、その人の日常を想像してみてください。

image

2. 自分の悩みを投稿する

文章を直接入力するほか、音声入力も利用できます。話した内容は文字に変換され、投稿する前に確認・修正できます。

投稿はアカウントに紐づいて管理されますが、ほかの利用者には匿名で表示されます。

image

3. 自分に合った表示で読む

投稿の表示は、原文・ひらがな・英語に切り替えられます。

同じ内容でも、読みやすい表現は人によって違います。読む人が表示を選べるようにすることで、声が届く範囲を広げたいと考えました。

共感した投稿にリアクションすることもできます。

image

4. クイズで、違う立場を想像する

LINEの公式アカウントから1日1回クイズが届きます。

クイズでは、3人分の属性と3件の実際の投稿を見て、誰の悩みなのかを対応付けます。

「この悩みを持つのは、どんな人だろう?」

そう考えてから答えを見ることで、自分の想像と実際の声との違いに気づくきっかけを作ります。

正解数に加えて、「この立場の人も、こんなことで困っているんだ」という発見を持ち帰ってもらいたい機能です。

履歴から正答率を見ることもできます。

image

推し技術

ベクトル検索とクラスタリングで、言い方の違う悩みをつなぐ

同じような悩みでも、使う言葉は人によって異なります。

たとえば、「昼休みに食堂で座れない」と「お昼の混雑で休む時間が足りない」は、表現が違っても近い困りごとかもしれません。

目安箱では、投稿をEmbeddingという意味を表す数値データに変換し、Cloudflare Vectorizeで近い投稿を探します。類似度が基準を満たす場合は、同じグループにまとめます。

さらに、グループのラベルと要約をAIで生成し、どのような悩みが集まっているのかを伝えます。

単語の一致だけでは拾いにくい、悩み同士のつながりを見つけるための仕組みです。

仕組み

全体構成

主な技術スタック

  • フロントエンド:React / Vite / TypeScript
  • LINE連携:LIFF / LINE Messaging API
  • API:Hono / Cloudflare Workers
  • データベース:Cloudflare D1 / Drizzle ORM
  • 非同期処理:Cloudflare Queues
  • AI処理:Cloudflare Workers AI
  • ベクトル検索:Cloudflare Vectorize
  • フロントエンド・API間の型共有:Hono RPC

インフラ構成

image

AIによる非同期処理のフロー

悩みを書いて投稿したあと、AIの処理が終わるまで待たされると、きちんと届いたのか不安になります。

そこで、最初に原文をD1へ保存し、翻訳や分類はQueuesを通じて後から処理する構成にしました。 AI処理が失敗しても、保存した原文を閲覧できるようにしています。

3つの生成処理は順番に待つ必要がないため、まとめて並行に実行しています。AIの応答時間が投稿体験に影響しないようにするための分離です。

Queueによる冪等性についてもこだわりました。Queueは少なくとも一回実行されるという仕組みになっており、ネットワークの一時的な失敗やAIの応答エラーで、同じ投稿の処理が2回走ることがあります。 そこで以下のような仕組みを導入することで必要な処理を一回だけ実行するように実装しています。

  • 処理の入口で、すでに完了している投稿かどうかをD1の状態で判定し、完了済みならそのまま終了する
  • ひらがな・英語のうち片方だけ生成済みなら、残りだけを生成してAIの呼び出しを無駄にしない
  • グループの割り当ては先にD1へ確定させ、そのあとでVectorizeへベクトルを登録する

処理に失敗したとしても、失敗したと記録されてQueueが自動的に再試行する仕組みを導入しています。

クラスタリングを自動的に行う

クラスタを先に決めておくのではなく、投稿が集まるにつれて自動的にグループが作成される形にしました。

投稿をEmbeddingに変換したあと、Vectorizeで意味の近い投稿を上位10件探します。類似度が基準(コサイン類似度0.7)を超える投稿があれば、その投稿が属するグループに加わります。近いものが見つからなければ、その投稿自身が新しいグループの起点になります。

どんな悩みがあるのか、開発者が選ばなくても実際に届いた声から新しいクラスタが作成されるような実装をしています。

グループができたあとは、そこに集まった公開投稿(最大10件)からラベルと要約をAIで生成します。プロンプトの中で、連絡先・URL・個人名・住所や攻撃的な表現を出力しないよう指示し、生成結果はJSON形式とラベル・要約が空でないことを検証してから保存します。形式が不正だった場合は保存せず、Queuesの再試行に任せます。

Embeddingは生成モデルが変わると数値の意味も変わってしまいます。古いベクトルと新しいベクトルが混ざると類似度の判定が成り立たないという特徴があります。 投稿ごとに「モデル名 + インデックス世代」を組み合わせた文字列を保存しています。モデルを差し替えると保存済みの値と一致しなくなり、再処理されたときだけベクトルが作り直されます。

レコメンドについて

LINEログイン済みの利用者には、次に読む悩みをおすすめします。 通常Webとログイン前のLIFFでは、新着順で表示します。

おすすめ順では、生成AIに順位を決めさせるのではなく、直近50件の閲覧履歴や投稿の属性を使ったルールベースの方法で候補を選びます。 公開中の投稿から新しいものを候補にし、本人の投稿を除いたうえで、未読の投稿や、まだ読んでいないテーマを優先します。

同じテーマ、地域、年代の投稿に偏らないよう、ページ内の構成も考慮します。よく読んでいるテーマは控えめにし、同じテーマが続かないようにします。利用者のプロフィールと同じ都道府県や地方の投稿も一部優先します。

新しい投稿ほど優先し、その加点は24時間ごとに半減します。また、利用者と日本時間の日付に応じた小さなゆらぎを加え、同じ並びが続きにくくしています。投稿には「未読のテーマ」「同じ県の声」など、おすすめの理由も表示します。推薦処理に失敗した場合は新着順に切り替え、悩みを読み続けられるようにしています。

デプロイを簡潔にすることによる開発者体験の向上

このプロジェクトではLINEミニアプリを用いているため、ローカルホストによるUIの確認や、LINE独自の実装の動作確認などができません。そこで、GitHubのPull Requestをマージすることによる即時デプロイを構築しています。

開発者はdevelopブランチから各自の作業ブランチをチェックアウトして、pull requestの作成まで進めます。 developブランチに自身の作業内容をマージして、developブランチからmainブランチにマージをすることでCloudflareの環境へ自動デプロイ、D1へ自動マイグレーションを行います。また、マイグレーションにはDrizzleを採用することで、コードの中で型安全を保ちつつ、継続的なマイグレーションも実現しています。

ソフトウェアアーキテクチャ

レイヤードアーキテクチャを採用することでコードの責務を適切に分割しました。開発者がどのコードをどこに書いたらいいのかドキュメントに明文化するなど工夫をすることで開発スピードと品質を高めました。

音声・翻訳・分類でのモデルの使い分け

Workers AIでは、処理の目的に合わせてモデルを使い分けています。

用途と採用モデルは以下の通りです。

音声の文字起こし : OpenAI Whisper 英語翻訳・ひらがな変換 : Llama 3.1 8B 悩みのラベル・要約生成 : Llama 3.1 8B Embedding生成 : Qwen3 Embedding 0.6B

音声は投稿前に文字起こしし、利用者が内容を確かめられるようにしました。翻訳や分類は、投稿の保存後に処理します。

Hono RPCで画面とAPIをつなぐ

フロントエンドからは、バックエンドのAPIの型を参照しています。

送信する項目や返ってくるデータの変更を型検査で見つけやすくすることで、投稿・フィード・クイズなど、複数の機能をつなぐ開発を支えています。

UI/UX

誰かの悩みに、立ち止まって出会うUI

本アプリケーションで大切にしたのは、悩みを一覧の中の一件として消費するのではなく、ひとつの声として受け止められることです。

そこで、一般的なタイムラインではなく、紙のノートを1ページずつめくる構成にしました。リング綴じや付箋、クレヨン風の質感、ページをめくる動きを取り入れ、誰かが書いたノートを開くような体験をつくっています。

また、文字の大きさや行間、背景とのコントラスト比も定量的な数値をもとに配慮することによってアクセシビリティにも配慮しました。

紙とクレヨンの柔らかい世界観に、読みやすさと迷わず操作できる配置を組み合わせました。

参加方法の違いを、UIの中で吸収する

「伝えたい」「読みたい」「考えてみたい」という入口は、人によって異なります。その違いを利用者の努力に任せず、UIの中で選べるようにしました。

  • 投稿では、文章入力と音声入力を選べる
  • 閲覧では、原文・ひらがな・英語を切り替えられる
  • おすすめやクイズから、読むきっかけをつくる
  • 共感した投稿にはリアクションを返せる

読む人には表示方法の選択肢を用意し、さまざまな人が無理なく参加できるようにしました。

これは単に機能を増やすためではなく、声を届ける人と受け取る人の両方に参加しやすい入口を用意し、インクルーシブな社会につなげるための設計です。

処理の工夫

Hono RPCの AppType からAPIのレスポンス型を推論し、画面とAPIのデータ形式を型安全につないでいるため、通信の遅延や失敗が起きても表示が不整合になりにくい構成にしました。

クイズの送信に失敗した場合も、判別可能なユニオン型で管理しているため、保存済みなら回答済み画面に復帰し、リクエストには世代番号を付け、未保存でも選択内容を残して再試行できます。

多言語表示では、言語を切り替えてもクイズの問題順や回答内容を保持します。読みやすさのための機能が、操作のやり直しを生まないようにしました。 また、型を使って各辞書のキーを揃えているため、ひらがなだけ文言が不足している場合も、実装時点で検出できるようにしています。

処理を待つ間も、スケルトン画面ではなく、アニメーションを含んだページを挟むことにより、ユーザーに待ち時間を感じさせず、目安箱の世界観を保持したまま、画面を表示することができました。

見た目のために操作性を犠牲にせず、待ち時間も体験の一部になるようにしました。

LINEミニアプリの特性上の苦労

LINEの内蔵ブラウザでは、PCブラウザと同じように動かない部分があります。

スワイプ中の指の移動量は useRef に保持しており、 requestAnimationFrame で紙の動きを更新しています。入力中は更新する範囲を限定したり、Reactのstateを毎フレーム更新しないことで、紙の傾きやページ送りを滑らかにし重くならないような工夫があります。

また、縦画面やセーフエリア、縦スクロールと横スワイプの判定をtransformで補間し、レイアウト計算を抑えています。また、指の移動方向から縦スクロールと横スワイプを判定し、スワイプ直後のリンク誤操作を防いでいるため、LINEの中でも紙をめくる感覚と誤操作の起きにくさを両立しました。

最後に、こだわったところ

紙のノートのような画面

リング綴じ、付箋、クレヨン風の装飾、ページをめくる動きを取り入れました。

誰かが書いたノートを開くような感覚で、一つの声を読んで、次の声へ進める画面にしています。

普段使うLINEから参加できる

公開投稿はブラウザから読めて、投稿やクイズへの参加はLINEミニアプリから行います。

まずは読むだけでも触れられる入口と、継続して参加する入口を用意しました。

AIの結果がなくても、原文が残る

翻訳や分類は、投稿を読みやすく、見つけやすくするための補助です。

その処理の成否にかかわらず、利用者が書いた言葉を残すことを設計の中心に置いています。

成長・協力ポイント

  • チームメンバー初めてLINEミニアプリでの実装だったので非常に苦戦しましたが無事作り切ることができたと思っています。
  • 普段Go言語などを使用しているエンジニアばかりでしたが、Hono,Cloudflareというスタックを用いて作り切ることに挑戦できました。
  • 普段から頻繁にDiscordを動かし、アイデア出しの時点から動機的にミーティングするなどということに取り組みました。

今後の展望

今後は、準備中の読み上げ機能をつなぎ、音声で投稿を読む選択肢も増やしていきたいと考えています。

また、LINEのリッチメニューも活用し、ミニアプリを開かなくても、LINE上から悩みを読んだり、共感した悩みにリアクションしたりできる仕組みを検討しています。日常的に使うLINEの中で、より少ない操作で誰かの声に触れられるようにすることで、悩みを「知る」「反応する」までのハードルをさらに下げていきたいです。

さらに、分類やおすすめが、どのくらい新しい気づきにつながるのかを確かめながら、クイズの見せ方や翻訳の読みやすさも改善していきます。

「こんなことで困っている人がいるんだ」

目安箱を開いたときに生まれるその小さな気づきを、誰かの立場を考えるきっかけにつなげていきたいです。

mimu

@c0becbc6e7391fe9