青训营×豆包MarsCode技术训练营 基于模拟对话的数字人平台项目(服务端设计)

163 阅读4分钟

图片.png

本项目的总体技术架构图就如上图所示。界面提供了展示对话的功能,但是主要的大语言模型的调用逻辑,以及数字人视频流的传输逻辑还要在后端中完成。由于本项目涉及大量在本地运行的人工智能api,而这些模型的调用与推理逻辑大多都是基于python的,所以本项目的后端服务器都基于flask的架构。Flask是一个轻量级的Python Web框架,它被设计得非常灵活,让你可以快速地创建Web应用程序。Flask的核心非常简单,但它也提供了强大的扩展机制,可以让你自由地选择适合你的工具和库来构建应用程序。

可以看一下部分的文字问答的服务端代码:

@__app.route('/api/start-live', methods=['post'])
def api_start_live():
    # time.sleep(5)
    fay_booter.start()
    time.sleep(1)
    wsa_server.get_web_instance().add_cmd({"liveState": 1})
    return '{"result":"successful"}'


@__app.route('/api/stop-live', methods=['post'])
def api_stop_live():
    # time.sleep(1)
    fay_booter.stop()
    time.sleep(1)
    wsa_server.get_web_instance().add_cmd({"liveState": 0})
    return '{"result":"successful"}'


@__app.route('/api/send', methods=['post'])
def api_send():
    data = request.values.get('data')
    info = json.loads(data)
    fay_core.send_for_answer(info['msg'])
    return '{"result":"successful"}'


@__app.route('/api/get-msg', methods=['post'])
def api_get_Msg():
    contentdb = content_db.new_instance()
    user_value = contentdb.get_user_value()
    list = contentdb.get_list('all', 'desc', user_value, 1000)
    relist = []
    i = len(list) - 1
    while i >= 0:
        relist.append(
            dict(type=list[i][0], way=list[i][1], content=list[i][2], createtime=list[i][3], timetext=list[i][4]))
        i -= 1

    return json.dumps({'list': relist})

在文本的传输方面,本项目使用的是websocket架构,通过流式的方法进行对话内容的传输。但是对话的文本不仅要让大语言模型知道,还需要保存在数据库中,完成历史数据的功能。数据库方面的服务端代码如下所示:

class Content_Db:

    def __init__(self) -> None:
        self.lock = threading.Lock()
           
   

    #初始化
    def init_db(self):
        conn = sqlite3.connect('fay.db')
        c = conn.cursor()
        c.execute('''CREATE TABLE IF NOT EXISTS T_Msg
            (id INTEGER PRIMARY KEY     autoincrement,
            type        char(10),
            way        char(10),
            content           TEXT    NOT NULL,
            createtime         Int);''')
        conn.commit()
        conn.close()
       

    #添加对话
    @synchronized
    def add_content(self,type,way,content):
        now = datetime.datetime.now()
        date_str = now.strftime("%Y%m%d%H%M%S")
        conn = sqlite3.connect("fay.db")
        cur = conn.cursor()
        try:
            cur.execute("SELECT user FROM T_current WHERE id = ?", (1,))
            user_value = cur.fetchone()

            if user_value:  # 如果查询结果不为空
                user_value = user_value[0]  # 获取元组中的第一个值,即 user 列的值
                print(f"User value: {user_value}")
            else:  # 如果查询结果为空
                print("No user with id 1 found.")
            if type == "member":
                type = user_value

            cur.execute("insert into T_Msg (type,way,content,createtime) values (?,?,?,?)",(type,way,content,date_str))
            conn.commit()
        except:
               util.log(1, "请检查参数是否有误")
               conn.close()
               return 0
        conn.close()
        return cur.lastrowid
     
        

    #获取对话内容
    @synchronized
    def get_list(self,way,order,user_value,limit):
        conn = sqlite3.connect("fay.db")
        cur = conn.cursor()
        try:
            # 定义查询,获取所有符合条件的行及其下一行
            query = """
                   WITH TargetRows AS (
                       SELECT id
                       FROM T_Msg
                       WHERE type = ?
                   ),
                   NextRows AS (
                       SELECT id AS target_id, id + 1 AS next_id
                       FROM TargetRows
                   ),
                   CombinedRows AS (
                       SELECT target_id AS id FROM NextRows
                       UNION
                       SELECT next_id AS id FROM NextRows
                   )
                   SELECT type, way, content, createtime, datetime(createtime, 'unixepoch', 'localtime') AS timetext
                   FROM T_Msg
                   WHERE id IN (SELECT id FROM CombinedRows)
                   ORDER BY id DESC
                   LIMIT ?;
                   """

            # 执行查询
            cur.execute(query, (user_value, limit))
            results = cur.fetchall()

对话内容还需要添加是用户的内容还是数字人的回答内容来进行区分。在测试阶段,本项目还同时调用了多种大语言模型,获取大语言模型回答内容的主要逻辑就是以下代码:

# 文本消息处理
def send_for_answer(msg):
    contentdb = content_db.new_instance()
    contentdb.add_content('member', 'send', msg)
    wsa_server.get_web_instance().add_cmd({"panelReply": {"type": "member", "content": msg}})
    textlist = []
    text = None
    text = qa_service.question('qa', msg)

    # 人设问答
    if text is not None:
        keyword = qa_service.question('Persona', msg)
        if keyword is not None:
            text = config_util.config["attribute"][keyword]

    # 全局问答
    if text is None:
        text, textlist = determine_nlp_strategy(msg)

    contentdb.add_content('fay', 'send', text)
    wsa_server.get_web_instance().add_cmd({"panelReply": {"type": "fay", "content": text}})
    if len(textlist) > 1:
        i = 1
        while i < len(textlist):
            contentdb.add_content('fay', 'send', textlist[i]['text'])
            wsa_server.get_web_instance().add_cmd({"panelReply": {"type": "fay", "content": textlist[i]['text']}})
            i += 1
    fay_booter.feiFei.a_msg = text
    MyThread(target=fay_booter.feiFei.say, args=['interact']).start()
    return text

由于神经辐射场建模的人物表情动作是依靠音频驱动的,所以文本转音频之后,再将音频输入模型进行视频流的生成。这个过程还是比较长的,一开始当获取到gpt的回答内容再到完整的视频出来,大概需要一分钟左右。这种程度的延迟是非常影响观感的。后来使用了全流程的流式传输,从文本流到音频流再到视频流,延迟降到了15秒左右。当然如果出现网络问题,又会带来音画不同步,音频部分内容丢失的情况。多种模型的连续调用对设备的要求也是一路走高。使用4090作为服务端的情况,生成的视频才勉强稳定在50帧左右,帧数如果到20帧以下,又会出现严重的数据紊乱问题。一个没有经过系统优化的项目真是不忍直视。好在在一切顺利的情况下,此项目还是能完成预设的所有功能。

但是这种现状也让我开始思考使用真人复刻的正确性,它的设备要求仍然很高,再加上本地运行的api,和轻量化的初衷可以说是背道而驰了。更多的内容和改进方案就在以后的文章中分享吧。