赞
踩
我想实现的效果是,我的服务器提供两个路由网址,网页A用于拍照、然后录音,把照片和录音传给服务器,服务器发射信号,通知另一个路由的网页B更新,把刚刚传来的照片和录音显示在网页上。
然后网页B用户根据这个照片和录音,回答一些问题,输入答案的文本,传给服务器。服务器拿到答案后,再发射信号,把这个结果显示在网页A上。
这就得用到双向通信(其实有点类似两个网页聊天的功能,而且支持发送语音、图片、文本三种消息)。这里用的是 socket.io 包。在本地写还是很好写的,但是,部署到服务器上之后,就出了很多 bug。很多坑,这里把我遇到的记下来,防止再次犯错。
这里只记录关键代码,也就是容易掉坑的代码。整个项目我之后会上传到 github 上。传好了补连接。
socket.emit("upload_completed")
,通知服务器数据已经上传了upload_completed
信号,收到该信号后,服务器作为中转站,广播信号 emit('data_updated', data, broadcast=True)
通知前端该更新数据了data_updated
信号,修改自己的页面,展示图片和录音。等用户在该网页填好答案之后,点击发送按钮,网页 B 发生信号 socket.emit("annotated_answer")
annotated_answer
信号,收到该信号后,作为中转站,广播信号 emit('send_answer', answer, broadcast=True)
send_answer
,收到该信号后,把结果显示在网页上我这里只贴最关键的代码,加上注释,直接把这个代码粘上去,是会报错的。
@app.route('/upload', methods=['POST']) def app_upload_file(): # 保存图片 img_file = request.files['img'] if img_file.filename == '': return jsonify({'error': 'No image'}), 400 try: image_path = os.path.join(app.config['UPLOAD_FOLDER'], img_file.filename) img_file.save(image_path) shutil.copy(image_path, os.path.join(os.path.dirname(__file__), 'static/show.jpg')) # 用于展示在网页上 log(f"save image: {image_path}") except Exception as e: return jsonify({'error': str(e)}), 500 try: # 传过来的就是文本 question = request.form['question'] except: question = "请描述图片内容" return jsonify({"image": img_file.filename, "question": question}) @app.route('/upload/speech', methods=['POST']) def recognize_speech(): speech_file = request.files['speech'] try: save_path = os.path.join(app.config['UPLOAD_FOLDER'], speech_file.filename) speech_file_path = os.path.join(app.config['UPLOAD_FOLDER'], save_path) speech_file.save(speech_file_path) # question = speech2txt(speech_file_path) # print('百度识别结果:', question) except Exception as e: return jsonify({'error': str(e)}), 500 return jsonify({"speech": speech_file.filename}) @socketio.on('upload_completed') def handle_upload_completed(data): # pip install flask-socketio eventlet print(data) try: emit('data_updated', data, broadcast=True) except Exception as e: print(e) emit('error', {'error': str(e)}) @socketio.on('upload_speech_completed') def handle_upload_speech_completed(data): # pip install flask-socketio eventlet try: emit('data_speech_updated', data, broadcast=True) except Exception as e: print(e) emit('error', {'error': str(e)}) @socketio.on('annotated_answer') def handle_annotated_answer(answer): log(f'get answer from annotator: {answer}') try: emit('send_answer', answer, broadcast=True) except Exception as e: print(e) if __name__ == '__main__': # app.run(host='0.0.0.0', port=8099) # 这个地方!!看清楚!看清楚!要调用 socketio 的 run 方法,不是用 app 的 run 方法,不然没法双向连接的 socketio.run(app, host='0.0.0.0', allow_unsafe_werkzeug=True, port=8099)
注意这里只贴了一部分代码,关于文件怎么上传的,也就是引入的 camera.js 和 recorder.js 这俩文件的内容,在我这这篇文章里贴了: flask 后端 + 微信小程序和网页两种前端:调用硬件(相机和录音)和上传至服务器
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/full_button.css') }}" type="text/css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> </head> <body> <div style="display: flex"> <div> <video id="videoElement" autoplay="autoplay" muted="muted" style="width: 40px"></video> <img id="photo" alt="你的照片" src="" style="display: none"> </div> <div id="answer" class="answer-text">答案等待中...</div> </div> <div class="button-grid"> <button id="snapButton">拍摄照片</button> <button id="recorderButton">录音</button> <button id="captionButton">描述图片</button> <button id="vqaButton">回答问题</button> </div> {# <input type="text" id="textQuestion" placeholder="请输入问题...">#} <script> // 这里最最最关键的就是这个网址,如果你在本地跑,要填 localhost,不能填 127.0.0.1;如果是部署在服务器,要填成服务器的地址,不然肯定是连不上的。 const socket = io.connect('http://localhost:8099'); // 连接到Flask服务器 socket.on('send_answer', function (data) { // 接收到服务器返回的答案,震动提示,把答案显示在页面上 console.log('接收到答案:', data); document.getElementById('answer').textContent = data; navigator.vibrate([200]); // 震动提示收到答案 }) var imageBlob = null; // 拍摄的图片 var speechBlob = null; // 提出的问题 // 生成随机文件名 function randomFilename() { let now = new Date().getTime(); let str = `xxxxxxxx-xxxx-${now}-yxxx`; return str.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16) }) } </script> <script type="text/javascript" src="../static/js/user_camera.js"></script> <script type="text/javascript" src="../static/js/user_recorder.js"></script> <script> // 绑定 caption 按钮 document.getElementById('captionButton').onclick = function () { if (imageBlob == null) { alert('请先拍摄照片,再点击“描述图片”按钮') } else { const captionFormData = new FormData(); let imgFilename = randomFilename()+'.jpg'; captionFormData.append('img', imageBlob, imgFilename); captionFormData.append('question', '请描述图片内容'); fetch('http://localhost:8099/upload', { method: 'POST', body: captionFormData }) .then(response => { console.log('response:', response); if (response.status === 200) { console.log('发射信号 upload_completed'); // 注意!!这里发射的信号,带的数据,得是URL.createObjectURL(imageBlob)不能是别的不能是别的不能是别的,重要的事情说3遍!!不然无法正确地显示在网页 B 上 socket.emit('upload_completed', {'image': URL.createObjectURL(imageBlob), 'question': '请描述图片内容'}); } }) .then(data => console.log('data:', data)) .catch(error => console.error(error)); } }; // 绑定 vqa 按钮 document.getElementById('vqaButton').onclick = function () { if (imageBlob == null) { alert('请先拍摄照片,再点击“描述图片”按钮') } else { if (speechBlob == null) { alert('您还没有提问,请先点击录音按钮录音提问') } else { let filename = randomFilename(); // 先发语音再发图片,因为发了图片之后会提示听录音 const speechFormData = new FormData(); speechFormData.append('speech', speechBlob, filename+'.wav'); fetch('http://localhost:8099/upload/speech', { method: 'POST', body: speechFormData }) .then(response => { console.log('response:', response); if (response.status === 200) { console.log('成功上传音频', response); socket.emit('upload_speech_completed', {'speech': window.URL.createObjectURL(speechBlob)}) } }) .then(data => console.log('data:', data)) .catch(error => console.error(error)); const imgFormData = new FormData(); imgFormData.append('img', imageBlob, filename+'.jpg'); fetch('http://localhost:8099/upload', { method: 'POST', body: imgFormData }) .then(response => { console.log('response:', response); if (response.status === 200) { console.log('发射信号 upload_completed'); socket.emit('upload_completed', { 'image': URL.createObjectURL(imageBlob), 'question': '请听录音'}); } }) .then(data => console.log('data:', data)) .catch(error => console.error(error)); } } }; </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>human-annotation</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> </head> <body> <img id="image" src="" alt="Your Image"> <audio id="audioPlayer" controls class="audio-player"></audio> <div style="display: flex">提问:<div id="question"></div></div> <input type="text" id="textInput" placeholder="请输入答案..."> <button id="submitButton">发送</button> <script> // 这里也是,大坑,大坑啊!!这个地址要填对,本地用 localhost,云端用云端服务器地址啊! var socket = io.connect('http://localhost:8099'); // 连接到Flask服务器 socket.on('data_updated', function(data) { // 当接收到来自服务器的数据时,更新页面内容 var img = document.getElementById('image'); img.src = data.image; console.log('img.src'); // document.getElementById('image').innerHTML = '<img src="' + data.image + '" alt="Uploaded Image">'; document.getElementById('question').textContent = data.question; }); socket.on('data_speech_updated', function (data) { var audioPlayer = document.getElementById("audioPlayer"); audioPlayer.src = data.speech; }); // 监听按钮点击事件 document.getElementById('submitButton').addEventListener('click', function() { // 获取输入框中的文本 var message = document.getElementById('textInput').value; // 验证消息是否为空 if (message.trim() !== '') { // 通过Socket.IO发送消息给服务器 socket.emit('annotated_answer', message); // 清空输入框 document.getElementById('textInput').value = ''; } else { alert('Please enter a message.'); } }); </script> </body> </html>
用 gunicorn 部署
配置文件:
运行命令:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。