写在前面的话
上章说了智能爬取,拿到了网上小说的信息,这章开始利用这些数据进行智能朗读。搜索网上朗读的方法,主要包括微软自带的speeker,三方智能语音api。经过筛选,我选择了语音包还算丰富(主要妹子声音甜美)的百度api进行智能朗读(文本转语音,这里主要是MP3格式,wav貌似测试有问题),阅读功能用微软com自带的控件。
小说数据UI展示
小说的信息主要包括小说的基本信息,小说的章节信息,小说的文本详细信息。这里围绕这个,根据window form设计一个界面。
- 小说列表UI展示
左侧展示数据库服务器里面的小说列表(数据绑定),代码如下:
DataSet dataSet= dbProvider.ExecuteDataSet($"select BookName,Id from BookBasic"); if (dataSet != null) { ListbookInfos = new List (); foreach (DataRow item in dataSet.Tables[0].Rows) { bookInfos.Add(new BookInfo() { Id = item[1].ToString(), BookName = item[0].ToString() }); } listBox1.DataSource = bookInfos; listBox1.DisplayMember = "BookName"; }
筛选功能:
private void textBox2_TextChanged(object sender, EventArgs e) { string text = textBox2.Text; if (text == "请输入小说名") return; DataSet dataSet = dbProvider.ExecuteDataSet($"select BookName,Id from BookBasic where BookName like '%{text}%'"); if (dataSet != null) { ListbookInfos = new List (); foreach (DataRow item in dataSet.Tables[0].Rows) { bookInfos.Add(new BookInfo() { Id = item[1].ToString(), BookName = item[0].ToString() }); } listBox1.DataSource = bookInfos; } }
- 小说详情UI展示
右边列表展示左侧选中小说的详细信息,包括小说的名称、作者、图标(图标用blob存储)等,点击开始阅读,查看小说列表信息
代码如下:
private void listBox1_SelectedIndexChanged(object sender, EventArgs e) { pnlBookInfo.Visible = true; pnlList.Visible = false; pnlDetail.Visible = false; BookInfo bookInfo = listBox1.SelectedItem as BookInfo; DataSet dataSet = dbProvider.ExecuteDataSet($"select * from BookBasic where Id='{bookInfo.Id}'"); if (dataSet != null) { DataRow dataRow = dataSet.Tables[0].Rows[0]; bookInfo.Author = dataRow["Author"].ToString(); bookInfo.LatestChapter = dataRow["LatestChapter"].ToString(); bookInfo.Desc1 = dataRow["Desc1"].ToString(); try { bookInfo.Image = (byte[])dataRow["Image"]; string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".jpg"); using (FileStream stream = new FileStream(tempPath, FileMode.Create)) { stream.Write(bookInfo.Image, 0, bookInfo.Image.Length); } pictureBox1.Image = Image.FromFile(tempPath); } catch { } } bindingSource1.DataSource = bookInfo; }
- 小说列表信息展示
查看小说的列表信息,提供返回和排序的功能
代码如下:
private void SetChapterLst(string order="asc") { BookInfo bookInfo = bindingSource1.DataSource as BookInfo; //初始化小说列表 DataSet dataSet = dbProvider.ExecuteDataSet($"select Title,DId from BookContent where Id='{bookInfo.Id}' order by cast(Chapter as decimal(6,0)) {order}"); ListbookInfos = new List (); foreach (DataRow item in dataSet.Tables[0].Rows) { bookInfos.Add(new BookInfo() { Id = item[1].ToString(), BookName = item[0].ToString() }); } listBox2.DataSource = bookInfos; listBox2.DisplayMember = "BookName"; }
- 小说内容展示
点击章节列表信息展示小说信息
代码如下:
private void SetDetail(string Id) { //初始化小说列表 DataSet dataSet = dbProvider.ExecuteDataSet($"select * from BookContent where DId='{Id}'"); DataRow dataRow = dataSet.Tables[0].Rows[0]; BookInfo bookInfo = new BookInfo() { Desc1 = dataRow["Content"].ToString().Replace("笔趣阁手机端 http://m.biquwu.cc ", ""), BookName = dataRow["Title"].ToString(), }; bindingSource2.DataSource = bookInfo; }
朗读功能
朗读功能的实现主要包含2个部分,一个部分是将小说文本转为语音文件,一部分是将语音文本按照一定的顺序播放出来。
- 文本转语音
百度api提供了很多人工智能的功能(需要申请账号和秘钥),有兴趣自己可以研究。这次用到文本转语音的接口,主要是以接口的形式请求返回(需要token),代码如下:
////// 获取Token /// /// /// /// private string getTokon() { string token = redisConfig._GetKey<string>("baidu_token"); if (string.IsNullOrEmpty(token)) { WebClient webClient = new WebClient(); webClient.BaseAddress = "https://openapi.baidu.com"; string result = webClient.DownloadString($"https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id={API_key}&client_secret={API_secret_key}"); webClient.Dispose(); dynamic json = JToken.Parse(result); token = json.access_token; long expires_in = json.expires_in; redisConfig._AddKey<string>("baidu_token", token, new TimeSpan(expires_in * 1000 * 1000 * 10)); } return token; } public void GetAudio(string filePath,string text) { //获取参数 int vol = redisConfig._GetKey<int>("vol"); if (vol == default(int)) vol = 5; int pit = redisConfig._GetKey<int>("pit"); if (pit == default(int)) pit = 5; int spd = redisConfig._GetKey<int>("spd"); if (spd == default(int)) spd = 5; int per = redisConfig._GetKey<int>("per"); if (per == default(int)) per = 0; string token = getTokon(); var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip }; HttpClient httpClient = new HttpClient(handler); httpClient.BaseAddress = new Uri("http://tsn.baidu.com/text2audio"); //await异步等待回应 var response = httpClient.PostAsync($"http://tsn.baidu.com/text2audio?lan=zh&ctp=2&vol={vol}&per={per}&spd={spd}&pit={pit}&aue=3&tok={token}&tex={HttpUtility.UrlEncode(HttpUtility.UrlEncode(text))}&cuid={Guid.NewGuid()}&aue=6", null).Result; //确保HTTP成功状态值 response.EnsureSuccessStatusCode(); //await异步读取最后的JSON(注意此时gzip已经被自动解压缩了,因为上面的AutomaticDecompression = DecompressionMethods.GZip) byte[] result = response.Content.ReadAsByteArrayAsync().Result; string resonse= response.Content.ReadAsStringAsync().Result; using (FileStream fileStream=new FileStream(filePath,FileMode.Create)) { fileStream.Write(result, 0, result.Length); } }
小说的内容是一串长文本内容,这里如果自己用baiduapi请求会提示超长(官网提示最长200字符),所以我们需要通过符号和长度进行截取,在将各个语音文件逐个播放,不就实现了顺序播放了。
代码如下:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { BookInfo bookInfo = e.Argument as BookInfo; string dir = Path.Combine(Path.GetTempPath(), bookInfo.BookName); if (Directory.Exists(dir) == false) { Directory.CreateDirectory(dir); } int index = 0; int lastLen = 0; //播放章节标题 string filePath = Path.Combine(dir, index + ".mp3"); baiduApi.GetAudio(filePath, bookInfo.BookName); backgroundWorker1.ReportProgress(0, filePath); keyValuePairs.Add(index, new int[] { 0, 0 }); index++; //请求资源 if (!string.IsNullOrEmpty(bookInfo.Desc1)) { string[] content = bookInfo.Desc1.Split('。', ',', ';', ',', '.'); int current = 1; foreach (var txt in content) { string item = txt.Trim(); if (string.IsNullOrEmpty(item)) { continue; } int len = item.Length; for (int i = 0; i <= len / 50; i++) { string text = item.Substring(i * 50, Math.Min(item.Substring(i * 50).Length, 50)); filePath = Path.Combine(dir, index + ".mp3"); baiduApi.GetAudio(filePath, text); backgroundWorker1.ReportProgress((int)(current*100/content.Length), filePath); //播放文字长度 int start = bookInfo.Desc1.IndexOf(text, lastLen); lastLen = start + Math.Min(item.Substring(i * 50).Length, 50); keyValuePairs.Add(index, new int[] { start, Math.Min(item.Substring(i * 50).Length, 50) }); index++; } current++; } } } private int played = 0; private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { toolStripProgressBar1.Visible = true; toolStripProgressBar1.Value = e.ProgressPercentage; string filePath = e.UserState.ToString(); IWMPMedia media = axWindowsMediaPlayer1.newMedia(filePath); //参数为歌曲路径 playList.appendItem(media); if (axWindowsMediaPlayer1.playState == WMPLib.WMPPlayState.wmppsReady) { //捕获异常 并忽略异常 try { axWindowsMediaPlayer1.Ctlcontrols.play(); } catch (Exception) { } } }
阅读器其他细节完善
-
朗读的同时小说文本跟着进度高亮(richtext实现)
这里的实现主要是可以监控播放的状态改变事件,代码如下:
private void AxWindowsMediaPlayer1_PlayStateChange(object sender, AxWMPLib._WMPOCXEvents_PlayStateChangeEvent e) { //高亮文本 if (axWindowsMediaPlayer1.playState == WMPLib.WMPPlayState.wmppsTransitioning) { HighlightText(); } if (axWindowsMediaPlayer1.playState == WMPLib.WMPPlayState.wmppsMediaEnded) { played++; //最后一个 if (played > 0 && played == playList.count) { //清空播放列表 playList.clear(); keyValuePairs.Clear(); played = 0; richTextBox1.Text = string.Empty; //删除播放文件 BookInfo bookInfo = bindingSource2.DataSource as BookInfo; string dir = Path.Combine(Path.GetTempPath(), bookInfo.BookName); try { Directory.Delete(dir, true); } catch { } //自动播放下一章 bookInfo = listBox2.SelectedItem as BookInfo; ListbookInfos = listBox2.DataSource as List ; int index = bookInfos.IndexOf(bookInfo); if (index > 0 && index < bookInfos.Count - 1) { button3_Click(null, null); bookInfo = bindingSource2.DataSource as BookInfo; if (!backgroundWorker1.IsBusy) backgroundWorker1.RunWorkerAsync(bookInfo); } else { if (!backgroundWorker1.IsBusy) backgroundWorker1.RunWorkerAsync(new BookInfo() { BookName = "当前目录已播放完" }); } } } } /// /// 高亮显示文本 /// private void HighlightText() { if (keyValuePairs.ContainsKey(played)) { int[] selected = keyValuePairs[played]; if (richTextBox1.Text.Length >= selected[0] + selected[1]) { int index = richTextBox1.Find(richTextBox1.Text.Substring(selected[0], selected[1])); if (index >= 0) { richTextBox1.SelectionStart = selected[0]; richTextBox1.SelectionLength = played < (keyValuePairs.Count - 1) ? (keyValuePairs[played + 1][0] - selected[0]) : selected[1]; //richTextBox1.SelectionFont = new Font(richTextBox1.SelectionFont, FontStyle.Regular); richTextBox1.SelectionBackColor = SystemColors.Highlight; richTextBox1.SelectionColor = Color.White; } } } }
- 小说语速、语调和语音库切换(实现不实时,暂时未优化)
用到redis缓存配置(配置参数百度官网可以看到),后台请求api的参数动态redis获取实现
参数 | 可需 | 描述 |
---|---|---|
tex | 必填 | 合成的文本,使用UTF-8编码。小于2048个中文字或者英文数字。(文本在百度服务器内转换为GBK后,长度必须小于4096字节) |
tok | 必填 | 开放平台获取到的开发者access_token(见上面的“鉴权认证机制”段落) |
cuid | 必填 | 用户唯一标识,用来计算UV值。建议填写能区分用户的机器 MAC 地址或 IMEI 码,长度为60字符以内 |
ctp | 必填 | 客户端类型选择,web端填写固定值1 |
lan | 必填 | 固定值zh。语言选择,目前只有中英文混合模式,填写固定值zh |
spd | 选填 | 语速,取值0-15,默认为5中语速 |
pit | 选填 | 音调,取值0-15,默认为5中语调 |
vol | 选填 | 音量,取值0-15,默认为5中音量 |
per(基础音库) | 选填 | 度小宇=1,度小美=0,度逍遥=3,度丫丫=4 |
per(精品音库) | 选填 | 度博文=106,度小童=110,度小萌=111,度米朵=103,度小娇=5 |
aue | 选填 | 3为mp3格式(默认); 4为pcm-16k;5为pcm-8k;6为wav(内容同pcm-16k); 注意aue=4或者6是语音识别要求的格式,但是音频内容不是语音识别要求的自然人发音,所以识别效果会受影响。 |
代码如下:
private void FormSetting_Load(object sender, EventArgs e) { //初始化语音库 ListperInfos = new List (); perInfos.Add(new PerInfo() { Val = 0, Display = "度小美" }); perInfos.Add(new PerInfo() { Val = 1, Display = "度小宇" }); perInfos.Add(new PerInfo() { Val = 3, Display = "度逍遥" }); perInfos.Add(new PerInfo() { Val = 4, Display = "度丫丫" }); perInfos.Add(new PerInfo() { Val = 106, Display = "度博文" }); perInfos.Add(new PerInfo() { Val = 110, Display = "度小童" }); perInfos.Add(new PerInfo() { Val = 111, Display = "度小萌" }); perInfos.Add(new PerInfo() { Val = 103, Display = "度米朵" }); perInfos.Add(new PerInfo() { Val = 5, Display = "度小娇" }); per.DataSource = perInfos; per.DisplayMember = "Display"; per.ValueMember = "Val"; //初始化设置Redis foreach (Control ctl in groupBox1.Controls) { string key = ctl.Name; int val = redisConfigInfo._GetKey<int>(key); if (ctl.GetType()==typeof(TrackBar)) { TrackBar trackBar = ctl as TrackBar; if (val == default(int)) { val = 5; } trackBar.Value = val; } else if(ctl.GetType()==typeof(ComboBox)) { ComboBox comboBox = ctl as ComboBox; if (val == default(int)) { val = 0; } comboBox.SelectedValue = val; } } }
代码地址
完整github代码地址