From 45fed857cb0dbb466ae45c24ba0b43e600847149 Mon Sep 17 00:00:00 2001 From: polyrabbit Date: Tue, 14 Nov 2023 22:49:56 +0800 Subject: [PATCH] Weixin no longer provides recognition field in voice message now --- .github/workflows/deploy.yml | 2 +- Dockerfile | 3 ++ WeCron/remind/models/remind.py | 10 +++- WeCron/wxhook/message_handler.py | 57 ++++++++++++++++++--- WeCron/wxhook/tests/test_message_handler.py | 17 ++++++ requirements.txt | 5 +- 6 files changed, 81 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5957f80..5995fe5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -44,7 +44,7 @@ jobs: docker-build-push: name: Docker Build runs-on: ubuntu-latest - needs: unit-test + # needs: unit-test # TODO: enable back after migrating from python 2.7 steps: - uses: actions/checkout@v3 - name: Set up Docker Buildx diff --git a/Dockerfile b/Dockerfile index 3d2176d..63305cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,9 @@ EXPOSE $VCAP_APP_PORT HEALTHCHECK CMD curl --fail http://localhost:$VCAP_APP_PORT/?healthy || exit 1 +RUN apt-get update +RUN apt-get install -y ffmpeg libavcodec-extra + COPY requirements.txt $WORKDIR RUN pip install --no-cache-dir -r requirements.txt diff --git a/WeCron/remind/models/remind.py b/WeCron/remind/models/remind.py index 46dd7b7..7378323 100644 --- a/WeCron/remind/models/remind.py +++ b/WeCron/remind/models/remind.py @@ -17,6 +17,7 @@ from common import wechat_client from remind.utils import nature_time from remind.signals import participant_modified +from wechatpy import WeChatException logger = logging.getLogger(__name__) @@ -141,15 +142,20 @@ def notify_user_by_id(self, uid): ('重复:' + self.get_repeat_text() + '\n') if self.has_repeat() else '', '详情' % self.get_absolute_url(True)) message_params['raw_text'] = raw_text - self.send_template_message_async(message_params, self.desc, name) + self.send_template_message_async(message_params, self.desc, user) @threads(10, timeout=60) - def send_template_message_async(self, message_params, desc, uname): + def send_template_message_async(self, message_params, desc, user): + uname = user.get_full_name() if 'raw_text' in message_params: try: res = wechat_client.message.send_text(message_params['user_id'], message_params['raw_text']) logger.info('Successfully send notification(%s) to user %s in text mode', desc, uname) return res + except WeChatException as e: + if e.errcode == 45015: + user.last_login = now() - timedelta(hours=49) + user.save(update_fields=['last_login']) except: logger.exception('Failed to send text notification(%s) to user %s', desc, uname) try: diff --git a/WeCron/wxhook/message_handler.py b/WeCron/wxhook/message_handler.py index 38d29ec..e6ca20c 100644 --- a/WeCron/wxhook/message_handler.py +++ b/WeCron/wxhook/message_handler.py @@ -1,7 +1,8 @@ -#coding: utf-8 +# coding: utf-8 from __future__ import unicode_literals, absolute_import import logging import json +import os import re from datetime import timedelta @@ -10,6 +11,7 @@ from wechatpy.replies import TextReply, TransferCustomerServiceReply, ImageReply from wechatpy.exceptions import WeChatClientException from shove import Shove +from pydub import AudioSegment from common import wechat_client from remind.models import Remind @@ -75,7 +77,7 @@ def handle_text(self, reminder=None): logger.exception('Semantic parse error') return self.text_reply( '\U0001F648抱歉,我还只是一个比较初级的定时机器人,理解不了您刚才所说的话:\n\n“%s”\n\n' - '或者您可以换个姿势告诉我该怎么定时,比如这样:\n\n' + '或者您可以换个姿势告诉我该怎么定时,比如这样:\n\n' '“两个星期后提醒我去复诊”。\n' '“周五晚上提醒我打电话给老妈”。\n' '“每月20号提醒我还信用卡[捂脸]”。' % self.message.content @@ -133,7 +135,7 @@ def handle_unsubscribe_event(self): def handle_unknown(self): return self.text_reply( - '/:jj如需设置提醒,只需用语音或文字告诉我就行了,比如这样:\n\n' + '/:jj如需设置提醒,只需用语音或文字告诉我就行了,比如这样:\n\n' '“两个星期后提醒我去复诊”。\n' '“周五晚上提醒我打电话给老妈”。\n' '“每月20号提醒我还信用卡[捂脸]”。' @@ -149,6 +151,9 @@ def handle_unknown_event(self): def handle_voice(self): self.message.content = getattr(self.message, 'recognition', '') if not self.message.content: + self.message.content = speech_to_text(getattr(self.message, 'media_id', '')) + if not self.message.content: + logger.info('No "recognition" field for media_id "%s" and speech_to_text returns nothing', getattr(self.message, 'media_id', 'NOT_EXIST')) return self.text_reply( '\U0001F648哎呀,看起来微信的语音转文字功能又双叒叕罬蝃抽风了,请重试一遍,或者直接发文字给我~' ) @@ -166,15 +171,15 @@ def handle_click_event(self): remind_text_list = self.format_remind_list(time_reminds) if remind_text_list: return self.text_reply('/:sunHi %s, 你今天的提醒有:\n\n%s' % (self.user.get_full_name(), - '\n'.join(remind_text_list))) + '\n'.join(remind_text_list))) return self.text_reply('/:coffee今天没有提醒,休息一下吧!') elif self.message.key.lower() == 'time_remind_tomorrow': - tomorrow = timezone.now()+timedelta(days=1) + tomorrow = timezone.now() + timedelta(days=1) time_reminds = self.user.get_time_reminds().filter(time__date=tomorrow).order_by('time').all() remind_text_list = self.format_remind_list(time_reminds, True) if remind_text_list: return self.text_reply('/:sunHi %s, 你明天的提醒有:\n\n%s' % (self.user.get_full_name(), - '\n'.join(remind_text_list))) + '\n'.join(remind_text_list))) return self.text_reply('/:coffee明天还没有提醒,休息一下吧!') elif self.message.key.lower() == 'customer_service': logger.info('Transfer to customer service for %s', self.user.get_full_name()) @@ -211,10 +216,10 @@ def format_remind_list(reminds, next_run_found=False): emoji = '\U0001F552' # Clock # takewhile is too aggressive if rem.time < now: - emoji = '\U00002713 ' # Done + emoji = '\U00002713 ' # Done elif not next_run_found: next_run_found = True - emoji = '\U0001F51C' # Soon + emoji = '\U0001F51C' # Soon remind_text_list.append('%s %s - %s' % (emoji, rem.local_time_string('G:i'), rem.get_absolute_url(True), rem.title())) return remind_text_list @@ -234,3 +239,39 @@ def handle_message(msg): # shove['last_msgid'] = msgid return resp_msg + +def speech_to_text(media_id): + if not media_id: + return None + media_url = wechat_client.media.get_url(media_id).replace('http://', 'https://') + media_resp = wechat_client.get(media_url) + if len(media_resp.content) == 0: + logger.warn('Failed to download media id %s', media_id) + return '' + + fname = 'audio.amr' + d = media_resp.headers['content-disposition'] + matches = re.findall("filename=(.+)", d) + if matches and len(matches) > 0: + fname = matches[0].strip('"') + fpath = '/tmp/' + fname + with open(fpath, 'wb') as f: + f.write(media_resp.content) + + try: + audio = AudioSegment.from_file(fpath) + out_file = audio.export(format='mp3') + mp3_content = out_file.read() + out_file.close() + finally: + os.remove(fpath) + + submit_url = 'https://api.weixin.qq.com/cgi-bin/media/voice/addvoicetorecofortext?access_token=%s&format=mp3&voice_id=%s' % (wechat_client.access_token, media_id) + submit_json = wechat_client.post(submit_url, files={fname: mp3_content}) + if submit_json.get('errcode') != 0 and submit_json.get('errcode') != '0': + logger.warn('Failed to submit media id %s: %s', media_id, submit_json) + return '' + + text_url = 'https://api.weixin.qq.com/cgi-bin/media/voice/queryrecoresultfortext?access_token=%s&voice_id=%s&lang=zh_CN' % (wechat_client.access_token, media_id) + text_json = wechat_client.post(text_url) + return text_json.get('result') diff --git a/WeCron/wxhook/tests/test_message_handler.py b/WeCron/wxhook/tests/test_message_handler.py index 7ef50d6..0f2739a 100644 --- a/WeCron/wxhook/tests/test_message_handler.py +++ b/WeCron/wxhook/tests/test_message_handler.py @@ -164,6 +164,23 @@ def test_voice_with_media_id(self): self.assertEqual(media_id, r.media_id) r.delete() + def test_speech_to_text(self): + media_id = 'aZCOJETyQl-PVZhye4aKMb6V89gmsiGX9sCyiWpAwjd9dvEYwXxgXAZY3G29nc2b' + req_text = """ + + + + 1357290913 + + + + 12345678901234565 + + """ % (media_id) + wechat_msg = self.build_wechat_msg(req_text) + resp_xml = handle_message(wechat_msg) + self.assertIn('重试', resp_xml) # Should have no exception + def test_video(self): req_text = """ diff --git a/requirements.txt b/requirements.txt index f7dfe5b..6891a93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ jieba==0.42.1 python_dateutil==2.6.0 shove==0.6.6 coverage -Pillow==4.3.0 +Pillow==6.2.2 py-lru-cache==0.1.4 -grip +grip==4.6.2 +pydub==0.25.1 \ No newline at end of file