示例#1
0
 def initDetector(self):
     if self.detector is not None:
         self.detector.terminate()
     models = [
         constants.getHotwordModel(config.get('hotword', 'wukong.pmdl')),
         constants.getHotwordModel(utils.get_do_not_bother_on_hotword()),
         constants.getHotwordModel(utils.get_do_not_bother_off_hotword())
     ]
     self.detector = snowboydecoder.HotwordDetector(models,
                                                    sensitivity=config.get(
                                                        'sensitivity', 0.5))
     # main loop
     try:
         self.detector.start(
             detected_callback=[
                 self._detected_callback, self._do_not_bother_on_callback,
                 self._do_not_bother_off_callback
             ],
             audio_recorder_callback=self._conversation.converse,
             interrupt_check=self._interrupt_callback,
             silent_count_threshold=config.get('silent_threshold', 15),
             recording_timeout=config.get('recording_timeout', 5) * 4,
             sleep_time=0.03)
         self.detector.terminate()
     except Exception as e:
         logger.critical('离线唤醒机制初始化失败:{}'.format(e))
示例#2
0
 def post(self):
     if self.validate(self.get_argument('validate', default=None)):
         qaStr = self.get_argument('qa')
         qaJson = os.path.join(constants.TEMP_PATH, 'qa_json')
         try:
             make_json.convert(qaStr, qaJson)
             solr_tools.clear_documents(config.get('/anyq/host', '0.0.0.0'),
                                        'collection1',
                                        config.get('/anyq/solr_port', '8900')
             )
             solr_tools.upload_documents(config.get('/anyq/host', '0.0.0.0'),
                                         'collection1',
                                         config.get('/anyq/solr_port', '8900'),
                                         qaJson,
                                         10
             )
             with open(constants.getQAPath(), 'w') as f:
                 f.write(qaStr)
             res = {'code': 0, 'message': 'ok'}
             self.write(json.dumps(res))
         except Exception as e:
             logger.error(e)
             res = {'code': 1, 'message': '提交失败,请检查内容'}
             self.write(json.dumps(res))
     else:
         res = {'code': 1, 'message': 'illegal visit'}
         self.write(json.dumps(res))
     self.finish()
示例#3
0
 def post(self):
     if self.validate(self.get_argument("validate", default=None)):
         qaStr = self.get_argument("qa")
         qaJson = os.path.join(constants.TEMP_PATH, "qa_json")
         try:
             make_json.convert(qaStr, qaJson)
             solr_tools.clear_documents(
                 config.get("/anyq/host", "0.0.0.0"),
                 "collection1",
                 config.get("/anyq/solr_port", "8900"),
             )
             solr_tools.upload_documents(
                 config.get("/anyq/host", "0.0.0.0"),
                 "collection1",
                 config.get("/anyq/solr_port", "8900"),
                 qaJson,
                 10,
             )
             with open(constants.getQAPath(), "w") as f:
                 f.write(qaStr)
             res = {"code": 0, "message": "ok"}
             self.write(json.dumps(res))
         except Exception as e:
             logger.error(e)
             res = {"code": 1, "message": "提交失败,请检查内容"}
             self.write(json.dumps(res))
     else:
         res = {"code": 1, "message": "illegal visit"}
         self.write(json.dumps(res))
     self.finish()
示例#4
0
    def run(self):
        self.init()
        models = [
            constants.getHotwordModel(config.get('hotword', 'wukong.pmdl')),
            constants.getHotwordModel(utils.get_do_not_bother_on_hotword()),
            constants.getHotwordModel(utils.get_do_not_bother_off_hotword())
        ]

        # capture SIGINT signal, e.g., Ctrl+C
        signal.signal(signal.SIGINT, self._signal_handler)
        detector = snowboydecoder.HotwordDetector(models,
                                                  sensitivity=config.get(
                                                      'sensitivity', 0.5))
        print('Listening... Press Ctrl+C to exit')

        # site
        server.run()

        # main loop
        detector.start(detected_callback=[
            self._detected_callback, self._do_not_bother_on_callback,
            self._do_not_bother_off_callback
        ],
                       audio_recorder_callback=self._conversation.converse,
                       interrupt_check=self._interrupt_callback,
                       silent_count_threshold=5,
                       sleep_time=0.03)
        detector.terminate()
