├── .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 |
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 |
82 |
83 |
84 | 首先我们来说下基本的文生图流程,首先加入 sd checkpoint ,和 vae 模型,vae 可选,但 sd 是必须的,如果觉得我这个模型不好,可以自行去 c站 找大佬微调好的模型,
85 |
86 | 填写好正向词和反向词,接个 Ksampler 就可以生成人像了
87 |
88 | ### 2. DW Pose 生成骨骼图 & ControlNet 控制人物姿态
89 |
90 |
91 |
92 |
93 |
94 | 人物生成好了,下一步要生成特定的动作的话,有时候语言很难描述,我们需要借助 controlnet 来结合 pose 的姿态图来让 sd 生成特定动作的任务,这就是左下角的作用
95 |
96 | ### 3. AnimateDiff 生成视频
97 |
98 |
99 |
100 |
101 |
102 | 这两块搞好之后,可以看到任务以特定的动作生成了,下面,我们加入动作,用到的算法是 Animatediff 简单的串起来,就可以了
103 |
104 | ### 4. 插帧提升帧率
105 |
106 |
107 |
108 |
109 |
110 | 我们把生成的图片合成为视频,原始是 8帧,我们对它进行一个插帧,让视频更加丝滑,这就是右上角的功能
111 |
112 | ### 5. 提升分辨率
113 |
114 |
115 |
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 |
6 |
7 |
8 |
9 |
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 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
销冠 - AI 卖货主播大模型
22 |
23 |
28 |
29 |
30 | 首页
31 |
32 |
33 |
34 | 商品管理
35 |
36 | 商品列表
37 |
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 |
64 |
65 |
66 |
67 |
133 |
--------------------------------------------------------------------------------
/frontend/src/components/BarChartComponent.vue:
--------------------------------------------------------------------------------
1 |
57 |
58 |
59 |
60 |
61 |
62 |
65 |
--------------------------------------------------------------------------------
/frontend/src/components/BreadCrumb.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
28 |
29 | {{ item.meta.title }}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/frontend/src/components/LineChartComponent.vue:
--------------------------------------------------------------------------------
1 |
120 |
121 |
122 |
123 |
124 |
125 |
128 |
--------------------------------------------------------------------------------
/frontend/src/components/MessageComponent.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 主播
43 |
44 |
45 |
{{ props.userName }}
46 |
47 |
{{ props.datetime }}
48 |
49 |
50 |
51 |
52 |
57 |
58 |
59 | {{ props.message }}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
89 |
--------------------------------------------------------------------------------
/frontend/src/components/NavbarComponent.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {{ userInfoItem.username }}
52 | 用户配置
53 |
54 |
55 |
56 | 退出系统
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
79 |
--------------------------------------------------------------------------------
/frontend/src/components/VideoComponent.vue:
--------------------------------------------------------------------------------
1 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/frontend/src/layouts/BaseLayout.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
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 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
84 |
85 |
86 |
87 |
97 |
--------------------------------------------------------------------------------
/frontend/src/views/digital-human/DigitalHumanView.vue:
--------------------------------------------------------------------------------
1 |
76 |
77 |
78 |
79 |
80 |
86 |
87 |
88 |
89 | 新增主播
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
{{ item.name }}
99 |
100 | {{ item.character }}
101 |
102 |
103 |
104 |
105 | 详情
106 |
107 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
156 |
--------------------------------------------------------------------------------
/frontend/src/views/error/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 404 Not Found
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/frontend/src/views/order/OrderView.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | {{ row.status }}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
91 |
--------------------------------------------------------------------------------
/frontend/src/views/system/SystemPluginsView.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {{ item.plugin_name }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
{{ item.plugin_name }}
36 |
{{ item.describe }}
37 |
38 |
39 |
40 | {{ item.enabled ? '已启动' : '未启动' }}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
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 |
--------------------------------------------------------------------------------