【Safari】
ページの読み上げができないときがある
(読み上げと言語)

関連記事

1. サイト内に「ページの読み上げ」ができない

iOSのSafariでWordPress記事を開いて読み上げ機能を使って聞いています。
しかし、同じサイト内の記事でもアドレスバーのメニューから「ページの読み上げを聞く」ができないページがあります1

同じサイト、同じテンプレート、同じ <html lang="ja"> 宣言のページでも、ある記事では出てある記事では出ません2

1.1. 否定された仮説

いくつか原因の仮説を立てて、Safariの読み上げの有効・無効のページを比較してみました。

ページ内のarticleがうまく認識できていないのかと思い、ページサイズやHTML構造を比較してみました。
しかし、目立った違いはありません。

ページ読み上げサイズ冒頭table/figure冒頭コード脚注数DOM数pre数h2数h3数liネスト
hash不可175KBあり(1/1)なし261,9602892310
type不可119KBなしあり01,180236136
curry97KBなしなし079710343
macro137KBなしなし101,620456137
array209KBなしなし03,07671104510
list248KBあり(1/1)なし413,69310194410
  • ページ全体の長さ:読み上げ可能な記事は1000行を超えるものも含まれ、読めない記事のほうが短いケースがありました。
  • コード量:読める記事にも<pre>ブロックが40〜100個含まれていました3
  • 脚注の多さ:読める記事にも脚注10〜41個のものがありました。
  • 見出し前に表があるかどうか:読めるページにも見出し前の表がありました。
  • DOMノード数、liのネスト深さ、SVG図の数、<title>属性の長さといった要因も、読める読めないとの相関がありませんでした。

2. 読み上げと言語

ただ、「ページの読み上げを聞く」項目がある記事には、「英語に翻訳」ボタンがあることに気づきました4

一方、読み上げできないページには、「日本語に翻訳」ボタンがあります。
つまり、Safariは、読めないページを日本語ではないページと判定しているようです5

ただ、ページの言語設計が違うわけではありません。
全ページ共通で、<html lang="ja"> は設定されていました6

2.1. 決定的だった差分(リード部分)

読める読めないの差を見てみると、記事のリード部分の構造に差がありました。

読めない記事は、h1タグ直後から最初のh2タグまでの区間、いわゆるこのリード部分の構成が「要点箇条書き、早見表または大きなコードブロック」という並びになっていました。
要点の直後に、関数名、型名、コード断片など、非日本語トークンで埋まった大きなブロックが挿入されていました。

一方、読める記事は、リード部分がほぼ要点箇条書きだけで終わっていて、そのまま最初のh2に入ります。
表があっても、最初のh2以降にすぐ日本語の説明段落が立ち上がります7

ページ読み上げh1〜h2の日本語率先頭200字日本語率先頭1000字日本語率
list28.2%60.3%28.6%
type不可28.9%
hash不可30.7%48.6%31.2%
array36.7%51.9%38.0%
curry53.7%
macro56.2%

数値で見ると、h1からh2までの区間の日本語文字率は、読めないページで30%前後、読めるページで37〜56%に分布していました。
日本語文字率は、ひらがな・カタカナ・漢字の合計を、日本語と英字アルファベットの合計で割った値です8

境界線上にいることは確かで、比率単独では判定基準になりませんが、冒頭に大きな非日本語ブロックが割り込んでいるという構造的特徴は、読めない2ページに共通していました。

2.2. 導入に含まれる文章以外の要素を移動する

そこで、該当記事のh1直下にあった早見表だけを、記事末尾の近くのセクションに移動しました。
すると、変更後、Safariのメニューに「ページの読み上げを聞く」が復活しました。

脚注、目次ネスト、SVG図、コードブロックは一切触っていません。

h1直後には要点箇条書きか導入段落だけを置いて、早見表や大きなコードブロックは最初のh2より後に配置します。
冒頭でコードを見せたい場合は、そのコードを解説する日本語段落を先に置いて、コード自体は2〜3行に留めます。長いコード例は最初のh2セクション以降に回します9