示例#5
0
    def _start_osc(self):
        if not importlib.util.find_spec("pythonosc"):
            logger.critical("错误:请先安装 python-osc !")
            return

        from pythonosc import dispatcher as dsp
        from pythonosc import osc_server

        dispatcher = dsp.Dispatcher()
        dispatcher.map("/muse/elements/blink", self.blink_handler, "EEG")
        dispatcher.map("/muse/elements/jaw_clench", self.jaw_clench_handler,
                       "EEG")

        try:
            server = osc_server.ThreadingOSCUDPServer(
                (
                    config.get("/muse/ip", "127.0.0.1"),
                    int(config.get("/muse/port", "5001")),
                ),
                dispatcher,
            )
            logger.info("Muse serving on {}".format(server.server_address))
            server.serve_forever()
        except Exception as e:
            logger.error(e)
示例#6
0
 def activeListen(self, silent=False):
     """主动问一个问题(适用于多轮对话)"""
     if config.get("/LED/enable", False):
         LED.wakeup()
     logger.debug("activeListen")
     try:
         if not silent:
             Player.play(constants.getData("beep_hi.wav"))
         listener = snowboydecoder.ActiveListener([
             constants.getHotwordModel(config.get("hotword", "wukong.pmdl"))
         ])
         voice = listener.listen(
             silent_count_threshold=config.get("silent_threshold", 15),
             recording_timeout=config.get("recording_timeout", 5) * 4,
         )
         if not silent:
             Player.play(constants.getData("beep_lo.wav"))
         if voice:
             query = self.asr.transcribe(voice)
             utils.check_and_delete(voice)
             return query
         return ""
     except Exception as e:
         logger.error("主动聆听失败".format(e))
         return ""
示例#7
0
 def activeListen(self, silent=False):
     """ 主动问一个问题(适用于多轮对话) """
     if config.get('/LED/enable', False):
         LED.wakeup()
     logger.debug('activeListen')
     try:
         if not silent:
             #time.sleep(1)
             Player.play(constants.getData('beep_hi.wav'))
         listener = snowboydecoder.ActiveListener([
             constants.getHotwordModel(config.get('hotword', 'wukong.pmdl'))
         ])
         voice = listener.listen(
             silent_count_threshold=config.get('silent_threshold', 15),
             recording_timeout=config.get('recording_timeout', 5) * 4)
         if not silent:
             Player.play(constants.getData('beep_lo.wav'))
         if voice:
             query = self.asr.transcribe(voice)
             utils.check_and_delete(voice)
             return query
         return ''
     except Exception as e:
         logger.error(e)
         return ''
示例#8
0
    def doResponse(self, query, UUID='', onSay=None):
        statistic.report(1)
        self.interrupt()
        self.appendHistory(0, query, UUID)

        if config.get('/LED/enable', False):
            LED.think()

        if onSay:
            self.onSay = onSay

        if query.strip() == '':
            self.pardon()
            return

        lastImmersiveMode = self.immersiveMode

        if not self.brain.query(query):
            # 没命中技能,使用机器人回复
            msg = self.ai.chat(query)
            self.say(msg, True, onCompleted=self.checkRestore)
        else:
            if lastImmersiveMode is not None and lastImmersiveMode != self.matchPlugin:
                time.sleep(1)
                if self.player is not None and self.player.is_playing():
                    logger.debug('等说完再checkRestore')
                    self.player.appendOnCompleted(lambda: self.checkRestore())
                else:
                    logger.debug('checkRestore')
                    self.checkRestore()

        if config.get('/LED/enable', False):
            LED.off()
示例#9
0
    def initDetector(self):
        if self.detector is not None:
            self.detector.terminate()

        models = [
            constants.getHotwordModel(config.get('hotword', 'xiaofeng.pmdl'))
        ]

        # gql 检测唤醒词
        self.detector = snowboydecoder.HotwordDetector(models,
                                                       sensitivity=config.get(
                                                           'sensitivity', 0.5))

        # main loop
        try:
            callbacks = self._detected_callback

            self.detector.start(
                detected_callback=callbacks,
                audio_recorder_callback=self._conversation.converse,
                interrupt_check=self._interrupt_callback,
                silent_count_threshold=config.get('silent_threshold', 15),
                recording_timeout=config.get('recording_timeout', 5) * 4,
                sleep_time=0.03)
            self.detector.terminate()
        except Exception as e:
            print("error")
            logger.critical('离线唤醒机制初始化失败:{}'.format(e))
