Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +88 -0
- README.md +3 -9
- __pycache__/main_ui.cpython-311.pyc +0 -0
- colab_launcher.py +157 -0
- installed_tools.json +40 -0
- main_ui.py +1556 -0
- requirements.txt +12 -0
- tools/beatsyncer/__init__.py +1 -0
- tools/beatsyncer/__pycache__/__init__.cpython-311.pyc +0 -0
- tools/beatsyncer/__pycache__/__init__.cpython-312.pyc +0 -0
- tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-311.pyc +0 -0
- tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-312.pyc +0 -0
- tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-311.pyc +0 -0
- tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-312.pyc +0 -0
- tools/beatsyncer/beatsyncer_tool.py +470 -0
- tools/beatsyncer/beatsyncer_ui.py +151 -0
- tools/beatsyncer/clips/.DS_Store +0 -0
- tools/beatsyncer/clips/1657591309536.mp4 +3 -0
- tools/beatsyncer/clips/1657591680538.mp4 +3 -0
- tools/beatsyncer/clips/1657591832727.mp4 +3 -0
- tools/beatsyncer/clips/1657616409629.mp4 +3 -0
- tools/beatsyncer/clips/1657616591950.mp4 +3 -0
- tools/beatsyncer/clips/1657616660848.mp4 +3 -0
- tools/beatsyncer/clips/1657616730783.mp4 +3 -0
- tools/beatsyncer/clips/1657616788731.mp4 +3 -0
- tools/beatsyncer/clips/1657616954951.mp4 +3 -0
- tools/beatsyncer/clips/1657923632784.mp4 +3 -0
- tools/beatsyncer/clips/288298605_1687187951639494_579221224540621563_n.mp4 +3 -0
- tools/beatsyncer/clips/289438954_5129286887125053_220682033208031067_n.mp4 +3 -0
- tools/beatsyncer/clips/289568281_1462345327513048_3022742540007505592_n.mp4 +3 -0
- tools/beatsyncer/clips/290154393_565236208447925_526246679178540543_n.mp4 +3 -0
- tools/beatsyncer/clips/290702607_425330016154178_254797435320646581_n.mp4 +3 -0
- tools/beatsyncer/clips/290990526_409256130973654_5788653198617875871_n.mp4 +3 -0
- tools/beatsyncer/clips/291032176_120067890734229_1814549935092808193_n.mp4 +3 -0
- tools/beatsyncer/clips/657591201007.mp4 +3 -0
- tools/beatsyncer/clips/657591253340.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_001.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_002.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_003.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_004.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_005.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_006.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_007.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_008.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_009.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_010.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_001.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_002.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_003.mp4 +3 -0
- tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_004.mp4 +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,91 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tools/beatsyncer/clips/1657591309536.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
tools/beatsyncer/clips/1657591680538.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
tools/beatsyncer/clips/1657591832727.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
tools/beatsyncer/clips/1657616409629.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
tools/beatsyncer/clips/1657616591950.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
tools/beatsyncer/clips/1657616660848.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
tools/beatsyncer/clips/1657616730783.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
tools/beatsyncer/clips/1657616788731.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
tools/beatsyncer/clips/1657616954951.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
tools/beatsyncer/clips/1657923632784.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
tools/beatsyncer/clips/288298605_1687187951639494_579221224540621563_n.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
tools/beatsyncer/clips/289438954_5129286887125053_220682033208031067_n.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
tools/beatsyncer/clips/289568281_1462345327513048_3022742540007505592_n.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
tools/beatsyncer/clips/290154393_565236208447925_526246679178540543_n.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
tools/beatsyncer/clips/290702607_425330016154178_254797435320646581_n.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
tools/beatsyncer/clips/290990526_409256130973654_5788653198617875871_n.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
tools/beatsyncer/clips/291032176_120067890734229_1814549935092808193_n.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
tools/beatsyncer/clips/657591201007.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
tools/beatsyncer/clips/657591253340.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_003.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_004.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_005.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_006.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_007.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_008.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_009.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_010.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 65 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 66 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 67 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_003.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 68 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_004.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 69 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_005.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 70 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_006.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 71 |
+
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_007.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 72 |
+
tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 73 |
+
tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 74 |
+
tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_003.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 75 |
+
tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_004.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 76 |
+
tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_005.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 77 |
+
tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_006.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 78 |
+
tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 79 |
+
tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 80 |
+
tools/beatsyncer/clips/Snapinsta.app_video_7944DC0BB36028CA2D36023098F3029C_video_dashinit.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 81 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 82 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 83 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_003.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 84 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_004.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 85 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_005.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 86 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_006.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 87 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_007.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 88 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_008.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 89 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_009.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 90 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_010.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 91 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_011.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 92 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_012.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 93 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_013.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 94 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_014.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 95 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_015.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 96 |
+
tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_016.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 97 |
+
tools/beatsyncer/clips/ai-generated-8079926_1280.png filter=lfs diff=lfs merge=lfs -text
|
| 98 |
+
tools/beatsyncer/clips/ai-generated-8601539_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 99 |
+
tools/beatsyncer/clips/ai-generated-9151678_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 100 |
+
tools/beatsyncer/clips/antique-car-7577214_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 101 |
+
tools/beatsyncer/clips/automobile-1850862_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 102 |
+
tools/beatsyncer/clips/bmw-6236208_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 103 |
+
tools/beatsyncer/clips/car-171415_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 104 |
+
tools/beatsyncer/clips/car-219813_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 105 |
+
tools/beatsyncer/clips/car-5548243_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 106 |
+
tools/beatsyncer/clips/car-5548244_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 107 |
+
tools/beatsyncer/clips/car-6306695_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 108 |
+
tools/beatsyncer/clips/car-6483720_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 109 |
+
tools/beatsyncer/clips/car-6589240_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 110 |
+
tools/beatsyncer/clips/car-6658749_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 111 |
+
tools/beatsyncer/clips/car-7139944_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 112 |
+
tools/beatsyncer/clips/car-7203265_1280(1).jpg filter=lfs diff=lfs merge=lfs -text
|
| 113 |
+
tools/beatsyncer/clips/car-7203265_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 114 |
+
tools/beatsyncer/clips/car-7725834_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 115 |
+
tools/beatsyncer/clips/car-7830737_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 116 |
+
tools/beatsyncer/clips/car-8141820_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 117 |
+
tools/beatsyncer/clips/fashion-1399346_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 118 |
+
tools/beatsyncer/clips/mercedes-benz-7383931_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 119 |
+
tools/beatsyncer/clips/speedy-car-8391554_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 120 |
+
tools/beatsyncer/clips/sport-4155825_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 121 |
+
tools/beatsyncer/clips/vintage-car-7300881_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 122 |
+
tools/beatsyncer/clips/vintage-rolls-royce-7414656_1280.jpg filter=lfs diff=lfs merge=lfs -text
|
| 123 |
+
tools/beatsyncer/tmp/input_audio.mp3 filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,12 +1,6 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: pink
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
---
|
| 11 |
-
|
| 12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AI_Tools_Platform
|
| 3 |
+
app_file: main_ui.py
|
|
|
|
|
|
|
| 4 |
sdk: gradio
|
| 5 |
+
sdk_version: 4.19.2
|
|
|
|
|
|
|
| 6 |
---
|
|
|
|
|
|
__pycache__/main_ui.cpython-311.pyc
ADDED
|
Binary file (64.7 kB). View file
|
|
|
colab_launcher.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import importlib.util
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
# Define paths
|
| 8 |
+
DRIVE_ROOT = "/content/drive/MyDrive/AI_Tools_Platform"
|
| 9 |
+
TOOLS_DIR = os.path.join(DRIVE_ROOT, "tools")
|
| 10 |
+
INSTALLED_TOOLS_JSON = os.path.join(DRIVE_ROOT, "installed_tools.json")
|
| 11 |
+
|
| 12 |
+
# Import the Tool class from main_ui
|
| 13 |
+
from main_ui import Tool, MainUI
|
| 14 |
+
|
| 15 |
+
def setup_environment():
|
| 16 |
+
print("Setting up environment...")
|
| 17 |
+
|
| 18 |
+
# Create tools directory in Google Drive if it doesn't exist
|
| 19 |
+
os.makedirs(TOOLS_DIR, exist_ok=True)
|
| 20 |
+
print(f"Tools directory in Drive: {TOOLS_DIR}")
|
| 21 |
+
|
| 22 |
+
# Create installed_tools.json in Google Drive if it doesn't exist
|
| 23 |
+
if not os.path.exists(INSTALLED_TOOLS_JSON):
|
| 24 |
+
with open(INSTALLED_TOOLS_JSON, 'w') as f:
|
| 25 |
+
json.dump({"tools": []}, f, indent=4)
|
| 26 |
+
print(f"Created initial installed_tools.json in Drive")
|
| 27 |
+
else:
|
| 28 |
+
print(f"Using existing installed_tools.json from Drive")
|
| 29 |
+
|
| 30 |
+
# Verify the content of installed_tools.json
|
| 31 |
+
try:
|
| 32 |
+
with open(INSTALLED_TOOLS_JSON, 'r') as f:
|
| 33 |
+
tools_data = json.load(f)
|
| 34 |
+
if not isinstance(tools_data, dict) or "tools" not in tools_data:
|
| 35 |
+
print("Warning: installed_tools.json has incorrect format. Fixing...")
|
| 36 |
+
with open(INSTALLED_TOOLS_JSON, 'w') as f:
|
| 37 |
+
json.dump({"tools": []}, f, indent=4)
|
| 38 |
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
| 39 |
+
print("Error: installed_tools.json is not valid JSON. Fixing...")
|
| 40 |
+
with open(INSTALLED_TOOLS_JSON, 'w') as f:
|
| 41 |
+
json.dump({"tools": []}, f, indent=4)
|
| 42 |
+
|
| 43 |
+
# Add tools directory to Python path
|
| 44 |
+
if TOOLS_DIR not in sys.path:
|
| 45 |
+
sys.path.append(TOOLS_DIR)
|
| 46 |
+
if os.getcwd() not in sys.path:
|
| 47 |
+
sys.path.append(os.getcwd())
|
| 48 |
+
|
| 49 |
+
print("\nEnvironment set up successfully!")
|
| 50 |
+
|
| 51 |
+
# Override MainUI methods to use direct file paths
|
| 52 |
+
class ColabMainUI(MainUI):
|
| 53 |
+
def __init__(self):
|
| 54 |
+
# Set up paths before calling parent's __init__
|
| 55 |
+
self.tools = {} # Initialize tools dictionary
|
| 56 |
+
self.tools_dir = TOOLS_DIR
|
| 57 |
+
self.installed_tools_json = INSTALLED_TOOLS_JSON
|
| 58 |
+
self.marketplace_url = "https://marketplace.vidplus.app/api/marketplace"
|
| 59 |
+
self.version = "0.1"
|
| 60 |
+
|
| 61 |
+
# Create tools directory if it doesn't exist
|
| 62 |
+
os.makedirs('tools', exist_ok=True)
|
| 63 |
+
|
| 64 |
+
# Load tools manually instead of calling parent's __init__
|
| 65 |
+
self.load_installed_tools()
|
| 66 |
+
|
| 67 |
+
def load_installed_tools(self):
|
| 68 |
+
"""Load installed tools directly from Google Drive"""
|
| 69 |
+
print(f"Loading tools from {self.installed_tools_json}")
|
| 70 |
+
try:
|
| 71 |
+
if os.path.exists(self.installed_tools_json):
|
| 72 |
+
with open(self.installed_tools_json, 'r') as f:
|
| 73 |
+
data = json.load(f)
|
| 74 |
+
|
| 75 |
+
for tool_data in data.get('tools', []):
|
| 76 |
+
# If ui_module is specified, dynamically import it
|
| 77 |
+
if tool_data.get('ui_module'):
|
| 78 |
+
try:
|
| 79 |
+
# Modify the module path to use the direct path
|
| 80 |
+
module_name = tool_data['ui_module']
|
| 81 |
+
if module_name.startswith('tools.'):
|
| 82 |
+
# Extract the relative path and use the full path
|
| 83 |
+
rel_path = module_name.replace('tools.', '')
|
| 84 |
+
module_path = os.path.join(TOOLS_DIR, rel_path.replace('.', '/') + '.py')
|
| 85 |
+
print(f"Loading module from: {module_path}")
|
| 86 |
+
|
| 87 |
+
# Use importlib to import the module
|
| 88 |
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
| 89 |
+
if spec:
|
| 90 |
+
module = importlib.util.module_from_spec(spec)
|
| 91 |
+
spec.loader.exec_module(module)
|
| 92 |
+
ui_class = getattr(module, tool_data['ui_class'])
|
| 93 |
+
|
| 94 |
+
self.tools[tool_data['id']] = Tool(
|
| 95 |
+
name=tool_data['name'],
|
| 96 |
+
description=tool_data['description'],
|
| 97 |
+
ui_class=ui_class,
|
| 98 |
+
icon=tool_data['icon'],
|
| 99 |
+
version=tool_data['version'],
|
| 100 |
+
ui_module=tool_data['ui_module']
|
| 101 |
+
)
|
| 102 |
+
else:
|
| 103 |
+
print(f"Could not find module: {module_path}")
|
| 104 |
+
else:
|
| 105 |
+
# Standard import for non-tools modules
|
| 106 |
+
module = importlib.import_module(module_name)
|
| 107 |
+
ui_class = getattr(module, tool_data['ui_class'])
|
| 108 |
+
|
| 109 |
+
self.tools[tool_data['id']] = Tool(
|
| 110 |
+
name=tool_data['name'],
|
| 111 |
+
description=tool_data['description'],
|
| 112 |
+
ui_class=ui_class,
|
| 113 |
+
icon=tool_data['icon'],
|
| 114 |
+
version=tool_data['version'],
|
| 115 |
+
ui_module=tool_data['ui_module']
|
| 116 |
+
)
|
| 117 |
+
except (ImportError, AttributeError) as e:
|
| 118 |
+
print(f"Error loading tool {tool_data['name']}: {e}")
|
| 119 |
+
import traceback
|
| 120 |
+
traceback.print_exc()
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"Error loading installed tools: {e}")
|
| 123 |
+
import traceback
|
| 124 |
+
traceback.print_exc()
|
| 125 |
+
|
| 126 |
+
def save_installed_tools(self):
|
| 127 |
+
"""Save installed tools directly to Google Drive"""
|
| 128 |
+
data = {
|
| 129 |
+
"tools": [
|
| 130 |
+
{
|
| 131 |
+
"id": tool_id,
|
| 132 |
+
"name": tool.name,
|
| 133 |
+
"description": tool.description,
|
| 134 |
+
"icon": tool.icon,
|
| 135 |
+
"version": tool.version,
|
| 136 |
+
"ui_class": tool.ui_class.__name__,
|
| 137 |
+
"ui_module": tool.ui_module
|
| 138 |
+
}
|
| 139 |
+
for tool_id, tool in self.tools.items()
|
| 140 |
+
]
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
with open(self.installed_tools_json, 'w') as f:
|
| 144 |
+
json.dump(data, f, indent=4)
|
| 145 |
+
print(f"Saved tools configuration to {self.installed_tools_json}")
|
| 146 |
+
|
| 147 |
+
if __name__ == "__main__":
|
| 148 |
+
setup_environment()
|
| 149 |
+
|
| 150 |
+
# Import and run the modified UI
|
| 151 |
+
print("\nStarting AI Tools Platform...")
|
| 152 |
+
|
| 153 |
+
ui = ColabMainUI()
|
| 154 |
+
demo = ui.create_ui()
|
| 155 |
+
demo.queue()
|
| 156 |
+
demo.title = "AI Tools Platform"
|
| 157 |
+
demo.launch(debug=True, share=True) # share=True to get a public URL
|
installed_tools.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tools": [
|
| 3 |
+
{
|
| 4 |
+
"id": "image_background_remover",
|
| 5 |
+
"name": "Image Background Remover",
|
| 6 |
+
"description": "A tool that removes backgrounds from images",
|
| 7 |
+
"icon": "\ud83d\uddbc\ufe0f",
|
| 8 |
+
"version": "1.0.0",
|
| 9 |
+
"ui_class": "ImageBackgroundRemoverUI",
|
| 10 |
+
"ui_module": "tools.image_background_remover.image_background_remover_ui"
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"id": "beatsyncer",
|
| 14 |
+
"name": "Beatsyncer",
|
| 15 |
+
"description": "Create amazing videos synchronized with music beats",
|
| 16 |
+
"icon": "\ud83c\udfb5",
|
| 17 |
+
"version": "1.0.0",
|
| 18 |
+
"ui_class": "BeatsyncerUI",
|
| 19 |
+
"ui_module": "tools.beatsyncer.beatsyncer_ui"
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": "image_background_remover",
|
| 23 |
+
"name": "Image Background Remover",
|
| 24 |
+
"description": "A tool that removes backgrounds from images",
|
| 25 |
+
"icon": "\ud83d\uddbc\ufe0f",
|
| 26 |
+
"version": "1.0.0",
|
| 27 |
+
"ui_class": "ImageBackgroundRemoverUI",
|
| 28 |
+
"ui_module": "tools.image_background_remover.image_background_remover_ui"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"id": "image_background_remover",
|
| 32 |
+
"name": "Image Background Remover",
|
| 33 |
+
"description": "A tool that removes backgrounds from images",
|
| 34 |
+
"icon": "\ud83d\uddbc\ufe0f",
|
| 35 |
+
"version": "1.0.0",
|
| 36 |
+
"ui_class": "ImageBackgroundRemoverUI",
|
| 37 |
+
"ui_module": "tools.image_background_remover.image_background_remover_ui"
|
| 38 |
+
}
|
| 39 |
+
]
|
| 40 |
+
}
|
main_ui.py
ADDED
|
@@ -0,0 +1,1556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import importlib
|
| 4 |
+
import requests
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import subprocess
|
| 7 |
+
|
| 8 |
+
class Tool:
|
| 9 |
+
def __init__(self, name, description, ui_class, icon="🔧", version="0.1", ui_module=None):
|
| 10 |
+
self.name = name
|
| 11 |
+
self.description = description
|
| 12 |
+
self.ui_class = ui_class
|
| 13 |
+
self.icon = icon
|
| 14 |
+
self.version = version
|
| 15 |
+
self.ui_module = ui_module
|
| 16 |
+
|
| 17 |
+
class MainUI:
|
| 18 |
+
def __init__(self):
|
| 19 |
+
# Load installed tools from JSON
|
| 20 |
+
self.tools = {}
|
| 21 |
+
self.load_installed_tools()
|
| 22 |
+
|
| 23 |
+
# Marketplace server URL
|
| 24 |
+
self.marketplace_url = "https://marketplace.vidplus.app/api/marketplace"
|
| 25 |
+
self.version = "0.1"
|
| 26 |
+
|
| 27 |
+
# Create tools directory if it doesn't exist
|
| 28 |
+
os.makedirs('tools', exist_ok=True)
|
| 29 |
+
|
| 30 |
+
def load_installed_tools(self):
|
| 31 |
+
"""Load installed tools from JSON file"""
|
| 32 |
+
try:
|
| 33 |
+
if os.path.exists('installed_tools.json'):
|
| 34 |
+
with open('installed_tools.json', 'r') as f:
|
| 35 |
+
data = json.load(f)
|
| 36 |
+
|
| 37 |
+
for tool_data in data.get('tools', []):
|
| 38 |
+
# If ui_module is specified, dynamically import it
|
| 39 |
+
if tool_data.get('ui_module'):
|
| 40 |
+
try:
|
| 41 |
+
module = importlib.import_module(tool_data['ui_module'])
|
| 42 |
+
ui_class = getattr(module, tool_data['ui_class'])
|
| 43 |
+
|
| 44 |
+
self.tools[tool_data['id']] = Tool(
|
| 45 |
+
name=tool_data['name'],
|
| 46 |
+
description=tool_data['description'],
|
| 47 |
+
ui_class=ui_class,
|
| 48 |
+
icon=tool_data['icon'],
|
| 49 |
+
version=tool_data['version'],
|
| 50 |
+
ui_module=tool_data['ui_module']
|
| 51 |
+
)
|
| 52 |
+
except (ImportError, AttributeError) as e:
|
| 53 |
+
print(f"Error loading tool {tool_data['name']}: {e}")
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"Error loading installed tools: {e}")
|
| 56 |
+
# Fallback to default BeatSyncer tool
|
| 57 |
+
# self.tools = {
|
| 58 |
+
# "beatsyncer": Tool(
|
| 59 |
+
# name="BeatSyncer",
|
| 60 |
+
# description="Create amazing videos synchronized with music beats",
|
| 61 |
+
# ui_class=BeatSyncerUI,
|
| 62 |
+
# icon="🎵"
|
| 63 |
+
# )
|
| 64 |
+
# }
|
| 65 |
+
|
| 66 |
+
def save_installed_tools(self):
|
| 67 |
+
"""Save installed tools to JSON file"""
|
| 68 |
+
data = {
|
| 69 |
+
"tools": [
|
| 70 |
+
{
|
| 71 |
+
"id": tool_id,
|
| 72 |
+
"name": tool.name,
|
| 73 |
+
"description": tool.description,
|
| 74 |
+
"icon": tool.icon,
|
| 75 |
+
"version": tool.version,
|
| 76 |
+
"ui_class": tool.ui_class.__name__,
|
| 77 |
+
"ui_module": tool.ui_module
|
| 78 |
+
}
|
| 79 |
+
for tool_id, tool in self.tools.items()
|
| 80 |
+
]
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
with open('installed_tools.json', 'w') as f:
|
| 84 |
+
json.dump(data, f, indent=4)
|
| 85 |
+
|
| 86 |
+
def fetch_marketplace_tools(self):
|
| 87 |
+
"""Fetch available tools from marketplace server"""
|
| 88 |
+
try:
|
| 89 |
+
response = requests.get(f"{self.marketplace_url}/tools")
|
| 90 |
+
if response.status_code == 200:
|
| 91 |
+
# Get the list of tools from the marketplace
|
| 92 |
+
marketplace_tools = response.json()
|
| 93 |
+
|
| 94 |
+
# Ensure we're working with the latest state of installed tools
|
| 95 |
+
# This is important after a tool has been removed
|
| 96 |
+
if os.path.exists('installed_tools.json'):
|
| 97 |
+
with open('installed_tools.json', 'r') as f:
|
| 98 |
+
data = json.load(f)
|
| 99 |
+
installed_tool_ids = [tool['id'] for tool in data.get('tools', [])]
|
| 100 |
+
else:
|
| 101 |
+
installed_tool_ids = []
|
| 102 |
+
|
| 103 |
+
# Update the is_installed flag for each tool
|
| 104 |
+
for tool in marketplace_tools:
|
| 105 |
+
tool['is_installed'] = tool['id'] in installed_tool_ids
|
| 106 |
+
|
| 107 |
+
return marketplace_tools
|
| 108 |
+
else:
|
| 109 |
+
return []
|
| 110 |
+
except Exception as e:
|
| 111 |
+
print(f"Error fetching marketplace tools: {e}")
|
| 112 |
+
return []
|
| 113 |
+
|
| 114 |
+
def fetch_tool_details(self, tool_id):
|
| 115 |
+
"""Fetch details for a specific tool from marketplace server"""
|
| 116 |
+
try:
|
| 117 |
+
response = requests.get(f"{self.marketplace_url}/tools/{tool_id}")
|
| 118 |
+
if response.status_code == 200:
|
| 119 |
+
return response.json()
|
| 120 |
+
else:
|
| 121 |
+
return None
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f"Error fetching tool details: {e}")
|
| 124 |
+
return None
|
| 125 |
+
|
| 126 |
+
def install_tool(self, tool_id):
|
| 127 |
+
"""Install a tool from the marketplace"""
|
| 128 |
+
try:
|
| 129 |
+
# Import required modules
|
| 130 |
+
import importlib
|
| 131 |
+
import importlib.util
|
| 132 |
+
import sys
|
| 133 |
+
import subprocess
|
| 134 |
+
import gradio as gr
|
| 135 |
+
|
| 136 |
+
# Fetch tool data
|
| 137 |
+
response = requests.get(f"{self.marketplace_url}/tools/{tool_id}/download")
|
| 138 |
+
if response.status_code != 200:
|
| 139 |
+
return f"Error: Failed to download tool (Status code: {response.status_code})"
|
| 140 |
+
|
| 141 |
+
tool_data = response.json()
|
| 142 |
+
tool_info = tool_data['tool']
|
| 143 |
+
tool_files = tool_data['files']
|
| 144 |
+
|
| 145 |
+
# Check and install dependencies if they exist
|
| 146 |
+
dependencies = tool_info.get('dependencies', [])
|
| 147 |
+
if dependencies:
|
| 148 |
+
try:
|
| 149 |
+
# Read current requirements.txt
|
| 150 |
+
current_requirements = []
|
| 151 |
+
if os.path.exists('requirements.txt'):
|
| 152 |
+
with open('requirements.txt', 'r') as f:
|
| 153 |
+
current_requirements = [line.strip() for line in f.readlines() if line.strip()]
|
| 154 |
+
|
| 155 |
+
# Check which dependencies need to be installed
|
| 156 |
+
to_install = []
|
| 157 |
+
to_add_to_requirements = []
|
| 158 |
+
|
| 159 |
+
for dep in dependencies:
|
| 160 |
+
# Extract just the package name (ignore version requirements)
|
| 161 |
+
pkg_name = dep.split('>=')[0].split('==')[0].split('>')[0].split('<')[0].strip()
|
| 162 |
+
|
| 163 |
+
# Check if dependency is already in requirements.txt
|
| 164 |
+
is_in_requirements = False
|
| 165 |
+
for req in current_requirements:
|
| 166 |
+
req_name = req.split('>=')[0].split('==')[0].split('>')[0].split('<')[0].strip()
|
| 167 |
+
if req_name == pkg_name:
|
| 168 |
+
is_in_requirements = True
|
| 169 |
+
break
|
| 170 |
+
|
| 171 |
+
if not is_in_requirements:
|
| 172 |
+
to_add_to_requirements.append(dep)
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
# Try to import the package to check if it's installed
|
| 176 |
+
importlib.import_module(pkg_name)
|
| 177 |
+
except ImportError:
|
| 178 |
+
# Package not installed, add to installation list
|
| 179 |
+
to_install.append(dep)
|
| 180 |
+
|
| 181 |
+
# Install missing dependencies
|
| 182 |
+
if to_install:
|
| 183 |
+
for dep in to_install:
|
| 184 |
+
# Use subprocess to run pip install
|
| 185 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", dep])
|
| 186 |
+
|
| 187 |
+
# Append new dependencies to requirements.txt
|
| 188 |
+
if to_add_to_requirements:
|
| 189 |
+
with open('requirements.txt', 'a') as f:
|
| 190 |
+
for dep in to_add_to_requirements:
|
| 191 |
+
f.write(f"\n{dep}")
|
| 192 |
+
print(f"Added {len(to_add_to_requirements)} dependencies to requirements.txt")
|
| 193 |
+
|
| 194 |
+
except Exception as e:
|
| 195 |
+
return f"Error installing dependencies: {e}"
|
| 196 |
+
|
| 197 |
+
# Create tool directory
|
| 198 |
+
tool_dir = os.path.join('tools', tool_id)
|
| 199 |
+
os.makedirs(tool_dir, exist_ok=True)
|
| 200 |
+
|
| 201 |
+
# Write tool files
|
| 202 |
+
for file_name, file_content in tool_files.items():
|
| 203 |
+
# Rename files to match our convention
|
| 204 |
+
target_file_name = file_name
|
| 205 |
+
if file_name.endswith('_tool_logic.py'):
|
| 206 |
+
target_file_name = f"{tool_id}_tool.py"
|
| 207 |
+
elif file_name.endswith('_tool_ui.py'):
|
| 208 |
+
target_file_name = f"{tool_id}_ui.py"
|
| 209 |
+
|
| 210 |
+
with open(os.path.join(tool_dir, target_file_name), 'w') as f:
|
| 211 |
+
f.write(file_content)
|
| 212 |
+
|
| 213 |
+
# Create __init__.py to make the directory a package
|
| 214 |
+
with open(os.path.join(tool_dir, '__init__.py'), 'w') as f:
|
| 215 |
+
f.write(f"# {tool_info['name']} package\n")
|
| 216 |
+
|
| 217 |
+
# Add tool to installed tools
|
| 218 |
+
ui_module = f"tools.{tool_id}.{tool_id}_ui"
|
| 219 |
+
ui_class = f"{tool_id.title().replace('_', '')}UI"
|
| 220 |
+
|
| 221 |
+
# Update installed_tools.json
|
| 222 |
+
with open('installed_tools.json', 'r') as f:
|
| 223 |
+
data = json.load(f)
|
| 224 |
+
|
| 225 |
+
data['tools'].append({
|
| 226 |
+
"id": tool_id,
|
| 227 |
+
"name": tool_info['name'],
|
| 228 |
+
"description": tool_info['description'],
|
| 229 |
+
"icon": tool_info['icon'],
|
| 230 |
+
"version": tool_info['version'],
|
| 231 |
+
"ui_class": ui_class,
|
| 232 |
+
"ui_module": ui_module
|
| 233 |
+
})
|
| 234 |
+
|
| 235 |
+
with open('installed_tools.json', 'w') as f:
|
| 236 |
+
json.dump(data, f, indent=4)
|
| 237 |
+
|
| 238 |
+
# Dynamically load the newly installed tool
|
| 239 |
+
try:
|
| 240 |
+
# Reload the module if it was already imported
|
| 241 |
+
if ui_module in sys.modules:
|
| 242 |
+
importlib.reload(sys.modules[ui_module])
|
| 243 |
+
|
| 244 |
+
# Import the module
|
| 245 |
+
module = importlib.import_module(ui_module)
|
| 246 |
+
ui_class_obj = getattr(module, ui_class)
|
| 247 |
+
|
| 248 |
+
# Add the tool to self.tools
|
| 249 |
+
self.tools[tool_id] = Tool(
|
| 250 |
+
name=tool_info['name'],
|
| 251 |
+
description=tool_info['description'],
|
| 252 |
+
ui_class=ui_class_obj,
|
| 253 |
+
icon=tool_info['icon'],
|
| 254 |
+
version=tool_info['version'],
|
| 255 |
+
ui_module=ui_module
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
# Flag to indicate this is a newly installed tool that needs special handling
|
| 259 |
+
self.tools[tool_id].newly_installed = True
|
| 260 |
+
|
| 261 |
+
# Return success message
|
| 262 |
+
return f"Successfully installed {tool_info['name']}. The tool is now available in the Tools page."
|
| 263 |
+
except Exception as e:
|
| 264 |
+
print(f"Error dynamically loading tool: {e}")
|
| 265 |
+
import traceback
|
| 266 |
+
traceback.print_exc()
|
| 267 |
+
# Still return success but with a restart message
|
| 268 |
+
return f"Successfully installed {tool_info['name']}. Please restart the application to use the new tool."
|
| 269 |
+
except Exception as e:
|
| 270 |
+
return f"Error installing tool: {e}"
|
| 271 |
+
|
| 272 |
+
def remove_tool(self, tool_id):
|
| 273 |
+
"""Remove an installed tool"""
|
| 274 |
+
try:
|
| 275 |
+
if tool_id not in self.tools:
|
| 276 |
+
return "Error: Tool is not installed"
|
| 277 |
+
|
| 278 |
+
# Get tool info before removal
|
| 279 |
+
tool_name = self.tools[tool_id].name
|
| 280 |
+
|
| 281 |
+
# Remove tool directory
|
| 282 |
+
tool_dir = os.path.join('tools', tool_id)
|
| 283 |
+
if os.path.exists(tool_dir):
|
| 284 |
+
import shutil
|
| 285 |
+
shutil.rmtree(tool_dir)
|
| 286 |
+
print(f"Removed tool directory: {tool_dir}")
|
| 287 |
+
|
| 288 |
+
# Remove tool from installed_tools.json
|
| 289 |
+
if os.path.exists('installed_tools.json'):
|
| 290 |
+
with open('installed_tools.json', 'r') as f:
|
| 291 |
+
data = json.load(f)
|
| 292 |
+
|
| 293 |
+
# Filter out the tool to be removed
|
| 294 |
+
data['tools'] = [tool for tool in data['tools'] if tool['id'] != tool_id]
|
| 295 |
+
|
| 296 |
+
# Save updated tools list
|
| 297 |
+
with open('installed_tools.json', 'w') as f:
|
| 298 |
+
json.dump(data, f, indent=4)
|
| 299 |
+
|
| 300 |
+
print(f"Removed tool from installed_tools.json: {tool_id}")
|
| 301 |
+
|
| 302 |
+
# Remove tool from self.tools
|
| 303 |
+
del self.tools[tool_id]
|
| 304 |
+
|
| 305 |
+
return f"✅ Successfully removed {tool_name}"
|
| 306 |
+
except Exception as e:
|
| 307 |
+
print(f"Error removing tool: {e}")
|
| 308 |
+
import traceback
|
| 309 |
+
traceback.print_exc()
|
| 310 |
+
return f"Error removing tool: {str(e)}"
|
| 311 |
+
|
| 312 |
+
def create_tool_card(self, name):
|
| 313 |
+
return f"""
|
| 314 |
+
<div style="
|
| 315 |
+
background: #E6E6E6;
|
| 316 |
+
border-radius: 16px;
|
| 317 |
+
width: 200px;
|
| 318 |
+
height: 200px;
|
| 319 |
+
display: flex;
|
| 320 |
+
justify-content: center;
|
| 321 |
+
align-items: center;
|
| 322 |
+
cursor: pointer;
|
| 323 |
+
transition: transform 0.2s;
|
| 324 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 325 |
+
&:hover {{
|
| 326 |
+
transform: scale(1.02);
|
| 327 |
+
}}">
|
| 328 |
+
<span style="color: #1a1a1a; font-size: 16px; font-weight: 500;">{name}</span>
|
| 329 |
+
</div>
|
| 330 |
+
"""
|
| 331 |
+
|
| 332 |
+
def create_ui(self):
|
| 333 |
+
css = """
|
| 334 |
+
:root {
|
| 335 |
+
--sidebar-width: 200px;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
body {
|
| 339 |
+
margin: 0;
|
| 340 |
+
padding: 0;
|
| 341 |
+
overflow: hidden;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
#sidebar {
|
| 345 |
+
background-color: #0A192F;
|
| 346 |
+
color: white;
|
| 347 |
+
padding: 32px 24px;
|
| 348 |
+
height: 100vh;
|
| 349 |
+
width: var(--sidebar-width);
|
| 350 |
+
position: fixed;
|
| 351 |
+
left: 0;
|
| 352 |
+
top: 0;
|
| 353 |
+
display: flex;
|
| 354 |
+
flex-direction: column;
|
| 355 |
+
box-sizing: border-box;
|
| 356 |
+
z-index: 1000;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
#main-content {
|
| 360 |
+
background-color: #0F172A;
|
| 361 |
+
min-height: 100vh;
|
| 362 |
+
margin-left: var(--sidebar-width);
|
| 363 |
+
padding: 50px;
|
| 364 |
+
box-sizing: border-box;
|
| 365 |
+
overflow-y: auto;
|
| 366 |
+
position: relative;
|
| 367 |
+
max-height: 100vh; /* Ensure content is scrollable */
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.nav-button {
|
| 371 |
+
background: none !important;
|
| 372 |
+
border: none !important;
|
| 373 |
+
box-shadow: none !important;
|
| 374 |
+
color: #94A3B8 !important;
|
| 375 |
+
padding: 8px 0 !important;
|
| 376 |
+
margin: 4px 0 !important;
|
| 377 |
+
min-width: unset !important;
|
| 378 |
+
width: 100% !important;
|
| 379 |
+
text-align: left !important;
|
| 380 |
+
font-size: 16px !important;
|
| 381 |
+
font-weight: 500 !important;
|
| 382 |
+
height: auto !important;
|
| 383 |
+
line-height: 1.2 !important;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.nav-button:hover {
|
| 387 |
+
color: white !important;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.nav-button[variant="primary"] {
|
| 391 |
+
color: white !important;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.tool-grid {
|
| 395 |
+
display: grid;
|
| 396 |
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
| 397 |
+
gap: 20px;
|
| 398 |
+
padding: 24px 0;
|
| 399 |
+
width: 100%;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.version-text {
|
| 403 |
+
margin-top: auto;
|
| 404 |
+
color: #64748B;
|
| 405 |
+
font-size: 12px;
|
| 406 |
+
padding-top: 16px;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.title {
|
| 410 |
+
font-size: 32px;
|
| 411 |
+
font-weight: 800;
|
| 412 |
+
margin-bottom: 32px;
|
| 413 |
+
color: white;
|
| 414 |
+
letter-spacing: -0.5px;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.page-title {
|
| 418 |
+
font-size: 28px;
|
| 419 |
+
font-weight: 700;
|
| 420 |
+
color: white;
|
| 421 |
+
margin: 0;
|
| 422 |
+
padding: 0;
|
| 423 |
+
letter-spacing: -0.5px;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
#content-wrapper {
|
| 427 |
+
width: 100%;
|
| 428 |
+
max-width: 100%;
|
| 429 |
+
padding: 24px;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.tool-card {
|
| 433 |
+
background: #1E293B !important;
|
| 434 |
+
border: none !important;
|
| 435 |
+
border-radius: 16px !important;
|
| 436 |
+
overflow: hidden !important;
|
| 437 |
+
transition: transform 0.2s !important;
|
| 438 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important;
|
| 439 |
+
height: 100% !important;
|
| 440 |
+
display: flex !important;
|
| 441 |
+
flex-direction: column !important;
|
| 442 |
+
width: auto !important;
|
| 443 |
+
min-height: 200px !important;
|
| 444 |
+
justify-content: flex-start !important;
|
| 445 |
+
align-items: stretch !important;
|
| 446 |
+
padding: 0 !important;
|
| 447 |
+
color: white !important;
|
| 448 |
+
text-align: left !important;
|
| 449 |
+
cursor: pointer !important;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.tool-card:hover {
|
| 453 |
+
transform: scale(1.02) !important;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.tool-card-img {
|
| 457 |
+
width: 100%;
|
| 458 |
+
height: 150px;
|
| 459 |
+
background: #2D3748;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
font-size: 48px;
|
| 464 |
+
color: #38BDF8;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.tool-card-content {
|
| 468 |
+
padding: 16px !important;
|
| 469 |
+
flex-grow: 1 !important;
|
| 470 |
+
display: flex !important;
|
| 471 |
+
flex-direction: column !important;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.tool-card-title {
|
| 475 |
+
font-size: 18px !important;
|
| 476 |
+
font-weight: 600 !important;
|
| 477 |
+
color: white !important;
|
| 478 |
+
margin: 0 0 8px 0 !important;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.tool-card-description {
|
| 482 |
+
font-size: 14px !important;
|
| 483 |
+
color: #94A3B8 !important;
|
| 484 |
+
margin: 0 !important;
|
| 485 |
+
flex-grow: 1 !important;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
.installed-indicator {
|
| 489 |
+
background-color: #10B981 !important;
|
| 490 |
+
color: white !important;
|
| 491 |
+
font-size: 12px !important;
|
| 492 |
+
font-weight: 600 !important;
|
| 493 |
+
padding: 4px 8px !important;
|
| 494 |
+
border-radius: 4px !important;
|
| 495 |
+
margin-top: 8px !important;
|
| 496 |
+
display: inline-block !important;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.tool-card.installed {
|
| 500 |
+
border: 2px solid #10B981 !important;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.add-tool-card {
|
| 504 |
+
background: rgba(255, 255, 255, 0.05) !important;
|
| 505 |
+
border: 2px dashed rgba(255, 255, 255, 0.2) !important;
|
| 506 |
+
color: rgba(255, 255, 255, 0.8) !important;
|
| 507 |
+
display: flex !important;
|
| 508 |
+
flex-direction: column !important;
|
| 509 |
+
justify-content: center !important;
|
| 510 |
+
align-items: center !important;
|
| 511 |
+
text-align: center !important;
|
| 512 |
+
padding: 16px !important;
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
.add-tool-icon {
|
| 516 |
+
font-size: 32px;
|
| 517 |
+
margin-bottom: 16px;
|
| 518 |
+
color: rgba(255, 255, 255, 0.5);
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.tool-card-button {
|
| 522 |
+
background: transparent !important;
|
| 523 |
+
border: none !important;
|
| 524 |
+
padding: 0 !important;
|
| 525 |
+
margin: 0 !important;
|
| 526 |
+
width: 100% !important;
|
| 527 |
+
height: auto !important;
|
| 528 |
+
min-height: 0 !important;
|
| 529 |
+
box-shadow: none !important;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.tool-details {
|
| 533 |
+
width: 100%;
|
| 534 |
+
max-width: 100%;
|
| 535 |
+
padding: 0;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.tool-details-container {
|
| 539 |
+
background: #0F172A;
|
| 540 |
+
border-radius: 0;
|
| 541 |
+
padding: 32px;
|
| 542 |
+
margin-bottom: 0;
|
| 543 |
+
max-height: 100vh;
|
| 544 |
+
overflow-y: auto;
|
| 545 |
+
width: 100%;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.tool-details-header {
|
| 549 |
+
display: flex;
|
| 550 |
+
align-items: center;
|
| 551 |
+
margin-bottom: 24px;
|
| 552 |
+
justify-content: space-between;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.tool-details-title-section {
|
| 556 |
+
display: flex;
|
| 557 |
+
align-items: center;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.tool-details-icon {
|
| 561 |
+
font-size: 28px;
|
| 562 |
+
margin-right: 16px;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.tool-details-title {
|
| 566 |
+
font-size: 28px;
|
| 567 |
+
font-weight: 700;
|
| 568 |
+
color: white;
|
| 569 |
+
margin: 0;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.tool-details-version {
|
| 573 |
+
font-size: 14px;
|
| 574 |
+
color: #94A3B8;
|
| 575 |
+
margin-left: 8px;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.tool-details-description {
|
| 579 |
+
font-size: 16px;
|
| 580 |
+
color: #E2E8F0;
|
| 581 |
+
margin-bottom: 32px;
|
| 582 |
+
max-width: 800px;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.tool-details-section-title {
|
| 586 |
+
font-size: 18px;
|
| 587 |
+
font-weight: 600;
|
| 588 |
+
color: white;
|
| 589 |
+
margin: 32px 0 16px 0;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
.tool-details-features {
|
| 593 |
+
display: flex;
|
| 594 |
+
flex-direction: column;
|
| 595 |
+
gap: 12px;
|
| 596 |
+
margin-bottom: 32px;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.tool-details-feature {
|
| 600 |
+
display: flex;
|
| 601 |
+
align-items: center;
|
| 602 |
+
margin-bottom: 0;
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
.tool-details-feature-icon {
|
| 606 |
+
color: #38BDF8;
|
| 607 |
+
margin-right: 12px;
|
| 608 |
+
font-size: 16px;
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.tool-details-feature-text {
|
| 612 |
+
color: #E2E8F0;
|
| 613 |
+
font-size: 16px;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
.tool-details-screenshots {
|
| 617 |
+
display: grid;
|
| 618 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 619 |
+
gap: 16px;
|
| 620 |
+
margin-top: 16px;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
.tool-details-screenshot {
|
| 624 |
+
width: 100%;
|
| 625 |
+
border-radius: 8px;
|
| 626 |
+
overflow: hidden;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.tool-details-video {
|
| 630 |
+
width: 100%;
|
| 631 |
+
aspect-ratio: 16/9;
|
| 632 |
+
border-radius: 8px;
|
| 633 |
+
overflow: hidden;
|
| 634 |
+
margin-top: 16px;
|
| 635 |
+
background: #1E293B;
|
| 636 |
+
border: none;
|
| 637 |
+
max-width: 800px;
|
| 638 |
+
height: 450px;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
.back-button {
|
| 642 |
+
background: transparent !important;
|
| 643 |
+
border: none !important;
|
| 644 |
+
color: #94A3B8 !important;
|
| 645 |
+
font-size: 14px !important;
|
| 646 |
+
padding: 8px 16px !important;
|
| 647 |
+
border-radius: 8px !important;
|
| 648 |
+
cursor: pointer !important;
|
| 649 |
+
transition: background-color 0.2s !important;
|
| 650 |
+
text-align: left !important;
|
| 651 |
+
width: auto !important;
|
| 652 |
+
margin-bottom: 24px !important;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.back-button:hover {
|
| 656 |
+
background: rgba(255, 255, 255, 0.1) !important;
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
.install-button {
|
| 660 |
+
background: #38BDF8 !important;
|
| 661 |
+
color: #0F172A !important;
|
| 662 |
+
font-weight: 600 !important;
|
| 663 |
+
padding: 12px 24px !important;
|
| 664 |
+
border-radius: 8px !important;
|
| 665 |
+
margin-top: 24px !important;
|
| 666 |
+
width: 100% !important;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
.install-button:hover {
|
| 670 |
+
background: #0EA5E9 !important;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.remove-button {
|
| 674 |
+
background: #EF4444 !important;
|
| 675 |
+
color: white !important;
|
| 676 |
+
font-weight: 600 !important;
|
| 677 |
+
padding: 12px 24px !important;
|
| 678 |
+
border-radius: 8px !important;
|
| 679 |
+
margin-top: 24px !important;
|
| 680 |
+
width: 100% !important;
|
| 681 |
+
border: none !important;
|
| 682 |
+
cursor: pointer !important;
|
| 683 |
+
transition: background-color 0.2s !important;
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
.remove-button:hover {
|
| 687 |
+
background: #DC2626 !important;
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
.tool-details-action .remove-button {
|
| 691 |
+
margin-top: 0 !important;
|
| 692 |
+
padding: 8px 16px !important;
|
| 693 |
+
font-size: 14px !important;
|
| 694 |
+
width: auto !important;
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
/* Confirmation dialog styling */
|
| 698 |
+
.confirmation-dialog {
|
| 699 |
+
background-color: #1E293B;
|
| 700 |
+
border-radius: 8px;
|
| 701 |
+
padding: 24px;
|
| 702 |
+
margin-bottom: 24px;
|
| 703 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 704 |
+
border: 1px solid #2D3748;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.confirmation-title {
|
| 708 |
+
font-size: 18px;
|
| 709 |
+
font-weight: 600;
|
| 710 |
+
color: white;
|
| 711 |
+
margin-bottom: 16px;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
.confirmation-message {
|
| 715 |
+
font-size: 14px;
|
| 716 |
+
color: #E2E8F0;
|
| 717 |
+
margin-bottom: 24px;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.confirmation-buttons {
|
| 721 |
+
display: flex;
|
| 722 |
+
justify-content: flex-end;
|
| 723 |
+
gap: 12px;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
.cancel-button {
|
| 727 |
+
background: #4B5563 !important;
|
| 728 |
+
color: white !important;
|
| 729 |
+
font-weight: 600 !important;
|
| 730 |
+
padding: 8px 16px !important;
|
| 731 |
+
border-radius: 8px !important;
|
| 732 |
+
border: none !important;
|
| 733 |
+
cursor: pointer !important;
|
| 734 |
+
transition: background-color 0.2s !important;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.cancel-button:hover {
|
| 738 |
+
background: #374151 !important;
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.confirm-button {
|
| 742 |
+
background: #EF4444 !important;
|
| 743 |
+
color: white !important;
|
| 744 |
+
font-weight: 600 !important;
|
| 745 |
+
padding: 8px 16px !important;
|
| 746 |
+
border-radius: 8px !important;
|
| 747 |
+
border: none !important;
|
| 748 |
+
cursor: pointer !important;
|
| 749 |
+
transition: background-color 0.2s !important;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
.confirm-button:hover {
|
| 753 |
+
background: #DC2626 !important;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
/* Modal styling */
|
| 757 |
+
#installation-modal {
|
| 758 |
+
max-width: 500px;
|
| 759 |
+
border-radius: 8px;
|
| 760 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
#installation-status {
|
| 764 |
+
font-size: 18px;
|
| 765 |
+
font-weight: 600;
|
| 766 |
+
margin-bottom: 12px;
|
| 767 |
+
padding-bottom: 8px;
|
| 768 |
+
border-bottom: 1px solid #e5e7eb;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
#installation-result {
|
| 772 |
+
font-size: 14px;
|
| 773 |
+
margin-bottom: 20px;
|
| 774 |
+
white-space: pre-wrap;
|
| 775 |
+
max-height: 300px;
|
| 776 |
+
overflow-y: auto;
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
#close-modal-btn {
|
| 780 |
+
background-color: #6B7280;
|
| 781 |
+
color: white;
|
| 782 |
+
border: none;
|
| 783 |
+
padding: 8px 16px;
|
| 784 |
+
border-radius: 4px;
|
| 785 |
+
cursor: pointer;
|
| 786 |
+
font-weight: 500;
|
| 787 |
+
transition: background-color 0.2s;
|
| 788 |
+
margin-top: 12px;
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
#close-modal-btn:hover {
|
| 792 |
+
background-color: #4B5563;
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
/* Installation result styling */
|
| 796 |
+
#installation-result-container {
|
| 797 |
+
margin-bottom: 20px;
|
| 798 |
+
border: 1px solid #2D3748;
|
| 799 |
+
border-radius: 8px;
|
| 800 |
+
padding: 16px;
|
| 801 |
+
background-color: #1E293B;
|
| 802 |
+
max-width: 100%;
|
| 803 |
+
position: relative;
|
| 804 |
+
z-index: 100;
|
| 805 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 806 |
+
margin-top: 0;
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
#installation-status {
|
| 810 |
+
font-size: 18px;
|
| 811 |
+
font-weight: 600;
|
| 812 |
+
margin-bottom: 12px;
|
| 813 |
+
padding-bottom: 8px;
|
| 814 |
+
border-bottom: 1px solid #2D3748;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
#installation-result {
|
| 818 |
+
font-size: 14px;
|
| 819 |
+
margin-bottom: 20px;
|
| 820 |
+
white-space: pre-wrap;
|
| 821 |
+
max-height: 300px;
|
| 822 |
+
overflow-y: auto;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
#close-result-btn {
|
| 826 |
+
background-color: #6B7280;
|
| 827 |
+
color: white;
|
| 828 |
+
border: none;
|
| 829 |
+
padding: 8px 16px;
|
| 830 |
+
border-radius: 4px;
|
| 831 |
+
cursor: pointer;
|
| 832 |
+
font-weight: 500;
|
| 833 |
+
transition: background-color 0.2s;
|
| 834 |
+
margin-top: 12px;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
#close-result-btn:hover {
|
| 838 |
+
background-color: #4B5563;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
.tool-details-action {
|
| 842 |
+
display: flex;
|
| 843 |
+
align-items: center;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.tool-details-action .install-button {
|
| 847 |
+
margin-top: 0 !important;
|
| 848 |
+
padding: 8px 16px !important;
|
| 849 |
+
font-size: 14px !important;
|
| 850 |
+
width: auto !important;
|
| 851 |
+
}
|
| 852 |
+
"""
|
| 853 |
+
|
| 854 |
+
with gr.Blocks(theme=gr.themes.Default(), css=css) as app:
|
| 855 |
+
# Define state variables
|
| 856 |
+
marketplace_tools_state = gr.State([])
|
| 857 |
+
current_tool_details_state = gr.State(None)
|
| 858 |
+
|
| 859 |
+
# Define event handlers first, before any references to them
|
| 860 |
+
def show_tools():
|
| 861 |
+
# Create updates to hide all tool containers
|
| 862 |
+
tool_container_updates = [gr.update(visible=False) for _ in tool_containers]
|
| 863 |
+
|
| 864 |
+
# Generate fresh HTML for tools grid based on current tools
|
| 865 |
+
tools_html = ''
|
| 866 |
+
for tool_id, tool in self.tools.items():
|
| 867 |
+
# Create HTML for each tool card
|
| 868 |
+
tools_html += f'''
|
| 869 |
+
<div class="tool-card" id="tool-card-{tool_id}" onclick="document.getElementById('tool-btn-{tool_id}').click()">
|
| 870 |
+
<div class="tool-card-img">
|
| 871 |
+
{tool.icon}
|
| 872 |
+
</div>
|
| 873 |
+
<div class="tool-card-content">
|
| 874 |
+
<h3 class="tool-card-title">{tool.name}</h3>
|
| 875 |
+
<p class="tool-card-description">{tool.description if hasattr(tool, 'description') else ""}</p>
|
| 876 |
+
</div>
|
| 877 |
+
</div>
|
| 878 |
+
'''
|
| 879 |
+
|
| 880 |
+
# Add "Add New Tool" button
|
| 881 |
+
tools_html += '''
|
| 882 |
+
<div class="tool-card add-tool-card" onclick="document.getElementById('add-tool-btn').click()">
|
| 883 |
+
<div class="add-tool-icon">+</div>
|
| 884 |
+
<div>Add New Tool from Marketplace</div>
|
| 885 |
+
</div>
|
| 886 |
+
'''
|
| 887 |
+
|
| 888 |
+
# Create HTML update for tools grid
|
| 889 |
+
tools_html_component = gr.HTML(f'<div class="tool-grid">{tools_html}</div>')
|
| 890 |
+
|
| 891 |
+
return [
|
| 892 |
+
gr.update(visible=True), # tools_content
|
| 893 |
+
gr.update(visible=False), # marketplace_content
|
| 894 |
+
gr.update(visible=False), # settings_content
|
| 895 |
+
gr.update(visible=False), # tool_interfaces
|
| 896 |
+
gr.update(visible=False), # tool_details_content
|
| 897 |
+
gr.update(variant="primary"), # tools_btn
|
| 898 |
+
gr.update(variant="secondary"), # marketplace_btn
|
| 899 |
+
gr.update(variant="secondary"), # settings_btn,
|
| 900 |
+
tools_html_component, # tools_html_component
|
| 901 |
+
gr.update(value="") # tool_interfaces_html
|
| 902 |
+
] + tool_container_updates
|
| 903 |
+
|
| 904 |
+
def show_marketplace():
|
| 905 |
+
# Fetch marketplace tools
|
| 906 |
+
tools = self.fetch_marketplace_tools()
|
| 907 |
+
|
| 908 |
+
# Create HTML for marketplace grid
|
| 909 |
+
html = '<div class="tool-grid">'
|
| 910 |
+
|
| 911 |
+
for i, tool in enumerate(tools):
|
| 912 |
+
# Use a unique ID for each card based on the tool ID
|
| 913 |
+
# Add a class to indicate if the tool is installed
|
| 914 |
+
installed_class = "installed" if tool.get('is_installed', False) else ""
|
| 915 |
+
|
| 916 |
+
# Add an indicator for installed tools
|
| 917 |
+
installed_indicator = '<div class="installed-indicator">✓ Installed</div>' if tool.get('is_installed', False) else ''
|
| 918 |
+
|
| 919 |
+
html += f'''
|
| 920 |
+
<div class="tool-card {installed_class}" id="card-{tool['id']}" onclick="document.getElementById('marketplace-tool-btn-{i}').click()">
|
| 921 |
+
<div class="tool-card-img">
|
| 922 |
+
{tool['icon']}
|
| 923 |
+
</div>
|
| 924 |
+
<div class="tool-card-content">
|
| 925 |
+
<h3 class="tool-card-title">{tool['name']}</h3>
|
| 926 |
+
<p class="tool-card-description">{tool['description']}</p>
|
| 927 |
+
{installed_indicator}
|
| 928 |
+
</div>
|
| 929 |
+
</div>
|
| 930 |
+
'''
|
| 931 |
+
|
| 932 |
+
html += '</div>'
|
| 933 |
+
|
| 934 |
+
# Hide installation result container and confirmation dialog
|
| 935 |
+
installation_result_container_update = gr.update(visible=False)
|
| 936 |
+
confirmation_dialog_update = gr.update(visible=False)
|
| 937 |
+
|
| 938 |
+
# Create updates for all tool containers
|
| 939 |
+
tool_container_updates = [gr.update(visible=False) for _ in tool_containers]
|
| 940 |
+
|
| 941 |
+
# Update marketplace_tools_state with the latest tools
|
| 942 |
+
# This ensures the marketplace shows the current state of available tools
|
| 943 |
+
|
| 944 |
+
return [
|
| 945 |
+
gr.update(visible=False), # tools_content
|
| 946 |
+
gr.update(visible=True), # marketplace_content
|
| 947 |
+
gr.update(visible=False), # settings_content
|
| 948 |
+
gr.update(visible=False), # tool_interfaces
|
| 949 |
+
gr.update(visible=False), # tool_details_content
|
| 950 |
+
gr.update(variant="secondary"), # tools_btn
|
| 951 |
+
gr.update(variant="primary"), # marketplace_btn
|
| 952 |
+
gr.update(variant="secondary"), # settings_btn
|
| 953 |
+
tools, # marketplace_tools_state - updated with latest tools
|
| 954 |
+
html, # marketplace_html
|
| 955 |
+
installation_result_container_update, # installation_result_container
|
| 956 |
+
confirmation_dialog_update, # confirmation_dialog
|
| 957 |
+
gr.update(value="") # tool_interfaces_html
|
| 958 |
+
] + tool_container_updates
|
| 959 |
+
|
| 960 |
+
def show_settings():
|
| 961 |
+
# Create updates to hide all tool containers
|
| 962 |
+
tool_container_updates = [gr.update(visible=False) for _ in tool_containers]
|
| 963 |
+
|
| 964 |
+
return [
|
| 965 |
+
gr.update(visible=False), # tools_content
|
| 966 |
+
gr.update(visible=False), # marketplace_content
|
| 967 |
+
gr.update(visible=True), # settings_content
|
| 968 |
+
gr.update(visible=False), # tool_interfaces
|
| 969 |
+
gr.update(visible=False), # tool_details_content
|
| 970 |
+
gr.update(variant="secondary"), # tools_btn
|
| 971 |
+
gr.update(variant="secondary"), # marketplace_btn
|
| 972 |
+
gr.update(variant="primary"), # settings_btn
|
| 973 |
+
gr.update(value="") # tool_interfaces_html
|
| 974 |
+
] + tool_container_updates
|
| 975 |
+
|
| 976 |
+
def show_tool(tool_id=None):
|
| 977 |
+
# Import gradio at the beginning of the function
|
| 978 |
+
import gradio as gr
|
| 979 |
+
|
| 980 |
+
# Check if the tool exists
|
| 981 |
+
if tool_id not in self.tools:
|
| 982 |
+
print(f"Tool {tool_id} not found")
|
| 983 |
+
return show_tools()
|
| 984 |
+
|
| 985 |
+
# Get the tool
|
| 986 |
+
tool = self.tools[tool_id]
|
| 987 |
+
|
| 988 |
+
# Check if this is a newly installed tool that needs special handling
|
| 989 |
+
if hasattr(tool, 'newly_installed') and tool.newly_installed:
|
| 990 |
+
print(f"Dynamically creating UI for newly installed tool: {tool_id}")
|
| 991 |
+
|
| 992 |
+
try:
|
| 993 |
+
# Create a message to inform the user that we're loading the tool
|
| 994 |
+
loading_message = f"""
|
| 995 |
+
<div style="text-align: center; padding: 20px;">
|
| 996 |
+
<h2>Loading {tool.name}...</h2>
|
| 997 |
+
<p>Please wait while we set up the tool interface.</p>
|
| 998 |
+
</div>
|
| 999 |
+
"""
|
| 1000 |
+
|
| 1001 |
+
# First, return a loading message
|
| 1002 |
+
loading_updates = [
|
| 1003 |
+
gr.update(visible=False), # tools_content
|
| 1004 |
+
gr.update(visible=False), # marketplace_content
|
| 1005 |
+
gr.update(visible=False), # settings_content
|
| 1006 |
+
gr.update(visible=True), # tool_interfaces
|
| 1007 |
+
gr.update(visible=False), # tool_details_content
|
| 1008 |
+
gr.update(variant="secondary"), # tools_btn
|
| 1009 |
+
gr.update(variant="secondary"), # marketplace_btn
|
| 1010 |
+
gr.update(variant="secondary"), # settings_btn
|
| 1011 |
+
gr.update(value=loading_message) # tool_interfaces_html
|
| 1012 |
+
] + [gr.update(visible=False) for _ in tool_containers]
|
| 1013 |
+
|
| 1014 |
+
# Instead of trying to create the UI dynamically, which can cause issues with Gradio components,
|
| 1015 |
+
# we'll create a simple HTML interface that provides basic functionality and instructions
|
| 1016 |
+
|
| 1017 |
+
# Get the tool class name and module for display
|
| 1018 |
+
tool_class_name = tool.ui_class.__name__
|
| 1019 |
+
tool_module_name = tool.ui_module
|
| 1020 |
+
|
| 1021 |
+
# Create a friendly HTML interface
|
| 1022 |
+
tool_html = f"""
|
| 1023 |
+
<div style="text-align: center; padding: 20px;">
|
| 1024 |
+
<h2>{tool.name} is Ready!</h2>
|
| 1025 |
+
<p>This tool has been successfully installed.</p>
|
| 1026 |
+
<p>For the best experience with this tool, please restart the application.</p>
|
| 1027 |
+
<p>After restarting, you'll be able to use all the features of this tool.</p>
|
| 1028 |
+
<!--<div style="margin-top: 20px;">
|
| 1029 |
+
<button onclick="window.location.reload()" style="background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;">Refresh Page</button>
|
| 1030 |
+
</div>-->
|
| 1031 |
+
</div>
|
| 1032 |
+
"""
|
| 1033 |
+
|
| 1034 |
+
# Remove the newly_installed flag so we don't try to create the UI again
|
| 1035 |
+
delattr(tool, 'newly_installed')
|
| 1036 |
+
|
| 1037 |
+
# Return the HTML interface
|
| 1038 |
+
return [
|
| 1039 |
+
gr.update(visible=False), # tools_content
|
| 1040 |
+
gr.update(visible=False), # marketplace_content
|
| 1041 |
+
gr.update(visible=False), # settings_content
|
| 1042 |
+
gr.update(visible=True), # tool_interfaces
|
| 1043 |
+
gr.update(visible=False), # tool_details_content
|
| 1044 |
+
gr.update(variant="secondary"), # tools_btn
|
| 1045 |
+
gr.update(variant="secondary"), # marketplace_btn
|
| 1046 |
+
gr.update(variant="secondary"), # settings_btn
|
| 1047 |
+
gr.update(value=tool_html) # tool_interfaces_html
|
| 1048 |
+
] + [gr.update(visible=False) for _ in tool_containers]
|
| 1049 |
+
except Exception as e:
|
| 1050 |
+
print(f"Error creating UI for newly installed tool: {e}")
|
| 1051 |
+
import traceback
|
| 1052 |
+
traceback.print_exc()
|
| 1053 |
+
|
| 1054 |
+
# If there's an error, show a message asking the user to restart
|
| 1055 |
+
restart_message = f"""
|
| 1056 |
+
<div style="text-align: center; padding: 20px;">
|
| 1057 |
+
<h2>Error Loading {tool.name}</h2>
|
| 1058 |
+
<p>There was an error setting up the tool interface:</p>
|
| 1059 |
+
<pre style="text-align: left; background: #f0f0f0; padding: 10px; border-radius: 5px; overflow: auto;">{str(e)}</pre>
|
| 1060 |
+
<p>Please restart the application to use this tool.</p>
|
| 1061 |
+
<div style="margin-top: 20px;">
|
| 1062 |
+
<button onclick="window.location.reload()" style="background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;">Refresh Page</button>
|
| 1063 |
+
</div>
|
| 1064 |
+
</div>
|
| 1065 |
+
"""
|
| 1066 |
+
|
| 1067 |
+
return [
|
| 1068 |
+
gr.update(visible=False), # tools_content
|
| 1069 |
+
gr.update(visible=False), # marketplace_content
|
| 1070 |
+
gr.update(visible=False), # settings_content
|
| 1071 |
+
gr.update(visible=True), # tool_interfaces
|
| 1072 |
+
gr.update(visible=False), # tool_details_content
|
| 1073 |
+
gr.update(variant="secondary"), # tools_btn
|
| 1074 |
+
gr.update(variant="secondary"), # marketplace_btn
|
| 1075 |
+
gr.update(variant="secondary"), # settings_btn
|
| 1076 |
+
gr.update(value=restart_message) # tool_interfaces_html
|
| 1077 |
+
] + [gr.update(visible=False) for _ in tool_containers]
|
| 1078 |
+
|
| 1079 |
+
# For existing tools with proper UI containers
|
| 1080 |
+
# Create a list of visibility updates for all tool containers
|
| 1081 |
+
updates = []
|
| 1082 |
+
|
| 1083 |
+
for i, container in enumerate(tool_containers):
|
| 1084 |
+
# Get the tool_id for this container
|
| 1085 |
+
container_tool_id = None
|
| 1086 |
+
for tid, t in self.tools.items():
|
| 1087 |
+
if hasattr(t, 'ui_container') and t.ui_container == container:
|
| 1088 |
+
container_tool_id = tid
|
| 1089 |
+
break
|
| 1090 |
+
|
| 1091 |
+
# Set visibility based on whether this is the selected tool
|
| 1092 |
+
if container_tool_id == tool_id:
|
| 1093 |
+
updates.append(gr.update(visible=True))
|
| 1094 |
+
else:
|
| 1095 |
+
updates.append(gr.update(visible=False))
|
| 1096 |
+
|
| 1097 |
+
return [
|
| 1098 |
+
gr.update(visible=False), # tools_content
|
| 1099 |
+
gr.update(visible=False), # marketplace_content
|
| 1100 |
+
gr.update(visible=False), # settings_content
|
| 1101 |
+
gr.update(visible=True), # tool_interfaces
|
| 1102 |
+
gr.update(visible=False), # tool_details_content
|
| 1103 |
+
gr.update(variant="secondary"), # tools_btn
|
| 1104 |
+
gr.update(variant="secondary"), # marketplace_btn
|
| 1105 |
+
gr.update(variant="secondary"), # settings_btn
|
| 1106 |
+
gr.update(value="") # tool_interfaces_html
|
| 1107 |
+
] + updates
|
| 1108 |
+
|
| 1109 |
+
def show_tool_details(tool_id):
|
| 1110 |
+
# Fetch tool details
|
| 1111 |
+
tool = self.fetch_tool_details(tool_id)
|
| 1112 |
+
|
| 1113 |
+
# Create updates to hide all tool containers
|
| 1114 |
+
tool_container_updates = [gr.update(visible=False) for _ in tool_containers]
|
| 1115 |
+
|
| 1116 |
+
if not tool:
|
| 1117 |
+
return [
|
| 1118 |
+
gr.update(visible=False), # tools_content
|
| 1119 |
+
gr.update(visible=True), # marketplace_content
|
| 1120 |
+
gr.update(visible=False), # settings_content
|
| 1121 |
+
gr.update(visible=False), # tool_interfaces
|
| 1122 |
+
gr.update(visible=False), # tool_details_content
|
| 1123 |
+
gr.update(variant="secondary"), # tools_btn
|
| 1124 |
+
gr.update(variant="primary"), # marketplace_btn
|
| 1125 |
+
gr.update(variant="secondary"), # settings_btn
|
| 1126 |
+
None, # current_tool_details_state
|
| 1127 |
+
"", # tool_details_html
|
| 1128 |
+
gr.update(visible=False), # installation_result_container
|
| 1129 |
+
gr.update(visible=False), # confirmation_dialog
|
| 1130 |
+
gr.update(value="") # tool_interfaces_html
|
| 1131 |
+
] + tool_container_updates
|
| 1132 |
+
|
| 1133 |
+
# Create features HTML
|
| 1134 |
+
features_html = "".join([
|
| 1135 |
+
f'<div class="tool-details-feature"><span class="tool-details-feature-icon">✓</span><span class="tool-details-feature-text">{feature}</span></div>'
|
| 1136 |
+
for feature in tool.get('features', [])
|
| 1137 |
+
])
|
| 1138 |
+
|
| 1139 |
+
# Create demo video HTML
|
| 1140 |
+
demo_video_html = f'<iframe class="tool-details-video" src="{tool.get("demo_video", "https://www.youtube.com/embed/dQw4w9WgXcQ")}" frameborder="0" allowfullscreen></iframe>' if True else ''
|
| 1141 |
+
|
| 1142 |
+
# Check if the tool is already installed
|
| 1143 |
+
# Use the is_installed flag if available, otherwise check self.tools
|
| 1144 |
+
is_installed = tool.get('is_installed', False) or tool_id in self.tools
|
| 1145 |
+
|
| 1146 |
+
# Determine which button to show based on installation status
|
| 1147 |
+
if is_installed:
|
| 1148 |
+
action_button = f'<button id="remove-tool-btn-inline" class="remove-button" onclick="document.getElementById(\'remove-btn\').click()">Remove Tool</button>'
|
| 1149 |
+
else:
|
| 1150 |
+
action_button = f'<button id="add-tool-btn-inline" class="install-button" onclick="document.getElementById(\'install-btn\').click()">Add Tool</button>'
|
| 1151 |
+
|
| 1152 |
+
# Create HTML for tool details
|
| 1153 |
+
html = f'''
|
| 1154 |
+
<div class="tool-details-container">
|
| 1155 |
+
<div class="tool-details">
|
| 1156 |
+
<div class="tool-details-header">
|
| 1157 |
+
<div class="tool-details-title-section">
|
| 1158 |
+
<div class="tool-details-icon">{tool['icon']}</div>
|
| 1159 |
+
<h2 class="tool-details-title">{tool['name']} <span class="tool-details-version">v{tool['version']}</span></h2>
|
| 1160 |
+
</div>
|
| 1161 |
+
<div class="tool-details-action">
|
| 1162 |
+
{action_button}
|
| 1163 |
+
</div>
|
| 1164 |
+
</div>
|
| 1165 |
+
<p class="tool-details-description">{tool['description']}</p>
|
| 1166 |
+
|
| 1167 |
+
<h3 class="tool-details-section-title">Features</h3>
|
| 1168 |
+
<div class="tool-details-features">
|
| 1169 |
+
{features_html}
|
| 1170 |
+
</div>
|
| 1171 |
+
|
| 1172 |
+
<h3 class="tool-details-section-title">Video</h3>
|
| 1173 |
+
{demo_video_html}
|
| 1174 |
+
</div>
|
| 1175 |
+
</div>
|
| 1176 |
+
'''
|
| 1177 |
+
|
| 1178 |
+
print(f"Setting current_tool_details_state to: {tool}")
|
| 1179 |
+
return [
|
| 1180 |
+
gr.update(visible=False), # tools_content
|
| 1181 |
+
gr.update(visible=False), # marketplace_content
|
| 1182 |
+
gr.update(visible=False), # settings_content
|
| 1183 |
+
gr.update(visible=False), # tool_interfaces
|
| 1184 |
+
gr.update(visible=True), # tool_details_content
|
| 1185 |
+
gr.update(variant="secondary"), # tools_btn
|
| 1186 |
+
gr.update(variant="secondary"), # marketplace_btn
|
| 1187 |
+
gr.update(variant="secondary"), # settings_btn
|
| 1188 |
+
tool, # current_tool_details_state
|
| 1189 |
+
html, # tool_details_html
|
| 1190 |
+
gr.update(visible=False), # installation_result_container
|
| 1191 |
+
gr.update(visible=False), # confirmation_dialog
|
| 1192 |
+
gr.update(value="") # tool_interfaces_html
|
| 1193 |
+
] + tool_container_updates
|
| 1194 |
+
|
| 1195 |
+
def install_selected_tool(tool_details):
|
| 1196 |
+
print(f"Installing tool: {tool_details}")
|
| 1197 |
+
|
| 1198 |
+
if not tool_details:
|
| 1199 |
+
print("No tool details provided")
|
| 1200 |
+
return [
|
| 1201 |
+
gr.update(visible=True), # installation_result_container
|
| 1202 |
+
"⚠️ Installation Failed", # installation_status
|
| 1203 |
+
"No tool selected to install." # installation_result
|
| 1204 |
+
]
|
| 1205 |
+
|
| 1206 |
+
try:
|
| 1207 |
+
# Install the tool
|
| 1208 |
+
result = self.install_tool(tool_details['id'])
|
| 1209 |
+
|
| 1210 |
+
if "Error" in result:
|
| 1211 |
+
return [
|
| 1212 |
+
gr.update(visible=True), # installation_result_container
|
| 1213 |
+
"⚠️ Installation Failed", # installation_status
|
| 1214 |
+
result # installation_result
|
| 1215 |
+
]
|
| 1216 |
+
else:
|
| 1217 |
+
# If installation was successful, we'll show a success message
|
| 1218 |
+
# The user can click "Close" to see the installation result
|
| 1219 |
+
# and then navigate to the tools page to see the new tool
|
| 1220 |
+
return [
|
| 1221 |
+
gr.update(visible=True), # installation_result_container
|
| 1222 |
+
"✅ Installation Successful", # installation_status
|
| 1223 |
+
result # installation_result
|
| 1224 |
+
]
|
| 1225 |
+
except Exception as e:
|
| 1226 |
+
print(f"Error installing tool: {e}")
|
| 1227 |
+
return [
|
| 1228 |
+
gr.update(visible=True), # installation_result_container
|
| 1229 |
+
"⚠️ Installation Failed", # installation_status
|
| 1230 |
+
f"Error installing tool: {e}" # installation_result
|
| 1231 |
+
]
|
| 1232 |
+
|
| 1233 |
+
# Create components after defining the functions
|
| 1234 |
+
with gr.Row():
|
| 1235 |
+
# Sidebar
|
| 1236 |
+
with gr.Column(elem_id="sidebar", scale=1):
|
| 1237 |
+
gr.Markdown("AI TOOLS", elem_classes="title")
|
| 1238 |
+
tools_btn = gr.Button("Tools", elem_classes="nav-button", variant="primary")
|
| 1239 |
+
marketplace_btn = gr.Button("Marketplace", elem_classes="nav-button", variant="secondary")
|
| 1240 |
+
settings_btn = gr.Button("Settings", elem_classes="nav-button", variant="secondary")
|
| 1241 |
+
gr.Markdown(f"version: {self.version}", elem_classes="version-text")
|
| 1242 |
+
|
| 1243 |
+
# Main content
|
| 1244 |
+
with gr.Column(elem_id="main-content", scale=4):
|
| 1245 |
+
# Define all content containers
|
| 1246 |
+
tools_content = gr.Column(visible=True)
|
| 1247 |
+
marketplace_content = gr.Column(visible=False)
|
| 1248 |
+
settings_content = gr.Column(visible=False)
|
| 1249 |
+
tool_interfaces = gr.Column(visible=False)
|
| 1250 |
+
tool_details_content = gr.Column(visible=False)
|
| 1251 |
+
|
| 1252 |
+
# Fill tools content
|
| 1253 |
+
with tools_content:
|
| 1254 |
+
gr.Markdown("Tools", elem_classes="page-title")
|
| 1255 |
+
|
| 1256 |
+
tool_buttons = []
|
| 1257 |
+
tool_ids = [] # Store tool_ids in the same order as tool_buttons
|
| 1258 |
+
|
| 1259 |
+
# Create HTML for all tools
|
| 1260 |
+
tools_html = ''
|
| 1261 |
+
|
| 1262 |
+
for tool_id, tool in self.tools.items():
|
| 1263 |
+
# Create HTML for each tool card
|
| 1264 |
+
tools_html += f'''
|
| 1265 |
+
<div class="tool-card" id="tool-card-{tool_id}" onclick="document.getElementById('tool-btn-{tool_id}').click()">
|
| 1266 |
+
<div class="tool-card-img">
|
| 1267 |
+
{tool.icon}
|
| 1268 |
+
</div>
|
| 1269 |
+
<div class="tool-card-content">
|
| 1270 |
+
<h3 class="tool-card-title">{tool.name}</h3>
|
| 1271 |
+
<p class="tool-card-description">{tool.description if hasattr(tool, 'description') else ""}</p>
|
| 1272 |
+
</div>
|
| 1273 |
+
</div>
|
| 1274 |
+
'''
|
| 1275 |
+
|
| 1276 |
+
# Create hidden buttons for tool selection
|
| 1277 |
+
btn = gr.Button(f"Select {tool.name}", visible=False, elem_id=f"tool-btn-{tool_id}")
|
| 1278 |
+
tool_buttons.append(btn)
|
| 1279 |
+
tool_ids.append(tool_id)
|
| 1280 |
+
|
| 1281 |
+
# Add "Add New Tool" button
|
| 1282 |
+
tools_html += '''
|
| 1283 |
+
<div class="tool-card add-tool-card" onclick="document.getElementById('add-tool-btn').click()">
|
| 1284 |
+
<div class="add-tool-icon">+</div>
|
| 1285 |
+
<div>Add New Tool from Marketplace</div>
|
| 1286 |
+
</div>
|
| 1287 |
+
'''
|
| 1288 |
+
|
| 1289 |
+
# Render the HTML inside a div with the tool-grid class
|
| 1290 |
+
tools_html_component = gr.HTML(f'<div class="tool-grid">{tools_html}</div>')
|
| 1291 |
+
|
| 1292 |
+
# Create hidden add tool button
|
| 1293 |
+
add_tool_btn = gr.Button("Add New Tool", visible=False, elem_id="add-tool-btn")
|
| 1294 |
+
|
| 1295 |
+
# Fill marketplace content
|
| 1296 |
+
with marketplace_content:
|
| 1297 |
+
gr.Markdown("Marketplace", elem_classes="page-title")
|
| 1298 |
+
marketplace_html = gr.HTML("Loading marketplace tools...")
|
| 1299 |
+
|
| 1300 |
+
# Create hidden buttons for marketplace tool details with unique IDs
|
| 1301 |
+
tool_buttons_container = gr.Column(visible=False)
|
| 1302 |
+
|
| 1303 |
+
# Create hidden buttons for marketplace tools
|
| 1304 |
+
marketplace_tool_buttons = []
|
| 1305 |
+
for i, tool in enumerate(self.fetch_marketplace_tools()):
|
| 1306 |
+
tool_btn = gr.Button(f"View {tool['name']}", visible=False, elem_id=f"marketplace-tool-btn-{i}")
|
| 1307 |
+
marketplace_tool_buttons.append((tool['id'], tool_btn))
|
| 1308 |
+
|
| 1309 |
+
# The click handlers for marketplace tool buttons will be set up after all components are defined
|
| 1310 |
+
|
| 1311 |
+
# Fill tool details content
|
| 1312 |
+
with tool_details_content:
|
| 1313 |
+
# Installation result section at the top
|
| 1314 |
+
with gr.Column(elem_id="installation-result-container", elem_classes="installation-result-container", visible=False) as installation_result_container:
|
| 1315 |
+
installation_status = gr.Markdown("Ready to install", elem_id="installation-status")
|
| 1316 |
+
installation_result = gr.Markdown("Click the Install Tool button to install the selected tool.", elem_id="installation-result")
|
| 1317 |
+
close_result_btn = gr.Button("Close", elem_id="close-result-btn")
|
| 1318 |
+
|
| 1319 |
+
# Confirmation dialog for tool removal
|
| 1320 |
+
with gr.Column(elem_id="confirmation-dialog", elem_classes="confirmation-dialog", visible=False) as confirmation_dialog:
|
| 1321 |
+
gr.Markdown("⚠️ Warning: Remove Tool", elem_id="confirmation-title", elem_classes="confirmation-title")
|
| 1322 |
+
gr.Markdown(
|
| 1323 |
+
"Are you sure you want to remove this tool? This action cannot be undone. "
|
| 1324 |
+
"All data created by this tool will be permanently deleted.",
|
| 1325 |
+
elem_id="confirmation-message",
|
| 1326 |
+
elem_classes="confirmation-message"
|
| 1327 |
+
)
|
| 1328 |
+
with gr.Row(elem_classes="confirmation-buttons"):
|
| 1329 |
+
cancel_btn = gr.Button("Cancel", elem_classes="cancel-button")
|
| 1330 |
+
confirm_btn = gr.Button("Yes, Remove Tool", elem_classes="confirm-button")
|
| 1331 |
+
|
| 1332 |
+
with gr.Row():
|
| 1333 |
+
with gr.Column(scale=1):
|
| 1334 |
+
back_btn = gr.Button("← Back to Marketplace", elem_classes="back-button")
|
| 1335 |
+
with gr.Column(scale=3):
|
| 1336 |
+
gr.Markdown("", elem_classes="page-title")
|
| 1337 |
+
|
| 1338 |
+
# Tool details content
|
| 1339 |
+
tool_details_html = gr.HTML("")
|
| 1340 |
+
|
| 1341 |
+
# Hidden install button (will be triggered by the inline button)
|
| 1342 |
+
install_btn = gr.Button("Add Tool", elem_classes="install-button", elem_id="install-btn", variant="primary", visible=False)
|
| 1343 |
+
|
| 1344 |
+
# Hidden remove button (will be triggered by the inline button)
|
| 1345 |
+
remove_btn = gr.Button("Remove Tool", elem_classes="remove-button", elem_id="remove-btn", variant="primary", visible=False)
|
| 1346 |
+
|
| 1347 |
+
# Add a direct event handler to the install button
|
| 1348 |
+
def debug_install_click():
|
| 1349 |
+
print("Install button clicked directly!")
|
| 1350 |
+
return [gr.update(visible=True), gr.update(value="Install button clicked!")]
|
| 1351 |
+
|
| 1352 |
+
# Add a direct event handler to the remove button to show confirmation dialog
|
| 1353 |
+
def show_confirmation_dialog():
|
| 1354 |
+
print("Remove button clicked - showing confirmation dialog")
|
| 1355 |
+
return gr.update(visible=True)
|
| 1356 |
+
|
| 1357 |
+
# Function to handle tool removal confirmation
|
| 1358 |
+
def remove_selected_tool(tool_details):
|
| 1359 |
+
print(f"Removing tool: {tool_details}")
|
| 1360 |
+
|
| 1361 |
+
if not tool_details:
|
| 1362 |
+
print("No tool details provided")
|
| 1363 |
+
return [
|
| 1364 |
+
gr.update(visible=False), # confirmation_dialog
|
| 1365 |
+
gr.update(visible=True), # installation_result_container
|
| 1366 |
+
"⚠️ Removal Failed", # installation_status
|
| 1367 |
+
"No tool selected to remove." # installation_result
|
| 1368 |
+
]
|
| 1369 |
+
|
| 1370 |
+
try:
|
| 1371 |
+
# Remove the tool
|
| 1372 |
+
result = self.remove_tool(tool_details['id'])
|
| 1373 |
+
|
| 1374 |
+
if "Error" in result:
|
| 1375 |
+
return [
|
| 1376 |
+
gr.update(visible=False), # confirmation_dialog
|
| 1377 |
+
gr.update(visible=True), # installation_result_container
|
| 1378 |
+
"⚠️ Removal Failed", # installation_status
|
| 1379 |
+
result # installation_result
|
| 1380 |
+
]
|
| 1381 |
+
else:
|
| 1382 |
+
# After successful removal, show success message
|
| 1383 |
+
# The user will need to click "Back to Marketplace" manually
|
| 1384 |
+
# This avoids issues with dynamic UI updates
|
| 1385 |
+
return [
|
| 1386 |
+
gr.update(visible=False), # confirmation_dialog
|
| 1387 |
+
gr.update(visible=True), # installation_result_container
|
| 1388 |
+
"✅ Removal Successful", # installation_status
|
| 1389 |
+
f"{result} - Click 'Back to Marketplace' to return. You may need to restart the application to see all changes." # installation_result
|
| 1390 |
+
]
|
| 1391 |
+
except Exception as e:
|
| 1392 |
+
print(f"Error removing tool: {e}")
|
| 1393 |
+
return [
|
| 1394 |
+
gr.update(visible=False), # confirmation_dialog
|
| 1395 |
+
gr.update(visible=True), # installation_result_container
|
| 1396 |
+
"⚠️ Removal Failed", # installation_status
|
| 1397 |
+
f"Error removing tool: {e}" # installation_result
|
| 1398 |
+
]
|
| 1399 |
+
|
| 1400 |
+
# Function to cancel removal
|
| 1401 |
+
def cancel_removal():
|
| 1402 |
+
return gr.update(visible=False)
|
| 1403 |
+
|
| 1404 |
+
debug_output = gr.Textbox(label="Debug Output", visible=False)
|
| 1405 |
+
|
| 1406 |
+
# Set up event handlers for buttons
|
| 1407 |
+
install_btn.click(
|
| 1408 |
+
fn=install_selected_tool,
|
| 1409 |
+
inputs=[current_tool_details_state],
|
| 1410 |
+
outputs=[installation_result_container, installation_status, installation_result]
|
| 1411 |
+
)
|
| 1412 |
+
remove_btn.click(fn=show_confirmation_dialog, outputs=confirmation_dialog)
|
| 1413 |
+
|
| 1414 |
+
# Set up confirmation dialog buttons
|
| 1415 |
+
cancel_btn.click(fn=cancel_removal, outputs=confirmation_dialog)
|
| 1416 |
+
confirm_btn.click(
|
| 1417 |
+
fn=remove_selected_tool,
|
| 1418 |
+
inputs=[current_tool_details_state],
|
| 1419 |
+
outputs=[confirmation_dialog, installation_result_container, installation_status, installation_result]
|
| 1420 |
+
)
|
| 1421 |
+
|
| 1422 |
+
# Fill settings content
|
| 1423 |
+
with settings_content:
|
| 1424 |
+
gr.Markdown("Settings", elem_classes="page-title")
|
| 1425 |
+
gr.Markdown("Settings options will appear here...")
|
| 1426 |
+
|
| 1427 |
+
# Fill tool interfaces
|
| 1428 |
+
with tool_interfaces:
|
| 1429 |
+
# Add an HTML component to display messages for newly installed tools
|
| 1430 |
+
tool_interfaces_html = gr.HTML("", elem_id="tool-interfaces-html")
|
| 1431 |
+
|
| 1432 |
+
for tool_id, tool in self.tools.items():
|
| 1433 |
+
with gr.Column(visible=False) as tool_container:
|
| 1434 |
+
tool_ui = tool.ui_class()
|
| 1435 |
+
tool_ui.create_ui()
|
| 1436 |
+
# Store the UI container in the tool object for later reference
|
| 1437 |
+
tool.ui_container = tool_container
|
| 1438 |
+
|
| 1439 |
+
# Set up event handlers after all components are defined
|
| 1440 |
+
# Set up tool button events
|
| 1441 |
+
|
| 1442 |
+
# Collect all tool containers for outputs
|
| 1443 |
+
tool_containers = []
|
| 1444 |
+
for tool_id, tool in self.tools.items():
|
| 1445 |
+
if hasattr(tool, 'ui_container'):
|
| 1446 |
+
tool_containers.append(tool.ui_container)
|
| 1447 |
+
|
| 1448 |
+
# Set up click handlers for marketplace tool buttons
|
| 1449 |
+
for i, (tool_id, tool_btn) in enumerate(marketplace_tool_buttons):
|
| 1450 |
+
# We need to create a separate function for each button to avoid lambda closure issues
|
| 1451 |
+
def make_handler(tid):
|
| 1452 |
+
return lambda: show_tool_details(tid)
|
| 1453 |
+
|
| 1454 |
+
handler = make_handler(tool_id)
|
| 1455 |
+
tool_btn.click(
|
| 1456 |
+
fn=handler,
|
| 1457 |
+
outputs=[
|
| 1458 |
+
tools_content, marketplace_content, settings_content, tool_interfaces,
|
| 1459 |
+
tool_details_content, tools_btn, marketplace_btn, settings_btn,
|
| 1460 |
+
current_tool_details_state, tool_details_html, installation_result_container,
|
| 1461 |
+
confirmation_dialog, tool_interfaces_html
|
| 1462 |
+
] + tool_containers
|
| 1463 |
+
)
|
| 1464 |
+
|
| 1465 |
+
for i, btn in enumerate(tool_buttons):
|
| 1466 |
+
if isinstance(btn, gr.Button): # Check if it's a button from the tools grid
|
| 1467 |
+
tool_id = tool_ids[i] # Get the corresponding tool_id
|
| 1468 |
+
|
| 1469 |
+
# We need to create a separate function for each button to avoid lambda closure issues
|
| 1470 |
+
def make_handler(tid):
|
| 1471 |
+
return lambda: show_tool(tid)
|
| 1472 |
+
|
| 1473 |
+
handler = make_handler(tool_id)
|
| 1474 |
+
btn.click(
|
| 1475 |
+
fn=handler,
|
| 1476 |
+
outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
|
| 1477 |
+
tool_details_content, tools_btn, marketplace_btn, settings_btn,
|
| 1478 |
+
tool_interfaces_html] + tool_containers
|
| 1479 |
+
)
|
| 1480 |
+
|
| 1481 |
+
# Set up navigation events
|
| 1482 |
+
tools_btn.click(
|
| 1483 |
+
fn=show_tools,
|
| 1484 |
+
outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
|
| 1485 |
+
tool_details_content, tools_btn, marketplace_btn, settings_btn,
|
| 1486 |
+
tools_html_component, tool_interfaces_html] + tool_containers
|
| 1487 |
+
)
|
| 1488 |
+
|
| 1489 |
+
marketplace_btn.click(
|
| 1490 |
+
fn=show_marketplace,
|
| 1491 |
+
outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
|
| 1492 |
+
tool_details_content, tools_btn, marketplace_btn, settings_btn,
|
| 1493 |
+
marketplace_tools_state, marketplace_html, installation_result_container,
|
| 1494 |
+
confirmation_dialog, tool_interfaces_html] + tool_containers
|
| 1495 |
+
)
|
| 1496 |
+
|
| 1497 |
+
settings_btn.click(
|
| 1498 |
+
fn=show_settings,
|
| 1499 |
+
outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
|
| 1500 |
+
tool_details_content, tools_btn, marketplace_btn, settings_btn,
|
| 1501 |
+
tool_interfaces_html] + tool_containers
|
| 1502 |
+
)
|
| 1503 |
+
|
| 1504 |
+
# Set up add tool button to navigate to marketplace
|
| 1505 |
+
add_tool_btn.click(
|
| 1506 |
+
fn=show_marketplace,
|
| 1507 |
+
outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
|
| 1508 |
+
tool_details_content, tools_btn, marketplace_btn, settings_btn,
|
| 1509 |
+
marketplace_tools_state, marketplace_html, installation_result_container,
|
| 1510 |
+
confirmation_dialog, tool_interfaces_html] + tool_containers
|
| 1511 |
+
)
|
| 1512 |
+
|
| 1513 |
+
# Set up event handlers for tool details
|
| 1514 |
+
back_btn.click(
|
| 1515 |
+
fn=show_marketplace,
|
| 1516 |
+
outputs=[
|
| 1517 |
+
tools_content, marketplace_content, settings_content, tool_interfaces,
|
| 1518 |
+
tool_details_content, tools_btn, marketplace_btn, settings_btn,
|
| 1519 |
+
marketplace_tools_state, marketplace_html, installation_result_container,
|
| 1520 |
+
confirmation_dialog
|
| 1521 |
+
] + tool_containers
|
| 1522 |
+
)
|
| 1523 |
+
|
| 1524 |
+
# Define a function to handle closing the installation result and navigating to tools
|
| 1525 |
+
def close_result_and_show_tools():
|
| 1526 |
+
# First hide the installation result container
|
| 1527 |
+
installation_result_update = gr.update(visible=False)
|
| 1528 |
+
|
| 1529 |
+
# Then get the updates from show_tools
|
| 1530 |
+
tools_updates = show_tools()
|
| 1531 |
+
|
| 1532 |
+
# Combine the updates
|
| 1533 |
+
return [installation_result_update] + tools_updates
|
| 1534 |
+
|
| 1535 |
+
# Set up close button for installation result
|
| 1536 |
+
close_result_btn.click(
|
| 1537 |
+
fn=close_result_and_show_tools,
|
| 1538 |
+
outputs=[installation_result_container,
|
| 1539 |
+
tools_content, marketplace_content, settings_content, tool_interfaces,
|
| 1540 |
+
tool_details_content, tools_btn, marketplace_btn, settings_btn,
|
| 1541 |
+
tools_html_component, tool_interfaces_html] + tool_containers
|
| 1542 |
+
)
|
| 1543 |
+
|
| 1544 |
+
return app
|
| 1545 |
+
|
| 1546 |
+
if __name__ == "__main__":
|
| 1547 |
+
ui = MainUI()
|
| 1548 |
+
demo = ui.create_ui()
|
| 1549 |
+
demo.queue() # Add queue for better handling of multiple requests
|
| 1550 |
+
demo.title = "AI Tools Platform" # Add a title
|
| 1551 |
+
demo.launch(
|
| 1552 |
+
debug=True,
|
| 1553 |
+
share=True,
|
| 1554 |
+
server_name="0.0.0.0",
|
| 1555 |
+
server_port=7860
|
| 1556 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
librosa==0.10.1
|
| 2 |
+
numpy==1.26.3
|
| 3 |
+
moviepy==2.1.2
|
| 4 |
+
opencv-python>=4.8.0
|
| 5 |
+
scikit-learn>=1.3.0
|
| 6 |
+
scenedetect~=0.6.5.2
|
| 7 |
+
gradio==4.19.2
|
| 8 |
+
flask==2.3.3
|
| 9 |
+
requests==2.31.0
|
| 10 |
+
rembg
|
| 11 |
+
Pillow
|
| 12 |
+
onnxruntime
|
tools/beatsyncer/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Beatsyncer package
|
tools/beatsyncer/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (178 Bytes). View file
|
|
|
tools/beatsyncer/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (166 Bytes). View file
|
|
|
tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-311.pyc
ADDED
|
Binary file (20.1 kB). View file
|
|
|
tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-312.pyc
ADDED
|
Binary file (18.3 kB). View file
|
|
|
tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-311.pyc
ADDED
|
Binary file (7.07 kB). View file
|
|
|
tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-312.pyc
ADDED
|
Binary file (6.08 kB). View file
|
|
|
tools/beatsyncer/beatsyncer_tool.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import random
|
| 3 |
+
import librosa
|
| 4 |
+
import numpy as np
|
| 5 |
+
from moviepy import *
|
| 6 |
+
|
| 7 |
+
class BeatSyncer:
|
| 8 |
+
AUDIO_EXTENSIONS = {'.mp3', '.wav', '.m4a', '.aac', '.ogg', '.flac'}
|
| 9 |
+
VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv'}
|
| 10 |
+
|
| 11 |
+
def __init__(self, audio_filename):
|
| 12 |
+
# Construct the full path to the audio file relative to the script's directory
|
| 13 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
+
input_path = os.path.join(script_dir, audio_filename)
|
| 15 |
+
|
| 16 |
+
# Convert to audio if needed
|
| 17 |
+
if self._is_video_file(input_path):
|
| 18 |
+
print(f"Converting video file '{input_path}' to audio...")
|
| 19 |
+
audio_path = self._convert_to_audio(input_path)
|
| 20 |
+
self.audio_path = audio_path
|
| 21 |
+
elif self._is_audio_file(input_path):
|
| 22 |
+
self.audio_path = input_path
|
| 23 |
+
else:
|
| 24 |
+
raise ValueError(f"Unsupported file format. File must be one of: {self.AUDIO_EXTENSIONS.union(self.VIDEO_EXTENSIONS)}")
|
| 25 |
+
|
| 26 |
+
self.y = None # Audio samples
|
| 27 |
+
self.sr = None # Sample rate
|
| 28 |
+
self.beat_times = None
|
| 29 |
+
self.beat_drop_times = None
|
| 30 |
+
|
| 31 |
+
def _is_audio_file(self, filepath):
|
| 32 |
+
"""Check if the file is an audio file based on its extension."""
|
| 33 |
+
return os.path.splitext(filepath)[1].lower() in self.AUDIO_EXTENSIONS
|
| 34 |
+
|
| 35 |
+
def _is_video_file(self, filepath):
|
| 36 |
+
"""Check if the file is a video file based on its extension."""
|
| 37 |
+
return os.path.splitext(filepath)[1].lower() in self.VIDEO_EXTENSIONS
|
| 38 |
+
|
| 39 |
+
def _convert_to_audio(self, video_path):
|
| 40 |
+
"""Convert a video file to audio and return the path to the audio file."""
|
| 41 |
+
try:
|
| 42 |
+
# Create tmp directory in project root if it doesn't exist
|
| 43 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 44 |
+
tmp_dir = os.path.join(script_dir, 'tmp')
|
| 45 |
+
os.makedirs(tmp_dir, exist_ok=True)
|
| 46 |
+
|
| 47 |
+
# Create audio filename in tmp directory
|
| 48 |
+
base_name = os.path.basename(video_path)
|
| 49 |
+
audio_name = os.path.splitext(base_name)[0] + '.mp3'
|
| 50 |
+
audio_path = os.path.join(tmp_dir, audio_name)
|
| 51 |
+
|
| 52 |
+
# Convert video to audio
|
| 53 |
+
video = VideoFileClip(video_path)
|
| 54 |
+
video.audio.write_audiofile(audio_path)
|
| 55 |
+
video.close()
|
| 56 |
+
|
| 57 |
+
return audio_path
|
| 58 |
+
except Exception as e:
|
| 59 |
+
raise ValueError(f"Error converting video to audio: {str(e)}")
|
| 60 |
+
|
| 61 |
+
def load_audio(self, duration_minutes=None):
|
| 62 |
+
"""
|
| 63 |
+
Load the audio file using librosa, limiting to duration_minutes if provided.
|
| 64 |
+
"""
|
| 65 |
+
try:
|
| 66 |
+
self.sr = 44100 # Set a standard sample rate
|
| 67 |
+
if duration_minutes is not None:
|
| 68 |
+
duration_seconds = duration_minutes * 60
|
| 69 |
+
else:
|
| 70 |
+
duration_seconds = None # Load full audio
|
| 71 |
+
self.y, _ = librosa.load(self.audio_path, sr=self.sr, duration=duration_seconds)
|
| 72 |
+
if self.y is None or len(self.y) == 0:
|
| 73 |
+
raise ValueError("Audio data is empty.")
|
| 74 |
+
duration = len(self.y) / self.sr
|
| 75 |
+
print(f"Loaded audio file '{self.audio_path}' with duration {duration:.2f} seconds.")
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"An error occurred while loading the audio file: {e}")
|
| 78 |
+
raise
|
| 79 |
+
|
| 80 |
+
def detect_beat_drops(self, sensitivity='medium'):
|
| 81 |
+
"""
|
| 82 |
+
Improved beat detection using onset_strength_multi and adjusted parameters.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
sensitivity (str): Detection sensitivity level ('low', 'medium', or 'high').
|
| 86 |
+
- low: Less sensitive, detects only major beat drops
|
| 87 |
+
- medium: Balanced sensitivity (default)
|
| 88 |
+
- high: More sensitive, detects subtle beat changes
|
| 89 |
+
"""
|
| 90 |
+
print("Detecting beat drops...")
|
| 91 |
+
if self.y is None or len(self.y) == 0:
|
| 92 |
+
raise ValueError("Audio data is empty. Cannot perform beat detection.")
|
| 93 |
+
|
| 94 |
+
# Map sensitivity levels to parameters
|
| 95 |
+
sensitivity_params = {
|
| 96 |
+
'low': {
|
| 97 |
+
'hop_length': 512, # Larger hop length for less granular detection
|
| 98 |
+
'std_multiplier': 1.0, # Higher threshold for beat detection
|
| 99 |
+
'min_interval': 0.05 # Longer minimum interval between beats
|
| 100 |
+
},
|
| 101 |
+
'medium': {
|
| 102 |
+
'hop_length': 256, # Default hop length
|
| 103 |
+
'std_multiplier': 0.5, # Default threshold
|
| 104 |
+
'min_interval': 0.02 # Default minimum interval
|
| 105 |
+
},
|
| 106 |
+
'high': {
|
| 107 |
+
'hop_length': 128, # Smaller hop length for more granular detection
|
| 108 |
+
'std_multiplier': 0.25, # Lower threshold for beat detection
|
| 109 |
+
'min_interval': 0.01 # Shorter minimum interval between beats
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
# Validate and get sensitivity parameters
|
| 114 |
+
sensitivity = sensitivity.lower()
|
| 115 |
+
if sensitivity not in sensitivity_params:
|
| 116 |
+
raise ValueError(f"Invalid sensitivity level. Must be one of: {list(sensitivity_params.keys())}")
|
| 117 |
+
|
| 118 |
+
params = sensitivity_params[sensitivity]
|
| 119 |
+
|
| 120 |
+
# Parameters for onset detection
|
| 121 |
+
hop_length = params['hop_length']
|
| 122 |
+
|
| 123 |
+
params.update({
|
| 124 |
+
"detrend": True,
|
| 125 |
+
"tightness": 100
|
| 126 |
+
})
|
| 127 |
+
|
| 128 |
+
# Compute the onset envelope using onset_strength_multi with adjusted parameters
|
| 129 |
+
onset_env_multi = librosa.onset.onset_strength_multi(
|
| 130 |
+
y=self.y,
|
| 131 |
+
sr=self.sr,
|
| 132 |
+
hop_length=params["hop_length"],
|
| 133 |
+
aggregate=np.median,
|
| 134 |
+
lag=1,
|
| 135 |
+
max_size=1,
|
| 136 |
+
detrend=params["detrend"],
|
| 137 |
+
center=True,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
# Sum across channels if multi-channel audio
|
| 141 |
+
onset_env = onset_env_multi.mean(axis=0)
|
| 142 |
+
|
| 143 |
+
# Beat tracking with adjusted parameters
|
| 144 |
+
tempo, beats = librosa.beat.beat_track(
|
| 145 |
+
onset_envelope=onset_env,
|
| 146 |
+
sr=self.sr,
|
| 147 |
+
hop_length=params["hop_length"],
|
| 148 |
+
tightness=params["tightness"],
|
| 149 |
+
units='time'
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
# Convert tempo to scalar if it's an array
|
| 153 |
+
tempo = float(np.mean(tempo)) if isinstance(tempo, np.ndarray) else float(tempo)
|
| 154 |
+
|
| 155 |
+
# Adjust tempo if it's unusually high (e.g., over 180 BPM)
|
| 156 |
+
if tempo >= 180:
|
| 157 |
+
tempo /= 2
|
| 158 |
+
print(f"Adjusted tempo: {tempo:.2f} BPM")
|
| 159 |
+
else:
|
| 160 |
+
print(f"Detected tempo: {tempo:.2f} BPM")
|
| 161 |
+
|
| 162 |
+
self.beat_times = beats
|
| 163 |
+
|
| 164 |
+
if len(self.beat_times) == 0:
|
| 165 |
+
raise ValueError("No beats detected in the audio.")
|
| 166 |
+
|
| 167 |
+
# Compute the difference in onset envelope with sensitivity-based threshold
|
| 168 |
+
onset_diff = np.diff(onset_env)
|
| 169 |
+
threshold = np.mean(onset_diff) + params['std_multiplier'] * np.std(onset_diff)
|
| 170 |
+
beat_drop_indices = np.where(onset_diff > threshold)[0] + 1
|
| 171 |
+
|
| 172 |
+
# Convert beat frames to times
|
| 173 |
+
onset_times = librosa.frames_to_time(
|
| 174 |
+
np.arange(len(onset_env)), sr=self.sr, hop_length=hop_length
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
# Map beat drop indices to times
|
| 178 |
+
beat_drop_times = onset_times[beat_drop_indices]
|
| 179 |
+
|
| 180 |
+
# Remove duplicate beat times
|
| 181 |
+
self.beat_drop_times = np.unique(beat_drop_times)
|
| 182 |
+
|
| 183 |
+
# Remove beat drops that are too close together based on sensitivity
|
| 184 |
+
min_interval = params['min_interval']
|
| 185 |
+
filtered_beat_drop_times = [self.beat_drop_times[0]]
|
| 186 |
+
for bd_time in self.beat_drop_times[1:]:
|
| 187 |
+
if bd_time - filtered_beat_drop_times[-1] >= min_interval:
|
| 188 |
+
filtered_beat_drop_times.append(bd_time)
|
| 189 |
+
self.beat_drop_times = np.array(filtered_beat_drop_times)
|
| 190 |
+
|
| 191 |
+
print(f"Detected {len(self.beat_drop_times)} beat drops with {sensitivity} sensitivity.")
|
| 192 |
+
|
| 193 |
+
def _get_media_files(self, media_dir):
|
| 194 |
+
"""
|
| 195 |
+
Get list of media files from the directory.
|
| 196 |
+
Returns a shuffled list of absolute paths to media files.
|
| 197 |
+
"""
|
| 198 |
+
supported_extensions = ('.mp4', '.avi', '.mov', '.png', '.jpg', '.jpeg', '.gif', '.mkv')
|
| 199 |
+
media_files = []
|
| 200 |
+
|
| 201 |
+
# Walk through directory
|
| 202 |
+
for root, _, files in os.walk(media_dir):
|
| 203 |
+
for file in files:
|
| 204 |
+
if file.lower().endswith(supported_extensions):
|
| 205 |
+
full_path = os.path.join(root, file)
|
| 206 |
+
media_files.append(full_path)
|
| 207 |
+
|
| 208 |
+
if not media_files:
|
| 209 |
+
raise ValueError(f"No supported media files found in directory: {media_dir}")
|
| 210 |
+
|
| 211 |
+
# Shuffle the media files list
|
| 212 |
+
random.shuffle(media_files)
|
| 213 |
+
print(f"Found and shuffled {len(media_files)} media files")
|
| 214 |
+
|
| 215 |
+
return media_files
|
| 216 |
+
|
| 217 |
+
def _calculate_text_duration(self, text):
|
| 218 |
+
"""
|
| 219 |
+
Calculate the duration a text should be displayed based on its word count.
|
| 220 |
+
|
| 221 |
+
Args:
|
| 222 |
+
text (str): The text to be displayed
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
float: Duration in seconds
|
| 226 |
+
"""
|
| 227 |
+
# Count words (split by whitespace)
|
| 228 |
+
word_count = len(text.split())
|
| 229 |
+
|
| 230 |
+
# Base duration calculation:
|
| 231 |
+
# - Minimum duration: 2 seconds
|
| 232 |
+
# - Add 0.5 seconds per word
|
| 233 |
+
# - Maximum duration: 8 seconds
|
| 234 |
+
duration = min(max(2, word_count * 0.5), 8)
|
| 235 |
+
|
| 236 |
+
return duration
|
| 237 |
+
|
| 238 |
+
def _wrap_text(self, text, max_chars_per_line=30):
|
| 239 |
+
"""
|
| 240 |
+
Wrap text to ensure it fits nicely on screen.
|
| 241 |
+
|
| 242 |
+
Args:
|
| 243 |
+
text (str): Text to wrap
|
| 244 |
+
max_chars_per_line (int): Maximum characters per line
|
| 245 |
+
|
| 246 |
+
Returns:
|
| 247 |
+
str: Wrapped text with newlines and increased line spacing
|
| 248 |
+
"""
|
| 249 |
+
words = text.split()
|
| 250 |
+
lines = []
|
| 251 |
+
current_line = []
|
| 252 |
+
current_length = 0
|
| 253 |
+
|
| 254 |
+
for word in words:
|
| 255 |
+
word_length = len(word)
|
| 256 |
+
if current_length + word_length + len(current_line) <= max_chars_per_line:
|
| 257 |
+
current_line.append(word)
|
| 258 |
+
current_length += word_length
|
| 259 |
+
else:
|
| 260 |
+
lines.append(' '.join(current_line))
|
| 261 |
+
current_line = [word]
|
| 262 |
+
current_length = word_length
|
| 263 |
+
|
| 264 |
+
if current_line:
|
| 265 |
+
lines.append(' '.join(current_line))
|
| 266 |
+
|
| 267 |
+
# Add extra line spacing by using double newlines
|
| 268 |
+
return '\n'.join(lines)
|
| 269 |
+
|
| 270 |
+
def sync_with_media_directory(self, media_dir, output_path, duration_minutes=None, video_size=None, dark_overlay=0.3,
|
| 271 |
+
text_overlays=None):
|
| 272 |
+
"""
|
| 273 |
+
Create a video by syncing media files with beat drops.
|
| 274 |
+
Args:
|
| 275 |
+
media_dir: Directory containing images and videos
|
| 276 |
+
output_path: Path where the output video will be saved
|
| 277 |
+
duration_minutes: Desired total duration in minutes
|
| 278 |
+
video_size: Tuple of (width, height) for output video
|
| 279 |
+
dark_overlay: Opacity of dark overlay (0.0 to 1.0, where 1.0 is completely black)
|
| 280 |
+
text_overlays: List of text strings or tuples (text, duration_seconds).
|
| 281 |
+
If only text is provided, duration will be calculated based on word count.
|
| 282 |
+
"""
|
| 283 |
+
print("Creating video from media files...")
|
| 284 |
+
media_files = self._get_media_files(media_dir)
|
| 285 |
+
|
| 286 |
+
if not media_files:
|
| 287 |
+
raise ValueError(f"No media files found in directory: {media_dir}")
|
| 288 |
+
|
| 289 |
+
# Prepare clips with dark theme
|
| 290 |
+
clips = []
|
| 291 |
+
|
| 292 |
+
# Set desired_total_duration
|
| 293 |
+
if duration_minutes is not None:
|
| 294 |
+
desired_total_duration = duration_minutes * 60 # Convert minutes to seconds
|
| 295 |
+
else:
|
| 296 |
+
desired_total_duration = len(self.y) / self.sr # Use the length of the audio
|
| 297 |
+
|
| 298 |
+
# Limit the beat_drop_times to the desired duration
|
| 299 |
+
beat_drop_times = self.beat_drop_times[self.beat_drop_times <= desired_total_duration]
|
| 300 |
+
|
| 301 |
+
if len(beat_drop_times) == 0:
|
| 302 |
+
raise ValueError("No beat drops detected within the desired duration.")
|
| 303 |
+
|
| 304 |
+
# Include the start (0) and end times to cover the entire duration
|
| 305 |
+
beat_times_full = np.concatenate(([0], beat_drop_times, [desired_total_duration]))
|
| 306 |
+
|
| 307 |
+
num_media = len(media_files)
|
| 308 |
+
media_index = 0
|
| 309 |
+
|
| 310 |
+
synced_clips = []
|
| 311 |
+
|
| 312 |
+
# Define the desired clip length for initial blank space
|
| 313 |
+
initial_clip_duration = 2 # 2 seconds for each clip in the initial blank space
|
| 314 |
+
base_scale = 1.2 # resized factor to make clips larger than the video size
|
| 315 |
+
zoom_factor = 0.009 # Zoom factor for zoom effects
|
| 316 |
+
|
| 317 |
+
# Loop over intervals between beat times to cover entire duration
|
| 318 |
+
for i in range(len(beat_times_full) - 1):
|
| 319 |
+
start_time = beat_times_full[i]
|
| 320 |
+
end_time = beat_times_full[i + 1]
|
| 321 |
+
interval_duration = end_time - start_time
|
| 322 |
+
|
| 323 |
+
if i == 0 and interval_duration > initial_clip_duration:
|
| 324 |
+
# Handle the initial blank space with multiple clips
|
| 325 |
+
num_initial_clips = int(np.ceil(interval_duration / initial_clip_duration))
|
| 326 |
+
for j in range(num_initial_clips):
|
| 327 |
+
clip_start_time = start_time + j * initial_clip_duration
|
| 328 |
+
clip_end_time = min(clip_start_time + initial_clip_duration, end_time)
|
| 329 |
+
clip_duration = clip_end_time - clip_start_time
|
| 330 |
+
|
| 331 |
+
media_file = media_files[media_index % num_media]
|
| 332 |
+
media_index += 1
|
| 333 |
+
|
| 334 |
+
if media_file.lower().endswith(('.mp4', '.avi', '.mov')):
|
| 335 |
+
# It's a video file
|
| 336 |
+
clip = VideoFileClip(media_file).subclipped(0, clip_duration).resized(base_scale)
|
| 337 |
+
|
| 338 |
+
else:
|
| 339 |
+
# It's an image file
|
| 340 |
+
clip = ImageClip(media_file).with_duration(clip_duration).resized(base_scale)
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
# cropped the clip to the desired video size
|
| 344 |
+
clip = clip.cropped(width=video_size[0], height=video_size[1], x_center=clip.w / 2,
|
| 345 |
+
y_center=clip.h / 2)
|
| 346 |
+
|
| 347 |
+
# Set the start time of the clip
|
| 348 |
+
clip = clip.with_start(clip_start_time)
|
| 349 |
+
|
| 350 |
+
# Apply random zoom in or zoom out effect with slower zoom
|
| 351 |
+
zoom_type = random.choice(['zoom_in', 'zoom_out', None])
|
| 352 |
+
# if zoom_type == 'zoom_in':
|
| 353 |
+
# # Slow down zoom in effect
|
| 354 |
+
# clip = clip.with_effects([vfx.Resize(lambda t: 1 + zoom_factor * (t / clip.duration))])
|
| 355 |
+
# elif zoom_type == 'zoom_out':
|
| 356 |
+
# # Slow down zoom out effect
|
| 357 |
+
# clip = clip.with_effects([vfx.Resize(lambda t: 1 + zoom_factor * (1 - t / clip.duration))])
|
| 358 |
+
|
| 359 |
+
# Add random transition to the clip
|
| 360 |
+
if synced_clips:
|
| 361 |
+
transition_type = random.choice(['crossfadein', 'fadein', None])
|
| 362 |
+
if transition_type == 'crossfadein':
|
| 363 |
+
# clip = clip.crossfadein(0.1)
|
| 364 |
+
print()
|
| 365 |
+
elif transition_type == 'fadein':
|
| 366 |
+
# clip = clip.fadein(0.1)
|
| 367 |
+
print()
|
| 368 |
+
|
| 369 |
+
synced_clips.append(clip)
|
| 370 |
+
else:
|
| 371 |
+
# Handle regular intervals between beat drops
|
| 372 |
+
media_file = media_files[media_index % num_media]
|
| 373 |
+
media_index += 1
|
| 374 |
+
|
| 375 |
+
if media_file.lower().endswith(('.mp4', '.avi', '.mov')):
|
| 376 |
+
# It's a video file
|
| 377 |
+
clip = VideoFileClip(media_file).subclipped(0, interval_duration).resized(base_scale)
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
else:
|
| 381 |
+
# It's an image file
|
| 382 |
+
clip = ImageClip(media_file).with_duration(interval_duration).resized(base_scale)
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
# cropped the clip to the desired video size
|
| 386 |
+
clip = clip.cropped(width=video_size[0], height=video_size[1], x_center=clip.w / 2, y_center=clip.h / 2)
|
| 387 |
+
|
| 388 |
+
# Set the start time of the clip
|
| 389 |
+
clip = clip.with_start(start_time)
|
| 390 |
+
|
| 391 |
+
# Apply random zoom in or zoom out effect with slower zoom
|
| 392 |
+
zoom_type = random.choice(['zoom_in', 'zoom_out', None])
|
| 393 |
+
# if zoom_type == 'zoom_in':
|
| 394 |
+
# # Slow down zoom in effect
|
| 395 |
+
# clip = clip.with_effects([vfx.Resize(lambda t: 1 + zoom_factor * (t / clip.duration))])
|
| 396 |
+
# elif zoom_type == 'zoom_out':
|
| 397 |
+
# # Slow down zoom out effect
|
| 398 |
+
# clip = clip.with_effects([vfx.Resize(lambda t: 1 + zoom_factor * (1 - t / clip.duration))])
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
# If this is the last clip, apply fadeout
|
| 402 |
+
if i == len(beat_times_full) - 2:
|
| 403 |
+
fade_duration = min(2, clip.duration / 2)
|
| 404 |
+
|
| 405 |
+
clip = clip.with_effects([vfx.FadeOut(fade_duration)])
|
| 406 |
+
|
| 407 |
+
synced_clips.append(clip)
|
| 408 |
+
|
| 409 |
+
# Create the final video
|
| 410 |
+
final_clip = CompositeVideoClip(synced_clips, size=video_size)
|
| 411 |
+
|
| 412 |
+
# Create a list of all clips for final composition
|
| 413 |
+
all_clips = [final_clip]
|
| 414 |
+
|
| 415 |
+
# Add dark overlay if enabled
|
| 416 |
+
if dark_overlay > 0:
|
| 417 |
+
# Create a black ColorClip with the same size as the video
|
| 418 |
+
black_clip = ColorClip(size=video_size, color=(0, 0, 0))
|
| 419 |
+
black_clip = black_clip.with_duration(final_clip.duration)
|
| 420 |
+
black_clip = black_clip.with_opacity(dark_overlay)
|
| 421 |
+
all_clips.append(black_clip)
|
| 422 |
+
|
| 423 |
+
# Add text overlays if provided
|
| 424 |
+
if text_overlays:
|
| 425 |
+
current_time = 0
|
| 426 |
+
for text_item in text_overlays:
|
| 427 |
+
# Handle both string and tuple inputs
|
| 428 |
+
if isinstance(text_item, tuple):
|
| 429 |
+
text, duration = text_item
|
| 430 |
+
else:
|
| 431 |
+
text = text_item
|
| 432 |
+
duration = self._calculate_text_duration(text)
|
| 433 |
+
|
| 434 |
+
# Wrap text if needed
|
| 435 |
+
wrapped_text = self._wrap_text(text)
|
| 436 |
+
print(f"Adding text: '{wrapped_text}' with duration: {duration:.1f}s")
|
| 437 |
+
|
| 438 |
+
# Create text clip with white color and nice font
|
| 439 |
+
txt_clip = TextClip(text=wrapped_text, font_size=40, color='white',
|
| 440 |
+
font='Arial Black',method='label', interline=-1) # 'label' method handles multiline text better
|
| 441 |
+
|
| 442 |
+
# Center the text
|
| 443 |
+
txt_clip = txt_clip.with_position(('center', 'center'))
|
| 444 |
+
|
| 445 |
+
# Set the duration and start time
|
| 446 |
+
txt_clip = txt_clip.with_duration(duration)
|
| 447 |
+
txt_clip = txt_clip.with_start(current_time)
|
| 448 |
+
|
| 449 |
+
# Add fade in/out effects
|
| 450 |
+
txt_clip = txt_clip.with_effects([vfx.FadeIn(0.5), vfx.FadeOut(0.5)])
|
| 451 |
+
|
| 452 |
+
all_clips.append(txt_clip)
|
| 453 |
+
current_time += duration
|
| 454 |
+
|
| 455 |
+
# Create final composition with all layers
|
| 456 |
+
final_clip = CompositeVideoClip(all_clips, size=video_size)
|
| 457 |
+
|
| 458 |
+
# Set the audio of the final clip
|
| 459 |
+
audio_clip = AudioFileClip(self.audio_path).subclipped(0, desired_total_duration)
|
| 460 |
+
# Apply audio fadeout at the end
|
| 461 |
+
fade_duration = min(2, audio_clip.duration / 2)
|
| 462 |
+
audio_clip = audio_clip.with_effects([afx.AudioFadeOut(fade_duration)])
|
| 463 |
+
final_clip = final_clip.with_audio(audio_clip)
|
| 464 |
+
final_clip = final_clip.with_duration(desired_total_duration)
|
| 465 |
+
|
| 466 |
+
# Write the output video file
|
| 467 |
+
final_clip.write_videofile(
|
| 468 |
+
output_path, fps=30, threads=32, audio_codec="aac", preset='ultrafast'
|
| 469 |
+
)
|
| 470 |
+
print(f"Output video saved to '{output_path}'")
|
tools/beatsyncer/beatsyncer_ui.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import gradio as gr
|
| 3 |
+
|
| 4 |
+
import tempfile
|
| 5 |
+
import shutil
|
| 6 |
+
|
| 7 |
+
from tools.beatsyncer.beatsyncer_tool import BeatSyncer
|
| 8 |
+
|
| 9 |
+
class BeatsyncerUI:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.temp_dir = tempfile.mkdtemp()
|
| 12 |
+
# Use the existing clips directory from the project
|
| 13 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
+
self.clips_dir = os.path.join(script_dir, 'clips')
|
| 15 |
+
|
| 16 |
+
def process_video(
|
| 17 |
+
self,
|
| 18 |
+
audio_file,
|
| 19 |
+
duration_minutes,
|
| 20 |
+
sensitivity,
|
| 21 |
+
video_height,
|
| 22 |
+
dark_overlay,
|
| 23 |
+
text_input,
|
| 24 |
+
progress=gr.Progress()
|
| 25 |
+
):
|
| 26 |
+
try:
|
| 27 |
+
# Save audio file
|
| 28 |
+
audio_path = os.path.join(self.temp_dir, "input_audio" + os.path.splitext(audio_file.name)[1])
|
| 29 |
+
shutil.copy(audio_file.name, audio_path)
|
| 30 |
+
|
| 31 |
+
# Process text overlays
|
| 32 |
+
text_overlays = [text.strip() for text in text_input.split('\n') if text.strip()]
|
| 33 |
+
|
| 34 |
+
# Calculate video dimensions
|
| 35 |
+
video_width = int((video_height * 9) / 16) # 16:9 aspect ratio
|
| 36 |
+
|
| 37 |
+
# Set output path
|
| 38 |
+
output_path = os.path.join(self.temp_dir, "output_video.mp4")
|
| 39 |
+
|
| 40 |
+
# Convert duration to float if provided
|
| 41 |
+
if duration_minutes and duration_minutes.strip():
|
| 42 |
+
duration_minutes = float(duration_minutes)
|
| 43 |
+
else:
|
| 44 |
+
duration_minutes = None
|
| 45 |
+
|
| 46 |
+
progress(0.1, desc="Initializing BeatSyncer...")
|
| 47 |
+
|
| 48 |
+
# Initialize BeatSyncer
|
| 49 |
+
beat_syncer = BeatSyncer(audio_path)
|
| 50 |
+
|
| 51 |
+
progress(0.2, desc="Loading audio...")
|
| 52 |
+
beat_syncer.load_audio(duration_minutes=duration_minutes)
|
| 53 |
+
|
| 54 |
+
progress(0.3, desc="Detecting beat drops...")
|
| 55 |
+
beat_syncer.detect_beat_drops(sensitivity=sensitivity)
|
| 56 |
+
|
| 57 |
+
progress(0.4, desc="Creating video...")
|
| 58 |
+
beat_syncer.sync_with_media_directory(
|
| 59 |
+
self.clips_dir, # Using the existing clips directory
|
| 60 |
+
output_path,
|
| 61 |
+
duration_minutes=duration_minutes,
|
| 62 |
+
video_size=(video_width, video_height),
|
| 63 |
+
dark_overlay=float(dark_overlay),
|
| 64 |
+
text_overlays=text_overlays
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
progress(1.0, desc="Done!")
|
| 68 |
+
return output_path
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
raise gr.Error(f"Error: {str(e)}")
|
| 72 |
+
|
| 73 |
+
def create_ui(self):
|
| 74 |
+
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
| 75 |
+
gr.Markdown("""
|
| 76 |
+
# 🎵 BeatSyncer - Video Creation Tool
|
| 77 |
+
Create amazing videos synchronized with music beats!
|
| 78 |
+
|
| 79 |
+
Using media files from: {}
|
| 80 |
+
""".format(self.clips_dir))
|
| 81 |
+
|
| 82 |
+
with gr.Row():
|
| 83 |
+
with gr.Column(scale=2):
|
| 84 |
+
audio_input = gr.File(
|
| 85 |
+
label="Upload Audio/Video File",
|
| 86 |
+
file_types=[".mp3", ".wav", ".m4a", ".aac", ".ogg", ".flac", ".mp4", ".avi", ".mov", ".mkv", ".wmv", ".flv"]
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
duration = gr.Textbox(
|
| 91 |
+
label="Duration (minutes, optional)",
|
| 92 |
+
placeholder="Leave empty for full audio duration"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
sensitivity = gr.Radio(
|
| 96 |
+
choices=["low", "medium", "high"],
|
| 97 |
+
value="medium",
|
| 98 |
+
label="Beat Detection Sensitivity"
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
video_height = gr.Slider(
|
| 102 |
+
minimum=480,
|
| 103 |
+
maximum=1920,
|
| 104 |
+
value=720,
|
| 105 |
+
step=120,
|
| 106 |
+
label="Video Height (16:9 ratio)"
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
dark_overlay = gr.Slider(
|
| 110 |
+
minimum=0.0,
|
| 111 |
+
maximum=1.0,
|
| 112 |
+
value=0.3,
|
| 113 |
+
step=0.1,
|
| 114 |
+
label="Dark Overlay Intensity"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
text_input = gr.Textbox(
|
| 118 |
+
label="Text Overlays (one per line)",
|
| 119 |
+
placeholder="Enter text overlays...",
|
| 120 |
+
lines=5
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
create_btn = gr.Button("Create Video", variant="primary")
|
| 124 |
+
|
| 125 |
+
with gr.Column(scale=1):
|
| 126 |
+
output_video = gr.Video(
|
| 127 |
+
label="Output Video",
|
| 128 |
+
height=640, # Fixed height for preview
|
| 129 |
+
width=360 # Fixed width for preview
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
create_btn.click(
|
| 133 |
+
fn=self.process_video,
|
| 134 |
+
inputs=[
|
| 135 |
+
audio_input,
|
| 136 |
+
duration,
|
| 137 |
+
sensitivity,
|
| 138 |
+
video_height,
|
| 139 |
+
dark_overlay,
|
| 140 |
+
text_input
|
| 141 |
+
],
|
| 142 |
+
outputs=output_video
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
return app
|
| 146 |
+
|
| 147 |
+
# Create and launch the UI
|
| 148 |
+
if __name__ == "__main__":
|
| 149 |
+
ui = BeatSyncerUI()
|
| 150 |
+
demo = ui.create_ui()
|
| 151 |
+
demo.launch(debug=True, share=False, inbrowser=True)
|
tools/beatsyncer/clips/.DS_Store
ADDED
|
Binary file (16.4 kB). View file
|
|
|
tools/beatsyncer/clips/1657591309536.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:69aa9858fca54bda6ac78abe43cf4bd3081ef0d6a69e433603cfe3995219c97c
|
| 3 |
+
size 7222278
|
tools/beatsyncer/clips/1657591680538.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c531723d9ba697e7453cbc61d772e76957fca7ae83f2ecacaae4b18dd770d7d4
|
| 3 |
+
size 40783923
|
tools/beatsyncer/clips/1657591832727.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:37be14aab321518e220315f3673dc63113b9a601ae96d246d614b36ea9993a63
|
| 3 |
+
size 19891527
|
tools/beatsyncer/clips/1657616409629.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:30401ebf2010942c79f16f39dab912ba195c606959bab9500c52dcfa60039aca
|
| 3 |
+
size 10948753
|
tools/beatsyncer/clips/1657616591950.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:863652f4a1abe5d0d6ee5d0cca92cac3aab6deb7b7d900fa1dfb06289698a274
|
| 3 |
+
size 4494706
|
tools/beatsyncer/clips/1657616660848.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:09002494bd0c33f02fffecc9ec484da2630dac91399b2ee1adfa22433757b05d
|
| 3 |
+
size 8450597
|
tools/beatsyncer/clips/1657616730783.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1aad238b7f6623ad10af2413ece4b65113ebae42bb87e41cc39d8276864a7c67
|
| 3 |
+
size 6972420
|
tools/beatsyncer/clips/1657616788731.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ca25db432389307b40cd11a945d0b19ad0f72e9e7b9d34b2d8e37565e39beb3d
|
| 3 |
+
size 7192248
|
tools/beatsyncer/clips/1657616954951.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5021eb2d204cdc23fb8e938d0de0d6baa99ac1639726e2b0e3fe2e1e6566a7c3
|
| 3 |
+
size 9827398
|
tools/beatsyncer/clips/1657923632784.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5837c302c5fe15878b068e22a0ebb1a2fedb747d226ea81a9ba0631fca898540
|
| 3 |
+
size 8853773
|
tools/beatsyncer/clips/288298605_1687187951639494_579221224540621563_n.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5277a9bc08a20012285e5c438d043047802f525f82583e5c2d4106ec30da8cc3
|
| 3 |
+
size 2767934
|
tools/beatsyncer/clips/289438954_5129286887125053_220682033208031067_n.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba5f5469980c5df7e4910122e0cf818bd90fe98e3743650ecf189c706230009c
|
| 3 |
+
size 6625183
|
tools/beatsyncer/clips/289568281_1462345327513048_3022742540007505592_n.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4801482e63465e85a02fd9a7eacbbe0268954bf9674f55a10aa8c80c9e2753f0
|
| 3 |
+
size 1618834
|
tools/beatsyncer/clips/290154393_565236208447925_526246679178540543_n.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:671438acc46a10e657388d4c13d34dbecf5af5c6f9ae915b24caec154bf3390a
|
| 3 |
+
size 3465572
|
tools/beatsyncer/clips/290702607_425330016154178_254797435320646581_n.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:99d8e8e4ee7391d0dc4d3223e5d5d203cea89089fc507862718fdd203c5234f8
|
| 3 |
+
size 3268380
|
tools/beatsyncer/clips/290990526_409256130973654_5788653198617875871_n.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:191b27a28f64889c201c22daf0bc48ade99d9902b442c3a2d4e8e1ccaebb5060
|
| 3 |
+
size 2297732
|
tools/beatsyncer/clips/291032176_120067890734229_1814549935092808193_n.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c69c8c738b98305e0b40fbd63ddb6133b886eaf279e8894e232252dad075fc43
|
| 3 |
+
size 3250400
|
tools/beatsyncer/clips/657591201007.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ef0f3447388144458427572b64c768ec09cc88b87635fbece3192cc2eb973975
|
| 3 |
+
size 11149649
|
tools/beatsyncer/clips/657591253340.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b1d3c5bd6614ea8e72703a4f94e7590c29a792b9eb0f22504b0f5bb663efbdbb
|
| 3 |
+
size 6907398
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_001.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9340f70c21ebebfa4230c3891198b278b849a8a3f788bf7f7edd36285f89e830
|
| 3 |
+
size 2103745
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_002.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b2fff2e4e65479a29a450cdcb340c78564f5794167ee03352dcdc2d8fe856930
|
| 3 |
+
size 155997
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_003.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:762d0ffc6a657e3c5b1c190190a9b6a3d3ea7a9979239db6de2532658a1dd0b9
|
| 3 |
+
size 350743
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_004.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0365717355f8e0c8cb39dedbbba0553c1e6132fd2e793972336c2581933eb4d0
|
| 3 |
+
size 563440
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_005.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bf5ba4edc9c23e961ef5d3379790b0e32b573b314fee099de74b3b14b841a3d1
|
| 3 |
+
size 145942
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_006.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:174b2c1e98000a026427acde3bdde0914f60ecabfbab8ac93be5dd81a4cf3bc8
|
| 3 |
+
size 282620
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_007.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f56a247020f35d3ce480f106605fdd704cfddb7513f0b52d7f63646c6e5be4d9
|
| 3 |
+
size 353519
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_008.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:711df5b60e0ced8b818a49f32e6ca516193346a3b4ce63bb2188d3802f62aace
|
| 3 |
+
size 343334
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_009.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ab88f7973f2fee383ba852f55d46912aaee7ad849a21a46cdaedb8e957046c6f
|
| 3 |
+
size 204924
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_010.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ed2910d6e2e6b4159ba3290053526fa12573082a18dddd38f3d2eebc50ce15f
|
| 3 |
+
size 144843
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_001.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3b73792168a06223614b33179844a458bd8389f426980414f47ea0389f57cba2
|
| 3 |
+
size 1138637
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_002.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:85bcdde3e764e65ca3260b20c17918581dfce3acec4eb04e4813a2ac53e16587
|
| 3 |
+
size 566912
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_003.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:baa62fca4618f79cb7be3b2158509fc97f2f9e7f82991300b6e19b42df217a4d
|
| 3 |
+
size 544288
|
tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_004.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:102282c000ac3021d2c6749d0164663e936a6e07655d56c1850015fdb148a292
|
| 3 |
+
size 915749
|