【PyQt6】朗读小说西游记

说明

使用 QTextToSpeech 的来实现阅读

QTextToSpeech 用法

正常情况下,应该正常的
如果不能发声的话,嘿嘿,可是使用了精简版的系统,那就没啥办法了

from PyQt6 import QtTextToSpeech,  QtWidgets
import sys,time
'''
引擎:
    ['winrt', 'sapi', 'mock']
引擎的声音:  
    mock  ['Bob', 'Anne']   # 这个引擎不能用
    sapi  ['Microsoft Huihui Desktop']  # 这个可以,只有一个声音
    winrt ['Microsoft Huihui', 'Microsoft Yaoyao', 'Microsoft Kangkang']    # 这个可以
'''

# txt = '混沌未分天地乱茫茫渺渺无人见混沌未分天地乱茫茫渺渺无人见' # 无标点
# txt = '混沌未分天地乱,茫茫渺渺无人见.混沌未分天地乱,茫茫渺渺无人见.'   # 加入英文标点
txt = '混沌未分天地乱,茫茫渺渺无人见。混沌未分天地乱,茫茫渺渺无人见。'   # 加入中文标点

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    tts = QtTextToSpeech.QTextToSpeech(app)

    # print(QtTextToSpeech.QTextToSpeech.availableEngines())  # ['winrt', 'sapi', 'mock']
    tts.setEngine('winrt')
    tts.setVoice(tts.availableVoices()[1])
    # tts.setPitch(-0.1)
    # tts.setRate(0.05)
    
    
    # print(tts.engine(),tts.voice().name(), [voice.name() for voice in tts.availableVoices()] )


    start = time.time()
    tts.say(txt)
    # button = QtWidgets.QPushButton('退出')
    # button.clicked.connect(app.quit)
    # button.show()
    
    
    # locale = QtCore.QLocale.system()
    # print(f'amText = {locale.amText()}')
    # print(f'bcp47Name = {locale.bcp47Name()}')
    # print(f'currencySymbol = {locale.currencySymbol()}')
    # print(f'dateTimeFormat = {locale.dateTimeFormat()}')
    # print(f'dayName = {locale.dayName(1,locale.FormatType.ShortFormat)}')

    def on_tts_ready(state):
        print()
        if state == QtTextToSpeech.QTextToSpeech.State.Ready:
            app.quit()
    
    # QtCore.QTimer.singleShot(5000,app.quit)
    tts.stateChanged.connect(on_tts_ready)
    SystemExit(app.exec())
    
    print( f'{txt, len(txt), time.time() - start}')     
    
    # ('混沌未分天地乱茫茫渺渺无人见混沌未分天地乱茫茫渺渺无人见', 28, 7.014833927154541)
    # ('混沌未分天地乱,茫茫渺渺无人见.混沌未分天地乱,茫茫渺渺无人见.', 32, 7.7734575271606445)
    # ('混沌未分天地乱,茫茫渺渺无人见。混沌未分天地乱,茫茫渺渺无人见。', 32, 8.660011291503906)

界面 + TTS

程序样式如下图:
在这里插入图片描述
无框,拖动,右键菜单,滚轮改变透明度
自动缩放界面,估计在高分屏下会有问题,自己修改

功能按键:

  • ESC : 退出
  • Space: 暂停/继续

import time
from PyQt6 import QtTextToSpeech, QtCore, QtWidgets,QtGui
import sys,os,json5,math,re
from PyQt6.QtGui import QCloseEvent, QKeyEvent, QMouseEvent, QWheelEvent


chapter_line = -1   # 段落索引,从 1 开始,注意调整
file_idx = 0        # 文件索引,从 1 开始,注意调整

txt = ''        # 保存文章内容
p_list = []     # 保存分割后的段落 去除空行

short_word_list = []    # 保存分割段落后的短句
short_word_idx = -1      # 保存当前的短句索引 从 1 开始,注意调整

