🎤

PLAUD NotePin + Gemini で人生書き起こし (サマリもあるよ)

に公開

要約

  • PLAUD NotePin を使って,長時間の録音 + クラウド管理が簡単になった
  • PLAUD NotePin は便利だが,無制限に書き起こしをするのは少し高い(5000円 / 月)
  • Google Colab + Google Drive + Gemini 2.5 Proで全部書き起こししよう
  • ついでに,一日のサマリとリザルトを出せるコードも作った

免責

本記事で紹介する録音・自動文字起こしパイプラインは、研究・個人利用を前提とした技術的サンプルです。利用者は以下を必ず遵守してください。

  • 録音対象者から事前に書面または同等の方法で明確な同意を得ること。
  • 音声・転写データを扱う各クラウドサービスの利用規約・プライバシーポリシーおよび適用法令(個人情報保護法、通信の秘密、著作権法等)に従うこと。
  • API キーや機密情報は第三者に開示しないこと。適切なアクセス制御と暗号化を施すこと。
  • 本記事の手順・コードによって生じたいかなる損害、データ漏えい、法的問題、追加料金等についても、筆者および配布元は一切責任を負いません。利用は自己責任で行ってください。

PLAUD NotePin

PLAUD NotePin (以下,PLAUD) は,軽量・小型でクラウドと連携が簡単な録音デバイスです.(Amazon アフィリエイトリンクを利用しています.)

PLAUDは電池が続く限り連続録音をすることができ,また,その録音をクラウドにアップロードする容量に制限がないこと,そして首掛けでも負担が少ないことから,長い録音をすることに向いたデバイスです.

PLAUDは,サブスクリプションで無制限の文字起こしを用意していますが,その値段は5000 円 / 月 と,少し高いです.お金が沢山ある人は,これを便利に使うことができますが,私はそうではありませんでした.

自動書き起こし

そこで,高い精度で書き起こしをすることが可能なGoogle Gemini 2.5 Pro を使って,書き起こしのパイプラインを作ることにしました.

採用技術

  • Google Colaboratory (共有が簡単)
  • Gemini 2.5 Pro API (書き起こし精度が良いわりに安い)
  • Google Drive

Google Colaboratory - Google Drive の連携が簡単なので,今回はこのような構成にしましたが,ローカルですべてやることも可能です.

使い方

まず,app.plaud.ai にPC からアクセスして,mp3を手に入れます.
この際に,アプリから,Web アプリのデータアクセスを許可しなければいけません.

それができた場合,mp3をアップロードするフォルダを作ってください.

その後,以下のGoogle Colabを自分のためにコピーして利用してください.
https://colab.research.google.com/drive/1aneTdsSdvMMXIey_r996w59ZkdaAN9Qs?usp=sharing

コード中の,BASE_DIRは先ほど設定したファイルのパスにしてください.

BASE_DIR       = "/content/drive/MyDrive/YOUR_DIR"  ## TODO : ここを直す

以下が全体のコードです.

全体のコード
# 1. 必要なライブラリのインストール
!pip install -q -U google-generativeai pydub

# ffmpeg のインストール (pydubに必要)
# Colab環境では既にインストールされていることが多いですが、念のため実行します。
# 出力が多いため、 > /dev/null 2>&1 で非表示にしています。
!apt-get update > /dev/null 2>&1
!apt-get install -y ffmpeg > /dev/null 2>&1
print("ffmpeg installation check completed.")

# -*- coding: utf-8 -*-
"""
Batch audio transcription → daily summary generator (Gemini API)
================================================================
Colab‑friendly version with **detailed logging of Gemini calls** so you can see
exactly where time is spent.

Key additions
-------------
* `logging` is used instead of scattered `print()` to provide timestamps and
  clearer levels (`INFO`, `WARNING`, `ERROR`).
* `llm_transcribe()` & `summarise_date()` now log:
  - upload start / end with file‑ID and size
  - polling progress every 10 s (`.` dots)
  - request to `generate_content` start / end with response length
* All progress output works nicely in Google Colab (no fancy tqdm needed).
* Comments show quick setup tips (`pip install pydub`, `apt‑get install ffmpeg`).
"""

