diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..c462e96acbfb5a859f2350bcec48c38275769946 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,35 +1,35 @@ *.7z filter=lfs diff=lfs merge=lfs -text *.arrow filter=lfs diff=lfs merge=lfs -text *.bin filter=lfs diff=lfs merge=lfs -text +*.bin.* filter=lfs diff=lfs merge=lfs -text *.bz2 filter=lfs diff=lfs merge=lfs -text -*.ckpt filter=lfs diff=lfs merge=lfs -text *.ftz filter=lfs diff=lfs merge=lfs -text *.gz filter=lfs diff=lfs merge=lfs -text *.h5 filter=lfs diff=lfs merge=lfs -text *.joblib filter=lfs diff=lfs merge=lfs -text *.lfs.* filter=lfs diff=lfs merge=lfs -text -*.mlmodel filter=lfs diff=lfs merge=lfs -text *.model filter=lfs diff=lfs merge=lfs -text *.msgpack filter=lfs diff=lfs merge=lfs -text -*.npy filter=lfs diff=lfs merge=lfs -text -*.npz filter=lfs diff=lfs merge=lfs -text *.onnx filter=lfs diff=lfs merge=lfs -text *.ot filter=lfs diff=lfs merge=lfs -text *.parquet filter=lfs diff=lfs merge=lfs -text *.pb filter=lfs diff=lfs merge=lfs -text -*.pickle filter=lfs diff=lfs merge=lfs -text -*.pkl filter=lfs diff=lfs merge=lfs -text *.pt filter=lfs diff=lfs merge=lfs -text *.pth filter=lfs diff=lfs merge=lfs -text *.rar filter=lfs diff=lfs merge=lfs -text -*.safetensors filter=lfs diff=lfs merge=lfs -text saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.tar.* filter=lfs diff=lfs merge=lfs -text -*.tar filter=lfs diff=lfs merge=lfs -text *.tflite filter=lfs diff=lfs merge=lfs -text *.tgz filter=lfs diff=lfs merge=lfs -text -*.wasm filter=lfs diff=lfs merge=lfs -text *.xz filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text -*.zst filter=lfs diff=lfs merge=lfs -text -*tfevents* filter=lfs diff=lfs merge=lfs -text +*.zstandard filter=lfs diff=lfs merge=lfs -text +*.tfevents* filter=lfs diff=lfs merge=lfs -text +*.db* filter=lfs diff=lfs merge=lfs -text +*.ark* filter=lfs diff=lfs merge=lfs -text +**/*ckpt*data* filter=lfs diff=lfs merge=lfs -text +**/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text +**/*ckpt*.index filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -textcomponents/Chatbot/resources/dog.mp4 filter=lfs diff=lfs merge=lfs -text +components/Markdown/resources/dog.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..591a4d09126ca56317dff63a8958de0a03d103a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +.eggs/ +dist/ +*.pyc +__pycache__/ +*.py[cod] +*$py.class +__tmp/* +*.pyi +templates + +# common + +!.*ignore +!.*rc +!.gitattributes +!.aoneci.yml +!.editorconfig + +# Logs +logs +*.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +bower_components +node_modules/ +jspm_packages/ + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release +lib +dist + +# TypeScript v1 declaration files +typings/ + +# Output of 'npm pack' +*.tgz + +# xconsole +src/.xconsole +build +.faas_debug_tmp +.yarn +.yalc +yalc.lock +.eslintcache +.stylelintcache +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6898e9365f8c11bf792fab806c33e930086f16fa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ + +FROM python:3.9 + +WORKDIR /code + +COPY --link --chown=1000 . . + +RUN mkdir -p /tmp/cache/ +RUN chmod a+rwx -R /tmp/cache/ +ENV TRANSFORMERS_CACHE=/tmp/cache/ + +RUN pip install --no-cache-dir -r requirements.txt + +ENV PYTHONUNBUFFERED=1 GRADIO_ALLOW_FLAGGING=never GRADIO_NUM_PORTS=1 GRADIO_SERVER_NAME=0.0.0.0 GRADIO_SERVER_PORT=7860 SYSTEM=spaces + +CMD ["python", "app.py"] diff --git a/README.md b/README.md index 520f2679c3b7ebeb3466119a60a80c5e3b4d1c34..b792c0d0b9f7e306b0529b72762364cdc52e98cc 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ + --- -title: Modelscope Gradio Components -emoji: 🌍 -colorFrom: blue -colorTo: yellow +tags: [gradio-custom-component] +title: modelscope_gradio_components V0.0.1b8 +colorFrom: red +colorTo: red sdk: docker pinned: false +license: apache-2.0 --- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..ac95bb8ffc6f0e46b9a3bf2f85e874afc782a77e --- /dev/null +++ b/app.py @@ -0,0 +1,22 @@ +import gradio as gr +from components.Chatbot.app import docs as chatbot_docs +from components.Docs import Docs +from components.Markdown.app import docs as markdown_docs +from components.MultimodalInput.app import docs as multimodel_input_docs + +readme_docs = Docs(__file__) + +docs = [ + ["开始使用", readme_docs], + ["Chatbot", chatbot_docs], + ["Markdown", markdown_docs], + ["MultimodalInput", multimodel_input_docs], +] + +with gr.Blocks() as demo: + with gr.Tabs() as components_tabs: + for doc in docs: + with gr.TabItem(doc[0], id=doc[0]): + doc[1].render(components_tabs) + +demo.queue().launch() diff --git a/components/Chatbot/README.md b/components/Chatbot/README.md new file mode 100644 index 0000000000000000000000000000000000000000..cc1d1821957803a27ba91a3515b7d6832fb6e6d9 --- /dev/null +++ b/components/Chatbot/README.md @@ -0,0 +1,129 @@ +# Chatbot + +升级版的 gradio Chatbot。 + +- 支持前端匀速流式输出 message +- 支持输出多模态内容(音频、视频、语音、文件、文本) +- 支持多 agent 场景 +- 支持自定义渲染组件,并与 Python 侧事件交互 + +## 如何使用 + +### 基本使用 + + + +### 多模态 & 支持本地文件的展示 + + + +### 控制打字机单句 message 开关 + + + +### 支持手风琴内容展示 + +在返回的内容中加入 `accordion` 标签,可以在内容中加入手风琴,更多用法详见 Markdown 内置自定义标签 + +同时为了适配大模型的工具调用链路,额外对某些大模型的格式做了预设配置,支持下述格式的预设处理(会将下面的格式转换成上方`accordion`标签包裹形式) + +```python +import modelscope_gradio_components as mgr +from modelscope_gradio_components.components.Chatbot.llm_thinking_presets import qwen + +# 添加 qwen 解析预设 +mgr.Chatbot(llm_thinking_presets=[qwen()]) +``` + +```text +Action: image_gen +Action Input: {"text": "风和日丽", "resolution": "1024*1024"} +Observation: ![IMAGEGEN](https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/1d/a2/20231213/723609ee/1926736d-7c6e-4d2f-b438-b7746b3d89f5-1.png?Expires=1702537773&OSSAccessKeyId=LTAI5tQZd8AEcZX6KZV4G8qL&Signature=H%2B0rIn6BMfE%2BOr1uPb7%2Br9G3%2B5w%3D) 根据您的描述"风和日丽",我生成了一张图片。![](https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/1d/a2/20231213/723609ee/1926736d-7c6e-4d2f-b438-b7746b3d89f5-1.png?Expires=1702537773&OSSAccessKeyId=LTAI5tQZd8AEcZX6KZV4G8qL&Signature=H%2B0rIn6BMfE%2BOr1uPb7%2Br9G3%2B5w%3D) + +Action: 「任意文本表示,将展示为思考链调用的名称」 +Action Input: 「任意json or md 内容,将展示到调用过程的下拉框」 +Observation: 「任意 md 内容,将作为完成调用的展示的下拉框内」 +``` + + + +### 支持用户选择交互 + +在返回的内容中加入 `select-box` 标签,更多用法详见 Markdown 内置自定义标签 + + + +### 多 bot 场景 + + + +### 自定义标签(高阶用法,需要了解前端知识) + +详见 Markdown 组件 + +## API 及参数列表 + +以下 API 均为在原有 gradio Chatbot 外的额外拓展参数。 + +### value + +接口定义: + +```python + +class FileMessage(GradioModel): + file: FileData + alt_text: Optional[str] = None + + +class MultimodalMessage(GradioModel): + name: Optional[str] = None + text: Optional[str] = None + flushing: Optional[bool] = None + avatar: Optional[Union[str, FileData]] = '' + files: Optional[List[Union[FileMessage, dict, FileData, str]]] = None + +# 支持多 bot 场景 +MultimodalMessageItem = Optional[Union[MultimodalMessage, MultimodalInputData, + dict, str]] + + +class ChatbotData(GradioRootModel): + root: List[Tuple[Union[MultimodalMessageItem, List[MultimodalMessageItem]], + Union[MultimodalMessageItem, + List[MultimodalMessageItem]]]] +``` + +### props + +| 属性 | 类型 | 默认值 | 描述 | +| -------------------- | -------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| flushing | bool | True | 是否开启打字机效果。默认只有 bot 的 message 会开启,可以通过单独修改 message 的 flushing 属性精确控制每一条 message 的显示效果 | +| enable_base64 | bool | False | 是否支持渲染的内容为 base64,因为直接渲染 base64 有安全问题,默认为 False。 | +| preview | bool | True | 是否开启图片预览功能 | +| avatar_images | tuple\[str \| Path \| None \| dict \| list, str \| Path \| None \| dict\| list\] | None | 拓展gr.Chatbot的参数值,除了接收 url 外还可以接收 dict 和 list,dict 可以传入avatar和name字段,name字段在渲染时会显示在头像下方。
- 当传入 dict 时,必须包含有avatar字段。
- 当传入 list 时,一般对应多 bot 模式,每一项可以接收前面所有的值,每个 bot 的头像与 message 中 bot 的位置一一对应 | +| avatar_image_align | Literal['top', 'middle', 'bottom'] | 'bottom' | 控制头像与 message 的对齐方式,默认为下对齐 | +| avatar_image_width | int | 45 | 头像与名称的宽度 | +| flushing_speed | int | 3 | 打字机速度,值为 1 - 10,值越大速度越快 | +| llm_thinking_presets | list\[dict\] | \[\] | llm 思考链路解析预设,可以将 llm 调用工具的输出格式转为固定的前端展示格式,需要从modelscope_gradio_components.Chatbot.llm_thinking_presets引入,目前支持:qwen | +| custom_components | dict\[str, CustomComponentDict\] CustomComponentDict 定义见下方 | None | 支持用户定义自定义标签,并通过 js 控制标签渲染样式与触发 python 事件。 | + +**CustomComponent 定义如下** + +```python +class CustomComponentDict(TypedDict): + props: Optional[List[str]] + template: Optional[str] + js: Optional[str] +``` + +### 内置的自定义标签 + +见 Markdown 内置自定义标签 + +### event listeners + +| 事件 | 描述 | +| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mgr.Chatbot.flushed(fn, ···)` | 当打字机效果结束时触发。EventData 为:
- index:当前 message 的 index tuple。
- value:当前 message value。 | +| `mgr.Chatbot.custom(fn, ···)` | 自定义标签触发事件时触发,EventData 为:
- index:前 message 的 index tuple。
- tag:当前触发的标签。
- tag_index:当前触发标签的 index,此 index 在 mesage 的 index tuple 基础上重新计算。
- value:自定义传入的值。 | diff --git a/components/Chatbot/app.py b/components/Chatbot/app.py new file mode 100644 index 0000000000000000000000000000000000000000..77372ef291165f2cf77a8944f3f8b18773972190 --- /dev/null +++ b/components/Chatbot/app.py @@ -0,0 +1,6 @@ +from components.Docs import Docs + +docs = Docs(__file__) + +if __name__ == "__main__": + docs.render().queue().launch() diff --git a/components/Chatbot/demos/accordion.py b/components/Chatbot/demos/accordion.py new file mode 100644 index 0000000000000000000000000000000000000000..a3b95603051cb087a48b29b162e9ec3f66f82f01 --- /dev/null +++ b/components/Chatbot/demos/accordion.py @@ -0,0 +1,49 @@ +import os + +import gradio as gr + +import modelscope_gradio_components as mgr +from modelscope_gradio_components.components.Chatbot.llm_thinking_presets import \ + qwen + + +def resolve_assets(relative_path): + return os.path.join(os.path.dirname(__file__), "../resources", + relative_path) + + +conversation = [ + [ + None, { + "text": f""" +标签语法: + + +```json +{{"text": "风和日丽", "resolution": "1024*1024"}} +``` + + +qwen preset: +Action: image_gen +Action Input: {{"text": "风和日丽", "resolution": "1024*1024"}} +Observation: ![IMAGEGEN]({resolve_assets("screen.jpeg")}) 根据您的描述"风和日丽",我生成了一张图片。![]({resolve_assets("screen.jpeg")}) + +Action: 「任意文本表示,将展示为思考链调用的名称」 +Action Input: 「任意json or md 内容,将展示到调用过程的下拉框」 +Observation: 「任意 md 内容,将作为完成调用的展示的下拉框内」 +""", + "flushing": False + } + ], +] + +with gr.Blocks() as demo: + mgr.Chatbot( + value=conversation, + llm_thinking_presets=[qwen()], + height=600, + ) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Chatbot/demos/basic.py b/components/Chatbot/demos/basic.py new file mode 100644 index 0000000000000000000000000000000000000000..1dd7daee81a9f4eaa04ec3c7becbbfbeee026f3b --- /dev/null +++ b/components/Chatbot/demos/basic.py @@ -0,0 +1,55 @@ +import os +import time + +import gradio as gr + +import modelscope_gradio_components as mgr + +conversation = [ + [ + None, + { + # bot 第一句话关闭打字机效果,直接输入内容 + "text": "Hello I'm a chatbot", + "flushing": False + } + ], +] + + +def submit(_input, _chatbot): + _chatbot.append([_input, None]) + yield gr.update(interactive=False, value=None), _chatbot + time.sleep(2) + _chatbot[-1][1] = {"text": _input.text + '!'} + yield { + chatbot: _chatbot, + } + + +def flushed(): + return gr.update(interactive=True) + + +with gr.Blocks() as demo: + chatbot = mgr.Chatbot( + value=conversation, + avatar_images=[ + os.path.join(os.path.dirname(__file__), "../resources/user.jpeg"), + { + "name": + "bot", + "avatar": + os.path.join(os.path.dirname(__file__), + "../resources/bot.jpeg") + } + ], + height=600, + ) + + input = mgr.MultimodalInput() + input.submit(fn=submit, inputs=[input, chatbot], outputs=[input, chatbot]) + chatbot.flushed(fn=flushed, outputs=[input]) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Chatbot/demos/message_config.py b/components/Chatbot/demos/message_config.py new file mode 100644 index 0000000000000000000000000000000000000000..54e5ad7980519ec04dedfcbc9baf414fe74cda54 --- /dev/null +++ b/components/Chatbot/demos/message_config.py @@ -0,0 +1,34 @@ +import time + +import gradio as gr + +import modelscope_gradio_components as mgr + + +def submit(_chatbot): + _chatbot.append(["test user", "test bot"]) # 此时只有 bot 会开启打字机效果 + yield _chatbot + time.sleep(2) + _chatbot.append(["test user", { + "text": "test bot", + "flushing": False + }]) # 两者都没有打字机效果 + yield _chatbot + time.sleep(2) + _chatbot.append([{ + "text": "test user", + "flushing": True + }, { + "text": "test bot", + "flushing": False + }]) # user 会开启打字机效果 + yield _chatbot + + +with gr.Blocks() as demo: + chatbot = mgr.Chatbot(height=600, ) + button = gr.Button("Submit") + button.click(fn=submit, inputs=[chatbot], outputs=[chatbot]) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Chatbot/demos/multi_bots.py b/components/Chatbot/demos/multi_bots.py new file mode 100644 index 0000000000000000000000000000000000000000..2f9c1f6ab69b382db5a2943c710ce96758b39738 --- /dev/null +++ b/components/Chatbot/demos/multi_bots.py @@ -0,0 +1,91 @@ +import os +import time + +import gradio as gr + +import modelscope_gradio_components as mgr + + +def resolve_assets(relative_path): + return os.path.join(os.path.dirname(__file__), "../resources", + relative_path) + + +conversation = [ + [ + None, + { + # bot 第一句话关闭打字机效果,直接输入内容 + "text": "Hello I'm a chatbot", + "flushing": False + } + ], +] + + +def get_last_bot_message(chatbot): + return chatbot[-1][1] + + +def create_music_bot_message(text: str): + return { + "text": text, + } + + +def create_image_bot_message(text: str): + return { + "text": text, + } + + +def submit(_input, _chatbot): + _chatbot.append([_input, None]) + yield gr.update(interactive=False, value=None), _chatbot + _chatbot[-1][1] = [ + "Hello", + create_image_bot_message("Hello"), + create_music_bot_message("Hello") + ] + + time.sleep(2) + get_last_bot_message(_chatbot)[1][ + "text"] = f"""Hello, I\'m a image bot\n![image]({resolve_assets("user.jpeg")})""" + get_last_bot_message(_chatbot)[2][ + "text"] = f"""Hello, I\'m a music bot """ + yield { + chatbot: _chatbot, + } + + +def flushed(): + return gr.update(interactive=True) + + +with gr.Blocks() as demo: + chatbot = mgr.Chatbot( + value=conversation, + avatar_image_width=40, + avatar_images=[ + resolve_assets('user.jpeg'), + # default bot avatar and name + [{ + "name": "bot", + "avatar": resolve_assets('bot.jpeg') + }, { + "name": "image bot", + "avatar": resolve_assets('image-bot.jpeg') + }, { + "name": "music bot", + "avatar": resolve_assets('music-bot.jpeg') + }] + ], + height=600, + ) + + input = mgr.MultimodalInput() + input.submit(fn=submit, inputs=[input, chatbot], outputs=[input, chatbot]) + chatbot.flushed(fn=flushed, outputs=[input]) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Chatbot/demos/multimodal.py b/components/Chatbot/demos/multimodal.py new file mode 100644 index 0000000000000000000000000000000000000000..4e3f0931145dc73a833c4a5e1fac0df94e70bd9b --- /dev/null +++ b/components/Chatbot/demos/multimodal.py @@ -0,0 +1,43 @@ +import os + +import gradio as gr + +import modelscope_gradio_components as mgr + + +def resolve_assets(relative_path): + return os.path.join(os.path.dirname(__file__), "../resources", + relative_path) + + +conversation = [ + [ + None, { + "text": f""" +图片 + +![image]({resolve_assets("bot.jpeg")}) + + + +视频 + + + +音频 + + +""", + "flushing": False + } + ], +] + +with gr.Blocks() as demo: + mgr.Chatbot( + value=conversation, + height=600, + ) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Chatbot/demos/select-box.py b/components/Chatbot/demos/select-box.py new file mode 100644 index 0000000000000000000000000000000000000000..e204a2d6936b17b2efb2cf8c421cbff944fa29e3 --- /dev/null +++ b/components/Chatbot/demos/select-box.py @@ -0,0 +1,48 @@ +import json + +import gradio as gr + +import modelscope_gradio_components as mgr + +# label 为对用户展示值,value 为实际选择值 +options = [{"label": "A", "value": "a"}, "b", "c"] + +conversation = [[ + None, f""" +Single Select: + +Multiple Select: + +Vertical Direction: + + + +Card Shape: + + + + + + + + +""" +]] + + +# 必须使用 gr.EventData 显示标注 +def fn(data: gr.EventData): + print(data._data) + + +with gr.Blocks() as demo: + chatbot = mgr.Chatbot( + value=conversation, + flushing=False, + height=600, + ) + # 所有自定义标签都会触发 custom 事件 + chatbot.custom(fn=fn) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Chatbot/resources/audio.wav b/components/Chatbot/resources/audio.wav new file mode 100644 index 0000000000000000000000000000000000000000..105190ad88e2e177540361de340e54feb1587f3c Binary files /dev/null and b/components/Chatbot/resources/audio.wav differ diff --git a/components/Chatbot/resources/bot.jpeg b/components/Chatbot/resources/bot.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5fde8cc45f61b677c0581e6889b11e269c35be08 Binary files /dev/null and b/components/Chatbot/resources/bot.jpeg differ diff --git a/components/Chatbot/resources/custom_components/custom_select.js b/components/Chatbot/resources/custom_components/custom_select.js new file mode 100644 index 0000000000000000000000000000000000000000..d2b2c5d8b41e7a50d72d219ffaad6f81c8c67824 --- /dev/null +++ b/components/Chatbot/resources/custom_components/custom_select.js @@ -0,0 +1,26 @@ +(props, cc, { el, onMount }) => { + const options = JSON.parse(props.options); + el.innerHTML = ` + ${options + .map((option) => { + return `
+ +
`; + }) + .join('')} + `; + onMount(() => { + const inputs = Array.from(el.getElementsByTagName('input')); + Array.from(el.getElementsByTagName('label')).forEach((label, i) => { + label.addEventListener('click', () => { + inputs.forEach((input) => { + input.checked = false; + }); + const input = label.getElementsByTagName('input')[0]; + input.checked = true; + // 通过 cc.dispatch 向 python 侧发送通知 + cc.dispatch(options[i]); + }); + }); + }); +}; diff --git a/components/Chatbot/resources/dog.mp4 b/components/Chatbot/resources/dog.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..062b9c81317de43f392c56e9e03444bf8cc31d51 --- /dev/null +++ b/components/Chatbot/resources/dog.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39d086ce29e48cf76e5042d2f3f0611ee46575f70fa3dc0c40dd4cfffde3d933 +size 8626383 diff --git a/components/Chatbot/resources/image-bot.jpeg b/components/Chatbot/resources/image-bot.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5df101fedca7e0b76eaa1119506ca4c1941c9fc7 Binary files /dev/null and b/components/Chatbot/resources/image-bot.jpeg differ diff --git a/components/Chatbot/resources/music-bot.jpeg b/components/Chatbot/resources/music-bot.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..00288fb576845921bbc3247ca8da3fbbdea12800 Binary files /dev/null and b/components/Chatbot/resources/music-bot.jpeg differ diff --git a/components/Chatbot/resources/screen.jpeg b/components/Chatbot/resources/screen.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..574735acb117e86c5c0850e2b5489b8f8efa20cc Binary files /dev/null and b/components/Chatbot/resources/screen.jpeg differ diff --git a/components/Chatbot/resources/user.jpeg b/components/Chatbot/resources/user.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..536948b6bd19cb0b49c44b74e2790198301520e5 Binary files /dev/null and b/components/Chatbot/resources/user.jpeg differ diff --git a/components/Docs.py b/components/Docs.py new file mode 100644 index 0000000000000000000000000000000000000000..f68ae5dc5552a92c925f2ae4dfe56b5eaffc7bec --- /dev/null +++ b/components/Docs.py @@ -0,0 +1,147 @@ +import os +import re +from typing import Callable + +import gradio as gr + +import modelscope_gradio_components as mgr + +from .parse_markdown import parse_markdown + +with open(os.path.join(os.path.dirname(__file__), "tab-link.js")) as f: + tab_link_js = f.read() + +custom_components = { + "tab-link": { + "props": ["tab", "component-tab"], + "js": tab_link_js + } +} + + +def remove_formatter(markdown_text): + pattern = r"^---[\s\S]*?---" + + replaced_text = re.sub(pattern, "", markdown_text) + + return replaced_text + + +def list_demos(dir_path: str, prefix=''): + result = [] + if (not os.path.isdir(dir_path)): + return result + for name in os.listdir(dir_path): + path = os.path.join(dir_path, name) + + if os.path.isfile(path): + result.append(prefix + name) + elif os.path.isdir(path): + sub_prefix = prefix + name + '/' + result.extend(list_demos(path, sub_prefix)) + + return result + + +def get_demo_modules(file_path: str): + import importlib.util + + demos = [ + demo for demo in list_demos( + os.path.join(os.path.dirname(file_path), "demos")) + if demo.endswith(".py") + ] + demo_modules = {} + for demo in demos: + demo_name = demo.split(".")[0] + spec = importlib.util.spec_from_file_location( + "demo", os.path.join(os.path.dirname(file_path), "demos", demo)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + demo_modules[demo_name] = module + return demo_modules + + +class Docs: + + def __init__(self, file_path: str, markdown_files: list = None): + self.file_path = file_path + self.demo_modules = get_demo_modules(file_path) + # default current directory + self.markdown_files = markdown_files if markdown_files else [ + file_name for file_name in os.listdir(os.path.dirname(file_path)) + if file_name.endswith(".md") + ] + self.tabs = None + + def read_file(self, relative_path: str): + with open(os.path.join(os.path.dirname(self.file_path), relative_path), + "r") as f: + return f.read() + + def render_demo(self, demo_name, prefix='', suffix=''): + content = self.read_file(f"./demos/{demo_name}.py") + module = self.demo_modules[demo_name] + with gr.Accordion("Show Demo", open=False): + with gr.Row(): + with gr.Column(): + mgr.Markdown(f""" +{prefix} +````python +{content} +```` +{suffix} +""", + header_links=True, + custom_components=custom_components) + with gr.Column(): + module.demo.render() + + def render_markdown(self, + markdown_file, + on_tab_link_click: Callable = None, + components_tabs=None): + items = parse_markdown(remove_formatter(self.read_file(markdown_file)), + read_file=self.read_file) + for item in items: + if item["type"] == "text": + md = mgr.Markdown(item["value"], + header_links=True, + custom_components=custom_components) + # 过滤 + deps = [dep for dep in [components_tabs, self.tabs] if dep] + if len(deps) > 0: + md.custom(fn=on_tab_link_click, outputs=deps) + elif item["type"] == "demo": + self.render_demo(item["name"], + prefix=item["prefix"], + suffix=item["suffix"]) + + def render(self, components_tabs=None): + + def tab_link_click(data: gr.EventData): + tab: str = data._data["value"].get("tab", '') + component_tab: str = data._data["value"].get("component_tab", '') + if tab and tabs: + return {tabs: gr.update(selected=tab)} + elif components_tabs and component_tab: + return {components_tabs: gr.update(selected=component_tab)} + + with gr.Blocks() as demo: + + if len(self.markdown_files) > 1: + with gr.Tabs() as tabs: + self.tabs = tabs + + for markdown_file in self.markdown_files: + tab_name = ".".join(markdown_file.split(".")[:-1]) + with gr.TabItem(tab_name, id=tab_name): + self.render_markdown( + markdown_file, + on_tab_link_click=tab_link_click, + components_tabs=components_tabs) + else: + self.render_markdown(self.markdown_files[0], + on_tab_link_click=tab_link_click, + components_tabs=components_tabs) + return demo diff --git a/components/Markdown/README.md b/components/Markdown/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e95ff7fa6b18e2a0c1005ef4f32a93642ada89c4 --- /dev/null +++ b/components/Markdown/README.md @@ -0,0 +1,87 @@ +# Markdown + +升级版的 gradio Markdown。 + +- 支持输出多模态内容(音频、视频、语音、文件、文本) +- 支持自定义渲染组件,并与 Python 侧事件交互 + +## 如何使用 + +### 基本使用 + + + +### 多模态 & 支持本地文件的展示 + + + +### 支持手风琴内容展示 + +在返回的内容中加入 `accordion` 标签,更多用法详见 accordion + + + +### 支持用户选择交互 + +在返回的内容中加入 `select-box` 标签,更多用法详见 select-box + + + +### 自定义标签(高阶用法,需要了解前端知识) + + + +#### 引入 js + + + +template只能做简单的变量替换,如果想要引入更多自定义的行为,如条件判断、循环渲染等,请使用 js 控制 el 自行处理,下面是简单的示例: + + + +custom_select.js + +```js + +``` + + + + +#### 与 Python 侧交互 + +在 js 中可以使用`cc.dispatch`触发 Python 侧监听的`custom`事件,以前面的custom_select.js为例,我们在前端调用了`cc.dispatch(options[i])`,则会向 Python 侧同时发送通知。 + + + +## API 及参数列表 + +以下 API 均为在原有 gradio Markdown 外的额外拓展参数。 + +### props + +| 属性 | 类型 | 默认值 | 描述 | +| ----------------- | --------------------------------------------------------------- | ------ | --------------------------------------------------------------------------- | +| enable_base64 | bool | False | 是否支持渲染的内容为 base64,因为直接渲染 base64 有安全问题,默认为 False。 | +| preview | bool | True | 是否开启图片预览功能 | +| custom_components | dict\[str, CustomComponentDict\] CustomComponentDict 定义见下方 | None | 支持用户定义自定义标签,并通过 js 控制标签渲染样式与触发 python 事件。 | + +**CustomComponent 定义如下** + +```python +class CustomComponentDict(TypedDict): + props: Optional[List[str]] + template: Optional[str] + js: Optional[str] +``` + +### 内置的自定义标签 + +- select-box +- accordion + +### event listeners + +| 事件 | 描述 | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `mgr.Markdown.custom(fn, ···)` | 自定义标签触发事件时触发,EventData 为:
- index:当前 message 的 index tuple ([message index, user group(index 0) or bot group(index 1), user/bot group index])。
- tag:当前触发的标签。
- tag_index:当前触发标签的 index,此 index 在 mesage 的 index tuple 基础上重新计算。
- value:自定义传入的值。 | diff --git a/components/Markdown/app.py b/components/Markdown/app.py new file mode 100644 index 0000000000000000000000000000000000000000..3c9d836b38ee51f5de91d43011f5f3aaaaf4aea3 --- /dev/null +++ b/components/Markdown/app.py @@ -0,0 +1,20 @@ +import os + +from components.Docs import Docs + + +def resolve(relative_path: str): + return os.path.join(os.path.dirname(__file__), relative_path) + + +docs = Docs( + __file__, + markdown_files=(["README.md"] + [ + f"custom_tags/{file_name}" + for file_name in os.listdir(resolve('custom_tags')) + if file_name.endswith(".md") + ]), +) + +if __name__ == "__main__": + docs.render().queue().launch() diff --git a/components/Markdown/custom_tags/accordion.md b/components/Markdown/custom_tags/accordion.md new file mode 100644 index 0000000000000000000000000000000000000000..6a62712515e4dc14cdfd17865deb80811fa15abb --- /dev/null +++ b/components/Markdown/custom_tags/accordion.md @@ -0,0 +1,23 @@ +# accordion + +在 markdown 文本中添加手风琴效果。 + +## 如何使用 + +### 基本使用 + + + +### 使用 accordion-title 标记 + +使用`::accordion-title[content]`的形式可以在标题输入 markdown 文本。 + + + +## API 及参数列表 + +### props + +| 属性 | 类型 | 默认值 | 描述 | +| ----- | ------ | ------ | ------------ | +| title | string | | 手风琴的标题 | diff --git a/components/Markdown/custom_tags/select-box.md b/components/Markdown/custom_tags/select-box.md new file mode 100644 index 0000000000000000000000000000000000000000..c309287508232e756d9dd039d9b49dadec959f53 --- /dev/null +++ b/components/Markdown/custom_tags/select-box.md @@ -0,0 +1,45 @@ +# select-box + +在 markdown 文本中添加选择交互框。 + +## 如何使用 + +### 基本使用 + + + +### Card 样式 + + + +### Card 自适应内部元素宽度 + + + +### 监听 Python 事件 + + + +## API 及参数列表 + +### value + +custom 事件中 custom_data value 对应值, 返回值为用户 options 传入的对应 value ,如果type="checkbox",则返回一个 list。 + +### props + +| 属性 | 类型 | 默认值 | 描述 | +| ------------ | ------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| type | 'checkbox' \| 'radio' | 'radio' | 选择框类型,'radio' 为单选框、'checkbox'为多选框。 | +| disabled | boolean | | 禁用选择,通常在需要读取历史信息二次渲染时会用到。 | +| value | string | | 默认选中值,通常适用于`type="checkbox"`时提前为用户选择部分选项和设置`disabled`后的默认值渲染。 | +| direction |  'horizontal' \| 'vertical' | 'horizontal' | 横向或竖向排列选择框 | +| shape | 'card' \| 'default' | 'default' | 选择框样式 | +| options | (string\| { label?: string, value?: string, imgSrc?: string})\[\] | | 为用户提供的选项值,每一项可以为 string 或 object。 当值为 object 时可以接收更多自定义值,其中imgSrc只有当shape="card"时才生效。 | +| select-once | boolean | false | 是否只允许用户选择一次 | +| submit-text | string | | 提交按钮的展示值,当该属性有值时,会展示提交按钮,此时用户只有点击提交按钮后才会触发选择事件。 | +| columns | number \| { xs?: number, sm?: number, md?: number, lg?: number, xl?: number, xxl?: number } | { xs: 1, sm:  2, md: 2, lg:  4} | 当shape="card"时才生效。每一行选项占用列数,值的范围为1 - 24,建议此项取值可以被 24 整除,否则可能列数会不符合预期。 当此项传入值为对象时,可以响应式控制每一行渲染列数,响应阈值如下:
- xs:屏幕 < 576px
- sm:屏幕 ≥ 576px
- md:屏幕 ≥ 768px
- lg:屏幕 ≥ 992px
- xl:屏幕 ≥ 1200px
- xxl:屏幕 ≥ 1600px 当direction为vertical时此配置不生效。 | +| item-width | string | | 当shape="card"时才生效。每个选项的宽度,如:'auto'、'100px',默认使用 columns 自动分配的宽度。 | +| item-height | string | | 当shape="card"时才生效。每个选项的高度,默认自适应元素高度。 | +| img-height | string | '160px' | 当shape="card"时才生效。每个选项中图片的高度。 | +| equal-height | boolean | false | 当shape="card"时才生效。是否每一行的选项高度都相等,会使用高度最高的选项。 | diff --git a/components/Markdown/demos/accordion.py b/components/Markdown/demos/accordion.py new file mode 100644 index 0000000000000000000000000000000000000000..d53349e79b996389397241f337a4fac0e86c78e0 --- /dev/null +++ b/components/Markdown/demos/accordion.py @@ -0,0 +1,30 @@ +import gradio as gr + +import modelscope_gradio_components as mgr + +with gr.Blocks() as demo: + mgr.Markdown(f""" +普通调用: + + + +```json +{{"text": "风和日丽", "resolution": "1024*1024"}} +``` + + + +使用 ::accordion-title 标记支持 markdown 语法: + + + +::accordion-title[调用 `tool`] + +```json +{{"text": "风和日丽", "resolution": "1024*1024"}} +``` + +""") + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/basic.py b/components/Markdown/demos/basic.py new file mode 100644 index 0000000000000000000000000000000000000000..61f4fd73db76c4e91a9381f2bf2af563bee141a5 --- /dev/null +++ b/components/Markdown/demos/basic.py @@ -0,0 +1,11 @@ +import gradio as gr + +import modelscope_gradio_components as mgr + +with gr.Blocks() as demo: + mgr.Markdown( + "This _example_ was **written** in [Markdown](https://en.wikipedia.org/wiki/Markdown)\n" + ) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom-tag.py b/components/Markdown/demos/custom-tag.py new file mode 100644 index 0000000000000000000000000000000000000000..787c3ea8a91eafb66fa0da4d8eea8fb565d7267d --- /dev/null +++ b/components/Markdown/demos/custom-tag.py @@ -0,0 +1,21 @@ +import gradio as gr + +import modelscope_gradio_components as mgr + +with gr.Blocks() as demo: + mgr.Markdown( + f""" +custom tag: +""", + custom_components={ + # key 为标签名 + "custom-tag": { + # 自定义标签允许接收的值,可在调用标签时由用户传入 + "props": ["value"], + # 实际渲染时的 template, 可以使用 {} 将用户传入的 props 替换。 + "template": "
{value}
" + } + }) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom-tag2.py b/components/Markdown/demos/custom-tag2.py new file mode 100644 index 0000000000000000000000000000000000000000..03b3d4448039bbc1572a92031911189bb2f6cf73 --- /dev/null +++ b/components/Markdown/demos/custom-tag2.py @@ -0,0 +1,38 @@ +import gradio as gr + +import modelscope_gradio_components as mgr + +with gr.Blocks() as demo: + mgr.Markdown( + f""" +custom tag: +""", + custom_components={ + # key 为标签名 + "custom-tag": { + "props": ["value"], + "template": + "", + # js 接收一个 function + "js": + """ +(props, cc, { el, onMount }) => { + // onMount 会在 template 渲染完成后调用 + onMount(() => { + // el 是当前自定义标签挂载的 container + console.log(el) + }) + console.log(props.children) // 默认会包含 children,可以拿到 xx 标签内的内容 xx + // 可以返回一个对象,对象里的值会与 props 做并集最后渲染到模板中 + return { + value: 'Click Me: ' + props.value, + onClick: () => { + alert('hello') + } + } +}""" + } + }) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom-tag3.py b/components/Markdown/demos/custom-tag3.py new file mode 100644 index 0000000000000000000000000000000000000000..5b1b6600c80ad77b963fa520a70f9c19aeaf5729 --- /dev/null +++ b/components/Markdown/demos/custom-tag3.py @@ -0,0 +1,31 @@ +import json +import os + +import gradio as gr + +import modelscope_gradio_components as mgr + +options = ["a", "b", "c"] + + +def resolve_assets(relative_path): + return os.path.join(os.path.dirname(__file__), "../resources", + relative_path) + + +with open(resolve_assets("./custom_components/custom_select.js"), 'r') as f: + custom_select_js = f.read() + +with gr.Blocks() as demo: + mgr.Markdown(value=f""" +custom tag: +""", + custom_components={ + "custom-select": { + "props": ["options"], + "js": custom_select_js, + } + }) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom-tag4.py b/components/Markdown/demos/custom-tag4.py new file mode 100644 index 0000000000000000000000000000000000000000..73bce9cfb79e90f00254b8c87f2a2d7296109c05 --- /dev/null +++ b/components/Markdown/demos/custom-tag4.py @@ -0,0 +1,39 @@ +import json +import os + +import gradio as gr + +import modelscope_gradio_components as mgr + +options = ["a", "b", "c"] + + +def resolve_assets(relative_path): + return os.path.join(os.path.dirname(__file__), "../resources", + relative_path) + + +with open(resolve_assets("./custom_components/custom_select.js"), 'r') as f: + custom_select_js = f.read() + + +# 注意一定要显示指明类型,gradio 使用 ioc 机制注入值 +def fn(data: gr.EventData): + # custom {'index': [0, 1, 0], 'tag': 'custom-select', 'tag_index': 0, 'value': 'option A'} + print("custom value", data._data) + + +with gr.Blocks() as demo: + md = mgr.Markdown(value=f""" +custom tag: +""", + custom_components={ + "custom-select": { + "props": ["options"], + "js": custom_select_js, + } + }) + md.custom(fn=fn) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom_tags/accordion/accordion-title.py b/components/Markdown/demos/custom_tags/accordion/accordion-title.py new file mode 100644 index 0000000000000000000000000000000000000000..4e23e746616c5dfec35ea5a66f29497cdeb4afc8 --- /dev/null +++ b/components/Markdown/demos/custom_tags/accordion/accordion-title.py @@ -0,0 +1,19 @@ +import gradio as gr + +import modelscope_gradio_components as mgr + +with gr.Blocks() as demo: + mgr.Markdown(f""" + + +::accordion-title[调用 `tool`] + +```json +{{"text": "风和日丽", "resolution": "1024*1024"}} +``` + + +""") + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom_tags/accordion/basic.py b/components/Markdown/demos/custom_tags/accordion/basic.py new file mode 100644 index 0000000000000000000000000000000000000000..60ca98080f9cf464f7024acd1cdf8ad85097d98e --- /dev/null +++ b/components/Markdown/demos/custom_tags/accordion/basic.py @@ -0,0 +1,17 @@ +import gradio as gr + +import modelscope_gradio_components as mgr + +with gr.Blocks() as demo: + mgr.Markdown(f""" + + +```json +{{"text": "风和日丽", "resolution": "1024*1024"}} +``` + + +""") + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom_tags/select-box/basic.py b/components/Markdown/demos/custom_tags/select-box/basic.py new file mode 100644 index 0000000000000000000000000000000000000000..cd2821db0eb043bae526d3d7c783a469fbb9209c --- /dev/null +++ b/components/Markdown/demos/custom_tags/select-box/basic.py @@ -0,0 +1,21 @@ +import json + +import gradio as gr + +import modelscope_gradio_components as mgr + +options = [{"label": "A", "value": "a"}, "b", "c"] + +with gr.Blocks() as demo: + mgr.Markdown( + f"""Single Select: + +Multiple Select: + +Vertical Direction: + + +""", ) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom_tags/select-box/card_shape.py b/components/Markdown/demos/custom_tags/select-box/card_shape.py new file mode 100644 index 0000000000000000000000000000000000000000..636b2974ca145b29ef73e413eec409a03d2104b4 --- /dev/null +++ b/components/Markdown/demos/custom_tags/select-box/card_shape.py @@ -0,0 +1,32 @@ +import json +import os + +import gradio as gr + +import modelscope_gradio_components as mgr + +# card 支持额外传入 imgSrc 属性作为样式封面 +options = [{ + "label": + "A", + "imgSrc": + os.path.join(os.path.dirname(__file__), '../../../resources/screen.jpeg'), + "value": + "a" +}, "b", "c", "d"] + +with gr.Blocks() as demo: + mgr.Markdown( + f""" + +Custom Columns: + + + +Vertical Direction: + + +""") + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom_tags/select-box/card_shape_width_auto.py b/components/Markdown/demos/custom_tags/select-box/card_shape_width_auto.py new file mode 100644 index 0000000000000000000000000000000000000000..9adbe8f45632c975fa7f541059fc45cfb6e51d7e --- /dev/null +++ b/components/Markdown/demos/custom_tags/select-box/card_shape_width_auto.py @@ -0,0 +1,26 @@ +import json +import os + +import gradio as gr + +import modelscope_gradio_components as mgr + +# card 支持额外传入 imgSrc 属性作为样式封面 +options = [{ + "label": + "A", + "imgSrc": + os.path.join(os.path.dirname(__file__), '../../../resources/screen.jpeg'), + "value": + "a" +}, "b", "c", "d"] + +with gr.Blocks() as demo: + mgr.Markdown( + # 填写 item-width="auto" + f""" + +""", ) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/custom_tags/select-box/python_events.py b/components/Markdown/demos/custom_tags/select-box/python_events.py new file mode 100644 index 0000000000000000000000000000000000000000..f1487aaafb4bb2f07daf29a537c4413a908d378c --- /dev/null +++ b/components/Markdown/demos/custom_tags/select-box/python_events.py @@ -0,0 +1,23 @@ +import json + +import gradio as gr + +import modelscope_gradio_components as mgr + +options = [{"label": "A", "value": "a"}, "b", "c"] + + +def fn(data: gr.EventData): + custom_data = data._data + if (custom_data["tag"] == "select-box"): + print(custom_data["value"]) # 用户选择的值,与 options 中的 value 对应 + + +with gr.Blocks() as demo: + md = mgr.Markdown( + f"" + ) + md.custom(fn=fn) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/multimodal.py b/components/Markdown/demos/multimodal.py new file mode 100644 index 0000000000000000000000000000000000000000..7c6074105a216ceb4995dc721dfd7f8172947913 --- /dev/null +++ b/components/Markdown/demos/multimodal.py @@ -0,0 +1,31 @@ +import os + +import gradio as gr + +import modelscope_gradio_components as mgr + + +def resolve_assets(relative_path): + return os.path.join(os.path.dirname(__file__), "../resources", + relative_path) + + +with gr.Blocks() as demo: + mgr.Markdown(f""" +图片 + +![image]({resolve_assets("bot.jpeg")}) + + + +视频 + + + +音频 + + +""") + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/demos/select-box.py b/components/Markdown/demos/select-box.py new file mode 100644 index 0000000000000000000000000000000000000000..bfe975cb3c109bfc2c42420c19e5969d3b1e2921 --- /dev/null +++ b/components/Markdown/demos/select-box.py @@ -0,0 +1,32 @@ +import json + +import gradio as gr + +import modelscope_gradio_components as mgr + +# label 为对用户展示值,value 为实际选择值 +options = [{"label": "A", "value": "a"}, "b", "c"] + +with gr.Blocks() as demo: + mgr.Markdown(f""" +Single Select: + +Multiple Select: + +Vertical Direction: + + + +Card Shape: + + + + + + + + +""") + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/Markdown/resources/audio.wav b/components/Markdown/resources/audio.wav new file mode 100644 index 0000000000000000000000000000000000000000..105190ad88e2e177540361de340e54feb1587f3c Binary files /dev/null and b/components/Markdown/resources/audio.wav differ diff --git a/components/Markdown/resources/bot.jpeg b/components/Markdown/resources/bot.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5fde8cc45f61b677c0581e6889b11e269c35be08 Binary files /dev/null and b/components/Markdown/resources/bot.jpeg differ diff --git a/components/Markdown/resources/custom_components/custom_select.js b/components/Markdown/resources/custom_components/custom_select.js new file mode 100644 index 0000000000000000000000000000000000000000..d2b2c5d8b41e7a50d72d219ffaad6f81c8c67824 --- /dev/null +++ b/components/Markdown/resources/custom_components/custom_select.js @@ -0,0 +1,26 @@ +(props, cc, { el, onMount }) => { + const options = JSON.parse(props.options); + el.innerHTML = ` + ${options + .map((option) => { + return `
+ +
`; + }) + .join('')} + `; + onMount(() => { + const inputs = Array.from(el.getElementsByTagName('input')); + Array.from(el.getElementsByTagName('label')).forEach((label, i) => { + label.addEventListener('click', () => { + inputs.forEach((input) => { + input.checked = false; + }); + const input = label.getElementsByTagName('input')[0]; + input.checked = true; + // 通过 cc.dispatch 向 python 侧发送通知 + cc.dispatch(options[i]); + }); + }); + }); +}; diff --git a/components/Markdown/resources/dog.mp4 b/components/Markdown/resources/dog.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..062b9c81317de43f392c56e9e03444bf8cc31d51 --- /dev/null +++ b/components/Markdown/resources/dog.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39d086ce29e48cf76e5042d2f3f0611ee46575f70fa3dc0c40dd4cfffde3d933 +size 8626383 diff --git a/components/Markdown/resources/screen.jpeg b/components/Markdown/resources/screen.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..574735acb117e86c5c0850e2b5489b8f8efa20cc Binary files /dev/null and b/components/Markdown/resources/screen.jpeg differ diff --git a/components/Markdown/resources/user.jpeg b/components/Markdown/resources/user.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..536948b6bd19cb0b49c44b74e2790198301520e5 Binary files /dev/null and b/components/Markdown/resources/user.jpeg differ diff --git a/components/MultimodalInput/README.md b/components/MultimodalInput/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d98029e5adc6f54f066f900211daf7fb1e8e5e15 --- /dev/null +++ b/components/MultimodalInput/README.md @@ -0,0 +1,50 @@ +# MutilmodalInput  + +多模态输入框,支持上传文件、录音、照相等功能。 + +- 支持文本输入+文件上传共同提交 +- 支持文件上传时的图片、音频预览 +- 提交内容作为 Chatbot 输入多模态内容作为用户输入问题自动匹配 +- 支持用户录音和拍照 + +## 如何使用 + +### 基本使用 + + + +### 与 Chatbot 配合使用 + + + +### 配置上传/提交按钮 + + + +### 允许用户录音或拍照 + + + +## API 及参数列表 + +以下 API 均为在原有 gradio Textbox 外的额外拓展参数。 + +### value + +接口定义: + +```python +class MultimodalInputData(GradioModel): + files: List[Union[FileData, str]] = [] + text: str +``` + +### props + +| 属性 | 类型 | 默认值 | 描述 | +| ------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------- | +| sources | list\[Literal\['upload', 'microphone','webcam'\]\] | \['upload'\] | 上传文件的类型列表。 "upload"会提供上文文件按钮。 "microphone"支持用户录音输入。 "webcam"支持用户照相生成图片或视频 | +| webcam_props | dict | None | webcam 组件属性,目前支持传入mirror_webcam(bool)、include_audio(bool) | +| upload_button_props | dict | None | 上传文件按钮属性,同 gradio UploadButton | +| submit_button_props | dict | None | 提交按钮属性,同 gradio Button | +| file_preview_props | dict | None | 文件预览组件属性,目前支持传入 height (int) | diff --git a/components/MultimodalInput/app.py b/components/MultimodalInput/app.py new file mode 100644 index 0000000000000000000000000000000000000000..77372ef291165f2cf77a8944f3f8b18773972190 --- /dev/null +++ b/components/MultimodalInput/app.py @@ -0,0 +1,6 @@ +from components.Docs import Docs + +docs = Docs(__file__) + +if __name__ == "__main__": + docs.render().queue().launch() diff --git a/components/MultimodalInput/demos/basic.py b/components/MultimodalInput/demos/basic.py new file mode 100644 index 0000000000000000000000000000000000000000..a8bc1d2066949125b52492a2d86cc82bb26fe9dd --- /dev/null +++ b/components/MultimodalInput/demos/basic.py @@ -0,0 +1,16 @@ +import gradio as gr + +import modelscope_gradio_components as mgr + + +def fn(value): + # value 包含 text 与 files + print(value.text, value.files) + + +with gr.Blocks() as demo: + input = mgr.MultimodalInput() + input.change(fn=fn, inputs=[input]) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/MultimodalInput/demos/config_buttons.py b/components/MultimodalInput/demos/config_buttons.py new file mode 100644 index 0000000000000000000000000000000000000000..31ea46c6adfef7cecfdaa1b1cc804cb592d824cd --- /dev/null +++ b/components/MultimodalInput/demos/config_buttons.py @@ -0,0 +1,16 @@ +import gradio as gr + +import modelscope_gradio_components as mgr + + +def fn(value): + print(value.text, value.files) + + +with gr.Blocks() as demo: + input = mgr.MultimodalInput(upload_button_props=dict(variant="primary"), + submit_button_props=dict(visible=False)) + input.change(fn=fn, inputs=[input]) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/MultimodalInput/demos/upload_sources.py b/components/MultimodalInput/demos/upload_sources.py new file mode 100644 index 0000000000000000000000000000000000000000..ab7c51f8e6cefeecfca19b1d6109a4a185e115b5 --- /dev/null +++ b/components/MultimodalInput/demos/upload_sources.py @@ -0,0 +1,15 @@ +import gradio as gr + +import modelscope_gradio_components as mgr + + +def fn(value): + print(value.text, value.files) + + +with gr.Blocks() as demo: + input = mgr.MultimodalInput(sources=["upload", "microphone", "webcam"]) + input.change(fn=fn, inputs=[input]) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/MultimodalInput/demos/with_chatbot.py b/components/MultimodalInput/demos/with_chatbot.py new file mode 100644 index 0000000000000000000000000000000000000000..65700aebdae53c509c1978428446e4c44e4cc29a --- /dev/null +++ b/components/MultimodalInput/demos/with_chatbot.py @@ -0,0 +1,36 @@ +import time + +import gradio as gr + +import modelscope_gradio_components as mgr + + +def fn(input, chatbot): + chatbot.append([{ + "text": input.text, + "files": input.files, + }, None]) + yield { + user_input: mgr.MultimodalInput(interactive=False), + user_chatbot: chatbot + } + time.sleep(2) + chatbot[-1][1] = {"text": "Hello!"} + yield {user_chatbot: chatbot} + + +# 打字机效果结束触发 +def flushed(): + return mgr.MultimodalInput(interactive=True) + + +with gr.Blocks() as demo: + user_chatbot = mgr.Chatbot() + user_input = mgr.MultimodalInput() + user_input.submit(fn=fn, + inputs=[user_input, user_chatbot], + outputs=[user_input, user_chatbot]) + user_chatbot.flushed(fn=flushed, outputs=[user_input]) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/components/parse_markdown.py b/components/parse_markdown.py new file mode 100644 index 0000000000000000000000000000000000000000..447c5ab691176b3bce702f44f7f4a0451b25ae54 --- /dev/null +++ b/components/parse_markdown.py @@ -0,0 +1,81 @@ +from html.parser import HTMLParser + + +def default_read_file(path): + with open(path, "r") as f: + return f.read() + + +enable_tags = ["demo", "demo-prefix", "demo-suffix", "file"] + + +class MarkdownParser(HTMLParser): + + def __init__(self, read_file=None): + super().__init__() + self.value = [{"type": "text", "value": ""}] + self.tag_stack = [] + self.read_file = read_file or default_read_file + self.current_tag = None + + def get_value(self): + return self.value + + def handle_data(self, data: str) -> None: + if self.value[-1]["type"] == "text": + self.value[-1]["value"] += data + elif self.current_tag is None: + self.value.append({"type": "text", "value": data}) + elif self.current_tag == "demo-prefix": + self.value[-1]["prefix"] += data + elif self.current_tag == "demo-suffix": + self.value[-1]["suffix"] += data + + def handle_startendtag(self, tag: str, + attrs: list[tuple[str, str | None]]) -> None: + if tag not in enable_tags: + self.handle_data(self.get_starttag_text()) + return + + def handle_starttag(self, tag: str, + attrs: list[tuple[str, str | None]]) -> None: + if (tag not in enable_tags): + self.handle_data(self.get_starttag_text()) + return + if tag == "demo": + self.value.append({ + "type": "demo", + "name": dict(attrs)["name"], + "prefix": "", + "suffix": "" + }) + elif tag == "file": + content = self.read_file(dict(attrs)["src"]) + if self.value[-1]["type"] == "text": + self.value[-1]["value"] += content + elif self.current_tag == "demo-prefix": + self.value[-1]["prefix"] += content + elif self.current_tag == "demo-suffix": + self.value[-1]["suffix"] += content + self.current_tag = tag + self.tag_stack.append(self.current_tag) + + def handle_endtag(self, tag: str) -> None: + + if (tag not in enable_tags): + self.handle_data(f"") + return + if (len(self.tag_stack) > 0): + self.tag_stack.pop() + if (len(self.tag_stack) > 0): + self.current_tag = self.tag_stack[-1] + else: + self.current_tag = None + else: + self.current_tag = None + + +def parse_markdown(markdown: str, read_file=None): + parser = MarkdownParser(read_file=read_file) + parser.feed(markdown) + return parser.get_value() diff --git a/components/tab-link.js b/components/tab-link.js new file mode 100644 index 0000000000000000000000000000000000000000..bcd9143dcafadafedfb4b711e704a6ca7e32b74c --- /dev/null +++ b/components/tab-link.js @@ -0,0 +1,15 @@ +(props, cc, { onMount, el }) => { + onMount(() => { + el.addEventListener('click', () => { + cc.dispatch({ + tab: props.tab, + component_tab: props['component-tab'], + }); + }); + }); + const children = props.children[0].value; + el.innerHTML = `${children}`; + el.style.display = 'inline-block'; + el.style.cursor = 'pointer'; + el.style.color = 'var(--link-text-color)'; +}; diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ab4bfbab5d7e2b99db0091abdd89c69fe47bf1bb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +modelscope_gradio_components +modelscope_gradio_components-0.0.1b8-py3-none-any.whl \ No newline at end of file