👓

Flask + VOICEVOX で日本語論文読み上げツール試作してみた

に公開

🧩 概要

論文を読むとき、どうしても「読む体力」が持たない瞬間があります。
ずっとパソコンと向き合って作業しているため、目が疲れてしまい、パソコンの画面を集中して向き合うことで読む論文作業は全然進みませんでした。
特に英語が苦手なこともあり、英語論文は全然頭に入ってきませんでした..

知人に愚痴をこぼしたところ、オーディブルが良いのではないかと勧められました。
そこで既存の論文読み上げアプリを調査したものの、Illuminateなど読み上げだけでなく要約など高度なテキスト処理が可能である高性能なツールがありますが、アカウント登録が必要であったり、無料での利用では制限があったりと、そもそもただ読み上げて欲しいだけなこと、そして「手軽かつ好きなだけ」使えそうなものはあまりありませんでした。

そこで自己研鑽も兼ねて、PythonとFlask、そして初めて触れる VOICEVOX (無料で自然な音声を生成することが可能)を使って、「自分のPDFを自然な音声で読めるツール」を作ってみました。

まず第一段階として、日本語論文に対応可能なツールとして試作しました。

実現したこと

  • 日本語論文PDFの読み込み、読み上げ
  • 読み上げている文を1文ずつ表示
  • セクションごとに読み上げ部分を選択可能に
    • アブスト部分から読み上げ部分と定義
    • 参考文献も除外

⚙️ システム概要

ローカルで簡単に動かせるかつ、GUIも実装したい、ということから、Flaskを使用したアプリを作成しました。

使用ライブラリ

  • Flask
  • PyMuPDF
  • VOICEVOX

ファイル構成

- app.py # 実行ファイル
- static/js/
	- main_zun.js #mainの処理
- templates
	- index.html #表示画面の作成

🎯 設計思想・開発の方向性

このツールの設計で重視しているのは「最小限で快適に使えること」です。
特に意識したのは次の3点です:

  1. 操作の簡単さ

    • ファイルを選ぶだけ or ドラッグでアップロード
    • 柔軟に音声の再生、停止を可能に
  2. 段落単位の読み上げ

    • PDF全体ではなく、「今読んでいる章」をピックアップして聞ける
      • 章を最後まで読み上げる前でも、音声を停止し、別の章を選択・再生することが可能
    • さらに、短い文章ごとに表示するようにすることで、「読みながら聴く」という用途の場合でも、そのままPDFを読む場合よりは負担を下げられるよう工夫
  3. 言語切り替えの柔軟さ

    • 日本語・英語を手動選択可能に
    • (現状は日本語のみですが、拡張できるようにUIを設計)

🧰 実装メモ:主要な処理

フロントエンド

  • fetchを使って /upload にPDFをPOST
  • アップロード後に解析結果(各セクション)を受け取り、セレクトボックスに追加
  • FormDataの構造やエラーハンドリングを共通化
const fileInput = document.getElementById("pdfFile");

form.addEventListener("submit", async (e) => {
	e.preventDefault();
	const formData = new FormData();
	formData.append("pdf", fileInput.files[0]);

	errorEl.textContent = "";
	sectionSelect.innerHTML = "";
	sections = [];

	try {
		const res = await fetch("/upload", { method: "POST", body: formData });
		const data = await res.json();
		if (data.error) {
			errorEl.textContent = data.error;
			output.textContent = "";
			return;
		}
		sections = data.sections;
		sections.forEach((sec, i) => {
			const opt = document.createElement("option");
			opt.value = i;
			opt.textContent = sec.title;
			sectionSelect.appendChild(opt);
		});
		currentSectionIndex = 0;
		output.textContent = sections[currentSectionIndex].text;
		controls.style.display = "block";
	} catch (err) {
		errorEl.textContent = "サーバとの通信に失敗しました。";
	}
});
  • VOICEVOXによる読み上げ
  • 一文ずつ読み上げるようにすることで、一文ごとの読み上げ中分の表示も可能に
async function speakNextSentenceVOICEVOX(sentences) {
    if (sentenceIndex >= sentences.length) return;

    const text = sentences[sentenceIndex];
    const speakerId = 3; // ずんだもん
    const queryUrl = `http://127.0.0.1:50021/audio_query?text=${encodeURIComponent(text)}&speaker=${speakerId}`;
    const synthUrl = `http://127.0.0.1:50021/synthesis?speaker=${speakerId}`;

    try {
        const queryResponse = await fetch(queryUrl, { method: "POST" });
        const queryData = await queryResponse.json();

        const audioResponse = await fetch(synthUrl, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(queryData)
        });
        const audioBlob = await audioResponse.blob();

        // 再生
        const audioUrl = URL.createObjectURL(audioBlob);
        const audio = new Audio(audioUrl);
        currentAudio = audio;

        // 🔹 再生開始時に現在の文を表示
        audio.onplay = () => {
            if (currentReading) {
                currentReading.textContent = sentences[sentenceIndex];
            }
        };

        // 🔹 再生完了時に次の文へ
        audio.onended = () => {
            if (!isPaused) {
                sentenceIndex++;
                speakNextSentenceVOICEVOX(sentences);
            }
        };

        audio.play();

    } catch (err) {
        console.error("VOICEVOX再生中にエラー:", err);
    }
}

