megutech

自身の備忘録として主にWEBサーバー周りの技術について投稿しています。

ArcThumb をリリースしました — Explorer でコミックアーカイブのサムネイルを出す Rust 製シェル拡張

ArcThumb

ArcThumb という Windows 用のシェル拡張を作りました。
ZIP / CBZ / RAR / CBR / 7z / CB7 / CBT、それからEPUB・FB2・MOBI/AZW/AZW3 を Explorer のサムネイルとプレビューペインに表示します。

citrussoda.com

GitHub: https://github.com/citrussoda-com/ArcThumb
インストーラ: Releases から ArcThumb-Setup.exe をどうぞ。

なぜ作ったか

長らく CBX Shell を使っていたのですが、開発が止まって久しく、WebP など新しいフォーマットに対応していません。
後継として DarkThumbs もありますが、こちらも更新は止まっています。

ファイルにぱっとサムネイルが付いていると、納品アセットを管理するときに何のファイルか一目で分かって便利だったのですが、 WebP に対応していなくて、たまーに不便を感じていました。 しかし 2026 年現在、各種 AI のおかげで「欲しいな。でも作るのは面倒だな。」と思ったら作ってもらえる時代になりました。 ぱっとWebを探してもなかったので、ならいっちょここでつくるかな、と。

Rust を選んだのは、null 安全で最近はやりとのことだったので、書いてみたかったというだけです。
Rust は書いたことがなかったので、Claudeの助けを得ながら作成しました。

すいません、本当はほとんどClaudeが書きました。。。

できること

  • アーカイブの中の最初の画像(あるいは表紙らしき画像)を Explorer のサムネイルとして表示
  • EPUB は OPF マニフェスト、FB2 は <coverpage>、MOBI/AZW/AZW3 は EXTH 201 を読んで「ちゃんとした表紙」を選ぶ
  • IPreviewHandler も実装したので、Alt+P のプレビューペインにも同じ表紙が出る
  • 設定 GUI(arcthumb-config.exe)で対応拡張子・並び順・表紙ファイル名の優先・プレビューペインのON/OFF・言語(日本語/英語)を切り替え
  • インストールは %LOCALAPPDATA% 配下のユーザー単位。管理者権限はいりません

対応している画像フォーマットは JPEG / PNG / GIF / BMP / TIFF / ICO / WebP。
AVIF・HEIC・SVGはまだです(リファレンスデコーダが重い C 依存を引き連れてくるので保留中)。

対応フォーマットや使い方の詳細は、ArcThumb の紹介ページ にまとめています。

スクリーンショット

各種画像形式をいろいろな圧縮方法でサムネイル表示しているエクスプローラーのスクリーンショット

ArcThumb config image

使い方

  1. Releases からインストーラを落として実行
  2. Explorer でコミックフォルダを開く

もしサムネイルが出ない場合は、設定 GUI の「サムネイルを再生成」ボタンを押してください。
Windowsのサムネイルキャッシュを掃除して、エクスプローラーを再起動します。

既知の制限

  • AVIF / HEIC / SVG / DjVu は未対応
  • アニメ GIF・アニメ WebP は 1 フレーム目だけ
  • 暗号化アーカイブは開けません
  • デコード後 256MB を超える画像はスキップ

ライセンスと今後

MIT または Apache-2.0 のデュアルライセンスです。
広告も Pro 版もありません。
もし気に入った方は https://github.com/sponsors/remu1519 から一杯のコーヒーをいただけると嬉しいです。

正直、長期メンテを約束できる状況ではないです。
ただ、自分が毎日使うものなので、自分が困らない範囲では直し続けます。
バグ報告と要望は https://github.com/citrussoda-com/ArcThumb/issues に。

PDFと画像をピクセル単位で比較できるツール「PDF Diff」

デザイナーから納品されたデザイン画像をもとに、PDFkitでPDFを生成する仕事があった。
ピクセルパーフェクトを求められたものの、元画像と生成したPDFを目視で見比べるのは限界がある。
ブラウザ上でピクセル単位の比較ができるツールがほしかったので作った。

