1. 程式人生 > >簡易DIY智慧聊天機器人

簡易DIY智慧聊天機器人

前言

大二忙裡偷閒,花了一個月左右自己利用了Python+ESP8266 DIY 了一個智慧聊天機器人,呼叫的是圖靈機器人的體驗API,現在把DIY過程記錄下來,希望能分享給別的對這方面有興趣的人。

DIY前的準備

1.STM32F429IG作為主控晶片

2.ESP8266,用來與自己電腦上伺服器通訊

3.VS1053,用來儲存和播放音樂

硬體方面很簡單,當然也可以自己興趣拓展,比如自己加一塊顯示屏什麼的,都是可以的。

電腦端伺服器Python

思路是,電腦利用Python開伺服器,等待ESP8266的連線,連線上後,STM32會發送給服務端剛剛錄下的音樂,然後呼叫百度語音識別api,就可以將剛剛的錄下的音樂傳送給百度語音識別,百度語音會返回識別完成的字串,再呼叫圖靈機器人的api,把識別後的字串傳送出去,就會得到聊天的回覆語句,最後一步,將回復語句傳送給 百度語音合成,生成的回覆語句的mp3,傳送給stm32,stm32再通過VS1053播放,以上就實現了聊天的功能。

流程就是   vs1053>錄音下的語句(stm32)  >百度語音識別 >圖靈機器人 >百度語音生成 >stm32>vs1053

流程很簡單,那麼直接上程式碼

#coding=utf-8

from socket import *
import sys
import json
import base64
from urllib.request import urlopen
from urllib.request import Request
from urllib.error import URLError
from urllib.parse import urlencode
import string
import requests

class DemoError(Exception):
    pass


"""  獲取TOKEN"""



def fetch_token():
    TOKEN_URL = 'http://openapi.baidu.com/oauth/2.0/token'
    SCOPE = 'audio_voice_assistant_get'  # 有此scope表示有asr能力,沒有請在網頁裡勾選
    API_KEY = '你的api_key'
    SECRET_KEY = '你的api_secret'    
    params = {'grant_type': 'client_credentials',
              'client_id': API_KEY,
              'client_secret': SECRET_KEY}
    post_data = urlencode(params)
    post_data = post_data.encode( 'utf-8')
    req = Request(TOKEN_URL, post_data)
    try:
        f = urlopen(req)
        result_str = f.read()
    except URLError as err:
        result_str = err.read()
    result_str =  result_str.decode()

    result = json.loads(result_str)
    if ('access_token' in result.keys() and 'scope' in result.keys()):
        if not SCOPE in result['scope'].split(' '):
            raise DemoError('scope is not correct')
        return result['access_token']
    else:
        raise DemoError('MAYBE API_KEY or SECRET_KEY not correct: access_token or scope not found in token response')



"""  語音識別"""


ASR_URL = 'http://vop.baidu.com/server_api'
def voice_judge():
    token = fetch_token()
    # 需要識別的檔案
    AUDIO_FILE = '8k.wav' #只支援 pcm/wav/amr
    # 檔案格式
    FORMAT = AUDIO_FILE[-3:];  # 檔案字尾 pcm/wav/amr
    # 根據文件填寫PID,選擇語言及識別模型
    DEV_PID = 1537;  # 1537 表示識別普通話,使用輸入法模型。1536表示識別普通話,使用搜索模型
    CUID = '123456PYTHON';
    # 取樣率
    RATE = 8000;  # 固定值
    speech_data = []
    with open(AUDIO_FILE, 'rb') as speech_file:
        speech_data = speech_file.read()
    length = len(speech_data)
    if length == 0:
        raise DemoError('file %s length read 0 bytes' % AUDIO_FILE)
    speech = base64.b64encode(speech_data)

    speech = str(speech, 'utf-8')
    params = {'dev_pid': DEV_PID,
              'format': FORMAT,
              'rate': RATE,
              'token': token,
              'cuid': CUID,
              'channel': 1,
              'speech': speech,
              'len': length
              }
    post_data = json.dumps(params, sort_keys=False)
    req = Request(ASR_URL, post_data.encode('utf-8'))
    req.add_header('Content-Type', 'application/json')
    try:
        f = urlopen(req)
        result_str = f.read()
    except  URLError as err:
        result_str = err.read()

    result_str = str(result_str, 'utf-8')
    return (result_str)

	
