2017/12/24(日)女の子風ボイスの朗読EPUBを手作りしてみた

Web技術pawa

こちらは「ピクシブ株式会社 Advent Calendar 2017」 の24日目の記事です。

ピクシブ株式会社の pawa です。ピクシブでは主に「pixivコミック」「pixivノベル」「ピクシブ文芸」の開発に携わっています。今年の自身6記事目の Advent Calendar 記事です!

これまでに書いた Advent Calendar 記事:

  1. 青空文庫のごく簡単な紹介
  2. Amazon Polly で青空文庫の作品の音読を聴く
  3. 「青空朗読」の紹介【音声同期Epub】
  4. 体育会系Webエンジニアであることを生かしてスポーツライフをもっと楽しくしてみた
  5. Voice Works Plus で青空文庫の作品を朗読してみた

今回のテーマ

ここまで Advent Calendar を書いてきて、「『青空朗読』の紹介【音声同期Epub】」で音声同期EPUBを作るための知識を得て、「Voice Works Plus で青空文庫の作品を朗読してみた」で朗読音声を得ました。

今回は、その知識と朗読音声を使って、実際に「朗読EPUBを作ってみた」というお話です。何か好きな作品があれば、朗読を聴いたり自身でも朗読してみることによって、その作品・著者に対する理解がより深まると思います。

どのように朗読EPUBを実現するか

Synchronized Multimedia Integration Language(SMIL)」と「EPUB Media Overlays 3.0」で実現します。

前者はWikipediaの記事では以下のように説明されています。

Synchronized Multimedia Integration Languageは、WWW上でマルチメディアコンテンツを表現するためのマークアップ言語の一つである。静止画、動画、音声、文字(テキスト)などの、位置レイアウト、時間軸上でのレイアウトを、Extensible Markup Language (XML) フォーマットで記述することで統合し、再生させることができる。略称はSMILで、スマイルと読む。同期マルチメディア統合言語と日本語訳されることもある。

後者の仕様は日本語版では以下のように説明されてます。

EPUB Media Overlays 3.0 は、EPUB Content Document と同期する音声表現用に、[SMIL](同期マルチメディア統合言語)、Package Document、EPUBR Style Sheet、および EPUB Content Document の使用方法を定義する。

「EPUB Media Overlays 3.0」の仕様に従って「Synchronized Multimedia Integration Language」でどういうタイミングで同期するかを記述することで、EPUBリーダーでよしなに再生できるようにしました。

(EPUBファイルの構成を可能な限りシンプルにするため、Readium というEPUBリーダーでしか動作チェックしていません。)

EPUBファイルの構成

理解・応用しやすいように最小限の構成にしました。

rodoku-epub/
├── META-INF
│   └── container.xml # 'OEBPS/content.opf' の場所を指定してあるだけのxml
├── OEBPS
│   ├── audio
│   │   └── 001.mp3 # 朗読音声
│   ├── content.opf # 本の情報を定義してあるファイル
│   ├── page
│   │   ├── 001.smil # 朗読音声とテキストの同期タイミングが書き連ねられているファイル
│   │   └── 001.xhtml # 本文
│   ├── style
│   │   └── main.css # スタイル指定
│   └── toc.xhtml # 目次
└── mimetype # EPUBのおまじない

これらのファイルを以下のように zip コマンドで固めると朗読EPUBの出来上がりです。

zip -0 ../rodoku.epub  mimetype
zip -r ../rodoku.epub * -x mimetype

rodoku.epub

audio/001.mp3

サーカス」(著:中原中也)という詩の朗読音声です。(詳細:「Voice Works Plus で青空文庫の作品を朗読してみた」)

content.opf

ごくありふれた情報定義という具合です。 <meta property="media:active-class">media-overlay-active</meta> の指定により、音声再生中のテキストにCSSクラス「media-overlay-active」が適用されて、再生が終わったらそのCSSクラスが外れます。(再生中のテキストの背景にCSSで色を付けるために使用します。)

<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="book-id" version="3.0" xml:lang="ja">
  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:identifier id="book-id">urn:uuid:0f8d6eec-a24a-46d4-b870-8d2ffcab1c52</dc:identifier>
    <dc:title>サーカス</dc:title>
    <dc:language>ja</dc:language>
    <dc:creator id="creator1">中原中也</dc:creator>
    <dc:creator id="creator2">pawa</dc:creator>
    <dc:publisher>朗読部ブログ</dc:publisher>
    <meta property="dcterms:modified">2017-12-25T09:00:00Z</meta>
    <meta refines="#book-id" property="identifier-type">uuid</meta>
    <meta refines="#creator1" property="role" scheme="marc:relators">aut</meta>
    <meta refines="#creator2" property="role" scheme="marc:relators">nrt</meta>
    <meta property="rendition:layout">reflowable</meta>
    <meta property="rendition:orientation">auto</meta>
    <meta property="rendition:spread">auto</meta>
    <meta property="media:duration" refines="#smil001">0:01:26</meta>
    <meta property="media:duration">0:01:26</meta>
    <meta property="media:active-class">media-overlay-active</meta>
  </metadata>

  <manifest>
    <item id="nav" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav" />
    <item id="main-style" href="style/main.css" media-type="text/css" />
    <item id="chap001" href="page/001.xhtml" media-type="application/xhtml+xml" media-overlay="smil001" />
    <item id="smil001" media-type="application/smil+xml" href="page/001.smil" />
    <item id="audio001" media-type="audio/mpeg" href="audio/001.mp3" />
  </manifest>

  <spine page-progression-direction="rtl">
    <itemref idref="chap001" />
  </spine>
