You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 08:30:05