者:胡世川 - 西門子數字化工業集團自動化部
客戶經常問到:出現嚴重故障時,能不能自動語音播報消息文本?因為做不到時時刻刻盯著監控畫面。
So easy!
有視頻有真相
,時長00:14
實驗環境:
實現思路:
|
.......
MSG_RTDATA_STRUCT mRT;
MSG_CSDATA_STRUCT sM; // holds alarm info
MSG_TEXT_STRUCT tMeld; // holds message text info
CMN_ERROR pError;
memset( &mRT, 0, sizeof( MSG_RTDATA_STRUCT ) );
.......
if(mRT.dwMsgState==MSG_STATE_COME)
{
MSRTGetMsgCSData(mRT.dwMsgNr, &sM, &pError);
MSRTGetMsgText(0, sM.dwTextID[0], &tMeld, &pError);
SetTagBit("alarmComing",TRUE); //置位VBS腳本觸發器
SetTagChar("alarmText",tMeld.szText); //報警消息文本
}
Dim speaker, alarmText
Dim alarmComing
alarmComing=HMIRuntime.Tags("alarmComing").Read
alarmText=HMIRuntime.Tags("alarmText").Read
If alarmComing=1 Then
Set speaker=CreateObject("SAPI.SpVoice")
speaker.rate=0 '語速
speaker.volume=100 ‘音量
speaker.Speak alarmText
HMIRuntime.Tags("alarmComing").write 0
End If
End Function
若采用PC蜂鳴器提醒報警到來,可參考下面鏈接:
www.ad.siemens.com.cn/service/elearning/course/1791.html
來源:人機常情 WinCC(微信公眾號)
情不算好盯盤容易困,那就讓小姐姐甜美的聲音播報一下當前上證指數吧。實現電腦語音播報只需短短三行代碼就能實現,代碼如下:
import win32com.client
speaker=win32com.client.Dispatch("SAPI.SpVoice")
speaker.Speak("當前上證指數:3259.86")
就這么簡單!別忘了安裝pywin32模塊。
當然要有點實用價值還是得費點工夫,接下來我們做一個能實時的動態的播報上證指數的小程序。制作過程中會運用到“tkinter”,Tkinter模塊("Tk 接口")是Python的標準Tk GUI工具包的接口.Tk和Tkinter可以在大多數的Unix平臺下使用,同樣可以應用在Windows和Macintosh系統里.Tk8.0的后續版本可以實現本地窗口風格,并良好地運行在絕大多數平臺中.。還有“requests”,沒錯那個“讓 HTTP 服務人類”的家伙,大名鼎鼎的自動化測試(爬蟲)工具。
第一步:導入所需模塊
import re
import requests
import tkinter as tk
import win32com.client
第二步:定義獲取數據鏈接
url="https://xueqiu.com/service/v5/stock/batch/quote?symbol=SH000001"
heads={
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,"
"application/signed-exchange;v=b3;q=0.9",
"Accept-Language": "zh-CN,zh;q=0.9",
"Host": "xueqiu.com",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/99.0.4844.51 Safari/537.36 "
}
第三步:制作一個windwos窗口
class bobao(tk.Tk):
def __init__(self):
super(bobao, self).__init__()
self.title("上證指數語音播報")
self.zs_text=""
self.lal=tk.Label(self,
text=self.zs_text,
font=("DS-Digital", 40),
padx=10,
pady=10,
background="black",
foreground="red"
)
第四步:定義一個請求數據的方法
def update_re(self):
r=requests.get(url, headers=heads)
data=r.text
current=re.findall('"current":(\d+\.\d+)', data)
speaker=win32com.client.Dispatch("SAPI.SpVoice")
speaker.Speak("當前上證指數")
speaker.Speak(current)
self.zs_text=current
self.lal.config(text=self.zs_text)
self.after(360000, self.update_re)
這里要注意self.after(360000, self.update_re)的時間單位是毫秒,不要設置得太小,經免把人家的服務器搞宕機。
完整代碼:
登錄阿里云,選擇菜單:產品->人工智能->語音合成
點擊“申請開通”,然后在“管理控制臺”創建一個項目
復制 appkey
注意,token只有1天有效,所以需要通過接口去定時獲取
查看接口文檔
由于sdk需要引入很多第三方jar包,所以建議對接RESTful API
copy接口文檔里的demo代碼,把申請到token和appkey粘貼進去,可以直接運行,demo會生成一個syAudio.wav文件,使用語言播放器直接播放就可以。
根據文檔提示需要在工程中引入三個jar包:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.9.1</version>
</dependency>
<!-- http://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.42</version>
</dependency>
<!-- 獲取token使用 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.7.1</version>
</dependency>
package com.hsoft.web.util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSONObject;
import com.hsoft.commutil.props.PropertiesUtil;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class SpeechRestfulUtil {
private static Logger logger=LoggerFactory.getLogger(SpeechRestfulUtil.class);
private String accessToken;
private String appkey;
private static SpeechRestfulUtil getInstance() {
String appkey=PropertiesUtil.getProperty("aliyun.voice.appkey");
String token=AliTokenUtil.getToken();
return new SpeechRestfulUtil(appkey, token);
}
private SpeechRestfulUtil(String appkey, String token) {
this.appkey=appkey;
this.accessToken=token;
}
/**
* HTTPS GET 請求
*/
private byte[] processGETRequet(String text, String format, int sampleRate) {
/**
* 設置HTTPS GET請求
* 1.使用HTTPS協議
* 2.語音識別服務域名:nls-gateway.cn-shanghai.aliyuncs.com
* 3.語音識別接口請求路徑:/stream/v1/tts
* 4.設置必須請求參數:appkey、token、text、format、sample_rate
* 5.設置可選請求參數:voice、volume、speech_rate、pitch_rate
*/
String url="https://nls-gateway.cn-shanghai.aliyuncs.com/stream/v1/tts";
url=url + "?appkey=" + appkey;
url=url + "&token=" + accessToken;
url=url + "&text=" + text;
url=url + "&format=" + format;
url=url + "&sample_rate=" + String.valueOf(sampleRate);
// voice 發音人,可選,默認是xiaoyun
// url=url + "&voice=" + "xiaoyun";
// volume 音量,范圍是0~100,可選,默認50
// url=url + "&volume=" + String.valueOf(50);
// speech_rate 語速,范圍是-500~500,可選,默認是0
url=url + "&speech_rate=" + String.valueOf(100);
// pitch_rate 語調,范圍是-500~500,可選,默認是0
// url=url + "&pitch_rate=" + String.valueOf(0);
// System.out.println("URL: " + url);
/**
* 發送HTTPS GET請求,處理服務端的響應
*/
Request request=new Request.Builder()
.url(url)
.get()
.build();
byte[] bytes=null;
try {
OkHttpClient client=new OkHttpClient();
Response response=client.newCall(request).execute();
String contentType=response.header("Content-Type");
if ("audio/mpeg".equals(contentType)) {
bytes=response.body().bytes();
// File f=new File(audioSaveFile);
// FileOutputStream fout=new FileOutputStream(f);
// fout.write(response.body().bytes());
// fout.close();
// System.out.println(f.getAbsolutePath());
logger.info("The GET SpeechRestful succeed!");
}
else {
// ContentType 為 null 或者為 "application/json"
String errorMessage=response.body().string();
logger.info("The GET SpeechRestful failed: " + errorMessage);
}
response.close();
} catch (Exception e) {
logger.error("processGETRequet",e);
}
return bytes;
}
/**
* HTTPS POST 請求
*/
private byte[] processPOSTRequest(String text, String audioSaveFile, String format, int sampleRate) {
/**
* 設置HTTPS POST請求
* 1.使用HTTPS協議
* 2.語音合成服務域名:nls-gateway.cn-shanghai.aliyuncs.com
* 3.語音合成接口請求路徑:/stream/v1/tts
* 4.設置必須請求參數:appkey、token、text、format、sample_rate
* 5.設置可選請求參數:voice、volume、speech_rate、pitch_rate
*/
String url="https://nls-gateway.cn-shanghai.aliyuncs.com/stream/v1/tts";
JSONObject taskObject=new JSONObject();
taskObject.put("appkey", appkey);
taskObject.put("token", accessToken);
taskObject.put("text", text);
taskObject.put("format", format);
taskObject.put("sample_rate", sampleRate);
// voice 發音人,可選,默認是xiaoyun
// taskObject.put("voice", "xiaoyun");
// volume 音量,范圍是0~100,可選,默認50
// taskObject.put("volume", 50);
// speech_rate 語速,范圍是-500~500,可選,默認是0
// taskObject.put("speech_rate", 0);
// pitch_rate 語調,范圍是-500~500,可選,默認是0
// taskObject.put("pitch_rate", 0);
String bodyContent=taskObject.toJSONString();
// System.out.println("POST Body Content: " + bodyContent);
RequestBody reqBody=RequestBody.create(MediaType.parse("application/json"), bodyContent);
Request request=new Request.Builder()
.url(url)
.header("Content-Type", "application/json")
.post(reqBody)
.build();
byte[] bytes=null;
try {
OkHttpClient client=new OkHttpClient();
Response response=client.newCall(request).execute();
String contentType=response.header("Content-Type");
if ("audio/mpeg".equals(contentType)) {
bytes=response.body().bytes();
logger.info("The POST SpeechRestful succeed!");
}
else {
// ContentType 為 null 或者為 "application/json"
String errorMessage=response.body().string();
logger.info("The POST SpeechRestful failed: " + errorMessage);
}
response.close();
} catch (Exception e) {
logger.error("processPOSTRequest",e);
}
return bytes;
}
public static byte[] text2voice(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
SpeechRestfulUtil demo=SpeechRestfulUtil.getInstance();
// String text="會員收款87.12元";
// 采用RFC 3986規范進行urlencode編碼
String textUrlEncode=text;
try {
textUrlEncode=URLEncoder.encode(textUrlEncode, "UTF-8")
.replace("+", "%20")
.replace("*", "%2A")
.replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
logger.error("encode",e);
}
// String audioSaveFile="syAudio.wav";
String format="wav";
int sampleRate=16000;
return demo.processGETRequet(textUrlEncode, format, sampleRate);
}
}
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.http.ProtocolType;
import com.aliyuncs.profile.DefaultProfile;
import com.hsoft.commutil.props.PropertiesUtil;
public class AliTokenUtil {
private static Logger logger=LoggerFactory.getLogger(AliTokenUtil.class);
// 您的地域ID
private static final String REGIONID="cn-shanghai";
// 獲取Token服務域名
private static final String DOMAIN="nls-meta.cn-shanghai.aliyuncs.com";
// API 版本
private static final String API_VERSION="2019-02-28";
// API名稱
private static final String REQUEST_ACTION="CreateToken";
// 響應參數
private static final String KEY_TOKEN="Token";
private static final String KEY_ID="Id";
private static final String KEY_EXPIRETIME="ExpireTime";
private static volatile String TOKEN="";
private static volatile long EXPIRETIME=0L;
public static String getToken() {
if (StringUtils.isNotBlank(TOKEN)) {
if (EXPIRETIME - System.currentTimeMillis() / 1000 > 3600) {
return TOKEN;
}
}
try {
String accessKeyId=PropertiesUtil.getProperty("aliyun.accessId");;
String accessKeySecret=PropertiesUtil.getProperty("aliyun.accessKey");;
// 創建DefaultAcsClient實例并初始化
DefaultProfile profile=DefaultProfile.getProfile(REGIONID, accessKeyId, accessKeySecret);
IAcsClient client=new DefaultAcsClient(profile);
CommonRequest request=new CommonRequest();
request.setDomain(DOMAIN);
request.setVersion(API_VERSION);
request.setAction(REQUEST_ACTION);
request.setMethod(MethodType.POST);
request.setProtocol(ProtocolType.HTTPS);
CommonResponse response=client.getCommonResponse(request);
logger.info(response.getData());
if (response.getHttpStatus()==200) {
JSONObject result=JSON.parseObject(response.getData());
TOKEN=result.getJSONObject(KEY_TOKEN).getString(KEY_ID);
EXPIRETIME=result.getJSONObject(KEY_TOKEN).getLongValue(KEY_EXPIRETIME);
logger.info("獲取到的Token: " + TOKEN + ",有效期時間戳(單位:秒): " + EXPIRETIME);
} else {
logger.info("獲取Token失?。?#34;);
}
} catch (Exception e) {
logger.error("getToken error!", e);
}
return TOKEN;
}
}
當然,我們的目的不是得到一個音頻文件,而是在web站點上可以直接聽見聲音。
為此,需要引入Websocket,將得到的音頻資源直接推送到web頁面上,然后使用FileReader對象直接播放
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
</exclusions>
</dependency>
public class VoiceHandler extends AbstractWebSocketHandler {
private static final Logger logger=LoggerFactory.getLogger(VoiceHandler.class);
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
VoicePool.add(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
VoicePool.remove(session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
logger.debug("receive Msg :" + message.getPayload());
TextMessage msg=new TextMessage(message.getPayload());
session.sendMessage(msg);
}
}
public class VoicePool {
private static final Logger logger=LoggerFactory.getLogger(VoicePool.class);
private static Map<String, WebSocketSession> pool=new ConcurrentHashMap<String, WebSocketSession>();
private static Map<Long, List<String>> userMap=new ConcurrentHashMap<Long, List<String>>();
private static final ExecutorService threadPool=Executors.newFixedThreadPool(50);
public static void add(WebSocketSession inbound) {
pool.put(inbound.getId(), inbound);
Map<String, String> map=ParamUtil.parser(inbound.getUri().getQuery());
Long companyId=Long.valueOf(map.get("companyId"));
logger.info("add companyId:{}", companyId);
List<String> lstInBound=null;
if (companyId !=null) {
lstInBound=userMap.get(companyId);
if (lstInBound==null) {
lstInBound=new ArrayList<String>();
userMap.put(companyId, lstInBound);
}
lstInBound.add(inbound.getId());
}
logger.info("add connetion {},total size {}", inbound.getId(), pool.size());
}
public static void remove(WebSocketSession socket) {
String sessionId=socket.getId();
List<String> lstInBound=null;
Map<String, String> map=ParamUtil.parser(socket.getUri().getQuery());
Long companyId=Long.valueOf(map.get("companyId"));
logger.info("remove companyId:{}", companyId);
if (StringUtils.isNotBlank(sessionId)) {
if (companyId !=null) {
lstInBound=userMap.get(companyId);
if (lstInBound !=null) {
lstInBound.remove(sessionId);
if (lstInBound.isEmpty()) {
userMap.remove(companyId);
}
}
}
}
pool.remove(sessionId);
logger.info("remove connetion {},total size {}", sessionId, pool.size());
}
/** 推送信息 */
public static void broadcast(VoiceMsgVo vo) {
Long companyId=vo.getCompanyId();
if (companyId==null || companyId==0L) {
return;
}
List<String> lstInBoundId=userMap.get(companyId);
if (lstInBoundId==null || lstInBoundId.isEmpty()) {
return;
}
byte[] bytes=SpeechRestfulUtil.text2voice(vo.getText());
if (bytes==null) {
return;
}
threadPool.execute(() -> {
try {
for (String id : lstInBoundId) {
// 發送給指定用戶
WebSocketSession connection=pool.get(id);
if (connection !=null) {
synchronized (connection) {
BinaryMessage msg=new BinaryMessage(bytes);
connection.sendMessage(msg);
}
}
}
} catch (Exception e) {
logger.error("broadcast error: companyId:{}", companyId, e);
}
});
}
}
消息對象bean
public class VoiceMsgVo {
private String text;
private Long companyId;
}
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(voiceHandler(), "/ws/voice").setAllowedOrigins("*");
}
@Bean
public VoiceHandler voiceHandler() {
return new VoiceHandler();
}
}
隨便創建頁面,引入下面的js
var audioContext=new (window.AudioContext || window.webkitAudioContext)();
var Chat={};
Chat.socket=null;
Chat.connect=(function(host) {
if ("WebSocket" in window) {
Chat.socket=new WebSocket(host);
} else if ("MozWebSocket" in window) {
Chat.socket=new MozWebSocket(host);
} else {
Console.log("Error: WebSocket is not supported by this browser.");
return;
}
Chat.socket.onopen=function() {
Console.log("Info: 語音播報已啟動.");
// 心跳檢測重置
heartCheck.reset().start(Chat.socket);
};
Chat.socket.onclose=function() {
Console.log("Info: 語音播報已關閉.");
};
Chat.socket.onmessage=function(message) {
heartCheck.reset().start(Chat.socket);
if (message.data==null || message.data=='' || "HeartBeat"==message.data){
//心跳消息
return;
}
var reader=new FileReader();
reader.onload=function(evt) {
if (evt.target.readyState==FileReader.DONE) {
audioContext.decodeAudioData(evt.target.result,
function(buffer) {
// 解碼成pcm流
var audioBufferSouceNode=audioContext
.createBufferSource();
audioBufferSouceNode.buffer=buffer;
audioBufferSouceNode
.connect(audioContext.destination);
audioBufferSouceNode.start(0);
}, function(e) {
console.log(e);
});
}
};
reader.readAsArrayBuffer(message.data);
};
});
Chat.initialize=function() {
Chat.companyId=_currCompanyId;
if (window.location.protocol=="http:") {
Chat.connect("ws://" + window.location.host + "/ws/voice?companyId="+Chat.companyId);
} else {
Chat.connect("wss://" + window.location.host + "/ws/voice?companyId="+Chat.companyId);
}
};
Chat.sendMessage=(function() {
var message=document.getElementById("chat").value;
if (message !="") {
Chat.socket.send(message);
document.getElementById("chat").value="";
}
});
var Console={};
Console.log=(function(message) {
var _console=document.getElementById("console");
if (_console==null || _console==undefined){
console.log(message);
return;
}
var p=document.createElement("p");
p.style.wordWrap="break-word";
p.innerHTML=message;
_console.appendChild(p);
while(_console.childNodes.length>25)
{
_console.removeChild(_console.firstChild);
}
_console.scrollTop=_console.scrollHeight;
});
Chat.initialize();
//心跳檢測
var heartCheck={
timeout : 60000,// 60秒
timeoutObj : null,
serverTimeoutObj : null,
reset : function() {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start : function(ws) {
var self=this;
this.timeoutObj=setTimeout(function() {
// 這里發送一個心跳,后端收到后,返回一個心跳消息,
// onmessage拿到返回的心跳就說明連接正常
// console.log('start heartCheck');
ws.send("HeartBeat");
self.serverTimeoutObj=setTimeout(function() {// 如果超過一定時間還沒重置,說明后端主動斷開了
ws.close();// 如果onclose會執行reconnect,我們執行ws.close()就行了.如果直接執行reconnect
// 會觸發onclose導致重連兩次
}, self.timeout)
}, this.timeout)
}
}
啟動工程,從后臺發送一段消息
*請認真填寫需求信息,我們會在24小時內與您取得聯系。