示例#10
0
    def init(self):
        global conversation
        self.detector = None
        self._interrupted = False        
        print('''
********************************************************
*          wukong-robot - 中文语音对话机器人           *
*          (c) 2019 潘伟洲 <*****@*****.**>              *
*     https://github.com/wzpan/wukong-robot.git        *
********************************************************

            后台管理端:http://{}:{}
            如需退出,可以按 Ctrl-4 组合键

'''.format(config.get('/server/host', '0.0.0.0'), config.get('/server/port', '5000')))
        config.init()
        self._conversation = Conversation(self._profiling)
        self._conversation.say('{} 你好!试试对我喊唤醒词叫醒我吧'.format(config.get('first_name', '主人')), True)
        self._observer = Observer()
        event_handler = ConfigMonitor(self._conversation)
        self._observer.schedule(event_handler, constants.CONFIG_PATH, False)
        self._observer.schedule(event_handler, constants.DATA_PATH, False)
        self._observer.start()
        if config.get('/muse/enable', False):
            self._wakeup = multiprocessing.Event()
            self.bci = BCI.MuseBCI(self._wakeup)
            self.bci.start()
            thread.start_new_thread(self._loop_event, ())
示例#11
0
 def post(self):
     if self.get_argument('username') == config.get('/server/username') and \
        hashlib.md5(self.get_argument('password').encode('utf-8')).hexdigest() \
        == config.get('/server/validate'):
         self.set_cookie("validation", config.get('/server/validate'))
         self.redirect("/")
     else:
         self.render('login.html', error="登录失败")
示例#12
0
 def reload(self):
     """ 重新初始化 """
     try:
         self.asr = ASR.get_engine_by_slug(config.get('asr_engine', 'tencent-asr'))
         self.ai = AI.get_robot_by_slug(config.get('robot', 'tuling'))
         self.tts = TTS.get_engine_by_slug(config.get('tts_engine', 'baidu-tts'))
     except Exception as e:
         logger.critical("对话初始化失败:{}".format(e))
示例#13
0
 def get_config(self):
     appid = config.get('/emotibot/appid', '')
     location = config.get('location', '深圳')
     more = config.get('active_mode', False)        
     return {
         'appid': appid,
         'location': location,
         'more': more
     }
示例#14
0
def wakeup():
    if config.get('/LED/enable', False):
        if config.get('/LED/type') == 'aiy':
            thread.start_new_thread(aiy.wakeup, ())
        elif config.get('/LED/type') == 'respeaker':
            from robot.drivers.pixels import pixels
            pixels.wakeup()
        else:
            logger.error('错误:不支持的灯光类型')
示例#15
0
def start_server():
    if config.get('/server/enable', False):
        host = config.get('/server/host', '0.0.0.0')
        port = config.get('/server/port', '5000')
        try:
            asyncio.set_event_loop(asyncio.new_event_loop())
            application.listen(int(port))
            tornado.ioloop.IOLoop.instance().start()
        except Exception as e:
            logger.critical('服务器启动失败: {}'.format(e))
示例#16
0
def wakeup():
    if config.get("/LED/enable", False):
        if config.get("/LED/type") == "aiy":
            thread.start_new_thread(aiy.wakeup, ())
        elif config.get("/LED/type") == "respeaker":
            from robot.drivers.pixels import pixels

            pixels.wakeup()
        else:
            logger.error("错误:不支持的灯光类型")
示例#17
0
 def post(self):
     if self.get_argument("username") == config.get(
             "/server/username") and hashlib.md5(
                 self.get_argument("password").encode("utf-8")).hexdigest(
                 ) == config.get("/server/validate"):
         print("success")
         self.set_secure_cookie("validation",
                                config.get("/server/validate"))
         self.redirect("/")
     else:
         self.render("login.html", error="登录失败")
示例#18
0
 def reload(self):
     """ 重新初始化 """
     try:
         self.asr = ASR.get_engine_by_slug(config.get('asr_engine', 'tencent-asr'))
         self.ai = AI.get_robot_by_slug(config.get('robot', 'tuling'))
         self.tts = TTS.get_engine_by_slug(config.get('tts_engine', 'baidu-tts'))
         self.nlu = NLU.get_engine_by_slug(config.get('nlu_engine', 'unit'))
         self.player = None
         self.brain = Brain(self)
         self.brain.printPlugins()
     except Exception as e:
         logger.critical("对话初始化失败:{}".format(e))