citrussoda.com

何ができるか

PDFと画像(PNG/JPEG/WebP)をアップロードすると、キャンバス上で重ねて表示できる。

比較モードは3つある。

オーバーレイモードでは画像の透明度を変えながらPDFに重ねて確認できる。
差分モードではピクセルの違いを赤でハイライト表示してくれる。
スライドモードでは左右にスワイプしてPDFと画像を切り替えられる。

透明度・位置・スケール・ズームもスライダーやマウス操作で調整できるので、細かいズレも見つけやすい。

使い方

  1. PDFと画像をドラッグ&ドロップでアップロード
  2. 表示モードを選んで透明度や位置を調整
  3. ズームしてピクセル差分を確認

操作はキーボードショートカットにも対応していて、ホイールでズーム、ドラッグで画像移動やキャンバスのパンができる。

技術的な話

  • PDF描画は pdf.js を使用
  • Canvas APIで2つのレイヤーを重ね合わせて描画
  • 差分モードはピクセルごとにRGB値を比較して差分を赤チャンネルに出力
  • ファイルはサーバーに送信せず、すべてブラウザ内で処理される

データがサーバーに送られないので、社外秘のデザインデータでも安心して使える。

まとめ

欲しい機能のサービスが見当たらないとき、作りたいものの仕様をしっかり固めてあげればAIがさくっと作ってくれる。
こういったシンプルなツールなら本当にあっという間にできてしまう。

その分本業に集中できるし、開発のスピードも上がる。
すごい時代になったなと思う。

citrussoda.com

RDS PostgreSQL Blue/Greenデプロイでメジャーアップグレード

Blue/Green デプロイを利用してPostgreSQLのメジャーバージョンアップを試みた際、何回かエラーに出会い躓いたので手順を残します。

環境

サービス バージョン
Amazon RDS PostgreSQL 15.x から 18.x
アップグレード方式 Blue/Green デプロイメント

手順

1. パラメータグループの作成

Blue/Greenの同期に論理レプリケーションを使用するため、BlueとGreen両方で rds.logical_replication を有効にしたパラメータグループが必要です。

「パラメータグループ」から「パラメータグループの作成」を選択して、以下の2つを作成する。

用途 ファミリー グループ名の例
Blue用 postgres15 postgres15-logical
Green用 postgres18 postgres18-params

タイプはどちらもDB parameter groupを選択。作成したらそれぞれ rds.logical_replication1 にする。 PostgreSQL15用のParameter Groupsを作成し、rds.logical_replicationのValueを1にしている。

PostgreSQL18用のParameter Groupsを作成し、rds.logical_replicationのValueを1にしている。

2. Blue用パラメータグループの適用

  1. データベースから対象インスタンスを開き、「変更」を選択
    • パラメータグループを postgres15-logical に変更し、すぐに適用
  2. インスタンスを再起動してパラメータをin-syncにする

移行対象のRDSの編集画面でparameter groupを先ほど作ったPostgreSQL15のParameter groupに変更している

3. Blue/Green デプロイメントの作成

  1. データベースから対象インスタンスを選択
  2. 「アクション」から「Blue/Green デプロイメントの作成」を選択
  3. エンジンバージョン: 18.x
  4. DBパラメータグループ: postgres18-params
  5. 「ステージング環境を作成」をクリック

Blue/Green deployment画面でpramater groupとして先ほど作ったPostgreSQL18用のparameter groupを選択している。

これでGreen環境がPostgreSQL 18で作成されて、論理レプリケーションで同期が始まります。

4. Green環境の動作確認

  1. Green環境のステータスが「利用可能」になるまで待つ
  2. Green環境のエンドポイントに接続して、アプリが正常に動くか確認する

