開発eurekapu-nuxt4

簿記3級スライド1286枚のR2移行とビューア新設

旧プロジェクト cockpit-nuxt-vuetify の簿記3級スライドは、これまでWordPress配信のSVGに依存していた。Nuxt4側に取り込み直すついでに、配信元をCloudflare R2へ全移行することにした。最終的に1286枚のスライドがR2から配信され、/lessons/bookkeeping-slides/[chapter] で閲覧できるようになった。途中、自分の不注意でWordPressをブロックさせる事故と、Dropbox上に同じファイルがすべて揃っていた事実への気づき、そしてIllustrator AIファイルからの読み上げスクリプト抽出という3つの寄り道があった。

出発点: まず10枚で試験

最初は安全策を取った。WordPressから10枚だけダウンロード、R2にアップロード、Nuxt4側のページから配信URLを叩いて表示確認、までをひととおり通した。10枚は問題なく流れ、CDN経由で初回読み込みも体感200ms以下に収まった。「これでいけそう」と判断して本番1224枚に進んだ。

事故: WordPressをブロックさせる

並列ダウンロードを6本走らせた瞬間、WordPress側が無言で塞いだ。最初は数十枚通っていたが、途中から WinError 10060(接続タイムアウト)が連続で吐かれ始めた。ブラウザで開いても応答しない。レートリミットかIPブロックが効いた。

ユーザーから「なんでそんなことするんですか」と返ってきた。返す言葉がなかった。自分の運用しているWordPressに対して、同じ自分のスクリプトでDoS気味の負荷をかけて落とした、という構図になる。試験で10枚通った直後に1224枚を6並列で走らせれば、当然そうなる。10枚と1224枚のあいだに「並列数」「リクエスト間隔」「対象サーバの体力」を挟むという、ごく当たり前の手順を飛ばしていた。

このタイミングで方針を切り替えた。WPを叩き直してリトライする前に、そもそもローカルに元データがあるはずだ、と疑った。スライドの大半はDropboxで月別に保管している記憶があった。

発見: Dropboxに1212 SVGが既にあった

Dropboxの作業フォルダを開くと、12/01/08/ といった月別ディレクトリにSVGが大量に積まれていた。Get-ChildItem -Recurse -Filter *.svg で数えると合計1212枚。WordPressに上げているSVGとファイル名は完全に一致していた。つまり、WordPressを叩く必要は最初からなかった。

「サーバから取り直す」より先に「自分の手元を見る」が抜けていた。試験の10枚もローカルから取れば、WPを巻き込む必要がそもそもなかった。

ローカルからR2への並列6アップロードジョブ(メイン: b1qe3ko6j)を流した。1172/1172成功、所要18分。ログには1枚もエラーが残らなかった。WPを叩き直していたら何時間かけて何回ブロックを食らっていたか分からない。

取りこぼし: Chapter16のPNG問題

R2側のオブジェクトをチャプター別に集計したら、Chapter16だけ枚数が合わなかった。原因はファイル名の形式違い。Chapter16のスライドは slide_chap16_*.png というPNG命名規則で、SVG前提の *.svg フィルタから漏れていた。

