huajianmo / multimediaChatbot

构建了可用于多个对话框架的场景型对话的聊天机器人。支持吐槽类情景及情感倾诉型情景。使用wechaty,结合Paddlehub的人脸特征点、动漫人脸生成、对联生成、情感分类,ernie迁移,人脸变胖

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Paddlehub+Wechaty微信聊天机器人--多轮对话吐槽,emoi及打肿脸视频动态实时生成并回复

Watch the video

Watch the video

项目描述

构建了可用于多个对话框架的场景型对话的聊天机器人。支持吐槽类情景及情感倾诉型情景。使用wechaty,结合Paddlehub的人脸特征点、动漫人脸生成、对联生成、情感分类,ernie迁移,人脸变胖

项目结构

-|data
-|resource
-README.MD

使用方式

A:在AI Studio的脚本任务上运行本项目 B:在配置比较好的服务器上运行

吐槽树洞——让机器人陪你一起吐槽、倾诉

效果可见b站视频(建议右键新建窗口打开): b站视频

A.摘要

1.功能简介

本项目构建了一个用于场景型多轮对话,这里可用于让人们吐槽(图嘈)发泄,以及情感倾诉。根据对话内容,分析对话者是情感倾诉还是要吐槽发泄,会进入多轮对话文本、爬虫获取信息、卡通人脸生成、吐槽对称句生成、变胖人脸生成、情话生成、动态个性化表情动图生成、视频合成等多模块组合到一个聊天模块下,可方便根据聊天情景快速切换到自己的场景使用

本项目中对话为特定情景下的引导性对话,非开放性闲聊对话。但多资源反馈部分,也可以接入开放性闲聊对话,增加对话乐趣。

2.整体流程

序号 内容 简述
1 经Wechaty获取用户输入 定义用户输入的内容,进行触发,并判断用户输入内容是图片还是文本进行encode给到机器人框架
2 基于机器人框架获取用户信息并获取回复内容 根据设定的多轮逻辑,及当前用户信息及回复阶段,获得应回复的内容
3 基于回复内容调用各技能模块 回复逻辑已预设好回复的多媒体类别,进行decode后调用各模块
4 各生成模块生成文件或文本 各模块的模型根据当前的用户信息,生成动图、视频、文本等个性化回复内容。各模块见A-3
5 机器人框架载入这些文件或文本 进行汇总,可以一次回复包含多个或多类别内容
6 通过Wechaty回复用户 真实发到微信上

补充说明:

1 botProces中的bot_manager使用类似“简易工厂模式”。(熟悉工厂模式的筒子可以忽略本段)。每一个触发聊天的用户都会生成一个user_bot,用户的输入就好像工厂里面的原材料,经过bot_manager分配到各个工序的工人(各个技能模块,如:卡通人脸生成、爬虫、变胖人脸等)进行处理,最终组装好的产品给到用户。不同用户的输入就像不同的原材料,不断送进工厂处理,流水的bot铁打不变的bot_manager,而每个user_bot装载的是整个聊天过程中的所有对话。以上纯属个人胡扯,工厂模式正规解释具体见:https://www.cnblogs.com/wly923/archive/2013/05/10/3068313.html

2 talkProcess中会把botProcess中返回的对话内容,“翻译”成真正发给用户的内容。例如:是文本的直接返回,要生成cartoon图的将调用卡通图生成模块生成,要生成被打肿了的脸的则调对应的模块生成视频,然后把资源地址给到botmanger,botManger再读取文件发动图、表情gif或视频给用户。

3.各模块单独效果及简介

调用Paddlehub来丰富chatbot的技能,直接使用或自己的算法结合Paddlehub模型来使用:

