Flutter应用与Postman调用Flask API时音频分类结果不一致问题
问题描述
我开发了一款Flutter应用,可录制音频并发送至Python Flask API进行分类。该API使用Librosa提取特征,通过预训练机器学习模型完成音频分类,但目前遇到预测结果不一致的问题:
- Flutter应用上传音频到API时分类结果错误;但用Postman上传同一应用录制的训练用音频文件时,分类结果正确。
- 两种场景下的录音均为16kHz采样率的.wav格式。
Flutter代码
Future<void> _startRecording() async { try { Record record = Record(); if (await record.hasPermission()) { print("startRecording() hasPermission "); Directory tempDir = await getTemporaryDirectory(); String tempPath = tempDir.path + '/audio.wav'; await record.start(path: tempPath); setState(() { _isRecording = true; _audioPath = tempPath; print("tempPath $tempPath"); }); print("Start Recording - _audioPath: $_audioPath"); } } catch (e) { print("startRecording() has no Permission"); print(e); } } Future<void> _stopRecording() async { try { Record record = Record(); String? path = await record.stop(); if (path != null) { setState(() { _isRecording = false; _audioPath = path; }); // Call the upload method here print("Stop Recording - _audioPath: $_audioPath"); } } catch (e) { print(e); } Timer(Duration(seconds: 1), () { uploadAudio(File(_audioPath!), 'balla'); }); } Future<void> uploadAudio(File audioFile, String inputWord) async { var request = http.MultipartRequest('POST', Uri.parse('http://192.168.8.181:5000/predict')); request.fields['input_word'] = inputWord; request.files.add(http.MultipartFile.fromBytes('audio_file', await audioFile.readAsBytes(), filename: 'audio.wav')); var response = await request.send(); if (response.statusCode == 200) { var result = await http.Response.fromStream(response); print('Result: ${result.body}'); var parsedJson = json.decode(result.body); if (parsedJson['result'] == "Correct Answer") { audioPlayer.dispose(); audioPlayer.pause(); Navigator.push( context, MaterialPageRoute( builder: (context) => Correct(), ), ); } if (parsedJson['result'] == "Wrong Answer") { audioPlayer.dispose(); audioPlayer.pause(); Navigator.push( context, MaterialPageRoute( builder: (context) => InCorrect(), ), ); } } else { print('Failed to upload audio'); } }
Flask API代码
from flask import Flask, request, jsonify from pydub import AudioSegment import os import librosa import numpy as np import joblib import subprocess import logging app = Flask(__name__) # Initialize logging logging.basicConfig(filename='app.log', level=logging.INFO) # Define max_length max_length = 100 # Function to convert audio file bit rate def convert_audio_bit_rate(audio_file_path, target_bit_rate=256000): output_file_path = "converted_audio.wav" try: # Delete the existing converted file if it exists if os.path.exists(output_file_path): os.remove(output_file_path) subprocess.run([ "ffmpeg", "-i", audio_file_path, "-ab", str(target_bit_rate), output_file_path ]) except Exception as e: logging.error(f"Error in converting audio: {e}") return None return output_file_path def predict_class(audio_file_path): try: # Load the scaler, label encoder, and the model scaler = joblib.load('scaler.pkl') le = joblib.load('label_encoder.pkl') model = joblib.load('Student_audio_model.pkl') # Load the audio file waveform, sample_rate = librosa.load(audio_file_path, sr=None) # Feature extraction mfcc = librosa.feature.mfcc(y=waveform, sr=sample_rate) # Padding feature array to a fixed length if mfcc.shape[1] < max_length: pad_width = max_length - mfcc.shape[1] mfcc = np.pad(mfcc, pad_width=((0, 0), (0, pad_width)), mode='constant') else: mfcc = mfcc[:, :max_length] # Reshaping and scaling features = mfcc.reshape(1, -1) features = scaler.transform(features) # Prediction predicted_class = model.predict(features) # Convert integer label to original class label predicted_label = le.inverse_transform(predicted_class)[0] except Exception as e: logging.error(f"Error in prediction: {e}") return None return predicted_label @app.route('/predict', methods=['POST']) def predict(): try: # Get the audio file audio_file = request.files["audio_file"] # Save the audio file audio_file_path = "uploaded_audio.wav" audio_file.save(audio_file_path) # Predict the class predicted_class = predict_class(audio_file_path) if predicted_class is None: return jsonify({"result": "Error in prediction"}), 500 # Get the input_word input_word = request.form["input_word"] # Clean-up # if os.path.exists(audio_file_path): # os.remove(audio_file_path) #if os.path.exists(converted_audio_file_path): # os.remove(converted_audio_file_path) if input_word == predicted_class: return jsonify({"result": "Correct Answer"}) else: return jsonify({"result": "Wrong Answer"}) except Exception as e: logging.error(f"General error: {e}") return jsonify({"result": "An error occurred"}), 500 @app.errorhandler(404) def not_found(error): return jsonify({"error": "Not Found"}), 404 @app.errorhandler(500) def internal_error(error): return jsonify({"error": "Internal Server Error"}), 500 if __name__ == '__main__': app.run(host="0.0.0.0", port=5000)
问题排查与解决方案
核心差异在于「Flutter直接上传」和「Postman上传本地文件」的音频数据或处理逻辑不一致,以下是针对性解决步骤:
1. 强制Flutter录制参数匹配训练集
即使采样率是16kHz,音频的位深、声道数可能和训练集不匹配:
- 修改Flutter录制代码,明确指定参数:
await record.start( path: tempPath, sampleRate: 16000, bitRate: 256000, // 16bit位深对应256kbps numChannels: 1, // 单声道,和训练集保持一致 ); - 在Flask端打印上传音频的参数,对比训练集:
import wave with wave.open(audio_file_path, 'rb') as wf: logging.info(f"上传音频参数: 采样率={wf.getframerate()}, 声道={wf.getnchannels()}, 位深={wf.getsampwidth()*8}")
2. 验证上传前后音频文件一致性
检查Flutter上传过程中是否存在数据损坏:
- Flutter端打印本地文件字节数:
var fileBytes = await audioFile.readAsBytes(); print("本地文件字节数: ${fileBytes.length}"); - Flask端打印保存后的文件大小:
logging.info(f"保存后的文件大小: {os.path.getsize(audio_file_path)} bytes") - 将Flutter录制的文件导出到本地,用Postman上传该文件测试:如果结果正确,说明问题出在上传流程;如果错误,说明录制本身有问题。
3. 对齐特征提取逻辑
确保预测时的特征提取和训练时完全一致:
- 明确指定MFCC的参数(比如
n_mfcc数量),避免默认值差异:mfcc = librosa.feature.mfcc(y=waveform, sr=sample_rate, n_mfcc=13) # 和训练时参数一致 - 检查padding/截断逻辑:训练时如果是对MFCC时间轴做padding到100,预测时的
max_length和mode='constant'必须完全匹配。
4. 修复音频预处理缺失问题
API中定义了convert_audio_bit_rate函数,但预测时未调用。如果训练集音频经过比特率转换,预测时必须同步处理:
# 在predict函数中,保存文件后添加: converted_path = convert_audio_bit_rate(audio_file_path) if converted_path is None: return jsonify({"result": "音频转换失败"}), 500 predicted_class = predict_class(converted_path)
如果训练时未做比特率转换,直接删除该冗余函数。
5. 对比两次上传文件的MD5值
验证Flutter上传的文件和本地文件是否完全相同:
import hashlib def get_file_md5(file_path): with open(file_path, 'rb') as f: return hashlib.md5(f.read()).hexdigest() # 在predict函数中添加: logging.info(f"上传文件MD5: {get_file_md5(audio_file_path)}")
分别用Flutter和Postman上传同一文件,对比MD5:若不同,说明Flutter的文件读取或上传逻辑有问题。
内容的提问来源于stack exchange,提问作者Dasun Dola
相关产品推荐
相关产品推荐