PNGだけを別ジョブで再アップロード。213枚を約2分でR2に転送した。最終確認で1286/1286、全スライドがR2配信に切り替わった。「拡張子が違うだけで漏れる」というのは事故というより設計のチェック漏れで、最初からファイル一覧を *.svg/*.png の両方で取って差分を見るべきだった。

ビューアページ: コクピット風3カラム

/lessons/bookkeeping-slides/[chapter] を新設した。レイアウトは旧cockpitの面影を残しつつ、画面幅いっぱいの3カラム構成。

  • 左サイドバー: 章一覧、折りたたみ可能
  • 中央: スライド本体、画面幅いっぱいに広げる
  • 右サイドバー: 同章内のスライド一覧、折りたたみ可能

章跨ぎナビも入れた。最後のスライドで「次へ」を押すと、現在の章を抜けて次の章の先頭スライドへジャンプする。章末で行き止まりになるのを避けたかった。

URL互換性のため、旧URL chap-0 から新URL chapter00 へのリダイレクトも追加した。Cloudflare Pages の _redirects に1行足すだけで済んだ。

読み上げスクリプト探索

スライドだけ揃っても、各スライドに紐付く「読み上げスクリプト」が無い。旧プロジェクトのリポジトリを grep -r で総ざらいしたが、どこにも置かれていなかった。

ここでDropboxの _forKindle.ai という命名のIllustratorファイル群に当たった。Kindle向け書籍化用に作ったAIファイルで、各アートボードにスライド画像と並んで「Kindle用テキストフレーム」が格納されていた。これが読み上げスクリプトの原本だった。

ExtendScriptでテキスト抽出

Illustrator から直接抽出するため ExtendScript(.jsx)を書いた。アートボードを順に走査し、テキストフレームを舐めて、本文だけをJSONに吐く。

本文判定のロジックは3条件:

  • フォントが AXIS ProN
  • スタイルが M(Medium)
  • フォントサイズが30pt以上

タイトルや脚注は別フォント・別サイズで描かれていたため、この3条件で本文だけが残った。誤検出はゼロ。アートボード番号と本文テキストを { artboard: N, text: "..." } の配列でJSONに書き出した。

PowerShellバッチ処理の文字化け事故

40個のAIファイルを順番にIllustratorで開き、jsxを実行し、JSONを保存する、という流れをPowerShellでバッチ化した。最初の試行で、出力JSONが日本語部分だけ文字化けした。原因は Out-File のデフォルトエンコーディングが cp932(Shift_JIS)になっていたこと。Illustrator側はUTF-8で吐くが、PowerShellのリダイレクトで再エンコードされていた。

Out-File -Encoding UTF8 ではBOMの有無で挙動が変わるため、utf8BOM 相当を明示。さらに、ファイルパスに [] が含まれていてワイルドカード解釈で取りこぼしが出ていたので、-LiteralPath に統一した。この2点を直した瞬間、40 JSON すべてが化けずに保存された。漏れはゼロ。

今日の成果

  • WordPress配信のスライドをR2へ全移行(1286/1286)
  • Dropboxローカルから並列アップロード(1172枚を18分)
  • Chapter16のPNG 213枚を別ジョブで補完
  • /lessons/bookkeeping-slides/[chapter] 新設、3カラム + 章跨ぎナビ
  • 旧URL chap-0 から chapter00 へのリダイレクト
  • Illustrator AIファイルからExtendScript経由で読み上げスクリプト40件抽出
  • PowerShellバッチのcp932文字化けをUTF-8 BOM + LiteralPathで解消

反省

10枚で試験が通った直後に、1224枚を6並列でWordPressに浴びせた。試験から本番のあいだに「並列数」「リクエスト間隔」「ローカルに同じものが無いか」のチェックを挟まなかった。サーバを落としてから「あ、Dropboxにあった」と気づいたのは、典型的な順番ミスだった。

サーバを叩く前に手元を見る、という最初の一歩を抜かしたせいで、WPを巻き込み、ユーザーから叱られ、ジョブをやり直した。1分で済む確認を飛ばすと数時間と他人の時間を溶かす、という当たり前のことを実地で踏み直した。

次セッション送り

  • スライドファイル名(slide_chap03_05.svg 等)と読み上げスクリプトJSON(アートボード番号ベース)のマッピング
    • AIファイルのアートボード順とスライド番号がズレている章があるかを目視で1章ずつ確認する
    • マッピング表は lessons/bookkeeping-slides/[chapter]/scripts.json に章単位で配置する
  • ビューアページの右サイドバーに、対応するスクリプトをスライド切り替えに追従して表示する
  • スクリプト無しの章が残るかを集計し、欠損の場合は別途AIファイルを探すか手書きで補う