</package>

style/main.css

縦書きの簡単な設定と再生中のテキストの背景色を付ける設定のみのごく単純なCSSです。

@charset "UTF-8";

body {
  writing-mode: vertical-rl;
  -epub-writing-mode: vertical-rl;

  line-height: 1.8;
  letter-spacing: 0.1em;

  font-family: YuMincho, 'Hiragino Mincho ProN', 'Hiragino Mincho Pro', 'MS Mincho', serif;
}

.media-overlay-active {
  background-color:#def;
}

page/001.xhtml

本文です。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns:epub="http://www.idpf.org/2007/ops" xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja" class="vrtl">
<head>
  <meta charset="UTF-8"/>
  <title>サーカス</title>
  <link href="../style/main.css" rel="stylesheet" type="text/css"/>
</head>
<body>
  <div class="main">

<p>  <span id="nrt0000">サーカス</span><br />
<br />
<span id="nrt0001">幾時代かがありまして</span><br />
  <span id="nrt0002">茶色い戦争ありました</span><br />
<br />
<span id="nrt0003">幾時代かがありまして</span><br />
  <span id="nrt0004">冬は疾風吹きました</span><br />
<br />
<span id="nrt0005">幾時代かがありまして</span><br />
  <span id="nrt0006">今夜<ruby>此処<rp>(</rp><rt>ここ</rt><rp>)</rp></ruby>での<ruby>一<rp>(</rp><rt>ひ</rt><rp>)</rp></ruby>と<ruby>殷盛<rp>(</rp><rt>さか</rt><rp>)</rp></ruby>り</span><br />
    <span id="nrt0007">今夜此処での一殷盛り</span><br />
<br />
<span id="nrt0008">サーカス小屋は高い<ruby>梁<rp>(</rp><rt>はり</rt><rp>)</rp></ruby></span><br />
  <span id="nrt0009">そこに一つのブランコだ</span><br />
<span id="nrt0010">見えるともないブランコだ</span><br />
<br />
<span id="nrt0011">頭<ruby>倒<rp>(</rp><rt>さか</rt><rp>)</rp></ruby>さに手を垂れて</span><br />
  <span id="nrt0012">汚れ木綿の<ruby>屋蓋<rp>(</rp><rt>やね</rt><rp>)</rp></ruby>のもと</span><br />
<span id="nrt0013">ゆあーん ゆよーん ゆやゆよん</span><br />
<br />
<span id="nrt0014">それの近くの白い灯が</span><br />
  <span id="nrt0015"><ruby>安値<rp>(</rp><rt>やす</rt><rp>)</rp></ruby>いリボンと息を吐き</span><br />
<br />
<span id="nrt0016">観客様はみな鰯</span><br />
  <span id="nrt0017"><ruby>咽喉<rp>(</rp><rt>のんど</rt><rp>)</rp></ruby>が鳴ります<ruby>牡蠣殻<rp>(</rp><rt>かきがら</rt><rp>)</rp></ruby>と</span><br />
<span id="nrt0018">ゆあーん ゆよーん ゆやゆよん</span><br />
<br />
     <span id="nrt0019"><ruby>屋外<rp>(</rp><rt>やぐわい</rt><rp>)</rp></ruby>は真ッ<ruby>闇<rp>(</rp><rt>くら</rt><rp>)</rp></ruby> <ruby>闇<rp>(</rp><rt>くら</rt><rp>)</rp></ruby>の<ruby>闇<rp>(</rp><rt>くら</rt><rp>)</rp></ruby></span><br />
     <span id="nrt0020">夜は<ruby>劫々<rp>(</rp><rt>こふこふ</rt><rp>)</rp></ruby>と更けまする</span><br />
     <span id="nrt0021"><ruby>落下傘奴<rp>(</rp><rt>らくかがさめ</rt><rp>)</rp></ruby>のノスタルヂアと</span><br />
     <span id="nrt0022">ゆあーん ゆよーん ゆやゆよん</span></p>

  </div>
</body>
</html>

page/001.smil

音声開始位置と音声終了位置を書き連ねているファイルです。記述が大変過ぎて到底「スマイル」にはなりませんでした。😫 二度と手作業ではやりたくないです!

