@ -0,0 +1,47 @@ |
|||||
|
name: Build&test video-srt-windows |
||||
|
|
||||
|
on: |
||||
|
pull_request: |
||||
|
branches: |
||||
|
- master |
||||
|
- test |
||||
|
- i18n |
||||
|
push: |
||||
|
branches: |
||||
|
- master |
||||
|
- test |
||||
|
- i18n |
||||
|
|
||||
|
|
||||
|
jobs: |
||||
|
Build: |
||||
|
runs-on: windows-latest |
||||
|
strategy: |
||||
|
matrix: |
||||
|
ver: [ 'test','build' ] |
||||
|
go: [ '1.15','1.14', '1.13' ] |
||||
|
name: ${{ matrix.ver }} video-srt-windows With Go${{ matrix.go }} |
||||
|
steps: |
||||
|
- uses: actions/checkout@v2 |
||||
|
|
||||
|
- name: Setup go ${{ matrix.go }} |
||||
|
uses: actions/setup-go@v2 |
||||
|
with: |
||||
|
go-version: ${{ matrix.go }} |
||||
|
|
||||
|
|
||||
|
- name: ${{ matrix.ver }} video-srt-windows in go ${{ matrix.go }} |
||||
|
run: go ${{ matrix.ver }} |
||||
|
- name: Cache local go package |
||||
|
uses: actions/cache@v2 |
||||
|
with: |
||||
|
path: ~/go/pkg/mod |
||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} |
||||
|
restore-keys: | |
||||
|
${{ runner.os }}-go- |
||||
|
- name: Artifact Pack |
||||
|
uses: actions/upload-artifact@v2 |
||||
|
with: |
||||
|
name: videosrt-${{ matrix.ver }}-${{ matrix.go }}.exe |
||||
|
path: videosrt.exe |
||||
|
|
@ -0,0 +1,6 @@ |
|||||
|
tests |
||||
|
data/json* |
||||
|
go.sum |
||||
|
*.zip |
||||
|
*.exe |
||||
|
.idea |
@ -0,0 +1,339 @@ |
|||||
|
GNU GENERAL PUBLIC LICENSE |
||||
|
Version 2, June 1991 |
||||
|
|
||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc., |
||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
||||
|
Everyone is permitted to copy and distribute verbatim copies |
||||
|
of this license document, but changing it is not allowed. |
||||
|
|
||||
|
Preamble |
||||
|
|
||||
|
The licenses for most software are designed to take away your |
||||
|
freedom to share and change it. By contrast, the GNU General Public |
||||
|
License is intended to guarantee your freedom to share and change free |
||||
|
software--to make sure the software is free for all its users. This |
||||
|
General Public License applies to most of the Free Software |
||||
|
Foundation's software and to any other program whose authors commit to |
||||
|
using it. (Some other Free Software Foundation software is covered by |
||||
|
the GNU Lesser General Public License instead.) You can apply it to |
||||
|
your programs, too. |
||||
|
|
||||
|
When we speak of free software, we are referring to freedom, not |
||||
|
price. Our General Public Licenses are designed to make sure that you |
||||
|
have the freedom to distribute copies of free software (and charge for |
||||
|
this service if you wish), that you receive source code or can get it |
||||
|
if you want it, that you can change the software or use pieces of it |
||||
|
in new free programs; and that you know you can do these things. |
||||
|
|
||||
|
To protect your rights, we need to make restrictions that forbid |
||||
|
anyone to deny you these rights or to ask you to surrender the rights. |
||||
|
These restrictions translate to certain responsibilities for you if you |
||||
|
distribute copies of the software, or if you modify it. |
||||
|
|
||||
|
For example, if you distribute copies of such a program, whether |
||||
|
gratis or for a fee, you must give the recipients all the rights that |
||||
|
you have. You must make sure that they, too, receive or can get the |
||||
|
source code. And you must show them these terms so they know their |
||||
|
rights. |
||||
|
|
||||
|
We protect your rights with two steps: (1) copyright the software, and |
||||
|
(2) offer you this license which gives you legal permission to copy, |
||||
|
distribute and/or modify the software. |
||||
|
|
||||
|
Also, for each author's protection and ours, we want to make certain |
||||
|
that everyone understands that there is no warranty for this free |
||||
|
software. If the software is modified by someone else and passed on, we |
||||
|
want its recipients to know that what they have is not the original, so |
||||
|
that any problems introduced by others will not reflect on the original |
||||
|
authors' reputations. |
||||
|
|
||||
|
Finally, any free program is threatened constantly by software |
||||
|
patents. We wish to avoid the danger that redistributors of a free |
||||
|
program will individually obtain patent licenses, in effect making the |
||||
|
program proprietary. To prevent this, we have made it clear that any |
||||
|
patent must be licensed for everyone's free use or not licensed at all. |
||||
|
|
||||
|
The precise terms and conditions for copying, distribution and |
||||
|
modification follow. |
||||
|
|
||||
|
GNU GENERAL PUBLIC LICENSE |
||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION |
||||
|
|
||||
|
0. This License applies to any program or other work which contains |
||||
|
a notice placed by the copyright holder saying it may be distributed |
||||
|
under the terms of this General Public License. The "Program", below, |
||||
|
refers to any such program or work, and a "work based on the Program" |
||||
|
means either the Program or any derivative work under copyright law: |
||||
|
that is to say, a work containing the Program or a portion of it, |
||||
|
either verbatim or with modifications and/or translated into another |
||||
|
language. (Hereinafter, translation is included without limitation in |
||||
|
the term "modification".) Each licensee is addressed as "you". |
||||
|
|
||||
|
Activities other than copying, distribution and modification are not |
||||
|
covered by this License; they are outside its scope. The act of |
||||
|
running the Program is not restricted, and the output from the Program |
||||
|
is covered only if its contents constitute a work based on the |
||||
|
Program (independent of having been made by running the Program). |
||||
|
Whether that is true depends on what the Program does. |
||||
|
|
||||
|
1. You may copy and distribute verbatim copies of the Program's |
||||
|
source code as you receive it, in any medium, provided that you |
||||
|
conspicuously and appropriately publish on each copy an appropriate |
||||
|
copyright notice and disclaimer of warranty; keep intact all the |
||||
|
notices that refer to this License and to the absence of any warranty; |
||||
|
and give any other recipients of the Program a copy of this License |
||||
|
along with the Program. |
||||
|
|
||||
|
You may charge a fee for the physical act of transferring a copy, and |
||||
|
you may at your option offer warranty protection in exchange for a fee. |
||||
|
|
||||
|
2. You may modify your copy or copies of the Program or any portion |
||||
|
of it, thus forming a work based on the Program, and copy and |
||||
|
distribute such modifications or work under the terms of Section 1 |
||||
|
above, provided that you also meet all of these conditions: |
||||
|
|
||||
|
a) You must cause the modified files to carry prominent notices |
||||
|
stating that you changed the files and the date of any change. |
||||
|
|
||||
|
b) You must cause any work that you distribute or publish, that in |
||||
|
whole or in part contains or is derived from the Program or any |
||||
|
part thereof, to be licensed as a whole at no charge to all third |
||||
|
parties under the terms of this License. |
||||
|
|
||||
|
c) If the modified program normally reads commands interactively |
||||
|
when run, you must cause it, when started running for such |
||||
|
interactive use in the most ordinary way, to print or display an |
||||
|
announcement including an appropriate copyright notice and a |
||||
|
notice that there is no warranty (or else, saying that you provide |
||||
|
a warranty) and that users may redistribute the program under |
||||
|
these conditions, and telling the user how to view a copy of this |
||||
|
License. (Exception: if the Program itself is interactive but |
||||
|
does not normally print such an announcement, your work based on |
||||
|
the Program is not required to print an announcement.) |
||||
|
|
||||
|
These requirements apply to the modified work as a whole. If |
||||
|
identifiable sections of that work are not derived from the Program, |
||||
|
and can be reasonably considered independent and separate works in |
||||
|
themselves, then this License, and its terms, do not apply to those |
||||
|
sections when you distribute them as separate works. But when you |
||||
|
distribute the same sections as part of a whole which is a work based |
||||
|
on the Program, the distribution of the whole must be on the terms of |
||||
|
this License, whose permissions for other licensees extend to the |
||||
|
entire whole, and thus to each and every part regardless of who wrote it. |
||||
|
|
||||
|
Thus, it is not the intent of this section to claim rights or contest |
||||
|
your rights to work written entirely by you; rather, the intent is to |
||||
|
exercise the right to control the distribution of derivative or |
||||
|
collective works based on the Program. |
||||
|
|
||||
|
In addition, mere aggregation of another work not based on the Program |
||||
|
with the Program (or with a work based on the Program) on a volume of |
||||
|
a storage or distribution medium does not bring the other work under |
||||
|
the scope of this License. |
||||
|
|
||||
|
3. You may copy and distribute the Program (or a work based on it, |
||||
|
under Section 2) in object code or executable form under the terms of |
||||
|
Sections 1 and 2 above provided that you also do one of the following: |
||||
|
|
||||
|
a) Accompany it with the complete corresponding machine-readable |
||||
|
source code, which must be distributed under the terms of Sections |
||||
|
1 and 2 above on a medium customarily used for software interchange; or, |
||||
|
|
||||
|
b) Accompany it with a written offer, valid for at least three |
||||
|
years, to give any third party, for a charge no more than your |
||||
|
cost of physically performing source distribution, a complete |
||||
|
machine-readable copy of the corresponding source code, to be |
||||
|
distributed under the terms of Sections 1 and 2 above on a medium |
||||
|
customarily used for software interchange; or, |
||||
|
|
||||
|
c) Accompany it with the information you received as to the offer |
||||
|
to distribute corresponding source code. (This alternative is |
||||
|
allowed only for noncommercial distribution and only if you |
||||
|
received the program in object code or executable form with such |
||||
|
an offer, in accord with Subsection b above.) |
||||
|
|
||||
|
The source code for a work means the preferred form of the work for |
||||
|
making modifications to it. For an executable work, complete source |
||||
|
code means all the source code for all modules it contains, plus any |
||||
|
associated interface definition files, plus the scripts used to |
||||
|
control compilation and installation of the executable. However, as a |
||||
|
special exception, the source code distributed need not include |
||||
|
anything that is normally distributed (in either source or binary |
||||
|
form) with the major components (compiler, kernel, and so on) of the |
||||
|
operating system on which the executable runs, unless that component |
||||
|
itself accompanies the executable. |
||||
|
|
||||
|
If distribution of executable or object code is made by offering |
||||
|
access to copy from a designated place, then offering equivalent |
||||
|
access to copy the source code from the same place counts as |
||||
|
distribution of the source code, even though third parties are not |
||||
|
compelled to copy the source along with the object code. |
||||
|
|
||||
|
4. You may not copy, modify, sublicense, or distribute the Program |
||||
|
except as expressly provided under this License. Any attempt |
||||
|
otherwise to copy, modify, sublicense or distribute the Program is |
||||
|
void, and will automatically terminate your rights under this License. |
||||
|
However, parties who have received copies, or rights, from you under |
||||
|
this License will not have their licenses terminated so long as such |
||||
|
parties remain in full compliance. |
||||
|
|
||||
|
5. You are not required to accept this License, since you have not |
||||
|
signed it. However, nothing else grants you permission to modify or |
||||
|
distribute the Program or its derivative works. These actions are |
||||
|
prohibited by law if you do not accept this License. Therefore, by |
||||
|
modifying or distributing the Program (or any work based on the |
||||
|
Program), you indicate your acceptance of this License to do so, and |
||||
|
all its terms and conditions for copying, distributing or modifying |
||||
|
the Program or works based on it. |
||||
|
|
||||
|
6. Each time you redistribute the Program (or any work based on the |
||||
|
Program), the recipient automatically receives a license from the |
||||
|
original licensor to copy, distribute or modify the Program subject to |
||||
|
these terms and conditions. You may not impose any further |
||||
|
restrictions on the recipients' exercise of the rights granted herein. |
||||
|
You are not responsible for enforcing compliance by third parties to |
||||
|
this License. |
||||
|
|
||||
|
7. If, as a consequence of a court judgment or allegation of patent |
||||
|
infringement or for any other reason (not limited to patent issues), |
||||
|
conditions are imposed on you (whether by court order, agreement or |
||||
|
otherwise) that contradict the conditions of this License, they do not |
||||
|
excuse you from the conditions of this License. If you cannot |
||||
|
distribute so as to satisfy simultaneously your obligations under this |
||||
|
License and any other pertinent obligations, then as a consequence you |
||||
|
may not distribute the Program at all. For example, if a patent |
||||
|
license would not permit royalty-free redistribution of the Program by |
||||
|
all those who receive copies directly or indirectly through you, then |
||||
|
the only way you could satisfy both it and this License would be to |
||||
|
refrain entirely from distribution of the Program. |
||||
|
|
||||
|
If any portion of this section is held invalid or unenforceable under |
||||
|
any particular circumstance, the balance of the section is intended to |
||||
|
apply and the section as a whole is intended to apply in other |
||||
|
circumstances. |
||||
|
|
||||
|
It is not the purpose of this section to induce you to infringe any |
||||
|
patents or other property right claims or to contest validity of any |
||||
|
such claims; this section has the sole purpose of protecting the |
||||
|
integrity of the free software distribution system, which is |
||||
|
implemented by public license practices. Many people have made |
||||
|
generous contributions to the wide range of software distributed |
||||
|
through that system in reliance on consistent application of that |
||||
|
system; it is up to the author/donor to decide if he or she is willing |
||||
|
to distribute software through any other system and a licensee cannot |
||||
|
impose that choice. |
||||
|
|
||||
|
This section is intended to make thoroughly clear what is believed to |
||||
|
be a consequence of the rest of this License. |
||||
|
|
||||
|
8. If the distribution and/or use of the Program is restricted in |
||||
|
certain countries either by patents or by copyrighted interfaces, the |
||||
|
original copyright holder who places the Program under this License |
||||
|
may add an explicit geographical distribution limitation excluding |
||||
|
those countries, so that distribution is permitted only in or among |
||||
|
countries not thus excluded. In such case, this License incorporates |
||||
|
the limitation as if written in the body of this License. |
||||
|
|
||||
|
9. The Free Software Foundation may publish revised and/or new versions |
||||
|
of the General Public License from time to time. Such new versions will |
||||
|
be similar in spirit to the present version, but may differ in detail to |
||||
|
address new problems or concerns. |
||||
|
|
||||
|
Each version is given a distinguishing version number. If the Program |
||||
|
specifies a version number of this License which applies to it and "any |
||||
|
later version", you have the option of following the terms and conditions |
||||
|
either of that version or of any later version published by the Free |
||||
|
Software Foundation. If the Program does not specify a version number of |
||||
|
this License, you may choose any version ever published by the Free Software |
||||
|
Foundation. |
||||
|
|
||||
|
10. If you wish to incorporate parts of the Program into other free |
||||
|
programs whose distribution conditions are different, write to the author |
||||
|
to ask for permission. For software which is copyrighted by the Free |
||||
|
Software Foundation, write to the Free Software Foundation; we sometimes |
||||
|
make exceptions for this. Our decision will be guided by the two goals |
||||
|
of preserving the free status of all derivatives of our free software and |
||||
|
of promoting the sharing and reuse of software generally. |
||||
|
|
||||
|
NO WARRANTY |
||||
|
|
||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY |
||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN |
||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES |
||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED |
||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS |
||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE |
||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, |
||||
|
REPAIR OR CORRECTION. |
||||
|
|
||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR |
||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, |
||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING |
||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED |
||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY |
||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER |
||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE |
||||
|
POSSIBILITY OF SUCH DAMAGES. |
||||
|
|
||||
|
END OF TERMS AND CONDITIONS |
||||
|
|
||||
|
How to Apply These Terms to Your New Programs |
||||
|
|
||||
|
If you develop a new program, and you want it to be of the greatest |
||||
|
possible use to the public, the best way to achieve this is to make it |
||||
|
free software which everyone can redistribute and change under these terms. |
||||
|
|
||||
|
To do so, attach the following notices to the program. It is safest |
||||
|
to attach them to the start of each source file to most effectively |
||||
|
convey the exclusion of warranty; and each file should have at least |
||||
|
the "copyright" line and a pointer to where the full notice is found. |
||||
|
|
||||
|
<one line to give the program's name and a brief idea of what it does.> |
||||
|
Copyright (C) <year> <name of author> |
||||
|
|
||||
|
This program is free software; you can redistribute it and/or modify |
||||
|
it under the terms of the GNU General Public License as published by |
||||
|
the Free Software Foundation; either version 2 of the License, or |
||||
|
(at your option) any later version. |
||||
|
|
||||
|
This program is distributed in the hope that it will be useful, |
||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
GNU General Public License for more details. |
||||
|
|
||||
|
You should have received a copy of the GNU General Public License along |
||||
|
with this program; if not, write to the Free Software Foundation, Inc., |
||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
||||
|
|
||||
|
Also add information on how to contact you by electronic and paper mail. |
||||
|
|
||||
|
If the program is interactive, make it output a short notice like this |
||||
|
when it starts in an interactive mode: |
||||
|
|
||||
|
Gnomovision version 69, Copyright (C) year name of author |
||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. |
||||
|
This is free software, and you are welcome to redistribute it |
||||
|
under certain conditions; type `show c' for details. |
||||
|
|
||||
|
The hypothetical commands `show w' and `show c' should show the appropriate |
||||
|
parts of the General Public License. Of course, the commands you use may |
||||
|
be called something other than `show w' and `show c'; they could even be |
||||
|
mouse-clicks or menu items--whatever suits your program. |
||||
|
|
||||
|
You should also get your employer (if you work as a programmer) or your |
||||
|
school, if any, to sign a "copyright disclaimer" for the program, if |
||||
|
necessary. Here is a sample; alter the names: |
||||
|
|
||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program |
||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker. |
||||
|
|
||||
|
<signature of Ty Coon>, 1 April 1989 |
||||
|
Ty Coon, President of Vice |
||||
|
|
||||
|
This General Public License does not permit incorporating your program into |
||||
|
proprietary programs. If your program is a subroutine library, you may |
||||
|
consider it more useful to permit linking proprietary applications with the |
||||
|
library. If this is what you want to do, use the GNU Lesser General |
||||
|
Public License instead of this License. |
@ -0,0 +1,116 @@ |
|||||
|
# VideoSrt |
||||
|
|
||||
|
[![GitHub license](https://img.shields.io/github/license/wxbool/video-srt-windows?logo=Apache&style=flat-square)](https://github.com/wxbool/video-srt-windows/blob/master/LICENSE) |
||||
|
[![GitHub stars](https://img.shields.io/github/stars/wxbool/video-srt-windows?style=flat-square)](https://github.com/wxbool/video-srt-windows/stargazers) |
||||
|
[![GitHub forks](https://img.shields.io/github/forks/wxbool/video-srt-windows?logo=github&style=flat-square)](https://github.com/wxbool/video-srt-windows/network) |
||||
|
![GitHub Action](https://github.com/wxbool/video-srt-windows/workflows/Go/badge.svg?branch=master) |
||||
|
|
||||
|
## 简介 |
||||
|
|
||||
|
`VideoSrt` 是用 `Golang`语言,基于 [lxn/walk](https://github.com/lxn/walk) Windows-GUI 工具包开发。 |
||||
|
|
||||
|
这是一个可以识别视频语音自动生成字幕SRT文件的开源软件工具。<br />适用于快速、批量的为媒体(视频/音频)生成中/英文字幕、文本文件的业务场景。 |
||||
|
|
||||
|
0.3.3 版本将会使用以下接口: |
||||
|
- 阿里云 [OSS对象存储](https://www.aliyun.com/product/oss?spm=5176.12825654.eofdhaal5.13.e9392c4aGfj5vj&aly_as=K11FcpO8) |
||||
|
- 阿里云 [录音文件识别](https://ai.aliyun.com/nls/filetrans?spm=5176.12061031.1228726.1.47fe3cb43I34mn) |
||||
|
- 百度翻译开放平台 [翻译API](http://api.fanyi.baidu.com/api/trans/product/index) |
||||
|
- 腾讯云 [翻译API](https://cloud.tencent.com/product/tmt) |
||||
|
|
||||
|
软件帮助文档/使用教程看这个:[https://www.yuque.com/viggo-t7cdi/videosrt](https://www.yuque.com/viggo-t7cdi/videosrt) |
||||
|
|
||||
|
B站Up主自制教程:[https://search.bilibili.com/all?keyword=videosrt](https://search.bilibili.com/all?keyword=videosrt) |
||||
|
|
||||
|
线上“字幕生成/字幕翻译”解决方案:[字幕酱(付费)](https://www.zimujiang.com/aff?code=aannv4os) |
||||
|
|
||||
|
线上“文本配音/字幕配音/文章转视频”解决方案:[幕言](https://www.mu-yan.com/?videosrt) |
||||
|
|
||||
|
<a name="0b884e4f"></a> |
||||
|
## 界面预览 |
||||
|
|
||||
|
![](https://pic.downk.cc/item/5f28c7ea14195aa594369a59.gif) |
||||
|
|
||||
|
## 应用场景 |
||||
|
|
||||
|
- 识别**视频/音频**的语音生成字幕文件(支持中英互译,双语字幕) |
||||
|
- 提取**视频/音频**的语音文本 |
||||
|
- 批量翻译、过滤处理/编码SRT字幕文件 |
||||
|
|
||||
|
|
||||
|
<a name="b89d37d3"></a> |
||||
|
## 软件优势 |
||||
|
|
||||
|
- 使用阿里云语音识别接口,准确度高,标准普通话/英语识别率95%以上 |
||||
|
- 视频识别无需上传原视频,方便快速且节省时间 |
||||
|
- 支持多任务多文件批量处理 |
||||
|
- 支持视频、音频常见多种格式文件 |
||||
|
- 支持同时输出字幕SRT文件、LRC文件、普通文本3种类型 |
||||
|
- 支持语气词过滤、自定义文本过滤、正则过滤等,使软件生成的字幕更加精准 |
||||
|
- 支持字幕中英互译、双语字幕输出,及日语、韩语、法语、德语、西班牙语、俄语、意大利语、泰语等 |
||||
|
- 支持多翻译引擎(百度翻译、腾讯云翻译) |
||||
|
- 支持批量翻译、编码SRT字幕文件 |
||||
|
|
||||
|
<a name="Download"></a> |
||||
|
## Download |
||||
|
|
||||
|
<a name="e66a66f1"></a> |
||||
|
##### 下载地址: |
||||
|
- (v0.3.3)(含ffmpeg依赖) [点我下载](http://file.viggo.site/video-srt/0.3.3/video-srt-gui-ffmpeg-0.3.3-x64.zip) |
||||
|
- (v0.3.3)(不含ffmpeg依赖) [点我下载](http://file.viggo.site/video-srt/0.3.3/video-srt-gui-0.3.3-x64.zip) |
||||
|
- (v0.2.6)(含ffmpeg依赖) [点我下载](http://file.viggo.site/video-srt/0.2.6/video-srt-gui-ffmpeg-0.2.6-x64.zip) |
||||
|
- (v0.2.6)(不含ffmpeg依赖) [点我下载](http://file.viggo.site/video-srt/0.2.6/video-srt-gui-0.2.6-x64.zip) |
||||
|
|
||||
|
你也可以到 [release](https://github.com/wxbool/video-srt-windows/releases) 页面下载其他版本 |
||||
|
|
||||
|
<a name="1bbbb204"></a> |
||||
|
## 注意事项 |
||||
|
|
||||
|
- 软件目录下的 `data`目录为数据存储目录,请勿删除。否则可能会导致配置丢失 |
||||
|
- 项目使用了 [ffmpeg](http://ffmpeg.org/) 依赖,除非您的电脑已经安装了`ffmpeg`环境,否则请下载包含`ffmpeg`依赖的软件包 |
||||
|
|
||||
|
<a name="9a751511"></a> |
||||
|
## 升级说明 |
||||
|
|
||||
|
- 先下载最新版本的软件包 |
||||
|
- 然后用旧版本软件的 `data` 文件夹覆盖新版软件的 `data` 文件夹 |
||||
|
- 0.2.6 升级至 0.2.9 以上的版本时,由于翻译设置无法直接兼容低版本,可能需要重新在软件创建翻译引擎才能继续使用翻译功能 |
||||
|
|
||||
|
## 其他 |
||||
|
今日,有小伙伴在淘宝、闲鱼上发现一些界面相似的盗版且额外收费的软件。由于`VideoSrt` 是开源软件,所以不排除个别不良用户通过微调软件源代码,然后进行二次售卖甚至传播病毒。 |
||||
|
|
||||
|
目前已知的有:`大象字幕` |
||||
|
|
||||
|
因此,软件的安装包不要在第三方平台渠道下载,尽量在 github/码云/帮助文档 等官方渠道的页面进行下载。 |
||||
|
|
||||
|
小伙伴们要擦亮眼睛,打击盗版,望周知! |
||||
|
|
||||
|
|
||||
|
## FAQ |
||||
|
##### 1.为什么Linux和Mac不能用? |
||||
|
因为`VideoSrt`的GUI是使用[lxn/walk](https://github.com/lxn/walk)开发的,仅支持Windows的GUI,如果您想在Linux上使用,可以体验[CLI版本](https://github.com/wxbool/video-srt) |
||||
|
|
||||
|
##### 2.使用此软件会产生费用吗? |
||||
|
如果您适量使用本软件(各个API的免费使用额度可以自行查询),将不会产生费用。 |
||||
|
如果您大量使用,建议根据自己的情况购买各个平台的资源包,以满足需求。 |
||||
|
|
||||
|
##### 3.难受,为什么我一直报错? |
||||
|
报错的原因有很多,软件配置错误、阿里云、腾讯云等账户权限问题都可能会导致软件显示错误。如果您遇到麻烦,建议加入`QQ群`与我们交流。 |
||||
|
|
||||
|
|
||||
|
<a name="f3dc992e"></a> |
||||
|
## 交流&联系 |
||||
|
|
||||
|
- QQ交流群3:[742310571](https://jq.qq.com/?_wv=1027&k=OPKLP16H) (已满) |
||||
|
- QQ交流群2:[1035527812](https://jq.qq.com/?_wv=1027&k=lzbkMgzJ) (980/1000) |
||||
|
- QQ交流群1:[109695078](https://jq.qq.com/?_wv=1027&k=5Eco2hO) (已满) |
||||
|
|
||||
|
|
||||
|
<a name="AyJ3E"></a> |
||||
|
## 捐赠&支持 |
||||
|
|
||||
|
![](https://pic2.superbed.cn/item/5e00b93476085c3289dd2dc0.png) |
||||
|
|
||||
|
#### 每日领支付宝红包(2021.8 ~ 2021.11) |
||||
|
<div style="text-align: center;"> |
||||
|
<img src="https://cdn.nlark.com/yuque/0/2021/png/695280/1633671491324-b3375ad6-027b-4297-bd7f-2f42784dccbf.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_23%2Ctext_VmlkZW9TcnQ%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10%2Fresize%2Cw_794%2Climit_0" width=220 /> |
||||
|
</div> |
@ -0,0 +1,268 @@ |
|||||
|
package aliyun |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"errors" |
||||
|
"github.com/aliyun/alibaba-cloud-sdk-go/sdk" |
||||
|
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" |
||||
|
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses" |
||||
|
"strconv" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
ALIYUN_CLOUND_REGION_CHA = 1 //中国
|
||||
|
ALIYUN_CLOUND_REGION_INT = 2 //海外
|
||||
|
) |
||||
|
|
||||
|
//阿里云语音识别引擎
|
||||
|
type AliyunClound struct { |
||||
|
AccessKeyId string |
||||
|
AccessKeySecret string |
||||
|
AppKey string |
||||
|
Region int |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//阿里云录音文件识别结果集
|
||||
|
type AliyunAudioRecognitionResult struct { |
||||
|
Text string //文本结果
|
||||
|
TranslateText string //翻译文本结果
|
||||
|
ChannelId int64 //音轨ID
|
||||
|
BeginTime int64 //该句的起始时间偏移,单位为毫秒
|
||||
|
EndTime int64 //该句的结束时间偏移,单位为毫秒
|
||||
|
SilenceDuration int64 //本句与上一句之间的静音时长,单位为秒
|
||||
|
SpeechRate int64 //本句的平均语速,单位为每分钟字数
|
||||
|
EmotionValue int64 //情绪能量值1-10,值越高情绪越强烈
|
||||
|
} |
||||
|
|
||||
|
//阿里云识别词语数据集
|
||||
|
type AliyunAudioWord struct { |
||||
|
Word string |
||||
|
ChannelId int64 |
||||
|
BeginTime int64 |
||||
|
EndTime int64 |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// 定义常量
|
||||
|
const REGION_ID_REGION_INT string = "ap-southeast-1" |
||||
|
const ENDPOINT_NAME_REGION_INT string = "ap-southeast-1" |
||||
|
const PRODUCT_REGION_INT string = "nls-filetrans" |
||||
|
const DOMAIN_REGION_INT string = "filetrans.ap-southeast-1.aliyuncs.com" |
||||
|
const API_VERSION_REGION_INT string = "2019-08-23" |
||||
|
|
||||
|
const REGION_ID_REGION_CHA string = "cn-shanghai" |
||||
|
const ENDPOINT_NAME_REGION_CHA string = "cn-shanghai" |
||||
|
const PRODUCT_REGION_CHA string = "nls-filetrans" |
||||
|
const DOMAIN_REGION_CHA string = "filetrans.cn-shanghai.aliyuncs.com" |
||||
|
const API_VERSION_REGION_CHA string = "2018-08-17" |
||||
|
|
||||
|
|
||||
|
const POST_REQUEST_ACTION string = "SubmitTask" |
||||
|
const GET_REQUEST_ACTION string = "GetTaskResult" |
||||
|
// 请求参数key
|
||||
|
const KEY_APP_KEY string = "appkey" |
||||
|
const KEY_FILE_LINK string = "file_link" |
||||
|
const KEY_VERSION string = "version" |
||||
|
const KEY_ENABLE_WORDS string = "enable_words" |
||||
|
|
||||
|
//是否打开ITN,中文数字将转为阿拉伯数字输出,默认值为false
|
||||
|
const KEY_ENABLE_INVERSE_TEXT_NORMAL = "enable_inverse_text_normalization" |
||||
|
//是否启⽤语义断句,取值:true/false,默认值false
|
||||
|
const KEY_ENABLE_SEMANTIC_SENTENCE_DETECTION = "enable_semantic_sentence_detection" |
||||
|
//是否启用时间戳校准功能,取值:true/false,默认值false
|
||||
|
const KEY_ENABLE_TIMESTAMP_ALIGNMENT = "enable_timestamp_alignment" |
||||
|
|
||||
|
// 响应参数key
|
||||
|
const KEY_TASK string = "Task" |
||||
|
const KEY_TASK_ID string = "TaskId" |
||||
|
const KEY_STATUS_TEXT string = "StatusText" |
||||
|
const KEY_RESULT string = "Result" |
||||
|
// 状态值
|
||||
|
const STATUS_SUCCESS string = "SUCCESS" |
||||
|
const STATUS_RUNNING string = "RUNNING" |
||||
|
const STATUS_QUEUEING string = "QUEUEING" |
||||
|
|
||||
|
|
||||
|
//发起录音文件识别
|
||||
|
//接口文档 https://help.aliyun.com/document_detail/90727.html?spm=a2c4g.11186623.6.581.691af6ebYsUkd1
|
||||
|
func (c AliyunClound) NewAudioFile(fileLink string) (string , *sdk.Client , error) { |
||||
|
regionId, domain, apiVersion, product := c.GetApiVariable() |
||||
|
|
||||
|
client, err := sdk.NewClientWithAccessKey(regionId, c.AccessKeyId, c.AccessKeySecret) |
||||
|
if err != nil { |
||||
|
return "" , client , err |
||||
|
} |
||||
|
client.SetConnectTimeout(time.Second * 20) |
||||
|
|
||||
|
postRequest := requests.NewCommonRequest() |
||||
|
postRequest.Domain = domain |
||||
|
postRequest.Version = apiVersion |
||||
|
postRequest.Product = product |
||||
|
postRequest.ApiName = POST_REQUEST_ACTION |
||||
|
postRequest.Method = "POST" |
||||
|
|
||||
|
|
||||
|
mapTask := make(map[string]string) |
||||
|
mapTask[KEY_APP_KEY] = c.AppKey |
||||
|
mapTask[KEY_FILE_LINK] = fileLink |
||||
|
// 新接入请使用4.0版本,已接入(默认2.0)如需维持现状,请注释掉该参数设置
|
||||
|
mapTask[KEY_VERSION] = "4.0" |
||||
|
// 设置是否输出词信息,默认为false,开启时需要设置version为4.0
|
||||
|
mapTask[KEY_ENABLE_WORDS] = "true" |
||||
|
|
||||
|
//统一后处理
|
||||
|
mapTask[KEY_ENABLE_INVERSE_TEXT_NORMAL] = "true" |
||||
|
mapTask[KEY_ENABLE_SEMANTIC_SENTENCE_DETECTION] = "true" |
||||
|
mapTask[KEY_ENABLE_TIMESTAMP_ALIGNMENT] = "true" |
||||
|
|
||||
|
// to json
|
||||
|
task, err := json.Marshal(mapTask) |
||||
|
if err != nil { |
||||
|
return "" , client , errors.New("to json error .") |
||||
|
} |
||||
|
postRequest.FormParams[KEY_TASK] = string(task) |
||||
|
// 发起请求
|
||||
|
postResponse, err := client.ProcessCommonRequest(postRequest) |
||||
|
if err != nil { |
||||
|
return "" , client , err |
||||
|
} |
||||
|
postResponseContent := postResponse.GetHttpContentString() |
||||
|
//校验请求
|
||||
|
if (postResponse.GetHttpStatus() != 200) { |
||||
|
return "" , client , errors.New("录音文件识别请求失败 , Http错误码 : " + strconv.Itoa(postResponse.GetHttpStatus())) |
||||
|
} |
||||
|
//解析数据
|
||||
|
var postMapResult map[string]interface{} |
||||
|
err = json.Unmarshal([]byte(postResponseContent), &postMapResult) |
||||
|
if err != nil { |
||||
|
return "" , client , errors.New("to map struct error .") |
||||
|
} |
||||
|
|
||||
|
var taskId = "" |
||||
|
var statusText = "" |
||||
|
statusText = postMapResult[KEY_STATUS_TEXT].(string) |
||||
|
|
||||
|
//检验结果
|
||||
|
if statusText == STATUS_SUCCESS { |
||||
|
taskId = postMapResult[KEY_TASK_ID].(string) |
||||
|
return taskId , client , nil |
||||
|
} |
||||
|
|
||||
|
return "" , client , errors.New("录音文件识别失败 , (" + c.GetErrorStatusTextMessage(statusText) + ")") |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取录音文件识别结果
|
||||
|
//接口文档 https://help.aliyun.com/document_detail/90727.html?spm=a2c4g.11186623.6.581.691af6ebYsUkd1
|
||||
|
func (c AliyunClound) GetAudioFileResult(taskId string , client *sdk.Client , logOutput func(text string) , callback func(result []byte)) (err error) { |
||||
|
_, domain, apiVersion, product := c.GetApiVariable() |
||||
|
|
||||
|
getRequest := requests.NewCommonRequest() |
||||
|
getRequest.Domain = domain |
||||
|
getRequest.Version = apiVersion |
||||
|
getRequest.Product = product |
||||
|
getRequest.ApiName = GET_REQUEST_ACTION |
||||
|
getRequest.Method = "GET" |
||||
|
getRequest.QueryParams[KEY_TASK_ID] = taskId |
||||
|
statusText := "" |
||||
|
|
||||
|
|
||||
|
var ( |
||||
|
trys = 0 |
||||
|
getResponse *responses.CommonResponse |
||||
|
getResponseContent string |
||||
|
) |
||||
|
|
||||
|
//遍历获取识别结果
|
||||
|
for trys < 10 { |
||||
|
|
||||
|
if trys != 0 { |
||||
|
logOutput("尝试重新查询识别结果,第" + strconv.Itoa(trys) + "次") |
||||
|
} |
||||
|
|
||||
|
getResponse, err = client.ProcessCommonRequest(getRequest) |
||||
|
if err != nil { |
||||
|
logOutput("查询识别结果失败:" + err.Error()) |
||||
|
trys++ |
||||
|
time.Sleep(time.Second * time.Duration(trys)) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
getResponseContent = getResponse.GetHttpContentString() |
||||
|
if (getResponse.GetHttpStatus() != 200) { |
||||
|
logOutput("查询识别结果失败,Http错误码:" + strconv.Itoa(getResponse.GetHttpStatus())) |
||||
|
trys++ |
||||
|
time.Sleep(time.Second * time.Duration(trys)) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
var getMapResult map[string]interface{} |
||||
|
err = json.Unmarshal([]byte(getResponseContent), &getMapResult) |
||||
|
if err != nil { |
||||
|
trys++ |
||||
|
logOutput("查询识别结果失败,解析结果失败:" + err.Error()) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
//校验遍历条件
|
||||
|
statusText = getMapResult[KEY_STATUS_TEXT].(string) |
||||
|
if statusText == STATUS_RUNNING || statusText == STATUS_QUEUEING { |
||||
|
time.Sleep(3 * time.Second) |
||||
|
} else { |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if statusText == STATUS_SUCCESS && getResponse != nil { |
||||
|
//调用回调函数
|
||||
|
callback(getResponse.GetHttpContentBytes()) |
||||
|
} else { |
||||
|
err = errors.New("录音文件识别失败 , (" + c.GetErrorStatusTextMessage(statusText) + ")") |
||||
|
return |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
//获取API常量
|
||||
|
func (c AliyunClound) GetApiVariable () (regionId string , domain string , apiVersion string , product string) { |
||||
|
if c.Region == 0 || c.Region == ALIYUN_CLOUND_REGION_CHA { |
||||
|
regionId = REGION_ID_REGION_CHA |
||||
|
domain = DOMAIN_REGION_CHA |
||||
|
apiVersion = API_VERSION_REGION_CHA |
||||
|
product = PRODUCT_REGION_CHA |
||||
|
} else if c.Region == ALIYUN_CLOUND_REGION_INT { |
||||
|
regionId = REGION_ID_REGION_INT |
||||
|
domain = DOMAIN_REGION_INT |
||||
|
apiVersion = API_VERSION_REGION_INT |
||||
|
product = PRODUCT_REGION_INT |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
//获取错误信息
|
||||
|
func (c AliyunClound) GetErrorStatusTextMessage (statusText string) string { |
||||
|
var code map[string]string = map[string]string{ |
||||
|
"REQUEST_APPKEY_UNREGISTERED":"阿里云智能语音项目未创建/无访问权限。请检查语音引擎Appkey是否填写错误;如果是海外地区,在软件创建语音引擎时,服务区域需要选择“海外”", |
||||
|
"USER_BIZDURATION_QUOTA_EXCEED":"单日2小时识别免费额度超出限制", |
||||
|
"FILE_DOWNLOAD_FAILED":"文件访问失败,请检查OSS存储空间访问权限。请将OSS存储空间设置为“公共读”", |
||||
|
"FILE_TOO_LARGE":"音频文件超出512MB", |
||||
|
"FILE_PARSE_FAILED":"音频文件解析失败,请检查音频文件是否有损坏", |
||||
|
"UNSUPPORTED_SAMPLE_RATE":"采样率不匹配", |
||||
|
"FILE_TRANS_TASK_EXPIRED":"音频文件识别任务过期,请重试", |
||||
|
"REQUEST_INVALID_FILE_URL_VALUE":"音频文件访问失败,请检查OSS存储空间访问权限", |
||||
|
"FILE_404_NOT_FOUND":"音频文件访问失败,请检查OSS存储空间访问权限", |
||||
|
"FILE_403_FORBIDDEN":"音频文件访问失败,请检查OSS存储空间访问权限", |
||||
|
"FILE_SERVER_ERROR":"音频文件访问失败,请检查请求的文件所在的服务是否可用", |
||||
|
"INTERNAL_ERROR":"识别内部通用错误,请稍候重试", |
||||
|
} |
||||
|
|
||||
|
if _, ok := code[statusText]; ok { |
||||
|
return code[statusText] |
||||
|
} else { |
||||
|
return statusText |
||||
|
} |
||||
|
} |
@ -0,0 +1,102 @@ |
|||||
|
package aliyun |
||||
|
|
||||
|
import ( |
||||
|
"github.com/aliyun/aliyun-oss-go-sdk/oss" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type AliyunOss struct { |
||||
|
Endpoint string |
||||
|
AccessKeyId string |
||||
|
AccessKeySecret string |
||||
|
BucketName string //yourBucketName
|
||||
|
BucketDomain string //Bucket 域名
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取Buckets列表
|
||||
|
func (c AliyunOss) GetListBuckets() ([]string , error) { |
||||
|
client, err := oss.New(c.Endpoint , c.AccessKeyId , c.AccessKeySecret) |
||||
|
if err != nil { |
||||
|
return nil,err |
||||
|
} |
||||
|
|
||||
|
lsRes, err := client.ListBuckets() |
||||
|
if err != nil { |
||||
|
return nil,err |
||||
|
} |
||||
|
|
||||
|
result := []string{} |
||||
|
for _, bucket := range lsRes.Buckets { |
||||
|
result = append(result , bucket.Name) |
||||
|
} |
||||
|
|
||||
|
return result,nil |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//上传本地文件
|
||||
|
//localFileName:本地文件
|
||||
|
//objectName:oss文件名称
|
||||
|
func (c AliyunOss) UploadFile(localFileName string , objectName string) (string , error) { |
||||
|
// 创建OSSClient实例
|
||||
|
client, err := oss.New(c.Endpoint , c.AccessKeyId , c.AccessKeySecret) |
||||
|
if err != nil { |
||||
|
return "",err |
||||
|
} |
||||
|
// 获取存储空间
|
||||
|
bucket, err := client.Bucket(c.BucketName) |
||||
|
if err != nil { |
||||
|
return "",err |
||||
|
} |
||||
|
|
||||
|
//分日期存储
|
||||
|
date := time.Now() |
||||
|
year := date.Year() |
||||
|
month := date.Month() |
||||
|
day := date.Day() |
||||
|
objectName = strconv.Itoa(year) + "/" + strconv.Itoa(int(month)) + "/" + strconv.Itoa(day) + "/" + objectName |
||||
|
|
||||
|
// 上传文件
|
||||
|
err = bucket.PutObjectFromFile(objectName , localFileName) |
||||
|
if err != nil { |
||||
|
return "",err |
||||
|
} |
||||
|
|
||||
|
return objectName , nil |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//删除oss文件
|
||||
|
func (c AliyunOss) DeleteFile(objectName string) error { |
||||
|
// 创建OSSClient实例
|
||||
|
client, err := oss.New(c.Endpoint , c.AccessKeyId , c.AccessKeySecret) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
// 获取存储空间
|
||||
|
bucket, err := client.Bucket(c.BucketName) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// 删除单个文件。objectName表示删除OSS文件时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
|
||||
|
// 如需删除文件夹,请将objectName设置为对应的文件夹名称。如果文件夹非空,则需要将文件夹下的所有object删除后才能删除该文件夹。
|
||||
|
err = bucket.DeleteObject(objectName) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取文件 url link
|
||||
|
func (c AliyunOss) GetObjectFileUrl(objectFile string) string { |
||||
|
if strings.Index(c.BucketDomain, "http://") == -1 && strings.Index(c.BucketDomain, "https://") == -1 { |
||||
|
return "http://" + c.BucketDomain + "/" + objectFile |
||||
|
} else { |
||||
|
return c.BucketDomain + "/" + objectFile |
||||
|
} |
||||
|
} |
@ -0,0 +1,416 @@ |
|||||
|
package aliyun |
||||
|
|
||||
|
import ( |
||||
|
"github.com/buger/jsonparser" |
||||
|
"regexp" |
||||
|
"strings" |
||||
|
"unicode" |
||||
|
"unicode/utf8" |
||||
|
"videosrt/app/tool" |
||||
|
) |
||||
|
|
||||
|
|
||||
|
type AliyunAudioRecognitionResultBlock struct { |
||||
|
AliyunAudioRecognitionResult |
||||
|
Blocks []int |
||||
|
BlockEmptyTag bool |
||||
|
BlockEmptyHandle bool |
||||
|
} |
||||
|
|
||||
|
//阿里云录音录音文件识别 - 智能分段处理
|
||||
|
func AliyunAudioResultWordHandle(result [] byte , callback func (vresult *AliyunAudioRecognitionResult)) { |
||||
|
var audioResult = make(map[int64][] *AliyunAudioRecognitionResultBlock) |
||||
|
var wordResult = make(map[int64][]*AliyunAudioWord) |
||||
|
var err error |
||||
|
|
||||
|
//获取录音识别数据集
|
||||
|
_, err = jsonparser.ArrayEach(result, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { |
||||
|
text , _ := jsonparser.GetString(value, "Text") |
||||
|
channelId , _ := jsonparser.GetInt(value, "ChannelId") |
||||
|
beginTime , _ := jsonparser.GetInt(value, "BeginTime") |
||||
|
endTime , _ := jsonparser.GetInt(value, "EndTime") |
||||
|
silenceDuration , _ := jsonparser.GetInt(value, "SilenceDuration") |
||||
|
speechRate , _ := jsonparser.GetInt(value, "SpeechRate") |
||||
|
emotionValue , _ := jsonparser.GetInt(value, "EmotionValue") |
||||
|
|
||||
|
vresult := &AliyunAudioRecognitionResultBlock {} |
||||
|
vresult.Text = text |
||||
|
vresult.ChannelId = channelId |
||||
|
vresult.BeginTime = beginTime |
||||
|
vresult.EndTime = endTime |
||||
|
vresult.SilenceDuration = silenceDuration |
||||
|
vresult.SpeechRate = speechRate |
||||
|
vresult.EmotionValue = emotionValue |
||||
|
|
||||
|
_ , isPresent := audioResult[channelId] |
||||
|
if isPresent { |
||||
|
//追加
|
||||
|
audioResult[channelId] = append(audioResult[channelId] , vresult) |
||||
|
} else { |
||||
|
//初始
|
||||
|
audioResult[channelId] = []*AliyunAudioRecognitionResultBlock{} |
||||
|
audioResult[channelId] = append(audioResult[channelId] , vresult) |
||||
|
} |
||||
|
} , "Result", "Sentences") |
||||
|
if err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
|
||||
|
//获取词语数据集
|
||||
|
_, err = jsonparser.ArrayEach(result , func(value []byte, dataType jsonparser.ValueType, offset int, err error) { |
||||
|
word , _ := jsonparser.GetString(value, "Word") |
||||
|
channelId , _ := jsonparser.GetInt(value, "ChannelId") |
||||
|
beginTime , _ := jsonparser.GetInt(value, "BeginTime") |
||||
|
endTime , _ := jsonparser.GetInt(value, "EndTime") |
||||
|
vresult := &AliyunAudioWord{ |
||||
|
Word:word, |
||||
|
ChannelId:channelId, |
||||
|
BeginTime:beginTime, |
||||
|
EndTime:endTime, |
||||
|
} |
||||
|
_, isPresent := wordResult[channelId] |
||||
|
if isPresent { |
||||
|
//追加
|
||||
|
wordResult[channelId] = append(wordResult[channelId] , vresult) |
||||
|
} else { |
||||
|
//初始
|
||||
|
wordResult[channelId] = []*AliyunAudioWord{} |
||||
|
wordResult[channelId] = append(wordResult[channelId] , vresult) |
||||
|
} |
||||
|
} , "Result" , "Words") |
||||
|
if err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
var symbol = []string{"?","。",",","!",";","、","?",".",",","!"} |
||||
|
//数据集处理
|
||||
|
for _ , value := range audioResult { |
||||
|
for _ , data := range value { |
||||
|
// filter
|
||||
|
data.Text = FilterText(data.Text) |
||||
|
|
||||
|
data.Blocks = GetTextBlock(data.Text) |
||||
|
data.Text = ReplaceStrs(data.Text , symbol , "") |
||||
|
|
||||
|
if len(data.Blocks) == 0 { |
||||
|
data.BlockEmptyTag = true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//遍历输出
|
||||
|
for _,value := range wordResult { |
||||
|
|
||||
|
var block string = "" |
||||
|
var blockRune int = 0 |
||||
|
var lastBlock int = 0 |
||||
|
|
||||
|
var beginTime int64 = 0 |
||||
|
var blockBool = false |
||||
|
|
||||
|
var ischinese = IsChineseWords(value) //校验中文
|
||||
|
|
||||
|
var chineseNumberWordIndexs []int |
||||
|
var chineseNumberDiffLength int = 0 |
||||
|
|
||||
|
for i , word := range value { |
||||
|
if blockBool || i == 0 { |
||||
|
beginTime = word.BeginTime |
||||
|
blockBool = false |
||||
|
} |
||||
|
|
||||
|
if ischinese && block == "" { |
||||
|
chineseNumberWordIndexs = []int{} |
||||
|
chineseNumberDiffLength = 0 |
||||
|
} |
||||
|
|
||||
|
if ischinese { |
||||
|
block += word.Word |
||||
|
if tool.CheckChineseNumber(word.Word) && FindSliceIntCount(chineseNumberWordIndexs , i) == 0 { |
||||
|
cl := tool.ChineseNumberToLowercaseLength(word.Word) - utf8.RuneCountInString(word.Word) |
||||
|
if cl > 0 { |
||||
|
chineseNumberDiffLength += cl |
||||
|
chineseNumberWordIndexs = append(chineseNumberWordIndexs , i) |
||||
|
} else { |
||||
|
//例外
|
||||
|
if i != 0 { |
||||
|
newWord := value[i-1].Word + word.Word |
||||
|
cl := tool.ChineseNumberToLowercaseLength(newWord) - utf8.RuneCountInString(newWord) |
||||
|
if cl > 0 { |
||||
|
chineseNumberDiffLength += cl |
||||
|
chineseNumberWordIndexs = append(chineseNumberWordIndexs , i) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
block += CompleSpace(word.Word) //补全空格
|
||||
|
} |
||||
|
|
||||
|
blockRune = utf8.RuneCountInString(block) |
||||
|
|
||||
|
//fmt.Println("chineseNumberDiffLength : " , chineseNumberWordIndexs , chineseNumberDiffLength , word.Word)
|
||||
|
|
||||
|
for channel , p := range audioResult { |
||||
|
if word.ChannelId != channel { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
for windex , w := range p { |
||||
|
|
||||
|
if (word.BeginTime >= w.BeginTime && word.EndTime <= w.EndTime) || ((word.BeginTime < w.EndTime && word.EndTime > w.EndTime) && (FindSliceIntCount(w.Blocks , -1) != len(w.Blocks))) { |
||||
|
flag := false |
||||
|
early := false |
||||
|
|
||||
|
if !w.BlockEmptyTag { |
||||
|
for t , B := range w.Blocks{ |
||||
|
//fmt.Println("blockRune : " , blockRune , B , word.Word)
|
||||
|
if ((blockRune >= B) || (blockRune + chineseNumberDiffLength >= B)) && B != -1 { |
||||
|
flag = true |
||||
|
|
||||
|
//fmt.Println(w.Blocks)
|
||||
|
//fmt.Println(B , lastBlock , (B - lastBlock) , word.Word)
|
||||
|
//fmt.Println(w.Text)
|
||||
|
//fmt.Println( block )
|
||||
|
//fmt.Println("\n")
|
||||
|
|
||||
|
var thisText = "" |
||||
|
//容错机制
|
||||
|
if t == (len(w.Blocks) - 1) { |
||||
|
thisText = SubString(w.Text , lastBlock , 10000) |
||||
|
} else { |
||||
|
//下个词提前结束
|
||||
|
if i < len(value)-1 && value[i+1].BeginTime >= w.EndTime{ |
||||
|
thisText = SubString(w.Text , lastBlock , 10000) |
||||
|
early = true |
||||
|
} else { |
||||
|
thisText = SubString(w.Text , lastBlock , (B - lastBlock)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
lastBlock = B |
||||
|
if early == true { |
||||
|
//全部设置为-1
|
||||
|
for vt,vb := range w.Blocks{ |
||||
|
if vb != -1 { |
||||
|
w.Blocks[vt] = -1; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
w.Blocks[t] = -1 |
||||
|
} |
||||
|
|
||||
|
vresult := &AliyunAudioRecognitionResult{ |
||||
|
Text:thisText, |
||||
|
ChannelId:channel, |
||||
|
BeginTime:beginTime, |
||||
|
EndTime:word.EndTime, |
||||
|
SilenceDuration:w.SilenceDuration, |
||||
|
SpeechRate:w.SpeechRate, |
||||
|
EmotionValue:w.EmotionValue, |
||||
|
} |
||||
|
callback(vresult) //回调传参
|
||||
|
|
||||
|
blockBool = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//fmt.Println("word.Word : " , word.Word)
|
||||
|
//fmt.Println(block)
|
||||
|
|
||||
|
if FindSliceIntCount(w.Blocks , -1) == len(w.Blocks) { |
||||
|
//全部截取完成
|
||||
|
block = "" |
||||
|
lastBlock = 0 |
||||
|
} |
||||
|
//容错机制
|
||||
|
if FindSliceIntCount(w.Blocks , -1) == (len(w.Blocks)-1) && flag == false { |
||||
|
var thisText = SubString(w.Text , lastBlock , 10000) |
||||
|
|
||||
|
w.Blocks[len(w.Blocks) - 1] = -1 |
||||
|
//vresult
|
||||
|
vresult := &AliyunAudioRecognitionResult{ |
||||
|
Text:thisText, |
||||
|
ChannelId:channel, |
||||
|
BeginTime:beginTime, |
||||
|
EndTime:w.EndTime, |
||||
|
SilenceDuration:w.SilenceDuration, |
||||
|
SpeechRate:w.SpeechRate, |
||||
|
EmotionValue:w.EmotionValue, |
||||
|
} |
||||
|
|
||||
|
//fmt.Println( thisText )
|
||||
|
//fmt.Println( block )
|
||||
|
//fmt.Println( word.Word , beginTime, w.EndTime , flag , word.EndTime )
|
||||
|
|
||||
|
callback(vresult) //回调传参
|
||||
|
|
||||
|
//覆盖下一段落的时间戳
|
||||
|
if windex < (len(p)-1) { |
||||
|
beginTime = p[windex+1].BeginTime |
||||
|
} else { |
||||
|
beginTime = w.EndTime |
||||
|
} |
||||
|
|
||||
|
//清除参数
|
||||
|
block = "" |
||||
|
lastBlock = 0 |
||||
|
} |
||||
|
} else { |
||||
|
|
||||
|
//清除参数
|
||||
|
block = "" |
||||
|
lastBlock = 0 |
||||
|
blockBool = true |
||||
|
|
||||
|
if w.BlockEmptyHandle == false { |
||||
|
vresult := &AliyunAudioRecognitionResult{ |
||||
|
Text:w.Text, |
||||
|
ChannelId:w.ChannelId, |
||||
|
BeginTime:w.BeginTime, |
||||
|
EndTime:w.EndTime, |
||||
|
SilenceDuration:w.SilenceDuration, |
||||
|
SpeechRate:w.SpeechRate, |
||||
|
EmotionValue:w.EmotionValue, |
||||
|
} |
||||
|
callback(vresult) //回调传参
|
||||
|
w.BlockEmptyHandle = true |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
func FindSliceIntCount(slice []int , target int) int { |
||||
|
c := 0 |
||||
|
for _ , v := range slice { |
||||
|
if target == v { |
||||
|
c++ |
||||
|
} |
||||
|
} |
||||
|
return c |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//批量替换多个关键词文本
|
||||
|
func ReplaceStrs(strs string , olds []string , s string) string { |
||||
|
for _ , word := range olds { |
||||
|
strs = strings.Replace(strs , word , s , -1) |
||||
|
} |
||||
|
return strs |
||||
|
} |
||||
|
|
||||
|
func StringIndex(strs string , word rune) int { |
||||
|
strsRune := []rune(strs) |
||||
|
for i,v := range strsRune { |
||||
|
if v == word { |
||||
|
return i |
||||
|
} |
||||
|
} |
||||
|
return -1 |
||||
|
} |
||||
|
|
||||
|
//补全右边空格
|
||||
|
func CompleSpace(s string) string { |
||||
|
s = strings.TrimLeft(s , " "); |
||||
|
s = strings.TrimRight(s , " "); |
||||
|
return s + " "; |
||||
|
} |
||||
|
|
||||
|
func IsChineseWords(words []*AliyunAudioWord) bool { |
||||
|
for _,v := range words { |
||||
|
if (IsChineseChar(v.Word)){ |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func IsChineseChar(str string) bool { |
||||
|
for _, r := range str { |
||||
|
if unicode.Is(unicode.Scripts["Han"], r) || (regexp.MustCompile("[\u3002\uff1b\uff0c\uff1a\u201c\u201d\uff08\uff09\u3001\uff1f\u300a\u300b]").MatchString(string(r))) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func IndexRunes(strs string , olds []rune) int { |
||||
|
min := -1 |
||||
|
for i , word := range olds { |
||||
|
index := StringIndex(strs , word) |
||||
|
//println( "ts : " , index)
|
||||
|
if i == 0 { |
||||
|
min = index |
||||
|
} else { |
||||
|
if min == -1 { |
||||
|
min = index |
||||
|
} else { |
||||
|
if index < min && index != -1 { |
||||
|
min = index |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return min |
||||
|
} |
||||
|
|
||||
|
func GetTextBlock(strs string) ([]int) { |
||||
|
var symbol_zhcn = []rune{'?','。',',','!',';','、','?','.',',','!'} |
||||
|
//var symbol_en = []rune{'?','.',',','!'}
|
||||
|
strsRune := []rune(strs) |
||||
|
|
||||
|
blocks := []int{} |
||||
|
for { |
||||
|
index := IndexRunes(strs , symbol_zhcn) |
||||
|
if index == -1 { |
||||
|
break |
||||
|
} |
||||
|
strs = string(strsRune[0:index]) + string(strsRune[(index + 1):]) |
||||
|
strsRune = []rune(strs) |
||||
|
blocks = append(blocks , index) |
||||
|
} |
||||
|
return blocks |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func SubString(str string , begin int ,length int) (substr string) { |
||||
|
// 将字符串的转换成[]rune
|
||||
|
rs := []rune(str) |
||||
|
lth := len(rs) |
||||
|
|
||||
|
// 简单的越界判断
|
||||
|
if begin < 0 { |
||||
|
begin = 0 |
||||
|
} |
||||
|
if begin >= lth { |
||||
|
begin = lth |
||||
|
} |
||||
|
if length < 0 { |
||||
|
length = 0 |
||||
|
} |
||||
|
end := begin + length |
||||
|
if end > lth { |
||||
|
end = lth |
||||
|
} |
||||
|
// 返回子串
|
||||
|
return string(rs[begin:end]) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//过滤文本
|
||||
|
func FilterText(text string) string { |
||||
|
//去除换行符
|
||||
|
re, _ := regexp.Compile("[\n|\r|\r\n]+") |
||||
|
text = re.ReplaceAllString(text, "") |
||||
|
return text |
||||
|
} |
@ -0,0 +1,97 @@ |
|||||
|
package app |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"regexp" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"videosrt/app/tool" |
||||
|
) |
||||
|
|
||||
|
|
||||
|
//语气词过滤
|
||||
|
func ModalWordsFilter(s string , w string) string { |
||||
|
tmpText := strings.ReplaceAll(s , w , "") |
||||
|
if strings.TrimSpace(tmpText) == "" || tool.CheckOnlySymbolText(strings.TrimSpace(tmpText)) { |
||||
|
return "" |
||||
|
} else { |
||||
|
//尝试过滤重复语气词
|
||||
|
compile, e := regexp.Compile(w + "{2,}") |
||||
|
if e != nil { |
||||
|
return s |
||||
|
} |
||||
|
return compile.ReplaceAllString(s , "") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//自定义规则过滤
|
||||
|
func DefinedWordRuleFilter(s string , rule *AppDefinedFilterRule) string { |
||||
|
if rule.Way == FILTER_TYPE_STRING { |
||||
|
//文本过滤
|
||||
|
s = strings.ReplaceAll(s , rule.Target , rule.Replace) |
||||
|
} else if rule.Way == FILTER_TYPE_REGX { |
||||
|
//正则过滤
|
||||
|
compile, e := regexp.Compile(rule.Target) |
||||
|
if e != nil { |
||||
|
return s |
||||
|
} |
||||
|
s = compile.ReplaceAllString(s , rule.Replace) |
||||
|
} |
||||
|
if strings.TrimSpace(s) == "" || tool.CheckOnlySymbolText(strings.TrimSpace(s)) { |
||||
|
return "" |
||||
|
} |
||||
|
return s |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//拼接字幕字符串
|
||||
|
func MakeSubtitleText(index int , startTime int64 , endTime int64 , text string , translateText string , bilingualSubtitleSwitch bool , bilingualAsc bool) string { |
||||
|
var content bytes.Buffer |
||||
|
content.WriteString(strconv.Itoa(index)) |
||||
|
content.WriteString("\r\n") |
||||
|
content.WriteString(tool.SubtitleTimeMillisecond(startTime , true)) |
||||
|
content.WriteString(" --> ") |
||||
|
content.WriteString(tool.SubtitleTimeMillisecond(endTime , true)) |
||||
|
content.WriteString("\r\n") |
||||
|
|
||||
|
//输出双语字幕
|
||||
|
if bilingualSubtitleSwitch { |
||||
|
if bilingualAsc { |
||||
|
content.WriteString(text) |
||||
|
content.WriteString("\r\n") |
||||
|
content.WriteString(translateText) |
||||
|
} else { |
||||
|
content.WriteString(translateText) |
||||
|
content.WriteString("\r\n") |
||||
|
content.WriteString(text) |
||||
|
} |
||||
|
} else { |
||||
|
content.WriteString(text) |
||||
|
} |
||||
|
|
||||
|
content.WriteString("\r\n") |
||||
|
content.WriteString("\r\n") |
||||
|
return content.String() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//拼接文本格式
|
||||
|
func MakeText(index int , startTime int64 , endTime int64 , text string) string { |
||||
|
var content bytes.Buffer |
||||
|
content.WriteString(text) |
||||
|
content.WriteString("\r\n") |
||||
|
content.WriteString("\r\n") |
||||
|
return content.String() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//拼接歌词文本
|
||||
|
func MakeMusicLrcText(index int , startTime int64 , endTime int64 , text string) string { |
||||
|
var content bytes.Buffer |
||||
|
content.WriteString("[") |
||||
|
content.WriteString(tool.MusicLrcTextMillisecond(startTime)) |
||||
|
content.WriteString("]") |
||||
|
content.WriteString(text) |
||||
|
content.WriteString("\r\n") |
||||
|
return content.String() |
||||
|
} |
@ -0,0 +1,665 @@ |
|||||
|
package app |
||||
|
|
||||
|
import ( |
||||
|
"videosrt/app/aliyun" |
||||
|
"videosrt/app/datacache" |
||||
|
"videosrt/app/translate" |
||||
|
) |
||||
|
|
||||
|
//输出文件类型
|
||||
|
const( |
||||
|
OUTPUT_SRT = 1 //字幕SRT文件
|
||||
|
OUTPUT_STRING = 2 //普通文本
|
||||
|
OUTPUT_LRC = 3 //LRC文本
|
||||
|
) |
||||
|
|
||||
|
//输出文件编码
|
||||
|
const( |
||||
|
OUTPUT_ENCODE_UTF8 = 1 //文件编码 utf-8
|
||||
|
OUTPUT_ENCODE_UTF8_BOM = 2 //文件编码 utf-8 带 BOM
|
||||
|
) |
||||
|
|
||||
|
//翻译语言
|
||||
|
const ( |
||||
|
LANGUAGE_ZH = 1 //中文
|
||||
|
LANGUAGE_EN = 2 //英文
|
||||
|
LANGUAGE_JP = 3 //日语
|
||||
|
LANGUAGE_KOR = 4 //韩语
|
||||
|
LANGUAGE_FRA = 5 //法语 fra
|
||||
|
LANGUAGE_DE = 6 //德语 de
|
||||
|
LANGUAGE_SPA = 7 //西班牙语 spa
|
||||
|
LANGUAGE_RU = 8 //俄语 ru
|
||||
|
LANGUAGE_IT = 9 //意大利语 it
|
||||
|
LANGUAGE_TH = 10 //泰语 th
|
||||
|
) |
||||
|
|
||||
|
//缓存结构
|
||||
|
type OssAppStruct struct { |
||||
|
Data *datacache.AppCache |
||||
|
} |
||||
|
type TranslateEngineAppStruct struct { |
||||
|
Data *datacache.AppCache |
||||
|
} |
||||
|
type SpeechEngineAppStruct struct { |
||||
|
Data *datacache.AppCache |
||||
|
} |
||||
|
type AppSetingsAppStruct struct { |
||||
|
Data *datacache.AppCache |
||||
|
} |
||||
|
type AppFilterAppStruct struct { |
||||
|
Data *datacache.AppCache |
||||
|
} |
||||
|
|
||||
|
var RootDir string |
||||
|
var Oss *OssAppStruct |
||||
|
var Translate *TranslateEngineAppStruct |
||||
|
var Engine *SpeechEngineAppStruct |
||||
|
var Setings *AppSetingsAppStruct |
||||
|
var Filter *AppFilterAppStruct |
||||
|
|
||||
|
|
||||
|
func init() { |
||||
|
RootDir = GetAppRootDir() |
||||
|
if RootDir == "" { |
||||
|
panic("应用根目录获取失败") |
||||
|
} |
||||
|
|
||||
|
Oss = new(OssAppStruct) |
||||
|
Translate = new(TranslateEngineAppStruct) |
||||
|
Engine = new(SpeechEngineAppStruct) |
||||
|
Setings = new(AppSetingsAppStruct) |
||||
|
Filter = new(AppFilterAppStruct) |
||||
|
|
||||
|
Oss.Data = datacache.NewAppCahce(RootDir , "oss") |
||||
|
Translate.Data = datacache.NewAppCahce(RootDir , "translate_engine") |
||||
|
Engine.Data = datacache.NewAppCahce(RootDir , "engine") |
||||
|
Setings.Data = datacache.NewAppCahce(RootDir , "setings") |
||||
|
Filter.Data = datacache.NewAppCahce(RootDir , "filter") |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//表单结构
|
||||
|
type OperateFrom struct { |
||||
|
EngineId int //当前语音引擎
|
||||
|
TranslateEngineId int //当前翻译引擎
|
||||
|
|
||||
|
TranslateSwitch bool //字幕翻译开关
|
||||
|
BilingualSubtitleSwitch bool //是否输出双语字幕
|
||||
|
InputLanguage int //输入字幕语言
|
||||
|
OutputLanguage int //输出字幕语言
|
||||
|
OutputMainSubtitleInputLanguage bool //双语主字幕(输入语言)
|
||||
|
|
||||
|
OutputSrt bool |
||||
|
OutputLrc bool |
||||
|
OutputTxt bool |
||||
|
|
||||
|
OutputType *AppSetingsOutput //输出文件类型
|
||||
|
OutputEncode int //输出文件编码
|
||||
|
SoundTrack int //输出音轨
|
||||
|
} |
||||
|
|
||||
|
//输出类型选项
|
||||
|
type OutputSelects struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
} |
||||
|
|
||||
|
//输出音轨类型选项
|
||||
|
type SoundTrackSelects struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
} |
||||
|
|
||||
|
//字幕翻译语言选项列表
|
||||
|
type LanguageSelects struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
} |
||||
|
|
||||
|
type AppSetingsOutput struct { |
||||
|
SRT bool |
||||
|
LRC bool |
||||
|
TXT bool |
||||
|
} |
||||
|
|
||||
|
//应用配置结构
|
||||
|
type AppSetings struct { |
||||
|
CurrentEngineId int //目前语音引擎Id
|
||||
|
CurrentTranslateEngineId int //目前翻译引擎Id
|
||||
|
MaxConcurrency int //任务最大处理并发数
|
||||
|
OutputType *AppSetingsOutput //输出文件类型
|
||||
|
OutputEncode int //输出文件编码
|
||||
|
SrtFileDir string //Srt文件输出目录
|
||||
|
SoundTrack int //输出音轨
|
||||
|
|
||||
|
TranslateSwitch bool //字幕翻译开关
|
||||
|
BilingualSubtitleSwitch bool //是否输出双语字幕
|
||||
|
InputLanguage int //输入字幕语言
|
||||
|
OutputLanguage int //输出字幕语言
|
||||
|
OutputMainSubtitleInputLanguage bool //双语主字幕(输入语言)
|
||||
|
|
||||
|
CloseIntelligentBlockSwitch bool //关闭智能分段
|
||||
|
CloseNewVersionMessage bool //关闭软件新版本提醒(默认开启)[false开启 true关闭]
|
||||
|
CloseAutoDeleteOssTempFile bool //关闭自动删除临时音频文件(默认开启)[false开启 true关闭]
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
const ( |
||||
|
FILTER_TYPE_STRING = 1 //文本过滤
|
||||
|
FILTER_TYPE_REGX = 2 //正则过滤
|
||||
|
) |
||||
|
//自定义过滤器规则
|
||||
|
type AppDefinedFilterRule struct { |
||||
|
Target string //目标规则
|
||||
|
Replace string //替换规则
|
||||
|
Way int //规则类型
|
||||
|
} |
||||
|
//应用字幕过滤器结构
|
||||
|
type AppFilterSetings struct { |
||||
|
//通用过滤器
|
||||
|
GlobalFilter struct{ |
||||
|
Switch bool |
||||
|
Words string //过滤词组
|
||||
|
} |
||||
|
//自定义过滤器
|
||||
|
DefinedFilter struct{ |
||||
|
Switch bool |
||||
|
Rule [] *AppDefinedFilterRule |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//任务文件列表 - 结构
|
||||
|
type TaskHandleFile struct { |
||||
|
Files [] string |
||||
|
} |
||||
|
|
||||
|
//根据配置初始化表单
|
||||
|
func (from *OperateFrom) Init(setings *AppSetings) { |
||||
|
from.OutputType = new(AppSetingsOutput) |
||||
|
if setings.CurrentEngineId != 0 { |
||||
|
from.EngineId = setings.CurrentEngineId |
||||
|
} |
||||
|
if setings.CurrentTranslateEngineId != 0 { |
||||
|
from.TranslateEngineId = setings.CurrentTranslateEngineId |
||||
|
} |
||||
|
|
||||
|
if !setings.OutputType.LRC && !setings.OutputType.SRT && !setings.OutputType.TXT { |
||||
|
from.OutputType.SRT = true |
||||
|
from.OutputSrt = true |
||||
|
} else { |
||||
|
from.OutputType = setings.OutputType |
||||
|
if setings.OutputType.SRT { |
||||
|
from.OutputSrt = true |
||||
|
} |
||||
|
if setings.OutputType.TXT { |
||||
|
from.OutputTxt = true |
||||
|
} |
||||
|
if setings.OutputType.LRC { |
||||
|
from.OutputLrc = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if setings.OutputEncode == 0 { |
||||
|
from.OutputEncode = OUTPUT_ENCODE_UTF8 //默认编码
|
||||
|
} else { |
||||
|
from.OutputEncode = setings.OutputEncode |
||||
|
} |
||||
|
|
||||
|
from.OutputMainSubtitleInputLanguage = setings.OutputMainSubtitleInputLanguage |
||||
|
|
||||
|
if setings.SoundTrack == 0 { |
||||
|
from.SoundTrack = 1 //默认输出音轨一
|
||||
|
} else { |
||||
|
from.SoundTrack = setings.SoundTrack |
||||
|
} |
||||
|
|
||||
|
//默认翻译设置
|
||||
|
if setings.InputLanguage == 0 { |
||||
|
from.InputLanguage = LANGUAGE_ZH |
||||
|
} else { |
||||
|
from.InputLanguage = setings.InputLanguage |
||||
|
} |
||||
|
if setings.OutputLanguage == 0 { |
||||
|
from.OutputLanguage = LANGUAGE_ZH |
||||
|
} else { |
||||
|
from.OutputLanguage = setings.OutputLanguage |
||||
|
} |
||||
|
|
||||
|
from.TranslateSwitch = setings.TranslateSwitch |
||||
|
from.BilingualSubtitleSwitch = setings.BilingualSubtitleSwitch |
||||
|
} |
||||
|
|
||||
|
//获取 输出文件选项列表
|
||||
|
func GetOutputOptionsSelects() []*OutputSelects { |
||||
|
return []*OutputSelects{ |
||||
|
&OutputSelects{Id:OUTPUT_SRT , Name:"字幕文件"}, |
||||
|
&OutputSelects{Id:OUTPUT_STRING , Name:"普通文本"}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//获取 输出文件编码选项列表
|
||||
|
func GetOutputEncodeOptionsSelects() []*OutputSelects { |
||||
|
return []*OutputSelects{ |
||||
|
&OutputSelects{Id:OUTPUT_ENCODE_UTF8 , Name:"UTF-8"}, |
||||
|
&OutputSelects{Id:OUTPUT_ENCODE_UTF8_BOM , Name:"UTF-8-BOM"}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//获取 输出音轨选项列表
|
||||
|
func GetSoundTrackSelects() []*SoundTrackSelects { |
||||
|
return []*SoundTrackSelects{ |
||||
|
&SoundTrackSelects{Id:3 , Name:"全部"}, |
||||
|
&SoundTrackSelects{Id:1 , Name:"音轨一"}, |
||||
|
&SoundTrackSelects{Id:2 , Name:"音轨二"}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取 允许翻译[输入字幕语言]列表
|
||||
|
func GetTranslateInputLanguageOptionsSelects() []*LanguageSelects { |
||||
|
return []*LanguageSelects{ |
||||
|
&LanguageSelects{Id:LANGUAGE_ZH , Name:"中文"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_EN , Name:"英文"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_JP , Name:"日语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_KOR , Name:"韩语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_FRA , Name:"法语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_DE , Name:"德语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_SPA , Name:"西班牙语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_RU , Name:"俄语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_IT , Name:"意大利语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_TH , Name:"泰语"}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//获取 允许翻译[输出字幕语言]列表
|
||||
|
func GetTranslateOutputLanguageOptionsSelects() []*LanguageSelects { |
||||
|
return []*LanguageSelects{ |
||||
|
&LanguageSelects{Id:LANGUAGE_ZH , Name:"中文"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_EN , Name:"英文"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_JP , Name:"日语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_KOR , Name:"韩语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_FRA , Name:"法语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_DE , Name:"德语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_SPA , Name:"西班牙语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_RU , Name:"俄语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_IT , Name:"意大利语"}, |
||||
|
&LanguageSelects{Id:LANGUAGE_TH , Name:"泰语"}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取 应用配置
|
||||
|
func (setings *AppSetingsAppStruct) GetCacheAppSetingsData() *AppSetings { |
||||
|
data := new(AppSetings) |
||||
|
data.OutputType = new(AppSetingsOutput) |
||||
|
vdata := setings.Data.Get(data) |
||||
|
if v, ok := vdata.(*AppSetings); ok { |
||||
|
return v |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
//设置 应用配置
|
||||
|
func (setings *AppSetingsAppStruct) SetCacheAppSetingsData(data *AppSetings) { |
||||
|
setings.Data.Set(data) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
//获取 应用过滤器配置
|
||||
|
func (setings *AppFilterAppStruct) GetCacheAppFilterData() *AppFilterSetings { |
||||
|
data := new(AppFilterSetings) |
||||
|
vdata := setings.Data.Get(data) |
||||
|
if v, ok := vdata.(*AppFilterSetings); ok { |
||||
|
return v |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
//设置 应用过滤器配置
|
||||
|
func (setings *AppFilterAppStruct) SetCacheAppFilterData(data *AppFilterSetings) { |
||||
|
setings.Data.Set(data) |
||||
|
} |
||||
|
|
||||
|
//过滤类型选项结构
|
||||
|
type FilterTypeSelects struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
} |
||||
|
//获取 过滤类型选项列表
|
||||
|
func GetFilterTypeOptionsSelects() []*FilterTypeSelects { |
||||
|
return []*FilterTypeSelects{ |
||||
|
&FilterTypeSelects{Id:FILTER_TYPE_STRING , Name:"文本替换"}, |
||||
|
&FilterTypeSelects{Id:FILTER_TYPE_REGX , Name:"正则替换"}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
//阿里云OSS - 缓存结构
|
||||
|
type AliyunOssCache struct { |
||||
|
aliyun.AliyunOss |
||||
|
} |
||||
|
|
||||
|
//设置 阿里云OSS 缓存
|
||||
|
func (oss *OssAppStruct) SetCacheAliyunOssData(data *AliyunOssCache) { |
||||
|
oss.Data.Set(data) |
||||
|
} |
||||
|
|
||||
|
//获取 阿里云OSS 缓存数据
|
||||
|
func (oss *OssAppStruct) GetCacheAliyunOssData() *AliyunOssCache { |
||||
|
data := new(AliyunOssCache) |
||||
|
vdata := oss.Data.Get(data) |
||||
|
if v, ok := vdata.(*AliyunOssCache); ok { |
||||
|
return v |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
//阿里云语音识别引擎 - 缓存结构
|
||||
|
type AliyunEngineCache struct { |
||||
|
aliyun.AliyunClound |
||||
|
Id int //Id
|
||||
|
Alias string //别名
|
||||
|
} |
||||
|
|
||||
|
//阿里云语音识别引擎 - 列表缓存结构
|
||||
|
type AliyunEngineListCache struct { |
||||
|
Engine [] *AliyunEngineCache |
||||
|
} |
||||
|
|
||||
|
//语音引擎选项
|
||||
|
type EngineSelects struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
} |
||||
|
|
||||
|
//阿里云语音引擎区域选项
|
||||
|
type AliyunEngineRegionSelects struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
} |
||||
|
|
||||
|
//获取 阿里云语音引擎区域选项列表
|
||||
|
func GetAliyunEngineRegionOptionSelects() []*BaiduAuthTypeSelects { |
||||
|
return []*BaiduAuthTypeSelects{ |
||||
|
&BaiduAuthTypeSelects{Id:aliyun.ALIYUN_CLOUND_REGION_CHA , Name:"中国"}, |
||||
|
&BaiduAuthTypeSelects{Id:aliyun.ALIYUN_CLOUND_REGION_INT , Name:"海外"}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//获取 引擎选项列表
|
||||
|
func (speechEng *SpeechEngineAppStruct) GetEngineOptionsSelects() []*EngineSelects { |
||||
|
engines := make([]*EngineSelects , 0) |
||||
|
//获取数据
|
||||
|
data := speechEng.GetCacheAliyunEngineListData() |
||||
|
|
||||
|
for _,v := range data.Engine { |
||||
|
engines = append(engines , &EngineSelects{ |
||||
|
Id:v.Id, |
||||
|
Name:v.Alias, |
||||
|
}) |
||||
|
} |
||||
|
return engines |
||||
|
} |
||||
|
|
||||
|
//根据选择引擎id 获取 引擎数据
|
||||
|
func (speechEng *SpeechEngineAppStruct) GetEngineById(id int) (*AliyunEngineCache , bool) { |
||||
|
//获取数据
|
||||
|
data := speechEng.GetCacheAliyunEngineListData() |
||||
|
for _,v := range data.Engine { |
||||
|
if id == v.Id { |
||||
|
return v , true |
||||
|
} |
||||
|
} |
||||
|
return nil , false |
||||
|
} |
||||
|
|
||||
|
//获取 当前引擎id 下标
|
||||
|
func (speechEng *SpeechEngineAppStruct) GetCurrentIndex(data []*EngineSelects , id int) int { |
||||
|
for index,v := range data { |
||||
|
if v.Id == id { |
||||
|
return index |
||||
|
} |
||||
|
} |
||||
|
return -1 |
||||
|
} |
||||
|
|
||||
|
//获取 阿里云语音识别引擎 缓存数据
|
||||
|
func (speechEng *SpeechEngineAppStruct) GetCacheAliyunEngineListData() *AliyunEngineListCache { |
||||
|
data := new(AliyunEngineListCache) |
||||
|
vdata := speechEng.Data.Get(data) |
||||
|
if v, ok := vdata.(*AliyunEngineListCache); ok { |
||||
|
return v |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
//设置 阿里云语音识别引擎 缓存
|
||||
|
func (speechEng *SpeechEngineAppStruct) SetCacheAliyunEngineListData(data *AliyunEngineListCache) { |
||||
|
speechEng.Data.Set(data) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//根据id 删除 阿里云语音识别引擎 缓存数据
|
||||
|
func (speechEng *SpeechEngineAppStruct) RemoveCacheAliyunEngineData(id int) (bool) { |
||||
|
var ok = false |
||||
|
var newEngine = make([]*AliyunEngineCache , 0) |
||||
|
origin := speechEng.GetCacheAliyunEngineListData() |
||||
|
|
||||
|
total := len(origin.Engine) |
||||
|
for i,engine := range origin.Engine { |
||||
|
if engine.Id == id { |
||||
|
if i == (total - 1) { |
||||
|
newEngine = origin.Engine[:i] |
||||
|
} else { |
||||
|
newEngine = append(origin.Engine[:i] , origin.Engine[i+1:]...) |
||||
|
} |
||||
|
ok = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if ok { |
||||
|
origin.Engine = newEngine |
||||
|
//更新缓存数据
|
||||
|
speechEng.SetCacheAliyunEngineListData(origin) |
||||
|
} |
||||
|
return ok |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
//声明引擎供应商
|
||||
|
const ( |
||||
|
TRANSLATE_SUPPLIER_BAIDU = 1 //百度翻译
|
||||
|
TRANSLATE_SUPPLIER_TENGXUNYUN = 2 //腾讯云翻译
|
||||
|
) |
||||
|
|
||||
|
//翻译引擎 - 数据结构
|
||||
|
type TranslateEngineStruct struct { |
||||
|
TengxunyunEngine translate.TengxunyunTranslate |
||||
|
BaiduEngine translate.BaiduTranslate |
||||
|
|
||||
|
Supplier int //引擎供应商
|
||||
|
Id int //Id
|
||||
|
Alias string //别名
|
||||
|
} |
||||
|
|
||||
|
//翻译引擎 - 列表缓存结构
|
||||
|
type TranslateEngineListCacheStruct struct { |
||||
|
Engine [] *TranslateEngineStruct |
||||
|
} |
||||
|
|
||||
|
//翻译引擎选项 - 数据结构
|
||||
|
type TranslateEngineSelects struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
} |
||||
|
|
||||
|
//获取 翻译引擎 选项列表
|
||||
|
func (translateEng *TranslateEngineAppStruct) GetTranslateEngineOptionsSelects() []*TranslateEngineSelects { |
||||
|
engines := make([]*TranslateEngineSelects , 0) |
||||
|
//获取数据
|
||||
|
data := translateEng.GetCacheTranslateEngineListData() |
||||
|
|
||||
|
for _,v := range data.Engine { |
||||
|
engines = append(engines , &TranslateEngineSelects{ |
||||
|
Id:v.Id, |
||||
|
Name:v.Alias, |
||||
|
}) |
||||
|
} |
||||
|
return engines |
||||
|
} |
||||
|
|
||||
|
//根据 选择翻译引擎id 获取数据
|
||||
|
func (translateEng *TranslateEngineAppStruct) GetTranslateEngineById(id int) (*TranslateEngineStruct , bool) { |
||||
|
//获取数据
|
||||
|
data := translateEng.GetCacheTranslateEngineListData() |
||||
|
for _,v := range data.Engine { |
||||
|
if id == v.Id { |
||||
|
return v , true |
||||
|
} |
||||
|
} |
||||
|
return nil , false |
||||
|
} |
||||
|
|
||||
|
//获取 当前翻译引擎id 下标
|
||||
|
func (translateEng *TranslateEngineAppStruct) GetCurrentTranslateEngineIndex(data []*TranslateEngineSelects , id int) int { |
||||
|
for index,v := range data { |
||||
|
if v.Id == id { |
||||
|
return index |
||||
|
} |
||||
|
} |
||||
|
return -1 |
||||
|
} |
||||
|
|
||||
|
//设置 翻译引擎 数据缓存
|
||||
|
func (translateEng *TranslateEngineAppStruct) SetCacheTranslateEngineListData(data *TranslateEngineListCacheStruct) { |
||||
|
translateEng.Data.Set(data) |
||||
|
} |
||||
|
|
||||
|
//根据id 删除 翻译引擎 数据缓存
|
||||
|
func (translateEng *TranslateEngineAppStruct) RemoveCacheTranslateEngineData(id int) (bool) { |
||||
|
var ok = false |
||||
|
var newEngine = make([]*TranslateEngineStruct , 0) |
||||
|
origin := translateEng.GetCacheTranslateEngineListData() |
||||
|
|
||||
|
total := len(origin.Engine) |
||||
|
for i,engine := range origin.Engine { |
||||
|
if engine.Id == id { |
||||
|
if i == (total - 1) { |
||||
|
newEngine = origin.Engine[:i] |
||||
|
} else { |
||||
|
newEngine = append(origin.Engine[:i] , origin.Engine[i+1:]...) |
||||
|
} |
||||
|
ok = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if ok { |
||||
|
origin.Engine = newEngine |
||||
|
//更新缓存数据
|
||||
|
translateEng.SetCacheTranslateEngineListData(origin) |
||||
|
} |
||||
|
return ok |
||||
|
} |
||||
|
|
||||
|
//获取 翻译引擎 数据缓存
|
||||
|
func (translateEng *TranslateEngineAppStruct) GetCacheTranslateEngineListData() *TranslateEngineListCacheStruct { |
||||
|
data := new(TranslateEngineListCacheStruct) |
||||
|
vdata := translateEng.Data.Get(data) |
||||
|
if v, ok := vdata.(*TranslateEngineListCacheStruct); ok { |
||||
|
return v |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取不同翻译引擎的语音字符标识
|
||||
|
func GetLanguageChar(Language int , Supplier int) string { |
||||
|
if Supplier == TRANSLATE_SUPPLIER_BAIDU { |
||||
|
switch Language { |
||||
|
case LANGUAGE_ZH: |
||||
|
return "zh" |
||||
|
case LANGUAGE_EN: |
||||
|
return "en" |
||||
|
case LANGUAGE_JP: |
||||
|
return "jp" |
||||
|
case LANGUAGE_KOR: |
||||
|
return "kor" |
||||
|
case LANGUAGE_FRA: |
||||
|
return "fra" |
||||
|
case LANGUAGE_DE: |
||||
|
return "de" |
||||
|
case LANGUAGE_SPA: |
||||
|
return "spa" |
||||
|
case LANGUAGE_RU: |
||||
|
return "ru" |
||||
|
case LANGUAGE_IT: |
||||
|
return "it" |
||||
|
case LANGUAGE_TH: |
||||
|
return "th" |
||||
|
} |
||||
|
} |
||||
|
if Supplier == TRANSLATE_SUPPLIER_TENGXUNYUN { |
||||
|
switch Language { |
||||
|
case LANGUAGE_ZH: |
||||
|
return "zh" |
||||
|
case LANGUAGE_EN: |
||||
|
return "en" |
||||
|
case LANGUAGE_JP: |
||||
|
return "jp" |
||||
|
case LANGUAGE_KOR: |
||||
|
return "kr" |
||||
|
case LANGUAGE_FRA: |
||||
|
return "fr" |
||||
|
case LANGUAGE_DE: |
||||
|
return "de" |
||||
|
case LANGUAGE_SPA: |
||||
|
return "es" |
||||
|
case LANGUAGE_RU: |
||||
|
return "ru" |
||||
|
case LANGUAGE_IT: |
||||
|
return "it" |
||||
|
case LANGUAGE_TH: |
||||
|
return "th" |
||||
|
} |
||||
|
} |
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//百度翻译账号认证类型选项
|
||||
|
type BaiduAuthTypeSelects struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
} |
||||
|
|
||||
|
//获取 百度翻译账号认证类型
|
||||
|
func GetBaiduTranslateAuthenTypeOptionsSelects() []*BaiduAuthTypeSelects { |
||||
|
return []*BaiduAuthTypeSelects{ |
||||
|
&BaiduAuthTypeSelects{Id:translate.ACCOUNT_COMMON_AUTHEN , Name:"标准版"}, |
||||
|
&BaiduAuthTypeSelects{Id:translate.ACCOUNT_SENIOR_AUTHEN , Name:"高级版"}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,53 @@ |
|||||
|
package datacache |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
"videosrt/app/tool" |
||||
|
) |
||||
|
|
||||
|
type AppCache struct { |
||||
|
RootDir string |
||||
|
Dir string |
||||
|
File string |
||||
|
} |
||||
|
|
||||
|
func NewAppCahce(rootdir string , file string) *AppCache { |
||||
|
c := new(AppCache) |
||||
|
c.Dir = "data/json" |
||||
|
c.RootDir = rootdir |
||||
|
c.File = c.RootDir + "/" + c.Dir + "/" + file + ".json" |
||||
|
|
||||
|
c.initDir() |
||||
|
return c |
||||
|
} |
||||
|
|
||||
|
func (app *AppCache) Set(data interface{}) { |
||||
|
file := app.File |
||||
|
if err := SavetoJson(data , file); err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func (app *AppCache) Get(structs interface{}) interface{} { |
||||
|
file := app.File |
||||
|
err, data := GettoJson(file, structs) |
||||
|
if err != nil { |
||||
|
if os.IsNotExist(err) { |
||||
|
return structs |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func (app *AppCache) initDir() { |
||||
|
fileDir := app.RootDir + "/" + app.Dir |
||||
|
if !tool.DirExists(fileDir) { |
||||
|
//创建目录
|
||||
|
if err := tool.CreateDir(fileDir , false); err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,31 @@ |
|||||
|
package datacache |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"io/ioutil" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
func SavetoJson(data interface{} , path string) error { |
||||
|
string , err := json.Marshal(data) |
||||
|
if err != nil{ |
||||
|
return err |
||||
|
} |
||||
|
err = ioutil.WriteFile(path , string , os.ModePerm) |
||||
|
if err != nil{ |
||||
|
return err |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func GettoJson(path string , structs interface{}) (error , interface{}) { |
||||
|
data , err := ioutil.ReadFile(path) |
||||
|
if err != nil{ |
||||
|
return err , structs |
||||
|
} |
||||
|
err = json.Unmarshal(data , structs) |
||||
|
if err != nil{ |
||||
|
return err , structs |
||||
|
} |
||||
|
return nil , structs |
||||
|
} |
@ -0,0 +1,57 @@ |
|||||
|
package ffmpeg |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"os" |
||||
|
"os/exec" |
||||
|
"strings" |
||||
|
"syscall" |
||||
|
"videosrt/app/tool" |
||||
|
) |
||||
|
|
||||
|
type Ffmpeg struct { |
||||
|
Os string //ffmpeg 文件目录
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
//提取视频音频
|
||||
|
func ExtractAudio (video string , tmpAudio string) (error) { |
||||
|
//校验
|
||||
|
if e := VailFfmpegLibrary();e != nil { |
||||
|
return e |
||||
|
} |
||||
|
cmd := exec.Command("ffmpeg" , "-i" , video , "-ar" , "16000" , tmpAudio) |
||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} |
||||
|
err := cmd.Run() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//校验依赖库
|
||||
|
func VailFfmpegLibrary() error { |
||||
|
ts := exec.Command("ffmpeg" , "-version") |
||||
|
ts.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} |
||||
|
if err := ts.Run() ; err != nil { |
||||
|
return errors.New("请先安装 ffmpeg 依赖 ,并设置环境变量") |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//校验ffmpeg并加入临时环境遍历
|
||||
|
func VailTempFfmpegLibrary(rootDir string) { |
||||
|
ffmpegDir := tool.WinDir(rootDir + "/ffmpeg") |
||||
|
|
||||
|
if tool.DirExists(ffmpegDir) { |
||||
|
//临时加入用户环境变量
|
||||
|
path := os.Getenv("PATH") |
||||
|
ps := strings.Split(path , ";") |
||||
|
ps = append(ps , ffmpegDir) |
||||
|
path = strings.Join(ps , ";") |
||||
|
|
||||
|
_ = os.Setenv("PATH", path) |
||||
|
} |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
package app |
||||
|
|
||||
|
import ( |
||||
|
"github.com/lxn/walk" |
||||
|
) |
||||
|
|
||||
|
type TaskLog struct { |
||||
|
TextEdit *walk.TextEdit |
||||
|
} |
||||
|
|
||||
|
func NewTasklog(textEdit *walk.TextEdit) *TaskLog { |
||||
|
task := new(TaskLog) |
||||
|
task.TextEdit = textEdit |
||||
|
return task |
||||
|
} |
||||
|
|
||||
|
func (t *TaskLog) SetTextEdit(textEdit *walk.TextEdit) { |
||||
|
t.TextEdit = textEdit |
||||
|
} |
||||
|
|
||||
|
func (t *TaskLog) AppendLogText(s string) { |
||||
|
t.TextEdit.AppendText(s + "\r\n") |
||||
|
} |
||||
|
|
||||
|
func (t *TaskLog) ClearLogText() { |
||||
|
t.TextEdit.SetText("") |
||||
|
} |
@ -0,0 +1,148 @@ |
|||||
|
package parse |
||||
|
|
||||
|
import ( |
||||
|
"bufio" |
||||
|
"errors" |
||||
|
"os" |
||||
|
"strings" |
||||
|
"videosrt/app/tool" |
||||
|
) |
||||
|
|
||||
|
//允许解析的文件类型
|
||||
|
const ( |
||||
|
PARSE_FILE_SRT = 1 |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
PARSE_SRT_NUMBER = 1 |
||||
|
PARSE_SRT_TIME_RANGE = 2 |
||||
|
PARSE_SRT_SUBTITLE_TEXT = 3 |
||||
|
PARSE_SRT_TRIM = 4 |
||||
|
) |
||||
|
|
||||
|
//字幕行结构
|
||||
|
type SrtRows struct { |
||||
|
Id int //字幕自然行id
|
||||
|
Number string //字幕序号
|
||||
|
TimeStart string //字幕开始时间戳
|
||||
|
TimeStartSecond float64 //字幕开始(秒)
|
||||
|
TimeEnd string //字幕结束时间戳
|
||||
|
TimeEndSecond float64 //字幕结束(秒)
|
||||
|
Text []string //字幕文本
|
||||
|
} |
||||
|
|
||||
|
//字幕文件属性
|
||||
|
type Srt struct { |
||||
|
File string |
||||
|
FileType int |
||||
|
AppointBilingualRows int //指定使用N行字幕文本[双语字幕参数]
|
||||
|
} |
||||
|
|
||||
|
//字幕解析器
|
||||
|
type SubtitleParse struct { |
||||
|
Srt *Srt |
||||
|
Rows []*SrtRows |
||||
|
} |
||||
|
|
||||
|
//获取解析字幕实例
|
||||
|
func NewSubtitleParse(srt *Srt) (*SubtitleParse) { |
||||
|
parse := new(SubtitleParse) |
||||
|
if srt.FileType == 0 { |
||||
|
srt.FileType = PARSE_FILE_SRT //默认文件类型
|
||||
|
} |
||||
|
|
||||
|
parse.Srt = srt |
||||
|
return parse |
||||
|
} |
||||
|
|
||||
|
//解析文本
|
||||
|
func (parse *SubtitleParse) Parse() error { |
||||
|
srtfile := parse.Srt.File |
||||
|
//校验文件
|
||||
|
if tool.VaildFile(srtfile) == false { |
||||
|
return errors.New("字幕文件不存在") |
||||
|
} |
||||
|
|
||||
|
if file, err := os.Open(srtfile); err!=nil { |
||||
|
return errors.New("打开字幕文件失败") |
||||
|
} else { |
||||
|
|
||||
|
lineRows := 0 |
||||
|
lineStart := false |
||||
|
lineRowSrt := new(SrtRows) |
||||
|
scanner := bufio.NewScanner(file) |
||||
|
|
||||
|
for scanner.Scan() { |
||||
|
lineText := scanner.Text() |
||||
|
|
||||
|
//逐行读取
|
||||
|
if strResult, rowsType , err := ParseSrtRows(lineText , lineStart); err != nil { |
||||
|
return err |
||||
|
} else { |
||||
|
|
||||
|
//分支处理
|
||||
|
switch rowsType { |
||||
|
case PARSE_SRT_NUMBER: |
||||
|
lineRowSrt = new(SrtRows) |
||||
|
lineRowSrt.Id = lineRows |
||||
|
lineRowSrt.Number = strResult |
||||
|
|
||||
|
lineStart = true |
||||
|
break |
||||
|
case PARSE_SRT_TIME_RANGE: |
||||
|
srtTimes := strings.Split(strResult , "-") |
||||
|
lineRowSrt.TimeStart = srtTimes[0] |
||||
|
lineRowSrt.TimeEnd = srtTimes[1] |
||||
|
break |
||||
|
case PARSE_SRT_SUBTITLE_TEXT: |
||||
|
lineRowSrt.Text = append(lineRowSrt.Text , strResult) |
||||
|
break |
||||
|
case PARSE_SRT_TRIM: |
||||
|
//结束上一段字幕输入
|
||||
|
if lineStart == true { |
||||
|
if lineRowSrt.TimeStart != "" && lineRowSrt.TimeEnd != "" && len(lineRowSrt.Text) > 0 { |
||||
|
if fsecond, err := SrtTimeFormatToSecond(lineRowSrt.TimeStart);err != nil { |
||||
|
return err |
||||
|
} else { |
||||
|
lineRowSrt.TimeStartSecond = fsecond |
||||
|
} |
||||
|
if fsecond, err := SrtTimeFormatToSecond(lineRowSrt.TimeEnd);err != nil { |
||||
|
return err |
||||
|
} else { |
||||
|
lineRowSrt.TimeEndSecond = fsecond |
||||
|
} |
||||
|
lineRows++; |
||||
|
//追加行
|
||||
|
parse.Rows = append(parse.Rows , lineRowSrt) |
||||
|
} |
||||
|
lineStart = false |
||||
|
} |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
//未结束的最后一行
|
||||
|
if lineStart == true { |
||||
|
if lineRowSrt.TimeStart != "" && lineRowSrt.TimeEnd != "" && len(lineRowSrt.Text) > 0 { |
||||
|
if fsecond, err := SrtTimeFormatToSecond(lineRowSrt.TimeStart);err != nil { |
||||
|
return err |
||||
|
} else { |
||||
|
lineRowSrt.TimeStartSecond = fsecond |
||||
|
} |
||||
|
if fsecond, err := SrtTimeFormatToSecond(lineRowSrt.TimeEnd);err != nil { |
||||
|
return err |
||||
|
} else { |
||||
|
lineRowSrt.TimeEndSecond = fsecond |
||||
|
} |
||||
|
lineRows++; |
||||
|
//追加行
|
||||
|
parse.Rows = append(parse.Rows , lineRowSrt) |
||||
|
} |
||||
|
lineStart = false |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
return nil |
||||
|
} |
@ -0,0 +1,88 @@ |
|||||
|
package parse |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"regexp" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
|
||||
|
//解析SRT行文本
|
||||
|
func ParseSrtRows(rowText string , lineStart bool) (string , int , error) { |
||||
|
rowText = strings.TrimSpace(rowText) |
||||
|
|
||||
|
if _,e := strconv.Atoi(rowText);e == nil && lineStart == false { //识别为字幕序号
|
||||
|
return rowText , PARSE_SRT_NUMBER , nil |
||||
|
} |
||||
|
r := regexp.MustCompile(`(\d{1,2}:\d{1,2}:\d{1,2}[,|\.]\d{2,4})\s*-->\s*(\d{1,2}:\d{1,2}:\d{1,2}[,|\.]\d{2,4})`) |
||||
|
if r.Match([]byte(rowText)) == true { |
||||
|
timeResult := r.FindSubmatch([]byte(rowText)) |
||||
|
if len(timeResult) != 3 { |
||||
|
return "" , 0 , errors.New("解析字幕时间错误") |
||||
|
} |
||||
|
times := strings.Join([]string{string(timeResult[1]) , string(timeResult[2])} , "-") |
||||
|
return times , PARSE_SRT_TIME_RANGE , nil |
||||
|
} |
||||
|
if rowText != "" { |
||||
|
return rowText , PARSE_SRT_SUBTITLE_TEXT , nil |
||||
|
} else { |
||||
|
return rowText , PARSE_SRT_TRIM , nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//校验srt字幕时间格式
|
||||
|
func VaildateSrtTimeFormat(time string) bool { |
||||
|
r := regexp.MustCompile(`^(\d{1,2}:\d{1,2}:\d{1,2}[,|\.]\d{2,4})$`) |
||||
|
return r.Match([]byte(time)) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//srt字幕时间文本 -> 秒
|
||||
|
func SrtTimeFormatToSecond(time string) (float64 , error) { |
||||
|
if VaildateSrtTimeFormat(time) == false { |
||||
|
return 0 , errors.New("字幕时间格式不正确") |
||||
|
} |
||||
|
time = strings.Replace(time , "," , "." , 1) |
||||
|
timeSplit := strings.Split(time , ":") |
||||
|
if len(timeSplit) != 3 { |
||||
|
return 0 , errors.New("字幕时间格式不正确") |
||||
|
} |
||||
|
var second float64 = 0 |
||||
|
for k,v := range timeSplit { |
||||
|
if vn, err := strconv.ParseFloat(v , 64); err != nil { |
||||
|
return 0 , errors.New("字幕时间解析失败") |
||||
|
} else { |
||||
|
if k == 0 { |
||||
|
second += vn * 3600 |
||||
|
} else if k == 1 { |
||||
|
second += vn * 60 |
||||
|
} else if k == 2 { |
||||
|
second += vn |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return second , nil |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func SubString(str string , begin int ,length int) (substr string) { |
||||
|
// 将字符串的转换成[]rune
|
||||
|
rs := []rune(str) |
||||
|
lth := len(rs) |
||||
|
|
||||
|
// 简单的越界判断
|
||||
|
if begin < 0 { |
||||
|
begin = 0 |
||||
|
} |
||||
|
if begin >= lth { |
||||
|
begin = lth |
||||
|
} |
||||
|
end := begin + length |
||||
|
if end > lth { |
||||
|
end = lth |
||||
|
} |
||||
|
// 返回子串
|
||||
|
return string(rs[begin:end]) |
||||
|
} |
@ -0,0 +1,451 @@ |
|||||
|
package app |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"path" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"time" |
||||
|
"videosrt/app/parse" |
||||
|
"videosrt/app/tool" |
||||
|
"videosrt/app/translate" |
||||
|
) |
||||
|
|
||||
|
//字幕行结构
|
||||
|
type SrtRows struct { |
||||
|
Id int //字幕自然行id
|
||||
|
Number string //字幕序号
|
||||
|
TimeStart string //字幕开始时间戳
|
||||
|
TimeStartSecond float64 //字幕开始(秒)
|
||||
|
TimeStartMilliSecond int64 //字幕开始(毫秒)
|
||||
|
TimeEnd string //字幕结束时间戳
|
||||
|
TimeEndSecond float64 //字幕结束(秒)
|
||||
|
TimeEndMilliSecond int64 //字幕结束(毫秒)
|
||||
|
Text string //字幕文本
|
||||
|
TranslateText string //翻译字幕文本
|
||||
|
} |
||||
|
|
||||
|
//应用翻译配置
|
||||
|
type SrtTranslateStruct struct { |
||||
|
TranslateSwitch bool //字幕翻译开关
|
||||
|
Supplier int //引擎供应商
|
||||
|
BilingualSubtitleSwitch bool //是否输出双语字幕
|
||||
|
InputLanguage int //输入字幕语言
|
||||
|
OutputLanguage int //输出字幕语言
|
||||
|
OutputMainSubtitleInputLanguage bool //双语主字幕(输入语言)
|
||||
|
|
||||
|
BaiduTranslate translate.BaiduTranslate //百度翻译
|
||||
|
TengxunyunTranslate translate.TengxunyunTranslate //腾讯云翻译
|
||||
|
} |
||||
|
|
||||
|
//统一翻译结果
|
||||
|
type SrtTranslateResult struct { |
||||
|
From string //翻译源语言
|
||||
|
To string //译文语言
|
||||
|
TransResultSrc string //翻译结果(原文)
|
||||
|
TransResultDst string //翻译结果(译文)
|
||||
|
} |
||||
|
|
||||
|
//主应用
|
||||
|
type SrtTranslateApp struct { |
||||
|
AppDir string //应用根目录
|
||||
|
SrtDir string //文件输出目录
|
||||
|
OutputType *AppSetingsOutput //输出文件类型
|
||||
|
OutputEncode int //输出文件编码
|
||||
|
MaxConcurrency int //最大处理并发数
|
||||
|
TranslateCfg *SrtTranslateStruct //翻译配置
|
||||
|
FilterSetings *AppFilterSetings //过滤器配置
|
||||
|
|
||||
|
LogHandler func(s string , file string) //日志回调
|
||||
|
SuccessHandler func(file string) //成功回调
|
||||
|
FailHandler func(file string) //失败回调
|
||||
|
} |
||||
|
|
||||
|
//获取应用
|
||||
|
func NewSrtTranslateApp(appDir string) *SrtTranslateApp { |
||||
|
app := new(SrtTranslateApp) |
||||
|
app.AppDir = appDir |
||||
|
app.OutputEncode = OUTPUT_ENCODE_UTF8 //默认输出文件编码
|
||||
|
app.MaxConcurrency = 2 |
||||
|
|
||||
|
//实例应用翻译配置
|
||||
|
app.TranslateCfg = new(SrtTranslateStruct) |
||||
|
return app |
||||
|
} |
||||
|
|
||||
|
//加载翻译设置
|
||||
|
func (app *SrtTranslateApp) InitTranslateConfig (translateSettings *SrtTranslateStruct) { |
||||
|
app.TranslateCfg = translateSettings |
||||
|
} |
||||
|
//加载过滤器配置
|
||||
|
func (app *SrtTranslateApp) InitFilterConfig (filterSetings *AppFilterSetings) { |
||||
|
app.FilterSetings = filterSetings |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func (app *SrtTranslateApp) SetSrtDir(dir string) { |
||||
|
app.SrtDir = dir |
||||
|
} |
||||
|
|
||||
|
func (app *SrtTranslateApp) SetOutputType(output *AppSetingsOutput) { |
||||
|
app.OutputType = output |
||||
|
} |
||||
|
|
||||
|
func (app *SrtTranslateApp) SetOutputEncode(encode int) { |
||||
|
app.OutputEncode = encode |
||||
|
} |
||||
|
|
||||
|
func (app *SrtTranslateApp) SetMaxConcurrency(number int) { |
||||
|
if number == 0 { |
||||
|
number = 2 |
||||
|
} |
||||
|
app.MaxConcurrency = number |
||||
|
} |
||||
|
|
||||
|
func (app *SrtTranslateApp) SetSuccessHandler(callback func(file string)) { |
||||
|
app.SuccessHandler = callback |
||||
|
} |
||||
|
|
||||
|
func (app *SrtTranslateApp) SetFailHandler(callback func(file string)) { |
||||
|
app.FailHandler = callback |
||||
|
} |
||||
|
|
||||
|
func (app *SrtTranslateApp) SetLogHandler(callback func(s string , file string)) { |
||||
|
app.LogHandler = callback |
||||
|
} |
||||
|
|
||||
|
//输出日志
|
||||
|
func (app *SrtTranslateApp) Log(s string , file string) { |
||||
|
app.LogHandler(s , file) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//应用运行
|
||||
|
func (app *SrtTranslateApp) Run(srtfile string) { |
||||
|
//致命错误捕获
|
||||
|
defer func() { |
||||
|
//拦截panic
|
||||
|
if err := recover(); err != nil { |
||||
|
//失败回调
|
||||
|
app.FailHandler(srtfile) |
||||
|
|
||||
|
e , ok := err.(error) |
||||
|
if ok { |
||||
|
app.Log("错误:" + e.Error() , srtfile) |
||||
|
} else { |
||||
|
panic(err) |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
//校验文件
|
||||
|
if tool.VaildFile(srtfile) != true { |
||||
|
panic("字幕文件不存在") |
||||
|
} |
||||
|
|
||||
|
//分析、解析字幕
|
||||
|
srt := &parse.Srt{File:srtfile} |
||||
|
subtitleParse := parse.NewSubtitleParse(srt) |
||||
|
if e := subtitleParse.Parse(); e!=nil { |
||||
|
panic("字幕文件解析错误:" + e.Error()) |
||||
|
} |
||||
|
//获取字幕结构
|
||||
|
srtRows := app.SrtHandleGetData(subtitleParse) |
||||
|
|
||||
|
app.Log("字幕文件解析成功" , srtfile) |
||||
|
|
||||
|
//字幕翻译
|
||||
|
app.SrtTranslate(srtfile , srtRows) |
||||
|
|
||||
|
//字幕过滤
|
||||
|
app.SrtFilters(srtRows , srtfile) |
||||
|
|
||||
|
//输出文件
|
||||
|
if app.OutputType.SRT { |
||||
|
app.SrtOutputFile(srtfile , srtRows , OUTPUT_SRT) |
||||
|
} |
||||
|
if app.OutputType.LRC { |
||||
|
app.SrtOutputFile(srtfile , srtRows , OUTPUT_LRC) |
||||
|
} |
||||
|
if app.OutputType.TXT { |
||||
|
app.SrtOutputFile(srtfile , srtRows , OUTPUT_STRING) |
||||
|
} |
||||
|
|
||||
|
app.Log("处理完成" , srtfile) |
||||
|
|
||||
|
//成功回调
|
||||
|
app.SuccessHandler(srtfile) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//字幕结构处理
|
||||
|
func (app *SrtTranslateApp) SrtHandleGetData(srt *parse.SubtitleParse) ([] *SrtRows) { |
||||
|
data := make([]*SrtRows , 0) |
||||
|
for _,rows := range srt.Rows { |
||||
|
temp := new(SrtRows) |
||||
|
temp.Id = rows.Id |
||||
|
temp.Number = rows.Number |
||||
|
temp.TimeStart = rows.TimeStart |
||||
|
temp.TimeStartSecond = rows.TimeStartSecond |
||||
|
temp.TimeStartMilliSecond = int64(rows.TimeStartSecond * 1000) |
||||
|
temp.TimeEnd = rows.TimeEnd |
||||
|
temp.TimeEndSecond = rows.TimeEndSecond |
||||
|
temp.TimeEndMilliSecond = int64(rows.TimeEndSecond * 1000) |
||||
|
|
||||
|
line := srt.Srt.AppointBilingualRows |
||||
|
if line < 0 || line > 1 { |
||||
|
line = 0 |
||||
|
} |
||||
|
if len(rows.Text) == 0 { |
||||
|
continue |
||||
|
} |
||||
|
temp.Text = rows.Text[line] |
||||
|
data = append(data , temp) |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//字幕翻译处理
|
||||
|
func (app *SrtTranslateApp) SrtTranslate(file string , srtRows []*SrtRows) { |
||||
|
//输出日志
|
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
app.Log("字幕翻译处理中 ..." , file) |
||||
|
|
||||
|
//百度翻译标准版
|
||||
|
if app.TranslateCfg.Supplier == TRANSLATE_SUPPLIER_BAIDU && app.TranslateCfg.BaiduTranslate.AuthenType == translate.ACCOUNT_COMMON_AUTHEN { |
||||
|
app.Log("你使用的是 “百度翻译标准版” 账号,翻译速度较慢,请耐心等待 ..." , file) |
||||
|
} |
||||
|
} else { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
total := len(srtRows) |
||||
|
var lastrv float64 = 0 |
||||
|
//翻译处理
|
||||
|
index := 0 |
||||
|
for _,data := range srtRows { |
||||
|
transResult,e := app.RunTranslate(data.Text , file) |
||||
|
if e != nil { |
||||
|
panic(e) //终止翻译
|
||||
|
} |
||||
|
data.TranslateText = strings.TrimSpace(transResult.TransResultDst) //译文
|
||||
|
index++ |
||||
|
|
||||
|
rv := (float64(index)/float64(total))*100 |
||||
|
if (rv - lastrv) > 20 { |
||||
|
//输出比例
|
||||
|
app.Log("字幕翻译已处理:" + fmt.Sprintf("%.2f" , rv) + "%" , file) |
||||
|
lastrv = rv |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//字幕过滤处理
|
||||
|
func (app *SrtTranslateApp) SrtFilters (srtRows []*SrtRows , file string) { |
||||
|
if !app.FilterSetings.DefinedFilter.Switch && !app.FilterSetings.GlobalFilter.Switch { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
app.Log("字幕过滤处理中 ..." , file) |
||||
|
|
||||
|
//语气词过滤
|
||||
|
if app.FilterSetings.GlobalFilter.Switch && strings.TrimSpace(app.FilterSetings.GlobalFilter.Words) != "" { |
||||
|
modalWords := strings.Split(app.FilterSetings.GlobalFilter.Words , "\r\n") |
||||
|
for _ , row := range srtRows { |
||||
|
for _ , w := range modalWords { |
||||
|
row.Text = ModalWordsFilter(row.Text , w) |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
row.TranslateText = ModalWordsFilter(row.TranslateText , w) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
//自定义规则过滤
|
||||
|
if app.FilterSetings.DefinedFilter.Switch && len(app.FilterSetings.DefinedFilter.Rule) > 0 { |
||||
|
rules := app.FilterSetings.DefinedFilter.Rule |
||||
|
for _ , row := range srtRows { |
||||
|
for _ , ru := range rules { |
||||
|
row.Text = DefinedWordRuleFilter(row.Text , ru) |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
row.TranslateText = DefinedWordRuleFilter(row.TranslateText , ru) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//文件输出
|
||||
|
func (app *SrtTranslateApp) SrtOutputFile(file string , srtRows []*SrtRows , outputType int) { |
||||
|
var subfileDir string |
||||
|
if app.SrtDir == "" { |
||||
|
subfileDir = path.Dir(file) |
||||
|
} else { |
||||
|
subfileDir = app.SrtDir |
||||
|
} |
||||
|
subfile := tool.GetFileBaseName(file) |
||||
|
//输出文件名
|
||||
|
thisfile := subfileDir + "/" + subfile + "_translaste" |
||||
|
//文件后缀
|
||||
|
if outputType == OUTPUT_SRT { |
||||
|
thisfile += ".srt" |
||||
|
} else if outputType == OUTPUT_STRING { |
||||
|
thisfile += ".txt" |
||||
|
} else if outputType == OUTPUT_LRC { |
||||
|
thisfile += ".lrc" |
||||
|
} |
||||
|
//创建文件
|
||||
|
fd, e := os.Create(thisfile) |
||||
|
if e != nil { |
||||
|
panic("创建文件失败:" + e.Error()) |
||||
|
} |
||||
|
defer fd.Close() |
||||
|
//文件编码分支
|
||||
|
if app.OutputEncode == OUTPUT_ENCODE_UTF8_BOM { |
||||
|
if _, e := fd.Write([]byte{0xEF, 0xBB, 0xBF});e != nil { |
||||
|
panic(e) |
||||
|
} |
||||
|
} |
||||
|
//歌词头
|
||||
|
if outputType == OUTPUT_LRC { |
||||
|
_,_ = fd.WriteString("[ar:]\r\n[ti:]\r\n[al:]\r\n[by:]\r\n") |
||||
|
} |
||||
|
|
||||
|
//主字幕
|
||||
|
bilingualAsc := app.TranslateCfg.OutputMainSubtitleInputLanguage |
||||
|
index := 0 |
||||
|
|
||||
|
//普通文本容器
|
||||
|
var txtOutputContent bytes.Buffer |
||||
|
var txtTransalteOutputContent bytes.Buffer |
||||
|
|
||||
|
for _ , data := range srtRows { |
||||
|
var linestr string |
||||
|
|
||||
|
if data.Text == "" { |
||||
|
continue //跳过空行
|
||||
|
} |
||||
|
|
||||
|
//字幕、歌词文件处理
|
||||
|
if outputType == OUTPUT_SRT || outputType == OUTPUT_LRC { |
||||
|
//拼接
|
||||
|
if outputType == OUTPUT_SRT { |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
if app.TranslateCfg.BilingualSubtitleSwitch { |
||||
|
linestr = MakeSubtitleText(index , data.TimeStartMilliSecond , data.TimeEndMilliSecond , data.Text , data.TranslateText , true , bilingualAsc) |
||||
|
} else { |
||||
|
linestr = MakeSubtitleText(index , data.TimeStartMilliSecond , data.TimeEndMilliSecond , data.TranslateText , "" , false , true) |
||||
|
} |
||||
|
} else { |
||||
|
linestr = MakeSubtitleText(index , data.TimeStartMilliSecond , data.TimeEndMilliSecond , data.Text , "" , false , true) |
||||
|
} |
||||
|
} else if outputType == OUTPUT_LRC { |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
linestr = MakeMusicLrcText(index , data.TimeStartMilliSecond , data.TimeEndMilliSecond , data.TranslateText) |
||||
|
} else { |
||||
|
linestr = MakeMusicLrcText(index , data.TimeStartMilliSecond , data.TimeEndMilliSecond , data.Text) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//写入行
|
||||
|
if _, e = fd.WriteString(linestr);e != nil { |
||||
|
panic("写入文件失败:" + e.Error()) |
||||
|
} |
||||
|
} else if outputType == OUTPUT_STRING { |
||||
|
//普通文本处理
|
||||
|
txtOutputContent.WriteString(data.Text) |
||||
|
txtOutputContent.WriteString("\r\n") |
||||
|
|
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
txtTransalteOutputContent.WriteString(data.TranslateText) |
||||
|
txtTransalteOutputContent.WriteString("\r\n") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
index++ |
||||
|
} |
||||
|
|
||||
|
//写入文本文件
|
||||
|
if outputType == OUTPUT_STRING { |
||||
|
txtOutputContent.WriteString("\r\n\r\n\r\n\r\n\r\n") |
||||
|
if _, e = fd.WriteString(txtOutputContent.String());e != nil { |
||||
|
panic("写入文件失败:" + e.Error()) |
||||
|
} |
||||
|
if _, e = fd.WriteString(txtTransalteOutputContent.String());e != nil { |
||||
|
panic("写入文件失败:" + e.Error()) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//统一运行翻译结果
|
||||
|
func (app *SrtTranslateApp) RunTranslate(s string , file string) (*SrtTranslateResult , error) { |
||||
|
var trys int = 0 |
||||
|
translateResult := new(SrtTranslateResult) |
||||
|
|
||||
|
if app.TranslateCfg.Supplier == TRANSLATE_SUPPLIER_BAIDU { |
||||
|
if app.TranslateCfg.BaiduTranslate.AuthenType == translate.ACCOUNT_COMMON_AUTHEN { //百度翻译标准版
|
||||
|
//休眠1010毫秒
|
||||
|
time.Sleep(time.Millisecond * 1010) |
||||
|
} else { |
||||
|
//休眠200毫秒
|
||||
|
time.Sleep(time.Millisecond * 200) |
||||
|
} |
||||
|
|
||||
|
from := GetLanguageChar(app.TranslateCfg.InputLanguage , TRANSLATE_SUPPLIER_BAIDU) |
||||
|
to := GetLanguageChar(app.TranslateCfg.OutputLanguage , TRANSLATE_SUPPLIER_BAIDU) |
||||
|
|
||||
|
//发起翻译请求
|
||||
|
baiduResult,transErr := app.TranslateCfg.BaiduTranslate.TranslateBaidu(s , from , to) |
||||
|
for transErr != nil && trys <= 5 { |
||||
|
trys++ |
||||
|
app.Log("翻译请求失败,重试第" + strconv.Itoa(trys) + "次 ..." , file) |
||||
|
time.Sleep(time.Second * time.Duration(trys)) |
||||
|
//重试
|
||||
|
baiduResult,transErr = app.TranslateCfg.BaiduTranslate.TranslateBaidu(s , from , to) |
||||
|
} |
||||
|
if transErr != nil { |
||||
|
return translateResult,errors.New("翻译失败!错误信息:" + transErr.Error()) |
||||
|
} |
||||
|
|
||||
|
translateResult.TransResultDst = baiduResult.TransResultDst |
||||
|
translateResult.TransResultSrc = baiduResult.TransResultSrc |
||||
|
translateResult.From = baiduResult.From |
||||
|
translateResult.To = baiduResult.To |
||||
|
|
||||
|
return translateResult,nil |
||||
|
} else if app.TranslateCfg.Supplier == TRANSLATE_SUPPLIER_TENGXUNYUN { |
||||
|
//休眠
|
||||
|
t := app.TranslateCfg.TengxunyunTranslate.TranslateSleepTime(app.MaxConcurrency) |
||||
|
time.Sleep(t) |
||||
|
|
||||
|
from := GetLanguageChar(app.TranslateCfg.InputLanguage , TRANSLATE_SUPPLIER_TENGXUNYUN) |
||||
|
to := GetLanguageChar(app.TranslateCfg.OutputLanguage , TRANSLATE_SUPPLIER_TENGXUNYUN) |
||||
|
|
||||
|
//发起翻译请求
|
||||
|
txResult,transErr := app.TranslateCfg.TengxunyunTranslate.TranslateTengxunyun(s , from , to) |
||||
|
for transErr != nil && trys <= 5 { |
||||
|
trys++ |
||||
|
app.Log("翻译请求失败,重试第" + strconv.Itoa(trys) + "次 ..." , file) |
||||
|
time.Sleep(time.Second * time.Duration(trys)) |
||||
|
//重试
|
||||
|
txResult,transErr = app.TranslateCfg.TengxunyunTranslate.TranslateTengxunyun(s , from , to) |
||||
|
} |
||||
|
if transErr != nil { |
||||
|
return translateResult,errors.New("翻译失败!错误信息:" + transErr.Error()) |
||||
|
} |
||||
|
|
||||
|
translateResult.TransResultDst = txResult.TransResultDst |
||||
|
translateResult.TransResultSrc = txResult.TransResultSrc |
||||
|
translateResult.From = txResult.From |
||||
|
translateResult.To = txResult.To |
||||
|
|
||||
|
return translateResult,nil |
||||
|
} else { |
||||
|
return translateResult,errors.New("翻译失败!缺少翻译引擎!") |
||||
|
} |
||||
|
} |
@ -0,0 +1,92 @@ |
|||||
|
package app |
||||
|
|
||||
|
type VideoMultitask struct { |
||||
|
MaxConcurrencyNumber int //最大运行并发数
|
||||
|
Total int //任务总数
|
||||
|
QueueFile []string //任务队列
|
||||
|
CurrentIndex int //已处理的下标
|
||||
|
FinishNumber int //已完成的任务数量
|
||||
|
|
||||
|
VideoSrt *VideoSrt |
||||
|
} |
||||
|
|
||||
|
func NewVideoMultitask(concurrencyNumber int) (*VideoMultitask) { |
||||
|
app := new(VideoMultitask) |
||||
|
if concurrencyNumber == 0 { |
||||
|
//默认并发数
|
||||
|
app.MaxConcurrencyNumber = 2 |
||||
|
} else { |
||||
|
app.MaxConcurrencyNumber = concurrencyNumber |
||||
|
} |
||||
|
return app |
||||
|
} |
||||
|
|
||||
|
//设置 任务队列
|
||||
|
func (task *VideoMultitask) SetQueueFile(queue []string) { |
||||
|
task.QueueFile = queue |
||||
|
task.Total = len(queue) |
||||
|
} |
||||
|
|
||||
|
//设置 任务队列
|
||||
|
func (task *VideoMultitask) SetVideoSrt(v *VideoSrt) { |
||||
|
task.VideoSrt = v |
||||
|
} |
||||
|
|
||||
|
//设置 并发数量
|
||||
|
func (task *VideoMultitask) SetMaxConcurrencyNumber(n int) { |
||||
|
task.MaxConcurrencyNumber = n |
||||
|
} |
||||
|
|
||||
|
//并发运行
|
||||
|
func (task *VideoMultitask) Run() { |
||||
|
//初始参数
|
||||
|
task.CurrentIndex = -1 |
||||
|
task.FinishNumber = 0 |
||||
|
|
||||
|
number := 1 |
||||
|
//并发调用
|
||||
|
for number <= task.MaxConcurrencyNumber && task.CurrentIndex < (task.Total - 1){ |
||||
|
if task.CurrentIndex == -1 { |
||||
|
task.CurrentIndex = 0; |
||||
|
path := task.QueueFile[task.CurrentIndex] |
||||
|
go func() { |
||||
|
task.VideoSrt.Run(path) |
||||
|
}() |
||||
|
} else { |
||||
|
task.CurrentIndex++ |
||||
|
path := task.QueueFile[task.CurrentIndex] |
||||
|
go func() { |
||||
|
task.VideoSrt.Run(path) |
||||
|
}() |
||||
|
} |
||||
|
number++ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (task *VideoMultitask) RunOver() bool { |
||||
|
//fmt.Println("RunOver:" , task.CurrentIndex)
|
||||
|
|
||||
|
if task.CurrentIndex >= (task.Total - 1) { |
||||
|
//任务队列处理完成
|
||||
|
return true |
||||
|
} |
||||
|
//执行
|
||||
|
task.CurrentIndex++ |
||||
|
path := task.QueueFile[task.CurrentIndex] |
||||
|
go func() { |
||||
|
task.VideoSrt.Run(path) |
||||
|
}() |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//标记已完成
|
||||
|
func (task *VideoMultitask) FinishTask() bool { |
||||
|
task.FinishNumber++ |
||||
|
//fmt.Println("FinishTask:" , task.FinishNumber)
|
||||
|
|
||||
|
if (task.FinishNumber == task.Total) { |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
@ -0,0 +1,89 @@ |
|||||
|
package app |
||||
|
|
||||
|
type TranslateMultitask struct { |
||||
|
MaxConcurrencyNumber int //最大运行并发数
|
||||
|
Total int //任务总数
|
||||
|
QueueFile []string //任务队列
|
||||
|
CurrentIndex int //已处理的下标
|
||||
|
FinishNumber int //已完成的任务数量
|
||||
|
|
||||
|
SrtTranslateApp *SrtTranslateApp |
||||
|
} |
||||
|
|
||||
|
func NewTranslateMultitask(concurrencyNumber int) (*TranslateMultitask) { |
||||
|
app := new(TranslateMultitask) |
||||
|
if concurrencyNumber == 0 { |
||||
|
//默认并发数
|
||||
|
app.MaxConcurrencyNumber = 2 |
||||
|
} else { |
||||
|
app.MaxConcurrencyNumber = concurrencyNumber |
||||
|
} |
||||
|
return app |
||||
|
} |
||||
|
|
||||
|
//设置 任务队列
|
||||
|
func (task *TranslateMultitask) SetQueueFile(queue []string) { |
||||
|
task.QueueFile = queue |
||||
|
task.Total = len(queue) |
||||
|
} |
||||
|
|
||||
|
//设置 任务队列
|
||||
|
func (task *TranslateMultitask) SetSrtTranslateApp(app *SrtTranslateApp) { |
||||
|
task.SrtTranslateApp = app |
||||
|
} |
||||
|
|
||||
|
//设置 并发数量
|
||||
|
func (task *TranslateMultitask) SetMaxConcurrencyNumber(n int) { |
||||
|
task.MaxConcurrencyNumber = n |
||||
|
} |
||||
|
|
||||
|
//并发运行
|
||||
|
func (task *TranslateMultitask) Run() { |
||||
|
//初始参数
|
||||
|
task.CurrentIndex = -1 |
||||
|
task.FinishNumber = 0 |
||||
|
|
||||
|
number := 1 |
||||
|
//并发调用
|
||||
|
for number <= task.MaxConcurrencyNumber && task.CurrentIndex < (task.Total - 1){ |
||||
|
if task.CurrentIndex == -1 { |
||||
|
task.CurrentIndex = 0; |
||||
|
path := task.QueueFile[task.CurrentIndex] |
||||
|
go func() { |
||||
|
task.SrtTranslateApp.Run(path) |
||||
|
}() |
||||
|
} else { |
||||
|
task.CurrentIndex++ |
||||
|
path := task.QueueFile[task.CurrentIndex] |
||||
|
go func() { |
||||
|
task.SrtTranslateApp.Run(path) |
||||
|
}() |
||||
|
} |
||||
|
number++ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (task *TranslateMultitask) RunOver() bool { |
||||
|
if task.CurrentIndex >= (task.Total - 1) { |
||||
|
//任务队列处理完成
|
||||
|
return true |
||||
|
} |
||||
|
//执行
|
||||
|
task.CurrentIndex++ |
||||
|
path := task.QueueFile[task.CurrentIndex] |
||||
|
go func() { |
||||
|
task.SrtTranslateApp.Run(path) |
||||
|
}() |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//标记已完成
|
||||
|
func (task *TranslateMultitask) FinishTask() bool { |
||||
|
task.FinishNumber++ |
||||
|
|
||||
|
if (task.FinishNumber == task.Total) { |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
@ -0,0 +1,122 @@ |
|||||
|
package tool |
||||
|
|
||||
|
import ( |
||||
|
"regexp" |
||||
|
"strings" |
||||
|
"unicode/utf8" |
||||
|
) |
||||
|
|
||||
|
|
||||
|
func CheckChineseNumber(s string) bool { |
||||
|
regx := regexp.MustCompile("(.*)([一|二|两|三|四|五|六|七|八|九|十|百|千|万|亿]+)(.*)") |
||||
|
return regx.MatchString(s) |
||||
|
} |
||||
|
|
||||
|
func ChineseNumberToLowercaseLength(s string) int { |
||||
|
st := GetStringUtf8Length(s) |
||||
|
regx := regexp.MustCompile("([一|二|两|三|四|五|六|七|八|九|十|百|千|万|亿]+)(.*)") |
||||
|
s = regx.ReplaceAllString(s , "$1") |
||||
|
if s == "" || !IsChineseNumber(s) { |
||||
|
return st |
||||
|
} |
||||
|
rst := GetStringUtf8Length(s) |
||||
|
cha_t := 0 |
||||
|
if st > rst { |
||||
|
cha_t = st - rst |
||||
|
} |
||||
|
s = strings.TrimSpace(s) |
||||
|
zhTexts := strings.Split(s , "") |
||||
|
zhTextsLens := len(zhTexts) |
||||
|
|
||||
|
numberPosi := true |
||||
|
maxBaseNumber := 1 |
||||
|
|
||||
|
if s == "十" { |
||||
|
maxBaseNumber = 2 |
||||
|
} else { |
||||
|
for i:=0; i<zhTextsLens; i++ { |
||||
|
if numberPosi == false { |
||||
|
switch zhTexts[i] { |
||||
|
case "十": |
||||
|
maxBaseNumber = 2 |
||||
|
break |
||||
|
case "百": |
||||
|
maxBaseNumber = 3 |
||||
|
break |
||||
|
case "千": |
||||
|
maxBaseNumber = 4 |
||||
|
break |
||||
|
case "万": |
||||
|
maxBaseNumber = 5 |
||||
|
break |
||||
|
} |
||||
|
break |
||||
|
} |
||||
|
numberPosi = !numberPosi |
||||
|
} |
||||
|
} |
||||
|
return maxBaseNumber+cha_t |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func IsChineseNumber(text string) bool { |
||||
|
text = strings.TrimSpace(text) |
||||
|
if text == "" { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
zhTexts := strings.Split(text , "") |
||||
|
zhTextsLens := len(zhTexts) |
||||
|
|
||||
|
numberPosi := true |
||||
|
for i:=0; i<zhTextsLens; i++ { |
||||
|
if !ValiChineseNumberChar(zhTexts[i] , !numberPosi) { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
numberPosi = !numberPosi |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func ValiChineseNumberChar(s string , unit bool) bool { |
||||
|
zh_number := []string{"一","二","两","三","四","五","六","七","八","九","十","百","千","万"} |
||||
|
zh_unit := []string{"十","百","千","万","亿"} |
||||
|
|
||||
|
if unit { |
||||
|
for _,v := range zh_unit { |
||||
|
if v == s { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
for _,v := range zh_number { |
||||
|
if v == s { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取utf8字符长度
|
||||
|
func GetStringUtf8Length(s string) int { |
||||
|
return utf8.RuneCountInString(s) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//检测文本是否仅符号
|
||||
|
func CheckOnlySymbolText(s string) bool { |
||||
|
if GetStringUtf8Length(s) > 6 { |
||||
|
return false |
||||
|
} |
||||
|
regx := regexp.MustCompile(`^(\\|\{|\}|\[|\]|(|)|\(|\)|\*|/|~|<|>|_|\-|\+|=|&|%|\$|@|#|—|」|「|!|,|。|。||、|?|;|:|‘|’|”|“|"|'|,|\.|\?|;|:|!|\s)+$`) |
||||
|
if regx.Match([]byte(s)) { |
||||
|
return true |
||||
|
} else { |
||||
|
return false |
||||
|
} |
||||
|
} |
@ -0,0 +1,248 @@ |
|||||
|
package tool |
||||
|
|
||||
|
import ( |
||||
|
"crypto/md5" |
||||
|
"encoding/hex" |
||||
|
"math/rand" |
||||
|
"os" |
||||
|
"os/exec" |
||||
|
"path" |
||||
|
"runtime" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"syscall" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
//验证 s 是否存在 slice 中
|
||||
|
func InSliceString(s string , slices []string) bool { |
||||
|
for _,v := range slices { |
||||
|
if v == s { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
//Windows下Dir路径转换
|
||||
|
func WinDir(dir string) string { |
||||
|
return strings.Replace(dir , "\\" , "/" , -1) |
||||
|
} |
||||
|
|
||||
|
//获取文件名称(不带后缀)
|
||||
|
func GetFileBaseName(filepath string) string { |
||||
|
basefile := path.Base(filepath) |
||||
|
ext := path.Ext(filepath) |
||||
|
|
||||
|
return strings.Replace(basefile , ext , "" , 1) |
||||
|
} |
||||
|
|
||||
|
//检验目录是否存在
|
||||
|
func DirExists(path string) bool { |
||||
|
_, err := os.Stat(path) |
||||
|
if err == nil { |
||||
|
return true |
||||
|
} |
||||
|
if os.IsNotExist(err) { |
||||
|
return false |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//创建目录
|
||||
|
func CreateDir(path string , all bool) error { |
||||
|
var err error |
||||
|
if all { |
||||
|
err = os.Mkdir(path, os.ModePerm) |
||||
|
} else { |
||||
|
err = os.MkdirAll(path, os.ModePerm) |
||||
|
} |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// open opens the specified URL in the default browser of the user.
|
||||
|
func OpenUrl(url string) error { |
||||
|
var cmd string |
||||
|
var args []string |
||||
|
|
||||
|
switch runtime.GOOS { |
||||
|
case "windows": |
||||
|
cmd = "cmd" |
||||
|
args = []string{"/c", "start"} |
||||
|
case "darwin": |
||||
|
cmd = "open" |
||||
|
default: // "linux", "freebsd", "openbsd", "netbsd"
|
||||
|
cmd = "xdg-open" |
||||
|
} |
||||
|
args = append(args, url) |
||||
|
c := exec.Command(cmd, args...) |
||||
|
c.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} |
||||
|
return c.Start() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取随机字符串
|
||||
|
func GetRandomCodeString(len int) string { |
||||
|
seed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" |
||||
|
seedArr := strings.Split(seed , "") |
||||
|
|
||||
|
result := []string{} |
||||
|
index := 0 |
||||
|
for index < len { |
||||
|
s := GetIntRandomNumber(0 , 61) |
||||
|
result = append(result , seedArr[s]) |
||||
|
|
||||
|
index++ |
||||
|
} |
||||
|
|
||||
|
return strings.Join(result , "") |
||||
|
} |
||||
|
|
||||
|
//设置随机种子
|
||||
|
func SetRandomSeed() { |
||||
|
rand.Seed(time.Now().Unix()) //设置随机种子
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取某范围的随机整数
|
||||
|
func GetIntRandomNumber(min int64 , max int64) int64 { |
||||
|
return rand.Int63n(max - min) + min |
||||
|
} |
||||
|
|
||||
|
//字幕时间戳转换
|
||||
|
func SubtitleTimeMillisecond(time int64 , showMillisecond bool) string { |
||||
|
var miao int64 = 0 |
||||
|
var min int64 = 0 |
||||
|
var hours int64 = 0 |
||||
|
var millisecond int64 = 0 |
||||
|
|
||||
|
millisecond = (time % 1000) |
||||
|
miao = (time / 1000) |
||||
|
|
||||
|
if miao > 59 { |
||||
|
min = (time / 1000) / 60 |
||||
|
miao = miao % 60 |
||||
|
} |
||||
|
if min > 59 { |
||||
|
hours = (time / 1000) / 3600 |
||||
|
min = min % 60 |
||||
|
} |
||||
|
|
||||
|
//00:00:06,770
|
||||
|
var miaoText = RepeatStr(strconv.FormatInt(miao , 10) , "0" , 2 , true) |
||||
|
var minText = RepeatStr(strconv.FormatInt(min , 10) , "0" , 2 , true) |
||||
|
var hoursText = RepeatStr(strconv.FormatInt(hours , 10) , "0" , 2 , true) |
||||
|
var millisecondText = RepeatStr(strconv.FormatInt(millisecond , 10) , "0" , 3 , true) |
||||
|
|
||||
|
if showMillisecond { |
||||
|
return hoursText + ":" + minText + ":" + miaoText + "," + millisecondText |
||||
|
} |
||||
|
return hoursText + ":" + minText + ":" + miaoText |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
//歌词时间戳转换
|
||||
|
func MusicLrcTextMillisecond(time int64) string { |
||||
|
var miao int64 = 0 |
||||
|
var min int64 = 0 |
||||
|
var millisecond int64 = 0 |
||||
|
|
||||
|
millisecond = (time % 1000) |
||||
|
miao = (time / 1000) |
||||
|
|
||||
|
if miao > 59 { |
||||
|
min = (time / 1000) / 60 |
||||
|
miao = miao % 60 |
||||
|
} |
||||
|
|
||||
|
millisecond = millisecond / 10 |
||||
|
|
||||
|
//00:00:06,770
|
||||
|
var miaoText = RepeatStr(strconv.FormatInt(miao , 10) , "0" , 2 , true) |
||||
|
var minText = RepeatStr(strconv.FormatInt(min , 10) , "0" , 2 , true) |
||||
|
var millisecondText = RepeatStr(strconv.FormatInt(millisecond , 10) , "0" , 2 , true) |
||||
|
|
||||
|
return minText + ":" + miaoText + "." + millisecondText |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func RepeatStr(str string , s string , length int , before bool) string { |
||||
|
ln := len(str) |
||||
|
|
||||
|
if ln >= length { |
||||
|
return str |
||||
|
} |
||||
|
|
||||
|
if before { |
||||
|
return strings.Repeat(s , (length - ln)) + str |
||||
|
} else { |
||||
|
return str + strings.Repeat(s , (length - ln)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//校验文件是否存在
|
||||
|
func VaildFile (file string) bool { |
||||
|
_, err := os.Stat(file) //os.Stat获取文件信息
|
||||
|
if err != nil { |
||||
|
if os.IsExist(err) { |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//文本md5
|
||||
|
func Md5String(str string) string { |
||||
|
h := md5.New() |
||||
|
h.Write([]byte(str)) |
||||
|
return hex.EncodeToString(h.Sum(nil)) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//版本号比较
|
||||
|
func CompareVersion(version1 string, version2 string) int { |
||||
|
v1 := strings.Split(version1, ".") |
||||
|
v2 := strings.Split(version2, ".") |
||||
|
|
||||
|
var limit int |
||||
|
|
||||
|
if len(v1) > len(v2) { |
||||
|
limit = len(v1) |
||||
|
} else { |
||||
|
limit = len(v2) |
||||
|
} |
||||
|
|
||||
|
for { |
||||
|
if len(v1) >= limit { |
||||
|
break |
||||
|
} |
||||
|
v1 = append(v1, "0") |
||||
|
} |
||||
|
|
||||
|
for { |
||||
|
if len(v2) >= limit { |
||||
|
break |
||||
|
} |
||||
|
v2 = append(v2, "0") |
||||
|
} |
||||
|
|
||||
|
for i := 0; i < limit; i++ { |
||||
|
num1, _ := strconv.Atoi(v1[i]) |
||||
|
num2, _ := strconv.Atoi(v2[i]) |
||||
|
if num1 > num2 { |
||||
|
return 1 |
||||
|
} |
||||
|
if num1 < num2 { |
||||
|
return -1 |
||||
|
} |
||||
|
} |
||||
|
return 0 |
||||
|
} |
@ -0,0 +1,110 @@ |
|||||
|
package translate |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"github.com/buger/jsonparser" |
||||
|
"io/ioutil" |
||||
|
"net/http" |
||||
|
"net/url" |
||||
|
"strconv" |
||||
|
"time" |
||||
|
"videosrt/app/tool" |
||||
|
) |
||||
|
|
||||
|
//百度翻译
|
||||
|
type BaiduTranslate struct { |
||||
|
AppId string //appid
|
||||
|
AppSecret string //appsecret
|
||||
|
AuthenType int //账号认证类型
|
||||
|
} |
||||
|
|
||||
|
//百度翻译结果集
|
||||
|
type BaiduTranslateResult struct { |
||||
|
From string //翻译源语言
|
||||
|
To string //译文语言
|
||||
|
TransResultSrc string //翻译结果(原文)
|
||||
|
TransResultDst string //翻译结果(译文)
|
||||
|
ErrorCode int64 //错误码(仅当出现错误时存在)
|
||||
|
ErrorMsg string //错误消息(仅当出现错误时存在)
|
||||
|
} |
||||
|
|
||||
|
//常量
|
||||
|
const ( |
||||
|
TRANS_API string = "https://fanyi-api.baidu.com/api/trans/vip/translate" |
||||
|
|
||||
|
//账号认证类型
|
||||
|
ACCOUNT_COMMON_AUTHEN int = 1 //标准版
|
||||
|
ACCOUNT_SENIOR_AUTHEN int = 2 //高级版
|
||||
|
) |
||||
|
|
||||
|
|
||||
|
//百度api文档
|
||||
|
//http://api.fanyi.baidu.com/api/trans/product/apidoc
|
||||
|
//支持语言列表 http://api.fanyi.baidu.com/api/trans/product/apidoc#languageList
|
||||
|
func (trans *BaiduTranslate) TranslateBaidu (strings string , from string , to string) (*BaiduTranslateResult , error) { |
||||
|
|
||||
|
params := &url.Values{} |
||||
|
|
||||
|
params.Add("q" , strings) |
||||
|
params.Add("appid" , trans.AppId) |
||||
|
params.Add("salt" , strconv.FormatInt(tool.GetIntRandomNumber(10000 , 99999) , 10)) |
||||
|
params.Add("from" , from) |
||||
|
params.Add("to" , to) |
||||
|
params.Add("sign" , trans.BuildSign(strings , params.Get("salt"))) |
||||
|
|
||||
|
return trans.CallRequest(params) |
||||
|
} |
||||
|
|
||||
|
//生成加密sign
|
||||
|
func (trans *BaiduTranslate) BuildSign (strings string , salt string) string { |
||||
|
str := trans.AppId + strings + salt + trans.AppSecret |
||||
|
return tool.Md5String(str) |
||||
|
} |
||||
|
|
||||
|
//发起请求
|
||||
|
func (trans *BaiduTranslate) CallRequest (params *url.Values ) (*BaiduTranslateResult , error) { |
||||
|
url := TRANS_API + "?" + params.Encode() |
||||
|
|
||||
|
request, e := http.NewRequest(http.MethodGet, url , nil) |
||||
|
if e != nil { |
||||
|
return nil,e |
||||
|
} |
||||
|
http.DefaultClient.Timeout = time.Second * 60 |
||||
|
//do request
|
||||
|
response, e := http.DefaultClient.Do(request) |
||||
|
if e != nil { |
||||
|
return nil,e |
||||
|
} |
||||
|
//content
|
||||
|
content, e := ioutil.ReadAll(response.Body) |
||||
|
if e != nil { |
||||
|
return nil,e |
||||
|
} |
||||
|
|
||||
|
//解析数据
|
||||
|
errorCode , _ := jsonparser.GetString(content , "error_code") |
||||
|
errorMsg , _ := jsonparser.GetString(content , "error_msg") |
||||
|
from , _ := jsonparser.GetString(content , "from") |
||||
|
to , _ := jsonparser.GetString(content , "to") |
||||
|
|
||||
|
errorCodeInt , _ := strconv.Atoi(errorCode) |
||||
|
|
||||
|
result := &BaiduTranslateResult{ |
||||
|
ErrorCode:int64(errorCodeInt), |
||||
|
ErrorMsg:errorMsg, |
||||
|
From:from, |
||||
|
To:to, |
||||
|
} |
||||
|
|
||||
|
_, _ = jsonparser.ArrayEach(content, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { |
||||
|
result.TransResultSrc, _ = jsonparser.GetString(value, "src") |
||||
|
result.TransResultDst, _ = jsonparser.GetString(value, "dst") |
||||
|
}, "trans_result") |
||||
|
|
||||
|
//翻译错误校验
|
||||
|
if result.ErrorCode != 0 { |
||||
|
return nil , errors.New(result.ErrorMsg) |
||||
|
} |
||||
|
|
||||
|
return result,nil |
||||
|
} |
@ -0,0 +1,91 @@ |
|||||
|
package translate |
||||
|
|
||||
|
import ( |
||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" |
||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" |
||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" |
||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/regions" |
||||
|
v20180321 "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tmt/v20180321" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
//腾讯云机器翻译
|
||||
|
type TengxunyunTranslate struct { |
||||
|
SecretId string //secretId
|
||||
|
SecretKey string //secretKey
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
//腾讯云翻译结果集
|
||||
|
type TengxunyunTranslateResult struct { |
||||
|
From string //翻译源语言
|
||||
|
To string //译文语言
|
||||
|
TransResultSrc string //翻译结果(原文)
|
||||
|
TransResultDst string //翻译结果(译文)
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
//调起腾讯云机器文本翻译
|
||||
|
func (trans *TengxunyunTranslate) TranslateTengxunyun (strings string , from string , to string) (*TengxunyunTranslateResult , error) { |
||||
|
//fmt.Println("TranslateTengxunyun : " , strings , from , to)
|
||||
|
credential := common.NewCredential( |
||||
|
trans.SecretId , |
||||
|
trans.SecretKey , |
||||
|
) |
||||
|
|
||||
|
cpf := profile.NewClientProfile() |
||||
|
client, _ := v20180321.NewClient(credential, regions.Guangzhou, cpf) |
||||
|
request := v20180321.NewTextTranslateRequest() |
||||
|
|
||||
|
var SourceText *string = new(string) |
||||
|
var Source *string = new(string) |
||||
|
var Target *string = new(string) |
||||
|
var ProjectId *int64 = new(int64) |
||||
|
|
||||
|
//*SourceText = "这个需求做不了"
|
||||
|
//*Source = "zh"
|
||||
|
//*Target = "en"
|
||||
|
//*ProjectId = 0
|
||||
|
|
||||
|
*SourceText = strings |
||||
|
*Source = from |
||||
|
*Target = to |
||||
|
*ProjectId = 0 |
||||
|
|
||||
|
request.SourceText = SourceText |
||||
|
request.Source = Source |
||||
|
request.Target = Target |
||||
|
request.ProjectId = ProjectId |
||||
|
|
||||
|
result := new(TengxunyunTranslateResult) |
||||
|
|
||||
|
// 通过client对象调用想要访问的接口,需要传入请求对象
|
||||
|
response, err := client.TextTranslate(request) |
||||
|
// 处理异常
|
||||
|
if errorObj, ok := err.(*errors.TencentCloudSDKError); ok { |
||||
|
return result , errorObj |
||||
|
} |
||||
|
// 非SDK异常,直接失败。实际代码中可以加入其他的处理。
|
||||
|
if err != nil { |
||||
|
return result , err |
||||
|
} |
||||
|
|
||||
|
result.From = *response.Response.Source |
||||
|
result.To = *response.Response.Target |
||||
|
result.TransResultSrc = strings |
||||
|
result.TransResultDst = *response.Response.TargetText |
||||
|
|
||||
|
return result,nil |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取并发请求停顿的时间
|
||||
|
func (trans *TengxunyunTranslate) TranslateSleepTime (maxConcurrency int) (time.Duration) { |
||||
|
if maxConcurrency == 1 { |
||||
|
return time.Millisecond * 250 |
||||
|
} else if maxConcurrency == 2 { |
||||
|
return time.Millisecond * 600 |
||||
|
} else { |
||||
|
return time.Millisecond * time.Duration(maxConcurrency) * 900 |
||||
|
} |
||||
|
} |
@ -0,0 +1,83 @@ |
|||||
|
package app |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"github.com/PuerkitoBio/goquery" |
||||
|
"github.com/lxn/walk" |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
VERSION_SOURCE string = "https://gitee.com/641453620/video-srt-windows/tags" |
||||
|
) |
||||
|
|
||||
|
type AppVersion struct { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
//根据码云查询新版本
|
||||
|
func (v *AppVersion) GetVersion () (string , error) { |
||||
|
|
||||
|
timeout := time.Duration(2 * time.Second) |
||||
|
client := &http.Client{ |
||||
|
Timeout:timeout, |
||||
|
} |
||||
|
//获取版本来源html
|
||||
|
response, err := client.Get(VERSION_SOURCE) |
||||
|
if err != nil { |
||||
|
return "" , err |
||||
|
} |
||||
|
defer response.Body.Close() |
||||
|
if response.StatusCode != 200 { |
||||
|
return "" , errors.New("status code error : " + strconv.Itoa(response.StatusCode)) |
||||
|
} |
||||
|
|
||||
|
//加载html
|
||||
|
html, err := goquery.NewDocumentFromReader(response.Body) |
||||
|
if err != nil { |
||||
|
return "" , err |
||||
|
} |
||||
|
|
||||
|
//查找节点
|
||||
|
vs := "" |
||||
|
html.Find("#git-tags-container .releases-tags-wrap .releases-tag-content .tag-list .tag-item").Each(func(i int, s *goquery.Selection) { |
||||
|
if vs != "" { |
||||
|
return |
||||
|
} |
||||
|
tag , is := s.Find(".tag-name a").Attr("title") |
||||
|
if is && tag != "" { |
||||
|
vs = strings.TrimSpace(tag) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
return vs,nil |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//显示更新提醒
|
||||
|
func (v *AppVersion) ShowVersionNotifyInfo (version string , own *MyMainWindow) error { |
||||
|
mw, err := walk.NewMainWindow() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
ni, err := walk.NewNotifyIcon(mw) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
defer func() { |
||||
|
time.Sleep(time.Second * 15) |
||||
|
_ = ni.Dispose() |
||||
|
}() |
||||
|
|
||||
|
if err := ni.SetVisible(true); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if err := ni.ShowMessage("更新提醒" , "检测到VideoSrt的新版本(v"+version+"),请及时下载更新哦") ; err != nil { |
||||
|
return err |
||||
|
} |
||||
|
return nil |
||||
|
} |
@ -0,0 +1,836 @@ |
|||||
|
package app |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"github.com/buger/jsonparser" |
||||
|
"os" |
||||
|
"path" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"time" |
||||
|
"videosrt/app/aliyun" |
||||
|
"videosrt/app/ffmpeg" |
||||
|
"videosrt/app/tool" |
||||
|
"videosrt/app/translate" |
||||
|
) |
||||
|
|
||||
|
//应用翻译配置
|
||||
|
type VideoSrtTranslateStruct struct { |
||||
|
TranslateSwitch bool //字幕翻译开关
|
||||
|
Supplier int //引擎供应商
|
||||
|
BilingualSubtitleSwitch bool //是否输出双语字幕
|
||||
|
InputLanguage int //输入字幕语言
|
||||
|
OutputLanguage int //输出字幕语言
|
||||
|
OutputMainSubtitleInputLanguage bool //双语主字幕(输入语言)
|
||||
|
|
||||
|
BaiduTranslate translate.BaiduTranslate //百度翻译
|
||||
|
TengxunyunTranslate translate.TengxunyunTranslate //腾讯云翻译
|
||||
|
} |
||||
|
|
||||
|
//统一翻译结果
|
||||
|
type VideoSrtTranslateResult struct { |
||||
|
From string //翻译源语言
|
||||
|
To string //译文语言
|
||||
|
TransResultSrc string //翻译结果(原文)
|
||||
|
TransResultDst string //翻译结果(译文)
|
||||
|
} |
||||
|
|
||||
|
//主应用
|
||||
|
type VideoSrt struct { |
||||
|
Ffmpeg ffmpeg.Ffmpeg |
||||
|
AliyunOss aliyun.AliyunOss //oss
|
||||
|
AliyunClound aliyun.AliyunClound //语音识别引擎
|
||||
|
|
||||
|
CloseAutoDeleteOssTempFile bool //关闭自动删除临时音频文件(默认开启)[false开启 true关闭]
|
||||
|
CloseIntelligentBlockSwitch bool //关闭智能分段处理(默认开启)
|
||||
|
TempDir string //临时文件目录
|
||||
|
AppDir string //应用根目录
|
||||
|
SrtDir string //字幕文件输出目录
|
||||
|
OutputType *AppSetingsOutput //输出文件类型
|
||||
|
OutputEncode int //输出文件编码
|
||||
|
SoundTrack int //输出音轨(0输出全部音轨)
|
||||
|
|
||||
|
MaxConcurrency int //最大处理并发数
|
||||
|
|
||||
|
TranslateCfg *VideoSrtTranslateStruct //翻译配置
|
||||
|
FilterSetings *AppFilterSetings //过滤器配置
|
||||
|
|
||||
|
LogHandler func(s string , video string) //日志回调
|
||||
|
SuccessHandler func(video string) //成功回调
|
||||
|
FailHandler func(video string) //失败回调
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
//获取应用
|
||||
|
func NewApp(appDir string) *VideoSrt { |
||||
|
app := new(VideoSrt) |
||||
|
|
||||
|
app.TempDir = "temp/audio" |
||||
|
app.AppDir = appDir |
||||
|
app.OutputEncode = OUTPUT_ENCODE_UTF8 //默认输出文件编码
|
||||
|
app.MaxConcurrency = 2 |
||||
|
|
||||
|
//实例应用翻译配置
|
||||
|
app.TranslateCfg = new(VideoSrtTranslateStruct) |
||||
|
return app |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//应用加载配置
|
||||
|
func (app *VideoSrt) InitAppConfig(oss *AliyunOssCache , engine *AliyunEngineCache) { |
||||
|
//oss
|
||||
|
app.AliyunOss.Endpoint = oss.Endpoint |
||||
|
app.AliyunOss.AccessKeyId = oss.AccessKeyId |
||||
|
app.AliyunOss.AccessKeySecret = oss.AccessKeySecret |
||||
|
app.AliyunOss.BucketName = oss.BucketName |
||||
|
app.AliyunOss.BucketDomain = oss.BucketDomain |
||||
|
|
||||
|
//engine
|
||||
|
app.AliyunClound.AppKey = engine.AppKey |
||||
|
app.AliyunClound.AccessKeyId = engine.AccessKeyId |
||||
|
app.AliyunClound.AccessKeySecret = engine.AccessKeySecret |
||||
|
app.AliyunClound.Region = engine.Region |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//加载翻译设置
|
||||
|
func (app *VideoSrt) InitTranslateConfig (translateSettings *VideoSrtTranslateStruct) { |
||||
|
app.TranslateCfg = translateSettings |
||||
|
} |
||||
|
//加载过滤器配置
|
||||
|
func (app *VideoSrt) InitFilterConfig (filterSetings *AppFilterSetings) { |
||||
|
app.FilterSetings = filterSetings |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func (app *VideoSrt) SetCloseAutoDeleteOssTempFile(state bool) { |
||||
|
app.CloseAutoDeleteOssTempFile = state |
||||
|
} |
||||
|
|
||||
|
func (app *VideoSrt) SetCloseIntelligentBlockSwitch(state bool) { |
||||
|
app.CloseIntelligentBlockSwitch = state |
||||
|
} |
||||
|
|
||||
|
func (app *VideoSrt) SetSrtDir(dir string) { |
||||
|
app.SrtDir = dir |
||||
|
} |
||||
|
|
||||
|
func (app *VideoSrt) SetOutputType(output *AppSetingsOutput) { |
||||
|
app.OutputType = output |
||||
|
} |
||||
|
|
||||
|
func (app *VideoSrt) SetOutputEncode(encode int) { |
||||
|
app.OutputEncode = encode |
||||
|
} |
||||
|
|
||||
|
func (app *VideoSrt) SetSoundTrack(track int) { |
||||
|
app.SoundTrack = track |
||||
|
} |
||||
|
|
||||
|
func (app *VideoSrt) SetMaxConcurrency(number int) { |
||||
|
if number == 0 { |
||||
|
number = 2 |
||||
|
} |
||||
|
app.MaxConcurrency = number |
||||
|
} |
||||
|
|
||||
|
func (app *VideoSrt) SetSuccessHandler(callback func(video string)) { |
||||
|
app.SuccessHandler = callback |
||||
|
} |
||||
|
|
||||
|
func (app *VideoSrt) SetFailHandler(callback func(video string)) { |
||||
|
app.FailHandler = callback |
||||
|
} |
||||
|
|
||||
|
func (app *VideoSrt) SetLogHandler(callback func(s string , video string)) { |
||||
|
app.LogHandler = callback |
||||
|
} |
||||
|
|
||||
|
//输出日志
|
||||
|
func (app *VideoSrt) Log(s string , v string) { |
||||
|
app.LogHandler(s , v) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//清空临时目录
|
||||
|
func (app *VideoSrt) ClearTempDir() { |
||||
|
//临时目录
|
||||
|
tmpAudioDir := app.AppDir + "/" + app.TempDir |
||||
|
if tool.DirExists(tmpAudioDir) { |
||||
|
//清空
|
||||
|
if remove := os.RemoveAll(tmpAudioDir); remove != nil { |
||||
|
app.Log("清空临时文件夹失败,请手动操作" , "警告") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//应用运行
|
||||
|
func (app *VideoSrt) Run(video string) { |
||||
|
var tmpAudio string = "" |
||||
|
|
||||
|
//致命错误捕获
|
||||
|
defer func() { |
||||
|
//拦截panic
|
||||
|
if err := recover(); err != nil { |
||||
|
//失败回调
|
||||
|
app.FailHandler(video) |
||||
|
//fmt.Println( err )
|
||||
|
|
||||
|
e , ok := err.(error) |
||||
|
if ok { |
||||
|
app.Log("错误:" + e.Error() , video) |
||||
|
} else { |
||||
|
panic(err) |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
//校验媒体文件
|
||||
|
if tool.VaildFile(video) != true { |
||||
|
panic("媒体文件不存在") |
||||
|
} |
||||
|
|
||||
|
tmpAudioDir := app.AppDir + "/" + app.TempDir |
||||
|
if !tool.DirExists(tmpAudioDir) { |
||||
|
//创建目录
|
||||
|
if err := tool.CreateDir(tmpAudioDir , false); err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
} |
||||
|
tmpAudioFile := tool.GetRandomCodeString(15) + ".mp3" |
||||
|
tmpAudio = tmpAudioDir + "/" + tmpAudioFile //临时音频文件
|
||||
|
|
||||
|
app.Log("提取音频文件 ..." , video) |
||||
|
|
||||
|
//分离/转换媒体音频
|
||||
|
ExtractVideoAudio(video , tmpAudio) |
||||
|
|
||||
|
app.Log("上传音频文件 ..." , video) |
||||
|
|
||||
|
//上传音频至OSS
|
||||
|
tempfile := UploadAudioToClound(app.AliyunOss , tmpAudio) |
||||
|
//获取完整链接
|
||||
|
filelink := app.AliyunOss.GetObjectFileUrl(tempfile) |
||||
|
|
||||
|
app.Log("上传文件成功 , 识别中 ..." , video) |
||||
|
|
||||
|
defer func() { |
||||
|
//清理oss音频文件
|
||||
|
if app.CloseAutoDeleteOssTempFile == false { |
||||
|
if e := DeleteOssCloundTempAudio(app.AliyunOss , tempfile); e!=nil { |
||||
|
app.Log("OSS临时音频清理失败,建议手动删除" , video) |
||||
|
} else { |
||||
|
app.Log("OSS临时音频清理成功" , video) |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
//关闭软件智能分段
|
||||
|
if app.CloseIntelligentBlockSwitch { |
||||
|
app.Log("您已关闭了软件智能分段处理,将输出原样结果" , video) |
||||
|
} |
||||
|
|
||||
|
//阿里云录音文件识别
|
||||
|
AudioResult , IntelligentBlockResult := AliyunAudioRecognition(app , video , app.AliyunClound, filelink) |
||||
|
|
||||
|
app.Log("文件识别成功 , 字幕处理中 ..." , video) |
||||
|
|
||||
|
//翻译字幕块
|
||||
|
if e := AliyunAudioResultTranslate(app , video , AudioResult , IntelligentBlockResult); e != nil { |
||||
|
app.Log("字幕翻译失败:" + e.Error() , video) |
||||
|
app.Log("已强制关闭翻译,仅输出原始字幕文件" , video) |
||||
|
app.TranslateCfg.TranslateSwitch = false |
||||
|
} |
||||
|
|
||||
|
//字幕过滤
|
||||
|
AliyunResultFilter(app , video , AudioResult , IntelligentBlockResult) |
||||
|
|
||||
|
//输出文件
|
||||
|
if app.OutputType.SRT { |
||||
|
AliyunAudioResultMakeSubtitleFile(app , video , OUTPUT_SRT , AudioResult , IntelligentBlockResult) |
||||
|
} |
||||
|
if app.OutputType.LRC { |
||||
|
AliyunAudioResultMakeSubtitleFile(app , video , OUTPUT_LRC , AudioResult , IntelligentBlockResult) |
||||
|
} |
||||
|
if app.OutputType.TXT { |
||||
|
AliyunAudioResultMakeSubtitleFile(app , video , OUTPUT_STRING , AudioResult , IntelligentBlockResult) |
||||
|
} |
||||
|
|
||||
|
//校验字幕段落是否为空
|
||||
|
if CheckEmptyResult(AudioResult) { |
||||
|
app.Log("检测到识别结果为空,生成字幕失败(检查:媒体文件人声是否清晰?语音引擎与媒体语言是否匹配?)" , video) |
||||
|
} |
||||
|
|
||||
|
app.Log("处理完成" , video) |
||||
|
|
||||
|
//删除临时文件
|
||||
|
if tmpAudio != "" { |
||||
|
if remove := os.Remove(tmpAudio); remove != nil { |
||||
|
app.Log("删除临时文件失败,请手动删除" , video) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//成功回调
|
||||
|
app.SuccessHandler(video) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
//统一运行翻译结果
|
||||
|
func (app *VideoSrt) RunTranslate(s string , file string) (*VideoSrtTranslateResult , error) { |
||||
|
var trys int = 0 |
||||
|
translateResult := new(VideoSrtTranslateResult) |
||||
|
|
||||
|
if app.TranslateCfg.Supplier == TRANSLATE_SUPPLIER_BAIDU { |
||||
|
if app.TranslateCfg.BaiduTranslate.AuthenType == translate.ACCOUNT_COMMON_AUTHEN { //百度翻译标准版
|
||||
|
//休眠1010毫秒
|
||||
|
time.Sleep(time.Millisecond * 1050) |
||||
|
} else { |
||||
|
//休眠200毫秒
|
||||
|
time.Sleep(time.Millisecond * 200) |
||||
|
} |
||||
|
|
||||
|
from := GetLanguageChar(app.TranslateCfg.InputLanguage , TRANSLATE_SUPPLIER_BAIDU) |
||||
|
to := GetLanguageChar(app.TranslateCfg.OutputLanguage , TRANSLATE_SUPPLIER_BAIDU) |
||||
|
|
||||
|
//发起翻译请求
|
||||
|
baiduResult,transErr := app.TranslateCfg.BaiduTranslate.TranslateBaidu(s , from , to) |
||||
|
for transErr != nil && trys <= 5 { |
||||
|
trys++ |
||||
|
app.Log("翻译请求失败,重试第" + strconv.Itoa(trys) + "次 ..." , file) |
||||
|
time.Sleep(time.Second * time.Duration(trys)) |
||||
|
//重试
|
||||
|
baiduResult,transErr = app.TranslateCfg.BaiduTranslate.TranslateBaidu(s , from , to) |
||||
|
} |
||||
|
if transErr != nil { |
||||
|
return translateResult,errors.New("翻译失败!错误信息:" + transErr.Error()) |
||||
|
} |
||||
|
|
||||
|
translateResult.TransResultDst = baiduResult.TransResultDst |
||||
|
translateResult.TransResultSrc = baiduResult.TransResultSrc |
||||
|
translateResult.From = baiduResult.From |
||||
|
translateResult.To = baiduResult.To |
||||
|
|
||||
|
return translateResult,nil |
||||
|
} else if app.TranslateCfg.Supplier == TRANSLATE_SUPPLIER_TENGXUNYUN { |
||||
|
//休眠
|
||||
|
t := app.TranslateCfg.TengxunyunTranslate.TranslateSleepTime(app.MaxConcurrency) |
||||
|
time.Sleep(t) |
||||
|
|
||||
|
from := GetLanguageChar(app.TranslateCfg.InputLanguage , TRANSLATE_SUPPLIER_TENGXUNYUN) |
||||
|
to := GetLanguageChar(app.TranslateCfg.OutputLanguage , TRANSLATE_SUPPLIER_TENGXUNYUN) |
||||
|
|
||||
|
//发起翻译请求
|
||||
|
txResult,transErr := app.TranslateCfg.TengxunyunTranslate.TranslateTengxunyun(s , from , to) |
||||
|
for transErr != nil && trys <= 5 { |
||||
|
trys++ |
||||
|
app.Log("翻译请求失败,重试第" + strconv.Itoa(trys) + "次 ..." , file) |
||||
|
time.Sleep(time.Second * time.Duration(trys)) |
||||
|
//重试
|
||||
|
txResult,transErr = app.TranslateCfg.TengxunyunTranslate.TranslateTengxunyun(s , from , to) |
||||
|
} |
||||
|
if transErr != nil { |
||||
|
return translateResult,errors.New("翻译失败!错误信息:" + transErr.Error()) |
||||
|
} |
||||
|
|
||||
|
translateResult.TransResultDst = txResult.TransResultDst |
||||
|
translateResult.TransResultSrc = txResult.TransResultSrc |
||||
|
translateResult.From = txResult.From |
||||
|
translateResult.To = txResult.To |
||||
|
|
||||
|
return translateResult,nil |
||||
|
} else { |
||||
|
return translateResult,errors.New("翻译失败!缺少翻译引擎!") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//提取视频音频文件
|
||||
|
func ExtractVideoAudio(video string , tmpAudio string) { |
||||
|
if err := ffmpeg.ExtractAudio(video , tmpAudio); err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//上传音频至oss
|
||||
|
func UploadAudioToClound(target aliyun.AliyunOss , audioFile string) string { |
||||
|
name := "" |
||||
|
//提取文件名称
|
||||
|
if fileInfo, e := os.Stat(audioFile);e != nil { |
||||
|
panic(e) |
||||
|
} else { |
||||
|
name = fileInfo.Name() |
||||
|
} |
||||
|
|
||||
|
//上传
|
||||
|
if file , e := target.UploadFile(audioFile , name); e != nil { |
||||
|
panic(e) |
||||
|
} else { |
||||
|
return file |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//清理临时音频文件
|
||||
|
func DeleteOssCloundTempAudio (target aliyun.AliyunOss , objectName string) error { |
||||
|
//删除
|
||||
|
if e := target.DeleteFile(objectName); e != nil { |
||||
|
return e |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
//检查是否为空输出
|
||||
|
func CheckEmptyResult(AudioResult map[int64][] *aliyun.AliyunAudioRecognitionResult) bool { |
||||
|
empty := true |
||||
|
for _,v := range AudioResult { |
||||
|
for range v { |
||||
|
empty = false |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
return empty |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//阿里云录音文件识别
|
||||
|
func AliyunAudioRecognition(app *VideoSrt , video string , engine aliyun.AliyunClound , filelink string) (AudioResult map[int64][] *aliyun.AliyunAudioRecognitionResult , IntelligentBlockResult map[int64][] *aliyun.AliyunAudioRecognitionResult) { |
||||
|
//创建识别请求
|
||||
|
taskid, client, e := engine.NewAudioFile(filelink) |
||||
|
if e != nil { |
||||
|
panic(e) |
||||
|
} |
||||
|
|
||||
|
AudioResult = make(map[int64][] *aliyun.AliyunAudioRecognitionResult) |
||||
|
IntelligentBlockResult = make(map[int64][] *aliyun.AliyunAudioRecognitionResult) |
||||
|
|
||||
|
//遍历获取识别结果
|
||||
|
resultError := engine.GetAudioFileResult(taskid , client , func(text string) { |
||||
|
//日志输出
|
||||
|
app.Log(text , video) |
||||
|
} , func(result []byte) { |
||||
|
//结果处理
|
||||
|
statusText, _ := jsonparser.GetString(result, "StatusText") //结果状态
|
||||
|
|
||||
|
if statusText == aliyun.STATUS_SUCCESS { |
||||
|
|
||||
|
if !app.CloseIntelligentBlockSwitch { |
||||
|
//获取智能分段结果
|
||||
|
aliyun.AliyunAudioResultWordHandle(result , func(vresult *aliyun.AliyunAudioRecognitionResult) { |
||||
|
channelId := vresult.ChannelId |
||||
|
|
||||
|
_ , isPresent := IntelligentBlockResult[channelId] |
||||
|
if isPresent { |
||||
|
//追加
|
||||
|
IntelligentBlockResult[channelId] = append(IntelligentBlockResult[channelId] , vresult) |
||||
|
} else { |
||||
|
//初始
|
||||
|
IntelligentBlockResult[channelId] = []*aliyun.AliyunAudioRecognitionResult{} |
||||
|
IntelligentBlockResult[channelId] = append(IntelligentBlockResult[channelId] , vresult) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
//获取原始结果
|
||||
|
_, err := jsonparser.ArrayEach(result, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { |
||||
|
text , _ := jsonparser.GetString(value, "Text") |
||||
|
channelId , _ := jsonparser.GetInt(value, "ChannelId") |
||||
|
beginTime , _ := jsonparser.GetInt(value, "BeginTime") |
||||
|
endTime , _ := jsonparser.GetInt(value, "EndTime") |
||||
|
silenceDuration , _ := jsonparser.GetInt(value, "SilenceDuration") |
||||
|
speechRate , _ := jsonparser.GetInt(value, "SpeechRate") |
||||
|
emotionValue , _ := jsonparser.GetInt(value, "EmotionValue") |
||||
|
|
||||
|
vresult := &aliyun.AliyunAudioRecognitionResult { |
||||
|
Text:text, |
||||
|
ChannelId:channelId, |
||||
|
BeginTime:beginTime, |
||||
|
EndTime:endTime, |
||||
|
SilenceDuration:silenceDuration, |
||||
|
SpeechRate:speechRate, |
||||
|
EmotionValue:emotionValue, |
||||
|
} |
||||
|
|
||||
|
_ , isPresent := AudioResult[channelId] |
||||
|
if isPresent { |
||||
|
//追加
|
||||
|
AudioResult[channelId] = append(AudioResult[channelId] , vresult) |
||||
|
} else { |
||||
|
//初始
|
||||
|
AudioResult[channelId] = []*aliyun.AliyunAudioRecognitionResult{} |
||||
|
AudioResult[channelId] = append(AudioResult[channelId] , vresult) |
||||
|
} |
||||
|
} , "Result", "Sentences") |
||||
|
|
||||
|
if err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
if (resultError != nil) { |
||||
|
panic(resultError) |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//阿里云识别字幕块翻译处理
|
||||
|
func AliyunAudioResultTranslate(app *VideoSrt , video string , AudioResult map[int64][] *aliyun.AliyunAudioRecognitionResult , IntelligentBlockResult map[int64][] *aliyun.AliyunAudioRecognitionResult) error { |
||||
|
//输出日志
|
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
app.Log("字幕翻译处理中 ..." , video) |
||||
|
|
||||
|
//百度翻译标准版
|
||||
|
if app.TranslateCfg.Supplier == TRANSLATE_SUPPLIER_BAIDU && app.TranslateCfg.BaiduTranslate.AuthenType == translate.ACCOUNT_COMMON_AUTHEN { |
||||
|
app.Log("你使用的是 “百度翻译标准版” 账号,翻译速度较慢,请耐心等待 ..." , video) |
||||
|
} |
||||
|
} else { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
//输出音轨
|
||||
|
outputSoundTrack := app.SoundTrack |
||||
|
|
||||
|
//计算总任务行数
|
||||
|
totalRow := 0 //总处理行数
|
||||
|
var lastrv float64 = 0 //最后进度(%)
|
||||
|
|
||||
|
//开启智能分段
|
||||
|
if !app.CloseIntelligentBlockSwitch { |
||||
|
if app.OutputType.SRT || app.OutputType.LRC { |
||||
|
for channel , result := range IntelligentBlockResult { |
||||
|
soundChannel := channel + 1 |
||||
|
if outputSoundTrack != 3 && outputSoundTrack != 0 { |
||||
|
if outputSoundTrack != int(soundChannel) { |
||||
|
continue //跳出非输出音轨转换
|
||||
|
} |
||||
|
} |
||||
|
totalRow += len(result) |
||||
|
} |
||||
|
} |
||||
|
if app.OutputType.TXT { |
||||
|
for channel,result := range AudioResult { |
||||
|
soundChannel := channel + 1 |
||||
|
if outputSoundTrack != 3 && outputSoundTrack != 0 { |
||||
|
if outputSoundTrack != int(soundChannel) { |
||||
|
continue //跳出非输出音轨转换
|
||||
|
} |
||||
|
} |
||||
|
totalRow += len(result) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
index := 0 |
||||
|
//翻译任务
|
||||
|
if app.OutputType.SRT || app.OutputType.LRC { |
||||
|
for channel , result := range IntelligentBlockResult { |
||||
|
soundChannel := channel + 1 |
||||
|
if outputSoundTrack != 3 && outputSoundTrack != 0 { |
||||
|
if outputSoundTrack != int(soundChannel) { |
||||
|
continue //跳出非输出音轨转换
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for _ , data := range result { |
||||
|
transResult,e := app.RunTranslate(data.Text , video) |
||||
|
if e != nil { |
||||
|
return e |
||||
|
//panic(e) //终止翻译
|
||||
|
} |
||||
|
data.TranslateText = strings.TrimSpace(transResult.TransResultDst) //译文
|
||||
|
|
||||
|
index++ |
||||
|
rv := (float64(index)/float64(totalRow))*100 |
||||
|
if (rv - lastrv) > 20 { |
||||
|
//输出比例
|
||||
|
app.Log("字幕翻译已处理:" + fmt.Sprintf("%.2f" , rv) + "%" , video) |
||||
|
lastrv = rv |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if app.OutputType.TXT { |
||||
|
for channel,result := range AudioResult { |
||||
|
soundChannel := channel + 1 |
||||
|
if outputSoundTrack != 3 && outputSoundTrack != 0 { |
||||
|
if outputSoundTrack != int(soundChannel) { |
||||
|
continue //跳出非输出音轨转换
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for _ , data := range result { |
||||
|
transResult,e := app.RunTranslate(data.Text , video) |
||||
|
if e != nil { |
||||
|
return e |
||||
|
//panic(e) //终止翻译
|
||||
|
} |
||||
|
data.TranslateText = strings.TrimSpace(transResult.TransResultDst) //译文
|
||||
|
|
||||
|
index++ |
||||
|
rv := (float64(index)/float64(totalRow))*100 |
||||
|
if (rv - lastrv) > 20 { |
||||
|
//输出比例
|
||||
|
app.Log("字幕翻译已处理:" + fmt.Sprintf("%.2f" , rv) + "%" , video) |
||||
|
lastrv = rv |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} else { //关闭智能分段
|
||||
|
for channel,result := range AudioResult { |
||||
|
soundChannel := channel + 1 |
||||
|
if outputSoundTrack != 3 && outputSoundTrack != 0 { |
||||
|
if outputSoundTrack != int(soundChannel) { |
||||
|
continue //跳出非输出音轨转换
|
||||
|
} |
||||
|
} |
||||
|
totalRow += len(result) |
||||
|
} |
||||
|
|
||||
|
index := 0 |
||||
|
//翻译任务
|
||||
|
for channel,result := range AudioResult { |
||||
|
soundChannel := channel + 1 |
||||
|
if outputSoundTrack != 3 && outputSoundTrack != 0 { |
||||
|
if outputSoundTrack != int(soundChannel) { |
||||
|
continue //跳出非输出音轨转换
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for _ , data := range result { |
||||
|
transResult,e := app.RunTranslate(data.Text , video) |
||||
|
if e != nil { |
||||
|
return e |
||||
|
//panic(e) //终止翻译
|
||||
|
} |
||||
|
data.TranslateText = strings.TrimSpace(transResult.TransResultDst) //译文
|
||||
|
index++ |
||||
|
rv := (float64(index)/float64(totalRow))*100 |
||||
|
if (rv - lastrv) > 20 { |
||||
|
//输出比例
|
||||
|
app.Log("字幕翻译已处理:" + fmt.Sprintf("%.2f" , rv) + "%" , video) |
||||
|
lastrv = rv |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
//阿里云识别字幕过滤
|
||||
|
func AliyunResultFilter(app *VideoSrt , video string , AudioResult map[int64][] *aliyun.AliyunAudioRecognitionResult , IntelligentBlockResult map[int64][] *aliyun.AliyunAudioRecognitionResult) { |
||||
|
if !app.FilterSetings.DefinedFilter.Switch && !app.FilterSetings.GlobalFilter.Switch { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
app.Log("字幕过滤处理中 ..." , video) |
||||
|
|
||||
|
//语气词过滤
|
||||
|
if app.FilterSetings.GlobalFilter.Switch && strings.TrimSpace(app.FilterSetings.GlobalFilter.Words) != "" { |
||||
|
modalWords := strings.Split(app.FilterSetings.GlobalFilter.Words , "\r\n") |
||||
|
|
||||
|
for _,result := range IntelligentBlockResult { |
||||
|
for _ , data := range result { |
||||
|
for _ , w := range modalWords { |
||||
|
|
||||
|
data.Text = ModalWordsFilter(data.Text , w) |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
data.TranslateText = ModalWordsFilter(data.TranslateText , w) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
for _,result := range AudioResult { |
||||
|
for _ , data := range result { |
||||
|
for _ , w := range modalWords { |
||||
|
|
||||
|
data.Text = ModalWordsFilter(data.Text , w) |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
data.TranslateText = ModalWordsFilter(data.TranslateText , w) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//自定义规则过滤
|
||||
|
if app.FilterSetings.DefinedFilter.Switch && len(app.FilterSetings.DefinedFilter.Rule) > 0 { |
||||
|
rules := app.FilterSetings.DefinedFilter.Rule |
||||
|
|
||||
|
for _,result := range IntelligentBlockResult { |
||||
|
for _ , data := range result { |
||||
|
for _ , rule := range rules { |
||||
|
|
||||
|
data.Text = DefinedWordRuleFilter(data.Text , rule) |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
data.TranslateText = DefinedWordRuleFilter(data.TranslateText , rule) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
for _,result := range AudioResult { |
||||
|
for _ , data := range result { |
||||
|
for _ , rule := range rules { |
||||
|
|
||||
|
data.Text = DefinedWordRuleFilter(data.Text , rule) |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
data.TranslateText = DefinedWordRuleFilter(data.TranslateText , rule) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
//阿里云录音识别结果集生成字幕文件
|
||||
|
func AliyunAudioResultMakeSubtitleFile(app *VideoSrt , video string , outputType int , AudioResult map[int64][] *aliyun.AliyunAudioRecognitionResult , IntelligentBlockResult map[int64][] *aliyun.AliyunAudioRecognitionResult) { |
||||
|
var subfileDir string |
||||
|
if app.SrtDir == "" { |
||||
|
subfileDir = path.Dir(video) |
||||
|
} else { |
||||
|
subfileDir = app.SrtDir |
||||
|
} |
||||
|
|
||||
|
subfile := tool.GetFileBaseName(video) |
||||
|
|
||||
|
//注入合适的数据块
|
||||
|
thisAudioResult := make(map[int64][] *aliyun.AliyunAudioRecognitionResult) |
||||
|
|
||||
|
//开启智能分段
|
||||
|
if !app.CloseIntelligentBlockSwitch { |
||||
|
if outputType == OUTPUT_SRT || outputType == OUTPUT_LRC { |
||||
|
thisAudioResult = IntelligentBlockResult |
||||
|
} else if outputType == OUTPUT_STRING { |
||||
|
thisAudioResult = AudioResult |
||||
|
} |
||||
|
} else { |
||||
|
thisAudioResult = AudioResult |
||||
|
} |
||||
|
|
||||
|
|
||||
|
oneSoundChannel := false //是否输出单条音轨
|
||||
|
//根据音轨,输出文件
|
||||
|
for channel,result := range thisAudioResult { |
||||
|
soundChannel := channel + 1 |
||||
|
if app.SoundTrack != 3 && app.SoundTrack != 0 { |
||||
|
oneSoundChannel = true |
||||
|
if app.SoundTrack != int(soundChannel) { |
||||
|
//跳过此音轨
|
||||
|
continue |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var thisfile string |
||||
|
//文件名称
|
||||
|
if oneSoundChannel { |
||||
|
thisfile = subfileDir + "/" + subfile |
||||
|
} else { |
||||
|
thisfile = subfileDir + "/" + subfile + "_channel_" + strconv.FormatInt(soundChannel , 10) |
||||
|
} |
||||
|
//输出文件类型
|
||||
|
if outputType == OUTPUT_SRT { |
||||
|
thisfile += ".srt" |
||||
|
} else if outputType == OUTPUT_STRING { |
||||
|
thisfile += ".txt" |
||||
|
} else if outputType == OUTPUT_LRC { |
||||
|
thisfile += ".lrc" |
||||
|
} |
||||
|
|
||||
|
//创建文件
|
||||
|
file, e := os.Create(thisfile) |
||||
|
if e != nil { |
||||
|
panic(e) |
||||
|
} |
||||
|
defer file.Close() |
||||
|
|
||||
|
//文件编码分支
|
||||
|
if app.OutputEncode == OUTPUT_ENCODE_UTF8_BOM { |
||||
|
if _, e := file.Write([]byte{0xEF, 0xBB, 0xBF});e != nil { |
||||
|
panic(e) |
||||
|
} |
||||
|
} |
||||
|
//歌词头
|
||||
|
if outputType == OUTPUT_LRC { |
||||
|
_,_ = file.WriteString("[ar:]\r\n[ti:]\r\n[al:]\r\n[by:]\r\n") |
||||
|
} |
||||
|
|
||||
|
//主字幕
|
||||
|
bilingualAsc := app.TranslateCfg.OutputMainSubtitleInputLanguage |
||||
|
index := 0 |
||||
|
|
||||
|
//普通文本容器
|
||||
|
var txtOutputContent bytes.Buffer |
||||
|
var txtTransalteOutputContent bytes.Buffer |
||||
|
|
||||
|
for _ , data := range result { |
||||
|
var linestr string |
||||
|
|
||||
|
if data.Text == "" { |
||||
|
continue //跳过空行
|
||||
|
} |
||||
|
|
||||
|
//字幕、歌词文件处理
|
||||
|
if outputType == OUTPUT_SRT || outputType == OUTPUT_LRC { |
||||
|
//拼接
|
||||
|
if outputType == OUTPUT_SRT { |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
if app.TranslateCfg.BilingualSubtitleSwitch { |
||||
|
linestr = MakeSubtitleText(index , data.BeginTime , data.EndTime , data.Text , data.TranslateText , true , bilingualAsc) |
||||
|
} else { |
||||
|
linestr = MakeSubtitleText(index , data.BeginTime , data.EndTime , data.TranslateText , "" , false , true) |
||||
|
} |
||||
|
} else { |
||||
|
linestr = MakeSubtitleText(index , data.BeginTime , data.EndTime , data.Text , "" , false , true) |
||||
|
} |
||||
|
} else if outputType == OUTPUT_LRC { |
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
linestr = MakeMusicLrcText(index , data.BeginTime , data.EndTime , data.TranslateText) |
||||
|
} else { |
||||
|
linestr = MakeMusicLrcText(index , data.BeginTime , data.EndTime , data.Text) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//写入行
|
||||
|
if _, e = file.WriteString(linestr);e != nil { |
||||
|
panic(e) |
||||
|
} |
||||
|
} else if outputType == OUTPUT_STRING { |
||||
|
//普通文本处理
|
||||
|
txtOutputContent.WriteString(data.Text) |
||||
|
txtOutputContent.WriteString("\r\n") |
||||
|
|
||||
|
if app.TranslateCfg.TranslateSwitch { |
||||
|
txtTransalteOutputContent.WriteString(data.TranslateText) |
||||
|
txtTransalteOutputContent.WriteString("\r\n") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
index++ |
||||
|
} |
||||
|
|
||||
|
//写入文本文件
|
||||
|
if outputType == OUTPUT_STRING { |
||||
|
txtOutputContent.WriteString("\r\n\r\n\r\n\r\n\r\n") |
||||
|
if _, e = file.WriteString(txtOutputContent.String());e != nil { |
||||
|
panic(e) |
||||
|
} |
||||
|
if _, e = file.WriteString(txtTransalteOutputContent.String());e != nil { |
||||
|
panic(e) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
set GO111MODULE=on |
||||
|
set GOARCH=386 |
||||
|
rsrc -manifest main.manifest -ico serviceMonitor.ico -o rsrc.syso |
||||
|
go build -ldflags="-s -w -H=windowsgui" |
||||
|
upx -9 ./serviceMonitor.exe |
After Width: | Height: | Size: 400 B |
After Width: | Height: | Size: 407 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 297 B |
After Width: | Height: | Size: 452 B |
After Width: | Height: | Size: 438 B |
After Width: | Height: | Size: 344 B |
After Width: | Height: | Size: 491 B |
After Width: | Height: | Size: 292 B |
After Width: | Height: | Size: 369 B |
After Width: | Height: | Size: 376 B |
After Width: | Height: | Size: 374 B |
After Width: | Height: | Size: 388 B |
After Width: | Height: | Size: 371 B |
After Width: | Height: | Size: 551 B |
@ -0,0 +1,20 @@ |
|||||
|
module videosrt |
||||
|
|
||||
|
go 1.12 |
||||
|
|
||||
|
require ( |
||||
|
github.com/PuerkitoBio/goquery v1.5.0 |
||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.60.268 |
||||
|
github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible |
||||
|
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect |
||||
|
github.com/buger/jsonparser v0.0.0-20191004114745-ee4c978eae7e |
||||
|
github.com/lxn/walk v0.0.0-20191121152919-b7c43041fb1b |
||||
|
github.com/lxn/win v0.0.0-20191106123917-121afc750dd3 // indirect |
||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect |
||||
|
github.com/satori/go.uuid v1.2.0 // indirect |
||||
|
github.com/tencentcloud/tencentcloud-sdk-go v3.0.128+incompatible |
||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e // indirect |
||||
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect |
||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect |
||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect |
||||
|
) |
@ -0,0 +1,15 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> |
||||
|
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="SomeFunkyNameHere" type="win32"/> |
||||
|
<dependency> |
||||
|
<dependentAssembly> |
||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/> |
||||
|
</dependentAssembly> |
||||
|
</dependency> |
||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3"> |
||||
|
<windowsSettings> |
||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness> |
||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware> |
||||
|
</windowsSettings> |
||||
|
</application> |
||||
|
</assembly> |