# reg_str = r"[^。?”]+[?。”]+"
# 用句号分割
pat_str1 = r'[^“]{30,}。'
pat_str2 = r'[^“]*?“.*?[。!?]”'
pat_str3 = f'{pat_str1}|{pat_str2}'

reg_str = pat_str3
reg = re.compile(reg_str)

x=y=500
opacity = 1

isReadChapter = False

################################ 读取配置 ##########################################
# file_idx      文章的索引
# chapter_line  段落的索引
###################################################################################
def load_setting():
    global chapter_line,file_idx,short_word_idx
    global x,y,opacity
    
    setting_file = f'{xyj_dir+"progress.txt"}'
    if not os.path.exists(setting_file):
        file_idx = 1
        chapter_line = 1
        short_word_idx = 1
        return
    
    # 读取配置文件
    with open(xyj_dir+"progress.txt",'r',encoding='utf-8') as f:
        dic = json5.loads(f.read())
        file_idx = dic['file_idx']
        chapter_line = dic['line']
        short_word_idx = dic['words']
        if file_idx == 0:
            file_idx =1
        if file_idx > len(file_list):
            file_idx = len(file_list)
        if chapter_line == 0:
            chapter_line =1
        if short_word_idx == 0:
            short_word_idx =1
        x =  dic['x']
        y =  dic['y']
        opacity =  dic['opacity']
            

        
################################ 加载文章内容,并分段 ################################
# txt       保存文章内容
# p_list    保存分割后的段落 去除空行
###################################################################################
def load_chapater():
    global txt,p_list,short_word_list,chapter_line,short_word_idx
    
    with open(xyj_dir+file_list[file_idx-1],'r',encoding='utf-8') as f:
        txt = f.read()
        p_list = txt.split('\n')
        # 移除列表中的空字符串
        # p_list = [x.strip() for x in p_list if x.strip()!='']  # comment @ 2024年1月31日
    if chapter_line > len(p_list):
        chapter_line = 1

    paragraph = p_list[chapter_line-1]
    short_word_list = reg.findall(paragraph)
    # 针对没有匹配到的段落
    if not short_word_list:
        short_word_list.append(paragraph)
        
    if short_word_idx > len(short_word_list):
        short_word_idx = 1
    # print(1111,short_word_list)
    window.title_lab.setText(file_list[file_idx-1])

################################ 重新设置窗口大小 ################################
# 重新计算合适的大小,并调整窗口
###################################################################################
def resize():
    t = f'{chapter_line}  {short_word_list[short_word_idx-1]}' 
    txt_len = len(t) 
    txt_pix_len = txt_len * window.zh_width * window.line_height
    width = math.ceil(math.sqrt(txt_pix_len)*2) + 100
    if width < 350:
        width = 350
    height = math.ceil(txt_pix_len/width) + 40
    # 窗口右侧位置不变  
    ox = window.pos().x()
    rw = window.width()
    window.resize(width,height)   
    window.move(ox+rw-width,window.pos().y())

################################ 重新设置窗口大小 ################################
# 重新计算合适的大小,并调整窗口
###################################################################################
def read_and_show():
    global short_word_list,short_word_idx

    shortwords = short_word_list[short_word_idx-1]
    
    # t = f'{window.lable_lineheight_css}{chapter_line:02}:  {p_list[chapter_line-1]}'
    t = f'{window.lable_lineheight_css}{chapter_line}/{len(p_list)}:{short_word_idx}/{len(short_word_list)}: {shortwords}'
    label.setText(t)
    
    # 重新设置窗口大小
    resize()
    
    tts.say(shortwords)