バックエンド

  • /upload でPDFを受け取り、一時保存
  • PyMuPDFで段落ごとにテキスト抽出
    • アブストラクトなどから記録
    • 参考文献以降は記録から除外
  • JSON形式でフロントエンドに返す
    • sectionsリスト:要素.. {"title":**, "text":"**~"}をJSON化

また、以下の関数で章の判別を行なっています。(結構無理やりやっています)

def is_heading(block):
	"""行頭に番号付き(数字またはローマ数字)見出しがあるかを判定"""
	text = block["text"]#.strip()
	if not text:
		return False
	
	fontsize = block["fontsize"]
	if fontsize <= 10:
		return False
	
	# 「数字. 」で始まる場合(例: 1. Introduction)
	if re.match(r"^\s{0,3}\d+\.\s", text):
		return True
	
	if re.match(r"^\d\s{0,2}", text):
		return True
	
	# 「I.」〜「X.」などのローマ数字見出し(I〜Xに限定)
	if re.match(r"^\s{0,3}(?:I|II|III|IV|V|VI|VII|VIII|IX|X)\.\s", text):	
		return True
	
	return False

本関数では、以下のような場合の章見出しを検知対象にしています。

  • 「1.」「2.」もしくは「1」「2」のような章見出し
  • 「Ⅰ.」「Ⅱ.」のようなローマ数字の章見出し

また、フォントサイズが10より大きいテキストを前提としています。
(失敗するときは結構失敗します)

🎲 使い方説明 / 手順

  1. PDFをアップロード
  2. PDF読み込むボタンを押す
  3. テキストの読み込みに成功したら、読み上げて欲しいSectionを選択する
  4. 読み上げ音声を選択
    1. 「システム音声」「ずんだもん」を選択可能
    2. 「ずんだもん」の利用の場合は、あらかじめ「VOICEVOXアプリ」を立ち上げている必要あり
  5. 「再生」ボタンを押す
    1. 適宜、「一時停止」や「停止」を押す

🧪 詰まったところ

実装してみると、思った以上にPDF構造のばらつきが大きく、 セクション抽出の部分で何度もつまずきました。
また、アブストラクトがある論文、ない論文があったりと、場合分けに苦労しました。

特に、論文ごとにフォントサイズや章見出しの形式が不均一であったため、見出し検出が安定しないという課題が一番苦労しました。
とりあえず、10ptくらいが本文と固定値で仕分けるようにしましたが、今後の改善として、「1ページ分だけ一旦解析のためだけに読み取り、よく利用されるフォントサイズなどを記録・見出し検出処理に流用」などの工夫を加える必要があると考えています。

🧭 今後の展望

現状のシステムの問題点として、以下のことが挙げられる。

  1. 数式を含む文章のレイアウト崩れ
    • 数式まじりの文章だと、読み込む際にその文章が全体が崩れてしまい、読み上げる際に変になってしまいます。
    • LLMによる読み上げ台本への変換などで解決可能だと考えていますが、料金や使用量を気にしなければならないLLMは最終手段にしたいと考えています。
  2. 画質の低いPDFへの対応
    • 古い論文やスキャンPDFでは文字認識が難しいため、OCR(例:Tesseractなど)の利用も視野に入れています。
    • ただし、自身があまり古い論文を読むことは少ないため、優先度は低めに考えています。
  3. 見出し識別の精度向上
    • 現状は固定フォントサイズで判定していますが、フォーマットの異なる論文では誤検出が発生します。
    • 前述のように、ページごとに統計的にフォント傾向を抽出するなどの改善を予定しています。
  4. 英語論文への対応
    • 現在は日本語論文のみを対象にしていますが、英語論文の読み上げも大きな課題です。(システム作成の動機でもありますので)
    • 理想としては、VOICEVOXのような自然音声の英語版があればベストですが、現状は見つかっていません。
    • 現状案として、pyopenjtalk や他のTTSエンジンを組み合わせることで、簡易的な多言語対応を目指すことを考えています。

あとは、簡単に立ち上げられるようにしたいと考えています。
このシステムの作成は「無料・簡単・即使える」をモットーに進めているので、高機能化よりもまずは使いやすさを優先しつつ、少しずつ改良を重ねていく予定です。

💬 まとめ

  • 文字を追うのが疲れる論文を耳で読むことで負荷を軽減することを可能にするシステムの実現を目指しました
  • 今回の実装を通して、論文読み上げシステムの最低限の基盤を構築することができたと思います。
  • 実際に利用してみましたが、論文を目で追わなくても読める、というのは便利でしたし、英語も対応可能にしたいという思いが強くなりました
    • (あと、今まで音声発生の際に利用していたシステム音声とは違い、いい感じのずんだもんの声に読んでもらえるのは少しテンション上がりました。)
  • ただ作っていて、論文PDFのフォーマットの多様さから一律にいい感じに読み取れるシステムを作るのは難しいと感じました。
    • LLMの利用はできるだけ避けたいですが、最終的にLLM処理機能を実装することになりそうです..

ここまで読んでくださり、ありがとうございました。

参考リンク / 出典

VOICEVOX

https://voicevox.hiroshiba.jp/

GitHubリポジトリ

https://github.com/suzu-ki/papaer_read

  • 開発中です
  • 動作保証なし
  • 個人学習用です

Discussion