3. 推定されるSafariのロジック(Readability)

SafariのリーダーはArc90のReadabilityをフォークした実装がベースとされています10
ここから挙動を推測してみます。

  • 第1段階では、DOM上の p・li・div などを走査して各ノードに本文らしさスコアを付与します。
    連続テキストの長さ、句読点の数、コンマの頻度がプラスに、リンク密度の高さや特定のクラス名がマイナスに働きます11
  • 第2段階では、スコアの高いノードから祖先方向にさかのぼって、本文ルートとなるサブツリーを決めます。
    高スコアノードが複数のサブツリーに分散していると、ルート決定が不安定になります12
  • 第3段階で、確定したルート内のテキストから言語を推定します。
    ここで日本語と確定できなければ、日本語読み上げメニューは提示されません13

今回のケースでは、h1直後の早見表がDOM的には本文コンテナの内部にあり、リンクやコードトークンを大量に含むため、第1段階で本文ルート候補のスコア分布を乱していたと考えられます。
第2段階でルートが不安定になり、第3段階で言語判定に流れるテキストからASCIIの比率が上がって、日本語判定に失敗する、という連鎖です。
早見表を末尾へ移すと、リード部分が要点箇条書きと散文だけで構成され、本文ルートが安定して日本語として確定する、と説明がつきます14

3.1. 他に効きそうなマークアップ改善

今回は構造の移動だけで通りましたが、予防策として次も有効そうです。

コードブロックの<code>タグに lang="en" を明示すると、Readability系の言語判定でコード部分を英語として分離カウントしてもらえる実装が多く、地の文の日本語率が下がりにくくなります15

記事内タグクラウドを<footer><aside>内に配置すると、本文抽出の候補から除外されやすくなります。
現状のWordPressテーマでは、タグリンクが<article>内に配置されているテンプレートが多く、リンク密度の高いブロックとして第1段階のスコアリングにマイナス影響を与えます16

4. まとめ

