飞道的博客

为了保护小姐姐的眼睛,我用自动化做了一款语音机器人

322人阅读  评论(0)

点击上方“AirPython”,选择“加为星标

第一时间关注 Python 技术干货!

1. 场景

最近一位小姐姐在微信上向我抱怨,说自己每天坐地铁上下班,路上会阅读一些好的文章来提升自己。

但上了一天的班,实在太累了;如果戴上耳机的同时,文章能自动阅读起来,就好了!

本篇文章将带大家用自动化技术,来实现这一功能。

2. 实现步骤

1 步,新建 Android 项目

使用 Android Studio 新建一个项目,并创建一个无障碍服务,设置只处理微信应用内的页面事件


   
  1. //新建一个服务
  2. public class MsgService extends AccessibilityService
  3. {
  4.   @Override
  5.   public void onAccessibilityEvent(AccessibilityEvent event)
  6.   {
  7.         
  8.   }
  9. }
  10. //通过packageNames指定只处理微信App页面事件
  11. <?xml version= "1.0" encoding= "utf-8"?>
  12. <accessibility-service xmlns:android= "http://schemas.android.com/apk/res/android"
  13.     android:accessibilityEventTypes= "typeWindowStateChanged"
  14.     android:accessibilityFeedbackType= "feedbackGeneric"
  15.     android:accessibilityFlags= "flagDefault"
  16.     android:canRetrieveWindowContent= "true"
  17.     android:description= "@string/desc"
  18.     android:notificationTimeout= "100"
  19.     android:packageNames= "com.tencent.mm" />

2 步,安装文字转语言引擎

由于系统内置的 Pico TTS 不支持中文,为了更好地将文字转为语音,这里先下载安装 Google 文字转语音 这款App,然后将首选引擎切换到 Google 文字转语言引擎

3 步,获取公众号文章内容

使用 Android SDK 自带的 uiautomatorviewer 打开某一篇公众号文章的页面元素树

通过分析,发现一篇文章的正文内容都包含在控件中 text 属性中,因此,我们只需要遍历出所有的控件,找出所有 text 属性不为空的内容。

需要注意的是,由于微信基于腾讯 X5 内核,内容包裹在 WebView 内部,直接获取控件是获取不到的,因此,需要在服务初始化的时候配置 flags 为增强


   
  1. //新建一个服务
  2. @Override
  3. protected void onServiceConnected()
  4. {
  5.     super.onServiceConnected();
  6.     AccessibilityServiceInfo serviceInfo =  new AccessibilityServiceInfo();
  7.     serviceInfo.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
  8.     serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
  9.     serviceInfo.packageNames =  new String[]{ "com.tencent.mm"};
  10.     serviceInfo.notificationTimeout =  100;
  11.      //保证能够获取到WebView内部的控件元素
  12.     serviceInfo.flags = serviceInfo.flags | AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
  13.     setServiceInfo(serviceInfo);
  14.     Toast.makeText(MsgService.this,  "连接服务成功",
  15.                 Toast.LENGTH_SHORT).show();
  16. }

接着,先找到 WebView 控件,然后遍历子元素,找出所有子元素 text 不为空的内容


   
  1.   /***
  2.   * 获取所有的文本内容
  3.   * @param webNode
  4.   * @return
  5.   */
  6. private void getAllContents(AccessibilityNodeInfo webNode)
  7. {
  8.      for ( int i =  0; i < webNode.getChildCount(); i++)
  9.     {
  10.         AccessibilityNodeInfo tempNode = webNode.getChild(i);
  11.         String id = tempNode.getViewIdResourceName();
  12.          //过滤
  13.          if (TextUtils.equals( "meta_content", id))
  14.         {
  15.              continue;
  16.         }
  17.         String tempContent = tempNode.getText().toString().trim();
  18.          //加入内容
  19.          if (!TextUtils.isEmpty(tempContent))
  20.         {
  21.             contents.add(tempContent);
  22.         }
  23.          //循环遍历
  24.          //判断是否有子节点
  25.          if (tempNode.getChildCount() >  0)
  26.         {
  27.              for ( int j =  0; j < tempNode.getChildCount(); j++)
  28.             {
  29.                 getAllContents(tempNode.getChild(j));
  30.             }
  31.         }
  32.     }
  33. }

最后,将文章内容 分段 存储到配置文件中


   
  1. StringBuilder sb =  new StringBuilder();
  2. for ( int i =  0; i < contents.size(); i++)
  3. {
  4.     sb. append(contents.get(i)). append( ";;;");
  5.     Log.d( "xag", contents.get(i));
  6. }
  7. Log.d( "xag""*******************获取完成*********************");
  8. //存储
  9. SpUtil.clear(BaseApplication.getInstance());
  10. SpUtil.put( "contents", sb.toString());

4 步,添加悬浮框

