活用edge-tts库朗读对话文本

 Python安装后的edge-tts 库可以使用微软 Edge 浏览器的语音合成功能将文本转换成语音。edge-tts是微软神经网络语音朗读服务,只是它剃除了朗读标签和一些功能,用户无法享用完整版优质的语音朗读服务。好在于它的语音朗读质量优越, 连IVONA也跟不上。剃除了朗读标签后,你会很快发现,它只会调用固定的单个语音库来朗读整篇文章,如果想要朗读包涵角色的对话的文本,这可害苦了想使用免费版的用户,偏偏在外语教学与学习辅助中,听对话朗读又是一个重要的学习途径,设计与编写一个可以处理包涵角色朗读的小工具是必要的。(注:微软edge-tts库经常升级,端口也会随着发生改变,若不及时升级会发生不能合成声音的错误警报。相关应用程序可以下面链接下载。20241029)

设计界面:(有点丑)

功能:

以上是用python设计编写的,它可以

1. 同时处理16角色的文本,按段成MP3及歌词, 

2. 生成的MP3转成立体声音,高大音量,

3. 内置edge-tts全部的语音库任意选择

4. 角色与语音库选择后动态保存

5.支持简单文本编辑,支持普通文本转MP3和歌词

相关关键代码:


 # 创建角色文本框和语音库ComboBox
role_entries = []
voice_lib_comboboxes = []