""" 聊天回覆"""
def get_response(msg):
    api = 'http://openapi.tuling123.com/openapi/api/v2'
    dat = {
        "perception": {
            "inputText": {
                "text": msg
            },
            "inputImage": {
                "url": "imageUrl"
            },
            "selfInfo": {
                "location": {
                    "city": "廈門",
                }
            }
        },
        "userInfo": {
            "apiKey": '你的api_key',
            "userId": "隨意的使用者id,用來判斷是否為同一人,因為圖靈機器人會根據上文回覆"
        }
    }
    dat = json.dumps(dat)
    r = requests.post(api, data=dat).json()
 
 
    mesage = r['results'][0]['values']['text']
    return mesage	



""" 語音生成"""



def voice_make(msg):
    token = fetch_token()
    # 發音人選擇, 0為普通女聲,1為普通男生,3為情感合成-度逍遙,4為情感合成-度丫丫,預設為普通女聲
    PER = 4
    # 語速,取值0-15,預設為5中語速
    SPD = 2
    # 音調,取值0-15,預設為5中語調
    PIT = 5
    # 音量,取值0-9,預設為5中音量
    VOL = 5
    # 下載的檔案格式, 3:mp3(default) 4: pcm-16k 5: pcm-8k 6. wav
    AUE = 3

    FORMATS = {3: "mp3", 4: "pcm", 5: "pcm", 6: "wav"}
    FORMAT = FORMATS[AUE]

    CUID = "123456PYTHON"

    TTS_URL = 'http://tsn.baidu.com/text2audio'
    
    
    params = {'tok': token, 'tex': msg, 'per': PER, 'spd': SPD, 'pit': PIT, 'vol': VOL, 'aue': AUE, 'cuid': CUID,
              'lan': 'zh', 'ctp': 1}  # lan ctp 固定引數
    data = urlencode(params)

    req = Request(TTS_URL, data.encode('utf-8'))

    has_error = False
    try:
        f = urlopen(req)
        result_str = f.read()
        has_error = ('Content-Type' not in f.headers.keys() or f.headers['Content-Type'].find('audio/') < 0)
    except  URLError as err:
        result_str = err.read()
        has_error = True

    save_file = "error.txt" if has_error else 'result.' + FORMAT
    with open(save_file, 'wb') as of:
        of.write(result_str)
    if has_error:
        result_str = str(result_str, 'utf-8')





#伺服器,主程式
HOST = '你當前電腦的ip地址' 
PORT = 80
BUFSIZ = 0x500000
ADDR=(HOST,PORT)
AUDIO_FILE = '8k.wav' #只支援 pcm/wav/amr
s = socket(AF_INET, SOCK_STREAM)
s.bind(ADDR)
s.listen(5)
while True:
    print('waiting for connecting...')
    print('')
    c, addr = s.accept()
    print('..connected from:', addr)
    speech_file= open(AUDIO_FILE, 'r+')
    speech_file.seek(0)
    speech_file.truncate()   #清空檔案
    speech_file.close( )           
    while True:
        data = c.recv(BUFSIZ)
        if not data:
            break
        speech_file= open(AUDIO_FILE, 'ab+')
        speech_file.write(data)
        speech_file.flush()
        speech_file.close( )       

    c.close()            
    mystr=voice_judge()
    result = json.loads(mystr)
    if(result['err_no']==0):
        mystr = "".join(result['result'])
    else:
        mystr="無效"
    print(mystr)        
    mybyte = bytes(mystr, encoding = "gbk")    
    reply=get_response(mystr)
    voice_make(reply)
    print(reply)
    c, addr = s.accept()
    speech_file= open('result.mp3', 'rb')
    data=speech_file.read()
    speech_file.close( )       
    c.sendall(data)
    time.sleep(1)   
    c.close()


s.close()

STM32F429程式碼

stm32f429的流程也很簡單,就是按下按鍵,開始錄音,再按一下結束錄音,然後等待回傳回來的音訊檔案並且播放。

另外一個模組就是ESP8266,ESP8266的程式碼也是很簡單的,我使用的是模組,所以很簡單的呼叫api就好了,如果使用的是正統的esp8266,除了傳輸速度慢了一些,別的應該都一樣。至於esp8266的配置,這邊就不詳細說明了,網路一大堆這個東西,我之前也用過NodeMcu實現過,Arduino調庫調起來也是容易實現的。

