scrapy+baiduapi搭建一个私人小说阅读器(智能爬取加智能朗读)(二)

写在前面的话

上章说了智能爬取,拿到了网上小说的信息,这章开始利用这些数据进行智能朗读。搜索网上朗读的方法,主要包括微软自带的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代码地址

  

 

posted @ 2020-01-14 10:53  comicwang  阅读(847)  评论(0编辑  收藏  举报