Search

2026/08/18

Docker nginx+CertbotでLet's Encrypt証明書を複数ドメイン1本に統合し、自動更新する方法

Docker nginx+CertbotでLet's Encrypt証明書を複数ドメイン1本に統合し、自動更新する方法



1:要約

Dockerでnginxを運用しているVPSで、Let's Encryptの証明書が複数作成され、 どの証明書をnginxが利用しているのか分かりにくくなっていました。 また、Certbotによる証明書更新も自動化されておらず、証明書の期限切れが発生していました。

そこで今回は、複数のドメイン・サブドメインを 1つのLet's Encrypt証明書に統合し、 Docker版Certbotとsystemd timerを使って自動更新する構成に整理します。

この記事では例として、以下の4ドメインを利用します。

example.com
www1.example.com
www2.example.com
www3.example.com

最終的な構成は次のようになります。

Let's Encrypt
       ↑
       │ HTTP-01認証
       │
Docker nginx
       │
       ├─ /.well-known/acme-challenge/
       │
       ↓
/srv/web/certbot/www

Docker Certbot
       │
       ├─ 証明書管理名:example.com
       │
       └─ 4ドメインを1証明書で管理
              │
              ↓
/srv/web/certbot/conf

systemd timer
       │
       ↓
docker run certbot renew
       │
       ↓
更新成功時のみ nginx reload

ポイントは、Ubuntu本体にCertbotを追加インストールせず、 公式のDocker版Certbotに統一することです。 Certbotを二重管理しないため、後から見ても構成が分かりやすくなります。

3:内容

3-1.今回の構成

nginxとCertbotはDockerで動かし、証明書データだけをVPS上の永続ディレクトリに保存します。

/srv/web/certbot/
├── conf/
│   ├── live/
│   ├── archive/
│   └── renewal/
│
└── www/
    └── .well-known/
        └── acme-challenge/

役割は次の通りです。

  • /srv/web/certbot/conf:証明書、秘密鍵、Certbotの更新設定
  • /srv/web/certbot/www:Let's EncryptのHTTP-01認証で使用

nginxコンテナとCertbotコンテナから同じディレクトリを見ることで、 nginxを停止せずに証明書を更新できます。

3-2.Let's Encrypt証明書を複数ドメインで1本にする

Let's Encryptでは、1枚の証明書に複数のDNS名を含めることができます。

今回は次の4ドメインを1枚にします。

example.com
www1.example.com
www2.example.com
www3.example.com

Certbot上の管理名も、分かりやすく example.com に統一します。

最終的には次のようになります。

Certificate Name: example.com

Identifiers:
  example.com
  www1.example.com
  www2.example.com
  www3.example.com

サブドメインごとに証明書を作る必要がなければ、 このようにまとめた方が管理対象を減らせます。

3-3.nginxとCertbotのディレクトリ共有

nginxには、例えば次のようにマウントします。

- /srv/web/certbot/conf:/etc/letsencrypt
- /srv/web/certbot/www:/var/www/certbot

実際のマウント状態は次のコマンドで確認できます。

docker inspect nginx \
  --format '{{range .Mounts}}{{println .Source " -> " .Destination}}{{end}}'

例えば次のようになっていればOKです。

/srv/web/certbot/www  -> /var/www/certbot
/srv/web/certbot/conf -> /etc/letsencrypt

3-4.ACME challenge用のnginx設定

Let's EncryptのHTTP-01認証では、 HTTPの80番ポートから特定のURLへアクセスできる必要があります。

