スマホからURLを共有 → GitHub Issue作成(ラベル: pip)→ GitHub Actionsが即時処理 →
AI要約をMarkdownで articles/ に保存 → Slack通知 → Obsidianで閲覧。
コスト: $0(全て無料枠内で運用)
スマホ: iOSショートカット(URL共有)
→ GitHub Issue 作成(ラベル: pip, bodyにURL)
↓ issues:opened トリガー
GitHub Actions: 即時実行
├── ラベル "pip" チェック(なければスキップ)
├── Issue bodyからURL取得(環境変数経由で安全に抽出)
├── 重複URL検出(既存記事と同一URLならスキップ)
├── trafilatura で本文抽出
├── Gemini 2.5 Flash Lite で要約(REST API, JSON出力)
├── Markdown生成 → articles/ に commit & push
├── Slack通知(Incoming Webhook)
└── Issue close
Daily Cron:
└── articles/ で自動整理
90日経過 → 削除
↓
Obsidian Git同期 → 閲覧・検索
| レイヤー | 技術 | コスト |
|---|---|---|
| URL入力 | iOSショートカット → GitHub Issue API | $0 |
| ワークフロー | GitHub Actions (issues:opened + daily cron) |
$0(月2000分無料枠) |
| 本文抽出 | trafilatura(Python OSS) | $0 |
| AI要約 | Gemini 2.5 Flash Lite REST API(requestsで直接呼び出し) |
$0 |
| 記事保存 | リポジトリの articles/(Markdown) |
$0 |
| 通知 | Slack Incoming Webhook | $0 |
| 閲覧 | Obsidian + Git同期プラグイン | $0 |
Gemini呼び出しについて:
google-genai等のSDKは使わず、requestsライブラリで REST API を直接呼び出す。responseMimeType: application/json+responseSchemaでJSON出力を強制する。スキーマの型名は大文字(STRING,INTEGER,OBJECT,ARRAY)を使用する(REST API仕様)。
<your-repo>/
process_article.py # メインスクリプト(記事処理)
cleanup_articles.py # 自動整理スクリプト
pyproject.toml # Python依存管理
Inbox.md # Obsidian DataView: 過去7日間の記事一覧
Important.md # Obsidian DataView: 過去30日間の重要記事一覧(importance = 3)
terraform/ # GCPリソース管理(API有効化のみ)
main.tf
variables.tf
articles/ # 記事Markdown保存先
.github/workflows/
pip_process.yml # Issue trigger + workflow_dispatch: 記事処理
pip_cleanup.yml # Daily cron: 自動整理
| シークレット | 用途 | 登録先 |
|---|---|---|
GEMINI_API_KEY |
Gemini API 呼び出し | GitHub Secrets |
SLACK_WEBHOOK_URL |
Slack通知 | GitHub Secrets |
- ワークフロー: リポジトリへの commit & push はデフォルトの
GITHUB_TOKEN(contents: writepermission)で可能。追加PATは不要。 - iOSショートカット: リポジトリへのIssue作成用に Fine-grained PAT を1つ発行。
- Repository access: 対象リポジトリのみ
- Permissions: Issues (Read and write) のみ
-
GCPプロジェクト作成
gcloud projects create <your-project-id>
-
ADC認証
gcloud auth application-default login
TerraformではAPI有効化のみ管理する。APIキーはgcloudで作成する。
-
terraform/main.tf作成terraform { required_version = ">= 1.0" required_providers { google = { source = "hashicorp/google" version = "~> 6.0" } } } provider "google" { project = var.gcp_project_id } resource "google_project_service" "generative_language" { service = "generativelanguage.googleapis.com" disable_on_destroy = false } resource "google_project_service" "apikeys" { service = "apikeys.googleapis.com" disable_on_destroy = false }
-
terraform/variables.tf作成variable "gcp_project_id" { description = "GCPプロジェクトID" type = string }
-
.gitignoreに追加terraform/.terraform/ terraform/*.tfstate* terraform/terraform.tfvars -
Terraform 実行
cd terraform echo 'gcp_project_id = "<your-project-id>"' > terraform.tfvars terraform init terraform plan terraform apply
-
APIキー作成
gcloud services api-keys create \ --display-name="PIP - Gemini API" \ --api-target=service=generativelanguage.googleapis.com \ --project=<your-project-id>
-
APIキー取得
# 作成されたキーのUID(出力に含まれる)を指定 gcloud services api-keys get-key-string <KEY_UID> --project=<your-project-id>
-
GitHub Secrets に
GEMINI_API_KEYを登録
-
Slack Incoming Webhook 作成
- 通知用チャンネルを決めて作成
- GitHub Secrets の
SLACK_WEBHOOK_URLに登録
-
pipラベル作成- リポジトリに
pipラベルを追加
- リポジトリに
-
articles/.gitkeep作成
process_article.py— URL → 要約 → Markdown生成 → 保存 → 通知
-
pyproject.toml作成[project] name = "pip" version = "0.1.0" requires-python = ">=3.10" dependencies = [ "requests", "trafilatura", ]
-
uv lock実行
-
extract_content(url: str) -> dict実装- trafilatura で本文抽出
- 戻り値:
{ "title": str, "body": str, "source_url": str } - 抽出失敗時:
bodyを空文字にし、タイトルはURLのドメインで代替
-
Gemini プロンプト定義
- 自分の興味・関心に基づくペルソナを定義し、記事を多角的に評価
- ペルソナ例:
- ソフトウェアエンジニア: 使用言語、クラウド、AI/LLM、アーキテクチャ等
- 個人投資家: 株式、経済指標、マクロ経済等
- その他、自分の趣味・関心事
- JSON出力指定:
{ "title": "記事タイトル(日本語以外は日本語に翻訳)", "abstract": "100文字程度の要約", "importance": 3, "category": "tech", "insights": "パーソナライズされた洞察", "summary": "記事全体の詳細な要約", "tags": ["キーワード1", "キーワード2"] } - importance基準(1-3の3段階):
- 3: 必ず読むべき(直接的な業務影響、大きな機会、重要なトレンド)
- 2: 時間があれば読む(一般的に有用な情報、間接的に関連)
- 1: 自分との関連性が低い
- category: 自分の関心領域に合わせて定義(例:
tech,investment,hobby,other等)
-
summarize_article(title: str, body: str, url: str) -> dict実装- Gemini 2.5 Flash Lite REST API を
requestsで直接呼び出し - エンドポイント:
https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent system_instructionでシステムプロンプト、generationConfigでresponseMimeType+responseSchemaを指定- JSONレスポンスのパース(失敗時はデフォルト値でフォールバック)
- 本文が長すぎる場合は200,000文字で切り詰め
- importanceは
max(1, min(3, ...))でバリデーション
- Gemini 2.5 Flash Lite REST API を
-
generate_markdown(article: dict, content: str) -> str実装- テンプレート:
--- url: {url} title: "{title}" importance: {importance} category: {category} tags: [{tags}] created: {YYYY-MM-DDTHH:MM} --- ## Abstract {abstract} ## Insights {insights} ## Summary {summary} ## Original {取得テキスト(5000文字で切り詰め、超過時は「(以下省略)」を付与)}
- テンプレート:
-
ファイル名生成
- フォーマット:
YYYYMMDD_タイトル.md - サニタイズ: 特殊文字除去(
/,\,:,*,?,",<,>,|) - 長さ制限: タイトル部分150文字以内に切り詰め
- フォーマット:
-
is_duplicate(url: str) -> bool実装articles/配下の既存.mdファイルのfrontmatterurl:を走査- 同一URLが存在すれば
Trueを返す - 重複時はGemini API呼び出しも含めスキップ(コスト・処理時間の節約)
-
save_article(filename: str, content: str)実装articles/{filename}にファイル書き込みos.makedirs(exist_ok=True)でディレクトリ自動作成- git commit & push はワークフロー側で実行
-
notify_slack(article: dict)実装- Incoming Webhook で Block Kit 送信
- ヘッダー: カテゴリ別アイコン + タイトル
- 本文: 要約 + 重要度(★3段階表示)
- タグ: バッククォート付きで表示
- リンク: 元記事URLボタン
SLACK_WEBHOOK_URL未設定時はスキップ
-
main()実装- 環境変数
TARGET_URLからURL取得 urlparseでスキーム検証(http/https のみ許可)- 重複URL検出 → extract → summarize → generate_markdown → save → notify → GITHUB_OUTPUT出力 の順で実行
- GITHUB_OUTPUT: 記事タイトルを
article_titleとして出力(ワークフローのコミットメッセージ用)
- 環境変数
-
.github/workflows/pip_process.yml作成name: PIP Process on: issues: types: [opened, reopened] workflow_dispatch: inputs: url: description: '処理するURL' required: true type: string permissions: contents: write issues: write jobs: process: if: >- (github.event_name == 'workflow_dispatch') || (github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'pip')) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Extract URL id: extract env: ISSUE_BODY: ${{ github.event.issue.body || '' }} DISPATCH_URL: ${{ github.event.inputs.url || '' }} run: | if [ -n "$DISPATCH_URL" ]; then echo "url=$DISPATCH_URL" >> "$GITHUB_OUTPUT" else URL=$(echo "$ISSUE_BODY" | grep -oP 'https?://\S+' | head -1) echo "url=$URL" >> "$GITHUB_OUTPUT" fi - name: Validate URL run: | URL="${{ steps.extract.outputs.url }}" if [ -z "$URL" ]; then echo "ERROR: URLが取得できませんでした" exit 1 fi echo "処理対象URL: $URL" - name: Process article id: process env: TARGET_URL: ${{ steps.extract.outputs.url }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: uv run process_article.py - name: Commit and push article run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add articles/ if git diff --cached --quiet; then echo "変更なし" else TITLE="${{ steps.process.outputs.article_title }}" if [ -z "$TITLE" ]; then TITLE="new article" fi git commit -m "add: $TITLE" git push fi - name: Close issue if: github.event_name == 'issues' env: GH_TOKEN: ${{ github.token }} run: gh issue close ${{ github.event.issue.number }} --repo ${{ github.repository }}
ポイント:
- Issue bodyのURL抽出は環境変数経由(
${{ }}を直接shellに展開しない)でインジェクション対策 - スクリプト失敗時は Close issue ステップに到達しないため、Issueはopenのまま(reopenで再実行可能)
workflow_dispatchでURL直接指定の手動実行もサポート
- Issue bodyのURL抽出は環境変数経由(
-
cleanup_articles.py実装- frontmatter パース(簡易regex:
---で囲まれたYAML部分を抽出) createdフィールドを読み取り(YYYY-MM-DDTHH:MM形式)- 条件: 90日経過 → 削除(
os.remove) - 削除件数のログ出力
- frontmatter パース(簡易regex:
-
.github/workflows/pip_cleanup.yml作成name: PIP Cleanup on: schedule: - cron: '0 19 * * *' # 毎日 UTC 19:00(JST 04:00 等、自分のタイムゾーンに合わせて調整) workflow_dispatch: permissions: contents: write jobs: cleanup: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Run cleanup run: uv run cleanup_articles.py - name: Commit and push changes run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add articles/ if git diff --cached --quiet; then echo "変更なし" else git commit -m "chore: cleanup old articles" git push fi
-
Fine-grained PAT 発行
- Repository: 対象リポジトリのみ
- Permissions: Issues (Read and write) のみ
-
ショートカット作成
- 名前: 「PIPに保存」
- トリガー: Share Sheet(URL受付)
- 処理:
- 入力からURLを取得
- GitHub API呼び出し:
POST https://api.github.com/repos/<owner>/<repo>/issues Headers: Authorization: Bearer {PAT} Content-Type: application/json Body: { "title": "pip: {URL}", "body": "{URL}", "labels": ["pip"] } - 完了通知: 「PIPに送信しました」
-
動作テスト
- Safariの共有メニューからショートカット実行
- Issue作成 → Actions起動 → Slack通知の一連を確認
-
リポジトリをObsidian Vaultとして開く
-
Obsidian Gitプラグイン設定
- 自動Pull間隔: 5分
-
Dataview プラグイン設定
Inbox.md(過去7日間の記事一覧):TABLE importance AS "重要度", category AS "分類", tags AS "タグ" FROM "articles" WHERE date(created) >= date(now) - dur(7 days) SORT created DESCImportant.md(過去30日間の重要記事一覧):TABLE category AS "分類", tags AS "タグ" FROM "articles" WHERE importance = 3 AND date(created) >= date(now) - dur(30 days) SORT created DESC
-
動作確認
-
E2Eテスト: メインパイプライン
pipラベル付きIssueを手動作成- GitHub Actions が起動し、処理が完了すること
articles/に Markdownが保存されること- Slack通知が届くこと
- Issueがcloseされること
-
E2Eテスト: エラーケース
- 無効なURL → エラー通知、Issueはopenのまま
- trafilatura抽出失敗 → タイトル・URLのみで保存(フォールバック)
- Gemini APIエラー → デフォルト値で保存
pipラベルなしのIssue → スキップされること- 重複URL → スキップされること
-
E2Eテスト: 自動整理
- テスト用ファイル(created: 91日前)を手動作成
- workflow_dispatch で手動実行
- 90日経過した記事が削除されること
- 90日未満の記事は残ること
-
iOSショートカットからのE2Eテスト
-
Obsidian同期テスト
Phase 0 (事前準備) ← GCPプロジェクト + Terraform + gcloud + Secrets登録
↓
Phase 1 (スクリプト実装) ← コア処理。ローカルで動作確認可能
↓
Phase 2 (GitHub Actions) ← Issue → 自動処理が動くようになる
↓
Phase 3 (iOSショートカット) ← スマホから使えるようになる
↓
Phase 4 (Obsidian) ← 閲覧環境の整備
↓
Phase 5 (テスト) ← 全体の動作確認
| # | 作業 | 場所 |
|---|---|---|
| 1 | GCP認証(gcloud auth application-default login) |
ターミナル |
| 2 | GCPプロジェクト作成(gcloud projects create <your-project-id>) |
ターミナル |
| 3 | Slack Incoming Webhook 作成 → SLACK_WEBHOOK_URL 登録 |
Slack + GitHub Secrets |
| 4 | pip ラベル作成 |
リポジトリ Issues > Labels |
| 5 | Gemini APIキー作成(gcloud) → GEMINI_API_KEY 登録 |
ターミナル + GitHub Secrets |
| 6 | Fine-grained PAT 発行(Phase 3で使用) | GitHub Settings > Developer settings |
このプランは汎用テンプレートです。以下を自分の用途に合わせて調整してください。
| 項目 | 説明 |
|---|---|
| ペルソナ | システムプロンプトの評価ペルソナを自分の興味・職種に合わせて定義 |
| カテゴリ | category の選択肢を自分の関心領域に合わせて変更 |
| 重要度基準 | importance 1-3 の判定基準をペルソナに合わせて調整 |
| 自動整理期間 | 90日は一例。保存期間は好みで変更可能 |
| cron時刻 | タイムゾーンに合わせてcronスケジュールを調整 |
| 通知先 | Slack以外(Discord, LINE等)にも変更可能 |