from __future__ import annotations
import os, re, math, time, shutil, datetime, logging, sys
from typing import List, Dict
from pydub import AudioSegment           # pip install pydub  (needs ffmpeg)
import google.generativeai as genai      # pip install google-generativeai
from google.colab import drive, userdata

# ---------------------------------------------------------------------------
# 1. CONFIGURATION
# ---------------------------------------------------------------------------
BASE_DIR       = "/content/drive/MyDrive/YOUR_DIR"  ## TODO : ここを直す
AUDIO_DIR      = os.path.join(BASE_DIR, "mp3")
TRANSCRIPT_DIR = os.path.join(BASE_DIR, "transcript")
SUMMARY_DIR    = os.path.join(BASE_DIR, "summary")

for d in (AUDIO_DIR, TRANSCRIPT_DIR, SUMMARY_DIR):
    os.makedirs(d, exist_ok=True)

CHUNK_MINUTES = 60
CHUNK_MS      = CHUNK_MINUTES * 60 * 1000
MODEL_NAME    = "gemini-2.5-pro-preview-03-25"  
WAIT_SEC      = 5                       # delay between chunk requests
TEMP_DIR      = "temp_audio_chunks_for_gemini"
AUDIO_EXTS    = (".mp3", ".wav", ".m4a")
TIMESTAMP_RE  = re.compile(r"(\d{4}-\d{2}-\d{2})[ _](\d{2})[:_](\d{2})[:_](\d{2})")

# ---------- logging setup ----------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    stream=sys.stdout,
)
log = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
SUMMARY_PROMPT_TMPL = """あなたは私のパーソナルアシスタントです.以下は {date} の行動・発言の書き起こしです.これを読んで Markdown 形式で次の6セクションを出力してください。

## 1. 日々の要約
主要出来事を箇条書き

## 2. レポート
今日進捗したこと・完了したタスク

## 3. ネクストアクション
明日以降に取るべき具体的行動

## 4. 反省
改善すべき点・気づき

## 5. 今日あった面白かったこと
興味深いやりとりやアイデア

## 6. 推奨事項
追加で書き留めておくと良いポイント

## 7. 一日のリザルト 
S++ - C . フレーバテキストなので,ユーザを少し楽しませながらも鬱陶しくないコメント

---

```text
{transcript}

"""

---------------------------------------------------------------------------

2. HELPERS

---------------------------------------------------------------------------

def format_td(td: datetime.timedelta) -> str:
h, rem = divmod(int(td.total_seconds()), 3600)
m, s = divmod(rem, 60)
return f"{h:02}:{m:02}:{s:02}"

def detect_start(path: str) -> datetime.datetime | None:
m = TIMESTAMP_RE.search(os.path.basename(path))
if not m:
return None
d, hh, mm, ss = m.groups()
try:
return datetime.datetime.strptime(f"{d} {hh}:{mm}:{ss}", "%Y-%m-%d %H:%M:%S")
except ValueError:
return None

def ensure_tmp():
if os.path.exists(TEMP_DIR):
shutil.rmtree(TEMP_DIR)
os.makedirs(TEMP_DIR, exist_ok=True)

def split_audio(path: str) -> List[str]:
print('audio split start.....')
audio = AudioSegment.from_file(path)
dur = len(audio)
files = []
for i in range(math.ceil(dur / CHUNK_MS)):
p = os.path.join(TEMP_DIR, f"chunk_{i+1:03d}.wav")
audio[i*CHUNK_MS : min((i+1)*CHUNK_MS, dur)].export(p, format="wav")
files.append(p)
return files

---------------------------------------------------------------------------

3. GEMINI CALL WRAPPERS with logging

---------------------------------------------------------------------------

def llm_transcribe(chunk: str, prompt: str) -> str:
size = os.path.getsize(chunk)
log.info(f"Uploading {os.path.basename(chunk)} ({size/1024:.1f} KB) …")
fobj = genai.upload_file(path=chunk)
log.info(f"→ file_id: {fobj.name}")
dots = 0
while fobj.state.name == "PROCESSING":
time.sleep(10)
dots += 1
print("." , end="", flush=True)
fobj = genai.get_file(fobj.name)
print()
if fobj.state.name != "ACTIVE":
raise RuntimeError(f"upload state {fobj.state.name}")
log.info("Calling Gemini generate_content …")
print("Calling Gemini generate_content …")
t0 = time.time()
txt = genai.GenerativeModel(MODEL_NAME).generate_content([fobj, prompt], request_options={"timeout": 600}).text or ""
log.info(f"↳ received {len(txt)} chars in {time.time()-t0:.1f}s")
genai.delete_file(fobj.name)
return txt

