android Paddle 视频字幕识别TTS语音

Posted SaiKeis

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android Paddle 视频字幕识别TTS语音相关的知识,希望对你有一定的参考价值。

OcrDemo

文章目录

介绍

本项目是给盲人提供的一款看电影的实时英文字幕读取的软件。
主要采用的技术:
MediaProjection截取 + AccessibilityService + 动态横竖屏切换实时读取 + 百度底层开源OCR + EventBus进程间通信 + 讯飞TTS语音合成
攻克的技术难点:

1,后台截取图片时,每次截取都会弹窗授权
2,获取不到播放页面
3,获取不到视频软件是否打开
4,使用handler导致性能卡顿
5,竖屏截图时,切换横屏时导致横屏截取的是竖屏的大小
6,AccessibilityService的onkeyeven方法始终无反应
7,横竖屏切换数据不同步 导致截图宽高出错

有技术大佬可以一起探讨,自动定位字幕位置的方法,原因有亮点:

  • 一视频播放页面无效文字太多,比如左上角影名,右下角广告,右上角动态广告并且这些不是固定的也就是说有的视频有有的没有。
  • 二视频字幕不固定,比如:综艺节目演员字幕在左下角,导演字幕在中间,还有后期剪辑上去的歪歪扭扭的字幕,电影在中间,还有在上部分屏幕中间,还有在视频右下角的不是字幕的版本号
  • 三如何去除无效文字,我们知道无效文字的位置但是机器不知道,并且ocr每次返回的文字坐标也不固定所以我现在采取的是找规律只找有效。

我查到了此大佬的方式:

  • https://blog.csdn.net/flavioy/article/details/120218378?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&utm_relevant_index=2
  • 但是我也不会用他的方式哈哈哈
  • 若有大佬支持我将感激万分。gitee联系我拉你进去

下面是演示视频:

基于paddle的文字互转语音

手势操作版本请查看我另一篇文章:

https://blog.csdn.net/qq_39469700/article/details/123880230 手势执行操作

1:主要代码

相关代码贴出来 因为涉及公司秘密 所以不能贴出全部代码 这是我封装的工具类

