2026年8月14日 金曜日
AI時短ラボ
活用· 約31

字数でテロップの行数を検査すると通り抜ける──inline-blockのチップと英単語の塊で、85字以内なのに3行になる

Remotionで組んだ動画のテロップは、折り返しを字数で検査しても素通りする。display: inline-block の強調チップと半角英数の連なりが「途中で折り返せない塊」だからである。折り返し幅1460px・fontSize 48pxの実測では和文は1行30字だが、チップの中身が和文30字(箱幅1468px)になると、スライド本文がわずか32字でも2行を食う。2026年8月5日に踏んだ実例(本文57字・旧85字チェックはPASS・実際は3行)を、CSS Display Module Level 3のatomic inline定義と静止画の実測とあわせて2026年8月14日に再現した記録。

執筆・編集:
目次

動画のテロップが図解カードに被る事故を「1スライド85字以内」のような字数閾値で防ごうとすると、通り抜ける。強調用の赤チップは display: inline-block で、CSSの仕様上「行をまたいで分割できない箱」だからである。チップ1個は字数では数文字でも、行の残り幅に入らなければ丸ごと次の行に落ちる。半角英数の連なりを whiteSpace: 'nowrap' でまとめている場合も同じで、字数と行数の対応が崩れる。直し方は、閾値を字数から実描画幅に変えること──実フォントで1塊ずつの幅を測り、塊単位の折り返しをシミュレートすることである。当サイトの動画パイプラインでは、この差で本文57字(旧チェックの実効61字・閾値85字)のスライドが3行になり、テロップ帯の上端が図解カードの下端より16px上まで食い込んだ(カードが帯の上に描かれるので、映像では帯の上部が隠れる)。

  1. display: inline-block の要素は仕様上「atomic inline」で、W3Cの定義に "cannot split across lines"(行をまたいで分割できない)と書かれている。だから字数が少なくても行を食う
  2. 折り返し幅1460px・fontSize 48pxで実測すると、和文は太さによらず1文字48.0px=1行30字。ところがチップの中身が和文30字(箱幅1468px)になると、それだけで2行になる。スライド本文はわずか32字である(2026年8月14日、Pillow 12.2.0とヒラギノ角ゴシック W8で計測)
  3. 1文字=1つの inline-block にすると、今度は英単語が途中で割れる。 CSS Text Module Level 3は「Web互換性のため、それぞれのatomic inlineの前後にソフト折り返し機会がある」と明記している。OpenAIO / penAI に割れるのはこれが原因で、修正は塊を whiteSpace: 'nowrap' で包むこと

症状:チェックは緑、映像は3行

同じスライドを、字数ベースの旧チェックと実描画幅ベースの新チェックに通した実際の出力である(2026年8月14日、macOS 15.1.1/Python 3.14.5で実行)。台本は後述の実例1件だけを入れた再現用ファイルを使った。

字数ベース(check_card_overlap.py、閾値85字・チップ1個につき-4字):

=== check_card_overlap: repro_telop.json ===
図の配置 1枚 / 全2枚 閾値=85字(チップ1個につき-4字)
✅ カード表示中に閾値超えのスライドは無し
exit=0

実描画幅ベース(check_telop_lines.py、折り返し幅1460px・48px):

=== check_telop_lines: repro_telop.json ===
折り返し幅1460px / 48px / 図解表示中は2行まで

❌ カードと接触する恐れ 1件:
  slide1 ch1: 3行(aisi_01.png 表示中)
    そして与えられたシステムプロンプトはこうなのだ。「君はCTF競技に参加するサイバーセキュリティの専門家だ」なのだ。

対処: スライド分割(トリムしない)/その位置で clearmedia:true
exit=1

同じ1行のテキストで、片方はexit 0、もう片方はexit 1である。 字数チェックは「エラーを出さない」のではなく、「見ている量が違う」。

もう1つの症状は英単語の分割で、これはパイプライン側のソースコメントに日付つきで残っている(src/ui.jsx 52〜53行目、原文そのまま)。

// 半角英数の連なりを1つのinline-blockにまとめ、単語の途中で改行されるのを防ぐ
// (1文字=1スパンのままだと OpenAI が「O / penAI」に割れる。2026-08-05修正)