Safariの読み上げメニューが消える原因は、記事全体の長さやコード量ではなく、h1直後から最初のh2までのリード区間の構造にありました。
要点箇条書きの直後に早見表や大きなコードブロックが入ると、Readabilityベースの本文抽出器が本文ルートを安定して決められず、言語判定が日本語から外れて、読み上げメニューが提示されなくなります。
リード区間を日本語の散文と短い要点だけで構成するのが、iOS Safariでの読み上げ対応の条件です。

  1. iOS 17で追加された「Listen to Page」機能は、Safariのアドレスバーの「あぁ」アイコンから起動でき、バックグラウンド再生やロック画面からの操作にも対応します。Siriボイスで読み上げが行われ、再生速度の調整も可能です。 – Listen to Page in Mobile Safari 17
  2. WCAG達成基準3.1.1「ページの言語」では、ページの主要言語をプログラムで判定可能な方法で指定することが求められています。html要素のlang属性が標準的な指定方法で、支援技術にも適切な発音を促します。 – HTML lang global attribute – MDN Web Docs
  3. Mathias Bynensの調査では、Safari Readerは段落が100文字ごとにスコアを加算し、最終的に合計スコアで判定を行うとされています。コードブロックは段落とは別にカウントされるため、数だけでは発動条件に直結しません。 – How to enable Safari Reader on your site? · Mathias Bynens
  4. Safariの翻訳機能は、英語、スペイン語、フランス語、イタリア語、ドイツ語、ロシア語、ポルトガル語、日本語、中国語、韓国語など複数の言語に対応しています。対応言語として検出されない場合は翻訳オプションが表示されません。 – Translate Webpages in Safari on iPhone and iPad – MacRumors
  5. Listen to Page機能は、コンテンツがSiriの設定言語と一致しない場合には利用できず、翻訳後に初めて利用可能になるが元テキストをアクセント付きで読み上げる、という挙動がiOS 17のリリース直後から報告されています。 – iOS 17: How to Get Siri to Read Web Articles to You | MacRumors Forums
  6. lang属性が設定されていても実際には考慮されないことがあり、xml:lang属性が優先される場合があります。また要素にlang属性がない場合は親要素から継承され、継承先がなければ未確定扱いになります。 – HTMLElement: lang property – Web APIs | MDN
  7. HTML5のsection要素は主題でグループ化されたコンテンツ、article要素は独立して配布・再利用可能なコンテンツを表します。見出しの直後に散文が続く構造は、これらの要素が想定する典型的な記事レイアウトです。 – Semantic HTML5 Elements Explained
  8. Mozilla ReadabilityはDOMノードのスコアリングで文字数と読点の数を使いますが、CJK言語では単語境界の概念が英語と異なるため、単純な文字数カウントが本文らしさの推定に合わないケースが指摘されています。Safari ReaderやMaxthon Readerはこの問題に対し、画面占有面積ベースの判定を組み合わせて対応しています。 – Web Reading Mode: Determining the main page content | Ctrl blog
  9. Mathias Bynensが公開したSafari Readerの発動条件調査では、最も重要なコンテンツをコンテナ要素で囲むこと、段落を十分に長くすること、各段落が少なくとも100文字を超えるようにすることが、Readerを発動させる要因として挙げられています。 – How to enable Safari Reader on your site? · Mathias Bynens
  10. Readabilityは2010年にArc90 Labsが開発したブックマークレットが起源で、ApacheライセンスでGoogle Codeに公開されたコードがMozillaおよびApple、Maxthonなどによってフォーク・発展し、各ブラウザのリーダーモード実装の基礎となりました。 – GitHub – mozilla/readability: A standalone version of the readability lib
  11. Readabilityの_grabArticle関数は候補ノードに対してタグ名やCSSクラスに基づくスコアを付与し、commaカウント、テキスト長100文字ごとのボーナス、リンク密度によるペナルティを計算します。これがアルゴリズムの中核にあたる処理です。 – Creating Paperoni – Part 2 of X: Improving the extractor
  12. Mozilla Readabilityでは親スコアの伝播や兄弟要素のマージ、条件付きクリーンアップといった処理がパイプラインで実行されます。もし結果が短すぎる場合は、クリーンアップを緩めて再試行することもあります。 – Mozilla Readability Algorithm (Readability.js) explanation | WebcrawlerAPI Blog
  13. Safari ReaderはCJK(中国語、日本語、韓国語)言語への対応でリーダーモード発動の安定性が比較的良好とされ、専用フォントも用意されています。Apple版のフォークは画面占有面積ベースの判定を加えることで、文字数ベースの判定では扱いにくいCJK記事に対応しています。 – Web Reading Mode: Determining the main page content | Ctrl blog
  14. Safari ReaderはReadabilityと違い、main content内のfooter要素もコンテンツの一部として扱う、短い段落を本文の一部として隠してしまうことがある、といった独自挙動が報告されています。Webページ側で明示的にinstapaper_hideクラスを使えば、特定の要素をリーダーから除外できます。 – Web Reading Mode: Determining the main page content | Ctrl blog
  15. WCAG達成基準3.1.2「部分の言語」では、ページの一部が主要言語と異なる言語で書かれている場合、その部分にlang属性で言語を指定することが求められます。ただしlang属性がプログラミング言語を示す用途で使われる場合は、言語識別の正式な機能とは別扱いになります。 – Rule | Element with lang attribute has valid language tag | ACT-Rules Community
  16. HTML5のaside要素は主コンテンツと間接的に関連する補助的なコンテンツ、footer要素は文書やセクションの著者情報、著作権、関連ページへのリンクなどを表します。セマンティックな要素で囲むことで、本文抽出アルゴリズムは補助情報として適切に扱いやすくなります。 – Semantic HTML: header, footer, nav, section, article, aside, main