def llm_summarise(prompt: str) -> str:
log.info("Gemini summarisation request …")
t0 = time.time()
txt = genai.GenerativeModel(MODEL_NAME).generate_content(prompt, request_options={"timeout": 600}).text or ""
log.info(f"↳ summary length {len(txt)} chars in {time.time()-t0:.1f}s")
return txt

---------------------------------------------------------------------------

4. TRANSCRIPTION

---------------------------------------------------------------------------

def transcribe_file(path: str):
start = detect_start(path)
if start is None:
log.warning(f"[SKIP] cannot parse start time: {os.path.basename(path)}")
return

ensure_tmp()
chunks = split_audio(path)
log.info(f"{os.path.basename(path)} → {len(chunks)} chunks")

lines: List[str] = []
for idx, ch in enumerate(chunks, 1):
    log.info(f"chunk {idx}/{len(chunks)} start")
    chunk_start = start + datetime.timedelta(milliseconds=(idx-1)*CHUNK_MS)
    prompt = (
        "一人称視点の音声が添付されるので,当人の行動や音声を推定して書き起こしてください."\
        "衣擦れや物音は省略してください.\n"\
        f"{start:%Y-%m-%d %H:%M:%S} 開始として,時間を付記するようにしてください.\n"\
        f"例:\n[{format_td(chunk_start-start)}] : 「ここに発言や行動」\n"
    )
    raw = llm_transcribe(ch, prompt)
    off = 0
    for ln in raw.splitlines():
        ln = ln.strip()
        if ln.startswith("["):
            lines.append(ln)
        elif ln:
            abs_dt = chunk_start + datetime.timedelta(seconds=off)
            lines.append(f"[{abs_dt:%Y-%m-%d %H:%M:%S}] : {ln}")
            off += 2
    if idx < len(chunks):
        time.sleep(WAIT_SEC)

out = os.path.join(
    TRANSCRIPT_DIR,
    os.path.splitext(os.path.basename(path))[0] + "_transcript.txt"
)
with open(out, "w", encoding="utf-8") as f:
    f.write(f"Transcription for: {os.path.basename(path)}\n")
    f.write(f"Audio Start Time: {start:%Y-%m-%d %H:%M:%S}\n")
    f.write(f"Chunk Duration: {CHUNK_MINUTES} minutes\n" + "="*40 + "\n\n")
    f.write("\n".join(lines))
log.info(f"saved → {os.path.basename(out)}")

---------------------------------------------------------------------------

5. DAILY SUMMARY

---------------------------------------------------------------------------

def collect_by_date() -> Dict[str, List[str]]:
pending: Dict[str, List[str]] = {}
for fn in os.listdir(TRANSCRIPT_DIR):
if fn.endswith("_transcript.txt"):
d = fn[:10] # YYYY-MM-DD
if not os.path.exists(os.path.join(SUMMARY_DIR, f"{d}_daily_report.txt")):
pending.setdefault(d, []).append(os.path.join(TRANSCRIPT_DIR, fn))
return pending

def summarise_date(d: str, paths: List[str]):
log.info(f"Summarising {d} ({len(paths)} files)…")
text = "\n\n".join(open(p, encoding="utf-8").read() for p in paths)[:30000]
summary = llm_summarise(SUMMARY_PROMPT_TMPL.format(date=d, transcript=text))
with open(os.path.join(SUMMARY_DIR, f"{d}_daily_report.txt"), "w", encoding="utf-8") as f:
f.write(summary)
log.info(f"saved → {d}_daily_report.txt")

def run_summaries():
pend = collect_by_date()
if not pend:
log.info("No new dates to summarise.")
return
for d, files in sorted(pend.items()):
try:
summarise_date(d, files)
except Exception as e:
log.error(f"Summary error for {d}: {e}")
time.sleep(5)