序号 模块 简介 效果 主要脚本 调用的模型
3.1 情感分类
模块
获取用户输入内容的情感倾向及程度(置信度) emotionModule.py 结合emotion_detection_textcnn
模型
3.2 漫画人脸
生成
调整图片尺寸或增加图片的纹理复杂度,
中值滤波,再卡通生成(不然脸上会很花)
cartonModule.py 结合AnimeGan模型
3.3 对偶句生成 根据用户的吐槽语句,生成吐槽的语句 coupletModule.py ernie_gen_couplet/ernie_gen
模型
3.4 情话生成 根据用户的倾诉,生成一些情话,安抚用户 loveModule.py ernie_gen_lover_words模型
3.5 人脸特征点 在多个人脸变形或口型或换脸中使用 landmarkModule.py face_landmark_localization模型
3.6 百度爬虫 爬取用户想要吐槽对象的图片或相关信息 crawlerModule.py 自己弄
3.7 人脸变胖 使用类似瘦脸的反向算法,让人变成胖子 faceFatModule.py 自己弄
3.8 人脸五官迁
移到动态
表情包
1.下载网上一些表情,用Paddlehub的ocr截
取文字内容及区域
2.提取吐槽对象的人脸的五官并得到二值
化的黑白五官
3.该五官与表情包原来的五官进行对齐,
并替换原来的五官
4.基于新的五官生成闭嘴的图,与原开口的图
组合起来成gif或视频
5.在原来文字的位置写上新的吐槽语句
emoiModule.py 自己弄
3.9 视频/动图
生成
使用类似瘦脸的反向算法,让人变成胖子 movieModule.py 自己弄
3.10 词向量匹配 1.我收集了吐槽大会的几百句经典语录。
2.对经典吐槽语句的句子的n系列词性的词组
建立句子vector向量库
3.对用户的输入句子进行相似计算,
匹配相似度高的经典吐槽语录。
还记得你之前跟我说,我能及格猪都能上树。
可教练我拿到驾照了,猪不仅能上树还能上高速
,我再给你来个倒车入库,压线了难受不?
以前我不敢顶撞你,现在顶撞你可抗不住。
roastModule.py w2v_weibo_target_word-word_
dim300词向量
3.11 微信机器人
框架
现在唯一还可用的微信聊天机器人框架Wechaty run_bot.py Wechaty

B. 主要功能脚本简述(只介绍部分代码,具体请参见项目内脚本)

1. botclass.py 定义的每个聊天对象:

class imgContainer():
    def __init__(self):
        self.landmarks=[]#用户发的原始图的人脸特征点
        self.imgPath='' #用户发的图的路径
        # self.cartoonImgPath=[]
        self.roiImg=[] #用户发的图切出人脸附近区域
        self.roiCartoon=[] # 人脸附近区域进行漫画话
        self.specialImg=[] #进行特殊处理的图,如:吐槽是打脸的图
        self.filtImg=[] #
        self.roiLandmarks=[] #变成emoi表情时,进行人脸对齐时的人脸特征点
        self.emoi=[] #生成的emoi表情图片
        self.emoiWordsArea=[] #emoi的文字所在区域
        self.emoiMoviePath=''#emoi输出的视频或gif的地址
class bot(imgContainer):
    def __init__(self):
        self.param_init()
        self.user_init()
    def user_init(self):
        self.userName = ''#用户微信名称,暂时作为唯一id
    def param_init(self):
        super(bot,self).__init__()
        self.imgbot=imgContainer()
        self.client=""
        self.inTime=0 #进入时间
        self.queryList=[] #用户说的话
        self.replys=[] #每次回复,回复用户的内容(列表)
        self.replys_index=0 #回复用户的话回复到第几部分
        self.talkPic=[] 

        self.step=0#当前step
        self.stepRecord=[]#经历过的step
        self.emoition=''#情感类别
        self.emoitionRatio=0#情感强度
        self.abstract=''#用户对吐槽物的概述
        self.description=[]#用户对吐槽物的进一步描述
        self.ganImgPath=[]# 生成的图片路径
        self.transImgPath=[] #进行处理后图片路径
        self.ganTextList=[] #生成的文本列表
        self.moviePath='' #生成的视频地址

2. botProcess.py 定义的每个聊天对象:

后面有时间或者大家反响好就更换成状态机。有接触过游戏引擎的筒子都应该了解了,能实现多状态切换。之前实现了一套,在xmind编辑流程图,输出后作为chatbot不同主题的流程剧本,可以有多主题以及不同主题间跳转。

from collections import deque
import botClass
import time
import cv2
# from emoiProcess import face_drawer
# from morphImg import face_movie
import numpy as np


