From 05f8469074e50902ef3be23f26943b6b9add5c51 Mon Sep 17 00:00:00 2001 From: VIVIMAN Date: Sun, 9 Jan 2022 12:05:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=AF=E5=8F=82=E8=80=83?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gui_learn/.github/workflows/go.yml | 47 ++ gui_learn/.gitignore | 6 + gui_learn/LICENSE | 339 ++++++++ gui_learn/README.md | 116 +++ gui_learn/app/aliyun/engine.go | 268 ++++++ gui_learn/app/aliyun/oss.go | 102 +++ gui_learn/app/aliyun/tool.go | 416 +++++++++ gui_learn/app/app_tool.go | 97 +++ gui_learn/app/data.go | 665 +++++++++++++++ gui_learn/app/datacache/cache.go | 53 ++ gui_learn/app/datacache/json.go | 31 + gui_learn/app/event.go | 1128 +++++++++++++++++++++++++ gui_learn/app/ffmpeg/ffmpeg.go | 57 ++ gui_learn/app/log.go | 27 + gui_learn/app/parse/srt.go | 148 ++++ gui_learn/app/parse/tool.go | 88 ++ gui_learn/app/srt.go | 451 ++++++++++ gui_learn/app/task.go | 92 ++ gui_learn/app/task_tanslate.go | 89 ++ gui_learn/app/tool/chinese_simple.go | 122 +++ gui_learn/app/tool/tool.go | 248 ++++++ gui_learn/app/translate/baidu.go | 110 +++ gui_learn/app/translate/tengxunyun.go | 91 ++ gui_learn/app/version.go | 83 ++ gui_learn/app/video.go | 836 ++++++++++++++++++ gui_learn/build.bat | 5 + gui_learn/data/img/about.png | Bin 0 -> 400 bytes gui_learn/data/img/app-setings.png | Bin 0 -> 407 bytes gui_learn/data/img/app.ico | Bin 0 -> 1150 bytes gui_learn/data/img/gitee.png | Bin 0 -> 297 bytes gui_learn/data/img/github.png | Bin 0 -> 452 bytes gui_learn/data/img/index.png | Bin 0 -> 438 bytes gui_learn/data/img/media.png | Bin 0 -> 344 bytes gui_learn/data/img/muyan.png | Bin 0 -> 491 bytes gui_learn/data/img/new.png | Bin 0 -> 292 bytes gui_learn/data/img/open.png | Bin 0 -> 369 bytes gui_learn/data/img/oss.png | Bin 0 -> 376 bytes gui_learn/data/img/setings.png | Bin 0 -> 374 bytes gui_learn/data/img/translate.png | Bin 0 -> 388 bytes gui_learn/data/img/version.png | Bin 0 -> 371 bytes gui_learn/data/img/voice.png | Bin 0 -> 551 bytes gui_learn/go.mod | 20 + gui_learn/main.go | 1065 +++++++++++++++++++++++ gui_learn/main.manifest | 15 + gui_learn/rsrc.syso | Bin 0 -> 2381 bytes 45 files changed, 6815 insertions(+) create mode 100644 gui_learn/.github/workflows/go.yml create mode 100644 gui_learn/.gitignore create mode 100644 gui_learn/LICENSE create mode 100644 gui_learn/README.md create mode 100644 gui_learn/app/aliyun/engine.go create mode 100644 gui_learn/app/aliyun/oss.go create mode 100644 gui_learn/app/aliyun/tool.go create mode 100644 gui_learn/app/app_tool.go create mode 100644 gui_learn/app/data.go create mode 100644 gui_learn/app/datacache/cache.go create mode 100644 gui_learn/app/datacache/json.go create mode 100644 gui_learn/app/event.go create mode 100644 gui_learn/app/ffmpeg/ffmpeg.go create mode 100644 gui_learn/app/log.go create mode 100644 gui_learn/app/parse/srt.go create mode 100644 gui_learn/app/parse/tool.go create mode 100644 gui_learn/app/srt.go create mode 100644 gui_learn/app/task.go create mode 100644 gui_learn/app/task_tanslate.go create mode 100644 gui_learn/app/tool/chinese_simple.go create mode 100644 gui_learn/app/tool/tool.go create mode 100644 gui_learn/app/translate/baidu.go create mode 100644 gui_learn/app/translate/tengxunyun.go create mode 100644 gui_learn/app/version.go create mode 100644 gui_learn/app/video.go create mode 100644 gui_learn/build.bat create mode 100644 gui_learn/data/img/about.png create mode 100644 gui_learn/data/img/app-setings.png create mode 100644 gui_learn/data/img/app.ico create mode 100644 gui_learn/data/img/gitee.png create mode 100644 gui_learn/data/img/github.png create mode 100644 gui_learn/data/img/index.png create mode 100644 gui_learn/data/img/media.png create mode 100644 gui_learn/data/img/muyan.png create mode 100644 gui_learn/data/img/new.png create mode 100644 gui_learn/data/img/open.png create mode 100644 gui_learn/data/img/oss.png create mode 100644 gui_learn/data/img/setings.png create mode 100644 gui_learn/data/img/translate.png create mode 100644 gui_learn/data/img/version.png create mode 100644 gui_learn/data/img/voice.png create mode 100644 gui_learn/go.mod create mode 100644 gui_learn/main.go create mode 100644 gui_learn/main.manifest create mode 100644 gui_learn/rsrc.syso diff --git a/gui_learn/.github/workflows/go.yml b/gui_learn/.github/workflows/go.yml new file mode 100644 index 0000000..3a7cef5 --- /dev/null +++ b/gui_learn/.github/workflows/go.yml @@ -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 + diff --git a/gui_learn/.gitignore b/gui_learn/.gitignore new file mode 100644 index 0000000..2873879 --- /dev/null +++ b/gui_learn/.gitignore @@ -0,0 +1,6 @@ +tests +data/json* +go.sum +*.zip +*.exe +.idea diff --git a/gui_learn/LICENSE b/gui_learn/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/gui_learn/LICENSE @@ -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. + + + Copyright (C) + + 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. + + , 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. diff --git a/gui_learn/README.md b/gui_learn/README.md new file mode 100644 index 0000000..cc395d3 --- /dev/null +++ b/gui_learn/README.md @@ -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文件的开源软件工具。
适用于快速、批量的为媒体(视频/音频)生成中/英文字幕、文本文件的业务场景。 + +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) + + +## 界面预览 + +![](https://pic.downk.cc/item/5f28c7ea14195aa594369a59.gif) + +## 应用场景 + +- 识别**视频/音频**的语音生成字幕文件(支持中英互译,双语字幕) +- 提取**视频/音频**的语音文本 +- 批量翻译、过滤处理/编码SRT字幕文件 + + + +## 软件优势 + +- 使用阿里云语音识别接口,准确度高,标准普通话/英语识别率95%以上 +- 视频识别无需上传原视频,方便快速且节省时间 +- 支持多任务多文件批量处理 +- 支持视频、音频常见多种格式文件 +- 支持同时输出字幕SRT文件、LRC文件、普通文本3种类型 +- 支持语气词过滤、自定义文本过滤、正则过滤等,使软件生成的字幕更加精准 +- 支持字幕中英互译、双语字幕输出,及日语、韩语、法语、德语、西班牙语、俄语、意大利语、泰语等 +- 支持多翻译引擎(百度翻译、腾讯云翻译) +- 支持批量翻译、编码SRT字幕文件 + + +## Download + + +##### 下载地址: +- (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) 页面下载其他版本 + + +## 注意事项 + +- 软件目录下的 `data`目录为数据存储目录,请勿删除。否则可能会导致配置丢失 +- 项目使用了 [ffmpeg](http://ffmpeg.org/) 依赖,除非您的电脑已经安装了`ffmpeg`环境,否则请下载包含`ffmpeg`依赖的软件包 + + +## 升级说明 + +- 先下载最新版本的软件包 +- 然后用旧版本软件的 `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群`与我们交流。 + + + +## 交流&联系 + +- 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) (已满) + + + +## 捐赠&支持 + +![](https://pic2.superbed.cn/item/5e00b93476085c3289dd2dc0.png) + +#### 每日领支付宝红包(2021.8 ~ 2021.11) +
+ +
diff --git a/gui_learn/app/aliyun/engine.go b/gui_learn/app/aliyun/engine.go new file mode 100644 index 0000000..4651b8d --- /dev/null +++ b/gui_learn/app/aliyun/engine.go @@ -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 + } +} \ No newline at end of file diff --git a/gui_learn/app/aliyun/oss.go b/gui_learn/app/aliyun/oss.go new file mode 100644 index 0000000..152d964 --- /dev/null +++ b/gui_learn/app/aliyun/oss.go @@ -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 + } +} \ No newline at end of file diff --git a/gui_learn/app/aliyun/tool.go b/gui_learn/app/aliyun/tool.go new file mode 100644 index 0000000..27a99e9 --- /dev/null +++ b/gui_learn/app/aliyun/tool.go @@ -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 +} \ No newline at end of file diff --git a/gui_learn/app/app_tool.go b/gui_learn/app/app_tool.go new file mode 100644 index 0000000..6a27cc7 --- /dev/null +++ b/gui_learn/app/app_tool.go @@ -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() +} \ No newline at end of file diff --git a/gui_learn/app/data.go b/gui_learn/app/data.go new file mode 100644 index 0000000..a609b1e --- /dev/null +++ b/gui_learn/app/data.go @@ -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:"高级版"}, + } +} \ No newline at end of file diff --git a/gui_learn/app/datacache/cache.go b/gui_learn/app/datacache/cache.go new file mode 100644 index 0000000..760e24f --- /dev/null +++ b/gui_learn/app/datacache/cache.go @@ -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) + } + } +} \ No newline at end of file diff --git a/gui_learn/app/datacache/json.go b/gui_learn/app/datacache/json.go new file mode 100644 index 0000000..c23fd6f --- /dev/null +++ b/gui_learn/app/datacache/json.go @@ -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 +} diff --git a/gui_learn/app/event.go b/gui_learn/app/event.go new file mode 100644 index 0000000..e190024 --- /dev/null +++ b/gui_learn/app/event.go @@ -0,0 +1,1128 @@ +package app + +import ( + "errors" + "github.com/lxn/walk" + . "github.com/lxn/walk/declarative" + "log" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "videosrt/app/aliyun" + "videosrt/app/tool" + "videosrt/app/translate" +) + +type MyMainWindow struct { + *walk.MainWindow +} + +//创建一个提示消息 +func (mw *MyMainWindow) NewInformationTips(title string , message string) { + walk.MsgBox(mw, title , message , walk.MsgBoxIconInformation) +} + +//创建一个错误消息 +func (mw *MyMainWindow) NewErrormationTips(title string , message string) { + walk.MsgBox(mw, title , message , walk.MsgBoxIconWarning) +} + + +// 运行 应用设置 Dialog +func(mw *MyMainWindow) RunAppSetingDialog(owner walk.Form , confirmCall func(*AppSetings)) { + var seting *AppSetings + var dlg *walk.Dialog + var db *walk.DataBinder + var acceptPB, cancelPB *walk.PushButton + + seting = Setings.GetCacheAppSetingsData() //查询缓存数据 + //fmt.Println( setings ) + if seting.MaxConcurrency == 0 { + seting.MaxConcurrency = 2 //默认并发数 + } + + Dialog{ + AssignTo: &dlg, + Title: "软件设置", + Font:Font{Family: "微软雅黑", PointSize: 9}, + DefaultButton: &acceptPB, + CancelButton: &cancelPB, + DataBinder: DataBinder{ + AssignTo: &db, + Name: "setings", + DataSource: seting, + ErrorPresenter: ToolTipErrorPresenter{}, + }, + MinSize: Size{450, 220}, + Layout: VBox{}, + Children: []Widget{ + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + Label{ + Text: "任务处理并发数:", + }, + NumberEdit{ + Value: Bind("MaxConcurrency", Range{1, 20}), + Decimals: 0, + }, + + Label{ + Text: "文件输出目录:", + }, + LineEdit{ + Text: Bind("SrtFileDir"), + }, + Label{ + ColumnSpan: 2, + Text: "说明:\r\n“文件输出目录” 若留空,则默认与媒体文件输出到同一目录下", + TextColor:walk.RGB(190 , 190 , 190), + MinSize:Size{Height:40}, + }, + + + Label{ + Text: "关闭软件智能分段处理:", + }, + CheckBox{ + Checked: Bind("CloseIntelligentBlockSwitch"), + }, + Label{ + Text: "关闭OSS临时文件清理:", + }, + CheckBox{ + Checked: Bind("CloseAutoDeleteOssTempFile"), + }, + Label{ + Text: "关闭软件新版本提醒:", + }, + CheckBox{ + Checked: Bind("CloseNewVersionMessage"), + }, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + AssignTo: &acceptPB, + Text: "保存", + OnClicked: func() { + if err := db.Submit(); err != nil { + log.Fatal(err) + return + } + //目录校验 + if seting.SrtFileDir != "" { + tmpDir := tool.WinDir(seting.SrtFileDir) + if !tool.DirExists(tmpDir) { + mw.NewErrormationTips("错误" , "目录无效/不存在:" + seting.SrtFileDir) + return; + } + seting.SrtFileDir = tmpDir + } + + //设置缓存 + Setings.SetCacheAppSetingsData(seting) + + //设置回调 + confirmCall(seting) + + dlg.Accept() + }, + }, + PushButton{ + AssignTo: &cancelPB, + Text: "取消", + OnClicked: func() { dlg.Cancel() }, + }, + }, + }, + }, + }.Run( owner ) +} + + +// 运行 新建语音引擎 Dialog +func(mw *MyMainWindow) RunSpeechEngineSetingDialog(owner walk.Form , confirmCall func()) { + var engine *AliyunEngineCache + var dlg *walk.Dialog + var db *walk.DataBinder + var acceptPB, cancelPB *walk.PushButton + + engine = new(AliyunEngineCache) + + engine.Region = aliyun.ALIYUN_CLOUND_REGION_CHA //默认值 + + Dialog{ + AssignTo: &dlg, + Title: "新建语音引擎", + Font:Font{Family: "微软雅黑", PointSize: 9}, + DefaultButton: &acceptPB, + CancelButton: &cancelPB, + DataBinder: DataBinder{ + AssignTo: &db, + Name: "engine", + DataSource: engine, + ErrorPresenter: ToolTipErrorPresenter{}, + }, + MinSize: Size{500, 300}, + Layout: VBox{}, + Children: []Widget{ + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + Label{ + Text: "名称/别名:", + }, + LineEdit{ + Text: Bind("Alias"), + }, + + Label{ + Text: "AppKey:", + }, + LineEdit{ + Text: Bind("AppKey"), + }, + + Label{ + Text: "AccessKeyId:", + }, + LineEdit{ + Text: Bind("AccessKeyId"), + }, + + Label{ + Text: "AccessKeySecret:", + }, + LineEdit{ + Text: Bind("AccessKeySecret"), + }, + + Label{ + Text: "服务区域:", + }, + ComboBox{ + Value: Bind("Region", SelRequired{}), + BindingMember: "Id", + DisplayMember: "Name", + Model: GetAliyunEngineRegionOptionSelects(), + }, + + Label{ + ColumnSpan: 2, + Text: "说明:\r\n“语音识别” 目前使用的是阿里云语音服务商,请填写相关的引擎配置。", + TextColor:walk.RGB(190 , 190 , 190), + }, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + AssignTo: &acceptPB, + Text: "确定新增", + OnClicked: func() { + if err := db.Submit(); err != nil { + log.Print(err) + return + } + //参数验证 + if (engine.Alias == "") { + mw.NewErrormationTips("提示" , "请填写 名称/别名") + return + } + if (engine.AppKey == "") { + mw.NewErrormationTips("提示" , "请填写 AppKey") + return + } + if (engine.AccessKeyId == "") { + mw.NewErrormationTips("提示" , "请填写 AccessKeyId") + return + } + if (engine.AccessKeySecret == "") { + mw.NewErrormationTips("提示" , "请填写 AccessKeySecret") + return + } + + //去空格 + engine.AppKey = strings.TrimSpace(engine.AppKey) + engine.AccessKeyId = strings.TrimSpace(engine.AccessKeyId) + engine.AccessKeySecret = strings.TrimSpace(engine.AccessKeySecret) + + //获取缓存数据 + localData := Engine.GetCacheAliyunEngineListData() + //生成id + lens := len(localData.Engine) + if lens == 0 { + engine.Id = 1 + } else { + engine.Id = localData.Engine[ lens - 1 ].Id + 1 + } + //追加数据 + localData.Engine = append(localData.Engine , engine) + //缓存数据 + Engine.SetCacheAliyunEngineListData(localData) + + //调用回调 + confirmCall() + + dlg.Accept() + }, + }, + PushButton{ + AssignTo: &cancelPB, + Text: "取消", + OnClicked: func() { dlg.Cancel() }, + }, + }, + }, + }, + }.Run( owner ) +} + + +// 运行 新建[百度]翻译引擎 Dialog +func(mw *MyMainWindow) RunBaiduTranslateEngineSetingDialog(owner walk.Form , confirmCall func()) { + var engine *TranslateEngineStruct + var dlg *walk.Dialog + var db *walk.DataBinder + var acceptPB, cancelPB *walk.PushButton + + engine = new(TranslateEngineStruct) + + //默认值 + engine.Supplier = TRANSLATE_SUPPLIER_BAIDU + engine.BaiduEngine.AuthenType = translate.ACCOUNT_COMMON_AUTHEN + + Dialog{ + AssignTo: &dlg, + Title: "新建翻译引擎(百度翻译)", + Font:Font{Family: "微软雅黑", PointSize: 9}, + DefaultButton: &acceptPB, + CancelButton: &cancelPB, + DataBinder: DataBinder{ + AssignTo: &db, + Name: "engine", + DataSource: engine, + ErrorPresenter: ToolTipErrorPresenter{}, + }, + MinSize: Size{500, 300}, + Layout: VBox{}, + Children: []Widget{ + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + Label{ + Text: "名称/别名:", + }, + LineEdit{ + Text: Bind("Alias"), + }, + + Label{ + Text: "AppId:", + }, + LineEdit{ + Text: Bind("BaiduEngine.AppId"), + }, + + Label{ + Text: "AppSecret:", + }, + LineEdit{ + Text: Bind("BaiduEngine.AppSecret"), + }, + + Label{ + Text: "账号认证类型:", + }, + ComboBox{ + Value: Bind("BaiduEngine.AuthenType", SelRequired{}), + BindingMember: "Id", + DisplayMember: "Name", + Model: GetBaiduTranslateAuthenTypeOptionsSelects(), + }, + + TextLabel{ + ColumnSpan: 2, + Row: 5, + Text: "\r\n说明:\r\n请填写在 “百度翻译开放平台” 的申请密钥,注意 “标准版” 和 “高级版” 的选择", + TextColor:walk.RGB(190 , 190 , 190), + }, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + AssignTo: &acceptPB, + Text: "确定新增", + OnClicked: func() { + if err := db.Submit(); err != nil { + log.Print(err) + return + } + //参数验证 + if (engine.Alias == "") { + mw.NewErrormationTips("提示" , "请填写 名称/别名") + return + } + if (engine.BaiduEngine.AppId == "") { + mw.NewErrormationTips("提示" , "请填写 AppId") + return + } + if (engine.BaiduEngine.AppSecret == "") { + mw.NewErrormationTips("提示" , "请填写 AppSecret") + return + } + + //去空格 + engine.BaiduEngine.AppId = strings.TrimSpace(engine.BaiduEngine.AppId) + engine.BaiduEngine.AppSecret = strings.TrimSpace(engine.BaiduEngine.AppSecret) + + //获取缓存数据 + localData := Translate.GetCacheTranslateEngineListData() + //生成id + lens := len(localData.Engine) + if lens == 0 { + engine.Id = 1 + } else { + engine.Id = localData.Engine[ lens - 1 ].Id + 1 + } + //追加数据 + localData.Engine = append(localData.Engine , engine) + //缓存数据 + Translate.SetCacheTranslateEngineListData(localData) + + //调用回调 + confirmCall() + + dlg.Accept() + }, + }, + PushButton{ + AssignTo: &cancelPB, + Text: "取消", + OnClicked: func() { dlg.Cancel() }, + }, + }, + }, + }, + }.Run( owner ) +} + +// 运行 新建[腾讯云]翻译引擎 Dialog +func(mw *MyMainWindow) RunTengxunyunTranslateEngineSetingDialog(owner walk.Form , confirmCall func()) { + var engine *TranslateEngineStruct + var dlg *walk.Dialog + var db *walk.DataBinder + var acceptPB, cancelPB *walk.PushButton + + engine = new(TranslateEngineStruct) + + //默认值 + engine.Supplier = TRANSLATE_SUPPLIER_TENGXUNYUN + + Dialog{ + AssignTo: &dlg, + Title: "新建翻译引擎(腾讯云)", + Font:Font{Family: "微软雅黑", PointSize: 9}, + DefaultButton: &acceptPB, + CancelButton: &cancelPB, + DataBinder: DataBinder{ + AssignTo: &db, + Name: "engine", + DataSource: engine, + ErrorPresenter: ToolTipErrorPresenter{}, + }, + MinSize: Size{500, 300}, + Layout: VBox{}, + Children: []Widget{ + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + Label{ + Text: "名称/别名:", + }, + LineEdit{ + Text: Bind("Alias"), + }, + + Label{ + Text: "SecretId:", + }, + LineEdit{ + Text: Bind("TengxunyunEngine.SecretId"), + }, + + Label{ + Text: "SecretKey:", + }, + LineEdit{ + Text: Bind("TengxunyunEngine.SecretKey"), + }, + + TextLabel{ + ColumnSpan: 2, + Row: 5, + Text: "\r\n说明:\r\n请填写 “腾讯云” 创建的子用户密钥", + TextColor:walk.RGB(190 , 190 , 190), + }, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + AssignTo: &acceptPB, + Text: "确定新增", + OnClicked: func() { + if err := db.Submit(); err != nil { + log.Print(err) + return + } + //参数验证 + if (engine.Alias == "") { + mw.NewErrormationTips("提示" , "请填写 名称/别名") + return + } + if (engine.TengxunyunEngine.SecretId == "") { + mw.NewErrormationTips("提示" , "请填写 SecretId") + return + } + if (engine.TengxunyunEngine.SecretKey == "") { + mw.NewErrormationTips("提示" , "请填写 SecretKey") + return + } + + //去空格 + engine.TengxunyunEngine.SecretId = strings.TrimSpace(engine.TengxunyunEngine.SecretId) + engine.TengxunyunEngine.SecretKey = strings.TrimSpace(engine.TengxunyunEngine.SecretKey) + + //获取缓存数据 + localData := Translate.GetCacheTranslateEngineListData() + //生成id + lens := len(localData.Engine) + if lens == 0 { + engine.Id = 1 + } else { + engine.Id = localData.Engine[ lens - 1 ].Id + 1 + } + //追加数据 + localData.Engine = append(localData.Engine , engine) + //缓存数据 + Translate.SetCacheTranslateEngineListData(localData) + + //调用回调 + confirmCall() + + dlg.Accept() + }, + }, + PushButton{ + AssignTo: &cancelPB, + Text: "取消", + OnClicked: func() { dlg.Cancel() }, + }, + }, + }, + }, + }.Run( owner ) +} + + + + +//运行 Oss对象存储配置 Dialog +func (mw *MyMainWindow) RunObjectStorageSetingDialog(owner walk.Form) { + var oss *AliyunOssCache + var dlg *walk.Dialog + var db *walk.DataBinder + var acceptPB, cancelPB *walk.PushButton + + oss = Oss.GetCacheAliyunOssData() //查询缓存数据 + + Dialog{ + AssignTo: &dlg, + Title: "OSS对象存储设置", + Font:Font{Family: "微软雅黑", PointSize: 9}, + DefaultButton: &acceptPB, + CancelButton: &cancelPB, + DataBinder: DataBinder{ + AssignTo: &db, + Name: "oss", + DataSource: oss, + ErrorPresenter: ToolTipErrorPresenter{}, + }, + MinSize: Size{500, 300}, + Layout: VBox{}, + Children: []Widget{ + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + Label{ + Text: "Endpoint:", + }, + LineEdit{ + Text: Bind("Endpoint"), + }, + + Label{ + Text: "AccessKeyId:", + }, + LineEdit{ + Text: Bind("AccessKeyId"), + }, + + Label{ + Text: "AccessKeySecret:", + }, + LineEdit{ + Text: Bind("AccessKeySecret"), + }, + + Label{ + Text: "BucketName:", + }, + LineEdit{ + Text: Bind("BucketName"), + }, + + Label{ + Text: "BucketDomain:", + }, + LineEdit{ + Text: Bind("BucketDomain"), + }, + + Label{ + ColumnSpan: 2, + Text: "说明:\r\n“OSS对象存储”目前使用的是阿里云服务,请填写相关的服务配置。", + TextColor:walk.RGB(190 , 190 , 190), + }, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + AssignTo: &acceptPB, + Text: "保存", + OnClicked: func() { + if err := db.Submit(); err != nil { + log.Fatal(err) + return + } + //参数验证 + if (oss.Endpoint == "") { + mw.NewInformationTips("提示" , "请填写 Endpoint") + return + } + if (oss.AccessKeyId == "") { + mw.NewInformationTips("提示" , "请填写 AccessKeyId") + return + } + if (oss.AccessKeySecret == "") { + mw.NewInformationTips("提示" , "请填写 AccessKeySecret") + return + } + if (oss.BucketName == "") { + mw.NewInformationTips("提示" , "请填写 BucketName") + return + } + if (oss.BucketDomain == "") { + mw.NewInformationTips("提示" , "请填写 BucketDomain") + return + } + + //去空格 + oss.Endpoint = strings.TrimSpace(oss.Endpoint) + oss.AccessKeyId = strings.TrimSpace(oss.AccessKeyId) + oss.AccessKeySecret = strings.TrimSpace(oss.AccessKeySecret) + oss.BucketName = strings.TrimSpace(oss.BucketName) + oss.BucketDomain = strings.TrimSpace(oss.BucketDomain) + + //设置缓存 + Oss.SetCacheAliyunOssData(oss) + + dlg.Accept() + }, + }, + PushButton{ + AssignTo: &cancelPB, + Text: "取消", + OnClicked: func() { dlg.Cancel() }, + }, + }, + }, + }, + }.Run( owner ) +} + + +//运行 语气词过滤设置 Dialog +func (mw *MyMainWindow) RunGlobalFilterSetingDialog (owner walk.Form , historyWords string , confirmCall func(words string)) { + var dlg *walk.Dialog + var db *walk.DataBinder + var acceptPB, cancelPB *walk.PushButton + + var tmpData = new(AppFilterSetings) + tmpData.GlobalFilter.Words = historyWords + + Dialog{ + AssignTo: &dlg, + Title: "全局语气词过滤设置", + Font:Font{Family: "微软雅黑", PointSize: 9}, + DefaultButton: &acceptPB, + CancelButton: &cancelPB, + DataBinder: DataBinder{ + AssignTo: &db, + Name: "filter", + DataSource: tmpData, + }, + MinSize: Size{500, 300}, + Layout: VBox{}, + Children: []Widget{ + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + Label{ + ColumnSpan: 1, + Text: "过滤语气词:", + }, + TextEdit{ + ColumnSpan: 1, + MinSize: Size{150, 80}, + Text: Bind("GlobalFilter.Words"), + VScroll: true, + }, + Label{ + ColumnSpan: 2, + Text: "说明:\r\n“过滤语气词” 支持设置多个,请保持每个词语都单独一行", + TextColor:walk.RGB(190 , 190 , 190), + }, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + AssignTo: &acceptPB, + Text: "保存", + OnClicked: func() { + if err := db.Submit(); err != nil { + log.Fatal(err) + return + } + confirmCall(tmpData.GlobalFilter.Words) + //参数验证 + dlg.Accept() + }, + }, + PushButton{ + AssignTo: &cancelPB, + Text: "取消", + OnClicked: func() { dlg.Cancel() }, + }, + }, + }, + }, + }.Run( owner ) +} + + + +type DefinedRuleTableRows struct { + Id int + Target string //目标规则 + Replace string //替换规则 + Way int //规则类型 +} +type DefinedRuleTableModel struct { + walk.SortedReflectTableModelBase + maxIndex int + items []*DefinedRuleTableRows +} +func NewDefinedRuleTableModel () *DefinedRuleTableModel { + t := new(DefinedRuleTableModel) + return t +} +func (m *DefinedRuleTableModel) Items() interface{} { + return m.items +} +func (m *DefinedRuleTableModel) AddRow (row *DefinedRuleTableRows) { + m.maxIndex++ + row.Id = m.maxIndex; + m.items = append(m.items , row) +} +func (m *DefinedRuleTableModel) BatchDelRow (indexs []int) { + id := make([]int , 0) + for row_i , row_v := range m.items { + for _ , op_v := range indexs { + if row_i == op_v { + id = append(id , row_v.Id) + } + } + } + for _ , vid := range id { + m.DelRow(vid) + } +} +func (m *DefinedRuleTableModel) GetRowIndex (index int) *DefinedRuleTableRows { + tmp := new(DefinedRuleTableRows) + for row_i , row_v := range m.items { + if row_i == index { + tmp = row_v + break + } + } + return tmp +} +func (m *DefinedRuleTableModel) DelRow (id int) { + t := len(m.items) + for row_i , row_v := range m.items { + if row_v.Id == id { + if row_i == 0 { + if t <= 1 { + m.items = make([]*DefinedRuleTableRows , 0) + } else { + m.items = m.items[row_i+1:] + } + } else if row_i+1 >= t { + m.items = m.items[:row_i] + } else { + m.items = append(m.items[:row_i] , m.items[row_i+1:]...) + } + break + } + } +} +func (tv *DefinedRuleTableModel) SetAndInitFilterRules (rules []*AppDefinedFilterRule) { + //初始化 + tv.maxIndex = 0 + tv.items = make([]*DefinedRuleTableRows , 0) + + for _ , v := range rules { + tv.maxIndex++ + tv.items = append(tv.items , &DefinedRuleTableRows{ + Id:tv.maxIndex, + Target:v.Target, + Replace:v.Replace, + Way:v.Way, + }) + } +} +func (tv *DefinedRuleTableModel) GetFilterRuleResult () []*AppDefinedFilterRule { + result := make([]*AppDefinedFilterRule , 0) + for _ , v := range tv.items { + result = append(result , &AppDefinedFilterRule{ + Target:v.Target, + Replace:v.Replace, + Way:v.Way, + }) + } + return result +} + + +//运行 自定义过滤设置 Dialog +func (mw *MyMainWindow) RunDefinedFilterSetingDialog (owner walk.Form , historyRule []*AppDefinedFilterRule , confirmCall func(rule []*AppDefinedFilterRule)) { + var dlg *walk.Dialog + var acceptPB, cancelPB *walk.PushButton + var tv *walk.TableView + + tableModel := NewDefinedRuleTableModel() + tableModel.SetAndInitFilterRules(historyRule) + + var currentIndexs []int = make([]int , 0) //选择的项 + + Dialog{ + AssignTo: &dlg, + Title: "自定义过滤设置", + Font:Font{Family: "微软雅黑", PointSize: 9}, + DefaultButton: &acceptPB, + CancelButton: &cancelPB, + MinSize: Size{600, 500}, + Layout: VBox{}, + Children: []Widget{ + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + PushButton{ + Text: "新增规则", + OnClicked: func() { + copyRow := new(DefinedRuleTableRows) + if len(currentIndexs) == 1 { + copyRow = tableModel.GetRowIndex(currentIndexs[0]) + } + + mw.RunNewDefinedFilterRuleDialog(mw , copyRow , func(rule *DefinedRuleTableRows) { + tableModel.AddRow(rule) + tv.SetModel(tableModel) + }) + }, + }, + PushButton{ + Text: "删除规则", + OnClicked: func() { + if len(currentIndexs) < 1 { + mw.NewErrormationTips("错误" , "请选择操作的对象") + return + } + tableModel.BatchDelRow(currentIndexs) + tv.SetModel(tableModel) + }, + }, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + TableView{ + Name:"tableView", + AssignTo: &tv, + AlternatingRowBG: true, + NotSortableByHeaderClick: true, + MultiSelection:true, + Columns: []TableViewColumn{ + {Title: "编号", DataMember: "Id" , Width:90}, + {Title: "类型", DataMember: "Way", Width:90 , FormatFunc: func(value interface{}) string { + switch v := value.(type) { + case int: + if v == FILTER_TYPE_STRING { + return "文本过滤" + } + if v == FILTER_TYPE_REGX { + return "正则过滤" + } + return "" + default: + return "" + } + }}, + {Title: "目标规则", DataMember: "Target", Width:165}, + {Title: "替换规则", DataMember: "Replace", Width:185}, + }, + Model: tableModel, + OnSelectedIndexesChanged: func() { + indexs := tv.SelectedIndexes() + if (len(indexs) > 0) { + currentIndexs = indexs + } else { + currentIndexs = []int{} + } + }, + }, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + AssignTo: &acceptPB, + Text: "保存", + OnClicked: func() { + confirmCall(tableModel.GetFilterRuleResult()) + + //参数验证 + dlg.Accept() + }, + }, + PushButton{ + AssignTo: &cancelPB, + Text: "取消", + OnClicked: func() { dlg.Cancel() }, + }, + }, + }, + }, + }.Run(owner) +} + +//新建自定义过滤规则 Dialog +func (mw *MyMainWindow) RunNewDefinedFilterRuleDialog (owner walk.Form , copyRows *DefinedRuleTableRows , confirmCall func(rule *DefinedRuleTableRows)) { + var dlg *walk.Dialog + var db *walk.DataBinder + var acceptPB, cancelPB *walk.PushButton + + var tmpData = new(DefinedRuleTableRows) + if copyRows.Id != 0 { + tmpData.Target = copyRows.Target + tmpData.Replace = copyRows.Replace + tmpData.Way = copyRows.Way + } else { + tmpData.Way = 1 //默认 + } + + Dialog{ + AssignTo: &dlg, + Title: "新增自定义过滤规则", + Font:Font{Family: "微软雅黑", PointSize: 9}, + DefaultButton: &acceptPB, + CancelButton: &cancelPB, + DataBinder: DataBinder{ + AssignTo: &db, + Name: "defined", + DataSource: tmpData, + }, + MinSize: Size{500, 300}, + Layout: VBox{}, + Children: []Widget{ + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + Label{ + ColumnSpan: 1, + Text: "目标规则:", + }, + LineEdit{ + ColumnSpan: 1, + MinSize: Size{Width:150}, + Text: Bind("Target"), + }, + Label{ + ColumnSpan: 1, + Text: "替换规则:", + }, + LineEdit{ + ColumnSpan: 1, + MinSize: Size{Width:150}, + Text: Bind("Replace"), + }, + Label{ + Text: "过滤类型:", + }, + ComboBox{ + Value: Bind("Way", SelRequired{}), + BindingMember: "Id", + DisplayMember: "Name", + Model: GetFilterTypeOptionsSelects(), + }, + + Label{ + ColumnSpan: 2, + Text: "说明:\r\n1.“目标规则” 填写查找的文本/正则, “替换规则” 填写替换的文本/正则\r\n2.过滤类型为正则时,“替换规则” 允许使用 $1...$9 进行反向引用", + TextColor:walk.RGB(190 , 190 , 190), + }, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + AssignTo: &acceptPB, + Text: "保存", + OnClicked: func() { + if err := db.Submit(); err != nil { + log.Fatal(err) + return + } + if strings.TrimSpace(tmpData.Target) == "" { + mw.NewErrormationTips("错误" , "必须填写目标规则噢") + return + } + if tmpData.Way == FILTER_TYPE_REGX { + //正则规则 + //校验规则 + _, e := regexp.Compile(tmpData.Target) + if e != nil { + mw.NewErrormationTips("错误" , "目标正则规则格式校验不通过,请检查是否正确") + return + } + } + + confirmCall(tmpData) + //参数验证 + dlg.Accept() + }, + }, + PushButton{ + AssignTo: &cancelPB, + Text: "取消", + OnClicked: func() { dlg.Cancel() }, + }, + }, + }, + }, + }.Run( owner ) +} + + + + +//打开 Github +func (mw *MyMainWindow) OpenAboutGithub() { + tool.OpenUrl("https://github.com/wxbool/video-srt-windows") +} + +//打开 Gitee +func (mw *MyMainWindow) OpenAboutGitee() { + tool.OpenUrl("https://gitee.com/641453620/video-srt-windows") +} + + +//支持的文件后缀校验 +func VaildateHandleFiles(files [] string , mediaExt bool , srtExt bool) ([]string , error) { + result := []string{} + allowExts := []string{} + mediaExts := []string{ + ".mp4",".mpeg",".mkv",".wmv",".avi",".m4v",".mov",".flv",".rmvb",".3gp",".f4v", + ".mp3",".wav",".aac",".wma",".flac",".m4a", + } + srtExts := []string{".srt"} + + if mediaExt { + allowExts = append(allowExts , mediaExts...) + } + if srtExt { + allowExts = append(allowExts , srtExts...) + } + + for _,f := range files { + f = tool.WinDir(f) + if thisFile, err := os.Stat(f); err != nil { + if os.IsNotExist(err) { + return result , errors.New("文件不存在:" + f) + } + return result , errors.New("文件校验不通过:" + f) + } else { + if thisFile.IsDir() { + return result , errors.New("不允许操作文件夹:" + f) + } + //校验视频格式后缀 + ext := path.Ext(f) + if !tool.InSliceString(strings.ToLower(ext) , allowExts) { + return result , errors.New("文件后缀不允许:" + f) + } + //允许加入 + result = append(result , f) + } + } + //数量限制 + if len(result) > 300 { + return result , errors.New("文件数量不允许超过 300 个") + } + return result , nil +} + + +//获取应用根目录 +func GetAppRootDir() string { + if rootDir , err := filepath.Abs(filepath.Dir(os.Args[0])); err != nil { + return "" + } else { + return tool.WinDir(rootDir) + } +} \ No newline at end of file diff --git a/gui_learn/app/ffmpeg/ffmpeg.go b/gui_learn/app/ffmpeg/ffmpeg.go new file mode 100644 index 0000000..a9646b2 --- /dev/null +++ b/gui_learn/app/ffmpeg/ffmpeg.go @@ -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) + } +} \ No newline at end of file diff --git a/gui_learn/app/log.go b/gui_learn/app/log.go new file mode 100644 index 0000000..02caed6 --- /dev/null +++ b/gui_learn/app/log.go @@ -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("") +} \ No newline at end of file diff --git a/gui_learn/app/parse/srt.go b/gui_learn/app/parse/srt.go new file mode 100644 index 0000000..237b3c1 --- /dev/null +++ b/gui_learn/app/parse/srt.go @@ -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 +} diff --git a/gui_learn/app/parse/tool.go b/gui_learn/app/parse/tool.go new file mode 100644 index 0000000..2260a1c --- /dev/null +++ b/gui_learn/app/parse/tool.go @@ -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]) +} \ No newline at end of file diff --git a/gui_learn/app/srt.go b/gui_learn/app/srt.go new file mode 100644 index 0000000..767a67a --- /dev/null +++ b/gui_learn/app/srt.go @@ -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("翻译失败!缺少翻译引擎!") + } +} \ No newline at end of file diff --git a/gui_learn/app/task.go b/gui_learn/app/task.go new file mode 100644 index 0000000..92804e0 --- /dev/null +++ b/gui_learn/app/task.go @@ -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 +} \ No newline at end of file diff --git a/gui_learn/app/task_tanslate.go b/gui_learn/app/task_tanslate.go new file mode 100644 index 0000000..22943b8 --- /dev/null +++ b/gui_learn/app/task_tanslate.go @@ -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 +} \ No newline at end of file diff --git a/gui_learn/app/tool/chinese_simple.go b/gui_learn/app/tool/chinese_simple.go new file mode 100644 index 0000000..5f0c66f --- /dev/null +++ b/gui_learn/app/tool/chinese_simple.go @@ -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 6 { + return false + } + regx := regexp.MustCompile(`^(\\|\{|\}|\[|\]|(|)|\(|\)|\*|/|~|<|>|_|\-|\+|=|&|%|\$|@|#|—|」|「|!|,|。|。|‍|、|?|;|:|‘|’|”|“|"|'|,|\.|\?|;|:|!|\s)+$`) + if regx.Match([]byte(s)) { + return true + } else { + return false + } +} \ No newline at end of file diff --git a/gui_learn/app/tool/tool.go b/gui_learn/app/tool/tool.go new file mode 100644 index 0000000..e6e6dbd --- /dev/null +++ b/gui_learn/app/tool/tool.go @@ -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 +} \ No newline at end of file diff --git a/gui_learn/app/translate/baidu.go b/gui_learn/app/translate/baidu.go new file mode 100644 index 0000000..18f6ee1 --- /dev/null +++ b/gui_learn/app/translate/baidu.go @@ -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 +} \ No newline at end of file diff --git a/gui_learn/app/translate/tengxunyun.go b/gui_learn/app/translate/tengxunyun.go new file mode 100644 index 0000000..9d938bd --- /dev/null +++ b/gui_learn/app/translate/tengxunyun.go @@ -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 + } +} \ No newline at end of file diff --git a/gui_learn/app/version.go b/gui_learn/app/version.go new file mode 100644 index 0000000..d4f619e --- /dev/null +++ b/gui_learn/app/version.go @@ -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 +} \ No newline at end of file diff --git a/gui_learn/app/video.go b/gui_learn/app/video.go new file mode 100644 index 0000000..4d1a195 --- /dev/null +++ b/gui_learn/app/video.go @@ -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) + } + } + } +} \ No newline at end of file diff --git a/gui_learn/build.bat b/gui_learn/build.bat new file mode 100644 index 0000000..cfe8b15 --- /dev/null +++ b/gui_learn/build.bat @@ -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 diff --git a/gui_learn/data/img/about.png b/gui_learn/data/img/about.png new file mode 100644 index 0000000000000000000000000000000000000000..3235487c896cc25c5fa1693121caedfde71c7a9c GIT binary patch literal 400 zcmV;B0dM|^P)QMwTaNRfu!7^Zre={fT7-i8(5+T=%#fkol3JFD<>PkxO7R1Kdp%`V#U9D8%rR6owR@W+y-`4GR^L14P=KCT~f~ zBUNnOy)TG}gK=jhVG=@GyDQ4c0lk6ciSK$Dy6UY0Aok()nW3KdhPDsp?Rptbb3lJ! z$4SOcUFwykG{|2q1HAwYZC*t|sU!CNjzv5ecSW8(32ib=Ohg zsP)zXN{?8yalbwFKoy{}jpEjy8;PDX@Qrk1g_R*Z@tCBTsa9@jm;kuSz|JOmn3t?9 u4?c#;C6Eb1zZfR07%Ya#`*xUqGucO9FV7L00_n>D0000JEhG)!709ZDW!% zs20u90>u5eJ9x3`267gVEgPcQJELJlpsPb(b_UTOxW5>13Xf_Ck;1jcEfY~gq!}$i zm{k~TOSLDm6E}Nz{)OyuY4(gj3kv`InGRuLdX`w&nGuz}Vru{Z002ovPDHLkV1ji) Bs@?zq literal 0 HcmV?d00001 diff --git a/gui_learn/data/img/app.ico b/gui_learn/data/img/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..24f04c68ad6d9918015eb06e4f24acb9b19a5306 GIT binary patch literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x%b1w7uEGvU#uoP7lFf#>Tp6tFqy z@#Y){Y~sjbAT{Xfjx}jT9c|Q%LC43NHC>SPg4o#fA8pe5cC<{!#6a`hos0z^4~v4k(QP-FN^CzVK*M_PqD&3qaYLYT*BW28IvJ h3=AL|gb&mMu|Jdsv61lq#|#YLe*^J%pdNmRdH}xEW>^3K literal 0 HcmV?d00001 diff --git a/gui_learn/data/img/gitee.png b/gui_learn/data/img/gitee.png new file mode 100644 index 0000000000000000000000000000000000000000..8f2f25e81a9f5d8a83620ffccf28bed6c325143f GIT binary patch literal 297 zcmV+^0oMMBP){{HpUIy04Kl+%ni5!PQpqjHaGy0U_bVB$qXhzhVj>%PP*%T z^{TpyxU8v_FCLX@3Q#4Oxt>4U1>!RZ&{m-ig?N@UD~aBiNu$nz=1>pIaZwQdi)cB&gNY}m7l=~_^}Rj*7iVl zUb*=vAS}UsbU+tGRNC83412@W=n8%tFtO|w6}vG2@dPIJ85LB~5MDGZB8yuGuUdY8 vZ_xM#FmyJ{`ps8PWI$vAVb9qugBijJe?)ri&3z?w00000NkvXXu0mjfMEQ4P literal 0 HcmV?d00001 diff --git a/gui_learn/data/img/github.png b/gui_learn/data/img/github.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad2aa4357adb2f89eaa24963c89da512ab7ab14 GIT binary patch literal 452 zcmV;#0XzPQP)<(z6l;!?W5X7BmnnAb&bfFEE$wf z6~WNv2+5DY?@T#a+6lwCaGl`}riS>=@|!LdDWc(m&T|uKFkp?GoWUfQrjq*WXy06EEBZ8fx`} zEd5#ieX^lll^Rt9?nJ%xU`OPD2XY6b4VM;0D;Q~Lr=VAew!n;0B6>;G=zL(@ zwJLKd&-9XZAn+Z`9MLz7-f-JleG_wJ=fKr=^jAKM*iBgdh42ff+oA(ANvcmf0Ce*-M$3jAkaIM4c*w{SwB;0KsI76TqOO!1d*z%Y38V~79${~0j+ zXEd1VFPA`;0U%BPm>54W{`pq`GX(Aeg@*kc*n;ChgX0g7cn-(~|NiweGBSQ(`2XMF z&p$T98G(`y;09yZH`mDaZv*1}bU)8Giag1Q;(cGBGmYG~i0D6Oo2{ypSOeT)yH;Xmc zWG03?O#l81;55KVT?p=cm=_!kWD6MoGo?aYz;KX}f#E$#foN|e8^-*XK@02xM)qe6 zjKAfv7;r4=|NsBY7-@lt>G}0q%RL065vBkX$DsJdmZ*@`V7LG-3CT?Q_zVDf&A0ck(fOLYX0DQwJ5vBu|U^++#7$u6iEp(7{5-K1!KsXt+CJvzd5HpvN zo_4-{J3FJ;xlYk|zXt12D>QE{kM7lX;{>=cG^tZBEC3$Wb9uTdoTzE`vCwn7X(v9n zpu0=6h0c848n7aIDFchYb9rn-s~oQZqcEzR9Q2aP`Bp&LSiL!n+ff*8?af|5&*cGF z2AU&Y3ghM>jAlQr{Rt434#TUU@ABASAV`K`lvR)#fN^4w2&MG^;?Cb@#R(3qZWfepa%%^9)KoAVX=uR=a#7APLpaz0wf{DAmnP4D@ zpnE26kIsx7Txw*XsopMK(yKGh`ohO#RbBtrQ&oQ>eDTAV^l8`6BC$mx*!7Bh-WK5+ zf?g}j)Y;SeIzU?ZlL)wk08$f%MjQ#!A{a-2q3Vqu_jfw4qkIgo-Cz_5mX)-cU+aqJ z!j?v@cNoyviR}Dq>K*}3SW-ovO^S2J#~milXENu>e3uBe%hDQ-0k#{Kkmw>xngb)I zN~N_{)X;F5gmgLQ8AAO>NegR#V2glE5D87Odvr7lS|8MIW~{%^+dFTd5y%$7o|4w! z10Xz6l7Ba~?qO-a(3c%}2@stnFTDpi{)#H5etiY-Qx#LIA8eu4c02=OXnT3?MbmzB zh$7e~#BZv=vyW>O!IqNN$a~gkCsrNpaD)jX>G(dcKkVp*g*g}0-Ewzww(Z>K8ztt1~!JK-!M%^~1K hS@QKaAJb=E_8pH>@DUdzcLo3e002ovPDHLkV1jnO+SvdA literal 0 HcmV?d00001 diff --git a/gui_learn/data/img/new.png b/gui_learn/data/img/new.png new file mode 100644 index 0000000000000000000000000000000000000000..6078e318c61085fdb3dfaa9d9f933e04aff84f57 GIT binary patch literal 292 zcmV+<0o(qGP)1m%008W# z_HKiKXp8*j@k(AzZYO{#FrCyPodNHVUH}V;-4LMb4}e0u{{xI~stNS!Kt*oCLFV=H zK{76)O|Bq=dqy>%mbR9@0o@W^vRi6LX{n`4Ay&ppEKE!Y zbIgGJMZ%ciwD+EG?m2fxu;r00^_T!U{qC6n`^&K19d#9!!=dYfDIo6>#3Kb$0Z|g+ zI}2W@%i&-XWDcO1BYn{9+2-^)^}j|Y6X0;2n82wHqPDHA1G|ydt>pZQF zX)^17b&bTWQn+^kXjrA-!}?zU`blc2UW9Z$-N_0?WwWsV=QFv+-c0ERaB;(YtN?c1=Z1x#1+tQaOBljZXLerhC(l!W9nY`kfV*{7 zz320X^V;4hib(;A-lR|P)Ho$P!ctp-Rj{b*C_&K8%qcg11(ZBrUygAgp1s!hhh}eN z5kZ&aHiY|W-L9?x3b!wnt;OOgyq&83)LL@=E{IFxpE`D2d2FQgOCyl6&JYS65iA%x z`3^Cl==p5|tt@R_0w-^dJutBez@$g;A`3u3XXe=L6rkk#0}#hq01&RGb-S4ce0E?g zptlwxH3<%*PJBynCv%byOCc=nua$#(0LLqUaD*w7e6S8+225ZhnvwtiOqm`r5%d8W Wj&~8^gCf2F0000MjW6IfQmqi5oLMc>hoU!LR&#C8gBfq+n6=NN|8zdY(Gn+E|f*P-Ve zTYxs_9SHy`D401wgC_u{qh-N(2t9pc7$nbS4#L?%L&?|%o?dk+tfd11{*Dg=h@v0B ztAfgHjQzU3k+xl@Ch>ceB}(9^WA&#|zR%uP%p8GGNzV2$G3G=RO+KcBjQlG81UX2H U5lYYeI{*Lx07*qoM6N<$g7LKxPEFI8+@R~RSb`U#=9fS^i$8nv#3lKuUabGTT zv-{ot|85*oY=9@O#~gW)z7&TP`bROKA4obu@(jlSjaJC?%j)_7E6@?Rha|!rP^2MRmi#}a9 i(RA8$2ABU<{rm+)lc%Q`T64ny0000gcS+{%-)43bh3@`gW*tcfa4YGqF*yLFl!9U>X&%kdm{KYX za?T+O6=W;v%iR}oR{*MEn=w5zv&tY|E~m+qv9ooih`@r?C&JqUmWFOL(B$%wh!B1FVkPQ`v)z5-4UcP RYIgtt002ovPDHLkV1nbQnaThF literal 0 HcmV?d00001 diff --git a/gui_learn/data/img/voice.png b/gui_learn/data/img/voice.png new file mode 100644 index 0000000000000000000000000000000000000000..e6bc5c9017984643806df69e80bc703fd6e4620a GIT binary patch literal 551 zcmV+?0@(eDP)rJ>78J9;eEq{v*mCXZoITfNa2nvD zqu^j|EVQmBM^zA;*BU3?dU@jf`-mM^9vpzlqqx9bTd5#AK&n4BKpLBVh6QV%{9Lj5 zdBx7F_aAXofnWGrC({l8$xjRy}viaj(G z6#la^PGJ21e*&`YAUSv}I4CFxfCLUac<=$Hf#E*`#5P8T3orwb)u9*wO1?}?j2jpk z{wJW@0&_ChbqtIP{{8zO087>==>TN&KNiLfjEs!u|Nr}U;QxPyi(p$A8T1*Mm>mB9 z|8L0j=YIgqYbY*24j4CGg#d>COa=`98DgMbSi->g@4~Lj4>l0YOehBFD1j3Ua>60X p08o- + + + + + + + + + + PerMonitorV2, PerMonitor + True + + + \ No newline at end of file diff --git a/gui_learn/rsrc.syso b/gui_learn/rsrc.syso new file mode 100644 index 0000000000000000000000000000000000000000..3ad9dffbde63a17f23a5c68fa11ee13d64926527 GIT binary patch literal 2381 zcmcguOHUI~6uuPBd9~K30kwHE~hTnRd#6)2fN_gs=B?zVo_=$vqm05R~2P z2r17RBQZxfnzvj2dDBct5~f>02c-ef@BQJ*mj-WtJ0VjWVe2%ETVT8lqpwjOBV-Ls z`a|~DH4<`fr82A{o?W|!{?ibB%;S0u{nv2z5s(32z6`G=WV643F$L=!CRq{4iL?MD zK*aln$$yZAym_TqFdzH|vmb6{q_A)nX3YMu{oo$ztk!!x9X&I5vGwd~Jh{}mKgh*- z7=La$`gLx4M`<-4O-Jv~-_~2^^JX{VW5JZ_@n7AvR@m_P1tW@e@i1cy*W;H!zPx=_ zdVckQX9xLj@1?ga&YTzj)V@!S@7WamLtJkNV>%p_&ydEF&x9f4y4)zGdI2(eO#e#9|^DP_yt^b*nN`x%IcLPVn>;=H_ z%82s-N(+R1`vLd{oK0nY{JKO)3&ez98T>-6A<4e;mMKk8N9dgO$cb27mMGJBN@wFe z@`=%dZF^)%xGGDjCTFxq&QT%v^=??7RE40{n3TYgDOtCn|WjkD>LU3om(K5PAH8<-( zL{V8{ss)n~Zqb8TX5@~+@(^{XEV(%w4kmTBt6f%nS1D>!25z)mWycG^-*pEni3JYB zBMcg^gfQeM9y_J86rU8aL2g-`wGDFSI@}cUa$a4SRsFo|?3k%*M`&)88x*dmt8;JX zSXW1)qeIiuiBwvaOqGpi)p4&3eu7@KO09g7)st%eN!7N&kLp4X{NZ)(k^|*QLV+>a z5$d|oHzIIUF0HK(OsWoLaC^S}jO*Inic;yAShZtf&@sx$&|#%Lp6F64RrC$Zy~ETQ z;!JnBbFzJ>w7i0>0#0fL@E6YGoxeICb+UBXQNT%339mp>2~-yJVeL1U{}5>+{u85# T;Nt