From c5d03fb3cbf5105aa45dc131474260cf140b748b Mon Sep 17 00:00:00 2001 From: Vega Date: Mon, 9 May 2022 18:44:02 +0800 Subject: [PATCH] Upgrade to new web service (#529) * Init new GUI * Remove unused codes * Reset layout * Add samples * Make framework to support multiple pages * Add vc mode * Add preprocessing mode * Add training mode * Remove text input in vc mode * Add entry for GUI and revise readme * Move requirement together * Add error raise when no model folder found * Add readme --- .gitignore | 1 - .vscode/launch.json | 8 + README-CN.md | 16 +- README.md | 8 + gui/___init__.py => mkgui/__init__.py | 0 {gui => mkgui}/app.py | 70 +- mkgui/app_vc.py | 167 +++++ mkgui/base/__init__.py | 2 + mkgui/base/api/__init__.py | 1 + mkgui/base/api/fastapi_utils.py | 102 +++ mkgui/base/components/__init__.py | 0 mkgui/base/components/outputs.py | 43 ++ mkgui/base/components/types.py | 46 ++ mkgui/base/core.py | 203 ++++++ mkgui/base/ui/__init__.py | 1 + mkgui/base/ui/schema_utils.py | 129 ++++ mkgui/base/ui/streamlit_ui.py | 885 ++++++++++++++++++++++++++ mkgui/base/ui/streamlit_utils.py | 13 + mkgui/preprocess.py | 96 +++ mkgui/static/mb.png | Bin 0 -> 5748 bytes mkgui/train.py | 156 +++++ ppg2mel/__init__.py | 9 +- ppg2mel/preprocess.py | 1 + ppg2mel/train.py | 11 +- ppg2mel/train/solver.py | 3 +- requirements.txt | 6 +- requirements_vc.txt | 3 - samples/T0055G0013S0005.wav | Bin 0 -> 121002 bytes toolbox/__init__.py | 7 +- utils/util.py | 6 + vocoder/hifigan/env.py | 7 - vocoder/hifigan/inference.py | 8 +- vocoder/hifigan/train.py | 1 - vocoder_train.py | 2 +- web.py | 26 +- 35 files changed, 1966 insertions(+), 71 deletions(-) rename gui/___init__.py => mkgui/__init__.py (100%) rename {gui => mkgui}/app.py (68%) create mode 100644 mkgui/app_vc.py create mode 100644 mkgui/base/__init__.py create mode 100644 mkgui/base/api/__init__.py create mode 100644 mkgui/base/api/fastapi_utils.py create mode 100644 mkgui/base/components/__init__.py create mode 100644 mkgui/base/components/outputs.py create mode 100644 mkgui/base/components/types.py create mode 100644 mkgui/base/core.py create mode 100644 mkgui/base/ui/__init__.py create mode 100644 mkgui/base/ui/schema_utils.py create mode 100644 mkgui/base/ui/streamlit_ui.py create mode 100644 mkgui/base/ui/streamlit_utils.py create mode 100644 mkgui/preprocess.py create mode 100644 mkgui/static/mb.png create mode 100644 mkgui/train.py delete mode 100644 requirements_vc.txt create mode 100644 samples/T0055G0013S0005.wav diff --git a/.gitignore b/.gitignore index 7df88c7..5302980 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ *.bbl *.bcf *.toc -*.wav *.sh */saved_models !vocoder/saved_models/pretrained/** diff --git a/.vscode/launch.json b/.vscode/launch.json index 23e5203..85cf175 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -61,5 +61,13 @@ "-m", ".\\ppg2mel\\saved_models\\best_loss_step_304000.pth", "--wav_dir", ".\\wavs\\input", "--ref_wav_path", ".\\wavs\\pkq.mp3", "-o", ".\\wavs\\output\\" ] }, + { + "name": "GUI", + "type": "python", + "request": "launch", + "program": "mkgui\\base\\_cli.py", + "console": "integratedTerminal", + "args": [] + }, ] } diff --git a/README-CN.md b/README-CN.md index d0bfffa..038deb5 100644 --- a/README-CN.md +++ b/README-CN.md @@ -18,6 +18,15 @@ 🌍 **Webserver Ready** 可伺服你的训练结果,供远程调用 +### 进行中的工作 +* GUI/客户端大升级与合并 +[X] 初始化框架 `./mkgui` (基于streamlit + fastapi)和 [技术设计](https://vaj2fgg8yn.feishu.cn/docs/doccnvotLWylBub8VJIjKzoEaee) +[X] 增加 Voice Cloning and Conversion的演示页面 +[X] 增加Voice Conversion的预处理preprocessing 和训练 training 页面 +[ ] 增加其他的的预处理preprocessing 和训练 training 页面 +* 模型后端基于ESPnet2升级 + + ## 开始 ### 1. 安装要求 > 按照原始存储库测试您是否已准备好所有环境。 @@ -82,15 +91,10 @@ ### 3. 启动程序或工具箱 您可以尝试使用以下命令: -### 3.1 启动Web程序: +### 3.1 启动Web程序(v2): `python web.py` 运行成功后在浏览器打开地址, 默认为 `http://localhost:8080` -![123](https://user-images.githubusercontent.com/12797292/135494044-ae59181c-fe3a-406f-9c7d-d21d12fdb4cb.png) -> 注:目前界面比较buggy, -> * 第一次点击`录制`要等待几秒浏览器正常启动录音,否则会有重音 -> * 录制结束不要再点`录制`而是`停止` > * 仅支持手动新录音(16khz), 不支持超过4MB的录音,最佳长度在5~15秒 -> * 默认使用第一个找到的模型,有动手能力的可以看代码修改 `web\__init__.py`。 ### 3.2 启动工具箱: `python demo_toolbox.py -d ` diff --git a/README.md b/README.md index 9bfcd78..443dcf0 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,14 @@ ### [DEMO VIDEO](https://www.bilibili.com/video/BV17Q4y1B7mY/) +### Ongoing Works(Helps Needed) +* Major upgrade on GUI/Client and unifying web and toolbox +[X] Init framework `./mkgui` and [tech design](https://vaj2fgg8yn.feishu.cn/docs/doccnvotLWylBub8VJIjKzoEaee) +[X] Add demo part of Voice Cloning and Conversion +[X] Add preprocessing and training for Voice Conversion +[ ] Add preprocessing and training for Encoder/Synthesizer/Vocoder +* Major upgrade on model backend based on ESPnet2(not yet started) + ## Quick Start ### 1. Install Requirements diff --git a/gui/___init__.py b/mkgui/__init__.py similarity index 100% rename from gui/___init__.py rename to mkgui/__init__.py diff --git a/gui/app.py b/mkgui/app.py similarity index 68% rename from gui/app.py rename to mkgui/app.py index d753126..9487284 100644 --- a/gui/app.py +++ b/mkgui/app.py @@ -8,9 +8,11 @@ import librosa from scipy.io.wavfile import write import re import numpy as np -from opyrator.components.types import FileContent +from mkgui.base.components.types import FileContent from vocoder.hifigan import inference as gan_vocoder from synthesizer.inference import Synthesizer +from typing import Any +import matplotlib.pyplot as plt # Constants AUDIO_SAMPLES_DIR = 'samples\\' @@ -27,20 +29,31 @@ if os.path.isdir(AUDIO_SAMPLES_DIR): if os.path.isdir(SYN_MODELS_DIRT): synthesizers = Enum('synthesizers', list((file.name, file) for file in Path(SYN_MODELS_DIRT).glob("**/*.pt"))) print("Loaded synthesizer models: " + str(len(synthesizers))) +else: + raise Exception(f"Model folder {SYN_MODELS_DIRT} doesn't exist.") + if os.path.isdir(ENC_MODELS_DIRT): encoders = Enum('encoders', list((file.name, file) for file in Path(ENC_MODELS_DIRT).glob("**/*.pt"))) print("Loaded encoders models: " + str(len(encoders))) +else: + raise Exception(f"Model folder {ENC_MODELS_DIRT} doesn't exist.") + if os.path.isdir(VOC_MODELS_DIRT): vocoders = Enum('vocoders', list((file.name, file) for file in Path(VOC_MODELS_DIRT).glob("**/*gan*.pt"))) print("Loaded vocoders models: " + str(len(synthesizers))) +else: + raise Exception(f"Model folder {VOC_MODELS_DIRT} doesn't exist.") class Input(BaseModel): + message: str = Field( + ..., example="欢迎使用工具箱, 现已支持中文输入!", alias="文本内容" + ) local_audio_file: audio_input_selection = Field( ..., alias="输入语音(本地wav)", description="选择本地语音文件." ) - upload_audio_file: FileContent = Field(..., alias="或上传语音", + upload_audio_file: FileContent = Field(default=None, alias="或上传语音", description="拖拽或点击上传.", mime_type="audio/wav") encoder: encoders = Field( ..., alias="编码模型", @@ -48,37 +61,48 @@ class Input(BaseModel): ) synthesizer: synthesizers = Field( ..., alias="合成模型", - description="选择语音编码模型文件." + description="选择语音合成模型文件." ) vocoder: vocoders = Field( - ..., alias="语音编码模型", - description="选择语音编码模型文件(目前只支持HifiGan类型)." - ) - message: str = Field( - ..., example="欢迎使用工具箱, 现已支持中文输入!", alias="输出文本内容" + ..., alias="语音解码模型", + description="选择语音解码模型文件(目前只支持HifiGan类型)." ) +class AudioEntity(BaseModel): + content: bytes + mel: Any + class Output(BaseModel): - result_file: FileContent = Field( - ..., - mime_type="audio/wav", - description="输出音频", - ) - source_file: FileContent = Field( - ..., - mime_type="audio/wav", - description="原始音频.", - ) + __root__: tuple[AudioEntity, AudioEntity] -def mocking_bird(input: Input) -> Output: - """欢迎使用MockingBird Web 2""" + def render_output_ui(self, streamlit_app, input) -> None: # type: ignore + """Custom output UI. + If this method is implmeneted, it will be used instead of the default Output UI renderer. + """ + src, result = self.__root__ + + streamlit_app.subheader("Synthesized Audio") + streamlit_app.audio(result.content, format="audio/wav") + + fig, ax = plt.subplots() + ax.imshow(src.mel, aspect="equal", interpolation="none") + ax.set_title("mel spectrogram(Source Audio)") + streamlit_app.pyplot(fig) + fig, ax = plt.subplots() + ax.imshow(result.mel, aspect="equal", interpolation="none") + ax.set_title("mel spectrogram(Result Audio)") + streamlit_app.pyplot(fig) + + +def synthesize(input: Input) -> Output: + """synthesize(合成)""" # load models encoder.load_model(Path(input.encoder.value)) current_synt = Synthesizer(Path(input.synthesizer.value)) gan_vocoder.load_model(Path(input.vocoder.value)) # load file - if input.upload_audio_file != NULL: + if input.upload_audio_file != None: with open(TEMP_SOURCE_AUDIO, "w+b") as f: f.write(input.upload_audio_file.as_bytes()) f.seek(0) @@ -87,6 +111,8 @@ def mocking_bird(input: Input) -> Output: wav, sample_rate = librosa.load(input.local_audio_file.value) write(TEMP_SOURCE_AUDIO, sample_rate, wav) #Make sure we get the correct wav + source_spec = Synthesizer.make_spectrogram(wav) + # preprocess encoder_wav = encoder.preprocess_wav(wav, sample_rate) embed, _, _ = encoder.embed_utterance(encoder_wav, return_partials=True) @@ -114,4 +140,4 @@ def mocking_bird(input: Input) -> Output: source_file = f.read() with open(TEMP_RESULT_AUDIO, "rb") as f: result_file = f.read() - return Output(source_file=source_file, result_file=result_file) \ No newline at end of file + return Output(__root__=(AudioEntity(content=source_file, mel=source_spec), AudioEntity(content=result_file, mel=spec))) \ No newline at end of file diff --git a/mkgui/app_vc.py b/mkgui/app_vc.py new file mode 100644 index 0000000..3e4c793 --- /dev/null +++ b/mkgui/app_vc.py @@ -0,0 +1,167 @@ +from asyncio.windows_events import NULL +from synthesizer.inference import Synthesizer +from pydantic import BaseModel, Field +from encoder import inference as speacker_encoder +import torch +import os +from pathlib import Path +from enum import Enum +import ppg_extractor as Extractor +import ppg2mel as Convertor +import librosa +from scipy.io.wavfile import write +import re +import numpy as np +from mkgui.base.components.types import FileContent +from vocoder.hifigan import inference as gan_vocoder +from typing import Any +import matplotlib.pyplot as plt + + +# Constants +AUDIO_SAMPLES_DIR = 'samples\\' +EXT_MODELS_DIRT = "ppg_extractor\\saved_models" +CONV_MODELS_DIRT = "ppg2mel\\saved_models" +VOC_MODELS_DIRT = "vocoder\\saved_models" +TEMP_SOURCE_AUDIO = "wavs/temp_source.wav" +TEMP_TARGET_AUDIO = "wavs/temp_target.wav" +TEMP_RESULT_AUDIO = "wavs/temp_result.wav" + +# Load local sample audio as options TODO: load dataset +if os.path.isdir(AUDIO_SAMPLES_DIR): + audio_input_selection = Enum('samples', list((file.name, file) for file in Path(AUDIO_SAMPLES_DIR).glob("*.wav"))) +# Pre-Load models +if os.path.isdir(EXT_MODELS_DIRT): + extractors = Enum('extractors', list((file.name, file) for file in Path(EXT_MODELS_DIRT).glob("**/*.pt"))) + print("Loaded extractor models: " + str(len(extractors))) +else: + raise Exception(f"Model folder {EXT_MODELS_DIRT} doesn't exist.") + +if os.path.isdir(CONV_MODELS_DIRT): + convertors = Enum('convertors', list((file.name, file) for file in Path(CONV_MODELS_DIRT).glob("**/*.pth"))) + print("Loaded convertor models: " + str(len(convertors))) +else: + raise Exception(f"Model folder {CONV_MODELS_DIRT} doesn't exist.") + +if os.path.isdir(VOC_MODELS_DIRT): + vocoders = Enum('vocoders', list((file.name, file) for file in Path(VOC_MODELS_DIRT).glob("**/*gan*.pt"))) + print("Loaded vocoders models: " + str(len(vocoders))) +else: + raise Exception(f"Model folder {VOC_MODELS_DIRT} doesn't exist.") + +class Input(BaseModel): + local_audio_file: audio_input_selection = Field( + ..., alias="输入语音(本地wav)", + description="选择本地语音文件." + ) + upload_audio_file: FileContent = Field(default=None, alias="或上传语音", + description="拖拽或点击上传.", mime_type="audio/wav") + local_audio_file_target: audio_input_selection = Field( + ..., alias="目标语音(本地wav)", + description="选择本地语音文件." + ) + upload_audio_file_target: FileContent = Field(default=None, alias="或上传目标语音", + description="拖拽或点击上传.", mime_type="audio/wav") + extractor: extractors = Field( + ..., alias="编码模型", + description="选择语音编码模型文件." + ) + convertor: convertors = Field( + ..., alias="转换模型", + description="选择语音转换模型文件." + ) + vocoder: vocoders = Field( + ..., alias="语音编码模型", + description="选择语音解码模型文件(目前只支持HifiGan类型)." + ) + +class AudioEntity(BaseModel): + content: bytes + mel: Any + +class Output(BaseModel): + __root__: tuple[AudioEntity, AudioEntity, AudioEntity] + + def render_output_ui(self, streamlit_app, input) -> None: # type: ignore + """Custom output UI. + If this method is implmeneted, it will be used instead of the default Output UI renderer. + """ + src, target, result = self.__root__ + + streamlit_app.subheader("Synthesized Audio") + streamlit_app.audio(result.content, format="audio/wav") + + fig, ax = plt.subplots() + ax.imshow(src.mel, aspect="equal", interpolation="none") + ax.set_title("mel spectrogram(Source Audio)") + streamlit_app.pyplot(fig) + fig, ax = plt.subplots() + ax.imshow(target.mel, aspect="equal", interpolation="none") + ax.set_title("mel spectrogram(Target Audio)") + streamlit_app.pyplot(fig) + fig, ax = plt.subplots() + ax.imshow(result.mel, aspect="equal", interpolation="none") + ax.set_title("mel spectrogram(Result Audio)") + streamlit_app.pyplot(fig) + +def convert(input: Input) -> Output: + """convert(转换)""" + # load models + extractor = Extractor.load_model(Path(input.extractor.value)) + convertor = Convertor.load_model(Path(input.convertor.value)) + # current_synt = Synthesizer(Path(input.synthesizer.value)) + gan_vocoder.load_model(Path(input.vocoder.value)) + + # load file + if input.upload_audio_file != None: + with open(TEMP_SOURCE_AUDIO, "w+b") as f: + f.write(input.upload_audio_file.as_bytes()) + f.seek(0) + src_wav, sample_rate = librosa.load(TEMP_SOURCE_AUDIO) + else: + src_wav, sample_rate = librosa.load(input.local_audio_file.value) + write(TEMP_SOURCE_AUDIO, sample_rate, src_wav) #Make sure we get the correct wav + + if input.upload_audio_file_target != None: + with open(TEMP_TARGET_AUDIO, "w+b") as f: + f.write(input.upload_audio_file_target.as_bytes()) + f.seek(0) + ref_wav, _ = librosa.load(TEMP_TARGET_AUDIO) + else: + ref_wav, _ = librosa.load(input.local_audio_file_target.value) + write(TEMP_TARGET_AUDIO, sample_rate, ref_wav) #Make sure we get the correct wav + + ppg = extractor.extract_from_wav(src_wav) + # Import necessary dependency of Voice Conversion + from utils.f0_utils import compute_f0, f02lf0, compute_mean_std, get_converted_lf0uv + ref_lf0_mean, ref_lf0_std = compute_mean_std(f02lf0(compute_f0(ref_wav))) + speacker_encoder.load_model(Path("encoder/saved_models/pretrained_bak_5805000.pt")) + embed = speacker_encoder.embed_utterance(ref_wav) + lf0_uv = get_converted_lf0uv(src_wav, ref_lf0_mean, ref_lf0_std, convert=True) + min_len = min(ppg.shape[1], len(lf0_uv)) + ppg = ppg[:, :min_len] + lf0_uv = lf0_uv[:min_len] + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + _, mel_pred, att_ws = convertor.inference( + ppg, + logf0_uv=torch.from_numpy(lf0_uv).unsqueeze(0).float().to(device), + spembs=torch.from_numpy(embed).unsqueeze(0).to(device), + ) + mel_pred= mel_pred.transpose(0, 1) + breaks = [mel_pred.shape[1]] + mel_pred= mel_pred.detach().cpu().numpy() + + # synthesize and vocode + wav, sample_rate = gan_vocoder.infer_waveform(mel_pred) + + # write and output + write(TEMP_RESULT_AUDIO, sample_rate, wav) #Make sure we get the correct wav + with open(TEMP_SOURCE_AUDIO, "rb") as f: + source_file = f.read() + with open(TEMP_TARGET_AUDIO, "rb") as f: + target_file = f.read() + with open(TEMP_RESULT_AUDIO, "rb") as f: + result_file = f.read() + + + return Output(__root__=(AudioEntity(content=source_file, mel=Synthesizer.make_spectrogram(src_wav)), AudioEntity(content=target_file, mel=Synthesizer.make_spectrogram(ref_wav)), AudioEntity(content=result_file, mel=Synthesizer.make_spectrogram(wav)))) \ No newline at end of file diff --git a/mkgui/base/__init__.py b/mkgui/base/__init__.py new file mode 100644 index 0000000..6905fa0 --- /dev/null +++ b/mkgui/base/__init__.py @@ -0,0 +1,2 @@ + +from .core import Opyrator diff --git a/mkgui/base/api/__init__.py b/mkgui/base/api/__init__.py new file mode 100644 index 0000000..a0c4102 --- /dev/null +++ b/mkgui/base/api/__init__.py @@ -0,0 +1 @@ +from .fastapi_app import create_api diff --git a/mkgui/base/api/fastapi_utils.py b/mkgui/base/api/fastapi_utils.py new file mode 100644 index 0000000..adf582a --- /dev/null +++ b/mkgui/base/api/fastapi_utils.py @@ -0,0 +1,102 @@ +"""Collection of utilities for FastAPI apps.""" + +import inspect +from typing import Any, Type + +from fastapi import FastAPI, Form +from pydantic import BaseModel + + +def as_form(cls: Type[BaseModel]) -> Any: + """Adds an as_form class method to decorated models. + + The as_form class method can be used with FastAPI endpoints + """ + new_params = [ + inspect.Parameter( + field.alias, + inspect.Parameter.POSITIONAL_ONLY, + default=(Form(field.default) if not field.required else Form(...)), + ) + for field in cls.__fields__.values() + ] + + async def _as_form(**data): # type: ignore + return cls(**data) + + sig = inspect.signature(_as_form) + sig = sig.replace(parameters=new_params) + _as_form.__signature__ = sig # type: ignore + setattr(cls, "as_form", _as_form) + return cls + + +def patch_fastapi(app: FastAPI) -> None: + """Patch function to allow relative url resolution. + + This patch is required to make fastapi fully functional with a relative url path. + This code snippet can be copy-pasted to any Fastapi application. + """ + from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html + from starlette.requests import Request + from starlette.responses import HTMLResponse + + async def redoc_ui_html(req: Request) -> HTMLResponse: + assert app.openapi_url is not None + redoc_ui = get_redoc_html( + openapi_url="./" + app.openapi_url.lstrip("/"), + title=app.title + " - Redoc UI", + ) + + return HTMLResponse(redoc_ui.body.decode("utf-8")) + + async def swagger_ui_html(req: Request) -> HTMLResponse: + assert app.openapi_url is not None + swagger_ui = get_swagger_ui_html( + openapi_url="./" + app.openapi_url.lstrip("/"), + title=app.title + " - Swagger UI", + oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, + ) + + # insert request interceptor to have all request run on relativ path + request_interceptor = ( + "requestInterceptor: (e) => {" + "\n\t\t\tvar url = window.location.origin + window.location.pathname" + '\n\t\t\turl = url.substring( 0, url.lastIndexOf( "/" ) + 1);' + "\n\t\t\turl = e.url.replace(/http(s)?:\/\/[^/]*\//i, url);" # noqa: W605 + "\n\t\t\te.contextUrl = url" + "\n\t\t\te.url = url" + "\n\t\t\treturn e;}" + ) + + return HTMLResponse( + swagger_ui.body.decode("utf-8").replace( + "dom_id: '#swagger-ui',", + "dom_id: '#swagger-ui',\n\t\t" + request_interceptor + ",", + ) + ) + + # remove old docs route and add our patched route + routes_new = [] + for app_route in app.routes: + if app_route.path == "/docs": # type: ignore + continue + + if app_route.path == "/redoc": # type: ignore + continue + + routes_new.append(app_route) + + app.router.routes = routes_new + + assert app.docs_url is not None + app.add_route(app.docs_url, swagger_ui_html, include_in_schema=False) + assert app.redoc_url is not None + app.add_route(app.redoc_url, redoc_ui_html, include_in_schema=False) + + # Make graphql realtive + from starlette import graphql + + graphql.GRAPHIQL = graphql.GRAPHIQL.replace( + "({{REQUEST_PATH}}", '("." + {{REQUEST_PATH}}' + ) diff --git a/mkgui/base/components/__init__.py b/mkgui/base/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mkgui/base/components/outputs.py b/mkgui/base/components/outputs.py new file mode 100644 index 0000000..f4859c6 --- /dev/null +++ b/mkgui/base/components/outputs.py @@ -0,0 +1,43 @@ +from typing import List + +from pydantic import BaseModel + + +class ScoredLabel(BaseModel): + label: str + score: float + + +class ClassificationOutput(BaseModel): + __root__: List[ScoredLabel] + + def __iter__(self): # type: ignore + return iter(self.__root__) + + def __getitem__(self, item): # type: ignore + return self.__root__[item] + + def render_output_ui(self, streamlit) -> None: # type: ignore + import plotly.express as px + + sorted_predictions = sorted( + [prediction.dict() for prediction in self.__root__], + key=lambda k: k["score"], + ) + + num_labels = len(sorted_predictions) + if len(sorted_predictions) > 10: + num_labels = streamlit.slider( + "Maximum labels to show: ", + min_value=1, + max_value=len(sorted_predictions), + value=len(sorted_predictions), + ) + fig = px.bar( + sorted_predictions[len(sorted_predictions) - num_labels :], + x="score", + y="label", + orientation="h", + ) + streamlit.plotly_chart(fig, use_container_width=True) + # fig.show() diff --git a/mkgui/base/components/types.py b/mkgui/base/components/types.py new file mode 100644 index 0000000..125809a --- /dev/null +++ b/mkgui/base/components/types.py @@ -0,0 +1,46 @@ +import base64 +from typing import Any, Dict, overload + + +class FileContent(str): + def as_bytes(self) -> bytes: + return base64.b64decode(self, validate=True) + + def as_str(self) -> str: + return self.as_bytes().decode() + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(format="byte") + + @classmethod + def __get_validators__(cls) -> Any: # type: ignore + yield cls.validate + + @classmethod + def validate(cls, value: Any) -> "FileContent": + if isinstance(value, FileContent): + return value + elif isinstance(value, str): + return FileContent(value) + elif isinstance(value, (bytes, bytearray, memoryview)): + return FileContent(base64.b64encode(value).decode()) + else: + raise Exception("Wrong type") + +# # 暂时无法使用,因为浏览器中没有考虑选择文件夹 +# class DirectoryContent(FileContent): +# @classmethod +# def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: +# field_schema.update(format="path") + +# @classmethod +# def validate(cls, value: Any) -> "DirectoryContent": +# if isinstance(value, DirectoryContent): +# return value +# elif isinstance(value, str): +# return DirectoryContent(value) +# elif isinstance(value, (bytes, bytearray, memoryview)): +# return DirectoryContent(base64.b64encode(value).decode()) +# else: +# raise Exception("Wrong type") diff --git a/mkgui/base/core.py b/mkgui/base/core.py new file mode 100644 index 0000000..8166a33 --- /dev/null +++ b/mkgui/base/core.py @@ -0,0 +1,203 @@ +import importlib +import inspect +import re +from typing import Any, Callable, Type, Union, get_type_hints + +from pydantic import BaseModel, parse_raw_as +from pydantic.tools import parse_obj_as + + +def name_to_title(name: str) -> str: + """Converts a camelCase or snake_case name to title case.""" + # If camelCase -> convert to snake case + name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() + # Convert to title case + return name.replace("_", " ").strip().title() + + +def is_compatible_type(type: Type) -> bool: + """Returns `True` if the type is opyrator-compatible.""" + try: + if issubclass(type, BaseModel): + return True + except Exception: + pass + + try: + # valid list type + if type.__origin__ is list and issubclass(type.__args__[0], BaseModel): + return True + except Exception: + pass + + return False + + +def get_input_type(func: Callable) -> Type: + """Returns the input type of a given function (callable). + + Args: + func: The function for which to get the input type. + + Raises: + ValueError: If the function does not have a valid input type annotation. + """ + type_hints = get_type_hints(func) + + if "input" not in type_hints: + raise ValueError( + "The callable MUST have a parameter with the name `input` with typing annotation. " + "For example: `def my_opyrator(input: InputModel) -> OutputModel:`." + ) + + input_type = type_hints["input"] + + if not is_compatible_type(input_type): + raise ValueError( + "The `input` parameter MUST be a subclass of the Pydantic BaseModel or a list of Pydantic models." + ) + + # TODO: return warning if more than one input parameters + + return input_type + + +def get_output_type(func: Callable) -> Type: + """Returns the output type of a given function (callable). + + Args: + func: The function for which to get the output type. + + Raises: + ValueError: If the function does not have a valid output type annotation. + """ + type_hints = get_type_hints(func) + if "return" not in type_hints: + raise ValueError( + "The return type of the callable MUST be annotated with type hints." + "For example: `def my_opyrator(input: InputModel) -> OutputModel:`." + ) + + output_type = type_hints["return"] + + if not is_compatible_type(output_type): + raise ValueError( + "The return value MUST be a subclass of the Pydantic BaseModel or a list of Pydantic models." + ) + + return output_type + + +def get_callable(import_string: str) -> Callable: + """Import a callable from an string.""" + callable_seperator = ":" + if callable_seperator not in import_string: + # Use dot as seperator + callable_seperator = "." + + if callable_seperator not in import_string: + raise ValueError("The callable path MUST specify the function. ") + + mod_name, callable_name = import_string.rsplit(callable_seperator, 1) + mod = importlib.import_module(mod_name) + return getattr(mod, callable_name) + + +class Opyrator: + def __init__(self, func: Union[Callable, str]) -> None: + if isinstance(func, str): + # Try to load the function from a string notion + self.function = get_callable(func) + else: + self.function = func + + self._action = "Execute" + self._input_type = None + self._output_type = None + + if not callable(self.function): + raise ValueError("The provided function parameters is not a callable.") + + if inspect.isclass(self.function): + raise ValueError( + "The provided callable is an uninitialized Class. This is not allowed." + ) + + if inspect.isfunction(self.function): + # The provided callable is a function + self._input_type = get_input_type(self.function) + self._output_type = get_output_type(self.function) + + try: + # Get name + self._name = name_to_title(self.function.__name__) + except Exception: + pass + + try: + # Get description from function + doc_string = inspect.getdoc(self.function) + if doc_string: + self._action = doc_string + except Exception: + pass + elif hasattr(self.function, "__call__"): + # The provided callable is a function + self._input_type = get_input_type(self.function.__call__) # type: ignore + self._output_type = get_output_type(self.function.__call__) # type: ignore + + try: + # Get name + self._name = name_to_title(type(self.function).__name__) + except Exception: + pass + + try: + # Get action from + doc_string = inspect.getdoc(self.function.__call__) # type: ignore + if doc_string: + self._action = doc_string + + if ( + not self._action + or self._action == "Call" + ): + # Get docstring from class instead of __call__ function + doc_string = inspect.getdoc(self.function) + if doc_string: + self._action = doc_string + except Exception: + pass + else: + raise ValueError("Unknown callable type.") + + @property + def name(self) -> str: + return self._name + + @property + def action(self) -> str: + return self._action + + @property + def input_type(self) -> Any: + return self._input_type + + @property + def output_type(self) -> Any: + return self._output_type + + def __call__(self, input: Any, **kwargs: Any) -> Any: + + input_obj = input + + if isinstance(input, str): + # Allow json input + input_obj = parse_raw_as(self.input_type, input) + + if isinstance(input, dict): + # Allow dict input + input_obj = parse_obj_as(self.input_type, input) + + return self.function(input_obj, **kwargs) diff --git a/mkgui/base/ui/__init__.py b/mkgui/base/ui/__init__.py new file mode 100644 index 0000000..593b254 --- /dev/null +++ b/mkgui/base/ui/__init__.py @@ -0,0 +1 @@ +from .streamlit_ui import render_streamlit_ui diff --git a/mkgui/base/ui/schema_utils.py b/mkgui/base/ui/schema_utils.py new file mode 100644 index 0000000..a2be43c --- /dev/null +++ b/mkgui/base/ui/schema_utils.py @@ -0,0 +1,129 @@ +from typing import Dict + + +def resolve_reference(reference: str, references: Dict) -> Dict: + return references[reference.split("/")[-1]] + + +def get_single_reference_item(property: Dict, references: Dict) -> Dict: + # Ref can either be directly in the properties or the first element of allOf + reference = property.get("$ref") + if reference is None: + reference = property["allOf"][0]["$ref"] + return resolve_reference(reference, references) + + +def is_single_string_property(property: Dict) -> bool: + return property.get("type") == "string" + + +def is_single_datetime_property(property: Dict) -> bool: + if property.get("type") != "string": + return False + return property.get("format") in ["date-time", "time", "date"] + + +def is_single_boolean_property(property: Dict) -> bool: + return property.get("type") == "boolean" + + +def is_single_number_property(property: Dict) -> bool: + return property.get("type") in ["integer", "number"] + + +def is_single_file_property(property: Dict) -> bool: + if property.get("type") != "string": + return False + # TODO: binary? + return property.get("format") == "byte" + + +def is_single_directory_property(property: Dict) -> bool: + if property.get("type") != "string": + return False + return property.get("format") == "path" + +def is_multi_enum_property(property: Dict, references: Dict) -> bool: + if property.get("type") != "array": + return False + + if property.get("uniqueItems") is not True: + # Only relevant if it is a set or other datastructures with unique items + return False + + try: + _ = resolve_reference(property["items"]["$ref"], references)["enum"] + return True + except Exception: + return False + + +def is_single_enum_property(property: Dict, references: Dict) -> bool: + try: + _ = get_single_reference_item(property, references)["enum"] + return True + except Exception: + return False + + +def is_single_dict_property(property: Dict) -> bool: + if property.get("type") != "object": + return False + return "additionalProperties" in property + + +def is_single_reference(property: Dict) -> bool: + if property.get("type") is not None: + return False + + return bool(property.get("$ref")) + + +def is_multi_file_property(property: Dict) -> bool: + if property.get("type") != "array": + return False + + if property.get("items") is None: + return False + + try: + # TODO: binary + return property["items"]["format"] == "byte" + except Exception: + return False + + +def is_single_object(property: Dict, references: Dict) -> bool: + try: + object_reference = get_single_reference_item(property, references) + if object_reference["type"] != "object": + return False + return "properties" in object_reference + except Exception: + return False + + +def is_property_list(property: Dict) -> bool: + if property.get("type") != "array": + return False + + if property.get("items") is None: + return False + + try: + return property["items"]["type"] in ["string", "number", "integer"] + except Exception: + return False + + +def is_object_list_property(property: Dict, references: Dict) -> bool: + if property.get("type") != "array": + return False + + try: + object_reference = resolve_reference(property["items"]["$ref"], references) + if object_reference["type"] != "object": + return False + return "properties" in object_reference + except Exception: + return False diff --git a/mkgui/base/ui/streamlit_ui.py b/mkgui/base/ui/streamlit_ui.py new file mode 100644 index 0000000..2e5159d --- /dev/null +++ b/mkgui/base/ui/streamlit_ui.py @@ -0,0 +1,885 @@ +import datetime +import inspect +import mimetypes +import sys +from os import getcwd, unlink +from platform import system +from tempfile import NamedTemporaryFile +from typing import Any, Callable, Dict, List, Type +from PIL import Image + +import pandas as pd +import streamlit as st +from fastapi.encoders import jsonable_encoder +from loguru import logger +from pydantic import BaseModel, ValidationError, parse_obj_as + +from mkgui.base import Opyrator +from mkgui.base.core import name_to_title +from mkgui.base.ui import schema_utils +from mkgui.base.ui.streamlit_utils import CUSTOM_STREAMLIT_CSS + +STREAMLIT_RUNNER_SNIPPET = """ +from mkgui.base.ui import render_streamlit_ui +from mkgui.base import Opyrator + +import streamlit as st + +# TODO: Make it configurable +# Page config can only be setup once +st.set_page_config( + page_title="MockingBird", + page_icon="🧊", + layout="wide") + +render_streamlit_ui() +""" + +# with st.spinner("Loading MockingBird GUI. Please wait..."): +# opyrator = Opyrator("{opyrator_path}") + + +def launch_ui(port: int = 8501) -> None: + with NamedTemporaryFile( + suffix=".py", mode="w", encoding="utf-8", delete=False + ) as f: + f.write(STREAMLIT_RUNNER_SNIPPET) + f.seek(0) + + import subprocess + + python_path = f'PYTHONPATH="$PYTHONPATH:{getcwd()}"' + if system() == "Windows": + python_path = f"set PYTHONPATH=%PYTHONPATH%;{getcwd()} &&" + subprocess.run( + f"""set STREAMLIT_GLOBAL_SHOW_WARNING_ON_DIRECT_EXECUTION=false""", + shell=True, + ) + + subprocess.run( + f"""{python_path} "{sys.executable}" -m streamlit run --server.port={port} --server.headless=True --runner.magicEnabled=False --server.maxUploadSize=50 --browser.gatherUsageStats=False {f.name}""", + shell=True, + ) + + f.close() + unlink(f.name) + + +def function_has_named_arg(func: Callable, parameter: str) -> bool: + try: + sig = inspect.signature(func) + for param in sig.parameters.values(): + if param.name == "input": + return True + except Exception: + return False + return False + + +def has_output_ui_renderer(data_item: BaseModel) -> bool: + return hasattr(data_item, "render_output_ui") + + +def has_input_ui_renderer(input_class: Type[BaseModel]) -> bool: + return hasattr(input_class, "render_input_ui") + + +def is_compatible_audio(mime_type: str) -> bool: + return mime_type in ["audio/mpeg", "audio/ogg", "audio/wav"] + + +def is_compatible_image(mime_type: str) -> bool: + return mime_type in ["image/png", "image/jpeg"] + + +def is_compatible_video(mime_type: str) -> bool: + return mime_type in ["video/mp4"] + + +class InputUI: + def __init__(self, session_state, input_class: Type[BaseModel]): + self._session_state = session_state + self._input_class = input_class + + self._schema_properties = input_class.schema(by_alias=True).get( + "properties", {} + ) + self._schema_references = input_class.schema(by_alias=True).get( + "definitions", {} + ) + + def render_ui(self, streamlit_app_root) -> None: + if has_input_ui_renderer(self._input_class): + # The input model has a rendering function + # The rendering also returns the current state of input data + self._session_state.input_data = self._input_class.render_input_ui( # type: ignore + st, self._session_state.input_data + ) + return + + # print(self._schema_properties) + for property_key in self._schema_properties.keys(): + property = self._schema_properties[property_key] + + if not property.get("title"): + # Set property key as fallback title + property["title"] = name_to_title(property_key) + + try: + if "input_data" in self._session_state: + self._store_value( + property_key, + self._render_property(streamlit_app_root, property_key, property), + ) + except Exception as e: + print("Exception!", e) + pass + + def _get_default_streamlit_input_kwargs(self, key: str, property: Dict) -> Dict: + streamlit_kwargs = { + "label": property.get("title"), + "key": key, + } + + if property.get("description"): + streamlit_kwargs["help"] = property.get("description") + return streamlit_kwargs + + def _store_value(self, key: str, value: Any) -> None: + data_element = self._session_state.input_data + key_elements = key.split(".") + for i, key_element in enumerate(key_elements): + if i == len(key_elements) - 1: + # add value to this element + data_element[key_element] = value + return + if key_element not in data_element: + data_element[key_element] = {} + data_element = data_element[key_element] + + def _get_value(self, key: str) -> Any: + data_element = self._session_state.input_data + key_elements = key.split(".") + for i, key_element in enumerate(key_elements): + if i == len(key_elements) - 1: + # add value to this element + if key_element not in data_element: + return None + return data_element[key_element] + if key_element not in data_element: + data_element[key_element] = {} + data_element = data_element[key_element] + return None + + def _render_single_datetime_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) + + if property.get("format") == "time": + if property.get("default"): + try: + streamlit_kwargs["value"] = datetime.time.fromisoformat( # type: ignore + property.get("default") + ) + except Exception: + pass + return streamlit_app.time_input(**streamlit_kwargs) + elif property.get("format") == "date": + if property.get("default"): + try: + streamlit_kwargs["value"] = datetime.date.fromisoformat( # type: ignore + property.get("default") + ) + except Exception: + pass + return streamlit_app.date_input(**streamlit_kwargs) + elif property.get("format") == "date-time": + if property.get("default"): + try: + streamlit_kwargs["value"] = datetime.datetime.fromisoformat( # type: ignore + property.get("default") + ) + except Exception: + pass + with streamlit_app.container(): + streamlit_app.subheader(streamlit_kwargs.get("label")) + if streamlit_kwargs.get("description"): + streamlit_app.text(streamlit_kwargs.get("description")) + selected_date = None + selected_time = None + date_col, time_col = streamlit_app.columns(2) + with date_col: + date_kwargs = {"label": "Date", "key": key + "-date-input"} + if streamlit_kwargs.get("value"): + try: + date_kwargs["value"] = streamlit_kwargs.get( # type: ignore + "value" + ).date() + except Exception: + pass + selected_date = streamlit_app.date_input(**date_kwargs) + + with time_col: + time_kwargs = {"label": "Time", "key": key + "-time-input"} + if streamlit_kwargs.get("value"): + try: + time_kwargs["value"] = streamlit_kwargs.get( # type: ignore + "value" + ).time() + except Exception: + pass + selected_time = streamlit_app.time_input(**time_kwargs) + return datetime.datetime.combine(selected_date, selected_time) + else: + streamlit_app.warning( + "Date format is not supported: " + str(property.get("format")) + ) + + def _render_single_file_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) + file_extension = None + if "mime_type" in property: + file_extension = mimetypes.guess_extension(property["mime_type"]) + + uploaded_file = streamlit_app.file_uploader( + **streamlit_kwargs, accept_multiple_files=False, type=file_extension + ) + if uploaded_file is None: + return None + + bytes = uploaded_file.getvalue() + if property.get("mime_type"): + if is_compatible_audio(property["mime_type"]): + # Show audio + streamlit_app.audio(bytes, format=property.get("mime_type")) + if is_compatible_image(property["mime_type"]): + # Show image + streamlit_app.image(bytes) + if is_compatible_video(property["mime_type"]): + # Show video + streamlit_app.video(bytes, format=property.get("mime_type")) + return bytes + + def _render_single_string_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) + + if property.get("default"): + streamlit_kwargs["value"] = property.get("default") + elif property.get("example"): + # TODO: also use example for other property types + # Use example as value if it is provided + streamlit_kwargs["value"] = property.get("example") + + if property.get("maxLength") is not None: + streamlit_kwargs["max_chars"] = property.get("maxLength") + + if ( + property.get("format") + or ( + property.get("maxLength") is not None + and int(property.get("maxLength")) < 140 # type: ignore + ) + or property.get("writeOnly") + ): + # If any format is set, use single text input + # If max chars is set to less than 140, use single text input + # If write only -> password field + if property.get("writeOnly"): + streamlit_kwargs["type"] = "password" + return streamlit_app.text_input(**streamlit_kwargs) + else: + # Otherwise use multiline text area + return streamlit_app.text_area(**streamlit_kwargs) + + def _render_multi_enum_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) + reference_item = schema_utils.resolve_reference( + property["items"]["$ref"], self._schema_references + ) + # TODO: how to select defaults + return streamlit_app.multiselect( + **streamlit_kwargs, options=reference_item["enum"] + ) + + def _render_single_enum_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + + streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) + reference_item = schema_utils.get_single_reference_item( + property, self._schema_references + ) + + if property.get("default") is not None: + try: + streamlit_kwargs["index"] = reference_item["enum"].index( + property.get("default") + ) + except Exception: + # Use default selection + pass + + return streamlit_app.selectbox( + **streamlit_kwargs, options=reference_item["enum"] + ) + + def _render_single_dict_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + + # Add title and subheader + streamlit_app.subheader(property.get("title")) + if property.get("description"): + streamlit_app.markdown(property.get("description")) + + streamlit_app.markdown("---") + + current_dict = self._get_value(key) + if not current_dict: + current_dict = {} + + key_col, value_col = streamlit_app.columns(2) + + with key_col: + updated_key = streamlit_app.text_input( + "Key", value="", key=key + "-new-key" + ) + + with value_col: + # TODO: also add boolean? + value_kwargs = {"label": "Value", "key": key + "-new-value"} + if property["additionalProperties"].get("type") == "integer": + value_kwargs["value"] = 0 # type: ignore + updated_value = streamlit_app.number_input(**value_kwargs) + elif property["additionalProperties"].get("type") == "number": + value_kwargs["value"] = 0.0 # type: ignore + value_kwargs["format"] = "%f" + updated_value = streamlit_app.number_input(**value_kwargs) + else: + value_kwargs["value"] = "" + updated_value = streamlit_app.text_input(**value_kwargs) + + streamlit_app.markdown("---") + + with streamlit_app.container(): + clear_col, add_col = streamlit_app.columns([1, 2]) + + with clear_col: + if streamlit_app.button("Clear Items", key=key + "-clear-items"): + current_dict = {} + + with add_col: + if ( + streamlit_app.button("Add Item", key=key + "-add-item") + and updated_key + ): + current_dict[updated_key] = updated_value + + streamlit_app.write(current_dict) + + return current_dict + + def _render_single_reference( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + reference_item = schema_utils.get_single_reference_item( + property, self._schema_references + ) + return self._render_property(streamlit_app, key, reference_item) + + def _render_multi_file_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) + + file_extension = None + if "mime_type" in property: + file_extension = mimetypes.guess_extension(property["mime_type"]) + + uploaded_files = streamlit_app.file_uploader( + **streamlit_kwargs, accept_multiple_files=True, type=file_extension + ) + uploaded_files_bytes = [] + if uploaded_files: + for uploaded_file in uploaded_files: + uploaded_files_bytes.append(uploaded_file.read()) + return uploaded_files_bytes + + def _render_single_boolean_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) + + if property.get("default"): + streamlit_kwargs["value"] = property.get("default") + return streamlit_app.checkbox(**streamlit_kwargs) + + def _render_single_number_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) + + number_transform = int + if property.get("type") == "number": + number_transform = float # type: ignore + streamlit_kwargs["format"] = "%f" + + if "multipleOf" in property: + # Set stepcount based on multiple of parameter + streamlit_kwargs["step"] = number_transform(property["multipleOf"]) + elif number_transform == int: + # Set step size to 1 as default + streamlit_kwargs["step"] = 1 + elif number_transform == float: + # Set step size to 0.01 as default + # TODO: adapt to default value + streamlit_kwargs["step"] = 0.01 + + if "minimum" in property: + streamlit_kwargs["min_value"] = number_transform(property["minimum"]) + if "exclusiveMinimum" in property: + streamlit_kwargs["min_value"] = number_transform( + property["exclusiveMinimum"] + streamlit_kwargs["step"] + ) + if "maximum" in property: + streamlit_kwargs["max_value"] = number_transform(property["maximum"]) + + if "exclusiveMaximum" in property: + streamlit_kwargs["max_value"] = number_transform( + property["exclusiveMaximum"] - streamlit_kwargs["step"] + ) + + if property.get("default") is not None: + streamlit_kwargs["value"] = number_transform(property.get("default")) # type: ignore + else: + if "min_value" in streamlit_kwargs: + streamlit_kwargs["value"] = streamlit_kwargs["min_value"] + elif number_transform == int: + streamlit_kwargs["value"] = 0 + else: + # Set default value to step + streamlit_kwargs["value"] = number_transform(streamlit_kwargs["step"]) + + if "min_value" in streamlit_kwargs and "max_value" in streamlit_kwargs: + # TODO: Only if less than X steps + return streamlit_app.slider(**streamlit_kwargs) + else: + return streamlit_app.number_input(**streamlit_kwargs) + + def _render_object_input(self, streamlit_app: st, key: str, property: Dict) -> Any: + properties = property["properties"] + object_inputs = {} + for property_key in properties: + property = properties[property_key] + if not property.get("title"): + # Set property key as fallback title + property["title"] = name_to_title(property_key) + # construct full key based on key parts -> required later to get the value + full_key = key + "." + property_key + object_inputs[property_key] = self._render_property( + streamlit_app, full_key, property + ) + return object_inputs + + def _render_single_object_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + # Add title and subheader + title = property.get("title") + streamlit_app.subheader(title) + if property.get("description"): + streamlit_app.markdown(property.get("description")) + + object_reference = schema_utils.get_single_reference_item( + property, self._schema_references + ) + return self._render_object_input(streamlit_app, key, object_reference) + + def _render_property_list_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + + # Add title and subheader + streamlit_app.subheader(property.get("title")) + if property.get("description"): + streamlit_app.markdown(property.get("description")) + + streamlit_app.markdown("---") + + current_list = self._get_value(key) + if not current_list: + current_list = [] + + value_kwargs = {"label": "Value", "key": key + "-new-value"} + if property["items"]["type"] == "integer": + value_kwargs["value"] = 0 # type: ignore + new_value = streamlit_app.number_input(**value_kwargs) + elif property["items"]["type"] == "number": + value_kwargs["value"] = 0.0 # type: ignore + value_kwargs["format"] = "%f" + new_value = streamlit_app.number_input(**value_kwargs) + else: + value_kwargs["value"] = "" + new_value = streamlit_app.text_input(**value_kwargs) + + streamlit_app.markdown("---") + + with streamlit_app.container(): + clear_col, add_col = streamlit_app.columns([1, 2]) + + with clear_col: + if streamlit_app.button("Clear Items", key=key + "-clear-items"): + current_list = [] + + with add_col: + if ( + streamlit_app.button("Add Item", key=key + "-add-item") + and new_value is not None + ): + current_list.append(new_value) + + streamlit_app.write(current_list) + + return current_list + + def _render_object_list_input( + self, streamlit_app: st, key: str, property: Dict + ) -> Any: + + # TODO: support max_items, and min_items properties + + # Add title and subheader + streamlit_app.subheader(property.get("title")) + if property.get("description"): + streamlit_app.markdown(property.get("description")) + + streamlit_app.markdown("---") + + current_list = self._get_value(key) + if not current_list: + current_list = [] + + object_reference = schema_utils.resolve_reference( + property["items"]["$ref"], self._schema_references + ) + input_data = self._render_object_input(streamlit_app, key, object_reference) + + streamlit_app.markdown("---") + + with streamlit_app.container(): + clear_col, add_col = streamlit_app.columns([1, 2]) + + with clear_col: + if streamlit_app.button("Clear Items", key=key + "-clear-items"): + current_list = [] + + with add_col: + if ( + streamlit_app.button("Add Item", key=key + "-add-item") + and input_data + ): + current_list.append(input_data) + + streamlit_app.write(current_list) + return current_list + + def _render_property(self, streamlit_app: st, key: str, property: Dict) -> Any: + if schema_utils.is_single_enum_property(property, self._schema_references): + return self._render_single_enum_input(streamlit_app, key, property) + + if schema_utils.is_multi_enum_property(property, self._schema_references): + return self._render_multi_enum_input(streamlit_app, key, property) + + if schema_utils.is_single_file_property(property): + return self._render_single_file_input(streamlit_app, key, property) + + if schema_utils.is_multi_file_property(property): + return self._render_multi_file_input(streamlit_app, key, property) + + if schema_utils.is_single_datetime_property(property): + return self._render_single_datetime_input(streamlit_app, key, property) + + if schema_utils.is_single_boolean_property(property): + return self._render_single_boolean_input(streamlit_app, key, property) + + if schema_utils.is_single_dict_property(property): + return self._render_single_dict_input(streamlit_app, key, property) + + if schema_utils.is_single_number_property(property): + return self._render_single_number_input(streamlit_app, key, property) + + if schema_utils.is_single_string_property(property): + return self._render_single_string_input(streamlit_app, key, property) + + if schema_utils.is_single_object(property, self._schema_references): + return self._render_single_object_input(streamlit_app, key, property) + + if schema_utils.is_object_list_property(property, self._schema_references): + return self._render_object_list_input(streamlit_app, key, property) + + if schema_utils.is_property_list(property): + return self._render_property_list_input(streamlit_app, key, property) + + if schema_utils.is_single_reference(property): + return self._render_single_reference(streamlit_app, key, property) + + streamlit_app.warning( + "The type of the following property is currently not supported: " + + str(property.get("title")) + ) + raise Exception("Unsupported property") + + +class OutputUI: + def __init__(self, output_data: Any, input_data: Any): + self._output_data = output_data + self._input_data = input_data + + def render_ui(self, streamlit_app) -> None: + try: + if isinstance(self._output_data, BaseModel): + self._render_single_output(streamlit_app, self._output_data) + return + if type(self._output_data) == list: + self._render_list_output(streamlit_app, self._output_data) + return + except Exception as ex: + streamlit_app.exception(ex) + # Fallback to + streamlit_app.json(jsonable_encoder(self._output_data)) + + def _render_single_text_property( + self, streamlit: st, property_schema: Dict, value: Any + ) -> None: + # Add title and subheader + streamlit.subheader(property_schema.get("title")) + if property_schema.get("description"): + streamlit.markdown(property_schema.get("description")) + if value is None or value == "": + streamlit.info("No value returned!") + else: + streamlit.code(str(value), language="plain") + + def _render_single_file_property( + self, streamlit: st, property_schema: Dict, value: Any + ) -> None: + # Add title and subheader + streamlit.subheader(property_schema.get("title")) + if property_schema.get("description"): + streamlit.markdown(property_schema.get("description")) + if value is None or value == "": + streamlit.info("No value returned!") + else: + # TODO: Detect if it is a FileContent instance + # TODO: detect if it is base64 + file_extension = "" + if "mime_type" in property_schema: + mime_type = property_schema["mime_type"] + file_extension = mimetypes.guess_extension(mime_type) or "" + + if is_compatible_audio(mime_type): + streamlit.audio(value.as_bytes(), format=mime_type) + return + + if is_compatible_image(mime_type): + streamlit.image(value.as_bytes()) + return + + if is_compatible_video(mime_type): + streamlit.video(value.as_bytes(), format=mime_type) + return + + filename = ( + (property_schema["title"] + file_extension) + .lower() + .strip() + .replace(" ", "-") + ) + streamlit.markdown( + f'', + unsafe_allow_html=True, + ) + + def _render_single_complex_property( + self, streamlit: st, property_schema: Dict, value: Any + ) -> None: + # Add title and subheader + streamlit.subheader(property_schema.get("title")) + if property_schema.get("description"): + streamlit.markdown(property_schema.get("description")) + + streamlit.json(jsonable_encoder(value)) + + def _render_single_output(self, streamlit: st, output_data: BaseModel) -> None: + try: + if has_output_ui_renderer(output_data): + if function_has_named_arg(output_data.render_output_ui, "input"): # type: ignore + # render method also requests the input data + output_data.render_output_ui(streamlit, input=self._input_data) # type: ignore + else: + output_data.render_output_ui(streamlit) # type: ignore + return + except Exception: + # Use default auto-generation methods if the custom rendering throws an exception + logger.exception( + "Failed to execute custom render_output_ui function. Using auto-generation instead" + ) + + model_schema = output_data.schema(by_alias=False) + model_properties = model_schema.get("properties") + definitions = model_schema.get("definitions") + + if model_properties: + for property_key in output_data.__dict__: + property_schema = model_properties.get(property_key) + if not property_schema.get("title"): + # Set property key as fallback title + property_schema["title"] = property_key + + output_property_value = output_data.__dict__[property_key] + + if has_output_ui_renderer(output_property_value): + output_property_value.render_output_ui(streamlit) # type: ignore + continue + + if isinstance(output_property_value, BaseModel): + # Render output recursivly + streamlit.subheader(property_schema.get("title")) + if property_schema.get("description"): + streamlit.markdown(property_schema.get("description")) + self._render_single_output(streamlit, output_property_value) + continue + + if property_schema: + if schema_utils.is_single_file_property(property_schema): + self._render_single_file_property( + streamlit, property_schema, output_property_value + ) + continue + + if ( + schema_utils.is_single_string_property(property_schema) + or schema_utils.is_single_number_property(property_schema) + or schema_utils.is_single_datetime_property(property_schema) + or schema_utils.is_single_boolean_property(property_schema) + ): + self._render_single_text_property( + streamlit, property_schema, output_property_value + ) + continue + if definitions and schema_utils.is_single_enum_property( + property_schema, definitions + ): + self._render_single_text_property( + streamlit, property_schema, output_property_value.value + ) + continue + + # TODO: render dict as table + + self._render_single_complex_property( + streamlit, property_schema, output_property_value + ) + return + + def _render_list_output(self, streamlit: st, output_data: List) -> None: + try: + data_items: List = [] + for data_item in output_data: + if has_output_ui_renderer(data_item): + # Render using the render function + data_item.render_output_ui(streamlit) # type: ignore + continue + data_items.append(data_item.dict()) + # Try to show as dataframe + streamlit.table(pd.DataFrame(data_items)) + except Exception: + # Fallback to + streamlit.json(jsonable_encoder(output_data)) + + +def getOpyrator(mode: str) -> Opyrator: + if mode == None or mode.startswith('VC'): + from mkgui.app_vc import convert + return Opyrator(convert) + if mode == None or mode.startswith('预处理'): + from mkgui.preprocess import preprocess + return Opyrator(preprocess) + if mode == None or mode.startswith('模型训练'): + from mkgui.train import train + return Opyrator(train) + from mkgui.app import synthesize + return Opyrator(synthesize) + + +def render_streamlit_ui() -> None: + # init + session_state = st.session_state + session_state.input_data = {} + # Add custom css settings + st.markdown(f"", unsafe_allow_html=True) + + with st.spinner("Loading MockingBird GUI. Please wait..."): + session_state.mode = st.sidebar.selectbox( + '模式选择', + ( "AI拟音", "VC拟音", "预处理", "模型训练") + ) + if "mode" in session_state: + mode = session_state.mode + else: + mode = "" + opyrator = getOpyrator(mode) + title = opyrator.name + mode + + col1, col2, _ = st.columns(3) + col2.title(title) + col2.markdown("欢迎使用MockingBird Web 2") + + image = Image.open('.\\mkgui\\static\\mb.png') + col1.image(image) + + st.markdown("---") + left, right = st.columns([0.4, 0.6]) + + with left: + st.header("Control 控制") + InputUI(session_state=session_state, input_class=opyrator.input_type).render_ui(st) + execute_selected = st.button(opyrator.action) + if execute_selected: + with st.spinner("Executing operation. Please wait..."): + try: + input_data_obj = parse_obj_as( + opyrator.input_type, session_state.input_data + ) + session_state.output_data = opyrator(input=input_data_obj) + session_state.latest_operation_input = input_data_obj # should this really be saved as additional session object? + except ValidationError as ex: + st.error(ex) + else: + # st.success("Operation executed successfully.") + pass + + with right: + st.header("Result 结果") + if 'output_data' in session_state: + OutputUI( + session_state.output_data, session_state.latest_operation_input + ).render_ui(st) + if st.button("Clear"): + # Clear all state + for key in st.session_state.keys(): + del st.session_state[key] + session_state.input_data = {} + st.experimental_rerun() + else: + # placeholder + st.caption("请使用左侧控制板进行输入并运行获得结果") + + diff --git a/mkgui/base/ui/streamlit_utils.py b/mkgui/base/ui/streamlit_utils.py new file mode 100644 index 0000000..beb6e65 --- /dev/null +++ b/mkgui/base/ui/streamlit_utils.py @@ -0,0 +1,13 @@ +CUSTOM_STREAMLIT_CSS = """ +div[data-testid="stBlock"] button { + width: 100% !important; + margin-bottom: 20px !important; + border-color: #bfbfbf !important; +} +section[data-testid="stSidebar"] div { + max-width: 10rem; +} +pre code { + white-space: pre-wrap; +} +""" diff --git a/mkgui/preprocess.py b/mkgui/preprocess.py new file mode 100644 index 0000000..9d41994 --- /dev/null +++ b/mkgui/preprocess.py @@ -0,0 +1,96 @@ +from pydantic import BaseModel, Field +import os +from pathlib import Path +from enum import Enum +from typing import Any + + +# Constants +EXT_MODELS_DIRT = "ppg_extractor\\saved_models" +ENC_MODELS_DIRT = "encoder\\saved_models" + + +if os.path.isdir(EXT_MODELS_DIRT): + extractors = Enum('extractors', list((file.name, file) for file in Path(EXT_MODELS_DIRT).glob("**/*.pt"))) + print("Loaded extractor models: " + str(len(extractors))) +else: + raise Exception(f"Model folder {EXT_MODELS_DIRT} doesn't exist.") + +if os.path.isdir(ENC_MODELS_DIRT): + encoders = Enum('encoders', list((file.name, file) for file in Path(ENC_MODELS_DIRT).glob("**/*.pt"))) + print("Loaded encoders models: " + str(len(encoders))) +else: + raise Exception(f"Model folder {ENC_MODELS_DIRT} doesn't exist.") + +class Model(str, Enum): + VC_PPG2MEL = "ppg2mel" + +class Dataset(str, Enum): + AIDATATANG_200ZH = "aidatatang_200zh" + AIDATATANG_200ZH_S = "aidatatang_200zh_s" + +class Input(BaseModel): + # def render_input_ui(st, input) -> Dict: + # input["selected_dataset"] = st.selectbox( + # '选择数据集', + # ("aidatatang_200zh", "aidatatang_200zh_s") + # ) + # return input + model: Model = Field( + Model.VC_PPG2MEL, title="目标模型", + ) + dataset: Dataset = Field( + Dataset.AIDATATANG_200ZH, title="数据集选择", + ) + datasets_root: str = Field( + ..., alias="数据集根目录", description="输入数据集根目录(相对/绝对)", + format=True, + example="..\\trainning_data\\" + ) + output_root: str = Field( + ..., alias="输出根目录", description="输出结果根目录(相对/绝对)", + format=True, + example="..\\trainning_data\\" + ) + n_processes: int = Field( + 2, alias="处理线程数", description="根据CPU线程数来设置", + le=32, ge=1 + ) + extractor: extractors = Field( + ..., alias="特征提取模型", + description="选择PPG特征提取模型文件." + ) + encoder: encoders = Field( + ..., alias="语音编码模型", + description="选择语音编码模型文件." + ) + +class AudioEntity(BaseModel): + content: bytes + mel: Any + +class Output(BaseModel): + __root__: tuple[str, int] + + def render_output_ui(self, streamlit_app, input) -> None: # type: ignore + """Custom output UI. + If this method is implmeneted, it will be used instead of the default Output UI renderer. + """ + sr, count = self.__root__ + streamlit_app.subheader(f"Dataset {sr} done processed total of {count}") + +def preprocess(input: Input) -> Output: + """Preprocess(预处理)""" + finished = 0 + if input.model == Model.VC_PPG2MEL: + from ppg2mel.preprocess import preprocess_dataset + finished = preprocess_dataset( + datasets_root=Path(input.datasets_root), + dataset=input.dataset, + out_dir=Path(input.output_root), + n_processes=input.n_processes, + ppg_encoder_model_fpath=Path(input.extractor.value), + speaker_encoder_model=Path(input.encoder.value) + ) + # TODO: pass useful return code + return Output(__root__=(input.dataset, finished)) \ No newline at end of file diff --git a/mkgui/static/mb.png b/mkgui/static/mb.png new file mode 100644 index 0000000000000000000000000000000000000000..abd804cab48147cdfafc4a385cf501322bca6e1c GIT binary patch literal 5748 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 V&EL21(34^eO;uf$G9}xv{{hN{34Q Dict: + # input["selected_dataset"] = st.selectbox( + # '选择数据集', + # ("aidatatang_200zh", "aidatatang_200zh_s") + # ) + # return input + model: Model = Field( + Model.VC_PPG2MEL, title="模型类型", + ) + # datasets_root: str = Field( + # ..., alias="预处理数据根目录", description="输入目录(相对/绝对),不适用于ppg2mel模型", + # format=True, + # example="..\\trainning_data\\" + # ) + output_root: str = Field( + ..., alias="输出目录(可选)", description="建议不填,保持默认", + format=True, + example="" + ) + continue_mode: bool = Field( + True, alias="继续训练模式", description="选择“是”,则从下面选择的模型中继续训练", + ) + gpu: bool = Field( + True, alias="GPU训练", description="选择“是”,则使用GPU训练", + ) + verbose: bool = Field( + True, alias="打印详情", description="选择“是”,输出更多详情", + ) + # TODO: Move to hiden fields by default + convertor: convertors = Field( + ..., alias="转换模型", + description="选择语音转换模型文件." + ) + extractor: extractors = Field( + ..., alias="特征提取模型", + description="选择PPG特征提取模型文件." + ) + encoder: encoders = Field( + ..., alias="语音编码模型", + description="选择语音编码模型文件." + ) + njobs: int = Field( + 8, alias="进程数", description="适用于ppg2mel", + ) + seed: int = Field( + default=0, alias="初始随机数", description="适用于ppg2mel", + ) + model_name: str = Field( + ..., alias="新模型名", description="仅在重新训练时生效,选中继续训练时无效", + example="test" + ) + model_config: str = Field( + ..., alias="新模型配置", description="仅在重新训练时生效,选中继续训练时无效", + example=".\\ppg2mel\\saved_models\\seq2seq_mol_ppg2mel_vctk_libri_oneshotvc_r4_normMel_v2" + ) + +class AudioEntity(BaseModel): + content: bytes + mel: Any + +class Output(BaseModel): + __root__: tuple[str, int] + + def render_output_ui(self, streamlit_app, input) -> None: # type: ignore + """Custom output UI. + If this method is implmeneted, it will be used instead of the default Output UI renderer. + """ + sr, count = self.__root__ + streamlit_app.subheader(f"Dataset {sr} done processed total of {count}") + +def train(input: Input) -> Output: + """Train(训练)""" + + print(">>> OneShot VC training ...") + params = AttrDict() + params.update({ + "gpu": input.gpu, + "cpu": not input.gpu, + "njobs": input.njobs, + "seed": input.seed, + "verbose": input.verbose, + "load": input.convertor.value, + "warm_start": False, + }) + if input.continue_mode: + # trace old model and config + p = Path(input.convertor.value) + params.name = p.parent.name + # search a config file + model_config_fpaths = list(p.parent.rglob("*.yaml")) + if len(model_config_fpaths) == 0: + raise "No model yaml config found for convertor" + config = HpsYaml(model_config_fpaths[0]) + params.ckpdir = p.parent.parent + params.config = model_config_fpaths[0] + params.logdir = os.path.join(p.parent, "log") + else: + # Make the config dict dot visitable + config = HpsYaml(input.config) + np.random.seed(input.seed) + torch.manual_seed(input.seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(input.seed) + mode = "train" + from ppg2mel.train.train_linglf02mel_seq2seq_oneshotvc import Solver + solver = Solver(config, params, mode) + solver.load_data() + solver.set_model() + solver.exec() + print(">>> Oneshot VC train finished!") + + # TODO: pass useful return code + return Output(__root__=(input.dataset, 0)) \ No newline at end of file diff --git a/ppg2mel/__init__.py b/ppg2mel/__init__.py index 53ee3b2..cc54db8 100644 --- a/ppg2mel/__init__.py +++ b/ppg2mel/__init__.py @@ -191,12 +191,15 @@ class MelDecoderMOLv2(AbsMelDecoder): return mel_outputs[0], mel_outputs_postnet[0], alignments[0] -def load_model(train_config, model_file, device=None): - +def load_model(model_file, device=None): + # search a config file + model_config_fpaths = list(model_file.parent.rglob("*.yaml")) + if len(model_config_fpaths) == 0: + raise "No model yaml config found for convertor" if device is None: device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - model_config = HpsYaml(train_config) + model_config = HpsYaml(model_config_fpaths[0]) ppg2mel_model = MelDecoderMOLv2( **model_config["model"] ).to(device) diff --git a/ppg2mel/preprocess.py b/ppg2mel/preprocess.py index 6da9054..0feee6e 100644 --- a/ppg2mel/preprocess.py +++ b/ppg2mel/preprocess.py @@ -110,3 +110,4 @@ def preprocess_dataset(datasets_root, dataset, out_dir, n_processes, ppg_encoder t_fid_file.close() d_fid_file.close() e_fid_file.close() + return len(wav_file_list) diff --git a/ppg2mel/train.py b/ppg2mel/train.py index fed7501..d3ef729 100644 --- a/ppg2mel/train.py +++ b/ppg2mel/train.py @@ -31,15 +31,10 @@ def main(): parser.add_argument('--njobs', default=8, type=int, help='Number of threads for dataloader/decoding.', required=False) parser.add_argument('--cpu', action='store_true', help='Disable GPU training.') - parser.add_argument('--no-pin', action='store_true', - help='Disable pin-memory for dataloader') - parser.add_argument('--test', action='store_true', help='Test the model.') + # parser.add_argument('--no-pin', action='store_true', + # help='Disable pin-memory for dataloader') parser.add_argument('--no-msg', action='store_true', help='Hide all messages.') - parser.add_argument('--finetune', action='store_true', help='Finetune model') - parser.add_argument('--oneshotvc', action='store_true', help='Oneshot VC model') - parser.add_argument('--bilstm', action='store_true', help='BiLSTM VC model') - parser.add_argument('--lsa', action='store_true', help='Use location-sensitive attention (LSA)') - + ### paras = parser.parse_args() diff --git a/ppg2mel/train/solver.py b/ppg2mel/train/solver.py index 264a91c..9ca71cb 100644 --- a/ppg2mel/train/solver.py +++ b/ppg2mel/train/solver.py @@ -93,6 +93,7 @@ class BaseSolver(): def load_ckpt(self): ''' Load ckpt if --load option is specified ''' + print(self.paras) if self.paras.load is not None: if self.paras.warm_start: self.verbose(f"Warm starting model from checkpoint {self.paras.load}.") @@ -100,7 +101,7 @@ class BaseSolver(): self.paras.load, map_location=self.device if self.mode == 'train' else 'cpu') model_dict = ckpt['model'] - if len(self.config.model.ignore_layers) > 0: + if "ignore_layers" in self.config.model and len(self.config.model.ignore_layers) > 0: model_dict = {k:v for k, v in model_dict.items() if k not in self.config.model.ignore_layers} dummy_dict = self.model.state_dict() diff --git a/requirements.txt b/requirements.txt index 21becf4..5c64e70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,7 @@ flask_cors==3.0.10 gevent==21.8.0 flask_restx tensorboard -opyrator -streamlit==1.3.1 \ No newline at end of file +streamlit==1.8.0 +PyYAML==5.4.1 +torch_complex +espnet \ No newline at end of file diff --git a/requirements_vc.txt b/requirements_vc.txt deleted file mode 100644 index 871fdee..0000000 --- a/requirements_vc.txt +++ /dev/null @@ -1,3 +0,0 @@ -PyYAML==5.4.1 -torch_complex -espnet \ No newline at end of file diff --git a/samples/T0055G0013S0005.wav b/samples/T0055G0013S0005.wav new file mode 100644 index 0000000000000000000000000000000000000000..4fcc65cd1a9bcd3ac3adbfe593696585a8ce5f36 GIT binary patch literal 121002 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 0: + config_fpath = model_config_fpaths[0] + else: + config_fpath = "./vocoder/hifigan/config_16k_.json" with open(config_fpath) as f: data = f.read() json_config = json.loads(data) diff --git a/vocoder/hifigan/train.py b/vocoder/hifigan/train.py index 987bcca..8760274 100644 --- a/vocoder/hifigan/train.py +++ b/vocoder/hifigan/train.py @@ -12,7 +12,6 @@ from torch.utils.data import DistributedSampler, DataLoader import torch.multiprocessing as mp from torch.distributed import init_process_group from torch.nn.parallel import DistributedDataParallel -from vocoder.hifigan.env import AttrDict, build_env from vocoder.hifigan.meldataset import MelDataset, mel_spectrogram, get_dataset_filelist from vocoder.hifigan.models import Generator, MultiPeriodDiscriminator, MultiScaleDiscriminator, feature_loss, generator_loss,\ discriminator_loss diff --git a/vocoder_train.py b/vocoder_train.py index d3ad0f5..1ef0e30 100644 --- a/vocoder_train.py +++ b/vocoder_train.py @@ -1,7 +1,7 @@ from utils.argutils import print_args from vocoder.wavernn.train import train from vocoder.hifigan.train import train as train_hifigan -from vocoder.hifigan.env import AttrDict +from utils.util import AttrDict from pathlib import Path import argparse import json diff --git a/web.py b/web.py index 56ac93c..d232530 100644 --- a/web.py +++ b/web.py @@ -1,11 +1,21 @@ -from web import webApp -from gevent import pywsgi as wsgi +import os +import sys +import typer +cli = typer.Typer() + +@cli.command() +def launch_ui(port: int = typer.Option(8080, "--port", "-p")) -> None: + """Start a graphical UI server for the opyrator. + + The UI is auto-generated from the input- and output-schema of the given function. + """ + # Add the current working directory to the sys path + # This is required to resolve the opyrator path + sys.path.append(os.getcwd()) + + from mkgui.base.ui.streamlit_ui import launch_ui + launch_ui(port) if __name__ == "__main__": - app = webApp() - host = app.config.get("HOST") - port = app.config.get("PORT") - print(f"Web server: http://{host}:{port}") - server = wsgi.WSGIServer((host, port), app) - server.serve_forever() + cli() \ No newline at end of file