スライド制作を3時間→5秒に縮めた話 ─ 「Inkscapeで手修正しない」を実現するまで
はじめに
競走馬の予想スコアを算出する自作Python予測システム(V5py / V58py)を個人開発しつつ、その週次の成果をYouTubeチャンネル「71才隠居ジジィの競馬新聞」で動画にして公開しています。
前回の記事「YouTube動画のスライド制作を20時間→3時間に減らした話」では、xlsxの予想結果表をそのままSVG化する スライド画像加工.py によって、動画1本分のスライド制作を20時間から3時間まで削った経緯を書きました。
今回はその続きです。3時間はまだ「全部自動で生成した後、Inkscapeを開いて何枚か手直しする」時間を含んでいました。この記事は、その手直しをゼロにするまでの話——3時間を1.5時間、最後は5秒にした、2026年7月中旬〜下旬の開発記録です。
今回作ったツール
| ツール | 役割 |
|---|---|
batch_slide_auto.py |
スライド画像加工.pyを無改変のままimportし、①〜④+テキスト自動書き込みまでを対話なしで一括実行するバッチドライバー |
json_to_text.py |
Claude.aiの会話エクスポート(JSON)を日本語訳付きでテキスト化するツール(本記事のような開発記録を作るための資料整理用) |
メインの スライド画像加工.py 自体もこの期間にNo.25〜60まで、大小合わせて30件以上の改修を重ねています。
出発点:「動いてはいるが、Inkscapeで直さないと使えない」スライド
前回時点で、xlsxの表をSVGスライドへ自動合成する仕組みはできていました。しかし実際に週次で回してみると、次のような「動くには動くが、微妙に気持ち悪い」状態が残っていました。
- 馬柱5走:過去走数が少ない・多いレースで表がキャラクターと重なる、または不自然に伸び縮みする
- 推しレース表:上位5抜粋の小さい表が、メインの表やキャラクターとバランスの悪い位置に置かれる
- 予想表:3つの表(順位表・スコア内訳・買い目提案)の縦幅がバラバラで、そのたびにInkscapeで手で揃えていた
依頼主(Yujiさん)からの要求は一貫していました。
「私が求めているのは、出来上がったスライドをInkscapeで修正しないスライドにしてくれたらいい」
以下、この「Inkscapeで修正しない」を実現するために踏んだ3つの設計転換と、そこからさらに一歩進んだ「マーカー方式の拡張」について書きます。
① 馬柱5走:「決め打ち登録」から「実測ルックアップ+歪ませないフォールバック」へ
過去走数(N)ごとに表の位置・高さはまったく違います。最初はN=2〜4だけ、Inkscapeで手動調整した結果を実測して登録していました。
_BASYU_PLACEMENT = {
1: {"x": 77, "y": 382, "height": 298},
2: {"x": 86, "y": 203, "height": 511},
3: {"x": 95, "y": 160, "height": 571},
4: {"x": 88, "y": 150, "height": 562},
5: {"x": 86, "y": 135, "height": 562},
}
過去走数と位置・高さの関係はきれいな線形式にならなかったため、比例計算ではなくこの実測ルックアップテーブル方式を採用しています。問題は「登録されていないN」が来たときのフォールバックでした。最初は登録済みの平均高さを一律適用しましたが、これは中身の少ないN(1走)を不自然に引き伸ばし、中身の多いNを逆に押し潰すという縦方向の歪みを生みました。
「コード側対応に変えたら半分以上のスライドで表の位置・形がズレた」
修正版は発想を変えました。高さも幅と同じく原寸100%(歪みなし)をそのまま使い、位置だけキャラ画像の縦中心に表の縦中心を合わせる——揃えるべきは「見た目のサイズ」ではなく「基準となる中心線」でした。最終的にはN=1・N=5のケースも実測値としてテーブルへ正式登録し、フォールバックは「将来N=6以降が出た時の保険」という位置づけに収まっています。
② 推しレース表:「キャラ基準」から「実際に描画された表を基準にした相対配置」へ
上位5抜粋(表小)の配置は、当初「キャラの真上、キャラの横幅内で中央寄せ」という設計でした。ここに鋭い指摘が入ります。
「スライド内にはそれぞれ置かれているものがあり、そのバランスを考えないで何でこんな設計になる」
スライドの中には表大・表小・キャラの3つがあるのに、表小の配置がキャラだけを見て決まっている——これでは表大との位置関係が崩れて当然でした。修正後は、表大が実際にどれだけの幅で描画されたか(main_w、戻り値として受け取るよう変更)を使い、「表大の実描画右端〜キャンバス右端」の空間全体を基準に中央寄せする設計に変わっています。
main_w, _main_h = _place_part_svg(
root, main_path, _V10_START_X, _V10_START_Y,
left_box_w, left_box_h, scale, target_scale=_V10_MAIN_TARGET_SCALE,
scale_x_mult=main_scale_x_mult, scale_y_mult=main_scale_y_mult,
fill_box_width=main_fill_box_width)
if top5_path is not None:
top_zone_x = _V10_START_X + main_w + _V10_GAP
top_box_w = canvas_w - top_zone_x
top_box_h = bbox["y"] - _V10_GAP - _V10_START_Y
_place_part_svg(root, top5_path, top_zone_x, _V10_START_Y,
top_box_w, top_box_h, scale, target_scale=1.0, center_x=True,
center_bias=_OSHI_TOP5_CENTER_BIAS, ...)
決め打ちの座標ではなく、隣にある要素の実測値から自分の置き場所を計算する——これが「データが変わっても毎回崩れない」配置の作り方でした。
③ 予想表:2回失敗してから見えた「揃えるべきは幅だけ」という答え
3つの表(順位表・スコア内訳・買い目提案)は行数が違うため、単純に積み上げると縦の長さがバラバラに見えます。ここは実装が2回失敗しました。
1回目:縦横比を保ったまま高さをスロットにフィットさせる方式。高さは揃いましたが、副作用で幅がバラバラになりました。それを横方向・縦方向を別倍率で引き伸ばして無理やり合わせにいったところ、表の中の文字そのものが横に伸びたり潰れたりしました。
「数値なんてどうでもいい、ちゃんと使えるスライドが欲しい」
2回目:発想を変え、生成前の行の高さを調整して3表を同じ高さに強制する方式。今度は行数の少ない表(買い目提案)の行が23pxまで圧縮され、窮屈な見た目になりました。
「あり得ない」
3回目でようやく決着しました。高さを揃える強制を完全に撤去し、各表は自然な行数ぶんの自然高さのまま生成、幅だけを列の余白調整(width_scale)で3表とも同一幅に統一。最終配置は3表とも「同一の」final_scaleだけをかけます——表ごとに倍率を変えると文字サイズが表によってバラバラに見えるためです。
# 幅は3表のうち最も自然に広いものを共通目標にする
target_w_native = max(w for w, h in naturals)
for (name, limit), (w_nat, h_nat) in zip(table_defs, naturals):
width_scale_i = target_w_native / w_nat
# width_scaleで列の余白だけ調整。font_sizeは変えない=文字は歪まない
final_scale = box_w_px / target_w_native
# 3表とも同一のfinal_scaleのみを適用。
# はみ出す場合だけ、3表とも同じ比率でfinal_scale自体を追加縮小する
2回とも「高さを合わせよう」としたのが間違いで、揃えるべきは最初から「幅」だけでした。
①③に共通するのは、見た目の数値(px)を無理に揃えることと、中身(縦横比・文字サイズ)を守ることは両立しないケースがあるという教訓です。②は逆に、配置は常に「他の要素の実測値」を基準に計算すべきという教訓でした。
④ IDに依存しないマーカー方式 ─ 表題から外部データ取得結果・集計値まで
3つのレイアウトが片付いた後も、残っていた手作業がありました。①〜④のスライドの表題を、xlsxを見ながらInkscapeで毎回打ち直す作業です。
最初はSVG内の<text>要素のIDを決め打ちで書き換える案も検討しましたが、背景SVGを将来作り直すとIDが変わって壊れてしまいます。そこで採用したのが、背景SVG側にあらかじめ専用のマーカー文字列を仕込んでおき、それを検索して置き換えるという方式でした。
_TITLE_PLACEHOLDER_MARKER = "★表題自動挿入エリア★"
def _replace_title_text(svg_path, new_title):
"""
svg_path内の<text>要素を走査し、表題とみなせる要素をnew_titleに書き換える。
判定は2段階:
1. マーカー方式: <tspan>(無ければ<text>直下)の文字列が
_TITLE_PLACEHOLDER_MARKERと完全一致する要素を最優先で探す。
2. 見つからなければ、従来の日付パターン方式にフォールバックする
(背景SVGがまだ旧形式のままの場合の後方互換用)。
IDに依存しないため、背景SVGが将来作り直されても
(マーカー文字列さえ入っていれば)追従できる。
"""
bak_path = _backup_before_edit(svg_path)
tree = etree.parse(svg_path)
root = tree.getroot()
text_els = list(root.iter("{%s}text" % SVG_NS))
for text_el in text_els:
tspans = text_el.findall("{%s}tspan" % SVG_NS)
target = tspans[0] if tspans else text_el
content = (target.text or "").strip()
if content == _TITLE_PLACEHOLDER_MARKER:
target.text = new_title
tree.write(svg_path, xml_declaration=True, encoding="utf-8", standalone=False)
return True, bak_path
# ...フォールバック処理は省略
書き換え前には必ず自動でバックアップを取る(元名_bak_YYYYMMDDHHMMSS.svgとして退避)設計も、前回記事で紹介した「同名ファイルの上書き確認」の考え方をそのまま踏襲しています。
このマーカー方式がよくできていたのは、表題以外にも横展開できたことでした。7/22〜23、まだ残っていた2つの手作業——オープニングスライドの開催日・会場の書き込みと、結果検証スライドの的中率統計の書き込み——を自動化した際、どちらも同じ「専用マーカーを探して置き換える」設計をそのまま流用しています。
オープニングの開催地取得(公開されているカレンダー情報の取得処理)では、3回続けて的外れな実装を出してしまいました。正規表現の力技、文字エンコーディングの誤指定、CSSクラス名の誤推測——そのたびに「たぶんこうだろう」で実装しては、実機テストで文字化けや0件ヒットとして跳ね返されました。
「推測はやめで、確認できるものは最初から確認しろ」
実際のブラウザのHTMLソースをそのまま見せてもらったことで、ようやく決着しました。開催場名は<span class="JyoName">という専用タグで囲まれており、複雑な前方一致マッチングなど最初から不要だったのです。①〜③で繰り返した「推測で先に進んで後で覆る」という同じパターンが、外部サイトのデータ取得という新しい場面でも、形を変えて再演されていました。
結果検証スライドの的中率統計も、Inkscapeの要素ID(保存のたびに変わってしまう)に頼らず、★結果照合TOP5自動挿入エリア★という専用マーカーを新設する形で決着しています。表題1箇所のために作った仕組みが、外部サイトから取得したデータ、xlsxの集計値と、対象を広げるごとに効いてきた格好です。
[6][7]完成 ─ 「手で直す」メニュー項目がなくなった日
7/22〜23、この2つが実装できたことで、メニュー画面は最終的にこうなりました。
==================================================
スライド画像加工.py
v3.54 No.60
作業フォルダ: /home/yuji/競馬予想システム/10_video/スライド加工
==================================================
--------------------------------------------------
[1] 予想表
[2] 推しレース表
[3] 結果検証表
[4] 馬柱5走
[5] テキスト修正(表題自動書き込み)
[6] オープニングテキスト修正
[7] 結果検証テキスト修正
[8] All(1〜4まとめて実行)
[9] 終了
番号を選んでEnter:
[6]と[7]、それぞれ実際に動かした様子です。
番号を選んでEnter: 6
--- オープニングテキスト修正 を実行します ---
背景SVG(オープニング)候補(更新日時が新しい順):
[1] 基本オープニングスライドV1.svg
番号を選んでEnter(空Enterで[1]を選択): 1
対象日を入力(YYYYMMDD、Enter=本日20260723) > 20260719
🌐 開催日程カレンダーを取得...
✅ 開催場: 福島・小倉・函館
✅ 書き込み完了: 2026年 7月19日(日)福島 小倉 函館
📁 保存先: .../20260719_オープニングスライド.svg
--- オープニングテキスト修正 が完了しました ---
公開されているレースカレンダー情報を取得し、<span class="JyoName">タグから開催場名を取得。基本テンプレートの空欄に、表題([5])と同じマーカー方式でそのまま書き込まれます。
番号を選んでEnter: 7
--- 結果検証テキスト修正 を実行します ---
③結果検証表 元xlsx候補(更新日時が新しい順):
[1] osushi_kekka_hikaku_20260719.xlsx
[2] osushi_kekka_hikaku_20260718.xlsx
番号を選んでEnter(空Enterで[1]を選択): 1
✅ 推しレースTOP5集計を書き換えました:
推しレース 5件中 2件 複勝的中 的中率 40%
1位 1件 複勝的中 的中率 0%
2位 1件 複勝的中 的中率 0%
3位 1件 複勝的中 的中率 20%
全レース 35件 13件 複勝 的中率 37.1%
--- 結果検証テキスト修正 が完了しました ---
xlsxから計算したTOP5的中率が、★結果照合TOP5自動挿入エリア★というマーカー位置にそのまま書き込まれます。表題1箇所のために作った仕組みが、外部サイトから取得したデータ、xlsxの集計値と、対象を広げるごとに効いてきました。
これで①〜④・オープニング・結果検証すべてのテキストが自動で埋まるようになり、動画1本分のスライドから「手で直す」工程がひとつも残らなくなりました。
まとめ:数字と、数字にならない教訓
前回記事の「20時間→3時間」から、今回さらに縮みました。
20時間 → 3時間 → 1.5時間 → 5秒
手修正込みで1.5時間まで縮んだ実感を確認したのが7月中旬、そしてbatch_slide_auto.pyと一連のレイアウト修正により、最終的に手修正が一切不要な状態で5秒まで到達しています。
数字に出ない収穫もいくつかあります。
- px単位で見た目を揃えることと、中身(縦横比・文字サイズ)を守ることは両立しないことがある。 無理に揃えようとすると必ずどこかが歪む。揃えるべき軸を1つに絞ることが先。
- 配置は常に「他の要素の実測値」を基準に計算する。 決め打ち座標は、今のデータでたまたま合っているだけ。
- 「たぶん合っているはず」で先に進まない。 レンダリングして目で見る一手間を省くと、必ず後で高くつく。これは自前のレイアウト計算でも、外部サイトからのデータ取得でも同じだった。
- ID依存の仕組みは、いつか壊れる。 マーカー文字列という「意味のある目印」に置き換えたことで、表題以外にも展開でき、背景SVGを将来作り直しても壊れにくくなった。
個人開発は課題を1つ潰すと次の課題が見える、という循環が今回も続いています。すでに次の自動化のネタも仕込み中です。







