mcpField
I
interview-transcription

interview-transcription

github.com/jamditis创意与媒体438204 次安装4 天前更新
代码仓库

关于 interview-transcription

面向记者的访谈管理工作流,覆盖录音配置、转录、引语核验和消息源整理。提供自动与手动处理方法,帮助将录音转为可核实、可用于报道的材料。

主要功能

  1. 01录音设置与备份建议
  2. 02自动及手动转录流程
  3. 03说话人区分与时间戳
  4. 04引语提取和准确性核验
  5. 05消息源数据库与批量处理

适用场景

  1. 01转录并整理采访录音
  2. 02为事实核查生成带时间戳的引语
  3. 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 是目前效果最好的模型
WhisperXWhisper + 说话人区分m-bain/whisperX。免费。提供带说话人 ID 的词级时间戳。pyannote 模型需要 Hugging Face 令牌
faster-whisper速度优化版 WhisperCTranslate2 后端。在准确度相同的情况下,速度约为原版 Whisper 的 4 倍。WhisperX 内部使用
whisper.cpp适合 CPU 的 Whisper 移植版C++ 实现。可在 Raspberry Pi 上运行 small/medium 模型
pyannote.audio独立的说话人区分工具如果已有其他来源的转录文本,可直接使用
MacWhisper / BuzzWhisper 的图形界面封装面向不想使用命令行的记者,提供 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 风格的稿件以供发布

技能元数据

字段值
version1.0.0
created2025-12-26
updated2026-05-08
authorJoe Amditis
domainjournalism, research
complexityintermediate

评论

支持 Markdown