原因1:inline-blockは「行をまたげない箱」だと仕様に書いてある

CSS Display Module Level 3(W3C Candidate Recommendation Draft、2026年6月5日版/2026年8月14日確認)の用語定義がそのまま答えになっている。

atomic inline An inline-level box that is replaced (such as an image) or that establishes a new formatting context (such as an inline-block or inline-table) and cannot split across lines (as inline boxes and ruby containers can).

(atomic inline:置換要素であるか(画像など)、新しいフォーマッティングコンテキストを確立する(inline-blockやinline-tableなど)インラインレベルのボックスで、インラインボックスやルビコンテナと違って行をまたいで分割できないもの)

MDNの display も同じ内容を別の言い方で書いている(2026年8月14日確認)。

inline-block The element generates a block box that will be flowed with surrounding content as if it were a single inline box (behaving much like a replaced element would). It is equivalent to inline flow-root.

(inline-block:この要素は、周囲のコンテンツと一緒に、あたかも1つのインラインボックスであるかのように流し込まれるブロックボックスを生成する(置換要素とよく似た振る舞いをする)。inline flow-root と等価である)

「1つのインラインボックスであるかのように」=どんなに長くても1個の粒である。つまり字数は行数の代理指標にならない。

幅が足りないとき何が起きるか

塊と行の関係は3通りに分かれる。当パイプラインの実装(src/ui.jsx 99〜105行目の帯スタイルと、121〜124行目のチップスタイル)に即して書く。

状況 挙動
チップの箱幅 ≦ 行の残り幅 その行に載る
チップの箱幅 > 行の残り幅、かつ ≦ 折り返し可能幅 塊ごと次の行へ落ちる(前の行に空きが残る)
チップの箱幅 > 折り返し可能幅 チップ内部で折り返して縦に伸び、実質全幅の箱になる。後続の文字は次の行から始まる

3つ目は当パイプラインのソースが「実測」として記録している挙動で、CSS 2.2の幅計算から導ける結論と同じになる(ただし今回ブラウザで直接再現したわけではない。後述の但し書きを参照)。CSS 2.2の10.3.9節(inline-block・非置換要素)は「If 'width' is 'auto', the used value is the shrink-to-fit width as for floating elements.(widthが'auto'なら、使用値は浮動要素と同じshrink-to-fit幅になる)」と書き、その計算式を10.3.5節で示している。

Then the shrink-to-fit width is: min(max(preferred minimum width, available width), preferred width).

(shrink-to-fit幅は、min(max(最小推奨幅, 利用可能幅), 推奨幅) である)

利用可能幅より推奨幅が大きければ、箱の幅は利用可能幅で頭打ちになり、中身は箱の中で折り返す。横にはみ出すのではなく、縦に伸びて全幅を占める。 その結果、後ろに置いた文字は次の行に押し出される。

原因2:1文字ずつinline-blockにすると、今度は単語が割れる

文字送りアニメーションのために1文字=1 spanにして display: inline-block を当てると、原因1が裏返って襲ってくる。CSS Text Module Level 3(W3C Candidate Recommendation Draft、2026年6月8日版/2026年8月14日確認)にこう書かれている。

For Web-compatibility there is a soft wrap opportunity before and after each replaced element or other atomic inline, even when adjacent to a character that would normally suppress them, including U+00A0 NO-BREAK SPACE.

(Web互換性のため、それぞれの置換要素や他のatomic inlineの前後にソフト折り返し機会がある。通常はそれを抑制するはずの文字(U+00A0 NO-BREAK SPACEを含む)に隣接していても同様である)

OpenAI を6個のinline-blockに割れば、単語の内部に5つの折り返し機会が生まれる。 O / penAI で折れるのはこの規定どおりの動きで、当パイプラインが2026年8月5日に踏んだのがこれである(ただし割れる瞬間そのものを今回私が観測したわけではない。根拠はソースコメントと当時の記録)。日本語は既定の改行規則でほぼどこでも折れるため字数と行数がだいたい対応するが、欧文はそうではない。MDNの word-break の値の説明が、この非対称をそのまま書いている(2026年8月14日確認)。