for i in range(17):
    frame = tk.Frame()
    frame.pack(pady=5)

    # 创建角色文本框
    role_entry = tk.Entry(frame, width=20)
    role_entry.pack(side=tk.LEFT, padx=5)
    role_entries.append(role_entry)
    role_entry.bind("<KeyRelease>", on_role_change)  # 角色文本框变化时自动保存

    # 创建语音库ComboBox

    voice_lib_combobox = ttk.Combobox(frame, values=["af-ZA-AdriNeural-Female",
"af-ZA-WillemNeural-Male",
"am-ET-AmehaNeural-Male",
"am-ET-MekdesNeural-Female",
"ar-AE-FatimaNeural-Female",
"ar-AE-HamdanNeural-Male",
"ar-BH-AliNeural-Male",
"ar-BH-LailaNeural-Female",
"ar-DZ-AminaNeural-Female",
"ar-DZ-IsmaelNeural-Male",
"ar-EG-SalmaNeural-Female",
"ar-EG-ShakirNeural-Male",
"ar-IQ-BasselNeural-Male",
"ar-IQ-RanaNeural-Female",
"ar-JO-SanaNeural-Female",
"ar-JO-TaimNeural-Male",
"ar-KW-FahedNeural-Male",
"ar-KW-NouraNeural-Female",
"ar-LB-LaylaNeural-Female",
"ar-LB-RamiNeural-Male",
"ar-LY-ImanNeural-Female",
"ar-LY-OmarNeural-Male",
"ar-MA-JamalNeural-Male",
"ar-MA-MounaNeural-Female",
"ar-OM-AbdullahNeural-Male",
"ar-OM-AyshaNeural-Female",
"ar-QA-AmalNeural-Female",
"ar-QA-MoazNeural-Male",
"ar-SA-HamedNeural-Male",
"ar-SA-ZariyahNeural-Female",
"ar-SY-AmanyNeural-Female",
"ar-SY-LaithNeural-Male",
"ar-TN-HediNeural-Male",
"ar-TN-ReemNeural-Female",
"ar-YE-MaryamNeural-Female",
"ar-YE-SalehNeural-Male",
"az-AZ-BabekNeural-Male",
"az-AZ-BanuNeural-Female",
"bg-BG-BorislavNeural-Male",
"bg-BG-KalinaNeural-Female",
"bn-BD-NabanitaNeural-Female",
"bn-BD-PradeepNeural-Male",
"bn-IN-BashkarNeural-Male",
"bn-IN-TanishaaNeural-Female",
"bs-BA-GoranNeural-Male",
"bs-BA-VesnaNeural-Female",
"ca-ES-EnricNeural-Male",
"ca-ES-JoanaNeural-Female",
"cs-CZ-AntoninNeural-Male",
"cs-CZ-VlastaNeural-Female",
"cy-GB-AledNeural-Male",
"cy-GB-NiaNeural-Female",
"da-DK-ChristelNeural-Female",
"da-DK-JeppeNeural-Male",
"de-AT-IngridNeural-Female",
"de-AT-JonasNeural-Male",
"de-CH-JanNeural-Male",
"de-CH-LeniNeural-Female",
"de-DE-AmalaNeural-Female",
"de-DE-ConradNeural-Male",
"de-DE-FlorianMultilingualNeural-Male",
"de-DE-KatjaNeural-Female",
"de-DE-KillianNeural-Male",
"de-DE-SeraphinaMultilingualNeural-Female",
"el-GR-AthinaNeural-Female",
"el-GR-NestorasNeural-Male",
"en-AU-NatashaNeural-Female",
"en-AU-WilliamNeural-Male",
"en-CA-ClaraNeural-Female",
"en-CA-LiamNeural-Male",
"en-GB-LibbyNeural-Female",
"en-GB-MaisieNeural-Female",
"en-GB-RyanNeural-Male",
"en-GB-SoniaNeural-Female",
"en-GB-ThomasNeural-Male",
"en-HK-SamNeural-Male",
"en-HK-YanNeural-Female",
"en-IE-ConnorNeural-Male",
"en-IE-EmilyNeural-Female",
"en-IN-NeerjaExpressiveNeural-Female",
"en-IN-NeerjaNeural-Female",
"en-IN-PrabhatNeural-Male",
"en-KE-AsiliaNeural-Female",
"en-KE-ChilembaNeural-Male",
"en-NG-AbeoNeural-Male",
"en-NG-EzinneNeural-Female",
"en-NZ-MitchellNeural-Male",
"en-NZ-MollyNeural-Female",
"en-PH-JamesNeural-Male",
"en-PH-RosaNeural-Female",
"en-SG-LunaNeural-Female",
"en-SG-WayneNeural-Male",
"en-TZ-ElimuNeural-Male",
"en-TZ-ImaniNeural-Female",
"en-US-AnaNeural-Female",
"en-US-AndrewMultilingualNeural-Male",
"en-US-AndrewNeural-Male",
"en-US-AriaNeural-Female",
"en-US-AvaMultilingualNeural-Female",
"en-US-AvaNeural-Female",
"en-US-BrianMultilingualNeural-Male",
"en-US-BrianNeural-Male",
"en-US-ChristopherNeural-Male",
"en-US-EmmaMultilingualNeural-Female",
"en-US-EmmaNeural-Female",
"en-US-EricNeural-Male",
"en-US-GuyNeural-Male",
"en-US-JennyNeural-Female",
"en-US-MichelleNeural-Female",
"en-US-RogerNeural-Male",
"en-US-SteffanNeural-Male",
"en-ZA-LeahNeural-Female",
"en-ZA-LukeNeural-Male",
"es-AR-ElenaNeural-Female",
"es-AR-TomasNeural-Male",
"es-BO-MarceloNeural-Male",
"es-BO-SofiaNeural-Female",
"es-CL-CatalinaNeural-Female",
"es-CL-LorenzoNeural-Male",
"es-CO-GonzaloNeural-Male",
"es-CO-SalomeNeural-Female",
"es-CR-JuanNeural-Male",
"es-CR-MariaNeural-Female",
"es-CU-BelkysNeural-Female",
"es-CU-ManuelNeural-Male",
"es-DO-EmilioNeural-Male",
"es-DO-RamonaNeural-Female",
"es-EC-AndreaNeural-Female",
"es-EC-LuisNeural-Male",
"es-ES-AlvaroNeural-Male",
"es-ES-ElviraNeural-Female",
"es-ES-XimenaNeural-Female",
"es-GQ-JavierNeural-Male",
"es-GQ-TeresaNeural-Female",
"es-GT-AndresNeural-Male",
"es-GT-MartaNeural-Female",
"es-HN-CarlosNeural-Male",
"es-HN-KarlaNeural-Female",
"es-MX-DaliaNeural-Female",
"es-MX-JorgeNeural-Male",
"es-NI-FedericoNeural-Male",
"es-NI-YolandaNeural-Female",
"es-PA-MargaritaNeural-Female",
"es-PA-RobertoNeural-Male",
"es-PE-AlexNeural-Male",
"es-PE-CamilaNeural-Female",
"es-PR-KarinaNeural-Female",
"es-PR-VictorNeural-Male",
"es-PY-MarioNeural-Male",
"es-PY-TaniaNeural-Female",
"es-SV-LorenaNeural-Female",
"es-SV-RodrigoNeural-Male",
"es-US-AlonsoNeural-Male",
"es-US-PalomaNeural-Female",
"es-UY-MateoNeural-Male",
"es-UY-ValentinaNeural-Female",
"es-VE-PaolaNeural-Female",
"es-VE-SebastianNeural-Male",
"et-EE-AnuNeural-Female",
"et-EE-KertNeural-Male",
"fa-IR-DilaraNeural-Female",
"fa-IR-FaridNeural-Male",
"fi-FI-HarriNeural-Male",
"fi-FI-NooraNeural-Female",
"fil-PH-AngeloNeural-Male",
"fil-PH-BlessicaNeural-Female",
"fr-BE-CharlineNeural-Female",
"fr-BE-GerardNeural-Male",
"fr-CA-AntoineNeural-Male",
"fr-CA-JeanNeural-Male",
"fr-CA-SylvieNeural-Female",
"fr-CA-ThierryNeural-Male",
"fr-CH-ArianeNeural-Female",
"fr-CH-FabriceNeural-Male",
"fr-FR-DeniseNeural-Female",
"fr-FR-EloiseNeural-Female",
"fr-FR-HenriNeural-Male",
"fr-FR-RemyMultilingualNeural-Male",
"fr-FR-VivienneMultilingualNeural-Female",
"ga-IE-ColmNeural-Male",
"ga-IE-OrlaNeural-Female",
"gl-ES-RoiNeural-Male",
"gl-ES-SabelaNeural-Female",
"gu-IN-DhwaniNeural-Female",
"gu-IN-NiranjanNeural-Male",
"he-IL-AvriNeural-Male",
"he-IL-HilaNeural-Female",
"hi-IN-MadhurNeural-Male",
"hi-IN-SwaraNeural-Female",
"hr-HR-GabrijelaNeural-Female",
"hr-HR-SreckoNeural-Male",
"hu-HU-NoemiNeural-Female",
"hu-HU-TamasNeural-Male",
"id-ID-ArdiNeural-Male",
"id-ID-GadisNeural-Female",
"is-IS-GudrunNeural-Female",
"is-IS-GunnarNeural-Male",
"it-IT-DiegoNeural-Male",
"it-IT-ElsaNeural-Female",
"it-IT-GiuseppeNeural-Male",
"it-IT-IsabellaNeural-Female",
"ja-JP-KeitaNeural-Male",
"ja-JP-NanamiNeural-Female",
"jv-ID-DimasNeural-Male",
"jv-ID-SitiNeural-Female",
"ka-GE-EkaNeural-Female",
"ka-GE-GiorgiNeural-Male",
"kk-KZ-AigulNeural-Female",
"kk-KZ-DauletNeural-Male",
"km-KH-PisethNeural-Male",
"km-KH-SreymomNeural-Female",
"kn-IN-GaganNeural-Male",
"kn-IN-SapnaNeural-Female",
"ko-KR-HyunsuNeural-Male",
"ko-KR-InJoonNeural-Male",
"ko-KR-SunHiNeural-Female",
"lo-LA-ChanthavongNeural-Male",
"lo-LA-KeomanyNeural-Female",
"lt-LT-LeonasNeural-Male",
"lt-LT-OnaNeural-Female",
"lv-LV-EveritaNeural-Female",
"lv-LV-NilsNeural-Male",
"mk-MK-AleksandarNeural-Male",
"mk-MK-MarijaNeural-Female",
"ml-IN-MidhunNeural-Male",
"ml-IN-SobhanaNeural-Female",
"mn-MN-BataaNeural-Male",
"mn-MN-YesuiNeural-Female",
"mr-IN-AarohiNeural-Female",
"mr-IN-ManoharNeural-Male",
"ms-MY-OsmanNeural-Male",
"ms-MY-YasminNeural-Female",
"mt-MT-GraceNeural-Female",
"mt-MT-JosephNeural-Male",
"my-MM-NilarNeural-Female",
"my-MM-ThihaNeural-Male",
"nb-NO-FinnNeural-Male",
"nb-NO-PernilleNeural-Female",
"ne-NP-HemkalaNeural-Female",
"ne-NP-SagarNeural-Male",
"nl-BE-ArnaudNeural-Male",
"nl-BE-DenaNeural-Female",
"nl-NL-ColetteNeural-Female",
"nl-NL-FennaNeural-Female",
"nl-NL-MaartenNeural-Male",
"pl-PL-MarekNeural-Male",
"pl-PL-ZofiaNeural-Female",
"ps-AF-GulNawazNeural-Male",
"ps-AF-LatifaNeural-Female",
"pt-BR-AntonioNeural-Male",
"pt-BR-FranciscaNeural-Female",
"pt-BR-ThalitaNeural-Female",
"pt-PT-DuarteNeural-Male",
"pt-PT-RaquelNeural-Female",
"ro-RO-AlinaNeural-Female",
"ro-RO-EmilNeural-Male",
"ru-RU-DmitryNeural-Male",
"ru-RU-SvetlanaNeural-Female",
"si-LK-SameeraNeural-Male",
"si-LK-ThiliniNeural-Female",
"sk-SK-LukasNeural-Male",
"sk-SK-ViktoriaNeural-Female",
"sl-SI-PetraNeural-Female",
"sl-SI-RokNeural-Male",
"so-SO-MuuseNeural-Male",
"so-SO-UbaxNeural-Female",
"sq-AL-AnilaNeural-Female",
"sq-AL-IlirNeural-Male",
"sr-RS-NicholasNeural-Male",
"sr-RS-SophieNeural-Female",
"su-ID-JajangNeural-Male",
"su-ID-TutiNeural-Female",
"sv-SE-MattiasNeural-Male",
"sv-SE-SofieNeural-Female",
"sw-KE-RafikiNeural-Male",
"sw-KE-ZuriNeural-Female",
"sw-TZ-DaudiNeural-Male",
"sw-TZ-RehemaNeural-Female",
"ta-IN-PallaviNeural-Female",
"ta-IN-ValluvarNeural-Male",
"ta-LK-KumarNeural-Male",
"ta-LK-SaranyaNeural-Female",
"ta-MY-KaniNeural-Female",
"ta-MY-SuryaNeural-Male",
"ta-SG-AnbuNeural-Male",
"ta-SG-VenbaNeural-Female",
"te-IN-MohanNeural-Male",
"te-IN-ShrutiNeural-Female",
"th-TH-NiwatNeural-Male",
"th-TH-PremwadeeNeural-Female",
"tr-TR-AhmetNeural-Male",
"tr-TR-EmelNeural-Female",
"uk-UA-OstapNeural-Male",
"uk-UA-PolinaNeural-Female",
"ur-IN-GulNeural-Female",
"ur-IN-SalmanNeural-Male",
"ur-PK-AsadNeural-Male",
"ur-PK-UzmaNeural-Female",
"uz-UZ-MadinaNeural-Female",
"uz-UZ-SardorNeural-Male",
"vi-VN-HoaiMyNeural-Female",
"vi-VN-NamMinhNeural-Male",
"zh-CN-XiaoxiaoNeural-Female",
"zh-CN-XiaoyiNeural-Female",
"zh-CN-YunjianNeural-Male",
"zh-CN-YunxiNeural-Male",
"zh-CN-YunxiaNeural-Male",
"zh-CN-YunyangNeural-Male",
"zh-CN-liaoning-XiaobeiNeural-Female",
"zh-CN-shaanxi-XiaoniNeural-Female",
"zh-HK-HiuGaaiNeural-Female",
"zh-HK-HiuMaanNeural-Female",
"zh-HK-WanLungNeural-Male",
"zh-TW-HsiaoChenNeural-Female",
"zh-TW-HsiaoYuNeural-Female",
"zh-TW-YunJheNeural-Male",
"zu-ZA-ThandoNeural-Female",
"zu-ZA-ThembaNeural-Male",
 ],width=35)
    voice_lib_combobox.pack(side=tk.LEFT, padx=5)
    voice_lib_comboboxes.append(voice_lib_combobox)
    voice_lib_combobox.bind("<<ComboboxSelected>>", on_voice_change)  # ComboBox选择变化时自动保存