void User_BSP_Init()
{
    delay_init(168);  	    // 初始化系統時鐘,主頻為168M 
    SDRAM_Init();           //SDRAM初始化
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  // 配置NVIC為優先順序組2 
    LED_GPIO_Config();      //配置板載LED
    USART_Config();         //配置串列埠
    USART_IT_ENABLE();      //開啟串列埠接收中斷
    EXTI_Key_Config();      //開啟Key的外部中斷        
    Fatfs_Flash_Format();   //初始化Fatfs_SPI_Flash
    M8266_Module_User_Init(); //初始化M8266,並列印相關資訊
    VS_Init();              //初始化VS1053
    f_mount(&fs,"1:",1);     //掛載SPI_Flash 為碟符 1:
    
}

這是BSP的初始化

void User_main()
{
    OS_ERR  err;
    OSSchedRoundRobinCfg(DEF_ENABLED,0,&err);   //開啟時間片轉輪排程  10*系統節拍  即10ms
    OSMemCreate(&uC_mem,"uC/Data",uC_Data,4,16,&err);       //開啟記憶體管理系統 ,128個記憶體塊,每個4個位元組
    OSTaskCreate(&USART1_Get_TCB,"串列埠接收",USART1_Get,0,USART1_Get_PRIO,USART1_Get_STK,USART1_Get_STK_SIZE/10,USART1_Get_STK_SIZE,0,0,0,(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),&err); 
    OSTaskCreate(&Key_TCB,"按鍵中斷",Key_On,0,Key_PRIO,Key_Stk,Key_Stk_Size/10,Key_Stk_Size,0,0,0,(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),&err);    
    OSTaskCreate(&USART1_OK_TCB,"串列埠接收完成",USART1_OK,0,USART1_OK_PRIO,USART1_OK_STK,USART1_OK_STK_SIZE/10,USART1_OK_STK_SIZE,0,0,0,(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),&err); 
    OSTaskCreate(&M8266_Get_TCB,"M8266接收",M8266_Get,0,M8266_Get_PRIO,M8266_Gett_Stk,M8266_Get_Stk_Size/10,M8266_Get_Stk_Size,0,0,0,(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),&err); 
   M8266_Module_Join_AP((u8*)WiFi_SSID,(u8*)WiFi_PAWD,Hostname);     
    
}

上面是幾個主要任務,並且esp8266連線上你的熱點

static void  Key_On (void *p_arg)
{
    OS_ERR  err;
    unsigned long file_size ;
    CPU_SR_ALLOC();
    LED_TOGGLE;            
    OSTimeDly(300,OS_OPT_TIME_DLY,&err);               
    LED_TOGGLE;            
    OSTimeDly(300,OS_OPT_TIME_DLY,&err);               
    LED_TOGGLE;   
    OSTimeDly(300,OS_OPT_TIME_DLY,&err);               
    LED_TOGGLE;         //閃燈表示準備完成
    while(1)
    {
    OSTaskSemPend (0,OS_OPT_PEND_BLOCKING,NULL,&err);            
    OS_CRITICAL_ENTER();
    M8266_Module_Set_Connect(Goal_Ip,Remote_Port,LinkNum,10);    //連線
    OS_CRITICAL_EXIT();           
    f_unlink("1:錄音檔案.wav"); 
    f_unlink("1:音樂檔案.mp3");              
    vs1053_record_start();    //開始錄音
    OSTaskCreate(&Record_TCB,"錄音",Record,0,Record_PRIO,Record_Stk,Record_Stk_Size/10,Record_Stk_Size,2,0,0,(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),&err); 
    OSTaskSemPend (0,OS_OPT_PEND_BLOCKING,NULL,&err);        
    OSTaskDel(&Record_TCB,&err);
    vs1053_record_stop("1:錄音檔案.wav"); //停止錄音,並且儲存在外部Flash中
    OSTimeDly(100,OS_OPT_TIME_DLY,&err);       
    M8266_Module_SendFile((uint8_t*)"1:錄音檔案.wav",LinkNum);      //傳送錄音檔案
    printf("音樂檔案大小是 %ld Byte,%.2f KB\r\n",file_size,(double)file_size/1024);  
    M8266WIFI_SPI_Delete_Connection(LinkNum,NULL);      //斷開連線
    OSTimeDly(1500,OS_OPT_TIME_DLY,&err);               
    M8266_Module_Set_Connect(Goal_Ip,Remote_Port,LinkNum,10);     //開啟連線,等待服務端傳送處理好的回覆音訊檔案       
    OSTimeDly(1000,OS_OPT_TIME_DLY,&err);                       
    M8266WIFI_SPI_Delete_Connection(LinkNum,NULL);          
    LED_TOGGLE;            
    }
}

