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 匹配和正则规则(手机号、链接等)检测违规内容。

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

文章仅供参考用。

相关推荐
用户019027581617 小时前
Dify / Coze 低代码 Agent 怎么接 A 股行情?
python
布莱克6058 小时前
Swift 常量详解:从 let 到编译期优化
服务器·开发语言·ssh·swift·常量·let
Dumbness8 小时前
不传 day 参数,座位图显示全空:用 Python 标准库手写 Web 框架,请求到业务代码前的 5 个细节
后端·python
布莱克6058 小时前
Swift 可选类型(Optionals)详解:从原理到实战
开发语言·安全·ssh·swift·optional·可选类型
databook9 小时前
收上来一堆 Excel,怎么保证没改错
python·数据分析
2601_9679873710 小时前
Python 教程:默认字典(defaultdict)全维度深度指南
python·性能优化·数据处理·collections·defaultdict
鱼听禅10 小时前
Ubuntu安装YOLOv8进行菌落识别训练
linux·python·yolo·ubuntu
ZhengEnCi10 小时前
Agent 查不出代码 Bug 怎么办:一份带时间戳的调试日志与读日志排查流程
python·ai编程
瑞雪兆丰年兮10 小时前
JavaWeb-day14-SpringBootWeb原理篇
java·开发语言·spring boot·spring·bean·mybatis·javaweb
威哥爱编程11 小时前
模拟面试智能体开发实战 05:给面试官接上题库和检索,题目结构化、大纲可溯源,AI 不再凭印象出题
python·agent·harmonyos