0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

無料画像ギャラリー構築メモ(GitHub Actions + Cloudflare Pages + 自動キャプションリネーム)

Last updated at Posted at 2025-09-08

無料でギャラリーホスティングしたいなと思って。
GPTさんに相談したところ、タイトルの方法が良いのではと打診を受け。
実装途中で透かし(WaterMark)を動的に入れたいと思って追加リクエストをして、実装しました。
初版ではBLIP部分が英単語だったため、タイトルとして不適当と考え、日本語化(翻訳)するように変換をかけています。(ファイル名の観点では英語が良いと思いますが、今回は日本語も許されるので。)

概要:ローカルで画像生成AIを逆利用して画像をOCRで自動キャプション→安全な英数字ファイル名へリネームし、GitHub Actionsでindex.jsonを自動生成、Cloudflare Pagesで配信します。
効果:検索性・並び順の安定、URLの汚染防止、作品タイトルの自動化。

  1. アーキテクチャ(図解)

[Dev PC] --(auto-caption rename)--> /public/images/*
--push--> [GitHub Repo]
--CI--> public/index.json を生成
--> [Cloudflare Pages] で配信
--> Browser が index.json を fetch してグリッド表示
2. 自動キャプション・リネーム(ローカル運用)
画像の内容を短い説明語に要約し、半角英数字+ハイフンの安全なファイル名に自動変換します。EXIF日時(なければ更新日時)を先頭に付けることで、時系列にも強い命名にします。

スクリプト(CPUでOK):rename_by_content.py

使い方

1) 依存をインストール

pip install -U transformers pillow tqdm unidecode

2) ドライラン(計画だけ表示)

python rename_by_content.py "public/images"

3) 実行(リネーム適用)

python rename_by_content.py "public/images" --apply --date-prefix
モデルは Salesforce/blip-image-captioning-base(初回のみ自動DL)。
長いキャプションは3〜8語程度に圧縮→slug化(英数字/hyphenのみ)。
重複名は -1, -2… を自動付加して衝突回避。
Git連携の例(任意/安全運用)

# .git/hooks/pre-commit (実行権限を付与: chmod +x .git/hooks/pre-commit)
#!/bin/sh
python3 tools/rename_by_content.py ./public/images --date-prefix
git add public/images

イラスト/アニメ調が中心の場合(代替):自動タグ付け(WD14/DeepDanbooru)で上位タグを連結して命名すると安定します。
スクショ/書類中心の場合:OCR(Tesseract+pytesseract)で見出し行を抽出→命名が有効。

  1. リポジトリ構成(例)
    repo-root/
    ├─ public/
    │ ├─ index.html
    │ ├─ index.json ← Actionsで自動生成
    │ └─ images/
    │ ├─ 20250907_vampire-throne.png
    │ └─ 20250906_azure-solitude.jpg
    └─ .github/
    └─ workflows/
    └─ build-images-index.yml
  2. GitHub Actions(index.json 自動生成)
    /.github/workflows/build-images-index.yml
    ※ 1イベント内で paths と paths-ignore を同時指定しないこと。
build-images-index.yml
name: Build images index
on:
  push:
    paths:
      - 'public/images/**'
  workflow_dispatch:

permissions:
  contents: write

jobs:
  build-index:
    runs-on: ubuntu-latest
    if: ${{ github.actor != 'github-actions[bot]' }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Generate public/index.json
        run: |
          node -e "
          const fs = require('fs');
          const path = require('path');
          const root = process.cwd();
          const imagesDir = path.join(root, 'public', 'images');
          const outPath   = path.join(root, 'public', 'index.json');
          const exts = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
          const list = [];
          function walk(dir) {
            for (const name of fs.readdirSync(dir)) {
              const p = path.join(dir, name);
              const stat = fs.statSync(p);
              if (stat.isDirectory()) walk(p);
              else {
                const ext = path.extname(p).toLowerCase();
                if (exts.has(ext)) {
                  const rel = path.relative(path.join(root, 'public'), p).replace(/\\\\/g, '/');
                  const title = path.basename(p, ext);
                  list.push({ src: rel, title });
                }
              }
            }
          }
          if (!fs.existsSync(imagesDir)) { console.error('images dir not found:', imagesDir); process.exit(1); }
          walk(imagesDir);
          list.sort((a, b) => a.src.localeCompare(b.src));
          fs.writeFileSync(outPath, JSON.stringify(list, null, 2));
          console.log('Wrote', outPath, list.length, 'items');
          "
      - name: Commit & Push index.json
        run: |
          if [ -n "$(git status --porcelain)" ]; then
            git config user.name  'github-actions[bot]'
            git config user.email 'github-actions[bot]@users.noreply.github.com'
            git add public/index.json
            git commit -m 'chore: update images index'
            git push
          else
            echo 'No changes to commit'
          fi
  1. フロント(最小実装)
    public/index.html(index.jsonをfetch→グリッド表示)
<!doctype html>
<meta charset="utf-8">
<title>Image Gallery</title>
<div id="g" style="display:grid; grid-template-columns:repeat(auto-fill, minmax(180px,1fr)); gap:12px;"></div>
<script>
  fetch('./index.json')
    .then(r => r.json())
    .then(list => {
      const g = document.getElementById('g');
      list.forEach(({src, title}) => {
        const wrap = document.createElement('div');
        wrap.style.border = '1px solid #ddd';
        wrap.style.padding = '8px';
        wrap.innerHTML =
          '<img loading="lazy" style="width:100%;height:auto;display:block;" src=\"'+ src +'\" alt=\"\">' +
          '<div style=\"font:12px/1.4 sans-serif; margin-top:6px; color:#555;\">'+ title +'</div>';
        g.appendChild(wrap);
      });
    })
    .catch(e => {
      document.getElementById('g').textContent = 'index.json が見つかりませんでした: ' + e;
    });
</script>
  1. 運用Tips(命名規則と品質)
    命名規則:YYYYMMDD_短い説明語.ext(半角英数字とハイフンのみ)。
    一貫性優先:あとで一括置換・集計しやすい。
    タグ駆動:アニメ調はWD14タグ上位5件を-連結すると検索性UP。
    テキスト系:OCRで見出し行→短縮→スラグ化が有効。
  2. トラブルシューティング
    画像が表示されない:index.htmlとindex.jsonの相対パス(./index.jsonか/index.json)を確認。拡張子の大文字小文字も厳密。
    Cloudflare Pages「Skipped」:コミットメッセージの [skip ci] など/差分なし/自動ビルド設定を確認。
    Actionsのイベント定義エラー:1イベントで paths と paths-ignore を同時指定しない。
    モデルDL不可:社内プロキシでブロックされる場合は、事前に開発端末でモデルを取得してキャッシュを配布。
    必要に応じて、自動サムネ生成・タグ別 index.json・OGP対応・作品ページ自動生成なども拡張できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?