static void    Record (void *p_arg)
{
    OS_ERR  err;
	CPU_SR_ALLOC();
    OSTimeDly(500,OS_OPT_TIME_DLY,&err);
    LED_TOGGLE;              
    
    while(1)
    {
     OS_CRITICAL_ENTER();   
     vs1053_record_run();    
     OS_CRITICAL_EXIT();        
     OSTimeDly(33,OS_OPT_TIME_DLY,&err);  //經過測試大概33ms收集一次,音質最佳
    }        
}

這是按鍵任務,應該是最主要的任務。還有錄音時建立的任務。

static void  M8266_Get (void *p_arg)
{
    OS_ERR  err;
    u16 recv_data_num;
    u16 status;
    u16 wifi_get_flag;
    unsigned long file_size ;
    
    while(1)
    {
        status=M8266_Module_GetData(NULL,&recv_data_num);
        if(status!=0x0001)
        {
            wifi_get_flag=0;        
            while(status)
            {
            memcpy(&VS1053_Mem[wifi_get_flag],test_get,recv_data_num);
            memset(test_get,0,recv_data_num);                       
            wifi_get_flag+= recv_data_num;           
            status=M8266_Module_GetData(NULL,&recv_data_num);                                          
            }
             memcpy(VS1053_Mem,test_get,recv_data_num);
             memset(test_get,0,recv_data_num);       
             wifi_get_flag+=recv_data_num;  
            //將接收到的音樂檔案儲存到 VS1053_Mem SDRAM中

          printf("接收到 %d Byte\r\n",wifi_get_flag);  
          vs1053_write_misic_file("1:音樂檔案.mp3",wifi_get_flag);  //寫入Flash中
          OSTimeDly(100,OS_OPT_TIME_DLY,&err);        
          vs1053_player_song((uint8_t*)"1:音樂檔案.mp3",&file_size); //播放剛剛儲存的文集
          printf("音樂檔案大小是 %ld Byte\r\n",file_size);        
        }
        OSTimeDly(50,OS_OPT_TIME_DLY,&err);        
    }           
}

然後是ESP8266接收到音訊資料後,播放音訊的任務

其餘部分就是一些串列埠部分的任務了

static  void   USART1_OK(void *p_arg)  //串列埠接收完成任務
{
    OS_ERR  err;
    uint32_t M8266_flag;
    uint32_t Debug_flag;
    u16 status;
    while(1)
    {
    OSTaskSemPend (0,OS_OPT_PEND_BLOCKING,NULL,&err);
    M8266_flag=0;
    Debug_flag=0;    
    printf("接收到 %d 串列埠資料\r\n",Write_Usart_flag);   
    while(M8266_flag<Write_Usart_flag)
    {
         if(Write_Usart_flag-M8266_flag<=1024)
         {
         Debug_flag+=M8266WIFI_SPI_Send_Data(&Usart_Mem[M8266_flag],Write_Usart_flag-M8266_flag,LinkNum,&status);
         M8266_flag+=Write_Usart_flag-M8266_flag;       
         }
         else
         {              
         Debug_flag+=M8266WIFI_SPI_Send_Data(&Usart_Mem[M8266_flag],1024,LinkNum,&status);
         M8266_flag+=1024;
         } 
    }    
    printf("成功傳送 %d Byte\r\n",Debug_flag);
    memset(Usart_Mem,0,Write_Usart_flag);  
    Write_Usart_flag=0; 
    }
}


static void  USART1_Get (void *p_arg)
{
	OS_ERR         err;
	OS_MSG_SIZE    msg_size;
	char * pMsg;
	while (DEF_TRUE) 
    {                                           
        pMsg = OSTaskQPend(0,OS_OPT_PEND_BLOCKING,&msg_size,NULL,&err);     //無限期限堵塞等待
        Usart_Mem[Write_Usart_flag]=*pMsg;
        Write_Usart_flag++;
        OSMemPut(&uC_mem,pMsg,&err);            		// 退還記憶體塊       
	}       
}

以上就是幾個主要部分了,很簡單。