簡易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); // 退還記憶體塊
}
}
以上就是幾個主要部分了,很簡單。