者:胡世川 - 西门子数字化工业集团自动化部
客户经常问到:出现严重故障时,能不能自动语音播报消息文本?因为做不到时时刻刻盯着监控画面。
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失败!");
}
} 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小时内与您取得联系。