################################ tts 状态变化 ################################
###################################################################################
def On_TTS_State_Changed(state: QtTextToSpeech.QTextToSpeech.State):
    global chapter_line,file_idx,short_word_idx,short_word_list,isReadChapter
    # ==== 设置tts的音量
    tts.setVolume(100)

    # ==== tts Ready状态
    if state == QtTextToSpeech.QTextToSpeech.State.Ready:
        
        # 开始状态,读取配置
        if isReadChapter == False:
            load_chapater()
            read_and_show()
            isReadChapter = True
            return
        
        short_word_idx += 1
        
        # 段落没读完
        if short_word_idx <= len(short_word_list):   
            read_and_show()
        else :
            chapter_line += 1
            short_word_idx = 1     # 到达新的段落, 短句索引复位
            
            # add 2024年1月31日
            while chapter_line <= len(p_list) and p_list[chapter_line-1].strip()==''  :
                chapter_line += 1
            # 2024年1月31日 <= end
            
            if chapter_line <= len(p_list):
                paragraph = p_list[chapter_line-1]
                short_word_list = reg.findall(paragraph)
                # 移除列表中的空字符串
                short_word_list = [x.strip() for x in short_word_list if x.strip()!='']  

                # 针对没有匹配到的段落
                if not short_word_list:
                    short_word_list.append(paragraph)

                read_and_show()
            else:
                file_idx += 1
                if file_idx > len(file_list):  # 章节已经全部读完 退出
                    file_idx -= 1
                    chapter_line -= 1
                    short_word_idx = len(short_word_list)
                    window.label.setText("章节已经全部读完")
                    return
                # 重新设置 章节的行数
                chapter_line = 1
                load_chapater()
                read_and_show()


################################ Quit() ################################
########################################################################
def QtQuit():
    tts.blockSignals(True)
    tts.stop()
    QtCore.QTimer.singleShot(100,app.quit)