# 加载之前保存的角色和语音库
load_roles()
load_voices()

roles_audio_path = r"C:\edgetts\rolesaudio"
os.makedirs(roles_audio_path, exist_ok=True)  # Ensure the directory exists

# Initialize a counter for naming MP3 files uniquely
mp3_counter = 10000

async def main(TEXT, VOICE):
    """Main async function to generate MP3."""
    if not TEXT.strip():
        print("TEXT is empty, audio processing not executed.")
        messagebox.showerror("文本为空", "文本为空,未执行音频处理")
        return

    global mp3_counter  # Access the global counter

    try:
        # Increment the counter and create the filenames
        mp3_counter += 1
        output_file = os.path.join(roles_audio_path, f"{mp3_counter}.mp3")
        webvtt_file = os.path.join(roles_audio_path, f"{mp3_counter}.vtt")
        
        communicate = edge_tts.Communicate(TEXT, VOICE)
        submaker = edge_tts.SubMaker()

        # Write audio data to the file
        with open(output_file, "wb") as file:
            async for chunk in communicate.stream():
                if chunk["type"] == "audio":
                    file.write(chunk["data"])
                elif chunk["type"] == "WordBoundary":
                    submaker.create_sub((chunk["offset"], chunk["duration"]), chunk["text"])

        # Write subtitles to the WebVTT file
        with open(webvtt_file, "w", encoding="utf-8") as file:
            file.write(submaker.generate_subs())

        print(f"Generated file: {output_file} and {webvtt_file}")

    except FileNotFoundError:
        print("Error: Unable to find output file or write to path.")
    except Exception as e:
        print(f"Error processing audio and metadata: {e}")
        messagebox.showerror("网络连接失败", f"处理音频和元数据时发生错误: {e}")