/**
* 截屏相关工具类
* 录屏没有封装
* 可以一次授权 连续截屏
*/
public class MediaProjrct implements ImageReader.OnImageAvailableListener 
   /**
    * 截屏管理器
    */
   private static MediaProjectionManager systemService;
   private MediaProjection mediaProjection;
   @SuppressLint("StaticFieldLeak")
   public static MediaProjrct mediaProjrct;
   private VirtualDisplay virtualDisplay;
   @SuppressLint("StaticFieldLeak")
   public static Activity activity;
   private static int densityDpi;

   /**
    * 类创建 就初始化相关权限
    */
   @RequiresApi(api = Build.VERSION_CODES.M)
   public void with(Activity activity) 
       if (mediaProjrct == null) 
           MediaProjrct.activity = activity;
           mediaProjrct = new MediaProjrct();
       
       initCutManger();
   

   /**
    * 获取截瓶权限
    *
    * @return
    */
   @RequiresApi(api = Build.VERSION_CODES.M)
   @SuppressLint("WrongConstant")
   public void initCutManger() 
       densityDpi = activity.getResources().getDisplayMetrics().densityDpi;
       systemService = (MediaProjectionManager) activity.getSystemService(MEDIA_PROJECTION_SERVICE);
       Intent screenCaptureIntent = systemService.createScreenCaptureIntent();
       activity.startActivityForResult(screenCaptureIntent, ApkNames.PERMISSION_CODE);
       EventBus.getDefault().register(this);
   

   /**
    * 交给actvity 在mainactivity中的相同重载的方法中使用
    */
   @RequiresApi(api = Build.VERSION_CODES.M)
   public void onActivityReust(int requestCode, int resultCode, Intent data) 
       if (requestCode != ApkNames.PERMISSION_CODE) 
           return;
       
       if (resultCode != RESULT_OK) 
           return;
       
       mediaProjection = systemService.getMediaProjection(resultCode, data);
   

   private int hight;

   @Subscribe(threadMode = ThreadMode.MAIN)
   public void eventPost(String mhight) 
       hight = Integer.parseInt(mhight);
       startScreenCapture();
   

   int screenW;
   int screenH;

   /**
    * 截屏
    * VirtualDisplay表示一个虚拟显示,显示的内容render到 createVirtualDisplay()参数的Surface。
    * 因为virtual display内容render到应用程序提供的surface,所以当进程终止时,它将会自动释放,并且所以剩余的窗口都会被强制删除。但是,你仍然需要在使用完后显式地调用release()方法。
    * 此处的 with  和 hight 表示
    * 截图的宽和高
    * 但是由于不匹配会有黑色边框  所以加入三木
    * 如果此类的mhight == hight 则直接获取屏幕的高度 否则创建的截图宽和高是匹配的 但是截取的图片是全屏  然后导致有黑色边框
    * 反之直接创建屏幕的大小
    * <p>
    * 问题场景
    * 1 竖屏状态时需要截取的是视频view的宽和高  但是surface创建的是屏幕的宽和高 所以导创建的宽和高是匹配的 但是截取的却是整个屏幕 而我们需要的是视频播放的view 所以就不符合我们需求
    * 2 横屏状态下 直接截取当前屏幕的宽和高 。
    * <p>
    * 以上会有一个问题   就是长时间  横屏或者竖屏 突然横屏或者竖屏会导致img buff缓冲区不足
    */
   public void startScreenCapture() 
       if (mediaProjection != null) 
           screenW = WindoesCut.getScreenW(activity);
           screenH = WindoesCut.getScreenH(activity);
           ImageReader mImageReader = ImageReader.newInstance(screenW, screenH, 0x1, 1);
           mImageReader.setOnImageAvailableListener(this, null);
           virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", screenW, screenH,
                   densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null);
       
   

   /**
    * 这里的一定要设置为virtualDisplay = null
    * 虽然他会每次使用结束自动释放 但是你还是需要手动释放
    * 否则导致 bitmap 花屏
    */
   private void stopScreenCapture() 
       if (virtualDisplay != null) 
           virtualDisplay.release();
           virtualDisplay = null;
           bitmap = null;
       
   

   private Bitmap bitmap;

   @Override
   public void onImageAvailable(ImageReader reader) 
       Image image = reader.acquireLatestImage();
       if (image != null) 
           final Image.Plane[] planes = image.getPlanes();
           if (planes.length > 0) 
               ByteBuffer buffer = planes[0].getBuffer();
               //每个像素的间距
               int pixelStride = planes[0].getPixelStride();
               //总的间距
               int rowStride = planes[0].getRowStride();
               int rowPadding1 = rowStride - pixelStride * screenW;
               if (bitmap == null) 
                   bitmap = Bitmap.createBitmap((screenW + (rowPadding1 / pixelStride)), screenH, Bitmap.Config.ARGB_8888);
               
               try 
                   bitmap.copyPixelsFromBuffer(buffer);
                   AuButBitmap auButBitmap = null;
                   if (auButBitmap == null) 
                       auButBitmap = new AuButBitmap();
                   
                   auButBitmap.setBitmap(bitmap);
                   auButBitmap.setHight(hight);
                   auButBitmap.setScreen(WindoesCut.isH(activity));
                   EventBus.getDefault().post(auButBitmap);
                catch (Exception e) 
                   int h = WindoesCut.isH(activity);
                   switch (h) 
                       case Surface.ROTATION_0:
                           Log.e("TAG", "onImageAvailable: buffer 异常当前竖屏" + hight);
                           break;
                       case Surface.ROTATION_90:
                       case Surface.ROTATION_270:
                           Log.e("TAG", "onImageAvailable: buffer 异常当前横屏" + hight);
                           break;
                   
               
               image.close();
               stopScreenCapture();
           
       
   

   /**
    * 此处的imageReader 和 okhttp的 body 一样  必须要获取一遍后再使用  否则会导致空指针
    * 因为imageReader 只能获取一次 所以要创建一个变量保存下来
    * stopScreenCapture()
    * <p>
    * 因为涉及到横竖屏幕的切换 所以要及时吧bitmap设置未null
    * 不然会出现  竖屏状态时 图片完好  但是切换到横屏时  横屏截取的图片却和竖屏一样 反之也是
    * 注意 这里的bitmapcreate的宽和高 并没有像上面一样进行判断  所以细节就在这个地方
    * 因为我们竖屏状态下截取的不是整个屏幕 所以我们要把surface的宽高 进行截取 截取的就是bitmap的高所以就会符合我们的需求
    * <p>
    * Buffer not large enough for pixels
    */



