Commit f8dc8db5 authored by 翟艳秋(20软)'s avatar 翟艳秋(20软)

add doc and fix the bugs

parent f90ee73d
......@@ -7,3 +7,4 @@ chineseocr_usage.py
easyOCR_usage.py
dist
build
log
from PyQt5.QtCore import Qt, QRect, QPointF
from PyQt5.QtGui import QPainter, QColor
from PyQt5.QtWidgets import QSlider, QWidget, QVBoxLayout, QProxyStyle, QStyle, QStyleOptionSlider
class SliderStyle(QProxyStyle):
def subControlRect(self, control, option, subControl, widget=None):
rect = super(SliderStyle, self).subControlRect(
control, option, subControl, widget)
if subControl == QStyle.SC_SliderHandle:
if option.orientation == Qt.Horizontal:
# 高度1/3
radius = int(widget.height() / 3)
offset = int(radius / 3)
if option.state & QStyle.State_MouseOver:
x = min(rect.x() - offset, widget.width() - radius)
x = x if x >= 0 else 0
else:
radius = offset
x = min(rect.x(), widget.width() - radius)
rect = QRect(x, int((rect.height() - radius) / 2),
radius, radius)
else:
# 宽度1/3
radius = int(widget.width() / 3)
offset = int(radius / 3)
if option.state & QStyle.State_MouseOver:
y = min(rect.y() - offset, widget.height() - radius)
y = y if y >= 0 else 0
else:
radius = offset
y = min(rect.y(), widget.height() - radius)
rect = QRect(int((rect.width() - radius) / 2),
y, radius, radius)
return rect
return rect
class PaintQSlider(QSlider):
def __init__(self, *args, **kwargs):
super(PaintQSlider, self).__init__(*args, **kwargs)
# 设置代理样式,主要用于计算和解决鼠标点击区域
self.setStyle(SliderStyle())
def paintEvent(self, _):
option = QStyleOptionSlider()
self.initStyleOption(option)
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 中间圆圈的位置
rect = self.style().subControlRect(
QStyle.CC_Slider, option, QStyle.SC_SliderHandle, self)
# 画中间白色线条
painter.setPen(Qt.white)
painter.setBrush(Qt.white)
if self.orientation() == Qt.Horizontal:
y = self.height() / 2
painter.drawLine(QPointF(0, y), QPointF(self.width(), y))
else:
x = self.width() / 2
painter.drawLine(QPointF(x, 0), QPointF(x, self.height()))
# 画圆
painter.setPen(Qt.NoPen)
if option.state & QStyle.State_MouseOver: # 双重圆
# 半透明大圆
r = rect.height() / 2
painter.setBrush(QColor(255, 255, 255, 100))
painter.drawRoundedRect(rect, r, r)
# 实心小圆(上下左右偏移4)
rect = rect.adjusted(4, 4, -4, -4)
r = rect.height() / 2
painter.setBrush(QColor(255, 255, 255, 255))
painter.drawRoundedRect(rect, r, r)
# 绘制文字
painter.setPen(Qt.white)
if self.orientation() == Qt.Horizontal: # 在上方绘制文字
x, y = rect.x(), rect.y() - rect.height() - 2
else: # 在左侧绘制文字
x, y = rect.x() - rect.width() - 2, rect.y()
painter.drawText(
x, y, rect.width(), rect.height(),
Qt.AlignCenter, str(self.value())
)
else: # 实心圆
r = rect.height() / 2
painter.setBrush(Qt.white)
painter.drawRoundedRect(rect, r, r)
# class Window(QWidget):
# def __init__(self, *args, **kwargs):
# super(Window, self).__init__(*args, **kwargs)
# self.setAttribute(Qt.WA_StyledBackground, True)
# layout = QVBoxLayout(self)
# layout.addWidget(PaintQSlider(Qt.Vertical, self, minimumWidth=90))
# layout.addWidget(PaintQSlider(Qt.Horizontal, self, minimumHeight=90))
# if __name__ == '__main__':
# import sys
# from PyQt5.QtWidgets import QApplication
# app = QApplication(sys.argv)
# w = Window()
# w.setStyleSheet('QWidget {background: gray;}')
# w.show()
# sys.exit(app.exec_())
\ No newline at end of file
# accessibility_movie_2
二期的无障碍电影制作工具,主要包含旁白区间检测和旁白及字幕导出两个功能; 二期主要提升用户体验、产品功能与易用性,设计成熟的操作界面进行人机交互,另外为了方便用户使用,使用pyinstaller进行打包。
注意要自行下载mp4的解码器,不然无法播放视频
\ No newline at end of file
import sys
import os
from PyQt5.QtCore import *;
from PyQt5.QtGui import *;
from PyQt5.QtWidgets import *;
from assemble_dialog_ui import Ui_Dialog
from utils import validate_and_get_filepath, replace_path_suffix
"""
视频合成 技术实现步骤:
1、dialog发送点击信号(把所有信息都带上,放到一个list里,不然太长了)
2、synthesis绑定该信号,并触发自身槽函数
3、开启多线程,进行视频合成
4、注意,dialog组件和synthesis组件都绑定到mainwindow上,信号就是方便他们交互的。
"""
class Assemble_Dialog(QDialog, Ui_Dialog):
# 开始合成信号,传参分别是video_path,audio_dir, sheet_path,speed_info, caption_path, speaker_info
# 注意这里打包成一个list,在worker端进行解参数,避免传参过多
start_assemble_signal = pyqtSignal(list)
def __init__(self, projectContext):
super(Assemble_Dialog, self).__init__()
self.projectContext = projectContext
self.setupUi(self)
self.setWindowTitle("合成")
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("开始合成")
self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
self.pushButton.clicked.connect(self.openFile)
self.pushButton_2.clicked.connect(self.openTableFile)
# 注意,不能直接绑定到buttonBox上。
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self.start_assemble)
self.lineEdit_3.setText(projectContext.speaker_info)
self.lineEdit_4.setText(projectContext.speaker_speed)
# self.show.connect(self.init_self_slot)
def init_self(self):
# print("self.projectContext.speaker_info", self.projectContext.speaker_info)
self.lineEdit.setText(self.projectContext.video_path)
self.lineEdit_2.setText(self.projectContext.excel_path)
self.lineEdit_3.setText(self.projectContext.speaker_info)
self.lineEdit_4.setText(self.projectContext.speaker_speed)
def openFile(self):
file_info = QFileDialog.getOpenFileNames(self, '选择视频', os.getcwd(), "All Files(*);;Text Files(*.txt)")
file_name, ok = validate_and_get_filepath(file_info)
if ok and file_name != "":
self.lineEdit.setText(file_name)
def openTableFile(self):
now_path = os.path.join(os.getcwd(), self.lineEdit.text())
#todo: 方法提取出来放到utils里
now_path = now_path.replace(os.path.splitext(now_path)[-1], ".xlsx")
print("path:", now_path)
file_info = QFileDialog.getOpenFileNames(self, '选择文件路径', now_path, "'xlsx(*.xlsx)'")
print(file_info)
file_name, ok = validate_and_get_filepath(file_info)
print(file_name, ok)
if ok and file_name != "":
self.lineEdit_2.setText(file_name)
def start_assemble(self):
print("start_assemble")
video_path = self.lineEdit.text()
# 默认 输出的音频是工程目录+/output
audio_dir = os.path.join(self.projectContext.project_base_dir, "output")
sheet_path = self.lineEdit_2.text()
speaker_info = self.lineEdit_3.text()
speed_info = self.lineEdit_4.text()
#todo 后续变成常量存起来,或者做成配置
# caption_path = replace_path_suffix(self.lineEdit.text(), ".srt")
caption_path = os.path.join(audio_dir, os.path.basename(video_path).split('.')[0] + ".srt")
print("video_path: ",video_path)
print("audio_dir: ",audio_dir)
print("sheet_path: ",sheet_path)
print("speed_info: ",speed_info)
print("caption_path: ",caption_path)
print("speaker_info: ",speaker_info)
self.start_assemble_signal.emit([video_path, audio_dir, sheet_path,speed_info, caption_path, speaker_info])
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("./res/images/eagle_2.ico"))
dialog = Assemble_Dialog()
dialog.show()
"""旁白音频合成对话框
技术实现步骤:
1. dialog发送点击信号(把所有信息都带上,放到一个list里,不然太长了)
2. synthesis绑定该信号,并触发自身槽函数
3. 开启多线程,进行视频合成
4. 注意,dialog组件和synthesis组件都绑定到mainwindow上,信号就是方便他们交互的。
"""
import sys
import os
from PyQt5.QtCore import *;
from PyQt5.QtGui import *;
from PyQt5.QtWidgets import *;
from assemble_dialog_ui import Ui_Dialog
from utils import validate_and_get_filepath, replace_path_suffix
class Assemble_Dialog(QDialog, Ui_Dialog):
# 开始合成信号,传参分别是video_path,audio_dir, sheet_path,speed_info, caption_path, speaker_info
# 注意这里打包成一个list,在worker端进行解参数,避免传参过多
start_assemble_signal = pyqtSignal(list)
def __init__(self, projectContext):
super(Assemble_Dialog, self).__init__()
self.projectContext = projectContext
self.setupUi(self)
self.setWindowTitle("合成")
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("开始合成")
self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
self.pushButton.clicked.connect(self.open_file)
self.pushButton_2.clicked.connect(self.open_table_file)
# 注意,不能直接绑定到buttonBox上。
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self.start_assemble)
self.lineEdit_3.setText(projectContext.speaker_info)
self.lineEdit_4.setText(projectContext.speaker_speed)
# self.show.connect(self.init_self_slot)
def init_self(self):
"""使用工程配置信息初始化语音合成界面
"""
# print("self.projectContext.speaker_info", self.projectContext.speaker_info)
self.lineEdit.setText(self.projectContext.video_path)
self.lineEdit_2.setText(self.projectContext.excel_path)
self.lineEdit_3.setText(self.projectContext.speaker_info)
self.lineEdit_4.setText(self.projectContext.speaker_speed)
def open_file(self):
"""选择视频文件
"""
file_info = QFileDialog.getOpenFileNames(self, '选择视频', os.getcwd(), "All Files(*);;Text Files(*.txt)")
file_name, ok = validate_and_get_filepath(file_info)
if ok and file_name != "":
self.lineEdit.setText(file_name)
def open_table_file(self):
"""选择旁白表格文件
"""
now_path = os.path.join(os.getcwd(), self.lineEdit.text())
#todo: 方法提取出来放到utils里
now_path = now_path.replace(os.path.splitext(now_path)[-1], ".xlsx")
print("path:", now_path)
file_info = QFileDialog.getOpenFileNames(self, '选择文件路径', now_path, "'xlsx(*.xlsx)'")
print(file_info)
file_name, ok = validate_and_get_filepath(file_info)
print(file_name, ok)
if ok and file_name != "":
self.lineEdit_2.setText(file_name)
def start_assemble(self):
"""开始语音合成
读取当前界面中的用户输入,并通过信号发送到主界面调用旁白音频合成算法
"""
print("start_assemble")
video_path = self.lineEdit.text()
# 默认 输出的音频是工程目录+/output
audio_dir = os.path.join(self.projectContext.project_base_dir, "output")
sheet_path = self.lineEdit_2.text()
speaker_info = self.lineEdit_3.text()
speed_info = self.lineEdit_4.text()
#todo 后续变成常量存起来,或者做成配置
# caption_path = replace_path_suffix(self.lineEdit.text(), ".srt")
caption_path = os.path.join(audio_dir, os.path.basename(video_path).split('.')[0] + ".srt")
print("video_path: ",video_path)
print("audio_dir: ",audio_dir)
print("sheet_path: ",sheet_path)
print("speed_info: ",speed_info)
print("caption_path: ",caption_path)
print("speaker_info: ",speaker_info)
self.start_assemble_signal.emit([video_path, audio_dir, sheet_path,speed_info, caption_path, speaker_info])
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("./res/images/eagle_2.ico"))
dialog = Assemble_Dialog()
dialog.show()
sys.exit(app.exec_())
\ No newline at end of file
"""常用的一些常量
类型如下:
- Content类是界面中字幕+旁白表格,标注常用列的编号及表格对应名称
- Aside类是界面中的旁白表格,标注常用列的编号、表格对应名称和语速选项
- Subtitle类是界面中的字幕表格,标注时间戳对应列编号和表格对应名称
- Pathes类提供常用资源的路径,如说话人信息文件
"""
import os
......@@ -29,8 +37,6 @@ class Subtitle:
TimeFormatColumns = [0, 1]
dir_path = os.path.dirname(os.path.abspath(__file__))
class Pathes:
dir_path = os.path.dirname(os.path.abspath(__file__))
speaker_conf_path = os.path.join(dir_path, "res/speakers.json")
"""新建工程界面相关响应
新建工程界面相关响应如下:
- chooese_root: 选择存放工程的文件夹的函数
- create_project: 点击“确认”时调用,用于确认输入信息正确性,从而正确地新建工程
- close_dialog: 点击“取消”时调用,用于关闭对话框
"""
import os
from PyQt5.QtCore import *;
from PyQt5.QtGui import *;
......@@ -17,12 +26,19 @@ class Create_Dialog(QDialog, Ui_Dialog):
self.cancel.clicked.connect(self.close_dialog)
def choose_root(self):
"""选择存放新工程的文件夹
"""
root_info = QFileDialog.getExistingDirectory(self, "选择工程文件夹", os.getcwd())
if len(self.root_input.text()) != 0 and len(root_info) == 0:
return
self.root_input.setText(root_info)
def create_project(self):
"""创建新的工程
读取用户输入,进行合理性判断,存在错误时弹窗提示,否则在该路径下创建以工程名命名的文件夹,并通过信号传递工程路径,对主界面进行更新,然后自动关闭该界面。
"""
self.project_name = self.name_input.text()
self.dir_path = self.root_input.text()
if os.path.exists(self.dir_path) and len(self.project_name) > 0:
......@@ -40,4 +56,7 @@ class Create_Dialog(QDialog, Ui_Dialog):
self.prompt_dialog.show_with_msg("请输入合法文件夹路径")
def close_dialog(self):
"""关闭窗口
"""
self.close()
\ No newline at end of file
import cv2
if __name__ == '__main__':
video_path = 'D:/mystudy/Eagle/accessibility_movie_1/test37second.mp4'
video = cv2.VideoCapture(video_path)
fps = video.get(cv2.CAP_PROP_FPS)
frame_cnt = video.get(cv2.CAP_PROP_FRAME_COUNT)
duration = frame_cnt / fps
print("fps:", fps, type(fps))
print("frame_cnt:", frame_cnt, type(frame_cnt))
print("duration:", duration, type(duration))
\ No newline at end of file
"""旁白区间检测界面相关响应
旁白区间检测界面相关响应如下:
- init_self: 初始化当前界面
- open_file: 选择待处理的视频
- open_table_file: 选择字幕和旁白输出的表格在本地的存放位置
- start_detect: 确认是否开始旁白区间检测
"""
import sys
import os
from PyQt5.QtCore import *;
......@@ -19,23 +29,34 @@ class Detect_Dialog(QDialog, Ui_Dialog):
self.setWindowTitle("检测")
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("开始检测")
self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
self.pushButton.clicked.connect(self.openFile)
self.pushButton_2.clicked.connect(self.openTableFile)
self.pushButton.clicked.connect(self.open_file)
self.pushButton_2.clicked.connect(self.open_table_file)
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self.start_detect)
self.prompt_dialog = Prompt_Dialog()
def init_self(self):
"""根据工程配置信息初始化界面
"""
self.lineEdit.setText(self.projectContext.video_path)
self.lineEdit_2.setText(self.projectContext.excel_path)
def openFile(self):
def open_file(self):
"""选择待处理的视频
根据工程当前选定的视频为起始点打开一个窗口供用户进行视频选择,在该窗口中只显示mp4、rmvb、mkv和avi格式的视频文件,避免用户选择到非视频文件。
"""
root_dir = os.getcwd() if self.projectContext.video_path is None else os.path.dirname(self.projectContext.video_path)
file_info = QFileDialog.getOpenFileNames(self, '选择视频', root_dir, "Video Files(*.mp4 *.rmvb *mkv *avi)")
file_name, ok = validate_and_get_filepath(file_info)
if ok and file_name != "":
self.lineEdit.setText(file_name)
def openTableFile(self):
def open_table_file(self):
"""选择旁白和字幕输出表格的路径
以视频路径为基础构造表格路径,以此为基点打开一个选择文件路径的窗口,在该窗口中默认保存文件后缀为xlsx,在确认文件路径正确后,在表格中更新对应文本框中的内容。
"""
now_path = os.path.join(os.getcwd(), self.lineEdit.text())
now_path = now_path.replace(os.path.splitext(now_path)[-1], ".xlsx")
print("path:", now_path)
......@@ -47,12 +68,12 @@ class Detect_Dialog(QDialog, Ui_Dialog):
self.lineEdit_2.setText(file_name)
def start_detect(self):
# 发出一个信号,开始检测了
# 版本1.0:(当前版本)
# 让主窗口接受,新起进程检测并和主线程交互
# 版本2.0:
# 在主窗口启动时,就启动一个QThread,专门接收该信号并进行检测,
# 发出该信号后,由QThread和主窗口同时接收,然后让他俩通过信号交互即可。
"""开始检测
对视频和表格路径判空后,将视频和表格路径通过信号传递给主界面,从而在主界面中新起线程启动旁白区间检测
[todo] 方法2: 在主窗口启动时,就启动一个QThread,专门接收该信号并进行检测,发出该信号后,由QThread和主窗口同时接收,然后让他俩通过信号交互即可。
"""
if self.lineEdit.text()!="" and self.lineEdit_2.text()!="":
self.start_detect_signal.emit(self.lineEdit.text(), self.lineEdit_2.text())
else:
......
......@@ -34,9 +34,11 @@ from main_window import MainWindow, Element
up_b, down_b = 0, 0
# 初始化ocr工具
ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False, use_gpu=False)
# ocr = EasyOCR()
# ocr = ChineseOCR()
paddle_dir = "res/.paddleocr/2.3.0.1/ocr/"
cur_cls_model_dir = paddle_dir + "cls/ch_ppocr_mobile_v2.0_cls_infer"
cur_det_model_dir = paddle_dir + "det/ch/ch_PP-OCRv2_det_infer"
cur_rec_model_dir = paddle_dir + "rec/ch/ch_PP-OCRv2_rec_infer"
ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False, use_gpu=False, cls_model_dir=cur_cls_model_dir, det_model_dir=cur_det_model_dir, rec_model_dir=cur_rec_model_dir)
# 正常语速为4字/秒
normal_speed = 4
......@@ -181,14 +183,14 @@ def normalize(text: str) -> str:
return text
def detect_subtitle(img: np.ndarray) -> Union[str, None]:
def detect_subtitle(img: np.ndarray) -> Tuple[Union[str, None], float]:
"""检测当前画面得到字幕信息
Args:
img (np.ndarray): 当前画面
Returns:
Union[str, None]: 字幕信息(没有字幕时返回None)
Tuple[Union[str, None]]: 字幕信息(没有字幕时返回None)和置信度
"""
subTitle = ''
height = down_b - up_b
......@@ -214,16 +216,18 @@ def detect_subtitle(img: np.ndarray) -> Union[str, None]:
gradient = np.arctan(abs((rect[1][1] - rect[0][1]) / (rect[1][0] - rect[0][0])))
# log.append("文本:{},置信度:{},中心点:{},斜率:{},字体大小:{}".format(txt, confidence, mid / img.shape[1], gradient,
# font_size)) 置信度>0.7 & 斜率<0.1 & 字幕偏移量<=25 & 字幕中心在画面宽的0.4-0.6之间
# print("文本:{},置信度:{},中心点:{},斜率:{},字体大小:{}".format(txt, confidence, mid / img.shape[1], gradient, font_size))
# print("差距:{}".format(abs(rect[0][1] - 30) + abs(img.shape[0] - rect[2][1] - 30)))
print("文本:{},置信度:{},中心点:{},斜率:{},字体大小:{}".format(txt, confidence, mid / img.shape[1], gradient, font_size))
print("字体大小差距: {}", format(height - font_size))
print("高度中心:{}".format((rect[0][1] + rect[1][1])/2/img.shape[0]))
conf_thred1 = 0.7
conf_thred2 = 0.85
# conf_thred1 = 0.1
# conf_thred2 = 0.4
# conf_thred1 = 0.5
# conf_thred2 = 0.7
if confidence > conf_thred1 and gradient < 0.1 and 0.4 < mid / img.shape[1] < 0.6 and \
abs(rect[0][1] - 30) + abs(img.shape[0] - rect[2][1] - 30) <= font_size - 10:
if (rect[0][1] + rect[1][1])/2/img.shape[0] > 0.5 or (rect[0][1] + rect[1][1])/2/img.shape[0] <= 0.1:
continue
if confidence > conf_thred1 and gradient < 0.1 and 0.4 < mid / img.shape[1] < 0.6:
subTitle += txt
conf = max(conf,confidence)
# possible_txt.append([txt, mid/img.shape[1]])
......
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
assemble\_dialog module
=======================
.. automodule:: assemble_dialog
:members:
:undoc-members:
:show-inheritance:
assemble\_dialog\_ui module
===========================
.. automodule:: assemble_dialog_ui
:members:
:undoc-members:
:show-inheritance:
import sys
from os.path import abspath, dirname
sys.path.insert(0, dirname(dirname(dirname(abspath(__file__)))))
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = '无障碍电影制作系统'
copyright = '2023, 翟艳秋'
author = '翟艳秋'
# The full version, including alpha/beta/rc tags
release = 'v2.1.0'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.apidoc',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
'sphinx.ext.doctest',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.mathjax',
'rst2pdf.pdfbuilder',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'zh_CN'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
htmlhelp_basename = 'PYTHON doc'
# -- Options for LATEX output -------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
'preamble': '''
\\hypersetup{unicode=true}
\\usepackage{CJKutf8}
\\AtBeginDocument{\\begin{CJK}{UTF8}{gbsn}}
\\AtEndDocument{\\end{CJK}}
''',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Accessibility.tex', 'PYTHON Documentation',
'xxxx', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'PYTHON', 'PYTHON ',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'PYTHON', 'PYTHON',
author, 'PYTHON', 'One line description of project.',
'Miscellaneous'),
]
# -- Extension configuration -------------------------------------------------
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
\ No newline at end of file
constant module
===============
.. automodule:: constant
:members:
:undoc-members:
:show-inheritance:
create\_dialog module
=====================
.. automodule:: create_dialog
:members:
:undoc-members:
:show-inheritance:
create\_dialog\_ui module
=========================
.. automodule:: create_dialog_ui
:members:
:undoc-members:
:show-inheritance:
detect\_dialog module
=====================
.. automodule:: detect_dialog
:members:
:undoc-members:
:show-inheritance:
detect\_dialog\_ui module
=========================
.. automodule:: detect_dialog_ui
:members:
:undoc-members:
:show-inheritance:
detect\_with\_asr module
========================
.. automodule:: detect_with_asr
:members:
:undoc-members:
:show-inheritance:
detect\_with\_ocr module
========================
.. automodule:: detect_with_ocr
:members:
:undoc-members:
:show-inheritance:
.. Assistant Production System of Narration For Barrier-Free Films documentation master file, created by
sphinx-quickstart on Sun Jun 18 22:25:10 2023.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Assistant Production System of Narration For Barrier-Free Films's documentation!
===========================================================================================
.. toctree::
:maxdepth: 1
:caption: Get Started
:hidden:
:glob:
.. toctree::
:maxdepth: 2
:caption: UI
:glob:
main_window_ui
assemble_dialog_ui
create_dialog_ui
detect_dialog_ui
operation_dialog_ui
prompt_dialog_ui
setting_dialog_ui
start
.. toctree::
:maxdepth: 2
:caption: funcs_for_ui
:glob:
main_window
assemble_dialog
create_dialog
detect_dialog
operation_dialog
prompt_dialog
setting_dialog
start
.. toctree::
:maxdepth: 2
:caption: API
:glob:
constant
detect_with_asr
detect_with_ocr
judge_subtitle
management
myvideoslider
myVideoWidget
mywidgetcontents
narratage_detection
render
speech_synthesis
split_wav
synthesis
utils
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
judge\_subtitle module
======================
.. automodule:: judge_subtitle
:members:
:undoc-members:
:show-inheritance:
main\_window module
===================
.. automodule:: main_window
:members:
:undoc-members:
:show-inheritance:
main\_window\_ui module
=======================
.. automodule:: main_window_ui
:members:
:undoc-members:
:show-inheritance:
management module
=================
.. automodule:: management
:members:
:undoc-members:
:show-inheritance:
accessibility_movie_2
=====================
.. toctree::
:maxdepth: 4
assemble_dialog
assemble_dialog_ui
constant
create_dialog
create_dialog_ui
detect_dialog
detect_dialog_ui
detect_with_asr
detect_with_ocr
judge_subtitle
main_window
main_window_ui
management
myVideoWidget
myvideoslider
mywidgetcontents
narratage_detection
operation_dialog
operation_dialog_ui
prompt_dialog
prompt_dialog_ui
render
setting_dialog
setting_dialog_ui
speech_synthesis
split_wav
start
synthesis
utils
myVideoWidget module
====================
.. automodule:: myVideoWidget
:members:
:undoc-members:
:show-inheritance:
myvideoslider module
====================
.. automodule:: myvideoslider
:members:
:undoc-members:
:show-inheritance:
mywidgetcontents module
=======================
.. automodule:: mywidgetcontents
:members:
:undoc-members:
:show-inheritance:
narratage\_detection module
===========================
.. automodule:: narratage_detection
:members:
:undoc-members:
:show-inheritance:
operation\_dialog module
========================
.. automodule:: operation_dialog
:members:
:undoc-members:
:show-inheritance:
operation\_dialog\_ui module
============================
.. automodule:: operation_dialog_ui
:members:
:undoc-members:
:show-inheritance:
prompt\_dialog module
=====================
.. automodule:: prompt_dialog
:members:
:undoc-members:
:show-inheritance:
prompt\_dialog\_ui module
=========================
.. automodule:: prompt_dialog_ui
:members:
:undoc-members:
:show-inheritance:
render module
=============
.. automodule:: render
:members:
:undoc-members:
:show-inheritance:
setting\_dialog module
======================
.. automodule:: setting_dialog
:members:
:undoc-members:
:show-inheritance:
setting\_dialog\_ui module
==========================
.. automodule:: setting_dialog_ui
:members:
:undoc-members:
:show-inheritance:
speech\_synthesis module
========================
.. automodule:: speech_synthesis
:members:
:undoc-members:
:show-inheritance:
split\_wav module
=================
.. automodule:: split_wav
:members:
:undoc-members:
:show-inheritance:
start module
============
.. automodule:: start
:members:
:undoc-members:
:show-inheritance:
synthesis module
================
.. automodule:: synthesis
:members:
:undoc-members:
:show-inheritance:
utils module
============
.. automodule:: utils
:members:
:undoc-members:
:show-inheritance:
"""主界面相关响应
.. code-block:: python
from main_window import MainWindow
mainWindow = MainWindow(project_path)
mainWindow.show()
"""
import time
import os
......@@ -32,6 +41,7 @@ import re
class MainWindow(QMainWindow, Ui_MainWindow):
EXIT_CODE_REBOOT = -12345678
renew_signal = pyqtSignal(str)
def __init__(self, project_path):
super(MainWindow, self).__init__()
self.setupUi(self)
......@@ -43,7 +53,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# todo:后续改成QThread的组件
self.synthesis = SynthesisProcessor()
self.synthesis.show_warning_signal.connect(self.show_warning_msg_box)
self.synthesis.synthesis_callback_signal.connect(self.deal_synthesis_callback_slot)
self.synthesis.synthesis_callback_signal.connect(
self.deal_synthesis_callback_slot)
# 检测对话框
self.detect_dialog = Detect_Dialog(self.projectContext)
self.detect_dialog.start_detect_signal.connect(self.start_detect)
......@@ -59,7 +70,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 工程导出相关组件
self.export = ExportProcessor()
self.export.show_warning_signal.connect(self.show_warning_msg_box)
self.export.export_callback_signal.connect(self.deal_export_callback_slot)
self.export.export_callback_signal.connect(
self.deal_export_callback_slot)
# 设置框
self.setting_dialog = Setting_Dialog(self.projectContext)
......@@ -67,7 +79,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 提示框
self.prompt_dialog = Prompt_Dialog()
self.prompt_dialog.setModal(True)
self.continue_detect_prompt_dialog = Prompt_Dialog(self.continue_detect)
self.continue_detect_prompt_dialog = Prompt_Dialog(
self.continue_detect)
# 操作框
self.operation_dialog = Operation_Dialog(self)
......@@ -88,13 +101,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.export_timer.timeout.connect(self.check_if_export_over_slot)
self.video_timer = QTimer()
self.video_timer.timeout.connect(self.change_videotime_label_slot)
self.video_timer.start(5000) # todo 作为参数配置
self.video_timer.start(1000) # todo 作为参数配置
self.refresh_tab_timer = QTimer()
self.refresh_tab_timer.timeout.connect(self.refresh_tab_slot)
"""
状态栏相关空间
"""状态栏相关空间
"""
self.statusbarLabel = QLabel()
self.statusbarLabel.setText(" 休息中")
......@@ -112,8 +124,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.statusbar.addPermanentWidget(self.progressBar, stretch=9)
self.statusbar.addPermanentWidget(self.progressLabel, stretch=1)
"""
菜单栏
"""菜单栏
"""
self.setting.triggered.connect(self.show_setting_dialog) # 设置
self.action_3.triggered.connect(self.show_detect_dialog)
......@@ -138,25 +150,24 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# self.action_view_history.triggered.connect(self.view_history_slot)
self.action_operate.triggered.connect(self.show_operate_dialog)
self.action_operate.setEnabled(False)
self.action_insert_aside_from_now.triggered.connect(self.insert_aside_from_now_slot)
self.action_insert_aside_from_now.triggered.connect(
self.insert_aside_from_now_slot)
self.action_insert_aside_from_now.setEnabled(False)
self.insert_aside_from_now_btn.clicked.connect(self.insert_aside_from_now_slot)
self.insert_aside_from_now_btn.clicked.connect(
self.insert_aside_from_now_slot)
self.insert_aside_from_now_btn.setEnabled(False)
"""
视频相关信息
"""
# 视频时长,全局变量
self.video_duration = None
"""视频预览相关信息
"""
self.video_duration = None # 视频时长,全局变量
self.sld_video_pressed = False # 判断当前进度条识别否被鼠标点击
self.videoFullScreen = False # 判断当前widget是否全屏
self.videoFullScreenWidget = myVideoWidget() # 创建一个全屏的widget
self.player = QMediaPlayer()
self.player.setVideoOutput(self.wgt_video) # 视频播放输出的widget,就是上面定义的
self.player.durationChanged.connect(self.player_change_slot)
# self.btn_open.clicked.connect(self.open_excel) # 打开excel文件按钮
self.btn_play.clicked.connect(self.playVideo) # play
# self.btn_stop.clicked.connect(self.pauseVideo) # pause
self.player.positionChanged.connect(
self.changeSlide) # change Slide
self.videoFullScreenWidget.doubleClickedItem.connect(
......@@ -171,15 +182,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.sld_audio.valueChanged.connect(self.volumeChange) # 控制声音播放
self.kd_slider.valueChanged.connect(self.scale_change_slot)
"""
旁白音频预览相关信息
"""旁白音频预览
"""
self.audio_player = QMediaPlayer()
# self.audio_player.stateChanged.connect(self.release_audio_file)
"""表格相关信息
"""
表格相关信息
"""
self.curTab = 0
# 设置表格每一列的宽度
......@@ -188,8 +197,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.zm_tableWidget.setColumnCount(len(subtitle_header))
self.zm_tableWidget.setHorizontalHeaderLabels(subtitle_header)
zm_tableHeader = self.zm_tableWidget.horizontalHeader()
zm_tableHeader.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
zm_tableHeader.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
zm_tableHeader.setSectionResizeMode(
0, QtWidgets.QHeaderView.ResizeToContents)
zm_tableHeader.setSectionResizeMode(
1, QtWidgets.QHeaderView.ResizeToContents)
zm_tableHeader.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
content_header = self.projectContext.contentHeader
......@@ -198,23 +209,29 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 允许打开上下文菜单
self.all_tableWidget.setContextMenuPolicy(Qt.CustomContextMenu)
# 绑定事件
self.all_tableWidget.customContextMenuRequested.connect(self.generateMenu)
self.all_tableWidget.customContextMenuRequested.connect(
self.generateMenu)
all_tableHead = self.all_tableWidget.horizontalHeader()
all_tableHead.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
all_tableHead.setSectionResizeMode(
0, QtWidgets.QHeaderView.ResizeToContents)
all_tableHead.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
all_tableHead.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
all_tableHead.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
all_tableHead.setSectionResizeMode(
3, QtWidgets.QHeaderView.ResizeToContents)
aside_header = self.projectContext.aside_header
self.pb_tableWidget.setColumnCount(len(aside_header))
self.pb_tableWidget.setHorizontalHeaderLabels(aside_header)
pb_tableHead = self.pb_tableWidget.horizontalHeader()
pb_tableHead.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
pb_tableHead.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
pb_tableHead.setSectionResizeMode(
0, QtWidgets.QHeaderView.ResizeToContents)
pb_tableHead.setSectionResizeMode(
1, QtWidgets.QHeaderView.ResizeToContents)
# pb_tableHead.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
pb_tableHead.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
pb_tableHead.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
pb_tableHead.setSectionResizeMode(
3, QtWidgets.QHeaderView.ResizeToContents)
self.all_tableWidget.resizeRowsToContents()
self.pb_tableWidget.resizeRowsToContents()
self.zm_tableWidget.resizeRowsToContents()
......@@ -229,10 +246,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.zm_tableWidget.itemDoubleClicked.connect(self.change_video_time)
self.all_tableWidget.itemDoubleClicked.connect(self.change_video_time)
self.all_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.all_tableWidget.itemDoubleClicked.connect(self.all_item_changed_by_double_clicked_slot)
self.all_tableWidget.itemDoubleClicked.connect(
self.all_item_changed_by_double_clicked_slot)
self.pb_tableWidget.itemDoubleClicked.connect(self.writeHistory)
self.pb_tableWidget.itemDoubleClicked.connect(self.pb_item_changed_by_double_clicked_slot)
self.pb_tableWidget.itemDoubleClicked.connect(
self.pb_item_changed_by_double_clicked_slot)
self.pb_tableWidget.itemChanged.connect(self.rewriteHistory)
# todo 现在只在【旁白】tab上双击修改,会保存表格到本地,【字幕旁白】tab上不行。(【字幕旁白】tab上无法修改旁白)
self.pb_tableWidget.itemChanged.connect(self.write2ProjectFromAside)
......@@ -244,7 +263,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 在进行redo_undo时,会触发itemchange,但是这时候不能覆写历史。但是需要写入project。(注意命名思路:在进行redo的时候,会有两步操作,写入history和写入project。我们只希望他不写入history,所以命名中要带有history)
self.can_write_history = True
self.previewed_audio = {}
self.is_video_playing = False
# 表格中的内容是否被更改,需要刷新
......@@ -264,13 +282,21 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 更新工程信息
self.projectContext.Init(project_path)
self.update_ui()
# 打印到log文件中
t = RunThread(funcName=make_print_to_file, args=os.path.join(os.getcwd(), 'log'), name="logging")
print(t)
make_print_to_file(os.path.join(os.getcwd(),'log'))
make_print_to_file(os.path.join(os.getcwd(), 'log'))
def generateMenu(self, pos):
print("pos",pos)
"""当用户右击字幕旁白表格时,弹出菜单项
Args:
pos : 用户右击的位置
"""
if self.curTab != 0:
return
print("pos", pos)
# 获取点击行号
for i in self.all_tableWidget.selectionModel().selection().indexes():
......@@ -279,29 +305,33 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 如果选择的行索引小于2,弹出上下文菜单
menu = QMenu()
item1 = menu.addAction("删除")
# 转换坐标系
screenPos = self.all_tableWidget.mapToGlobal(pos)
print("转换后的坐标", screenPos)
# 被阻塞
action = menu.exec(screenPos)
if action == item1:
print('选择了第1个菜单项',self.all_tableWidget.item(rowNum,0).text()
,self.all_tableWidget.item(rowNum,1).text()
,self.all_tableWidget.item(rowNum,2).text())
print('选择了第1个菜单项', self.all_tableWidget.item(rowNum, 0).text(), self.all_tableWidget.item(
rowNum, 1).text(), self.all_tableWidget.item(rowNum, 2).text())
self.del_line_operation_slot(rowNum + 1)
return
return
# 重写关闭Mmainwindow窗口
def closeEvent(self, event):
"""在用户试图关闭主界面窗口,弹窗提醒用户避免误触
Args:
event : 关闭操作
"""
# buttonBox = QtWidgets.QMessageBox()
# btn_save_and_close = buttonBox.addButton("保存并退出", QtWidgets.QMessageBox.YesRole)
# btn_not_save_and_close = buttonBox.addButton("不保存并退出", QtWidgets.QMessageBox.YesRole)
# buttonBox.exec_()
# 如果没有进行任何修改就退出了的话,确认退出?在退出前,先保存一下
replp = QtWidgets.QMessageBox.question(self, u'警告', u'确认退出?',
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if replp == QtWidgets.QMessageBox.Yes:
self.projectContext.save_project(False)
event.accept()
......@@ -312,38 +342,62 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 重写改变窗口大小事件
def resizeEvent(self, *args, **kwargs):
"""重写改变窗口大小事件,同时调整时间戳的长度
"""
super().resizeEvent(*args, **kwargs)
position = self.kd_slider.value()
self.scale_change_slot(position)
def show_detect_dialog(self):
"""弹出旁白区间检测相关信息填写窗口
"""
self.detect_dialog.init_self()
self.detect_dialog.show()
def show_assemble_dialog(self):
"""弹出旁白音频合成相关信息填写窗口
"""
self.assemble_dialog.init_self()
self.assemble_dialog.show()
def show_setting_dialog(self):
"""弹出设置窗口
"""
self.setting_dialog.showDialog()
def show_create_dialog(self):
"""弹出新建工程窗口
"""
self.create_dialog.show()
def show_warning_msg_box(self, msg: str):
"""弹出警示信息
Args:
msg (str): 警告信息
"""
replp = QtWidgets.QMessageBox.question(self, u'警告', msg,
QtWidgets.QMessageBox.Yes)
# 在已打开一个工程的情况下打开或新建另一个工程,初始化界面
def init_project(self, project_path):
def init_project(self, project_path: str):
"""在已打开一个工程的情况下打开或新建另一个工程,初始化界面
Args:
project_path (str): 新工程路径
"""
if self.projectContext.project_base_dir is not None:
# 如果目前界面中正在处理其他工程,需要让用户确认是否退出当前界面
replp = QtWidgets.QMessageBox.question(self, u'警告', u'是否退出当前工程?',
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if replp == QtWidgets.QMessageBox.Yes:
self.projectContext.save_project(False)
print("emit close Event")
# 把其他进程结束掉
for t in self.all_threads:
if t.is_alive():
......@@ -359,37 +413,51 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.projectContext.Init(project_path)
self.update_ui()
# 导入视频
def import_slot(self):
"""导入视频,并对主界面中的相关控件进行对应更新
"""
video_path = self.openVideoFile().path()
print("[import_slot] video_path=" + video_path)
if video_path == "" or video_path == None:
return
if video_path[0] == '/':
if video_path[0] == '/' and ":" in video_path:
video_path = video_path[1:]
self.projectContext.video_path = video_path
excel_name = os.path.splitext(os.path.basename(video_path))[0]
self.projectContext.excel_path = os.path.join(self.projectContext.project_base_dir, excel_name + ".xlsx")
self.projectContext.excel_path = os.path.join(
self.projectContext.project_base_dir, excel_name + ".xlsx")
self.action_export.setEnabled(True)
self.action_operate.setEnabled(True)
self.action_insert_aside_from_now.setEnabled(True)
self.insert_aside_from_now_btn.setEnabled(True)
# todo: 后续这段代码公共的可以抽出来
# 打开某个工程
def open_project_slot(self):
project_path = QFileDialog.getExistingDirectory(self, "选择工程文件夹", os.getcwd())
"""打开工程,并初始化界面
通过选择文件夹界面确认待打开的工程路径,如果未选择则无事发生,否则就根据选择的工程文件夹路径使用init_project函数更新界面
"""
project_path = QFileDialog.getExistingDirectory(
self, "选择工程文件夹", os.getcwd())
# print("[import_slot] project_path=" + project_path)
if project_path == "" or project_path == None:
return
return
self.init_project(project_path)
def update_ui(self):
"""更新交互界面中的空间
1. 如果表格路径不为None,则将表格内容导入到界面中;
2. 如果这个工程之前没有检测的视频,那么就需要将导入视频的按钮设置为可点,否则就直接导入对应的视频;
3. 如果之前的检测任务尚未完成,可弹窗提示用户是否需要继续检测。
"""
if self.projectContext.project_base_dir is None:
return
return
self.action_export.setEnabled(True)
self.action_3.setEnabled(True)
self.action_4.setEnabled(True)
......@@ -403,28 +471,44 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.import_movie.setEnabled(True)
else:
if not os.path.exists(video_path):
self.prompt_dialog.show_dialog_signal.emit(f"该工程原检测的视频对应路径{video_path}目前已失效,请导入新的视频重新开始检测或移动视频到对应位置,并重新启动工程")
self.prompt_dialog.show_dialog_signal.emit(
f"该工程原检测的视频对应路径{video_path}目前已失效,请导入新的视频重新开始检测或移动视频到对应位置,并重新启动工程")
self.import_movie.setEnabled(True)
return
else:
self.player.setMedia(QMediaContent(QUrl.fromLocalFile(video_path))) # 选取视频文件
self.player.setMedia(QMediaContent(
QUrl.fromLocalFile(video_path))) # 选取视频文件
self.playVideo() # 播放视频
self.action_insert_aside_from_now.setEnabled(True)
self.insert_aside_from_now_btn.setEnabled(True)
if self.projectContext.detected == True and self.projectContext.nd_process < 1:
self.continue_detect_prompt_dialog.setModal(True)
self.continue_detect_prompt_dialog.show_with_msg("您的上次任务未完成,是否继续检测?")
self.continue_detect_prompt_dialog.show_with_msg(
"您的上次任务未完成,是否继续检测?")
def continue_detect(self):
print(f"继续检测,video_path={self.projectContext.video_path}, book_path={self.projectContext.excel_path}")
self.start_detect(self.projectContext.video_path, self.projectContext.excel_path)
"""继续旁白区间检测任务
重新调用start_detect函数继续进行旁白区间检测任务
"""
print(
f"继续检测,video_path={self.projectContext.video_path}, book_path={self.projectContext.excel_path}")
self.start_detect(self.projectContext.video_path,
self.projectContext.excel_path)
def start_detect(self, video_path, book_path):
"""检测旁白
def start_detect(self, video_path: str, book_path: str):
"""开始旁白区间检测
绑定到旁白推荐tab栏中的“开始检测”按钮上。
Args:
video_path (str): 待检测视频路径
book_path (str): 表格路径
绑定到旁白区间检测tab栏中的“开始检测”按钮上。
首先检测各种输入的合理性,然后更新UI中组件的状态,使用多线程调用旁白区间检测函数和进度条更新函数。
函数运行过程中实时监测函数的运行情况,如果发现函数报错,则中断线程,弹出报错窗口,否则等待函数正常结束,并更新UI中的组件。
"""
# 检查是否已有线程在运行旁白检测
......@@ -444,21 +528,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
elif not os.path.exists(os.path.dirname(book_path)):
self.show_warning_msg_box("请重新确认表格存放路径是否正确")
return
# todo:
# if not check_timePoint(startTime.get()):
# self.show_warning_msg_box("请确认开始时间是否正确")
# return
# elif not check_timePoint(endTime.get()):
# self.show_warning_msg_box("请确认结束时间是否正确")
# return
# self.projectContext.Init(os.path.dirname(
# book_path), os.path.basename(video_path))
if self.projectContext.excel_path is None:
self.projectContext.excel_path = book_path
# 获取视频的时长等信息,初始化开始结束时间
startTime = "00:01:00"
startTime = "00:00:20"
video = cv2.VideoCapture(video_path)
fps = video.get(cv2.CAP_PROP_FPS)
......@@ -493,7 +568,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
from narratage_detection import detect
self.refresh_tab_timer.start(10000) # 10秒一刷新
self.refresh_tab_timer.start(10000) # 10秒一刷新
t = RunThread(funcName=detect,
args=(video_path, start_time, end_time,
book_path, state, 1, self),
......@@ -511,16 +586,35 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.detect_timer.start(5000)
def check_if_detect_over_slot(self):
"""确认旁白区间检测任务是否完成
"""
self.check_if_over("检测")
def check_if_synthesis_over_slot(self):
"""确认旁白音频合成任务是否完成
"""
self.check_if_over("合成")
def check_if_export_over_slot(self):
"""确认自动渲染导出任务是否完成
"""
self.check_if_over("导出")
# type = 检测 或 合成 或 导出
def check_if_over(self, type):
def check_if_over(self, type: str):
"""确认传入的待检测任务是否完成
Args:
type (str): 待判断的任务类型,分为"检测"、"合成"和"导出"
首先更新任务状态和对应的进度条百分比;
接下来根据待判断的任务类型确认任务是否已完成,如果任务完成了就停掉对应的timer;
如果发现线程中途退出了,说明有异常出现会弹出窗口提醒用户联系开发者处理。
"""
alive = True
print(self.state)
if self.state != [None]:
......@@ -551,6 +645,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.projectContext.nd_process = 1
def deal_synthesis_callback_slot(self, threads, state):
"""实现旁白音频合成任务状态在界面中的实时显示,更新界面中的对应变量,每5s更新一次任务状态
Args:
threads : 执行旁白音频合成的线程
state (list) : 任务执行状态
"""
self.statusbarLabel.setText(" 准备合成:")
self.progressBar.setValue(0)
self.state = self.synthesis.state
......@@ -558,124 +658,142 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.synthesis_timer.start(5000)
def deal_export_callback_slot(self, threads, state):
"""实现自动渲染导出任务状态在界面中的实时显示,更新界面中的对应变量,每5s更新一次任务状态
Args:
threads : 执行自动渲染导出的线程
state (list) : 任务执行状态
"""
self.statusbarLabel.setText(" 准备导出:")
self.progressBar.setValue(0)
self.state = self.export.state
self.threads = self.export.threads
self.export_timer.start(1000)
"""
刻度相关
原期望效果:在最左刻度时,恰好打满【时间轴区】,当最右刻度时,时间轴上每一刻度对应的时间是1s.
目前的期望效果:在最左刻度时,时间轴区中一格刻度显示为6s左右(即一条长段为1分钟);在最右刻度时,时间轴中一格刻度显示为0.2s。
"""
def emit_scale_change_slot(self):
"""根据刻度改变时间轴的长度
获取刻度的值,并据此更新时间轴的长度
"""
position = self.kd_slider.value()
self.scale_change_slot(position)
def scale_change_slot(self, position):
def scale_change_slot(self, position: int):
"""根据刻度的当前选值改变时间轴长度
Args:
position (int): 目前刻度的选值
原期望效果:在最左刻度时,恰好打满【时间轴区】,当最右刻度时,时间轴上每一刻度对应的时间是1s。
目前的期望效果:在最左刻度时,时间轴区中一格刻度显示为6s左右(即一条长段为1分钟);在最右刻度时,时间轴中一格刻度显示为0.2s。
"""
if self.player.duration() == 0:
return
area_width = self.scrollArea.width()
self.sld_video.resize(int(area_width + 10),
self.sld_video.height())
self.scrollAreaWidgetContents.resize(int(area_width + 20), self.scrollAreaWidgetContents.height())
return
area_width = self.scrollArea.width()
# max_sld_video_size = 50 * (self.player.duration() / 1000) # 50 * 视频秒数
# min_sld_video_size = self.player.duration() / (6 * 1000)
max_sld_video_size = 10 * (self.player.duration() / 1000) # 10 * 视频秒数
min_sld_video_size = area_width
magnification = round(
position / self.kd_slider.maximum() * 100) # [1,100]
now_sld_video_size = min_sld_video_size + \
((magnification - 1) * (max_sld_video_size - min_sld_video_size) / 99) # float
if min_sld_video_size > max_sld_video_size:
now_sld_video_size = min_sld_video_size - 10
print("before====")
print("self.sld_video", self.sld_video.size())
print("self.scrollAreaWidgetContents",
self.scrollAreaWidgetContents.size())
print("self.sld_video.maximum()", self.sld_video.maximum())
print("self.sld_video", self.sld_video.size())
self.sld_video.resize(int(now_sld_video_size + 10),
self.sld_video.height())
print("self.sld_video.maximum()", self.sld_video.maximum())
self.scrollAreaWidgetContents.resize(
int(now_sld_video_size + 20), self.scrollAreaWidgetContents.height())
print("after====")
print("self.sld_video", self.sld_video.size())
print("self.scrollAreaWidgetContents", self.scrollAreaWidgetContents.size())
# print("min_sld_video_size", min_sld_video_size, "magnification", magnification, "max_sld_video_size", max_sld_video_size)
# print("now_sld_video_size", now_sld_video_size)
# print("计算", ((magnification-1) * (max_sld_video_size - min_sld_video_size) / 99))
print("self.scrollAreaWidgetContents",
self.scrollAreaWidgetContents.size())
"""
video相关
"""
def volumeChange(self, position: int):
"""根据当前音量条调整音量
def volumeChange(self, position):
Args:
position (int): 当前音量条的选值
"""
volume = round(position/self.sld_audio.maximum()*100)
print("vlume %f" % volume)
self.player.setVolume(volume)
self.lab_audio.setText("音量:"+str(volume)+"%")
self.lab_audio.setText("音量:" + str(volume) + "%")
# postion 取值[0,self.sld_video.maximum()]
def clickedSlider(self, position):
def clickedSlider(self, position: int):
"""点击时间轴时,对应切换视频播放进度
Args:
position (int): 浮标在时间轴上的位置
根据当前浮标在时间轴上的位置对应切换视频的播放进度,并更新视频播放进度标签
"""
if self.player.duration() > 0: # 开始播放后才允许进行跳转
self.init_previewed_audio()
video_position = int(
(position / self.sld_video.maximum()) * self.player.duration())
self.player.setPosition(video_position)
self.lab_video.setText(utils.transfer_second_to_time(str(round(video_position/1000,2))))
self.lab_video.setText(utils.transfer_second_to_time(
str(round(video_position/1000, 2))))
else:
self.sld_video.setValue(0)
# position 取值[0, self.sld_video.maximum()]
def moveSlider(self, position):
def moveSlider(self, position: int):
"""移动时间轴上的浮标时,对应切换视频播放进度
Args:
position (int): 浮标在时间轴上的位置
根据当前浮标在时间轴上的位置切换对应视频时间
"""
self.sld_video_pressed = True
if self.player.duration() > 0: # 开始播放后才允许进行跳转
self.init_previewed_audio()
video_position = int(
(position / self.sld_video.maximum()) * self.player.duration())
self.player.setPosition(video_position)
self.lab_video.setText(utils.transfer_second_to_time(str(round(video_position/1000,2))))
self.lab_video.setText(utils.transfer_second_to_time(
str(round(video_position/1000, 2))))
def pressSlider(self):
"""更新按压时间轴的状态,此时用户按住了时间轴上的浮标
"""
self.sld_video_pressed = True
def releaseSlider(self):
"""更新按压时间轴的状态,此时用户释放了时间轴上的浮标
"""
self.sld_video_pressed = False
# position 取值[0,总时长]
def changeSlide(self, position):
def changeSlide(self, position: int):
"""根据目前视频的播放进度更新时间轴上的浮标位置和对应的时间戳
Args:
position (int): 视频播放进度,取值[0,总时长]
"""
if not self.sld_video_pressed: # 进度条被鼠标点击时不更新
self.vidoeLength = self.player.duration()+0.1
self.sld_video.setValue(round((position/self.vidoeLength)*self.sld_video.maximum()))
self.lab_video.setText(utils.transfer_second_to_time(str(round(position/1000,2))))
"""
# 播放音频
# 0、视频必须在播放中
if self.is_video_playing == False:
return
# 1、先找到要播放的音频(一个电影最多2000条字幕或旁白)
# todo: 验证2k条的旁白,O(n)找到待播放音频,会不会很慢
audio_path = None
for i in range(len(self.projectContext.aside_list)-1, -1, -1):
if position/1000 > float(self.projectContext.aside_list[i].st_time_sec):
audio_path = os.path.dirname(self.projectContext.excel_path) + (
"/tmp/%.2f.wav" % float(self.projectContext.aside_list[i].st_time_sec))
break
# 2、如果找到了该音频并且该次预览中没有播放过,则新起一个线程播放
if audio_path != None and os.path.exists(audio_path) and os.path.basename(audio_path) not in self.previewed_audio:
t = RunThread(funcName=self.play_audio,
args=(audio_path, self.previewed_audio),
name="play_audio")
t.start()
self.all_threads.append(t)
print("previewed_audio:", self.previewed_audio)
"""
# 一条语音的最长播放时间是10秒
def play_audio(self, path, previewed_audio):
self.vidoeLength = self.player.duration() + 0.1
self.sld_video.setValue(
round((position/self.vidoeLength)*self.sld_video.maximum()))
self.lab_video.setText(utils.transfer_second_to_time(
str(round(position/1000, 2))))
def play_audio(self, path: str):
"""播放旁白音频
Args:
path (str): 待播放旁白音频的路径
"""
# 如果没有该音频,则直接return
if not os.path.exists(path):
return
......@@ -684,11 +802,16 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.audio_player.setMedia(content)
self.audio_player.play()
print("播放", path)
previewed_audio[os.path.basename(path)] = 1
def openVideoFile(self):
path = QFileDialog.getOpenFileUrl(self, "选择待导入视频", QUrl(os.getcwd()), "Video Files(*.mp4 *.rmvb *mkv *avi *.);; 所有文件(*.*)")[0]
print(path.url()[8:])
"""导入视频
会弹出一个选择视频的界面,该界面以工程所在文件夹为界面起点,用户可切换至其他位置选择待处理的视频,界面中只显示mp4、rmvb、mkv和avi格式的文件(用户也可以切换至“所有文件”选择其他格式的文件)。
"""
path = QFileDialog.getOpenFileUrl(self, "选择待导入视频", QUrl(
os.getcwd()), "Video Files(*.mp4 *.rmvb *mkv *avi *.);; 所有文件(*.*)")[0]
# print(path.url()[8:])
if len(path.url()) == 0 or not os.path.exists(path.url()[8:]):
return path
self.player.setMedia(QMediaContent(path)) # 选取视频文件
......@@ -699,58 +822,64 @@ class MainWindow(QMainWindow, Ui_MainWindow):
return path
# 在初始化工程时
def init_ProjectContext_VideoPath(self, path):
def init_ProjectContext_VideoPath(self, path: str):
"""初始化工程的待处理视频路径
Args:
path (str): 视频路径
"""
# video_dir = os.path.dirname(path)
# video_name = os.path.basenampath) # self.projectContext.Init(video_dir, video_name)
self.projectContext.setVideoPath(path)
def init_previewed_audio(self):
self.previewed_audio = {}
def playVideo(self):
"""播放视频
"""
if not self.player.media().isNull():
# 正在播放中,那就是暂停
if self.is_video_playing is True:
self.player.pause()
self.is_video_playing = False
self.btn_play.setIcon(QIcon('res\images\播放.svg'))
self.init_previewed_audio()
else:
self.player.play()
self.is_video_playing = True
self.btn_play.setIcon(QIcon("res\images\暂停.svg"))
self.init_previewed_audio()
def videoDoubleClicked(self, text):
"""双击视频进行全屏转换
"""
if self.player.duration() > 0: # 开始播放后才允许进行全屏操作
if self.videoFullScreen:
self.player.setVideoOutput(self.wgt_video)
self.playVideo()
self.videoFullScreenWidget.hide()
self.videoFullScreen = False
else:
self.videoFullScreenWidget.show()
self.player.setVideoOutput(self.videoFullScreenWidget)
self.playVideo()
self.videoFullScreenWidget.setFullScreen(1)
self.videoFullScreen = True
def open_excel_with_project_path(self):
"""打开工程下的表格,并使用该内容初始化界面中的表格
"""
self.projectContext.load_excel_from_path()
self.all_tableWidget_idx = 0
self.pb_tableWidget_idx = 0
self.zm_tableWidget_idx = 0
self.set_table_to_window()
def set_table_to_window(self, need_refresh_all = True, user_operation = False):
"""
把projectContext里的内容呈现在UI上。
在初始化工程时,need_refresh_all = True,代表是从无到有的加载,决定了表格更新的起始坐标。
user_operation代表是否是用户双击进行的操作,此时需要initial_ing置为true。
user_operation=False的情况:因为添加一行而进行的被迫修改
user_operation=True的情况:用户双击操作表格,通过【操作表格】进行增删改,通过【在此处添加旁白】来添加一行
initial_ing为true时,不会生成音频,不会写入历史记录
"""
def set_table_to_window(self, need_refresh_all=True, user_operation=False):
"""使用表格内容初始化主界面中的表格
Args:
need_refresh_all (bool, optional): 在初始化工程时,need_refresh_all = True,代表是从无到有的加载,决定了表格更新的起始坐标。Defaults to True.
user_operation (bool, optional): user_operation代表是否是用户双击进行的操作,此时需要initial_ing置为true。置为False是:因为添加一行而进行的被迫修改
置为True时:用户双击操作表格,通过【操作表格】进行增删改,通过【在此处添加旁白】来添加一行。Defaults to False.
"""
if not user_operation:
self.projectContext.initial_ing = True
subtitle_list = self.projectContext.subtitle_list
......@@ -760,7 +889,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# self.zm_tableWidget.clear()
# self.pb_tableWidget.clear()
self.zm_tableWidget.setRowCount(len(subtitle_list))
st_idx = 0 if need_refresh_all else self.zm_tableWidget_idx
st_idx = 0 if need_refresh_all else self.zm_tableWidget_idx
for i in range(st_idx, len(subtitle_list)):
self.setElememtToTable(self.zm_tableWidget, subtitle_list[i], i)
......@@ -783,9 +912,17 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# self.zm_tableWidget.resizeRowsToContents()
if not user_operation:
# initial_ing为true时,不会生成音频,不会写入历史记录
self.projectContext.initial_ing = False
def setElememtToTable(self, table: QTableWidget, elem: Element, idx: int):
"""在表中添加一行
Args:
table (QTableWidget): 待操作的表格
elem (Element): 待添加的信息
idx (int): 行号(从0开始)
"""
elem_list = elem.to_list()
time_format_col_list = []
if table.objectName() == constant.Content.ObjectName:
......@@ -803,7 +940,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
btn.clicked.connect(self.audio_preview_slot)
table.setCellWidget(idx, col, btn)
for j in range(len(elem_list)):
text = elem_list[j]
if type(text) == str and '插入旁白,推荐' in text:
......@@ -814,7 +950,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
text = text[0]
# text = text[text.index('推荐'):]
# 需要格式化成hh:mm:ss格式
if j in time_format_col_list and type(text)==str and len(text) != 0:
if j in time_format_col_list and type(text) == str and len(text) != 0:
text = utils.transfer_second_to_time(text)
if table.objectName() == constant.Aside.ObjectName and j == constant.Aside.SpeedColumnNumber:
qcombo = QtWidgets.QComboBox()
......@@ -831,8 +967,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 只有Content页的字幕列和 Aside页的字幕列 可编辑
def audio_preview_slot(self):
"""音频预览,会同步播放视频,并更新视频信息相关组件
"""
btn = self.sender()
# 法1:按照物理位置。这样的结果不太对
idx = self.pb_tableWidget.indexAt(btn.pos())
......@@ -842,19 +980,20 @@ class MainWindow(QMainWindow, Ui_MainWindow):
audio_path = None
pos_sec = utils.trans_to_seconds(item.text())
audio_path = os.path.dirname(self.projectContext.excel_path) + (
"/tmp/%.3f.wav" % pos_sec)
"/tmp/%.3f.wav" % pos_sec)
print("待播放的音频文件为", audio_path)
if audio_path is not None:
# 确认该音频是否正在合成中
# 确认该音频是否正在合成中
for t in self.all_threads:
if t.name == "single_speech_synthesis" and t.is_alive():
if audio_path in t._args:
self.prompt_dialog.show_with_msg("音频正在合成,请稍候")
return
return
# 2、如果找到了该音频,则新起一个线程播放
if audio_path != None and os.path.exists(audio_path):
print(audio_path)
t = RunThread(funcName=self.play_audio,
args=(audio_path, self.previewed_audio),
args=(audio_path, ),
name="play_audio")
t.start()
self.all_threads.append(t)
......@@ -866,6 +1005,16 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.prompt_dialog.show_with_msg("暂无音频可供预览,请重新生成")
def checkIfTableItemCanChange(self, table: QTableWidget, i: int, j: int):
"""确认单元格是否可编辑
Args:
table (QTableWidget): 待操作的表格
i (int): 单元格对应行
j (int): 单元格对应列
Returns:
bool: True(可编辑) or False(不可编辑)
"""
if table.objectName() == self.all_tableWidget.objectName():
return True
if table.objectName() == self.pb_tableWidget.objectName() and j in constant.Aside.ActivateColumns:
......@@ -873,6 +1022,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
return False
def save_project(self):
"""保存工程
"""
err_info = self.projectContext.save_project(True)
if err_info == None:
self.prompt_dialog.show_dialog_signal.emit("保存工程成功")
......@@ -881,6 +1033,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
"保存工程失败!\n错误为:" + err_info)
def change_video_time(self, item):
"""根据双击起始时间,切换视频当前播放位置
Args:
item : 双击的单元格
"""
if item is None:
print("WARNING!!!")
return
......@@ -889,21 +1046,34 @@ class MainWindow(QMainWindow, Ui_MainWindow):
print("row, col = %s, %s" % (row, col))
text = item.text() # 获取内容
self.init_previewed_audio()
# 停下旁白预览
self.audio_player.setMedia(QMediaContent())
if self.checkIfVideoTimeCanChange(row, col):
if self.checkIfVideoTimeCanChange(col):
self.video_timer.stop()
self.video_timer.start(1000) # 双击的时候,就重启计时器,避免他跳转回video.position的地方去。
# 双击的时候,就重启计时器,避免他跳转回video.position的地方去。
self.video_timer.start(1000)
sec_float = utils.trans_to_seconds(text)
self.player.setPosition(int(float(sec_float)*1000))
def checkIfVideoTimeCanChange(self, row, col):
def checkIfVideoTimeCanChange(self, col: int) -> bool:
"""检查双击的单元格是否为起始时间列中的单元格
Args:
col (int): 列号
Returns:
bool: True(是时间格式的单元格) or False(不是时间格式的单元格)
"""
if col == constant.Aside.StartTimeColumn:
return True
return False
def writeHistory(self, item):
"""保存在主界面表格中对旁白文本的修改
Args:
item : 被修改的单元格
"""
if self.can_write_history == False:
self.can_write_history = True
return
......@@ -919,23 +1089,15 @@ class MainWindow(QMainWindow, Ui_MainWindow):
if col == constant.Aside.AsideColumnNumber:
self.projectContext.history_push(row, text, text)
# def writeHistoryFromContent(self, item):
# print("writeHistoryFromContent")
# if item is None:
# print("WRONG!!!! item Is None")
# return
# else:
# row = item.row() # 获取行数
# col = item.column() # 获取列数 注意是column而不是col哦
# text = item.text() # 获取内容
# if col == constant.Content.ActivateColumn:
#
# self.projectContext.history_push(row, text, text)
def generate_audio_slot(self, item):
"""
在set表格的时候(初始化),不会触发。只有双击修改的时候才会触发
"""
"""生成临时旁白音频
Args:
item : 被选中的单元格
在set表格的时候(初始化),不会触发。只有双击修改或切换语速时才会触发
"""
if self.projectContext.initial_ing == True:
return
if item is None:
......@@ -960,9 +1122,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 合成这一段语音
self.do_generate_audio_by_aside_row(int(row))
def do_generate_audio_by_aside_row(self, row):
"""
传入pb_tableWidget的一个下标,生成对应音频
def do_generate_audio_by_aside_row(self, row: int):
"""根据行号生成对应旁白文本的临时音频
Args:
row (int): 旁白表格中的行号
传入pb_tableWidget中的行号,生成对应音频
"""
from speech_synthesis import speech_synthesis, Speaker, choose_speaker
audio_dir = os.path.dirname(self.projectContext.excel_path)
......@@ -983,13 +1149,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
if text is None or len(text) == 0:
if os.path.exists(wav_path):
os.remove(wav_path)
return
return
# 把目前在执行的单条语音的合成线程先停掉
for t in self.all_threads:
if wav_path in t._args and t.name == "single_speech_synthesis" and t.is_alive():
stop_thread(t)
t = RunThread(funcName=speech_synthesis,
args=(text, wav_path, speaker, speed),
name="single_speech_synthesis")
......@@ -998,14 +1164,31 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.all_threads.append(t)
def is_user_editing(self):
"""确认是否为编辑状态
Returns:
bool: True(处于编辑状态) or False(处于非编辑状态)
"""
return self.user_editing_content or self.user_editing_aside
def set_user_edit(self, val:bool):
def set_user_edit(self, val: bool):
"""设置用户编辑状态
Args:
val (bool): True or False
将用户编辑状态设置为统一状态
"""
print("set_user_edit: ", val)
self.user_editing_content = val
self.user_editing_aside = val
def rewriteHistory(self, item):
"""旁白发生变化时,记录对应变化为history,方便进行“撤销”和“重做”
Args:
item : 旁白表格中发生变化的单元格
"""
if self.projectContext.initial_ing == True:
return
if self.is_user_editing() == False:
......@@ -1073,6 +1256,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# self.projectContext.history_push(opt.row, opt.old_str, text)
def write2ProjectFromAside(self, item):
"""将表格中修改的内容更新至工程中
Args:
item: 修改的单元格
"""
if self.projectContext.initial_ing == True:
return
if self.is_user_editing() == False:
......@@ -1090,29 +1278,22 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 更新【字幕旁白】这个tab里的字,如果是语速,那就更新语速这一列,如果是旁白,那就更新旁白这一列
print("行号", row)
print("开始时间", self.projectContext.aside_list[row].st_time_sec)
idx = self.projectContext.aside_subtitle_2contentId(self.projectContext.aside_list[row])
idx = self.projectContext.aside_subtitle_2contentId(
self.projectContext.aside_list[row])
print("对应index", idx)
if col == constant.Aside.AsideColumnNumber:
self.all_tableWidget.setItem(int(idx), constant.Content.AsideColumnNumber, QTableWidgetItem(text))
self.all_tableWidget.setItem(
int(idx), constant.Content.AsideColumnNumber, QTableWidgetItem(text))
self.projectContext.refresh_aside(row, text)
elif col == constant.Aside.SpeedColumnNumber:
self.all_tableWidget.setItem(int(idx), constant.Content.SpeedColumnNumber, QTableWidgetItem(text))
self.all_tableWidget.setItem(
int(idx), constant.Content.SpeedColumnNumber, QTableWidgetItem(text))
self.projectContext.refresh_aside_speed(row, text)
# def write2ProjectFromContent(self, item):
# print("write2ProjectFromContent")
# if item is None:
# print("WRONG!!!! item Is None")
# return
# else:
# row = item.row() # 获取行数
# col = item.column() # 获取列数 注意是column而不是col哦
# text = item.text() # 获取内容
#
# if col == constant.Content.ActivateColumn:
# self.projectContext.refresh_element(row, text)
def undo_slot(self):
"""撤销之前对表格内容的修改操作
"""
self.can_write_history = False
self.user_editing_aside = True
record = self.projectContext.history_pop()
......@@ -1125,6 +1306,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.action_redo.setEnabled(True)
def redo_slot(self):
"""重做之前撤销的对表格内容的修改操作
"""
self.can_write_history = False
self.user_editing_aside = True
record = self.projectContext.history_redo()
......@@ -1136,8 +1320,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.pb_tableWidget.setItem(
row, constant.Aside.AsideColumnNumber, item)
# debug用的,现在暂时没啥用
def view_history_slot(self):
"""查看历史操作
debug用的,现在暂时没啥用
"""
print("=="*10)
print("self.sld_video.maximumSize", self.sld_video.maximumSize(),
"self.sld_video.maximum", self.sld_video.maximum())
......@@ -1151,22 +1339,31 @@ class MainWindow(QMainWindow, Ui_MainWindow):
print("=="*10)
def change_videotime_label_slot(self):
"""改变视频的当前播放时间
"""
# print("in [change_videotime_label_slot]")
if self.player.duration() > 0: # 开始播放后才执行
self.change_videotime_label()
self.change_table_select_rows()
def change_videotime_label(self):
position = self.player.position()/1000
duration = self.player.duration()/1000
"""根据当前视频的播放时间对应改变时间标签
"""
position = self.player.position() / 1000
duration = self.player.duration() / 1000
cur_time = utils.transfer_second_to_time(str(position))[:8]
duration_time = utils.transfer_second_to_time(str(duration))[:8]
self.label_2.setText(cur_time + "/" + duration_time)
# video_timer触发timeout信号的时候,会执行该函数,修改高亮行
def change_table_select_rows(self):
"""根据视频播放时间改变当前表格的高亮行
video_timer触发timeout信号的时候,会执行该函数,修改高亮行
"""
cur_time = self.player.position() / 1000
if self.curTab == 0:
all_elements = self.projectContext.all_elements
......@@ -1188,20 +1385,32 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# break
def player_change_slot(self, new_duration: int):
# 在【打开工程】操作的时候,设置sld_video的最大值为电影秒数。
"""改变视频的时长标签
Args:
new_duration (int): 视频总时长
在【打开工程】操作的时候,设置sld_video的最大值为电影秒数。
"""
print("打开工程:self.player.duration()", self.player.duration())
print("设置最大值为:", self.player.duration()/1000 + 1)
self.sld_video.setMaximum(self.player.duration()/1000 + 1)
def refresh_tab_slot(self):
"""
刷新整个表格
"""刷新整个表格
将表格内容更新至界面中,并保存当前工程内容
"""
self.set_table_to_window(need_refresh_all=False)
self.projectContext.save_project(False)
def export_all(self):
# 存放合成音频的文件夹被命名为output
"""导出无障碍电影视频
存放合成音频的文件夹被命名为output,通过export对象中的export_slot函数完成自动渲染导出,如果此时尚未有合成音频,会弹窗提示用户。
"""
output_dir = os.path.join(self.projectContext.project_base_dir, "output")
if os.path.exists(output_dir) and len(os.listdir(output_dir)) > 0:
self.export.export_slot(self.projectContext.video_path, output_dir)
......@@ -1209,9 +1418,17 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.prompt_dialog.show_with_msg("暂时无合成音频,请至少生成一条\n 旁白音频后再尝试导出")
def show_operate_dialog(self):
"""展示“操作表格”界面
"""
self.operation_dialog.show()
def insert_aside_from_now_slot(self):
"""在当前位置插入旁白
根据当前时间找到表格中合适插入的位置,然后在对应位置添加旁白
"""
if self.player.duration() == 0 or self.projectContext.project_base_dir in [None, ""]:
self.prompt_dialog.show_with_msg("插入失败!未检测到视频或工程!")
return
......@@ -1222,13 +1439,19 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# 其实end_time目前是没啥用的,可以删掉了
print("cur_lens", len(self.projectContext.all_elements))
if idx < len(self.projectContext.all_elements) - 1:
self.add_line_operation_slot(idx, str(cur_time), self.projectContext.all_elements[idx+1].st_time_sec,
"", "插入旁白,推荐字数为0", "",self.projectContext.speaker_speed)
self.add_line_operation_slot(idx, str(cur_time), self.projectContext.all_elements[idx+1].st_time_sec, "", "插入旁白,推荐字数为0", "", self.projectContext.speaker_speed)
else:
self.add_line_operation_slot(idx, str(cur_time), str(cur_time+1),
"", "插入旁白,推荐字数为0", "",self.projectContext.speaker_speed)
self.add_line_operation_slot(idx, str(cur_time), str(cur_time+1), "", "插入旁白,推荐字数为0", "", self.projectContext.speaker_speed)
def calculate_element_row(self, cur_time: float):
"""确认表格中适合cur_time的插入位置
Args:
cur_time (float): 想要插入旁白的当前时间
def calculate_element_row(self, cur_time):
Returns:
int: 合适位置所在的行号
"""
idx = 0
while idx < len(self.projectContext.all_elements):
if float(cur_time) < float(self.projectContext.all_elements[idx].st_time_sec):
......@@ -1236,7 +1459,18 @@ class MainWindow(QMainWindow, Ui_MainWindow):
idx += 1
return idx
def add_line_operation_slot(self, row, start_time, end_time, subtitle, suggest, aside, speed):
def add_line_operation_slot(self, row: int, start_time: str, end_time: str, subtitle: str, suggest: str, aside: str, speed: str):
"""添加一行信息
Args:
row (int): 在该行后添加信息
start_time (str): 起始时间
end_time (str): 终止时间
subtitle (str): 字幕文本
suggest (str): 建议
aside (str): 旁白文本
speed (str): 旁白语速
"""
# 注意,这里需要用同一对象,不能生成多个Element
new_element = Element(start_time, end_time, subtitle, suggest, aside, speed)
self.projectContext.all_elements.insert(int(row), new_element)
......@@ -1261,8 +1495,21 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.refresh_tab_slot()
self.prompt_dialog.show_with_msg("操作成功!!请查看变化")
# 只需要改all_elements就可以了,因为是同一对象
def mod_line_operation_slot(self, row, start_time, end_time, subtitle, suggest, aside, speed):
def mod_line_operation_slot(self, row: int, start_time: str, end_time: str, subtitle: str, suggest: str, aside: str, speed: str):
"""修改一行
Args:
row (int): 行号
start_time (str): 起始时间
end_time (str): 终止时间
subtitle (str): 字幕文本
suggest (str): 建议
aside (str): 旁白文本
speed (str): 旁白语速
只需要改all_elements就可以了,因为是同一对象
"""
# 保留原来的element
to_be_modify_element = copy.deepcopy(self.projectContext.all_elements[int(row) - 1])
......@@ -1286,14 +1533,25 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.setElememtToTable(self.zm_tableWidget, elem, i)
self.projectContext.subtitle_list[i] = elem
break
self.setElememtToTable(self.all_tableWidget, elem, int(row)-1)
self.projectContext.all_elements[int(row) - 1] = elem
self.refresh_tab_slot()
self.prompt_dialog.show_with_msg("操作成功!!请查看变化")
# 只有row起作用
def del_line_operation_slot(self, row, start_time=0, end_time=0, subtitle="", suggest="", aside="", speed=""):
def del_line_operation_slot(self, row: int, start_time="0", end_time="0", subtitle="", suggest="", aside="", speed=""):
"""删除一行
Args:
row (int): 行号
start_time (str, optional): 起始时间. Defaults to "0".
end_time (str, optional): 终止时间. Defaults to "0".
subtitle (str, optional): 字幕文本. Defaults to "".
suggest (str, optional): 建议. Defaults to "".
aside (str, optional): 旁白文本. Defaults to "".
speed (str, optional): 旁白语速. Defaults to "".
"""
to_be_delete_element = self.projectContext.all_elements[int(row) - 1]
if self.projectContext.all_elements[int(row)-1].suggest is not None \
and "插入旁白,推荐字数为" in self.projectContext.all_elements[int(row)-1].suggest:
......@@ -1301,7 +1559,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
if to_be_delete_element.equalTo(self.projectContext.aside_list[i]):
self.pb_tableWidget_idx = i
# 将对应音频删除
wav_path = self.projectContext.project_base_dir + '/tmp/%.2f.wav' % float(self.projectContext.aside_list[i].st_time_sec)
wav_path = self.projectContext.project_base_dir + \
'/tmp/%.2f.wav' % float(
self.projectContext.aside_list[i].st_time_sec)
if os.path.exists(wav_path):
os.remove(wav_path)
self.projectContext.aside_list.pop(i)
......@@ -1319,6 +1579,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.prompt_dialog.show_with_msg("操作成功!!请查看变化")
def pb_item_changed_by_double_clicked_slot(self, item):
"""双击后修改旁白文本
Args:
item : 双击选中的单元格
"""
if item is None:
return
row = item.row() # 获取行数
......@@ -1331,19 +1596,26 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.user_editing_aside = True
def all_item_changed_by_double_clicked_slot(self, item):
"""暂时没有用,原本是打算用来做双击编辑的
"""
if item is None:
return
row = item.row() # 获取行数
col = item.column() # 获取列数 注意是column而不是col哦
text = item.text() # 获取内容
if col not in constant.Content.ActivateColumns:
return
print("已经set user_editing_content=True")
self.user_editing_content = True
def getTabId(self, x):
"""切换显示在界面中的表格,同时高亮当前时间在表格中的对应行
Args:
x (str): 用户点击的表格编号(0、1、2)
"""
self.curTab = int(x)
# 切换的时候,自动高亮一行
cur_time = self.player.position() / 1000
......@@ -1359,18 +1631,23 @@ class MainWindow(QMainWindow, Ui_MainWindow):
if utils.trans_to_seconds(aside_list[i].st_time_sec) <= cur_time:
self.pb_tableWidget.selectRow(i)
break
def change_audio_speed(self):
"""换语速
首先定位到待切换语速的那一行,释放当前播放的音频文件,并替换对应旁白文本的语速,同时更新字幕旁白表格中的语速,然后自动生成新的音频。
"""
combo = self.sender()
idx = self.pb_tableWidget.indexAt(combo.pos())
row = idx.row()
print("index:", row)
# 将audio_player的资源置空
print("释放当前播放的音频文件")
self.audio_player.setMedia(QMediaContent())
self.projectContext.aside_list[row].speed = combo.currentText()
# 更新字幕旁白表格里对应行的语速
all_idx = self.projectContext.aside_subtitle_2contentId(self.projectContext.aside_list[row])
all_idx = self.projectContext.aside_subtitle_2contentId(
self.projectContext.aside_list[row])
self.projectContext.all_elements[int(all_idx)].speed = combo.currentText()
self.all_tableWidget.setItem(int(all_idx), constant.Content.SpeedColumnNumber, QTableWidgetItem(combo.currentText()))
self.do_generate_audio_by_aside_row(row)
\ No newline at end of file
self.do_generate_audio_by_aside_row(int(row))
\ No newline at end of file
......@@ -273,6 +273,7 @@ class Ui_MainWindow(object):
self.sld_video.setTickInterval(1)
self.sld_video.setObjectName("sld_video")
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.scrollArea.setGeometry(QtCore.QRect(0, 0, 820, 42))
self.zm_slider_layout.addWidget(self.scrollArea)
self.lab_video = QtWidgets.QLabel(self.verticalWidget_2)
self.lab_video.setMaximumSize(QtCore.QSize(16777215, 50))
......@@ -358,8 +359,6 @@ class Ui_MainWindow(object):
self.action_3.setObjectName("action_3")
self.action_4 = QtWidgets.QAction(MainWindow)
self.action_4.setObjectName("action_4")
self.action_refresh_tab = QtWidgets.QAction(MainWindow)
self.action_refresh_tab.setObjectName("action_refresh_tab")
self.action_operate = QtWidgets.QAction(MainWindow)
self.action_operate.setObjectName("action_operate")
self.action_export = QtWidgets.QAction(MainWindow)
......@@ -386,7 +385,6 @@ class Ui_MainWindow(object):
self.menu_3.addAction(self.action_3)
self.menu_3.addAction(self.action_4)
self.menu_3.addSeparator()
self.menu_3.addAction(self.action_refresh_tab)
self.menubar.addAction(self.menu.menuAction())
self.menubar.addAction(self.menu_2.menuAction())
self.menubar.addAction(self.menu_3.menuAction())
......@@ -419,7 +417,6 @@ class Ui_MainWindow(object):
self.action_redo.setText(_translate("MainWindow", "重做"))
self.action_3.setText(_translate("MainWindow", "旁白区间检测"))
self.action_4.setText(_translate("MainWindow", "旁白音频合成"))
self.action_refresh_tab.setText(_translate("MainWindow", "刷新且保存表格"))
self.action_operate.setText(_translate("MainWindow", "操作表格"))
self.action_export.setText(_translate("MainWindow", "导出"))
self.action_insert_aside_from_now.setText(_translate("MainWindow", "当前位置插入旁白"))
......
import os.path
import threading
import traceback
import sys
import json
from enum import Enum
import datetime
import openpyxl
import constant
from openpyxl.styles import PatternFill, Alignment
from utils import replace_path_suffix
from speech_synthesis import Speaker
class RunThread(threading.Thread):
"""复写线程类,用于解决主线程无法捕捉子线程中异常的问题
"""
def __init__(self, funcName, name, args=()):
"""初始化类中的各项数据
:param funcName: 线程调用的函数名
:param name: 线程名
:param args: 传入函数的各项参数
"""
threading.Thread.__init__(self)
self._args = args
self._funcName = funcName
self._name = name
self.exitcode = 0
self.exception = None
self.exc_traceback = ''
def run(self): # Overwrite run() method, put what you want the thread do here
"""运行线程,捕捉错误并更新参数
"""
try:
self._run()
except Exception as e:
self.exitcode = 1 # 如果线程异常退出,将该标志位设置为1,正常退出为0
self.exception = e
self.exc_traceback = ''.join(
traceback.format_exception(*sys.exc_info())) # 在改成员变量中记录异常信息
print(self.exc_traceback)
def _run(self):
"""运行函数,并合理抛出异常
Raises:
e: 运行函数时遇到异常时,将该异常抛出,以便进一步处理
"""
try:
self._funcName(*self._args)
except Exception as e:
raise e
# 计划支持两种操作符,修改和删除(目前还是仅支持修改)
# todo:删除是指对字幕Element的删除(比如重复的进行合并)
class Operation(Enum):
Modify = 0
# Delete = 1
# 维护一条历史记录
# 目前仅支持对旁白做修改
class OperateRecord:
def __init__(self, row: int, operation: Operation, old: str, new: str):
self.row = row
self.operation = operation
self.old_str = old
self.new_str = new
def to_string(self)->str:
s = "{row=%d, opt=%s, oldStr=%s, newStr=%s}"%(self.row, str(self.operation), self.old_str, self.new_str)
return s
# 每一行的具体信息,"起始时间", "终止时间", "字幕", '建议', '解说脚本'
class Element:
def __init__(self, st_time_sec: str, ed_time_sec: str, subtitle, suggest, aside, speed = "1.00(4字/秒)"):
self.st_time_sec = st_time_sec
self.ed_time_sec = ed_time_sec
self.subtitle = subtitle
self.suggest = suggest
self.aside = aside
self.speed = speed
# 判断当前元素是否是字幕
def is_subtitle(self):
return self.subtitle != None and self.subtitle != ""
# 判断当前元素是否是旁白
def is_aside(self):
return not self.is_subtitle()
def to_list(self):
return [self.st_time_sec, self.ed_time_sec, self.subtitle, self.suggest, self.aside, self.speed]
def to_short_list(self):
return [self.st_time_sec, self.subtitle, self.aside, self.speed]
def to_aside_list(self):
# return [self.st_time_sec, self.ed_time_sec, self.suggest, self.aside, self.speed]
return [self.st_time_sec, self.suggest, self.aside, self.speed]
def to_subtitle_list(self):
return [self.st_time_sec, self.ed_time_sec, self.subtitle]
def print_self(self):
print("st_time_sec:",self.st_time_sec,"ed_time_sec:",self.ed_time_sec,
"subtitle:",self.subtitle,"suggest:",self.suggest, "aside:", self.aside, "speed:", self.speed)
def equalTo(self, other)->bool:
return self.st_time_sec == other.st_time_sec
class ProjectContext:
def __init__(self):
self.project_base_dir = None
self.video_path = None
self.excel_path = None
self.conf_path = 'res/conf.ini'
self.subtitle_list = []
self.aside_list = []
self.all_elements = []
self.speaker_info = None
self.speaker_speed = None
self.duration = 0
# 一些常量
self.header = ["起始时间", "终止时间", "字幕", '建议', '解说脚本', "语速"]
self.aside_header = ['起始时间', '推荐字数', '解说脚本',"语速", "预览音频"]
self.subtitle_header = ["起始时间", "终止时间", "字幕"]
self.contentHeader = ["起始时间", "字幕", "解说脚本", "语速"]
self.excel_sheet_name = "旁白插入位置建议"
self.history_records = []
self.records_pos = 0
#是否处于初始化中:在set_table_to_window时为True,主要用来做undo和redo记录操作历史的时候用到该变量,值为False才进行undo等操作。
self.initial_ing = False
self.speakers = []
self.init_speakers()
# 字幕检测进度,主要是待检测视频的初始时间
self.detected = False
self.nd_process = 0.00
self.last_time = 0.00
self.caption_boundings = []
self.has_subtitle = True
# 第一时间加载配置(这里主要是说话人的相关配置)
self.load_conf()
def clear(self):
self.subtitle_list = []
self.aside_list = []
self.all_elements = []
self.history_records = []
self.records_pos = 0
def Init(self, project_dir):
if project_dir is None or not os.path.exists(project_dir):
return
# 有的时候路径是 '/F:/out1/test.xlsx',有的时候是'F:/out1/test.xlsx'
if project_dir[0] == '/':
project_dir = project_dir[1:]
self.project_base_dir = project_dir
self.load_conf()
def load_conf(self):
this_conf_path = os.path.join(self.project_base_dir, 'conf.ini') if self.project_base_dir is not None else self.conf_path
# 如果当前工程里还没有对应的配置文件,那么选择使用全局的配置文件进行初始化,否则就使用当前工程的配置文件
if os.path.exists(this_conf_path):
self.conf_path = this_conf_path
if not os.path.exists(self.conf_path):
print("conf file does not exist, 找管理员要")
return
with open(self.conf_path, 'r', encoding='utf8') as f:
info = json.load(f)
# print(json.dumps(info, ensure_ascii=False, indent=4))
self.video_path = info["video_path"]
self.excel_path = info["excel_path"]
self.speaker_info = info["speaker_info"]["speaker_id"]
self.speaker_speed = info["speaker_info"]["speaker_speed"]
self.detected = info["detection_info"]["detected"]
self.nd_process = info["detection_info"]["nd_process"]
self.last_time = info["detection_info"]["last_time"]
self.caption_boundings = info["detection_info"]["caption_boundings"]
self.has_subtitle = info["detection_info"]["has_subtitle"]
# 当前工程下没有配置文件,就初始化一份
if self.conf_path != this_conf_path:
self.conf_path = this_conf_path
self.save_conf()
def save_conf(self):
with open(self.conf_path, 'w', encoding='utf-8') as f:
# if len(self.caption_boundings) > 0:
# print(type(self.caption_boundings[0]))
# 将context里包含的一些信息保留下来,包括工程的检测进度、检测中间产物(excel)、视频路径、说话人信息
info = {
"video_path": self.video_path,
"excel_path": self.excel_path,
"detection_info": {
"detected": self.detected,
"nd_process": self.nd_process,
"last_time": self.last_time,
"caption_boundings": self.caption_boundings,
"has_subtitle": self.has_subtitle
},
"speaker_info": {
"speaker_id": self.speaker_info,
"speaker_speed": self.speaker_speed
}
}
f.write(json.dumps(info))
def setVideoPath(self, video_path):
self.video_path = video_path
def setExcelPath(self, excel_path):
self.excel_path = excel_path
# 目前只是把excel保存到文件中
# 先备份文件,再覆盖主文件,可选是否需要备份,默认需要备份
# 20221030:添加旁白检测的进度
def save_project(self, need_save_new: bool=False) -> str:
self.save_conf()
# all_element = sorted(all_element, key=lambda x: float(x.st_time_sec))
print("current excel_path:", self.excel_path)
if self.excel_path == None:
return "保存路径为空"
if need_save_new:
new_excel_path = replace_path_suffix(self.excel_path, datetime.datetime.now().strftime('%Y%m%d%H%M%S')+'.xlsx')
err_info = save_excel_to_to_path(self.all_elements, new_excel_path, self.header, self.excel_sheet_name)
if err_info != None:
return err_info
err_info = save_excel_to_to_path(self.all_elements, self.excel_path, self.header, self.excel_sheet_name)
if err_info != None:
return err_info
return None
def refresh_aside(self, row, aside: str)->None:
self.aside_list[int(row)].aside = aside
if not self.initial_ing:
save_excel_to_to_path(self.all_elements, self.excel_path, self.header, self.excel_sheet_name)
def refresh_aside_speed(self, row, speed: str)->None:
self.aside_list[int(row)].speed = speed
if not self.initial_ing:
save_excel_to_to_path(self.all_elements, self.excel_path, self.header, self.excel_sheet_name)
def refresh_element(self, row, aside: str):
self.all_elements[int(row)].aside = aside
if not self.initial_ing:
save_excel_to_to_path(self.all_elements, self.excel_path, self.header, self.excel_sheet_name)
# 加载整个工程,填充到ProjectContext上下文中
def load_project(self):
pass
# todo: 其实现在ed_time_sec基本没有用到,所以可以忽略不计。
def load_excel_from_path(self):
d = read_sheet(self.excel_path)
self.clear()
# todo:现在是只用None判断是否是字幕,后续是否也需要用""来?
for i in range(len(d["字幕"])):
st_time_sec, ed_time_sec, subtitle, suggest, aside, speed = [d[x][i] for x in self.header]
# 当前条目是字幕
if d["字幕"][i] != None:
self.subtitle_list.append(Element(st_time_sec, ed_time_sec, subtitle, suggest, aside, speed))
self.all_elements.append(self.subtitle_list[-1])
else:
if d["起始时间"][i] is None:
if i == 0:
st_time_sec = "0.01"
else:
try:
st_time_sec = "%.2f"%(float(d["终止时间"][i-1])+0.01)
except Exception as e:
# 如果是两端连续旁白,那是没有终止时间的,需要做微调,这里是直接用上一条旁白的起始时间。
st_time_sec = "%.2f"%(float(d["起始时间"][i-1])+0.01)
else:
st_time_sec = d["起始时间"][i]
if d["终止时间"][i] is None:
# 如果是最后一条
if i == len(d["字幕"]) - 1:
ed_time_sec = "360000" if self.duration == 0 else self.duration # todo 默认最大时长是100h
else:
ed_time_sec = "%.2f"%(float(d["起始时间"][i + 1]) - 0.01)
else:
ed_time_sec = d["终止时间"][i]
self.aside_list.append(Element(st_time_sec, ed_time_sec, subtitle, suggest, aside, speed))
self.all_elements.append(self.aside_list[-1])
# print("[load_excel_from_path] ", end='')
# self.all_elements[-1].print_self()
# 现在仅支持对修改操作的记录
def history_push(self, row, old, new):
print(old, new)
if self.records_pos == len(self.history_records):
self.history_records.append(OperateRecord(row, Operation.Modify, old, new))
else:
self.history_records[self.records_pos] = OperateRecord(row, Operation.Modify, old, new)
self.records_pos += 1
def history_pop(self)-> OperateRecord:
if len(self.history_records) == 0:
return None
print(f"[history_pop] {self.history_records}, {self.records_pos}")
self.records_pos -= 1
return self.history_records[self.records_pos]
def history_redo(self)->OperateRecord:
if self.records_pos == len(self.history_records):
return None
res = self.history_records[self.records_pos]
self.records_pos += 1
return res
# 把【字幕id】或者【旁白id】转化成【内容id】
def aside_subtitle_2contentId(self, aside_element: Element)->int:
for i in range(len(self.all_elements)):
if aside_element.equalTo(self.all_elements[i]):
return i
# 报错
return None
def get_all_speaker_info(self):
"""获取所有说话人的名字、性别及年龄段等信息
用于显示在人机交互界面上,方便用户了解说话人并进行选择
"""
f = open(constant.Pathes.speaker_conf_path, encoding="utf-8")
content = json.load(f)
speaker_name = []
for speaker in content["speaker_details"]:
speaker_name.append(
",".join([speaker["name"], speaker["gender"], speaker["age_group"]]))
if self.speaker_info is None:
self.speaker_info = speaker_name[0]
return tuple(speaker_name)
def init_speakers(self):
"""初始化说话人信息
相关配置文件为"speaker.json",如果有信息需要修改,请在speaker.json中修改
"""
f = open("./res/speakers.json", encoding="utf-8")
content = json.load(f)
for speaker_info in content["speaker_details"]:
self.speakers.append(Speaker(speaker_info))
def choose_speaker(self, speaker_name: str) -> Speaker:
"""选择说话人
Args:
speaker_name (str): 用户选择的说话人名字
Returns:
Speaker: 返回对应说话人,如果没有这个说话人则报错
"""
for speaker in self.speakers:
if speaker.name == speaker_name:
return speaker
raise ValueError
def save_excel_to_to_path(all_element, new_excel_path, header, excel_sheet_name):
def save_excel_thread(all_element, new_excel_path, header, excel_sheet_name):
backup_path = None
if os.path.exists(new_excel_path):
import time
backup_path = os.path.dirname(new_excel_path) + "/tmp_"+str(time.time())+".xlsx"
# os.remove(new_excel_path)
os.rename(new_excel_path, backup_path)
try:
create_sheet(new_excel_path, "旁白插入位置建议", [header])
# for element in all_element:
# write_to_sheet(new_excel_path, excel_sheet_name, element.to_list())
write_to_sheet(new_excel_path, excel_sheet_name, all_element)
except:
exception_info = ''.join(
traceback.format_exception(*sys.exc_info()))
print("保存表格到路径[%s]失败"%(new_excel_path))
print(exception_info)
if os.path.exists(new_excel_path):
os.remove(new_excel_path)
if backup_path != None:
os.rename(backup_path,new_excel_path)
return exception_info
else:
if backup_path != None:
os.remove(backup_path)
print("保存表格到路径[%s]成功"%(new_excel_path))
return None
t = RunThread(funcName=save_excel_thread,
args=(all_element, new_excel_path, header, excel_sheet_name),
name="save_excel")
# t.setDaemon(True)
t.start()
def write_to_sheet(path: str, sheet_name: str, valuelist: list):
"""向已存在的表格中写入一行数据
Args:
path (str): 表格(book)的存储位置
sheet_name (str): excel表内的表(sheet)的名字
value (list): 要插入表内的一行数据
"""
workbook = openpyxl.load_workbook(path)
sheet = workbook.get_sheet_by_name(sheet_name)
for value_element in valuelist:
value = value_element.to_list()
index = len(value)
# 把None换成空串
value = ["" if x == None else x for x in value]
cur_row = sheet.max_row
# print("cur_row:", cur_row)
for j in range(0, index):
sheet.cell(row=cur_row + 1, column=j + 1, value=str(value[j]))
if value[j] == '' or '插入旁白' in str(value[j]):
sheet.cell(row=cur_row + 1, column=j + 1).fill = PatternFill(fill_type='solid', fgColor='ffff00')
if j == 2:
sheet.cell(row=cur_row + 1, column=j + 1).alignment = Alignment(wrapText=True)
workbook.save(path)
def create_sheet(path: str, sheet_name: str, value: list):
"""根据给定的表头,初始化表格
Args:
path (str): 表格(book)的存储位置
sheet_name (str): 表(sheet)的名字
value (list): 表头内容为['起始时间','终止时间','字幕','建议','旁白解说脚本']
"""
index = len(value)
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet.title = sheet_name
# 将字幕对应的那一列扩宽一些
sheet.column_dimensions['C'].width = 50
sheet.column_dimensions['D'].width = 30
for i in range(0, index):
for j in range(0, len(value[i])):
sheet.cell(row=i + 1, column=j + 1, value=str(value[i][j]))
workbook.save(path)
def read_sheet(book_path: str, sheet_name: str = "") -> dict:
"""读表
从表格中读出所有的内容,用dict保存(表格的格式固定,第一行为表头(起始时间|终止时间|字幕|建议|解说脚本))
Args:
book_path (str): 表格的存储路径
sheet_name (str, optional): 想要读取的表在excel表格中的名字. Defaults to "".
Returns:
dict: 表格中的所有内容,key为该列表头,value为列中的数据
"""
workbook = openpyxl.load_workbook(book_path)
sheet = workbook.active
rows = sheet.max_row
cols = sheet.max_column
sheet_content = {}
# 读取xlsx中每列的内容,默认第一行是各列的列名
for i in range(1, rows + 1):
for j in range(1, cols + 1):
if i == 1:
sheet_content[sheet.cell(1, j).value] = []
else:
sheet_content[sheet.cell(1, j).value].append(
sheet.cell(i, j).value)
return sheet_content
def create_detail_day() -> str:
"""生成当天日期
Returns:
str: 当天日期
"""
daytime = datetime.datetime.now().strftime('day' + '%Y_%m_%d')
return daytime
def make_print_to_file(path: str = './'):
"""将print的内容输出到log文件夹中
Args:
path (str, optional): 设置的log文件夹路径. Defaults to './'.
"""
if not os.path.exists(path):
os.mkdir(path)
filename = create_detail_day() + '.log'
f = open(os.path.join(path, filename), 'a', encoding='utf-8')
print("log文件夹地址", path)
sys.stdout = f
if __name__ == '__main__':
# d = read_sheet("test37second.xlsx")
# print(d["字幕"])
# print(d.keys())
# ctx = ProjectContext()
# ctx.setExcelPath("d:/123")
# print(ctx.excel_path)
e1 = Element(0,1,1,1,1)
# e1.st_time_sec = 0
e2 = Element(1,1,1,1,1)
# e2.st_time_sec = 1
all_element = [e2, e1]
all_element = sorted(all_element, key = lambda x: x.st_time_sec)
print(all_element[0].st_time_sec)
import os.path
import threading
import traceback
import sys
import json
from enum import Enum
import datetime
import openpyxl
import constant
from openpyxl.styles import PatternFill, Alignment
from utils import replace_path_suffix, transfer_second_to_time
from speech_synthesis import Speaker
class RunThread(threading.Thread):
"""复写线程类,用于解决主线程无法捕捉子线程中异常的问题
"""
def __init__(self, funcName, name, args=()):
"""初始化类中的各项数据
:param funcName: 线程调用的函数名
:param name: 线程名
:param args: 传入函数的各项参数
"""
threading.Thread.__init__(self)
self._args = args
self._funcName = funcName
self._name = name
self.exitcode = 0
self.exception = None
self.exc_traceback = ''
def run(self): # Overwrite run() method, put what you want the thread do here
"""运行线程,当运行过程中发生错误时捕捉错误信息,并更新参数,再抛出错误,从而在主界面中提醒用户出现异常
"""
try:
self._run()
except Exception as e:
self.exitcode = 1 # 如果线程异常退出,将该标志位设置为1,正常退出为0
self.exception = e
self.exc_traceback = ''.join(
traceback.format_exception(*sys.exc_info())) # 在改成员变量中记录异常信息
print("error_info", self.exc_traceback)
# raise e
def _run(self):
"""运行函数,并合理抛出异常
Raises:
e: 运行函数时遇到异常时,将该异常抛出,以便进一步处理
"""
try:
self._funcName(*self._args)
except Exception as e:
raise e
# 计划支持两种操作符,修改和删除(目前还是仅支持修改)
# todo:删除是指对字幕Element的删除(比如重复的进行合并)
class Operation(Enum):
Modify = 0
# Delete = 1
# 维护一条历史记录
# 目前仅支持对旁白做修改
class OperateRecord:
def __init__(self, row: int, operation: Operation, old: str, new: str):
self.row = row
self.operation = operation
self.old_str = old
self.new_str = new
def to_string(self)->str:
s = "{row=%d, opt=%s, oldStr=%s, newStr=%s}"%(self.row, str(self.operation), self.old_str, self.new_str)
return s
# 每一行的具体信息,"起始时间", "终止时间", "字幕", '建议', '解说脚本'
class Element:
def __init__(self, st_time_sec: str, ed_time_sec: str, subtitle, suggest, aside, speed = "1.00(4字/秒)"):
self.st_time_sec = st_time_sec
self.ed_time_sec = ed_time_sec
self.subtitle = subtitle
self.suggest = suggest
self.aside = aside
self.speed = speed
# 判断当前元素是否是字幕
def is_subtitle(self):
return self.subtitle != None and self.subtitle != ""
# 判断当前元素是否是旁白
def is_aside(self):
return not self.is_subtitle()
def to_list(self):
return [self.st_time_sec, self.ed_time_sec, self.subtitle, self.suggest, self.aside, self.speed]
def to_short_list(self):
return [self.st_time_sec, self.subtitle, self.aside, self.speed]
def to_aside_list(self):
# return [self.st_time_sec, self.ed_time_sec, self.suggest, self.aside, self.speed]
return [self.st_time_sec, self.suggest, self.aside, self.speed]
def to_subtitle_list(self):
return [self.st_time_sec, self.ed_time_sec, self.subtitle]
def print_self(self):
print("st_time_sec:",self.st_time_sec,"ed_time_sec:",self.ed_time_sec,
"subtitle:",self.subtitle,"suggest:",self.suggest, "aside:", self.aside, "speed:", self.speed)
def equalTo(self, other)->bool:
return self.st_time_sec == other.st_time_sec
class ProjectContext:
def __init__(self):
self.project_base_dir = None
self.video_path = None
self.excel_path = None
self.conf_path = 'res/conf.ini'
self.subtitle_list = []
self.aside_list = []
self.all_elements = []
self.speaker_info = None
self.speaker_speed = None
self.duration = 0
# 一些常量
self.header = ["起始时间", "终止时间", "字幕", '建议', '解说脚本', "语速"]
self.write_header = ["起始时间", "起始时间(转换后)", "终止时间", "终止时间(转换后)", "字幕", '建议', '解说脚本', "语速"]
self.aside_header = ['起始时间', '推荐字数', '解说脚本',"语速", "预览音频"]
self.subtitle_header = ["起始时间", "终止时间", "字幕"]
self.contentHeader = ["起始时间", "字幕", "解说脚本", "语速"]
self.excel_sheet_name = "旁白插入位置建议"
self.history_records = []
self.records_pos = 0
#是否处于初始化中:在set_table_to_window时为True,主要用来做undo和redo记录操作历史的时候用到该变量,值为False才进行undo等操作。
self.initial_ing = False
self.speakers = []
self.init_speakers()
# 字幕检测进度,主要是待检测视频的初始时间
self.detected = False
self.nd_process = 0.00
self.last_time = 0.00
self.caption_boundings = []
self.has_subtitle = True
# 第一时间加载配置(这里主要是说话人的相关配置)
self.load_conf()
def clear(self):
self.subtitle_list = []
self.aside_list = []
self.all_elements = []
self.history_records = []
self.records_pos = 0
def Init(self, project_dir):
if project_dir is None or not os.path.exists(project_dir):
return
# 有的时候路径是 '/F:/out1/test.xlsx',有的时候是'F:/out1/test.xlsx'
if project_dir[0] == '/':
project_dir = project_dir[1:]
self.project_base_dir = project_dir
self.load_conf()
def load_conf(self):
this_conf_path = os.path.join(self.project_base_dir, 'conf.ini') if self.project_base_dir is not None else self.conf_path
# 如果当前工程里还没有对应的配置文件,那么选择使用全局的配置文件进行初始化,否则就使用当前工程的配置文件
if os.path.exists(this_conf_path):
self.conf_path = this_conf_path
if not os.path.exists(self.conf_path):
print("conf file does not exist, 找管理员要")
return
with open(self.conf_path, 'r', encoding='utf8') as f:
info = json.load(f)
# print(json.dumps(info, ensure_ascii=False, indent=4))
self.video_path = info["video_path"]
self.excel_path = info["excel_path"]
self.speaker_info = info["speaker_info"]["speaker_id"]
self.speaker_speed = info["speaker_info"]["speaker_speed"]
self.detected = info["detection_info"]["detected"]
self.nd_process = info["detection_info"]["nd_process"]
self.last_time = info["detection_info"]["last_time"]
self.caption_boundings = info["detection_info"]["caption_boundings"]
self.has_subtitle = info["detection_info"]["has_subtitle"]
# 当前工程下没有配置文件,就初始化一份
if self.conf_path != this_conf_path:
self.conf_path = this_conf_path
self.save_conf()
def save_conf(self):
with open(self.conf_path, 'w', encoding='utf-8') as f:
# if len(self.caption_boundings) > 0:
# print(type(self.caption_boundings[0]))
# 将context里包含的一些信息保留下来,包括工程的检测进度、检测中间产物(excel)、视频路径、说话人信息
info = {
"video_path": self.video_path,
"excel_path": self.excel_path,
"detection_info": {
"detected": self.detected,
"nd_process": self.nd_process,
"last_time": self.last_time,
"caption_boundings": self.caption_boundings,
"has_subtitle": self.has_subtitle
},
"speaker_info": {
"speaker_id": self.speaker_info,
"speaker_speed": self.speaker_speed
}
}
f.write(json.dumps(info))
def setVideoPath(self, video_path):
self.video_path = video_path
def setExcelPath(self, excel_path):
self.excel_path = excel_path
# 目前只是把excel保存到文件中
# 先备份文件,再覆盖主文件,可选是否需要备份,默认需要备份
# 20221030:添加旁白检测的进度
def save_project(self, need_save_new: bool=False) -> str:
self.save_conf()
# all_element = sorted(all_element, key=lambda x: float(x.st_time_sec))
print("current excel_path:", self.excel_path)
if self.excel_path == None:
return "保存路径为空"
if need_save_new:
new_excel_path = replace_path_suffix(self.excel_path, datetime.datetime.now().strftime('%Y%m%d%H%M%S')+'.xlsx')
err_info = save_excel_to_path(self.all_elements, new_excel_path, self.write_header, self.excel_sheet_name)
if err_info != None:
return err_info
err_info = save_excel_to_path(self.all_elements, self.excel_path, self.write_header, self.excel_sheet_name)
if err_info != None:
return err_info
return None
def refresh_aside(self, row, aside: str)->None:
self.aside_list[int(row)].aside = aside
if not self.initial_ing:
save_excel_to_path(self.all_elements, self.excel_path, self.write_header, self.excel_sheet_name)
def refresh_aside_speed(self, row, speed: str)->None:
self.aside_list[int(row)].speed = speed
if not self.initial_ing:
save_excel_to_path(self.all_elements, self.excel_path, self.write_header, self.excel_sheet_name)
def refresh_element(self, row, aside: str):
self.all_elements[int(row)].aside = aside
if not self.initial_ing:
save_excel_to_path(self.all_elements, self.excel_path, self.write_header, self.excel_sheet_name)
# 加载整个工程,填充到ProjectContext上下文中
def load_project(self):
pass
# todo: 其实现在ed_time_sec基本没有用到,所以可以忽略不计。
def load_excel_from_path(self):
d = read_sheet(self.excel_path)
self.clear()
# todo:现在是只用None判断是否是字幕,后续是否也需要用""来?
for i in range(len(d["字幕"])):
st_time_sec, ed_time_sec, subtitle, suggest, aside, speed = [d[x][i] for x in self.header]
# 当前条目是字幕
if d["字幕"][i] != None:
self.subtitle_list.append(Element(st_time_sec, ed_time_sec, subtitle, suggest, aside, speed))
self.all_elements.append(self.subtitle_list[-1])
else:
if d["起始时间"][i] is None:
if i == 0:
st_time_sec = "0.01"
else:
try:
st_time_sec = "%.2f"%(float(d["终止时间"][i-1])+0.01)
except Exception as e:
# 如果是两端连续旁白,那是没有终止时间的,需要做微调,这里是直接用上一条旁白的起始时间。
st_time_sec = "%.2f"%(float(d["起始时间"][i-1])+0.01)
else:
st_time_sec = d["起始时间"][i]
if d["终止时间"][i] is None:
# 如果是最后一条
if i == len(d["字幕"]) - 1:
ed_time_sec = "360000" if self.duration == 0 else self.duration # todo 默认最大时长是100h
else:
ed_time_sec = "%.2f"%(float(d["起始时间"][i + 1]) - 0.01)
else:
ed_time_sec = d["终止时间"][i]
self.aside_list.append(Element(st_time_sec, ed_time_sec, subtitle, suggest, aside, speed))
self.all_elements.append(self.aside_list[-1])
# print("[load_excel_from_path] ", end='')
# self.all_elements[-1].print_self()
# 现在仅支持对修改操作的记录
def history_push(self, row, old, new):
print(old, new)
if self.records_pos == len(self.history_records):
self.history_records.append(OperateRecord(row, Operation.Modify, old, new))
else:
self.history_records[self.records_pos] = OperateRecord(row, Operation.Modify, old, new)
self.records_pos += 1
def history_pop(self)-> OperateRecord:
if len(self.history_records) == 0:
return None
print(f"[history_pop] {self.history_records}, {self.records_pos}")
self.records_pos -= 1
return self.history_records[self.records_pos]
def history_redo(self)->OperateRecord:
if self.records_pos == len(self.history_records):
return None
res = self.history_records[self.records_pos]
self.records_pos += 1
return res
# 把【字幕id】或者【旁白id】转化成【内容id】
def aside_subtitle_2contentId(self, aside_element: Element)->int:
for i in range(len(self.all_elements)):
if aside_element.equalTo(self.all_elements[i]):
return i
# 报错
return None
def get_all_speaker_info(self):
"""获取所有说话人的名字、性别及年龄段等信息
用于显示在人机交互界面上,方便用户了解说话人并进行选择
"""
f = open(constant.Pathes.speaker_conf_path, encoding="utf-8")
content = json.load(f)
speaker_name = []
for speaker in content["speaker_details"]:
speaker_name.append(
",".join([speaker["name"], speaker["gender"], speaker["age_group"]]))
if self.speaker_info is None:
self.speaker_info = speaker_name[0]
return tuple(speaker_name)
def init_speakers(self):
"""初始化说话人信息
相关配置文件为"speaker.json",如果有信息需要修改,请在speaker.json中修改
"""
f = open("./res/speakers.json", encoding="utf-8")
content = json.load(f)
for speaker_info in content["speaker_details"]:
self.speakers.append(Speaker(speaker_info))
def choose_speaker(self, speaker_name: str) -> Speaker:
"""选择说话人
Args:
speaker_name (str): 用户选择的说话人名字
Returns:
Speaker: 返回对应说话人,如果没有这个说话人则报错
"""
for speaker in self.speakers:
if speaker.name == speaker_name:
return speaker
raise ValueError
def save_excel_to_path(all_element, new_excel_path, header, excel_sheet_name):
def save_excel_thread(all_element, new_excel_path, header, excel_sheet_name):
backup_path = None
if os.path.exists(new_excel_path):
import time
backup_path = os.path.dirname(new_excel_path) + "/tmp_"+str(time.time())+".xlsx"
# os.remove(new_excel_path)
os.rename(new_excel_path, backup_path)
try:
create_sheet(new_excel_path, "旁白插入位置建议", [header])
# for element in all_element:
# write_to_sheet(new_excel_path, excel_sheet_name, element.to_list())
write_to_sheet(new_excel_path, excel_sheet_name, all_element)
except:
exception_info = ''.join(
traceback.format_exception(*sys.exc_info()))
print("保存表格到路径[%s]失败"%(new_excel_path))
print(exception_info)
if os.path.exists(new_excel_path):
os.remove(new_excel_path)
if backup_path != None:
os.rename(backup_path,new_excel_path)
return exception_info
else:
if backup_path != None:
os.remove(backup_path)
print("保存表格到路径[%s]成功"%(new_excel_path))
return None
t = RunThread(funcName=save_excel_thread,
args=(all_element, new_excel_path, header, excel_sheet_name),
name="save_excel")
# t.setDaemon(True)
t.start()
def write_to_sheet(path: str, sheet_name: str, valuelist: list):
"""向已存在的表格中写入一行数据
Args:
path (str): 表格(book)的存储位置
sheet_name (str): excel表内的表(sheet)的名字
value (list): 要插入表内的一行数据
"""
workbook = openpyxl.load_workbook(path)
sheet = workbook.get_sheet_by_name(sheet_name)
for value_element in valuelist:
value = value_element.to_list()
# 把None换成空串
value = ["" if x == None else x for x in value]
value.insert(1, transfer_second_to_time(value[0])) if value[0] != "" else value.insert(1, "")
value.insert(3, transfer_second_to_time(value[2])) if value[2] != "" else value.insert(3, "")
index = len(value)
cur_row = sheet.max_row
for j in range(0, index):
sheet.cell(row=cur_row + 1, column=j + 1, value=str(value[j]))
if value[j] == '' or '插入旁白' in str(value[j]):
sheet.cell(row=cur_row + 1, column=j + 1).fill = PatternFill(fill_type='solid', fgColor='ffff00')
if j == 4:
sheet.cell(row=cur_row + 1, column=j + 1).alignment = Alignment(wrapText=True)
workbook.save(path)
def create_sheet(path: str, sheet_name: str, value: list):
"""根据给定的表头,初始化表格
Args:
path (str): 表格(book)的存储位置
sheet_name (str): 表(sheet)的名字
value (list): 表头内容为['起始时间','终止时间','字幕','建议','旁白解说脚本']
"""
index = len(value)
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet.title = sheet_name
# 将字幕对应的那一列扩宽一些
sheet.column_dimensions['B'].width = 25
sheet.column_dimensions['D'].width = 25
sheet.column_dimensions['E'].width = 50
sheet.column_dimensions['F'].width = 30
for i in range(0, index):
for j in range(0, len(value[i])):
sheet.cell(row=i + 1, column=j + 1, value=str(value[i][j]))
workbook.save(path)
def read_sheet(book_path: str, sheet_name: str = "") -> dict:
"""读表
从表格中读出所有的内容,用dict保存(表格的格式固定,第一行为表头(起始时间|终止时间|字幕|建议|解说脚本))
Args:
book_path (str): 表格的存储路径
sheet_name (str, optional): 想要读取的表在excel表格中的名字. Defaults to "".
Returns:
dict: 表格中的所有内容,key为该列表头,value为列中的数据
"""
workbook = openpyxl.load_workbook(book_path)
sheet = workbook.active
rows = sheet.max_row
cols = sheet.max_column
sheet_content = {}
# 读取xlsx中每列的内容,默认第一行是各列的列名
for i in range(1, rows + 1):
for j in range(1, cols + 1):
if i == 1:
sheet_content[sheet.cell(1, j).value] = []
else:
sheet_content[sheet.cell(1, j).value].append(
sheet.cell(i, j).value)
return sheet_content
def create_detail_day() -> str:
"""生成当天日期
Returns:
str: 当天日期
"""
daytime = datetime.datetime.now().strftime('day' + '%Y_%m_%d')
return daytime
def make_print_to_file(path: str = './'):
"""将print的内容输出到log文件夹中
Args:
path (str, optional): 设置的log文件夹路径. Defaults to './'.
"""
if not os.path.exists(path):
os.mkdir(path)
filename = create_detail_day() + '.log'
f = open(os.path.join(path, filename), 'a', encoding='utf-8')
print("log文件夹地址", path)
sys.stdout = f
if __name__ == '__main__':
# d = read_sheet("test37second.xlsx")
# print(d["字幕"])
# print(d.keys())
# ctx = ProjectContext()
# ctx.setExcelPath("d:/123")
# print(ctx.excel_path)
e1 = Element(0,1,1,1,1)
# e1.st_time_sec = 0
e2 = Element(1,1,1,1,1)
# e2.st_time_sec = 1
all_element = [e2, e1]
all_element = sorted(all_element, key = lambda x: x.st_time_sec)
print(all_element[0].st_time_sec)
import sys
from PyQt5.QtWidgets import (QWidget, QTableWidget, QHBoxLayout, QApplication, QTableWidgetItem, QAbstractItemView)
from PyQt5 import QtCore
class myTableWidget(QWidget):
def __init__(self):
super(myTableWidget,self).__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("QTableWidget演示")
self.resize(430, 230);
layout = QHBoxLayout()
tablewidget = QTableWidget()
tablewidget.setRowCount(4)
tablewidget.setColumnCount(6)
layout.addWidget(tablewidget)
tablewidget.setHorizontalHeaderLabels(['时间','文字'])
nameItem = QTableWidgetItem("0:00")
tablewidget.setItem(0,0,nameItem)
ageItem = QTableWidgetItem("24")
tablewidget.setItem(0,1,ageItem)
jgItem = QTableWidgetItem("北京")
jgItem.setFlags(QtCore.Qt.ItemIsEnabled)
tablewidget.setItem(0,2,jgItem)
# 禁止编辑
# tablewidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
# 整行选择
tablewidget.setSelectionBehavior(QAbstractItemView.SelectRows)
# 调整列和行
tablewidget.resizeColumnsToContents()
tablewidget.resizeRowsToContents()
# tablewidget.horizontalHeader().setVisible(False)
# tablewidget.verticalHeader().setVisible(False)
tablewidget.setVerticalHeaderLabels(["a","b"])
# 隐藏表格线
tablewidget.setShowGrid(False)
self.setLayout(layout)
tablewidget.itemClicked.connect(self.show_data)
def show_data(self, Item):
# 如果单元格对象为空
if Item is None:
return
else:
row = Item.row() # 获取行数
col = Item.column() # 获取列数 注意是column而不是col哦
text = Item.text() # 获取内容
# 输出测试
print('row = ', row)
print('col =', col)
print('text = ', text)
if __name__ == '__main__':
app = QApplication(sys.argv)
example = myTableWidget()
example.show()
sys.exit(app.exec_())
import sys
import os
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import utils
from operation_dialog_ui import Ui_Dialog
# from main_window import MainWindow
# todo 注意,删除行,添加行,暂不支持【撤销与重做】功能!!!
class Operation_Dialog(QDialog, Ui_Dialog):
# 开始检测信号,传参分别是movie路径和输出表格路径
start_add_signal = pyqtSignal(str, str, str, str, str, str, str)
start_mod_signal = pyqtSignal(str, str, str, str, str, str, str)
start_del_signal = pyqtSignal(str, str, str, str, str, str, str)
def __init__(self, mainWindow):
super(Operation_Dialog, self).__init__()
self.setupUi(self)
self.setWindowTitle("操作表格")
self.mainWindow = mainWindow
self.pushButton.clicked.connect(self.check_validate_slot)
self.pushButton_2.clicked.connect(self.remake_slot)
# 如果是【修改一行】,选择行的时候要瞬间更新成目前行的内容
self.pushButton_3.clicked.connect(self.fill_row_info_slot)
self.pickStartPos.clicked.connect(self.pick_start_pos_slot)
self.pickEndPos.clicked.connect(self.pick_end_pos_slot)
self.buttonBox.setEnabled(False)
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(
self.start_operation_slot)
# 字幕/旁白 选择框
self.comboBox.currentIndexChanged.connect(self.zmpb_change_slot)
# 增加一行/删除一行 选择框
self.comboBox_2.currentIndexChanged.connect(self.adddel_change_slot)
self.speed_list = ["1.00(4字/秒)", "1.10(4.5字/秒)", "1.25(5字/秒)",
"1.50(6字/秒)", "1.75(7字/秒)", "2.00(8字/秒)", "2.50(10字/秒)"]
self.comboBox_3.addItems(self.speed_list)
self.lineEdits = [self.lineEdit, self.lineEdit_4, self.lineEdit_5, self.lineEdit_6]
self.timeEdits = [self.timeEdit, self.timeEdit_2]
self.zmpb_change_slot()
self.adddel_change_slot()
def pick_start_pos_slot(self):
time = utils.transfer_second_to_time(
str(self.mainWindow.player.position()/1000))
st_time = QTime.fromString(time, "hh:mm:ss.zzz")
print("st_time", st_time.hour(), st_time.minute(), st_time.second(), st_time.msec())
self.timeEdit.setTime(st_time)
def pick_end_pos_slot(self):
time = utils.transfer_second_to_time(
str(self.mainWindow.player.position()/1000))
self.timeEdit_2.setTime(QTime.fromString(time, "hh:mm:ss.zzz"))
def zmpb_change_slot(self):
# 如果是删除,则直接return
if self.comboBox_2.currentText() == "删除一行":
return
self.set_all_user_component_status(True)
self.pickStartPos.setEnabled(True)
self.pickEndPos.setEnabled(True)
self.pushButton_3.setEnabled(True)
if self.comboBox.currentText() == "字幕":
self.lineEdit_5.setEnabled(False)
self.lineEdit_6.setEnabled(False)
self.comboBox_3.setEnabled(False)
else:
self.timeEdit_2.setEnabled(False)
self.lineEdit_4.setEnabled(False)
self.lineEdit_5.setEnabled(False)
# 如果是删除,则只需要【行数】即可
def adddel_change_slot(self):
if self.comboBox_2.currentText() in ["增加一行", "修改一行"]:
self.zmpb_change_slot()
else:
for i in range(1, len(self.lineEdits)):
self.lineEdits[i].setEnabled(False)
for timeEdit in self.timeEdits:
timeEdit.setEnabled(False)
# 修改完后需要重新检测
def remake_slot(self):
self.buttonBox.setEnabled(False)
self.set_all_user_component_status(True)
self.pickStartPos.setEnabled(True)
self.pickEndPos.setEnabled(True)
self.pushButton_3.setEnabled(True)
self.zmpb_change_slot()
self.adddel_change_slot()
def check_validate_slot(self):
# 校验行数
# todo 用tableWidget还是用all_element?
rowCount = self.mainWindow.all_tableWidget.rowCount()
try:
row_number = int(self.lineEdit.text())
assert 1 <= row_number <= rowCount
except Exception as e:
self.mainWindow.prompt_dialog.show_with_msg(
"校验失败!总行数为[%d],你的输入为[%s]!!" % (rowCount, self.lineEdit.text()))
return False
# 校验时间填写是否是hh:mm:ss格式的
try:
time = self.timeEdit.time()
import re
if type(time) == QTime and len(time.toString("hh:mm:ss.zzz")) > 0:
x = re.match(
"^(([0-1]\d)|(2[0-4])):[0-5]\d:[0-5]\d(.\d{1,3})?$", time.toString("hh:mm:ss.zzz"))
assert x != None
time = self.timeEdit_2.time()
if type(time) == QTime and len(time.toString("hh:mm:ss.zzz")) > 0:
x = re.match(
"^(([0-1]\d)|(2[0-4])):[0-5]\d:[0-5]\d(.\d{1,3})?$", time.toString("hh:mm:ss.zzz"))
assert x != None
except Exception as e:
self.mainWindow.prompt_dialog.show_with_msg(
"校验失败!起始或结束时间输入的格式有误!应该为hh:mm:ss.zzz!!")
return False
list_idx = row_number-1
# 这些是只有【add】才需要检测的
if self.comboBox_2.currentText() in ["增加一行", "修改一行"]:
# 校验起始时间、结束时间
start_time_f, end_time_f = 0.0, 0.0
try:
print("start_time_f:", self.timeEdit.time().toString("hh:mm:ss.zzz"))
start_time_f = float(
utils.trans_to_seconds(self.timeEdit.time().toString("hh:mm:ss.zzz")))
if self.comboBox.currentText() == "字幕":
end_time_f = float(
utils.trans_to_seconds(self.timeEdit_2.time().toString("hh:mm:ss.zzz")))
assert start_time_f < end_time_f
if row_number < rowCount:
# 要求结束时间在下一行的起始时间前面,对增加和修改都适用
assert float(self.mainWindow.projectContext.all_elements[list_idx+1].st_time_sec) >= end_time_f
# 要求起始时间在上一行后面,在下一行前面
if self.comboBox_2.currentText() == "增加一行":
if row_number >= 1:
assert float(self.mainWindow.projectContext.all_elements[list_idx].st_time_sec) < start_time_f
if row_number < rowCount:
assert float(self.mainWindow.projectContext.all_elements[list_idx+1].st_time_sec) > start_time_f
elif self.comboBox_2.currentText() == "修改一行":
if row_number > 1:
assert float(self.mainWindow.projectContext.all_elements[list_idx-1].st_time_sec) < start_time_f
if row_number < rowCount:
assert float(self.mainWindow.projectContext.all_elements[list_idx+1].st_time_sec) > start_time_f
# 要求start_time_f小于总时长
assert start_time_f < self.mainWindow.player.duration() / 1000
except Exception as e:
import traceback
exc_traceback = ''.join(
traceback.format_exception(*sys.exc_info()))
print("[Catch Exception in start.py]", exc_traceback)
self.mainWindow.prompt_dialog.show_with_msg(
"校验失败!起始时间或结束时间输入有误!!")
return False
# 校验推荐字数
# if self.comboBox.currentText() == "旁白":
# try:
# suggest_words_count = int(self.lineEdit_5.text())
# assert suggest_words_count <= 100
# except Exception as e:
# self.mainWindow.prompt_dialog.show_with_msg(
# "校验失败!推荐字数填入有误!!")
# return False
# 这些是只有 [modify] | [delete]才需要检测的
if self.comboBox_2.currentText() != "增加一行":
try:
suggest = self.mainWindow.projectContext.all_elements[row_number-1].suggest
print("suggest:", suggest)
# 如果当前行是旁白
if suggest is not None and "插入旁白" in suggest:
assert self.comboBox.currentText() == "旁白"
else:
assert self.comboBox.currentText() == "字幕"
except Exception as e:
self.mainWindow.prompt_dialog.show_with_msg(
"校验失败!待操作的行不是[%s]" % (self.comboBox.currentText()))
return False
# 检测通过
self.mainWindow.prompt_dialog.show_with_msg("校验成功!!")
self.buttonBox.setEnabled(True)
self.set_all_user_component_status(False)
self.pickStartPos.setEnabled(False)
self.pickEndPos.setEnabled(False)
self.pushButton_3.setEnabled(False)
def set_all_user_component_status(self, status: bool):
for lineEdit in self.lineEdits:
lineEdit.setEnabled(status)
for timeEdit in self.timeEdits:
timeEdit.setEnabled(status)
self.comboBox.setEnabled(status)
self.comboBox_2.setEnabled(status)
self.comboBox_3.setEnabled(status)
def start_operation_slot(self):
start_time, end_time = ["%02d:%02d:%02d.%03d" % (x.time().hour(), x.time().minute(), x.time().second(), x.time().msec()) for x in self.timeEdits]
row, subtitle, suggest, aside = [
x.text() for x in self.lineEdits]
speed = self.comboBox_3.currentText()
# 将hh:mm:ss转成秒的形式传给mainWindow
if type(start_time) == str and len(start_time) > 0:
start_time = str(utils.trans_to_seconds(start_time))
if type(end_time) == str and len(end_time) > 0:
end_time = str(utils.trans_to_seconds(end_time))
if self.comboBox.currentText() == "字幕":
suggest = ""
aside = ""
else: # 如果是旁白
end_time = ""
subtitle = ""
suggest = "插入旁白,推荐字数为" + suggest
if self.comboBox_2.currentText() == "增加一行":
suggest = "插入旁白,推荐字数为0"
# 别忘复原
self.buttonBox.setEnabled(False)
self.zmpb_change_slot()
self.adddel_change_slot()
# 根据增删两种操作,分别触发不同信号。
if self.comboBox_2.currentText() == "增加一行":
self.start_add_signal.emit(
row, start_time, end_time, subtitle, suggest, aside, speed)
elif self.comboBox_2.currentText() == "修改一行":
self.start_mod_signal.emit(
row, start_time, end_time, subtitle, suggest, aside, speed)
else:
self.start_del_signal.emit(
row, start_time, end_time, subtitle, suggest, aside, speed)
def fill_row_info_slot(self):
text = self.lineEdit.text()
print("fill_row_info_slot, text:", text)
if text is not None:
try:
rowCount = self.mainWindow.all_tableWidget.rowCount()
row_number = int(self.lineEdit.text())
assert 1 <= row_number <= rowCount
elem = self.mainWindow.projectContext.all_elements[int(
row_number)-1]
# todo, 改成可以spinbox的那种形式
self.timeEdit.setTime(QTime.fromString(
str(utils.transfer_second_to_time(elem.st_time_sec)), "hh:mm:ss.zzz"))
self.timeEdit_2.setTime(QTime.fromString(str(utils.transfer_second_to_time(
elem.ed_time_sec)), "hh:mm:ss.zzz")) if len(elem.ed_time_sec) > 0 else self.timeEdit_2.setEnabled(False)
self.lineEdit_4.setText(elem.subtitle)
self.lineEdit_6.setText(elem.aside)
self.comboBox_3.setCurrentIndex(
self.speed_list.index(elem.speed))
# 如果是旁白的话
if elem.suggest is not None and "推荐字数为" in elem.suggest:
self.lineEdit_5.setText(
elem.suggest[elem.suggest.index("推荐字数为")+5:])
except Exception as e:
print("exception:", e)
pass
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("./res/images/eagle_2.ico"))
dialog = Operation_Dialog()
dialog.show()
sys.exit(app.exec_())
import sys
import os
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import utils
from operation_dialog_ui import Ui_Dialog
# from main_window import MainWindow
# todo 注意,删除行,添加行,暂不支持【撤销与重做】功能!!!
class Operation_Dialog(QDialog, Ui_Dialog):
# 开始检测信号,传参分别是movie路径和输出表格路径
start_add_signal = pyqtSignal(str, str, str, str, str, str, str)
start_mod_signal = pyqtSignal(str, str, str, str, str, str, str)
start_del_signal = pyqtSignal(str, str, str, str, str, str, str)
def __init__(self, mainWindow):
super(Operation_Dialog, self).__init__()
self.setupUi(self)
self.setWindowTitle("操作表格")
self.mainWindow = mainWindow
self.pushButton.clicked.connect(self.check_validate_slot)
self.pushButton_2.clicked.connect(self.remake_slot)
# 如果是【修改一行】,选择行的时候要瞬间更新成目前行的内容
self.pushButton_3.clicked.connect(self.fill_row_info_slot)
self.pickStartPos.clicked.connect(self.pick_start_pos_slot)
self.pickEndPos.clicked.connect(self.pick_end_pos_slot)
self.buttonBox.setEnabled(False)
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(
self.start_operation_slot)
# 字幕/旁白 选择框
self.comboBox.currentIndexChanged.connect(self.zmpb_change_slot)
# 增加一行/删除一行 选择框
self.comboBox_2.currentIndexChanged.connect(self.adddel_change_slot)
self.speed_list = ["1.00(4字/秒)", "1.10(4.5字/秒)", "1.25(5字/秒)",
"1.50(6字/秒)", "1.75(7字/秒)", "2.00(8字/秒)", "2.50(10字/秒)"]
self.comboBox_3.addItems(self.speed_list)
self.lineEdits = [self.lineEdit, self.lineEdit_4, self.lineEdit_5, self.lineEdit_6]
self.timeEdits = [self.timeEdit, self.timeEdit_2]
self.zmpb_change_slot()
self.adddel_change_slot()
def pick_start_pos_slot(self):
"""选择当前时间为起始时间
"""
time = utils.transfer_second_to_time(str(self.mainWindow.player.position()/1000))
st_time = QTime.fromString(time, "hh:mm:ss.zzz")
print("st_time", st_time.hour(), st_time.minute(), st_time.second(), st_time.msec())
self.timeEdit.setTime(st_time)
def pick_end_pos_slot(self):
"""选择当前时间为终止时间
"""
time = utils.transfer_second_to_time(
str(self.mainWindow.player.position()/1000))
self.timeEdit_2.setTime(QTime.fromString(time, "hh:mm:ss.zzz"))
def zmpb_change_slot(self):
"""对界面中相关组件进行“可选”或“不可选”的更改
更改如下:
- “删除一行”时,不做任何更改
- 一般将所有的用户可修改控件置为可选
- 如果用户选择的是“字幕”,则将推荐字数、旁白文本和语速置为不可更改模式
- 如果用户选择的是“旁白”,就将终止时间、字幕文本和推荐字数置为不可更改模式
"""
# 如果是删除,则直接return
if self.comboBox_2.currentText() == "删除一行":
return
self.set_all_user_component_status(True)
self.pickStartPos.setEnabled(True)
self.pickEndPos.setEnabled(True)
self.pushButton_3.setEnabled(True)
if self.comboBox.currentText() == "字幕":
self.lineEdit_5.setEnabled(False)
self.lineEdit_6.setEnabled(False)
self.comboBox_3.setEnabled(False)
else:
self.timeEdit_2.setEnabled(False)
self.lineEdit_4.setEnabled(False)
self.lineEdit_5.setEnabled(False)
# 如果是删除,则只需要【行数】即可
def adddel_change_slot(self):
"""针对三种模式,分别对修改界面中相关控件进行“可选”和“不可选”的切换
进行了如下更改:
- 增加一行和修改一行时使用zmpb_change_slot直接进行切换
- 删除一行时,将除行号之外的其他可编辑控件均置为不可操作模式
"""
if self.comboBox_2.currentText() in ["增加一行", "修改一行"]:
self.zmpb_change_slot()
else:
for i in range(1, len(self.lineEdits)):
self.lineEdits[i].setEnabled(False)
for timeEdit in self.timeEdits:
timeEdit.setEnabled(False)
def remake_slot(self):
"""用户完成某一操作的修改后,将界面中的控件重新置为初始可选状态
"""
self.buttonBox.setEnabled(False)
self.set_all_user_component_status(True)
self.pickStartPos.setEnabled(True)
self.pickEndPos.setEnabled(True)
self.pushButton_3.setEnabled(True)
self.zmpb_change_slot()
self.adddel_change_slot()
def check_validate_slot(self) -> bool:
"""确认输入的待更改信息是否正确
Returns:
bool: True or False
"""
# 校验行数
# todo 用tableWidget还是用all_element?
rowCount = self.mainWindow.all_tableWidget.rowCount()
try:
row_number = int(self.lineEdit.text())
assert 1 <= row_number <= rowCount
except Exception as e:
self.mainWindow.prompt_dialog.show_with_msg(
"校验失败!总行数为[%d],你的输入为[%s]!!" % (rowCount, self.lineEdit.text()))
return False
# 校验时间填写是否是hh:mm:ss格式的
try:
time = self.timeEdit.time()
import re
if type(time) == QTime and len(time.toString("hh:mm:ss.zzz")) > 0:
x = re.match(
"^(([0-1]\d)|(2[0-4])):[0-5]\d:[0-5]\d(.\d{1,3})?$", time.toString("hh:mm:ss.zzz"))
assert x != None
time = self.timeEdit_2.time()
if type(time) == QTime and len(time.toString("hh:mm:ss.zzz")) > 0:
x = re.match(
"^(([0-1]\d)|(2[0-4])):[0-5]\d:[0-5]\d(.\d{1,3})?$", time.toString("hh:mm:ss.zzz"))
assert x != None
except Exception as e:
self.mainWindow.prompt_dialog.show_with_msg(
"校验失败!起始或结束时间输入的格式有误!应该为hh:mm:ss.zzz!!")
return False
list_idx = row_number-1
# 这些是只有【add】才需要检测的
if self.comboBox_2.currentText() in ["增加一行", "修改一行"]:
# 校验起始时间、结束时间
start_time_f, end_time_f = 0.0, 0.0
try:
print("start_time_f:", self.timeEdit.time().toString("hh:mm:ss.zzz"))
start_time_f = float(
utils.trans_to_seconds(self.timeEdit.time().toString("hh:mm:ss.zzz")))
if self.comboBox.currentText() == "字幕":
end_time_f = float(
utils.trans_to_seconds(self.timeEdit_2.time().toString("hh:mm:ss.zzz")))
assert start_time_f < end_time_f
if row_number < rowCount:
# 要求结束时间在下一行的起始时间前面,对增加和修改都适用
assert float(self.mainWindow.projectContext.all_elements[list_idx+1].st_time_sec) >= end_time_f
# 要求起始时间在上一行后面,在下一行前面
if self.comboBox_2.currentText() == "增加一行":
if row_number >= 1:
assert float(self.mainWindow.projectContext.all_elements[list_idx].st_time_sec) < start_time_f
if row_number < rowCount:
assert float(self.mainWindow.projectContext.all_elements[list_idx+1].st_time_sec) > start_time_f
elif self.comboBox_2.currentText() == "修改一行":
if row_number > 1:
assert float(self.mainWindow.projectContext.all_elements[list_idx-1].st_time_sec) < start_time_f
if row_number < rowCount:
assert float(self.mainWindow.projectContext.all_elements[list_idx+1].st_time_sec) > start_time_f
# 要求start_time_f小于总时长
assert start_time_f < self.mainWindow.player.duration() / 1000
except Exception as e:
import traceback
exc_traceback = ''.join(
traceback.format_exception(*sys.exc_info()))
print("[Catch Exception in start.py]", exc_traceback)
self.mainWindow.prompt_dialog.show_with_msg(
"校验失败!起始时间或结束时间输入有误!!")
return False
# 校验推荐字数
# if self.comboBox.currentText() == "旁白":
# try:
# suggest_words_count = int(self.lineEdit_5.text())
# assert suggest_words_count <= 100
# except Exception as e:
# self.mainWindow.prompt_dialog.show_with_msg(
# "校验失败!推荐字数填入有误!!")
# return False
# 这些是只有 [modify] | [delete]才需要检测的
if self.comboBox_2.currentText() != "增加一行":
try:
suggest = self.mainWindow.projectContext.all_elements[row_number-1].suggest
print("suggest:", suggest)
# 如果当前行是旁白
if suggest is not None and "插入旁白" in suggest:
assert self.comboBox.currentText() == "旁白"
else:
assert self.comboBox.currentText() == "字幕"
except Exception as e:
self.mainWindow.prompt_dialog.show_with_msg(
"校验失败!待操作的行不是[%s]" % (self.comboBox.currentText()))
return False
# 检测通过
self.mainWindow.prompt_dialog.show_with_msg("校验成功!!")
self.buttonBox.setEnabled(True)
self.set_all_user_component_status(False)
self.pickStartPos.setEnabled(False)
self.pickEndPos.setEnabled(False)
self.pushButton_3.setEnabled(False)
def set_all_user_component_status(self, status: bool):
"""将所有用户可操作控件置为同一状态
Args:
status (bool): 想要切换的状态,True or False.
"""
for lineEdit in self.lineEdits:
lineEdit.setEnabled(status)
for timeEdit in self.timeEdits:
timeEdit.setEnabled(status)
self.comboBox.setEnabled(status)
self.comboBox_2.setEnabled(status)
self.comboBox_3.setEnabled(status)
def start_operation_slot(self):
"""执行用户提交的操作
"""
start_time, end_time = ["%02d:%02d:%02d.%03d" % (x.time().hour(), x.time().minute(), x.time().second(), x.time().msec()) for x in self.timeEdits]
row, subtitle, suggest, aside = [
x.text() for x in self.lineEdits]
speed = self.comboBox_3.currentText()
# 将hh:mm:ss转成秒的形式传给mainWindow
if type(start_time) == str and len(start_time) > 0:
start_time = str(utils.trans_to_seconds(start_time))
if type(end_time) == str and len(end_time) > 0:
end_time = str(utils.trans_to_seconds(end_time))
if self.comboBox.currentText() == "字幕":
suggest = ""
aside = ""
else: # 如果是旁白
end_time = ""
subtitle = ""
suggest = "插入旁白,推荐字数为" + suggest
if self.comboBox_2.currentText() == "增加一行":
suggest = "插入旁白,推荐字数为0"
# 别忘复原
self.buttonBox.setEnabled(False)
self.zmpb_change_slot()
self.adddel_change_slot()
# 根据增删两种操作,分别触发不同信号。
if self.comboBox_2.currentText() == "增加一行":
self.start_add_signal.emit(
row, start_time, end_time, subtitle, suggest, aside, speed)
elif self.comboBox_2.currentText() == "修改一行":
self.start_mod_signal.emit(
row, start_time, end_time, subtitle, suggest, aside, speed)
else:
self.start_del_signal.emit(
row, start_time, end_time, subtitle, suggest, aside, speed)
def fill_row_info_slot(self):
"""自动填入该行的相关信息
"""
text = self.lineEdit.text()
print("fill_row_info_slot, text:", text)
if text is not None:
try:
rowCount = self.mainWindow.all_tableWidget.rowCount()
row_number = int(self.lineEdit.text())
assert 1 <= row_number <= rowCount
elem = self.mainWindow.projectContext.all_elements[int(
row_number)-1]
# todo, 改成可以spinbox的那种形式
self.timeEdit.setTime(QTime.fromString(
str(utils.transfer_second_to_time(elem.st_time_sec)), "hh:mm:ss.zzz"))
self.timeEdit_2.setTime(QTime.fromString(str(utils.transfer_second_to_time(
elem.ed_time_sec)), "hh:mm:ss.zzz")) if len(elem.ed_time_sec) > 0 else self.timeEdit_2.setEnabled(False)
self.lineEdit_4.setText(elem.subtitle)
self.lineEdit_6.setText(elem.aside)
self.comboBox_3.setCurrentIndex(
self.speed_list.index(elem.speed))
# 如果是旁白的话
if elem.suggest is not None and "推荐字数为" in elem.suggest:
self.lineEdit_5.setText(
elem.suggest[elem.suggest.index("推荐字数为")+5:])
except Exception as e:
print("exception:", e)
pass
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("./res/images/eagle_2.ico"))
dialog = Operation_Dialog()
dialog.show()
sys.exit(app.exec_())
def f(a,b,c,d):
print(a,b,c,d)
# f([1,2,3))
data = [ 'ACME', 50, 91.1, (2012, 12, 21) ]
name, shares, price, date = data
print(name)
from enum import Enum
class Operation(Enum):
Modify = 0
Delete = 1
# print(Operation.Delete)
print(Operation(('修改', "xiugai")))
\ No newline at end of file
import sys
import os
from PyQt5.QtCore import *;
from PyQt5.QtGui import *;
from PyQt5.QtWidgets import *;
from prompt_dialog_ui import Ui_Dialog
from utils import validate_and_get_filepath
"""
通用类
提示型,文本框
想要显示本提示框时,直接发送show_dialog_signal信号,并且把想要提示的内容作为传递过来即可。
"""
class Prompt_Dialog(QDialog, Ui_Dialog):
#开始检测信号,传参分别是movie路径和输出表格路径
show_dialog_signal = pyqtSignal(str)
def __init__(self, ok_func = None):
super(Prompt_Dialog, self).__init__()
self.setupUi(self)
self.setWindowTitle("提示框")
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("确认")
self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
self.show_dialog_signal.connect(self.show_with_msg)
if ok_func != None:
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(ok_func)
def show_with_msg(self, msg):
self.label.setText(msg)
import sys
import os
from PyQt5.QtCore import *;
from PyQt5.QtGui import *;
from PyQt5.QtWidgets import *;
from prompt_dialog_ui import Ui_Dialog
from utils import validate_and_get_filepath
"""
通用类
提示型,文本框
想要显示本提示框时,直接发送show_dialog_signal信号,并且把想要提示的内容作为传递过来即可。
"""
class Prompt_Dialog(QDialog, Ui_Dialog):
#开始检测信号,传参分别是movie路径和输出表格路径
show_dialog_signal = pyqtSignal(str)
def __init__(self, ok_func = None):
super(Prompt_Dialog, self).__init__()
self.setupUi(self)
self.setWindowTitle("提示框")
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("确认")
self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
self.show_dialog_signal.connect(self.show_with_msg)
if ok_func != None:
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(ok_func)
def show_with_msg(self, msg: str):
"""在弹窗中展示提示语句
Args:
msg (str): 提示语句
"""
self.label.setText(msg)
self.show()
\ No newline at end of file
......@@ -32,15 +32,33 @@
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>60</x>
<y>30</y>
<width>291</width>
<x>50</x>
<y>40</y>
<width>300</width>
<height>71</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>请输入文字</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</widget>
<resources/>
......
......@@ -20,6 +20,7 @@ class Ui_Dialog(object):
self.buttonBox.setObjectName("buttonBox")
self.label = QtWidgets.QLabel(Dialog)
self.label.setGeometry(QtCore.QRect(60, 30, 291, 71))
self.label.setWordWrap(True)
self.label.setObjectName("label")
self.retranslateUi(Dialog)
......
paddlepaddle==2.2.0
paddleocr==2.3.0.1
qt_material
soundfile
azure-cognitiveservices-speech==1.19.0
openpyxl
opencv-python
PyQt5
\ No newline at end of file
cn2an==0.5.16
Flask==2.0.2
flask_cors==3.0.10
ijson==3.2.1
LAC==2.1.2
librosa==0.8.1
numpy==1.19.3
onnxruntime==1.14.0
opencv_python==4.4.0.46
openpyxl==3.0.9
paddle==1.0.2
paddleocr==2.3.0.1
paddlepaddle==2.2.0
paddlespeech==0.1.0
parakeet==0.24
Pillow==9.5.0
pyaudio==0.2.13
pyclipper==1.3.0
pydub==0.25.1
PyQt5==5.15.9
PyYAML==6.0
qt_material==2.12
resampy==0.2.2
scipy==1.3.0
Shapely==1.8.0
SoundFile==0.10.2
tqdm==4.62.3
visualdl==2.2.1
webrtcvad==2.0.10
yacs==0.1.8
zhconv==1.4.3
......@@ -41,6 +41,11 @@ class Setting_Dialog(QDialog, Ui_Dialog):
self.pushButton.clicked.connect(self.play_audio_slot)
def content_fresh(self):
"""刷新界面中的内容
将工程信息中的说话人信息、说话人语速更新到界面中,如果未选择则初始化为第一个选项
"""
if self.projectContext.speaker_info is None:
self.comboBox.setCurrentIndex(0)
else:
......@@ -51,16 +56,26 @@ class Setting_Dialog(QDialog, Ui_Dialog):
self.comboBox_2.setCurrentIndex(self.speed_li_2.index(self.projectContext.speaker_speed))
def speaker_change_slot(self):
"""切换说话人
将当前的说话人设置为工程的说话人,并保存到配置文件中
"""
self.projectContext.speaker_info = self.comboBox.currentText()
self.projectContext.save_conf()
# print("self.projectContext.speaker_info:", self.projectContext.speaker_info)
def speed_change_slot(self):
"""切换语速
将当前的语速设置为工程的语速,并保存到配置文件中
"""
self.projectContext.speaker_speed = self.comboBox_2.currentText()
self.projectContext.save_conf()
def play_audio_slot(self):
"""播放说话人的样例音频
"""新起一个线程来播放说话人的样例音频
根据用户选择的说话人更新对应样例音频
停止上一个说话人的音频
......@@ -75,13 +90,21 @@ class Setting_Dialog(QDialog, Ui_Dialog):
audioPlayed = winsound.PlaySound(chosenSpeaker.voice_example, winsound.SND_ASYNC)
thread_it(f, name="playAudio")
# 重写关闭窗口事件,使得可以停止播放样例视频
def closeEvent(self, event):
"""重写关闭窗口事件,使得可以停止播放样例视频
Args:
event: 关闭窗口事件
"""
global audioPlayed
winsound.PlaySound(audioPlayed, winsound.SND_PURGE)
event.accept()
def showDialog(self):
"""刷新信息并展示窗口
"""
self.content_fresh()
self.show()
......
......@@ -14,6 +14,7 @@
from speech_synthesis import ss_and_export
ss_and_export(video_path, sheet_path, audio_dir, speed, caption_path, speaker_name, state)
"""
import json
import os
......@@ -25,6 +26,7 @@ import numpy as np
from azure.cognitiveservices.speech import SpeechConfig, SpeechSynthesizer, ResultReason, AudioDataStream
from azure.cognitiveservices.speech.audio import AudioOutputConfig
import openpyxl
import shutil
tmp_file = 'tmp.wav'
adjusted_wav_path = "adjusted.wav"
......@@ -58,7 +60,7 @@ def init_speakers():
相关配置文件为"speaker.json",如果有信息需要修改,请在speaker.json中修改
"""
f = open("speakers.json", encoding="utf-8")
f = open("./res/speakers.json", encoding="utf-8")
content = json.load(f)
global speakers
for speaker_info in content["speaker_details"]:
......@@ -82,12 +84,15 @@ def choose_speaker(speaker_name: str) -> Speaker:
def speech_synthesis(text: str, output_file: str, speaker: Speaker, speed: float = 1.0):
"""用于合成讲解音频并输出
分为两步走:第一步在项目文件夹生成tmp.wav;第二步在output_file路径下生成调整音量和语速后的音频
分为两步走:第一步在项目文件夹生成tmp.wav;第二步在output_file路径下生成调整音量和语速后的音频
Args:
text (str): 解说文本
output_file (str): 输出文件路径
speaker (Speaker): 说话人
speed (float, optional): 指定的音频语速. Defaults to 1.0.
"""
speech_config = SpeechConfig(
subscription="db34d38d2d3447d482e0f977c66bd624",
......@@ -143,13 +148,15 @@ def change_speed_and_volume(wav_path: str, speed: float = 1.0):
def read_sheet(book_path: str, sheet_name: str = "") -> dict:
"""读表
从表格中读出所有的内容,用dict保存(表格的格式固定,第一行为表头(起始时间|终止时间|字幕|建议|解说脚本))
从表格中读出所有的内容,用dict保存(表格的格式固定,第一行为表头(起始时间|终止时间|字幕|建议|解说脚本))
Args:
book_path (str): 表格的存储路径
sheet_name (str, optional): 想要读取的表在excel表格中的名字. Defaults to "".
Returns:
dict: 表格中的所有内容,key为该列表头,value为列中的数据
"""
workbook = openpyxl.load_workbook(book_path)
sheet = workbook.active
......@@ -324,7 +331,9 @@ def ss_and_export(video_path: str, sheet_path: str, output_dir: str, speed: floa
# 音频输出位置路径
root_path = output_dir
# 如果文件夹不存在,则新建文件夹
# 如果文件夹已存在,就删除已有的文件夹,避免用户之前已有的一些中间结果
if os.path.exists(root_path):
shutil.rmtree(root_path)
if not os.path.exists(root_path):
os.mkdir(root_path)
......@@ -358,8 +367,6 @@ def ss_and_export(video_path: str, sheet_path: str, output_dir: str, speed: floa
if os.path.exists(tmp_file):
time.sleep(1)
os.remove(tmp_file)
# os.remove(origin_wav_path)
# os.remove(adjusted_wav_path)
state[0] = 1.00
......@@ -371,11 +378,4 @@ if __name__ == '__main__':
speed = 1.25
caption_file = os.path.join(output_dir, os.path.basename(video_path) + ".srt")
speaker_name = '晓秋'
ss_and_export(video_path, sheet_path, output_dir, speed, caption_file, speaker_name)
# import pprint
# d = read_sheet("./test37second.xlsx")
# pprint.pprint(d)
# init_speakers()
# speaker_name = "晓秋"
# speaker = choose_speaker(speaker_name)
# speech_synthesis("今天我们讲解的电影是何以笙箫默,它讲述了", r"D:\AddCaption\cur_version\accessibility_movie_2\test.wav", speaker, 0.5)
\ No newline at end of file
ss_and_export(video_path, sheet_path, output_dir, speed, caption_file, speaker_name)
\ No newline at end of file
......@@ -12,6 +12,11 @@ os.environ['PYQTGRAPH_QT_LIB'] = 'PyQt5'
project_path = None
def change_project_path(path):
"""用于切换当前工程路径
Args:
path (str): 用户选择或新建的工程路径
"""
global project_path
project_path = path
......
"""各种工具函数
工具函数描述如下:
- check_sheet_content: 检查表头是否符合预期
- get_progress_with_cmd: 获取ffmpeg处理视频的进度
- get_sheetHead: 获取表头
- replace_path_suffix: 替换路径后缀
- stop_thread: 停止线程
- trans_to_seconds: 将"hh:mm:ss.xxx"格式的时间转换为秒
- transfer_second_to_time: 将秒转换为"hh:mm:ss.xxx"格式的时间
- validate_and_get_filepath: 确认传入参数正确性,并返回文件路径
"""
import os, sys
import openpyxl
import subprocess
......@@ -6,8 +20,17 @@ import threading
import time
import inspect
import ctypes
from typing import Tuple
def validate_and_get_filepath(file_info) -> Tuple[str, bool]:
"""确认传入参数正确性,并返回文件路径
Args:
file_info: 文件选择窗口中返回的文件信息
def validate_and_get_filepath(file_info):
Returns:
Tuple[str, bool]: 文件路径和传入信息是否包含路径
"""
if type(file_info[0]) == str:
return file_info[0], True
# 兼容 空
......@@ -19,7 +42,7 @@ def trans_to_seconds(timePoint: str) -> float:
"""将用户输入的时间字符串转换为秒数
Args:
timePoint (str): 时间字符串
timePoint (str): "hh:mm:ss.xxx"格式的时间字符串
Returns:
float: 时间字符串对应秒数
......@@ -33,8 +56,13 @@ def trans_to_seconds(timePoint: str) -> float:
return time_in_seconds
def transfer_second_to_time(sec: str) -> str:
"""
输入xxxx.xxx秒,输出xx:xx:xx.xxx的格式
"""将秒数转换为"hh:mm:ss.xxx"格式的时间字符串
Args:
sec (str): 待转换的描述
Returns:
str: "hh:mm:ss.xxx"格式的时间字符串
"""
duration = int(float(sec))
hour = int(duration/3600)
......@@ -44,7 +72,16 @@ def transfer_second_to_time(sec: str) -> str:
time = "%02d:%02d:%02d.%03d" % (hour, minutes, seconds, msec)
return time
def replace_path_suffix(path, new_suffix):
def replace_path_suffix(path: str, new_suffix: str) -> str:
"""替换文件路径后缀
Args:
path (str): 原文件路径
new_suffix (str): 新的后缀
Returns:
str: 新的文件路径
"""
return path.replace(os.path.splitext(path)[-1], new_suffix)
def check_sheet_content(book_path: str) -> bool:
......@@ -86,11 +123,21 @@ def get_sheetHead(book_path: str) -> list:
return sheet_head
def get_progress_with_cmd(cmd: str, state=None):
"""获取ffmpeg命令行的执行进度
Args:
cmd (str): 用于后期处理的ffmpeg命令行
state (optional): 用于同步主界面中进度条的信号量. Defaults to None.
"""
if state is None:
state = [0]
pre = state[0]
process = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, encoding='utf-8', text=True)
startup = subprocess.STARTUPINFO()
startup.dwFlags = subprocess.STARTF_USESHOWWINDOW
startup.wShowWindow = subprocess.SW_HIDE
process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, encoding='utf-8', text=True,
startupinfo=startup)
for line in process.stdout:
print(line)
duration_res = re.search(r'\sDuration: (?P<duration>\S+)', line)
......@@ -126,46 +173,12 @@ def _async_raise(tid, exctype):
def stop_thread(thread):
"""杀死线程
Args:
thread: 待杀死的线程
"""
_async_raise(thread.ident, SystemExit)
if __name__ == '__main__':
x = transfer_second_to_time("12000.923")
print(x)
x = transfer_second_to_time("79.925")
print(x)
y = trans_to_seconds("1:00:00.92")
print(y)
z = transfer_second_to_time("1200.923")
print(z)
import time
strtime = "1:00:00.92"
# time.strptime(strtime, "%H:%M:%S")
import re
print("------------"*10)
# tests = ["00:12:34a", "01:12:34", "10:12:34.567", "12:12:34.89", "24:12:34.8", "2:34.2", "12:34.", "12:34.0", "02:34.0", "00:34.0"]
tests = ["01:12:34", "10:12:34.567", "12:12:34.89", "24:12:34.8", "2:34.2", "12:12:34.", "01:12:34.0", "02:02:34.0", "00:00:34.0"]
for s in tests:
x = re.match("^(([0-1]\d)|(2[0-4])):[0-5]\d:[0-5]\d(.\d{1,2})?$", s)
print("当前=", s)
print(x)
if x:
print(x.group())
print(x.group(1))
print("---------------------------------")
# print(re.match("^(([0-1]\d)|(2[0-4])):[0-5]\d$", "a12:34"))
# print(re.match("^(([0-1]\d)|(2[0-4])):[0-5]\d$", "a"))
# print(re.match("[^abc]", "plain"))
# print("------------")
# q = re.match("p|(pl)ain", "plain")
# print(q)
# print(q.group())
# print("------------")
# qq = re.match("ya(msen|nsen|nsem)", "yansen’s blog")
# print(qq)
# print(qq.group())
# print(qq.group(1))
\ No newline at end of file
pass
\ No newline at end of file
......@@ -46,7 +46,7 @@ exe = EXE(
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment