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语音的主要内容,如果未能解决你的问题,请参考以下文章
短视频运营短视频剪辑 ③ ( 添加字幕 | 智能识别字幕 | 修改字幕 | 字幕预设 | 字幕换行 | 使用字幕作为封面主题 )