commit a817a8531610d365a5a41f14e05e21b780cb42df Author: quicksandzn Date: Fri Jun 20 11:49:50 2025 +0800 init diff --git a/.difyignore b/.difyignore new file mode 100644 index 0000000..9ac13a9 --- /dev/null +++ b/.difyignore @@ -0,0 +1,178 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Vscode +.vscode/ + +# Git +.git/ +.gitignore + +# Mac +.DS_Store + +# Windows +Thumbs.db diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b27dcb6 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +INSTALL_METHOD=remote +REMOTE_INSTALL_HOST=debug.dify.ai +REMOTE_INSTALL_PORT=5003 +REMOTE_INSTALL_KEY=********-****-****-****-************ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea98be9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,171 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Vscode +.vscode/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/GUIDE.md b/GUIDE.md new file mode 100644 index 0000000..0041b42 --- /dev/null +++ b/GUIDE.md @@ -0,0 +1,117 @@ +## User Guide of how to develop a Dify Plugin + +Hi there, looks like you have already created a Plugin, now let's get you started with the development! + +### Choose a Plugin type you want to develop + +Before start, you need some basic knowledge about the Plugin types, Plugin supports to extend the following abilities in Dify: +- **Tool**: Tool Providers like Google Search, Stable Diffusion, etc. it can be used to perform a specific task. +- **Model**: Model Providers like OpenAI, Anthropic, etc. you can use their models to enhance the AI capabilities. +- **Endpoint**: Like Service API in Dify and Ingress in Kubernetes, you can extend a http service as an endpoint and control its logics using your own code. + +Based on the ability you want to extend, we have divided the Plugin into three types: **Tool**, **Model**, and **Extension**. + +- **Tool**: It's a tool provider, but not only limited to tools, you can implement an endpoint there, for example, you need both `Sending Message` and `Receiving Message` if you are building a Discord Bot, **Tool** and **Endpoint** are both required. +- **Model**: Just a model provider, extending others is not allowed. +- **Extension**: Other times, you may only need a simple http service to extend the functionalities, **Extension** is the right choice for you. + +I believe you have chosen the right type for your Plugin while creating it, if not, you can change it later by modifying the `manifest.yaml` file. + +### Manifest + +Now you can edit the `manifest.yaml` file to describe your Plugin, here is the basic structure of it: + +- version(version, required):Plugin's version +- type(type, required):Plugin's type, currently only supports `plugin`, future support `bundle` +- author(string, required):Author, it's the organization name in Marketplace and should also equals to the owner of the repository +- label(label, required):Multi-language name +- created_at(RFC3339, required):Creation time, Marketplace requires that the creation time must be less than the current time +- icon(asset, required):Icon path +- resource (object):Resources to be applied + - memory (int64):Maximum memory usage, mainly related to resource application on SaaS for serverless, unit bytes + - permission(object):Permission application + - tool(object):Reverse call tool permission + - enabled (bool) + - model(object):Reverse call model permission + - enabled(bool) + - llm(bool) + - text_embedding(bool) + - rerank(bool) + - tts(bool) + - speech2text(bool) + - moderation(bool) + - node(object):Reverse call node permission + - enabled(bool) + - endpoint(object):Allow to register endpoint permission + - enabled(bool) + - app(object):Reverse call app permission + - enabled(bool) + - storage(object):Apply for persistent storage permission + - enabled(bool) + - size(int64):Maximum allowed persistent memory, unit bytes +- plugins(object, required):Plugin extension specific ability yaml file list, absolute path in the plugin package, if you need to extend the model, you need to define a file like openai.yaml, and fill in the path here, and the file on the path must exist, otherwise the packaging will fail. + - Format + - tools(list[string]): Extended tool suppliers, as for the detailed format, please refer to [Tool Guide](https://docs.dify.ai/docs/plugins/standard/tool_provider) + - models(list[string]):Extended model suppliers, as for the detailed format, please refer to [Model Guide](https://docs.dify.ai/docs/plugins/standard/model_provider) + - endpoints(list[string]):Extended Endpoints suppliers, as for the detailed format, please refer to [Endpoint Guide](https://docs.dify.ai/docs/plugins/standard/endpoint_group) + - Restrictions + - Not allowed to extend both tools and models + - Not allowed to have no extension + - Not allowed to extend both models and endpoints + - Currently only supports up to one supplier of each type of extension +- meta(object) + - version(version, required):manifest format version, initial version 0.0.1 + - arch(list[string], required):Supported architectures, currently only supports amd64 arm64 + - runner(object, required):Runtime configuration + - language(string):Currently only supports python + - version(string):Language version, currently only supports 3.12 + - entrypoint(string):Program entry, in python it should be main + +### Install Dependencies + +- First of all, you need a Python 3.11+ environment, as our SDK requires that. +- Then, install the dependencies: + ```bash + pip install -r requirements.txt + ``` +- If you want to add more dependencies, you can add them to the `requirements.txt` file, once you have set the runner to python in the `manifest.yaml` file, `requirements.txt` will be automatically generated and used for packaging and deployment. + +### Implement the Plugin + +Now you can start to implement your Plugin, by following these examples, you can quickly understand how to implement your own Plugin: + +- [OpenAI](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/openai): best practice for model provider +- [Google Search](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/google): a simple example for tool provider +- [Neko](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/neko): a funny example for endpoint group + +### Test and Debug the Plugin + +You may already noticed that a `.env.example` file in the root directory of your Plugin, just copy it to `.env` and fill in the corresponding values, there are some environment variables you need to set if you want to debug your Plugin locally. + +- `INSTALL_METHOD`: Set this to `remote`, your plugin will connect to a Dify instance through the network. +- `REMOTE_INSTALL_HOST`: The host of your Dify instance, you can use our SaaS instance `https://debug.dify.ai`, or self-hosted Dify instance. +- `REMOTE_INSTALL_PORT`: The port of your Dify instance, default is 5003 +- `REMOTE_INSTALL_KEY`: You should get your debugging key from the Dify instance you used, at the right top of the plugin management page, you can see a button with a `debug` icon, click it and you will get the key. + +Run the following command to start your Plugin: + +```bash +python -m main +``` + +Refresh the page of your Dify instance, you should be able to see your Plugin in the list now, but it will be marked as `debugging`, you can use it normally, but not recommended for production. + +### Package the Plugin + +After all, just package your Plugin by running the following command: + +```bash +dify-plugin plugin package ./ROOT_DIRECTORY_OF_YOUR_PLUGIN +``` + +you will get a `plugin.difypkg` file, that's all, you can submit it to the Marketplace now, look forward to your Plugin being listed! + + +## User Privacy Policy + +Please fill in the privacy policy of the plugin if you want to make it published on the Marketplace, refer to [PRIVACY.md](PRIVACY.md) for more details. \ No newline at end of file diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..efd92e5 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,27 @@ +## Privacy + +### Introduction +We are committed to protecting your privacy. This Privacy Policy outlines that we do not collect, use, or process any personal data from our users. + +### Information Collection +We do not collect any personal information through our plugin. + + +### Use of Information +Since we do not collect any personal information, there is no data to use for any purpose. + +### Sharing of Information +We do not share any information with third parties, as we do not collect any data. + +### Data Security +As no data is collected, there is no need for data security measures related to personal information. + +### Your Rights +You retain full privacy rights, as no personal data is collected or stored. + +### Changes to This Policy +We reserve the right to update or modify this Privacy Policy at any time. Changes will be effective immediately upon posting to our website. + +### Contact Us +If you have any questions about this Privacy Policy, please contact us at [quicksandzn@gmail.com]. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b45d48f --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +## Elasticsearch + +- **Author:** [quicksandzn](https://github.com/quicksandznzn) +- **Github:** https://github.com/quicksandznzn/dify-plugin-minimax-kit +- **Version:** 0.0.1 +- **Type:** tool + +### Description +- Image Generation +- Music Generation +- Voice Clone +- Video Generation + +### Usage + +- Set Up Authorization + +![img.png](_assets/img.png) + +- Image Generation + +![img.png](_assets/image_generation.png) + +- Music Generation + +![img.png](_assets/music_generation.png) + +- Voice Clone + +![img.png](_assets/voice_clone.png) + +- Video Generation + +![img.png](_assets/video_generation.png) + diff --git a/_assets/icon.svg b/_assets/icon.svg new file mode 100644 index 0000000..2a60bd4 --- /dev/null +++ b/_assets/icon.svg @@ -0,0 +1 @@ +Minimax \ No newline at end of file diff --git a/_assets/image_generation.png b/_assets/image_generation.png new file mode 100644 index 0000000..23fa7fb Binary files /dev/null and b/_assets/image_generation.png differ diff --git a/_assets/img.png b/_assets/img.png new file mode 100644 index 0000000..937dd4c Binary files /dev/null and b/_assets/img.png differ diff --git a/_assets/music_generation.png b/_assets/music_generation.png new file mode 100644 index 0000000..bfb36d5 Binary files /dev/null and b/_assets/music_generation.png differ diff --git a/_assets/video_generation.png b/_assets/video_generation.png new file mode 100644 index 0000000..e498171 Binary files /dev/null and b/_assets/video_generation.png differ diff --git a/_assets/voice_clone.png b/_assets/voice_clone.png new file mode 100644 index 0000000..d8199a1 Binary files /dev/null and b/_assets/voice_clone.png differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..568839f --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +from dify_plugin import Plugin, DifyPluginEnv + +plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120)) + +if __name__ == "__main__": + plugin.run() diff --git a/manifest.yaml b/manifest.yaml new file mode 100644 index 0000000..8848099 --- /dev/null +++ b/manifest.yaml @@ -0,0 +1,31 @@ +version: 0.0.1 +type: plugin +author: quicksandzn +name: minimax_kit +label: + en_US: MiniMax Kit + zh_Hans: MiniMax 工具箱 +description: + en_US: MiniMax Kit + zh_Hans: MiniMax 工具箱 +icon: icon.svg +resource: + memory: 268435456 + permission: + tool: + enabled: true +plugins: + tools: + - provider/minimax.yaml +meta: + version: 0.0.1 + arch: + - amd64 + - arm64 + runner: + language: python + version: "3.12" + entrypoint: main +created_at: 2025-06-17T14:55:25.497297+08:00 +privacy: PRIVACY.md +verified: false diff --git a/provider/minimax.py b/provider/minimax.py new file mode 100644 index 0000000..454797f --- /dev/null +++ b/provider/minimax.py @@ -0,0 +1,22 @@ +from typing import Any +from dify_plugin import ToolProvider +from dify_plugin.errors.tool import ToolProviderCredentialValidationError +from tools.base import MiniMaxBaseTool + + +class MiniMaxProvider(ToolProvider): + def _validate_credentials(self, credentials: dict[str, Any]) -> None: + try: + api_key = credentials.get("api_key") + group_id = credentials.get("group_id") + response = MiniMaxBaseTool(api_key=api_key, group_id=group_id).file_list( + purpose="retrieval" + ) + response.raise_for_status() + status_code = response.json().get("base_resp", {}).get("status_code", -1) + if status_code != 0: + raise ToolProviderCredentialValidationError( + f"Invalid credentials. Please check your API key and group ID. {response.text}" + ) + except Exception as e: + raise ToolProviderCredentialValidationError(str(e)) diff --git a/provider/minimax.yaml b/provider/minimax.yaml new file mode 100644 index 0000000..f92d241 --- /dev/null +++ b/provider/minimax.yaml @@ -0,0 +1,45 @@ +identity: + author: quicksandzn + name: minimax_kit + label: + en_US: MiniMax Kit + zh_Hans: MiniMax Kit + description: + en_US: MiniMax Kit + zh_Hans: MiniMax Kit + icon: icon.svg +credentials_for_provider: + api_key: + type: secret-input + required: true + label: + en_US: Api Key + zh_Hans: Api Key + placeholder: + en_US: Please input your Api Key + zh_Hans: 请输入你的 Api Key + help: + en_US: Get your Api Key from MiniMax + zh_Hans: 从 MiniMax 获取您的 Api Key + url: https://platform.minimaxi.com/user-center/basic-information/interface-key + group_id: + type: secret-input + required: true + label: + en_US: Group ID + zh_Hans: Group ID + placeholder: + en_US: Please input your Group ID + zh_Hans: 请输入你的 Group ID + help: + en_US: Get your Group ID from MiniMax + zh_Hans: 从 MiniMax 获取您的 Group ID + url: https://platform.minimaxi.com/user-center/basic-information +tools: + - tools/image_generation.yaml + - tools/music_generation.yaml + - tools/voice_clone.yaml + - tools/video_generation.yaml +extra: + python: + source: provider/minimax.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8596ddb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +dify_plugin>=0.3.0,<0.4.0 +requests>=2.31.0 \ No newline at end of file diff --git a/tools/base.py b/tools/base.py new file mode 100644 index 0000000..27af2e2 --- /dev/null +++ b/tools/base.py @@ -0,0 +1,165 @@ +import requests + +API_ENDPOINT = "https://api.minimaxi.com/v1" + + +class MiniMaxBaseTool: + def __init__(self, api_key: str, group_id: str): + self.api_key = api_key + self.group_id = group_id + if not self.api_key: + raise ValueError("Api key are required") + if not self.group_id: + raise ValueError("Group id are required") + + def _get_headers(self) -> dict: + headers = { + "Authorization": f"Bearer {self.api_key}", + } + return headers + + def _request(self, method: str, url: str, **kwargs) -> requests.Response: + return requests.request(method, url, headers=self._get_headers(), **kwargs) + + def text_to_image( + self, + model: str, + prompt: str, + aspect_ratio: str, + response_format: str, + prompt_optimizer: bool, + n: int, + ) -> requests.Response: + response = self._request( + "POST", + f"{API_ENDPOINT}/image_generation", + json={ + "model": model, + "prompt": prompt, + "aspect_ratio": aspect_ratio, + "response_format": response_format, + "prompt_optimizer": prompt_optimizer, + "n": n, + }, + ) + return response + + def music_upload( + self, file_name: str, file_blob: bytes, mime_type: str + ) -> requests.Response: + files = [("file", (file_name, file_blob, mime_type))] + response = self._request( + "POST", + f"{API_ENDPOINT}/music_upload", + data={"purpose": "song"}, + files=files, + ) + return response + + def music_generation( + self, + model: str, + refer_voice: str, + refer_instrumental: str, + refer_vocal: str, + lyrics: str, + ) -> requests.Response: + response = self._request( + "POST", + f"{API_ENDPOINT}/music_generation", + json={ + "model": model, + "refer_voice": refer_voice, + "refer_instrumental": refer_instrumental, + "refer_vocal": refer_vocal, + "lyrics": lyrics, + }, + ) + return response + + def file_upload( + self, file_name: str, file_blob: bytes, mime_type: str, purpose: str + ) -> requests.Response: + files = [("file", (file_name, file_blob, mime_type))] + response = self._request( + "POST", + f"{API_ENDPOINT}/files/upload", + params={ + "GroupId": self.group_id, + }, + data={"purpose": purpose}, + files=files, + ) + return response + + def voice_clone( + self, + model: str, + file_id: str, + voice_id: str, + text: str, + accuracy: float, + need_noise_reduction: bool, + need_volume_normalization: bool, + ) -> requests.Response: + response = self._request( + "POST", + f"{API_ENDPOINT}/voice_clone", + json={ + "model": model, + "file_id": file_id, + "voice_id": voice_id, + "text": text, + "accuracy": accuracy, + "need_noise_reduction": need_noise_reduction, + "need_volume_normalization": need_volume_normalization, + }, + ) + return response + + def video_generation_task(self, task_id: str): + response = self._request( + "GET", + f"{API_ENDPOINT}/query/video_generation", + params={"task_id": task_id}, + ) + return response + + def video_generation( + self, + model: str, + prompt: str, + prompt_optimizer: bool, + duration: int, + resolution: str, + first_frame_image: str, + ) -> requests.Response: + response = self._request( + "POST", + f"{API_ENDPOINT}/video_generation", + data={ + "model": model, + "prompt": prompt, + "prompt_optimizer": prompt_optimizer, + "duration": duration, + "resolution": resolution, + "first_frame_image": first_frame_image, + }, + ) + return response + + def file_retrieve(self, file_id: str) -> requests.Response: + response = self._request( + "GET", + f"{API_ENDPOINT}/files/retrieve", + params={"GroupId": self.group_id, "file_id": file_id}, + ) + return response + + def file_list(self, purpose: str) -> requests.Response: + response = self._request( + "GET", + f"{API_ENDPOINT}/files/list", + params={"GroupId": self.group_id, "purpose": purpose}, + ) + return response diff --git a/tools/image_generation.py b/tools/image_generation.py new file mode 100644 index 0000000..9c4dbbc --- /dev/null +++ b/tools/image_generation.py @@ -0,0 +1,51 @@ +from collections.abc import Generator +from typing import Any +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage +from tools.base import MiniMaxBaseTool + + +class MiniMaxImageGenerationTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + api_key = self.runtime.credentials.get("api_key") + group_id = self.runtime.credentials.get("group_id") + minimax = MiniMaxBaseTool(api_key=api_key, group_id=group_id) + + model = tool_parameters.get("model") + prompt = tool_parameters.get("prompt") + aspect_ratio = tool_parameters.get("aspect_ratio") + prompt_optimizer = tool_parameters.get("prompt_optimizer") + n = tool_parameters.get("n") + + response = minimax.text_to_image( + model=model, + prompt=prompt, + aspect_ratio=aspect_ratio, + response_format="url", + prompt_optimizer=prompt_optimizer, + n=n, + ) + if response.status_code != 200: + yield self.create_text_message( + f"Image generation failed {response.status_code} {response.text}" + ) + return + status_code = response.json().get("base_resp", {}).get("status_code", -1) + if status_code != 0: + yield self.create_text_message(f"Image generation failed {response.text}") + return + image_data = response.json().get("data", {}) + image_urls = image_data.get("image_urls") + + if not image_urls: + yield self.create_text_message(f"Image generation failed {response.text}") + return + + for image_url in image_urls: + yield self.create_image_message(image_url) + + image_data = { + "image_urls": image_urls, + } + + yield self.create_json_message(image_data) diff --git a/tools/image_generation.yaml b/tools/image_generation.yaml new file mode 100644 index 0000000..0c45d96 --- /dev/null +++ b/tools/image_generation.yaml @@ -0,0 +1,102 @@ +identity: + name: image_generation + author: quicksandzn + label: + en_US: Image Generation + zh_Hans: 图片生成 +description: + human: + en_US: Image Generation + zh_Hans: 图片生成 + llm: Generate images using text prompt words +parameters: + - name: model + type: select + required: true + options: + - value: image-01 + label: + en_US: image-01 + zh_Hans: image-01 + - value: image-01-live + label: + en_US: image-01-live + zh_Hans: image-01-live + default: image-01 + label: + en_US: Model Name + zh_Hans: 模型名称 + human_description: + en_US: Model Name + zh_CN: 模型名称 + form: form + - name: prompt + type: string + required: true + label: + en_US: Prompt + zh_Hans: 文本提示词 + human_description: + en_US: Generate the description of the image. + zh_CN: 生成图像的描述 + form: llm + - name: aspect_ratio + type: select + required: false + options: + - value: "1:1" + label: + en_US: "1:1" + zh_Hans: " 1:1" + - value: "16:9" + label: + en_US: "16:9" + zh_Hans: "16:9" + - value: "4:3" + label: + en_US: "4:3" + zh_Hans: "4:3" + - value: "3:2" + label: + en_US: "3:2" + zh_Hans: "3:2" + - value: "2:3" + label: + en_US: "2:3" + zh_Hans: "2:3" + - value: "3:4" + label: + en_US: "3:4" + zh_Hans: "3:4" + - value: "9:16" + label: + en_US: "9:16" + zh_Hans: "9:16" + - value: "21:9" + label: + en_US: "21:9" + zh_Hans: "21:9" + default: "1:1" + label: + en_US: Aspect Ratio + zh_Hans: 宽高比 + human_description: + en_US: Used to control the aspect ratio of the generated image + zh_CN: 用于控制生成图像的宽高比 + form: form + - name: n + type: number + required: false + default: 1 + max: 9 + min: 1 + label: + en_US: Number of generated + zh_Hans: 生成数量 + human_description: + en_US: Generate the description of the image. + zh_CN: Used to control the number of images generated in a single request + form: form +extra: + python: + source: tools/image_generation.py diff --git a/tools/music_generation.py b/tools/music_generation.py new file mode 100644 index 0000000..de22b2e --- /dev/null +++ b/tools/music_generation.py @@ -0,0 +1,65 @@ +from collections.abc import Generator +from typing import Any +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage +from tools.base import MiniMaxBaseTool + + +class MiniMaxMusicGenerationTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + api_key = self.runtime.credentials.get("api_key") + group_id = self.runtime.credentials.get("group_id") + minimax = MiniMaxBaseTool(api_key=api_key, group_id=group_id) + + model = tool_parameters.get("model") + song_file = tool_parameters.get("song") + refer_vocal = tool_parameters.get("refer_vocal") + lyrics = tool_parameters.get("lyrics") + + upload_response = minimax.music_upload( + file_name=song_file.filename, + file_blob=song_file.blob, + mime_type=song_file.mime_type, + ) + if upload_response.status_code != 200: + yield self.create_text_message( + f"Music generation upload failed {upload_response.status_code} {upload_response.text}" + ) + return + upload_data = upload_response.json() + voice_id = upload_data.get("voice_id") + instrumental_id = upload_data.get("instrumental_id") + if not voice_id or not instrumental_id: + yield self.create_text_message( + f"Music generation upload failed {upload_response.text}" + ) + return + gen_response = minimax.music_generation( + model=model, + refer_voice=voice_id, + refer_instrumental=instrumental_id, + refer_vocal=None if not refer_vocal else refer_vocal, + lyrics=lyrics, + ) + if gen_response.status_code != 200: + yield self.create_text_message( + f"Music generation failed {gen_response.status_code} {gen_response.text}" + ) + return + status_code = gen_response.json().get("base_resp", {}).get("status_code", -1) + if status_code != 0: + yield self.create_text_message( + f"Music generation failed {gen_response.text}" + ) + return + audio_hex = gen_response.json().get("data", {}).get("audio") + + if not audio_hex: + yield self.create_text_message( + f"Music generation failed {gen_response.text}" + ) + return + (self.create_text_message("Audio generated successfully"),) + yield self.create_blob_message( + blob=bytes.fromhex(audio_hex), meta={"mime_type": "audio/mpeg"} + ) diff --git a/tools/music_generation.yaml b/tools/music_generation.yaml new file mode 100644 index 0000000..60f6f4d --- /dev/null +++ b/tools/music_generation.yaml @@ -0,0 +1,64 @@ +identity: + name: music_generation + author: quicksandzn + label: + en_US: Music Generation + zh_Hans: 音乐生成 +description: + human: + en_US: Music Generation + zh_Hans: 音乐生成 + llm: Music Generation +parameters: + - name: model + type: select + required: true + options: + - value: music-01 + label: + en_US: music-01 + zh_Hans: music-01 + default: music-01 + label: + en_US: Model Name + zh_Hans: 模型名称 + human_description: + en_US: Model Name + zh_CN: 模型名称 + form: form + - name: song + type: file + required: true + label: + en_US: Song File + zh_Hans: 歌曲文件 + human_description: + en_US: Song File + zh_CN: 歌曲文件 + form: llm + - name: refer_vocal + type: string + required: false + label: + en_US: Voice ID + zh_Hans: 声音ID + human_description: + en_US: The sound ID used to replace the generated music timbre when generating music + zh_CN: 生成音乐时用来替换生成音乐音色的声音ID + form: form + - name: lyrics + type: string + required: false + label: + en_US: Lyrics + zh_Hans: 歌词 + help: + en_US: "Lyrics: Use line breaks (\n) to separate each line of lyrics. Use two consecutive line breaks (\n\n) to add pauses in the middle of the lyrics. Use double hyphens (##) at the beginning and end to add accompaniment. Supports up to 200 characters (each Chinese character, punctuation mark, and letter counts as one character)." + zh_CN: "歌词,使用换行符(\n)分隔每行歌词,使用两个连续换行符(\n\n)可以在歌词中间添加停顿,使用双井号(##)添加在首尾可以添加伴奏,支持最长200字符(每个汉字、标点和字母都算1个字符)。" + human_description: + en_US: Lyrics + zh_CN: 歌词 + form: llm +extra: + python: + source: tools/music_generation.py diff --git a/tools/video_generation.py b/tools/video_generation.py new file mode 100644 index 0000000..79aa5ab --- /dev/null +++ b/tools/video_generation.py @@ -0,0 +1,103 @@ +import time +from collections.abc import Generator +from typing import Any +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage +from tools.base import MiniMaxBaseTool +import logging +from dify_plugin.config.logger_format import plugin_logger_handler + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logger.addHandler(plugin_logger_handler) + + +class MiniMaxVideoGenerationTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + api_key = self.runtime.credentials.get("api_key") + group_id = self.runtime.credentials.get("group_id") + minimax = MiniMaxBaseTool(api_key=api_key, group_id=group_id) + + model = tool_parameters.get("model") + prompt = tool_parameters.get("prompt") + prompt_optimizer = tool_parameters.get("prompt_optimizer") + duration = tool_parameters.get("duration") + resolution = tool_parameters.get("resolution") + first_frame_image = tool_parameters.get("first_frame_image") + + response = minimax.video_generation( + model=model, + prompt=prompt, + prompt_optimizer=prompt_optimizer, + duration=duration, + resolution=resolution, + first_frame_image=None if not first_frame_image else first_frame_image, + ) + if response.status_code != 200: + yield self.create_text_message( + f"Video generation failed {response.status_code} {response.text}" + ) + return + task_id = response.json().get("task_id") + if not task_id: + yield self.create_text_message(f"Video generation failed {response.text}") + return + yield self.create_text_message(f"Video generation task id {task_id}") + max_retries = 100 + retry_count = 0 + interval = 5 + video_file_id = None + + while retry_count < max_retries: + time.sleep(interval) + + task_response = minimax.video_generation_task(task_id=task_id) + if task_response.status_code != 200: + yield self.create_text_message( + f"Video generation task failed {task_response.status_code} {task_response.text}" + ) + break + task_json = task_response.json() + status_code = task_json.get("base_resp", {}).get("status_code", -1) + if status_code != 0: + yield self.create_text_message( + f"Video generation task failed {task_response.text}" + ) + break + + task_status = task_json.get("status") + + match task_status: + case "Preparing": + logger.debug("Video generation task status preparing") + case "Queueing": + logger.debug("Video generation task status queueing") + case "Processing": + logger.debug("Video generation task status processing") + case "Success": + video_file_id = task_json.get("file_id") + yield self.create_text_message( + "Video generation task status Success" + ) + break + case "failed": + yield self.create_text_message( + f"Video generation task status failed {task_response.text}" + ) + break + + retry_count += 1 + + if not video_file_id: + yield self.create_text_message("Video generation failed") + return + file_response = minimax.file_retrieve(file_id=video_file_id) + if file_response.status_code != 200: + yield self.create_text_message( + f"Video generation get file failed {file_response.status_code} {file_response.text}" + ) + return + video_url = file_response.json().get("file", {}).get("download_url") + yield self.create_image_message(video_url) + video_data = {"video_url": video_url} + yield self.create_json_message(video_data) diff --git a/tools/video_generation.yaml b/tools/video_generation.yaml new file mode 100644 index 0000000..b78487e --- /dev/null +++ b/tools/video_generation.yaml @@ -0,0 +1,126 @@ +identity: + name: video_generation + author: quicksandzn + label: + en_US: Video Generation + zh_Hans: 视频生成 +description: + human: + en_US: Video Generation + zh_Hans: 视频生成 + llm: Video Generation +parameters: + - name: model + type: select + required: true + options: + - value: MiniMax-Hailuo-02 + label: + en_US: MiniMax-Hailuo-02 + zh_Hans: MiniMax-Hailuo-02 + - value: T2V-01-Director + label: + en_US: T2V-01-Director + zh_Hans: T2V-01-Director + - value: I2V-01-Director + label: + en_US: I2V-01-Director + zh_Hans: I2V-01-Director + - value: S2V-01 + label: + en_US: S2V-01 + zh_Hans: S2V-01 + - value: I2V-01-live + label: + en_US: I2V-01-live + zh_Hans: I2V-01-live + - value: I2V-01 + label: + en_US: I2V-01 + zh_Hans: I2V-01 + - value: T2V-01 + label: + en_US: T2V-01 + zh_Hans: T2V-01 + default: MiniMax-Hailuo-02 + label: + en_US: Model Name + zh_Hans: 模型名称 + human_description: + en_US: Model Name + zh_CN: 模型名称 + form: form + - name: prompt + type: string + required: false + label: + en_US: Prompt + zh_Hans: 文本提示词 + human_description: + en_US: Generate the description of the video. + zh_CN: 生成视频的描述 + form: llm + - name: prompt_optimizer + type: boolean + required: false + default: true + label: + en_US: Prompt Optimizer + zh_Hans: 提示词优化 + human_description: + en_US: The model will automatically optimize the incoming prompt + zh_CN: 模型会自动优化传入的prompt + form: form + - name: duration + type: select + required: true + options: + - value: "6" + label: + en_US: "6" + zh_Hans: "6" + - value: "10" + label: + en_US: "10" + zh_Hans: "10" + default: "6" + label: + en_US: Video Duration + zh_Hans: 视频时长 + human_description: + en_US: Generated video duration + zh_CN: 生成视频时长 + form: form + - name: resolution + type: select + required: true + options: + - value: 768P + label: + en_US: 768P + zh_Hans: 768P + - value: 1080P + label: + en_US: 1080P + zh_Hans: 1080P + default: 768P + label: + en_US: Resolution + zh_Hans: 分辨率 + human_description: + en_US: Resolution + zh_CN: 分辨率 + form: form + - name: first_frame_image + type: string + required: false + label: + en_US: First Frame Image + zh_Hans: 首帧画面 + human_description: + en_US: The model will generate a video based on the image passed in this parameter as the first frame + zh_CN: 模型将以此参数中传入的图片为首帧画面来生成视频 + form: form +extra: + python: + source: tools/video_generation.py diff --git a/tools/voice_clone.py b/tools/voice_clone.py new file mode 100644 index 0000000..5a14724 --- /dev/null +++ b/tools/voice_clone.py @@ -0,0 +1,77 @@ +import uuid +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage +from tools.base import MiniMaxBaseTool + + +class MiniMaxVoiceCloneTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + api_key = self.runtime.credentials.get("api_key") + group_id = self.runtime.credentials.get("group_id") + minimax = MiniMaxBaseTool(api_key=api_key, group_id=group_id) + + model = tool_parameters.get("model") + ref_voice = tool_parameters.get("ref_voice") + voice_id = tool_parameters.get("voice_id") + if not voice_id: + voice_id = f"voice_{uuid.uuid4()}" + text = tool_parameters.get("text") + accuracy = tool_parameters.get("accuracy", 0.7) + need_noise_reduction = tool_parameters.get("need_noise_reduction", False) + need_volume_normalization = tool_parameters.get( + "need_volume_normalization", False + ) + + upload_response = minimax.file_upload( + file_name=ref_voice.filename, + file_blob=ref_voice.blob, + mime_type=ref_voice.mime_type, + purpose="voice_clone", + ) + if upload_response.status_code != 200: + yield self.create_text_message( + f"Voice clone upload failed {upload_response.status_code} {upload_response.text}" + ) + return + upload_data = upload_response.json().get("file", {}) + file_id = upload_data.get("file_id") + if not file_id: + yield self.create_text_message( + f"Voice clone upload failed {upload_response.text}" + ) + return + clone_response = minimax.voice_clone( + model=model, + file_id=file_id, + voice_id=voice_id, + text=text, + accuracy=accuracy, + need_noise_reduction=need_noise_reduction, + need_volume_normalization=need_volume_normalization, + ) + if clone_response.status_code != 200: + yield self.create_text_message( + f"Voice clone failed {clone_response.status_code} {clone_response.text}" + ) + return + status_code = clone_response.json().get("base_resp", {}).get("status_code", -1) + if status_code != 0: + yield self.create_text_message(f"Voice clone failed {clone_response.text}") + return + + demo_audio = clone_response.json().get("demo_audio") + + if demo_audio: + yield self.create_text_message(demo_audio) + + # response = requests.get(demo_audio, timeout=60) + # response.raise_for_status() + # yield self.create_blob_message( + # blob=response.content, meta={"mime_type": "audio/mpeg"} + # ) + + voice_clone_data = {"voice_id": voice_id, "demo_audio": demo_audio} + yield self.create_json_message(voice_clone_data) diff --git a/tools/voice_clone.yaml b/tools/voice_clone.yaml new file mode 100644 index 0000000..9e94e9b --- /dev/null +++ b/tools/voice_clone.yaml @@ -0,0 +1,109 @@ +identity: + name: voice_clone + author: quicksandzn + label: + en_US: Voice Clone + zh_Hans: 声音克隆 +description: + human: + en_US: Voice Clone + zh_Hans: 声音克隆 + llm: Voice Clone +parameters: + - name: text + type: string + required: false + label: + en_US: Text + zh_Hans: 试听文本 + human_description: + en_US: Text + zh_CN: 试听文本 + form: llm + - name: model + type: select + required: true + options: + - value: speech-02-hd + label: + en_US: speech-02-hd + zh_Hans: speech-02-hd + - value: speech-02-turbo + label: + en_US: speech-02-turbo + zh_Hans: speech-02-turbo + - value: speech-01-hd + label: + en_US: speech-01-hd + zh_Hans: speech-01-hd + - value: speech-01-turbo + label: + en_US: speech-01-turbo + zh_Hans: speech-01-turbo + default: speech-02-hd + label: + en_US: Model Name + zh_Hans: 模型名称 + human_description: + en_US: Model Name + zh_CN: 模型名称 + form: form + - name: ref_voice + type: file + required: true + label: + en_US: Reference Voice File + zh_Hans: 参考声音文件 + human_description: + en_US: Reference Voice File + zh_CN: 参考声音文件 + form: llm + - name: voice_id + type: string + required: false + label: + en_US: Voice ID (Customize) + zh_Hans: 声音ID (自定义) + human_description: + en_US: Voice ID, It will be automatically generated if it is empty + zh_CN: 声音ID,如果为空自动生成 + form: form + - name: accuracy + type: number + required: false + min: 0.1 + max: 1 + default: 0.7 + label: + en_US: Accuracy threshold + zh_Hans: 文本校验准确率阈值 + human_description: + en_US: Accuracy threshold + zh_CN: 文本校验准确率阈值 + form: form + - name: need_noise_reduction + type: boolean + required: false + default: false + label: + en_US: Noise reduction + zh_Hans: 是否开启降噪 + human_description: + en_US: Noise reduction + zh_CN: 是否开启降噪 + form: form + - name: need_volume_normalization + type: boolean + required: false + default: false + label: + en_US: Volume normalization + zh_Hans: 是否开启音量归一化 + human_description: + en_US: Volume normalization + zh_CN: 是否开启音量归一化 + form: form + +extra: + python: + source: tools/voice_clone.py