class botManger():
    def __init__(self,maxuser):
        self.userBotDict={}
        self.userTimeDict={}
        self.maxuser=maxuser
        self.splitNum=5
        self.loopLimit=4
        self.rightWords=['对','是','没错','嗯','yes','Yes','YES','right','Right']
        self.falseWords=['不','没有','No','no','NO']
        self.joinWords=['吐槽','图嘈','倾诉','树洞','心事']
        self.replyList=["""”图槽树洞“  这里是一个你可以倾诉对某人的心事,发泄吐槽某人的地方。我是一个机器人,你不用担心会泄漏出去。
           你只需要发你想向他/她倾诉的人或要吐槽的人或物的照片或名字就可以开始了""",
           "88~~ 主人我没空 不跟你玩了"]
    def createBot(self,username,content):
        print('bot process create', username)
        self.userBotDict[username]=botClass.bot()
        self.userBotDict[username].userName=username
        return self.updateBot(username,content)
    def updateBot(self,username,content):
        print('bot process update', username,content)
        # print('botprocess self.userTimeDict[username]',self.userTimeDict)
        self.userTimeDict[username]=time.time()
        self.userBotDict[username].queryList.append(content)
        # print('botprocess self.userTimeDict[username]',self.userTimeDict)
        ##
        return self.talk(username,content)
    def get_target_img(self,username,content,step):
        self.userBotDict[username].step = step

        if content[:self.splitNum]=='#pic#':
            try:
                # time.sleep(0.2)
                self.userBotDict[username].imgPath = content[self.splitNum:]
                # print('img----', self.userBotDict[username].img.shape)
            except Exception as e:
                pass
            # print('du tu', e)
            # time.sleep(1)
            # self.userBotDict[username].img = cv2.imread(content[self.splitNum:])
        replys=['#str#'+username+' 请一两句话概况你想对他/它说的话']

        self.userBotDict[username].replys_index = len(replys)
        self.userBotDict[username].replys.append(replys)
        return self.userBotDict[username]
    def TrueFalse(self,words):
        for rw in self.rightWords:
            if rw in words:
                for fw in self.falseWords:
                    if fw in words:
                        return False
                return True
        return False

    def talk(self,username,content):
        ##########跳出死循环
        if len(self.userBotDict[username].stepRecord)>=self.loopLimit:
            if sum(self.userBotDict[username].stepRecord[-1*self.loopLimit:])== \
                    self.userBotDict[username].stepRecord[-1]**self.loopLimit:
                self.userBotDict[username].step =100
        ##########"""这部分后面用状态机替换掉就不贴出来了,可以源文件里看"""
    def removeBot(self,dictKey):
        print('bot process remove',dictKey)
        del self.userTimeDict[dictKey]
        del self.userBotDict[dictKey]
    def getBotList(self):
        return self.userBotDict
    def run(self,username,content):
        if username in self.userBotDict.keys():
            print('botprocess already user;',self.userBotDict[username])
            return self.updateBot(username,content)
        else:
            for word in self.joinWords:
                if word in content:
                    print('join word',word)
                    if len(self.userBotDict)>self.maxuser:
                        oldest=min(list(self.userTimeDict.values()))
                        inList = list(self.userTimeDict.keys())[list(self.userTimeDict.values()).index(oldest)]
                        self.removeBot(inList)

                    return self.createBot(username, content)

        return None
if __name__ == "__main__":
    from talkProcess import talkManger
    tm=talkManger()

    bm=botManger(2)
    from testTemplate import *

    inputsList=inputsList1
    for inputs in inputsList:
        print('inputs[1]',inputs[1])
        re=bm.run(inputs[0],inputs[1])

        for i in range(re.replys_index):
            re,rdict=tm.run(re)
            print(rdict)

3. talkprocess.py 实现多种形式的对话(支持动图、图片、文本、视频):

结合返回内容的包头,是#str# #emo# #mov#来进行各种爬取图片、文本生成、图片生成、表情包生成、视频生成、卡通化、打肿的人脸等的生成