################################ 界面类 ################################
class MWidget(QtWidgets.QWidget):
    # =========================== __init__() ===========================
    def __init__(self):
        super().__init__()
        
        # ------------ 状态和属性 ------------ 
        self.setWindowFlags(QtCore.Qt.WindowType.WindowStaysOnTopHint | QtCore.Qt.WindowType.Tool | QtCore.Qt.WindowType.FramelessWindowHint)
        # Qt::WA_NoSystemBackground
        # self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground,True)
        self.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus)
        
        # 位置和透明度
        self.move(x,y)
        self.setWindowOpacity(opacity)
        
        # ------------ 标题部分 ------------
        self.title = QtWidgets.QWidget()
        title_layout = QtWidgets.QHBoxLayout(self.title)
        title_layout.setContentsMargins(0,3,0,3)
        self.title_lab = QtWidgets.QLabel()
        title_layout.addStretch(1)
        title_layout.addWidget(self.title_lab)
        title_layout.addStretch(1)
        self.title.setStyleSheet('background:#e8e8ff')
        
        # self.title_lab 设置字体
        font = self.title_lab.font()
        font.setFamily('楷体')
        # font.setPixelSize(14  )
        font.setPointSize(10)
        font.setBold(True)
        self.title_lab.setFont(font)
        
        self.title_lab.setText("ESC : 退出  空格: 暂停/继续")
        
        ## ------------ 主体部分 ------------
        self.label = QtWidgets.QLabel()
        # self.label.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextSelectableByMouse) # comment @ 2024年1月31日
        # 设置换行
        self.label.setWordWrap(True)
        # 设置对其方式
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop)
        self.label.setContentsMargins(5,1,1,0)
        
        # self.label 设置字体
        # font = self.label.font()
        # font.setFamily('楷体')
        # font.setPixelSize(12)
        font.setBold(False)
        font.setPointSize(10)
        font_metric = QtGui.QFontMetrics(font)
        self.zh_width = font_metric.horizontalAdvance('中')
        self.line_height = math.ceil(font_metric.height() * 1.2)
        self.lable_lineheight_css = f'<p style="line-height:{self.line_height}px;">'
        # self.lable_lineheight_css = ''
        self.label.setFont(font)
        

        # ------------ 简单布局 ------------
        self.main_lay = QtWidgets.QVBoxLayout(self)
        # 加入标题
        self.main_lay.addWidget(self.title)
        
        # 加入主体
        self.main_lay.addWidget(self.label,1)

        # 加入状态栏
        # self.main_lay.addWidget(self.status_bar)
        
        # 设置
        self.main_lay.setStretch(1,1)
        self.main_lay.setContentsMargins(0,0,0,0)
        self.main_lay.setSpacing(1)
        
        
        # ------------ tts ------------
        tts.setVolume(0)
        tts.stateChanged.connect(On_TTS_State_Changed)
        tts.say("hello")
        time.sleep(1)
        
        # ------------ 上下文菜单 ------------
        self.action = QtGui.QAction('progress.txt')
        self.action.triggered.connect(self.contextMenu)
        
        self.addAction(self.action)
        self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.ActionsContextMenu)
        
    
    # =========================== 关闭事件 ===========================
    def closeEvent(self, a0: QCloseEvent | None) -> None:
        QtQuit()
        return super().closeEvent(a0)
    
    # =========================== 鼠标点击事件 ===========================
    def mousePressEvent(self, a0: QMouseEvent | None) -> None:
        self.start_pos = a0.pos()
        return super().mousePressEvent(a0)
    
    # =========================== 鼠标释放事件 ===========================
    def mouseReleaseEvent(self, a0: QMouseEvent | None) -> None:
        self.start_pos = QtCore.QPoint()
        return super().mouseReleaseEvent(a0)
    
    # =========================== 鼠标移动事件 ===========================
    def mouseMoveEvent(self, a0: QMouseEvent | None) -> None:
        move_pos = a0.pos()
        self.move(self.pos()+ move_pos-self.start_pos )
        # self.start_pos = move_pos
        return super().mouseMoveEvent(a0)

    # =========================== 按键按压事件 ===========================
    def keyPressEvent(self, a0: QKeyEvent | None) -> None:
        if a0.key() == QtCore.Qt.Key.Key_Escape:
            QtQuit()
        elif a0.key() == QtCore.Qt.Key.Key_Space:
            if tts.state() == QtTextToSpeech.QTextToSpeech.State.Paused:
                tts.resume() 
            elif tts.state() == QtTextToSpeech.QTextToSpeech.State.Speaking:
                tts.pause()
        else:
            return super().keyPressEvent(a0)
        
    # =========================== 鼠标滚轮事件 ===========================
    # 修改 window 的不透明度
    def wheelEvent(self, a0: QWheelEvent | None) -> None:
        delta = a0.angleDelta()
        opacity = self.windowOpacity()
        if delta.y() < 0:
            opacity -= 0.1
            opacity = 0.2 if opacity < 0.2  else opacity
        if delta.y() > 0:
            opacity += 0.1
            opacity = 1 if opacity >= 1  else opacity
        self.setWindowOpacity(opacity)
        return super().wheelEvent(a0)
    
    # =========================== 菜单事件 handler ===========================
    def contextMenu(self):
        path = os.getcwd()
        path = f'{path}/{xyj_dir}progress.txt'
        #打开txt文件
        os.startfile(path)
        pass

################################ __main__ ################################
if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    
    xyj_dir = r'小说/西游记/'

    # 获取文章列表
    file_list = os.listdir(xyj_dir)[1:]
    tts = QtTextToSpeech.QTextToSpeech(app)

    load_setting()
    #=========================== 简单界面 ===========================
    window = MWidget()
    label = window.label
    window.show()
    window.activateWindow()

    # print(window.baseSize().width(),window.baseSize().height())
    # print(window.sizeIncrement().width(),window.sizeIncrement().height())


    app.exec()


    with open(xyj_dir+"progress.txt",'w',encoding='utf-8') as f:
        
        _json = fr'''{{
    {'"file_idx"':10}: {file_idx:5},    // 章节序序号
    {'"line"':10}: {chapter_line:5},    // 章节段落号
    {'"words"':10}: {short_word_idx:5},   // 分词号
    {'"x"':10}: {window.pos().x():5},   // 窗口位置 x
    {'"y"':10}: {window.pos().y():5},   // 窗口位置 y
    {'"opacity"':10}: {window.windowOpacity():5},   // 不透明
}}'''
        # dic = json5.loads(_json)
        # f.write(json5.dumps(dic,indent=4))
        f.write(_json)


  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值