示例#19
0
def start_server(con, wk):
    global conversation, wukong
    conversation = con
    wukong = wk
    if config.get('/server/enable', False):
        port = config.get('/server/port', '5000')
        try:
            asyncio.set_event_loop(asyncio.new_event_loop())
            application.listen(int(port))
            tornado.ioloop.IOLoop.instance().start()
        except Exception as e:
            logger.critical('服务器启动失败: {}'.format(e))
示例#20
0
    def say(self, msg, cache=False, plugin='', onCompleted=None, wait=False):
        """ 
        说一句话
        :param msg: 内容
        :param cache: 是否缓存这句话的音频
        :param plugin: 来自哪个插件的消息(将带上插件的说明)
        :param onCompleted: 完成的回调
        :param wait: 是否要等待说完(为True将阻塞主线程直至说完这句话)
        """
        if plugin != '':
            self.appendHistory(1, "[{}] {}".format(plugin, msg))
        else:
            self.appendHistory(1, msg)
        pattern = r'^https?://.+'
        if re.match(pattern, msg):
            logger.info("内容包含URL,所以不读出来")
            return
        voice = ''
        cache_path = ''
        if utils.getCache(msg):
            logger.info("命中缓存,播放缓存语音")
            if config.get('/tts_engine') == 'hass-tts':
                voice = self.tts.get_speech(msg)
            else:
                voice = utils.getCache(msg)
                cache_path = utils.getCache(msg)
        else:
            try:
                voice = self.tts.get_speech(msg)
                if voice != '':
                    cache_path = utils.saveCache(voice, msg)
            except Exception as e:
                logger.error('保存缓存失败:{}'.format(e))
        if self.onSay:
            logger.info(cache)
            audio = 'http://{}:{}/audio/{}'.format(
                config.get('/server/host'), config.get('/server/port'),
                os.path.basename(cache_path))
            logger.info('onSay: {}, {}'.format(msg, audio))
            if plugin != '':
                self.onSay("[{}] {}".format(plugin, msg), audio)
            else:
                self.onSay(msg, audio)
            self.onSay = None

        if onCompleted is None:
            onCompleted = lambda: self._onCompleted(msg)
        self.player = Player.SoxPlayer()
        self.player.play(voice, not cache, onCompleted, wait)
        if not cache:
            utils.check_and_delete(cache_path, 60)  # 60秒后将自动清理不缓存的音频
        utils.lruCache()  # 清理缓存
示例#21
0
 def activeListen(self):
     """ 主动问一个问题(适用于多轮对话) """
     time.sleep(1)
     Player.play(constants.getData('beep_hi.wav'))
     listener = snowboydecoder.ActiveListener(
         [constants.getHotwordModel(config.get('hotword', 'wukong.pmdl'))])
     voice = listener.listen(
         silent_count_threshold=config.get('silent_threshold', 15),
         recording_timeout=config.get('recording_timeout', 5) * 4)
     Player.play(constants.getData('beep_lo.wav'))
     query = self.asr.transcribe(voice)
     utils.check_and_delete(voice)
     return query
示例#22
0
    def handle(self, query):
        province = config.get('/Covid/province')
        if '疫情情况' in query:
            logger.info('命中 <疫情情况> 插件')
            try:
                # 所在省份的患病相关人数情况
                url_num = 'https://lab.isaaclin.cn/nCoV/api/area?latest=1&province={}'.format(
                    config.get('/Covid/province'))
                data_num = requests.get(url=url_num)
                # 累计确诊人数
                confirmedCount = data_num.json(
                )["results"][0]['confirmedCount']
                # 治愈人数
                curedCount = data_num.json()["results"][0]['curedCount']
                updateTime = (
                    data_num.json()["results"][0]['updateTime']) / 1000
                date = time.localtime(updateTime)  # 利用localtime()转换为时间数组
                format_date = time.strftime('%Y-%m-%d %H:%M:%S',
                                            date)  # 利用strftime()将其格式化为需要的格式

                res = '截止' + str(
                    format_date) + ',' + province + '累计确诊人数' + str(
                        confirmedCount) + '例,' + '治愈人数' + str(curedCount) + '例'
                self.con.say(res, True)
                logger.info(res)
            except Exception as e:
                logger.error(e)
                self.con.say("疫情情况查询失败了!", True)

        elif '疫情新闻' in query:
            logger.info('命中 <疫情新闻> 插件')
            try:
                # 这里由于处过重点省份外,其他省份新闻的信息性不高,所以采用全国新闻
                # 全国性质的新闻
                url_msg = 'http://lab.isaaclin.cn/nCoV/api/overall'
                msgs = requests.get(url=url_msg)
                # 新闻的标题(考虑到内容过长,影响体验,使用标题进行播报)
                msg = msgs.json()["results"][0]["generalRemark"]
                uptime = (msgs.json()["results"][0]["updateTime"]) / 1000
                date = time.localtime(uptime)  # 利用localtime()转换为时间数组
                format_date = time.strftime('%Y-%m-%d ',
                                            date)  # 利用strftime()将其格式化为需要的格式
                msgs = format_date + '新闻:' + msg
                self.con.say(msgs, True)
                logger.info(msgs)
            except Exception as e:
                logger.error(e)
                self.con.say("疫情新闻查询失败了!", True)
        else:
            logger.error('疫情情况获取失败了!')
            self.con.say("疫情情况获取失败了!", True)
示例#23
0
 def reInit(self):
     """重新初始化"""
     try:
         self.asr = ASR.get_engine_by_slug(
             config.get("asr_engine", "tencent-asr"))
         self.ai = AI.get_robot_by_slug(config.get("robot", "tuling"))
         self.tts = TTS.get_engine_by_slug(
             config.get("tts_engine", "baidu-tts"))
         self.nlu = NLU.get_engine_by_slug(config.get("nlu_engine", "unit"))
         self.player = None
         self.brain = Brain(self)
         self.brain.printPlugins()
     except Exception as e:
         logger.critical("对话初始化失败:{}".format(e))
示例#24
0
    def query(self, text):
        """
        query 模块

        Arguments:
        text -- 用户输入
        """

        args = {
            "service_id":
            config.get("/unit/service_id", "S13442"),
            "api_key":
            config.get("/unit/api_key", "w5v7gUV3iPGsGntcM84PtOOM"),
            "secret_key":
            config.get("/unit/secret_key", "KffXwW6E1alcGplcabcNs63Li6GvvnfL"),
        }
        parsed = self.conversation.doParse(text, **args)

        for plugin in self.plugins:
            if not self.isValid(plugin, text, parsed) and not self.isImmersive(
                    plugin, text, parsed):
                continue

            logger.info("'{}' 命中技能 {}".format(text, plugin.SLUG))
            self.conversation.matchPlugin = plugin.SLUG

            if plugin.IS_IMMERSIVE:
                self.conversation.setImmersiveMode(plugin.SLUG)

            continueHandle = False
            try:
                self.handling = True
                continueHandle = plugin.handle(text, parsed)
                self.handling = False
            except Exception:
                logger.critical("Failed to execute plugin", exc_info=True)
                reply = "抱歉,插件{}出故障了,晚点再试试吧".format(plugin.SLUG)
                self.conversation.say(reply, plugin=plugin.SLUG)
            else:
                logger.debug(
                    "Handling of phrase '%s' by " + "plugin '%s' completed",
                    text,
                    plugin.SLUG,
                )
            finally:
                if not continueHandle:
                    return True

        logger.debug("No plugin was able to handle phrase {} ".format(text))
        return False
示例#25
0
    def say(self,
            msg,
            cache=False,
            plugin='',
            onCompleted=None,
            wait=False,
            resident=False):
        """
        说一句话
        :param resident:
        :param msg: 内容
        :param cache: 是否缓存这句话的音频
        :param plugin: 来自哪个插件的消息(将带上插件的说明)
        :param onCompleted: 完成的回调
        :param wait: 是否要等待说完(为True将阻塞主线程直至说完这句话)
        """
        self.appendHistory(1, msg, plugin=plugin)
        if re.match(r'^https?://.+', msg):
            logger.info("内容包含URL,所以不读出来")
            self.onSay(msg, '', plugin=plugin)
            self.onSay = None
            return
        voice = utils.getCache(msg, resident=resident)
        cache_path = ''
        if voice:
            logger.info("命中缓存,播放缓存语音")
            cache_path = voice
        else:
            try:
                voice = self.tts.get_speech(msg, resident=resident)
                cache_path = utils.saveCache(voice, msg, resident=resident)
            except Exception as e:
                logger.error('保存缓存失败:{}'.format(e))
        if self.onSay:
            logger.info(cache)
            audio = 'http://{}:{}/audio/{}'.format(
                config.get('/server/host'), config.get('/server/port'),
                os.path.basename(cache_path))
            logger.info('onSay: {}, {}'.format(msg, audio))
            self.onSay(msg, audio, plugin=plugin)
            self.onSay = None
        if onCompleted is None:

            def onCompleted():
                self._onCompleted(msg)

        self.player = Player.getPlayerByFileName(voice)
        self.player.play(voice, not cache, onCompleted, wait)
        utils.lruCache()  # 清理缓存
