写在前面的话
上章说了智能爬取,拿到了网上小说的信息,这章开始利用这些数据进行智能朗读。搜索网上朗读的方法,主要包括微软自带的speeker,三方智能语音api。经过筛选,我选择了语音包还算丰富(主要妹子声音甜美)的百度api进行智能朗读(文本转语音,这里主要是MP3格式,wav貌似测试有问题),阅读功能用微软com自带的控件。
小说数据UI展示
小说的信息主要包括小说的基本信息,小说的章节信息,小说的文本详细信息。这里围绕这个,根据window form设计一个界面。
(本人非专业UI,界面丑陋请谅解)
- 小说列表UI展示
左侧展示数据库服务器里面的小说列表(数据绑定),代码如下:
DataSet dataSet= dbProvider.ExecuteDataSet($"select BookName,Id from BookBasic");
if (dataSet != null)
{
List<BookInfo> bookInfos = new List<BookInfo>();
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)
{
List<BookInfo> bookInfos = new List<BookInfo>();
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}");
List<BookInfo> bookInfos = new List<BookInfo>();
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),代码如下:
/// <summary>
/// 获取Token
/// </summary>
/// <param name="para_API_key"></param>
/// <param name="para_API_secret_key"></param>
/// <returns></returns>
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;
List<BookInfo> bookInfos = listBox2.DataSource as List<BookInfo>;
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 = "当前目录已播放完" });
}
}
}
}
/// <summary>
/// 高亮显示文本
/// </summary>
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)
{
//初始化语音库
List<PerInfo> perInfos = new List<PerInfo>();
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代码地址
来源:https://www.cnblogs.com/comicwang/p/12190845.html