/**
* ocr识别文字的工具类
* 作用 去重等
*/
public class TextUtils 

   /**
    * 获取字符串相等的个数
    * @param s1
    * @param s2
    * @return
    */
   public static int getEquals(String s1, String s2) 
       if (isEmpty(s1) && isEmpty(s2)) 
           return getPercent(s1, s2);
       
       return 0;
   


   private static int getPercent(String s1, String s2) 
       int num = 0;
       int length1 = s1.length();
       int length2 = s2.length();
       for (int i = 0; i < length1; i++) 
           for (int j = 0; j < length2; j++) 
               char c1 = s1.charAt(i);
               char c2 = s2.charAt(j);
               if (c1 == c2) 
                   num++;
               
           
       
       NumberFormat numberFormat = NumberFormat.getInstance();
       numberFormat.setMaximumFractionDigits(0);
       return Integer.parseInt(numberFormat.format((float) num / (float) length1 * 100));
   

   /**
    * 判断字符串是否为空
    *
    * @param s
    * @return
    */
   public static boolean isEmpty(String s) 
       if (s == null || s.equals("")) 
           return true;
        else 
           return false;
       
   

   /**
    * 判断是否包含特殊字符
    *
    * @param str
    * @return
    */
   public static boolean isSpecialChar(String str) 
       String regEx = "[0o _`~!@#$%^&*()+=|':;',\\\\[\\\\]<>/?~!@#¥%……&*()——+|【】‘;:”“’。,、?]|\\n|\\r|\\t";
       Pattern p = Pattern.compile(regEx);
       Matcher m = p.matcher(str);
       return m.find();
   


  //字符串第一个字 是不是英文
   public static boolean isFristAZ(String s)
       String frist = s.substring(0, 1);
       boolean matches = frist.matches("[a-zA-Z]+");
       return matches;
   

  //字符串第一个字是不是数字
   public static boolean isFristNum(String s)
       String frist = s.substring(0, 1);
       boolean matches = frist.matches("[0-9]+");
       return matches;
   


   /**
    * 判断第一句话和第二句话是否差不多
    * @param s1
    * @param s2
    * @return
    */
   public static boolean isContain(String s1, String s2) 
       if (!isEmpty(s1) && !isEmpty(s2)) 
           if (s1.length() > s2.length()) 
               return s1.contains(s2);
            else 
               return s2.contains(s1);
           
       
       return false;
   




/**
* 屏幕相关工具
* user gewu
* time 22031815
*/
public class WindoesCut 

   /**
    * 获取的是视频view的宽
    * @param rect
    * @return
    */
   public static int getWith(Rect rect) 
       return rect.right - rect.left;
   

   /**
    * 获取的是视频的高
    * @param rect
    * @return
    */
   public static int gethight(Rect rect) 
       if (rect == null) 
           return 0;
       
       return rect.bottom - rect.top;
   

   /**
    * 裁剪一定高度保留下面
    * @param srcBitmap
    * @param needHeight
    * @return
    */
   public static Bitmap cropBitmapBottom(Bitmap srcBitmap, int needwith, int needHeight) 
       /**裁剪保留下部分的第一个像素的Y坐标*/
       int needY = srcBitmap.getHeight() - needHeight;
       /**裁剪关键步骤*/
       return Bitmap.createBitmap(srcBitmap, needwith, needY, srcBitmap.getWidth(), needHeight);
   


   /**
    * 获取的是屏幕
    * @param context
    * @return
    */
   private static DisplayMetrics getDisplayMetrics(Context context) 
       DisplayMetrics metrics = new DisplayMetrics();
       getDispaly(context).getMetrics(metrics);
       return metrics;
   

   /**
    * 获取屏幕管理器
    * @param context
    * @return
    */
   private static Display getDispaly(Context context) 
       WindowManager systemService = (WindowManager) (context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE));
       return systemService.getDefaultDisplay();
   

   /**
    * 获取屏幕宽度
    * @param context
    * @return
    */
   public static int getScreenW(Context context) 
       return getDisplayMetrics(context).widthPixels;
   

   /**
    * 获取屏幕高度
    * @param context
    * @return
    */
   public static int getScreenH(Context context) 
       return getDisplayMetrics(context).heightPixels;
   

   /**
    * 屏幕旋转角度
    * 如果屏幕旋转90°或者270°是判断为横屏
    */
   public static int isH(Context context) 
       int angle = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
       return angle;
   