示例#26
0
 def isValidImmersive(self, plugin, text, parsed):
     patterns = config.get("/{}/patterns".format(plugin.SLUG), [])
     if len(patterns) > 0:
         return plugin.isValidImmersive(text, parsed) or self.match(
             patterns, text)
     else:
         return plugin.isValidImmersive(text, parsed)
示例#27
0
 def train(self, w1, w2, w3, m):
     '''
     传入三个wav文件,生成snowboy的.pmdl模型
     '''
     def get_wave(fname):
         with open(fname, 'rb') as infile:
             return base64.b64encode(infile.read()).decode('utf-8')
     url = 'https://snowboy.kitt.ai/api/v1/train/'
     data = {
         "name": "wukong-robot",
         "language": "zh",
         "token": config.get('snowboy_token', ''),
         "voice_samples": [
             {"wave": get_wave(w1)},
             {"wave": get_wave(w2)},
             {"wave": get_wave(w3)}
         ]
     }
     response = requests.post(url, json=data)
     if response.ok:
         with open(m, "wb") as outfile:
             outfile.write(response.content)
         return 'Snowboy模型已保存至{}'.format(m)
     else:
         return "Snowboy模型生成失败,原因:{}".format(response.text)
示例#28
0
 def upload(self, threadNum=10):
     """
     手动上传 QA 集语料,重建 solr 索引
     """
     try:
         qaJson = os.path.join(constants.TEMP_PATH, 'qa_json')
         make_json.run(constants.getQAPath(), qaJson)
         solr_tools.clear_documents(config.get('/anyq/host', '0.0.0.0'),
                                    'collection1',
                                    config.get('/anyq/solr_port', '8900'))
         solr_tools.upload_documents(config.get('/anyq/host',
                                                '0.0.0.0'), 'collection1',
                                     config.get('/anyq/solr_port', '8900'),
                                     qaJson, threadNum)
     except Exception as e:
         logger.error("上传失败:{}".format(e))
示例#29
0
    def init(self):
        global conversation
        self.detector = None
        self._interrupted = False
        print('''
********************************************************
*          wukong-robot - 中文语音对话机器人           *
*          (c) 2019 潘伟洲 <*****@*****.**>              *
*     https://github.com/wzpan/wukong-robot.git        *
********************************************************

            如需退出,可以按 Ctrl-4 组合键。

''')

        config.init()
        self._i2c = I2c()
        self._i2c.start()
        self._conversation = Conversation(self._i2c, self._profiling)
        self._conversation.say(
            '{} 你好!试试对我喊唤醒词叫醒我吧'.format(config.get('first_name', '主人')), True)
        self._observer = Observer()
        event_handler = ConfigMonitor(self._conversation)
        self._observer.schedule(event_handler, constants.CONFIG_PATH, False)
        self._observer.schedule(event_handler, constants.DATA_PATH, False)
        self._observer.start()
示例#30
0
    def getSubject(self, msg):
        """
            Returns the title of an email

            Arguments:
            msg -- the email

            Returns:
            Title of the email.
        """
        subject = email.header.decode_header(msg['subject'])
        if isinstance(subject[0][0], bytes):
            try:
                sub = subject[0][0].decode('utf-8')
            except UnicodeDecodeError:
                sub = subject[0][0].decode('gbk')
        else:
            sub = subject[0][0]
        to_read = False
        if sub.strip() == '':
            return ''
        to_read = config.get('/email/read_email_title', True)
        if to_read:
            return '邮件标题为 %s' % sub
        return ''