normal Use the default line break rule. break-all To prevent overflow, word breaks should be inserted between any two characters (excluding Chinese/Japanese/Korean text). keep-all Word breaks should not be used for Chinese/Japanese/Korean (CJK) text. Non-CJK text behavior is the same as for normal.

(normal:既定の改行規則を使う/break-all:あふれを防ぐため、中国語・日本語・韓国語のテキストを除いて、任意の2文字の間に単語の分割を入れる/keep-all:CJKテキストでは単語の分割を使わない。非CJKテキストの挙動はnormalと同じ)

対処に使う white-space: nowrap は、現在のMDNでは white-space 本体ではなく text-wrap-mode 側に説明が移っている。white-space のページには「the value nowrap is instead interpreted as a value for text-wrap-modenowrapという値は代わりにtext-wrap-modeの値として解釈される)」という注記がある。text-wrap-modenowrap の説明は次のとおり。

Text does not wrap across lines. It will overflow its containing element rather than breaking onto a new line.

(テキストは行をまたいで折り返さない。新しい行に折るのではなく、包含要素からあふれる)

「折り返さない」は「あふれる」とセットなので、nowrapで包む単位は必ず折り返し可能幅より小さくしておく必要がある。

実測:どこで字数が嘘になるか

当パイプラインのテロップ帯の実装値は src/ui.jsx にある。帯は maxWidth: 1580padding: '34px 60px 30px'fontSize: 48fontWeight: 800lineHeight: 1.32check_telop_lines.py はここから 1580 − 60 × 2 = 1460px を折り返し可能幅として使っている(帯には3pxの白い枠もあるため、静止画から測った実際の折り返し可能幅は1454pxだった。後述の但し書きを参照)。赤チップの span は padding: '0 10px'margin: '0 4px'display: 'inline-block' なので、左右で 10 + 10 + 4 + 4 = 28px が中身の幅に上乗せされる。

2026年8月14日にPillow 12.2.0でヒラギノ角ゴシック W8(48px)の描画幅を測った結果である。

文字列 W8 W6 W3
(和文1文字) 48.0px 48.0px 48.0px
A 41.8px 38.8px 36.0px
OpenAI 206.0px 189.9px 175.1px

**和文は太さによらず1文字48.0px(フォントサイズと同じ)**なので、和文だけなら字数はほぼ幅そのものになる。ここから境界が計算できる。

条件 境界
1行に入る和文 30字(30×48=1440px ≦ 1460px、31字=1488pxで超える)
2行に収まる和文(図解カード表示中の上限) 60字
3行に収まる和文 90字

旧チェックの85字という閾値は、和文だけなら「3行以内」として正しかった。 崩れるのは塊が混じったときだけである。チップだけを置いた場合を測るとこうなる。

チップの中身 箱幅(中身+28px) 行数 スライド本文の字数 旧チェックの実効字数
和文29字 1420px 1行 31字 35字
和文30字 1468px 2行 32字 36字
和文31字 1516px 2行 33字 37字

本文32字、閾値85字に対して実効36字。それでも2行である。 差は箱幅1468pxと折り返し可能幅1460pxの8pxしかない(実測の1454pxで測れば14px)。字数では絶対に見えない十数pxが、行数を1つ増やす。

踏んだ実例(2026年8月5日)

当サイトの動画パイプラインで実際に事故になったスライドを、同じ計算器で分解する。テキストは次の1行だった。

そして与えられたシステムプロンプトはこうなのだ。「君はCTF競技に参加するサイバーセキュリティの専門家だ」なのだ。
見る量 判定
字数 57字(チップ1個) 旧チェックの実効61字 → 閾値85字に対しPASS
前半「そして〜こうなのだ。」24字 1152.0px 1行目に載る(残り308.0px)
チップ「君は〜専門家だ」27字 1285.4px(中身1257.4+28) 残り308.0pxに入らない → 2行目へ
末尾「なのだ。」4字 192.0px 2行目の残り174.6pxに入りきらない → 3行目へ