原始代码如下,可以根据这个代码进行加工升级,就可以估出类似的朗读机。

import tkinter as tk
from tkinter import ttk, messagebox
import edge_tts
import asyncio
import re  # 导入正则表达式模块
import os

# 可选的语音列表
voices = [
    "zh-CN-XiaoxiaoNeural", "zh-CN-YunyangNeural",  # 中文女声1, 中文男声
    "en-US-JennyNeural", "en-US-GuyNeural",        # 英文女声1, 英文男声
    "de-DE-KatjaNeural", "de-DE-MichaelNeural",    # 德文女声1, 德文男声
    "fr-FR-DeniseNeural", "fr-FR-HugoNeural",      # 法文女声1, 法文男声
    "ja-JP-AikoNeural", "ja-JP-KenjiNeural",        # 日文女声1, 日文男声
    "es-ES-MariaNeural", "es-ES-PabloNeural",       # 西班牙女声1, 西班牙男声
    "it-IT-RobertaNeural", "it-IT-GiorgioNeural",   # 意大利女声1, 意大利男声
    "ko-KR-SumiNeural", "ko-KR-MinSookNeural"      # 韩文女声1, 韩文男声
]

class TextToSpeechApp:
    def __init__(self, root):
        self.root = root
        self.root.title("对话朗读程序")
        
        # 初始化角色与语音选择
        self.character_entries = []
        self.voice_comboboxes = []
        self.voice_map = {}  # 初始化语音映射
        
        # 创建角色输入框和语音选择框
        for i in range(16):
            character_entry = tk.Entry(root, width=15)
            character_entry.grid(row=i, column=0, padx=10, pady=5)
            self.character_entries.append(character_entry)

            voice_combobox = ttk.Combobox(root, values=voices, width=30)
            voice_combobox.grid(row=i, column=1, padx=10, pady=5)
            self.voice_comboboxes.append(voice_combobox)
            voice_combobox.current(0)  # 默认选择第一个声音

        # 创建对话文本输入框
        self.dialogue_frame = tk.Frame(root)
        self.dialogue_frame.grid(row=0, column=2, rowspan=16, padx=10, pady=5)

        self.dialogue_text = tk.Text(self.dialogue_frame, height=10, width=50)
        self.dialogue_text.pack(expand=True, fill='both')

        # 创建对话框边框
        scrollbar = tk.Scrollbar(self.dialogue_frame, command=self.dialogue_text.yview)
        scrollbar.pack(side='right', fill='y')
        self.dialogue_text['yscrollcommand'] = scrollbar.set

        # 创建朗读按钮
        read_button = tk.Button(root, text="朗读对话", command=self.read_dialogue)
        read_button.grid(row=16, column=0, columnspan=3, pady=10)

    async def speak_dialogue(self, dialogue_lines):
        for character, line in dialogue_lines:
            voice = self.voice_map.get(character)
            if voice:
                # 生成有效的文件名
                output_file = f"{character}.mp3"
                communicate = edge_tts.Communicate(text=line.strip(), voice=voice)
                await communicate.save(output_file)
                print(f"生成语音文件:{output_file}")
            else:
                print(f"未找到角色 {character} 的声音设置")

    def read_dialogue(self):
        # 获取对话文本
        dialogue_content = self.dialogue_text.get("1.0", tk.END).strip()
        if not dialogue_content:
            messagebox.showwarning("警告", "请在对话文本框中输入内容!")
            return
        
        # 收集角色及其对应对话内容
        dialogue_lines = []
        lines = dialogue_content.split('\n')

        # 填充语音映射
        for i, line in enumerate(lines):
            match = re.match(r'([A-Z]):\s*(.*)', line.strip())
            if match:
                character = match.group(1)  # 角色名
                dialogue_line = match.group(2)  # 对话内容
                # 获取当前选定的语音
                selected_voice = self.voice_comboboxes[i].get()
                self.voice_map[character] = selected_voice  # 更新语音映射
                dialogue_lines.append((character, dialogue_line))

        if dialogue_lines:
            asyncio.run(self.speak_dialogue(dialogue_lines))
        else:
            messagebox.showwarning("警告", "未检测到有效的对话!")

if __name__ == "__main__":
    root = tk.Tk()
    app = TextToSpeechApp(root)
    root.mainloop()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值