server {
    listen 80;
    listen [::]:80;

    server_name
        example.com
        www1.example.com
        www2.example.com
        www3.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/certbot;
        try_files $uri =404;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

通常のHTTPアクセスはHTTPSへリダイレクトしますが、 /.well-known/acme-challenge/ だけはCertbot用にHTTPでアクセス可能にします。

3-5.ACME challengeの疎通確認

証明書発行前に、テストファイルを作成します。

sudo mkdir -p /srv/web/certbot/www/.well-known/acme-challenge

echo 'certbot-test' | sudo tee \
  /srv/web/certbot/www/.well-known/acme-challenge/test.txt

各ドメインから確認します。

curl http://example.com/.well-known/acme-challenge/test.txt
curl http://www1.example.com/.well-known/acme-challenge/test.txt
curl http://www2.example.com/.well-known/acme-challenge/test.txt
curl http://www3.example.com/.well-known/acme-challenge/test.txt

すべてで次の文字列が返れば正常です。

certbot-test

この確認を先に行っておくと、Certbot実行時の認証失敗をかなり減らせます。

3-6.4ドメイン入り証明書を発行する

公式Certbot Dockerイメージを使います。

docker run --rm \
  -v /srv/web/certbot/conf:/etc/letsencrypt \
  -v /srv/web/certbot/www:/var/www/certbot \
  certbot/certbot certonly \
  --webroot \
  -w /var/www/certbot \
  --cert-name example.com \
  -d example.com \
  -d www1.example.com \
  -d www2.example.com \
  -d www3.example.com

成功すると、証明書は次の場所に作成されます。

/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem

実ファイルはVPS側の /srv/web/certbot/conf に保存されているため、Certbotコンテナを削除しても証明書は残ります。

証明書一覧は次のコマンドで確認できます。

docker run --rm \
  -v /srv/web/certbot/conf:/etc/letsencrypt \
  certbot/certbot certificates

3-7.nginxを新しい証明書へ切り替える

4つのHTTPS serverで同じ証明書を指定します。

ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

nginx設定を確認します。

docker exec nginx nginx -t

正常ならreloadします。

docker exec nginx nginx -s reload

実際に読み込まれている証明書設定も確認できます。

docker exec nginx nginx -T 2>/dev/null \
  | grep -E 'server_name|ssl_certificate'

HTTPS通信も確認します。

curl -I https://example.com
curl -I https://www1.example.com
curl -I https://www2.example.com
curl -I https://www3.example.com

HTTPステータスが200以外でも、アプリケーション側が400や302を返している場合があります。 重要なのはSSL証明書エラーが発生していないことです。

3-8.古い証明書を整理する

過去に複数の証明書を作っている場合、 livearchiverenewal に古い設定が残っていることがあります。

まず一覧を確認します。

docker run --rm \
  -v /srv/web/certbot/conf:/etc/letsencrypt \
  certbot/certbot certificates

nginxが古い証明書を参照していないことを確認してから、 Certbot管理下の不要な証明書を削除します。

docker run --rm \
  -v /srv/web/certbot/conf:/etc/letsencrypt \
  certbot/certbot delete \
  --cert-name 古い証明書名 \
  --non-interactive

/etc/letsencrypt/live配下を単純にrmするのは避けます。

Certbotは livearchiverenewal を連動して管理しているためです。

ただし、過去の作業で live が消えているのに archiverenewal/*.conf だけが残った壊れた設定では、手動整理が必要になる場合があります。

作業前にはバックアップを取っておくと安全です。

sudo tar -czf /root/letsencrypt-backup.tar.gz \
  /srv/web/certbot/conf

3-9.Certbotの自動更新テスト

自動化する前に、実際には証明書を更新しない --dry-run を実行します。

docker run --rm \
  -v /srv/web/certbot/conf:/etc/letsencrypt \
  -v /srv/web/certbot/www:/var/www/certbot \
  certbot/certbot renew \
  --dry-run

正常なら次のようなメッセージが表示されます。

Congratulations, all simulated renewals succeeded

ここまで成功してから自動実行を設定するのが安全です。

3-10.systemd timerでCertbotを自動実行する

Docker版Certbotは、コンテナ自身がcronのように常駐して定期実行する構成ではありません。

そこでUbuntu側のsystemd timerを使って、 定期的にDocker版Certbotを起動します。

まずserviceを作ります。

sudo tee /etc/systemd/system/certbot-docker-renew.service > /dev/null <<'EOF'
[Unit]
Description=Renew Let's Encrypt certificates with Docker Certbot
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RuntimeDirectory=certbot-renew

ExecStartPre=/usr/bin/rm -f /run/certbot-renew/renewed

ExecStart=/usr/bin/docker run --rm \
  -v /srv/web/certbot/conf:/etc/letsencrypt \
  -v /srv/web/certbot/www:/var/www/certbot \
  -v /run/certbot-renew:/run/certbot-renew \
  certbot/certbot renew \
  --deploy-hook "touch /run/certbot-renew/renewed"

ExecStartPost=/bin/sh -c 'if [ -f /run/certbot-renew/renewed ]; then echo "Certificate renewed - reloading nginx"; /usr/bin/docker exec nginx nginx -s reload; else echo "No certificate renewal - nginx reload not required"; fi'
EOF

このserviceでは、証明書が実際に更新された場合だけ nginx -s reload を実行します。

次にtimerを作ります。

sudo tee /etc/systemd/system/certbot-docker-renew.timer > /dev/null <<'EOF'
[Unit]
Description=Run Docker Certbot renewal twice daily

[Timer]
OnCalendar=*-*-* 03,15:00:00
RandomizedDelaySec=30m
Persistent=true
Unit=certbot-docker-renew.service

[Install]
WantedBy=timers.target
EOF

systemdへ設定を読み込ませます。

sudo systemctl daemon-reload

timerを有効化して、その場で開始します。

sudo systemctl enable --now certbot-docker-renew.timer

この例では1日2回、3時台と15時台にCertbotが更新チェックを行います。

毎回証明書を発行するわけではありません。 certbot renew が更新期限を判断し、更新が必要な場合だけ証明書を更新します。

3-11.自動更新の状態を確認する

timerの状態を確認します。

systemctl status certbot-docker-renew.timer --no-pager

次回実行時刻はこちらです。

systemctl list-timers --all | grep certbot

有効化状態も確認できます。

systemctl is-enabled certbot-docker-renew.timer
systemctl is-active certbot-docker-renew.timer

正常なら次のようになります。

enabled
active

timerを待たずにserviceだけ手動実行することもできます。

sudo systemctl start certbot-docker-renew.service

実行ログを確認します。

sudo journalctl \
  -u certbot-docker-renew.service \
  -n 100 \
  --no-pager

証明書を取得したばかりの場合は更新されないため、 次のような内容になるのが正常です。

Certificate not yet due for renewal
No certificate renewal - nginx reload not required

3-12.省保守化のポイント

今回の構成で重要なのは、Certbotや証明書の管理方法をできるだけ増やさないことです。

最終的には次の状態を目指します。

証明書
└─ example.com
   ├─ example.com
   ├─ www1.example.com
   ├─ www2.example.com
   └─ www3.example.com

Certbot
└─ Docker版のみ

認証方式
└─ webroot

定期実行
└─ systemd timer

Webサーバー
└─ Docker nginx

Ubuntuへ別のCertbotをインストールすると、 「OS版Certbot」と「Docker版Certbot」の2系統が存在することになります。

特別な理由がなければ、今回のようにDocker版Certbotだけに統一した方が、 数か月後に設定を見直したときにも理解しやすくなります。

また、証明書をサブドメインごとに分割すると、 renewal設定、証明書ファイル、期限確認対象も増えます。

同じサーバーで利用していて、同じタイミングで更新して問題ないドメインなら、 1枚の証明書へまとめることで保守対象を減らせます。

最終チェック用コマンド

# 証明書一覧
docker run --rm \
  -v /srv/web/certbot/conf:/etc/letsencrypt \
  certbot/certbot certificates

# 更新シミュレーション
docker run --rm \
  -v /srv/web/certbot/conf:/etc/letsencrypt \
  -v /srv/web/certbot/www:/var/www/certbot \
  certbot/certbot renew --dry-run

# nginxが使用している証明書
docker exec nginx nginx -T 2>/dev/null \
  | grep -E 'server_name|ssl_certificate'

# timer確認
systemctl list-timers --all | grep certbot

# 更新ログ
sudo journalctl \
  -u certbot-docker-renew.service \
  -n 100 \
  --no-pager

この構成にしておけば、通常の運用ではCertbotを手動で操作する必要はほとんどなく、 systemd timerが定期チェックし、必要なタイミングで証明書を更新できます。

2026/08/17

dockerとは起動失敗なく仮想環境を複数で作って、それぞれ繋げて便利に使うもの

docker使っているけど簡単に説明すると何?
起動するときに余計なこと考えずに失敗しなくて、
それぞれの仮想環境を複数で作って、
それぞれ繋げて便利に使うもの
みたいな感じ。(てきとう)

その分、必要なファイル容量は大きくなるし、
管理することも増えるよ
みたいなことも。(当てずっぽう)

調べたりAIにまとめてもらったものの備忘録です。


DockerでLaravelを動かす仕組み――nginx・PHP・PostgreSQL・Portainer・WUDの関係

Ubuntu VPS上でLaravelのWebアプリ、データベース、Docker管理画面、更新確認ツールを動かす構成を初心者向けに整理します。Laravelは通常のブラウザから利用でき、PortainerとWUDは管理者だけがブラウザで開ける構成です。

1. 全体構成

インターネット上のブラウザ
        │ HTTP / HTTPS
        ▼
Ubuntu VPS
└─ Docker Engine
   ├─ nginxコンテナ ─────── 公開窓口
   │        │ FastCGI
   │        ▼
   ├─ PHP-FPMコンテナ ──── Laravelを実行
   │        │
   │        ▼
   ├─ PostgreSQLコンテナ ─ データを保存
   ├─ Portainerコンテナ ── Docker管理画面
   └─ WUDコンテナ ─────── イメージ更新を確認

Docker Engineは1つだけです。nginx、PHP-FPM、PostgreSQL、Portainer、WUDは、同じDocker Engine上で動く別々のコンテナです。LaravelはPHP-FPMコンテナ内で実行されるアプリケーションです。

2. 各コンテナの役割

要素役割外部公開
nginxブラウザからのアクセスを受け、PHP処理をPHP-FPMへ渡す80・443番
PHP-FPMPHPを実行し、Laravelの処理結果をnginxへ返す公開しない
Laravel画面表示、ログイン、検索、データ処理などWebアプリ本体nginx経由
PostgreSQLLaravelが使うデータベース公開しない
Portainer CEDockerをGUIで確認・管理するlocalhostのみ
WUDDockerイメージの更新を検出するlocalhostのみ

3. ブラウザからアクセスする流れ

  1. ブラウザでLaravel用ドメインを開く
  2. VPSの80番または443番へ接続する
  3. nginxがリクエストを受ける
  4. PHP処理をPHP-FPMへ渡す
  5. Laravelが必要に応じてPostgreSQLへ接続する
  6. 処理結果がnginx経由でブラウザへ戻る
https://example.com
        ↓
VPSの443番
        ↓
nginx
        ↓
php:9000
        ↓
Laravel
        ↓
postgres:5432
ブラウザアクセスは可能です。 外部へ公開するのはnginxの80・443番だけです。PHPの9000番とPostgreSQLの5432番はDocker内部だけで利用します。

4. コンテナ同士の通信

同じDockerネットワークに接続したコンテナは、Composeのサービス名で通信できます。

  • nginxからPHPへ:php:9000
  • LaravelからPostgreSQLへ:postgres:5432

LaravelのDB_HOSTにはpostgresを指定します。PHPコンテナ内のlocalhostはPHPコンテナ自身を意味するためです。

5. 推奨ディレクトリ構成

/srv/
├─ laravel-app/
│  ├─ compose.yml
│  ├─ .env
│  ├─ docker/
│  │  ├─ nginx/default.conf
│  │  └─ php/Dockerfile
│  └─ src/
│     ├─ artisan
│     ├─ app/
│     ├─ public/
│     └─ storage/
└─ docker-management/
   └─ compose.yml

Laravel側と管理ツール側を別のComposeプロジェクトに分けます。それぞれでdocker compose up -dを実行すると、同じDocker Engine上へ追加されます。

6. Laravel側のCompose例

/srv/laravel-app/compose.ymlの基本例です。

name: laravel-app

services:
  nginx:
    image: nginx:alpine
    container_name: laravel-nginx
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./src:/var/www/html:ro
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - php
    networks:
      - app-network

  php:
    build:
      context: ./docker/php
    container_name: laravel-php
    restart: unless-stopped
    working_dir: /var/www/html
    volumes:
      - ./src:/var/www/html
    environment:
      APP_ENV: production
      DB_CONNECTION: pgsql
      DB_HOST: postgres
      DB_PORT: 5432
      DB_DATABASE: laravel
      DB_USERNAME: laravel
      DB_PASSWORD: ${DB_PASSWORD}
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - app-network

  postgres:
    image: postgres:16-alpine
    container_name: laravel-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: laravel
      POSTGRES_USER: laravel
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U laravel -d laravel"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

volumes:
  postgres_data:

networks:
  app-network:

PostgreSQLにはportsを書いていないため、VPS外部から5432番へ直接接続できません。Composeと同じディレクトリの.envには、DB_PASSWORD=長いランダムなパスワードを設定します。

7. Portainer・WUD側のCompose例

/srv/docker-management/compose.ymlの例です。

name: docker-management

services:
  portainer:
    image: portainer/portainer-ce:2.39.6
    container_name: portainer
    restart: unless-stopped
    ports:
      - "127.0.0.1:9443:9443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data

  wud:
    image: getwud/wud:8.3.1
    container_name: wud
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      TZ: Asia/Tokyo
      WUD_WATCHER_LOCAL_CRON: "0 6 * * *"
      WUD_WATCHER_LOCAL_WATCHBYDEFAULT: "true"
      WUD_WATCHER_LOCAL_WATCHEVENTS: "false"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - wud_data:/store

volumes:
  portainer_data:
  wud_data:

PortainerとWUDはVPSの127.0.0.1だけに公開します。インターネットから直接は開けません。

8. nginx設定例

/srv/laravel-app/docker/nginx/default.confの基本例です。

server {
    listen 80;
    server_name example.com;
    root /var/www/html/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass php:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~ /\.ht {
        deny all;
    }
}

fastcgi_pass php:9000;により、PHP処理をphpサービスへ渡します。

これはHTTPの基本例です。本番公開ではドメインを設定し、Let's EncryptなどでHTTPS化してください。

9. 起動方法

Laravel側

cd /srv/laravel-app
docker compose config
docker compose build
docker compose up -d
docker compose ps

管理ツール側

cd /srv/docker-management
docker compose config
docker compose pull
docker compose up -d
docker compose ps

全体は次で確認できます。

docker ps
docker compose ls

10. 管理画面へのブラウザアクセス

WindowsのPowerShellからSSHトンネルを開きます。

ssh -L 9443:127.0.0.1:9443 -L 3000:127.0.0.1:3000 ユーザー名@VPSのIPアドレス

SSH接続を開いたまま、ブラウザで次を開きます。

  • Laravel:https://取得したドメイン
  • Portainer:https://localhost:9443
  • WUD:http://localhost:3000

Laravelは一般利用者向けに公開し、PortainerとWUDはSSH接続できる管理者だけが利用します。

11. 更新とデータ管理

対象管理方法更新方法
UbuntuAPTセキュリティ更新は自動、通常更新は手動
Docker EngineAPTメンテナンス時間に手動
nginx・PHP・PostgreSQLWUDで検出Composeから手動
LaravelGitなどコード配備、Composer、マイグレーション
コンテナの状態Portainerログと稼働状態を確認
cd /srv/laravel-app
docker compose pull
docker compose build --pull php
docker compose up -d
docker compose ps
docker compose logs --tail 100

PostgreSQLデータはpostgres_dataボリューム、Portainer設定はportainer_data、WUD履歴はwud_dataに保存されます。

PostgreSQLの16から17のようなメジャーバージョン変更は単純なイメージ更新ではありません。事前バックアップとデータ移行が必要です。また、docker compose down -vはボリュームを削除するため、通常は使いません。

主な確認コマンド

docker ps
docker compose ls
docker system df
df -h /

cd /srv/laravel-app
docker compose logs --tail 100 nginx
docker compose logs --tail 100 php
docker compose logs --tail 100 postgres
docker exec laravel-nginx nginx -t
docker compose exec php php artisan about

12. まとめ

  • nginxがブラウザからのアクセスを受ける
  • PHP-FPMがLaravelを実行する
  • LaravelがPostgreSQLへデータを保存する
  • PortainerがDocker全体をGUIで見えるようにする
  • WUDがコンテナイメージの更新を確認する

Laravelはドメインから通常のブラウザで利用できます。PortainerとWUDはlocalhostだけに公開し、SSHトンネル経由でブラウザ表示します。これにより、Webアプリの公開とDocker管理画面の保護を両立できます。

Dockerでは、稼働中のコンテナを直接作り込むより、Composeファイル、Dockerfile、nginx設定、Laravelソース、環境変数を設計図として保ち、同じ構成を再現できる状態にすることが重要です。

2026/06/25

USD12払えば無料枠が1000リクエスト/日で案外使えるOpenRouterのレビュー

AIのことなのに文章にAI使っていないので、読みづらかったらすみません。
APIでのAIツールOpenRouterのご紹介&無料枠のレビュー。

OpenRouterには無料で使えるものあります。
無課金だと50リクエスト/日であまり使えないですが、
USD10以上のクレジットを買うと、1,000リクエスト/日まで増えます。
結構使えます。
VsCodeの拡張機能CLINEを使って数日間いろいろやってみた感想を書いていきます。

結論
基本的には制限が厳しいので全部作って系のバイブコーディングには使えません。
しかし、それなりに使えるので、CodexやClaude Codeの制限を抑えるための補助ツールとしていいかも、です。
メインにするには制限が強すぎて、途中で止まるのが多い。
案外使えるので、少人数で1時間に10回のリクエストをAPIで使うとか、
OCRするとか、文章要約には便利。
アプリへの組み込みも使えるな、と思います。

問題点
1)制限かかってすぐに停止する。対処必須。
2)利用規約に一部のデータ保存とあるので情報流出の危険があるのかも。

使えそうなモデル
下記ですが2026/06/25現在のもので、入替えがあります。
cohere/north-mini-code:free                       
nvidia/nemotron-3.5-content-safety:free           
nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free
nvidia/nemotron-3-nano-30b-a3b:free               
poolside/laguna-xs.2:free                         
poolside/laguna-m.1:free                          
google/gemma-4-31b-it:free                        
nvidia/nemotron-3-super-120b-a12b:free            
liquid/lfm-2.5-1.2b-instruct:free                 
openai/gpt-oss-120b:free                          
openai/gpt-oss-20b:free                           

おすすめモデル
openai/gpt-oss-20b:free、google/gemma-4-31b-it:freeが標準的で安定した出力で使えます。
cohere/north-mini-code:freeがコーディングではいいかもな感触です。

といっても・・・
Groqの無料枠を使ってしまいます。
無課金でもOKだし、FreeTier枠が案外大きい。
ほかサービスと比較して速度がとっても速い!
(推測でしかありませんが、無課金の無料枠なのでデータ流出もある程度は許容しながらのものではないかと思います。)
Groqのレビューは下記↓
https://nokoshitamono.blogspot.com/2026/02/whisperllm0ai-apigroq-cloud20262.html


最後の感想
AIは従量課金が拡大していくサービスですが、便利なので安く使いたい。
そういう用途ならOpenRouterも一つの選択肢だと思います。
ただし、無料なのでいつ終了するのか分かりません。
でも、何かのサービスでこういう課金+無料枠のサービスは続いていくのではと思います。
そう入ってもコーディングではClaudeが圧倒して強いな、と思います。


勝手におすすめの本
必須ではないですが、ゲーム開発するならこの本読むと深い理解に繋がる。
そして、ホイジンガのホモ・ルーデンスも読みたくなる。
(ホモ・ルーデンスまで読まなくてもいいと思う。
時間は逆ですが、カイヨワを先にがわかりやすい)






[

2026/03/12

Xiaomi POCOの日本語入力が激遅!?設定1つでスマホが爆速になった話【 POCO F7 HyperOS 3.0.6.0 】2026/03/12版

Xiaomi POCOの日本語入力が激遅!?設定1つでスマホが爆速になった話【 POCO F7 HyperOS 3.0.6.0 】2026/03/12版

Xiaomi POCO F7 12GB+256GBを買った。

楽天市場のセールで買った。

使っていて問題なし。

しかし、最初からずっと感じていたのが「日本語キーボードの表示が遅い」です。

ぬるっとしたから上がってくるのがだいぶ遅い。

日本語キーボードが下から上がる動作の遅さ加減がひどい。

「スペックは高いはずなのに、なぜ?」と思い調べた。

Snapdragon 8s Gen 4なのに・・・。

AIと一緒に原因を探して突き止めました。

とある設定を1個変更するだけでかなりとても劇的に改善しました。

POCO F7使ってる人全員に知ってほしいので方法を紹介します。


ちなみに、使用感をお伝えすると、問題なく使えるAndroidスマホです。

不満なし。特徴なし。たいていのことは何でもできる。

カメラのコントラストが強めかも、くらい。

Xiaomi POCO X7 Pro 8GB+256GBでもいいかもと思う。

値段の同じくらいなモデルは大差なしと思います。


アマゾンの値段比較:2026/03/12現在

Xiaomi POCO F7 12GB+256GB : 5.5万

Xiaomi POCO X7 Pro 8GB+256GB :4.8万

Xiaomi POCO F7 Pro 12GB+512GB :7.3万

なんならredmiでもいいのかも。

Xiaomi REDMI Note Pro 15 5G 8+256GB :5.0万



遅い原因は「アニメーション」

POCO(Xiaomi / HyperOS)はUIアニメーションが強めに設定されています。

そのため

  • キーボード表示

  • アプリ起動

  • 画面遷移

などが 実際より遅く感じることがあります。

・・・というか実際に遅い。

遅すぎます。


爆速になった設定

結論から言うとアニメーションをOFFにする、です。

これだけでした。


設定方法

① 設定
② デバイス情報
HyperOSバージョンを7回タップ

すると「開発者モードが有効になりました」と表示されます。

次に

設定>追加設定>開発者オプション

以下開発者オプションのだいぶ下の方で下記をOFFに変更

※x0.5でもいいかも。

  • ウィンドウアニメーションスケール

  • トランジションアニメーションスケール

  • アニメーター再生時間スケール


実際の変化

設定後はキーボード表示が爆速、です。

というのは言い過ぎで、今までが遅すぎただけです。

標準に戻りました。

同じような現象に悩まされているPOCO F7ユーザーにはとても、すごくおすすめです。


さらに軽くなるかもの設定

もう1つおすすめがあります。RAM拡張をOFFです。

設定>追加設定>メモリ拡張>OFF

これで動作が安定、発熱減少、バッテリー改善になることがあるらしい・・・。

実感はよくわからないです。


POCO F7 ユーザーにおすすめの設定

動作改善のおすすめの設定1個。

開発者オプションのアニメーションOFF、です。

さらに、RAM拡張OFFもやるといいのかも?

キーボードの遅さで困っている人はぜひ試してみてください。

2026/02/27

RackNerd Clubとは何?少し使っての感想。低価格な海外VPS。試験向けに良さそう【中身は本家RackNerd、年間5400円の4GBプランおすすめ】

RackNerd Clubとは何?少し使っての感想。低価格な海外VPS。試験向けに良さそう【中身は本家RackNerd、年間5400円の4GBプランおすすめ】

海外VPSで検索すると出てくるRackNerdClub。その「RackNerd Club」の特価ページ(セール常設のように見えるやつ)について、 Redditのレビューや自分使ってみた感想を書きました。

先に結論:中身はアメリカのサーバーRackNerdと同じ。Redditで評判も普通。個人開発・小規模運用なら“普通に使える”。docker+Laravelとか動かして普通に使えた。おすすめは3.5GB・4GB。





RackNerd Clubって何?(ざっくり)

RackNerd Clubは、RackNerdのセール(ディール)をまとめて見せている日本語セールページです。クリックすると本家の英語ページに行きます。
日本語>https://racknerd.club/ja/
本家>https://www.racknerd.com/

メモリ2.5GBで約USD20年間 3220円は安い。
使えるのだろうかと不安になる。
でもテストするなら、NTT Web arena Indigo で2GB月額814円とか日本の格安VPSが多数あるのでそれでいいやと思ってしまう。

↓普通すぎるけど聞かない会社なので手が出しにくいウェブサイト・・・





Redditで多い評価:まずは「価格が安い」

Redditで調べてみました。
Redditでは「定価で買うよりセールページを見るべき」という趣旨のコメントが多め。 Black Fridayセールの価格の安いページがいつでも契約できます。
そこがさらに変に思えて契約するのが難しい・・・。

良い評判まとめ:個人用途・学習用途・小規模なら「十分」「2年以上問題なし」

ポジティブ寄りの意見で多いのは次のタイプです。

  • 「学習用途・検証環境・個人サイトなら十分」
  • 「2年以上動いていて大きな問題はない」
  • 「遅い/気になる点があったが、相談したら移行対応してくれた」
  • 「サポートが返信してくれた(IPv6有効化、rDNSなど)」

実際のスレ例:

注意点まとめ:「本番は冗長化した方がいい」「当たり外れ(ばらつき)」系の声もある

一方で、ネガティブ寄り・注意喚起寄りの意見も一定数あります。代表的なのは次のタイプ。

  • 「パフォーマンスが徐々に落ちた(オーバーセル疑惑)」
  • 「packet loss / network slowdown があった」
  • 「CPU steal が気になる」
  • 「本番クリティカル用途の“単独運用”は避ける」(=バックアップや冗長化前提が安心)
  • 「昔使った時に落ちまくった」という体験談

実際のスレ例:

まとめると、Redditの空気感はだいたいこうです: 「安い。個人用途ならOK。ただし“絶対止めたくない本番”は、単騎運用にしない」

(ざっくり)スペックの見方:迷ったら4GB

RackNerdのセールは時期で表記が変わりますが、だいたい次のレンジで出てきます。 迷ったら4GBが一番バランス良いです。

目安プラン 向いている用途 体感コメント
1GB〜2GB 検証、軽いWeb、踏み台、超小規模 安いが、Docker複数やDB同居だと窮屈になりがち
3GB 小規模アプリ、軽いAPI、WordPress数本 コスパ良いが、あと一歩余裕が欲しくなることがある
4GB(おすすめ) Laravel + DB + Nginx、Docker運用、軽めのAI系 安定運用の“最低ライン”として選びやすい

Redditでも「学習・検証には良い」「本番は冗長化」という意見が多いので、 4GBで余裕を作りつつ、バックアップや移行手順を最初から整えるのが現実的です。

スペックはこの画像。
racknerdのHome>何でもいいのでOrder now>カート手前ページで左のCategories一覧見る>BlackFriday2025
4GBで年間USD29.98+消費税=USD33/年
年間5339円|月間444円/1USD=161.8のカード支払い手数料込みで計算




おすすめの使い方

  • 個人開発・社内ツールの検証環境(まず動かして学ぶ)
  • 小規模のWeb/API(落ちても致命傷にならない設計)
  • バックアップ前提の本番(スナップショット、外部バックアップ、別リージョン待機など)

使ってみたRackNerd 4GB

4GBをカード決済で契約。
ユーザー名とパスワードがメールで送られてきた。
linuxサーバーはconoha wingをsshで数年使ってるだけ。

コマンド集を読みながら、わからないことはチャッピー全投げ。
Ubuntu24.0のLAサーバー。
OSアップデート、ファイヤーウォール、SSH、sudoユーザ作ってroot禁止の基本設定のみ。docker+nginx+Laravelでサンプルページ表示。
CloudFlareでDNS設定で公開。httpsも。

私のレベルですが、ここまでササッと遅滞なく公開までできた。
難しいことやらないなら普通のサーバーなのだと思います。
cp、mvなどのコマンド忘れてたのが一番時間かかりました。
Ubuntuだとnanoエディタあるので初心者には楽。
チャッピー指示でroot禁止を言われて、sudoできないユーザーでログインして焦りました・・・。
※RackNerdマイページのVNCサービスでrootログイン可能なのでここから再設定かOS再インストール。


結論

RackNerd Clubは「RackNerdの日本語版のセールページ」。
Redditでも“安い、普通に使える”という声。

ただし、品質にばらつき混雑などのコメントもあるため、 重要サービスは単独運用にしないのが安心。

迷うなら、最初は4GB
心配ならもっと低価格な2.5GBプランとか。使ってみて試す。
おそらく普通に付ける海外品質のVPSサービスです。
レスポンス気にするならNTT Web arena、さくらVPS、カゴヤ、Conohaが間違いないと思います。
低価格でそれなりのRAM容量でやりたい!
サーバー学びたい。
小規模な社内向けアプリの初期段階とかならRackNerdもありと思います。
ただし、海外なので各種リスクは考えての運用を。

2026/02/23

WhisperもLLMも無料で試せる?0円でAI APIを使う方法:Groq Cloud無料枠の簡単レビュー 2026年2月版

Groq Cloudは無料で使える?実際にAPIを試してわかった無料枠と制限まとめ

AI APIは便利ですが、気になるのは料金です。

OpenAIやAnthropicは従量課金制。
テスト段階ではできるだけコストを抑えたいものです。

そこで試してみたのが「Groq Cloudの無料枠(Free Tier)」です。
https://groq.com/

今回は、実際にAPIを使ってみた経験をもとに、

  • 無料枠の内容
  • どこまで実用的か
  • 制限は厳しいのか

を整理します。

目次の前にさらっと結論!
1:APIなので低スペックPCでも使える。便利
2:テストでやってみるなら全然OK
3:0円で使えるので消費量みて従量のコストが体感できる。怖くない
4:OpenAI形式なので汎用性のあるAPIが覚えられる
5:使ってみるとgeminiよりも応答の速いGroqすごいと思う
6:whisperでも応答が速い。Groqすごい
7:Groqの従量課金は高くないのでGroq使い勝手いいかも
8:調子にのって使っていると制限で使えなくなる

テスト環境はCPUは10世代のIntel+メモリ16GBの5万くらいの中古ノート
アマゾンならこういうの>https://amzn.to/3MTlDUd
ちゃんと動く。
PCではPowershellでURLアクセスとテキスト処理のみ。

もう少ししっかりテストするなら10万以下のこういうので↓いいと思う。
GROQのAPI運用ならローカルのスペックはそんなにいらない。
メモリ16GBでいいと思う。



Groqは応答速度が非常に速い。でも、OpenAIのgpt-5-nanoはかなりコストが安い
gpt-5-nanoでも十分ならOpenAIでいいのかも。nanoでも精度は良い。

https://developers.openai.com/api/docs/pricing
gpt-5-nano $0.05/1M-input $0.40/1M-Output
(↑1Mトークンでどこまでやれるの?の体感をGroqで試してみてもいいかも。)
(llama-3.3-70b-versatile、gpt-oss-120bで、GPT-4に足りないくらいの精度らしい)

Groq Cloudとは

Groq Cloudは、高速推論に特化したAI APIサービスです。

対応モデルには以下のようなものがあります。

  • Llama系モデル
  • Whisper(音声文字起こし)
  • Vision対応モデル

特に特徴的なのは、推論速度の速さです。

体感としては、他のAPIよりもレスポンスが非常に速い印象があります。
開発時のストレスが少ないのは大きな利点です。

無料枠(Free Tier)の内容

2026年2月時点での無料枠の概要は以下の通りです。
多くはないですが試験するには十分と思います。

Groq Free Tier Limits
Model RPM RPD TPM TPD
allam-2-7b307K6K500K
groq/compound3025070KNo limit
groq/compound-mini3025070KNo limit
llama-3.1-8b-instant3014.4K6K500K
llama-3.3-70b-versatile301K12K100K
meta-llama/llama-4-maverick-17b-128e-instruct301K6K500K
meta-llama/llama-4-scout-17b-16e-instruct301K30K500K
meta-llama/llama-guard-4-12b3014.4K15K500K
meta-llama/llama-prompt-guard-2-22m3014.4K15K500K
meta-llama/llama-prompt-guard-2-86m3014.4K15K500K
moonshotai/kimi-k2-instruct601K10K300K
moonshotai/kimi-k2-instruct-0905601K10K300K
openai/gpt-oss-120b301K8K200K
openai/gpt-oss-20b301K8K200K
openai/gpt-oss-safeguard-20b301K8K200K
qwen/qwen3-32b601K6K500K
Token Cost Comparison (1M tokens)
Model Input (1M) Output (1M)
OPENAI gpt-5-nano $0.05 $0.40
Groq GPT OSS 120B 128k $0.15 $0.60
Model Capabilities
Model テキスト 画像 特長
allam-2-7b×軽量高速
groq/compound×複合推論
groq/compound-mini×軽量複合
llama-3.1-8b-instant×超高速
llama-3.3-70b-versatile×高精度
meta-llama/llama-4-maverick-17b-128e-instruct視覚対応
meta-llama/llama-4-scout-17b-16e-instruct視覚高速
meta-llama/llama-guard-4-12b×安全判定
meta-llama/llama-prompt-guard-2-22m×入力検査
meta-llama/llama-prompt-guard-2-86m×高精検査
moonshotai/kimi-k2-instruct×長文強い
moonshotai/kimi-k2-instruct-0905×改良版
openai/gpt-oss-120b×大規模
openai/gpt-oss-20b×軽量版
openai/gpt-oss-safeguard-20b×安全特化
qwen/qwen3-32b×多言語

リクエスト制限

多くのモデルで 30リクエスト/分(RPM)

1日のリクエスト上限

モデルごとに異なります。

例:

  • llama-3.1-8b-instant → 約14,000回/日
  • llama-3.3-70b → 約1,000回/日

トークン制限

  • 1分あたり 6,000〜70,000トークン(モデルによる)
  • 1日あたり 100,000〜500,000トークン(モデルによる)

Whisper(音声API)

音声処理には時間ベースの制限があります。

例:

  • 1時間あたり 約120分の音声処理
  • 1日あたり 約8時間分

実際に使ってみた感想

テキスト処理

ブログ整形、OCR結果の整形、要約などは十分実用的です。
無料枠でも日常的なテスト用途には問題ありません。

Whisper文字起こし

2時間を超えるm4aファイルも、分割すれば対応可能です。
業務テスト用途としては十分な性能です。

注意点

  • 商用利用する場合は必ず利用規約を確認すること
  • 無料枠は予告なく変更される可能性があること
  • 同時実行数には制限があること

無料枠は実用レベルか

結論としては、開発・検証用途なら十分実用レベルです。

特に以下の用途には向いています。

  • OCR後のテキスト整形
  • Whisper文字起こし
  • 社内ツール開発
  • 小規模な自動化

本格運用の前段階としては非常に使いやすいサービスです。

まとめ

Groq Cloudは、無料枠でも十分に実用的なAI APIサービスです。

開発段階で従量課金を避けたい場合や、 まずは試してみたいという方には特におすすめできます。

といっても、OpenAIのAPIもとても高額な料金ではないので、 まずはやってみたい方ならいいかも。
Groqの反応速度は使い勝手よしです。


さらっとテストコード

<#
Get-Weather-Groq.ps1
- Open-Meteoで今日の天気(実データ)を取得
- Groq (llama-3.3-70b-versatile) に渡して「詳しく説明」させる
必要:
  $env:GROQ_API_KEY にAPIキーを設定
例:
  $env:GROQ_API_KEY="gsk_...."
  .\Get-Weather-Groq.ps1 -City "東京" -Language "ja"
#>

param(
  [Parameter(Mandatory=$false)][string]$City = "東京",
  [Parameter(Mandatory=$false)][string]$Language = "ja",
  [Parameter(Mandatory=$false)][string]$Timezone = "Asia/Tokyo",
  [Parameter(Mandatory=$false)][string]$Model = "llama-3.3-70b-versatile"
)

$ErrorActionPreference = "Stop"

# --- 0) Groq API Key ---
$apiKey = $env:GROQ_API_KEY
if ([string]::IsNullOrWhiteSpace($apiKey)) {
  throw "環境変数 GROQ_API_KEY が未設定です。例: `$env:GROQ_API_KEY='gsk_...'`"
}

# --- 1) 都市名 -> 緯度経度(Open-Meteo Geocoding) ---
$geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=$([uri]::EscapeDataString($City))&count=1&language=$Language&format=json"
$geo = Invoke-RestMethod -Uri $geoUrl -Method Get

if (-not $geo.results -or $geo.results.Count -eq 0) {
  throw "都市 '$City' の緯度経度が見つかりませんでした。別の表記で試してください。"
}

$place = $geo.results[0]
$lat = $place.latitude
$lon = $place.longitude
$resolvedName = $place.name
$admin = @($place.admin1, $place.country) -join ", "

# --- 2) 今日の天気(Open-Meteo Forecast) ---
# 取得項目(必要なら増やしてOK)
$meteoUrl = "https://api.open-meteo.com/v1/forecast" +
  "?latitude=$lat&longitude=$lon" +
  "&timezone=$([uri]::EscapeDataString($Timezone))" +
  "&forecast_days=1" +
  "&hourly=temperature_2m,apparent_temperature,precipitation_probability,precipitation,weathercode,windspeed_10m,windgusts_10m,winddirection_10m,relativehumidity_2m" +
  "&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,windspeed_10m_max,windgusts_10m_max,sunrise,sunset"

$wx = Invoke-RestMethod -Uri $meteoUrl -Method Get

# --- 3) Groq に「今日の天気を詳しく」説明させる ---
$today = (Get-Date).ToString("yyyy-MM-dd")
$system = @"
あなたは日本の天気解説の専門家です。
ユーザーが提供する天気APIのJSON(Open-Meteo)を根拠に、推測で補完せず、データに基づいて「今日の天気」を詳しく説明してください。
出力は日本語。読みやすい箇条書き中心。最後に「外出アドバイス」を短く付けてください。
"@

$user = @"
場所: $resolvedName ($admin)
日付: $today
次のOpen-Meteo JSONを解析して、今日の天気を詳しく解説してください。

--- OPEN-METEO JSON ---
$($wx | ConvertTo-Json -Depth 12)
"@

$endpoint = "https://api.groq.com/openai/v1/chat/completions"

$body = @{
  model = $Model
  messages = @(
    @{ role="system"; content=$system },
    @{ role="user"; content=$user }
  )
  temperature = 0.2
  top_p = 1
  max_tokens = 1200
} | ConvertTo-Json -Depth 10

$headers = @{
  "Authorization" = "Bearer $apiKey"
  "Content-Type"  = "application/json"
}

$res = Invoke-RestMethod -Method Post -Uri $endpoint -Headers $headers -Body $body
$text = $res.choices[0].message.content

# --- 4) 表示 ---
"=== 今日の天気(Groq解説)==="
$text

ページ上部へ戻る

2026/01/25

CPUでOCR処理:LightOnOCR-2-1B

PythonでのOCR処理。EasyOCRでやってました。

【2025年12月版】Windows で EasyOCR と Poppler を利用して PDF を OCR する手順
https://nokoshitamono.blogspot.com/2025/12/202512windows-easyocr-poppler-pdf-ocr.html

LightOnOCR-2-1Bでもやってみました。

EasyOCRは高速処理。
LightOnOCR-2-1Bはそれなりに時間がかかるけど、段組みの認識は強い。
CPUでも動くので使えるかも。
WikipediaのページをOCR処理した結果







モデルはどっかにダウンロードする
git lfs install
git clone https://huggingface.co/lightonai/LightOnOCR-2-1B c:/llamamodels/LightOnOCR-2-1B

あとは各ライブラリをpipでインストール。
pip checkで過不足ない感じにすれば動くはず。
torchはCUDA対応にすればGPU使えますが、CPUだけでも動きます。


# python lightonocr_local.py test.jpg --out result.txt
# python lightonocr_local.py book.pdf --pages 1,2,5-12

import argparse
import io
import os
from pathlib import Path
from typing import List, Optional, Tuple

import torch
from PIL import Image

# PDF対応(任意)
try:
    import pypdfium2 as pdfium
except Exception:
    pdfium = None

from transformers import LightOnOcrForConditionalGeneration, LightOnOcrProcessor

MODEL_PATH = r"c:/models/LightOnOCR-2-1B"

IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"}


def pick_device() -> Tuple[str, torch.dtype]:
    """
    LightOnOCR公式例に合わせて:
      - mps -> float32
      - cuda -> bfloat16(対応しないGPUなら float16 に切替推奨)
      - cpu -> float32
    """
    if torch.backends.mps.is_available():
        return "mps", torch.float32
    if torch.cuda.is_available():
        # bf16 が怪しい場合は float16 に変更してください
        return "cuda", torch.bfloat16
    return "cpu", torch.float32

def resolve_device_and_dtype(device_arg: str, dtype_arg: str, force_fp16: bool) -> Tuple[str, torch.dtype]:
    """
    CLI指定に基づいて device と dtype を安全に確定する。
    device_arg: auto|cpu|cuda|cuda:0|cuda:1|mps
    dtype_arg : auto|fp32|fp16|bf16
    """
    # --- device 決定 ---
    if device_arg == "auto":
        if torch.backends.mps.is_available():
            device = "mps"
        elif torch.cuda.is_available():
            device = "cuda"
        else:
            device = "cpu"
    else:
        device = device_arg

    # --- device 実在チェック ---
    if device.startswith("cuda"):
        if not torch.cuda.is_available():
            raise RuntimeError("CUDAが利用できません。--device cpu を指定してください。")
        if device != "cuda":
            # cuda:0 のような形式
            try:
                idx = int(device.split(":")[1])
            except Exception:
                raise RuntimeError("CUDA指定は cuda または cuda:0 の形式で指定してください。")

            if idx < 0 or idx >= torch.cuda.device_count():
                raise RuntimeError(f"{device} は存在しません。CUDAは {torch.cuda.device_count()}枚です。")

    if device == "mps" and not torch.backends.mps.is_available():
        raise RuntimeError("MPS が利用できません。")

    # --- dtype 決定 ---
    if dtype_arg == "auto":
        if device.startswith("cuda"):
            dtype = torch.float16 if force_fp16 else torch.bfloat16
        else:
            dtype = torch.float32
    else:
        mp = {"fp32": torch.float32, "fp16": torch.float16, "bf16": torch.bfloat16}
        if dtype_arg not in mp:
            raise ValueError("--dtype は auto|fp32|fp16|bf16 のいずれかです")
        dtype = mp[dtype_arg]

    # MPSはfp32が安全(fp16/bf16で不安定になりやすい)
    if device == "mps":
        dtype = torch.float32

    return device, dtype



def load_images_from_path(path: Path, page_spec: str = "") -> List[Image.Image]:
    """
    path が:
      - 画像ファイル: 1枚
      - フォルダ: 画像を全列挙
      - PDF: 各ページをレンダして画像化(pypdfium2必須)
    """
    if path.is_dir():
        imgs = []
        for p in sorted(path.rglob("*")):
            if p.suffix.lower() in IMAGE_EXTS:
                imgs.append(Image.open(p).convert("RGB"))
        return imgs

    if path.suffix.lower() in IMAGE_EXTS:
        return [Image.open(path).convert("RGB")]

    if path.suffix.lower() == ".pdf":
        if pdfium is None:
            raise RuntimeError("PDFを処理するには `pip install pypdfium2` が必要です。")

        pdf_data = path.read_bytes()
        pdf = pdfium.PdfDocument(pdf_data)

        page_indices = parse_page_range(page_spec, len(pdf))

        images = []
        for i in page_indices:
            page = pdf[i]
            pil_img = page.render(scale=2.77).to_pil()
            images.append(pil_img.convert("RGB"))

        return images

    raise ValueError(f"未対応の入力です: {path}")

def parse_page_range(spec: str, total_pages: int) -> List[int]:
    """
    ページ指定文字列を 0-based index のリストに変換
    """
    if not spec:
        return list(range(total_pages))

    pages = set()

    for part in spec.split(","):
        part = part.strip()

        if "-" in part:
            start, end = part.split("-", 1)

            start_i = int(start) - 1 if start else 0
            end_i = int(end) - 1 if end else total_pages - 1

            start_i = max(start_i, 0)
            end_i = min(end_i, total_pages - 1)

            for i in range(start_i, end_i + 1):
                pages.add(i)
        else:
            i = int(part) - 1
            if 0 <= i < total_pages:
                pages.add(i)

    return sorted(pages)



@torch.inference_mode()
def ocr_image(
    model: LightOnOcrForConditionalGeneration,
    processor: LightOnOcrProcessor,
    image: Image.Image,
    device: str,
    dtype: torch.dtype,
    max_new_tokens: int = 1024,
) -> str:
    """
    Transformersの公式モデルカードにある会話テンプレ方式でOCRする。 :contentReference[oaicite:2]{index=2}
    """
    conversation = [
        {
            "role": "user",
            "content": [
                {"type": "image", "image": image},
            ],
        }
    ]

    inputs = processor.apply_chat_template(
        conversation,
        add_generation_prompt=True,
        tokenize=True,
        return_dict=True,
        return_tensors="pt",
    )

    # floatテンソルだけ dtype を当て、他は device のみ移動
    inputs = {
        k: (v.to(device=device, dtype=dtype) if v.is_floating_point() else v.to(device))
        for k, v in inputs.items()
    }

    output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens)
    generated_ids = output_ids[0, inputs["input_ids"].shape[1] :]
    text = processor.decode(generated_ids, skip_special_tokens=True)
    return text.strip()


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("input", help="画像ファイル / 画像フォルダ / PDF のパス")
    ap.add_argument("--max_new_tokens", type=int, default=1024)
    ap.add_argument("--out", default="", help="出力テキスト保存先(未指定なら標準出力)")
    ap.add_argument("--page_sep", default="\n\n--- PAGE ---\n\n", help="複数ページの区切り")
    ap.add_argument("--force_fp16", action="store_true", help="CUDA時に fp16 を強制(bf16で不安定なら)")
    ap.add_argument("--pages", default="", help="PDFページ範囲(例: 1-5, 3-, -10, 1,3,5-7)※PDFのみ有効")
    ap.add_argument("--model", default="", help="HF model id or local path(未指定ならコード内の MODEL_PATH を使用)")

    # ★追加(GPU切替)
    ap.add_argument("--device", default="auto", help="実行デバイス: auto | cpu | cuda | cuda:0 | cuda:1 | mps")
    ap.add_argument("--dtype", default="auto", help="精度: auto | fp32 | fp16 | bf16(cuda以外はfp32推奨)")

    args = ap.parse_args()
    model_path = args.model if args.model else MODEL_PATH
    
    
    # ★追加(GPU切替)
    device, dtype = resolve_device_and_dtype(args.device, args.dtype, args.force_fp16)
    print(f"[INFO] device={device} dtype={dtype}")

    model = LightOnOcrForConditionalGeneration.from_pretrained(
        model_path,
        torch_dtype=dtype,
        local_files_only=True,
    ).to(device)

    processor = LightOnOcrProcessor.from_pretrained(
        model_path,
        local_files_only=True,
    )

    images = load_images_from_path(in_path, args.pages)
    if not images:
        raise RuntimeError("処理できる画像が見つかりませんでした。")

    texts: List[str] = []
    for idx, img in enumerate(images, start=1):
        print(f"[INFO] OCR {idx}/{len(images)} ...")
        txt = ocr_image(
            model=model,
            processor=processor,
            image=img,
            device=device,
            dtype=dtype,
            max_new_tokens=args.max_new_tokens,
        )
        texts.append(txt)

    final_text = args.page_sep.join(texts)

    if args.out:
        out_path = Path(args.out)
        out_path.write_text(final_text, encoding="utf-8")
        print(f"[INFO] saved: {out_path}")
    else:
        print(final_text)


if __name__ == "__main__":
    main()

2025/12/16

データSIMが使えるようになるまで1時間くらいかかるときもある


50GBのデータSIMカードをamazonで購入。
設定どおりにAndroid、iPhoneに挿して、10分20分待っても通信できず。。。
やり方わからずに発売元にも聞いてみた。
説明をしたりと、だいたい1時間くらい経過したらいつのまにか通信できるようになってた。
10分くらい待つ、とは聞いたことあったけど電波弱いと時間が必要かもと思います。

TORA様のSIMは通信速度も良かったです。
50GBも使えます。

eSIMと物理SIMがあるけど、端末やスマホを替えるかもしれないなら
物理SIMのほうが便利。
一つの端末で使い切りならeSIMの方が便利で少し安い。
30GBで3000円しないなら、たくさん使いそうなときだけeSIMで使えばいいのかもと思う。


360日で50GBが5,000円しないなら、サブ端末はこういうのでいいかも。
あとはWIFIを使うなど。
5000/12 =417円/月。



[

2025/12/12

【2025年12月版】WindowsでEasyOCRとPopplerを利用してPDFをOCRした文章の誤字脱字や改行を修正手順

前の続きです。

OCR処理したので文章が間違っていたりします。
主に誤字脱字と改行場所の修正が必要です。
解決策>languagetoolを使って修正する。 https://languagetool.org/
細かいところは直らないけど、ざっくり直る。
コードは下に。
JAVAが必要になりますのでインストールするか、zip解凍してパスを通す。
(Java17JREだとそれなりの容量です)





"""
OCR後の日本語テキストをローカルで整形・校正するパイプライン
- 前処理: 文字正規化 (neologdn) / 改行の再構成(OCR由来の不自然な改行を抑制)
- 校正: LanguageTool (ja-JP) を使った「安全寄り」自動修正
- 付帯: 修正候補ログ(JSONL) / 差分HTML生成

"""

from __future__ import annotations

import argparse
import html
import json
import os
import re
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import List, Tuple, Dict, Iterable

import neologdn
import regex  # pip: regex
from tqdm import tqdm
from rapidfuzz.distance import Levenshtein

import language_tool_python


# ----------------------------
# 設定
# ----------------------------

@dataclass
class Config:
    langtool_lang: str = "ja-JP"

    # 改行整形の方針(安全寄り)
    # - 行末が句点等で終わらない場合は、次の行を連結しやすい(OCRの行折返し対策)
    join_if_line_not_end_with: str = r"[。..!!\??]$"

    # ページ区切り行(OCR出力でよくある例)
    keep_page_markers: bool = True
    page_marker_regex: str = r"^\s*(=+)\s*Page\s*\d+\s*(=+)\s*$"

    # LanguageTool適用を安全寄りにするルール
    max_auto_apply_per_sentence: int = 6  # 1文あたり自動適用し過ぎない
    max_repl_len: int = 12                # 置換候補が長すぎるものは自動適用しない
    max_edit_distance: int = 3            # 元語と候補の編集距離が大きいものは自動適用しない
    min_token_len_for_apply: int = 1

    # 置換対象がこれにマッチする場合はスキップ(URLやコードっぽいもの、数値列など)
    skip_pattern: re.Pattern = re.compile(
        r"("
        r"https?://\S+|"
        r"www\.\S+|"
        r"[A-Za-z0-9_\-]{20,}|"          # 長い英数字(ハッシュ等)
        r"[\d]{8,}|"                    # 長い数字列
        r"`[^`]+`|"                     # インラインコード
        r")"
    )

    # 文分割(雑に句点で切ると壊れるので、まず段落→簡易文分割)
    sentence_split_regex: re.Pattern = re.compile(r"(?<=[。!?\?!])\s*")


# ----------------------------
# 文字正規化
# ----------------------------

def normalize_text(s: str) -> str:
    # neologdn: 全半角・連続記号・濁点分離などを良い感じに正規化
    s = neologdn.normalize(s)
    # いくつか追加の軽い正規化
    s = s.replace("\u00A0", " ")  # NBSP
    s = re.sub(r"[ \t]+", " ", s)
    return s


# ----------------------------
# 改行整形(OCRの行折返し対策)
# ----------------------------

def fix_linebreaks(text: str, cfg: Config) -> str:
    """
    OCR出力でありがちな「文中で改行されている」問題を緩和する。
    方針:
    - 空行は段落区切りとして維持
    - ページマーカー行は維持(設定で)
    - それ以外の行は、行末が句点/終端記号で終わらない場合は次行と連結
    """
    lines = text.splitlines()
    out: List[str] = []

    def is_page_marker(line: str) -> bool:
        return cfg.keep_page_markers and re.match(cfg.page_marker_regex, line) is not None

    buffer = ""

    for line in lines:
        raw = line.rstrip("\n")
        stripped = raw.strip()

        if is_page_marker(raw):
            # flush buffer
            if buffer:
                out.append(buffer.strip())
                buffer = ""
            out.append(raw)
            continue

        if stripped == "":
            # 空行 = 段落区切り
            if buffer:
                out.append(buffer.strip())
                buffer = ""
            out.append("")  # keep blank line
            continue

        # 連結対象判定
        if not buffer:
            buffer = stripped
        else:
            # 前行末が終端記号で終わらない → スペースで連結
            if re.search(cfg.join_if_line_not_end_with, buffer) is None:
                # 日本語では半角スペースより「そのまま連結」が自然な場合も多いが、
                # OCR後は単語境界が曖昧なのでスペース挟みで安全寄り
                buffer = buffer + " " + stripped
            else:
                # 文末っぽいなら段落内でも改行を段落内改行として維持(ただしここは好み)
                buffer = buffer + "\n" + stripped

    if buffer:
        out.append(buffer.strip())

    # 空行が多すぎる場合の軽い圧縮
    result = "\n".join(out)
    result = re.sub(r"\n{3,}", "\n\n", result)
    return result


# ----------------------------
# 文・段落の分割
# ----------------------------

def split_into_paragraphs(text: str, cfg: Config) -> List[str]:
    # ページマーカーは段落として独立扱い
    paras: List[str] = []
    buf: List[str] = []

    for line in text.splitlines():
        if cfg.keep_page_markers and re.match(cfg.page_marker_regex, line):
            if buf:
                paras.append("\n".join(buf).strip())
                buf = []
            paras.append(line.strip())
            continue

        if line.strip() == "":
            if buf:
                paras.append("\n".join(buf).strip())
                buf = []
            paras.append("")  # paragraph separator
        else:
            buf.append(line)

    if buf:
        paras.append("\n".join(buf).strip())

    # 空段落連続を抑制
    cleaned: List[str] = []
    prev_blank = False
    for p in paras:
        if p == "":
            if not prev_blank:
                cleaned.append("")
            prev_blank = True
        else:
            cleaned.append(p)
            prev_blank = False
    return cleaned


def split_paragraph_into_sentences(p: str, cfg: Config) -> List[str]:
    if p == "" or (cfg.keep_page_markers and re.match(cfg.page_marker_regex, p)):
        return [p]
    # 改行は文分割の妨げになるのでスペースへ(段落内の改行を保持したい場合は調整)
    tmp = p.replace("\n", " ")
    tmp = re.sub(r"\s{2,}", " ", tmp).strip()
    if not tmp:
        return [""]
    sents = [s.strip() for s in cfg.sentence_split_regex.split(tmp) if s.strip()]
    return sents if sents else [tmp]


# ----------------------------
# LanguageTool: 安全寄り自動適用
# ----------------------------

def apply_matches_safely(text: str, matches, cfg: Config) -> Tuple[str, List[Dict]]:
    """
    LanguageToolのmatchesを受け取り、安全寄りに自動適用する。
    - URL/コード/長い数字列などはスキップ
    - replacementが1個のみ & 短い & 編集距離が小さいものを優先
    - オフセットがずれるので、後ろから適用
    """
    logs: List[Dict] = []

    # 1文あたりの過剰適用抑制(matchesは文単位で呼ぶので単純に制限)
    applied = 0

    # offsetを使うので後ろから
    for m in sorted(matches, key=lambda x: x.offset, reverse=True):
        if applied >= cfg.max_auto_apply_per_sentence:
            break

        start = m.offset

        # 互換対応
        length = getattr(m, "errorLength", None)
        if length is None:
            length = m.error_length
        end = start + length

        frag = text[start:end]

        # スキップ条件
        if not frag or len(frag) < cfg.min_token_len_for_apply:
            continue
        if cfg.skip_pattern.search(frag):
            continue

        # 候補が無いならスキップ
        repls = list(m.replacements) if m.replacements else []
        if len(repls) != 1:
            # 候補が複数ある場合は自動適用しない(安全寄り)
            logs.append({
                "action": "skip_multi_candidates",
                "offset": start,
                "length": m.errorLength,
                "from": frag,
                "candidates": repls[:10],
                "ruleId": getattr(m, "ruleId", None),
                "message": getattr(m, "message", None),
            })
            continue

        repl = repls[0]
        if not repl or len(repl) > cfg.max_repl_len:
            logs.append({
                "action": "skip_long_candidate",
                "offset": start,
                "length": m.errorLength,
                "from": frag,
                "candidate": repl,
                "ruleId": getattr(m, "ruleId", None),
                "message": getattr(m, "message", None),
            })
            continue

        # 編集距離が大きいと誤訂正リスクが高い
        dist = Levenshtein.distance(frag, repl)
        if dist > cfg.max_edit_distance:
            logs.append({
                "action": "skip_far_edit",
                "offset": start,
                "length": m.errorLength,
                "from": frag,
                "candidate": repl,
                "edit_distance": dist,
                "ruleId": getattr(m, "ruleId", None),
                "message": getattr(m, "message", None),
            })
            continue

        # 適用
        new_text = text[:start] + repl + text[end:]
        logs.append({
            "action": "apply",
            "offset": start,
            "length": m.errorLength,
            "from": frag,
            "to": repl,
            "edit_distance": dist,
            "ruleId": getattr(m, "ruleId", None),
            "message": getattr(m, "message", None),
        })
        text = new_text
        applied += 1

    return text, logs


def proofread_text_with_languagetool(clean_text: str, cfg: Config) -> Tuple[str, List[Dict]]:
    """
    段落→文単位でLanguageToolを適用し、校正済みテキストとログを返す
    """
    tool = language_tool_python.LanguageTool(cfg.langtool_lang)

    paras = split_into_paragraphs(clean_text, cfg)

    out_paras: List[str] = []
    all_logs: List[Dict] = []

    for p in tqdm(paras, desc="LanguageTool proofreading", unit="para"):
        if p == "":
            out_paras.append("")
            continue
        if cfg.keep_page_markers and re.match(cfg.page_marker_regex, p):
            out_paras.append(p)
            continue

        sents = split_paragraph_into_sentences(p, cfg)
        fixed_sents: List[str] = []

        for s in sents:
            if not s:
                continue
            # 文単位でチェック(大量テキストでも安定)
            matches = tool.check(s)
            new_s, logs = apply_matches_safely(s, matches, cfg)

            # 文脈を壊しやすい過剰スペースを軽く整形
            new_s = re.sub(r"\s{2,}", " ", new_s).strip()

            # ログに文情報を付与
            for lg in logs:
                lg["sentence"] = s
                lg["sentence_fixed"] = new_s
            all_logs.extend(logs)

            fixed_sents.append(new_s)

        # 文間はスペースで連結(段落内の自然さ優先)
        out_paras.append(" ".join(fixed_sents).strip())

    result = "\n".join(out_paras)
    result = re.sub(r"\n{3,}", "\n\n", result)
    return result, all_logs


# ----------------------------
# 差分HTML
# ----------------------------

def make_diff_html(before: str, after: str, title: str) -> str:
    """
    最低限の差分表示(行単位)をHTMLで出す
    ※ もっと強力な差分が欲しければ difflib.HtmlDiff でもOK
    """
    import difflib

    before_lines = before.splitlines()
    after_lines = after.splitlines()

    diff = difflib.HtmlDiff(tabsize=2, wrapcolumn=120)
    html_body = diff.make_file(before_lines, after_lines, fromdesc="before", todesc="after", context=True, numlines=3)
    # タイトルだけ差し替え
    html_body = html_body.replace("", f"{html.escape(title)}")
    return html_body


# ----------------------------
# 入出力
# ----------------------------

def read_text(path: Path) -> str:
    # まずUTF-8、失敗ならCP932を試す(OCR出力でありがち)
    data = path.read_bytes()
    for enc in ("utf-8", "utf-8-sig", "cp932"):
        try:
            return data.decode(enc)
        except UnicodeDecodeError:
            continue
    # 最後はreplaceで
    return data.decode("utf-8", errors="replace")


def write_text(path: Path, text: str) -> None:
    path.write_text(text, encoding="utf-8", newline="\n")


def write_jsonl(path: Path, items: List[Dict]) -> None:
    with path.open("w", encoding="utf-8", newline="\n") as f:
        for it in items:
            f.write(json.dumps(it, ensure_ascii=False) + "\n")


def iter_input_txt(input_path: Path) -> List[Path]:
    if input_path.is_file():
        return [input_path]
    return sorted([p for p in input_path.rglob("*.txt") if p.is_file()])


# ----------------------------
# main
# ----------------------------

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", required=True, help="入力 .txt ファイル、または .txt を含むフォルダ")
    parser.add_argument("--outdir", required=True, help="出力フォルダ")
    parser.add_argument("--no-proofread", action="store_true", help="前処理のみ(LanguageTool校正なし)")
    parser.add_argument("--keep-page-markers", action="store_true", help="Pageマーカー行を維持する")
    args = parser.parse_args()

    cfg = Config(keep_page_markers=bool(args.keep_page_markers))

    in_path = Path(args.input)
    outdir = Path(args.outdir)
    outdir.mkdir(parents=True, exist_ok=True)

    inputs = iter_input_txt(in_path)
    if not inputs:
        print("入力 .txt が見つかりませんでした。", file=sys.stderr)
        sys.exit(1)

    for p in inputs:
        base = p.stem

        raw = read_text(p)

        # 1) 正規化
        norm = normalize_text(raw)

        # 2) 改行整形(OCR折返し対策)
        clean = fix_linebreaks(norm, cfg)

        clean_path = outdir / f"{base}_clean.txt"
        write_text(clean_path, clean)

        if args.no_proofread:
            continue

        # 3) LanguageTool 校正
        proof, logs = proofread_text_with_languagetool(clean, cfg)

        proof_path = outdir / f"{base}_proofread.txt"
        write_text(proof_path, proof)

        # 4) ログ
        log_path = outdir / f"{base}_suggestions.jsonl"
        write_jsonl(log_path, logs)

        # 5) 差分HTML
        diff_html = make_diff_html(clean, proof, title=f"diff: {base}")
        diff_path = outdir / f"{base}_diff.html"
        write_text(diff_path, diff_html)

        print(f"[OK] {p.name}")
        print(f"  clean : {clean_path}")
        print(f"  proof : {proof_path}")
        print(f"  log   : {log_path}")
        print(f"  diff  : {diff_path}")

    print("完了しました。")


if __name__ == "__main__":
    main()
[

【2025年12月版】Windows で EasyOCR と Poppler を利用して PDF を OCR する手順


【2025年12月版】Windows で EasyOCR と Poppler を利用して PDF を OCR する手順

画像だけのPDFファイルの画像を日本語のOCRにする方法をまとめてみました。
過去のPDFだとテキストでなく、画像形式のものも多い。
PDF>PNG化>OCR>テキスト化>読み上げソフト
で、読み上げでPDFファイルを聞きたいとき用です。
テスト的なのでどのくらい使えるかわからないです。
(お試しなので、メモとして記録)

スマホの読み上げでもいいし、こういうので車でも聞けるのが便利






以下では、Windows 環境で EasyOCR を動作させ、PDF ファイルをテキスト化するまでの流れを、順序立てて説明します。
Python 仮想環境の構築、Poppler のインストール、PDF の画像化、OCR スクリプトの作成と実行までを一通り解説します。


1. 作業ディレクトリへ移動する

まず、PowerShell を起動し、作業用のディレクトリへ移動します。

cd C:\Users\ユーザー名\Documents

※「ユーザー名」の部分は、ご自身の Windows のユーザー名に置き換えてください。


2. 仮想環境(venv)を作成し、有効化する

EasyOCR 用に独立した Python 環境(仮想環境)を作成します。これにより、他の Python プロジェクトやシステム全体の環境を汚さずにライブラリを管理できます。

python -m venv easyocr-env
.\easyocr-env\Scripts\activate

プロンプトに (easyocr-env) が表示されれば、有効化されています。


3. PyTorch(CPU版)のインストール

EasyOCR が内部で使用する機械学習ライブラリである PyTorch をインストールします。GPU を使用しない場合は、CPU 版で問題ありません。

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu

4. EasyOCR のインストール

続いて、OCR エンジン本体である EasyOCR をインストールします。

pip install easyocr

5. Poppler をインストールし、PATH を設定する

Python のライブラリ pdf2image を用いて PDF を画像に変換する際には、Poppler に含まれる pdfinfo.exe および pdftoppm.exe が必要となります。

5-1. Poppler のダウンロード

以下の URL から、Poppler for Windows の最新版 ZIP ファイルをダウンロードします。

https://github.com/oschwartz10612/poppler-windows/releases/

例:

poppler-24.02.0-0-x86_64.zip

5-2. Poppler の展開

ダウンロードした ZIP ファイルを、以下のように展開します。

C:\poppler

展開後、次のファイルが存在することを確認してください。

C:\poppler\bin\pdfinfo.exe
C:\poppler\bin\pdftoppm.exe

5-3. PATH へ Poppler の bin を追加する(PowerShell)

Poppler の bin フォルダへのパスを、ユーザー環境変数 PATH に追加します。PowerShell で以下を実行します。

$popplerPath = "C:\poppler\bin"
$currentPath = [System.Environment]::GetEnvironmentVariable("Path", "User")

if ($currentPath -notlike "*$popplerPath*") {
    $newPath = "$currentPath;$popplerPath"
    [System.Environment]::SetEnvironmentVariable("Path", $newPath, "User")
    Write-Host "Poppler path added to USER PATH."
} else {
    Write-Host "Poppler path is already in PATH."
}

その後、PowerShell を一度終了し、再度起動してください。
※Windowsの検索窓>環境変数を検索してクリック>ユーザーかシステムの「Path」選択で編集>「C:\poppler\bin」を追加が簡単なときもあります。

5-4. Poppler が動作しているか確認する

PowerShell 上で、次のいずれかのコマンドを実行します。

pdfinfo -v

または

pdftoppm -h

いずれかが実行され、バージョン情報やヘルプが表示されれば、Poppler のインストールと PATH 設定は正常に完了しています。


6. pdf2image と Pillow のインストール

PDF を画像へ変換するために、pdf2imagePillow をインストールします。

pip install pdf2image pillow

7. OCR 作業用のフォルダを作成する

OCR 対象の PDF ファイルやスクリプトを配置するためのフォルダを作成します。

C:\Users\ユーザー名\Documents\ocr-work

このディレクトリに、OCR 対象の PDF ファイル(例:sample.pdf)を保存します。


8. PDF から画像へ変換し、OCR を実行するスクリプトを作成する

先ほど作成した ocr-work フォルダ内に、pdf_ocr.py という名前のファイルを作成し、以下の内容で保存します。

from pdf2image import convert_from_path
from easyocr import Reader

# PDF のパス
pdf_path = r"C:\Users\ユーザー名\Documents\ocr-work\sample.pdf"

# OCR エンジンの準備
reader = Reader(['ja', 'en'], gpu=False)

# PDF を画像へ変換
pages = convert_from_path(pdf_path, dpi=300)

all_text = []

for i, page in enumerate(pages):
    img_path = f"page_{i+1}.png"
    page.save(img_path, "PNG")

    results = reader.readtext(img_path, detail=0)
    all_text.append("\n".join(results))

# テキストとして保存
with open("output.txt", "w", encoding="utf-8") as f:
    f.write("\n\n==== page break ====\n\n".join(all_text))

print("OCR 完了しました。output.txt を確認してください。")

上記の pdf_path 内のパスは、ご自身のユーザー名および PDF ファイル名に合わせて変更してください。


9. スクリプトの実行

PowerShell で以下のコマンドを実行します。

cd C:\Users\ユーザー名\Documents\ocr-work
..\easyocr-env\Scripts\activate
python pdf_ocr.py

同じフォルダ内に output.txt が生成され、OCR 結果のテキストが保存されます。


まとめ

本記事では、Windows 環境において EasyOCR を用いて PDF を OCR するために必要な設定と、スクリプト作成手順を解説しました。主な流れは以下の通りです。

  1. Python 仮想環境の構築
  2. PyTorch および EasyOCR のインストール
  3. Poppler の導入と PATH 設定
  4. pdf2image による PDF の画像化
  5. Python スクリプトによる OCR 実行とテキスト保存

これらの手順を順に実施することで、Python を用いた OCR 処理環境が完成します。必要に応じて、大量の PDF を一括処理するバッチスクリプトや、OCR 精度を向上させるための前処理スクリプトを追加で作成することも可能です。


#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
===========================================
PDF OCR スクリプト(EasyOCR 版)
===========================================

【使い方】
    python ocr_pdf_easyocr.py input.pdf output_dir [start_page] [end_page]

【例 1】PDF 全ページを OCR する
    python ocr_pdf_easyocr.py C:\work\input.pdf C:\work\ocr_out

【例 2】PDF の 1〜300 ページだけ OCR する
    python ocr_pdf_easyocr.py C:\work\input.pdf C:\work\ocr_out 1 300

【出力】
    ・page_0001.txt のような各ページの OCR 結果
    ・all_pages_combined.txt(全ページ結合)

事前準備(必要なライブラリ)
    pip install easyocr pdf2image pillow

※ Poppler をインストール済みで PATH が通っていること
※ Windows の場合 poppler の bin フォルダを PATH に追加
-------------------------------------------
"""


# ==========================
# ======= 設定(定数) ======
# ==========================

POPPLER_PATH = None            # Poppler の bin フォルダ。PATH が通っていれば None のまま
LANG_LIST = ['ja', 'en']       # OCR 言語リスト
DPI = 300                      # PDF → 画像変換の解像度(高いほど精度↑)
PAGE_START = 1                 # デフォルト開始ページ(引数があれば上書き)
PAGE_END = None                # デフォルト終了ページ(None = 最後まで)


# ==========================
# ========= import ==========
# ==========================

import os
import sys
from pathlib import Path
import numpy as np
from pdf2image import convert_from_path
import easyocr


# ==========================
# ======= メイン処理 =======
# ==========================

def ocr_pdf(pdf_path: Path, output_dir: Path,
            first_page: int, last_page: int | None):

    output_dir.mkdir(parents=True, exist_ok=True)

    print(f"[INFO] EasyOCR Reader 初期化中 (LANG={LANG_LIST})...")
    reader = easyocr.Reader(LANG_LIST)

    if last_page is None:
        last_page = 9999  # 最大値(最後のページまで試す)

    combined_text_lines: list[str] = []

    page_num = first_page
    while page_num <= last_page:
        print(f"[INFO] ページ {page_num} を画像化…")

        try:
            images = convert_from_path(
                pdf_path,
                dpi=DPI,
                first_page=page_num,
                last_page=page_num,
                poppler_path=POPPLER_PATH
            )
        except Exception as e:
            print(f"[INFO] ページ {page_num} は最終ページの可能性: {e}")
            break

        if not images:
            print(f"[INFO] ページ {page_num} の画像取得なし → 終了")
            break

        image = images[0]

        print(f"[INFO] ページ {page_num} を OCR 中…")
        result = reader.readtext(np.array(image), detail=0)
        page_text = "\n".join(result)

        # ページごとに保存
        page_txt_path = output_dir / f"page_{page_num:04d}.txt"
        page_txt_path.write_text(page_text, encoding="utf-8")
        print(f"[INFO] → {page_txt_path} に保存")

        # 結合用
        combined_text_lines.append(f"===== Page {page_num} =====")
        combined_text_lines.append(page_text)
        combined_text_lines.append("")

        page_num += 1

    # 全ページ結合ファイル
    combined_txt_path = output_dir / "all_pages_combined.txt"
    combined_txt_path.write_text("\n".join(combined_text_lines), encoding="utf-8")
    print(f"[INFO] 全ページ結合テキスト → {combined_txt_path}")


# ==========================
# ========= エントリ =========
# ==========================

def main():
    if len(sys.argv) < 3:
        print("使い方: python ocr_pdf_easyocr.py input.pdf output_dir [start_page] [end_page]")
        sys.exit(1)

    pdf_path = Path(sys.argv[1])
    output_dir = Path(sys.argv[2])

    first_page = int(sys.argv[3]) if len(sys.argv) >= 4 else PAGE_START
    last_page  = int(sys.argv[4]) if len(sys.argv) >= 5 else PAGE_END

    if not pdf_path.exists():
        print(f"[ERROR] PDF が見つかりません: {pdf_path}")
        sys.exit(1)

    ocr_pdf(pdf_path, output_dir, first_page, last_page)


if __name__ == "__main__":
    main()



2025/10/08

2025/10/10まで Xiaomi POCO F7 12G+256GBはアマゾンより楽天市場のほうがポイント分だけ安い、お得かもしれない

アマゾンの10/10までのセール
Xiaomi POCO F7 12GB+256GBが48,980円税込送料無料で安い。
POCOはシャオミにサブブランド。
コスパが良いことで知られている。

Xiaomi POCO X7 Pro 8GB+256GB:38,364円
Xiaomi POCO F7 12GB+256GB 48,980円
■勝手なおすすめ
とりあえず使えるサブスマホほしい:X7 Pro(メモリ8GB&Dimensity 8400-Ultra)
ちゃんと使えるメインなスマホ:F7 Pro(Snapdragon 8 Gen 3&メモリ12GB)
AI使いたい&コスパ良いスマホほしい:F7(メモリ12GB&Snapdragon 8 Gen 4)
(※F7無印がF7Proよりも上のSnapdragon 8 Gen 4なので欲しくなる。)

AI使うときにメモリが8GBと12GBだと使えるモデルが違うのでかなり変わってくる。
AIを期待するならメモリは容量大きいものをおすすめ。






がっちり使いたいならXiaomi 15T Pro 12GB+256GB
カメラはライカSummilux 5倍望遠カメラ搭載、MediaTek Dimensity 9400+搭載。
FeliCa対応、IP68防水防塵のフル機能。
109,800円でも他メーカーよりも安い。

もっとすごいXiaomi 15 Ultra 16GB+512GBもある。
ライカのカメラがすごい。というか普通にコンデジのカメラ。
Snapdragon 8 Elite Mobile Platformがすごい。
やたらとアフターサービスがすごい。
値段以上の満足感ありそう。


楽天市場にもシャオミストアあり。
もちろんXiaomi POCO F7 12GB+256GBもセールに。
そちらの値段は54,980円。
高いの・・・と思いきや6,000円OFFクーポンあり。
54980-6000=48980円、ということは同じ?
でも楽天カードや楽天モバイルならポイントが7%に。
48980円+3428ポイント

Xiaomiシリーズみて比較して方が良さそう。
これだけ幅広く展開しているがすごい。

Xiaomi POCO X7 Pro 8GB+256GB:38,364円
Xiaomi POCO F7 12GB+256GB 48,980円
Xiaomi POCO F7 Pro 12GB+256GB 64,980円
Xiaomi 15T Pro 12GB+256GB:109,800円
Xiaomi 15 Ultra 16GB+512GB:151,874円

Xiaomi POCO X7 Pro:MediaTek Dimensity 8400-Ultra
Xiaomi POCO F7 :Snapdragon 8 Gen 4
Xiaomi POCO F7 Pro:Snapdragon 8 Gen 3
Xiaomi 15T Pro:MediaTek Dimensity 9400+
Xiaomi 15 Ultra:Snapdragon 8 Elite Mobile Platform

2025/09/13

markdownとかテキストデータの同期 QOwnNotes+ownCloud obsidianの代替としても

当たり前でざっくりな話しですが「テキストデータで複数デバイスのファイル同期な使いやすいな」です。
(もちろんObsidianが一番使いやすいはず。使ってないけど。)

QOwnNotes+ownCloud 10( PHP) + ownCloud DesktopApp windows版で使っています。
PCからはQOwnNotesで、スマホはアプリやブラウザからで。
編集は主にPCから。スマホは見るだけや他スタッフとの情報共有で。
dropboxなども便利ですが、シンプルさだと自前クラウドがとても便利。
あれこれの機能は無いければ、必要最低限なところに使っていて落ち着く。

-------関係ない話し-----

すごく古い話しですが、PHPはマンモス本で学んだ懐かしい思い出が・・・。
PHP3とか4のときに。
更新はあったけど未だに便利に使えるのがすごい。

PHP最初の開発者のRasmusの名言が物語っているのかも。
プログラミングが嫌いだから誰かの作ったコードを再利用できる便利なツールを作った、みたいな。
https://en.wikiquote.org/wiki/Rasmus_Lerdorf

学ぶなら独習PHPがいいと思いますが、他言語を知っているならウェブの情報だけでもできそう。
(ベタな話しですが、本屋で「PHPの本を」と聞くと松下幸之助のPHPを教えてくれる。普通のPHPはこっち。)

・PHP PHP: Hypertext Preprocessor
・PHPとは、Peace and Happiness through Prosperity
--------------





QOwnNotes
ownCloud PHP版
conoha WINGで運用
※OwnClowdはPHP7系での動作なので注意を。

下記のような月額500円以下のサーバーを使って試してみるのもいいかも。
WebARENA Indigo®

限界があるけど、SSHが使えると共有サーバーでも使用範囲が広がるかも。
バリューサーバー
リトルサーバー

もちろん、さくら、X-server、ロリポップもSSH使えます。

[

2025/08/22

Nexcloud31のインストール Conoha Wing 共有サーバーでの自前クラウド構築

共有サーバーなので推奨ではありません。
Nextcloudをconoha Wingにインストールしてテスト動作
Nextcloud 31.0.7です。
Nextcloud https://nextcloud.com/


ウェブアクセスOK
iPhoneアプリからのアクセスもOK
共有サーバーなのでテスト的に。
本格運用は専用サーバーやVPSで。
OwncloudもNextcloudもPHP動作なので共有サーバーでも動く。

こういうの使わなくてもGoogleドライブもOneDriveもある。
Dropboxもある。
DS124/GやDS225+ /GでSynology Drive使えばDropboxみたいに使える。
ファイル共有というよりは、アクセス制限の軽い掲示板+αなら使えるのかもと思う。


DS225+ /G https://amzn.to/3VaAXw3
5.7万くらい

DS124/G https://amzn.to/3HJN9AY
2.5万くらい
※Synlogyだとlinux動いているのでNextcloudもインストールできる。

DropboxPlus 3年版 2TB  https://amzn.to/4fSZeAE
4.3万くらい ※5000ポイントあるので実質3.8万くらい

※価格は2025/08/22時点参考




Nextcloud インストール手順とトラブルシューティング

1. ZIPファイルのダウンロードと展開

Nextcloud公式インストールガイド

最新版のNextcloud ZIPファイル

curl -L https://download.nextcloud.com/server/releases/latest.zip -o nextcloud.zip
unzip nextcloud.zip -d /var/www/html/cloud

2. ファイルの整理とパーミッション設定

展開された`nextcloud`フォルダ内のすべてのファイルを`/var/www/html/cloud`ディレクトリに移動します。また、不要なファイルやフォルダを削除し、適切なパーミッションを設定します。

mv nextcloud/* cloud
mv nextcloud/.htaccess cloud
mv nextcloud/.reuse cloud
mv nextcloud/.user.ini cloud
rm -rf nextcloud nextcloud-10.0.10.zip

3. ファイルの所有者とグループ設定

`apache`ユーザーと`abc`グループに、`/var/www/html/cloud`ディレクトリ内のすべてのファイルとディレクトリの所有権を変更し、グループ書き込み権限を付与します。

chown -R apache:abc /var/www/html/cloud
chmod -R g+w /var/www/html/cloud


ここでインストールしたURLにアクセス
DBなどを設定して、ログインして正常動作しているのか確認する。
※DBアドレスは「localhost」がNGなので、IPアドレスで指定
※それ以外の設定はそのままでいけるはず。

使えるならここまででOK。
これ以降はnextcloudの管理者設定>概要のメッセージを見て必要に応じて対応する

4. リバースプロキシ設定

.htaccess に以下を追加(Nextcloud設置ディレクトリ直下)

一番下に追加

<IfModule mod_headers.c>
  RequestHeader set X-Forwarded-Proto "https"
  RequestHeader set X-Forwarded-Ssl "on"
</IfModule>

Nextcloud 側設定(config.php)

一番下に追加

'trusted_proxies' => ['127.0.0.1'], // WING 環境の localhost プロキシを信頼
'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR'],

5. PHP メモリ制限の設定

※これはphp.iniでの設定かも

.htaccess 一番下にに以下を追加

php_value memory_limit 512M
php_value upload_max_filesize 200M
php_value post_max_size 200M


6. HTTPSアクセス対応

iphoneアプリからアクセスできず。下記でOKになった。

◯安全ではないHTTP経由でのサイトへのアクセス。代わりにHTTPSを要求するようにサーバーを設定することを強くお勧めします。そうしないと「クリップボードにコピー」や「service workers」のような重要なウェブ機能が動作しません! 詳細については、ドキュメント↗を参照してください。

下記でiphoneアプリからOKになった。

Nextcloud 側の設定

config/config.php に追加:
'overwrite.cli.url' => 'https://yourdomain.com/nextcloud',
'overwriteprotocol' => 'https',

7. ブルートフォース対策制限の対応

◯あなたの接続アドレスが"1.2.3.4"として識別され、現在ブルートフォース対策制限が適用されているため、さまざまなリクエストのパフォーマンスが低下しています。接続アドレスがあなたのアドレスでない場合、プロキシが正しく設定されていない可能性があります。詳細については、ドキュメント↗を参照してください。

config.phpに追加

'trusted_proxies' => [
  '111.222.333.444',          // 表示されているプロキシIP
  '111.222.0.0/16'          // 共有環境でIPが変動するならCIDRで広めに(安全性とトレードオフ)
],
'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP'],

※'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR']をリバースプロキシでやっているなら重複するので調整する