2:获取视频的播放页面

这一块主要就是判断是否进入自己预定的视屏app 然后根据相关控件信息获取到播放视屏的view的rect 之后就可以获取到是视频view的宽度/高度

    /**
     * 获取视频view的rect
     */
    private void getVedioView() 
        AccessibilityNodeInfo rootInActiveWindow = getRootInActiveWindow();
        if (rootInActiveWindow != null && rootInActiveWindow.getChildCount() != 0) 
            for (int i = 0; i < rootInActiveWindow.getChildCount(); i++) 
                if (rootInActiveWindow.getChild(i).getChildCount() != 0) 
                    AccessibilityNodeInfo activeWindowChild = rootInActiveWindow.getChild(i);

                    int childCount = activeWindowChild.getChildCount();
                    for (int j = 0; j < childCount; j++) 
                        if (j + 2 > childCount || activeWindowChild.getChild(j).getClassName() == null) 
                            return;
                        
                        //这里的判断是 判断是否为播放视频的控件
                        if ((activeWindowChild.getChild(j).getClassName().equals("android.widget.RelativeLayout")
                                && activeWindowChild.getChild(j + 1).getClassName().equals("android.widget.TextView") && activeWindowChild.getChild(j + 2).getClassName().equals("android.widget.TextView"))
                                || (activeWindowChild.getChild(j).getClassName().equals("android.widget.RelativeLayout") && activeWindowChild.getChild(j + 1).getClassName().equals("android.widget.ImageView")
                                && activeWindowChild.getChild(j + 2).getClassName().equals("android.widget.TextView"))) 
                            AccessibilityNodeInfo child = activeWindowChild.getChild(j);
                            rect = new Rect();
                            child.getBoundsInScreen(rect);
                        
                    
                
            
        
    
3:判断是否在视频页面 并且监听是否实时横屏

这一块主要就是判断是否进入视频app 并且监听横竖屏 更改到service里面进行

    public void onAccessibilityEvent(AccessibilityEvent event) 
          //判断是否在腾讯视频&&是否在播放页面
          if (event.getPackageName().toString().equals(ApkNames.QQLIVE)) 
              if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) 
                  int isOnVideo = AccessHelper.getActivityName(event, this);
                  if (isOnVideo == 1) 
                      if (rect == null)
                          getVedioView();
                      
                      instion.setObj(WindoesCut.gethight(rect));
                  
              
          
      
  
      @Override
      public void onConfigurationChanged(Configuration newConfig) 
          super.onConfigurationChanged(newConfig);
          if (newConfig.orientation == 1) 
              instion.stopTimer();
              try 
                  Thread.sleep(1000);
               catch (InterruptedException e) 
                  e.printStackTrace();
              
              instion.setObj(WindoesCut.gethight(rect));
              instion.startTimer();
           else if (newConfig.orientation == 2) 
              instion.stopTimer();
              try 
                  Thread.sleep(1000);
               catch (InterruptedException e) 
                  e.printStackTrace();
              
              int screenH = WindoesCut.getScreenH(this);
              Log.i("test", "onConfigurationChanged: "+screenH);
              instion.setObj(screenH);
              instion.startTimer();
          
      
4:handler导致性能卡顿

导致handler的性能卡顿的原因就是,字幕是实时的ocr转换也是实时的,所以导致有时候,一个ocr转换的文字被message发送若干遍。后来我采用EVENBUS的方式返回相关的数据,最后放在mainactivity中进行相关数据操作,解决了数据不同步问题

         if (getIsRun()) 
             EventBus.getDefault().post(String.valueOf(mhight));
          else 
             Log.i("TAG", "run: ---------已经暂停......");
         