<?xml version="1.0" encoding="UTF-8"?>
<smil xmlns:epub="http://www.idpf.org/2007/ops" xmlns="http://www.w3.org/ns/SMIL" version="3.0">
  <body>
    <seq epub:textref="001.xhtml">
      <par id="par0000">
        <text src="001.xhtml#nrt0000" />
        <audio src="../audio/001.mp3" clipBegin="0.00" clipEnd="3.19" />
      </par>
      <par id="par0001">
        <text src="001.xhtml#nrt0001" />
        <audio src="../audio/001.mp3" clipBegin="3.19" clipEnd="6.58" />
      </par>
      <par id="par0002">
        <text src="001.xhtml#nrt0002" />
        <audio src="../audio/001.mp3" clipBegin="6.58" clipEnd="10.38" />
      </par>
      <par id="par0003">
        <text src="001.xhtml#nrt0003" />
        <audio src="../audio/001.mp3" clipBegin="10.38" clipEnd="13.30" />
      </par>
      <par id="par0004">
        <text src="001.xhtml#nrt0004" />
        <audio src="../audio/001.mp3" clipBegin="13.30" clipEnd="17.32" />
      </par>
      <par id="par0005">
        <text src="001.xhtml#nrt0005" />
        <audio src="../audio/001.mp3" clipBegin="17.32" clipEnd="20.49" />
      </par>
      <par id="par0006">
        <text src="001.xhtml#nrt0006" />
        <audio src="../audio/001.mp3" clipBegin="20.49" clipEnd="23.50" />
      </par>
      <par id="par0007">
          <text src="001.xhtml#nrt0007" />
          <audio src="../audio/001.mp3" clipBegin="23.50" clipEnd="28.02" />
      </par>
      <par id="par0008">
          <text src="001.xhtml#nrt0008" />
          <audio src="../audio/001.mp3" clipBegin="28.02" clipEnd="31.83" />
      </par>
      <par id="par0009">
          <text src="001.xhtml#nrt0009" />
          <audio src="../audio/001.mp3" clipBegin="31.83" clipEnd="35.08" />
      </par>
      <par id="par0010">
          <text src="001.xhtml#nrt0010" />
          <audio src="../audio/001.mp3" clipBegin="35.08" clipEnd="38.94" />
      </par>
      <par id="par0011">
          <text src="001.xhtml#nrt0011" />
          <audio src="../audio/001.mp3" clipBegin="38.94" clipEnd="42.09" />
      </par>
      <par id="par0012">
          <text src="001.xhtml#nrt0012" />
          <audio src="../audio/001.mp3" clipBegin="42.09" clipEnd="45.30" />
      </par>
      <par id="par0013">
          <text src="001.xhtml#nrt0013" />
          <audio src="../audio/001.mp3" clipBegin="45.30" clipEnd="50.17" />
      </par>
      <par id="par0014">
          <text src="001.xhtml#nrt0014" />
          <audio src="../audio/001.mp3" clipBegin="50.17" clipEnd="53.75" />
      </par>
      <par id="par0015">
          <text src="001.xhtml#nrt0015" />
          <audio src="../audio/001.mp3" clipBegin="53.75" clipEnd="57.42" />
      </par>
      <par id="par0016">
          <text src="001.xhtml#nrt0016" />
          <audio src="../audio/001.mp3" clipBegin="57.42" clipEnd="61.04" />
      </par>
      <par id="par0017">
          <text src="001.xhtml#nrt0017" />
          <audio src="../audio/001.mp3" clipBegin="61.04" clipEnd="64.54" />
      </par>
      <par id="par0018">
          <text src="001.xhtml#nrt0018" />
          <audio src="../audio/001.mp3" clipBegin="64.54" clipEnd="69.66" />
      </par>
      <par id="par0019">
          <text src="001.xhtml#nrt0019" />
          <audio src="../audio/001.mp3" clipBegin="69.66" clipEnd="73.23" />
      </par>
      <par id="par0020">
          <text src="001.xhtml#nrt0020" />
          <audio src="../audio/001.mp3" clipBegin="73.23" clipEnd="77.74" />
      </par>
      <par id="par0021">
          <text src="001.xhtml#nrt0021" />
          <audio src="../audio/001.mp3" clipBegin="77.74" clipEnd="81.59" />
      </par>
      <par id="par0022">
          <text src="001.xhtml#nrt0022" />
          <audio src="../audio/001.mp3" clipBegin="81.59" clipEnd="85.74" />
      </par>
    </seq>
  </body>
</smil>

作った朗読EPUBを聴いてみた

(音量MAXにしないと聞こえないかも)

なるほど⤴?

学び

  • 音声同期部分は手作業でやるにはつらすぎる
  • Readium なら意外と簡単に朗読(音声同期)EPUBを作れる
  • マンション・アパートで朗読すると隣人に気を使ってとても精神が消耗する

朗読部の本体を作る前にボイストレーニングにとても行きたくなりました!

Advent Calendar 最終日は CTO の @edvakf のターンです。

ピクシブでは小説に興味があるエンジニアも募集しています。