平衡点


2026/05/27

_ JpGU-AGU 2026 に来てます[Work

北海道からだと, 気温 +10℃なので, やっぱりシンドいね.

_ Beamer と pdfpc の組み合わせで発表原稿を表示させつつ, 読み上げをする

読んで字の如く.

発表資料の作成

\usepackage{pdfpc}
\newcommand<>{\talknote}[1]{\only#2{\pdfpcnote{#1}\relax}}

とでもしておいて, スライドの overlay に合わせて

\talknote{
  Thank you for the introduction.
  <br><br>
  Hello everyone.
  My name is Youhei Sasaki from Hokkaido Information University.
  <br><br>
  Today, I would like to talk about This titie.
  <br><br>
  To put it simply, this is a story about ...
}

とでも書いておくと pdfpc の presenter screen に原稿が表示される.

読み上げ音声の作成

あとは TeX ないの \talknote をパースして, Google の Cloud Text-to-Speech API に投げると, 良い感じに読み上げ音声が出てくる(ので, あとはシャドーイング).

スクリプトはこんなん.

import os
import re
import subprocess
from google.cloud import texttospeech

def main():
    # ------------------------------------------------------------------------
    # 設定エリア
    # ------------------------------------------------------------------------
    TEX_FILENAME = 'your_presentation.tex'
    SPEAKING_RATE = 0.7
    PITCH = 0.0
    OUTPUT_DIR = "audio_output"

    # --- 結合時の設定 -------------------------------------------------------
    # スライド間の無音時間(秒)
    SILENCE_DURATION_SEC = 2

    client = texttospeech.TextToSpeechClient()

    with open(TEX_FILENAME, 'r', encoding='utf-8') as f:
        tex_content = f.read()

    frames = re.split(r'\\begin\{frame\}', tex_content)[1:]
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    voice = texttospeech.VoiceSelectionParams(language_code="en-US", name="en-US-Journey-D")

    audio_config = texttospeech.AudioConfig(
        audio_encoding=texttospeech.AudioEncoding.MP3,
        speaking_rate=SPEAKING_RATE,
        pitch=PITCH
    )

    generated_files = []

    for frame_idx, frame_content in enumerate(frames, start=1):
        # --------------------------------------------------------------------
        # \talknote を探す前に、TeXのコメント(%から行末まで)を削除
        # --------------------------------------------------------------------
        frame_content_uncommented = re.sub(r'(?<!\\)%.*', '', frame_content)

        # コメントが除去されたクリーンな文字列から \talknote を抽出
        talknotes = re.finditer(r'\\talknote\s*(?:<([^>]+)>)?\s*\{(.*?)\}', frame_content_uncommented, re.DOTALL)

        for note_idx, note in enumerate(talknotes, start=1):
            overlay = note.group(1) or "1"
            raw_text = note.group(2)

            # <br><br> (あるいは <br>) を区切りとして処理
            chunks = re.split(r'<br\s*/?>\s*<br\s*/?>|<br\s*/?>', raw_text)

            processed_chunks = []
            for chunk in chunks:
                chunk = re.sub(r'\s+', ' ', chunk).strip()
                if chunk:
                    processed_chunks.append(chunk)

            final_text = '\n\n'.join(processed_chunks)

            print("-" * 50)
            print(f"Slide {frame_idx} (Overlay {overlay}) の処理を開始...")

            if not final_text:
                print(f"  -> 原稿が空のため、音声生成をスキップします。")
                continue

            print(f"  [DEBUG] 送信テキスト:\n{final_text}\n")

            filename = f"slide_{frame_idx:02d}_overlay{overlay}.mp3"
            filepath = os.path.join(OUTPUT_DIR, filename)

            try:
                res = client.synthesize_speech(
                    input=texttospeech.SynthesisInput(text=final_text),
                    voice=voice,
                    audio_config=audio_config
                )

                with open(filepath, "wb") as out:
                    out.write(res.audio_content)

                generated_files.append(filepath)
                print(f"  生成完了: {filename}")

            except Exception as e:
                print(f"  エラー発生: {e}")

    # ------------------------------------------------------------------------
    # ffmpeg を利用した無劣化結合 (無音区間の挿入)
    # ------------------------------------------------------------------------
    if generated_files:
        print("=" * 50)
        print(f"ffmpeg を使用して結合し、間に {SILENCE_DURATION_SEC} 秒の無音を挿入...")

        silence_file = os.path.join(OUTPUT_DIR, "silence.mp3")
        subprocess.run([
            "ffmpeg", "-y", "-f", "lavfi", "-i", "anullsrc=r=24000:cl=mono",
            "-t", str(SILENCE_DURATION_SEC), "-c:a", "libmp3lame", silence_file
        ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

        list_file = os.path.join(OUTPUT_DIR, "concat_list.txt")
        with open(list_file, "w", encoding="utf-8") as lf:
            for i, fpath in enumerate(generated_files):
                lf.write(f"file '{os.path.basename(fpath)}'\n")
                if i < len(generated_files) - 1:
                    lf.write(f"file 'silence.mp3'\n")

        combined_filepath = os.path.join(OUTPUT_DIR, "presentation_all.mp3")
        subprocess.run([
            "ffmpeg", "-y", "-f", "concat", "-safe", "0",
            "-i", list_file, "-c", "copy", combined_filepath
        ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

        if os.path.exists(list_file): os.remove(list_file)
        if os.path.exists(silence_file): os.remove(silence_file)

        print(f" 結合完了: {combined_filepath}")
        print("=" * 50)
    else:
        print("結合するファイルがありませんでした。")

if __name__ == "__main__":
    main()
  1. 音声ファイルの結合に ffmpeg を使ってるので別途インストールしておく.
  2. Goocle Cloud API で 「Cloud Text-to-Speech API」を使うので
    • Google Cloud Console でプロジェクトを作成し,「Cloud Text-to-Speech API」を有効化
    • 「IAMと管理」>「サービス アカウント」からキー(JSONファイル)を作成・ダウンロード
    • ダウンロードした JSON ファイルのパスを,環境変数に設定
      $ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account-file.json"
      
  3. venv で必要なライブラリのインストール
    • 仮想環境 (.venv) を作成
      $ python3 -m venv .venv
      $ source .venv/bin/activate
      
    • 必要な Python パッケージをインストール
      $ pip install google-cloud-texttospeech
      

でまあ, あとは適宜実行する.

忘れそうなのでここに書いておく.

_ Beamer で画像も covered/uncover を使いたい

これ, デフォルトでこうなっていて欲しんだけれどなぁ.

\RequirePackage{tikz}
% 透過度の設定(デフォルト15%. お好みで)
\newcommand{\coveredopacity}{0.15}
% \coveredincludegraphics 命令の定義
% d<> でオーバーレイ指定, O{} でオプション引数, m で必須引数を受け取る
\NewDocumentCommand{\coveredincludegraphics}{ d<> O{} m }{%
  \IfNoValueTF{#1}{%
    % オーバーレイ指定 (<...>) がない場合は, 通常の \includegraphics
    \includegraphics[#2]{#3}%
  }{%
    % オーバーレイ指定がある場合は, TikZ ノードの opacity を切り替え
    \begin{tikzpicture}[baseline=(current bounding box.south)]%
      \alt<#1>{%
        % 指定されたスライド (uncovered) では opacity=1 (100%)
        \node[opacity=1, inner sep=0pt, outer sep=0pt] {\includegraphics[#2]{#3}};
      }{%
        % それ以外のスライド (covered) では opacity=\coveredopacity (15%)
        \node[opacity=\coveredopacity, inner sep=0pt, outer sep=0pt] {\includegraphics[#2]{#3}};
      }%
    \end{tikzpicture}%
  }%
}

連絡先など

portrait

最近の日記

一覧

Back to Top ▲