为了更加方便地管理语音播放功能,新建一个系统悬浮窗,并设置按钮的点击事件,即:点击关闭按钮可以关闭悬浮框;点击复选框,可以切换到播放、暂停状态


   
  1. # 悬浮框依赖
  2. implementation  'com.github.princekin-f:EasyFloat:1.3.2'
  3. //显示悬浮框
  4. private void initFloatDialog()
  5. {
  6.     View currentFLoat = EasyFloat.getAppFloatView( "readmsg");
  7.      if (null == currentFLoat)
  8.     {
  9.          //初始化悬浮框View,并新增回调事件
  10.         EasyFloat.with(this).setLayout(R.layout.float_test,  new OnInvokeView()
  11.         {
  12.             @Override
  13.             public void invoke(View view)
  14.             {
  15.                 ImageView close_iv = view.findViewById(R.id.ivClose);
  16.                 final CheckBox float_cb = view.findViewById(R.id.float_cb);
  17.                 close_iv.setOnClickListener( new View.OnClickListener()
  18.                 {
  19.                     @Override
  20.                     public void onClick(View v)
  21.                     {
  22.                         EasyFloat.dismissAppFloat( "readmsg");
  23.                     }
  24.                 });
  25.                  //播放、停止切换功能    
  26.                 float_cb.setOnCheckedChangeListener( new CompoundButton.OnCheckedChangeListener()
  27.                 {
  28.                     @Override
  29.                     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
  30.                     {
  31.                         
  32.                     }
  33.                 });
  34.             }
  35.         }).setShowPattern(ShowPattern.ALL_TIME)
  36.         .setTag( "readmsg")
  37.         .setAnimator( new DefaultAnimator())
  38.         .setGravity(Gravity.END | Gravity.CENTER_VERTICAL,  -2200).show();
  39.     }
  40.      if (!EasyFloat.isShow(this,  "readmsg"))
  41.     {
  42.         EasyFloat.showAppFloat( "readmsg");
  43.     }
  44. }

5 步,过滤页面

为了提升用户体验,可以对页面进行过滤,保证只有在文章页面的时候,才显示系统悬浮框


   
  1. # 事件总线依赖
  2. implementation  'org.simple:androideventbus:1.0.5.1'
  3. //如果是微信公众号文章页面
  4. if (TextUtils.equals(currentClassName, CLASS_NAME_PAGE_ARTICLE))
  5. {
  6.      //等待页面加载
  7.     try
  8.     {
  9.         Thread.sleep( 5000);
  10.     } catch (InterruptedException e)
  11.     {
  12.         e.printStackTrace();
  13.     }
  14.      //发送显示悬浮框的事件
  15.     EventBus.getDefault().post( new ShowFloatBean( true));
  16. }
  17. //订阅事件,显示或隐藏悬浮框
  18. @Subscriber
  19. private void changeFloatStatus(ShowFloatBean showFloatBean)
  20. {
  21.     Log.d( "xag""接受到事件,展示或者隐藏:" + showFloatBean.isShow());
  22.     boolean showFloat = showFloatBean.isShow();
  23.      if (showFloat)
  24.     {
  25.         initFloatDialog();
  26.     }  else
  27.     {
  28.         EasyFloat.dismissAppFloat( "readmsg");
  29.     }
  30. }

6 步,实例化 TTS 对象

在 Application 中为 TTS 指定语言,并实例化语音播放 TTS 对象


   
  1. //初始化TTS
  2. private void initTTS()
  3. {
  4.      //初始化tts监听对象
  5.     tts =  new TextToSpeech(this, onInitListener);
  6.      //语音音调调节
  7.     tts.setPitch( 1.0f);
  8.     
  9.      //语音音速
  10.     tts.setSpeechRate( 0.8f);
  11. }
  12. /***
  13.  * 播放方法的封装
  14.  */
  15. public void speakContent(String content)
  16. {
  17.      if (null == tts)
  18.     {
  19.         initTTS();
  20.     }
  21.      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
  22.     {
  23.         tts.speak(content, TextToSpeech.QUEUE_ADD, null, null);
  24.     }  else
  25.     {
  26.         tts.speak(content, TextToSpeech.QUEUE_ADD, null);
  27.     }
  28. }

7 步,播放内容

点击播放按钮,就可以将当前页面的内容分段读出来


   
  1. //播放或者停止播放
  2. if (isChecked)
  3. {
  4.     String content = SpUtil.get( "contents""");
  5.     String[] contents = content.split( ";;;");
  6.     
  7.      //注意太长没法直接播放
  8.      for (String item : contents)
  9.     {
  10.         BaseApplication.getInstance().speakContent(item);
  11.     }
  12. else
  13. {
  14.     BaseApplication.getInstance().stopSpeak();
  15. }

需要注意的是,如果文本太长,没法播放出来,这里是分段的内容从存储文件中取出来,然后分段读出来

3. 最后

经过上面 7 步操作,当打开任意一篇微信公众号文章,悬浮框会自动显示,带上耳机,点击播放按钮,文章内容就能自动读出来了。

我已经将全部源码上传到后台,关注公众号后回复「 语音机器人 」即可获得全部源码。

如果你觉得文章还不错,请大家点赞分享下。你的肯定是我最大的鼓励和支持。

留言送书


今日赠书:《Google Hacking 渗透性测试者的利剑》

今日留言主题

你用自动化做过哪些有用的东西?

推荐阅读

我花 1 分钟写了一段爬虫,帮助小姐姐解放了双手

为了追到小姐姐,我用 Python 制作了一个机器人

抖音上好看的小姐姐,Python给你都下载了

THANDKS

- End -


转载:https://blog.csdn.net/hsh881025/article/details/106088908
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场