5. スイッチオーバー(本番切替)

  1. データベース一覧からBlue/Greenデプロイメントを選択
  2. 「アクション」から「スイッチオーバー」を選択
  3. 確認して実行

ダウンタイムは1分未満で、Green環境が本番に昇格します。エンドポイント名も自動で入れ替わるので、アプリ側の接続先を変える必要はありません。

対象インスタンスグループを選択し、switch overを押下しようとしている

6. 事後作業

  1. アプリの動作確認
  2. バージョンが18.xになっていることを確認
  3. 問題なければ旧Blue環境(例: xxxx-old1)を削除
  4. データベース一覧に残っているBlue/Greenデプロイメント(例: bg-deployment-1)も手動で削除する(旧Blue削除後も自動では消えない)
  5. 必要であれば本番インスタンスのパラメータグループを default.postgres18 に戻す
  6. 不要になったパラメータグループ(postgres15-logicalpostgres18-params)を削除

注意事項

再起動によるダウンタイム

数秒のダウンタイムを伴う再起動が3回発生する。

  1. パラメータグループの適用時
  2. スイッチオーバー時
  3. パラメータグループを戻すとき

3回目は必須ではなく、rds.logical_replication が有効なまま残るだけなので、次のメンテナンスウィンドウで自動適用されるのを待ってからパラメータグループを消してもいい。

料金

Green環境が存在する間はRDSインスタンス2台分の料金が発生する。

つまずいたポイント

最初はパラメータグループを作らずにそのままBlue/Greenデプロイメントでメジャーアップグレードしようとした。
すると以下のエラーが出た。

The current parameter group associated with the blue instance (blue-green-params-xxx) is not the default.
You need to explicitly specify a new parameter group (default or custom) for the green instance when creating a blue/green deployment with the --target-engine-version option set to an engine version that's different from the blue instance.

「Blueのパラメータグループがデフォルトじゃないから、Greenに使うパラメータグループを明示的に指定してくれ」ということらしいが、Blueはデフォルトのパラメータグループ。
どゆこと・・・。

ただ何故かマイナーバージョンアップでなら問題ない。
そこでマイナーバージョンアップを実施し、スイッチオーバーする前にGreenを18に上げてからすればいいのでは?と思って試したら、今度はこれ。

Major Version Upgrade not supported for Postgres Read Replica DB Instances

「Read ReplicaのPostgreSQLはメジャーバージョンアップグレードに対応していない」とのこと。
Blue/Greenデプロイメントで作成されるGreenはRead Replicaとして扱われるため、この方法もダメだった。

結局、正しい対処方法はBlue用・Green用それぞれに rds.logical_replication を有効にしたパラメータグループを作って割り当てること。 本記事の手順1・2がそれにあたる。
マイナーバージョンアップではAWSが勝手にやってくれるが、メジャーの場合は自分で用意しないといけない。

感想

簡単な作業だと思ったのに、やっぱり初めての作業はちょいちょい躓くんだなぁ。
ステージングで確認して良かった。

X (Twitter) のIntent URL `/intent/tweet` vs `/intent/post`

WebからXへシェアする機能を実装したところ、Xアプリが起動 -> アプリ内ブラウザ起動 -> Safariへリダイレクト -> Xアプリ起動という無限ループに陥った。
調査の結果、Intent URLのエンドポイントによって挙動が異なることが判明したので備忘録として残す。

環境

発生した問題

Webサイトにシェアボタンを実装し、/intent/post を使用したところ以下の無限ループが発生:

  1. Xアプリが起動
  2. アプリ内ブラウザが起動
  3. アプリ内ブラウザがデフォルトブラウザ(Safari)を開こうとする
  4. SafariがXアプリを開こうとする
  5. 1に戻る

調査

Intent URLの種類

以下の2種類のエンドポイントが存在する:

https://x.com/intent/tweet
https://x.com/intent/post

挙動の違い

/intent/tweet

  • Xアプリがネイティブで起動
  • 投稿画面が直接開く
  • 正常動作

