关于 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
双设备规则。 始终使用两台设备录音。至少使用手机作为备用设备。如果使用无线领夹麦克风,领夹麦克风单元内置的录音器算作一台设备;运行备用应用的手机则是第二台。
优先使用单声道,除非每位发言人都有专用麦克风,且各自连接到不同声道。两位发言人的声音都串入两个声道的立体声录音,在说话人分离方面不如清晰的单声道录音。
转录工作流
自动转录流程
原版 OpenAI Whisper 可将音频转录为文本,但不会分配说话人标签。要获得带说话人分离的输出(“Speaker 1:”/“Speaker 2:”/等),需要使用将 Whisper 与说话人分离模型相结合的工具,通常是 WhisperX(m-bain/whisperX)。它将 faster-whisper 转录与 pyannote.audio 说话人分离结合起来,一次处理即可生成带词级时间戳和说话人 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 后端)是经过速度优化的版本,在命令行中的用法相同。whisper.cpp 是面向资源受限设备(Raspberry Pi、较旧的笔记本电脑)的 C++ 移植版;它不包含说话人分离功能,但可在 CPU 上轻松运行 small/medium 模型。
手动转录模板
适用于敏感采访或 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 的图形界面封装 | 面向不想使用命令行的记者,提供 macOS / 跨平台图形界面 |
| 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 |