import paddlehub as hub
from loveModule import love_module
from emoiModule import emoi_movie_module
from movieModule import movie_maker
from emotionModule import emotion_module
from crawlerModule import pic_crawler
from  cartonModule import cartoon_face
from landmarkModule import landmarker
from coupletModule import couplet_module
import time
import cv2
import numpy as np
class talkManger():
    def __init__(self,outPath,tempPicPath,emoi_folder='data/emoi/',debug=False):
        self.splitNum=5
        self.love_module=love_module
        self.couplet_module=couplet_module
        self.landmarker=landmarker()
        self.cartoon_face=cartoon_face(self.landmarker)

        self.emoi_movie_maker=emoi_movie_module(emoi_folder, outPath, self.landmarker, debug)


        self.movie_maker=movie_maker(outPath,self.landmarker)
        self.emotion_module=emotion_module
        self.crawl_module=pic_crawler(tempPicPath)
        self.outPath=outPath
    def run(self,userDictBot,packageList=[],nums=5):
        # print('userDictBot',userDictBot)
        if len(packageList)==0:packageList=userDictBot.replys[-1]
        # print('packageList',packageList)
        package = packageList[len(packageList)-userDictBot.replys_index]
        # print('package',package)
        userDictBot.replys_index-=1
        # print('package[:self.splitNum]',package[:self.splitNum])
        if package[:self.splitNum]=='#str#':
            print({'str':package[self.splitNum:]})
            return userDictBot,{'str':package[self.splitNum:]}
        elif package[:self.splitNum]=='#cra#':
            result=self.crawl_module.run(package[self.splitNum:])
            userDictBot.imgPath=result[0]
            return userDictBot,{'pic':userDictBot.imgPath}

        elif package[:self.splitNum]=='#cla#':
            result =self.emotion_module.emotion_classify(texts=[package[self.splitNum:]])[0]
            print('emotion result:',result)
            if result['positive_probs']>result['negative_probs']:

                userDictBot.emoition='positive'
                userDictBot.emoitionRatio = result['positive_probs']
            else:

                userDictBot.emoition='negative'
                userDictBot.emoitionRatio = result['neutral_probs']


            return userDictBot,{}
        elif package[:self.splitNum] == '#gst#':
            inputs=eval('userDictBot.'+package[self.splitNum:])
            # if package[self.splitNum:]=='abstract':
            #     inputs=userDictBot.abstract
            # elif package[self.splitNum:]=='description':
            #     inputs = userDictBot.description

            if userDictBot.emoition=='positive' :

                result =self.love_module.generate(texts=[inputs],  beam_width=nums)[0][np.random.randint(nums)]

            else:
                result=self.couplet_module.generate(texts=[inputs],  beam_width=nums)[0][np.random.randint(nums)]

            userDictBot.ganTextList.append(result)
            return userDictBot, {'str': result}
        elif package[:self.splitNum] == '#gim#':
            if userDictBot.emoition=='positive' :
                emotion_flag=1
            else:
                emotion_flag = 2
            userDictBot=self.cartoon_face.run(userDictBot ,emotion_flag)
            path=self.outPath+str(time.time())+'.jpg'
            cv2.imwrite(path,userDictBot.roiCartoon)
            userDictBot.cartoonImgPath=path
            return userDictBot,{'pic':path}

        elif package[:self.splitNum] == '#emo#':
            inputs = eval('userDictBot.' + package[self.splitNum:])
            if userDictBot.emoition == 'positive':
                emotion_flag=1
            else:
                emotion_flag=2
            print('emo inputs',[inputs],emotion_flag)

            userDictBot = self.emoi_movie_maker.run(userDictBot,[inputs],emotion_flag)
            # userDictBot.emoiMoviePath=result
            return userDictBot, {'mov': userDictBot.emoiMoviePath}

        elif package[:self.splitNum] == '#mov#':
            if userDictBot.emoition == 'positive' or userDictBot.emoition == 'neutral':
                emotion_flag=1
            else:
                emotion_flag = 2
            userDictBot=self.movie_maker.run(userDictBot,emotion_flag)
            return userDictBot, {'mov': userDictBot.moviePath}

4.动态生成特定人脸的表情包 emoiModule.py

  1. 需要下载广大网友的表情包,运行表情包处理的代码,筛选出有一定文字长度及能识别到人脸的表情包。识别“文字位置”及“人脸特征点位置”,然后备着后面用(下图为原emoi图)

  1. 对用户发上来的人脸,识别人脸特征点,并框选特征点内所在的矩形(比特征点所在区域大及像素,效果会好些)。对该区域进行二值化等图像处理,提取出黑白的人脸五官图

  2. 把人脸五官的黑白图的五官通过本项目中的CVTools.py里面的morph脚本与表情包的人脸进行角度朝向位置的对齐。

  1. 用cv2.seamlessclone进行五官融合得到表情图a

  1. 生成根据人脸特征点生成一个闭嘴的图片,并融合到表情图a上,生成一张新的表情图b。下图为闭嘴图片,就是那一横(因这种是黑白emoi,不用很细致,直接opencv画出闭嘴的,不进行morph了)

  1. 序列插入图片,有闭嘴的图有原来的图间或组合在一起形成口在动。并在表情包原来打字的位置,重新用PIL库写上字(用字库会好看一些)

  2. 用emoi_movie_module 调用imageio或moviepy生成gif或视频mp4