/intent/post

  • Xアプリは起動する
  • しかしアプリ内ブラウザが起動
  • そのブラウザ内で投稿画面が表示される
  • 無限ループの原因

公式ドキュメントの確認

X Developer Platform の公式ドキュメントを確認したところ、/intent/tweet のみが記載されている

/intent/post については記載がない。

対応

Intent URLの変更

- https://x.com/intent/post?text=...
+ https://x.com/intent/tweet?text=...

結論

  • 使用すべきは /intent/tweet
  • 公式ドキュメントに記載されている
  • 挙動が安定している

感想

ブランディングで「これからはPostだ」という方針だったので /intent/post を使っていたが、無限ループに陥って調査したところ、公式ドキュメントには記載すらされていなかった。
結局 /intent/tweet に戻すことで解決した。

公式ドキュメントは大事。

シャドーイングも声真似も全部これ1つ。音声分析×練習ループで劇的成長できる「Rekoe」

この度個人で開発している 声トレ/声まね練習アプリ『Rekoe』 を公開しました。
この記事では、Rekoeの概要、主な機能、使い方、開発の背景、そして今後のアップデート計画をまとめます。
初めての方でも使い始めやすいよう構成しています。

こんな人におすすめ

ダウンロード

iOS

Rekoe

Rekoe

  • Tsuyoshi Matsunaga
  • エンターテインメント
  • 無料
apps.apple.com

Android

play.google.com

主な機能

1)録音・比較再生

  • ファイルやiTunesiOS限定)から素材を取り込み、音源を聞きながらワンタップ録音。
  • 録音後簡単に音声比較が可能です。

2)スペクトラム表示 + ピッチ/フォルマント

  • Hz×dBのスペクトラムで特徴を可視化。ピッチ推移とフォルマントで“どこを直すか”が明確に。

3)ディレクトリ・ソート・メモ

  • 録音や素材を整理し、メモで管理。
  • 練習ログを残すことで成長が可視化できます。

4)速度調整(タイムストレッチ)

  • フレーズをゆっくり再生して聞き取り→定着。
  • 慣れたら再生速度を戻して実戦に近づけます。

使い方

STEP 1:目標を決める

好きな声・再現したい声のフレーズを用意します。

STEP 2:聞く → 真似る → 録る

音源を聞きながら録音。余計な間はカットすると比較が楽です。

STEP 3:波形・スペクトラム・ピッチで差分確認

  • タイミング: 元音声と同じ間の取り方ができているか
  • ピッチ:高すぎ・低すぎ/上下の安定感
  • スペクトラム:出てほしい帯域が十分か
  • フォルマント:山の位置関係(F1/F2/F3)

STEP 4:1つだけ直す

一度に全部直そうとせず、「今日はピッチを合わせる」など1テーマに絞るのがコツです。

価格と機能(現時点)

  • 基本:無料(広告あり)
  • 広告非表示(300円)
  • 機能拡張パック(800円)
  • コンプリートパック(1000円)

開発の背景(裏話)

ポーズクロッキーというアプリを試した際、お手本を上に表示し、下でクロッキー、指定秒数で自動でお手本とクロッキーを重ね合わせ比較され、そのまま次のクロッキーへ。 という体験がよすぎて、これの音声版欲しいなと思ったのがきっかけ。

ここで自分が本当に感動したのは「重ね合わせ」そのものではなく、クロッキー→自動重ね合わせ→すぐ次のクロッキーという“練習ループが勝手に回り続ける設計”だった。 面倒な段取りをアプリが肩代わりするから、ユーザーは練習だけに集中できる。この思想を音声練習に持ち込みたかった。

ただ音声比較の場合、重ね合わせだけだと負担が大きい。そこで、FFT・ピッチ推定・フォルマント近傍の山検出による可視化を組み合わせ、素材選択→録音→自動比較(波形・スペクトラム・ピッチ)→次テイクまでを短い導線で回せるようにした。短いループを高速に回せるから、上達の手応えが得やすい、はず。

