From fff6f9f1e0cb44c14d9685f6d5ee2e1b88fd09e7 Mon Sep 17 00:00:00 2001 From: iflamed Date: Mon, 8 Jul 2024 18:51:06 +0800 Subject: [PATCH 01/10] add download models script and fastapi server to serve tts --- README.md | 20 ++++++++++++-------- download.py | 6 ++++++ main.py | 40 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 +++- 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 download.py create mode 100644 main.py diff --git a/README.md b/README.md index d341d97..0d9ca78 100644 --- a/README.md +++ b/README.md @@ -37,17 +37,13 @@ We strongly recommend that you download our pretrained `CosyVoice-300M` `CosyVoi If you are expert in this field, and you are only interested in training your own CosyVoice model from scratch, you can skip this step. -``` python -# SDK模型下载 -from modelscope import snapshot_download -snapshot_download('iic/CosyVoice-300M', local_dir='pretrained_models/CosyVoice-300M') -snapshot_download('iic/CosyVoice-300M-SFT', local_dir='pretrained_models/CosyVoice-300M-SFT') -snapshot_download('iic/CosyVoice-300M-Instruct', local_dir='pretrained_models/CosyVoice-300M-Instruct') -snapshot_download('iic/CosyVoice-ttsfrd', local_dir='pretrained_models/CosyVoice-ttsfrd') +Download models with python script. +``` shell +python download.py ``` +Download models with git, you should install `git lfs` first. ``` sh -# git模型下载,请确保已安装git lfs mkdir -p pretrained_models git clone https://www.modelscope.cn/iic/CosyVoice-300M.git pretrained_models/CosyVoice-300M git clone https://www.modelscope.cn/iic/CosyVoice-300M-SFT.git pretrained_models/CosyVoice-300M-SFT @@ -120,6 +116,14 @@ python3 webui.py --port 50000 --model_dir pretrained_models/CosyVoice-300M For advanced user, we have provided train and inference scripts in `examples/libritts/cosyvoice/run.sh`. You can get familiar with CosyVoice following this recipie. +**Serve with FastAPI** +```sh +# For development +fastapi dev --port 3003 +# For production +fastapi run --port 3003 +``` + **Build for deployment** Optionally, if you want to use grpc for service deployment, diff --git a/download.py b/download.py new file mode 100644 index 0000000..5890ac1 --- /dev/null +++ b/download.py @@ -0,0 +1,6 @@ +# SDK模型下载 +from modelscope import snapshot_download +snapshot_download('iic/CosyVoice-300M', local_dir='pretrained_models/CosyVoice-300M') +snapshot_download('iic/CosyVoice-300M-SFT', local_dir='pretrained_models/CosyVoice-300M-SFT') +snapshot_download('iic/CosyVoice-300M-Instruct', local_dir='pretrained_models/CosyVoice-300M-Instruct') +snapshot_download('iic/CosyVoice-ttsfrd', local_dir='pretrained_models/CosyVoice-ttsfrd') diff --git a/main.py b/main.py new file mode 100644 index 0000000..d212dd3 --- /dev/null +++ b/main.py @@ -0,0 +1,40 @@ +import io,time +from fastapi import FastAPI, Response +from fastapi.responses import HTMLResponse +from cosyvoice.cli.cosyvoice import CosyVoice +import torchaudio + +cosyvoice = CosyVoice('pretrained_models/CosyVoice-300M-SFT') +# sft usage +print(cosyvoice.list_avaliable_spks()) +app = FastAPI() + +@app.get("/api/voice/tts") +async def tts(query: str, role: str): + start = time.process_time() + output = cosyvoice.inference_sft(query, role) + end = time.process_time() + print("infer time:", end-start, "seconds") + buffer = io.BytesIO() + torchaudio.save(buffer, output['tts_speech'], 22050, format="wav") + buffer.seek(0) + return Response(content=buffer.read(-1), media_type="audio/wav") + +@app.get("/api/voice/roles") +async def roles(): + return {"roles": cosyvoice.list_avaliable_spks()} + +@app.get("/", response_class=HTMLResponse) +async def root(): + return """ + + + + + Api information + + + Get the supported tones from the Roles API first, then enter the tones and textual content in the TTS API for synthesis. Documents of API + + + """ diff --git a/requirements.txt b/requirements.txt index 39e1374..8129558 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,6 @@ soundfile==0.12.1 tensorboard==2.14.0 torch==2.0.1 torchaudio==2.0.2 -wget==3.2 \ No newline at end of file +wget==3.2 +fastapi==0.111.0 +fastapi-cli==0.0.4 \ No newline at end of file From 43b126adf3eda67c6f4a3c6a2bd22a657e769a93 Mon Sep 17 00:00:00 2001 From: iflamed Date: Mon, 8 Jul 2024 18:57:03 +0800 Subject: [PATCH 02/10] fix typo error --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0d9ca78..5ebecc1 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ For advanced user, we have provided train and inference scripts in `examples/lib You can get familiar with CosyVoice following this recipie. **Serve with FastAPI** +The `main.py` file has added a `TTS` api with `CosyVoice-300M-SFT` model, you can update the code based on **Basic Usage** as above. + ```sh # For development fastapi dev --port 3003 From 26719a169d290db74473be292636f7d51236ab8d Mon Sep 17 00:00:00 2001 From: iflamed Date: Mon, 8 Jul 2024 18:59:39 +0800 Subject: [PATCH 03/10] Update readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 5ebecc1..c08c898 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,10 @@ sudo yum install sox sox-devel ``` **Model download** +> *If you are expert in this field, and you are only interested in training your own CosyVoice model from scratch, you can skip this step.* We strongly recommend that you download our pretrained `CosyVoice-300M` `CosyVoice-300M-SFT` `CosyVoice-300M-Instruct` model and `CosyVoice-ttsfrd` resource. -If you are expert in this field, and you are only interested in training your own CosyVoice model from scratch, you can skip this step. - Download models with python script. ``` shell python download.py From a4ab4ead5f1281fab167653107082b480db30758 Mon Sep 17 00:00:00 2001 From: iflamed Date: Wed, 10 Jul 2024 19:53:43 +0800 Subject: [PATCH 04/10] support upload audio --- main.py | 40 ------------ runtime/python/fastapi_server.py | 102 +++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 40 deletions(-) delete mode 100644 main.py create mode 100644 runtime/python/fastapi_server.py diff --git a/main.py b/main.py deleted file mode 100644 index d212dd3..0000000 --- a/main.py +++ /dev/null @@ -1,40 +0,0 @@ -import io,time -from fastapi import FastAPI, Response -from fastapi.responses import HTMLResponse -from cosyvoice.cli.cosyvoice import CosyVoice -import torchaudio - -cosyvoice = CosyVoice('pretrained_models/CosyVoice-300M-SFT') -# sft usage -print(cosyvoice.list_avaliable_spks()) -app = FastAPI() - -@app.get("/api/voice/tts") -async def tts(query: str, role: str): - start = time.process_time() - output = cosyvoice.inference_sft(query, role) - end = time.process_time() - print("infer time:", end-start, "seconds") - buffer = io.BytesIO() - torchaudio.save(buffer, output['tts_speech'], 22050, format="wav") - buffer.seek(0) - return Response(content=buffer.read(-1), media_type="audio/wav") - -@app.get("/api/voice/roles") -async def roles(): - return {"roles": cosyvoice.list_avaliable_spks()} - -@app.get("/", response_class=HTMLResponse) -async def root(): - return """ - - - - - Api information - - - Get the supported tones from the Roles API first, then enter the tones and textual content in the TTS API for synthesis. Documents of API - - - """ diff --git a/runtime/python/fastapi_server.py b/runtime/python/fastapi_server.py new file mode 100644 index 0000000..f718373 --- /dev/null +++ b/runtime/python/fastapi_server.py @@ -0,0 +1,102 @@ +import os +import sys +import io,time +from fastapi import FastAPI, Response, File, UploadFile, Form +from fastapi.responses import HTMLResponse +from contextlib import asynccontextmanager +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append('{}/../..'.format(ROOT_DIR)) +sys.path.append('{}/../../third_party/Matcha-TTS'.format(ROOT_DIR)) +from cosyvoice.cli.cosyvoice import CosyVoice +from cosyvoice.utils.file_utils import load_wav +import numpy as np +import torch +import torchaudio +import logging +logging.getLogger('matplotlib').setLevel(logging.WARNING) + +class LaunchFailed(Exception): + pass + +@asynccontextmanager +async def lifespan(app: FastAPI): + model_dir = os.getenv("MODEL_DIR", "pretrained_models/CosyVoice-300M-SFT") + if model_dir: + logging.info("MODEL_DIR is {}", model_dir) + app.cosyvoice = CosyVoice('../../'+model_dir) + # sft usage + logging.info("Avaliable speakers {}", app.cosyvoice.list_avaliable_spks()) + else: + raise LaunchFailed("MODEL_DIR environment must set") + yield + +app = FastAPI(lifespan=lifespan) + +def buildResponse(output): + buffer = io.BytesIO() + torchaudio.save(buffer, output, 22050, format="wav") + buffer.seek(0) + return Response(content=buffer.read(-1), media_type="audio/wav") + +@app.post("/api/inference/sft") +@app.get("/api/inference/sft") +async def sft(tts: str = Form(), role: str = Form()): + start = time.process_time() + output = app.cosyvoice.inference_sft(tts, role) + end = time.process_time() + logging.info("infer time is {} seconds", end-start) + return buildResponse(output['tts_speech']) + +@app.post("/api/inference/zero-shot") +async def zeroShot(tts: str = Form(), prompt: str = Form(), audio: UploadFile = File()): + start = time.process_time() + prompt_speech = load_wav(audio.file, 16000) + prompt_audio = (prompt_speech.numpy() * (2**15)).astype(np.int16).tobytes() + prompt_speech_16k = torch.from_numpy(np.array(np.frombuffer(prompt_audio, dtype=np.int16))).unsqueeze(dim=0) + prompt_speech_16k = prompt_speech_16k.float() / (2**15) + + output = app.cosyvoice.inference_zero_shot(tts, prompt, prompt_speech_16k) + end = time.process_time() + logging.info("infer time is {} seconds", end-start) + return buildResponse(output['tts_speech']) + +@app.post("/api/inference/cross-lingual") +async def crossLingual(tts: str = Form(), audio: UploadFile = File()): + start = time.process_time() + prompt_speech = load_wav(audio.file, 16000) + prompt_audio = (prompt_speech.numpy() * (2**15)).astype(np.int16).tobytes() + prompt_speech_16k = torch.from_numpy(np.array(np.frombuffer(prompt_audio, dtype=np.int16))).unsqueeze(dim=0) + prompt_speech_16k = prompt_speech_16k.float() / (2**15) + + output = app.cosyvoice.inference_cross_lingual(tts, prompt_speech_16k) + end = time.process_time() + logging.info("infer time is {} seconds", end-start) + return buildResponse(output['tts_speech']) + +@app.post("/api/inference/instruct") +@app.get("/api/inference/instruct") +async def instruct(tts: str = Form(), role: str = Form(), instruct: str = Form()): + start = time.process_time() + output = app.cosyvoice.inference_instruct(tts, role, instruct) + end = time.process_time() + logging.info("infer time is {} seconds", end-start) + return buildResponse(output['tts_speech']) + +@app.get("/api/roles") +async def roles(): + return {"roles": app.cosyvoice.list_avaliable_spks()} + +@app.get("/", response_class=HTMLResponse) +async def root(): + return """ + + + + + Api information + + + Get the supported tones from the Roles API first, then enter the tones and textual content in the TTS API for synthesis. Documents of API + + + """ From eb53ccbc1967d7409976ab6ee7db676c69d265ee Mon Sep 17 00:00:00 2001 From: iflamed Date: Wed, 10 Jul 2024 23:11:45 +0800 Subject: [PATCH 05/10] add fastapi client --- README.md | 7 ++- runtime/python/fastapi_client.py | 78 ++++++++++++++++++++++++++++++++ runtime/python/fastapi_server.py | 7 +++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 runtime/python/fastapi_client.py diff --git a/README.md b/README.md index 23e4838..5cffa5d 100644 --- a/README.md +++ b/README.md @@ -121,10 +121,13 @@ You can get familiar with CosyVoice following this recipie. The `main.py` file has added a `TTS` api with `CosyVoice-300M-SFT` model, you can update the code based on **Basic Usage** as above. ```sh +cd runtime/python +# Set inference model +export MODEL_DIR=pretrained_models/CosyVoice-300M-Instruct # For development -fastapi dev --port 3003 +fastapi dev --port 6006 fastapi_server.py # For production -fastapi run --port 3003 +fastapi run --port 6006 fastapi_server.py ``` **Build for deployment** diff --git a/runtime/python/fastapi_client.py b/runtime/python/fastapi_client.py new file mode 100644 index 0000000..f4b3f12 --- /dev/null +++ b/runtime/python/fastapi_client.py @@ -0,0 +1,78 @@ +import argparse +import logging +import requests + +def saveResponse(path, response): + # 以二进制写入模式打开文件 + with open(path, 'wb') as file: + # 将响应的二进制内容写入文件 + file.write(response.content) + +def main(): + api = args.api_base + if args.mode == 'sft': + url = api + "/api/inference/sft" + payload={ + 'tts': args.tts_text, + 'role': args.spk_id + } + response = requests.request("POST", url, data=payload) + saveResponse(args.tts_wav, response) + elif args.mode == 'zero_shot': + url = api + "/api/inference/zero-shot" + payload={ + 'tts': args.tts_text, + 'prompt': args.prompt_text + } + files=[('audio', ('prompt_audio.wav', open(args.prompt_wav,'rb'), 'application/octet-stream'))] + response = requests.request("POST", url, data=payload, files=files) + saveResponse(args.tts_wav, response) + elif args.mode == 'cross_lingual': + url = api + "/api/inference/cross-lingual" + payload={ + 'tts': args.tts_text, + } + files=[('audio', ('prompt_audio.wav', open(args.prompt_wav,'rb'), 'application/octet-stream'))] + response = requests.request("POST", url, data=payload, files=files) + saveResponse(args.tts_wav, response) + else: + url = api + "/api/inference/instruct" + payload = { + 'tts': args.tts_text, + 'role': args.spk_id, + 'instruct': args.instruct_text + } + response = requests.request("POST", url, data=payload) + saveResponse(args.tts_wav, response) + logging.info("Response save to {}", args.tts_wav) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--api_base', + type=str, + default='http://127.0.0.1:6006') + parser.add_argument('--mode', + default='sft', + choices=['sft', 'zero_shot', 'cross_lingual', 'instruct'], + help='request mode') + parser.add_argument('--tts_text', + type=str, + default='你好,我是通义千问语音合成大模型,请问有什么可以帮您的吗?') + parser.add_argument('--spk_id', + type=str, + default='中文女') + parser.add_argument('--prompt_text', + type=str, + default='希望你以后能够做的比我还好呦。') + parser.add_argument('--prompt_wav', + type=str, + default='../../zero_shot_prompt.wav') + parser.add_argument('--instruct_text', + type=str, + default='Theo \'Crimson\', is a fiery, passionate rebel leader. Fights with fervor for justice, but struggles with impulsiveness.') + parser.add_argument('--tts_wav', + type=str, + default='demo.wav') + args = parser.parse_args() + prompt_sr, target_sr = 16000, 22050 + main() diff --git a/runtime/python/fastapi_server.py b/runtime/python/fastapi_server.py index f718373..2dbc619 100644 --- a/runtime/python/fastapi_server.py +++ b/runtime/python/fastapi_server.py @@ -1,3 +1,10 @@ +# Set inference model +# export MODEL_DIR=pretrained_models/CosyVoice-300M-Instruct +# For development +# fastapi dev --port 6006 fastapi_server.py +# For production deployment +# fastapi run --port 6006 fastapi_server.py + import os import sys import io,time From ee5cb5d2314f7807c0fbb9812b501a7d820243d2 Mon Sep 17 00:00:00 2001 From: iflamed Date: Wed, 10 Jul 2024 23:21:34 +0800 Subject: [PATCH 06/10] rewrite document --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cffa5d..e690c7e 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,8 @@ For advanced user, we have provided train and inference scripts in `examples/lib You can get familiar with CosyVoice following this recipie. **Serve with FastAPI** -The `main.py` file has added a `TTS` api with `CosyVoice-300M-SFT` model, you can update the code based on **Basic Usage** as above. + +The `runtime/python/fastapi_server.py` file contains the http API build with `FastAPI`. ```sh cd runtime/python From 75cb175ff507cc31dbc471998262ad5336d9bd9f Mon Sep 17 00:00:00 2001 From: iflamed Date: Wed, 10 Jul 2024 23:25:32 +0800 Subject: [PATCH 07/10] add http client demo for python --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e690c7e..5c40373 100644 --- a/README.md +++ b/README.md @@ -125,10 +125,14 @@ The `runtime/python/fastapi_server.py` file contains the http API build with `Fa cd runtime/python # Set inference model export MODEL_DIR=pretrained_models/CosyVoice-300M-Instruct + # For development -fastapi dev --port 6006 fastapi_server.py +fastapi dev --port 6006 fastapi_server.py # For production -fastapi run --port 6006 fastapi_server.py +fastapi run --port 6006 fastapi_server.py + +# Call the API with python client +python fastapi_client.py --api_base http://127.0.0.1:6006 --mode cross_lingual --tts_wav ./demo.wav ``` **Build for deployment** From 2e03e2e19b82b7a25df89257b0c81291c08fe20b Mon Sep 17 00:00:00 2001 From: iflamed Date: Wed, 10 Jul 2024 23:38:33 +0800 Subject: [PATCH 08/10] add git-lfs install link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c40373..66c87be 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Download models with python script. python download.py ``` -Download models with git, you should install `git lfs` first. +Download models with git, you should install [git lfs](https://github.com/git-lfs/git-lfs?utm_source=CosyVoice_site&utm_medium=download_models&utm_campaign=gitlfs#installing) first. ``` sh mkdir -p pretrained_models git clone https://www.modelscope.cn/iic/CosyVoice-300M.git pretrained_models/CosyVoice-300M From 3e87d925b86be0546685a43afcef043308e59152 Mon Sep 17 00:00:00 2001 From: iflamed Date: Thu, 11 Jul 2024 15:15:22 +0800 Subject: [PATCH 09/10] remove download.py --- README.md | 9 +++++++-- download.py | 6 ------ 2 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 download.py diff --git a/README.md b/README.md index 66c87be..0159b53 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,13 @@ sudo yum install sox sox-devel We strongly recommend that you download our pretrained `CosyVoice-300M` `CosyVoice-300M-SFT` `CosyVoice-300M-Instruct` model and `CosyVoice-ttsfrd` resource. Download models with python script. -``` shell -python download.py + +``` python +from modelscope import snapshot_download +snapshot_download('iic/CosyVoice-300M', local_dir='pretrained_models/CosyVoice-300M') +snapshot_download('iic/CosyVoice-300M-SFT', local_dir='pretrained_models/CosyVoice-300M-SFT') +snapshot_download('iic/CosyVoice-300M-Instruct', local_dir='pretrained_models/CosyVoice-300M-Instruct') +snapshot_download('iic/CosyVoice-ttsfrd', local_dir='pretrained_models/CosyVoice-ttsfrd') ``` Download models with git, you should install [git lfs](https://github.com/git-lfs/git-lfs?utm_source=CosyVoice_site&utm_medium=download_models&utm_campaign=gitlfs#installing) first. diff --git a/download.py b/download.py deleted file mode 100644 index 5890ac1..0000000 --- a/download.py +++ /dev/null @@ -1,6 +0,0 @@ -# SDK模型下载 -from modelscope import snapshot_download -snapshot_download('iic/CosyVoice-300M', local_dir='pretrained_models/CosyVoice-300M') -snapshot_download('iic/CosyVoice-300M-SFT', local_dir='pretrained_models/CosyVoice-300M-SFT') -snapshot_download('iic/CosyVoice-300M-Instruct', local_dir='pretrained_models/CosyVoice-300M-Instruct') -snapshot_download('iic/CosyVoice-ttsfrd', local_dir='pretrained_models/CosyVoice-ttsfrd') From 6faabaa703f3198b40268a422a200a84ecf7d04d Mon Sep 17 00:00:00 2001 From: iflamed Date: Thu, 11 Jul 2024 15:19:07 +0800 Subject: [PATCH 10/10] revert readme --- README.md | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0159b53..72b6e61 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ sudo yum install sox sox-devel ``` **Model download** -> *If you are expert in this field, and you are only interested in training your own CosyVoice model from scratch, you can skip this step.* We strongly recommend that you download our pretrained `CosyVoice-300M` `CosyVoice-300M-SFT` `CosyVoice-300M-Instruct` model and `CosyVoice-ttsfrd` resource. -Download models with python script. +If you are expert in this field, and you are only interested in training your own CosyVoice model from scratch, you can skip this step. ``` python +# SDK模型下载 from modelscope import snapshot_download snapshot_download('iic/CosyVoice-300M', local_dir='pretrained_models/CosyVoice-300M') snapshot_download('iic/CosyVoice-300M-SFT', local_dir='pretrained_models/CosyVoice-300M-SFT') @@ -48,8 +48,8 @@ snapshot_download('iic/CosyVoice-300M-Instruct', local_dir='pretrained_models/Co snapshot_download('iic/CosyVoice-ttsfrd', local_dir='pretrained_models/CosyVoice-ttsfrd') ``` -Download models with git, you should install [git lfs](https://github.com/git-lfs/git-lfs?utm_source=CosyVoice_site&utm_medium=download_models&utm_campaign=gitlfs#installing) first. ``` sh +# git模型下载,请确保已安装git lfs mkdir -p pretrained_models git clone https://www.modelscope.cn/iic/CosyVoice-300M.git pretrained_models/CosyVoice-300M git clone https://www.modelscope.cn/iic/CosyVoice-300M-SFT.git pretrained_models/CosyVoice-300M-SFT @@ -122,24 +122,6 @@ python3 webui.py --port 50000 --model_dir pretrained_models/CosyVoice-300M For advanced user, we have provided train and inference scripts in `examples/libritts/cosyvoice/run.sh`. You can get familiar with CosyVoice following this recipie. -**Serve with FastAPI** - -The `runtime/python/fastapi_server.py` file contains the http API build with `FastAPI`. - -```sh -cd runtime/python -# Set inference model -export MODEL_DIR=pretrained_models/CosyVoice-300M-Instruct - -# For development -fastapi dev --port 6006 fastapi_server.py -# For production -fastapi run --port 6006 fastapi_server.py - -# Call the API with python client -python fastapi_client.py --api_base http://127.0.0.1:6006 --mode cross_lingual --tts_wav ./demo.wav -``` - **Build for deployment** Optionally, if you want to use grpc for service deployment, @@ -170,4 +152,4 @@ You can also scan the QR code to join our official Dingding chat group. 5. We borrowed a lot of code from [WeNet](https://github.com/wenet-e2e/wenet). ## Disclaimer -The content provided above is for academic purposes only and is intended to demonstrate technical capabilities. Some examples are sourced from the internet. If any content infringes on your rights, please contact us to request its removal. +The content provided above is for academic purposes only and is intended to demonstrate technical capabilities. Some examples are sourced from the internet. If any content infringes on your rights, please contact us to request its removal. \ No newline at end of file