这里主要是MediaProjrct截屏后返回的bitmap 我们需要用到bitmap之后用ocr转换成文字,但是千万不要忘记销毁bitmap否则会消耗大量的资源。

        Bitmap bitmap1 = auButBitmap.getBitmap();
        int hight = auButBitmap.getHight();
        int screen = auButBitmap.getScreen();
        int screenH = WindoesCut.getScreenH(getApplicationContext());
        int screenW = WindoesCut.getScreenW(getApplicationContext());
        Bitmap bitmapBottom;
        switch (screen) 
            case Surface.ROTATION_0:
                bitmapBottom = Bitmap.createBitmap(bitmap1, 0, hight / 2, bitmap1.getWidth(), hight / 2);
                break;
            case Surface.ROTATION_90:
            case Surface.ROTATION_270:
                bitmapBottom = Bitmap.createBitmap(bitmap1, 0, bitmap1.getHeight() / 2, bitmap1.getWidth(), bitmap1.getHeight() / 2);
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + screen);
        
        //判断当前截图的高和宽  与  获取屏幕的高和宽是否有区别  如果横屏/竖屏下  截屏的宽/高比获取的屏幕的宽小 就说明截图不对
        if (bitmapBottom.getWidth() < screenW   ||  bitmapBottom.getHeight() > screenH)
            return;
        
//        ImageUtils.saveBitmap2file(bitmapBottom, getApplicationContext(), new Date().toString());
        recognitionText(bitmapBottom);
        bitmapBottom.recycle();

这里的识别文字用的是百度底层的PaddleLite开源框架,主要执行两个步骤,
1定位文字坐标,2返回文字内容
这里我把返回的文字进行了相关的处理:比如,包含,等于,相等率,非字符,英文等。第一次的想法是获取第一次的文字坐标Potion后根据第一次的坐标进行返回,
后来因为字幕的坐标的长度是不可控的,然后直接获取的视频高的2/3。

    runOnUiThread(new Runnable() 
               @Override
               public void run() 
                   predictor.setInputImage(bitmap);
                   boolean runModel = predictor.runModel();
                   if (runModel) 
                       List<String> textResult = predictor.getTextResult();
                       //既然有字幕 那么字幕的坐标一定不为null
                       if (textResult != null && textResult.size() != 0) 
                           oldtextResult = textResult.get(0);
   
                           if (TextUtils.isFristNum(oldtextResult) || TextUtils.isFristAZ(oldtextResult) || oldtextResult.equals(newtextResult)
                                   || oldtextResult.length() == 1  || oldtextResult.equals(ApkNames.TXLIVE)) 
                            else 
                               ttsUtils.startSpeech(oldtextResult);
                               newtextResult = oldtextResult;
                               Log.e("test", "handleMessage: 转换文字成功------字幕:" + oldtextResult);
                           
   
                           /**先判断是否包含上一次的文字
                            * 包含就是文字重复
                            * 不包含就进行操作
                            *
                            * 在进行判断是否包含特殊字符
                            * 包含就不操作
                            * 不包含就继续判断概率
                            *
                            */
                        else 
                           Log.i(TAG, "handleMessage: 转换文字失败");
                       
                   
               
           );
5:竖屏截图时,切换横屏时导致横屏截取的是竖屏的大小

如下:

    /**
     * 截屏
     * VirtualDisplay表示一个虚拟显示,显示的内容render到 createVirtualDisplay()参数的Surface。
     * 因为virtual display内容render到应用程序提供的surface,所以当进程终止时,它将会自动释放,并且所以剩余的窗口都会被强制删除。但是,你仍然需要在使用完后显式地调用release()方法。
     * 此处的 with  和 hight 表示
     * 截图的宽和高
     * 但是由于不匹配会有黑色边框  所以加入三木
     * 如果此类的mhight == hight 则直接获取屏幕的高度 否则创建的截图宽和高是匹配的 但是截取的图片是全屏  然后导致有黑色边框
     * 反之直接创建屏幕的大小
     * <p>
     * 问题场景
     * 1 竖屏状态时需要截取的是视频view的宽和高  但是surface创建的是屏幕的宽和高 所以导创建的宽和高是匹配的 但是截取的却是整个屏幕 而我们需要的是视频播放的view 所以就不符合我们需求
     * 2 横屏状态下 直接截取当前屏幕的宽和高 。
     * <p>
     * 以上会有一个问题   就是长时间  横屏或者竖屏 突然横屏或者竖屏会导致img buff缓冲区不足
     */
        public void startScreenCapture() 
            if (mediaProjection != null) 
                screenW = WindoesCut.getScreenW(activity);
                screenH = WindoesCut.getScreenH(activity);
                ImageReader mImageReader = ImageReader.newInstance(screenW, screenH, 0x1, 1);
                mImageReader.setOnImageAvailableListener(this, null);
                virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", screenW, screenH,
                        densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null);
            
        