よくある質問(FAQ)

Q. 歌やアニメのセリフでも練習できますか?

  1. 可能です。短いフレーズから始めると分析・改善がしやすくおすすめです。

Q. 録音データはどこに保存されますか?

  1. すべて端末内に保存され、ネットワークにアップロードされることはありません。

Q. どの帯域を見れば良いか分かりません。

  1. まずはピッチの安定→フォルマントの順がおすすめです。

Q. 推奨の練習時間は?

  1. 毎日10〜15分の短時間継続が効果的です。

Q. iTunesの楽曲はすべてインポートできますか?

  1. 原則、DRM保護のない楽曲が対象です。Apple MusicのストリーミングやDRM付き音源は取り込めません。

SvelteKitの遅延読み込み中にエラーが発生するとクラッシュする

昨日の記事で無事遅延読み込みは成功するようになりました。

megu-tech.hatenablog.com

しかし新たな問題として、この遅延読み込み処理中にエラーが発生すると、sveltekitがクラッシュすることがあるという現象に見舞われました。

環境

Service Version
@sveltejs/kit 2.5.28
Node.js 20.9.0

コード

遅延処理中に500エラーが発生するようにしています。

import type { PageServerLoad } from './$types';
import axios from 'axios';

export const load: PageServerLoad = async () => {
  return {
    data: axios.get('https://httpstat.us/500'),
  };
};

原因

https://kit.svelte.jp/docs/load#streaming-with-promises

レンダリングの開始時点 (本来この時点で catch される) より前に遅延ロード (lazy-loaded) された promise が失敗し、エラーを処理していない場合、server が "unhandled promise rejection" エラーでクラッシュする可能性があります。

公式ドキュメントに書いてありました。

対策

対策も公式ドキュメントに書いてありました。

fetchを使う

SvelteKit の fetchload 関数で直接使用する場合は SvelteKit がこのケースを処理してくれます。

Axios等を使っている場合、adapterとしてSvelteKitのfetchを使うようにしてあげるといいでしょう。

import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  return {
    data: fetch('https://httpstat.us/500'),
  };
};

握りつぶす

それ以外の promise の場合は、何もしない catch (noop-catch) をアタッチし、処理済であることを明示するだけで十分です。

import type { PageServerLoad } from './$types';
import axios from 'axios';

export const load: PageServerLoad = async () => {
  return {
    data: axios.get('https://httpstat.us/500').catch(() => { error: true }),
  };
};

実施した対応

握りつぶした場合、せっかくフロントで遅延時のエラーを処理する仕組みがSvelteKitにあるにもかかわらず使用できないので、fetchを使う方法を採用しました。

感想

やはり公式ドキュメントしか勝たん。

Nginx環境でのSvelteKit遅延読み込み問題と解決策

開発環境ではSveltekitの遅延読み込みは問題なく動いていたのですが、Nginxを経由する本番環境では機能せず、ページの表示に時間がかかってしまうという現象にでくわしました。

環境

Service Version
Nginx 1.18.0
@sveltejs/kit 2.5.28
Node.js 20.9.0

コード

+page.server.ts

import type { PageServerLoad } from './$types';

const getResponse = async (): Promise<void> => {
  await new Promise((resolve) => setTimeout(resolve, 10000));
};

export const load: PageServerLoad = async () => {
  return {
    data: getResponse()
  };
};
<script lang="ts">
    import type { PageData } from './$types';
    export let data: PageData;
</script>

{#await data.data}
    loading...
{:then data}
    遅延読み込み成功
{:catch}
    エラーが発生しました
{/await}

原因

プロキシ (例えば NGINX) を使用している場合は、プロキシされたサーバーからのレスポンスをバッファしないようにしてください。

公式ドキュメントに書いていました。

対応

NginxでStreamをbufferしないよう、下記設定を追加しました。

proxy_buffering off;

感想

公式ドキュメントしか勝たん。