幅はいずれも check_telop_lines.py と同じく1文字ずつ測って足した値である(前半24字を1つの文字列としてまとめて測ると、カーニングが効いて1145.3pxになる。差は6.7px。実装は1文字=1つのspanなので足し算の側を採った)。

(48px)・(48px)・(48px)までは2行目の残り174.6pxに入り、だけが押し出される計算になる。当時保存されていた静止画 out/qa_chip_test.png(1920×1080・ファイルの更新時刻は2026年8月5日15時55分)を開くと、3行目が「。」1文字だけの状態がそのまま写っていた。この1件については、計算と描画が同じ結論になっている。

同じ静止画のピクセルを2026年8月14日に測り直した結果が次である。図解カードのx範囲は484〜1436なので、カードに隠されない x=400/1450/1600 の3列で帯の上端を判定した(3列とも同じ値になった)。

静止画 テロップの行数 カードのクリーム色の下端 カードの白枠を含む下端 帯の白枠の上端 関係
out/qa_chip_test.png 3行 y=749 y=759 y=744 帯の上16pxがカードの下に潜り込む
out/qa_fix_test.png 2行 y=749 y=759 y=807 47pxの隙間

画面中央(x=960)だけを見ると qa_chip_test.png の帯は y=760 から始まっているように見えるが、それはカードが帯の上に描かれて帯の上部を隠しているからで、カードの外側で測ると帯の上端は y=744 である。

qa_fix_test.png の方は、テロップに AnthropicOpenAI が含まれていて、どちらも行の途中で割れずに収まっている。ファイル名は fix_test で、更新時刻は qa_chip_test.png と同じ2026年8月5日15時55分である。

対処

  1. 閾値を字数から実描画幅に変える。 実フォントを読み込んで塊ごとの幅を測り、行に載せていくシミュレーションを書く。当パイプラインの実装は123行で、標準ライブラリ以外の依存はPillowだけである
  2. チップは「1個の粒」として扱う。 中身の幅にpaddingとmarginを足した箱幅で判定する(当パイプラインでは+28px)
  3. 箱幅が折り返し可能幅を超えるケースを別扱いにする。 そのチップは内部で折り返して全幅を占め、後続の文字は次の行から始まる。そのチップが占める行数は ceil(箱幅 ÷ 折り返し可能幅) 行で、当パイプラインの実装は現在の行に ceil(箱幅 ÷ 折り返し可能幅) − 1 を足している
  4. 1文字=1 spanのアニメを使うなら、折り返せない単位を whiteSpace: 'nowrap' の inline-block でくくる。 文字ごとのスタッガーは中の span で維持できる。当パイプラインは /[0-9A-Za-z._-]/ にマッチする連なりを1単位にしている
  5. 超過したら文を削らず、スライドを分割する。 情報を落とさずに行数を下げられるのはこの手だけである。実例のテキストは、現在の台本では「そして与えられたシステムプロンプトはこうなのだ。」(24字)と「「君は〜専門家だ」なのだ。」(33字)の2枚に分かれており、幅シミュレーションではそれぞれ1行・2行になる
  6. 分割した後にもう一度lintを回す。 分割は語尾や話者の連続といった別の規律を壊しうる

観測条件(2026年8月5日事故/2026年8月14日再検証・当パイプライン1系統)

項目 内容
環境 macOS 15.1.1(Darwin 24.1.0・ビルド24B91)/Python 3.14.5/Pillow 12.2.0
対象 当サイトの動画パイプライン(Remotion+React)のテロップ実装 src/ui.jsx
帯の実装値 maxWidth: 1580 / padding: '34px 60px 30px' / fontSize: 48 / fontWeight: 800 / lineHeight: 1.32
フォント指定(CSS) 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', sans-serif
計測に使ったフォントファイル /System/Library/Fonts/ヒラギノ角ゴシック W8.ttc(48px・index=0)
事故の記録 2026年8月5日。字数チェックが全件パスした台本で、15スライドが3〜4行になり図解カードに接触
残っている静止画 out/qa_chip_test.png / out/qa_fix_test.png(いずれもファイル更新時刻2026年8月5日15時55分)

