├── .dockerignore ├── .gitignore ├── LICENSE ├── README.md ├── benchmark └── get_benchmark_report.py ├── compose.yaml ├── configs ├── api_cfg.yaml ├── conversation_cfg.yaml └── rag_config.yaml ├── dataset ├── gen_dataset │ ├── gen_dataset.py │ ├── merge_dataset.py │ └── train_dataset │ │ └── 1479_train.jsonl └── gen_instructions │ ├── fonts │ └── simfang.ttf │ └── gen_instruction.py ├── deploy.sh ├── doc ├── UI设计稿.drawio ├── community │ └── sponsor.jpg ├── database │ └── README.md ├── digital_human │ ├── README.md │ ├── download_models.py │ ├── images │ │ ├── comfyui-1.png │ │ ├── comfyui-2.png │ │ ├── comfyui-3.png │ │ ├── comfyui-4.png │ │ └── comfyui-5.png │ ├── streamer-sales-lelemiao-workflow-v1.0.json │ └── streamer-sales-lelemiao-workflow-v1.0.png ├── doc_images │ ├── admin-demo0.png │ ├── admin-demo1.png │ ├── admin-demo2-1.png │ ├── admin-demo2.png │ ├── admin-demo3-1.png │ ├── admin-demo3.png │ ├── admin-demo4-0.png │ ├── admin-demo4-1.png │ ├── admin-demo4-2.png │ ├── admin-demo4-3.png │ ├── admin-demo4-4.png │ ├── admin-demo4-5.png │ ├── admin-demo5.png │ ├── admin-demo6.png │ ├── admin-demo_gif.gif │ ├── architecture.png │ ├── demo.png │ ├── demo2.png │ ├── demo3.png │ ├── demo4.png │ ├── demo5.png │ ├── demo_gif.gif │ └── media_cited.gif └── frontend │ └── README.md ├── docker └── Dockerfile ├── environment.yml ├── finetune_configs └── internlm2_chat_7b │ └── internlm2_chat_7b_qlora_custom_data.py ├── frontend ├── .env ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── env.d.ts ├── index.html ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.vue │ ├── api │ │ ├── base.ts │ │ ├── dashboard.ts │ │ ├── digitalHuman.ts │ │ ├── llm.ts │ │ ├── product.ts │ │ ├── streamerInfo.ts │ │ ├── streamingRoom.ts │ │ ├── system.ts │ │ └── user.ts │ ├── assets │ │ ├── github.svg │ │ └── logo.png │ ├── components │ │ ├── AslideComponent.vue │ │ ├── BarChartComponent.vue │ │ ├── BreadCrumb.vue │ │ ├── FileUpload.vue │ │ ├── InfoDialogComponents.vue │ │ ├── LineChartComponent.vue │ │ ├── MessageComponent.vue │ │ ├── NavbarComponent.vue │ │ ├── StreamerInfoComponent.vue │ │ └── VideoComponent.vue │ ├── layouts │ │ └── BaseLayout.vue │ ├── main.ts │ ├── router │ │ └── index.ts │ ├── stores │ │ └── userToken.ts │ ├── style │ │ └── index.scss │ ├── utils │ │ └── navbar.ts │ └── views │ │ ├── digital-human │ │ ├── DigitalHumanEditDialogView.vue │ │ └── DigitalHumanView.vue │ │ ├── error │ │ └── NotFound.vue │ │ ├── home │ │ └── HomeView.vue │ │ ├── login │ │ └── LoginView.vue │ │ ├── order │ │ └── OrderView.vue │ │ ├── product │ │ ├── ProductEditView.vue │ │ └── ProductListView.vue │ │ ├── streaming │ │ ├── StreamingOnAirView.vue │ │ ├── StreamingRoomListView.vue │ │ └── StreamingRoomeEditView.vue │ │ └── system │ │ └── SystemPluginsView.vue ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json ├── vite.config.ts └── vitest.config.ts ├── requirements.txt ├── requirements ├── asr.txt ├── base.txt ├── digital_human.txt ├── train.txt └── tts.txt ├── server ├── __init__.py ├── asr │ ├── asr_server.py │ └── asr_worker.py ├── base │ ├── __init__.py │ ├── base_server.py │ ├── database │ │ ├── __init__.py │ │ ├── init_db.py │ │ ├── llm_db.py │ │ ├── product_db.py │ │ ├── streamer_info_db.py │ │ ├── streamer_room_db.py │ │ └── user_db.py │ ├── models │ │ ├── __init__.py │ │ ├── llm_model.py │ │ ├── product_model.py │ │ ├── streamer_info_model.py │ │ ├── streamer_room_model.py │ │ └── user_model.py │ ├── modules │ │ ├── __init__.py │ │ ├── agent │ │ │ ├── __init__.py │ │ │ ├── agent_worker.py │ │ │ └── delivery_time_query.py │ │ └── rag │ │ │ ├── __init__.py │ │ │ ├── feature_store.py │ │ │ ├── file_operation.py │ │ │ ├── rag_worker.py │ │ │ ├── retriever.py │ │ │ └── test_queries.json │ ├── queue_thread.py │ ├── routers │ │ ├── __init__.py │ │ ├── digital_human.py │ │ ├── llm.py │ │ ├── products.py │ │ ├── streamer_info.py │ │ ├── streaming_room.py │ │ └── users.py │ ├── server_info.py │ └── utils.py ├── digital_human │ ├── digital_human_server.py │ └── modules │ │ ├── __init__.py │ │ ├── digital_human_worker.py │ │ ├── musetalk │ │ ├── models │ │ │ ├── unet.py │ │ │ └── vae.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ ├── blending.py │ │ │ ├── dwpose │ │ │ │ ├── default_runtime.py │ │ │ │ └── rtmpose-l_8xb32-270e_coco-ubody-wholebody-384x288.py │ │ │ ├── face_detection │ │ │ │ ├── README.md │ │ │ │ ├── __init__.py │ │ │ │ ├── api.py │ │ │ │ ├── detection │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── core.py │ │ │ │ │ └── sfd │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── bbox.py │ │ │ │ │ │ ├── detect.py │ │ │ │ │ │ ├── net_s3fd.py │ │ │ │ │ │ └── sfd_detector.py │ │ │ │ ├── models.py │ │ │ │ └── utils.py │ │ │ ├── face_parsing │ │ │ │ ├── __init__.py │ │ │ │ ├── model.py │ │ │ │ └── resnet.py │ │ │ ├── preprocessing.py │ │ │ └── utils.py │ │ └── whisper │ │ │ ├── audio2feature.py │ │ │ └── whisper │ │ │ ├── __init__.py │ │ │ ├── __main__.py │ │ │ ├── assets │ │ │ ├── gpt2 │ │ │ │ ├── merges.txt │ │ │ │ ├── special_tokens_map.json │ │ │ │ ├── tokenizer_config.json │ │ │ │ └── vocab.json │ │ │ ├── mel_filters.npz │ │ │ └── multilingual │ │ │ │ ├── added_tokens.json │ │ │ │ ├── merges.txt │ │ │ │ ├── special_tokens_map.json │ │ │ │ ├── tokenizer_config.json │ │ │ │ └── vocab.json │ │ │ ├── audio.py │ │ │ ├── decoding.py │ │ │ ├── model.py │ │ │ ├── normalizers │ │ │ ├── __init__.py │ │ │ ├── basic.py │ │ │ ├── english.json │ │ │ └── english.py │ │ │ ├── tokenizer.py │ │ │ ├── transcribe.py │ │ │ └── utils.py │ │ └── realtime_inference.py ├── tts │ ├── modules │ │ ├── __init__.py │ │ ├── gpt_sovits │ │ │ ├── AR │ │ │ │ ├── __init__.py │ │ │ │ ├── models │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── t2s_lightning_module.py │ │ │ │ │ ├── t2s_model.py │ │ │ │ │ └── utils.py │ │ │ │ ├── modules │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── activation.py │ │ │ │ │ ├── embedding.py │ │ │ │ │ ├── lr_schedulers.py │ │ │ │ │ ├── optim.py │ │ │ │ │ ├── patched_mha_with_cache.py │ │ │ │ │ ├── scaling.py │ │ │ │ │ └── transformer.py │ │ │ │ └── utils │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── initialize.py │ │ │ │ │ └── io.py │ │ │ ├── inference_gpt_sovits.py │ │ │ ├── module │ │ │ │ ├── __init__.py │ │ │ │ ├── attentions.py │ │ │ │ ├── cnhubert.py │ │ │ │ ├── commons.py │ │ │ │ ├── core_vq.py │ │ │ │ ├── mel_processing.py │ │ │ │ ├── models.py │ │ │ │ ├── modules.py │ │ │ │ ├── mrte_model.py │ │ │ │ ├── quantize.py │ │ │ │ └── transforms.py │ │ │ ├── text │ │ │ │ ├── __init__.py │ │ │ │ ├── chinese.py │ │ │ │ ├── cleaner.py │ │ │ │ ├── cmudict-fast.rep │ │ │ │ ├── cmudict.rep │ │ │ │ ├── engdict-hot.rep │ │ │ │ ├── engdict_cache.pickle │ │ │ │ ├── english.py │ │ │ │ ├── namedict_cache.pickle │ │ │ │ ├── opencpop-strict.txt │ │ │ │ ├── symbols.py │ │ │ │ ├── tone_sandhi.py │ │ │ │ └── zh_normalization │ │ │ │ │ ├── README.md │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── char_convert.py │ │ │ │ │ ├── chronology.py │ │ │ │ │ ├── constants.py │ │ │ │ │ ├── num.py │ │ │ │ │ ├── phonecode.py │ │ │ │ │ ├── quantifier.py │ │ │ │ │ └── text_normlization.py │ │ │ └── utils.py │ │ ├── sambert_hifigan │ │ │ └── tts_sambert_hifigan.py │ │ └── tts_worker.py │ ├── tools.py │ └── tts_server.py └── web_configs.py ├── static ├── digital_human │ └── streamer_info_files │ │ ├── lelemiao.mp4 │ │ ├── lelemiao.png │ │ └── lelemiao.wav ├── product_files │ ├── images │ │ ├── beef.png │ │ ├── elec_toothblush.png │ │ ├── lip_stick.png │ │ ├── mask.png │ │ ├── oled_tv.png │ │ ├── pad.png │ │ ├── pants.png │ │ ├── pen.png │ │ ├── perfume.png │ │ ├── shampoo.png │ │ ├── wok.png │ │ └── yoga_mat.png │ └── instructions │ │ ├── beef.md │ │ ├── elec_toothblush.md │ │ ├── lip_stick.md │ │ ├── mask.md │ │ ├── oled_tv.md │ │ ├── pad.md │ │ ├── pants.md │ │ ├── pen.md │ │ ├── perfume.md │ │ ├── shampoo.md │ │ ├── wok.md │ │ └── yoga_mat.md └── user │ └── user-avatar.png ├── utils └── __init__.py └── weights └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .git* 2 | work_dirs 3 | weights 4 | *.yaml 5 | *.sh 6 | doc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | work_dir 162 | bk 163 | tmp 164 | doc/.$* 165 | response 166 | -------------------------------------------------------------------------------- /benchmark/get_benchmark_report.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from pathlib import Path 3 | 4 | import torch 5 | from lmdeploy import GenerationConfig, TurbomindEngineConfig, pipeline 6 | from prettytable import PrettyTable 7 | from transformers import AutoModelForCausalLM, AutoTokenizer 8 | from modelscope import snapshot_download 9 | 10 | 11 | def get_lmdeploy_benchmark(mode_name, model_format="hf", tag="LMDeploy (Turbomind)"): 12 | print(f"Processing {mode_name}") 13 | 14 | model_path = snapshot_download(mode_name, revision="master") 15 | 16 | backend_config = TurbomindEngineConfig(model_format=model_format, session_len=32768) 17 | gen_config = GenerationConfig( 18 | top_p=0.8, 19 | top_k=40, 20 | temperature=0.7, 21 | # max_new_tokens=4096 22 | ) 23 | pipe = pipeline(model_path, backend_config=backend_config) 24 | 25 | # warmup 26 | inp = "你好!" 27 | for i in range(5): 28 | print(f"Warm up...[{i+1}/5]") 29 | pipe([inp]) 30 | 31 | # test speed 32 | times = 10 33 | total_words = 0 34 | start_time = datetime.datetime.now() 35 | for i in range(times): 36 | response = pipe(["请介绍一下你自己。"], gen_config=gen_config) 37 | total_words += len(response[0].text) 38 | end_time = datetime.datetime.now() 39 | 40 | delta_time = end_time - start_time 41 | delta_time = delta_time.seconds + delta_time.microseconds / 1000000.0 42 | speed = total_words / delta_time 43 | 44 | print(f"{Path(model_path).name:<10}, {speed:.3f}") 45 | return [Path(model_path).name, tag, round(speed, 4)] 46 | 47 | 48 | def get_hf_benchmark(model_name, tag="transformer"): 49 | 50 | print(f"Processing {model_name}") 51 | 52 | model_path = snapshot_download(model_name, revision="master") 53 | 54 | tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) 55 | 56 | # Set `torch_dtype=torch.float16` to load model in float16, otherwise it will be loaded as float32 and cause OOM Error. 57 | model = AutoModelForCausalLM.from_pretrained(model_path, torch_dtype=torch.float16, trust_remote_code=True).cuda() 58 | model = model.eval() 59 | 60 | # warmup 61 | inp = "你好!" 62 | for i in range(5): 63 | print(f"Warm up...[{i + 1}/5]") 64 | response, history = model.chat(tokenizer, inp, history=[]) 65 | 66 | # test speed 67 | inp = "请介绍一下你自己。" 68 | times = 10 69 | total_words = 0 70 | start_time = datetime.datetime.now() 71 | for i in range(times): 72 | response, history = model.chat(tokenizer, inp, history=history) 73 | total_words += len(response) 74 | end_time = datetime.datetime.now() 75 | 76 | delta_time = end_time - start_time 77 | delta_time = delta_time.seconds + delta_time.microseconds / 1000000.0 78 | speed = total_words / delta_time 79 | print(f"{Path(model_path).name:<10}, {speed:.3f}") 80 | return [Path(model_path).name, tag, round(speed, 4)] 81 | 82 | 83 | if __name__ == "__main__": 84 | 85 | table = PrettyTable() 86 | table.field_names = ["Model", "Toolkit", "Speed (words/s)"] 87 | table.add_row(get_hf_benchmark("HinGwenWoong/streamer-sales-lelemiao-7b")) 88 | table.add_row(get_lmdeploy_benchmark("HinGwenWoong/streamer-sales-lelemiao-7b", model_format="hf")) 89 | table.add_row(get_lmdeploy_benchmark("HinGwenWoong/streamer-sales-lelemiao-7b-4bit", model_format="awq")) 90 | print(table) 91 | -------------------------------------------------------------------------------- /configs/api_cfg.yaml: -------------------------------------------------------------------------------- 1 | ali_qwen_api_key: {your_ali_qwen_api_key} 2 | baidu_ernie_api_key: {your_baidu_ernie_api_key} 3 | kimi_api_key: {kimi_api_key} -------------------------------------------------------------------------------- /configs/rag_config.yaml: -------------------------------------------------------------------------------- 1 | feature_store: 2 | reject_throttle: 0.3612170956128262 3 | embedding_model_path: "maidalun/bce-embedding-base_v1" 4 | reranker_model_path: "maidalun/bce-reranker-base_v1" 5 | work_dir: "./work_dirs/instruction_db" 6 | -------------------------------------------------------------------------------- /dataset/gen_instructions/fonts/simfang.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/dataset/gen_instructions/fonts/simfang.ttf -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 检查参数个数 4 | if [ "$#" -ne 1 ]; then 5 | echo "Error: 必须且只能提供一个参数." 6 | echo "可用的选项为: tts, dg, asr, llm, base 或者 frontend." 7 | exit 1 8 | fi 9 | 10 | # 进入虚拟环境 11 | # conda deactivate 12 | conda activate streamer-sales 13 | 14 | # 可选配置显卡 15 | # export CUDA_VISIBLE_DEVICES=1 16 | 17 | # 配置 huggingface 国内镜像地址 18 | export HF_ENDPOINT="https://hf-mirror.com" 19 | 20 | case $1 in 21 | tts) 22 | echo "正在启动 TTS 服务..." 23 | uvicorn server.tts.tts_server:app --host 0.0.0.0 --port 8001 24 | ;; 25 | 26 | dg) 27 | echo "正在启动 数字人 服务..." 28 | uvicorn server.digital_human.digital_human_server:app --host 0.0.0.0 --port 8002 29 | ;; 30 | 31 | asr) 32 | echo "正在启动 ASR 服务..." 33 | export MODELSCOPE_CACHE="./weights/asr_weights" 34 | uvicorn server.asr.asr_server:app --host 0.0.0.0 --port 8003 35 | ;; 36 | 37 | llm) 38 | echo "正在启动 LLM 服务..." 39 | export LMDEPLOY_USE_MODELSCOPE=True 40 | export MODELSCOPE_CACHE="./weights/llm_weights" 41 | lmdeploy serve api_server HinGwenWoong/streamer-sales-lelemiao-7b \ 42 | --server-port 23333 \ 43 | --model-name internlm2 \ 44 | --session-len 32768 \ 45 | --cache-max-entry-count 0.1 \ 46 | --model-format hf 47 | ;; 48 | 49 | llm-4bit) 50 | echo "正在启动 LLM-4bit 服务..." 51 | export LMDEPLOY_USE_MODELSCOPE=True 52 | export MODELSCOPE_CACHE="./weights/llm_weights" 53 | lmdeploy serve api_server HinGwenWoong/streamer-sales-lelemiao-7b-4bit \ 54 | --server-port 23333 \ 55 | --model-name internlm2 \ 56 | --session-len 32768 \ 57 | --cache-max-entry-count 0.1 \ 58 | --model-format awq 59 | ;; 60 | 61 | base) 62 | echo "正在启动 中台 服务..." 63 | # Agent Key (如果有请配置,没有请忽略) 64 | # export DELIVERY_TIME_API_KEY="${快递 EBusinessID},${快递 api_key}" 65 | # export WEATHER_API_KEY="${天气 API key}" 66 | uvicorn server.base.base_server:app --host 0.0.0.0 --port 8000 67 | ;; 68 | 69 | frontend) 70 | echo "正在启动 前端 服务..." 71 | cd frontend 72 | # npm install 73 | npm run dev 74 | ;; 75 | 76 | *) 77 | echo "错误: 不支持的参数 '$1'." 78 | echo "可用的选项为: tts, dg, asr, llm, llm-4bit, base 或者 frontend." 79 | exit 1 80 | ;; 81 | esac 82 | 83 | exit 0 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /doc/community/sponsor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/community/sponsor.jpg -------------------------------------------------------------------------------- /doc/database/README.md: -------------------------------------------------------------------------------- 1 | # 数据库 2 | 3 | ## 安装 4 | 5 | 参考[官网说明](https://www.postgresql.org/download/linux/ubuntu/),执行下面的命令就可以完成安装: 6 | 7 | ```bash 8 | sudo apt-get update 9 | sudo apt install -y postgresql-common 10 | sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh 11 | 12 | sudo apt update 13 | sudo apt install postgresql-16 14 | ``` 15 | 16 | ## 查看版本 17 | 18 | ```bash 19 | psql --version 20 | ``` 21 | 22 | ## 服务情况 23 | 24 | 安装 PostgreSQL 后,需要知道以下 3 个命令: 25 | 26 | - `sudo service postgresql status`: 用于检查数据库的状态。 27 | - `sudo service postgresql start`: 用于开始运行数据库。 28 | - `sudo service postgresql stop`: 用于停止运行数据库。 29 | 30 | ## 初始化密码 31 | 32 | 默认管理员用户 postgres 需要分配的密码才能连接到数据库。要设置密码,请执行以下操作: 33 | 34 | 1. 启动 postgres 服务:`sudo service postgresql start` 35 | 2. 输入命令:`sudo passwd postgres` 36 | 3. 系统将提示你输入新密码。 37 | 4. 关闭并重新打开终端。 38 | 5. 连接到 postgres 服务,并打开 psql shell:`sudo -u postgres psql`,或者临时切换用户的方法进入: `su - postgres && psql` 39 | 6. 成功输入 psql shell 后,将显示更改为如下所示的命令行:`postgres=#` 40 | 41 | ## 设置数据库用户名密码 42 | 43 | 上一步设置的是命令行登录的密码,下面修改使用网络连接用的用户名和密码: 44 | 45 | ```bash 46 | sudo -u postgres psql 47 | # 会出现 postgres=# 然后输入,xxx 就是密码,后续连接用这个密码,最后的 ; 不要漏了! 48 | alter role postgres with password 'xxx'; 49 | ``` 50 | 51 | ## 访问 52 | 53 | - 修改数据库访问权限配置文件 : `sudo vim /etc/postgresql/16/main/pg_hba.conf` 54 | 55 | ```diff 56 | -local all postgres peer 57 | +local all postgres trust 58 | ``` 59 | 60 | 在 IPv4 下面加入 61 | 62 | ```bash 63 | host all all 0.0.0.0/0 scram-sha-256 64 | ``` 65 | 66 | - 修改 数据库服务器参数配置 `sudo vim /etc/postgresql/16/main/postgresql.conf` 67 | 68 | 在 `# - Connection Settings -` 的 `#listen_addresses = 'localhost'` 前加入: 69 | 70 | ```bash 71 | listen_addresses = '*' 72 | ``` 73 | 74 | 重启服务 75 | 76 | ```bash 77 | sudo service postgresql restart 78 | ``` 79 | 80 | ## 新建数据库 81 | 82 | 需要先建立一个数据库, 83 | 84 | 1. 登录数据库: 85 | 86 | ```bash 87 | sudo -u postgres psql 88 | ``` 89 | 90 | 2. 创建数据库 91 | 92 | ```bash 93 | CREATE DATABASE streamer_sales_db; 94 | ``` 95 | 96 | 3. 查看目前所有的数据库 97 | 98 | ```bash 99 | \l 100 | ``` 101 | 102 | 后续数据表会在代码里面自行创建 103 | 104 | ## 数据库可视化 105 | 106 | [pgadmin](https://www.pgadmin.org/) 107 | -------------------------------------------------------------------------------- /doc/digital_human/README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI 使用文档 2 | 3 | ## 工作流 4 | 5 | 如果您已经有 ComfyUI 的环境,可以直接使用我的工作流: 6 | 7 |

8 | Demo gif 9 |

10 | 11 | **用法**:直接将上面的图片拖进去 ComfyUI 工作区就可以啦,但记得提前保存您的工作区哈。 12 | 13 | ## 功能点 14 | 15 | 我的 Workflow 具有以下功能点: 16 | 17 | - 生成人像图 18 | - DW Pose 生成骨骼图 19 | - ControlNet 控制人物姿态 20 | - AnimateDiff 生成视频 21 | - 插帧提升帧率 22 | - 提升分辨率 23 | 24 | ## 环境搭建 25 | 26 | ```bash 27 | git clone https://github.com/comfyanonymous/ComfyUI.git 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | 测试安装 32 | 33 | ```bash 34 | cd ComfyUI 35 | python main.py 36 | ``` 37 | 38 | ## 模型下载 39 | 40 | 执行脚本 `python download_models.py` 即可下载本项目需要用到的全部权重 41 | 42 | ## 插件安装 43 | 44 | 1. 首先需要手动拉取下【插件管理器】 45 | 46 | ```bash 47 | cd ComfyUI/custom_nodes 48 | git clone https://github.com/ltdrdata/ComfyUI-Manager.git 49 | ``` 50 | 51 | 2. 重启 ComfyUI 52 | 53 | 3. 刷新页面,点击右下角 【管理器】->【安装缺失节点】即可。 54 | 55 | 以下是我用到的插件: 56 | 57 | | 插件名 | 用途 | 58 | | :---------------------------: | :------------------------: | 59 | | AIGODLIKE-COMFYUI-TRANSLATION | 中文翻译 | 60 | | ComfyUI-Advanced-ControlNet | ContralNet 工具包升级版 | 61 | | ComfyUI-AnimateDiff-Evolved | AnimateDiff 动画生成 | 62 | | ComfyUI-Crystools | 机器资源监控 | 63 | | ComfyUI-Custom-Scripts | 模型管理 | 64 | | ComfyUI-Frame-Interpolation | 插帧 | 65 | | ComfyUI-Impact-Pack | | 66 | | ComfyUI-Manager | 插件管理器(必备) | 67 | | ComfyUI-VideoHelperSuite | 视频加载器 | 68 | | ComfyUI_FizzNodes | | 69 | | ComfyUI_IPAdapter_plus | IPAdapter 风格迁移 | 70 | | comfyui-portrait-master-zh-cn | 人物生成中文提示词辅助工具 | 71 | | comfyui-workspace-manager | 工作流管理器 | 72 | | comfyui_controlnet_aux | ContralNet 工具包 | 73 | | comfyui_segment_anything | SAM 工具包 | 74 | | sdxl_prompt_styler | SDXL 工具包 | 75 | 76 | ## Workflow 详解 77 | 78 | ### 1. 生成人像图 79 | 80 |

81 | workflow 82 |

83 | 84 | 首先我们来说下基本的文生图流程,首先加入 sd checkpoint ,和 vae 模型,vae 可选,但 sd 是必须的,如果觉得我这个模型不好,可以自行去 c站 找大佬微调好的模型, 85 | 86 | 填写好正向词和反向词,接个 Ksampler 就可以生成人像了 87 | 88 | ### 2. DW Pose 生成骨骼图 & ControlNet 控制人物姿态 89 | 90 |

91 | workflow 92 |

93 | 94 | 人物生成好了,下一步要生成特定的动作的话,有时候语言很难描述,我们需要借助 controlnet 来结合 pose 的姿态图来让 sd 生成特定动作的任务,这就是左下角的作用 95 | 96 | ### 3. AnimateDiff 生成视频 97 | 98 |

99 | workflow 100 |

101 | 102 | 这两块搞好之后,可以看到任务以特定的动作生成了,下面,我们加入动作,用到的算法是 Animatediff 简单的串起来,就可以了 103 | 104 | ### 4. 插帧提升帧率 105 | 106 |

107 | workflow 108 |

109 | 110 | 我们把生成的图片合成为视频,原始是 8帧,我们对它进行一个插帧,让视频更加丝滑,这就是右上角的功能 111 | 112 | ### 5. 提升分辨率 113 | 114 |

115 | workflow 116 |

117 | 118 | 因为 SD 1.5 默认的输出是 512 x 512,我们还要做个 scale ,让分辨率高一点,这就是右下角的功能。 119 | 120 | ## 配置数字人视频路径 121 | 122 | 生成好了 mp4 我们就可以修改下配置 [web_configs](../../server/web_configs.py#L74) 中的 `DIGITAL_HUMAN_VIDEO_PATH` 参数,后续就会用这个视频来生成口型了。 123 | 124 | ```diff 125 | - DIGITAL_HUMAN_VIDEO_PATH: str = r"./doc/digital_human/lelemiao_digital_human_video.mp4" 126 | + DIGITAL_HUMAN_VIDEO_PATH: str = r"新生成的 mp4 路径" 127 | ``` 128 | 129 | 另外,如果文件夹 `./work_dirs/digital_human` 存在,则需要进行将该文件夹删除,然后重启服务就可以了 130 | 131 | ## 开发参考网站 132 | 133 | - 模型下载网站:C站:https://civitai.com 134 | - 提示词网站:https://promlib.com/ 135 | - 工作流:https://openart.ai/workflows/home 136 | - 插件排行:https://www.nodecafe.org/ 137 | -------------------------------------------------------------------------------- /doc/digital_human/download_models.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" 4 | from huggingface_hub import hf_hub_download 5 | 6 | COMFYUI_PATH = r"/path/to/ComfyUI" 7 | 8 | # ============================================== 9 | # 官方 SD 权重 10 | # ============================================== 11 | hf_hub_download( 12 | repo_id="stabilityai/stable-diffusion-xl-base-1.0", 13 | filename="sd_xl_base_1.0.safetensors", 14 | local_dir=rf"{COMFYUI_PATH}/models/checkpoints", 15 | ) 16 | 17 | hf_hub_download( 18 | repo_id="runwayml/stable-diffusion-v1-5", 19 | filename="v1-5-pruned.safetensors", 20 | local_dir=rf"{COMFYUI_PATH}/models/checkpoints", 21 | ) 22 | 23 | # ============================================== 24 | # AnimateDiff 权重 25 | # ============================================== 26 | for animatediff_model in ["mm_sd_v15_v2.ckpt", "mm_sdxl_v10_beta.ckpt", "v3_sd15_mm.ckpt"]: 27 | hf_hub_download( 28 | repo_id="guoyww/animatediff", 29 | filename=animatediff_model, 30 | local_dir=rf"{COMFYUI_PATH}/models/animatediff_models", 31 | ) 32 | 33 | for animatediff_model in ["temporaldiff-v1-animatediff.safetensors"]: 34 | hf_hub_download( 35 | repo_id="CiaraRowles/TemporalDiff", 36 | filename=animatediff_model, 37 | local_dir=rf"{COMFYUI_PATH}/models/animatediff_models", 38 | ) 39 | 40 | for lora_model in [ 41 | "v2_lora_PanLeft.ckpt", 42 | "v2_lora_PanRight.ckpt", 43 | "v2_lora_RollingAnticlockwise.ckpt", 44 | "v2_lora_RollingClockwise.ckpt", 45 | "v2_lora_TiltDown.ckpt", 46 | "v2_lora_TiltUp.ckpt", 47 | "v2_lora_ZoomIn.ckpt", 48 | "v2_lora_ZoomOut.ckpt", 49 | ]: 50 | hf_hub_download( 51 | repo_id="guoyww/animatediff", 52 | filename=lora_model, 53 | local_dir=rf"{COMFYUI_PATH}/models/animatediff_motion_lora", 54 | ) 55 | 56 | # ============================================== 57 | # ControlNet 权重 58 | # ============================================== 59 | for controlnet_model in ["control_v11p_sd15_openpose.pth", "control_v11f1p_sd15_depth.pth", "control_v11p_sd15_seg.pth"]: 60 | hf_hub_download( 61 | repo_id="lllyasviel/ControlNet-v1-1", 62 | filename=controlnet_model, 63 | local_dir=rf"{COMFYUI_PATH}/models/controlnet", 64 | ) 65 | 66 | # ============================================== 67 | # SAM 权重 68 | # ============================================== 69 | for sam_model in ["groundingdino_swinb_cogcoor.pth", "GroundingDINO_SwinB.cfg.py"]: 70 | hf_hub_download( 71 | repo_id="ShilongLiu/GroundingDINO", 72 | filename=sam_model, 73 | local_dir=rf"{COMFYUI_PATH}/models/grounding-dino/", 74 | ) 75 | 76 | # ============================================== 77 | # IP-Adapter 权重 78 | # ============================================== 79 | for ip_adapter_model in ["models/ip-adapter-plus_sd15.safetensors"]: 80 | hf_hub_download( 81 | repo_id="h94/IP-Adapter", 82 | filename=ip_adapter_model, 83 | local_dir=rf"{COMFYUI_PATH}/models/ipadapter", 84 | ) 85 | 86 | for ip_adapter_clip_model in ["models/image_encoder/model.safetensors"]: 87 | hf_hub_download( 88 | repo_id="h94/IP-Adapter", 89 | filename=ip_adapter_clip_model, 90 | local_dir=rf"{COMFYUI_PATH}/models/clip_vision/", 91 | ) 92 | -------------------------------------------------------------------------------- /doc/digital_human/images/comfyui-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/digital_human/images/comfyui-1.png -------------------------------------------------------------------------------- /doc/digital_human/images/comfyui-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/digital_human/images/comfyui-2.png -------------------------------------------------------------------------------- /doc/digital_human/images/comfyui-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/digital_human/images/comfyui-3.png -------------------------------------------------------------------------------- /doc/digital_human/images/comfyui-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/digital_human/images/comfyui-4.png -------------------------------------------------------------------------------- /doc/digital_human/images/comfyui-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/digital_human/images/comfyui-5.png -------------------------------------------------------------------------------- /doc/digital_human/streamer-sales-lelemiao-workflow-v1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/digital_human/streamer-sales-lelemiao-workflow-v1.0.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo0.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo1.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo2-1.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo2.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo3-1.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo3.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo4-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo4-0.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo4-1.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo4-2.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo4-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo4-3.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo4-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo4-4.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo4-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo4-5.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo5.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo6.png -------------------------------------------------------------------------------- /doc/doc_images/admin-demo_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/admin-demo_gif.gif -------------------------------------------------------------------------------- /doc/doc_images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/architecture.png -------------------------------------------------------------------------------- /doc/doc_images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/demo.png -------------------------------------------------------------------------------- /doc/doc_images/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/demo2.png -------------------------------------------------------------------------------- /doc/doc_images/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/demo3.png -------------------------------------------------------------------------------- /doc/doc_images/demo4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/demo4.png -------------------------------------------------------------------------------- /doc/doc_images/demo5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/demo5.png -------------------------------------------------------------------------------- /doc/doc_images/demo_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/demo_gif.gif -------------------------------------------------------------------------------- /doc/doc_images/media_cited.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/doc/doc_images/media_cited.gif -------------------------------------------------------------------------------- /doc/frontend/README.md: -------------------------------------------------------------------------------- 1 | # 前端文档 2 | 3 | ## 安装 npm 4 | 5 | 参考[官网说明](https://nodejs.org/zh-cn/download/package-manager),执行下面的命令就可以完成安装: 6 | 7 | ```bash 8 | # layouts.download.codeBox.installsFnm 9 | curl -fsSL https://fnm.vercel.app/install | bash 10 | 11 | # layouts.download.codeBox.activateFNM 12 | source ~/.bashrc 13 | 14 | # layouts.download.codeBox.downloadAndInstallNodejs 15 | fnm use --install-if-missing 20 16 | 17 | # layouts.download.codeBox.verifiesRightNodejsVersion 18 | node -v # layouts.download.codeBox.shouldPrint 19 | 20 | # layouts.download.codeBox.verifiesRightNpmVersion 21 | npm -v # layouts.download.codeBox.shouldPrint 22 | ``` 23 | 24 | ## 启动项目 25 | 26 | 1. 进入前端项目: 27 | 28 | ```bash 29 | cd frontend 30 | ``` 31 | 32 | 2. 安装依赖库 33 | 34 | ```sh 35 | npm install 36 | ``` 37 | 38 | 3. 开发环境启动 39 | 40 | > Compile and Hot-Reload for Development 41 | 42 | ```sh 43 | npm run dev 44 | ``` 45 | 46 | 4. 生产环境打包 47 | 48 | > Type-Check, Compile and Minify for Production 49 | 50 | ```sh 51 | npm run build 52 | ``` 53 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pytorch/pytorch:2.1.2-cuda12.1-cudnn8-devel 2 | 3 | LABEL MAINTAINER="HinGwen.Wong" 4 | 5 | # 设置时区 6 | RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 7 | echo 'Asia/Shanghai' > /etc/timezone 8 | 9 | # 切换阿里源并安装必须的系统库 10 | RUN sed -i s/archive.ubuntu.com/mirrors.aliyun.com/g /etc/apt/sources.list \ 11 | && sed -i s/security.ubuntu.com/mirrors.aliyun.com/g /etc/apt/sources.list \ 12 | && apt-get update -y \ 13 | && apt-get install -y --no-install-recommends wget git libgl1 libglib2.0-0 unzip libpq-dev \ 14 | && apt-get clean \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | COPY . /workspace/Streamer-Sales 18 | WORKDIR /workspace/Streamer-Sales 19 | 20 | ENV HF_ENDPOINT="https://hf-mirror.com" 21 | ENV LANG="en_US.UTF-8" 22 | 23 | # 安装必备依赖环境 24 | RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \ 25 | && pip install --no-cache-dir -r requirements.txt 26 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_SERVER_URL = 'http://127.0.0.1:8000/api/v1' -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting' 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /frontend/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 销冠——卖货主播大模型 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "test:unit": "vitest", 11 | "build-only": "vite build", 12 | "type-check": "vue-tsc --build --force", 13 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 14 | "format": "prettier --write src/" 15 | }, 16 | "dependencies": { 17 | "@vueuse/core": "^11.0.1", 18 | "axios": "^1.7.7", 19 | "echarts": "^5.5.1", 20 | "element-plus": "^2.7.8", 21 | "md-editor-v3": "^4.18.1", 22 | "pinia": "^2.1.7", 23 | "pinia-plugin-persistedstate": "^3.2.1", 24 | "sass": "^1.77.8", 25 | "vue": "^3.4.29", 26 | "vue-router": "^4.3.3", 27 | "xgplayer": "^3.0.19" 28 | }, 29 | "devDependencies": { 30 | "@rushstack/eslint-patch": "^1.8.0", 31 | "@tsconfig/node20": "^20.1.4", 32 | "@types/jsdom": "^21.1.7", 33 | "@types/node": "^20.14.5", 34 | "@vitejs/plugin-vue": "^5.0.5", 35 | "@vue/eslint-config-prettier": "^9.0.0", 36 | "@vue/eslint-config-typescript": "^13.0.0", 37 | "@vue/test-utils": "^2.4.6", 38 | "@vue/tsconfig": "^0.5.1", 39 | "eslint": "^8.57.0", 40 | "eslint-plugin-vue": "^9.23.0", 41 | "jsdom": "^24.1.0", 42 | "npm-run-all2": "^6.2.0", 43 | "prettier": "^3.2.5", 44 | "typescript": "~5.4.0", 45 | "vite": "^5.3.1", 46 | "vite-plugin-vue-devtools": "^7.3.1", 47 | "vitest": "^1.6.0", 48 | "vue-tsc": "^2.0.21" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/api/base.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const request_handler = axios.create({ 4 | // baseURL: import.meta.env.BASE_SERVER_URL 5 | }) 6 | 7 | interface ResultPackage { 8 | success: boolean 9 | code: number 10 | message: string 11 | data: T 12 | timestamp: number 13 | } 14 | 15 | export { request_handler } 16 | export { type ResultPackage } 17 | -------------------------------------------------------------------------------- /frontend/src/api/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { request_handler, type ResultPackage } from '@/api/base' 2 | 3 | interface DashboardItem { 4 | registeredBrandNum: number //入驻品牌方 5 | productNum: number //商品数 6 | dailyActivity: number //日活 7 | todayOrder: number //订单量 8 | totalSales: number //销售额 9 | conversionRate: number //转化率 10 | 11 | orderNumList: number[] // 订单量 12 | totalSalesList: number[] // 销售额 13 | newUserList: number[] // 新增用户 14 | activityUserList: number[] // 活跃用户 15 | 16 | knowledgeBasesNum: number // 知识库数量 17 | digitalHumanNum: number // 数字人数量 18 | LiveRoomNum: number // 直播间数量 19 | } 20 | 21 | // 获取主控大屏信息 22 | const getDashboardInfoRequest = () => { 23 | return request_handler>({ 24 | method: 'GET', 25 | url: '/dashboard' 26 | }) 27 | } 28 | 29 | export { getDashboardInfoRequest, type DashboardItem } 30 | -------------------------------------------------------------------------------- /frontend/src/api/digitalHuman.ts: -------------------------------------------------------------------------------- 1 | import { request_handler } from '@/api/base' 2 | 3 | // 生成数字人视频接口 4 | const genDigitalHuamnVideoRequest = (streamerId_: number, salesDoc_: string) => { 5 | return request_handler({ 6 | method: 'POST', 7 | url: '/digital-human/gen', 8 | data: { streamerId: streamerId_, salesDoc: salesDoc_ } 9 | }) 10 | } 11 | 12 | export { genDigitalHuamnVideoRequest } 13 | -------------------------------------------------------------------------------- /frontend/src/api/llm.ts: -------------------------------------------------------------------------------- 1 | import { request_handler, type ResultPackage } from '@/api/base' 2 | import { header_authorization } from '@/api/user' 3 | 4 | // 获取后端主播信息 5 | const genSalesDocRequest = (productId: number, streamerId: number) => { 6 | return request_handler>({ 7 | method: 'GET', 8 | url: '/llm/gen_sales_doc', 9 | params: { streamer_id: streamerId, product_id: productId }, 10 | headers: { 11 | Authorization: header_authorization.value 12 | } 13 | }) 14 | } 15 | 16 | // 使用说明书总结生成商品信息接口 17 | const genProductInfoByLlmRequest = (productId: number) => { 18 | return request_handler({ 19 | method: 'GET', 20 | url: '/llm/gen_product_info', 21 | params: { product_id: productId }, 22 | headers: { 23 | Authorization: header_authorization.value 24 | } 25 | }) 26 | } 27 | 28 | export { genSalesDocRequest, genProductInfoByLlmRequest } 29 | -------------------------------------------------------------------------------- /frontend/src/api/product.ts: -------------------------------------------------------------------------------- 1 | import { request_handler, type ResultPackage } from '@/api/base' 2 | import type { StreamerInfo } from '@/api/streamerInfo' 3 | import { header_authorization } from '@/api/user' 4 | 5 | // 调用商品信息接口数据结构定义 6 | type ProductListType = { 7 | currentPage?: number // 当前页号 8 | pageSize?: number // 每页记录数 9 | productName?: string // 商品名称 10 | product_class?: string // 商品分类 11 | } 12 | 13 | interface ProductItem { 14 | user_id: number // User 识别号,用于区分不用的用户 15 | request_id: string // 请求 ID 16 | 17 | product_id: number 18 | product_name: string 19 | product_class: string 20 | heighlights: string 21 | image_path: string 22 | instruction: string 23 | departure_place: string 24 | delivery_company: string 25 | selling_price: number 26 | amount: number 27 | upload_date: string 28 | delete: boolean 29 | } 30 | 31 | interface ProductData { 32 | product_list: ProductItem[] 33 | currentPage: number 34 | pageSize: number 35 | totalSize: number 36 | } 37 | 38 | // 查询接口 39 | const productListRequest = (params_: ProductListType) => { 40 | return request_handler>({ 41 | method: 'GET', 42 | url: '/products/list', 43 | params: params_, 44 | headers: { 45 | Authorization: header_authorization.value 46 | } 47 | }) 48 | } 49 | 50 | // 查询指定商品的信息接口 51 | const getProductByIdRequest = async (productId: string) => { 52 | return request_handler>({ 53 | method: 'GET', 54 | url: `/products/info/${productId}`, 55 | headers: { 56 | Authorization: header_authorization.value 57 | } 58 | }) 59 | } 60 | 61 | // 添加或者更新商品接口 62 | const productCreadeOrEditRequest = (params: ProductItem) => { 63 | if (params.product_id === 0) { 64 | // 新增商品 65 | return request_handler>({ 66 | method: 'POST', // 新增 67 | url: '/products/create', 68 | data: params, 69 | headers: { 70 | Authorization: header_authorization.value 71 | } 72 | }) 73 | } else { 74 | // 修改商品 75 | return request_handler>({ 76 | method: 'PUT', // 新增 77 | url: `/products/edit/${params.product_id}`, 78 | data: params, 79 | headers: { 80 | Authorization: header_authorization.value 81 | } 82 | }) 83 | } 84 | } 85 | 86 | // 删除商品接口 87 | const deleteProductByIdRequest = async (productId: number) => { 88 | return request_handler>({ 89 | method: 'DELETE', 90 | url: `/products/delete/${productId}`, 91 | headers: { 92 | Authorization: header_authorization.value 93 | } 94 | }) 95 | } 96 | 97 | // 根据 ID 获取说明书内容 98 | const genProductInstructionContentRequest = (instructionPath_: string) => { 99 | // TODO 后续直接使用 axios 获取 100 | return request_handler({ 101 | method: 'POST', 102 | url: '/products/instruction', 103 | data: { instructionPath: instructionPath_ }, 104 | headers: { 105 | Authorization: header_authorization.value 106 | } 107 | }) 108 | } 109 | 110 | export { 111 | type ProductItem, 112 | type StreamerInfo, 113 | type ProductListType, 114 | type ProductData, 115 | productListRequest, 116 | productCreadeOrEditRequest, 117 | getProductByIdRequest, 118 | deleteProductByIdRequest, 119 | genProductInstructionContentRequest 120 | } 121 | -------------------------------------------------------------------------------- /frontend/src/api/streamerInfo.ts: -------------------------------------------------------------------------------- 1 | import { request_handler, type ResultPackage } from '@/api/base' 2 | import { header_authorization } from '@/api/user' 3 | 4 | interface StreamerInfo { 5 | user_id: number 6 | 7 | streamer_id: number 8 | name: string 9 | value: string 10 | character: string 11 | avatar: string 12 | 13 | tts_weight_tag: string 14 | tts_reference_audio: string 15 | tts_reference_sentence: string 16 | 17 | poster_image: string 18 | base_mp4_path: string 19 | 20 | delete: boolean 21 | } 22 | 23 | // 获取后端主播信息 24 | const streamerInfoListRequest = () => { 25 | return request_handler>({ 26 | method: 'GET', 27 | url: '/streamer/list', 28 | headers: { 29 | Authorization: header_authorization.value 30 | } 31 | }) 32 | } 33 | 34 | // 获取特定主播信息 35 | const streamerDetailInfoRequest = (streamerId: number) => { 36 | return request_handler>({ 37 | method: 'GET', 38 | url: `/streamer/info/${streamerId}`, 39 | headers: { 40 | Authorization: header_authorization.value 41 | } 42 | }) 43 | } 44 | 45 | // 更新特定主播信息 46 | const streamerEditDetailRequest = async (streamerItem: StreamerInfo) => { 47 | if (typeof streamerItem.streamer_id != 'number' || streamerItem.streamer_id === 0) { 48 | // 新建 49 | console.info(streamerItem) 50 | return request_handler>({ 51 | method: 'POST', 52 | url: '/streamer/create', 53 | data: streamerItem, 54 | headers: { 55 | Authorization: header_authorization.value 56 | } 57 | }) 58 | } else { 59 | return request_handler>({ 60 | method: 'PUT', 61 | url: `/streamer/edit/${streamerItem.streamer_id}`, 62 | data: streamerItem, 63 | headers: { 64 | Authorization: header_authorization.value 65 | } 66 | }) 67 | } 68 | } 69 | 70 | // 删除特定主播信息 71 | const deleteStreamerByIdRequest = (streamerId: number) => { 72 | return request_handler>({ 73 | method: 'DELETE', 74 | url: `/streamer/delete/${streamerId}`, 75 | headers: { 76 | Authorization: header_authorization.value 77 | } 78 | }) 79 | } 80 | 81 | export { 82 | type StreamerInfo, 83 | streamerInfoListRequest, 84 | streamerDetailInfoRequest, 85 | streamerEditDetailRequest, 86 | deleteStreamerByIdRequest 87 | } 88 | -------------------------------------------------------------------------------- /frontend/src/api/system.ts: -------------------------------------------------------------------------------- 1 | import { request_handler, type ResultPackage } from '@/api/base' 2 | 3 | interface SystemPluginsInfo { 4 | plugin_name: string 5 | describe: string 6 | avatar_color: string 7 | enabled: boolean 8 | } 9 | 10 | // 删除特定主播信息 11 | const getSystemPluginsInfoRequest = () => { 12 | return request_handler>({ 13 | method: 'GET', 14 | url: '/plugins_info' 15 | }) 16 | } 17 | 18 | export { type SystemPluginsInfo, getSystemPluginsInfoRequest } 19 | -------------------------------------------------------------------------------- /frontend/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { request_handler, type ResultPackage } from '@/api/base' 3 | import { type TokenItem, useTokenStore } from '@/stores/userToken' 4 | 5 | // 调用登录接口数据结构定义 6 | type loginFormType = { 7 | username: string 8 | password: string 9 | vertify_code?: string 10 | } 11 | 12 | interface UserInfo { 13 | user_id: number 14 | username: string 15 | avatar: string 16 | email: string 17 | create_time: string 18 | } 19 | 20 | // pinia 保存的 token 21 | const tokenStore = useTokenStore() 22 | 23 | // jwt 24 | const header_authorization = computed(() => { 25 | console.log('Update token') 26 | return `${tokenStore.token.token_type} ${tokenStore.token.access_token}` 27 | }) 28 | 29 | // 登录接口 30 | const loginRequest = (loginForm: loginFormType) => { 31 | const formData = new FormData() 32 | formData.append('username', loginForm.username) 33 | formData.append('password', loginForm.password) 34 | 35 | return request_handler({ 36 | method: 'POST', 37 | url: '/user/login', 38 | data: formData, 39 | headers: { 40 | 'Content-Type': 'application/x-www-form-urlencoded' 41 | } 42 | }) 43 | } 44 | 45 | // 获取用户信息接口 46 | const getUserInfoRequest = async () => { 47 | return request_handler>({ 48 | method: 'GET', 49 | url: '/user/me', 50 | headers: { 51 | Authorization: header_authorization.value 52 | } 53 | }) 54 | } 55 | 56 | export { loginRequest, header_authorization, getUserInfoRequest, type UserInfo } 57 | -------------------------------------------------------------------------------- /frontend/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/components/AslideComponent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 66 | 67 | 133 | -------------------------------------------------------------------------------- /frontend/src/components/BarChartComponent.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 61 | 62 | 65 | -------------------------------------------------------------------------------- /frontend/src/components/BreadCrumb.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/LineChartComponent.vue: -------------------------------------------------------------------------------- 1 | 120 | 121 | 124 | 125 | 128 | -------------------------------------------------------------------------------- /frontend/src/components/MessageComponent.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 66 | 67 | 89 | -------------------------------------------------------------------------------- /frontend/src/components/NavbarComponent.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 63 | 64 | 79 | -------------------------------------------------------------------------------- /frontend/src/components/VideoComponent.vue: -------------------------------------------------------------------------------- 1 | 100 | 101 | 104 | -------------------------------------------------------------------------------- /frontend/src/layouts/BaseLayout.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 32 | 33 | 42 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import { createPinia } from 'pinia' 4 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 5 | 6 | import ElementPlus from 'element-plus' 7 | 8 | import 'element-plus/dist/index.css' 9 | import zhCn from 'element-plus/es/locale/lang/zh-cn' 10 | 11 | import 'xgplayer/dist/index.min.css' 12 | 13 | import '@/style/index.scss' 14 | 15 | import App from './App.vue' 16 | import router from './router' 17 | 18 | const app = createApp(App) 19 | 20 | const pinia = createPinia() 21 | pinia.use(piniaPluginPersistedstate) // 持久化储存插件 22 | 23 | app.use(pinia) 24 | app.use(router) 25 | 26 | app.use(ElementPlus, { 27 | locale: zhCn 28 | }) 29 | app.mount('#app') 30 | -------------------------------------------------------------------------------- /frontend/src/stores/userToken.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | interface TokenItem { 5 | access_token: string 6 | token_type: string 7 | } 8 | 9 | const useTokenStore = defineStore('user-token', { 10 | state: () => { 11 | const token = ref({} as TokenItem) 12 | 13 | function saveToken(data: TokenItem) { 14 | token.value = data 15 | } 16 | 17 | return { token, saveToken } 18 | }, 19 | 20 | persist: { 21 | paths: ['token'], // 需要持久化保存的字段名 22 | storage: localStorage 23 | } 24 | }) 25 | 26 | export { type TokenItem, useTokenStore } 27 | -------------------------------------------------------------------------------- /frontend/src/style/index.scss: -------------------------------------------------------------------------------- 1 | /* 2 | 进入和离开动画可以使用不同 3 | 持续时间和速度曲线。 4 | */ 5 | .slide-fade-enter-active { 6 | transition: all 0.3s ease-out; 7 | } 8 | 9 | .slide-fade-leave-active { 10 | transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1); 11 | } 12 | 13 | .slide-fade-enter-from, 14 | .slide-fade-leave-to { 15 | transform: translateX(20px); 16 | opacity: 0; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/utils/navbar.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | // 侧边栏是否折叠 4 | export const isCollapse = ref(false) // TODO 是否可以用 Pinia ? 5 | -------------------------------------------------------------------------------- /frontend/src/views/digital-human/DigitalHumanEditDialogView.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 86 | 87 | 97 | -------------------------------------------------------------------------------- /frontend/src/views/digital-human/DigitalHumanView.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 121 | 122 | 156 | -------------------------------------------------------------------------------- /frontend/src/views/error/NotFound.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/views/order/OrderView.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 70 | 71 | 91 | -------------------------------------------------------------------------------- /frontend/src/views/system/SystemPluginsView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 49 | 50 | 90 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 8 | 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "noEmit": true, 13 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 14 | 15 | "module": "ESNext", 16 | "moduleResolution": "Bundler", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", 7 | 8 | "lib": [], 9 | "types": ["node", "jsdom"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig, loadEnv } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueDevTools from 'vite-plugin-vue-devtools' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | server: { 10 | proxy: { 11 | '/user': loadEnv('', process.cwd()).VITE_BASE_SERVER_URL, 12 | '/products': loadEnv('', process.cwd()).VITE_BASE_SERVER_URL, 13 | '/upload': loadEnv('', process.cwd()).VITE_BASE_SERVER_URL, 14 | '/streamer': loadEnv('', process.cwd()).VITE_BASE_SERVER_URL, 15 | '/llm': loadEnv('', process.cwd()).VITE_BASE_SERVER_URL, 16 | '/dashboard': loadEnv('', process.cwd()).VITE_BASE_SERVER_URL, 17 | '/streaming-room': loadEnv('', process.cwd()).VITE_BASE_SERVER_URL, 18 | '/plugins_info': loadEnv('', process.cwd()).VITE_BASE_SERVER_URL, 19 | '/digital-human': loadEnv('', process.cwd()).VITE_BASE_SERVER_URL 20 | } 21 | }, 22 | plugins: [vue(), vueDevTools()], 23 | resolve: { 24 | alias: { 25 | '@': fileURLToPath(new URL('./src', import.meta.url)) 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /frontend/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' 3 | import viteConfig from './vite.config' 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | environment: 'jsdom', 10 | exclude: [...configDefaults.exclude, 'e2e/**'], 11 | root: fileURLToPath(new URL('./', import.meta.url)) 12 | } 13 | }) 14 | ) 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/base.txt 2 | -r requirements/tts.txt 3 | -r requirements/digital_human.txt 4 | -r requirements/asr.txt 5 | -r requirements/train.txt -------------------------------------------------------------------------------- /requirements/asr.txt: -------------------------------------------------------------------------------- 1 | # Deploy 2 | modelscope==1.14.0 3 | 4 | # ASR 5 | funasr==1.0.27 6 | 7 | # Web app 2.0 8 | fastapi[all]==0.111.0 9 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | # Deploy 2 | lmdeploy==0.4.0 3 | modelscope==1.14.0 4 | opencv-python==4.9.0.80 5 | 6 | # RAG 7 | BCEmbedding==0.1.5 8 | langchain==0.2.0 9 | langchain-community==0.2.0 10 | loguru==0.7.2 11 | faiss-gpu==1.7.2 12 | 13 | # Agent 14 | lagent==0.2.2 15 | jionlp==1.5.14 16 | griffe==0.48.0 17 | class-registry==2.1.2 18 | 19 | # Web app 2.0 20 | fastapi[all]==0.111.0 21 | # sse-starlette==2.1.0 22 | PyJWT==2.9.0 23 | passlib==1.7.4 24 | sqlmodel==0.0.22 25 | psycopg==3.2.1 26 | -------------------------------------------------------------------------------- /requirements/digital_human.txt: -------------------------------------------------------------------------------- 1 | # Deploy 2 | modelscope==1.14.0 3 | 4 | # Digital human 5 | mmengine==0.10.4 6 | 7 | -f https://download.openmmlab.com/mmcv/dist/cu121/torch2.1/index.html 8 | mmcv==2.1.0 9 | 10 | mmdet==3.3.0 11 | mmpose==1.3.1 12 | diffusers==0.27.2 13 | wget==3.2 14 | 15 | # Web app 2.0 16 | fastapi[all]==0.111.0 17 | -------------------------------------------------------------------------------- /requirements/train.txt: -------------------------------------------------------------------------------- 1 | # Train 2 | xtuner[deepspeed]==0.1.19 3 | lmdeploy==0.4.0 4 | modelscope==1.14.0 5 | -------------------------------------------------------------------------------- /requirements/tts.txt: -------------------------------------------------------------------------------- 1 | # Deploy 2 | modelscope==1.14.0 3 | 4 | # TTS 2.0 5 | LangSegment==0.3.3 6 | librosa==0.9.2 7 | ffmpeg-python==0.2.0 8 | pytorch-lightning==2.2.5 9 | cn2an==0.5.22 10 | pypinyin==0.51.0 11 | jieba-fast==0.53 12 | wordsegment==1.3.1 13 | g2p-en==2.1.0 14 | matplotlib==3.9.0 15 | numpy==1.26.4 16 | 17 | # Web app 2.0 18 | fastapi[all]==0.111.0 19 | -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/__init__.py -------------------------------------------------------------------------------- /server/asr/asr_server.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.exceptions import RequestValidationError 3 | from fastapi.responses import PlainTextResponse 4 | from loguru import logger 5 | from pydantic import BaseModel 6 | 7 | from ..web_configs import WEB_CONFIGS 8 | from .asr_worker import load_asr_model, process_asr 9 | 10 | app = FastAPI() 11 | 12 | if WEB_CONFIGS.ENABLE_ASR: 13 | ASR_HANDLER = load_asr_model() 14 | else: 15 | ASR_HANDLER = None 16 | 17 | 18 | class ASRItem(BaseModel): 19 | user_id: int # User 识别号,用于区分不用的用户调用 20 | request_id: str # 请求 ID,用于生成 TTS & 数字人 21 | wav_path: str # wav 文件路径 22 | 23 | 24 | @app.post("/asr") 25 | async def get_asr(asr_item: ASRItem): 26 | # 语音转文字 27 | result = "" 28 | status = "success" 29 | if ASR_HANDLER is None: 30 | result = "ASR not enable in sever" 31 | status = "fail" 32 | logger.error(f"ASR not enable...") 33 | else: 34 | result = process_asr(ASR_HANDLER, asr_item.wav_path) 35 | logger.info(f"ASR res for id {asr_item.request_id}, res = {result}") 36 | 37 | return {"user_id": asr_item.user_id, "request_id": asr_item.request_id, "status": status, "result": result} 38 | 39 | 40 | @app.get("/asr/check") 41 | async def check_server(): 42 | return {"message": "server enabled"} 43 | 44 | 45 | @app.exception_handler(RequestValidationError) 46 | async def validation_exception_handler(request, exc): 47 | """调 API 入参错误的回调接口 48 | 49 | Args: 50 | request (_type_): _description_ 51 | exc (_type_): _description_ 52 | 53 | Returns: 54 | _type_: _description_ 55 | """ 56 | logger.info(request) 57 | logger.info(exc) 58 | return PlainTextResponse(str(exc), status_code=400) 59 | -------------------------------------------------------------------------------- /server/asr/asr_worker.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from funasr import AutoModel 4 | from funasr.download.name_maps_from_hub import name_maps_ms as NAME_MAPS_MS 5 | from modelscope import snapshot_download 6 | from modelscope.utils.constant import Invoke, ThirdParty 7 | 8 | from ..web_configs import WEB_CONFIGS 9 | 10 | 11 | def load_asr_model(): 12 | 13 | # 模型下载 14 | model_path_info = dict() 15 | for model_name in ["paraformer-zh", "fsmn-vad", "ct-punc"]: 16 | print(f"downloading asr model : {NAME_MAPS_MS[model_name]}") 17 | mode_dir = snapshot_download( 18 | NAME_MAPS_MS[model_name], 19 | revision="master", 20 | user_agent={Invoke.KEY: Invoke.PIPELINE, ThirdParty.KEY: "funasr"}, 21 | cache_dir=WEB_CONFIGS.ASR_MODEL_DIR, 22 | ) 23 | model_path_info[model_name] = mode_dir 24 | NAME_MAPS_MS[model_name] = mode_dir # 更新权重路径环境变量 25 | 26 | print(f"ASR model path info = {model_path_info}") 27 | # paraformer-zh is a multi-functional asr model 28 | # use vad, punc, spk or not as you need 29 | model = AutoModel( 30 | model="paraformer-zh", # 语音识别,带时间戳输出,非实时 31 | vad_model="fsmn-vad", # 语音端点检测,实时 32 | punc_model="ct-punc", # 标点恢复 33 | # spk_model="cam++" # 说话人确认/分割 34 | model_path=model_path_info["paraformer-zh"], 35 | vad_kwargs={"model_path": model_path_info["fsmn-vad"]}, 36 | punc_kwargs={"model_path": model_path_info["ct-punc"]}, 37 | ) 38 | return model 39 | 40 | 41 | def process_asr(model: AutoModel, wav_path): 42 | # https://github.com/modelscope/FunASR/blob/main/README_zh.md#%E5%AE%9E%E6%97%B6%E8%AF%AD%E9%9F%B3%E8%AF%86%E5%88%AB 43 | f_start_time = datetime.datetime.now() 44 | res = model.generate(input=wav_path, batch_size_s=50, hotword="魔搭") 45 | delta_time = datetime.datetime.now() - f_start_time 46 | 47 | try: 48 | print(f"ASR using time {delta_time}s, text: ", res[0]["text"]) 49 | res_str = res[0]["text"] 50 | except Exception as e: 51 | print("ASR 解析失败,无法获取到文字") 52 | return "" 53 | 54 | return res_str 55 | -------------------------------------------------------------------------------- /server/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/base/__init__.py -------------------------------------------------------------------------------- /server/base/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/base/database/__init__.py -------------------------------------------------------------------------------- /server/base/database/init_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : init_db.py 5 | @Time : 2024/09/06 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 数据库初始化 10 | """ 11 | 12 | from loguru import logger 13 | from pydantic import PostgresDsn 14 | from pydantic_core import MultiHostUrl 15 | from sqlmodel import SQLModel, create_engine 16 | 17 | from ...web_configs import WEB_CONFIGS 18 | 19 | ECHO_DB_MESG = True # 数据库执行中是否回显,for debug 20 | 21 | 22 | def sqlalchemy_db_url() -> PostgresDsn: 23 | """生成数据库 URL 24 | 25 | Returns: 26 | PostgresDsn: 数据库地址 27 | """ 28 | return MultiHostUrl.build( 29 | scheme="postgresql+psycopg", 30 | username=WEB_CONFIGS.POSTGRES_USER, 31 | password=WEB_CONFIGS.POSTGRES_PASSWORD, 32 | host=WEB_CONFIGS.POSTGRES_SERVER, 33 | port=WEB_CONFIGS.POSTGRES_PORT, 34 | path=WEB_CONFIGS.POSTGRES_DB, 35 | ) 36 | 37 | 38 | logger.info(f"connecting to db: {str(sqlalchemy_db_url())}") 39 | DB_ENGINE = create_engine(str(sqlalchemy_db_url()), echo=ECHO_DB_MESG) 40 | 41 | 42 | def create_db_and_tables(): 43 | """创建所有数据库和对应的表,有则跳过""" 44 | SQLModel.metadata.create_all(DB_ENGINE) 45 | -------------------------------------------------------------------------------- /server/base/database/llm_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : llm_db.py 5 | @Time : 2024/09/01 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 大模型对话数据库交互 10 | """ 11 | 12 | import yaml 13 | 14 | from ...web_configs import WEB_CONFIGS 15 | 16 | 17 | async def get_llm_product_prompt_base_info(): 18 | # 加载对话配置文件 19 | with open(WEB_CONFIGS.CONVERSATION_CFG_YAML_PATH, "r", encoding="utf-8") as f: 20 | dataset_yaml = yaml.safe_load(f) 21 | 22 | return dataset_yaml 23 | -------------------------------------------------------------------------------- /server/base/database/user_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : user_db.py 5 | @Time : 2024/08/31 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 用户信息数据库操作 10 | """ 11 | 12 | from sqlmodel import Session, select 13 | 14 | from ...web_configs import API_CONFIG 15 | from ..models.user_model import UserBaseInfo, UserInfo 16 | from .init_db import DB_ENGINE 17 | 18 | 19 | def get_db_user_info(id: int = -1, username: str = "", all_info: bool = False) -> UserBaseInfo | UserInfo | None: 20 | """查询数据库获取用户信息 21 | 22 | Args: 23 | id (int): 用户 ID 24 | username (str): 用户名 25 | all_info (bool): 是否返回含有密码串的敏感信息 26 | 27 | Returns: 28 | UserInfo | None: 用户信息,没有查到返回 None 29 | """ 30 | 31 | if username == "": 32 | # 使用 ID 的方式进行查询 33 | query = select(UserInfo).where(UserInfo.user_id == id) 34 | else: 35 | query = select(UserInfo).where(UserInfo.username == username) 36 | 37 | # 查询数据库 38 | with Session(DB_ENGINE) as session: 39 | results = session.exec(query).first() 40 | 41 | # 返回服务器地址 42 | results.avatar = API_CONFIG.REQUEST_FILES_URL + results.avatar 43 | 44 | if results is not None and all_info is False: 45 | # 返回不含用户敏感信息的基本信息 46 | results = UserBaseInfo(**results.model_dump()) 47 | 48 | return results 49 | -------------------------------------------------------------------------------- /server/base/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/base/models/__init__.py -------------------------------------------------------------------------------- /server/base/models/llm_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : llm_model.py 5 | @Time : 2024/09/01 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 大模型对话数据结构 10 | """ 11 | 12 | from pydantic import BaseModel 13 | 14 | 15 | class GenProductItem(BaseModel): 16 | gen_type: str 17 | instruction: str 18 | -------------------------------------------------------------------------------- /server/base/models/product_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : product_model.py 5 | @Time : 2024/08/30 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 商品数据类型定义 10 | """ 11 | 12 | from datetime import datetime 13 | from typing import List 14 | from pydantic import BaseModel 15 | from sqlmodel import Field, Relationship, SQLModel 16 | 17 | 18 | # ======================================================= 19 | # 数据库模型 20 | # ======================================================= 21 | 22 | 23 | class ProductInfo(SQLModel, table=True): 24 | """商品信息""" 25 | 26 | __tablename__ = "product_info" 27 | 28 | product_id: int | None = Field(default=None, primary_key=True, unique=True) 29 | product_name: str = Field(index=True, unique=True) 30 | product_class: str 31 | heighlights: str 32 | image_path: str 33 | instruction: str 34 | departure_place: str 35 | delivery_company: str 36 | selling_price: float 37 | amount: int 38 | upload_date: datetime = datetime.now() 39 | delete: bool = False 40 | 41 | user_id: int | None = Field(default=None, foreign_key="user_info.user_id") 42 | 43 | sales_info: list["SalesDocAndVideoInfo"] = Relationship(back_populates="product_info") 44 | 45 | 46 | # ======================================================= 47 | # 基本模型 48 | # ======================================================= 49 | 50 | 51 | class ProductPageItem(BaseModel): 52 | product_list: List[ProductInfo] = [] 53 | currentPage: int = 0 # 当前页数 54 | pageSize: int = 0 # 页面的组件数量 55 | totalSize: int = 0 # 总大小 56 | 57 | 58 | class ProductQueryItem(BaseModel): 59 | instructionPath: str = "" # 商品说明书路径,用于获取说明书内容 60 | -------------------------------------------------------------------------------- /server/base/models/streamer_info_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : streamer_info_model.py 5 | @Time : 2024/08/30 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 主播信息数据结构 10 | """ 11 | 12 | from typing import Optional 13 | from sqlmodel import Field, Relationship, SQLModel 14 | 15 | 16 | # ======================================================= 17 | # 数据库模型 18 | # ======================================================= 19 | class StreamerInfo(SQLModel, table=True): 20 | __tablename__ = "streamer_info" 21 | 22 | streamer_id: int | None = Field(default=None, primary_key=True, unique=True) 23 | name: str = Field(index=True, unique=True) 24 | character: str = "" 25 | avatar: str = "" # 头像 26 | 27 | tts_weight_tag: str = "" # 艾丝妲 28 | tts_reference_sentence: str = "" 29 | tts_reference_audio: str = "" 30 | 31 | poster_image: str = "" 32 | base_mp4_path: str = "" 33 | 34 | delete: bool = False 35 | 36 | user_id: int | None = Field(default=None, foreign_key="user_info.user_id") 37 | 38 | room_info: Optional["StreamRoomInfo"] | None = Relationship( 39 | back_populates="streamer_info", sa_relationship_kwargs={"lazy": "selectin"} 40 | ) 41 | -------------------------------------------------------------------------------- /server/base/models/user_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : user_model.py 5 | @Time : 2024/08/31 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 用户信息数据结构 10 | """ 11 | 12 | from datetime import datetime 13 | from ipaddress import IPv4Address 14 | from pydantic import BaseModel 15 | from sqlmodel import Field, SQLModel 16 | 17 | 18 | # ======================================================= 19 | # 基本模型 20 | # ======================================================= 21 | class TokenItem(BaseModel): 22 | access_token: str 23 | token_type: str 24 | 25 | 26 | class UserBaseInfo(BaseModel): 27 | user_id: int | None = Field(default=None, primary_key=True, unique=True) 28 | username: str = Field(index=True, unique=True) 29 | email: str | None = None 30 | avatar: str | None = None 31 | create_time: datetime = datetime.now() 32 | 33 | 34 | # ======================================================= 35 | # 数据库模型 36 | # ======================================================= 37 | class UserInfo(UserBaseInfo, SQLModel, table=True): 38 | 39 | __tablename__ = "user_info" 40 | 41 | hashed_password: str 42 | ip_address: IPv4Address | None = None 43 | delete: bool = False 44 | -------------------------------------------------------------------------------- /server/base/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/base/modules/__init__.py -------------------------------------------------------------------------------- /server/base/modules/agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/base/modules/agent/__init__.py -------------------------------------------------------------------------------- /server/base/modules/rag/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/base/modules/rag/__init__.py -------------------------------------------------------------------------------- /server/base/modules/rag/rag_worker.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | 4 | import torch 5 | from loguru import logger 6 | 7 | from ....web_configs import WEB_CONFIGS 8 | from ...database.product_db import get_db_product_info 9 | from .feature_store import gen_vector_db 10 | from .retriever import CacheRetriever 11 | 12 | # 基础配置 13 | CONTEXT_MAX_LENGTH = 3000 # 上下文最大长度 14 | GENERATE_TEMPLATE = "这是说明书:“{}”\n 客户的问题:“{}” \n 请阅读说明并运用你的性格进行解答。" # RAG prompt 模板 15 | 16 | # RAG 实例句柄 17 | RAG_RETRIEVER = None 18 | 19 | 20 | def build_rag_prompt(rag_retriever: CacheRetriever, product_name, prompt): 21 | 22 | real_retriever = rag_retriever.get(fs_id="default") 23 | 24 | if isinstance(real_retriever, tuple): 25 | logger.info(f" @@@ GOT real_retriever == tuple : {real_retriever}") 26 | return "" 27 | 28 | chunk, db_context, references = real_retriever.query( 29 | f"商品名:{product_name}。{prompt}", context_max_length=CONTEXT_MAX_LENGTH - 2 * len(GENERATE_TEMPLATE) 30 | ) 31 | logger.info(f"db_context = {db_context}") 32 | 33 | if db_context is not None and len(db_context) > 1: 34 | prompt_rag = GENERATE_TEMPLATE.format(db_context, prompt) 35 | else: 36 | logger.info("db_context get error") 37 | prompt_rag = prompt 38 | 39 | logger.info(f"RAG reference = {references}") 40 | logger.info("=" * 20) 41 | 42 | return prompt_rag 43 | 44 | 45 | def init_rag_retriever(rag_config: str, db_path: str): 46 | torch.cuda.empty_cache() 47 | 48 | retriever = CacheRetriever(config_path=rag_config) 49 | 50 | # 初始化 51 | retriever.get(fs_id="default", config_path=rag_config, work_dir=db_path) 52 | 53 | return retriever 54 | 55 | 56 | async def gen_rag_db(user_id, force_gen=False): 57 | """ 58 | 生成向量数据库。 59 | 60 | 参数: 61 | force_gen - 布尔值,当设置为 True 时,即使数据库已存在也会重新生成数据库。 62 | """ 63 | 64 | # 检查数据库目录是否存在,如果存在且force_gen为False,则不执行生成操作 65 | if Path(WEB_CONFIGS.RAG_VECTOR_DB_DIR).exists() and not force_gen: 66 | return 67 | 68 | if force_gen and Path(WEB_CONFIGS.RAG_VECTOR_DB_DIR).exists(): 69 | shutil.rmtree(WEB_CONFIGS.RAG_VECTOR_DB_DIR) 70 | 71 | # 仅仅遍历 instructions 字段里面的文件 72 | if Path(WEB_CONFIGS.PRODUCT_INSTRUCTION_DIR_GEN_DB_TMP).exists(): 73 | shutil.rmtree(WEB_CONFIGS.PRODUCT_INSTRUCTION_DIR_GEN_DB_TMP) 74 | Path(WEB_CONFIGS.PRODUCT_INSTRUCTION_DIR_GEN_DB_TMP).mkdir(exist_ok=True, parents=True) 75 | 76 | # 读取 yaml 文件,获取所有说明书路径,并移动到 tmp 目录 77 | product_list, _ = await get_db_product_info(user_id) 78 | 79 | for info in product_list: 80 | 81 | shutil.copyfile( 82 | Path( 83 | WEB_CONFIGS.SERVER_FILE_ROOT, 84 | WEB_CONFIGS.PRODUCT_FILE_DIR, 85 | WEB_CONFIGS.INSTRUCTIONS_DIR, 86 | Path(info.instruction).name, 87 | ), 88 | Path(WEB_CONFIGS.PRODUCT_INSTRUCTION_DIR_GEN_DB_TMP).joinpath(Path(info.instruction).name), 89 | ) 90 | 91 | logger.info("Generating rag database, pls wait ...") 92 | # 调用函数生成向量数据库 93 | gen_vector_db( 94 | WEB_CONFIGS.RAG_CONFIG_PATH, 95 | str(Path(WEB_CONFIGS.PRODUCT_INSTRUCTION_DIR_GEN_DB_TMP).absolute()), 96 | WEB_CONFIGS.RAG_VECTOR_DB_DIR, 97 | ) 98 | 99 | # 删除过程文件 100 | shutil.rmtree(WEB_CONFIGS.PRODUCT_INSTRUCTION_DIR_GEN_DB_TMP) 101 | 102 | 103 | async def load_rag_model(user_id): 104 | 105 | global RAG_RETRIEVER 106 | 107 | # 重新生成 RAG 向量数据库 108 | await gen_rag_db(user_id) 109 | 110 | # 加载 rag 模型 111 | RAG_RETRIEVER = init_rag_retriever(rag_config=WEB_CONFIGS.RAG_CONFIG_PATH, db_path=WEB_CONFIGS.RAG_VECTOR_DB_DIR) 112 | logger.info("load rag model done !...") 113 | 114 | 115 | async def rebuild_rag_db(user_id, db_name="default"): 116 | 117 | # 重新生成 RAG 向量数据库 118 | await gen_rag_db(user_id, force_gen=True) 119 | 120 | # 重新加载 retriever 121 | RAG_RETRIEVER.pop(db_name) 122 | RAG_RETRIEVER.get(fs_id=db_name, config_path=WEB_CONFIGS.RAG_CONFIG_PATH, work_dir=WEB_CONFIGS.RAG_VECTOR_DB_DIR) 123 | -------------------------------------------------------------------------------- /server/base/modules/rag/test_queries.json: -------------------------------------------------------------------------------- 1 | [ 2 | "我的商品是牛肉。饲养天数", 3 | "我的商品是唇膏。净含量是多少" 4 | ] -------------------------------------------------------------------------------- /server/base/queue_thread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : queue_thread.py 5 | @Time : 2024/09/02 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 队列调取相关逻辑(半废弃状态) 10 | """ 11 | 12 | 13 | from loguru import logger 14 | import requests 15 | import multiprocessing 16 | 17 | from ..web_configs import API_CONFIG 18 | from .server_info import SERVER_PLUGINS_INFO 19 | 20 | 21 | def process_tts(tts_text_queue): 22 | 23 | while True: 24 | try: 25 | text_chunk = tts_text_queue.get(block=True, timeout=1) 26 | except Exception as e: 27 | # logger.info(f"### {e}") 28 | continue 29 | logger.info(f"Get tts quene: {type(text_chunk)} , {text_chunk}") 30 | res = requests.post(API_CONFIG.TTS_URL, json=text_chunk) 31 | 32 | # # tts 推理成功,放入数字人队列进行推理 33 | # res_json = res.json() 34 | # tts_request_dict = { 35 | # "user_id": "123", 36 | # "request_id": text_chunk["request_id"], 37 | # "chunk_id": text_chunk["chunk_id"], 38 | # "tts_path": res_json["wav_path"], 39 | # } 40 | 41 | # DIGITAL_HUMAN_QUENE.put(tts_request_dict) 42 | 43 | logger.info(f"tts res = {res}") 44 | 45 | 46 | def process_digital_human(digital_human_queue): 47 | 48 | while True: 49 | try: 50 | text_chunk = digital_human_queue.get(block=True, timeout=1) 51 | except Exception as e: 52 | # logger.info(f"### {e}") 53 | continue 54 | logger.info(f"Get digital human quene: {type(text_chunk)} , {text_chunk}") 55 | res = requests.post(API_CONFIG.DIGITAL_HUMAN_URL, json=text_chunk) 56 | logger.info(f"digital human res = {res}") 57 | 58 | 59 | if SERVER_PLUGINS_INFO.tts_server_enabled: 60 | TTS_TEXT_QUENE = multiprocessing.Queue(maxsize=100) 61 | tts_thread = multiprocessing.Process(target=process_tts, args=(TTS_TEXT_QUENE,), name="tts_processer") 62 | tts_thread.start() 63 | else: 64 | TTS_TEXT_QUENE = None 65 | 66 | if SERVER_PLUGINS_INFO.digital_human_server_enabled: 67 | DIGITAL_HUMAN_QUENE = multiprocessing.Queue(maxsize=100) 68 | digital_human_thread = multiprocessing.Process( 69 | target=process_digital_human, args=(DIGITAL_HUMAN_QUENE,), name="digital_human_processer" 70 | ) 71 | digital_human_thread.start() 72 | else: 73 | DIGITAL_HUMAN_QUENE = None 74 | -------------------------------------------------------------------------------- /server/base/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/base/routers/__init__.py -------------------------------------------------------------------------------- /server/base/routers/digital_human.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : digital_human.py 5 | @Time : 2024/09/02 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 数字人接口 10 | """ 11 | 12 | 13 | from pathlib import Path 14 | import uuid 15 | import requests 16 | from fastapi import APIRouter 17 | from loguru import logger 18 | from pydantic import BaseModel 19 | 20 | from ...web_configs import API_CONFIG, WEB_CONFIGS 21 | from ..utils import ResultCode, make_return_data 22 | 23 | router = APIRouter( 24 | prefix="/digital-human", 25 | tags=["digital-human"], 26 | responses={404: {"description": "Not found"}}, 27 | ) 28 | 29 | 30 | class GenDigitalHumanVideoItem(BaseModel): 31 | streamerId: int 32 | salesDoc: str 33 | 34 | 35 | async def gen_tts_and_digital_human_video_app(streamer_id: int, sales_doc: str): 36 | logger.info(sales_doc) 37 | 38 | request_id = str(uuid.uuid1()) 39 | sentence_id = 1 # 直接推理,所以设置成 1 40 | user_id = "123" 41 | 42 | # 生成 TTS wav 43 | tts_json = { 44 | "user_id": user_id, 45 | "request_id": request_id, 46 | "sentence": sales_doc, 47 | "chunk_id": sentence_id, 48 | # "wav_save_name": chat_item.request_id + f"{str(sentence_id).zfill(8)}.wav", 49 | } 50 | tts_save_path = Path(WEB_CONFIGS.TTS_WAV_GEN_PATH, request_id + f"-{str(1).zfill(8)}.wav") 51 | logger.info(f"waiting for wav generating done: {tts_save_path}") 52 | _ = requests.post(API_CONFIG.TTS_URL, json=tts_json) 53 | 54 | # 生成数字人视频 55 | digital_human_gen_info = { 56 | "user_id": user_id, 57 | "request_id": request_id, 58 | "chunk_id": 0, 59 | "tts_path": str(tts_save_path), 60 | "streamer_id": str(streamer_id), 61 | } 62 | video_path = Path(WEB_CONFIGS.DIGITAL_HUMAN_VIDEO_OUTPUT_PATH).joinpath(request_id + ".mp4") 63 | logger.info(f"Generating digital human: {video_path}") 64 | _ = requests.post(API_CONFIG.DIGITAL_HUMAN_URL, json=digital_human_gen_info) 65 | 66 | # 删除过程文件 67 | tts_save_path.unlink() 68 | 69 | server_video_path = f"{API_CONFIG.REQUEST_FILES_URL}/{WEB_CONFIGS.STREAMER_FILE_DIR}/vid_output/{request_id}.mp4" 70 | logger.info(server_video_path) 71 | 72 | return server_video_path 73 | 74 | 75 | @router.post("/gen") 76 | async def get_digital_human_according_doc_api(gen_item: GenDigitalHumanVideoItem): 77 | """根据口播文案生成数字人介绍视频 78 | 79 | Args: 80 | gen_item (GenDigitalHumanVideoItem): _description_ 81 | 82 | """ 83 | server_video_path = await gen_tts_and_digital_human_video_app(gen_item.streamerId, gen_item.salesDoc) 84 | 85 | return make_return_data(True, ResultCode.SUCCESS, "成功", server_video_path) 86 | -------------------------------------------------------------------------------- /server/base/routers/products.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : products.py 5 | @Time : 2024/08/30 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 商品信息接口 10 | """ 11 | 12 | from pathlib import Path 13 | 14 | from fastapi import APIRouter, Depends 15 | 16 | from ...web_configs import WEB_CONFIGS 17 | from ..database.product_db import ( 18 | create_or_update_db_product_by_id, 19 | delete_product_id, 20 | get_db_product_info, 21 | ) 22 | from ..models.product_model import ProductInfo, ProductPageItem, ProductQueryItem 23 | from ..modules.rag.rag_worker import rebuild_rag_db 24 | from ..utils import ResultCode, make_return_data 25 | from .users import get_current_user_info 26 | 27 | router = APIRouter( 28 | prefix="/products", 29 | tags=["products"], 30 | responses={404: {"description": "Not found"}}, 31 | ) 32 | 33 | 34 | @router.get("/list", summary="获取分页商品信息接口") 35 | async def get_product_info_api( 36 | currentPage: int = 1, pageSize: int = 5, productName: str | None = None, user_id: int = Depends(get_current_user_info) 37 | ): 38 | product_list, db_product_size = await get_db_product_info( 39 | user_id=user_id, 40 | current_page=currentPage, 41 | page_size=pageSize, 42 | product_name=productName, 43 | ) 44 | 45 | res_data = ProductPageItem(product_list=product_list, currentPage=currentPage, pageSize=pageSize, totalSize=db_product_size) 46 | return make_return_data(True, ResultCode.SUCCESS, "成功", res_data) 47 | 48 | 49 | @router.get("/info/{productId}", summary="获取特定商品 ID 的详细信息接口") 50 | async def get_product_id_info_api(productId: int, user_id: int = Depends(get_current_user_info)): 51 | product_list, _ = await get_db_product_info(user_id=user_id, product_id=productId) 52 | 53 | if len(product_list) == 1: 54 | product_list = product_list[0] 55 | 56 | return make_return_data(True, ResultCode.SUCCESS, "成功", product_list) 57 | 58 | 59 | @router.post("/create", summary="新增商品接口") 60 | async def upload_product_api(upload_product_item: ProductInfo, user_id: int = Depends(get_current_user_info)): 61 | 62 | upload_product_item.user_id = user_id 63 | upload_product_item.product_id = None 64 | 65 | rebuild_rag_db_flag = create_or_update_db_product_by_id(0, upload_product_item) 66 | 67 | if WEB_CONFIGS.ENABLE_RAG and rebuild_rag_db_flag: 68 | # 重新生成 RAG 向量数据库 69 | await rebuild_rag_db(user_id) 70 | 71 | return make_return_data(True, ResultCode.SUCCESS, "成功", "") 72 | 73 | 74 | @router.put("/edit/{product_id}", summary="编辑商品接口") 75 | async def upload_product_api(product_id: int, upload_product_item: ProductInfo, user_id: int = Depends(get_current_user_info)): 76 | 77 | rebuild_rag_db_flag = create_or_update_db_product_by_id(product_id, upload_product_item, user_id) 78 | 79 | if WEB_CONFIGS.ENABLE_RAG and rebuild_rag_db_flag: 80 | # 重新生成 RAG 向量数据库 81 | await rebuild_rag_db(user_id) 82 | 83 | return make_return_data(True, ResultCode.SUCCESS, "成功", "") 84 | 85 | 86 | @router.delete("/delete/{productId}", summary="删除特定商品 ID 接口") 87 | async def upload_product_api(productId: int, user_id: int = Depends(get_current_user_info)): 88 | 89 | process_success_flag = await delete_product_id(productId, user_id) 90 | 91 | if not process_success_flag: 92 | return make_return_data(False, ResultCode.FAIL, "失败", "") 93 | 94 | if WEB_CONFIGS.ENABLE_RAG: 95 | # 重新生成 RAG 向量数据库 96 | await rebuild_rag_db(user_id) 97 | 98 | return make_return_data(True, ResultCode.SUCCESS, "成功", "") 99 | 100 | 101 | @router.post("/instruction", summary="获取对应商品的说明书内容接口", dependencies=[Depends(get_current_user_info)]) 102 | async def get_product_instruction_info_api(instruction_path: ProductQueryItem): 103 | """获取对应商品的说明书 104 | 105 | Args: 106 | instruction_path (ProductInstructionItem): 说明书路径 107 | 108 | """ 109 | # TODO 后续改为前端 axios 直接获取 110 | loacl_path = Path(WEB_CONFIGS.SERVER_FILE_ROOT).joinpath( 111 | WEB_CONFIGS.PRODUCT_FILE_DIR, WEB_CONFIGS.INSTRUCTIONS_DIR, Path(instruction_path.instructionPath).name 112 | ) 113 | if not loacl_path.exists(): 114 | return make_return_data(False, ResultCode.FAIL, "文件不存在", "") 115 | 116 | with open(loacl_path, "r") as f: 117 | instruction_content = f.read() 118 | 119 | return make_return_data(True, ResultCode.SUCCESS, "成功", instruction_content) 120 | -------------------------------------------------------------------------------- /server/base/server_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @File : server_info.py 5 | @Time : 2024/09/02 6 | @Project : https://github.com/PeterH0323/Streamer-Sales 7 | @Author : HinGwenWong 8 | @Version : 1.0 9 | @Desc : 组件信息获取逻辑 10 | """ 11 | 12 | 13 | import random 14 | import requests 15 | from loguru import logger 16 | 17 | from ..web_configs import API_CONFIG, WEB_CONFIGS 18 | 19 | 20 | class ServerPluginsInfo: 21 | 22 | def __init__(self) -> None: 23 | self.update_info() 24 | 25 | def update_info(self): 26 | 27 | self.tts_server_enabled = self._check_server(API_CONFIG.TTS_URL + "/check") 28 | self.digital_human_server_enabled = self._check_server(API_CONFIG.DIGITAL_HUMAN_CHECK_URL) 29 | self.asr_server_enabled = self._check_server(API_CONFIG.ASR_URL + "/check") 30 | self.llm_enabled = self._check_server(API_CONFIG.LLM_URL) 31 | 32 | if WEB_CONFIGS.AGENT_DELIVERY_TIME_API_KEY is None or WEB_CONFIGS.AGENT_WEATHER_API_KEY is None: 33 | self.agent_enabled = False 34 | else: 35 | self.agent_enabled = True 36 | 37 | self.rag_enabled = WEB_CONFIGS.ENABLE_RAG 38 | 39 | logger.info( 40 | "\nself check plugins info : \n" 41 | f"| llm | {self.llm_enabled} |\n" 42 | f"| rag | {self.rag_enabled} |\n" 43 | f"| tts | {self.tts_server_enabled} |\n" 44 | f"| digital hunam | {self.digital_human_server_enabled} |\n" 45 | f"| asr | {self.asr_server_enabled} |\n" 46 | f"| agent | {self.agent_enabled} |\n" 47 | ) 48 | 49 | @staticmethod 50 | def _check_server(url): 51 | 52 | try: 53 | res = requests.get(url) 54 | except requests.exceptions.ConnectionError: 55 | return False 56 | 57 | if res.status_code == 200: 58 | return True 59 | else: 60 | return False 61 | 62 | @staticmethod 63 | def _make_color_list(color_num): 64 | 65 | color_list = [ 66 | "#FF3838", 67 | "#FF9D97", 68 | "#FF701F", 69 | "#FFB21D", 70 | "#CFD231", 71 | "#48F90A", 72 | "#92CC17", 73 | "#3DDB86", 74 | "#1A9334", 75 | "#00D4BB", 76 | "#2C99A8", 77 | "#00C2FF", 78 | "#344593", 79 | "#6473FF", 80 | "#0018EC", 81 | "#8438FF", 82 | "#520085", 83 | "#CB38FF", 84 | "#FF95C8", 85 | "#FF37C7", 86 | ] 87 | 88 | return random.sample(color_list, color_num) 89 | 90 | def get_status(self): 91 | self.update_info() 92 | 93 | info_list = [ 94 | { 95 | "plugin_name": "LLM", 96 | "describe": "大语言模型,用于根据客户历史对话,生成对话信息", 97 | "enabled": self.llm_enabled, 98 | }, 99 | { 100 | "plugin_name": "RAG", 101 | "describe": "用于调用知识库实时更新信息", 102 | "enabled": self.rag_enabled, 103 | }, 104 | { 105 | "plugin_name": "TTS", 106 | "describe": "文字转语音,让主播的文字也能听到", 107 | "enabled": self.tts_server_enabled, 108 | }, 109 | { 110 | "plugin_name": "数字人", 111 | "describe": "数字人服务,用于生成数字人,需要和 TTS 一起开启才有效果", 112 | "enabled": self.digital_human_server_enabled, 113 | }, 114 | { 115 | "plugin_name": "Agent", 116 | "describe": "用于根据用户对话,获取网络的实时信息", 117 | "enabled": self.agent_enabled, 118 | }, 119 | { 120 | "plugin_name": "ASR", 121 | "describe": "语音转文字,让用户无需打字就可以和主播进行对话", 122 | "enabled": self.asr_server_enabled, 123 | }, 124 | ] 125 | 126 | # 生成图标背景色 127 | color_list = self._make_color_list(len(info_list)) 128 | for idx, color in enumerate(color_list): 129 | info_list[idx].update({"avatar_color": color}) 130 | 131 | return info_list 132 | 133 | 134 | SERVER_PLUGINS_INFO = ServerPluginsInfo() 135 | -------------------------------------------------------------------------------- /server/digital_human/digital_human_server.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.exceptions import RequestValidationError 3 | from fastapi.responses import PlainTextResponse 4 | from loguru import logger 5 | from pydantic import BaseModel 6 | 7 | 8 | from .modules.digital_human_worker import gen_digital_human_video_app, preprocess_digital_human_app 9 | 10 | 11 | app = FastAPI() 12 | 13 | 14 | class DigitalHumanItem(BaseModel): 15 | user_id: str # User 识别号,用于区分不用的用户调用 16 | request_id: str # 请求 ID,用于生成 TTS & 数字人 17 | streamer_id: str # 数字人 ID 18 | tts_path: str = "" # 文本 19 | chunk_id: int = 0 # 句子 ID 20 | 21 | 22 | class DigitalHumanPreprocessItem(BaseModel): 23 | user_id: str # User 识别号,用于区分不用的用户调用 24 | request_id: str # 请求 ID,用于生成 TTS & 数字人 25 | streamer_id: str # 数字人 ID 26 | video_path: str # 数字人视频 27 | 28 | 29 | @app.post("/digital_human/gen") 30 | async def get_digital_human(dg_item: DigitalHumanItem): 31 | """生成数字人视频""" 32 | save_tag = ( 33 | dg_item.request_id + ".mp4" if dg_item.chunk_id == 0 else dg_item.request_id + f"-{str(dg_item.chunk_id).zfill(8)}.mp4" 34 | ) 35 | mp4_path = await gen_digital_human_video_app(dg_item.streamer_id, dg_item.tts_path, save_tag) 36 | logger.info(f"digital human mp4 path = {mp4_path}") 37 | return {"user_id": dg_item.user_id, "request_id": dg_item.request_id, "digital_human_mp4_path": mp4_path} 38 | 39 | 40 | @app.post("/digital_human/preprocess") 41 | async def preprocess_digital_human(preprocess_item: DigitalHumanPreprocessItem): 42 | """数字人视频预处理,用于新增数字人""" 43 | 44 | _ = await preprocess_digital_human_app(str(preprocess_item.streamer_id), preprocess_item.video_path) 45 | 46 | logger.info(f"digital human process for {preprocess_item.streamer_id} done") 47 | return {"user_id": preprocess_item.user_id, "request_id": preprocess_item.request_id} 48 | 49 | 50 | @app.exception_handler(RequestValidationError) 51 | async def validation_exception_handler(request, exc): 52 | """调 API 入参错误的回调接口 53 | 54 | Args: 55 | request (_type_): _description_ 56 | exc (_type_): _description_ 57 | 58 | Returns: 59 | _type_: _description_ 60 | """ 61 | logger.info(request) 62 | logger.info(exc) 63 | return PlainTextResponse(str(exc), status_code=400) 64 | 65 | 66 | @app.get("/digital_human/check") 67 | async def check_server(): 68 | return {"message": "server enabled"} 69 | -------------------------------------------------------------------------------- /server/digital_human/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from torch import hub 2 | from ...web_configs import WEB_CONFIGS 3 | from pathlib import Path 4 | 5 | # 部分模型会使用 torch download 下载,需要设置路径 6 | hub.set_dir(str(Path(WEB_CONFIGS.DIGITAL_HUMAN_MODEL_DIR).joinpath("face-alignment"))) 7 | -------------------------------------------------------------------------------- /server/digital_human/modules/digital_human_worker.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from .realtime_inference import DIGITAL_HUMAN_HANDLER, gen_digital_human_preprocess, gen_digital_human_video 3 | from ...web_configs import WEB_CONFIGS 4 | 5 | 6 | async def gen_digital_human_video_app(stream_id, audio_path, save_tag): 7 | if DIGITAL_HUMAN_HANDLER is None: 8 | return None 9 | 10 | save_path = gen_digital_human_video( 11 | DIGITAL_HUMAN_HANDLER, 12 | stream_id, 13 | audio_path, 14 | work_dir=str(Path(WEB_CONFIGS.DIGITAL_HUMAN_VIDEO_OUTPUT_PATH).absolute()), 15 | video_path=save_tag, 16 | fps=DIGITAL_HUMAN_HANDLER.fps, 17 | ) 18 | 19 | return save_path 20 | 21 | 22 | async def preprocess_digital_human_app(stream_id, video_path): 23 | if DIGITAL_HUMAN_HANDLER is None: 24 | return None 25 | 26 | res = gen_digital_human_preprocess( 27 | DIGITAL_HUMAN_HANDLER, 28 | stream_id, 29 | work_dir=str(Path(WEB_CONFIGS.DIGITAL_HUMAN_VIDEO_OUTPUT_PATH).absolute()), 30 | video_path=video_path, 31 | ) 32 | 33 | return res 34 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/models/unet.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import math 4 | import json 5 | 6 | from diffusers import UNet2DConditionModel 7 | 8 | class PositionalEncoding(nn.Module): 9 | def __init__(self, d_model=384, max_len=5000): 10 | super(PositionalEncoding, self).__init__() 11 | pe = torch.zeros(max_len, d_model) 12 | position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) 13 | div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) 14 | pe[:, 0::2] = torch.sin(position * div_term) 15 | pe[:, 1::2] = torch.cos(position * div_term) 16 | pe = pe.unsqueeze(0) 17 | self.register_buffer('pe', pe) 18 | 19 | def forward(self, x): 20 | b, seq_len, d_model = x.size() 21 | pe = self.pe[:, :seq_len, :] 22 | x = x + pe.to(x.device) 23 | return x 24 | 25 | class UNet(): 26 | def __init__(self, 27 | unet_config, 28 | model_path, 29 | use_float16=False, 30 | ): 31 | with open(unet_config, 'r') as f: 32 | unet_config = json.load(f) 33 | self.model = UNet2DConditionModel(**unet_config) 34 | self.pe = PositionalEncoding(d_model=384) 35 | self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 36 | weights = torch.load(model_path) if torch.cuda.is_available() else torch.load(model_path, map_location=self.device) 37 | self.model.load_state_dict(weights) 38 | if use_float16: 39 | self.model = self.model.half() 40 | self.model.to(self.device) 41 | 42 | if __name__ == "__main__": 43 | unet = UNet() 44 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os.path import abspath, dirname 3 | current_dir = dirname(abspath(__file__)) 4 | parent_dir = dirname(current_dir) 5 | sys.path.append(parent_dir+'/utils') 6 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/blending.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import numpy as np 3 | import cv2 4 | from face_parsing import FaceParsing 5 | 6 | 7 | def init_face_parsing_model( 8 | resnet_path="./models/face-parse-bisent/resnet18-5c106cde.pth", face_model_pth="./models/face-parse-bisent/79999_iter.pth" 9 | ): 10 | fp_model = FaceParsing(resnet_path, face_model_pth) 11 | return fp_model 12 | 13 | 14 | def get_crop_box(box, expand): 15 | x, y, x1, y1 = box 16 | x_c, y_c = (x + x1) // 2, (y + y1) // 2 17 | w, h = x1 - x, y1 - y 18 | s = int(max(w, h) // 2 * expand) 19 | crop_box = [x_c - s, y_c - s, x_c + s, y_c + s] 20 | return crop_box, s 21 | 22 | 23 | def face_seg(image, fp_model): 24 | seg_image = fp_model(image) 25 | if seg_image is None: 26 | print("error, no person_segment") 27 | return None 28 | 29 | seg_image = seg_image.resize(image.size) 30 | return seg_image 31 | 32 | 33 | def get_image(image, face, face_box, fp_model, upper_boundary_ratio=0.5, expand=1.2): 34 | # print(image.shape) 35 | # print(face.shape) 36 | 37 | body = Image.fromarray(image[:, :, ::-1]) 38 | face = Image.fromarray(face[:, :, ::-1]) 39 | 40 | x, y, x1, y1 = face_box 41 | # print(x1-x,y1-y) 42 | crop_box, s = get_crop_box(face_box, expand) 43 | x_s, y_s, x_e, y_e = crop_box 44 | face_position = (x, y) 45 | 46 | face_large = body.crop(crop_box) 47 | ori_shape = face_large.size 48 | 49 | mask_image = face_seg(face_large, fp_model) 50 | mask_small = mask_image.crop((x - x_s, y - y_s, x1 - x_s, y1 - y_s)) 51 | mask_image = Image.new("L", ori_shape, 0) 52 | mask_image.paste(mask_small, (x - x_s, y - y_s, x1 - x_s, y1 - y_s)) 53 | 54 | # keep upper_boundary_ratio of talking area 55 | width, height = mask_image.size 56 | top_boundary = int(height * upper_boundary_ratio) 57 | modified_mask_image = Image.new("L", ori_shape, 0) 58 | modified_mask_image.paste(mask_image.crop((0, top_boundary, width, height)), (0, top_boundary)) 59 | 60 | blur_kernel_size = int(0.1 * ori_shape[0] // 2 * 2) + 1 61 | mask_array = cv2.GaussianBlur(np.array(modified_mask_image), (blur_kernel_size, blur_kernel_size), 0) 62 | mask_image = Image.fromarray(mask_array) 63 | 64 | face_large.paste(face, (x - x_s, y - y_s, x1 - x_s, y1 - y_s)) 65 | body.paste(face_large, crop_box[:2], mask_image) 66 | body = np.array(body) 67 | return body[:, :, ::-1] 68 | 69 | 70 | def get_image_prepare_material(image, face_box, fp_model, upper_boundary_ratio=0.5, expand=1.2): 71 | body = Image.fromarray(image[:, :, ::-1]) 72 | 73 | x, y, x1, y1 = face_box 74 | # print(x1-x,y1-y) 75 | crop_box, s = get_crop_box(face_box, expand) 76 | x_s, y_s, x_e, y_e = crop_box 77 | 78 | face_large = body.crop(crop_box) 79 | ori_shape = face_large.size 80 | 81 | mask_image = face_seg(face_large, fp_model) 82 | mask_small = mask_image.crop((x - x_s, y - y_s, x1 - x_s, y1 - y_s)) 83 | mask_image = Image.new("L", ori_shape, 0) 84 | mask_image.paste(mask_small, (x - x_s, y - y_s, x1 - x_s, y1 - y_s)) 85 | 86 | # keep upper_boundary_ratio of talking area 87 | width, height = mask_image.size 88 | top_boundary = int(height * upper_boundary_ratio) 89 | modified_mask_image = Image.new("L", ori_shape, 0) 90 | modified_mask_image.paste(mask_image.crop((0, top_boundary, width, height)), (0, top_boundary)) 91 | 92 | blur_kernel_size = int(0.1 * ori_shape[0] // 2 * 2) + 1 93 | mask_array = cv2.GaussianBlur(np.array(modified_mask_image), (blur_kernel_size, blur_kernel_size), 0) 94 | return mask_array, crop_box 95 | 96 | 97 | def get_image_blending(image, face, face_box, mask_array, crop_box): 98 | body = Image.fromarray(image[:, :, ::-1]) 99 | face = Image.fromarray(face[:, :, ::-1]) 100 | 101 | x, y, x1, y1 = face_box 102 | x_s, y_s, x_e, y_e = crop_box 103 | face_large = body.crop(crop_box) 104 | 105 | mask_image = Image.fromarray(mask_array) 106 | mask_image = mask_image.convert("L") 107 | face_large.paste(face, (x - x_s, y - y_s, x1 - x_s, y1 - y_s)) 108 | body.paste(face_large, crop_box[:2], mask_image) 109 | body = np.array(body) 110 | return body[:, :, ::-1] 111 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/dwpose/default_runtime.py: -------------------------------------------------------------------------------- 1 | default_scope = 'mmpose' 2 | 3 | # hooks 4 | default_hooks = dict( 5 | timer=dict(type='IterTimerHook'), 6 | logger=dict(type='LoggerHook', interval=50), 7 | param_scheduler=dict(type='ParamSchedulerHook'), 8 | checkpoint=dict(type='CheckpointHook', interval=10), 9 | sampler_seed=dict(type='DistSamplerSeedHook'), 10 | visualization=dict(type='PoseVisualizationHook', enable=False), 11 | badcase=dict( 12 | type='BadCaseAnalysisHook', 13 | enable=False, 14 | out_dir='badcase', 15 | metric_type='loss', 16 | badcase_thr=5)) 17 | 18 | # custom hooks 19 | custom_hooks = [ 20 | # Synchronize model buffers such as running_mean and running_var in BN 21 | # at the end of each epoch 22 | dict(type='SyncBuffersHook') 23 | ] 24 | 25 | # multi-processing backend 26 | env_cfg = dict( 27 | cudnn_benchmark=False, 28 | mp_cfg=dict(mp_start_method='fork', opencv_num_threads=0), 29 | dist_cfg=dict(backend='nccl'), 30 | ) 31 | 32 | # visualizer 33 | vis_backends = [ 34 | dict(type='LocalVisBackend'), 35 | # dict(type='TensorboardVisBackend'), 36 | # dict(type='WandbVisBackend'), 37 | ] 38 | visualizer = dict( 39 | type='PoseLocalVisualizer', vis_backends=vis_backends, name='visualizer') 40 | 41 | # logger 42 | log_processor = dict( 43 | type='LogProcessor', window_size=50, by_epoch=True, num_digits=6) 44 | log_level = 'INFO' 45 | load_from = None 46 | resume = False 47 | 48 | # file I/O backend 49 | backend_args = dict(backend='local') 50 | 51 | # training/validation/testing progress 52 | train_cfg = dict(by_epoch=True) 53 | val_cfg = dict() 54 | test_cfg = dict() 55 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/face_detection/README.md: -------------------------------------------------------------------------------- 1 | The code for Face Detection in this folder has been taken from the wonderful [face_alignment](https://github.com/1adrianb/face-alignment) repository. This has been modified to take batches of faces at a time. -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/face_detection/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = """Adrian Bulat""" 4 | __email__ = 'adrian.bulat@nottingham.ac.uk' 5 | __version__ = '1.0.1' 6 | 7 | from .api import FaceAlignment, LandmarksType, NetworkSize, YOLOv8_face 8 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/face_detection/detection/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import FaceDetector -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/face_detection/detection/sfd/__init__.py: -------------------------------------------------------------------------------- 1 | from .sfd_detector import SFDDetector as FaceDetector -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/face_detection/detection/sfd/detect.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | 4 | import cv2 5 | import numpy as np 6 | 7 | from .net_s3fd import s3fd 8 | from .bbox import * 9 | 10 | 11 | def detect(net, img, device): 12 | img = img - np.array([104, 117, 123]) 13 | img = img.transpose(2, 0, 1) 14 | img = img.reshape((1,) + img.shape) 15 | 16 | if 'cuda' in device: 17 | torch.backends.cudnn.benchmark = True 18 | 19 | img = torch.from_numpy(img).float().to(device) 20 | BB, CC, HH, WW = img.size() 21 | with torch.no_grad(): 22 | olist = net(img) 23 | 24 | bboxlist = [] 25 | for i in range(len(olist) // 2): 26 | olist[i * 2] = F.softmax(olist[i * 2], dim=1) 27 | olist = [oelem.data.cpu() for oelem in olist] 28 | for i in range(len(olist) // 2): 29 | ocls, oreg = olist[i * 2], olist[i * 2 + 1] 30 | FB, FC, FH, FW = ocls.size() # feature map size 31 | stride = 2**(i + 2) # 4,8,16,32,64,128 32 | anchor = stride * 4 33 | poss = zip(*np.where(ocls[:, 1, :, :] > 0.05)) 34 | for Iindex, hindex, windex in poss: 35 | axc, ayc = stride / 2 + windex * stride, stride / 2 + hindex * stride 36 | score = ocls[0, 1, hindex, windex] 37 | loc = oreg[0, :, hindex, windex].contiguous().view(1, 4) 38 | priors = torch.Tensor([[axc / 1.0, ayc / 1.0, stride * 4 / 1.0, stride * 4 / 1.0]]) 39 | variances = [0.1, 0.2] 40 | box = decode(loc, priors, variances) 41 | x1, y1, x2, y2 = box[0] * 1.0 42 | # cv2.rectangle(imgshow,(int(x1),int(y1)),(int(x2),int(y2)),(0,0,255),1) 43 | bboxlist.append([x1, y1, x2, y2, score]) 44 | bboxlist = np.array(bboxlist) 45 | if 0 == len(bboxlist): 46 | bboxlist = np.zeros((1, 5)) 47 | 48 | return bboxlist 49 | 50 | def batch_detect(net, imgs, device): 51 | imgs = imgs - np.array([104, 117, 123]) 52 | imgs = imgs.transpose(0, 3, 1, 2) 53 | 54 | if 'cuda' in device: 55 | torch.backends.cudnn.benchmark = True 56 | 57 | imgs = torch.from_numpy(imgs).float().to(device) 58 | BB, CC, HH, WW = imgs.size() 59 | with torch.no_grad(): 60 | olist = net(imgs) 61 | # print(olist) 62 | 63 | bboxlist = [] 64 | for i in range(len(olist) // 2): 65 | olist[i * 2] = F.softmax(olist[i * 2], dim=1) 66 | 67 | olist = [oelem.cpu() for oelem in olist] 68 | for i in range(len(olist) // 2): 69 | ocls, oreg = olist[i * 2], olist[i * 2 + 1] 70 | FB, FC, FH, FW = ocls.size() # feature map size 71 | stride = 2**(i + 2) # 4,8,16,32,64,128 72 | anchor = stride * 4 73 | poss = zip(*np.where(ocls[:, 1, :, :] > 0.05)) 74 | for Iindex, hindex, windex in poss: 75 | axc, ayc = stride / 2 + windex * stride, stride / 2 + hindex * stride 76 | score = ocls[:, 1, hindex, windex] 77 | loc = oreg[:, :, hindex, windex].contiguous().view(BB, 1, 4) 78 | priors = torch.Tensor([[axc / 1.0, ayc / 1.0, stride * 4 / 1.0, stride * 4 / 1.0]]).view(1, 1, 4) 79 | variances = [0.1, 0.2] 80 | box = batch_decode(loc, priors, variances) 81 | box = box[:, 0] * 1.0 82 | # cv2.rectangle(imgshow,(int(x1),int(y1)),(int(x2),int(y2)),(0,0,255),1) 83 | bboxlist.append(torch.cat([box, score.unsqueeze(1)], 1).cpu().numpy()) 84 | bboxlist = np.array(bboxlist) 85 | if 0 == len(bboxlist): 86 | bboxlist = np.zeros((1, BB, 5)) 87 | 88 | return bboxlist 89 | 90 | def flip_detect(net, img, device): 91 | img = cv2.flip(img, 1) 92 | b = detect(net, img, device) 93 | 94 | bboxlist = np.zeros(b.shape) 95 | bboxlist[:, 0] = img.shape[1] - b[:, 2] 96 | bboxlist[:, 1] = b[:, 1] 97 | bboxlist[:, 2] = img.shape[1] - b[:, 0] 98 | bboxlist[:, 3] = b[:, 3] 99 | bboxlist[:, 4] = b[:, 4] 100 | return bboxlist 101 | 102 | 103 | def pts_to_bb(pts): 104 | min_x, min_y = np.min(pts, axis=0) 105 | max_x, max_y = np.max(pts, axis=0) 106 | return np.array([min_x, min_y, max_x, max_y]) 107 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/face_detection/detection/sfd/sfd_detector.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | from torch.utils.model_zoo import load_url 4 | 5 | from ..core import FaceDetector 6 | 7 | from .net_s3fd import s3fd 8 | from .bbox import * 9 | from .detect import * 10 | 11 | models_urls = { 12 | 's3fd': 'https://www.adrianbulat.com/downloads/python-fan/s3fd-619a316812.pth', 13 | } 14 | 15 | 16 | class SFDDetector(FaceDetector): 17 | def __init__(self, device, path_to_detector=os.path.join(os.path.dirname(os.path.abspath(__file__)), 's3fd.pth'), verbose=False): 18 | super(SFDDetector, self).__init__(device, verbose) 19 | 20 | # Initialise the face detector 21 | if not os.path.isfile(path_to_detector): 22 | model_weights = load_url(models_urls['s3fd']) 23 | else: 24 | model_weights = torch.load(path_to_detector) 25 | 26 | self.face_detector = s3fd() 27 | self.face_detector.load_state_dict(model_weights) 28 | self.face_detector.to(device) 29 | self.face_detector.eval() 30 | 31 | def detect_from_image(self, tensor_or_path): 32 | image = self.tensor_or_path_to_ndarray(tensor_or_path) 33 | 34 | bboxlist = detect(self.face_detector, image, device=self.device) 35 | keep = nms(bboxlist, 0.3) 36 | bboxlist = bboxlist[keep, :] 37 | bboxlist = [x for x in bboxlist if x[-1] > 0.5] 38 | 39 | return bboxlist 40 | 41 | def detect_from_batch(self, images): 42 | bboxlists = batch_detect(self.face_detector, images, device=self.device) 43 | keeps = [nms(bboxlists[:, i, :], 0.3) for i in range(bboxlists.shape[1])] 44 | bboxlists = [bboxlists[keep, i, :] for i, keep in enumerate(keeps)] 45 | bboxlists = [[x for x in bboxlist if x[-1] > 0.5] for bboxlist in bboxlists] 46 | 47 | return bboxlists 48 | 49 | @property 50 | def reference_scale(self): 51 | return 195 52 | 53 | @property 54 | def reference_x_shift(self): 55 | return 0 56 | 57 | @property 58 | def reference_y_shift(self): 59 | return 0 60 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/face_parsing/__init__.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | import torchvision.transforms as transforms 4 | from PIL import Image 5 | 6 | from .model import BiSeNet 7 | 8 | 9 | class FaceParsing: 10 | def __init__(self, resnet_path, face_model_pth): 11 | self.resnet_path = resnet_path 12 | self.model_pth = face_model_pth 13 | 14 | self.net = self.model_init() 15 | self.preprocess = self.image_preprocess() 16 | 17 | def model_init(self): 18 | net = BiSeNet(self.resnet_path) 19 | if torch.cuda.is_available(): 20 | net.cuda() 21 | net.load_state_dict(torch.load(self.model_pth)) 22 | else: 23 | net.load_state_dict(torch.load(self.model_pth, map_location=torch.device("cpu"))) 24 | net.eval() 25 | return net 26 | 27 | def image_preprocess(self): 28 | return transforms.Compose( 29 | [ 30 | transforms.ToTensor(), 31 | transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)), 32 | ] 33 | ) 34 | 35 | def __call__(self, image, size=(512, 512)): 36 | if isinstance(image, str): 37 | image = Image.open(image) 38 | 39 | width, height = image.size 40 | with torch.no_grad(): 41 | image = image.resize(size, Image.BILINEAR) 42 | img = self.preprocess(image) 43 | if torch.cuda.is_available(): 44 | img = torch.unsqueeze(img, 0).cuda() 45 | else: 46 | img = torch.unsqueeze(img, 0) 47 | out = self.net(img)[0] 48 | parsing = out.squeeze(0).cpu().numpy().argmax(0) 49 | parsing[np.where(parsing > 13)] = 0 50 | parsing[np.where(parsing >= 1)] = 255 51 | parsing = Image.fromarray(parsing.astype(np.uint8)) 52 | return parsing 53 | 54 | 55 | if __name__ == "__main__": 56 | fp = FaceParsing() 57 | segmap = fp("154_small.png") 58 | segmap.save("res.png") 59 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/face_parsing/resnet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- encoding: utf-8 -*- 3 | 4 | import torch 5 | import torch.nn as nn 6 | import torch.nn.functional as F 7 | import torch.utils.model_zoo as modelzoo 8 | 9 | # from modules.bn import InPlaceABNSync as BatchNorm2d 10 | 11 | resnet18_url = 'https://download.pytorch.org/models/resnet18-5c106cde.pth' 12 | 13 | 14 | def conv3x3(in_planes, out_planes, stride=1): 15 | """3x3 convolution with padding""" 16 | return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, 17 | padding=1, bias=False) 18 | 19 | 20 | class BasicBlock(nn.Module): 21 | def __init__(self, in_chan, out_chan, stride=1): 22 | super(BasicBlock, self).__init__() 23 | self.conv1 = conv3x3(in_chan, out_chan, stride) 24 | self.bn1 = nn.BatchNorm2d(out_chan) 25 | self.conv2 = conv3x3(out_chan, out_chan) 26 | self.bn2 = nn.BatchNorm2d(out_chan) 27 | self.relu = nn.ReLU(inplace=True) 28 | self.downsample = None 29 | if in_chan != out_chan or stride != 1: 30 | self.downsample = nn.Sequential( 31 | nn.Conv2d(in_chan, out_chan, 32 | kernel_size=1, stride=stride, bias=False), 33 | nn.BatchNorm2d(out_chan), 34 | ) 35 | 36 | def forward(self, x): 37 | residual = self.conv1(x) 38 | residual = F.relu(self.bn1(residual)) 39 | residual = self.conv2(residual) 40 | residual = self.bn2(residual) 41 | 42 | shortcut = x 43 | if self.downsample is not None: 44 | shortcut = self.downsample(x) 45 | 46 | out = shortcut + residual 47 | out = self.relu(out) 48 | return out 49 | 50 | 51 | def create_layer_basic(in_chan, out_chan, bnum, stride=1): 52 | layers = [BasicBlock(in_chan, out_chan, stride=stride)] 53 | for i in range(bnum-1): 54 | layers.append(BasicBlock(out_chan, out_chan, stride=1)) 55 | return nn.Sequential(*layers) 56 | 57 | 58 | class Resnet18(nn.Module): 59 | def __init__(self, model_path): 60 | super(Resnet18, self).__init__() 61 | self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, 62 | bias=False) 63 | self.bn1 = nn.BatchNorm2d(64) 64 | self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) 65 | self.layer1 = create_layer_basic(64, 64, bnum=2, stride=1) 66 | self.layer2 = create_layer_basic(64, 128, bnum=2, stride=2) 67 | self.layer3 = create_layer_basic(128, 256, bnum=2, stride=2) 68 | self.layer4 = create_layer_basic(256, 512, bnum=2, stride=2) 69 | self.init_weight(model_path) 70 | 71 | def forward(self, x): 72 | x = self.conv1(x) 73 | x = F.relu(self.bn1(x)) 74 | x = self.maxpool(x) 75 | 76 | x = self.layer1(x) 77 | feat8 = self.layer2(x) # 1/8 78 | feat16 = self.layer3(feat8) # 1/16 79 | feat32 = self.layer4(feat16) # 1/32 80 | return feat8, feat16, feat32 81 | 82 | def init_weight(self, model_path): 83 | state_dict = torch.load(model_path) #modelzoo.load_url(resnet18_url) 84 | self_state_dict = self.state_dict() 85 | for k, v in state_dict.items(): 86 | if 'fc' in k: continue 87 | self_state_dict.update({k: v}) 88 | self.load_state_dict(self_state_dict) 89 | 90 | def get_params(self): 91 | wd_params, nowd_params = [], [] 92 | for name, module in self.named_modules(): 93 | if isinstance(module, (nn.Linear, nn.Conv2d)): 94 | wd_params.append(module.weight) 95 | if not module.bias is None: 96 | nowd_params.append(module.bias) 97 | elif isinstance(module, nn.BatchNorm2d): 98 | nowd_params += list(module.parameters()) 99 | return wd_params, nowd_params 100 | 101 | 102 | if __name__ == "__main__": 103 | net = Resnet18() 104 | x = torch.randn(16, 3, 224, 224) 105 | out = net(x) 106 | print(out[0].size()) 107 | print(out[1].size()) 108 | print(out[2].size()) 109 | net.get_params() 110 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/utils/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import numpy as np 4 | import torch 5 | 6 | # ffmpeg_path = os.getenv('FFMPEG_PATH') 7 | # if ffmpeg_path is None: 8 | # print("please download ffmpeg-static and export to FFMPEG_PATH. \nFor example: export FFMPEG_PATH=/musetalk/ffmpeg-4.4-amd64-static") 9 | # elif ffmpeg_path not in os.getenv('PATH'): 10 | # print("add ffmpeg to path") 11 | # os.environ["PATH"] = f"{ffmpeg_path}:{os.environ['PATH']}" 12 | 13 | 14 | from ..whisper.audio2feature import Audio2Feature 15 | from ..models.vae import VAE 16 | from ..models.unet import UNet,PositionalEncoding 17 | 18 | def load_all_model(audio2feature_model_path, vae_model_path, unet_model_dict): 19 | audio_processor = Audio2Feature(model_path=audio2feature_model_path) 20 | vae = VAE(model_path =vae_model_path) 21 | unet = UNet(unet_config=unet_model_dict['unet_config'], 22 | model_path =unet_model_dict['model_path']) 23 | pe = PositionalEncoding(d_model=384) 24 | return audio_processor,vae,unet,pe 25 | 26 | def get_file_type(video_path): 27 | _, ext = os.path.splitext(video_path) 28 | 29 | if ext.lower() in ['.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff']: 30 | return 'image' 31 | elif ext.lower() in ['.avi', '.mp4', '.mov', '.flv', '.mkv']: 32 | return 'video' 33 | else: 34 | return 'unsupported' 35 | 36 | def get_video_fps(video_path): 37 | video = cv2.VideoCapture(video_path) 38 | fps = video.get(cv2.CAP_PROP_FPS) 39 | video.release() 40 | return fps 41 | 42 | def datagen(whisper_chunks, 43 | vae_encode_latents, 44 | batch_size=8, 45 | delay_frame=0): 46 | whisper_batch, latent_batch = [], [] 47 | for i, w in enumerate(whisper_chunks): 48 | idx = (i+delay_frame)%len(vae_encode_latents) 49 | latent = vae_encode_latents[idx] 50 | whisper_batch.append(w) 51 | latent_batch.append(latent) 52 | 53 | if len(latent_batch) >= batch_size: 54 | whisper_batch = np.stack(whisper_batch) 55 | latent_batch = torch.cat(latent_batch, dim=0) 56 | yield whisper_batch, latent_batch 57 | whisper_batch, latent_batch = [], [] 58 | 59 | # the last batch may smaller than batch size 60 | if len(latent_batch) > 0: 61 | whisper_batch = np.stack(whisper_batch) 62 | latent_batch = torch.cat(latent_batch, dim=0) 63 | 64 | yield whisper_batch, latent_batch 65 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/__main__.py: -------------------------------------------------------------------------------- 1 | from .transcribe import cli 2 | 3 | 4 | cli() 5 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/assets/gpt2/special_tokens_map.json: -------------------------------------------------------------------------------- 1 | {"bos_token": "<|endoftext|>", "eos_token": "<|endoftext|>", "unk_token": "<|endoftext|>"} -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/assets/gpt2/tokenizer_config.json: -------------------------------------------------------------------------------- 1 | {"unk_token": "<|endoftext|>", "bos_token": "<|endoftext|>", "eos_token": "<|endoftext|>", "add_prefix_space": false, "model_max_length": 1024, "special_tokens_map_file": null, "name_or_path": "gpt2", "tokenizer_class": "GPT2Tokenizer"} -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/assets/mel_filters.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/digital_human/modules/musetalk/whisper/whisper/assets/mel_filters.npz -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/assets/multilingual/added_tokens.json: -------------------------------------------------------------------------------- 1 | {"<|endoftext|>": 50257} 2 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/assets/multilingual/special_tokens_map.json: -------------------------------------------------------------------------------- 1 | {"bos_token": "<|endoftext|>", "eos_token": "<|endoftext|>", "unk_token": "<|endoftext|>"} -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/assets/multilingual/tokenizer_config.json: -------------------------------------------------------------------------------- 1 | {"unk_token": {"content": "<|endoftext|>", "single_word": false, "lstrip": false, "rstrip": false, "normalized": true, "__type": "AddedToken"}, "bos_token": {"content": "<|endoftext|>", "single_word": false, "lstrip": false, "rstrip": false, "normalized": true, "__type": "AddedToken"}, "eos_token": {"content": "<|endoftext|>", "single_word": false, "lstrip": false, "rstrip": false, "normalized": true, "__type": "AddedToken"}, "add_prefix_space": false, "model_max_length": 1024, "special_tokens_map_file": null, "name_or_path": "multilingual", "errors": "replace", "tokenizer_class": "GPT2Tokenizer"} -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/normalizers/__init__.py: -------------------------------------------------------------------------------- 1 | from .basic import BasicTextNormalizer 2 | from .english import EnglishTextNormalizer 3 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/normalizers/basic.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unicodedata 3 | 4 | import regex 5 | 6 | # non-ASCII letters that are not separated by "NFKD" normalization 7 | ADDITIONAL_DIACRITICS = { 8 | "œ": "oe", 9 | "Œ": "OE", 10 | "ø": "o", 11 | "Ø": "O", 12 | "æ": "ae", 13 | "Æ": "AE", 14 | "ß": "ss", 15 | "ẞ": "SS", 16 | "đ": "d", 17 | "Đ": "D", 18 | "ð": "d", 19 | "Ð": "D", 20 | "þ": "th", 21 | "Þ": "th", 22 | "ł": "l", 23 | "Ł": "L", 24 | } 25 | 26 | 27 | def remove_symbols_and_diacritics(s: str, keep=""): 28 | """ 29 | Replace any other markers, symbols, and punctuations with a space, 30 | and drop any diacritics (category 'Mn' and some manual mappings) 31 | """ 32 | return "".join( 33 | c 34 | if c in keep 35 | else ADDITIONAL_DIACRITICS[c] 36 | if c in ADDITIONAL_DIACRITICS 37 | else "" 38 | if unicodedata.category(c) == "Mn" 39 | else " " 40 | if unicodedata.category(c)[0] in "MSP" 41 | else c 42 | for c in unicodedata.normalize("NFKD", s) 43 | ) 44 | 45 | 46 | def remove_symbols(s: str): 47 | """ 48 | Replace any other markers, symbols, punctuations with a space, keeping diacritics 49 | """ 50 | return "".join( 51 | " " if unicodedata.category(c)[0] in "MSP" else c for c in unicodedata.normalize("NFKC", s) 52 | ) 53 | 54 | 55 | class BasicTextNormalizer: 56 | def __init__(self, remove_diacritics: bool = False, split_letters: bool = False): 57 | self.clean = remove_symbols_and_diacritics if remove_diacritics else remove_symbols 58 | self.split_letters = split_letters 59 | 60 | def __call__(self, s: str): 61 | s = s.lower() 62 | s = re.sub(r"[<\[][^>\]]*[>\]]", "", s) # remove words between brackets 63 | s = re.sub(r"\(([^)]+?)\)", "", s) # remove words between parenthesis 64 | s = self.clean(s).lower() 65 | 66 | if self.split_letters: 67 | s = " ".join(regex.findall(r"\X", s, regex.U)) 68 | 69 | s = re.sub(r"\s+", " ", s) # replace any successive whitespace characters with a space 70 | 71 | return s 72 | -------------------------------------------------------------------------------- /server/digital_human/modules/musetalk/whisper/whisper/utils.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | from typing import Iterator, TextIO 3 | 4 | 5 | def exact_div(x, y): 6 | assert x % y == 0 7 | return x // y 8 | 9 | 10 | def str2bool(string): 11 | str2val = {"True": True, "False": False} 12 | if string in str2val: 13 | return str2val[string] 14 | else: 15 | raise ValueError(f"Expected one of {set(str2val.keys())}, got {string}") 16 | 17 | 18 | def optional_int(string): 19 | return None if string == "None" else int(string) 20 | 21 | 22 | def optional_float(string): 23 | return None if string == "None" else float(string) 24 | 25 | 26 | def compression_ratio(text) -> float: 27 | return len(text) / len(zlib.compress(text.encode("utf-8"))) 28 | 29 | 30 | def format_timestamp(seconds: float, always_include_hours: bool = False, decimal_marker: str = '.'): 31 | assert seconds >= 0, "non-negative timestamp expected" 32 | milliseconds = round(seconds * 1000.0) 33 | 34 | hours = milliseconds // 3_600_000 35 | milliseconds -= hours * 3_600_000 36 | 37 | minutes = milliseconds // 60_000 38 | milliseconds -= minutes * 60_000 39 | 40 | seconds = milliseconds // 1_000 41 | milliseconds -= seconds * 1_000 42 | 43 | hours_marker = f"{hours:02d}:" if always_include_hours or hours > 0 else "" 44 | return f"{hours_marker}{minutes:02d}:{seconds:02d}{decimal_marker}{milliseconds:03d}" 45 | 46 | 47 | def write_txt(transcript: Iterator[dict], file: TextIO): 48 | for segment in transcript: 49 | print(segment['text'].strip(), file=file, flush=True) 50 | 51 | 52 | def write_vtt(transcript: Iterator[dict], file: TextIO): 53 | print("WEBVTT\n", file=file) 54 | for segment in transcript: 55 | print( 56 | f"{format_timestamp(segment['start'])} --> {format_timestamp(segment['end'])}\n" 57 | f"{segment['text'].strip().replace('-->', '->')}\n", 58 | file=file, 59 | flush=True, 60 | ) 61 | 62 | 63 | def write_srt(transcript: Iterator[dict], file: TextIO): 64 | """ 65 | Write a transcript to a file in SRT format. 66 | 67 | Example usage: 68 | from pathlib import Path 69 | from whisper.utils import write_srt 70 | 71 | result = transcribe(model, audio_path, temperature=temperature, **args) 72 | 73 | # save SRT 74 | audio_basename = Path(audio_path).stem 75 | with open(Path(output_dir) / (audio_basename + ".srt"), "w", encoding="utf-8") as srt: 76 | write_srt(result["segments"], file=srt) 77 | """ 78 | for i, segment in enumerate(transcript, start=1): 79 | # write srt lines 80 | print( 81 | f"{i}\n" 82 | f"{format_timestamp(segment['start'], always_include_hours=True, decimal_marker=',')} --> " 83 | f"{format_timestamp(segment['end'], always_include_hours=True, decimal_marker=',')}\n" 84 | f"{segment['text'].strip().replace('-->', '->')}\n", 85 | file=file, 86 | flush=True, 87 | ) 88 | -------------------------------------------------------------------------------- /server/tts/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/tts/modules/__init__.py -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/AR/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/tts/modules/gpt_sovits/AR/__init__.py -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/AR/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/tts/modules/gpt_sovits/AR/models/__init__.py -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/AR/models/t2s_lightning_module.py: -------------------------------------------------------------------------------- 1 | # modified from https://github.com/yangdongchao/SoundStorm/blob/master/soundstorm/s1/AR/models/t2s_lightning_module.py 2 | # reference: https://github.com/lifeiteng/vall-e 3 | import os 4 | import sys 5 | 6 | now_dir = os.getcwd() 7 | sys.path.append(now_dir) 8 | from typing import Dict 9 | 10 | import torch 11 | from pytorch_lightning import LightningModule 12 | 13 | from .t2s_model import Text2SemanticDecoder 14 | from ..modules.lr_schedulers import WarmupCosineLRSchedule 15 | from ..modules.optim import ScaledAdam 16 | 17 | 18 | class Text2SemanticLightningModule(LightningModule): 19 | def __init__(self, config, output_dir, is_train=True): 20 | super().__init__() 21 | self.config = config 22 | self.top_k = 3 23 | self.model = Text2SemanticDecoder(config=config, top_k=self.top_k) 24 | pretrained_s1 = config.get("pretrained_s1") 25 | if pretrained_s1 and is_train: 26 | # print(self.load_state_dict(torch.load(pretrained_s1,map_location="cpu")["state_dict"])) 27 | print(self.load_state_dict(torch.load(pretrained_s1, map_location="cpu")["weight"])) 28 | if is_train: 29 | self.automatic_optimization = False 30 | self.save_hyperparameters() 31 | self.eval_dir = output_dir / "eval" 32 | self.eval_dir.mkdir(parents=True, exist_ok=True) 33 | 34 | def training_step(self, batch: Dict, batch_idx: int): 35 | opt = self.optimizers() 36 | scheduler = self.lr_schedulers() 37 | forward = self.model.forward if self.config["train"].get("if_dpo", False) == True else self.model.forward_old 38 | loss, acc = forward( 39 | batch["phoneme_ids"], 40 | batch["phoneme_ids_len"], 41 | batch["semantic_ids"], 42 | batch["semantic_ids_len"], 43 | batch["bert_feature"], 44 | ) 45 | self.manual_backward(loss) 46 | if batch_idx > 0 and batch_idx % 4 == 0: 47 | opt.step() 48 | opt.zero_grad() 49 | scheduler.step() 50 | 51 | self.log( 52 | "total_loss", 53 | loss, 54 | on_step=True, 55 | on_epoch=True, 56 | prog_bar=True, 57 | sync_dist=True, 58 | ) 59 | self.log( 60 | "lr", 61 | scheduler.get_last_lr()[0], 62 | on_epoch=True, 63 | prog_bar=True, 64 | sync_dist=True, 65 | ) 66 | self.log( 67 | f"top_{self.top_k}_acc", 68 | acc, 69 | on_step=True, 70 | on_epoch=True, 71 | prog_bar=True, 72 | sync_dist=True, 73 | ) 74 | 75 | def validation_step(self, batch: Dict, batch_idx: int): 76 | return 77 | 78 | def configure_optimizers(self): 79 | model_parameters = self.model.parameters() 80 | parameters_names = [] 81 | parameters_names.append([name_param_pair[0] for name_param_pair in self.model.named_parameters()]) 82 | lm_opt = ScaledAdam( 83 | model_parameters, 84 | lr=0.01, 85 | betas=(0.9, 0.95), 86 | clipping_scale=2.0, 87 | parameters_names=parameters_names, 88 | show_dominant_parameters=False, 89 | clipping_update_period=1000, 90 | ) 91 | 92 | return { 93 | "optimizer": lm_opt, 94 | "lr_scheduler": { 95 | "scheduler": WarmupCosineLRSchedule( 96 | lm_opt, 97 | init_lr=self.config["optimizer"]["lr_init"], 98 | peak_lr=self.config["optimizer"]["lr"], 99 | end_lr=self.config["optimizer"]["lr_end"], 100 | warmup_steps=self.config["optimizer"]["warmup_steps"], 101 | total_steps=self.config["optimizer"]["decay_steps"], 102 | ) 103 | }, 104 | } 105 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/AR/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/tts/modules/gpt_sovits/AR/modules/__init__.py -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/AR/modules/embedding.py: -------------------------------------------------------------------------------- 1 | # modified from https://github.com/lifeiteng/vall-e/blob/main/valle/modules/embedding.py 2 | import math 3 | 4 | import torch 5 | from torch import nn 6 | 7 | 8 | class TokenEmbedding(nn.Module): 9 | def __init__( 10 | self, 11 | embedding_dim: int, 12 | vocab_size: int, 13 | dropout: float = 0.0, 14 | ): 15 | super().__init__() 16 | 17 | self.vocab_size = vocab_size 18 | self.embedding_dim = embedding_dim 19 | 20 | self.dropout = torch.nn.Dropout(p=dropout) 21 | self.word_embeddings = nn.Embedding(self.vocab_size, self.embedding_dim) 22 | 23 | @property 24 | def weight(self) -> torch.Tensor: 25 | return self.word_embeddings.weight 26 | 27 | def embedding(self, index: int) -> torch.Tensor: 28 | return self.word_embeddings.weight[index : index + 1] 29 | 30 | def forward(self, x: torch.Tensor): 31 | x = self.word_embeddings(x) 32 | x = self.dropout(x) 33 | return x 34 | 35 | 36 | class SinePositionalEmbedding(nn.Module): 37 | def __init__( 38 | self, 39 | embedding_dim: int, 40 | dropout: float = 0.0, 41 | scale: bool = False, 42 | alpha: bool = False, 43 | ): 44 | super().__init__() 45 | self.embedding_dim = embedding_dim 46 | self.x_scale = math.sqrt(embedding_dim) if scale else 1.0 47 | self.alpha = nn.Parameter(torch.ones(1), requires_grad=alpha) 48 | self.dropout = torch.nn.Dropout(p=dropout) 49 | 50 | self.reverse = False 51 | self.pe = None 52 | self.extend_pe(torch.tensor(0.0).expand(1, 4000)) 53 | 54 | def extend_pe(self, x): 55 | """Reset the positional encodings.""" 56 | if self.pe is not None: 57 | if self.pe.size(1) >= x.size(1): 58 | if self.pe.dtype != x.dtype or self.pe.device != x.device: 59 | self.pe = self.pe.to(dtype=x.dtype, device=x.device) 60 | return 61 | pe = torch.zeros(x.size(1), self.embedding_dim) 62 | if self.reverse: 63 | position = torch.arange( 64 | x.size(1) - 1, -1, -1.0, dtype=torch.float32 65 | ).unsqueeze(1) 66 | else: 67 | position = torch.arange(0, x.size(1), dtype=torch.float32).unsqueeze(1) 68 | div_term = torch.exp( 69 | torch.arange(0, self.embedding_dim, 2, dtype=torch.float32) 70 | * -(math.log(10000.0) / self.embedding_dim) 71 | ) 72 | pe[:, 0::2] = torch.sin(position * div_term) 73 | pe[:, 1::2] = torch.cos(position * div_term) 74 | pe = pe.unsqueeze(0) 75 | self.pe = pe.to(device=x.device, dtype=x.dtype).detach() 76 | 77 | def forward(self, x: torch.Tensor) -> torch.Tensor: 78 | self.extend_pe(x) 79 | output = x.unsqueeze(-1) if x.ndim == 2 else x 80 | output = output * self.x_scale + self.alpha * self.pe[:, : x.size(1)] 81 | return self.dropout(output) 82 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/AR/modules/lr_schedulers.py: -------------------------------------------------------------------------------- 1 | # modified from https://github.com/yangdongchao/SoundStorm/blob/master/soundstorm/s1/AR/modules/lr_schedulers.py 2 | # reference: https://github.com/lifeiteng/vall-e 3 | import math 4 | 5 | import torch 6 | from matplotlib import pyplot as plt 7 | from torch import nn 8 | from torch.optim import Adam 9 | 10 | 11 | class WarmupCosineLRSchedule(torch.optim.lr_scheduler._LRScheduler): 12 | """ 13 | Implements Warmup learning rate schedule until 'warmup_steps', going from 'init_lr' to 'peak_lr' for multiple optimizers. 14 | """ 15 | 16 | def __init__( 17 | self, 18 | optimizer, 19 | init_lr, 20 | peak_lr, 21 | end_lr, 22 | warmup_steps=10000, 23 | total_steps=400000, 24 | current_step=0, 25 | ): 26 | self.init_lr = init_lr 27 | self.peak_lr = peak_lr 28 | self.end_lr = end_lr 29 | self.optimizer = optimizer 30 | self._warmup_rate = (peak_lr - init_lr) / warmup_steps 31 | self._decay_rate = (end_lr - peak_lr) / (total_steps - warmup_steps) 32 | self._current_step = current_step 33 | self.lr = init_lr 34 | self.warmup_steps = warmup_steps 35 | self.total_steps = total_steps 36 | self._last_lr = [self.lr] 37 | 38 | def set_lr(self, lr): 39 | self._last_lr = [g["lr"] for g in self.optimizer.param_groups] 40 | for g in self.optimizer.param_groups: 41 | # g['lr'] = lr 42 | g["lr"] = self.end_lr ###锁定用线性 43 | 44 | def step(self): 45 | if self._current_step < self.warmup_steps: 46 | lr = self.init_lr + self._warmup_rate * self._current_step 47 | 48 | elif self._current_step > self.total_steps: 49 | lr = self.end_lr 50 | 51 | else: 52 | decay_ratio = (self._current_step - self.warmup_steps) / ( 53 | self.total_steps - self.warmup_steps 54 | ) 55 | if decay_ratio < 0.0 or decay_ratio > 1.0: 56 | raise RuntimeError( 57 | "Decay ratio must be in [0.0, 1.0]. Fix LR scheduler settings." 58 | ) 59 | coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) 60 | lr = self.end_lr + coeff * (self.peak_lr - self.end_lr) 61 | 62 | self.lr = lr = self.end_lr = 0.002 ###锁定用线性###不听话,直接锁定! 63 | self.set_lr(lr) 64 | self.lr = lr 65 | self._current_step += 1 66 | return self.lr 67 | 68 | 69 | if __name__ == "__main__": 70 | m = nn.Linear(10, 10) 71 | opt = Adam(m.parameters(), lr=1e-4) 72 | s = WarmupCosineLRSchedule( 73 | opt, 1e-6, 2e-4, 1e-6, warmup_steps=2000, total_steps=20000, current_step=0 74 | ) 75 | lrs = [] 76 | for i in range(25000): 77 | s.step() 78 | lrs.append(s.lr) 79 | print(s.lr) 80 | 81 | plt.plot(lrs) 82 | plt.plot(range(0, 25000), lrs) 83 | plt.show() 84 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/AR/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def str2bool(str): 5 | return True if str.lower() == 'true' else False 6 | 7 | 8 | def get_newest_ckpt(string_list): 9 | # 定义一个正则表达式模式,用于匹配字符串中的数字 10 | pattern = r'epoch=(\d+)-step=(\d+)\.ckpt' 11 | 12 | # 使用正则表达式提取每个字符串中的数字信息,并创建一个包含元组的列表 13 | extracted_info = [] 14 | for string in string_list: 15 | match = re.match(pattern, string) 16 | if match: 17 | epoch = int(match.group(1)) 18 | step = int(match.group(2)) 19 | extracted_info.append((epoch, step, string)) 20 | # 按照 epoch 后面的数字和 step 后面的数字进行排序 21 | sorted_info = sorted( 22 | extracted_info, key=lambda x: (x[0], x[1]), reverse=True) 23 | # 获取最新的 ckpt 文件名 24 | newest_ckpt = sorted_info[0][2] 25 | return newest_ckpt 26 | 27 | 28 | # 文本存在且不为空时 return True 29 | def check_txt_file(file_path): 30 | try: 31 | with open(file_path, 'r') as file: 32 | text = file.readline().strip() 33 | assert text.strip() != '' 34 | return text 35 | except Exception: 36 | return False 37 | return False 38 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/AR/utils/initialize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Initialize modules for espnet2 neural networks.""" 3 | import torch 4 | from typeguard import check_argument_types 5 | 6 | 7 | def initialize(model: torch.nn.Module, init: str): 8 | """Initialize weights of a neural network module. 9 | 10 | Parameters are initialized using the given method or distribution. 11 | 12 | Custom initialization routines can be implemented into submodules 13 | as function `espnet_initialization_fn` within the custom module. 14 | 15 | Args: 16 | model: Target. 17 | init: Method of initialization. 18 | """ 19 | assert check_argument_types() 20 | print("init with", init) 21 | 22 | # weight init 23 | for p in model.parameters(): 24 | if p.dim() > 1: 25 | if init == "xavier_uniform": 26 | torch.nn.init.xavier_uniform_(p.data) 27 | elif init == "xavier_normal": 28 | torch.nn.init.xavier_normal_(p.data) 29 | elif init == "kaiming_uniform": 30 | torch.nn.init.kaiming_uniform_(p.data, nonlinearity="relu") 31 | elif init == "kaiming_normal": 32 | torch.nn.init.kaiming_normal_(p.data, nonlinearity="relu") 33 | else: 34 | raise ValueError("Unknown initialization: " + init) 35 | # bias init 36 | for name, p in model.named_parameters(): 37 | if ".bias" in name and p.dim() == 1: 38 | p.data.zero_() 39 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/AR/utils/io.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import torch 4 | import yaml 5 | 6 | 7 | def load_yaml_config(path): 8 | with open(path) as f: 9 | config = yaml.full_load(f) 10 | return config 11 | 12 | 13 | def save_config_to_yaml(config, path): 14 | assert path.endswith(".yaml") 15 | with open(path, "w") as f: 16 | f.write(yaml.dump(config)) 17 | f.close() 18 | 19 | 20 | def write_args(args, path): 21 | args_dict = dict( 22 | (name, getattr(args, name)) for name in dir(args) if not name.startswith("_") 23 | ) 24 | with open(path, "a") as args_file: 25 | args_file.write("==> torch version: {}\n".format(torch.__version__)) 26 | args_file.write( 27 | "==> cudnn version: {}\n".format(torch.backends.cudnn.version()) 28 | ) 29 | args_file.write("==> Cmd:\n") 30 | args_file.write(str(sys.argv)) 31 | args_file.write("\n==> args:\n") 32 | for k, v in sorted(args_dict.items()): 33 | args_file.write(" %s: %s\n" % (str(k), str(v))) 34 | args_file.close() 35 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/module/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/tts/modules/gpt_sovits/module/__init__.py -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/module/cnhubert.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import torch.nn as nn 4 | from transformers import HubertModel, Wav2Vec2FeatureExtractor 5 | 6 | logging.getLogger("numba").setLevel(logging.WARNING) 7 | 8 | 9 | class CNHubert(nn.Module): 10 | def __init__(self, cnhubert_base_path): 11 | super().__init__() 12 | self.model = HubertModel.from_pretrained(cnhubert_base_path) 13 | self.feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(cnhubert_base_path) 14 | 15 | def forward(self, x): 16 | input_values = self.feature_extractor(x, return_tensors="pt", sampling_rate=16000).input_values.to(x.device) 17 | feats = self.model(input_values)["last_hidden_state"] 18 | return feats 19 | 20 | 21 | def get_model(cnhubert_base_path): 22 | model = CNHubert(cnhubert_base_path) 23 | model.eval() 24 | return model 25 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/__init__.py: -------------------------------------------------------------------------------- 1 | from .symbols import * 2 | 3 | _symbol_to_id = {s: i for i, s in enumerate(symbols)} 4 | 5 | 6 | def cleaned_text_to_sequence(cleaned_text): 7 | """Converts a string of text to a sequence of IDs corresponding to the symbols in the text. 8 | Args: 9 | text: string to convert to a sequence 10 | Returns: 11 | List of integers corresponding to the symbols in the text 12 | """ 13 | phones = [_symbol_to_id[symbol] for symbol in cleaned_text] 14 | return phones 15 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/cleaner.py: -------------------------------------------------------------------------------- 1 | from . import chinese, cleaned_text_to_sequence, symbols, english 2 | 3 | language_module_map = {"zh": chinese, "en": english} 4 | special = [ 5 | # ("%", "zh", "SP"), 6 | ("¥", "zh", "SP2"), 7 | ("^", "zh", "SP3"), 8 | # ('@', 'zh', "SP4")#不搞鬼畜了,和第二版保持一致吧 9 | ] 10 | 11 | 12 | def clean_text(text, language): 13 | if(language not in language_module_map): 14 | language="en" 15 | text=" " 16 | for special_s, special_l, target_symbol in special: 17 | if special_s in text and language == special_l: 18 | return clean_special(text, language, special_s, target_symbol) 19 | language_module = language_module_map[language] 20 | norm_text = language_module.text_normalize(text) 21 | if language == "zh": 22 | phones, word2ph = language_module.g2p(norm_text) 23 | assert len(phones) == sum(word2ph) 24 | assert len(norm_text) == len(word2ph) 25 | else: 26 | phones = language_module.g2p(norm_text) 27 | word2ph = None 28 | 29 | for ph in phones: 30 | assert ph in symbols 31 | return phones, word2ph, norm_text 32 | 33 | 34 | def clean_special(text, language, special_s, target_symbol): 35 | """ 36 | 特殊静音段sp符号处理 37 | """ 38 | text = text.replace(special_s, ",") 39 | language_module = language_module_map[language] 40 | norm_text = language_module.text_normalize(text) 41 | phones = language_module.g2p(norm_text) 42 | new_ph = [] 43 | for ph in phones[0]: 44 | assert ph in symbols 45 | if ph == ",": 46 | new_ph.append(target_symbol) 47 | else: 48 | new_ph.append(ph) 49 | return new_ph, phones[1], norm_text 50 | 51 | 52 | def text_to_sequence(text, language): 53 | phones = clean_text(text) 54 | return cleaned_text_to_sequence(phones) 55 | 56 | 57 | if __name__ == "__main__": 58 | print(clean_text("你好%啊啊啊额、还是到付红四方。", "zh")) 59 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/engdict-hot.rep: -------------------------------------------------------------------------------- 1 | CHATGPT CH AE1 T JH IY1 P IY1 T IY1 2 | JSON JH EY1 S AH0 N -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/engdict_cache.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/tts/modules/gpt_sovits/text/engdict_cache.pickle -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/namedict_cache.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/server/tts/modules/gpt_sovits/text/namedict_cache.pickle -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/zh_normalization/README.md: -------------------------------------------------------------------------------- 1 | ## Supported NSW (Non-Standard-Word) Normalization 2 | 3 | |NSW type|raw|normalized| 4 | |:--|:-|:-| 5 | |serial number|电影中梁朝伟扮演的陈永仁的编号27149|电影中梁朝伟扮演的陈永仁的编号二七一四九| 6 | |cardinal|这块黄金重达324.75克
我们班的最高总分为583分|这块黄金重达三百二十四点七五克
我们班的最高总分为五百八十三分| 7 | |numeric range |12\~23
-1.5\~2|十二到二十三
负一点五到二| 8 | |date|她出生于86年8月18日,她弟弟出生于1995年3月1日|她出生于八六年八月十八日, 她弟弟出生于一九九五年三月一日| 9 | |time|等会请在12:05请通知我|等会请在十二点零五分请通知我 10 | |temperature|今天的最低气温达到-10°C|今天的最低气温达到零下十度 11 | |fraction|现场有7/12的观众投出了赞成票|现场有十二分之七的观众投出了赞成票| 12 | |percentage|明天有62%的概率降雨|明天有百分之六十二的概率降雨| 13 | |money|随便来几个价格12块5,34.5元,20.1万|随便来几个价格十二块五,三十四点五元,二十点一万| 14 | |telephone|这是固话0421-33441122
这是手机+86 18544139121|这是固话零四二一三三四四一一二二
这是手机八六一八五四四一三九一二一| 15 | ## References 16 | [Pull requests #658 of DeepSpeech](https://github.com/PaddlePaddle/DeepSpeech/pull/658/files) 17 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/zh_normalization/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from .text_normlization import * 15 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/zh_normalization/chronology.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import re 15 | 16 | from .num import DIGITS 17 | from .num import num2str 18 | from .num import verbalize_cardinal 19 | from .num import verbalize_digit 20 | 21 | 22 | def _time_num2str(num_string: str) -> str: 23 | """A special case for verbalizing number in time.""" 24 | result = num2str(num_string.lstrip('0')) 25 | if num_string.startswith('0'): 26 | result = DIGITS['0'] + result 27 | return result 28 | 29 | 30 | # 时刻表达式 31 | RE_TIME = re.compile(r'([0-1]?[0-9]|2[0-3])' 32 | r':([0-5][0-9])' 33 | r'(:([0-5][0-9]))?') 34 | 35 | # 时间范围,如8:30-12:30 36 | RE_TIME_RANGE = re.compile(r'([0-1]?[0-9]|2[0-3])' 37 | r':([0-5][0-9])' 38 | r'(:([0-5][0-9]))?' 39 | r'(~|-)' 40 | r'([0-1]?[0-9]|2[0-3])' 41 | r':([0-5][0-9])' 42 | r'(:([0-5][0-9]))?') 43 | 44 | 45 | def replace_time(match) -> str: 46 | """ 47 | Args: 48 | match (re.Match) 49 | Returns: 50 | str 51 | """ 52 | 53 | is_range = len(match.groups()) > 5 54 | 55 | hour = match.group(1) 56 | minute = match.group(2) 57 | second = match.group(4) 58 | 59 | if is_range: 60 | hour_2 = match.group(6) 61 | minute_2 = match.group(7) 62 | second_2 = match.group(9) 63 | 64 | result = f"{num2str(hour)}点" 65 | if minute.lstrip('0'): 66 | if int(minute) == 30: 67 | result += "半" 68 | else: 69 | result += f"{_time_num2str(minute)}分" 70 | if second and second.lstrip('0'): 71 | result += f"{_time_num2str(second)}秒" 72 | 73 | if is_range: 74 | result += "至" 75 | result += f"{num2str(hour_2)}点" 76 | if minute_2.lstrip('0'): 77 | if int(minute) == 30: 78 | result += "半" 79 | else: 80 | result += f"{_time_num2str(minute_2)}分" 81 | if second_2 and second_2.lstrip('0'): 82 | result += f"{_time_num2str(second_2)}秒" 83 | 84 | return result 85 | 86 | 87 | RE_DATE = re.compile(r'(\d{4}|\d{2})年' 88 | r'((0?[1-9]|1[0-2])月)?' 89 | r'(((0?[1-9])|((1|2)[0-9])|30|31)([日号]))?') 90 | 91 | 92 | def replace_date(match) -> str: 93 | """ 94 | Args: 95 | match (re.Match) 96 | Returns: 97 | str 98 | """ 99 | year = match.group(1) 100 | month = match.group(3) 101 | day = match.group(5) 102 | result = "" 103 | if year: 104 | result += f"{verbalize_digit(year)}年" 105 | if month: 106 | result += f"{verbalize_cardinal(month)}月" 107 | if day: 108 | result += f"{verbalize_cardinal(day)}{match.group(9)}" 109 | return result 110 | 111 | 112 | # 用 / 或者 - 分隔的 YY/MM/DD 或者 YY-MM-DD 日期 113 | RE_DATE2 = re.compile( 114 | r'(\d{4})([- /.])(0[1-9]|1[012])\2(0[1-9]|[12][0-9]|3[01])') 115 | 116 | 117 | def replace_date2(match) -> str: 118 | """ 119 | Args: 120 | match (re.Match) 121 | Returns: 122 | str 123 | """ 124 | year = match.group(1) 125 | month = match.group(3) 126 | day = match.group(4) 127 | result = "" 128 | if year: 129 | result += f"{verbalize_digit(year)}年" 130 | if month: 131 | result += f"{verbalize_cardinal(month)}月" 132 | if day: 133 | result += f"{verbalize_cardinal(day)}日" 134 | return result 135 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/zh_normalization/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import re 15 | import string 16 | 17 | from pypinyin.constants import SUPPORT_UCS4 18 | 19 | # 全角半角转换 20 | # 英文字符全角 -> 半角映射表 (num: 52) 21 | F2H_ASCII_LETTERS = { 22 | ord(char) + 65248: ord(char) 23 | for char in string.ascii_letters 24 | } 25 | 26 | # 英文字符半角 -> 全角映射表 27 | H2F_ASCII_LETTERS = {value: key for key, value in F2H_ASCII_LETTERS.items()} 28 | 29 | # 数字字符全角 -> 半角映射表 (num: 10) 30 | F2H_DIGITS = {ord(char) + 65248: ord(char) for char in string.digits} 31 | # 数字字符半角 -> 全角映射表 32 | H2F_DIGITS = {value: key for key, value in F2H_DIGITS.items()} 33 | 34 | # 标点符号全角 -> 半角映射表 (num: 32) 35 | F2H_PUNCTUATIONS = {ord(char) + 65248: ord(char) for char in string.punctuation} 36 | # 标点符号半角 -> 全角映射表 37 | H2F_PUNCTUATIONS = {value: key for key, value in F2H_PUNCTUATIONS.items()} 38 | 39 | # 空格 (num: 1) 40 | F2H_SPACE = {'\u3000': ' '} 41 | H2F_SPACE = {' ': '\u3000'} 42 | 43 | # 非"有拼音的汉字"的字符串,可用于NSW提取 44 | if SUPPORT_UCS4: 45 | RE_NSW = re.compile(r'(?:[^' 46 | r'\u3007' # 〇 47 | r'\u3400-\u4dbf' # CJK扩展A:[3400-4DBF] 48 | r'\u4e00-\u9fff' # CJK基本:[4E00-9FFF] 49 | r'\uf900-\ufaff' # CJK兼容:[F900-FAFF] 50 | r'\U00020000-\U0002A6DF' # CJK扩展B:[20000-2A6DF] 51 | r'\U0002A703-\U0002B73F' # CJK扩展C:[2A700-2B73F] 52 | r'\U0002B740-\U0002B81D' # CJK扩展D:[2B740-2B81D] 53 | r'\U0002F80A-\U0002FA1F' # CJK兼容扩展:[2F800-2FA1F] 54 | r'])+') 55 | else: 56 | RE_NSW = re.compile( # pragma: no cover 57 | r'(?:[^' 58 | r'\u3007' # 〇 59 | r'\u3400-\u4dbf' # CJK扩展A:[3400-4DBF] 60 | r'\u4e00-\u9fff' # CJK基本:[4E00-9FFF] 61 | r'\uf900-\ufaff' # CJK兼容:[F900-FAFF] 62 | r'])+') 63 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/zh_normalization/phonecode.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import re 15 | 16 | from .num import verbalize_digit 17 | 18 | # 规范化固话/手机号码 19 | # 手机 20 | # http://www.jihaoba.com/news/show/13680 21 | # 移动:139、138、137、136、135、134、159、158、157、150、151、152、188、187、182、183、184、178、198 22 | # 联通:130、131、132、156、155、186、185、176 23 | # 电信:133、153、189、180、181、177 24 | RE_MOBILE_PHONE = re.compile( 25 | r"(? str: 34 | if mobile: 35 | sp_parts = phone_string.strip('+').split() 36 | result = ','.join( 37 | [verbalize_digit(part, alt_one=True) for part in sp_parts]) 38 | return result 39 | else: 40 | sil_parts = phone_string.split('-') 41 | result = ','.join( 42 | [verbalize_digit(part, alt_one=True) for part in sil_parts]) 43 | return result 44 | 45 | 46 | def replace_phone(match) -> str: 47 | """ 48 | Args: 49 | match (re.Match) 50 | Returns: 51 | str 52 | """ 53 | return phone2str(match.group(0), mobile=False) 54 | 55 | 56 | def replace_mobile(match) -> str: 57 | """ 58 | Args: 59 | match (re.Match) 60 | Returns: 61 | str 62 | """ 63 | return phone2str(match.group(0)) 64 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/text/zh_normalization/quantifier.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import re 15 | 16 | from .num import num2str 17 | 18 | # 温度表达式,温度会影响负号的读法 19 | # -3°C 零下三度 20 | RE_TEMPERATURE = re.compile(r'(-?)(\d+(\.\d+)?)(°C|℃|度|摄氏度)') 21 | measure_dict = { 22 | "cm2": "平方厘米", 23 | "cm²": "平方厘米", 24 | "cm3": "立方厘米", 25 | "cm³": "立方厘米", 26 | "cm": "厘米", 27 | "db": "分贝", 28 | "ds": "毫秒", 29 | "kg": "千克", 30 | "km": "千米", 31 | "m2": "平方米", 32 | "m²": "平方米", 33 | "m³": "立方米", 34 | "m3": "立方米", 35 | "ml": "毫升", 36 | "m": "米", 37 | "mm": "毫米", 38 | "s": "秒" 39 | } 40 | 41 | 42 | def replace_temperature(match) -> str: 43 | """ 44 | Args: 45 | match (re.Match) 46 | Returns: 47 | str 48 | """ 49 | sign = match.group(1) 50 | temperature = match.group(2) 51 | unit = match.group(3) 52 | sign: str = "零下" if sign else "" 53 | temperature: str = num2str(temperature) 54 | unit: str = "摄氏度" if unit == "摄氏度" else "度" 55 | result = f"{sign}{temperature}{unit}" 56 | return result 57 | 58 | 59 | def replace_measure(sentence) -> str: 60 | for q_notation in measure_dict: 61 | if q_notation in sentence: 62 | sentence = sentence.replace(q_notation, measure_dict[q_notation]) 63 | return sentence 64 | -------------------------------------------------------------------------------- /server/tts/modules/gpt_sovits/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import traceback 3 | 4 | import ffmpeg 5 | import numpy as np 6 | 7 | 8 | def load_audio(file, sr): 9 | try: 10 | # https://github.com/openai/whisper/blob/main/whisper/audio.py#L26 11 | # This launches a subprocess to decode audio while down-mixing and resampling as necessary. 12 | # Requires the ffmpeg CLI and `ffmpeg-python` package to be installed. 13 | if os.path.exists(file) == False: 14 | raise RuntimeError("You input a wrong audio path that does not exists, please fix it!") 15 | out, _ = ( 16 | ffmpeg.input(file, threads=0) 17 | .output("-", format="f32le", acodec="pcm_f32le", ac=1, ar=sr) 18 | .run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True) 19 | ) 20 | except Exception as e: 21 | traceback.print_exc() 22 | raise RuntimeError(f"Failed to load audio: {e}") 23 | 24 | return np.frombuffer(out, np.float32).flatten() 25 | -------------------------------------------------------------------------------- /server/tts/modules/sambert_hifigan/tts_sambert_hifigan.py: -------------------------------------------------------------------------------- 1 | from modelscope.outputs import OutputKeys 2 | from modelscope.pipelines import pipeline 3 | from modelscope.utils.constant import Tasks 4 | 5 | # pip install kantts -f https://modelscope.oss-cn-beijing.aliyuncs.com/releases/repo.html 6 | # pip install pytorch_wavelets tensorboardX scipy==1.12.0 7 | 8 | 9 | def get_tts_model(): 10 | model_id = "damo/speech_sambert-hifigan_tts_zhisha_zh-cn_16k" 11 | sambert_hifigan_tts = pipeline(task=Tasks.text_to_speech, model=model_id) 12 | return sambert_hifigan_tts 13 | 14 | 15 | def gen_tts_wav(sambert_hifigan_tts, text, wav_path): 16 | # wav_path = 'output.wav' 17 | # text = '哈喽哈喽,家人们好啊!今天呀,咱们这儿可是有大大的福利等着大家哦你们猜猜看是什么呢?没错啦,就是这款超级棒哒【声波电动牙刷】啦!哎呀,我知道你们肯定都用过电动牙刷的,但是这款真的是太不一样了哦让我来给你们一一揭秘吧首先呢,它的【高效清洁】功能绝对是业界领先的哦,无论是顽固牙渍还是口腔死角,都能轻松搞定,让你们的牙齿每天都亮晶晶滴而且啊,它还有【减少手动压力】的设计,再也不用担心手酸手疼啦,轻轻松松就能拥有健康洁白的好笑容哦对了,还有【定时提醒】这个功能,再也不用担心刷牙时间不够啦,让你随时随地保持口腔卫生哦最厉害的是,它还有【智能模式调节】和【无线充电】的功能呢,简直是科技感爆棚哦而且,它的【噪音低】设计,就算是深夜刷牙也不会打扰到家人啦家人们,这样的电动牙刷是不是超级心动呢?快来把它带回家吧,让你的每一天都充满活力和好心情哦' 18 | print(f"gerning tts for {wav_path} ....") 19 | output = sambert_hifigan_tts(input=text) 20 | wav = output[OutputKeys.OUTPUT_WAV] 21 | with open(wav_path, "wb") as f: 22 | f.write(wav) 23 | print(f"gen tts for {wav_path} done!....") 24 | -------------------------------------------------------------------------------- /server/tts/modules/tts_worker.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | from ...web_configs import WEB_CONFIGS 5 | from .gpt_sovits.inference_gpt_sovits import gen_tts_wav, get_tts_model 6 | 7 | if WEB_CONFIGS.ENABLE_TTS: 8 | # samber 9 | # from utils.tts.sambert_hifigan.tts_sambert_hifigan import get_tts_model 10 | # TTS_HANDLER = get_tts_model() 11 | 12 | # gpt_sovits 13 | TTS_HANDLER = get_tts_model() 14 | else: 15 | TTS_HANDLER = None 16 | 17 | 18 | async def gen_tts_wav_app(cur_response, save_tag): 19 | # if save_tag == "": 20 | # save_tag = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".wav" 21 | 22 | tts_save_path = str(Path(WEB_CONFIGS.TTS_WAV_GEN_PATH).joinpath(save_tag).absolute()) 23 | if not Path(WEB_CONFIGS.TTS_WAV_GEN_PATH).exists(): 24 | Path(WEB_CONFIGS.TTS_WAV_GEN_PATH).mkdir(parents=True, exist_ok=True) 25 | 26 | # gen_tts_wav(st.session_state.tts_handler, cur_response, tts_save_path) 27 | 28 | # inp_ref = r"/root/hingwen_camp/utils/tts/gpt_sovits/weights/ref_wav/【开心】处理完之前的事情,这几天甚至都有空闲来车上转转了。.wav" 29 | text_language = "中英混合" 30 | gen_tts_wav( 31 | cur_response, 32 | text_language, 33 | TTS_HANDLER.bert_tokenizer, 34 | TTS_HANDLER.bert_model, 35 | TTS_HANDLER.ssl_model, 36 | TTS_HANDLER.vq_model, 37 | TTS_HANDLER.hps, 38 | TTS_HANDLER.max_sec, 39 | TTS_HANDLER.t2s_model, 40 | TTS_HANDLER.inp_ref, 41 | TTS_HANDLER.prompt_text, 42 | TTS_HANDLER.prompt, 43 | TTS_HANDLER.refer, 44 | TTS_HANDLER.bert1, 45 | TTS_HANDLER.phones1, 46 | TTS_HANDLER.zero_wav, 47 | tts_save_path, 48 | ) 49 | 50 | return tts_save_path 51 | -------------------------------------------------------------------------------- /server/tts/tools.py: -------------------------------------------------------------------------------- 1 | 2 | SYMBOL_SPLITS = { 3 | "。", 4 | "?", 5 | "!", 6 | "……", 7 | ".", 8 | "?", 9 | "!", 10 | "~", 11 | "…", 12 | } 13 | 14 | 15 | def make_text_chunk(original_text, strat_index, max_len=5, max_try=5000): 16 | cut_string = original_text 17 | end_index = strat_index 18 | 19 | while True: 20 | if original_text[end_index] in SYMBOL_SPLITS: 21 | end_index += 1 22 | cut_string = original_text[strat_index:end_index] 23 | break 24 | else: 25 | end_index += 1 26 | 27 | if end_index >= len(original_text): 28 | # 文本太短,没找到 29 | return 0, "" 30 | 31 | if end_index > max_try: 32 | # 有问题 33 | raise ValueError("Reach max try") 34 | return end_index, cut_string 35 | -------------------------------------------------------------------------------- /server/tts/tts_server.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from loguru import logger 3 | from pydantic import BaseModel 4 | 5 | 6 | from .modules.tts_worker import gen_tts_wav_app 7 | 8 | 9 | app = FastAPI() 10 | 11 | 12 | class TextToSpeechItem(BaseModel): 13 | user_id: str # User 识别号,用于区分不用的用户调用 14 | request_id: str # 请求 ID,用于生成 TTS & 数字人 15 | sentence: str # 文本 16 | chunk_id: int # 句子 ID 17 | 18 | 19 | @app.post("/tts") 20 | async def get_tts(tts_item: TextToSpeechItem): 21 | # 语音转文字 22 | wav_path = await gen_tts_wav_app(tts_item.sentence, tts_item.request_id + f"-{str(tts_item.chunk_id).zfill(8)}.wav") 23 | logger.info(f"tts wav path = {wav_path}") 24 | return {"user_id": tts_item.user_id, "request_id": tts_item.request_id, "wav_path": wav_path} 25 | 26 | 27 | @app.get("/tts/check") 28 | async def check_server(): 29 | return {"message": "server enabled"} 30 | -------------------------------------------------------------------------------- /static/digital_human/streamer_info_files/lelemiao.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/digital_human/streamer_info_files/lelemiao.mp4 -------------------------------------------------------------------------------- /static/digital_human/streamer_info_files/lelemiao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/digital_human/streamer_info_files/lelemiao.png -------------------------------------------------------------------------------- /static/digital_human/streamer_info_files/lelemiao.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/digital_human/streamer_info_files/lelemiao.wav -------------------------------------------------------------------------------- /static/product_files/images/beef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/beef.png -------------------------------------------------------------------------------- /static/product_files/images/elec_toothblush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/elec_toothblush.png -------------------------------------------------------------------------------- /static/product_files/images/lip_stick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/lip_stick.png -------------------------------------------------------------------------------- /static/product_files/images/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/mask.png -------------------------------------------------------------------------------- /static/product_files/images/oled_tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/oled_tv.png -------------------------------------------------------------------------------- /static/product_files/images/pad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/pad.png -------------------------------------------------------------------------------- /static/product_files/images/pants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/pants.png -------------------------------------------------------------------------------- /static/product_files/images/pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/pen.png -------------------------------------------------------------------------------- /static/product_files/images/perfume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/perfume.png -------------------------------------------------------------------------------- /static/product_files/images/shampoo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/shampoo.png -------------------------------------------------------------------------------- /static/product_files/images/wok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/wok.png -------------------------------------------------------------------------------- /static/product_files/images/yoga_mat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/product_files/images/yoga_mat.png -------------------------------------------------------------------------------- /static/product_files/instructions/beef.md: -------------------------------------------------------------------------------- 1 | # 澳洲进口和牛羽下肉产品说明书 2 | 3 | ## 产品名称 4 | 澳洲铂金和牛羽下肉 5 | 6 | ## 产品细节详情 7 | 8 | ### 生产信息 9 | - **生产日期**:2024年03月03日至2024年4月10日 10 | - **品牌**:研牛院 11 | - **加工工艺**:原切零添加 12 | - **牛种**:纯血和牛 13 | - **饲养天数**:800天 14 | 15 | ### 产品规格 16 | - **口味**:铂金和牛M9+ 17 | - **生鲜储存温度**:-18°C 18 | - **净含量**:500g/1000g 19 | - **包装方式**:片装 20 | 21 | ### 产地与加工 22 | - **产地**:澳大利亚 23 | - **厂名**:见标签 24 | - **厂址**:见标签 25 | - **厂家联系方式**:见标签 26 | - **配料表**:牛肉 27 | - **保质期**:365天 28 | 29 | ### 产品认证与荣誉 30 | - **认证**:100%MSA分级,国际清真认证 31 | - **荣誉**:屡获澳洲和牛金奖,2020年、2021年荣获珀斯皇家食品奖总冠军 32 | 33 | ## 卖点 34 | 35 | ### 高品质保证 36 | - 精选全血和牛,经过澳洲官方血统认证。 37 | - 饲养400天+,无任何生长激素。 38 | 39 | ### 独特口感 40 | - 铂金和牛羽下肉,口感香嫩,奶香浓郁。 41 | 42 | ### 严格的储存与运输 43 | - -18°C保存,确保新鲜。 44 | - 顺丰冷链全程锁鲜,多重保护。 45 | 46 | ### 品牌荣誉 47 | - 品牌铂金是澳洲凤凰品牌高端系列,享誉国际。 48 | 49 | ## 亮点 50 | 51 | ### 澳洲进口 52 | - 来自澳洲的高品质和牛,享受纯正的澳洲风味。 53 | 54 | ### 顶级工艺 55 | - 1.5-2厘米的原切工艺,保证每一片肉的品质。 56 | 57 | ### 荣誉认证 58 | - 获得多项国际认证,品质有保障。 59 | 60 | ### 健康饲养 61 | - 谷饲牛肉,800天的精心饲养。 62 | 63 | ### 严格包装 64 | - 采用食品级干冰和特厚泡沫箱,确保产品在运输过程中的品质。 65 | 66 | ### 品牌直发 67 | - 研牛院品牌直发,确保每一份产品都是最优质的。 68 | 69 | ### 温馨提示 70 | - 收到产品后请及时收取并冷冻保存,以保证最佳食用品质。 71 | -------------------------------------------------------------------------------- /static/product_files/instructions/elec_toothblush.md: -------------------------------------------------------------------------------- 1 | # 玉米声波电动牙刷说明书 2 | 3 | ## 产品简介 4 | **品牌**:玉米 5 | **型号**:玉米声波电动牙刷 6 | **生产企业**:玉米 7 | **类型**:声波式 8 | **款式**:礼盒款 9 | **适用人群**:成人 10 | **保修期**:24个月 11 | 12 | ## 产品细节详情 13 | 14 | ### 充电与续航 15 | - **充电模式**:常规充电 16 | - **续航**:150天超长续航(标准清洁模式续航65天) 17 | 18 | ### 刷头特性 19 | - **刷头**:无铜抑菌刷头 20 | - **刷毛**:进口纤密软毛,杜邦尼龙软刷,净柔呵护牙龈 21 | - **抑菌率**:>99% 22 | 23 | ### 防水性能 24 | - **防水等级**:IPX8级全身防水 25 | 26 | ### 智能功能 27 | - **智能类型**:其他智能 28 | - **智能LED大屏**:无级动力调节 29 | - **个性定制模式**:满足不同用户需求 30 | 31 | ### 清洁模式 32 | - **模式**:标准清洁、牙龈按摩、亮白抛光、敏感轻柔 33 | 34 | ### 附加配件 35 | - **刷头套装**:4刷头套装礼盒,365天多效清洁 36 | 37 | ## 产品卖点 38 | 39 | ### 高效清洁 40 | - **高频震动**:提供高效清洁力 41 | - **磁悬浮声波马达**:有效减少牙菌斑 42 | 43 | ### 智能科技 44 | - **AI精准洁牙**:智能识别并适应用户洁牙需求 45 | - **米家APP智能互联**:通过APP实现个性化设置 46 | 47 | ### 长效续航 48 | - **超长续航**:150天长效续航,减少充电次数 49 | 50 | ### 舒适体验 51 | - **低噪音设计**:减少刷牙时的噪音干扰 52 | - **全身防水**:提供安全的水下使用体验 53 | 54 | ### 精美设计 55 | - **高颜值色系**:浪漫紫、深海蓝、流光银 56 | - **精致礼盒**:适合送礼或自用 57 | 58 | ## 产品亮点 59 | 60 | ### 创新技术 61 | - **无铜抑菌刷头**:减少细菌滋生,更卫生 62 | - **360°无线自由充**:便捷充电,无束缚 63 | 64 | ### 健康护齿 65 | - **杜邦抗菌丝**:多重磨圆,更护龈 66 | - **28天牙菌斑下降2.48倍**:SGS认证,有效减少牙菌斑 67 | 68 | ### 便捷使用 69 | - **2分钟应急闪充**:快速充电,应对突发状况 70 | - **约4小时充满**:快速充电,减少等待时间 71 | 72 | ### 多效刷头 73 | - **专业清洁、敏感、全效刷头**:适配不同机型,满足不同需求 74 | 75 | ### 温馨提示 76 | - 电动牙刷类产品涉及个人卫生,拆封或使用后不支持七天无理由退换货。 77 | - 为保障消费者合理购买需求,避免异常订单,店铺有权对异常订单不发货且不进行赔付。 78 | 79 | ## 价格说明 80 | - **划线价格**:为参考价,非原价 81 | - **未划线价格**:实时标价,可能因活动、优惠券等发生变化 82 | - **最终价格**:以订单结算页价格为准 83 | 84 | ## 购买须知 85 | - 每笔订单同种商品只有一件能够享受主图优惠,需多件享受优惠,请分开下单购买。 86 | 87 | ## 品牌理念 88 | 玉米,智能生活品牌,致力于让每个人都能享受科技的乐趣,以人工智能、loT技术、先进的核心科技,全心打造每一款产品,为你带来可实现的智能生活方式。 -------------------------------------------------------------------------------- /static/product_files/instructions/lip_stick.md: -------------------------------------------------------------------------------- 1 | # Beautiful Lip 口红说明书 2 | 3 | ## 产品名称 4 | Beautiful Lip 口红 5 | 6 | ## 产品细节详情 7 | 8 | ### 产地 9 | - 韩国 10 | 11 | ### 包装种类 12 | - 基础包装 13 | 14 | ### 颜色分类 15 | - 特别提示:膏体只能旋出,无法收回 16 | 17 | ### 化妆品备案编号/注册证号 18 | - 国妆网备 19 | 20 | ### 功效 21 | - 保湿 22 | 23 | ### 规格类型 24 | - 正常规格 25 | 26 | ### 是否为特殊用途化妆品 27 | - 否 28 | 29 | ### 净含量 30 | - 1.7g 31 | 32 | ### 限期使用日期范围 33 | - 2026-03-14 34 | 35 | ### 保质期 36 | - 36个月 37 | 38 | ### 质地 39 | - 镜面 40 | 41 | ### 消费提醒 42 | - 国家药监局提醒:化妆品只能涂擦、喷酒或其他类似方法施用于人体表面,不得食用或注射。 43 | 44 | ### 购物金礼赠 45 | - 购前先充值¥1000/¥1500购物金,在本店累计正装订单实付消费购物金满¥1000/¥1500享有对应门槛礼赠。 46 | 47 | ## 卖点 48 | 49 | ### 创新三效合一 50 | - 专研配方集合润唇膏的滋润补水、唇膏的丰盈立体、唇蜜的水光亮泽。 51 | 52 | ### 即刻丰盈饱满 53 | - 一抹上唇,即刻填平唇纹,不斑驳,呈现真正的欲感“嘟嘟唇”。 54 | 55 | ### 持久润泽 56 | - 轻薄不糊嘴,双唇无负担,拯救干唇困扰,改善干燥双唇。 57 | 58 | ### 两大成分复配 59 | - 薄荷醇乳酸脂带来清凉感,复合成分调理和润养双唇。 60 | 61 | ## 亮点 62 | 63 | ### 色号推荐 64 | - #120 DESIRE 蜜桃冰沙:冷调烟粉色,甜美人间水蜜桃。 65 | - #100 RISE 肉桂奶茶:冷调裸米色,氧气白开水高级感。 66 | - 更多明星色号推荐,满足不同妆容需求。 67 | 68 | ### 质地 69 | - 一抹化水,触唇即融,打造晶莹剔透的水光镜面感。 70 | 71 | ### 使用方法 72 | - 轻轻旋转唇膏管底座旋出适量膏体进行涂抹。 73 | 74 | ### 唇妆家族 75 | - 搭配任意口红单品,轻松打造3D立体唇。 76 | 77 | ### 品牌故事 78 | - Beautiful Lip,奢华美妆品牌,以卓越创新力与重释奢美理念而闻名。 79 | 80 | ### 价格说明 81 | - 价格可能因使用优惠券、满减或特定优惠活动和时段等情形下发生变化,具体以结算页面为准。 82 | -------------------------------------------------------------------------------- /static/product_files/instructions/mask.md: -------------------------------------------------------------------------------- 1 | # TOPMASK光感润颜面膜说明书 2 | 3 | ## 产品名称 4 | **TOPMASK光感润颜面膜** 5 | 6 | ## 产品细节详情 7 | 8 | ### 基本信息 9 | - **化妆品备案编号/注册证号**:粤G妆网.. 10 | - **品牌**:TOPMASK 11 | - **规格类型**:正常规格 12 | - **包装种类**:基础包装 13 | - **是否为特殊用途化妆品**:否 14 | - **注册人/备案人地址**:广州市花都区 15 | - **生产企业**:广州市TOPMASK生物科技 16 | - **生产许可证号**:粤妆20160.. 17 | - **保质期**:3年 18 | - **限期使用日期范围**:2027-04-29至 19 | - **产地**:中国 20 | 21 | ### 产品特点 22 | - **核心功效**:补水修护紧致 23 | - **功效**:修护紧致保湿舒缓 24 | - **颜色分类**:6盒30片、10盒50片 25 | 26 | ## 卖点 27 | 28 | ### 多重功效 29 | - **水润滋养**:深层保湿,焕亮肌肤 30 | - **深层保湿**:改善肌肤暗沉,维持肌肤年轻感 31 | - **舒缓修护**:直达肌底,缓解敏感肌肤问题 32 | 33 | ### 专利成分 34 | - **酵母提取物** 35 | - **可溶性胶原** 36 | - **麦角硫因** 37 | 38 | ### 法国进口成分 39 | - **海茴香愈伤组织培养物滤液** 40 | - **刺云实提取物** 41 | - **番红花提取物** 42 | - **人参根提取物** 43 | 44 | ### 面膜材质 45 | - **细密红维膜车**:细轴纤维膜布,更好贴合肌肤 46 | - **轻薄透气**:不闷痘,肌肤畅快呼吸 47 | - **服帖无负担**:紧密贴合脸部,仿佛量身打造 48 | 49 | ### 使用方法 50 | 1. **清洁面部**:使用前确保面部清洁 51 | 2. **取出面膜**:取出面膜并轻压展开 52 | 3. **贴敷于面部**:贴合面部肌肤 53 | 4. **静敷15-20分钟**:让肌肤充分吸收精华 54 | 5. **取下面膜**:用清水洗净面部 55 | 6. **按摩面部**:促进剩余精华吸收 56 | 57 | ### 敏感肌友好 58 | - **无添加**:0增白剂、0色素、0防腐剂、0香精 59 | 60 | ## 亮点 61 | 62 | ### 安全性 63 | - 国家药监局提醒:化妆品只能施用于人体表面,不得食用或注射 64 | 65 | ### 价格说明 66 | - **划线价格**:商品的专柜价、吊牌价等,仅供参考 67 | - **未划线价格**:商品的实时标价,以订单结算页价格为准 68 | - **商家详情页**:可能标注促销价、优惠价等,具体以结算页面为准 69 | 70 | ### 包装展示 71 | - 产品外包装正面与背面展示 72 | 73 | --- 74 | **注意**:请正确使用化妆品,遵循产品说明进行使用。 -------------------------------------------------------------------------------- /static/product_files/instructions/oled_tv.md: -------------------------------------------------------------------------------- 1 | # 玉米电视6 OLED 65英寸说明书 2 | 3 | ## 产品名称 4 | 玉米电视6 OLED 65英寸 5 | 6 | ## 产品细节详情 7 | 8 | ### 基本参数 9 | - **HDMI接口数量**:3个 10 | - **上市时间**:2021-08 11 | - **能效备案号**:20210617-731400-08 12 | - **屏幕比例**:16:9 13 | - **语音遥控类型**:远场语音遥控 14 | - **保修期**:12个月 15 | - **包装尺寸**:1604x193x958mm 16 | - **接收制式**:SECAM, NTSC, PAL 17 | - **能效等级**:四级 18 | - **电视形态**:平板电视 19 | - **电视类型**:OLED电视 20 | - **分辨率**:4K 21 | 22 | ### 重量与尺寸 23 | - **净重(不含底座)**:22kg 24 | - **主机尺寸(不含底座)mm**:1448.7x83x105.3 25 | 26 | ### 其他特性 27 | - **颜色分类**:玉米电视6 OLED 65 28 | - **自发品牌**:玉米 29 | - **型号**:L65M7-Z2 30 | - **接口类型**:LAN端子,AV,HDMI 31 | - **智能类型**:其他智能 32 | - **屏幕尺寸**:65英寸 33 | - **含边框整屏尺寸**:1448.7x105.3x83mm 34 | - **堆码层数极限**:2层 35 | - **刷屏率**:60Hz 36 | - **存储容量**:3GB+32GB 37 | - **采购地**:中国大陆 38 | - **是否内置摄像头**:否 39 | 40 | ## 卖点 41 | 42 | ### 高端OLED自发光 43 | - **1000000:1** 惊艳对比度 44 | - **4K OLED** 829万个像素自发光 45 | - **98.5% P3** 电影级广色域 46 | 47 | ### 画质技术 48 | - **Onit MEMC** 星夜黑运动补偿 49 | - **全场景互联** 50 | - **1ms** 瞬时响应 51 | 52 | ### 音质体验 53 | - **杜比视界** 家庭影院 54 | - **4单元扬声器** 澎湃立体声场 55 | - **IMAX ENHANCED** 沉浸式电影技术 56 | 57 | ### 护眼与设计 58 | - **DC硬件级防蓝光** 有效减少有害蓝光 59 | - **178°广视角** 客厅任意方位,都是黄金视角 60 | - **4.6mm** 纤薄屏幕 61 | - **97%全面屏** 四面超窄边框 62 | 63 | ### 性能与智能 64 | - **MT9638旗舰处理器** 四核CPU 65 | - **3GB+32GB** 大存储 66 | - **双频Wi-Fi和蓝牙5.0** 67 | - **1ms瞬时响应** 电竞级显示 68 | 69 | ### 互联与安装 70 | - **全场景互联** 你好同学智能远场语音控制 71 | - **MIUI for TV3.0** 海量内容覆盖 72 | - **接口全面** HDMIx3, USBx2, Network 73 | - **个性化安装服务** 挂壁、座式可供选择 74 | 75 | ## 亮点 76 | - **OLED屏幕**:未来主流的屏幕显示技术,拥有让人一见倾心的惊艳画质。 77 | - **百万级对比度**:明暗细节丰富,媲美真实世界。 78 | - **星夜黑影像**:呈现出接近星夜的纯粹黑色,画面主体更生动明亮。 79 | - **10bit原色屏**:色彩自然细腻,影像栩栩如生。 80 | - **MEMC运动补偿**:高速移动镜头画面稳定,增强细节表现。 81 | - **超薄全面屏设计**:4.6mm纤薄屏幕,97%超高屏占比。 82 | - **智能语音控制**:解放双手,一声“小爱同学”即可控制。 83 | - **丰富接口**:满足影音、游戏等多样化需求。 84 | 85 | 请按照产品实物为准,详细阅读说明书,并按照指导进行操作。 -------------------------------------------------------------------------------- /static/product_files/instructions/pad.md: -------------------------------------------------------------------------------- 1 | # 玉米 Pad 6 Max 14英寸平板电脑说明书 2 | 3 | ## 产品名称 4 | **玉米 Pad 6 Max 14英寸** 5 | 6 | ## 产品细节详情 7 | 8 | ### 基本参数 9 | - **品牌**: 玉米 10 | - **型号**: 玉米 Pad 6 Max 11 | - **上市时间**: 2023-08-14 12 | - **生产企业**: 玉米通讯技术有限公司 13 | - **处理器型号**: 新一代骁龙8+移动平台 14 | - **处理器品牌**: 高通 15 | - **屏幕尺寸**: 14英寸 16 | - **分辨率**: 2880*1800 17 | - **内存容量**: 8GB/12GB/16GB 18 | - **存储容量**: 8GB+256GB/12GB+256GB 19 | - **通讯类型**: 不可插卡 20 | - **网络类型**: WIFI 21 | - **CPU主频**: 3.2GHz 22 | - **售后服务**: 全国联保 23 | - **保修期**: 12个月 24 | 25 | ### 颜色与设计 26 | - **颜色分类**: 银色、黑色 27 | 28 | ### 屏幕与显示 29 | - **屏幕类型**: 14英寸超大屏幕 30 | - **显示技术**: 2.8K高清,120Hz刷新率 31 | - **亮度**: 600nit最高亮度 32 | - **认证**: 德国莱茵三重认证、HDR10、P3色域 33 | 34 | ### 性能与配置 35 | - **处理器**: 骁龙8+旗舰芯片 36 | - **电池**: 10000mAh大电量 37 | - **快充**: 67W快充、33W反向充电 38 | - **芯片**: 玉米澎湃G1芯片 39 | - **存储技术**: LPDDR5X UFS 3.1 40 | 41 | ### 音响系统 42 | - **扬声器**: 8扬声器虚拟环绕声 43 | 44 | ### 配件与外设 45 | - **智能触控键盘**: 分体式设计,全尺寸大键程,新增15个独立快捷键 46 | - **触控笔**: 玉米焦点触控笔,5ms延迟,8192级压感 47 | 48 | ### 软件与系统 49 | - **操作系统**: YUMI系统 50 | - **功能**: 自由工作台、会议工具箱、PC级多任务协同 51 | 52 | ### 其他特性 53 | - **隐私保护**: ToF传感器+指示灯 54 | - **互联互通**: 支持NFC一碰秒传 55 | 56 | ## 卖点 57 | 58 | - **超大屏幕**: 14英寸巨屏,提供广阔视野和高效率办公体验。 59 | - **高清显示**: 2.8K分辨率,120Hz刷新率,P3色域,HDR10。 60 | - **强劲性能**: 骁龙8+旗舰处理器,LPDDR5X UFS 3.1高速存储。 61 | - **长效续航**: 10000mAh大电量,支持67W快充和33W反向充电。 62 | - **高效办公**: YUMI自由工作台,PC级多任务协同。 63 | - **专业音响**: 8扬声器系统,Dolby Atmos音效。 64 | - **智能配件**: 智能触控键盘和玉米焦点触控笔,提供高效输入和创作体验。 65 | - **隐私安全**: ToF传感器和指示灯,保护用户隐私。 66 | 67 | ## 亮点 68 | 69 | - **屏幕技术**: 六档可变刷新率,最高亮度600nit,全程DC调光。 70 | - **性能测试**: 性能测试数据优于其他安卓平板。 71 | - **续航能力**: 2.04天的续航模型,68分钟充至100%。 72 | - **办公软件**: 支持WPS Office,提供专业的文字排版和演示功能。 73 | - **会议助手**: 4麦克风阵列拾音系统,AI降噪,提高会议清晰度。 74 | - **翻译功能**: AI大模型翻译,提高翻译准确率。 75 | - **横屏适配**: Top200应用深度适配横屏,提升用户体验。 76 | 77 | 请按照说明书的指导进行操作,确保设备性能的最大化和安全使用。 -------------------------------------------------------------------------------- /static/product_files/instructions/pants.md: -------------------------------------------------------------------------------- 1 | # MARK SPORTS 新款男士训练运动速干九分裤说明书 2 | 3 | ## 产品名称 4 | - **男运动九分裤** 5 | 货号:512240999 6 | 系列:综训系列 7 | 品牌:MARK/火星马克 8 | 9 | ## 产品细节详情 10 | 11 | ### 面料与功能 12 | - **面料**:93%聚酯纤维 + 7%氨纶 13 | - **特点**:吸湿速干、高弹力、轻薄透气 14 | - **科技**:吸湿速干科技,质地轻盈,保持运动干爽 15 | 16 | ### 版型与设计 17 | - **版型**:修身常规宽松,高弹合体剪裁 18 | - **设计**:松紧脚口,符合人体工学的流畅版型 19 | - **口袋**:侧边拉链口袋,五弹力抽绳收腰 20 | 21 | ### 尺码信息 22 | - **尺码**:S, M, L, XL, 2XL, 3XL, 4XL 23 | - **尺码对应型号**:160/66A, 165/70A, 170/74A, 175/78A, 180/82A, 185/86A, 190/90A 24 | 25 | ### 尺码标准 26 | - **外长**:87, 89.5, 92, 94.5, 99, 102(cm) 27 | - **腰围**:68, 71, 74, 77, 80, 83, 86(cm) 28 | - **坐围**:86, 102, 106, 110, 114, 118, 122(cm) 29 | - **脾围**:61.6, 63.8, 66, 68.2, 70.4, 72.6, 74.8(cm) 30 | - **膝围**:43.4, 44.7, 46.4, 47.3, 48.6, 49.9, 51.2(cm) 31 | - **前浪**:27, 28, 29, 30, 31, 32, 33(cm) 32 | - **脚口**:25, 26, 27, 28, 29, 30, 31(cm) 33 | 34 | ## 卖点与亮点 35 | 36 | ### 吸湿速干科技 37 | - 采用国标GB/T21655.1-的吸湿速干科技,迅速吸收汗水,扩大浸湿面积,加速汗液蒸发。 38 | 39 | ### 弹力面料 40 | - 高弹力面料,适应不同体型,提供舒适自如的运动体验。 41 | 42 | ### 多场景穿搭 43 | - 适用于日常通勤、户外慢跑、运动健身、出行郊游等多种场景。 44 | 45 | ### 时尚设计 46 | - 松紧脚口设计,搭配五弹力抽绳,增添运动风格,利落有型。 47 | 48 | ### 精选面料 49 | - 精选锦氨弹力双面布,质地轻盈透气,助力排走汗水,保持干爽。 50 | 51 | ### 专业细节 52 | - 侧边拉链口袋设计,方便实用;五弹力抽绳收腰,更加舒适合身。 53 | 54 | ### 尺码选择 55 | - 提供详细的尺码信息,帮助消费者根据个人尺寸和习惯选择合适的尺码。 56 | 57 | ### 价格说明 58 | - 明确价格说明,帮助消费者理解商品价格构成。 59 | 60 | ### 购买渠道 61 | - 纯电商销售,提供线上购买的便利。 62 | 63 | 以上为MARK SPORTS新款男士训练运动速干九分裤的详细说明书。如需进一步了解或购买,请访问MARK官方网站或联系客服。 -------------------------------------------------------------------------------- /static/product_files/instructions/pen.md: -------------------------------------------------------------------------------- 1 | # 产品说明书 2 | 3 | ## 产品名称 4 | BEAR/黑熊 A16系列钢笔 5 | 6 | ## 产品细节详情 7 | 8 | ### 材质与设计 9 | - **笔杆材质**:金属 10 | - **笔身纹理**:光面 11 | - **笔尖成分**:铱金 12 | - **生产企业**:英雄 13 | - **笔尖嵌入方式**:明尖 14 | - **包装种类**:礼盒装 15 | - **上墨方式**:旋转吸墨 16 | 17 | ### 规格参数 18 | - **书写粗细**:0.35mm/0.38mm + 宝珠笔 19 | - **笔尖种类**:标准型 20 | - **墨水材质**:非碳素墨水 21 | - **货号**:英雄钢笔A16系列 22 | 23 | ### 适用场景 24 | - 学生用练字 25 | - 企业采购年会礼品 26 | - 商务礼品 27 | 28 | ### 颜色与定制 29 | - **颜色分类**:暮光绿三笔头礼盒 30 | - **定制刻字**:笔帽可刻4个字、笔杆可刻8个字(支持中文、英文、数字) 31 | - **礼盒定制印刷**:联系客服征明美雄HERO 32 | 33 | ### 产品信息 34 | - **产品尺寸**:138mm 35 | - **包装规格**:钢笔、礼盒、赠品(笔身免费刻字·礼盒支持印刷) 36 | 37 | ## 卖点 38 | 39 | ### 品牌历史 40 | - **品牌**:BEAR黑熊,创始于1930年,被誉为“国民钢笔”。 41 | 42 | ### 艺术美学 43 | - **莫兰迪色系**:典雅艺术,带有神秘又清雅的灰色调,令人心境平和。 44 | 45 | ### 书写体验 46 | - **流畅书写**:四种粗细选择,控墨有度,顺滑不刮纸。 47 | - **弹性笔尖**:四种粗细,提供不同的书写体验。 48 | - **防滑防汗**:笔握采用人体工程学设计,提供舒适的书写手感。 49 | 50 | ### 精美工艺 51 | - **全新烤漆工艺**:不易掉漆,保持外观亮丽。 52 | - **电镀笔帽**:离子质感电镀,亮泽圆滑,日久常新。 53 | 54 | ### 定制服务 55 | - **刻字服务**:提供个性化的刻字服务,增加礼品的纪念价值。 56 | 57 | ## 亮点 58 | 59 | - **匠心工艺**:近百年时光沉淀,融合现代智慧结晶。 60 | - **经典与现代**:莫兰迪色系与工作书写笔的完美结合。 61 | - **细节关怀**:笔盖可刻6个汉字或12个字母,笔杆可刻8个汉字或20个字母。 62 | - **品质保证**:金属材质,多次使用依旧不变形,提升整体握感。 63 | 64 | ## 温馨提示 65 | - 显示器存在色差,颜色以实际产品为准。 66 | - 刻字商品不支持退货,质量问题可更换相应问题部位。 67 | 68 | ## 字体与图案选择 69 | - 提供多种中文字体与英文字体选择。 70 | - 多种图案选择,可根据个人喜好定制。 71 | 72 | ## 价格说明 73 | - 价格以实时标价为准,具体成交价格可能因活动、优惠券等变化。 74 | - 商家详情页标注的价格可能为特定优惠活动下的价格。 -------------------------------------------------------------------------------- /static/product_files/instructions/perfume.md: -------------------------------------------------------------------------------- 1 | # 舒派白色薰衣草淡香氛说明书 2 | 3 | ## 产品名称 4 | 舒派白色薰衣草淡香氛 5 | 6 | ## 产品细节详情 7 | 8 | ### 产地 9 | - **法国** 10 | 11 | ### 包装种类 12 | - **礼盒装** 13 | 14 | ### 化妆品备案编号/注册证号 15 | - **国妆网备** 16 | 17 | ### 是否为特殊用途化妆品 18 | - **否** 19 | 20 | ### 净含量 21 | - **50ml** 22 | 23 | ### 消费提醒 24 | - 国家药监局提醒您:化妆品只能涂擦、喷洒或者其他类似方法施用于人体表面,不得食用或注射,请正确使用化妆品。 25 | 26 | ### 产品卖点 27 | 28 | #### 珍稀成分 29 | - **白色薰衣草**:颜色更淡雅,花香更清透,曾一度濒临灭绝,经7年研究自主培育后重绽光彩。 30 | 31 | #### 香调 32 | - **前调**:香柠檬、金桔叶 33 | - **中调**:白色薰衣草、白玫瑰 34 | - **后调**:雪松、白麝香 35 | 36 | #### 香氛体验 37 | - 创新运用香味更淡雅的白色薰衣草,搭配柔和的白玫瑰和温柔的白麝香,打造出如伪体香般的清新馥奇。 38 | 39 | ### 产品亮点 40 | 41 | #### 身心留白 42 | - 身心留白假日入梦,开启一场南法假日美梦。 43 | 44 | #### 可持续发展 45 | - 95%+成分源自自然,携手薰衣草捐赠基金会,践行“薰衣草有机培育计划”。 46 | 47 | #### 产品系列 48 | - 包括身体乳、沐浴露、护手霜等,提供全面的白色薰衣草系列体验。 49 | 50 | ### 使用方法 51 | - 轻轻喷洒于手腕或颈部附近。 52 | 53 | ### 注意事项 54 | 1. 远离热源或明火。 55 | 2. 本品含有芳樟醇、节烯、Q-异甲基紫罗兰酮、香叶醇、香茅醇、柠檬醛、香豆素。 56 | 57 | ### 产品成分 58 | - **变性乙醇、水、香精、薰衣草油** 59 | 60 | ### 产品规格 61 | - **产品编号**:7700xx 62 | - **限期使用日期**:见包装(因产品批次而异) 63 | 64 | ### 备案信息 65 | - **备案人/生产企业名称**:M&L实验室 66 | - **境内责任人名称**:舒派贸易(上海)有限公司 67 | 68 | ### 品质保证 69 | - 官方直供正品保证,享七天无理由退换货。 70 | 71 | ### 礼盒发放说明 72 | - 礼盒的发放数量取决于购买产品的总数量,不承诺一个产品或订单一个礼盒。 73 | 74 | ### 会员满额礼 75 | - 618活动期间购买正装商品且无退货行为,单笔订单实付满额赠礼。 76 | 77 | ### 价格说明 78 | - 商品的实时标价,具体成交价格根据商品参加活动或会员使用优惠券、积分等发生变化。 79 | 80 | ### 商家详情 81 | - 官方旗舰店所有产品都是官方直供官方直售原装产品,产品效期有保证。 -------------------------------------------------------------------------------- /static/product_files/instructions/shampoo.md: -------------------------------------------------------------------------------- 1 | # 鸡蛋花本草精华洗发露轻盈舒爽说明书 2 | 3 | ## 产品名称 4 | 鸡蛋花本草精华洗发露轻盈舒爽 5 | 6 | ## 产品细节详情 7 | 8 | ### 品牌 9 | - **品牌**:鸡蛋花 10 | 11 | ### 型号 12 | - **型号**:本草精华洗发露轻盈舒爽 13 | 14 | ### 净含量 15 | - **净含量**:500ml 16 | 17 | ### 产地 18 | - **产地**:中国大陆 19 | 20 | ### 香味 21 | - **香味**:果香、花香 22 | 23 | ### 适用发质 24 | - **适用发质**:油性至中性发质 25 | 26 | ### 功效 27 | - **功效**:保湿、改善发质 28 | 29 | ### 规格类型 30 | - **规格类型**:常规单品 31 | 32 | ### 特殊用途化妆品 33 | - **是否为特殊用途化妆品**:否 34 | 35 | ### 量贩装 36 | - **是否量贩装**:否 37 | 38 | ### 化妆品备案编号/注册证号 39 | - **备案编号/注册证号**:沪G妆网 40 | 41 | ### 是否进口 42 | - **是否进口**:否 43 | 44 | ### 生产厂家名称 45 | - **生产厂家名称**:江苏美丽化妆品股份有限公司 46 | 47 | ### 生产许可证号 48 | - **生产许可证号**:苏妆20xxx 49 | 50 | ### 注册人/备案人地址 51 | - **地址**:上海市闵行区北 52 | 53 | ### 注册人/备案人名称 54 | - **名称**:上海鸡蛋花日用品 55 | 56 | ### 消费提醒 57 | - **国家药监局提醒**:化妆品只能涂擦、喷酒或若其他类似方法施用于人体表面,不得食用或注射,请正确使用化妆品。 58 | 59 | ## 卖点 60 | 61 | ### 双重植萃精华 62 | - 蕴含夏枯草精华和桑叶精华,为秀发提供天然保湿与修护。 63 | 64 | ### 轻盈舒爽 65 | - 清爽如初,洗后不干涩,使秀发柔顺易梳理。 66 | 67 | ### 绵密泡沫 68 | - 质地轻盈,洗后秀发“喝饱”水,发丝不打结。 69 | 70 | ### 改善发质 71 | - 帮助改善油腻扁塌、分叉打结等秀发问题。 72 | 73 | ### 适用性广 74 | - 适用于油性至中性发质,满足不同消费者需求。 75 | 76 | ### 品牌故事 77 | - “鸡蛋花”是国内首位倡导洗发、护发分离概念的品牌,以亲民价格和优异品质赢得消费者青睐。 78 | 79 | ## 亮点 80 | 81 | ### 产品规格 82 | - 500ml/瓶,保质期3年(自生产日期起)。 83 | 84 | ### 成分 85 | - 包含水、夏枯草叶提取物、桑叶提取物等。 86 | 87 | ### 使用方法 88 | 1. 用温水湿发后,取适量洗发露于掌上。 89 | 2. 用指腹轻心,轻揉起泡。 90 | 3. 均匀涂抹于湿发,轻按摩头皮。 91 | 4. 以清水洗净。 92 | 93 | ### 包装变化 94 | - 新老包装随机发货,具体收到以实物为准。 95 | 96 | ### 价格说明 97 | - 商品的实时标价,具体成交价格根据商品参加活动或会员使用优惠券、积分等发生变化。 98 | 99 | ### 店铺活动 100 | - 年中开门红防脱固发强韧蓬松前9999份套装买1赠10等多重礼遇。 101 | 102 | ### 会员特权 103 | - 入会享新人礼包、购物享专属神券、充值享专属折扣等。 104 | 105 | ### 互动赢礼 106 | - 下单享专属加赠、会员享专属抽奖、互动赢专属壕礼等。 107 | 108 | 请按照说明书正确使用产品,以获得最佳效果。如有任何问题,欢迎咨询客服。 -------------------------------------------------------------------------------- /static/product_files/instructions/wok.md: -------------------------------------------------------------------------------- 1 | # 永牌炫彩易洁不粘煎炒锅说明书 2 | 3 | ## 产品名称 4 | - **型号**: EJ28RPxx/EC30SPxx/ECx2SPxx 5 | - **产品颜色**: 墨绿色 6 | - **产品规格**: 28cm/30cm/32cm 7 | - **锅盖类型**: 可立盖 8 | 9 | ## 产品细节详情 10 | - **品牌**: FOREVER/永牌 11 | - **材质**: 铝合金 12 | - **风格**: 中式 13 | - **产地**: 中国大陆 14 | - **流行元素**: 宫廷风 15 | - **深度**: 8.8cm 16 | - **净重**: 2.5kg 17 | - **设计使用年限**: 10年 18 | - **适用炉灶**: 燃气电磁炉通用 19 | 20 | ## 卖点与亮点 21 | 22 | ### 四大核心升级 23 | 1. **不粘升级**: 加大星星石撒点,提升不粘性能,无油煎蛋不翻车。 24 | 2. **翻炒升级**: 重量合适,R角翻炒弧设计,轻松翻炒。 25 | 3. **锅身升级**: 加高加厚锅身,更易导流。 26 | 4. **轻烟升级**: 少油轻烟,健康烹饪,洁净厨房。 27 | 28 | ### 锅具设计灵感 29 | - 源自法国巴黎圣母院哥特式建筑设计,将实用与美感相融合。 30 | 31 | ### 材质与工艺 32 | - **复合锅底**: 磁炉通用,适用于多种灶具。 33 | - **星星石不粘**: 易清洁,轻轻一擦,焕然如新。 34 | 35 | ### 使用与保养 36 | - **首次使用**: 37 | 1. 撕去标签,加水加热洗净。 38 | 2. 擦上植物油(牛油和猪油除外)。 39 | - **使用注意事项**: 40 | 1. 不使用铁铲和钢丝球。 41 | 2. 定期用清水煮一煮,保持不粘性。 42 | 43 | ### 产品保障 44 | - **一年质保**: 粘锅包退。 45 | - **保价30天**: 买贵退差价。 46 | - **七天无理由退换货**。 47 | 48 | ### 授权与正品保证 49 | - **授权编号**: SPT2411 50 | - **授权经销商**: 宁波厨房家居工贸有限公司 51 | - **授权渠道**: 天猫商城 52 | - **授权店铺**: 永牌厨房专卖店家居工贸有限公司 53 | 54 | ### 价格说明 55 | - **划线价格**: 非原价,仅供参考。 56 | - **未划线价格**: 实时标价,以订单结算页价格为准。 57 | -------------------------------------------------------------------------------- /static/product_files/instructions/yoga_mat.md: -------------------------------------------------------------------------------- 1 | # HEALTHY瑜伽垫产品说明书 2 | 3 | ## 产品名称 4 | - **HEALTHY瑜伽垫** 5 | 6 | ## 产品细节详情 7 | 8 | ### 材质与规格 9 | - **材质**: NBR 10 | - **厚度**: 10mm(初学者)、15mm(初学者) 11 | - **尺寸**: 12 | - 183*61cm(颜色:玫瑰粉、岩石灰、迷雾紫) 13 | - 185*80cm(颜色:樱花粉、岩石灰、薄荷绿) 14 | - 185*90cm(颜色:岩石灰、薄荷绿) 15 | 16 | ### 销售渠道 17 | - **纯电商**(只在线上销售) 18 | 19 | ### 产品特点 20 | - **环保耐用**: 无异味,专注训练不被打扰 21 | - **防水耐汗**: 清洗便捷,可直接水洗,晾干后即可使用 22 | - **轻便携带**: 赠送瑜伽专用绑带,随时随地运动 23 | - **权威检测**: 广东产品质量监督检验研究院检测报告 24 | 25 | ### 产品卖点 26 | - **六大核心优势**: 让训练更轻松 27 | - **环保耐用**: 质地柔软,加宽至80cm 28 | - **优质NBR材质**: 自在舒适运动保护关节 29 | - **防水耐汗**: 轻巧便携无负担 30 | - **加厚回弹**: 随手擦拭汗渍 31 | - **静音**: 一体成型无需锁边无胶水粘合 32 | - **大空间运动**: 不受限 33 | 34 | ### 产品亮点 35 | - **加宽加厚**: 185cm加长尺寸,80cm加宽区域 36 | - **静音设计**: 相当于悄悄说话声,不扰邻 37 | - **耐撕抗压**: 蜂窝结构工艺,使用更久 38 | - **沉浸式体验**: 10mm黄金比例厚度,静音更尽情 39 | 40 | ### 适用运动 41 | - **瑜伽**: 伸展、卷腹练习、肌肉拉伸 42 | - **有氧运动**: 轻巧便携,无负担 43 | 44 | ### 产品保养 45 | - **清洁**: 用湿抹布擦拭或清水清洗,平铺晾干 46 | - **收纳**: 将带有条纹的面朝内卷起收拢,防止出现褶皱或损坏 47 | - **气味**: 新瑜伽垫开封后会有材质本身的气味,通风处放置段时间,气味会散去 48 | - **温度**: 避免长时间靠近热源,防止老化 49 | 50 | ### 温馨提示 51 | - **产品尺寸**: 每个成品误差在1-5cm左右属于正常误差 52 | - **褶皱**: 运输过程会造成褶皱,平铺几天褶皱会消除 53 | - **色差**: 图片色差属于正常现象,请以实物为准 54 | 55 | ### 注意事项 56 | - **使用寿命**: 高品质健身垫一般能保持1-2年寿命 57 | - **包装**: 外包裹透明膜可直接撕开,避免使用剪刀 58 | - **课程**: 同款课程中使用的器械可能不完全相同 59 | 60 | ### 产品版本说明 61 | - **产品logo**: 处于更新阶段,版本随机发货 62 | 63 | ### 产品信息 64 | - **浸塑哑铃**: 材质浸塑铸铁,多种颜色和规格 65 | - **泡沫轴**: EVA材质,多种款式和颜色 66 | - **仰卧起坐辅助器**: PP材质,硅胶吸盘,NBR泡棉 67 | -------------------------------------------------------------------------------- /static/user/user-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterH0323/Streamer-Sales/11acb44b62edb93a1902d356f9c8b6f10a535cc8/static/user/user-avatar.png -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | class HParams: # Fix gpt-sovits torch.load 报缺少模块的问题 3 | def __init__(self, **kwargs): 4 | for k, v in kwargs.items(): 5 | if type(v) == dict: 6 | v = HParams(**v) 7 | self[k] = v 8 | 9 | def keys(self): 10 | return self.__dict__.keys() 11 | 12 | def items(self): 13 | return self.__dict__.items() 14 | 15 | def values(self): 16 | return self.__dict__.values() 17 | 18 | def __len__(self): 19 | return len(self.__dict__) 20 | 21 | def __getitem__(self, key): 22 | return getattr(self, key) 23 | 24 | def __setitem__(self, key, value): 25 | return setattr(self, key, value) 26 | 27 | def __contains__(self, key): 28 | return key in self.__dict__ 29 | 30 | def __repr__(self): 31 | return self.__dict__.__repr__() 32 | 33 | --------------------------------------------------------------------------------