6:AccessibilityService的onkeyeven方法始终无反应

查阅相关资料 android 10好像无解 如果您知道 请告诉我一下 谢谢。

7:其他相关代码

JNI层的java代码 主要就是获取文字坐标和文字

 public boolean runModel() 
        if (inputImage == null || !isLoaded()) 
            return false;
        
        Bitmap scaleImage = Utils.resizeWithStep(inputImage, Long.valueOf(inputShape[2]).intValue(), 32);
        int channels = (int) inputShape[1];
        int width = scaleImage.getWidth();
        int height = scaleImage.getHeight();
        float[] inputData = new float[channels * width * height];
        if (channels == 3) 
            int[] channelIdx = null;
            if (inputColorFormat.equalsIgnoreCase("RGB")) 
                channelIdx = new int[]0, 1, 2;
             else if (inputColorFormat.equalsIgnoreCase("BGR")) 
                channelIdx = new int[]2, 1, 0;
             else 
                return false;
            
            int[] channelStride = new int[]width * height, width * height * 2;
            int[] pixels = new int[width * height];
            scaleImage.getPixels(pixels, 0, scaleImage.getWidth(), 0, 0, scaleImage.getWidth(), scaleImage.getHeight());
            for (int i = 0; i < pixels.length; i++) 
                int color = pixels[i];
                float[] rgb = new float[](float) red(color) / 255.0f, (float) green(color) / 255.0f,
                        (float) blue(color) / 255.0f;
                inputData[i] = (rgb[channelIdx[0]] - inputMean[0]) / inputStd[0];
                inputData[i + channelStride[0]] = (rgb[channelIdx[1]] - inputMean[1]) / inputStd[1];
                inputData[i + channelStride[1]] = (rgb[channelIdx[2]] - inputMean[2]) / inputStd[2];
            
         else if (channels == 1) 
            int[] pixels = new int[width * height];
            scaleImage.getPixels(pixels, 0, scaleImage.getWidth(), 0, 0, scaleImage.getWidth(), scaleImage.getHeight());
            for (int i = 0; i < pixels.length; i++) 
                int color = pixels[i];
                float gray = (float) (red(color) + green(color) + blue(color)) / 3.0f / 255.0f;
                inputData[i] = (gray - inputMean[0]) / inputStd[0];
            
         else 
            return false;
        
        for (int i = 0; i < warmupIterNum; i++) 
            paddlePredictor.runImage(inputData, width, height, channels, inputImage);
        
        warmupIterNum = 0;
        results = paddlePredictor.runImage(inputData, width, height, channels, inputImage);
        results = postprocess(results);

        if (inputImage!=null)
            inputImage .recycle();
        
        return true;
    

    private ArrayList<OcrResultModel> postprocess(ArrayList<OcrResultModel> results) 
        for (OcrResultModel r : results) 
            StringBuffer word = new StringBuffer();
            for (int index : r.getWordIndex()) 
                if (index >= 0 && index < wordLabels.size()) 
                    word.append(wordLabels.get(index));
                 else 
                    Log.e(TAG, "Word index is not in label list:" + index);
                    word.append("×");
                
            
            r.setLabel(word.toString());
        
        return results;
    


    public boolean isLoaded() 
        return paddlePredictor != null && isLoaded;
    

    public void setInputImage(Bitmap image) 
        if (image == null) 
            return;
        
        this.inputImage = image.copy(Bitmap.Config.ARGB_8888, true);
    

    private List<String> textResults(ArrayList<OcrResultModel> results) 
        List<String> stringList = new ArrayList<>();
        for (int i = 0; i < results.size(); i++) 
            OcrResultModel result = results.get(i);
            stringList.add(result.getLabel());
        
        return stringList;
    

    private List<Point> potinResults(ArrayList<OcrResultModel> results) 
        List<Point> points = new ArrayList<>();
        for (int i = 0; i < results.size(); i++) 
            OcrResultModel result = results.get(i);
            points.addAll(result.getPoints());
        
        return points;
    

    public List<String> getTextResult() 
        return textResults(results);
    

    public List<Point> getTextPotion() 
        return potinResults(results);
    

自定义定时器


/**
 * 自定义简单计时器
 * user : gewu
 * time : 22031709
 */
