Python实现语音审核模型

这是一个语音审核Python代码,集成了语音识别和本地敏感词匹配,能快速判断音频中是否存在违规内容。

```python

-*- coding: utf-8 -*-

"""

语音内容审核 Demo

流程:音频转码(ffmpeg) -> 语音识别(ASR) -> 文本审核(敏感词DFA + 正则) -> 风险定级

"""

from future import annotations

import asyncio

import json

import logging

import os

import re

import subprocess

import tempfile

import unicodedata

from dataclasses import dataclass, asdict

from enum import IntEnum

from pathlib import Path

from typing import Any, Iterable, Protocol, Sequence

logger = logging.getLogger(name)

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

一、基础数据结构

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

class RiskLevel(IntEnum):

PASS = 0 # 通过

REVIEW = 1 # 疑似,转人工复审

BLOCK = 2 # 违规,直接拦截

RISK_LABELS = {

RiskLevel.PASS: "通过",

RiskLevel.REVIEW: "疑似-转人工",

RiskLevel.BLOCK: "违规-拦截",

}

CATEGORY_LABELS = {

"porn": "色情低俗",

"politics": "涉政敏感",

"violence": "暴力恐怖",

"abuse": "辱骂攻击",

"ad": "广告导流",

"pii": "个人隐私",

}

分类 -> 默认风险等级

DEFAULT_CATEGORY_RISK = {

"porn": RiskLevel.BLOCK,

"politics": RiskLevel.BLOCK,

"violence": RiskLevel.BLOCK,

"abuse": RiskLevel.REVIEW,

"ad": RiskLevel.REVIEW,

"pii": RiskLevel.REVIEW,

}

@dataclass

class Segment:

"""ASR 输出的一个语音片段"""

start: float

end: float

text: str

@dataclass

class Hit:

"""一条命中记录"""

keyword: str # 命中的原文内容

category: str # 违规分类

source: str # lexicon / regex

segment_index: int # 所属语音片段下标

text_start: int # 在片段文本中的起始偏移

text_end: int

time_start: float # 对应音频时间(秒,估算)

time_end: float

snippet: str # 上下文,便于人工复核

@dataclass

class ModerationResult:

audio: str

duration: float

risk: RiskLevel

text: str

segments: listSegment

hits: listHit

@property

def passed(self) -> bool:

return self.risk == RiskLevel.PASS

def to_dict(self) -> dictstr, Any:

return {

"audio": self.audio,

"duration": round(self.duration, 3),

"risk": self.risk.name,

"risk_label": RISK_LABELSself.risk,

"passed": self.passed,

"text": self.text,

"hits": asdict(h) for h in self.hits,

"segments": asdict(s) for s in self.segments,

}

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

二、敏感词匹配(DFA / Trie,O(n) 扫描,支持最长匹配)

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

_END = "\x00" # 词尾标记,正常文本不会包含该字符

归一化时需要跳过的干扰字符(空格 / 标点等,防止 "赌 博"、"赌*博" 绕过)

_SKIP_CHARS = set(

" \t\r\n\u3000,,.。!!??;;::'\"""''()()\[\]【】<>《》----_+*/\\|~`·、"

)

def normalize(text: str) -> tuplestr, list\[int]:

"""

归一化文本:NFKC(全角转半角)+ 小写 + 去干扰字符。

返回 (归一化文本, 每个字符在原文中的下标),便于把命中位置映射回原文。

"""

chars: liststr = \[\]

mapping: listint = \[\]

for i, raw in enumerate(text):

for ch in unicodedata.normalize("NFKC", raw).lower():

if ch.isspace() or ch in _SKIP_CHARS:

continue

chars.append(ch)

mapping.append(i)

return "".join(chars), mapping

class LexiconMatcher:

"""基于 Trie 的敏感词匹配器,返回最长匹配结果"""

def init(self) -> None:

self._root: dictstr, Any = {}

def add(self, word: str, category: str) -> None:

word = word.strip()

if not word:

return

node = self._root

for ch in word:

node = node.setdefault(ch, {})

node_END = category

def build(self, items: Iterabletuple\[str, str]) -> None:

for word, category in items:

self.add(word, category)

def match(self, text: str) -> listtuple\[int, int, str, str]:

"""返回 (start, end, word, category), ...,非重叠、最长优先"""

results: listtuple\[int, int, str, str] = \[\]

n = len(text)

i = 0

while i < n:

node = self._root

j = i

last_hit: tupleint, int, str, str | None = None

while j < n and textj in node:

node = nodetext\[j]

j += 1

if _END in node:

last_hit = (i, j, texti:j, node_END)

if last_hit:

results.append(last_hit)

i = last_hit1

else:

i += 1

return results

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

三、文本审核:词库 + 正则规则

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