try:
drive.mount('/content/drive')
print("Google Drive mounted successfully.")
except Exception as e:
print(f"Error mounting Google Drive: {e}")
# ドライブマウント失敗時は処理を続行できないため終了
raise SystemExit("Google Drive mount failed.")

4. Gemini API キーの設定

ColabのSecrets機能 (左側の鍵アイコン) を使用してAPIキーを設定してください。

名前: GOOGLE_API_KEY (参考コードに合わせる)

値: あなたのGemini APIキー

try:
GOOGLE_API_KEY = userdata.get('GOOGLE_API_KEY')
if not GOOGLE_API_KEY:
raise userdata.SecretNotFoundError("API Key value is empty.")
genai.configure(api_key=GOOGLE_API_KEY)
print("Gemini API Key configured.")
except userdata.SecretNotFoundError:
print("Error: GOOGLE_API_KEY not found or empty in Colab Secrets.")
print("Please add your Gemini API key using the Colab Secrets manager (key icon on the left).")
raise SystemExit("API Key configuration failed.")
except Exception as e:
print(f"An unexpected error occurred during API Key configuration: {e}")
raise SystemExit("API Key configuration failed.")

logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
force=True)

1) transcribe

audio_files = [f for f in os.listdir(AUDIO_DIR)
if f.lower().endswith(AUDIO_EXTS) and
not os.path.exists(os.path.join(
TRANSCRIPT_DIR, os.path.splitext(f)[0] + "_transcript.txt"))]
if audio_files:
log.info(f"Transcribing {len(audio_files)} new audio file(s)…")
for f in audio_files:
try:
transcribe_file(os.path.join(AUDIO_DIR, f))
except Exception as e:
log.error(f"Transcription error {f}: {e}")
else:
log.info("No new audio files to transcribe.")

if os.path.exists(TEMP_DIR):
shutil.rmtree(TEMP_DIR)

2) summaries

run_summaries()

log.info("All processing complete.")

このコードを実行すると,先ほど生成したその日ごとのtranscriptを基にしたサマリをだしてくれます.

出力例

書き起こしの出力は以下のようです.発言だけではなく,行動も記録できています.

{12:20:05} : それだけ?
{12:20:14} : なんで?
{12:20:15} : (歩いている音)
{12:20:55} : (歩いている音)
{12:21:22} : (歩いている音)
{12:23:47} : (店内アナウンス) これまで、2000都議団は5回にわたり条例提案を行うなど値上げを繰り返し求めてまいりました。
{12:24:08} : (店内アナウンス) しかし都議会での可決には至らず都民の願いは届きませんでした。
{12:24:13} : (店内アナウンス) こうした中、世論が高まり、ついに東京都は4度目の値上げに踏み切りました。

以下のように,一日の推奨事項を教えてもらえます.作業時間やら,あくびまで検知しているのはすごい!

## 6. 推奨事項
*   **タスク管理の徹底:** ネクストアクションに挙げたタスクを具体的なToDoリストにし、期限を設定して管理しましょう。
*   **アイデアと指摘の記録:** 研究に関する議論や研究会での指摘事項は、忘れずにメモアプリやScrapboxなどに記録し、後で参照できるようにしましょう。
*   **健康維持:** 書き起こしからはPC作業が長時間に及んでいる様子や、あくびなどがうかがえます。意識的に休憩を取り、集中力を維持できるようにしましょう。

使ってみた感想

  • 純粋に議事録や,何気ない会話の中のアイデアが後から拾えるのはすごい便利.
  • プライバシの問題はあるので,よく話す人には,デバイスが光っているときには録音していることを伝えること.初対面の人間に合うときは録音を切っておくなど.
  • 一日やっていたことを振り返るのも素晴らしい.
  • あと,あくびとか拾っているのはすごい.発言以外の行動履歴もとれそう.

コメント

  • zapierで自動で録音をGoogle Driveに転送することも検討したが,録音データをそのまま送ることはできず,一度サマリをしないと外部とは連携できない仕様だった.
  • 生のエクスポートが一件ずつしかできないことも体験的に不便.
  • しかし,内部サービスを使いやすいように設計することは当然なので,納得できる.お金がある人は,無制限プランを使って書き起こしをすると良い.

Discussion