public class TimerTasks extends java.util.TimerTask 

    private static boolean isRun;
    public static Timer timer;
    @SuppressLint("StaticFieldLeak")
    private static TimerTasks timerTasks;
    @SuppressLint("StaticFieldLeak")
    private int mhight;

    public static TimerTasks getInstion() 
        if (timer == null) 
            timer = new Timer();
        
        if (timerTasks == null) 
            timerTasks = new TimerTasks();
        
        return timerTasks;
    

    @Override
    public void run() 
        if (getIsRun()) 
            EventBus.getDefault().post(String.valueOf(mhight));
         else 
            Log.i("TAG", "run: ---------已经暂停......");
        
    

    /**
     * 先获取timer的运行状态
     * 如果不在运行 就直接设置为他的反
     */
    public void startTimer() 
        if (!getIsRun()) 
            isRun = true;
        
    

    /**
     * 暂停
     */
    public void stopTimer() 
        isRun = false;
    

    /**
     * 设置定时器的间隔时间
     */
    public void setTimer(int delay, int time) 
        if (timer == null) 
            Log.e("timer", "设置Timer: timer is null......");
            return;
        
        timer.schedule(timerTasks, delay, time);
    

    public void setObj(int hight) 
        this.mhight = hight;
    

    /**
     * 获取当前的定时器状态
     */
    protected boolean getIsRun() 
        return isRun;
    

    /**
     * 直接销毁定时器
     */
    public void cancelTimer() 
        if (timer == null) 
            Log.e("timer", "销毁Timer: timer is null......");
            return;
        
        stopTimer();
        timer.purge();
        timer.cancel();
        timer = null;
        timerTasks = null;
    


软件架构

最好的设计就是没有设计,本项目是mvc架构。
主要就是服务层和数据层获取到相关内容通知Activity进行更新。
没有UI 没有过多的页面绘制,主要的全部在逻辑后台。纯离线方式的实时字幕识别。

安装教程

打开本项目,里面有apk

使用说明

1,打开软件
2,点击获取两个权限
3,保持后台运行
4,打开相关视频app就OK了

参与贡献

如果您需要,可在此项目上加入实时翻译功能。
目前正在 训练文字识别模型 uping…

流程图

短视频运营短视频剪辑 ③ ( 添加字幕 | 智能识别字幕 | 修改字幕 | 字幕预设 | 字幕换行 | 使用字幕作为封面主题 )

文章目录





一、添加字幕 ( 智能识别字幕 )



在 素材 面板中 , 选择 " 文本 " 选项卡 , " 智能字幕 " , 然后选择 " 识别字幕 " , 即可设置字幕 ;

点击开始识别后 , 会将视频中的人声 , 自动转为字幕 ;

如果视频中没有人声 , 会提示 , 该视频没有人声 , 未识别到字幕 ;

如果成功识别出字幕 , 会显示如下内容 , 在时间轴视频的上方 , 会出现 TI 字幕对应的时间轴 ;





二、修改字幕 ( 字幕预设 | 字幕换行 )



在 " 时间轴 " 上 , 选择 智能识别 的字幕 , 可以在右上角的 " 文本 " 面板 , 修改字幕的文字 , 字体 , 样式 , 颜色 , 预设 等属性 ;

选择 预设样式 , 字幕就会变成如下样式 :

如果觉得文本太长 , 可以在 文本 中 , 进行换行操作 ;





三、使用字幕作为封面主题



在 左上角 素材库中 文本 选项卡 中 , 选择 " 新建文本 " , 然后选择 " 默认文本 " , 点击默认文本 右下角的 加号 按钮 , 将其添加到轨道中 , 然后拖动该字幕位于视频的位置 ;

右上角的 面板中 , 编辑该字幕内容 , 为字幕选择样式 , 最终在 播放器 中查看该 视频标题 字幕的样式 ;

以上是关于android Paddle 视频字幕识别TTS语音的主要内容,如果未能解决你的问题,请参考以下文章

Android TTS 文字转语音,中文需第三方插件

短视频运营短视频剪辑 ③ ( 添加字幕 | 智能识别字幕 | 修改字幕 | 字幕预设 | 字幕换行 | 使用字幕作为封面主题 )

如何在 YouTube api v3 中获取 YouTube 视频的 cc 字幕

抖音里字幕识别失败怎么办

android ffmpeg视频添加字幕和配音

VC++基于微软语音引擎开发语音识别总结