From bc8e005f599367daec8f93aa9bab845965fd5ac0 Mon Sep 17 00:00:00 2001 From: harry Date: Fri, 29 Mar 2024 17:13:25 +0800 Subject: [PATCH] 1. Added multi-language support to the UI 2. Optimized the voice name 3. Other UI optimizations --- app/models/schema.py | 76 ++-- app/services/task.py | 17 +- app/services/voice.py | 984 +++++++++++++++++++++++++++++++++++++++++- webui/Main.py | 251 +++++++---- webui/i18n/en.json | 51 +++ webui/i18n/zh.json | 51 +++ 6 files changed, 1293 insertions(+), 137 deletions(-) create mode 100644 webui/i18n/en.json create mode 100644 webui/i18n/zh.json diff --git a/app/models/schema.py b/app/models/schema.py index 730006d..d29a6b4 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -34,43 +34,43 @@ class MaterialInfo: duration: int = 0 -VoiceNames = [ - # zh-CN - "female-zh-CN-XiaoxiaoNeural", - "female-zh-CN-XiaoyiNeural", - "female-zh-CN-liaoning-XiaobeiNeural", - "female-zh-CN-shaanxi-XiaoniNeural", - - "male-zh-CN-YunjianNeural", - "male-zh-CN-YunxiNeural", - "male-zh-CN-YunxiaNeural", - "male-zh-CN-YunyangNeural", - - # "female-zh-HK-HiuGaaiNeural", - # "female-zh-HK-HiuMaanNeural", - # "male-zh-HK-WanLungNeural", - # - # "female-zh-TW-HsiaoChenNeural", - # "female-zh-TW-HsiaoYuNeural", - # "male-zh-TW-YunJheNeural", - - # en-US - - "female-en-US-AnaNeural", - "female-en-US-AriaNeural", - "female-en-US-AvaNeural", - "female-en-US-EmmaNeural", - "female-en-US-JennyNeural", - "female-en-US-MichelleNeural", - - "male-en-US-AndrewNeural", - "male-en-US-BrianNeural", - "male-en-US-ChristopherNeural", - "male-en-US-EricNeural", - "male-en-US-GuyNeural", - "male-en-US-RogerNeural", - "male-en-US-SteffanNeural", -] +# VoiceNames = [ +# # zh-CN +# "female-zh-CN-XiaoxiaoNeural", +# "female-zh-CN-XiaoyiNeural", +# "female-zh-CN-liaoning-XiaobeiNeural", +# "female-zh-CN-shaanxi-XiaoniNeural", +# +# "male-zh-CN-YunjianNeural", +# "male-zh-CN-YunxiNeural", +# "male-zh-CN-YunxiaNeural", +# "male-zh-CN-YunyangNeural", +# +# # "female-zh-HK-HiuGaaiNeural", +# # "female-zh-HK-HiuMaanNeural", +# # "male-zh-HK-WanLungNeural", +# # +# # "female-zh-TW-HsiaoChenNeural", +# # "female-zh-TW-HsiaoYuNeural", +# # "male-zh-TW-YunJheNeural", +# +# # en-US +# +# "female-en-US-AnaNeural", +# "female-en-US-AriaNeural", +# "female-en-US-AvaNeural", +# "female-en-US-EmmaNeural", +# "female-en-US-JennyNeural", +# "female-en-US-MichelleNeural", +# +# "male-en-US-AndrewNeural", +# "male-en-US-BrianNeural", +# "male-en-US-ChristopherNeural", +# "male-en-US-EricNeural", +# "male-en-US-GuyNeural", +# "male-en-US-RogerNeural", +# "male-en-US-SteffanNeural", +# ] class VideoParams: @@ -97,7 +97,7 @@ class VideoParams: video_language: Optional[str] = "" # auto detect - voice_name: Optional[str] = VoiceNames[0] + voice_name: Optional[str] = "" bgm_type: Optional[str] = "random" bgm_file: Optional[str] = "" bgm_volume: Optional[float] = 0.2 diff --git a/app/services/task.py b/app/services/task.py index e454a26..bf091a8 100644 --- a/app/services/task.py +++ b/app/services/task.py @@ -6,24 +6,11 @@ from os import path from loguru import logger from app.config import config -from app.models.schema import VideoParams, VoiceNames, VideoConcatMode +from app.models.schema import VideoParams, VideoConcatMode from app.services import llm, material, voice, video, subtitle from app.utils import utils -def _parse_voice(name: str): - # "female-zh-CN-XiaoxiaoNeural", - # remove first part split by "-" - if name not in VoiceNames: - name = VoiceNames[0] - - parts = name.split("-") - _lang = f"{parts[1]}-{parts[2]}" - _voice = f"{_lang}-{parts[3]}" - - return _voice, _lang - - def start(task_id, params: VideoParams): """ { @@ -40,7 +27,7 @@ def start(task_id, params: VideoParams): """ logger.info(f"start task: {task_id}") video_subject = params.video_subject - voice_name, language = _parse_voice(params.voice_name) + voice_name = voice.parse_voice_name(params.voice_name) paragraph_number = params.paragraph_number n_threads = params.n_threads max_clip_duration = params.video_clip_duration diff --git a/app/services/voice.py b/app/services/voice.py index cbaca4d..282667b 100644 --- a/app/services/voice.py +++ b/app/services/voice.py @@ -1,5 +1,4 @@ import asyncio -from concurrent.futures import ThreadPoolExecutor from xml.sax.saxutils import unescape from edge_tts.submaker import mktimestamp from loguru import logger @@ -8,6 +7,985 @@ import edge_tts from app.utils import utils +def get_all_voices(filter_locals=None) -> list[str]: + if filter_locals is None: + filter_locals = ["zh-CN", "en-US", "zh-HK", "zh-TW"] + voices_str = """ +Name: af-ZA-AdriNeural +Gender: Female + +Name: af-ZA-WillemNeural +Gender: Male + +Name: am-ET-AmehaNeural +Gender: Male + +Name: am-ET-MekdesNeural +Gender: Female + +Name: ar-AE-FatimaNeural +Gender: Female + +Name: ar-AE-HamdanNeural +Gender: Male + +Name: ar-BH-AliNeural +Gender: Male + +Name: ar-BH-LailaNeural +Gender: Female + +Name: ar-DZ-AminaNeural +Gender: Female + +Name: ar-DZ-IsmaelNeural +Gender: Male + +Name: ar-EG-SalmaNeural +Gender: Female + +Name: ar-EG-ShakirNeural +Gender: Male + +Name: ar-IQ-BasselNeural +Gender: Male + +Name: ar-IQ-RanaNeural +Gender: Female + +Name: ar-JO-SanaNeural +Gender: Female + +Name: ar-JO-TaimNeural +Gender: Male + +Name: ar-KW-FahedNeural +Gender: Male + +Name: ar-KW-NouraNeural +Gender: Female + +Name: ar-LB-LaylaNeural +Gender: Female + +Name: ar-LB-RamiNeural +Gender: Male + +Name: ar-LY-ImanNeural +Gender: Female + +Name: ar-LY-OmarNeural +Gender: Male + +Name: ar-MA-JamalNeural +Gender: Male + +Name: ar-MA-MounaNeural +Gender: Female + +Name: ar-OM-AbdullahNeural +Gender: Male + +Name: ar-OM-AyshaNeural +Gender: Female + +Name: ar-QA-AmalNeural +Gender: Female + +Name: ar-QA-MoazNeural +Gender: Male + +Name: ar-SA-HamedNeural +Gender: Male + +Name: ar-SA-ZariyahNeural +Gender: Female + +Name: ar-SY-AmanyNeural +Gender: Female + +Name: ar-SY-LaithNeural +Gender: Male + +Name: ar-TN-HediNeural +Gender: Male + +Name: ar-TN-ReemNeural +Gender: Female + +Name: ar-YE-MaryamNeural +Gender: Female + +Name: ar-YE-SalehNeural +Gender: Male + +Name: az-AZ-BabekNeural +Gender: Male + +Name: az-AZ-BanuNeural +Gender: Female + +Name: bg-BG-BorislavNeural +Gender: Male + +Name: bg-BG-KalinaNeural +Gender: Female + +Name: bn-BD-NabanitaNeural +Gender: Female + +Name: bn-BD-PradeepNeural +Gender: Male + +Name: bn-IN-BashkarNeural +Gender: Male + +Name: bn-IN-TanishaaNeural +Gender: Female + +Name: bs-BA-GoranNeural +Gender: Male + +Name: bs-BA-VesnaNeural +Gender: Female + +Name: ca-ES-EnricNeural +Gender: Male + +Name: ca-ES-JoanaNeural +Gender: Female + +Name: cs-CZ-AntoninNeural +Gender: Male + +Name: cs-CZ-VlastaNeural +Gender: Female + +Name: cy-GB-AledNeural +Gender: Male + +Name: cy-GB-NiaNeural +Gender: Female + +Name: da-DK-ChristelNeural +Gender: Female + +Name: da-DK-JeppeNeural +Gender: Male + +Name: de-AT-IngridNeural +Gender: Female + +Name: de-AT-JonasNeural +Gender: Male + +Name: de-CH-JanNeural +Gender: Male + +Name: de-CH-LeniNeural +Gender: Female + +Name: de-DE-AmalaNeural +Gender: Female + +Name: de-DE-ConradNeural +Gender: Male + +Name: de-DE-FlorianMultilingualNeural +Gender: Male + +Name: de-DE-KatjaNeural +Gender: Female + +Name: de-DE-KillianNeural +Gender: Male + +Name: de-DE-SeraphinaMultilingualNeural +Gender: Female + +Name: el-GR-AthinaNeural +Gender: Female + +Name: el-GR-NestorasNeural +Gender: Male + +Name: en-AU-NatashaNeural +Gender: Female + +Name: en-AU-WilliamNeural +Gender: Male + +Name: en-CA-ClaraNeural +Gender: Female + +Name: en-CA-LiamNeural +Gender: Male + +Name: en-GB-LibbyNeural +Gender: Female + +Name: en-GB-MaisieNeural +Gender: Female + +Name: en-GB-RyanNeural +Gender: Male + +Name: en-GB-SoniaNeural +Gender: Female + +Name: en-GB-ThomasNeural +Gender: Male + +Name: en-HK-SamNeural +Gender: Male + +Name: en-HK-YanNeural +Gender: Female + +Name: en-IE-ConnorNeural +Gender: Male + +Name: en-IE-EmilyNeural +Gender: Female + +Name: en-IN-NeerjaExpressiveNeural +Gender: Female + +Name: en-IN-NeerjaNeural +Gender: Female + +Name: en-IN-PrabhatNeural +Gender: Male + +Name: en-KE-AsiliaNeural +Gender: Female + +Name: en-KE-ChilembaNeural +Gender: Male + +Name: en-NG-AbeoNeural +Gender: Male + +Name: en-NG-EzinneNeural +Gender: Female + +Name: en-NZ-MitchellNeural +Gender: Male + +Name: en-NZ-MollyNeural +Gender: Female + +Name: en-PH-JamesNeural +Gender: Male + +Name: en-PH-RosaNeural +Gender: Female + +Name: en-SG-LunaNeural +Gender: Female + +Name: en-SG-WayneNeural +Gender: Male + +Name: en-TZ-ElimuNeural +Gender: Male + +Name: en-TZ-ImaniNeural +Gender: Female + +Name: en-US-AnaNeural +Gender: Female + +Name: en-US-AndrewNeural +Gender: Male + +Name: en-US-AriaNeural +Gender: Female + +Name: en-US-AvaNeural +Gender: Female + +Name: en-US-BrianNeural +Gender: Male + +Name: en-US-ChristopherNeural +Gender: Male + +Name: en-US-EmmaNeural +Gender: Female + +Name: en-US-EricNeural +Gender: Male + +Name: en-US-GuyNeural +Gender: Male + +Name: en-US-JennyNeural +Gender: Female + +Name: en-US-MichelleNeural +Gender: Female + +Name: en-US-RogerNeural +Gender: Male + +Name: en-US-SteffanNeural +Gender: Male + +Name: en-ZA-LeahNeural +Gender: Female + +Name: en-ZA-LukeNeural +Gender: Male + +Name: es-AR-ElenaNeural +Gender: Female + +Name: es-AR-TomasNeural +Gender: Male + +Name: es-BO-MarceloNeural +Gender: Male + +Name: es-BO-SofiaNeural +Gender: Female + +Name: es-CL-CatalinaNeural +Gender: Female + +Name: es-CL-LorenzoNeural +Gender: Male + +Name: es-CO-GonzaloNeural +Gender: Male + +Name: es-CO-SalomeNeural +Gender: Female + +Name: es-CR-JuanNeural +Gender: Male + +Name: es-CR-MariaNeural +Gender: Female + +Name: es-CU-BelkysNeural +Gender: Female + +Name: es-CU-ManuelNeural +Gender: Male + +Name: es-DO-EmilioNeural +Gender: Male + +Name: es-DO-RamonaNeural +Gender: Female + +Name: es-EC-AndreaNeural +Gender: Female + +Name: es-EC-LuisNeural +Gender: Male + +Name: es-ES-AlvaroNeural +Gender: Male + +Name: es-ES-ElviraNeural +Gender: Female + +Name: es-ES-XimenaNeural +Gender: Female + +Name: es-GQ-JavierNeural +Gender: Male + +Name: es-GQ-TeresaNeural +Gender: Female + +Name: es-GT-AndresNeural +Gender: Male + +Name: es-GT-MartaNeural +Gender: Female + +Name: es-HN-CarlosNeural +Gender: Male + +Name: es-HN-KarlaNeural +Gender: Female + +Name: es-MX-DaliaNeural +Gender: Female + +Name: es-MX-JorgeNeural +Gender: Male + +Name: es-NI-FedericoNeural +Gender: Male + +Name: es-NI-YolandaNeural +Gender: Female + +Name: es-PA-MargaritaNeural +Gender: Female + +Name: es-PA-RobertoNeural +Gender: Male + +Name: es-PE-AlexNeural +Gender: Male + +Name: es-PE-CamilaNeural +Gender: Female + +Name: es-PR-KarinaNeural +Gender: Female + +Name: es-PR-VictorNeural +Gender: Male + +Name: es-PY-MarioNeural +Gender: Male + +Name: es-PY-TaniaNeural +Gender: Female + +Name: es-SV-LorenaNeural +Gender: Female + +Name: es-SV-RodrigoNeural +Gender: Male + +Name: es-US-AlonsoNeural +Gender: Male + +Name: es-US-PalomaNeural +Gender: Female + +Name: es-UY-MateoNeural +Gender: Male + +Name: es-UY-ValentinaNeural +Gender: Female + +Name: es-VE-PaolaNeural +Gender: Female + +Name: es-VE-SebastianNeural +Gender: Male + +Name: et-EE-AnuNeural +Gender: Female + +Name: et-EE-KertNeural +Gender: Male + +Name: fa-IR-DilaraNeural +Gender: Female + +Name: fa-IR-FaridNeural +Gender: Male + +Name: fi-FI-HarriNeural +Gender: Male + +Name: fi-FI-NooraNeural +Gender: Female + +Name: fil-PH-AngeloNeural +Gender: Male + +Name: fil-PH-BlessicaNeural +Gender: Female + +Name: fr-BE-CharlineNeural +Gender: Female + +Name: fr-BE-GerardNeural +Gender: Male + +Name: fr-CA-AntoineNeural +Gender: Male + +Name: fr-CA-JeanNeural +Gender: Male + +Name: fr-CA-SylvieNeural +Gender: Female + +Name: fr-CA-ThierryNeural +Gender: Male + +Name: fr-CH-ArianeNeural +Gender: Female + +Name: fr-CH-FabriceNeural +Gender: Male + +Name: fr-FR-DeniseNeural +Gender: Female + +Name: fr-FR-EloiseNeural +Gender: Female + +Name: fr-FR-HenriNeural +Gender: Male + +Name: fr-FR-RemyMultilingualNeural +Gender: Male + +Name: fr-FR-VivienneMultilingualNeural +Gender: Female + +Name: ga-IE-ColmNeural +Gender: Male + +Name: ga-IE-OrlaNeural +Gender: Female + +Name: gl-ES-RoiNeural +Gender: Male + +Name: gl-ES-SabelaNeural +Gender: Female + +Name: gu-IN-DhwaniNeural +Gender: Female + +Name: gu-IN-NiranjanNeural +Gender: Male + +Name: he-IL-AvriNeural +Gender: Male + +Name: he-IL-HilaNeural +Gender: Female + +Name: hi-IN-MadhurNeural +Gender: Male + +Name: hi-IN-SwaraNeural +Gender: Female + +Name: hr-HR-GabrijelaNeural +Gender: Female + +Name: hr-HR-SreckoNeural +Gender: Male + +Name: hu-HU-NoemiNeural +Gender: Female + +Name: hu-HU-TamasNeural +Gender: Male + +Name: id-ID-ArdiNeural +Gender: Male + +Name: id-ID-GadisNeural +Gender: Female + +Name: is-IS-GudrunNeural +Gender: Female + +Name: is-IS-GunnarNeural +Gender: Male + +Name: it-IT-DiegoNeural +Gender: Male + +Name: it-IT-ElsaNeural +Gender: Female + +Name: it-IT-GiuseppeNeural +Gender: Male + +Name: it-IT-IsabellaNeural +Gender: Female + +Name: ja-JP-KeitaNeural +Gender: Male + +Name: ja-JP-NanamiNeural +Gender: Female + +Name: jv-ID-DimasNeural +Gender: Male + +Name: jv-ID-SitiNeural +Gender: Female + +Name: ka-GE-EkaNeural +Gender: Female + +Name: ka-GE-GiorgiNeural +Gender: Male + +Name: kk-KZ-AigulNeural +Gender: Female + +Name: kk-KZ-DauletNeural +Gender: Male + +Name: km-KH-PisethNeural +Gender: Male + +Name: km-KH-SreymomNeural +Gender: Female + +Name: kn-IN-GaganNeural +Gender: Male + +Name: kn-IN-SapnaNeural +Gender: Female + +Name: ko-KR-HyunsuNeural +Gender: Male + +Name: ko-KR-InJoonNeural +Gender: Male + +Name: ko-KR-SunHiNeural +Gender: Female + +Name: lo-LA-ChanthavongNeural +Gender: Male + +Name: lo-LA-KeomanyNeural +Gender: Female + +Name: lt-LT-LeonasNeural +Gender: Male + +Name: lt-LT-OnaNeural +Gender: Female + +Name: lv-LV-EveritaNeural +Gender: Female + +Name: lv-LV-NilsNeural +Gender: Male + +Name: mk-MK-AleksandarNeural +Gender: Male + +Name: mk-MK-MarijaNeural +Gender: Female + +Name: ml-IN-MidhunNeural +Gender: Male + +Name: ml-IN-SobhanaNeural +Gender: Female + +Name: mn-MN-BataaNeural +Gender: Male + +Name: mn-MN-YesuiNeural +Gender: Female + +Name: mr-IN-AarohiNeural +Gender: Female + +Name: mr-IN-ManoharNeural +Gender: Male + +Name: ms-MY-OsmanNeural +Gender: Male + +Name: ms-MY-YasminNeural +Gender: Female + +Name: mt-MT-GraceNeural +Gender: Female + +Name: mt-MT-JosephNeural +Gender: Male + +Name: my-MM-NilarNeural +Gender: Female + +Name: my-MM-ThihaNeural +Gender: Male + +Name: nb-NO-FinnNeural +Gender: Male + +Name: nb-NO-PernilleNeural +Gender: Female + +Name: ne-NP-HemkalaNeural +Gender: Female + +Name: ne-NP-SagarNeural +Gender: Male + +Name: nl-BE-ArnaudNeural +Gender: Male + +Name: nl-BE-DenaNeural +Gender: Female + +Name: nl-NL-ColetteNeural +Gender: Female + +Name: nl-NL-FennaNeural +Gender: Female + +Name: nl-NL-MaartenNeural +Gender: Male + +Name: pl-PL-MarekNeural +Gender: Male + +Name: pl-PL-ZofiaNeural +Gender: Female + +Name: ps-AF-GulNawazNeural +Gender: Male + +Name: ps-AF-LatifaNeural +Gender: Female + +Name: pt-BR-AntonioNeural +Gender: Male + +Name: pt-BR-FranciscaNeural +Gender: Female + +Name: pt-BR-ThalitaNeural +Gender: Female + +Name: pt-PT-DuarteNeural +Gender: Male + +Name: pt-PT-RaquelNeural +Gender: Female + +Name: ro-RO-AlinaNeural +Gender: Female + +Name: ro-RO-EmilNeural +Gender: Male + +Name: ru-RU-DmitryNeural +Gender: Male + +Name: ru-RU-SvetlanaNeural +Gender: Female + +Name: si-LK-SameeraNeural +Gender: Male + +Name: si-LK-ThiliniNeural +Gender: Female + +Name: sk-SK-LukasNeural +Gender: Male + +Name: sk-SK-ViktoriaNeural +Gender: Female + +Name: sl-SI-PetraNeural +Gender: Female + +Name: sl-SI-RokNeural +Gender: Male + +Name: so-SO-MuuseNeural +Gender: Male + +Name: so-SO-UbaxNeural +Gender: Female + +Name: sq-AL-AnilaNeural +Gender: Female + +Name: sq-AL-IlirNeural +Gender: Male + +Name: sr-RS-NicholasNeural +Gender: Male + +Name: sr-RS-SophieNeural +Gender: Female + +Name: su-ID-JajangNeural +Gender: Male + +Name: su-ID-TutiNeural +Gender: Female + +Name: sv-SE-MattiasNeural +Gender: Male + +Name: sv-SE-SofieNeural +Gender: Female + +Name: sw-KE-RafikiNeural +Gender: Male + +Name: sw-KE-ZuriNeural +Gender: Female + +Name: sw-TZ-DaudiNeural +Gender: Male + +Name: sw-TZ-RehemaNeural +Gender: Female + +Name: ta-IN-PallaviNeural +Gender: Female + +Name: ta-IN-ValluvarNeural +Gender: Male + +Name: ta-LK-KumarNeural +Gender: Male + +Name: ta-LK-SaranyaNeural +Gender: Female + +Name: ta-MY-KaniNeural +Gender: Female + +Name: ta-MY-SuryaNeural +Gender: Male + +Name: ta-SG-AnbuNeural +Gender: Male + +Name: ta-SG-VenbaNeural +Gender: Female + +Name: te-IN-MohanNeural +Gender: Male + +Name: te-IN-ShrutiNeural +Gender: Female + +Name: th-TH-NiwatNeural +Gender: Male + +Name: th-TH-PremwadeeNeural +Gender: Female + +Name: tr-TR-AhmetNeural +Gender: Male + +Name: tr-TR-EmelNeural +Gender: Female + +Name: uk-UA-OstapNeural +Gender: Male + +Name: uk-UA-PolinaNeural +Gender: Female + +Name: ur-IN-GulNeural +Gender: Female + +Name: ur-IN-SalmanNeural +Gender: Male + +Name: ur-PK-AsadNeural +Gender: Male + +Name: ur-PK-UzmaNeural +Gender: Female + +Name: uz-UZ-MadinaNeural +Gender: Female + +Name: uz-UZ-SardorNeural +Gender: Male + +Name: vi-VN-HoaiMyNeural +Gender: Female + +Name: vi-VN-NamMinhNeural +Gender: Male + +Name: zh-CN-XiaoxiaoNeural +Gender: Female + +Name: zh-CN-XiaoyiNeural +Gender: Female + +Name: zh-CN-YunjianNeural +Gender: Male + +Name: zh-CN-YunxiNeural +Gender: Male + +Name: zh-CN-YunxiaNeural +Gender: Male + +Name: zh-CN-YunyangNeural +Gender: Male + +Name: zh-CN-liaoning-XiaobeiNeural +Gender: Female + +Name: zh-CN-shaanxi-XiaoniNeural +Gender: Female + +Name: zh-HK-HiuGaaiNeural +Gender: Female + +Name: zh-HK-HiuMaanNeural +Gender: Female + +Name: zh-HK-WanLungNeural +Gender: Male + +Name: zh-TW-HsiaoChenNeural +Gender: Female + +Name: zh-TW-HsiaoYuNeural +Gender: Female + +Name: zh-TW-YunJheNeural +Gender: Male + +Name: zu-ZA-ThandoNeural +Gender: Female + +Name: zu-ZA-ThembaNeural +Gender: Male + """.strip() + voices = [] + name = '' + for line in voices_str.split("\n"): + line = line.strip() + if not line: + continue + if line.startswith("Name: "): + name = line[6:].strip() + if line.startswith("Gender: "): + gender = line[8:].strip() + if name and gender: + # voices.append({ + # "name": name, + # "gender": gender, + # }) + if filter_locals: + for filter_local in filter_locals: + if name.lower().startswith(filter_local.lower()): + voices.append(f"{name}-{gender}") + else: + voices.append(f"{name}-{gender}") + name = '' + voices.sort() + return voices + + +def parse_voice_name(name: str): + # zh-CN-XiaoyiNeural-Female + # zh-CN-YunxiNeural-Male + name = name.replace("-Female", "").replace("-Male", "").strip() + return name + + def tts(text: str, voice_name: str, voice_file: str) -> [SubMaker, None]: logger.info(f"start, voice name: {voice_name}") try: @@ -101,6 +1079,10 @@ def get_audio_duration(sub_maker: submaker.SubMaker): if __name__ == "__main__": + voices = get_all_voices() + print(voices) + print(len(voices)) + async def _do(): temp_dir = utils.storage_dir("temp") diff --git a/webui/Main.py b/webui/Main.py index 29be71f..341c294 100644 --- a/webui/Main.py +++ b/webui/Main.py @@ -1,15 +1,28 @@ +import json +import locale import streamlit as st - -st.set_page_config(page_title="MoneyPrinterTurbo", page_icon="🤖", layout="wide", - initial_sidebar_state="auto") import sys import os from uuid import uuid4 import platform import streamlit.components.v1 as components +import toml from loguru import logger -from app.models.schema import VideoParams, VideoAspect, VoiceNames, VideoConcatMode -from app.services import task as tm, llm + +st.set_page_config(page_title="MoneyPrinterTurbo", + page_icon="🤖", + layout="wide", + initial_sidebar_state="auto", + menu_items={ + 'Report a bug': "https://github.com/harry0703/MoneyPrinterTurbo/issues", + 'About': "# MoneyPrinterTurbo\nSimply provide a topic or keyword for a video, and it will " + "automatically generate the video copy, video materials, video subtitles, " + "and video background music before synthesizing a high-definition short " + "video.\n\nhttps://github.com/harry0703/MoneyPrinterTurbo" + }) + +from app.models.schema import VideoParams, VideoAspect, VideoConcatMode +from app.services import task as tm, llm, voice from app.utils import utils hide_streamlit_style = """ @@ -21,6 +34,35 @@ st.title("MoneyPrinterTurbo") root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) font_dir = os.path.join(root_dir, "resource", "fonts") song_dir = os.path.join(root_dir, "resource", "songs") +i18n_dir = os.path.join(root_dir, "webui", "i18n") +config_file = os.path.join(root_dir, "webui", ".streamlit", "webui.toml") + + +def load_config() -> dict: + try: + return toml.load(config_file) + except Exception as e: + return {} + + +cfg = load_config() + + +def save_config(): + with open(config_file, "w", encoding="utf-8") as f: + f.write(toml.dumps(cfg)) + + +def get_system_locale(): + try: + loc = locale.getdefaultlocale() + # zh_CN, zh_TW return zh + # en_US, en_GB return en + language_code = loc[0].split("_")[0] + return language_code + except Exception as e: + return "en" + if 'video_subject' not in st.session_state: st.session_state['video_subject'] = '' @@ -28,6 +70,8 @@ if 'video_script' not in st.session_state: st.session_state['video_script'] = '' if 'video_terms' not in st.session_state: st.session_state['video_terms'] = '' +if 'ui_language' not in st.session_state: + st.session_state['ui_language'] = cfg.get("ui_language", get_system_locale()) def get_all_fonts(): @@ -109,113 +153,154 @@ def init_log(): init_log() + +def load_locales(): + locales = {} + for root, dirs, files in os.walk(i18n_dir): + for file in files: + if file.endswith(".json"): + lang = file.split(".")[0] + with open(os.path.join(root, file), "r", encoding="utf-8") as f: + locales[lang] = json.loads(f.read()) + return locales + + +locales = load_locales() + + +def tr(key): + loc = locales.get(st.session_state['ui_language'], {}) + return loc.get("Translation", {}).get(key, key) + + +display_languages = [] +selected_index = 0 +for i, code in enumerate(locales.keys()): + display_languages.append(f"{code} - {locales[code].get('Language')}") + if code == st.session_state['ui_language']: + selected_index = i + +selected_language = st.selectbox("Language", options=display_languages, label_visibility='collapsed', + index=selected_index) +if selected_language: + code = selected_language.split(" - ")[0].strip() + st.session_state['ui_language'] = code + cfg['ui_language'] = code + save_config() + panel = st.columns(3) left_panel = panel[0] middle_panel = panel[1] right_panel = panel[2] -cfg = VideoParams() +params = VideoParams() with left_panel: with st.container(border=True): - st.write("**文案设置**") - cfg.video_subject = st.text_input("视频主题(给定一个关键词,:red[AI自动生成]视频文案)", - value=st.session_state['video_subject']).strip() + st.write(tr("Video Script Settings")) + params.video_subject = st.text_input(tr("Video Subject"), + value=st.session_state['video_subject']).strip() video_languages = [ - ("自动判断(Auto detect)", ""), + (tr("Auto Detect"), ""), ] - for lang in ["zh-CN", "zh-TW", "en-US"]: - video_languages.append((lang, lang)) + for code in ["zh-CN", "zh-TW", "en-US"]: + video_languages.append((code, code)) - selected_index = st.selectbox("生成视频脚本的语言(:blue[一般情况AI会自动根据你输入的主题语言输出])", + selected_index = st.selectbox(tr("Script Language"), index=0, options=range(len(video_languages)), # 使用索引作为内部选项值 format_func=lambda x: video_languages[x][0] # 显示给用户的是标签 ) - cfg.video_language = video_languages[selected_index][1] + params.video_language = video_languages[selected_index][1] - if cfg.video_language: - st.write(f"设置AI输出文案语言为: **:red[{cfg.video_language}]**") - - if st.button("点击使用AI根据**主题**生成 【视频文案】 和 【视频关键词】", key="auto_generate_script"): - with st.spinner("AI正在生成视频文案和关键词..."): - script = llm.generate_script(video_subject=cfg.video_subject, language=cfg.video_language) - terms = llm.generate_terms(cfg.video_subject, script) - st.toast('AI生成成功') + if st.button(tr("Generate Video Script and Keywords"), key="auto_generate_script"): + with st.spinner(tr("Generating Video Script and Keywords")): + script = llm.generate_script(video_subject=params.video_subject, language=params.video_language) + terms = llm.generate_terms(params.video_subject, script) st.session_state['video_script'] = script st.session_state['video_terms'] = ", ".join(terms) - cfg.video_script = st.text_area( - "视频文案(:blue[①可不填,使用AI生成 ②合理使用标点断句,有助于生成字幕])", + params.video_script = st.text_area( + tr("Video Script"), value=st.session_state['video_script'], height=180 ) - if st.button("点击使用AI根据**文案**生成【视频关键词】", key="auto_generate_terms"): - if not cfg.video_script: - st.error("请先填写视频文案") + if st.button(tr("Generate Video Keywords"), key="auto_generate_terms"): + if not params.video_script: + st.error(tr("Please Enter the Video Subject")) st.stop() - with st.spinner("AI正在生成视频关键词..."): - terms = llm.generate_terms(cfg.video_subject, cfg.video_script) - st.toast('AI生成成功') + with st.spinner(tr("Generating Video Keywords")): + terms = llm.generate_terms(params.video_subject, params.video_script) st.session_state['video_terms'] = ", ".join(terms) - cfg.video_terms = st.text_area( - "视频关键词(:blue[①可不填,使用AI生成 ②用**英文逗号**分隔,只支持英文])", + params.video_terms = st.text_area( + tr("Video Keywords"), value=st.session_state['video_terms'], height=50) with middle_panel: with st.container(border=True): - st.write("**视频设置**") + st.write(tr("Video Settings")) video_concat_modes = [ - ("顺序拼接", "sequential"), - ("随机拼接(推荐)", "random"), + (tr("Sequential"), "sequential"), + (tr("Random"), "random"), ] - selected_index = st.selectbox("视频拼接模式", + selected_index = st.selectbox(tr("Video Concat Mode"), index=1, options=range(len(video_concat_modes)), # 使用索引作为内部选项值 format_func=lambda x: video_concat_modes[x][0] # 显示给用户的是标签 ) - cfg.video_concat_mode = VideoConcatMode(video_concat_modes[selected_index][1]) + params.video_concat_mode = VideoConcatMode(video_concat_modes[selected_index][1]) video_aspect_ratios = [ - ("竖屏 9:16(抖音视频)", VideoAspect.portrait.value), - ("横屏 16:9(西瓜视频)", VideoAspect.landscape.value), - # ("方形 1:1", VideoAspect.square.value) + (tr("Portrait"), VideoAspect.portrait.value), + (tr("Landscape"), VideoAspect.landscape.value), ] - selected_index = st.selectbox("视频比例", + selected_index = st.selectbox(tr("Video Ratio"), options=range(len(video_aspect_ratios)), # 使用索引作为内部选项值 format_func=lambda x: video_aspect_ratios[x][0] # 显示给用户的是标签 ) - cfg.video_aspect = VideoAspect(video_aspect_ratios[selected_index][1]) + params.video_aspect = VideoAspect(video_aspect_ratios[selected_index][1]) - cfg.video_clip_duration = st.selectbox("视频片段最大时长(秒)", options=[2, 3, 4, 5, 6], index=1) - cfg.video_count = st.selectbox("同时生成视频数量", options=[1, 2, 3, 4, 5], index=0) + params.video_clip_duration = st.selectbox(tr("Clip Duration"), options=[2, 3, 4, 5, 6], index=1) + params.video_count = st.selectbox(tr("Number of Videos Generated Simultaneously"), options=[1, 2, 3, 4, 5], + index=0) with st.container(border=True): - st.write("**音频设置**") - # 创建一个映射字典,将原始值映射到友好名称 + st.write(tr("Audio Settings")) + voices = voice.get_all_voices(filter_locals=["zh-CN", "zh-HK", "zh-TW", "en-US"]) friendly_names = { voice: voice. - replace("female", "女性"). - replace("male", "男性"). - replace("zh-CN", "中文"). - replace("zh-HK", "香港"). - replace("zh-TW", "台湾"). - replace("en-US", "英文"). + replace("Female", tr("Female")). + replace("Male", tr("Male")). replace("Neural", "") for - voice in VoiceNames} - selected_friendly_name = st.selectbox("朗读声音", options=list(friendly_names.values())) + voice in voices} + saved_voice_name = cfg.get("voice_name", "") + saved_voice_name_index = 0 + if saved_voice_name in friendly_names: + saved_voice_name_index = list(friendly_names.keys()).index(saved_voice_name) + else: + for i, voice in enumerate(voices): + if voice.lower().startswith(st.session_state['ui_language'].lower()): + saved_voice_name_index = i + break + + selected_friendly_name = st.selectbox(tr("Speech Synthesis"), + options=list(friendly_names.values()), + index=saved_voice_name_index) + voice_name = list(friendly_names.keys())[list(friendly_names.values()).index(selected_friendly_name)] - cfg.voice_name = voice_name + params.voice_name = voice_name + cfg['voice_name'] = voice_name + save_config() bgm_options = [ - ("无背景音乐 No BGM", ""), - ("随机背景音乐 Random BGM", "random"), - ("自定义背景音乐 Custom BGM", "custom"), + (tr("No Background Music"), ""), + (tr("Random Background Music"), "random"), + (tr("Custom Background Music"), "custom"), ] - selected_index = st.selectbox("背景音乐", + selected_index = st.selectbox(tr("Background Music"), index=1, options=range(len(bgm_options)), # 使用索引作为内部选项值 format_func=lambda x: bgm_options[x][0] # 显示给用户的是标签 @@ -225,49 +310,49 @@ with middle_panel: # 根据选择显示或隐藏组件 if bgm_type == "custom": - custom_bgm_file = st.text_input("请输入自定义背景音乐的文件路径:") + custom_bgm_file = st.text_input(tr("Custom Background Music File")) if custom_bgm_file and os.path.exists(custom_bgm_file): - cfg.bgm_file = custom_bgm_file + params.bgm_file = custom_bgm_file # st.write(f":red[已选择自定义背景音乐]:**{custom_bgm_file}**") - cfg.bgm_volume = st.selectbox("背景音乐音量(0.2表示20%,背景声音不宜过高)", - options=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], index=2) + params.bgm_volume = st.selectbox(tr("Background Music Volume"), + options=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], index=2) with right_panel: with st.container(border=True): - st.write("**字幕设置**") - cfg.subtitle_enabled = st.checkbox("生成字幕(若取消勾选,下面的设置都将不生效)", value=True) + st.write(tr("Subtitle Settings")) + params.subtitle_enabled = st.checkbox(tr("Enable Subtitles"), value=True) font_names = get_all_fonts() - cfg.font_name = st.selectbox("字体", font_names) + params.font_name = st.selectbox(tr("Font"), font_names) subtitle_positions = [ - ("顶部(top)", "top"), - ("居中(center)", "center"), - ("底部(bottom,推荐)", "bottom"), + (tr("Top"), "top"), + (tr("Middle"), "center"), + (tr("Bottom"), "bottom"), ] - selected_index = st.selectbox("字幕位置", + selected_index = st.selectbox(tr("Position"), index=2, options=range(len(subtitle_positions)), # 使用索引作为内部选项值 format_func=lambda x: subtitle_positions[x][0] # 显示给用户的是标签 ) - cfg.subtitle_position = subtitle_positions[selected_index][1] + params.subtitle_position = subtitle_positions[selected_index][1] font_cols = st.columns([0.3, 0.7]) with font_cols[0]: - cfg.text_fore_color = st.color_picker("字幕颜色", "#FFFFFF") + params.text_fore_color = st.color_picker(tr("Font Color"), "#FFFFFF") with font_cols[1]: - cfg.font_size = st.slider("字幕大小", 30, 100, 60) + params.font_size = st.slider(tr("Font Size"), 30, 100, 60) stroke_cols = st.columns([0.3, 0.7]) with stroke_cols[0]: - cfg.stroke_color = st.color_picker("描边颜色", "#000000") + params.stroke_color = st.color_picker(tr("Stroke Color"), "#000000") with stroke_cols[1]: - cfg.stroke_width = st.slider("描边粗细", 0.0, 10.0, 1.5) + params.stroke_width = st.slider(tr("Stroke Width"), 0.0, 10.0, 1.5) -start_button = st.button("开始生成视频", use_container_width=True, type="primary") +start_button = st.button(tr("Generate Video"), use_container_width=True, type="primary") if start_button: task_id = str(uuid4()) - if not cfg.video_subject and not cfg.video_script: - st.error("视频主题 或 视频文案,不能同时为空") + if not params.video_subject and not params.video_script: + st.error(tr("Video Script and Subject Cannot Both Be Empty")) scroll_to_bottom() st.stop() @@ -283,11 +368,11 @@ if start_button: logger.add(log_received) - st.toast("正在生成视频,请稍候...") - logger.info("开始生成视频") - logger.info(utils.to_json(cfg)) + st.toast(tr("Generating Video")) + logger.info(tr("Start Generating Video")) + logger.info(utils.to_json(params)) scroll_to_bottom() - tm.start(task_id=task_id, params=cfg) + tm.start(task_id=task_id, params=params) open_task_folder(task_id) - logger.info(f"完成") + logger.info(tr("Video Generation Completed")) diff --git a/webui/i18n/en.json b/webui/i18n/en.json new file mode 100644 index 0000000..7a3e14d --- /dev/null +++ b/webui/i18n/en.json @@ -0,0 +1,51 @@ +{ + "Language": "English", + "Translation": { + "Video Script Settings": "**Video Script Settings**", + "Video Subject": "Video Subject (Provide a keyword, :red[AI will automatically generate] video script)", + "Script Language": "Language for Generating Video Script (AI will automatically output based on the language of your subject)", + "Generate Video Script and Keywords": "Click to use AI to generate 【Video Script】 and 【Video Keywords】 based on **subject**", + "Auto Detect": "Auto Detect", + "Video Script": "Video Script (:blue[① Optional, AI generated ② Proper punctuation helps with subtitle generation])", + "Generate Video Keywords": "Click to use AI to generate 【Video Keywords】 based on **script**", + "Please Enter the Video Subject": "Please Enter the Video Script First", + "Generating Video Script and Keywords": "AI is generating video script and keywords...", + "Generating Video Keywords": "AI is generating video keywords...", + "Video Keywords": "Video Keywords (:blue[① Optional, AI generated ② Use **English commas** for separation, English only])", + "Video Settings": "**Video Settings**", + "Video Concat Mode": "Video Concatenation Mode", + "Random": "Random Concatenation (Recommended)", + "Sequential": "Sequential Concatenation", + "Video Ratio": "Video Aspect Ratio", + "Portrait": "Portrait 9:16", + "Landscape": "Landscape 16:9", + "Clip Duration": "Maximum Duration of Video Clips (seconds)", + "Number of Videos Generated Simultaneously": "Number of Videos Generated Simultaneously", + "Audio Settings": "**Audio Settings**", + "Speech Synthesis": "Speech Synthesis Voice", + "Male": "Male", + "Female": "Female", + "Background Music": "Background Music", + "No Background Music": "No Background Music", + "Random Background Music": "Random Background Music", + "Custom Background Music": "Custom Background Music", + "Custom Background Music File": "Please enter the file path for custom background music:", + "Background Music Volume": "Background Music Volume (0.2 represents 20%, background music should not be too loud)", + "Subtitle Settings": "**Subtitle Settings**", + "Enable Subtitles": "Enable Subtitles (If unchecked, the settings below will not take effect)", + "Font": "Subtitle Font", + "Position": "Subtitle Position", + "Top": "Top", + "Center": "Middle", + "Bottom": "Bottom (Recommended)", + "Font Size": "Subtitle Font Size", + "Font Color": "Subtitle Font Color", + "Stroke Color": "Subtitle Outline Color", + "Stroke Width": "Subtitle Outline Width", + "Generate Video": "Generate Video", + "Video Script and Subject Cannot Both Be Empty": "Video Subject and Video Script cannot both be empty", + "Generating Video": "Generating video, please wait...", + "Start Generating Video": "Start Generating Video", + "Video Generation Completed": "Video Generation Completed" + } +} \ No newline at end of file diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json new file mode 100644 index 0000000..269c725 --- /dev/null +++ b/webui/i18n/zh.json @@ -0,0 +1,51 @@ +{ + "Language": "简体中文", + "Translation": { + "Video Script Settings": "**文案设置**", + "Video Subject": "视频主题(给定一个关键词,:red[AI自动生成]视频文案)", + "Script Language": "生成视频脚本的语言(一般情况AI会自动根据你输入的主题语言输出)", + "Generate Video Script and Keywords": "点击使用AI根据**主题**生成 【视频文案】 和 【视频关键词】", + "Auto Detect": "自动检测", + "Video Script": "视频文案(:blue[①可不填,使用AI生成 ②合理使用标点断句,有助于生成字幕])", + "Generate Video Keywords": "点击使用AI根据**文案**生成【视频关键词】", + "Please Enter the Video Subject": "请先填写视频文案", + "Generating Video Script and Keywords": "AI正在生成视频文案和关键词...", + "Generating Video Keywords": "AI正在生成视频关键词...", + "Video Keywords": "视频关键词(:blue[①可不填,使用AI生成 ②用**英文逗号**分隔,只支持英文])", + "Video Settings": "**视频设置**", + "Video Concat Mode": "视频拼接模式", + "Random": "随机拼接(推荐)", + "Sequential": "顺序拼接", + "Video Ratio": "视频比例", + "Portrait": "竖屏 9:16(抖音视频)", + "Landscape": "横屏 16:9(西瓜视频)", + "Clip Duration": "视频片段最大时长(秒)", + "Number of Videos Generated Simultaneously": "同时生成视频数量", + "Audio Settings": "**音频设置**", + "Speech Synthesis": "朗读声音", + "Male": "男性", + "Female": "女性", + "Background Music": "背景音乐", + "No Background Music": "无背景音乐", + "Random Background Music": "随机背景音乐", + "Custom Background Music": "自定义背景音乐", + "Custom Background Music File": "请输入自定义背景音乐的文件路径", + "Background Music Volume": "背景音乐音量(0.2表示20%,背景声音不宜过高)", + "Subtitle Settings": "**字幕设置**", + "Enable Subtitles": "启用字幕(若取消勾选,下面的设置都将不生效)", + "Font": "字幕字体", + "Position": "字幕位置", + "Top": "顶部", + "Center": "中间", + "Bottom": "底部(推荐)", + "Font Size": "字幕大小", + "Font Color": "字幕颜色", + "Stroke Color": "描边颜色", + "Stroke Width": "描边粗细", + "Generate Video": "生成视频", + "Video Script and Subject Cannot Both Be Empty": "视频主题 和 视频文案,不能同时为空", + "Generating Video": "正在生成视频,请稍候...", + "Start Generating Video": "开始生成视频", + "Video Generation Completed": "视频生成完成" + } +} \ No newline at end of file