iArt.ai
AI motion graphics software — the After Effects alternative anyone can use. Create animations and explainer videos from a text prompt in minutes.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
AI要約つきの個人用RSSリーダー。Next.js 16 / Supabase / Gemini API
Loading the full README. The saved excerpt is shown below.
AI要約つきの個人用RSSリーダー。大量の記事を素早く捌き、深掘りしたい記事は NotebookLM に投げてすぐ音声にできる状態まで用意する。 • PC: フォルダ / 記事リスト / 本文 の三ペイン。設定などの画面でもサイドバーは残る • スマホ: 単カラム+下部タブ、左右スワイプで既読・スター • 記事は自動で本文を取得し、Gemini で要点3行とタグを付ける • 本文は画像・動画・リンクごと出す(消毒済みHTML) • 「NotebookLM へ」で複数記事を1つの Markdown にまとめて渡す。Google Drive にも置ける • 毎朝のダイジェストを自動生成し、できたら Web Push で知らせる • アプリ内だけで音声とスライドも作れる(/listen・/watch)。 2人の対話か1人の語りを選べ、MP3 で手元に落とせるRead the full README ↗
AI要約つきの個人用RSSリーダー。大量の記事を素早く捌き、深掘りしたい記事は NotebookLM に投げてすぐ音声にできる状態まで用意する。
/listen・/watch)。
2人の対話か1人の語りを選べ、MP3 で手元に落とせる| 用途 | 無料枠 | |
|---|---|---|
| Supabase | DB / 認証 / スケジューラ | あり |
| Vercel | ホスティング | あり(Hobby) |
| Gemini API | 要約 | あり |
docs/status.mddocs/owner-todo.mddocs/quotas.mddocs/site-compat.mddocs/tts-options.md.env.local に SUPABASE_DB_URL(ダッシュボード上部の
「Connect」→ Session pooler の URI)と SUPABASE_DB_PASSWORD を入れて
npm run db:migrate。パスワードは記号もそのままでよく、URL 側は
[YOUR-PASSWORD] のままで構わない(スクリプトがエンコードして埋める)。
ダッシュボードの SQL Editor に手で貼ってもよい(supabase/migrations/ を番号順に)OWNER_USER_ID.env.example を .env.local にコピーして埋める。
CRON_SECRET は適当な長いランダム文字列でよい(openssl rand -hex 32 など)。
キーの名前は Supabase ダッシュボードの Project Settings > API Keys の表記に合わせている。
| ダッシュボードの表記 | 環境変数 | 旧称 |
|---|---|---|
Publishable key(sb_publishable_…) | NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | anon key |
Secret key(sb_secret_…) | SUPABASE_SECRET_KEY | service_role key |
.env ではなく .env.local に書くこと。両方あると .env.local が優先されるので、
片方を直したつもりでもう片方が効き続ける事故が起きる。
npm install
npm run dev
http://localhost:3000 → ログイン → 設定画面から OPML を取り込むかフィードを追加。
開発中は手で叩ける:
curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/poll
curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/jobs/run
毎朝ダイジェストも同じように叩ける。dry=1 は何も書かずに選抜結果だけ返すので、
「今日の8件」がどう選ばれるかを確かめてから本番で作れる:
curl -H "Authorization: Bearer $CRON_SECRET" 'http://localhost:3000/api/cron/digest?dry=1'
curl -H "Authorization: Bearer $CRON_SECRET" 'http://localhost:3000/api/cron/digest?force=1'
force=1 は生成時刻を待たずに作るが、その日のぶんが既にあれば作らない。
書き出した記事には exported_at が付いて選抜から外れるため、作り直しのつもりで
叩くと中身の違う2本目ができてしまう。本当に作り直すときは digests の当日行を
消してから叩くこと。
Vercel にデプロイして URL が決まったら、supabase/scheduler.sql の
__APP_URL__ と __CRON_SECRET__ を置き換えて Supabase の SQL Editor で流す。
Vercel Hobby の cron は1日1回までなので、1時間毎の巡回と5分毎のワーカーは
Supabase の pg_cron 側に持たせている。毎朝ダイジェストも同様で、Vercel Cron は
実行時刻が±59分ずれる(6時のダイジェストが7時前にできる)ため pg_cron に寄せた。
Supabase を東京以外に作ったときは vercel.json の regions も合わせること。
既定のままだと関数が米国東部で動き、クエリのたびに太平洋を往復する(後述)。
設定画面の入力欄は、次のどれを入れても動く。フィードのURLを知っている必要はない。
| 入れるもの | 動き |
|---|---|
| サイト名(「ナゾロジー」など) | Feedly の検索で探す |
| サイトのURL | ページの <link rel="alternate"> を見る。無ければ /feed などを当たる |
| フィードのURL | そのまま使う |
URL に見えるかどうかで振り分ける(空白があれば名前、example.com の形ならURL)。
URL として辿れなかった場合も、最後に名前として検索し直す。
見つかったものは登録する前に中身(タイトル・最新3件・購読者数)を出す。 登録した瞬間に記事も取り込むので、次の巡回を待たなくていい。
名前で探すときは打った語が Feedly に送られる(URL を貼った場合はそのサイトを 直接見に行くだけ)。検索の索引には死んだフィードが3件に1件ほど混じるので、 上位を並行で読みに行って生きているものだけを出す。応答が遅いものは 「死んでいる」と区別して、中身なしのまま候補に残す(捨てると拾えなくなるため)。
フィードの死に方は2通りあって、見え方が違う。
| 型 | 症状 | 見つけ方 |
|---|---|---|
| 取得に失敗する | 404・DNS切れ・タイムアウト | error_count が積み上がる |
| 更新が止まる | 取得は 200 で成功し続ける | feeds.last_article_at を見るしかない |
後者が厄介で、失敗回数は 0 のままなので一覧を眺めても分からない (実データでも MDN Blog が60日新着なしだった)。そのために 「最後に新しい記事が入った時刻」をフィードに持たせてある(0013、取り込み時に更新)。
フィードのURLは変わる。ドメイン移転や CMS の入れ替えで配信先が動く。
巡回はリダイレクトを自分で追って、恒久的なもの(301 / 308)だけ覚え直す。
redirect: 'follow' に任せると読めはするが移転先を覚えないので、古いURLを叩き続け、
それが消えた日に「取得に失敗するフィード」として死ぬ。移転していただけなのに。
一時的なもの(302 / 307)は追うだけで覚えない(相手が一時的と言っているものを
固定すると、戻ったときに追随できない)。
実際に NHK が www.nhk.or.jp から news.web.nhk へ移転していて、この仕組みで
自動的に付け替わった。
実際にはこちらのほうが多い。CMS を入れ替えて古いパスがいきなり 404 になる、 フィードのパスだけ変わる、旧パスが 200 を返すが中身が HTML になっている—— どれもリダイレクトが無いので追えない。
残っている手がかりはサイトのURLだけなので、巡回が自動でそこから探し直す。 3回続けて失敗した時点で1回試し、その後は10回ごと(相手が直ることもあるので 諦めきらない程度に)。1〜2回の失敗では動かない——落ちているだけの相手に 余計なリクエストを重ねないため。
設定画面の「気になるフィード」からは**手で「探し直す」**こともできる。 中身は自動のものと同じ。
どちらの経路でも feeds.url を差し替えるだけなので、購読・フォルダ・既読・スターは
フィードの id に紐づいていて何も失わない(購読をやめて登録し直すと全部消える)。
ドメインごと消えていれば探しようが無いので、「壊れている」として設定画面に出る。
対策は3か所に入れてある:
判定は lib/feeds/health.ts の純粋な関数(閾値: 失敗3回で不調、10回で壊れている、
新着60日で更新なし。登録から14日は様子を見る)。数回の失敗で騒がないのは、
相手の一時的な不調が珍しくないため。
止まったフィードを放置しても実害は小さい(巡回のたびに条件付きGETが1回走るだけ)。 消すかどうかは気になったときに決めればいい。
一覧が「これから読むもの」を捌く場所なのに対して、/library は「前に読んだあれ」を
掘り返す場所。既読も込みで全部を対象に、タグ・スター・書き出し済み・期間で絞れる。
絞り込みは全部 URL に載るので、よく使う組み合わせはブックマークできる。
検索は既定でタイトルだけを見る。「本文も探す」を入れると本文まで見る。 どちらも PGroonga の索引が効く(0015)。
もともと日本語の検索は弱かった。simple 辞書の tsvector は空白で区切るだけなので
日本語の文が丸ごと1語になり、0001 で索引を張ったまま一度も使えていない。
本文の ilike は索引が無く順スキャンだった。PGroonga は日本語の索引を張れるので、
「不整脈」のように文の途中にある語も索引で引ける。
索引を張るだけで既存の ilike が 80.8ms → 9.9ms(8.2倍)になった(記事843件で実測)。
クエリの書き換えは要らない。PGroonga は ilike も肩代わりするため。
さらに速くしたいときは PGroonga の &@~ 演算子を使うと 0.39ms(さらに25倍)になるが、
PostgREST から演算子を呼べないので RPC を立てて絞り込みを SQL に書き直す必要がある。
クエリ経路が二重になるので、検索が体感で遅くなってから考えればいい。
articles_title_trgm_idx と articles_fts_idx は PGroonga が肩代わりするので落とした
(プランナが選ばなくなり、外しても速度は同じだった)。articles の総容量は 6.5MB → 4.5MB。
スマホのホーム画面に追加すると、URLバー無しの単独アプリとして起動する。
マニフェストは src/app/manifest.ts、サービスワーカーは public/sw.js。
サービスワーカーはほとんど何もキャッシュしない。記事一覧も本文もログイン済みの
動的なページで、既読・スターの状態が刻々と変わるので、HTML や API の応答を握ると
「昨日の未読一覧」を見せる事故になる。やるのは圏外での画面遷移に offline.html を
出すことと、Web Push の受け口だけ。開発中は登録しない(Turbopack の差し替えと
噛み合わず、直したものが出てこない原因になる)。
アイコンは画像ライブラリを足さずに生成している:
npm run icons # public/icon-*.png を作り直す
This is the beginning of the README. Read the rest at the original. Read the full README ↗