interview-transcription について
記者向けのインタビュー管理ワークフローです。録音設定から文字起こし、引用の検証、情報源の整理まで、録音を検証可能な記事素材にする手順を扱います。自動処理と手動処理の両方を紹介します。
主な機能
- 01録音設定とバックアップの指針
- 02自動・手動の文字起こし手順
- 03話者識別とタイムスタンプ
- 04引用の抽出と正確性の確認
- 05情報源データベースと一括処理
活用シーン
- 01インタビュー録音の文字起こしと整理
- 02ファクトチェック用のタイムスタンプ付き引用作成
- 03記事のテーマに沿った情報源の検索・管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jamditis/ claude-skills-journalism --skill interview-transcriptionインストール後、Claude Code で/interview-transcriptionを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yaml
詳細
インタビューの文字起こしと管理
準備から公開まで、ジャーナリストがインタビューを管理するための実践的なワークフローです。
使用する場面
- インタビューの質問を準備するとき
- 音声/動画の録音を処理するとき
- 文字起こしを作成または管理するとき
- 複数の情報源からのメモを整理するとき
- 情報源との関係を管理するデータベースを構築するとき
- ファクトチェック用にタイムスタンプ付きの引用を作成するとき
- 録音を公開可能な引用に変換するとき
文字起こし用の録音設定
インタビュー前のリサーチ、質問の設計、帰属に関する合意、同意を得るための台本には、interview-prep スキルを使用してください。ここでは、文字起こしの品質に影響する録音設定のみを説明します。
# Standard recording configuration for clean transcription
RECORDING_SETTINGS = {
'format': 'wav', # Lossless for transcription
'sample_rate': 16000, # Whisper resamples to 16k anyway; 16k saves disk
'channels': 1, # Mono is fine for speech; stereo only if mics are positionally distinct
'backup': True, # Always run a backup recorder
}
# File naming convention
# YYYY-MM-DD_source-lastname_topic.wav
# Example: 2026-05-08_smith_budget-hearing.wav
2台のデバイスで録音するルール。 必ず2台のデバイスで録音してください。最低限、バックアップとしてスマートフォンを使用します。ワイヤレスのラベリアマイクを使用する場合は、ラベリアユニット内蔵のレコーダーが1台目、バックアップアプリを実行するスマートフォンが2台目です。
各話者に個別の専用マイクを割り当て、別々のチャンネルにルーティングしている場合を除き、モノラルが推奨されます。 両方の話者の音声が両方のチャンネルに漏れているステレオ録音は、クリーンなモノラル録音よりも話者ダイアライゼーションの精度が低くなります。
文字起こしのワークフロー
自動文字起こしパイプライン
標準の OpenAI Whisper は音声をテキストに文字起こししますが、話者ラベルは付与しません。話者ダイアライゼーション付きの出力(「Speaker 1:」/「Speaker 2:」など)を得るには、Whisper とダイアライゼーションモデルを組み合わせたツールが必要です。一般的には WhisperX(m-bain/whisperX)が使われ、faster-whisper による文字起こしと pyannote.audio によるダイアライゼーションを組み合わせ、1回の処理で単語レベルのタイムスタンプと話者 ID を出力します。
from pathlib import Path
import subprocess
import json
def transcribe_interview(
audio_path: str,
output_dir: str = "./transcripts",
diarize: bool = True,
hf_token: str | None = None,
min_speakers: int = 2,
max_speakers: int = 2,
) -> dict:
"""
Transcribe an interview using WhisperX (Whisper + pyannote diarization).
Returns a transcript with word-level timestamps and speaker labels.
Diarization needs a Hugging Face token with access to the pyannote
speaker-diarization-3.1 model. Accept the model EULA at
huggingface.co/pyannote/speaker-diarization-3.1 once, then pass the token.
"""
Path(output_dir).mkdir(exist_ok=True)
cmd = [
'whisperx', audio_path,
'--model', 'large-v3',
'--output_format', 'json',
'--output_dir', output_dir,
'--language', 'en',
'--compute_type', 'int8', # CPU-friendly; use 'float16' on GPU
'--min_speakers', str(min_speakers),
'--max_speakers', str(max_speakers),
]
if diarize:
cmd.append('--diarize')
if hf_token:
cmd += ['--hf_token', hf_token]
subprocess.run(cmd, check=True, capture_output=True)
json_path = Path(output_dir) / f"{Path(audio_path).stem}.json"
with open(json_path) as f:
return json.load(f)
def format_for_editing(transcript: dict) -> str:
"""Convert to journalist-friendly format with timestamps."""
lines = []
for segment in transcript.get('segments', []):
timestamp = format_timestamp(segment['start'])
text = segment['text'].strip()
lines.append(f"[{timestamp}] {text}")
return '\n\n'.join(lines)
def format_timestamp(seconds: float) -> str:
"""Convert seconds to HH:MM:SS format."""
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
return f"{h:02d}:{m:02d}:{s:02d}"
通常の Whisper に切り替える場合。 ダイアライゼーションが過剰な場合や、Hugging Face のトークンを取得できない場合は、--diarize フラグを削除してください。モデルは引き続き、正確なタイムスタンプ付きの文字起こしを出力します。文脈に基づいて手動で話者ラベルを付けてください。faster-whisper(CTranslate2 バックエンド)は速度を最適化したバリアントで、CLI でも同様に動作します。whisper.cpp はリソースに制約のあるマシン(Raspberry Pi、古いノートパソコン)向けの C++ 移植版です。ダイアライゼーションには対応していませんが、小規模/中規模のモデルを CPU で快適に実行できます。
手動文字起こし用テンプレート
機密性の高いインタビューや、AI による文字起こしがうまくいかない場合:
## Transcript: [Source] - [Date]
**Recording file**: [filename]
**Duration**: [XX:XX]
**Transcribed by**: [name]
**Verified against recording**: [ ] Yes / [ ] No
---
[00:00:15] **Q**: [Your question]
[00:00:45] **A**: [Source response - verbatim, including ums, pauses noted as (...)]
[00:01:30] **Q**: [Follow-up]
[00:01:42] **A**: [Response]
---
## Notes
- [Anything not captured in audio: gestures, documents shown, etc.]
## Potential quotes
- [00:01:42] "Quote that stands out" - context: [why it matters]
引用の抽出と検証
引用抜粋のワークフロー
from dataclasses import dataclass
from typing import Optional
import re
@dataclass
class Quote:
text: str
timestamp: str
speaker: str
context: str
verified: bool = False
used_in: Optional[str] = None
class QuoteBank:
"""Manage quotes from interview transcripts."""
def __init__(self):
self.quotes = []
def extract_quote(self, transcript: str, start_time: str,
end_time: str, speaker: str, context: str) -> Quote:
"""Extract and store a quote with metadata."""
# Pull text between timestamps
pattern = rf'\[{re.escape(start_time)}\](.+?)(?=\[\d|$)'
match = re.search(pattern, transcript, re.DOTALL)
if match:
text = match.group(1).strip()
quote = Quote(
text=text,
timestamp=start_time,
speaker=speaker,
context=context
)
self.quotes.append(quote)
return quote
return None
def verify_quote(self, quote: Quote, audio_path: str) -> bool:
"""Mark quote as verified against original recording."""
# In practice: listen to audio at timestamp, confirm accuracy
quote.verified = True
return True
def export_for_story(self) -> str:
"""Export verified quotes ready for publication."""
output = []
for q in self.quotes:
if q.verified:
output.append(f'"{q.text}"\n- {q.speaker}\n[Timestamp: {q.timestamp}]')
return '\n\n'.join(output)
引用の正確性チェックリスト
引用を公開する前に:
- [ ] Listened to original recording at timestamp
- [ ] Quote is verbatim (or clearly marked as paraphrased)
- [ ] Context preserved (not cherry-picked to change meaning)
- [ ] Speaker identified correctly
- [ ] Timestamp documented for fact-checker
- [ ] Source approved quote (if agreement made)
情報源管理データベース
インタビュー追跡用スキーマ
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Optional
from enum import Enum
class SourceStatus(Enum):
ACTIVE = "active" # Currently engaged
DORMANT = "dormant" # Not recently contacted
DECLINED = "declined" # Refused to participate
OFF_RECORD = "off_record" # Background only
class InterviewType(Enum):
ON_RECORD = "on_record"
BACKGROUND = "background"
DEEP_BACKGROUND = "deep_background"
OFF_RECORD = "off_record"
@dataclass
class Source:
name: str
organization: str
contact_info: dict # email, phone, signal, etc.
beat: str
status: SourceStatus = SourceStatus.ACTIVE
interviews: List['Interview'] = field(default_factory=list)
notes: str = ""
# Relationship tracking
first_contact: Optional[datetime] = None
trust_level: int = 1 # 1-5 scale
@dataclass
class Interview:
source: str
date: datetime
interview_type: InterviewType
recording_path: Optional[str] = None
transcript_path: Optional[str] = None
story_slug: Optional[str] = None
key_quotes: List[str] = field(default_factory=list)
follow_up_needed: bool = False
notes: str = ""
情報源の簡易検索
def find_sources_for_story(sources: List[Source], topic: str,
beat: str = None) -> List[Source]:
"""Find relevant sources for a new story."""
matches = []
for source in sources:
# Filter by beat if specified
if beat and source.beat != beat:
continue
# Only suggest active sources
if source.status != SourceStatus.ACTIVE:
continue
# Check if they've spoken on similar topics
for interview in source.interviews:
if topic.lower() in interview.notes.lower():
matches.append(source)
break
# Sort by trust level
return sorted(matches, key=lambda s: s.trust_level, reverse=True)
音声/動画の処理
複数の録音を一括処理する
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor
import json
def batch_transcribe(recordings_dir: str, output_dir: str) -> dict:
"""Process all recordings in a directory."""
recordings = list(Path(recordings_dir).glob('*.wav')) + \
list(Path(recordings_dir).glob('*.mp3')) + \
list(Path(recordings_dir).glob('*.m4a'))
results = {}
with ProcessPoolExecutor(max_workers=4) as executor:
futures = {
executor.submit(transcribe_interview, str(rec), output_dir): rec
for rec in recordings
}
for future in futures:
rec = futures[future]
try:
transcript = future.result()
results[rec.name] = {
'status': 'success',
'transcript': transcript
}
except Exception as e:
results[rec.name] = {
'status': 'error',
'error': str(e)
}
return results
動画インタビューからの抽出
import subprocess
def extract_audio_from_video(video_path: str, output_path: str = None) -> str:
"""Extract audio track from video for transcription."""
if output_path is None:
output_path = video_path.rsplit('.', 1)[0] + '.wav'
subprocess.run([
'ffmpeg', '-i', video_path,
'-vn', # No video
'-acodec', 'pcm_s16le', # WAV format
'-ar', '44100', # Sample rate
'-ac', '1', # Mono
output_path
], check=True)
return output_path
法的・倫理的な考慮事項
同意の記録
## Recording consent record
**Date**:
**Source name**:
**Recording type**: [ ] Audio [ ] Video
**Interview type**: [ ] On record [ ] Background [ ] Off record
### Consent obtained:
- [ ] Verbal consent recorded at start of interview
- [ ] Written consent form signed
- [ ] Email confirmation of consent
### Jurisdiction notes:
- Interview location state/country:
- One-party or two-party consent jurisdiction:
- Any specific restrictions agreed:
### Agreed terms:
- [ ] Full attribution allowed
- [ ] Organization attribution only
- [ ] Anonymous source
- [ ] Review quotes before publication
- [ ] Embargo until [date]:
録音同意に関する管轄区域
州ごとの一方当事者/全当事者の同意、隠し録音に関する規則、連邦法の優先適用については、interview-prep スキルを使用してください。このスキルでは、報道の自由を守るための記者委員会が公開する、継続的に更新される信頼性の高い情報源 Reporter's Recording Guide を参照しています。
管轄区域にかかわらず、録音について必ず明示的な同意を得てください。 すべての文字起こしファイルの冒頭に、同意のやり取りを逐語的に記録してください(タイムスタンプ、話者、回答)。これにより、どこでも法的に保護され、ファクトチェッカーが明確な状態から作業を始められます。
ツールとリソース
| ツール | 目的 | 備考 |
|---|---|---|
| OpenAI Whisper | ローカルでの文字起こし、話者分離なし | 無料で、オフラインで動作します。現在最良のモデルは large-v3 です |
| WhisperX | Whisper + 話者分離 | m-bain/whisperX。無料です。話者 ID 付きの単語レベルのタイムスタンプを取得できます。pyannote モデルには Hugging Face トークンが必要です |
| faster-whisper | 高速化に最適化された Whisper | CTranslate2 バックエンドを使用します。同じ精度で、通常の Whisper より約 4 倍高速です。WhisperX 内部で使用されています |
| whisper.cpp | CPU での動作に適した Whisper の移植版 | C++ による実装です。Raspberry Pi で small/medium モデルを実行できます |
| pyannote.audio | 単独で使える話者分離 | 別のソースから取得した文字起こしがすでにある場合に、直接使用します |
| MacWhisper / Buzz | Whisper の GUI ラッパー | コマンドラインを使いたくないジャーナリスト向けの macOS/クロスプラットフォーム対応 GUI です |
| Otter.ai | クラウド文字起こし、リアルタイム対応 | 機微な情報源に使う前に、プライバシーに関する取り扱いを確認してください。Otter Pilot はこれまで、予告なく会議に参加して文字起こしをインデックス化してきました。現在の設定を確認してください |
| Descript | 音声をテキストのように編集 | クリップの切り出しに適しています。クラウドでホストされています |
| Rev (human + AI) | 機微な内容の人手による文字起こし | 時間はかかりますが、より正確です。クラウドでホストされています |
| Trint | ジャーナリスト向け、共同作業に対応 | クラウドでホストされています。チーム機能があります |
| oTranscribe | 無料のウェブベースの手動文字起こし支援ツール | ローカルのみで動作します(ブラウザー内)。アップロードはありません。クラウドサービスに渡せないオフレコ素材に適しています |
関連スキル
- meeting-transcript-actions、検証済みの文字起こしから、発言者を明記したメモ、決定事項、担当者付きのアクションを作成します
- interview-prep、インタビュー前の調査、質問の設計、同意を得るための台本、録音に関する法律の管轄区域を扱います
- source-verification、インタビュー前に情報源の信頼性を検証します
- fact-check-workflow、公開前に録音と照合して引用を検証します
- foia-requests、インタビューの質問に役立つ文書を入手します
- data-journalism、インタビューで言及されたデータソースを分析します
- newsroom-style、逐語引用を AP スタイルの記事用原稿に整えます
スキルのメタデータ
| フィールド | 値 |
|---|---|
| version | 1.0.0 |
| created | 2025-12-26 |
| updated | 2026-05-08 |
| author | Joe Amditis |
| domain | journalism, research |
| complexity | intermediate |