REGEX_RULES: listtuple\[str, str, re.Pattern] = [

("pii", "手机号", re.compile(r"(?<!\d)13-9\d{9}(?!\d)")),

("pii", "身份证号", re.compile(r"(?<!\d)\d{17}\\dXx(?!\d)")),

("ad", "引流联系方式",

re.compile(r"(?:加|联系|私聊|咨询)?\s*(?:微信|vx|v信|薇信|qq|企鹅)\s*::?\s*a-zA-Z0-9_\\-{4,}", re.I)),

("ad", "外部链接", re.compile(r"(?:https?://|www\.)\^\\s,。!?+", re.I)),

]

class TextModerator:

def init(

self,

lexicon: Iterabletuple\[str, str] = (),

category_risk: dictstr, RiskLevel | None = None,

) -> None:

self.matcher = LexiconMatcher()

self.matcher.build(lexicon)

self.category_risk = {**DEFAULT_CATEGORY_RISK, **(category_risk or {})}

-- 时间估算:按字符比例映射到片段起止时间 ----------------------------

@staticmethod

def _time_at(seg: Segment, text: str, idx: int) -> float:

if not text:

return seg.start

ratio = min(max(idx / len(text), 0.0), 1.0)

return seg.start + (seg.end - seg.start) * ratio

def moderate(self, segments: SequenceSegment) -> listHit:

hits: listHit = \[\]

for si, seg in enumerate(segments):

hits.extend(self._scan_segment(si, seg))

去重(同一位置同一分类只保留一条)

seen: settuple\[int, int, int, str] = set()

unique: listHit = \[\]

for h in hits:

key = (h.segment_index, h.text_start, h.text_end, h.category)

if key not in seen:

seen.add(key)

unique.append(h)

return unique

def _scan_segment(self, si: int, seg: Segment) -> listHit:

hits: listHit = \[\]

text = seg.text

1) 敏感词库

norm, mapping = normalize(text)

if norm:

for s, e, _word, cat in self.matcher.match(norm):

ts, te = mappings, mappinge - 1 + 1

hits.append(Hit(

keyword=textts:te,

category=cat,

source="lexicon",

segment_index=si,

text_start=ts,

text_end=te,

time_start=round(self._time_at(seg, text, ts), 3),

time_end=round(self._time_at(seg, text, te), 3),

snippet=textmax(0, ts - 10): te + 10,

))

2) 正则规则(手机号、链接等,直接在原文上匹配)

for cat, name, pattern in REGEX_RULES:

for m in pattern.finditer(text):

ts, te = m.start(), m.end()

hits.append(Hit(

keyword=m.group(),

category=cat,

source=f"regex:{name}",

segment_index=si,

text_start=ts,

text_end=te,

time_start=round(self._time_at(seg, text, ts), 3),

time_end=round(self._time_at(seg, text, te), 3),

snippet=textmax(0, ts - 10): te + 10,

))

return hits

def calc_risk(self, hits: SequenceHit) -> RiskLevel:

level = RiskLevel.PASS

for h in hits:

level = max(level, self.category_risk.get(h.category, RiskLevel.REVIEW))

return level

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

四、语音识别引擎

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

class ASREngine(Protocol):

def transcribe(self, wav_path: Path) -> listSegment: ...

class WhisperASR:

"""本地 ASR(faster-whisper)。首次运行会自动下载模型。"""

def init(

self,

model_size: str = "small",

device: str = "cpu",

compute_type: str = "int8",

language: str = "zh",

) -> None:

from faster_whisper import WhisperModel # 延迟导入,避免无依赖时导入失败

self.model = WhisperModel(model_size, device=device, compute_type=compute_type)

self.language = language

def transcribe(self, wav_path: Path) -> listSegment:

segments, _info = self.model.transcribe(

str(wav_path),

language=self.language,

beam_size=5,

vad_filter=True, # 静音过滤,减少幻觉

vad_parameters={"min_silence_duration_ms": 500},

)

return [

Segment(float(s.start), float(s.end), s.text.strip())

for s in segments

if s.text and s.text.strip()

]

class CloudASR:

"""

云厂商 ASR 适配器(占位)。

生产环境建议直接使用云厂商的「语音内容审核」一体化接口,例如:

  • 腾讯云 音频审核 (AudioModeration)

  • 阿里云 语音反垃圾 (VoiceModeration)

它们除了文本违规,还能识别音频本身的风险(暴恐、低俗音频、声纹黑名单等)。

"""

def init(self, client: Any, bucket: str, **kwargs: Any) -> None:

self.client = client

self.bucket = bucket

def transcribe(self, wav_path: Path) -> listSegment:

raise NotImplementedError("按所选云厂商 SDK 实现")

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

五、音频工具

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

def convert_to_wav16k(src: Path, dst_dir: Path | None = None) -> Path:

"""用 ffmpeg 转成 16kHz 单声道 wav(ASR 标准输入)"""

fd, tmp = tempfile.mkstemp(suffix=".wav", dir=dst_dir)

os.close(fd)

cmd = [

"ffmpeg", "-nostdin", "-y", "-i", str(src),

"-vn", "-ac", "1", "-ar", "16000", "-f", "wav", tmp,

]

proc = subprocess.run(cmd, capture_output=True)

if proc.returncode != 0:

os.unlink(tmp)

raise RuntimeError(f"ffmpeg 转码失败: {proc.stderr.decode(errors='ignore')-500:}")

return Path(tmp)

def probe_duration(path: Path) -> float:

try:

out = subprocess.run(

["ffprobe", "-v", "error", "-show_entries", "format=duration",

"-of", "default=nw=1:nk=1", str(path)],

capture_output=True, text=True, check=True,

)

return float(out.stdout.strip())

except Exception:

return 0.0

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

六、审核主流程

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

class AudioModerator:

def init(

self,

asr: ASREngine | None = None,

text_moderator: TextModerator | None = None,

) -> None:

self.asr = asr or WhisperASR()

self.text_moderator = text_moderator or TextModerator(DEFAULT_LEXICON)

def moderate_file(self, audio_path: str | Path) -> ModerationResult:

src = Path(audio_path)

if not src.exists():

raise FileNotFoundError(src)

wav = convert_to_wav16k(src)

try:

duration = probe_duration(wav)

segments = self.asr.transcribe(wav)

finally:

wav.unlink(missing_ok=True)

hits = self.text_moderator.moderate(segments)

risk = self.text_moderator.calc_risk(hits)

full_text = "".join(s.text for s in segments)

result = ModerationResult(

audio=str(src),

duration=duration,

risk=risk,

text=full_text,

segments=segments,

hits=hits,

)

logger.info(

"审核完成 file=%s risk=%s hits=%d",

src.name, RISK_LABELSrisk, len(hits),

)

return result

async def moderate_file_async(self, audio_path: str | Path) -> ModerationResult:

"""ASR 是 CPU/IO 阻塞的,放到线程池里跑,避免阻塞事件循环"""

loop = asyncio.get_running_loop()

return await loop.run_in_executor(None, self.moderate_file, audio_path)

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

七、示例词库

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

DEFAULT_LEXICON: listtuple\[str, str] = [

色情低俗

("裸聊", "porn"), ("约炮", "porn"), ("一夜情", "porn"), ("黄色网站", "porn"),

涉政敏感(此处仅为占位示例,实际请接入合规词库服务)

("敏感词A", "politics"), ("敏感词B", "politics"),

暴力恐怖

("炸弹制作", "violence"), ("枪支买卖", "violence"),

辱骂

("傻逼", "abuse"), ("滚你妈", "abuse"), ("去死吧", "abuse"),

广告导流

("加我微信", "ad"), ("扫码进群", "ad"), ("内部渠道", "ad"),

赌博

("赌博", "porn"), ("博彩", "porn"), ("六合彩", "porn"),

]

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

八、使用示例

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

if name == "main":

import sys

logging.basicConfig(

level=logging.INFO,

format="%(asctime)s %(levelname)s %(name)s: %(message)s",

)

if len(sys.argv) < 2:

print("用法: python audio_moderation.py <音频文件>")

sys.exit(1)

moderator = AudioModerator()

同步调用

res = moderator.moderate_file(sys.argv1)

print(json.dumps(res.to_dict(), ensure_ascii=False, indent=2))

异步调用(批量场景)

async def main():

results = await asyncio.gather(*[

moderator.moderate_file_async(p) for p in files

])

asyncio.run(main())

```

审核流程与核心功能拆解

下面按处理顺序梳理一下它主要做了哪些事。

· 音频预处理:通过 ffmpeg 将任意格式音频转为 16kHz 单声道 WAV,方便后续识别。

· 语音识别:默认使用 faster-whisper 本地模型,输出带时间戳的语音片段。

· 文本审核:对识别文本做归一化后,用敏感词 DFA 匹配和正则规则(手机号、链接等)检测违规内容。

· 风险定级:根据命中分类映射到通过、疑似、拦截三个等级,并返回完整审核详情。

文章仅供参考用。

相关推荐
朝朝辞暮i2 小时前
C++ 第 21 课:struct —— 把一组相关数据打包在一起
开发语言·c++·算法
程序员清风2 小时前
Python 数据分析环境搭建:从 Jupyter 到 pandas
python·jupyter·数据分析
月光船幽幽2 小时前
加性偏移外推提升参数识别可靠性
python·算法
SHARK_pssm2 小时前
【C++——类和对象(下)】
开发语言·c++·经验分享·笔记
凯哥Java2 小时前
写代码怎么避免逻辑漏洞?
java·开发语言·人工智能·自动化
BD_Marathon3 小时前
消息对象中字段的说明
java·前端·python
Ai-_Man3 小时前
您您这可以把Dola的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。用AI导出鸭,答案是可以的
开发语言·前端·人工智能·小程序
Java后端的Ai之路3 小时前
Python进阶探索23 - Python中的Time与Datetime模块
开发语言·数据库·python·datetime·time
沫璃染墨3 小时前
《从零入门Linux系统篇(五十四):线程篇·七——互斥锁底层原理:从原子交换到线程竞争与锁实现》
linux·运维·服务器·开发语言·c++·驱动开发·系统架构