2026年8月14日に、パイプライン直下の台本JSONのうち slides 配列を持つ143本(合計11,915スライド、うち図解カード表示中は4,783スライド)を、現在の実描画幅シミュレーションで測り直した結果が次である。なおこの143本は計測時点のスナップショットで、同日16時46分に更新された台本1本(60スライド・うちカード表示中17スライド)を含んでいない。それを含めて測り直すと144本・11,975スライド・カード表示中4,800スライドになるが、下の表の3つの件数は変わらなかった。

指標 件数
旧85字チェックを通過するのに、幅シミュレーションでは3行以上 1,636スライド
うち図解カード表示中のスライド 620スライド(76本の台本)
チップ単体の箱幅が折り返し可能幅1460pxを超えた箇所 185件(46本の台本)

これは「過去の映像が実際にこうだった」という数字ではない。 現在の実装(2026年8月5日のnowrap修正・8月10日の禁則修正を含む)を前提に、過去の台本テキストを測り直したものである。当時のレンダはそれ以前の実装で行われている。

正直な但し書き

Chromeが実際にどこで折ったかは、静止画2枚以外で確認していない。 ヘッドレスブラウザで折り返し位置を直接測る検証は、今回は実行していない。本記事で「一致した」と書いたのは、out/qa_chip_test.png の3行目が「。」1文字だったという1件と、シミュレーションの計算結果が同じ結論になったことまでである。

折り返し可能幅1460pxは実装側の定数で、静止画から測った実測値は1454pxだった。 帯は maxWidth: 1580 に左右 padding 60pxだが、border: '3px solid rgba(255,255,255,0.8)' も付いている。静止画で帯の外形を測ると左端x=170・右端x=1749=ちょうど1580pxなので、box-sizing: border-box が効いていることになる。すると内側の折り返し可能幅は 1580 − 3×2 − 60×2 = 1454px で、check_telop_lines.py の定数1460pxは6px甘い。本記事の表は実装どおり1460pxで計算している。1454pxで計算し直しても、1行30字・チップ29字と30字の境界は変わらない(1440px・1420px・1468pxはいずれも1454pxの同じ側にある)。変わるのは余裕の見え方だけで、チップ和文30字のときの超過は8pxではなく14pxになる。なお、この2枚の静止画から言えるのは「実際の折り返し幅は約1447〜1477pxの間」までで、1454pxと1460pxを静止画だけで区別することはできないqa_fix_test.png の1行目が1446.9pxで収まり、qa_chip_test.png の2行目が1477.4pxで溢れているため)。

計測に使ったフォントファイルと、Chromeが選ぶフェイスが同一である確証はない。 CSSの指定は 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', sans-seriffontWeight: 800 で、計測スクリプトはヒラギノ角ゴシック W8のファイルを直接読んでいる。Chromeがどのフェイスを選び、合成太字を掛けたかは調べていない。和文は上の表のとおりW3・W6・W8のいずれでも48.0pxなので影響は欧文部分に限られるが、その欧文では差が出る(OpenAI はW8で206.0px、W6で189.9px、W3で175.1px。W8とW6の差が16.1px、W8とW3の差が30.9px)。W8で測っている限り、欧文については広めに(安全側に)見積もる方向のずれである。

シミュレーターと実装が完全には対応していない箇所がある。 src/ui.jsx は2026年8月10日の修正で禁則処理を入れており、行頭に来てはいけない文字(、。・!?」など)を直前の文字と同じnowrapブロックに入れている。一方、幅を測る check_telop_lines.py の分割関数は半角英数以外を1文字ずつに割るだけで、この禁則グループ化を実装していない。上の実例では行数の結論は変わらなかったが(が3行目に落ちるか、だ。が3行目に落ちるかの差)、両者が食い違いうる条件は残っている。これは今回コードを読み比べて見つけた差であって、実害が出たという観測ではない。

「2行までなら余裕、3行で接触する」の「接触」は、今回測った限りでは実際に重なっていた。 3行の qa_chip_test.png では帯の外形の上端がy=744、図解カードの外形(白枠を含む)の下端がy=759で、帯の上16pxがカードの下に潜り込んでいる。カードが帯より上に描かれるので、映像では帯の角丸の上部が隠れる形になる。ただし帯の内側には34pxのpaddingがあるため、テロップの文字自体はy=789から始まり、カードには掛かっていない。重なっているのは帯の箱の方である。2行の qa_fix_test.png では帯の上端がy=807で、カード下端との間に47px空いている。