5. 被打胖视频movieModule.py

1.首先,生成人脸变胖的图片:

——1.1 对用户发的图像进行人脸识别,获取人脸特征点

——1.2 使用类似瘦脸算法反向胖脸,参数可以根据之前emotion的检测到的强度来调节。卡通图真人图都可以,关键前一步拿较好的图片来识别人脸特征点。

——1.3 与几张手掌的png进行前后景融合,生成掌刮的图

——1.4利用上述生成的图片,用imageio或moviepy生成gif或视频mp4

6 人脸动漫化

AnimeGan可能训练的数据中,人脸特写的比较少。当遇到人脸特写或人脸所占像素较大时,且人脸光影比较明显时,会生成的人脸上很多一道道的线条。(像某个生成的李云龙那样)

所以,需要对图片进行预处理再生成,下面左图为直接生成,右图为缩小人脸分辨率,并进行磨皮,之后再加背景图增加纹理后再生成

增加纹路的操作如下: 把图贴到复杂的背景图上去生成漫画化,最后再截取原来我们需要的部分

但本项目中,无需这么复杂,只需:

1.截取人脸附近位置

2.调整截取图片分辨率到256像素左右

3.再均值滤波

4.调用AnimeGan生成即可

详见cartonModule.py

C. 运行

1.用wechaty框架完整运行本项目:

1.在一台云服务器上,docker pull wechaty_puppet_service_token_gateway的镜像。记得设定一个自己的唯一id,然后运行wechaty的docker:

export WECHATY_LOG="verbose"
export WECHATY_PUPPET="wechaty-puppet-wechat"
export WECHATY_PUPPET_SERVER_PORT="8080"
export WECHATY_TOKEN="your_write_id_what_you_like"#自定义的唯一id

docker run -ti \
--name wechaty_puppet_service_token_gateway \
--rm \
-e WECHATY_LOG \
-e WECHATY_PUPPET \
-e WECHATY_PUPPET_SERVER_PORT \
-e WECHATY_TOKEN \
-p "$WECHATY_PUPPET_SERVER_PORT:$WECHATY_PUPPET_SERVER_PORT" \
wechaty/wechaty:latest
  1. 下载本项目中的压缩包(里面包含有所的了,不用管压缩包外的脚本,那些只是用于给看aistudio的来查阅),解压所有文件到aistudio的脚本任务中或一台比较好的服务器上(可以与docker那台一样,也可不一样)。文件结构不要变

  2. 在这台较好的服务器或aistudio的脚本任务上,修改run.sh中的

WECHATY_PUPPET_SERVICE_TOKEN为wechaty中拿到的token ,修改 WECHATY_PUPPET_SERVICE_TOKEN为刚刚定义的唯一id:

export WECHATY_PUPPET_SERVICE_TOKEN="puppet_padlocal_you_get_from_wechaty"

WECHATY_PUPPET_SERVICE_TOKEN=your_write_id_what_you_like#自定义的唯一id
  1. 配置你的环境为python3.7,输入sh run.sh运行即可

2.只参考使用本项目多轮聊天模块:

1.可直接接入botprocess.py的返回,返回的是一个都是一个bot的对象及回复的内容,回复内容以字典形式,key为类型,value为图片视频文件路径或回复的文本

2.可能一次回复会有几项内容,数目为replys_index的值。当其不为0时,循环去run就好,可参考run_bot.py

3.给到botprocess的需要稍微封装一下,也可参考run_bot.py

D.总结

  1. 因时间较赶,未用上状态机进行聊天机器人的情景设置。若关注start或fork的人有兴趣可留言,后面更新此功能。只需xmind中设置流程图,文件导入到程序,就可实现多场景无需写代码就实现自己的多轮聊天机器人

  2. 希望大家若觉得不错可多多start或fork,支持一下,正在比赛中 哈~~

  3. 最后感谢党、感谢祖国、感谢Paddlehub、感谢Wechaty的支持

4.本项目参考了细菌,GT的项目,在此表示感谢

About

构建了可用于多个对话框架的场景型对话的聊天机器人。支持吐槽类情景及情感倾诉型情景。使用wechaty,结合Paddlehub的人脸特征点、动漫人脸生成、对联生成、情感分类,ernie迁移,人脸变胖

License:Apache License 2.0


Languages

Language:Python 98.1%Language:Shell 1.9%