当前位置: 首页 > news >正文

微信小程序-智慧社区项目开发完整技术文档(中)

微信小程序-智慧社区项目开发完整技术文档(中)

学习项目(仅作参考)

六、人脸识别模块深度开发

6.1 前端人脸识别页面实现

WXML结构
<!-- pages/face/face.wxml -->
<view class="face-container"><!-- 相机预览区域 --><view class="camera-section" wx:if="{{!isPreview}}"><camera device-position="{{devicePosition}}" flash="{{flash}}"class="camera"binderror="cameraError"></camera><!-- 人脸检测框(可选) --><view class="face-overlay" wx:if="{{showFaceFrame}}"><view class="face-frame" style="top: {{faceFrame.top}}%; left: {{faceFrame.left}}%; width: {{faceFrame.width}}%; height: {{faceFrame.height}}%;"></view></view></view><!-- 图片预览区域 --><view class="preview-section" wx:if="{{isPreview}}"><image class="preview-image" src="{{previewImage}}" mode="aspectFit"></image><view class="preview-info" wx:if="{{detectResult}}"><text class="info-text">{{detectResult.message}}</text><text class="info-score" wx:if="{{detectResult.score}}">相似度: {{detectResult.score}}%</text></view></view><!-- 控制面板 --><view class="control-panel"><view class="control-row"><button class="control-btn" bindtap="switchCamera"><text class="iconfont icon-switch-camera"></text><text class="btn-text">翻转</text></button><view class="capture-btn" bindtap="takePhoto" wx:if="{{!isPreview}}"><view class="capture-inner"></view></view><view class="capture-btn disabled" wx:else><view class="capture-inner"></view></view><button class="control-btn" bindtap="toggleFlash"><text class="iconfont icon-flash-{{flashIcon}}"></text><text class="btn-text">{{flashText}}</text></button></view><!-- 预览模式下的操作按钮 --><view class="preview-actions" wx:if="{{isPreview}}"><button class="action-btn retake" bindtap="retakePhoto">重拍</button><button class="action-btn confirm" bindtap="confirmDetection">开始识别</button></view></view><!-- 识别记录 --><view class="records-section" wx:if="{{detectRecords.length > 0}}"><view class="section-header"><text class="section-title">识别记录</text><text class="section-clear" bindtap="clearRecords">清空</text></view><scroll-view class="records-list" scroll-y><view class="record-item {{item.success ? 'success' : 'error'}}" wx:for="{{detectRecords}}" wx:key="id"bindtap="viewRecordDetail"data-id="{{item.id}}"><view class="record-icon"><text class="iconfont {{item.success ? 'icon-success' : 'icon-error'}}"></text></view><view class="record-content"><text class="record-text">{{item.message}}</text><text class="record-time">{{item.time}}</text><text class="record-user" wx:if="{{item.user_name}}">{{item.user_name}}</text></view><view class="record-score" wx:if="{{item.score}}"><text>{{item.score}}%</text></view></view></scroll-view></view><!-- 统计信息 --><view class="stats-section"><view class="stats-grid"><view class="stat-item"><text class="stat-value">{{stats.todayCount}}</text><text class="stat-label">今日识别</text></view><view class="stat-item"><text class="stat-value">{{stats.successCount}}</text><text class="stat-label">成功识别</text></view><view class="stat-item"><text class="stat-value">{{stats.successRate}}%</text><text class="stat-label">识别率</text></view></view></view><!-- 加载状态 --><van-overlay show="{{isLoading}}"><view class="loading-wrapper"><van-loading size="24px" vertical>{{loadingText}}</van-loading></view></van-overlay><!-- 识别结果弹窗 --><van-popup show="{{showResultPopup}}" position="top"overlay="{{false}}"custom-style="background: transparent;"><view class="result-popup {{detectResult.success ? 'success' : 'error'}}"><text class="popup-icon iconfont {{detectResult.success ? 'icon-success' : 'icon-error'}}"></text><text class="popup-text">{{detectResult.message}}</text><text class="popup-score" wx:if="{{detectResult.score}}">相似度: {{detectResult.score}}%</text></view></van-popup>
</view>
WXSS样式设计
/* pages/face/face.wxss */
.face-container {height: 100vh;display: flex;flex-direction: column;background: #000000;
}.camera-section {flex: 1;position: relative;overflow: hidden;
}.camera {width: 100%;height: 100%;
}.face-overlay {position: absolute;top: 0;left: 0;right: 0;bottom: 0;pointer-events: none;
}.face-frame {position: absolute;border: 4rpx solid #00ff00;border-radius: 12rpx;box-shadow: 0 0 20rpx rgba(0, 255, 0, 0.5);animation: pulse 2s infinite;
}@keyframes pulse {0% { opacity: 0.7; }50% { opacity: 1; }100% { opacity: 0.7; }
}.preview-section {flex: 1;position: relative;background: #000000;
}.preview-image {width: 100%;height: 100%;
}.preview-info {position: absolute;top: 40rpx;left: 0;right: 0;text-align: center;background: rgba(0, 0, 0, 0.7);padding: 20rpx;
}.info-text {display: block;color: #ffffff;font-size: 32rpx;margin-bottom: 10rpx;
}.info-score {display: block;color: #00ff00;font-size: 28rpx;
}.control-panel {background: rgba(0, 0, 0, 0.8);padding: 30rpx 40rpx;
}.control-row {display: flex;justify-content: space-between;align-items: center;margin-bottom: 20rpx;
}.control-btn {background: transparent;border: none;color: #ffffff;display: flex;flex-direction: column;align-items: center;gap: 10rpx;font-size: 24rpx;padding: 20rpx;
}.control-btn .iconfont {font-size: 48rpx;
}.capture-btn {width: 120rpx;height: 120rpx;border-radius: 50%;background: #ffffff;display: flex;align-items: center;justify-content: center;border: 4rpx solid rgba(255, 255, 255, 0.5);
}.capture-btn.disabled {opacity: 0.5;
}.capture-inner {width: 100rpx;height: 100rpx;border-radius: 50%;background: #ffffff;border: 2rpx solid #000000;
}.preview-actions {display: flex;justify-content: space-around;gap: 40rpx;
}.action-btn {flex: 1;padding: 24rpx;border-radius: 40rpx;font-size: 28rpx;border: none;
}.action-btn.retake {background: transparent;border: 2rpx solid #ffffff;color: #ffffff;
}.action-btn.confirm {background: #007AFF;color: #ffffff;
}.records-section {background: #ffffff;border-radius: 20rpx 20rpx 0 0;margin-top: -20rpx;position: relative;z-index: 10;
}.section-header {display: flex;justify-content: space-between;align-items: center;padding: 30rpx;border-bottom: 1rpx solid #f0f0f0;
}.section-title {font-size: 32rpx;font-weight: bold;color: #333333;
}.section-clear {font-size: 26rpx;color: #999999;
}.records-list {max-height: 400rpx;padding: 0 30rpx;
}.record-item {display: flex;align-items: center;padding: 30rpx 0;border-bottom: 1rpx solid #f8f8f8;
}.record-item.success {border-left: 6rpx solid #34C759;
}.record-item.error {border-left: 6rpx solid #FF3B30;
}.record-icon {width: 60rpx;height: 60rpx;border-radius: 50%;display: flex;align-items: center;justify-content: center;margin-right: 20rpx;flex-shrink: 0;
}.record-item.success .record-icon {background: #34C759;
}.record-item.error .record-icon {background: #FF3B30;
}.record-icon .iconfont {font-size: 32rpx;color: #ffffff;
}.record-content {flex: 1;min-width: 0;
}.record-text {display: block;font-size: 28rpx;color: #333333;margin-bottom: 8rpx;
}.record-time {display: block;font-size: 24rpx;color: #999999;margin-bottom: 4rpx;
}.record-user {display: block;font-size: 24rpx;color: #666666;
}.record-score {font-size: 28rpx;font-weight: bold;color: #34C759;margin-left: 20rpx;
}.stats-section {background: #ffffff;padding: 30rpx;border-top: 1rpx solid #f0f0f0;
}.stats-grid {display: flex;justify-content: space-around;
}.stat-item {display: flex;flex-direction: column;align-items: center;
}.stat-value {font-size: 36rpx;font-weight: bold;color: #007AFF;margin-bottom: 8rpx;
}.stat-label {font-size: 24rpx;color: #999999;
}.loading-wrapper {display: flex;align-items: center;justify-content: center;height: 100%;
}.result-popup {background: rgba(0, 0, 0, 0.8);margin: 40rpx;padding: 40rpx;border-radius: 20rpx;text-align: center;
}.result-popup.success {border: 2rpx solid #34C759;
}.result-popup.error {border: 2rpx solid #FF3B30;
}.popup-icon {display: block;font-size: 80rpx;margin-bottom: 20rpx;
}.result-popup.success .popup-icon {color: #34C759;
}.result-popup.error .popup-icon {color: #FF3B30;
}.popup-text {display: block;font-size: 32rpx;color: #ffffff;margin-bottom: 10rpx;
}.popup-score {display: block;font-size: 28rpx;color: #00ff00;
}
JavaScript逻辑实现
// pages/face/face.js
const { request, buildUrl } = require('../../utils/request')
const { showLoading, hideLoading, showError, formatTime } = require('../../utils/util')Page({data: {// 相机设置devicePosition: 'front',flash: 'off',flashIcon: 'off',flashText: '闪光灯',// 预览状态isPreview: false,previewImage: '',// 人脸检测showFaceFrame: false,faceFrame: {top: 30,left: 25,width: 50,height: 40},// 识别结果detectResult: {},showResultPopup: false,// 识别记录detectRecords: [],// 统计信息stats: {todayCount: 0,successCount: 0,successRate: 0},// 加载状态isLoading: false,loadingText: '识别中...',cameraContext: null},onLoad() {this.initCamera()this.loadStatistics()this.loadRecords()},onUnload() {if (this.data.cameraContext) {this.data.cameraContext = null}},onShow() {// 重新加载统计信息this.loadStatistics()},initCamera() {const cameraContext = wx.createCameraContext()this.setData({ cameraContext })// 监听相机帧(用于实时人脸检测)if (this.data.showFaceFrame) {this.startFaceDetection()}},startFaceDetection() {// 实时人脸检测(简化实现)const listener = this.data.cameraContext.onCameraFrame((frame) => {// 这里可以处理实时帧数据进行人脸检测// 实际项目中可以调用微信的AI能力或后端API})listener.start()},async loadStatistics() {try {const res = await request.get(buildUrl('face.statistics'))if (res.code === 200) {this.setData({ stats: res.data })}} catch (error) {console.error('加载统计信息失败:', error)}},async loadRecords() {try {const res = await request.get(buildUrl('face.records'), {page: 1,page_size: 10})if (res.code === 200) {this.setData({ detectRecords: res.data.list || [] })}} catch (error) {console.error('加载识别记录失败:', error)}},switchCamera() {this.setData({devicePosition: this.data.devicePosition === 'front' ? 'back' : 'front'})},toggleFlash() {const flashModes = ['off', 'on', 'auto']const flashIcons = ['off', 'on', 'auto']const flashTexts = ['闪光灯', '闪光灯开', '自动闪光']const currentIndex = flashModes.indexOf(this.data.flash)const nextIndex = (currentIndex + 1) % flashModes.lengththis.setData({flash: flashModes[nextIndex],flashIcon: flashIcons[nextIndex],flashText: flashTexts[nextIndex]})},takePhoto() {if (this.data.isLoading) returnthis.setData({ isLoading: true, loadingText: '拍照中...' })this.data.cameraContext.takePhoto({quality: 'high',success: (res) => {this.setData({previewImage: res.tempImagePath,isPreview: true,isLoading: false})},fail: (error) => {console.error('拍照失败:', error)showError('拍照失败')this.setData({ isLoading: false })}})},retakePhoto() {this.setData({isPreview: false,previewImage: '',detectResult: {}})},async confirmDetection() {if (!this.data.previewImage) {showError('请先拍摄照片')return}this.setData({ isLoading: true, loadingText: '识别中...' })try {const uploadRes = await request.upload(buildUrl('face.detect'),this.data.previewImage,{type: 'face_detect'})let detectResult = {}if (uploadRes.code === 100) {// 识别成功detectResult = {success: true,message: `识别成功: ${uploadRes.data.name}`,score: uploadRes.data.score,user_name: uploadRes.data.name,area: uploadRes.data.area}// 显示成功弹窗this.showResultPopup(detectResult)} else if (uploadRes.code === 102) {// 识别失败detectResult = {success: false,message: '识别失败: 非本小区人员'}// 显示失败弹窗this.showResultPopup(detectResult)} else {throw new Error(uploadRes.message)}// 添加识别记录this.addDetectRecord(detectResult)// 重新加载统计信息this.loadStatistics()} catch (error) {console.error('人脸识别失败:', error)showError('识别失败: ' + error.message)const detectResult = {success: false,message: '识别失败: 系统错误'}this.addDetectRecord(detectResult)} finally {this.setData({ isLoading: false })}},showResultPopup(result) {this.setData({detectResult: result,showResultPopup: true})// 3秒后自动关闭弹窗setTimeout(() => {this.setData({ showResultPopup: false })// 自动返回拍摄模式setTimeout(() => {this.retakePhoto()}, 500)}, 3000)},addDetectRecord(result) {const record = {id: Date.now(),time: formatTime(new Date(), 'HH:mm:ss'),success: result.success,message: result.message,score: result.score,user_name: result.user_name}const newRecords = [record, ...this.data.detectRecords]// 限制记录数量if (newRecords.length > 20) {newRecords.splice(20)}this.setData({ detectRecords: newRecords })// 保存到本地存储this.saveRecordsToStorage(newRecords)},saveRecordsToStorage(records) {try {wx.setStorageSync('face_detect_records', records.slice(0, 50)) // 最多保存50条} catch (error) {console.error('保存识别记录失败:', error)}},loadRecordsFromStorage() {try {const records = wx.getStorageSync('face_detect_records') || []this.setData({ detectRecords: records })} catch (error) {console.error('加载识别记录失败:', error)}},clearRecords() {wx.showModal({title: '确认清空',content: '确定要清空所有识别记录吗?',confirmColor: '#ff4d4f',success: (res) => {if (res.confirm) {this.setData({ detectRecords: [] })wx.removeStorageSync('face_detect_records')showError('记录已清空')}}})},viewRecordDetail(e) {const { id } = e.currentTarget.datasetconst record = this.data.detectRecords.find(r => r.id === id)if (record) {wx.showModal({title: '识别详情',content: `结果: ${record.message}\n时间: ${record.time}\n${record.score ? `相似度: ${record.score}%` : ''}`,showCancel: false})}},cameraError(e) {console.error('相机错误:', e.detail)showError('相机启动失败,请检查权限设置')// 引导用户开启相机权限wx.showModal({title: '相机权限未开启',content: '请在小程序设置中开启相机权限',confirmText: '去设置',success: (res) => {if (res.confirm) {wx.openSetting()}}})}
})

6.2 后端人脸识别API深度实现

数据模型设计
# smart/models.py
class FaceDetectionRecord(models.Model):"""人脸识别记录模型"""DETECT_RESULT_CHOICES = (('success', '识别成功'),('fail', '识别失败'),('error', '识别错误'),)collection = models.ForeignKey(Collection, on_delete=models.CASCADE, null=True, blank=True,verbose_name="关联采集记录")image = models.ImageField(upload_to='face_detect/%Y/%m/%d/', verbose_name="识别图片")score = models.FloatField(null=True, blank=True, verbose_name="相似度得分")result = models.CharField(max_length=20, choices=DETECT_RESULT_CHOICES, verbose_name="识别结果")message = models.CharField(max_length=200, verbose_name="结果描述")device_info = models.CharField(max_length=100, blank=True, verbose_name="设备信息")ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name="IP地址")location = models.CharField(max_length=100, blank=True, verbose_name="识别位置")create_time = models.DateTimeField(auto_now_add=True, verbose_name="识别时间")class Meta:db_table = 'smart_face_detection'verbose_name = '人脸识别记录'verbose_name_plural = verbose_nameordering = ['-create_time']indexes = [models.Index(fields=['result', 'create_time']),models.Index(fields=['collection', 'create_time']),]def __str__(self):return f"{self.get_result_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M')}"def get_absolute_image_url(self, request=None):"""获取完整的图片URL"""if self.image and hasattr(self.image, 'url'):if request:return request.build_absolute_uri(self.image.url)return self.image.urlreturn Noneclass FaceDetectionStatistics(models.Model):"""人脸识别统计模型"""date = models.DateField(unique=True, verbose_name="统计日期")total_count = models.IntegerField(default=0, verbose_name="总识别次数")success_count = models.IntegerField(default=0, verbose_name="成功次数")fail_count = models.IntegerField(default=0, verbose_name="失败次数")success_rate = models.FloatField(default=0, verbose_name="成功率")avg_score = models.FloatField(default=0, verbose_name="平均相似度")create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")class Meta:db_table = 'smart_face_statistics'verbose_name = '人脸识别统计'verbose_name_plural = verbose_nameordering = ['-date']def __str__(self):return f"{self.date} - 成功率: {self.success_rate}%"def calculate_success_rate(self):"""计算成功率"""if self.total_count > 0:return round((self.success_count / self.total_count) * 100, 2)return 0def save(self, *args, **kwargs):self.success_rate = self.calculate_success_rate()super().save(*args, **kwargs)
百度AI服务增强
# smart/libs/baidu_ai.py
import base64
import logging
from aip import AipFace
from django.conf import settings
from django.utils import timezonelogger = logging.getLogger(__name__)class BaiduAIService:"""百度AI服务增强版"""def __init__(self):self.face_client = AipFace(settings.BAIDU_AI_APP_ID,settings.BAIDU_AI_API_KEY,settings.BAIDU_AI_SECRET_KEY)self.group_id = getattr(settings, 'BAIDU_AI_GROUP_ID', 'smart_community')self.options = {'max_face_num': 1,'face_type': 'LIVE','liveness_control': 'LOW',  # 活体控制级别}def register_face(self, image_path, user_id, user_info=''):"""人脸注册增强版"""try:# 读取图片并编码image_data = self._read_and_encode_image(image_path)if not image_data:return self._error_result(-1, "无法读取图片文件")# 先检测人脸detect_result = self.face_client.detect(image_data, 'BASE64', self.options)if detect_result.get('error_code'):return self._error_result(detect_result['error_code'],f"人脸检测失败: {detect_result.get('error_msg', '未知错误')}")# 检查人脸数量face_num = detect_result['result']['face_num']if face_num == 0:return self._error_result(1001, "未检测到人脸")if face_num > 1:return self._error_result(1002, "检测到多张人脸,请上传单人照片")# 注册人脸result = self.face_client.addUser(image_data,'BASE64',self.group_id,user_id,user_info=user_info)logger.info(f"人脸注册请求: user_id={user_id}, result={result}")if result.get('error_code') == 0:return {'success': True,'face_token': result['result']['face_token'],'log_id': result['result']['log_id'],'location': detect_result['result']['face_list'][0]['location']}else:return self._error_result(result['error_code'],result.get('error_msg', '注册失败'))except Exception as e:logger.error(f"人脸注册异常: {str(e)}")return self._error_result(-1, f"系统异常: {str(e)}")def search_face(self, image_path, quality_control='NORMAL', liveness_control='NORMAL'):"""人脸搜索增强版"""try:# 读取图片并编码image_data = self._read_and_encode_image(image_path)if not image_data:return self._error_result(-1, "无法读取图片文件")# 搜索参数search_options = {'max_face_num': 1,'match_threshold': 80,  # 匹配阈值'quality_control': quality_control,'liveness_control': liveness_control,'max_user_num': 1,  # 返回最相似的一个用户}result = self.face_client.search(image_data,'BASE64',self.group_id,search_options)logger.info(f"人脸搜索结果: {result}")if result.get('error_code') == 0:user_list = result['result']['user_list']if user_list and user_list[0]['score'] >= 80:  # 相似度阈值best_match = user_list[0]return {'success': True,'user_id': best_match['user_id'],'score': round(best_match['score'], 2),'user_info': best_match['user_info'],'face_token': best_match.get('face_token', ''),'log_id': result['result']['log_id']}else:return self._error_result(1003, "未找到匹配的用户")else:return self._error_result(result['error_code'],result.get('error_msg', '搜索失败'))except Exception as e:logger.error(f"人脸搜索异常: {str(e)}")return self._error_result(-1, f"系统异常: {str(e)}")def multi_search_face(self, image_path, max_user_num=5):"""多人脸搜索"""try:image_data = self._read_and_encode_image(image_path)if not image_data:return self._error_result(-1, "无法读取图片文件")# 先检测人脸detect_result = self.face_client.detect(image_data, 'BASE64', {'max_face_num': 10,'face_type': 'LIVE'})if detect_result.get('error_code'):return self._error_result(detect_result['error_code'],f"人脸检测失败: {detect_result.get('error_msg', '未知错误')}")face_num = detect_result['result']['face_num']if face_num == 0:return self._error_result(1001, "未检测到人脸")# 对每张人脸进行搜索results = []for face in detect_result['result']['face_list']:# 提取单个人脸区域进行搜索face_image = self._extract_face_region(image_path, face['location'])if face_image:search_result = self.search_face(face_image)if search_result['success']:results.append({'location': face['location'],'user_info': search_result['user_info'],'score': search_result['score']})return {'success': True,'face_count': face_num,'results': results}except Exception as e:logger.error(f"多人脸搜索异常: {str(e)}")return self._error_result(-1, f"系统异常: {str(e)}")def delete_face(self, user_id, face_token):"""删除人脸"""try:result = self.face_client.faceDelete(user_id,self.group_id,face_token)logger.info(f"人脸删除结果: {result}")if result.get('error_code') == 0:return {'success': True}else:return self._error_result(result['error_code'],result.get('error_msg', '删除失败'))except Exception as e:logger.error(f"人脸删除异常: {str(e)}")return self._error_result(-1, f"系统异常: {str(e)}")def get_group_users(self):"""获取组内用户列表"""try:result = self.face_client.getGroupUsers(self.group_id)if result.get('error_code') == 0:return {'success': True,'user_count': result['result']['user_num'],'user_list': result['result']['user_id_list']}else:return self._error_result(result['error_code'],result.get('error_msg', '获取用户列表失败'))except Exception as e:logger.error(f"获取用户列表异常: {str(e)}")return self._error_result(-1, f"系统异常: {str(e)}")def _read_and_encode_image(self, image_path):"""读取图片并编码为base64"""try:with open(image_path, 'rb') as f:return base64.b64encode(f.read()).decode()except Exception as e:logger.error(f"读取图片失败: {str(e)}")return Nonedef _extract_face_region(self, image_path, location):"""提取人脸区域(简化实现)"""# 在实际项目中,这里应该使用OpenCV等库来提取人脸区域# 这里返回原图作为简化实现return image_pathdef _error_result(self, error_code, error_msg):"""生成错误结果"""return {'success': False,'error_code': error_code,'error_msg': error_msg}# 创建全局实例
baidu_ai_service = BaiduAIService()
视图集实现
# smart/views.py
import os
import tempfile
import json
from django.utils import timezone
from django.db.models import Count, Q, Avg
from django.db import transaction
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.viewsets import ModelViewSet
from .models import FaceDetectionRecord, FaceDetectionStatistics, Collection
from .serializers import FaceDetectionRecordSerializer
from .libs.baidu_ai import baidu_ai_serviceclass FaceDetectionAPIView(APIView):"""人脸检测API"""def post(self, request):try:if 'image' not in request.FILES:return Response({'code': 400,'message': '请上传图片文件','data': None}, status=status.HTTP_400_BAD_REQUEST)image_file = request.FILES['image']# 保存临时文件with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_file:for chunk in image_file.chunks():tmp_file.write(chunk)tmp_path = tmp_file.nametry:# 调用百度AI人脸搜索result = baidu_ai_service.search_face(tmp_path)# 保存识别记录detection_record = self._save_detection_record(request, image_file, result)if result['success']:# 根据user_id查找用户user_id = result['user_id']try:collection = Collection.objects.get(name_pinyin=user_id)# 更新识别记录关联detection_record.collection = collectiondetection_record.save()return Response({'code': 100,'message': '识别成功','data': {'name': collection.name,'score': result['score'],'area': collection.area.name,'avatar_url': collection.get_absolute_avatar_url(request),'record_id': detection_record.id}})except Collection.DoesNotExist:return Response({'code': 102,'message': '识别失败:未找到匹配用户','data': {'record_id': detection_record.id}})else:return Response({'code': result['error_code'],'message': f"识别失败:{result['error_msg']}",'data': {'record_id': detection_record.id}})finally:# 删除临时文件if os.path.exists(tmp_path):os.unlink(tmp_path)except Exception as e:logger.error(f"人脸检测异常: {str(e)}")return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)def _save_detection_record(self, request, image_file, result):"""保存识别记录"""# 获取客户端信息device_info = request.META.get('HTTP_USER_AGENT', '')ip_address = self._get_client_ip(request)# 确定识别结果if result['success']:result_type = 'success'message = f"识别成功: {result.get('user_info', '未知用户')}"score = result.get('score')else:result_type = 'fail' if result['error_code'] == 1003 else 'error'message = f"识别失败: {result['error_msg']}"score = None# 保存记录record = FaceDetectionRecord(image=image_file,score=score,result=result_type,message=message,device_info=device_info[:100],ip_address=ip_address,location=self._get_location_from_ip(ip_address))record.save()# 更新统计信息self._update_statistics(result_type, score)return recorddef _get_client_ip(self, request):"""获取客户端IP"""x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')if x_forwarded_for:ip = x_forwarded_for.split(',')[0]else:ip = request.META.get('REMOTE_ADDR')return ipdef _get_location_from_ip(self, ip):"""根据IP获取地理位置(简化实现)"""# 实际项目中可以调用IP地理位置服务return "未知位置"def _update_statistics(self, result_type, score):"""更新统计信息"""today = timezone.now().date()with transaction.atomic():# 获取或创建今日统计stats, created = FaceDetectionStatistics.objects.get_or_create(date=today,defaults={'total_count': 0,'success_count': 0,'fail_count': 0,'avg_score': 0})# 更新统计stats.total_count += 1if result_type == 'success':stats.success_count += 1# 更新平均分if score:total_score = stats.avg_score * (stats.success_count - 1) + scorestats.avg_score = total_score / stats.success_countelse:stats.fail_count += 1stats.save()class FaceDetectionRecordViewSet(ModelViewSet):"""人脸识别记录视图集"""queryset = FaceDetectionRecord.objects.all()serializer_class = FaceDetectionRecordSerializerdef get_queryset(self):queryset = super().get_queryset()# 时间过滤start_date = self.request.GET.get('start_date')end_date = self.request.GET.get('end_date')if start_date and end_date:queryset = queryset.filter(create_time__date__range=[start_date, end_date])# 结果过滤result = self.request.GET.get('result')if result:queryset = queryset.filter(result=result)return queryset.select_related('collection', 'collection__area').order_by('-create_time')@action(detail=False, methods=['get'])def statistics(self, request):"""获取识别统计"""today = timezone.now().date()try:# 获取今日统计today_stats = FaceDetectionStatistics.objects.filter(date=today).first()if not today_stats:# 如果没有今日统计,创建默认值today_stats = FaceDetectionStatistics(date=today,total_count=0,success_count=0,fail_count=0,success_rate=0,avg_score=0)# 获取历史统计total_stats = FaceDetectionRecord.objects.aggregate(total_count=Count('id'),success_count=Count('id', filter=Q(result='success')),avg_score=Avg('score', filter=Q(result='success')))data = {'todayCount': today_stats.total_count,'successCount': today_stats.success_count,'successRate': today_stats.success_rate,'avgScore': round(today_stats.avg_score, 2) if today_stats.avg_score else 0,'totalCount': total_stats['total_count'] or 0,'totalSuccessCount': total_stats['success_count'] or 0,'totalAvgScore': round(total_stats['avg_score'] or 0, 2)}return Response({'code': 200,'message': 'success','data': data})except Exception as e:logger.error(f"获取识别统计异常: {str(e)}")return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)@action(detail=False, methods=['get'])def daily_stats(self, request):"""获取每日统计"""try:days = int(request.GET.get('days', 7))end_date = timezone.now().date()start_date = end_date - timezone.timedelta(days=days-1)stats = FaceDetectionStatistics.objects.filter(date__range=[start_date, end_date]).order_by('date')# 构建返回数据dates = []total_counts = []success_counts = []success_rates = []current_date = start_datewhile current_date <= end_date:stat = stats.filter(date=current_date).first()dates.append(current_date.strftime('%m-%d'))total_counts.append(stat.total_count if stat else 0)success_counts.append(stat.success_count if stat else 0)success_rates.append(stat.success_rate if stat else 0)current_date += timezone.timedelta(days=1)data = {'dates': dates,'total_counts': total_counts,'success_counts': success_counts,'success_rates': success_rates}return Response({'code': 200,'message': 'success','data': data})except Exception as e:logger.error(f"获取每日统计异常: {str(e)}")return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)class FaceRegisterAPIView(APIView):"""人脸注册API"""def post(self, request, collection_id):try:collection = Collection.objects.get(id=collection_id)if not collection.avatar:return Response({'code': 400,'message': '该采集记录没有头像图片','data': None}, status=status.HTTP_400_BAD_REQUEST)# 检查是否已注册if collection.face_token:return Response({'code': 400,'message': '该用户已注册人脸信息','data': None}, status=status.HTTP_400_BAD_REQUEST)# 获取头像图片路径avatar_path = collection.avatar.path# 调用百度AI人脸注册result = baidu_ai_service.register_face(avatar_path,collection.name_pinyin,collection.name)if result['success']:# 更新face_tokencollection.face_token = result['face_token']collection.save()return Response({'code': 200,'message': '人脸注册成功','data': {'face_token': result['face_token'],'user_id': collection.name_pinyin}})else:return Response({'code': result['error_code'],'message': f"人脸注册失败:{result['error_msg']}",'data': None})except Collection.DoesNotExist:return Response({'code': 404,'message': '采集记录不存在','data': None}, status=status.HTTP_404_NOT_FOUND)except Exception as e:logger.error(f"人脸注册异常: {str(e)}")return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)def delete(self, request, collection_id):"""删除人脸注册"""try:collection = Collection.objects.get(id=collection_id)if not collection.face_token:return Response({'code': 400,'message': '该用户未注册人脸信息','data': None}, status=status.HTTP_400_BAD_REQUEST)# 调用百度AI删除人脸result = baidu_ai_service.delete_face(collection.name_pinyin,collection.face_token)if result['success']:# 清空face_tokencollection.face_token = ''collection.save()return Response({'code': 200,'message': '人脸删除成功','data': None})else:return Response({'code': result['error_code'],'message': f"人脸删除失败:{result['error_msg']}",'data': None})except Collection.DoesNotExist:return Response({'code': 404,'message': '采集记录不存在','data': None}, status=status.HTTP_404_NOT_FOUND)except Exception as e:logger.error(f"人脸删除异常: {str(e)}")return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
URL路由配置
# smart/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (FaceDetectionAPIView, FaceDetectionRecordViewSet,FaceRegisterAPIView
)router = DefaultRouter()
router.register(r'face/records', FaceDetectionRecordViewSet)urlpatterns = [path('face/detect/', FaceDetectionAPIView.as_view(), name='face-detect'),path('face/register/<int:collection_id>/', FaceRegisterAPIView.as_view(), name='face-register'),path('', include(router.urls)),
]

七、语音识别模块深度开发

7.1 前端语音识别页面实现

WXML结构
<!-- pages/voice/voice.wxml -->
<view class="voice-container"><!-- 语音识别结果 --><view class="result-section"><view class="section-header"><text class="section-title">识别结果</text><view class="result-actions"><button class="action-btn clear" bindtap="clearResult">清空</button><button class="action-btn copy" bindtap="copyResult">复制</button></view></view><scroll-view class="result-content" scroll-y><view class="result-text">{{recognitionResult}}</view><!-- 空状态 --><view class="empty-result" wx:if="{{!recognitionResult}}"><image src="/static/images/voice-empty.png" class="empty-image" /><text class="empty-text">点击下方按钮开始语音识别</text></view></scroll-view><!-- 识别统计 --><view class="result-stats" wx:if="{{recognitionResult}}"><view class="stat-item"><text class="stat-label">字数</text><text class="stat-value">{{textStats.wordCount}}</text></view><view class="stat-item"><text class="stat-label">时长</text><text class="stat-value">{{textStats.duration}}s</text></view><view class="stat-item"><text class="stat-label">置信度</text><text class="stat-value">{{textStats.confidence}}%</text></view></view></view><!-- 录音控制 --><view class="control-section"><view class="voice-visualizer" wx:if="{{isRecording}}"><view class="visualizer-bar" wx:for="{{visualizerData}}" wx:key="index"style="height: {{item}}rpx; background: {{item > 60 ? '#FF3B30' : item > 30 ? '#FF9500' : '#34C759'}};"></view></view><view class="recording-info" wx:if="{{isRecording}}"><text class="recording-text">录音中...</text><text class="recording-time">{{recordingTime}}s</text></view><view class="voice-controls"><!-- 录音按钮 --><view class="record-btn {{isRecording ? 'recording' : ''}}"bindtouchstart="startRecording"bindtouchend="stopRecording"bindtouchcancel="stopRecording"><view class="record-icon"><text class="iconfont {{isRecording ? 'icon-stop' : 'icon-mic'}}"></text></view><text class="record-text">{{isRecording ? '松开结束' : '按住说话'}}</text></view><!-- 功能按钮 --><view class="function-buttons"><button class="func-btn" bindtap="playAudio" disabled="{{!currentAudio}}"><text class="iconfont icon-play"></text><text class="func-text">播放</text></button><button class="func-btn" bindtap="saveResult" disabled="{{!recognitionResult}}"><text class="iconfont icon-save"></text><text class="func-text">保存</text></button><button class="func-btn" bindtap="showHistory"><text class="iconfont icon-history"></text><text class="func-text">历史</text></button></view></view></view><!-- 设置面板 --><view class="settings-panel"><view class="setting-item"><text class="setting-label">识别语言</text><picker range="{{languageOptions}}" value="{{languageIndex}}"bindchange="onLanguageChange"><view class="setting-value">{{languageOptions[languageIndex]}}</view></picker></view><view class="setting-item"><text class="setting-label">音频质量</text><picker range="{{qualityOptions}}" value="{{qualityIndex}}"bindchange="onQualityChange"><view class="setting-value">{{qualityOptions[qualityIndex]}}</view></picker></view><view class="setting-item"><text class="setting-label">自动标点</text><switch checked="{{autoPunctuation}}" bindchange="onAutoPunctuationChange" /></view></view><!-- 识别历史 --><van-popup show="{{showHistoryPanel}}" position="bottom" round bind:close="hideHistory"><view class="history-panel"><view class="panel-header"><text class="panel-title">识别历史</text><text class="panel-close iconfont icon-close" bindtap="hideHistory"></text></view><scroll-view class="history-list" scroll-y><view class="history-item" wx:for="{{recognitionHistory}}" wx:key="id"bindtap="useHistoryItem"data-item="{{item}}"><view class="history-content"><text class="history-text">{{item.text}}</text><text class="history-time">{{item.create_time}}</text></view><view class="history-actions"><text class="iconfont icon-play" bindtap="playHistoryAudio" data-url="{{item.audio_url}}"></text><text class="iconfont icon-delete" bindtap="deleteHistoryItem" data-id="{{item.id}}"></text></view></view><view class="history-empty" wx:if="{{recognitionHistory.length === 0}}"><text>暂无识别记录</text></view></scroll-view></view></van-popup><!-- 加载状态 --><van-overlay show="{{isLoading}}"><view class="loading-wrapper"><van-loading size="24px" vertical>{{loadingText}}</van-loading></view></van-overlay><!-- 音频播放器 --><audio src="{{currentAudio}}" id="voiceAudio" controls="{{false}}"binderror="onAudioError"></audio>
</view>
WXSS样式设计
/* pages/voice/voice.wxss */
.voice-container {min-height: 100vh;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);padding: 20rpx;display: flex;flex-direction: column;
}.result-section {background: rgba(255, 255, 255, 0.95);border-radius: 20rpx;padding: 30rpx;margin-bottom: 30rpx;backdrop-filter: blur(10rpx);box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
}.section-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 30rpx;
}.section-title {font-size: 32rpx;font-weight: bold;color: #333333;
}.result-actions {display: flex;gap: 20rpx;
}.action-btn {background: transparent;border: 1rpx solid #007AFF;color: #007AFF;border-radius: 20rpx;padding: 12rpx 24rpx;font-size: 24rpx;
}.action-btn.clear {border-color: #FF3B30;color: #FF3B30;
}.result-content {max-height: 300rpx;min-height: 200rpx;margin-bottom: 20rpx;
}.result-text {font-size: 28rpx;line-height: 1.6;color: #333333;white-space: pre-wrap;word-break: break-all;
}.empty-result {display: flex;flex-direction: column;align-items: center;justify-content: center;height: 200rpx;
}.empty-image {width: 120rpx;height: 120rpx;margin-bottom: 20rpx;opacity: 0.5;
}.empty-text {font-size: 28rpx;color: #999999;
}.result-stats {display: flex;justify-content: space-around;padding-top: 20rpx;border-top: 1rpx solid #f0f0f0;
}.stat-item {display: flex;flex-direction: column;align-items: center;
}.stat-label {font-size: 24rpx;color: #999999;margin-bottom: 8rpx;
}.stat-value {font-size: 28rpx;font-weight: bold;color: #007AFF;
}.control-section {background: rgba(255, 255, 255, 0.95);border-radius: 20rpx;padding: 40rpx 30rpx;margin-bottom: 30rpx;backdrop-filter: blur(10rpx);box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
}.voice-visualizer {display: flex;justify-content: center;align-items: flex-end;height: 80rpx;margin-bottom: 30rpx;gap: 4rpx;
}.visualizer-bar {width: 8rpx;border-radius: 4rpx;transition: height 0.1s ease;
}.recording-info {display: flex;justify-content: space-between;align-items: center;margin-bottom: 40rpx;padding: 20rpx;background: #f8f8f8;border-radius: 12rpx;
}.recording-text {font-size: 28rpx;color: #FF3B30;font-weight: 500;
}.recording-time {font-size: 32rpx;font-weight: bold;color: #FF3B30;
}.voice-controls {display: flex;flex-direction: column;align-items: center;gap: 40rpx;
}.record-btn {display: flex;flex-direction: column;align-items: center;gap: 20rpx;padding: 40rpx;border-radius: 50%;background: linear-gradient(135deg, #007AFF, #5856D6);box-shadow: 0 12rpx 40rpx rgba(0, 122, 255, 0.3);transition: all 0.3s ease;
}.record-btn.recording {background: linear-gradient(135deg, #FF3B30, #FF2D55);transform: scale(1.05);box-shadow: 0 12rpx 40rpx rgba(255, 59, 48, 0.3);
}.record-icon {width: 80rpx;height: 80rpx;border-radius: 50%;background: rgba(255, 255, 255, 0.2);display: flex;align-items: center;justify-content: center;
}.record-icon .iconfont {font-size: 40rpx;color: #ffffff;
}.record-text {font-size: 28rpx;color: #ffffff;font-weight: 500;
}.function-buttons {display: flex;justify-content: space-around;width: 100%;
}.func-btn {display: flex;flex-direction: column;align-items: center;gap: 12rpx;background: transparent;border: none;padding: 20rpx;border-radius: 16rpx;transition: background-color 0.3s;
}.func-btn:active {background: #f8f8f8;
}.func-btn[disabled] {opacity: 0.5;
}.func-btn .iconfont {font-size: 36rpx;color: #007AFF;
}.func-text {font-size: 24rpx;color: #666666;
}.settings-panel {background: rgba(255, 255, 255, 0.95);border-radius: 20rpx;padding: 30rpx;backdrop-filter: blur(10rpx);box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
}.setting-item {display: flex;justify-content: space-between;align-items: center;padding: 24rpx 0;border-bottom: 1rpx solid #f0f0f0;
}.setting-item:last-child {border-bottom: none;
}.setting-label {font-size: 28rpx;color: #333333;
}.setting-value {font-size: 28rpx;color: #007AFF;padding: 12rpx 24rpx;background: #f8f8f8;border-radius: 8rpx;
}.history-panel {background: #ffffff;border-radius: 20rpx 20rpx 0 0;padding: 40rpx 30rpx;max-height: 80vh;
}.panel-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 30rpx;
}.panel-title {font-size: 32rpx;font-weight: bold;color: #333333;
}.panel-close {font-size: 32rpx;color: #999999;padding: 20rpx;
}.history-list {max-height: 60vh;
}.history-item {display: flex;justify-content: space-between;align-items: flex-start;padding: 24rpx 0;border-bottom: 1rpx solid #f8f8f8;
}.history-content {flex: 1;margin-right: 20rpx;
}.history-text {display: block;font-size: 28rpx;color: #333333;line-height: 1.4;margin-bottom: 12rpx;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 2;overflow: hidden;
}.history-time {font-size: 24rpx;color: #999999;
}.history-actions {display: flex;gap: 20rpx;
}.history-actions .iconfont {font-size: 28rpx;color: #007AFF;padding: 12rpx;
}.history-actions .icon-delete {color: #FF3B30;
}.history-empty {text-align: center;padding: 80rpx 0;color: #999999;font-size: 28rpx;
}.loading-wrapper {display: flex;align-items: center;justify-content: center;height: 100%;
}
JavaScript逻辑实现
// pages/voice/voice.js
const { request, buildUrl } = require('../../utils/request')
const { showLoading, hideLoading, showError, showSuccess, formatTime } = require('../../utils/util')Page({data: {// 识别状态isRecording: false,isLoading: false,loadingText: '识别中...',// 识别结果recognitionResult: '',currentAudio: '',textStats: {wordCount: 0,duration: 0,confidence: 0},// 录音控制recordingTime: 0,recordingTimer: null,recorderManager: null,// 可视化数据visualizerData: Array(20).fill(10),visualizerTimer: null,// 设置选项languageOptions: ['普通话', '英语', '粤语', '四川话'],languageIndex: 0,qualityOptions: ['标准质量', '高质量', '低质量'],qualityIndex: 0,autoPunctuation: true,// 历史记录recognitionHistory: [],showHistoryPanel: false,// 音频播放audioContext: null},onLoad() {this.initRecorder()this.initAudio()this.loadRecognitionHistory()},onUnload() {this.cleanup()},onShow() {// 重新加载历史记录this.loadRecognitionHistory()},initRecorder() {const recorderManager = wx.getRecorderManager()recorderManager.onStart(() => {console.log('录音开始')this.startRecordingTimer()this.startVisualizer()})recorderManager.onStop((res) => {console.log('录音结束', res)this.stopRecordingTimer()this.stopVisualizer()this.handleRecordingResult(res)})recorderManager.onError((error) => {console.error('录音错误:', error)this.stopRecordingTimer()this.stopVisualizer()this.setData({ isRecording: false })showError('录音失败: ' + error.errMsg)})this.setData({ recorderManager })},initAudio() {const audioContext = wx.createInnerAudioContext()audioContext.onError((error) => {console.error('音频播放错误:', error)showError('音频播放失败')})this.setData({ audioContext })},cleanup() {if (this.data.recordingTimer) {clearInterval(this.data.recordingTimer)}if (this.data.visualizerTimer) {clearInterval(this.data.visualizerTimer)}if (this.data.audioContext) {this.data.audioContext.destroy()}},startRecording() {if (this.data.isRecording) returnthis.setData({ isRecording: true })const options = {duration: 60000, // 最长60秒sampleRate: 16000,numberOfChannels: 1,encodeBitRate: 48000,format: 'wav',frameSize: 10 // 每10ms一帧}this.data.recorderManager.start(options)},stopRecording() {if (!this.data.isRecording) returnthis.data.recorderManager.stop()},startRecordingTimer() {this.setData({ recordingTime: 0 })const timer = setInterval(() => {this.setData({ recordingTime: this.data.recordingTime + 1 })}, 1000)this.setData({ recordingTimer: timer })},stopRecordingTimer() {if (this.data.recordingTimer) {clearInterval(this.data.recordingTimer)this.setData({ recordingTimer: null })}},startVisualizer() {const timer = setInterval(() => {const newData = this.data.visualizerData.map(() => Math.max(10, Math.random() * 80))this.setData({ visualizerData: newData })}, 100)this.setData({ visualizerTimer: timer })},stopVisualizer() {if (this.data.visualizerTimer) {clearInterval(this.data.visualizerTimer)this.setData({ visualizerTimer: null })// 重置可视化数据this.setData({ visualizerData: Array(20).fill(10) })}},async handleRecordingResult(res) {this.setData({ isRecording: false,isLoading: true,loadingText: '识别中...',currentAudio: res.tempFilePath})try {// 上传音频文件进行识别const uploadRes = await request.upload(buildUrl('speech.recognize'),res.tempFilePath,{language: this.getLanguageCode(),quality: this.getQualityCode(),auto_punctuation: this.data.autoPunctuation},{onProgress: (progress) => {if (progress.progress === 100) {this.setData({ loadingText: '处理中...' })}}})if (uploadRes.code === 200) {const result = uploadRes.datathis.setData({recognitionResult: result.text,textStats: {wordCount: result.word_count || 0,duration: result.audio_duration || 0,confidence: Math.round((result.confidence || 0) * 100)}})showSuccess('识别成功')// 保存到历史记录this.saveToHistory(result)} else {throw new Error(uploadRes.message)}} catch (error) {console.error('语音识别失败:', error)showError('识别失败: ' + error.message)} finally {this.setData({ isLoading: false })}},getLanguageCode() {const languageMap = {0: 'zh', // 普通话1: 'en', // 英语2: 'yue', // 粤语3: 'sichuan' // 四川话}return languageMap[this.data.languageIndex] || 'zh'},getQualityCode() {const qualityMap = {0: 'standard',1: 'high',2: 'low'}return qualityMap[this.data.qualityIndex] || 'standard'},async saveToHistory(result) {try {const historyItem = {id: Date.now(),text: result.text,audio_url: this.data.currentAudio,create_time: formatTime(new Date(), 'MM-DD HH:mm'),duration: result.audio_duration,confidence: result.confidence,language: this.getLanguageCode()}const newHistory = [historyItem, ...this.data.recognitionHistory]// 限制历史记录数量if (newHistory.length > 50) {newHistory.splice(50)}this.setData({ recognitionHistory: newHistory })// 保存到本地存储this.saveHistoryToStorage(newHistory)} catch (error) {console.error('保存历史记录失败:', error)}},saveHistoryToStorage(history) {try {wx.setStorageSync('voice_recognition_history', history)} catch (error) {console.error('保存历史记录到本地失败:', error)}},loadRecognitionHistory() {try {const history = wx.getStorageSync('voice_recognition_history') || []this.setData({ recognitionHistory: history })} catch (error) {console.error('加载历史记录失败:', error)}},clearResult() {this.setData({recognitionResult: '',currentAudio: '',textStats: {wordCount: 0,duration: 0,confidence: 0}})},copyResult() {if (!this.data.recognitionResult) {showError('没有可复制的内容')return}wx.setClipboardData({data: this.data.recognitionResult,success: () => {showSuccess('已复制到剪贴板')},fail: () => {showError('复制失败')}})},playAudio() {if (!this.data.currentAudio) {showError('没有可播放的音频')return}this.data.audioContext.src = this.data.currentAudiothis.data.audioContext.play()},playHistoryAudio(e) {const audioUrl = e.currentTarget.dataset.urlif (audioUrl) {this.data.audioContext.src = audioUrlthis.data.audioContext.play()}},async saveResult() {if (!this.data.recognitionResult) {showError('没有可保存的内容')return}try {showLoading('保存中...')const saveRes = await request.post(buildUrl('speech.save'), {text: this.data.recognitionResult,audio_duration: this.data.textStats.duration,confidence: this.data.textStats.confidence / 100,language: this.getLanguageCode()})if (saveRes.code === 200) {showSuccess('保存成功')} else {throw new Error(saveRes.message)}} catch (error) {console.error('保存结果失败:', error)showError('保存失败: ' + error.message)} finally {hideLoading()}},showHistory() {this.setData({ showHistoryPanel: true })},hideHistory() {this.setData({ showHistoryPanel: false })},useHistoryItem(e) {const item = e.currentTarget.dataset.itemthis.setData({recognitionResult: item.text,textStats: {wordCount: item.text.length,duration: item.duration || 0,confidence: Math.round((item.confidence || 0) * 100)}})this.hideHistory()},deleteHistoryItem(e) {const id = e.currentTarget.dataset.ide.stopPropagation()wx.showModal({title: '确认删除',content: '确定要删除这条记录吗?',success: (res) => {if (res.confirm) {const newHistory = this.data.recognitionHistory.filter(item => item.id !== id)this.setData({ recognitionHistory: newHistory })this.saveHistoryToStorage(newHistory)showSuccess('删除成功')}}})},onLanguageChange(e) {this.setData({ languageIndex: e.detail.value })},onQualityChange(e) {this.setData({ qualityIndex: e.detail.value })},onAutoPunctuationChange(e) {this.setData({ autoPunctuation: e.detail.value })},onAudioError(e) {console.error('音频错误:', e.detail)showError('音频播放失败')},onShareAppMessage() {return {title: '智慧社区 - 语音识别',path: '/pages/voice/voice'}}
})

7.2 后端语音识别API深度实现

数据模型设计
# smart/models.py
class VoiceRecognitionRecord(models.Model):"""语音识别记录模型"""LANGUAGE_CHOICES = (('zh', '普通话'),('en', '英语'),('yue', '粤语'),('sichuan', '四川话'),)user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户")audio_file = models.FileField(upload_to='voice_recognition/%Y/%m/%d/', verbose_name="音频文件")text = models.TextField(verbose_name="识别文本")language = models.CharField(max_length: '20', choices=LANGUAGE_CHOICES, default='zh',verbose_name="识别语言")confidence = models.FloatField(default=0, verbose_name="置信度")audio_duration = models.FloatField(default=0, verbose_name="音频时长")word_count = models.IntegerField(default=0, verbose_name="字数")auto_punctuation = models.BooleanField(default=True, verbose_name="自动标点")device_info = models.CharField(max_length=200, blank=True, verbose_name="设备信息")ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name="IP地址")create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")class Meta:db_table = 'smart_voice_recognition'verbose_name = '语音识别记录'verbose_name_plural = verbose_nameordering = ['-create_time']indexes = [models.Index(fields=['user', 'create_time']),models.Index(fields=['language', 'create_time']),]def __str__(self):return f"{self.user.nickname} - {self.create_time.strftime('%Y-%m-%d %H:%M')}"def get_absolute_audio_url(self, request=None):"""获取完整的音频URL"""if self.audio_file and hasattr(self.audio_file, 'url'):if request:return request.build_absolute_uri(self.audio_file.url)return self.audio_file.urlreturn Nonedef save(self, *args, **kwargs):# 自动计算字数if self.text:self.word_count = len(self.text.strip())super().save(*args, **kwargs)
百度AI语音服务增强
# smart/libs/baidu_ai.py
import base64
import json
import logging
from aip import AipSpeech
from django.conf import settingslogger = logging.getLogger(__name__)class BaiduAISpeechService:"""百度AI语音服务增强版"""def __init__(self):self.speech_client = AipSpeech(settings.BAIDU_AI_APP_ID,settings.BAIDU_AI_API_KEY,settings.BAIDU_AI_SECRET_KEY)def speech_to_text(self, audio_data, language='zh', rate=16000, format='wav'):"""语音识别增强版"""try:# 参数映射language_map = {'zh': 1537,  # 普通话'en': 1737,  # 英语'yue': 1637, # 粤语'sichuan': 1837  # 四川话}dev_pid = language_map.get(language, 1537)# 识别选项options = {'dev_pid': dev_pid,}# 调用百度AI语音识别result = self.speech_client.asr(audio_data, format, rate, options)logger.info(f"语音识别结果: {result}")if result.get('err_no') == 0:text = result['result'][0]return {'success': True,'text': text,'confidence': self._calculate_confidence(result),'word_count': len(text)}else:error_msg = self._get_error_message(result.get('err_no'))return {'success': False,'error_code': result.get('err_no'),'error_msg': error_msg}except Exception as e:logger.error(f"语音识别异常: {str(e)}")return {'success': False,'error_code': -1,'error_msg': f"系统异常: {str(e)}"}def text_to_speech(self, text, language='zh', speed=5, pitch=5, volume=5):"""文本转语音"""try:# 参数设置options = {'spd': speed,  # 语速 (0-9)'pit': pitch,  # 音调 (0-9)'vol': volume, # 音量 (0-9)'per': self._get_voice_type(language)  # 发音人}result = self.speech_client.synthesis(text, language, 1, options)if not isinstance(result, dict):# 返回的是二进制音频数据return {'success': True,'audio_data': result}else:return {'success': False,'error_code': result.get('err_no'),'error_msg': result.get('err_msg', '合成失败')}except Exception as e:logger.error(f"文本转语音异常: {str(e)}")return {'success': False,'error_code': -1,'error_msg': f"系统异常: {str(e)}"}def long_speech_to_text(self, audio_file_path, language='zh'):"""长语音识别(适用于60秒以上的音频)"""try:# 读取音频文件with open(audio_file_path, 'rb') as f:audio_data = base64.b64encode(f.read()).decode()# 这里需要调用百度AI的长语音识别接口# 注意:长语音识别是异步的,需要先提交任务,再查询结果# 这里简化实现,实际项目中需要完整实现异步流程# 模拟长语音识别结果return {'success': True,'text': '这是长语音识别的模拟结果','confidence': 0.95,'word_count': 10,'task_id': 'mock_task_id'}except Exception as e:logger.error(f"长语音识别异常: {str(e)}")return {'success': False,'error_code': -1,'error_msg': f"系统异常: {str(e)}"}def _calculate_confidence(self, result):"""计算置信度(简化实现)"""# 百度AI没有直接返回置信度,这里可以根据业务需求实现# 可以基于结果的其他信息估算置信度return 0.9  # 默认返回90%置信度def _get_error_message(self, error_code):"""获取错误信息"""error_map = {3300: '输入参数不正确',3301: '音频质量过差',3302: '鉴权失败',3303: '语音服务器后端问题',3304: '请求GPS过大,超过限额',3305: '产品未开通',3307: '识别失败',3308: '音频过长',3309: '音频数据问题',3310: '输入的音频文件过大',3311: '采样率rate参数不在选项里',3312: '音频格式format参数不在选项里'}return error_map.get(error_code, '未知错误')def _get_voice_type(self, language):"""获取发音人类型"""voice_map = {'zh': 0,  # 女声'en': 1,  # 男声'yue': 3, # 粤语女声'sichuan': 4  # 四川话女声}return voice_map.get(language, 0)# 创建语音服务实例
baidu_ai_speech_service = BaiduAISpeechService()
语音识别视图
# smart/views/voice.py
import os
import tempfile
import logging
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.utils import timezone
from ..models import VoiceRecognitionRecord
from ..libs.baidu_ai import baidu_ai_speech_service
from ..decorators import login_required
from ..serializers import VoiceRecognitionRecordSerializerlogger = logging.getLogger(__name__)class VoiceRecognitionAPIView(APIView):"""语音识别API"""@login_requireddef post(self, request):try:if 'file' not in request.FILES:return Response({'code': 400,'message': '请上传音频文件','data': None}, status=status.HTTP_400_BAD_REQUEST)audio_file = request.FILES['file']# 获取识别参数language = request.data.get('language', 'zh')quality = request.data.get('quality', 'standard')auto_punctuation = request.data.get('auto_punctuation', True)# 验证文件类型if not self._validate_audio_file(audio_file):return Response({'code': 400,'message': '不支持的音频格式','data': None}, status=status.HTTP_400_BAD_REQUEST)# 保存临时文件with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp_file:for chunk in audio_file.chunks():tmp_file.write(chunk)tmp_path = tmp_file.nametry:# 读取音频数据with open(tmp_path, 'rb') as f:audio_data = f.read()# 根据质量设置采样率sample_rate = self._get_sample_rate(quality)# 调用百度AI语音识别result = baidu_ai_speech_service.speech_to_text(audio_data, language=language,rate=sample_rate)if result['success']:# 保存识别记录recognition_record = VoiceRecognitionRecord(user=request.user,audio_file=audio_file,text=result['text'],language=language,confidence=result.get('confidence', 0),audio_duration=self._get_audio_duration(tmp_path),auto_punctuation=auto_punctuation,device_info=request.META.get('HTTP_USER_AGENT', ''),ip_address=self._get_client_ip(request))recognition_record.save()# 序列化返回数据serializer = VoiceRecognitionRecordSerializer(recognition_record, context={'request': request})return Response({'code': 200,'message': '识别成功','data': {**serializer.data,'word_count': result.get('word_count', 0)}})else:return Response({'code': result['error_code'],'message': f"识别失败: {result['error_msg']}",'data': None})finally:# 删除临时文件if os.path.exists(tmp_path):os.unlink(tmp_path)except Exception as e:logger.error(f"语音识别异常: {str(e)}")return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)def _validate_audio_file(self, audio_file):"""验证音频文件"""allowed_extensions = ['.wav', '.mp3', '.m4a', '.amr']file_name = audio_file.name.lower()return any(file_name.endswith(ext) for ext in allowed_extensions)def _get_sample_rate(self, quality):"""根据质量获取采样率"""sample_rates = {'low': 8000,'standard': 16000,'high': 44100}return sample_rates.get(quality, 16000)def _get_audio_duration(self, file_path):"""获取音频时长(简化实现)"""# 实际项目中可以使用 librosa 或 pydub 等库来获取准确的音频时长# 这里返回一个估计值file_size = os.path.getsize(file_path)# 假设是16kHz, 16bit, 单声道的wav文件estimated_duration = file_size / (16000 * 2)return round(estimated_duration, 2)def _get_client_ip(self, request):"""获取客户端IP"""x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')if x_forwarded_for:ip = x_forwarded_for.split(',')[0]else:ip = request.META.get('REMOTE_ADDR')return ipclass VoiceRecognitionHistoryAPIView(APIView):"""语音识别历史记录API"""@login_requireddef get(self, request):try:# 获取查询参数page = int(request.GET.get('page', 1))page_size = int(request.GET.get('page_size', 20))language = request.GET.get('language')start_date = request.GET.get('start_date')end_date = request.GET.get('end_date')# 构建查询集queryset = VoiceRecognitionRecord.objects.filter(user=request.user)# 应用过滤条件if language:queryset = queryset.filter(language=language)if start_date and end_date:queryset = queryset.filter(create_time__date__range=[start_date, end_date])# 分页total_count = queryset.count()records = queryset.order_by('-create_time')[(page - 1) * page_size: page * page_size]# 序列化数据serializer = VoiceRecognitionRecordSerializer(records, many=True, context={'request': request})return Response({'code': 200,'message': 'success','data': {'list': serializer.data,'pagination': {'total': total_count,'page': page,'page_size': page_size,'total_pages': (total_count + page_size - 1) // page_size}}})except Exception as e:logger.error(f"获取语音识别历史异常: {str(e)}")return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)class TextToSpeechAPIView(APIView):"""文本转语音API"""@login_requireddef post(self, request):try:text = request.data.get('text')language = request.data.get('language', 'zh')speed = int(request.data.get('speed', 5))pitch = int(request.data.get('pitch', 5))volume = int(request.data.get('volume', 5))if not text:return Response({'code': 400,'message': '请输入要转换的文本','data': None}, status=status.HTTP_400_BAD_REQUEST)# 文本长度限制if len(text) > 1000:return Response({'code': 400,'message': '文本过长,请控制在1000字以内','data': None}, status=status.HTTP_400_BAD_REQUEST)# 调用文本转语音服务result = baidu_ai_speech_service.text_to_speech(text, language, speed, pitch, volume)if result['success']:# 将音频数据转换为base64audio_base64 = base64.b64encode(result['audio_data']).decode()return Response({'code': 200,'message': '转换成功','data': {'audio_data': audio_base64,'text': text,'language': language}})else:return Response({'code': result['error_code'],'message': f"转换失败: {result['error_msg']}",'data': None})except Exception as e:logger.error(f"文本转语音异常: {str(e)}")return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)class VoiceRecognitionStatisticsAPIView(APIView):"""语音识别统计API"""@login_requireddef get(self, request):try:from django.db.models import Count, Avg, Sumfrom django.utils import timezonefrom datetime import timedelta# 获取统计参数days = int(request.GET.get('days', 7))end_date = timezone.now().date()start_date = end_date - timedelta(days=days-1)# 查询统计数据stats = VoiceRecognitionRecord.objects.filter(user=request.user,create_time__date__range=[start_date, end_date]).aggregate(total_count=Count('id'),total_duration=Sum('audio_duration'),avg_confidence=Avg('confidence'),avg_word_count=Avg('word_count'))# 语言分布language_distribution = VoiceRecognitionRecord.objects.filter(user=request.user,create_time__date__range=[start_date, end_date]).values('language').annotate(count=Count('id')).order_by('-count')# 每日统计daily_stats = []current_date = start_datewhile current_date <= end_date:day_stats = VoiceRecognitionRecord.objects.filter(user=request.user,create_time__date=current_date).aggregate(count=Count('id'),total_duration=Sum('audio_duration') or 0)daily_stats.append({'date': current_date.strftime('%m-%d'),'count': day_stats['count'],'total_duration': round(day_stats['total_duration'], 2)})current_date += timedelta(days=1)data = {'overview': {'total_count': stats['total_count'] or 0,'total_duration': round(stats['total_duration'] or 0, 2),'avg_confidence': round(stats['avg_confidence'] or 0, 2),'avg_word_count': round(stats['avg_word_count'] or 0, 2)},'language_distribution': list(language_distribution),'daily_stats': daily_stats}return Response({'code': 200,'message': 'success','data': data})except Exception as e:logger.error(f"获取语音识别统计异常: {str(e)}")return Response({'code': 500,'message': f'服务器错误: {str(e)}','data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
URL路由配置
# smart/urls.py
from django.urls import path
from .views.voice import (VoiceRecognitionAPIView,VoiceRecognitionHistoryAPIView,TextToSpeechAPIView,VoiceRecognitionStatisticsAPIView
)urlpatterns = [path('speech/recognize/', VoiceRecognitionAPIView.as_view(), name='speech-recognize'),path('speech/history/', VoiceRecognitionHistoryAPIView.as_view(), name='speech-history'),path('speech/tts/', TextToSpeechAPIView.as_view(), name='speech-tts'),path('speech/statistics/', VoiceRecognitionStatisticsAPIView.as_view(), name='speech-statistics'),
]
      wx.showToast({title: '网络连接失败',icon: 'error'})reject(error)}})
})

}

// 其他方法保持不变…
}


### 10.2 后端用户认证系统#### 用户模型扩展```python
# smart/models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils import timezoneclass UserManager(BaseUserManager):"""自定义用户管理器"""def create_user(self, openid, nickname='', avatar='', **extra_fields):"""创建普通用户"""if not openid:raise ValueError('OpenID必须提供')user = self.model(openid=openid,nickname=nickname,avatar=avatar,**extra_fields)user.set_unusable_password()user.save(using=self._db)return userdef create_superuser(self, openid, nickname='', avatar='', **extra_fields):"""创建超级用户"""extra_fields.setdefault('is_staff', True)extra_fields.setdefault('is_superuser', True)if extra_fields.get('is_staff') is not True:raise ValueError('超级用户必须设置 is_staff=True')if extra_fields.get('is_superuser') is not True:raise ValueError('超级用户必须设置 is_superuser=True')return self.create_user(openid, nickname, avatar, **extra_fields)class User(AbstractBaseUser, PermissionsMixin):"""自定义用户模型"""ROLE_CHOICES = (('admin', '管理员'),('grid_member', '网格员'),('resident', '居民'),('visitor', '访客'),)openid = models.CharField(max_length=100, unique=True, verbose_name="微信OpenID")unionid = models.CharField(max_length=100, blank=True, verbose_name="微信UnionID")nickname = models.CharField(max_length=50, verbose_name="昵称")avatar = models.URLField(blank=True, verbose_name="头像")phone = models.CharField(max_length=20, blank=True, verbose_name="手机号")role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='visitor',verbose_name="角色")# 权限相关字段is_active = models.BooleanField(default=True, verbose_name="是否启用")is_staff = models.BooleanField(default=False, verbose_name="是否员工")is_superuser = models.BooleanField(default=False, verbose_name="是否超级用户")# 时间字段date_joined = models.DateTimeField(default=timezone.now, verbose_name="加入时间")last_login = models.DateTimeField(null=True, blank=True, verbose_name="最后登录时间")last_active = models.DateTimeField(null=True, blank=True, verbose_name="最后活跃时间")# 关联网格区域managed_areas = models.ManyToManyField('Area',related_name='managers',blank=True,verbose_name="管理的网格区域")objects = UserManager()USERNAME_FIELD = 'openid'REQUIRED_FIELDS = ['nickname']class Meta:db_table = 'smart_user'verbose_name = '用户'verbose_name_plural = verbose_namedef __str__(self):return f"{self.nickname}({self.role})"def get_full_name(self):return self.nicknamedef get_short_name(self):return self.nicknamedef update_last_active(self):"""更新最后活跃时间"""self.last_active = timezone.now()self.save(update_fields=['last_active'])@propertydef can_collect_info(self):"""是否有信息采集权限"""return self.role in ['admin', 'grid_member']@propertydef can_manage_system(self):"""是否有系统管理权限"""return self.role in ['admin']def has_area_permission(self, area):"""是否有指定区域的权限"""if self.role == 'admin':return Trueelif self.role == 'grid_member':return self.managed_areas.filter(id=area.id).exists()return Falseclass UserSession(models.Model):"""用户会话模型"""user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户")token = models.CharField(max_length=255, unique=True, verbose_name="访问令牌")refresh_token = models.CharField(max_length=255, unique=True, verbose_name="刷新令牌")expires_at = models.DateTimeField(verbose_name="过期时间")device_info = models.CharField(max_length=200, blank=True, verbose_name="设备信息")ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name="IP地址")is_active = models.BooleanField(default=True, verbose_name="是否有效")created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")class Meta:db_table = 'smart_user_session'verbose_name = '用户会话'verbose_name_plural = verbose_nameindexes = [models.Index(fields=['token']),models.Index(fields=['refresh_token']),models.Index(fields=['expires_at']),]def __str__(self):return f"{self.user.nickname} - {self.created_at}"def is_expired(self):"""检查会话是否过期"""return timezone.now() > self.expires_atdef refresh(self, new_expires_at):"""刷新会话"""self.expires_at = new_expires_atself.save()
http://www.dtcms.com/a/535878.html

相关文章:

  • 做设计用什么软件seo优化排名价格
  • 《算法通关指南数据结构和算法篇(3)--- 栈和stack》
  • 如何建设诗词网站盘县网站开发
  • 空间数据采集与管理丨在 ArcGIS Pro 中利用模型构建器批处理多维数据
  • 【数据结构】大话单链表
  • Volta 管理 Node.js 工具链指南
  • 《HTTP 中的“握手”:从 TCP 到 TLS 的安全通信之旅》
  • 计算机网络6
  • 信息咨询公司网站源码深圳白狐工业设计公司
  • 网站开发 李博如何建一个自己的网站
  • 智能家居设备离线视频回看功能设计:缓存、断网恢复与存储管理的硬核攻略
  • AIOT进军纳斯达克,推动Web3健康金融迈向全球资本市场
  • springAI +openAI 接入阿里云百炼大模型-通义千问
  • LeetCode 2441.与对应负数同时存在的最大正整数
  • 高性能推理引擎的基石:C++与硬件加速的完美融合
  • 从Jar包到K8s上线:全流程拆解+高可用实战
  • 大模型微调—LlamaFactory自定义微调数据集
  • 黑龙江微信网站开发网站页面高度
  • CodeBuddy编程实现:基于EdgeOne边缘安全加速平台的远程计算资源共享技术平台
  • Vue 模板语法深度解析:从文本插值到 HTML 渲染的核心逻辑
  • vue3 列表hooks
  • Nginx的安装与配置(window系统)
  • vue3虚拟列表
  • vue之异步更新队列
  • 软文推广有哪些企业关键词优化推荐
  • REFramework下载和安装教程(附安装包)
  • 散户如何做手机T0程序化交易(上)
  • JMeter:执行顺序与作用域
  • Java的自定义异常,throw和throws的对比
  • 哪些知名网站用wordpress建设摩托车是名牌吗