
简介这是一份基于语音识别的智能垃圾分类系统完整项目采用Python Django框架与MySQL数据库实现适合计算机相关专业学生用于毕业设计、课程设计或项目实战练手。项目划分为前台与后台两大模块前台支持语音上传识别垃圾类别、系统信息展示、个人资料与密码管理后台提供垃圾分类管理、用户维护和首页信息配置功能链路清晰可快速理解Django MTV架构与语音接口对接方式。资源包共305个文件以Python源码、HTML模板、CSS/JS样式脚本、GIF操作截图、SQL建库脚本及MP3/WAV音频样例为主压缩包约9.4MB结构规整便于按模块查阅。源码经亲测可用另附说明文档和演示视频能直观还原系统运行效果已有388人学习下载可作为开发参考或答辩展示材料。1. 为什么智能垃圾分类系统要用语音识别Django 版方案的全貌与交付形态在垃圾分类刚铺开那阵我见过不少小区人工纠察员拿着手机翻小程序对着“大棒骨”查半天——其实查到的又未必是本地分类口径。这个 django 项目实战的智能垃圾分类系统做的就是一件事用户对手机说话系统识别出物品名返回它属于可回收、有害、厨余还是其他垃圾。语音识别在这里不是炫技而是把“查表”这个动作从 3 次点击压缩到 1 秒内。系统本身用 Django 做后端和展示端交付形态是一个 zip 包源码、说明文档、演示视频都在里面。适合刚学完 Django 教程、想拿一个完整全栈项目练手并写进简历的从业者也适合要做社区或校园垃圾分类演示站点的团队。2. Django 后端与识别流程设计先定数据模型再谈语音和分类2.1 先拆业务再写代码垃圾分类不是分类模型问题是别名映射问题很多人在拿到“智能垃圾分类”这个题目时第一反应是上图像识别、上深度学习模型拿一张照片识别垃圾。但如果你真的去一线看使用场景会发现语音输入的占比极高手里提着一袋垃圾腾不出手拍照光线不好、湿垃圾裹着塑料袋摄像头拍出来一团黑。语音识别则只需要一句“卫生纸属于什么垃圾”系统就能落答案。把业务拆开看分类的难点不在“识别出这是什么”而在“同一个东西在不同人口里有几十种叫法”。大棒骨、筒子骨、扇骨本质上都是骨头归属厨余垃圾还是其他垃圾取决于本地厨余处理工艺湿纸巾在多数城市归其他垃圾但带“湿”字容易让新手惯性归入厨余。所以分类引擎的核心不是模型而是一张足够厚的别名映射表。语音识别负责把语音转成文本文本再去匹配别名表匹配到了就返回对应的垃圾类别。这个思路下Django 的 ORM、Admin 后台、数据迁移就全都有用了别名表可以随时在后台补不用发版。这也是我把方案定位成“Django 语音识别接口 关键字映射”而不是“Python 脚本跑识别模型”的原因。2.2 数据模型三个表定全局WasteCategory、WasteItem、GarbageRecord项目里我一般先落三个模型。第一个是 WasteCategory垃圾大类四分类或六分类看当地口径第二个是 WasteItem具体物品名和别名一个物品可以归到一个大类第三个是 GarbageRecord识别请求日志谁在什么时候问了什么、ASR 给了什么文本、最终命中哪个类别。这个模型是整套系统的地基别急着写视图先把表定下来。from django.db import models class WasteCategory(models.Model): name models.CharField(类别名, max_length32, uniqueTrue) code models.CharField(分类编码, max_length16, uniqueTrue) description models.TextField(分类说明, blankTrue) class Meta: db_table waste_category def __str__(self): return self.name class WasteItem(models.Model): category models.ForeignKey( WasteCategory, on_deletemodels.PROTECT, related_nameitems, verbose_name所属分类, ) name models.CharField(标准名称, max_length64, db_indexTrue) aliases models.JSONField(别名列表, defaultlist, blankTrue) note models.CharField(投放提示, max_length128, blankTrue) class Meta: db_table waste_item indexes [models.Index(fields[name], nameidx_item_name)] def __str__(self): return self.name class GarbageRecord(models.Model): query_text models.CharField(ASR识别文本, max_length256) matched_item models.ForeignKey( WasteItem, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name命中物品, ) source models.CharField(输入方式, max_length16, defaultvoice) created_at models.DateTimeField(识别时间, auto_now_addTrue) class Meta: db_table garbage_record这里有两个容易被新手忽略的设计点。一是 WasteItem 的 aliases 用了 JSONField 而不是单独建一张别名表因为别名只是一维字符串数组不存在多对多扩展JSON 字段足够满足后台维护和查询真要拆成表将来做“同义词推荐”时再迁移也不晚。二是 category 外键用了 on_deletemodels.PROTECT而不是 CASCADE。这样删分类时如果里面还有物品会直接报错阻止你误删避免把整张物品表一起带走。你在 Admin 后台维护数据时会感谢这个选择。GarbageRecord 里的 source 字段标记输入方式是语音还是手动打字后面分析用户习惯、看语音识别误识别率都靠它。2.3 创建 app 与项目结构识别服务为什么要独立成模块Django 创建 app 的命令是 python manage.py startapp但这个项目里我不建议把语音识别和分类逻辑塞进同一个 app。常见做法是拆成两个一个叫 waste_classifier负责物品别名匹配和分类结果输出另一个叫 voice_gateway负责录音文件接收、调用识别接口、把识别文本清洗后转交给分类模块。这样拆分的好处是将来想换一家语音识别服务商只动 voice_gateway 一个目录分类逻辑不碰。django-admin startproject garbage_ai . python manage.py startapp waste_classifier python manage.py startapp voice_gateway拆完之后的目录结构大致是garbage_ai/settings.py 放公共配置voice_gateway/views.py 只暴露一个接收音频的接口voice_gateway/services/asr_client.py 封装识别接口调用waste_classifier/services/matcher.py 负责文本清洗和别名查询。settings.py 里要做的额外配置包括上传目录、日志文件和数据库连接。演示视频的录制口径我这里顺便提一句别对着代码讲 PPT直接开一个浏览器窗口、一个后台日志窗口把“说话 - 接口响应 - 数据库多一条记录”全程录下来客户和面试官都容易信。很多人会问为什么不直接让前端调语音识别接口Django 只收文本我的理由是把识别放到后端可以统一控制密钥、限流和日志前端只做录音上传不关心你用的是哪家识别服务。这也能避免前端把密钥暴露到浏览器里属于 Django 项目实战里很实在的一道安全边界。3. 语音识别链路落地从录音上传到 Django 视图查询的完整实现3.1 语音上传接口request.FILES 接收 multipart 的完整链路前端用 MediaRecorder 录音拿到的是 webm 或 ogg 格式的 Blob通过 FormData 传给后端。Django 这边的视图函数要处理的就是一个 multipart/form-data 请求。我一般会限制文件大小、校验 Content-Type然后把文件改名为 uuid 加后缀落盘避免用户上传中文文件名带来的编码问题和路径穿越风险。import os import uuid from datetime import datetime from django.conf import settings from django.http import JsonResponse from django.views.decorators.http import require_POST from django.views.decorators.csrf import csrf_exempt csrf_exempt require_POST def upload_audio(request): if audio not in request.FILES: return JsonResponse({code: 400, msg: 缺少 audio 文件字段}, status400) audio_file request.FILES[audio] if audio_file.size settings.AUDIO_MAX_SIZE: return JsonResponse({code: 400, msg: 音频文件超过大小限制}, status400) allowed_type getattr(settings, AUDIO_ALLOWED_TYPES, [audio/webm, audio/ogg, audio/mp4]) if audio_file.content_type not in allowed_type: return JsonResponse({code: 400, msg: 不支持的音频格式}, status400) ext_map { audio/webm: .webm, audio/ogg: .ogg, audio/mp4: .m4a, audio/wav: .wav, } ext ext_map.get(audio_file.content_type, .bin) file_name f{uuid.uuid4().hex}_{datetime.now().strftime(%Y%m%d)}{ext} save_dir os.path.join(settings.MEDIA_ROOT, voice) os.makedirs(save_dir, exist_okTrue) save_path os.path.join(save_dir, file_name) with open(save_path, wb) as dest: for chunk in audio_file.chunks(): dest.write(chunk) request.session[last_audio_path] save_path return JsonResponse({code: 0, msg: 上传成功, audio_id: file_name})参数上要注意三个点。AUDIO_MAX_SIZE 我通常会设 2MB语音问答场景 10 秒以内的录音撑死一两百 KB2MB 已经给了很大余量Content-Type 白名单很关键否则前端传一个 text/plain 的假音频也能进来chunks() 而不是 read() 是为了避免大文件一次性塞进内存虽然这里文件不大但写顺手了就不会踩内存坑。csrf_exempt 只是因为小程序或普通 H5 前端不方便带 CSRF token内网演示可以这样公网部署不要裸奔建议换成 token 鉴权。3.2 识别与匹配ASR 文本清洗 关键词命中的参数设计录音落盘之后后端要把音频文件交给识别服务。这一步常见做法是调用云端 ASR 接口把文件路径或字节流传过去拿到返回的文本。你自己的项目里可以替换成任意你熟悉的服务代码上保持一个 asr_client 模块就行。我通常会设置两个参数timeout 控制在 5 秒内超过就降级到“请手动输入”top_k 设为 3让接口返回多个候选识别文本避免单条识别结果错了就全错。import re import jieba from waste_classifier.models import WasteItem def clean_asr_text(raw_text): text raw_text.strip().lower() text re.sub(r[。、\s], , text) noise_words [请问, 帮我查一下, 是什么垃圾, 属于什么垃圾, 怎么扔] for word in noise_words: text text.replace(word, ) return text def match_waste_item(query_text): cleaned clean_asr_text(query_text) candidates [] full_match WasteItem.objects.filter( name__iexactcleaned ).select_related(category).first() if full_match: return full_match, name for item in WasteItem.objects.select_related(category).all(): alias_list item.aliases or [] if cleaned in alias_list: return item, alias for alias in alias_list: if len(alias) 2 and alias in cleaned: candidates.append((len(alias), item, alias)) if candidates: candidates.sort(keylambda x: x[0], reverseTrue) return candidates[0][1], alias_partial words [w for w in jieba.lcut(cleaned) if len(w) 2] for word in words: item WasteItem.objects.filter(name__containsword).select_related(category).first() if item: return item, seg_fuzzy return None, no_hit这段逻辑看着简单里面对付的是语音识别常见的噪声。你会发现语音识别结果里经常带着“请问”“帮我查一下”这类寒暄词不做清洗直接全文本 LIKE 查询命中率很低。match_waste_item 里我按优先级排了四条路全名精确匹配别名精确匹配别名包含匹配最后才用 jieba 分词去撞名字。分词兜底这步很慢全表扫一遍带分词大概几百毫秒到一秒只放在最后一步用。这里插一句如果你想看 Django 执行查询和删除对象的细节打开 django-debug-toolbar 或者 captureQueries能看到每次匹配实际落了几条 SQL。别信“这代码跑得很快”的感觉看 SQL 数量才是真的。3.3 视图与缓存把“热词”查询变快的最小改动识别文本命中了一个 WasteItem 之后前端要展示的不只是“厨余垃圾”四个字最好还有投放提示和分类说明。所以分类查询视图在匹配后要重新读一次 category 和 item 信息。这里容易写出 N1 查询我习惯在视图里直接 select_related然后用函数级缓存挡掉重复问题。from django.core.cache import cache from django.http import JsonResponse from django.views.decorators.http import require_POST def build_result(item, match_type): category item.category return { item: item.name, aliases: item.aliases, category: category.name, category_code: category.code, note: item.note, match_type: match_type, } def classify_view(request): query_text request.POST.get(text, ) if not query_text: return JsonResponse({code: 400, msg: text 不能为空}, status400) cache_key fcls:{query_text} cached cache.get(cache_key) if cached is not None: return JsonResponse({code: 0, data: cached}) item, match_type match_waste_item(query_text) if item is None: return JsonResponse({code: 404, msg: 暂未收录该物品请换个说法}, status404) result build_result(item, match_type) cache.set(cache_key, result, timeout60 * 60 * 24) return JsonResponse({code: 0, data: result})参数说明缓存 key 用清洗后的文本timeout 设 24 小时因为别名表不会一天改几遍如果管理员更新了别名缓存可能还是旧的所以后台保存 WasteItem 时要顺手 cache.clear() 或删除相关 key这比纠结缓存穿透更实在。Django 自带缓存后端默认是本地内存单机部署够用演示视频里对着同一个问题连问三遍第二遍开始就是秒回效果非常直观而且不用写一行缓存框架代码。4. 智能垃圾分类系统上线避坑5 条最容易翻车的经验4.1 录音是 webm后端却是按 mp3 收的格式不一致导致识别结果为空现象前端 MediaRecorder 录制的是 audio/webm 格式后端识别接口只支持 wav/mp3调用识别服务返回“音频解码失败”或直接超时。原因是对 webm 容器的兼容性评估不足识别厂商普遍优先支持 mp3/wav。解决前端在录音时优先用 audio/mp4 或 audio/wav 编码如果 MediaRecorder 不支持后端在拿到 webm 后用 ffmpeg 转成 wav 再调用识别服务。我一般在 settings 里加一个 AUDIO_ALLOWED_TYPES 白名单并在启动时检查 ffmpeg 是否可用。这一条在项目演示时最容易翻车因为 windows 机器上大概率没装 ffmpeg等到现场演示才暴露就晚了。4.2 同音词把“湿纸巾”识别成“只纸巾”音近字命中失败的兜底策略现象用户说“湿纸巾”ASR 返回“只纸巾”别名表里只有“湿纸巾”匹配不到。原因语音识别对同音词和方言读音的错误是常态尤其纸巾/纸巾、大棒骨/大磅骨这类字。解决第一在语音识别配置里开启热词表把垃圾物品常用词动态注入识别服务这是最有效的办法第二在 match_waste_item 里增加编辑距离兜底对清洗后文本和别名做相似度判断相似度大于 0.85 时也视为命中。def similarity(a, b): if not a or not b: return 0 a, b a.lower(), b.lower() if a b: return 1.0 longer, shorter (a, b) if len(a) len(b) else (b, a) dp list(range(len(shorter) 1)) for i, ca in enumerate(longer, 1): prev dp[0] dp[0] i for j, cb in enumerate(shorter, 1): tmp dp[j] dp[j] min(dp[j] 1, dp[j - 1] 1, prev (ca ! cb)) prev tmp return 1 - dp[-1] / max(len(a), len(b))这段编辑距离算法是抄作业级别的代码O(m*n) 复杂度。注意只在精确匹配和别名包含匹配都失败时才调用因为每调一次就要全表扫一遍扛不住高并发。使用时会发现相似度阈值 0.85 这个值很玄学调低了误伤“大骨头”和“大棒骨”调高了对“只纸巾”无能为力实际是配着 ASR 热词表一起用的把识别准确率拉上去之后兜底逻辑碰到的次数就很少了。4.3 识别结果不落库、不打印调试成了黑匣子现象上线后用户反馈“我说了卫生纸它答可回收错了”你查数据库发现只有一条成功记录没有原始音频和识别文本。原因视图里只保存了最终分类结果没保存 ASR 原始返回。解决GarbageRecord 表除了 query_text再加一个 raw_response 字段或者直接记日志。每次识别请求都把原始文本、清洗后文本、命中物品、命中方式、耗时写入记录表。后面分析误识别率拿这张表按 source 分组统计就行别靠用户口头复述。4.4 LIKE 查询遇到“%”和“_”ORM 模糊匹配的通配符转义现象用户输入“100%纯棉毛巾”ORM 的 name__contains 把它变成 LIKE %100%纯棉毛巾%结果把所有含“100”的垃圾都搜出来了。原因LIKE 中 % 和 _ 是通配符用户语音识别文本里可能带这些字符。解决用 Django 的 escape 参数contains 查询时传 escape\并手动替换查询文本里的特殊字符。这属于很隐蔽的坑平时查“苹果”永远碰不到一旦测试语料里有百分号就全乱了。4.5 一个长音频拖死所有 workerGunicorn 超时与同步阻塞现象部署时用 gunicorn 起了 4 个 worker某用户传了一段 30 秒的录音识别服务响应慢结果 4 个 worker 全被堵住其他用户请求全部排队超时。原因同步视图里调用外部识别接口是阻塞式的worker 一旦被占用就干等。解决第一gunicorn 配置里把 timeout 调低比如 15 秒请求超时就返回错误而不是拖死 worker第二把音频识别改成异步任务用 celery 或 django-q 处理前端轮询结果第三没有异步条件时至少对上传视图加并发限制比如单 IP 同时只能有一个识别任务。演示项目用同步方案能跑但对外发布前这一步必须做。5. 让它更聪明关键词扩展、置信度阈值和 Django 查询优化三个进阶点到了这个阶段系统已经能跑通“说话 - 识别 - 匹配 - 返回分类”的完整链路但离“智能”还差一口气。第一个进阶点是关键词扩展。垃圾分类物品别名是个无限集你永远猜不全“泡沫箱、泡沫盒、白色泡沫”是同一种东西。我一般写一个管理命令定期从识别失败的记录里捞高频文本人工确认后批量导入别名表别让 GarbageRecord 里那些未命中的文本白白躺在数据库里。第二个进阶点是置信度阈值。ASR 接口返回 top_k 个候选时每个候选都有置信度分值。用户一声“卫生纸”可能识别成“卫生纸”和“卫生只”第一条置信度高第二条置信度低。把置信度写进匹配逻辑高置信度的候选先查一遍查到就返回低置信度的候选查到了也要带上“您说的是 X 吗”的确认文案。这个交互细节在演示时很加分证明你不是只接了接口而是理解了识别结果的不确定性。第三个进阶点是查询优化。当别名表超过一万条时match_waste_item 里的全表遍历和分词兜底会明显变慢。建议在 name 和 aliases 字段上建索引把 aliases 从 JSONField 拆成单独的 Alias 表匹配时用 Django Q 对象一次性查所有别名而不是遍历 Python 列表。这一步会让 SQL 从几十条降到一条。from django.db.models import Q aliases_q Q() for alias in [大骨头, 筒骨, 扇骨]: aliases_q | Q(aliases__icontainsalias) items WasteItem.objects.filter(aliases_q).select_related(category)这段 Q 对象在 PostgreSQL 下会变成 text 字段的 LIKE 查询配合 GIN 索引性能尚可但也提醒你JSONField 在 MySQL 里做模糊查询性能并不理想数据量大了之后拆表是正路。我自己的教训是第一版把“匹配成功”看得太重忽略了“匹配失败也是一笔财富”结果演示很顺畅一到真实用户就露怯。后来把每次 ASR 原始返回原样存档再回放清洗脚本迭代速度才上来。这个习惯一直留到现在希望帮到你。本文还有配套的精品资源点击获取