
简介面向Python Web开发学习者与毕业设计、课程设计学生这是一个基于语音识别的智能垃圾分类系统完整项目采用PythonDjangoMySQL实现涵盖前台信息展示、语音垃圾分类、用户管理、后台分类维护等模块可帮助掌握从语音输入到垃圾类别判断的完整链路。压缩包共305个文件大小9.4MB以py源码、HTML模板、CSS样式、JavaScript脚本为主同时包含MySQL数据库脚本、图标与音频素材、说明文档及演示视频便于直接运行与二次开发。目前已有388人学习源码亲测可用配有演示和说明适合作为项目参考、答辩支撑或课程设计扩展起点。1. 语音识别 智能垃圾分类为什么这个「毕设户型」值得照着做先说一个反直觉的结论这个项目的难点不在语音识别准确率而在识别之后的分类映射和误判兜底。标题里的“源码说明演示视频.zip”看起来像个平平无奇的课程设计包但真正动手的人会卡在同一个地方——语音识别把“矿泉水瓶”转对了系统却不知道怎么跟垃圾类别挂上钩。适合这个项目的人群很明确想拿 Django 做完整实战的新手需要给智慧城市或校园场景做可演示原型的开发者以及正在准备答辩、希望现场不翻车的毕业生。它把“录音 → 识别 → 分类 → 反馈”这条链路完整走一遍比零散的增删改查 API 更有说服力也更容易扩展成带硬件传感器的真实垃圾箱。2. 先把架构立住从音频采集到分类落地的完整链路与选型2.1 子系统划分录音端、Django 服务端、识别引擎、分类器四件套语音智能垃圾分类系统本质上是一条流水线。用户对着箱体说“矿泉水瓶”麦克风采集音频Django 收到音频后交给识别引擎识别引擎返回文字分类器根据文字判断垃圾类别最后把结果写进数据库并反馈给屏幕或闸机。很多新手把注意力全放在 Django 的 ORM 和视图上结果卡在音频格式和识别引擎调不上来。我一般会把工程拆成四个子模块录音端浏览器页面或硬件采集、Django 服务端、识别引擎、分类器。Django 在里面是粘合剂不是全部。四个子模块各自的输入输出很清晰子系统输入输出核心职责录音端麦克风声音音频文件采集、时长控制、格式转换Django 服务端音频文件JSON 响应接收、校验、调度、入库识别引擎音频文件文本 置信度语音转文字分类器文本 置信度垃圾类别别名归一、映射、兜底设计时要把“未识别”当成一等公民而不是异常。用户可能说“这个扔哪里”这句话本身没有垃圾名词也可能说“塑料瓶”但是口音导致识别成“素料瓶”。如果你在架构里不留兜底分支演示现场就会直接卡住。后面的模型设计里会有一个status字段专门记录pending / recognized / uncertain三种状态这就是为兜底留的口子。2.2 语音识别引擎选型命令词识别与开放 ASR 怎么平衡语音识别这一层是选择困难症的重灾区。离线命令词识别比如 Vosk 的固定词表模式或 PocketSphinx最大的优势是免费、低延迟、不依赖网络词表就几十个垃圾名词识别速度能控制在 1 秒内。缺点是用户一旦说出“帮我把这个瓶子扔一下”这种自然口语它只认词表里的“瓶子”前后缀会造成漏配。开放 ASR 服务正好相反口语理解能力强但延迟、费用、并发配额都是现场事故的伏笔。我通常的做法是先按离线命令词识别把整条链路跑通再在代码里把识别引擎封装成可替换的服务类。这样在线 ASR 只是换一个实现不用动视图和分类器。参数上的关键点有两个音频采样率 16k单声道识别前统一转成 wav 或 pcm。很多识别引擎对 8k 采样率或双声道的支持很不稳定识别率会肉眼可见地下降。给出一组常见的选型参数对比比较项离线命令词识别在线 ASR 服务音频格式wav / pcm16k 单声道wav / mp3 / amr具体看服务商文档词表自定义几十到几百个词条不限制依赖自然语言理解延迟0.5-1 秒1-3 秒加网络开销成本无按调用次数或音频时长计费更适合的场景演示、低成本试点、离线垃圾箱方言重、口语开放、可接受联网依赖2.3 分类映射为什么“识别准确”和“分类正确”是两码事这是最容易出 bug 的环节。语音识别把“矿泉水瓶”转对了只是第一步系统还得知道矿泉水瓶属于可回收物。真正现场跑起来会遇到三类麻烦。第一用户说的是“饮料瓶”“空瓶子”“矿泉水”这些口语词要能归一到一个标准物品名。第二用户说“这个扔哪里”句子本身没有物品名词识别再准也没法分类。第三口音问题“塑料”被识别成“速料”“电池”被识别成“电持”。所以映射表不能只做完全相等匹配要做一级归一化。我一般会建两张表一张是“语音文本 → 标准物品名”的别名表另一张是“标准物品名 → 垃圾类别”的分类表。别名表解决口语差异分类表解决归类规则。源码包里常见的做法是把这两张表放到data/word_map.json和data/category_map.json好处是保洁阿姨或运营人员也能维护不用为了改一个别名就碰代码。分类结果上还要带一个置信度字段。匹配到别名表且相似度高于 0.85才算高置信低于阈值时返回“请再说一遍”或“未识别”而不是随便给一个分类。这个“克制”在演示视频里看起来很智能实际上就是做好兜底。下一章要写的代码就是把这些逻辑落成 Django 里能直接跑的模型和视图。3. 在 Django 里创建 app 并落地核心代码模型、视图、识别服务一条线3.1 创建 app 并写清四个核心模型Django 项目实战的第一步通常是创建项目和应用。常见命令是django-admin startproject config .然后python3 manage.py startapp waste。这里的waste只放垃圾分类相关的业务音频处理和识别逻辑放到独立模块里避免 app 之间揉成一团。模型建议先建四个Category保存垃圾类别GarbageItem保存标准物品名和别名VoiceRecord保存每次语音识别记录ClassifyResult保存分类结果。下面是精简后的waste/models.pyfrom django.db import models class Category(models.Model): # code 用字符串如 recyclable / kitchen / harmful / other code models.CharField(max_length32, uniqueTrue, verbose_name类别编码) name models.CharField(max_length64, verbose_name类别名称) parent models.ForeignKey( self, nullTrue, blankTrue, on_deletemodels.CASCADE, verbose_name上级类别 ) description models.TextField(blankTrue, verbose_name垃圾分类说明) sort_order models.IntegerField(default0, verbose_name排序) class Meta: db_table waste_category class GarbageItem(models.Model): name models.CharField(max_length64, uniqueTrue, verbose_name标准物品名) category models.ForeignKey( Category, on_deletemodels.PROTECT, verbose_name所属垃圾类别 ) aliases models.JSONField(defaultlist, verbose_name语音别名/口语别名) class Meta: db_table waste_garbage_item class VoiceRecord(models.Model): STATUS_CHOICES [ (pending, 待识别), (recognized, 已识别), (uncertain, 低置信/未识别), ] device_no models.CharField(max_length16, blankTrue, verbose_name设备编号) audio_file models.FileField(upload_tovoice_records/%Y%m/, verbose_name音频) duration models.FloatField(default0, verbose_name音频时长(秒)) transcript models.TextField(blankTrue, verbose_name识别文本) confidence models.FloatField(default0, verbose_name置信度) status models.CharField(max_length16, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table waste_voice_record ordering [-created_at] class ClassifyResult(models.Model): voice_record models.OneToOneField( VoiceRecord, on_deletemodels.CASCADE, verbose_name对应语音记录 ) garbage_item models.ForeignKey( GarbageItem, nullTrue, blankTrue, on_deletemodels.SET_NULL ) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name最终类别) method models.CharField(max_length32, defaultrule, verbose_name分类方法) feedback models.CharField(max_length64, blankTrue, verbose_name用户反馈) class Meta: db_table waste_classify_result几个字段参数值得说明。Category.code用字符串而不是自增 id是因为前端和硬件闸机都要用稳定编码去映射演示视频里也常常直接显示“可回收物”而不是数字。GarbageItem.aliases用 JSONField 存数组比单独建一张别名表更直观查询时遍历数组也不会有性能问题因为数据量最多几千条。VoiceRecord.confidence是后面兜底逻辑的开关识别引擎没给置信度时默认 0分类器判断为低置信就不写入具体类别。ClassifyResult针对一次语音记录做一对一记录如果后续用户手动纠正可以更新feedback字段方便做准确率统计。3.2 用一张视图把“音频上传→识别→分类→入库”串起来视图层不建议拆成一堆细碎的接口先做一个核心上传接口。常见路径是POST /api/voice/upload/接收multipart/form-data的文件字段。下面是一个直接用 DjangoJsonResponse写的视图不依赖 DRF 也能跑通import json import os import subprocess import uuid from django.conf import settings from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from .models import VoiceRecord, ClassifyResult from services.asr_service import recognize_audio from services.classifier import classify_text FILE_MAX_SIZE 10 * 1024 * 1024 # 10MB csrf_exempt def upload_and_classify(request): if request.method ! POST: return JsonResponse({code: 405, msg: 只支持 POST}, status405) audio_file request.FILES.get(file) if not audio_file: return JsonResponse({code: 400, msg: 缺少 file 字段}, status400) if audio_file.size FILE_MAX_SIZE: return JsonResponse({code: 400, msg: 文件超过 10MB}, status400) # 保存原始文件 ext os.path.splitext(audio_file.name)[1].lower() or .wav safe_name f{uuid.uuid4().hex}{ext} saved_path os.path.join(settings.MEDIA_ROOT, voice_records, tmp) os.makedirs(saved_path, exist_okTrue) full_path os.path.join(saved_path, safe_name) with open(full_path, wb) as f: for chunk in audio_file.chunks(): f.write(chunk) # 统一转成 16k 单声道 wav方便识别引擎 wav_path os.path.join(saved_path, f{uuid.uuid4().hex}.wav) cmd [ ffmpeg, -y, -i, full_path, -ar, 16000, -ac, 1, -f, wav, wav_path ] result subprocess.run(cmd, capture_outputTrue) if result.returncode ! 0: return JsonResponse({code: 500, msg: 音频格式转换失败}, status500) # 识别 分类 transcript, confidence recognize_audio(wav_path) if not transcript: record VoiceRecord.objects.create( audio_filefvoice_records/tmp/{os.path.basename(wav_path)}, statusuncertain, transcript, confidence0, ) return JsonResponse({code: 200, data: {transcript: , category: None}}) category_code, category_name, sim_score classify_text(transcript) record VoiceRecord.objects.create( audio_filefvoice_records/tmp/{os.path.basename(wav_path)}, transcripttranscript, confidenceconfidence, statusrecognized if category_code else uncertain, ) if category_code: ClassifyResult.objects.create( voice_recordrecord, category_idcategory_code, methodrule, ) return JsonResponse({code: 200, data: { transcript: transcript, category_code: category_code, category_name: category_name, confidence: round(sim_score, 2), }})这段代码里有几个细节需要专门说明。第一文件上传后先保存原始文件再用 ffmpeg 转成 16k 单声道 wav这一步不要省识别引擎对音频格式很敏感。第二FILE_MAX_SIZE 10MB是经验值太长语音没有意义太短又可能没采到有效声音。第三保存路径用uuid重命名避免中文文件名和特殊字符在 Windows 下出问题。第四Category.objects的id这里直接用category_code映射实际工程里更好的是先查Category实例再传对象下面的分类器逻辑会写清楚。3.3 把识别引擎封装成可替换的服务模块标题里没写死用哪个语音识别服务所以代码里要留好切换口子。我一般会在项目根目录建一个services包里面放asr_service.py。这样视图层不用关心底层是 Vosk 还是在线 API后面现场部署想换引擎只改这个文件。# services/asr_service.py import os from django.conf import settings # settings 里增加字段例如 ASR_ENGINE vosk / online def recognize_audio(wav_path): engine getattr(settings, ASR_ENGINE, vosk) if engine vosk: return _recognize_with_vosk(wav_path) if engine online: return _recognize_with_online(wav_path) return , 0.0 def _recognize_with_vosk(wav_path): # 常见离线方案加载 Vosk 中文模型 try: from vosk import Model, KaldiRecognizer import wave, json as json_lib model_path getattr(settings, VOSK_MODEL_PATH, models/vosk-model-small-cn) model Model(model_path) wf wave.open(wav_path, rb) if wf.getframerate() ! 16000: return , 0.0 rec KaldiRecognizer(model, 16000) rec.AcceptWaveform(wf.readframes(wf.getnframes())) result json_lib.loads(rec.FinalResult()) text result.get(text, ) confidence float(result.get(confidence, 0.0)) return text, confidence except ImportError: raise RuntimeError(需要安装 vosk 并配置 VOSK_MODEL_PATH) def _recognize_with_online(wav_path): # 常见在线方案调用服务商的音频识别接口 # 这里只做方式说明不能直接跑通实际需要填认证 key 和 URL import requests api_url getattr(settings, ASR_ONLINE_URL, ) api_key getattr(settings, ASR_ONLINE_KEY, ) with open(wav_path, rb) as f: resp requests.post( api_url, headers{Authorization: fBearer {api_key}}, files{audio: f}, timeout15, ) result resp.json() return result.get(text, ), float(result.get(confidence, 0.0))参数说明ASR_ENGINE在settings.py里配置默认vosk是因为它适合离线演示在线 API 的 URL 和 key 是环境相关配置别直接写死在代码里。另一个关键参数是VOSK_MODEL_PATHVosk 需要单独下载中文模型文件源码目录里一般会有说明但不会把模型直接打进 zip。如果你解压后发现没有models/vosk-model-small-cn就去 Vosk 官网按对应版本下载这个在说明文档里通常会写清楚。3.4 分类器的阈值、相似度与未识别走查分类器是系统的“大脑”它拿到识别文本后要做四件事去除标点和空白转小写查别名表算相似度。这里用difflib.SequenceMatcher来做模糊匹配写起来简单也比强制“完全相等”可靠很多。下面是一个可运行的services/classifier.py# services/classifier.py import json import os import re from difflib import SequenceMatcher from django.conf import settings def _load_word_map(): path getattr(settings, WORD_MAP_PATH, data/word_map.json) if not os.path.exists(path): return [], {} with open(path, r, encodingutf-8) as f: return json.load(f), {} def normalize_text(text): text text.lower().strip() # 去掉中英文标点和空白 text re.sub(r[\s。、,.!?;:\\-], , text) return text def classify_text(transcript): # word_map 结构示例[{name: 矿泉水瓶, category_code: recyclable, aliases: [矿泉水瓶, 饮料瓶, 空瓶子]}] word_map, _ _load_word_map() transcript_clean normalize_text(transcript) best_score 0.0 best_item None best_category_code None for item in word_map: aliases item.get(aliases, []) [item[name]] for alias in aliases: alias_clean normalize_text(alias) score SequenceMatcher(None, transcript_clean, alias_clean).quick_ratio() if score best_score: best_score score best_item item best_category_code item.get(category_code) threshold getattr(settings, CLASSIFY_SIMILARITY_THRESHOLD, 0.85) if best_item and best_score threshold: return best_category_code, best_item[name], best_score return None, , best_score这里真正决定系统体验的是CLASSIFY_SIMILARITY_THRESHOLD。阈值设成 0.95误判少但很多口语化说法会被拒设成 0.75识别率高但“塑料瓶”和“塑料袋”可能互相串。我建议默认 0.85然后拿真实录音样本去调。quick_ratio()比ratio()快但对长度差很大的两个字符串不那么敏感所以别名表里要覆盖常见口语长度比如“矿泉水”和“矿泉水瓶”长度差一个字符quick_ratio的得分仍然能到 0.9 以上。如果连续多次低置信就把VoiceRecord.status标记成uncertain这就是分类器兜底的价值。4. 从源码到本地跑通搭建环境、测试接口、连上简易前端4.1 环境清单、依赖安装与 FFmpeg 检查拿到 zip 之后不要急着python manage.py runserver。先按通常的 Django 项目部署次序来解压 zip创建虚拟环境安装依赖。这里用python3 -m venv venv因为常见的部署环境是 Linux 服务器或 macOSWindows 上用py -m venv venv也一样。依赖核心就是 Django、Vosk、requests以及 FFmpeg。# 假设源码包已解压到 ~/smart_garbage/ cd ~/smart_garbage python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install django vosk requests # 检查 FFmpeg ffmpeg -version如果ffmpeg -version报错说明系统还没装 FFmpeg。macOS 用brew install ffmpegUbuntu/Debian 用sudo apt install ffmpegWindows 建议下载 ffmpeg 的 zip 包解压后加到系统 PATH。这一步骤很关键因为视图里把音频转 16k 单声道依赖 ffmpeg没装就会在转格式那里直接翻车。接下来配置settings.py。至少要改三处MEDIA_ROOT和MEDIA_URLVOSK_MODEL_PATH以及添加waste到INSTALLED_APPS。很多新手拿到源码后直接跳过这一步结果上传文件路径错、模型文件找不到。下面这段可以直接追加到settings.pyimport os MEDIA_ROOT os.path.join(BASE_DIR, media) MEDIA_URL /media/ VOSK_MODEL_PATH os.path.join(BASE_DIR, models, vosk-model-small-cn) ASR_ENGINE vosk # 可切换为 online CLASSIFY_SIMILARITY_THRESHOLD 0.85 WORD_MAP_PATH os.path.join(BASE_DIR, data, word_map.json)4.2 用 curl 和一段测试音频验证接口没有真实录音时可以先用文本合成语音生成测试音频。常见做法是用espeak-ng或在线 TTS 生成“矿泉水瓶”这句话的 wav然后交给接口。下面是用espeak-ng生成测试音频并用 curl 提交的写法# 生成 16k 单声道测试音频 espeak-ng -v zh -s 160 -w test.wav 矿泉水瓶 # 转成 16k 单声道 wav保险起见 ffmpeg -y -i test.wav -ar 16000 -ac 1 -f wav test_16k.wav # 提交到 Django 上传接口 curl -F filetest_16k.wav;typeaudio/wav \ http://127.0.0.1:8000/api/voice/upload/如果你手里没有 espeak-ng用手机录音后拖到电脑上转一下格式也可以。重点是接口返回的 JSON 里要有transcript和category_code。如果返回transcript: 先别急着改代码检查音频是不是 16k 单声道再检查 Vosk 模型路径是不是对的。这条链路上最容易翻车的就是格式和路径九个问题里有八个出在这两处。4.3 简易前端录音页MediaRecorder 采集并上传演示视频里最常见的画面是网页上有个录音按钮点击说话马上显示分类结果。前端实现用浏览器自带的MediaRecorder就够了不需要引第三方库。下面这段 HTML 放在 Django 模板里可运行button idrecordBtn开始录音/button button idstopBtn styledisplay:none;停止/button p idresult/p script let mediaRecorder; let chunks []; document.getElementById(recordBtn).onclick async () { const stream await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorder new MediaRecorder(stream); chunks []; mediaRecorder.ondataavailable (e) chunks.push(e.data); mediaRecorder.onstop async () { const blob new Blob(chunks, { type: mediaRecorder.mimeType || audio/webm }); const formData new FormData(); formData.append(file, blob, voice.webm); const resp await fetch(/api/voice/upload/, { method: POST, body: formData }); const data await resp.json(); document.getElementById(result).innerText 识别结果${data.data.transcript}类别${data.data.category_name}; stream.getTracks().forEach(track track.stop()); }; mediaRecorder.start(); document.getElementById(recordBtn).style.display none; document.getElementById(stopBtn).style.display inline; }; document.getElementById(stopBtn).onclick () { mediaRecorder.stop(); document.getElementById(recordBtn).style.display inline; document.getElementById(stopBtn).style.display none; }; /script这里有个明显区别浏览器录出来的通常是audio/webm或audio/ogg而后端视图已经用 ffmpeg 转成 wav 了所以接口层不需要刻意要求客户端必须传 wav。许多人写成只接受audio/wav的 content-type结果一到浏览器演示就报错。我推荐后端按扩展名和文件内容判断不要在 content-type 上卡死后面第 5 章会展开讲这个坑。4.4 在 Django 后台核验识别记录和分类台账接口跑通之后还要验证数据落库是否符合预期。在waste/admin.py里把记录模型注册进去Django 自带后台就能按时间、状态、类别去筛选。# waste/admin.py from django.contrib import admin from .models import Category, GarbageItem, VoiceRecord, ClassifyResult admin.register(VoiceRecord) class VoiceRecordAdmin(admin.ModelAdmin): list_display (id, device_no, transcript, confidence, status, created_at) list_filter (status, created_at) search_fields (transcript,) admin.register(ClassifyResult) class ClassifyResultAdmin(admin.ModelAdmin): list_display (voice_record, category, method, feedback)打开http://127.0.0.1:8000/admin/你能看到每条语音识别记录包括置信度。如果某条明显识别错误比如“矿泉水瓶”识别成“水瓶子”可以去调整word_map.json里的别名而不是改代码。这就是把规则做成数据文件的好处演示现场发现某类垃圾老分错当场在 JSON 里加一个别名重启服务或重新加载映射就能生效不用等重新部署。5. 踩坑记录语音垃圾分类从能跑到好用之间的关键问题5.1 现象接口返回“无结果”但音频文件确实上传成功了这是语音识别项目最经典的开局挫折。原因通常是采样率和声道不满足引擎要求。浏览器录的 webm 文件采样率可能是 48k双声道Vosk 官方模型明确要求 16k 单声道 16bit。解决方法是所有音频进引擎前先过一遍 ffmpeg 转格式不要直接拿原始上传文件去识别。我在 3.2 的视图里已经写了ffmpeg -ar 16000 -ac 1这个参数组合是硬性约定别删。5.2 现象上传文件一直报“格式不对”换 wav 也不行后端如果写request.FILES[file].content_type audio/wav才放行浏览器端很容易挂掉因为 MediaRecorder 的 mimeType 通常是audio/webm;codecsopus。更隐蔽的是有些手机浏览器录音后上报的 content-type 是空的。解决方式是别依赖 content-type按扩展名和文件头做粗略校验甚至只校验文件大小和能否被 ffmpeg 解复用。让 ffmpeg 成为格式判断的裁判比自己在 Python 里写白名单更可靠。5.3 现象后台台账日期差 8 小时凌晨的记录落到前一天Django 默认时区是UTC如果settings.py里没配TIME_ZONE和USE_TZ本地跑时会发现created_at显示的时间跟北京时间对不上。解决方式是设置TIME_ZONE Asia/Shanghai USE_TZ FalseUSE_TZ False的意思是数据库里存本地时间对演示项目更直观。如果你的项目里已经有大量用了 UTC 的数据改完时区后旧数据看起来会漂移所以这类设置最好在项目初始化时就定好别等项目写一半再改。5.4 现象解压后按说明启动报“模型路径不存在”或“模型加载失败”源码包里的说明文字很容易漏写模型文件。Vosk 中文模型体积从几十 MB 到两百 MB 不等很多 zip 交付包为了控制大小故意不包含模型只在 README 里给下载方式。新手如果只看着demo视频.zip里那几秒画面意识不到少了这个庞然大物。解决方法是启动前先确认models/vosk-model-small-cn目录完整至少包含am、conf、ivector、graph这些子目录。如果不齐下载对应版本放进目录不要改代码里写死的相对路径。5.5 现象现场演示时识别速度突然变慢甚至报“并发超限”用在线 ASR 时这个问题几乎必现。原因通常是免费配额用完或并发数超过限制而且演示现场没有网络或者网络差。解决方法是把默认引擎设为离线 Vosk在线 API 作为可选增强。另一个备选方案是降级策略在线识别失败时自动走离线词表匹配至少保证“矿泉水瓶”“电池”这种标准词能出结果。后端代码里可以用 try/except 包住_recognize_with_online异常时调_recognize_with_vosk这样演示不会死在第三方接口上。5.6 现象分类器把“大棒骨”分到厨余垃圾但演示视频里却显示正确这通常是word_map.json的别名和图片/视频不一致造成的。比如视频演示里录的是“大棒骨”分类器有对应的别名但实际部署里同一个名字在语音识别后变成了“大棒骨”却因为词表里写的是“棒骨”相似度不到阈值就被误分到其他类。解决方法是把易混词做一组“排除词表”例如“大棒骨”应当归到其他垃圾或厨余垃圾视当地政策而定然后把“棒骨”“大棒骨”“猪骨”等词统一追加到同一个标准物品名下面。做完之后要拿同一批测试音频重新跑一遍回归确认不会把“鸡骨头”也带偏。6. 一个救场技巧用拼音模糊匹配兜住方言和不标准普通话现场演示最吓人的不是引擎没识别而是用户真的说了一句塑料普通话识别结果跟标准词差几个字分类器又太死板直接判“未识别”。这里给一个低成本高回报的补丁先把识别文本转成拼音再用拼音做相似度匹配。这样可以兜住“塑料”识别成“速料”、“电池”识别成“电持”这类同音替换效果立竿见影。# services/classifier.py 追加拼音匹配逻辑 from pypinyin import lazy_pinyin from difflib import SequenceMatcher def to_pinyin_key(text): return .join(lazy_pinyin(text.lower())) def classify_text_with_pinyin(transcript): transcript_clean normalize_text(transcript) transcript_py to_pinyin_key(transcript_clean) best_score 0.0 best_category_code None for item in word_map: for alias in item.get(aliases, []): alias_py to_pinyin_key(normalize_text(alias)) score SequenceMatcher(None, transcript_py, alias_py).quick_ratio() if score best_score: best_score score best_category_code item.get(category_code) if best_score getattr(settings, CLASSIFY_PINYIN_THRESHOLD, 0.88): return best_category_code, , best_score return None, , best_score第 6 章可以加一个表格作为验证方法准备 20 句不同口音的样本统计加入拼音匹配前后的命中率变化。比如“速料袋”“拉极”“费电池”这类容易被语音引擎错写的话拼音匹配能救回一大部分。我在实际项目里的习惯是每改一次词表就跑一遍这 20 句回归样本把命中率记录贴在 README 里这样每次演示前心里有底。后来不管做哪个语音识别项目都会保留这一份“音频样本 期望结果”的回归清单不再临时抱佛脚。希望这个步骤也能帮你躲开那些现场翻车的尴尬时刻让你把更多精力放在用户拿起来就能用的真实体验上。本文还有配套的精品资源点击获取