根拠として挙げられている静止画のうち2枚は現存しない。 check_telop_lines.py のdocstringは実測の根拠として「still frame 5707」「frame15836」を挙げているが、2026年8月14日にパイプライン配下(node_modules除く)を検索したところ、ファイル名にこの番号を含むファイルは0件で、check_telop_lines.py 以外にこの2つの表記に言及しているファイルも0件だった。今回確認できたのは qa_chip_test.pngqa_fix_test.png の2枚である。

「15スライドが3〜4行だった」は当時の記録からの引用で、再現していない。 事故当時の台本ファイルは既に修正後の状態に上書きされており、当時の全件出力は再取得できない。本記事で再現したのは、その台本の該当箇所を現在の台本(分割後の2スライド)と静止画から組み直した1件である。

英単語が割れる症状そのものは、私が観測したのではない。 ソースコメント(2026年8月5日付)と当時の記録が根拠で、今回確認したのは修正後のコードパスと、AnthropicOpenAI が割れずに収まっている qa_fix_test.png の方である。

この記事の数値は1つのパイプライン・1つのフォント・1つの帯幅の話である。 折り返し可能幅・フォントサイズ・チップのpaddingが変われば境界の字数はすべて変わる。持ち帰れるのは「字数を幅の代理にすると、分割できない塊があるところで壊れる」という構造の方である。

出典

シェア: ポスト はてブ

出典・参照資料

AIニュースの解説を動画でも

YouTubeでは注目ニュースの背景を解説し、Xでは新着記事をお知らせしています。

コメント

まだコメントはありません。最初のコメントを書いてみませんか?

AIについて聞きたいことはありますか?

質問箱で無料で受け付けています。回答は公開され、他の方の参考にもなります。

質問箱を見る →

新しい記事をメールで受け取る

AIの新しい発表を、出典付きで整理して届けます。

関連記事

AI時短ラボ
活用08.14読了31

RemotionのLoopはコンポジション絶対フレームで位相を計算する──カード内の動画クリップが登場した瞬間に終盤から再生される原因と直し方

出典 ─ <Loop> | Remotion公式ドキュ
AI時短ラボ
活用08.14読了26

pkillはレンダプロセスを取り逃す──chrome-headless-shellの孤児36個でWindowServerが73秒無応答になった記録

出典 ─ Errors(セクション: pkill pattern matches the Claude Code process)
AI時短ラボ
活用08.14読了23

ffmpegのエンコード指定を出力ファイル名より後ろに置くと効かない──96000Hzのまま連結して尺が43秒に化けた話

出典 ─ ffmpeg Documentation -
AI時短ラボ
活用08.14読了28

Object.entries(true) は空配列を返す──設定JSONの型違いがエラーも警告も出さずに通り、起承転結インジケーターが4本分まるごと点灯しなかった

出典 ─ Object.entries
AI時短ラボ
活用08.14読了39

VOICEVOXの/audio_queryのmoras[]から口パクを作る──母音→口形状マップと、実wav尺へのratio補正が「?」で崩れる話

出典 ─ VOICEVOX ENGINE OSS AP
Remotionで長い動画のレンダリングがクラッシュする──分割レンダとffmpeg無劣化連結で完走させる手順の記事画像
活用07.16読了7

Remotionで長い動画のレンダリングがクラッシュする──分割レンダとffmpeg無劣化連結で完走させる手順

出典 ─ npx remotion render |
AIでショート動画を作り続ける工程──固定する5つと、毎回変える3つの記事画像
活用08.14読了16

AIでショート動画を作り続ける工程──固定する5つと、毎回変える3つ

出典 ─ YouTube ショートの作成を始める -
動画生成AIを数十本まとめて回す──CLIとWebの使い分けと、接続が切れた時の実ログの記事画像
活用08.14読了14

動画生成AIを数十本まとめて回す──CLIとWebの使い分けと、接続が切れた時の実ログ

出典 ─ Video generation - Ope