diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..e2aa5b67f3055fef9ae8a5da5aeb388dab795252 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,91 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657591309536.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657591680538.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657591832727.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657616409629.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657616591950.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657616660848.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657616730783.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657616788731.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657616954951.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/1657923632784.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/288298605_1687187951639494_579221224540621563_n.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/289438954_5129286887125053_220682033208031067_n.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/289568281_1462345327513048_3022742540007505592_n.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/290154393_565236208447925_526246679178540543_n.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/290702607_425330016154178_254797435320646581_n.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/290990526_409256130973654_5788653198617875871_n.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/291032176_120067890734229_1814549935092808193_n.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/657591201007.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/657591253340.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_003.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_004.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_005.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_006.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_007.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_008.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_009.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_010.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_003.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_004.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_005.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_006.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_007.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_003.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_004.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_005.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_006.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_7944DC0BB36028CA2D36023098F3029C_video_dashinit.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_001.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_002.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_003.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_004.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_005.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_006.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_007.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_008.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_009.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_010.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_011.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_012.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_013.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_014.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_015.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_016.mp4 filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/ai-generated-8079926_1280.png filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/ai-generated-8601539_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/ai-generated-9151678_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/antique-car-7577214_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/automobile-1850862_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/bmw-6236208_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-171415_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-219813_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-5548243_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-5548244_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-6306695_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-6483720_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-6589240_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-6658749_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-7139944_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-7203265_1280(1).jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-7203265_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-7725834_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-7830737_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/car-8141820_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/fashion-1399346_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/mercedes-benz-7383931_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/speedy-car-8391554_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/sport-4155825_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/vintage-car-7300881_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/clips/vintage-rolls-royce-7414656_1280.jpg filter=lfs diff=lfs merge=lfs -text
+tools/beatsyncer/tmp/input_audio.mp3 filter=lfs diff=lfs merge=lfs -text
diff --git a/README.md b/README.md
index e9f23b0b85f956ee4c3eb17604aa0bae8d8e547c..cd2798f27420c5e57f465ffc33c2ad269d860bb8 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,6 @@
---
-title: AI Tools Platform
-emoji: 💻
-colorFrom: blue
-colorTo: pink
+title: AI_Tools_Platform
+app_file: main_ui.py
sdk: gradio
-sdk_version: 5.48.0
-app_file: app.py
-pinned: false
+sdk_version: 4.19.2
---
-
-Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
diff --git a/__pycache__/main_ui.cpython-311.pyc b/__pycache__/main_ui.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ecafb89b82e8e778134b2ed2a9e85a6b65470dbe
Binary files /dev/null and b/__pycache__/main_ui.cpython-311.pyc differ
diff --git a/colab_launcher.py b/colab_launcher.py
new file mode 100644
index 0000000000000000000000000000000000000000..cddc5056bf0f4394234e98104cb6f2dca3a97425
--- /dev/null
+++ b/colab_launcher.py
@@ -0,0 +1,157 @@
+import os
+import sys
+import json
+import importlib.util
+from pathlib import Path
+
+# Define paths
+DRIVE_ROOT = "/content/drive/MyDrive/AI_Tools_Platform"
+TOOLS_DIR = os.path.join(DRIVE_ROOT, "tools")
+INSTALLED_TOOLS_JSON = os.path.join(DRIVE_ROOT, "installed_tools.json")
+
+# Import the Tool class from main_ui
+from main_ui import Tool, MainUI
+
+def setup_environment():
+ print("Setting up environment...")
+
+ # Create tools directory in Google Drive if it doesn't exist
+ os.makedirs(TOOLS_DIR, exist_ok=True)
+ print(f"Tools directory in Drive: {TOOLS_DIR}")
+
+ # Create installed_tools.json in Google Drive if it doesn't exist
+ if not os.path.exists(INSTALLED_TOOLS_JSON):
+ with open(INSTALLED_TOOLS_JSON, 'w') as f:
+ json.dump({"tools": []}, f, indent=4)
+ print(f"Created initial installed_tools.json in Drive")
+ else:
+ print(f"Using existing installed_tools.json from Drive")
+
+ # Verify the content of installed_tools.json
+ try:
+ with open(INSTALLED_TOOLS_JSON, 'r') as f:
+ tools_data = json.load(f)
+ if not isinstance(tools_data, dict) or "tools" not in tools_data:
+ print("Warning: installed_tools.json has incorrect format. Fixing...")
+ with open(INSTALLED_TOOLS_JSON, 'w') as f:
+ json.dump({"tools": []}, f, indent=4)
+ except (json.JSONDecodeError, UnicodeDecodeError):
+ print("Error: installed_tools.json is not valid JSON. Fixing...")
+ with open(INSTALLED_TOOLS_JSON, 'w') as f:
+ json.dump({"tools": []}, f, indent=4)
+
+ # Add tools directory to Python path
+ if TOOLS_DIR not in sys.path:
+ sys.path.append(TOOLS_DIR)
+ if os.getcwd() not in sys.path:
+ sys.path.append(os.getcwd())
+
+ print("\nEnvironment set up successfully!")
+
+# Override MainUI methods to use direct file paths
+class ColabMainUI(MainUI):
+ def __init__(self):
+ # Set up paths before calling parent's __init__
+ self.tools = {} # Initialize tools dictionary
+ self.tools_dir = TOOLS_DIR
+ self.installed_tools_json = INSTALLED_TOOLS_JSON
+ self.marketplace_url = "https://marketplace.vidplus.app/api/marketplace"
+ self.version = "0.1"
+
+ # Create tools directory if it doesn't exist
+ os.makedirs('tools', exist_ok=True)
+
+ # Load tools manually instead of calling parent's __init__
+ self.load_installed_tools()
+
+ def load_installed_tools(self):
+ """Load installed tools directly from Google Drive"""
+ print(f"Loading tools from {self.installed_tools_json}")
+ try:
+ if os.path.exists(self.installed_tools_json):
+ with open(self.installed_tools_json, 'r') as f:
+ data = json.load(f)
+
+ for tool_data in data.get('tools', []):
+ # If ui_module is specified, dynamically import it
+ if tool_data.get('ui_module'):
+ try:
+ # Modify the module path to use the direct path
+ module_name = tool_data['ui_module']
+ if module_name.startswith('tools.'):
+ # Extract the relative path and use the full path
+ rel_path = module_name.replace('tools.', '')
+ module_path = os.path.join(TOOLS_DIR, rel_path.replace('.', '/') + '.py')
+ print(f"Loading module from: {module_path}")
+
+ # Use importlib to import the module
+ spec = importlib.util.spec_from_file_location(module_name, module_path)
+ if spec:
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ ui_class = getattr(module, tool_data['ui_class'])
+
+ self.tools[tool_data['id']] = Tool(
+ name=tool_data['name'],
+ description=tool_data['description'],
+ ui_class=ui_class,
+ icon=tool_data['icon'],
+ version=tool_data['version'],
+ ui_module=tool_data['ui_module']
+ )
+ else:
+ print(f"Could not find module: {module_path}")
+ else:
+ # Standard import for non-tools modules
+ module = importlib.import_module(module_name)
+ ui_class = getattr(module, tool_data['ui_class'])
+
+ self.tools[tool_data['id']] = Tool(
+ name=tool_data['name'],
+ description=tool_data['description'],
+ ui_class=ui_class,
+ icon=tool_data['icon'],
+ version=tool_data['version'],
+ ui_module=tool_data['ui_module']
+ )
+ except (ImportError, AttributeError) as e:
+ print(f"Error loading tool {tool_data['name']}: {e}")
+ import traceback
+ traceback.print_exc()
+ except Exception as e:
+ print(f"Error loading installed tools: {e}")
+ import traceback
+ traceback.print_exc()
+
+ def save_installed_tools(self):
+ """Save installed tools directly to Google Drive"""
+ data = {
+ "tools": [
+ {
+ "id": tool_id,
+ "name": tool.name,
+ "description": tool.description,
+ "icon": tool.icon,
+ "version": tool.version,
+ "ui_class": tool.ui_class.__name__,
+ "ui_module": tool.ui_module
+ }
+ for tool_id, tool in self.tools.items()
+ ]
+ }
+
+ with open(self.installed_tools_json, 'w') as f:
+ json.dump(data, f, indent=4)
+ print(f"Saved tools configuration to {self.installed_tools_json}")
+
+if __name__ == "__main__":
+ setup_environment()
+
+ # Import and run the modified UI
+ print("\nStarting AI Tools Platform...")
+
+ ui = ColabMainUI()
+ demo = ui.create_ui()
+ demo.queue()
+ demo.title = "AI Tools Platform"
+ demo.launch(debug=True, share=True) # share=True to get a public URL
diff --git a/installed_tools.json b/installed_tools.json
new file mode 100644
index 0000000000000000000000000000000000000000..3cf71dd2041c18cb70f42a1b201183676600e9e1
--- /dev/null
+++ b/installed_tools.json
@@ -0,0 +1,40 @@
+{
+ "tools": [
+ {
+ "id": "image_background_remover",
+ "name": "Image Background Remover",
+ "description": "A tool that removes backgrounds from images",
+ "icon": "\ud83d\uddbc\ufe0f",
+ "version": "1.0.0",
+ "ui_class": "ImageBackgroundRemoverUI",
+ "ui_module": "tools.image_background_remover.image_background_remover_ui"
+ },
+ {
+ "id": "beatsyncer",
+ "name": "Beatsyncer",
+ "description": "Create amazing videos synchronized with music beats",
+ "icon": "\ud83c\udfb5",
+ "version": "1.0.0",
+ "ui_class": "BeatsyncerUI",
+ "ui_module": "tools.beatsyncer.beatsyncer_ui"
+ },
+ {
+ "id": "image_background_remover",
+ "name": "Image Background Remover",
+ "description": "A tool that removes backgrounds from images",
+ "icon": "\ud83d\uddbc\ufe0f",
+ "version": "1.0.0",
+ "ui_class": "ImageBackgroundRemoverUI",
+ "ui_module": "tools.image_background_remover.image_background_remover_ui"
+ },
+ {
+ "id": "image_background_remover",
+ "name": "Image Background Remover",
+ "description": "A tool that removes backgrounds from images",
+ "icon": "\ud83d\uddbc\ufe0f",
+ "version": "1.0.0",
+ "ui_class": "ImageBackgroundRemoverUI",
+ "ui_module": "tools.image_background_remover.image_background_remover_ui"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/main_ui.py b/main_ui.py
new file mode 100644
index 0000000000000000000000000000000000000000..66baba61271597861ed7fd952d70734a511d3e6c
--- /dev/null
+++ b/main_ui.py
@@ -0,0 +1,1556 @@
+import os
+import json
+import importlib
+import requests
+import gradio as gr
+import subprocess
+
+class Tool:
+ def __init__(self, name, description, ui_class, icon="🔧", version="0.1", ui_module=None):
+ self.name = name
+ self.description = description
+ self.ui_class = ui_class
+ self.icon = icon
+ self.version = version
+ self.ui_module = ui_module
+
+class MainUI:
+ def __init__(self):
+ # Load installed tools from JSON
+ self.tools = {}
+ self.load_installed_tools()
+
+ # Marketplace server URL
+ self.marketplace_url = "https://marketplace.vidplus.app/api/marketplace"
+ self.version = "0.1"
+
+ # Create tools directory if it doesn't exist
+ os.makedirs('tools', exist_ok=True)
+
+ def load_installed_tools(self):
+ """Load installed tools from JSON file"""
+ try:
+ if os.path.exists('installed_tools.json'):
+ with open('installed_tools.json', 'r') as f:
+ data = json.load(f)
+
+ for tool_data in data.get('tools', []):
+ # If ui_module is specified, dynamically import it
+ if tool_data.get('ui_module'):
+ try:
+ module = importlib.import_module(tool_data['ui_module'])
+ ui_class = getattr(module, tool_data['ui_class'])
+
+ self.tools[tool_data['id']] = Tool(
+ name=tool_data['name'],
+ description=tool_data['description'],
+ ui_class=ui_class,
+ icon=tool_data['icon'],
+ version=tool_data['version'],
+ ui_module=tool_data['ui_module']
+ )
+ except (ImportError, AttributeError) as e:
+ print(f"Error loading tool {tool_data['name']}: {e}")
+ except Exception as e:
+ print(f"Error loading installed tools: {e}")
+ # Fallback to default BeatSyncer tool
+ # self.tools = {
+ # "beatsyncer": Tool(
+ # name="BeatSyncer",
+ # description="Create amazing videos synchronized with music beats",
+ # ui_class=BeatSyncerUI,
+ # icon="🎵"
+ # )
+ # }
+
+ def save_installed_tools(self):
+ """Save installed tools to JSON file"""
+ data = {
+ "tools": [
+ {
+ "id": tool_id,
+ "name": tool.name,
+ "description": tool.description,
+ "icon": tool.icon,
+ "version": tool.version,
+ "ui_class": tool.ui_class.__name__,
+ "ui_module": tool.ui_module
+ }
+ for tool_id, tool in self.tools.items()
+ ]
+ }
+
+ with open('installed_tools.json', 'w') as f:
+ json.dump(data, f, indent=4)
+
+ def fetch_marketplace_tools(self):
+ """Fetch available tools from marketplace server"""
+ try:
+ response = requests.get(f"{self.marketplace_url}/tools")
+ if response.status_code == 200:
+ # Get the list of tools from the marketplace
+ marketplace_tools = response.json()
+
+ # Ensure we're working with the latest state of installed tools
+ # This is important after a tool has been removed
+ if os.path.exists('installed_tools.json'):
+ with open('installed_tools.json', 'r') as f:
+ data = json.load(f)
+ installed_tool_ids = [tool['id'] for tool in data.get('tools', [])]
+ else:
+ installed_tool_ids = []
+
+ # Update the is_installed flag for each tool
+ for tool in marketplace_tools:
+ tool['is_installed'] = tool['id'] in installed_tool_ids
+
+ return marketplace_tools
+ else:
+ return []
+ except Exception as e:
+ print(f"Error fetching marketplace tools: {e}")
+ return []
+
+ def fetch_tool_details(self, tool_id):
+ """Fetch details for a specific tool from marketplace server"""
+ try:
+ response = requests.get(f"{self.marketplace_url}/tools/{tool_id}")
+ if response.status_code == 200:
+ return response.json()
+ else:
+ return None
+ except Exception as e:
+ print(f"Error fetching tool details: {e}")
+ return None
+
+ def install_tool(self, tool_id):
+ """Install a tool from the marketplace"""
+ try:
+ # Import required modules
+ import importlib
+ import importlib.util
+ import sys
+ import subprocess
+ import gradio as gr
+
+ # Fetch tool data
+ response = requests.get(f"{self.marketplace_url}/tools/{tool_id}/download")
+ if response.status_code != 200:
+ return f"Error: Failed to download tool (Status code: {response.status_code})"
+
+ tool_data = response.json()
+ tool_info = tool_data['tool']
+ tool_files = tool_data['files']
+
+ # Check and install dependencies if they exist
+ dependencies = tool_info.get('dependencies', [])
+ if dependencies:
+ try:
+ # Read current requirements.txt
+ current_requirements = []
+ if os.path.exists('requirements.txt'):
+ with open('requirements.txt', 'r') as f:
+ current_requirements = [line.strip() for line in f.readlines() if line.strip()]
+
+ # Check which dependencies need to be installed
+ to_install = []
+ to_add_to_requirements = []
+
+ for dep in dependencies:
+ # Extract just the package name (ignore version requirements)
+ pkg_name = dep.split('>=')[0].split('==')[0].split('>')[0].split('<')[0].strip()
+
+ # Check if dependency is already in requirements.txt
+ is_in_requirements = False
+ for req in current_requirements:
+ req_name = req.split('>=')[0].split('==')[0].split('>')[0].split('<')[0].strip()
+ if req_name == pkg_name:
+ is_in_requirements = True
+ break
+
+ if not is_in_requirements:
+ to_add_to_requirements.append(dep)
+
+ try:
+ # Try to import the package to check if it's installed
+ importlib.import_module(pkg_name)
+ except ImportError:
+ # Package not installed, add to installation list
+ to_install.append(dep)
+
+ # Install missing dependencies
+ if to_install:
+ for dep in to_install:
+ # Use subprocess to run pip install
+ subprocess.check_call([sys.executable, "-m", "pip", "install", dep])
+
+ # Append new dependencies to requirements.txt
+ if to_add_to_requirements:
+ with open('requirements.txt', 'a') as f:
+ for dep in to_add_to_requirements:
+ f.write(f"\n{dep}")
+ print(f"Added {len(to_add_to_requirements)} dependencies to requirements.txt")
+
+ except Exception as e:
+ return f"Error installing dependencies: {e}"
+
+ # Create tool directory
+ tool_dir = os.path.join('tools', tool_id)
+ os.makedirs(tool_dir, exist_ok=True)
+
+ # Write tool files
+ for file_name, file_content in tool_files.items():
+ # Rename files to match our convention
+ target_file_name = file_name
+ if file_name.endswith('_tool_logic.py'):
+ target_file_name = f"{tool_id}_tool.py"
+ elif file_name.endswith('_tool_ui.py'):
+ target_file_name = f"{tool_id}_ui.py"
+
+ with open(os.path.join(tool_dir, target_file_name), 'w') as f:
+ f.write(file_content)
+
+ # Create __init__.py to make the directory a package
+ with open(os.path.join(tool_dir, '__init__.py'), 'w') as f:
+ f.write(f"# {tool_info['name']} package\n")
+
+ # Add tool to installed tools
+ ui_module = f"tools.{tool_id}.{tool_id}_ui"
+ ui_class = f"{tool_id.title().replace('_', '')}UI"
+
+ # Update installed_tools.json
+ with open('installed_tools.json', 'r') as f:
+ data = json.load(f)
+
+ data['tools'].append({
+ "id": tool_id,
+ "name": tool_info['name'],
+ "description": tool_info['description'],
+ "icon": tool_info['icon'],
+ "version": tool_info['version'],
+ "ui_class": ui_class,
+ "ui_module": ui_module
+ })
+
+ with open('installed_tools.json', 'w') as f:
+ json.dump(data, f, indent=4)
+
+ # Dynamically load the newly installed tool
+ try:
+ # Reload the module if it was already imported
+ if ui_module in sys.modules:
+ importlib.reload(sys.modules[ui_module])
+
+ # Import the module
+ module = importlib.import_module(ui_module)
+ ui_class_obj = getattr(module, ui_class)
+
+ # Add the tool to self.tools
+ self.tools[tool_id] = Tool(
+ name=tool_info['name'],
+ description=tool_info['description'],
+ ui_class=ui_class_obj,
+ icon=tool_info['icon'],
+ version=tool_info['version'],
+ ui_module=ui_module
+ )
+
+ # Flag to indicate this is a newly installed tool that needs special handling
+ self.tools[tool_id].newly_installed = True
+
+ # Return success message
+ return f"Successfully installed {tool_info['name']}. The tool is now available in the Tools page."
+ except Exception as e:
+ print(f"Error dynamically loading tool: {e}")
+ import traceback
+ traceback.print_exc()
+ # Still return success but with a restart message
+ return f"Successfully installed {tool_info['name']}. Please restart the application to use the new tool."
+ except Exception as e:
+ return f"Error installing tool: {e}"
+
+ def remove_tool(self, tool_id):
+ """Remove an installed tool"""
+ try:
+ if tool_id not in self.tools:
+ return "Error: Tool is not installed"
+
+ # Get tool info before removal
+ tool_name = self.tools[tool_id].name
+
+ # Remove tool directory
+ tool_dir = os.path.join('tools', tool_id)
+ if os.path.exists(tool_dir):
+ import shutil
+ shutil.rmtree(tool_dir)
+ print(f"Removed tool directory: {tool_dir}")
+
+ # Remove tool from installed_tools.json
+ if os.path.exists('installed_tools.json'):
+ with open('installed_tools.json', 'r') as f:
+ data = json.load(f)
+
+ # Filter out the tool to be removed
+ data['tools'] = [tool for tool in data['tools'] if tool['id'] != tool_id]
+
+ # Save updated tools list
+ with open('installed_tools.json', 'w') as f:
+ json.dump(data, f, indent=4)
+
+ print(f"Removed tool from installed_tools.json: {tool_id}")
+
+ # Remove tool from self.tools
+ del self.tools[tool_id]
+
+ return f"✅ Successfully removed {tool_name}"
+ except Exception as e:
+ print(f"Error removing tool: {e}")
+ import traceback
+ traceback.print_exc()
+ return f"Error removing tool: {str(e)}"
+
+ def create_tool_card(self, name):
+ return f"""
+
+ {name}
+
+ """
+
+ def create_ui(self):
+ css = """
+ :root {
+ --sidebar-width: 200px;
+ }
+
+ body {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ }
+
+ #sidebar {
+ background-color: #0A192F;
+ color: white;
+ padding: 32px 24px;
+ height: 100vh;
+ width: var(--sidebar-width);
+ position: fixed;
+ left: 0;
+ top: 0;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ z-index: 1000;
+ }
+
+ #main-content {
+ background-color: #0F172A;
+ min-height: 100vh;
+ margin-left: var(--sidebar-width);
+ padding: 50px;
+ box-sizing: border-box;
+ overflow-y: auto;
+ position: relative;
+ max-height: 100vh; /* Ensure content is scrollable */
+ }
+
+ .nav-button {
+ background: none !important;
+ border: none !important;
+ box-shadow: none !important;
+ color: #94A3B8 !important;
+ padding: 8px 0 !important;
+ margin: 4px 0 !important;
+ min-width: unset !important;
+ width: 100% !important;
+ text-align: left !important;
+ font-size: 16px !important;
+ font-weight: 500 !important;
+ height: auto !important;
+ line-height: 1.2 !important;
+ }
+
+ .nav-button:hover {
+ color: white !important;
+ }
+
+ .nav-button[variant="primary"] {
+ color: white !important;
+ }
+
+ .tool-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 20px;
+ padding: 24px 0;
+ width: 100%;
+ }
+
+ .version-text {
+ margin-top: auto;
+ color: #64748B;
+ font-size: 12px;
+ padding-top: 16px;
+ }
+
+ .title {
+ font-size: 32px;
+ font-weight: 800;
+ margin-bottom: 32px;
+ color: white;
+ letter-spacing: -0.5px;
+ }
+
+ .page-title {
+ font-size: 28px;
+ font-weight: 700;
+ color: white;
+ margin: 0;
+ padding: 0;
+ letter-spacing: -0.5px;
+ }
+
+ #content-wrapper {
+ width: 100%;
+ max-width: 100%;
+ padding: 24px;
+ }
+
+ .tool-card {
+ background: #1E293B !important;
+ border: none !important;
+ border-radius: 16px !important;
+ overflow: hidden !important;
+ transition: transform 0.2s !important;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important;
+ height: 100% !important;
+ display: flex !important;
+ flex-direction: column !important;
+ width: auto !important;
+ min-height: 200px !important;
+ justify-content: flex-start !important;
+ align-items: stretch !important;
+ padding: 0 !important;
+ color: white !important;
+ text-align: left !important;
+ cursor: pointer !important;
+ }
+
+ .tool-card:hover {
+ transform: scale(1.02) !important;
+ }
+
+ .tool-card-img {
+ width: 100%;
+ height: 150px;
+ background: #2D3748;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 48px;
+ color: #38BDF8;
+ }
+
+ .tool-card-content {
+ padding: 16px !important;
+ flex-grow: 1 !important;
+ display: flex !important;
+ flex-direction: column !important;
+ }
+
+ .tool-card-title {
+ font-size: 18px !important;
+ font-weight: 600 !important;
+ color: white !important;
+ margin: 0 0 8px 0 !important;
+ }
+
+ .tool-card-description {
+ font-size: 14px !important;
+ color: #94A3B8 !important;
+ margin: 0 !important;
+ flex-grow: 1 !important;
+ }
+
+ .installed-indicator {
+ background-color: #10B981 !important;
+ color: white !important;
+ font-size: 12px !important;
+ font-weight: 600 !important;
+ padding: 4px 8px !important;
+ border-radius: 4px !important;
+ margin-top: 8px !important;
+ display: inline-block !important;
+ }
+
+ .tool-card.installed {
+ border: 2px solid #10B981 !important;
+ }
+
+ .add-tool-card {
+ background: rgba(255, 255, 255, 0.05) !important;
+ border: 2px dashed rgba(255, 255, 255, 0.2) !important;
+ color: rgba(255, 255, 255, 0.8) !important;
+ display: flex !important;
+ flex-direction: column !important;
+ justify-content: center !important;
+ align-items: center !important;
+ text-align: center !important;
+ padding: 16px !important;
+ }
+
+ .add-tool-icon {
+ font-size: 32px;
+ margin-bottom: 16px;
+ color: rgba(255, 255, 255, 0.5);
+ }
+
+ .tool-card-button {
+ background: transparent !important;
+ border: none !important;
+ padding: 0 !important;
+ margin: 0 !important;
+ width: 100% !important;
+ height: auto !important;
+ min-height: 0 !important;
+ box-shadow: none !important;
+ }
+
+ .tool-details {
+ width: 100%;
+ max-width: 100%;
+ padding: 0;
+ }
+
+ .tool-details-container {
+ background: #0F172A;
+ border-radius: 0;
+ padding: 32px;
+ margin-bottom: 0;
+ max-height: 100vh;
+ overflow-y: auto;
+ width: 100%;
+ }
+
+ .tool-details-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 24px;
+ justify-content: space-between;
+ }
+
+ .tool-details-title-section {
+ display: flex;
+ align-items: center;
+ }
+
+ .tool-details-icon {
+ font-size: 28px;
+ margin-right: 16px;
+ }
+
+ .tool-details-title {
+ font-size: 28px;
+ font-weight: 700;
+ color: white;
+ margin: 0;
+ }
+
+ .tool-details-version {
+ font-size: 14px;
+ color: #94A3B8;
+ margin-left: 8px;
+ }
+
+ .tool-details-description {
+ font-size: 16px;
+ color: #E2E8F0;
+ margin-bottom: 32px;
+ max-width: 800px;
+ }
+
+ .tool-details-section-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: white;
+ margin: 32px 0 16px 0;
+ }
+
+ .tool-details-features {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-bottom: 32px;
+ }
+
+ .tool-details-feature {
+ display: flex;
+ align-items: center;
+ margin-bottom: 0;
+ }
+
+ .tool-details-feature-icon {
+ color: #38BDF8;
+ margin-right: 12px;
+ font-size: 16px;
+ }
+
+ .tool-details-feature-text {
+ color: #E2E8F0;
+ font-size: 16px;
+ }
+
+ .tool-details-screenshots {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 16px;
+ margin-top: 16px;
+ }
+
+ .tool-details-screenshot {
+ width: 100%;
+ border-radius: 8px;
+ overflow: hidden;
+ }
+
+ .tool-details-video {
+ width: 100%;
+ aspect-ratio: 16/9;
+ border-radius: 8px;
+ overflow: hidden;
+ margin-top: 16px;
+ background: #1E293B;
+ border: none;
+ max-width: 800px;
+ height: 450px;
+ }
+
+ .back-button {
+ background: transparent !important;
+ border: none !important;
+ color: #94A3B8 !important;
+ font-size: 14px !important;
+ padding: 8px 16px !important;
+ border-radius: 8px !important;
+ cursor: pointer !important;
+ transition: background-color 0.2s !important;
+ text-align: left !important;
+ width: auto !important;
+ margin-bottom: 24px !important;
+ }
+
+ .back-button:hover {
+ background: rgba(255, 255, 255, 0.1) !important;
+ }
+
+ .install-button {
+ background: #38BDF8 !important;
+ color: #0F172A !important;
+ font-weight: 600 !important;
+ padding: 12px 24px !important;
+ border-radius: 8px !important;
+ margin-top: 24px !important;
+ width: 100% !important;
+ }
+
+ .install-button:hover {
+ background: #0EA5E9 !important;
+ }
+
+ .remove-button {
+ background: #EF4444 !important;
+ color: white !important;
+ font-weight: 600 !important;
+ padding: 12px 24px !important;
+ border-radius: 8px !important;
+ margin-top: 24px !important;
+ width: 100% !important;
+ border: none !important;
+ cursor: pointer !important;
+ transition: background-color 0.2s !important;
+ }
+
+ .remove-button:hover {
+ background: #DC2626 !important;
+ }
+
+ .tool-details-action .remove-button {
+ margin-top: 0 !important;
+ padding: 8px 16px !important;
+ font-size: 14px !important;
+ width: auto !important;
+ }
+
+ /* Confirmation dialog styling */
+ .confirmation-dialog {
+ background-color: #1E293B;
+ border-radius: 8px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ border: 1px solid #2D3748;
+ }
+
+ .confirmation-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: white;
+ margin-bottom: 16px;
+ }
+
+ .confirmation-message {
+ font-size: 14px;
+ color: #E2E8F0;
+ margin-bottom: 24px;
+ }
+
+ .confirmation-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ }
+
+ .cancel-button {
+ background: #4B5563 !important;
+ color: white !important;
+ font-weight: 600 !important;
+ padding: 8px 16px !important;
+ border-radius: 8px !important;
+ border: none !important;
+ cursor: pointer !important;
+ transition: background-color 0.2s !important;
+ }
+
+ .cancel-button:hover {
+ background: #374151 !important;
+ }
+
+ .confirm-button {
+ background: #EF4444 !important;
+ color: white !important;
+ font-weight: 600 !important;
+ padding: 8px 16px !important;
+ border-radius: 8px !important;
+ border: none !important;
+ cursor: pointer !important;
+ transition: background-color 0.2s !important;
+ }
+
+ .confirm-button:hover {
+ background: #DC2626 !important;
+ }
+
+ /* Modal styling */
+ #installation-modal {
+ max-width: 500px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }
+
+ #installation-status {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 12px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #e5e7eb;
+ }
+
+ #installation-result {
+ font-size: 14px;
+ margin-bottom: 20px;
+ white-space: pre-wrap;
+ max-height: 300px;
+ overflow-y: auto;
+ }
+
+ #close-modal-btn {
+ background-color: #6B7280;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: background-color 0.2s;
+ margin-top: 12px;
+ }
+
+ #close-modal-btn:hover {
+ background-color: #4B5563;
+ }
+
+ /* Installation result styling */
+ #installation-result-container {
+ margin-bottom: 20px;
+ border: 1px solid #2D3748;
+ border-radius: 8px;
+ padding: 16px;
+ background-color: #1E293B;
+ max-width: 100%;
+ position: relative;
+ z-index: 100;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ margin-top: 0;
+ }
+
+ #installation-status {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 12px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #2D3748;
+ }
+
+ #installation-result {
+ font-size: 14px;
+ margin-bottom: 20px;
+ white-space: pre-wrap;
+ max-height: 300px;
+ overflow-y: auto;
+ }
+
+ #close-result-btn {
+ background-color: #6B7280;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: background-color 0.2s;
+ margin-top: 12px;
+ }
+
+ #close-result-btn:hover {
+ background-color: #4B5563;
+ }
+
+ .tool-details-action {
+ display: flex;
+ align-items: center;
+ }
+
+ .tool-details-action .install-button {
+ margin-top: 0 !important;
+ padding: 8px 16px !important;
+ font-size: 14px !important;
+ width: auto !important;
+ }
+ """
+
+ with gr.Blocks(theme=gr.themes.Default(), css=css) as app:
+ # Define state variables
+ marketplace_tools_state = gr.State([])
+ current_tool_details_state = gr.State(None)
+
+ # Define event handlers first, before any references to them
+ def show_tools():
+ # Create updates to hide all tool containers
+ tool_container_updates = [gr.update(visible=False) for _ in tool_containers]
+
+ # Generate fresh HTML for tools grid based on current tools
+ tools_html = ''
+ for tool_id, tool in self.tools.items():
+ # Create HTML for each tool card
+ tools_html += f'''
+
+ '''
+
+ # Add "Add New Tool" button
+ tools_html += '''
+
+ '''
+
+ # Create HTML update for tools grid
+ tools_html_component = gr.HTML(f'{tools_html}
')
+
+ return [
+ gr.update(visible=True), # tools_content
+ gr.update(visible=False), # marketplace_content
+ gr.update(visible=False), # settings_content
+ gr.update(visible=False), # tool_interfaces
+ gr.update(visible=False), # tool_details_content
+ gr.update(variant="primary"), # tools_btn
+ gr.update(variant="secondary"), # marketplace_btn
+ gr.update(variant="secondary"), # settings_btn,
+ tools_html_component, # tools_html_component
+ gr.update(value="") # tool_interfaces_html
+ ] + tool_container_updates
+
+ def show_marketplace():
+ # Fetch marketplace tools
+ tools = self.fetch_marketplace_tools()
+
+ # Create HTML for marketplace grid
+ html = ''
+
+ # Hide installation result container and confirmation dialog
+ installation_result_container_update = gr.update(visible=False)
+ confirmation_dialog_update = gr.update(visible=False)
+
+ # Create updates for all tool containers
+ tool_container_updates = [gr.update(visible=False) for _ in tool_containers]
+
+ # Update marketplace_tools_state with the latest tools
+ # This ensures the marketplace shows the current state of available tools
+
+ return [
+ gr.update(visible=False), # tools_content
+ gr.update(visible=True), # marketplace_content
+ gr.update(visible=False), # settings_content
+ gr.update(visible=False), # tool_interfaces
+ gr.update(visible=False), # tool_details_content
+ gr.update(variant="secondary"), # tools_btn
+ gr.update(variant="primary"), # marketplace_btn
+ gr.update(variant="secondary"), # settings_btn
+ tools, # marketplace_tools_state - updated with latest tools
+ html, # marketplace_html
+ installation_result_container_update, # installation_result_container
+ confirmation_dialog_update, # confirmation_dialog
+ gr.update(value="") # tool_interfaces_html
+ ] + tool_container_updates
+
+ def show_settings():
+ # Create updates to hide all tool containers
+ tool_container_updates = [gr.update(visible=False) for _ in tool_containers]
+
+ return [
+ gr.update(visible=False), # tools_content
+ gr.update(visible=False), # marketplace_content
+ gr.update(visible=True), # settings_content
+ gr.update(visible=False), # tool_interfaces
+ gr.update(visible=False), # tool_details_content
+ gr.update(variant="secondary"), # tools_btn
+ gr.update(variant="secondary"), # marketplace_btn
+ gr.update(variant="primary"), # settings_btn
+ gr.update(value="") # tool_interfaces_html
+ ] + tool_container_updates
+
+ def show_tool(tool_id=None):
+ # Import gradio at the beginning of the function
+ import gradio as gr
+
+ # Check if the tool exists
+ if tool_id not in self.tools:
+ print(f"Tool {tool_id} not found")
+ return show_tools()
+
+ # Get the tool
+ tool = self.tools[tool_id]
+
+ # Check if this is a newly installed tool that needs special handling
+ if hasattr(tool, 'newly_installed') and tool.newly_installed:
+ print(f"Dynamically creating UI for newly installed tool: {tool_id}")
+
+ try:
+ # Create a message to inform the user that we're loading the tool
+ loading_message = f"""
+
+
Loading {tool.name}...
+
Please wait while we set up the tool interface.
+
+ """
+
+ # First, return a loading message
+ loading_updates = [
+ gr.update(visible=False), # tools_content
+ gr.update(visible=False), # marketplace_content
+ gr.update(visible=False), # settings_content
+ gr.update(visible=True), # tool_interfaces
+ gr.update(visible=False), # tool_details_content
+ gr.update(variant="secondary"), # tools_btn
+ gr.update(variant="secondary"), # marketplace_btn
+ gr.update(variant="secondary"), # settings_btn
+ gr.update(value=loading_message) # tool_interfaces_html
+ ] + [gr.update(visible=False) for _ in tool_containers]
+
+ # Instead of trying to create the UI dynamically, which can cause issues with Gradio components,
+ # we'll create a simple HTML interface that provides basic functionality and instructions
+
+ # Get the tool class name and module for display
+ tool_class_name = tool.ui_class.__name__
+ tool_module_name = tool.ui_module
+
+ # Create a friendly HTML interface
+ tool_html = f"""
+
+
{tool.name} is Ready!
+
This tool has been successfully installed.
+
For the best experience with this tool, please restart the application.
+
After restarting, you'll be able to use all the features of this tool.
+
+
+ """
+
+ # Remove the newly_installed flag so we don't try to create the UI again
+ delattr(tool, 'newly_installed')
+
+ # Return the HTML interface
+ return [
+ gr.update(visible=False), # tools_content
+ gr.update(visible=False), # marketplace_content
+ gr.update(visible=False), # settings_content
+ gr.update(visible=True), # tool_interfaces
+ gr.update(visible=False), # tool_details_content
+ gr.update(variant="secondary"), # tools_btn
+ gr.update(variant="secondary"), # marketplace_btn
+ gr.update(variant="secondary"), # settings_btn
+ gr.update(value=tool_html) # tool_interfaces_html
+ ] + [gr.update(visible=False) for _ in tool_containers]
+ except Exception as e:
+ print(f"Error creating UI for newly installed tool: {e}")
+ import traceback
+ traceback.print_exc()
+
+ # If there's an error, show a message asking the user to restart
+ restart_message = f"""
+
+
Error Loading {tool.name}
+
There was an error setting up the tool interface:
+
{str(e)}
+
Please restart the application to use this tool.
+
+ Refresh Page
+
+
+ """
+
+ return [
+ gr.update(visible=False), # tools_content
+ gr.update(visible=False), # marketplace_content
+ gr.update(visible=False), # settings_content
+ gr.update(visible=True), # tool_interfaces
+ gr.update(visible=False), # tool_details_content
+ gr.update(variant="secondary"), # tools_btn
+ gr.update(variant="secondary"), # marketplace_btn
+ gr.update(variant="secondary"), # settings_btn
+ gr.update(value=restart_message) # tool_interfaces_html
+ ] + [gr.update(visible=False) for _ in tool_containers]
+
+ # For existing tools with proper UI containers
+ # Create a list of visibility updates for all tool containers
+ updates = []
+
+ for i, container in enumerate(tool_containers):
+ # Get the tool_id for this container
+ container_tool_id = None
+ for tid, t in self.tools.items():
+ if hasattr(t, 'ui_container') and t.ui_container == container:
+ container_tool_id = tid
+ break
+
+ # Set visibility based on whether this is the selected tool
+ if container_tool_id == tool_id:
+ updates.append(gr.update(visible=True))
+ else:
+ updates.append(gr.update(visible=False))
+
+ return [
+ gr.update(visible=False), # tools_content
+ gr.update(visible=False), # marketplace_content
+ gr.update(visible=False), # settings_content
+ gr.update(visible=True), # tool_interfaces
+ gr.update(visible=False), # tool_details_content
+ gr.update(variant="secondary"), # tools_btn
+ gr.update(variant="secondary"), # marketplace_btn
+ gr.update(variant="secondary"), # settings_btn
+ gr.update(value="") # tool_interfaces_html
+ ] + updates
+
+ def show_tool_details(tool_id):
+ # Fetch tool details
+ tool = self.fetch_tool_details(tool_id)
+
+ # Create updates to hide all tool containers
+ tool_container_updates = [gr.update(visible=False) for _ in tool_containers]
+
+ if not tool:
+ return [
+ gr.update(visible=False), # tools_content
+ gr.update(visible=True), # marketplace_content
+ gr.update(visible=False), # settings_content
+ gr.update(visible=False), # tool_interfaces
+ gr.update(visible=False), # tool_details_content
+ gr.update(variant="secondary"), # tools_btn
+ gr.update(variant="primary"), # marketplace_btn
+ gr.update(variant="secondary"), # settings_btn
+ None, # current_tool_details_state
+ "", # tool_details_html
+ gr.update(visible=False), # installation_result_container
+ gr.update(visible=False), # confirmation_dialog
+ gr.update(value="") # tool_interfaces_html
+ ] + tool_container_updates
+
+ # Create features HTML
+ features_html = "".join([
+ f'✓ {feature}
'
+ for feature in tool.get('features', [])
+ ])
+
+ # Create demo video HTML
+ demo_video_html = f'' if True else ''
+
+ # Check if the tool is already installed
+ # Use the is_installed flag if available, otherwise check self.tools
+ is_installed = tool.get('is_installed', False) or tool_id in self.tools
+
+ # Determine which button to show based on installation status
+ if is_installed:
+ action_button = f'Remove Tool '
+ else:
+ action_button = f'Add Tool '
+
+ # Create HTML for tool details
+ html = f'''
+
+ '''
+
+ print(f"Setting current_tool_details_state to: {tool}")
+ return [
+ gr.update(visible=False), # tools_content
+ gr.update(visible=False), # marketplace_content
+ gr.update(visible=False), # settings_content
+ gr.update(visible=False), # tool_interfaces
+ gr.update(visible=True), # tool_details_content
+ gr.update(variant="secondary"), # tools_btn
+ gr.update(variant="secondary"), # marketplace_btn
+ gr.update(variant="secondary"), # settings_btn
+ tool, # current_tool_details_state
+ html, # tool_details_html
+ gr.update(visible=False), # installation_result_container
+ gr.update(visible=False), # confirmation_dialog
+ gr.update(value="") # tool_interfaces_html
+ ] + tool_container_updates
+
+ def install_selected_tool(tool_details):
+ print(f"Installing tool: {tool_details}")
+
+ if not tool_details:
+ print("No tool details provided")
+ return [
+ gr.update(visible=True), # installation_result_container
+ "⚠️ Installation Failed", # installation_status
+ "No tool selected to install." # installation_result
+ ]
+
+ try:
+ # Install the tool
+ result = self.install_tool(tool_details['id'])
+
+ if "Error" in result:
+ return [
+ gr.update(visible=True), # installation_result_container
+ "⚠️ Installation Failed", # installation_status
+ result # installation_result
+ ]
+ else:
+ # If installation was successful, we'll show a success message
+ # The user can click "Close" to see the installation result
+ # and then navigate to the tools page to see the new tool
+ return [
+ gr.update(visible=True), # installation_result_container
+ "✅ Installation Successful", # installation_status
+ result # installation_result
+ ]
+ except Exception as e:
+ print(f"Error installing tool: {e}")
+ return [
+ gr.update(visible=True), # installation_result_container
+ "⚠️ Installation Failed", # installation_status
+ f"Error installing tool: {e}" # installation_result
+ ]
+
+ # Create components after defining the functions
+ with gr.Row():
+ # Sidebar
+ with gr.Column(elem_id="sidebar", scale=1):
+ gr.Markdown("AI TOOLS", elem_classes="title")
+ tools_btn = gr.Button("Tools", elem_classes="nav-button", variant="primary")
+ marketplace_btn = gr.Button("Marketplace", elem_classes="nav-button", variant="secondary")
+ settings_btn = gr.Button("Settings", elem_classes="nav-button", variant="secondary")
+ gr.Markdown(f"version: {self.version}", elem_classes="version-text")
+
+ # Main content
+ with gr.Column(elem_id="main-content", scale=4):
+ # Define all content containers
+ tools_content = gr.Column(visible=True)
+ marketplace_content = gr.Column(visible=False)
+ settings_content = gr.Column(visible=False)
+ tool_interfaces = gr.Column(visible=False)
+ tool_details_content = gr.Column(visible=False)
+
+ # Fill tools content
+ with tools_content:
+ gr.Markdown("Tools", elem_classes="page-title")
+
+ tool_buttons = []
+ tool_ids = [] # Store tool_ids in the same order as tool_buttons
+
+ # Create HTML for all tools
+ tools_html = ''
+
+ for tool_id, tool in self.tools.items():
+ # Create HTML for each tool card
+ tools_html += f'''
+
+ '''
+
+ # Create hidden buttons for tool selection
+ btn = gr.Button(f"Select {tool.name}", visible=False, elem_id=f"tool-btn-{tool_id}")
+ tool_buttons.append(btn)
+ tool_ids.append(tool_id)
+
+ # Add "Add New Tool" button
+ tools_html += '''
+
+ '''
+
+ # Render the HTML inside a div with the tool-grid class
+ tools_html_component = gr.HTML(f'{tools_html}
')
+
+ # Create hidden add tool button
+ add_tool_btn = gr.Button("Add New Tool", visible=False, elem_id="add-tool-btn")
+
+ # Fill marketplace content
+ with marketplace_content:
+ gr.Markdown("Marketplace", elem_classes="page-title")
+ marketplace_html = gr.HTML("Loading marketplace tools...")
+
+ # Create hidden buttons for marketplace tool details with unique IDs
+ tool_buttons_container = gr.Column(visible=False)
+
+ # Create hidden buttons for marketplace tools
+ marketplace_tool_buttons = []
+ for i, tool in enumerate(self.fetch_marketplace_tools()):
+ tool_btn = gr.Button(f"View {tool['name']}", visible=False, elem_id=f"marketplace-tool-btn-{i}")
+ marketplace_tool_buttons.append((tool['id'], tool_btn))
+
+ # The click handlers for marketplace tool buttons will be set up after all components are defined
+
+ # Fill tool details content
+ with tool_details_content:
+ # Installation result section at the top
+ with gr.Column(elem_id="installation-result-container", elem_classes="installation-result-container", visible=False) as installation_result_container:
+ installation_status = gr.Markdown("Ready to install", elem_id="installation-status")
+ installation_result = gr.Markdown("Click the Install Tool button to install the selected tool.", elem_id="installation-result")
+ close_result_btn = gr.Button("Close", elem_id="close-result-btn")
+
+ # Confirmation dialog for tool removal
+ with gr.Column(elem_id="confirmation-dialog", elem_classes="confirmation-dialog", visible=False) as confirmation_dialog:
+ gr.Markdown("⚠️ Warning: Remove Tool", elem_id="confirmation-title", elem_classes="confirmation-title")
+ gr.Markdown(
+ "Are you sure you want to remove this tool? This action cannot be undone. "
+ "All data created by this tool will be permanently deleted.",
+ elem_id="confirmation-message",
+ elem_classes="confirmation-message"
+ )
+ with gr.Row(elem_classes="confirmation-buttons"):
+ cancel_btn = gr.Button("Cancel", elem_classes="cancel-button")
+ confirm_btn = gr.Button("Yes, Remove Tool", elem_classes="confirm-button")
+
+ with gr.Row():
+ with gr.Column(scale=1):
+ back_btn = gr.Button("← Back to Marketplace", elem_classes="back-button")
+ with gr.Column(scale=3):
+ gr.Markdown("", elem_classes="page-title")
+
+ # Tool details content
+ tool_details_html = gr.HTML("")
+
+ # Hidden install button (will be triggered by the inline button)
+ install_btn = gr.Button("Add Tool", elem_classes="install-button", elem_id="install-btn", variant="primary", visible=False)
+
+ # Hidden remove button (will be triggered by the inline button)
+ remove_btn = gr.Button("Remove Tool", elem_classes="remove-button", elem_id="remove-btn", variant="primary", visible=False)
+
+ # Add a direct event handler to the install button
+ def debug_install_click():
+ print("Install button clicked directly!")
+ return [gr.update(visible=True), gr.update(value="Install button clicked!")]
+
+ # Add a direct event handler to the remove button to show confirmation dialog
+ def show_confirmation_dialog():
+ print("Remove button clicked - showing confirmation dialog")
+ return gr.update(visible=True)
+
+ # Function to handle tool removal confirmation
+ def remove_selected_tool(tool_details):
+ print(f"Removing tool: {tool_details}")
+
+ if not tool_details:
+ print("No tool details provided")
+ return [
+ gr.update(visible=False), # confirmation_dialog
+ gr.update(visible=True), # installation_result_container
+ "⚠️ Removal Failed", # installation_status
+ "No tool selected to remove." # installation_result
+ ]
+
+ try:
+ # Remove the tool
+ result = self.remove_tool(tool_details['id'])
+
+ if "Error" in result:
+ return [
+ gr.update(visible=False), # confirmation_dialog
+ gr.update(visible=True), # installation_result_container
+ "⚠️ Removal Failed", # installation_status
+ result # installation_result
+ ]
+ else:
+ # After successful removal, show success message
+ # The user will need to click "Back to Marketplace" manually
+ # This avoids issues with dynamic UI updates
+ return [
+ gr.update(visible=False), # confirmation_dialog
+ gr.update(visible=True), # installation_result_container
+ "✅ Removal Successful", # installation_status
+ f"{result} - Click 'Back to Marketplace' to return. You may need to restart the application to see all changes." # installation_result
+ ]
+ except Exception as e:
+ print(f"Error removing tool: {e}")
+ return [
+ gr.update(visible=False), # confirmation_dialog
+ gr.update(visible=True), # installation_result_container
+ "⚠️ Removal Failed", # installation_status
+ f"Error removing tool: {e}" # installation_result
+ ]
+
+ # Function to cancel removal
+ def cancel_removal():
+ return gr.update(visible=False)
+
+ debug_output = gr.Textbox(label="Debug Output", visible=False)
+
+ # Set up event handlers for buttons
+ install_btn.click(
+ fn=install_selected_tool,
+ inputs=[current_tool_details_state],
+ outputs=[installation_result_container, installation_status, installation_result]
+ )
+ remove_btn.click(fn=show_confirmation_dialog, outputs=confirmation_dialog)
+
+ # Set up confirmation dialog buttons
+ cancel_btn.click(fn=cancel_removal, outputs=confirmation_dialog)
+ confirm_btn.click(
+ fn=remove_selected_tool,
+ inputs=[current_tool_details_state],
+ outputs=[confirmation_dialog, installation_result_container, installation_status, installation_result]
+ )
+
+ # Fill settings content
+ with settings_content:
+ gr.Markdown("Settings", elem_classes="page-title")
+ gr.Markdown("Settings options will appear here...")
+
+ # Fill tool interfaces
+ with tool_interfaces:
+ # Add an HTML component to display messages for newly installed tools
+ tool_interfaces_html = gr.HTML("", elem_id="tool-interfaces-html")
+
+ for tool_id, tool in self.tools.items():
+ with gr.Column(visible=False) as tool_container:
+ tool_ui = tool.ui_class()
+ tool_ui.create_ui()
+ # Store the UI container in the tool object for later reference
+ tool.ui_container = tool_container
+
+ # Set up event handlers after all components are defined
+ # Set up tool button events
+
+ # Collect all tool containers for outputs
+ tool_containers = []
+ for tool_id, tool in self.tools.items():
+ if hasattr(tool, 'ui_container'):
+ tool_containers.append(tool.ui_container)
+
+ # Set up click handlers for marketplace tool buttons
+ for i, (tool_id, tool_btn) in enumerate(marketplace_tool_buttons):
+ # We need to create a separate function for each button to avoid lambda closure issues
+ def make_handler(tid):
+ return lambda: show_tool_details(tid)
+
+ handler = make_handler(tool_id)
+ tool_btn.click(
+ fn=handler,
+ outputs=[
+ tools_content, marketplace_content, settings_content, tool_interfaces,
+ tool_details_content, tools_btn, marketplace_btn, settings_btn,
+ current_tool_details_state, tool_details_html, installation_result_container,
+ confirmation_dialog, tool_interfaces_html
+ ] + tool_containers
+ )
+
+ for i, btn in enumerate(tool_buttons):
+ if isinstance(btn, gr.Button): # Check if it's a button from the tools grid
+ tool_id = tool_ids[i] # Get the corresponding tool_id
+
+ # We need to create a separate function for each button to avoid lambda closure issues
+ def make_handler(tid):
+ return lambda: show_tool(tid)
+
+ handler = make_handler(tool_id)
+ btn.click(
+ fn=handler,
+ outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
+ tool_details_content, tools_btn, marketplace_btn, settings_btn,
+ tool_interfaces_html] + tool_containers
+ )
+
+ # Set up navigation events
+ tools_btn.click(
+ fn=show_tools,
+ outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
+ tool_details_content, tools_btn, marketplace_btn, settings_btn,
+ tools_html_component, tool_interfaces_html] + tool_containers
+ )
+
+ marketplace_btn.click(
+ fn=show_marketplace,
+ outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
+ tool_details_content, tools_btn, marketplace_btn, settings_btn,
+ marketplace_tools_state, marketplace_html, installation_result_container,
+ confirmation_dialog, tool_interfaces_html] + tool_containers
+ )
+
+ settings_btn.click(
+ fn=show_settings,
+ outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
+ tool_details_content, tools_btn, marketplace_btn, settings_btn,
+ tool_interfaces_html] + tool_containers
+ )
+
+ # Set up add tool button to navigate to marketplace
+ add_tool_btn.click(
+ fn=show_marketplace,
+ outputs=[tools_content, marketplace_content, settings_content, tool_interfaces,
+ tool_details_content, tools_btn, marketplace_btn, settings_btn,
+ marketplace_tools_state, marketplace_html, installation_result_container,
+ confirmation_dialog, tool_interfaces_html] + tool_containers
+ )
+
+ # Set up event handlers for tool details
+ back_btn.click(
+ fn=show_marketplace,
+ outputs=[
+ tools_content, marketplace_content, settings_content, tool_interfaces,
+ tool_details_content, tools_btn, marketplace_btn, settings_btn,
+ marketplace_tools_state, marketplace_html, installation_result_container,
+ confirmation_dialog
+ ] + tool_containers
+ )
+
+ # Define a function to handle closing the installation result and navigating to tools
+ def close_result_and_show_tools():
+ # First hide the installation result container
+ installation_result_update = gr.update(visible=False)
+
+ # Then get the updates from show_tools
+ tools_updates = show_tools()
+
+ # Combine the updates
+ return [installation_result_update] + tools_updates
+
+ # Set up close button for installation result
+ close_result_btn.click(
+ fn=close_result_and_show_tools,
+ outputs=[installation_result_container,
+ tools_content, marketplace_content, settings_content, tool_interfaces,
+ tool_details_content, tools_btn, marketplace_btn, settings_btn,
+ tools_html_component, tool_interfaces_html] + tool_containers
+ )
+
+ return app
+
+if __name__ == "__main__":
+ ui = MainUI()
+ demo = ui.create_ui()
+ demo.queue() # Add queue for better handling of multiple requests
+ demo.title = "AI Tools Platform" # Add a title
+ demo.launch(
+ debug=True,
+ share=True,
+ server_name="0.0.0.0",
+ server_port=7860
+ )
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bfc77761499867c67cb18cf780decea68500a78d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,12 @@
+librosa==0.10.1
+numpy==1.26.3
+moviepy==2.1.2
+opencv-python>=4.8.0
+scikit-learn>=1.3.0
+scenedetect~=0.6.5.2
+gradio==4.19.2
+flask==2.3.3
+requests==2.31.0
+rembg
+Pillow
+onnxruntime
\ No newline at end of file
diff --git a/tools/beatsyncer/__init__.py b/tools/beatsyncer/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c5ffaa47c0c5aabb128a607463b25ff6df132c0
--- /dev/null
+++ b/tools/beatsyncer/__init__.py
@@ -0,0 +1 @@
+# Beatsyncer package
diff --git a/tools/beatsyncer/__pycache__/__init__.cpython-311.pyc b/tools/beatsyncer/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0455f85a42b0b6394c8fcdb3299ef1a36a46d536
Binary files /dev/null and b/tools/beatsyncer/__pycache__/__init__.cpython-311.pyc differ
diff --git a/tools/beatsyncer/__pycache__/__init__.cpython-312.pyc b/tools/beatsyncer/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fe1710051c724ce0887295870d0cbbca8ead476a
Binary files /dev/null and b/tools/beatsyncer/__pycache__/__init__.cpython-312.pyc differ
diff --git a/tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-311.pyc b/tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3586b6413d824cedc919a0bc6f03c90a9d3dcedd
Binary files /dev/null and b/tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-311.pyc differ
diff --git a/tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-312.pyc b/tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c1d9f10e58769410050683768edf313377bb7671
Binary files /dev/null and b/tools/beatsyncer/__pycache__/beatsyncer_tool.cpython-312.pyc differ
diff --git a/tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-311.pyc b/tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4a443f2f731584a0006cd814f38191daf5d44f47
Binary files /dev/null and b/tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-311.pyc differ
diff --git a/tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-312.pyc b/tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2a7af734268a487136ea7bb51eaa50882a35d519
Binary files /dev/null and b/tools/beatsyncer/__pycache__/beatsyncer_ui.cpython-312.pyc differ
diff --git a/tools/beatsyncer/beatsyncer_tool.py b/tools/beatsyncer/beatsyncer_tool.py
new file mode 100644
index 0000000000000000000000000000000000000000..edd6fa2bef518ada4c73df2c6c7627eb6c7b17ab
--- /dev/null
+++ b/tools/beatsyncer/beatsyncer_tool.py
@@ -0,0 +1,470 @@
+import os
+import random
+import librosa
+import numpy as np
+from moviepy import *
+
+class BeatSyncer:
+ AUDIO_EXTENSIONS = {'.mp3', '.wav', '.m4a', '.aac', '.ogg', '.flac'}
+ VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv'}
+
+ def __init__(self, audio_filename):
+ # Construct the full path to the audio file relative to the script's directory
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ input_path = os.path.join(script_dir, audio_filename)
+
+ # Convert to audio if needed
+ if self._is_video_file(input_path):
+ print(f"Converting video file '{input_path}' to audio...")
+ audio_path = self._convert_to_audio(input_path)
+ self.audio_path = audio_path
+ elif self._is_audio_file(input_path):
+ self.audio_path = input_path
+ else:
+ raise ValueError(f"Unsupported file format. File must be one of: {self.AUDIO_EXTENSIONS.union(self.VIDEO_EXTENSIONS)}")
+
+ self.y = None # Audio samples
+ self.sr = None # Sample rate
+ self.beat_times = None
+ self.beat_drop_times = None
+
+ def _is_audio_file(self, filepath):
+ """Check if the file is an audio file based on its extension."""
+ return os.path.splitext(filepath)[1].lower() in self.AUDIO_EXTENSIONS
+
+ def _is_video_file(self, filepath):
+ """Check if the file is a video file based on its extension."""
+ return os.path.splitext(filepath)[1].lower() in self.VIDEO_EXTENSIONS
+
+ def _convert_to_audio(self, video_path):
+ """Convert a video file to audio and return the path to the audio file."""
+ try:
+ # Create tmp directory in project root if it doesn't exist
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ tmp_dir = os.path.join(script_dir, 'tmp')
+ os.makedirs(tmp_dir, exist_ok=True)
+
+ # Create audio filename in tmp directory
+ base_name = os.path.basename(video_path)
+ audio_name = os.path.splitext(base_name)[0] + '.mp3'
+ audio_path = os.path.join(tmp_dir, audio_name)
+
+ # Convert video to audio
+ video = VideoFileClip(video_path)
+ video.audio.write_audiofile(audio_path)
+ video.close()
+
+ return audio_path
+ except Exception as e:
+ raise ValueError(f"Error converting video to audio: {str(e)}")
+
+ def load_audio(self, duration_minutes=None):
+ """
+ Load the audio file using librosa, limiting to duration_minutes if provided.
+ """
+ try:
+ self.sr = 44100 # Set a standard sample rate
+ if duration_minutes is not None:
+ duration_seconds = duration_minutes * 60
+ else:
+ duration_seconds = None # Load full audio
+ self.y, _ = librosa.load(self.audio_path, sr=self.sr, duration=duration_seconds)
+ if self.y is None or len(self.y) == 0:
+ raise ValueError("Audio data is empty.")
+ duration = len(self.y) / self.sr
+ print(f"Loaded audio file '{self.audio_path}' with duration {duration:.2f} seconds.")
+ except Exception as e:
+ print(f"An error occurred while loading the audio file: {e}")
+ raise
+
+ def detect_beat_drops(self, sensitivity='medium'):
+ """
+ Improved beat detection using onset_strength_multi and adjusted parameters.
+
+ Args:
+ sensitivity (str): Detection sensitivity level ('low', 'medium', or 'high').
+ - low: Less sensitive, detects only major beat drops
+ - medium: Balanced sensitivity (default)
+ - high: More sensitive, detects subtle beat changes
+ """
+ print("Detecting beat drops...")
+ if self.y is None or len(self.y) == 0:
+ raise ValueError("Audio data is empty. Cannot perform beat detection.")
+
+ # Map sensitivity levels to parameters
+ sensitivity_params = {
+ 'low': {
+ 'hop_length': 512, # Larger hop length for less granular detection
+ 'std_multiplier': 1.0, # Higher threshold for beat detection
+ 'min_interval': 0.05 # Longer minimum interval between beats
+ },
+ 'medium': {
+ 'hop_length': 256, # Default hop length
+ 'std_multiplier': 0.5, # Default threshold
+ 'min_interval': 0.02 # Default minimum interval
+ },
+ 'high': {
+ 'hop_length': 128, # Smaller hop length for more granular detection
+ 'std_multiplier': 0.25, # Lower threshold for beat detection
+ 'min_interval': 0.01 # Shorter minimum interval between beats
+ }
+ }
+
+ # Validate and get sensitivity parameters
+ sensitivity = sensitivity.lower()
+ if sensitivity not in sensitivity_params:
+ raise ValueError(f"Invalid sensitivity level. Must be one of: {list(sensitivity_params.keys())}")
+
+ params = sensitivity_params[sensitivity]
+
+ # Parameters for onset detection
+ hop_length = params['hop_length']
+
+ params.update({
+ "detrend": True,
+ "tightness": 100
+ })
+
+ # Compute the onset envelope using onset_strength_multi with adjusted parameters
+ onset_env_multi = librosa.onset.onset_strength_multi(
+ y=self.y,
+ sr=self.sr,
+ hop_length=params["hop_length"],
+ aggregate=np.median,
+ lag=1,
+ max_size=1,
+ detrend=params["detrend"],
+ center=True,
+ )
+
+ # Sum across channels if multi-channel audio
+ onset_env = onset_env_multi.mean(axis=0)
+
+ # Beat tracking with adjusted parameters
+ tempo, beats = librosa.beat.beat_track(
+ onset_envelope=onset_env,
+ sr=self.sr,
+ hop_length=params["hop_length"],
+ tightness=params["tightness"],
+ units='time'
+ )
+
+ # Convert tempo to scalar if it's an array
+ tempo = float(np.mean(tempo)) if isinstance(tempo, np.ndarray) else float(tempo)
+
+ # Adjust tempo if it's unusually high (e.g., over 180 BPM)
+ if tempo >= 180:
+ tempo /= 2
+ print(f"Adjusted tempo: {tempo:.2f} BPM")
+ else:
+ print(f"Detected tempo: {tempo:.2f} BPM")
+
+ self.beat_times = beats
+
+ if len(self.beat_times) == 0:
+ raise ValueError("No beats detected in the audio.")
+
+ # Compute the difference in onset envelope with sensitivity-based threshold
+ onset_diff = np.diff(onset_env)
+ threshold = np.mean(onset_diff) + params['std_multiplier'] * np.std(onset_diff)
+ beat_drop_indices = np.where(onset_diff > threshold)[0] + 1
+
+ # Convert beat frames to times
+ onset_times = librosa.frames_to_time(
+ np.arange(len(onset_env)), sr=self.sr, hop_length=hop_length
+ )
+
+ # Map beat drop indices to times
+ beat_drop_times = onset_times[beat_drop_indices]
+
+ # Remove duplicate beat times
+ self.beat_drop_times = np.unique(beat_drop_times)
+
+ # Remove beat drops that are too close together based on sensitivity
+ min_interval = params['min_interval']
+ filtered_beat_drop_times = [self.beat_drop_times[0]]
+ for bd_time in self.beat_drop_times[1:]:
+ if bd_time - filtered_beat_drop_times[-1] >= min_interval:
+ filtered_beat_drop_times.append(bd_time)
+ self.beat_drop_times = np.array(filtered_beat_drop_times)
+
+ print(f"Detected {len(self.beat_drop_times)} beat drops with {sensitivity} sensitivity.")
+
+ def _get_media_files(self, media_dir):
+ """
+ Get list of media files from the directory.
+ Returns a shuffled list of absolute paths to media files.
+ """
+ supported_extensions = ('.mp4', '.avi', '.mov', '.png', '.jpg', '.jpeg', '.gif', '.mkv')
+ media_files = []
+
+ # Walk through directory
+ for root, _, files in os.walk(media_dir):
+ for file in files:
+ if file.lower().endswith(supported_extensions):
+ full_path = os.path.join(root, file)
+ media_files.append(full_path)
+
+ if not media_files:
+ raise ValueError(f"No supported media files found in directory: {media_dir}")
+
+ # Shuffle the media files list
+ random.shuffle(media_files)
+ print(f"Found and shuffled {len(media_files)} media files")
+
+ return media_files
+
+ def _calculate_text_duration(self, text):
+ """
+ Calculate the duration a text should be displayed based on its word count.
+
+ Args:
+ text (str): The text to be displayed
+
+ Returns:
+ float: Duration in seconds
+ """
+ # Count words (split by whitespace)
+ word_count = len(text.split())
+
+ # Base duration calculation:
+ # - Minimum duration: 2 seconds
+ # - Add 0.5 seconds per word
+ # - Maximum duration: 8 seconds
+ duration = min(max(2, word_count * 0.5), 8)
+
+ return duration
+
+ def _wrap_text(self, text, max_chars_per_line=30):
+ """
+ Wrap text to ensure it fits nicely on screen.
+
+ Args:
+ text (str): Text to wrap
+ max_chars_per_line (int): Maximum characters per line
+
+ Returns:
+ str: Wrapped text with newlines and increased line spacing
+ """
+ words = text.split()
+ lines = []
+ current_line = []
+ current_length = 0
+
+ for word in words:
+ word_length = len(word)
+ if current_length + word_length + len(current_line) <= max_chars_per_line:
+ current_line.append(word)
+ current_length += word_length
+ else:
+ lines.append(' '.join(current_line))
+ current_line = [word]
+ current_length = word_length
+
+ if current_line:
+ lines.append(' '.join(current_line))
+
+ # Add extra line spacing by using double newlines
+ return '\n'.join(lines)
+
+ def sync_with_media_directory(self, media_dir, output_path, duration_minutes=None, video_size=None, dark_overlay=0.3,
+ text_overlays=None):
+ """
+ Create a video by syncing media files with beat drops.
+ Args:
+ media_dir: Directory containing images and videos
+ output_path: Path where the output video will be saved
+ duration_minutes: Desired total duration in minutes
+ video_size: Tuple of (width, height) for output video
+ dark_overlay: Opacity of dark overlay (0.0 to 1.0, where 1.0 is completely black)
+ text_overlays: List of text strings or tuples (text, duration_seconds).
+ If only text is provided, duration will be calculated based on word count.
+ """
+ print("Creating video from media files...")
+ media_files = self._get_media_files(media_dir)
+
+ if not media_files:
+ raise ValueError(f"No media files found in directory: {media_dir}")
+
+ # Prepare clips with dark theme
+ clips = []
+
+ # Set desired_total_duration
+ if duration_minutes is not None:
+ desired_total_duration = duration_minutes * 60 # Convert minutes to seconds
+ else:
+ desired_total_duration = len(self.y) / self.sr # Use the length of the audio
+
+ # Limit the beat_drop_times to the desired duration
+ beat_drop_times = self.beat_drop_times[self.beat_drop_times <= desired_total_duration]
+
+ if len(beat_drop_times) == 0:
+ raise ValueError("No beat drops detected within the desired duration.")
+
+ # Include the start (0) and end times to cover the entire duration
+ beat_times_full = np.concatenate(([0], beat_drop_times, [desired_total_duration]))
+
+ num_media = len(media_files)
+ media_index = 0
+
+ synced_clips = []
+
+ # Define the desired clip length for initial blank space
+ initial_clip_duration = 2 # 2 seconds for each clip in the initial blank space
+ base_scale = 1.2 # resized factor to make clips larger than the video size
+ zoom_factor = 0.009 # Zoom factor for zoom effects
+
+ # Loop over intervals between beat times to cover entire duration
+ for i in range(len(beat_times_full) - 1):
+ start_time = beat_times_full[i]
+ end_time = beat_times_full[i + 1]
+ interval_duration = end_time - start_time
+
+ if i == 0 and interval_duration > initial_clip_duration:
+ # Handle the initial blank space with multiple clips
+ num_initial_clips = int(np.ceil(interval_duration / initial_clip_duration))
+ for j in range(num_initial_clips):
+ clip_start_time = start_time + j * initial_clip_duration
+ clip_end_time = min(clip_start_time + initial_clip_duration, end_time)
+ clip_duration = clip_end_time - clip_start_time
+
+ media_file = media_files[media_index % num_media]
+ media_index += 1
+
+ if media_file.lower().endswith(('.mp4', '.avi', '.mov')):
+ # It's a video file
+ clip = VideoFileClip(media_file).subclipped(0, clip_duration).resized(base_scale)
+
+ else:
+ # It's an image file
+ clip = ImageClip(media_file).with_duration(clip_duration).resized(base_scale)
+
+
+ # cropped the clip to the desired video size
+ clip = clip.cropped(width=video_size[0], height=video_size[1], x_center=clip.w / 2,
+ y_center=clip.h / 2)
+
+ # Set the start time of the clip
+ clip = clip.with_start(clip_start_time)
+
+ # Apply random zoom in or zoom out effect with slower zoom
+ zoom_type = random.choice(['zoom_in', 'zoom_out', None])
+ # if zoom_type == 'zoom_in':
+ # # Slow down zoom in effect
+ # clip = clip.with_effects([vfx.Resize(lambda t: 1 + zoom_factor * (t / clip.duration))])
+ # elif zoom_type == 'zoom_out':
+ # # Slow down zoom out effect
+ # clip = clip.with_effects([vfx.Resize(lambda t: 1 + zoom_factor * (1 - t / clip.duration))])
+
+ # Add random transition to the clip
+ if synced_clips:
+ transition_type = random.choice(['crossfadein', 'fadein', None])
+ if transition_type == 'crossfadein':
+ # clip = clip.crossfadein(0.1)
+ print()
+ elif transition_type == 'fadein':
+ # clip = clip.fadein(0.1)
+ print()
+
+ synced_clips.append(clip)
+ else:
+ # Handle regular intervals between beat drops
+ media_file = media_files[media_index % num_media]
+ media_index += 1
+
+ if media_file.lower().endswith(('.mp4', '.avi', '.mov')):
+ # It's a video file
+ clip = VideoFileClip(media_file).subclipped(0, interval_duration).resized(base_scale)
+
+
+ else:
+ # It's an image file
+ clip = ImageClip(media_file).with_duration(interval_duration).resized(base_scale)
+
+
+ # cropped the clip to the desired video size
+ clip = clip.cropped(width=video_size[0], height=video_size[1], x_center=clip.w / 2, y_center=clip.h / 2)
+
+ # Set the start time of the clip
+ clip = clip.with_start(start_time)
+
+ # Apply random zoom in or zoom out effect with slower zoom
+ zoom_type = random.choice(['zoom_in', 'zoom_out', None])
+ # if zoom_type == 'zoom_in':
+ # # Slow down zoom in effect
+ # clip = clip.with_effects([vfx.Resize(lambda t: 1 + zoom_factor * (t / clip.duration))])
+ # elif zoom_type == 'zoom_out':
+ # # Slow down zoom out effect
+ # clip = clip.with_effects([vfx.Resize(lambda t: 1 + zoom_factor * (1 - t / clip.duration))])
+
+
+ # If this is the last clip, apply fadeout
+ if i == len(beat_times_full) - 2:
+ fade_duration = min(2, clip.duration / 2)
+
+ clip = clip.with_effects([vfx.FadeOut(fade_duration)])
+
+ synced_clips.append(clip)
+
+ # Create the final video
+ final_clip = CompositeVideoClip(synced_clips, size=video_size)
+
+ # Create a list of all clips for final composition
+ all_clips = [final_clip]
+
+ # Add dark overlay if enabled
+ if dark_overlay > 0:
+ # Create a black ColorClip with the same size as the video
+ black_clip = ColorClip(size=video_size, color=(0, 0, 0))
+ black_clip = black_clip.with_duration(final_clip.duration)
+ black_clip = black_clip.with_opacity(dark_overlay)
+ all_clips.append(black_clip)
+
+ # Add text overlays if provided
+ if text_overlays:
+ current_time = 0
+ for text_item in text_overlays:
+ # Handle both string and tuple inputs
+ if isinstance(text_item, tuple):
+ text, duration = text_item
+ else:
+ text = text_item
+ duration = self._calculate_text_duration(text)
+
+ # Wrap text if needed
+ wrapped_text = self._wrap_text(text)
+ print(f"Adding text: '{wrapped_text}' with duration: {duration:.1f}s")
+
+ # Create text clip with white color and nice font
+ txt_clip = TextClip(text=wrapped_text, font_size=40, color='white',
+ font='Arial Black',method='label', interline=-1) # 'label' method handles multiline text better
+
+ # Center the text
+ txt_clip = txt_clip.with_position(('center', 'center'))
+
+ # Set the duration and start time
+ txt_clip = txt_clip.with_duration(duration)
+ txt_clip = txt_clip.with_start(current_time)
+
+ # Add fade in/out effects
+ txt_clip = txt_clip.with_effects([vfx.FadeIn(0.5), vfx.FadeOut(0.5)])
+
+ all_clips.append(txt_clip)
+ current_time += duration
+
+ # Create final composition with all layers
+ final_clip = CompositeVideoClip(all_clips, size=video_size)
+
+ # Set the audio of the final clip
+ audio_clip = AudioFileClip(self.audio_path).subclipped(0, desired_total_duration)
+ # Apply audio fadeout at the end
+ fade_duration = min(2, audio_clip.duration / 2)
+ audio_clip = audio_clip.with_effects([afx.AudioFadeOut(fade_duration)])
+ final_clip = final_clip.with_audio(audio_clip)
+ final_clip = final_clip.with_duration(desired_total_duration)
+
+ # Write the output video file
+ final_clip.write_videofile(
+ output_path, fps=30, threads=32, audio_codec="aac", preset='ultrafast'
+ )
+ print(f"Output video saved to '{output_path}'")
diff --git a/tools/beatsyncer/beatsyncer_ui.py b/tools/beatsyncer/beatsyncer_ui.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccd24962e7e3fe0669e3446835dd3a6fbaf23794
--- /dev/null
+++ b/tools/beatsyncer/beatsyncer_ui.py
@@ -0,0 +1,151 @@
+import os
+import gradio as gr
+
+import tempfile
+import shutil
+
+from tools.beatsyncer.beatsyncer_tool import BeatSyncer
+
+class BeatsyncerUI:
+ def __init__(self):
+ self.temp_dir = tempfile.mkdtemp()
+ # Use the existing clips directory from the project
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ self.clips_dir = os.path.join(script_dir, 'clips')
+
+ def process_video(
+ self,
+ audio_file,
+ duration_minutes,
+ sensitivity,
+ video_height,
+ dark_overlay,
+ text_input,
+ progress=gr.Progress()
+ ):
+ try:
+ # Save audio file
+ audio_path = os.path.join(self.temp_dir, "input_audio" + os.path.splitext(audio_file.name)[1])
+ shutil.copy(audio_file.name, audio_path)
+
+ # Process text overlays
+ text_overlays = [text.strip() for text in text_input.split('\n') if text.strip()]
+
+ # Calculate video dimensions
+ video_width = int((video_height * 9) / 16) # 16:9 aspect ratio
+
+ # Set output path
+ output_path = os.path.join(self.temp_dir, "output_video.mp4")
+
+ # Convert duration to float if provided
+ if duration_minutes and duration_minutes.strip():
+ duration_minutes = float(duration_minutes)
+ else:
+ duration_minutes = None
+
+ progress(0.1, desc="Initializing BeatSyncer...")
+
+ # Initialize BeatSyncer
+ beat_syncer = BeatSyncer(audio_path)
+
+ progress(0.2, desc="Loading audio...")
+ beat_syncer.load_audio(duration_minutes=duration_minutes)
+
+ progress(0.3, desc="Detecting beat drops...")
+ beat_syncer.detect_beat_drops(sensitivity=sensitivity)
+
+ progress(0.4, desc="Creating video...")
+ beat_syncer.sync_with_media_directory(
+ self.clips_dir, # Using the existing clips directory
+ output_path,
+ duration_minutes=duration_minutes,
+ video_size=(video_width, video_height),
+ dark_overlay=float(dark_overlay),
+ text_overlays=text_overlays
+ )
+
+ progress(1.0, desc="Done!")
+ return output_path
+
+ except Exception as e:
+ raise gr.Error(f"Error: {str(e)}")
+
+ def create_ui(self):
+ with gr.Blocks(theme=gr.themes.Soft()) as app:
+ gr.Markdown("""
+ # 🎵 BeatSyncer - Video Creation Tool
+ Create amazing videos synchronized with music beats!
+
+ Using media files from: {}
+ """.format(self.clips_dir))
+
+ with gr.Row():
+ with gr.Column(scale=2):
+ audio_input = gr.File(
+ label="Upload Audio/Video File",
+ file_types=[".mp3", ".wav", ".m4a", ".aac", ".ogg", ".flac", ".mp4", ".avi", ".mov", ".mkv", ".wmv", ".flv"]
+ )
+
+
+ duration = gr.Textbox(
+ label="Duration (minutes, optional)",
+ placeholder="Leave empty for full audio duration"
+ )
+
+ sensitivity = gr.Radio(
+ choices=["low", "medium", "high"],
+ value="medium",
+ label="Beat Detection Sensitivity"
+ )
+
+ video_height = gr.Slider(
+ minimum=480,
+ maximum=1920,
+ value=720,
+ step=120,
+ label="Video Height (16:9 ratio)"
+ )
+
+ dark_overlay = gr.Slider(
+ minimum=0.0,
+ maximum=1.0,
+ value=0.3,
+ step=0.1,
+ label="Dark Overlay Intensity"
+ )
+
+ text_input = gr.Textbox(
+ label="Text Overlays (one per line)",
+ placeholder="Enter text overlays...",
+ lines=5
+ )
+
+ create_btn = gr.Button("Create Video", variant="primary")
+
+ with gr.Column(scale=1):
+ output_video = gr.Video(
+ label="Output Video",
+ height=640, # Fixed height for preview
+ width=360 # Fixed width for preview
+ )
+
+ create_btn.click(
+ fn=self.process_video,
+ inputs=[
+ audio_input,
+ duration,
+ sensitivity,
+ video_height,
+ dark_overlay,
+ text_input
+ ],
+ outputs=output_video
+ )
+
+ return app
+
+# Create and launch the UI
+if __name__ == "__main__":
+ ui = BeatSyncerUI()
+ demo = ui.create_ui()
+ demo.launch(debug=True, share=False, inbrowser=True)
\ No newline at end of file
diff --git a/tools/beatsyncer/clips/.DS_Store b/tools/beatsyncer/clips/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..0933e2c2a5349beb17e6a16b405e85127aa44dde
Binary files /dev/null and b/tools/beatsyncer/clips/.DS_Store differ
diff --git a/tools/beatsyncer/clips/1657591309536.mp4 b/tools/beatsyncer/clips/1657591309536.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..c45cfaa6d1c6954873fc06c00de947a4075637c8
--- /dev/null
+++ b/tools/beatsyncer/clips/1657591309536.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:69aa9858fca54bda6ac78abe43cf4bd3081ef0d6a69e433603cfe3995219c97c
+size 7222278
diff --git a/tools/beatsyncer/clips/1657591680538.mp4 b/tools/beatsyncer/clips/1657591680538.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..193b20b825fdb58a7a7436a25ae5245d0c7ebf7b
--- /dev/null
+++ b/tools/beatsyncer/clips/1657591680538.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c531723d9ba697e7453cbc61d772e76957fca7ae83f2ecacaae4b18dd770d7d4
+size 40783923
diff --git a/tools/beatsyncer/clips/1657591832727.mp4 b/tools/beatsyncer/clips/1657591832727.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..45634ee79eb4723124842f4c3b262b3bc665ebb3
--- /dev/null
+++ b/tools/beatsyncer/clips/1657591832727.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:37be14aab321518e220315f3673dc63113b9a601ae96d246d614b36ea9993a63
+size 19891527
diff --git a/tools/beatsyncer/clips/1657616409629.mp4 b/tools/beatsyncer/clips/1657616409629.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..23e2847759c9ccd891e6d7407d0d19e97dc96822
--- /dev/null
+++ b/tools/beatsyncer/clips/1657616409629.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:30401ebf2010942c79f16f39dab912ba195c606959bab9500c52dcfa60039aca
+size 10948753
diff --git a/tools/beatsyncer/clips/1657616591950.mp4 b/tools/beatsyncer/clips/1657616591950.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..c1458783f1ecf09c4a95a7d9717272d4fb107ad6
--- /dev/null
+++ b/tools/beatsyncer/clips/1657616591950.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:863652f4a1abe5d0d6ee5d0cca92cac3aab6deb7b7d900fa1dfb06289698a274
+size 4494706
diff --git a/tools/beatsyncer/clips/1657616660848.mp4 b/tools/beatsyncer/clips/1657616660848.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..b16ebd918bf58b01a3953d87552f2bfd16165a37
--- /dev/null
+++ b/tools/beatsyncer/clips/1657616660848.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:09002494bd0c33f02fffecc9ec484da2630dac91399b2ee1adfa22433757b05d
+size 8450597
diff --git a/tools/beatsyncer/clips/1657616730783.mp4 b/tools/beatsyncer/clips/1657616730783.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..7968b8b208853b2939bf7602029eb91088c42447
--- /dev/null
+++ b/tools/beatsyncer/clips/1657616730783.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1aad238b7f6623ad10af2413ece4b65113ebae42bb87e41cc39d8276864a7c67
+size 6972420
diff --git a/tools/beatsyncer/clips/1657616788731.mp4 b/tools/beatsyncer/clips/1657616788731.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..1d7b8155e88f296ffa104f016f58b987bf837bcc
--- /dev/null
+++ b/tools/beatsyncer/clips/1657616788731.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ca25db432389307b40cd11a945d0b19ad0f72e9e7b9d34b2d8e37565e39beb3d
+size 7192248
diff --git a/tools/beatsyncer/clips/1657616954951.mp4 b/tools/beatsyncer/clips/1657616954951.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..08b2065e67638082a7b4a2a2bf2cd8af18b98133
--- /dev/null
+++ b/tools/beatsyncer/clips/1657616954951.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5021eb2d204cdc23fb8e938d0de0d6baa99ac1639726e2b0e3fe2e1e6566a7c3
+size 9827398
diff --git a/tools/beatsyncer/clips/1657923632784.mp4 b/tools/beatsyncer/clips/1657923632784.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..48935b359fe295cf62d62cbb535bc2fe9dec0472
--- /dev/null
+++ b/tools/beatsyncer/clips/1657923632784.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5837c302c5fe15878b068e22a0ebb1a2fedb747d226ea81a9ba0631fca898540
+size 8853773
diff --git a/tools/beatsyncer/clips/288298605_1687187951639494_579221224540621563_n.mp4 b/tools/beatsyncer/clips/288298605_1687187951639494_579221224540621563_n.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..7727d24bc822bef81ceac6371b39dddc3d9f742f
--- /dev/null
+++ b/tools/beatsyncer/clips/288298605_1687187951639494_579221224540621563_n.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5277a9bc08a20012285e5c438d043047802f525f82583e5c2d4106ec30da8cc3
+size 2767934
diff --git a/tools/beatsyncer/clips/289438954_5129286887125053_220682033208031067_n.mp4 b/tools/beatsyncer/clips/289438954_5129286887125053_220682033208031067_n.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..df3d00aeb0b7cfbaddf6258c98e9ad96b475f490
--- /dev/null
+++ b/tools/beatsyncer/clips/289438954_5129286887125053_220682033208031067_n.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ba5f5469980c5df7e4910122e0cf818bd90fe98e3743650ecf189c706230009c
+size 6625183
diff --git a/tools/beatsyncer/clips/289568281_1462345327513048_3022742540007505592_n.mp4 b/tools/beatsyncer/clips/289568281_1462345327513048_3022742540007505592_n.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..dc9d994daf7961784f7c1fc67022b1dd3900bb8e
--- /dev/null
+++ b/tools/beatsyncer/clips/289568281_1462345327513048_3022742540007505592_n.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4801482e63465e85a02fd9a7eacbbe0268954bf9674f55a10aa8c80c9e2753f0
+size 1618834
diff --git a/tools/beatsyncer/clips/290154393_565236208447925_526246679178540543_n.mp4 b/tools/beatsyncer/clips/290154393_565236208447925_526246679178540543_n.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..4a135e2488cfab2332a86fb219ea33edf29987e2
--- /dev/null
+++ b/tools/beatsyncer/clips/290154393_565236208447925_526246679178540543_n.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:671438acc46a10e657388d4c13d34dbecf5af5c6f9ae915b24caec154bf3390a
+size 3465572
diff --git a/tools/beatsyncer/clips/290702607_425330016154178_254797435320646581_n.mp4 b/tools/beatsyncer/clips/290702607_425330016154178_254797435320646581_n.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..f770383e335645faea19decf0d04d656828e0b7d
--- /dev/null
+++ b/tools/beatsyncer/clips/290702607_425330016154178_254797435320646581_n.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:99d8e8e4ee7391d0dc4d3223e5d5d203cea89089fc507862718fdd203c5234f8
+size 3268380
diff --git a/tools/beatsyncer/clips/290990526_409256130973654_5788653198617875871_n.mp4 b/tools/beatsyncer/clips/290990526_409256130973654_5788653198617875871_n.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..eee41e5dd1fb123c60602b95289e514a0e9d07d6
--- /dev/null
+++ b/tools/beatsyncer/clips/290990526_409256130973654_5788653198617875871_n.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:191b27a28f64889c201c22daf0bc48ade99d9902b442c3a2d4e8e1ccaebb5060
+size 2297732
diff --git a/tools/beatsyncer/clips/291032176_120067890734229_1814549935092808193_n.mp4 b/tools/beatsyncer/clips/291032176_120067890734229_1814549935092808193_n.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..3288a956fe877523d833754cbc5584629fe4c9e5
--- /dev/null
+++ b/tools/beatsyncer/clips/291032176_120067890734229_1814549935092808193_n.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c69c8c738b98305e0b40fbd63ddb6133b886eaf279e8894e232252dad075fc43
+size 3250400
diff --git a/tools/beatsyncer/clips/657591201007.mp4 b/tools/beatsyncer/clips/657591201007.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..b09e942a6aca19e79c184e49186dc8f9f051f491
--- /dev/null
+++ b/tools/beatsyncer/clips/657591201007.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef0f3447388144458427572b64c768ec09cc88b87635fbece3192cc2eb973975
+size 11149649
diff --git a/tools/beatsyncer/clips/657591253340.mp4 b/tools/beatsyncer/clips/657591253340.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..fcdef13fcca2ba53a4b91bfa4c4bdee76930c286
--- /dev/null
+++ b/tools/beatsyncer/clips/657591253340.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b1d3c5bd6614ea8e72703a4f94e7590c29a792b9eb0f22504b0f5bb663efbdbb
+size 6907398
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_001.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_001.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..6f678c6be9e80825fca325a7f2e470131f9ca0b0
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_001.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9340f70c21ebebfa4230c3891198b278b849a8a3f788bf7f7edd36285f89e830
+size 2103745
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_002.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_002.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..0cde633fe1ecf575b564a47238d99b429047860e
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_002.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b2fff2e4e65479a29a450cdcb340c78564f5794167ee03352dcdc2d8fe856930
+size 155997
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_003.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_003.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..9cd4d7a08cc839a2f90677dc6684095922da244b
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_003.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:762d0ffc6a657e3c5b1c190190a9b6a3d3ea7a9979239db6de2532658a1dd0b9
+size 350743
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_004.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_004.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..59859b022c4d0efdc4aa585a886461aca970c4f7
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_004.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0365717355f8e0c8cb39dedbbba0553c1e6132fd2e793972336c2581933eb4d0
+size 563440
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_005.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_005.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..e6f838608c0da3c6e0f353a6c9faeb73885431db
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_005.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bf5ba4edc9c23e961ef5d3379790b0e32b573b314fee099de74b3b14b841a3d1
+size 145942
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_006.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_006.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..f9ccd1280eec4a3b64934a500a5c9528cfa86363
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_006.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:174b2c1e98000a026427acde3bdde0914f60ecabfbab8ac93be5dd81a4cf3bc8
+size 282620
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_007.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_007.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..a951bf465d45d507db55d7e9615f6f3b9a8fe91b
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_007.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f56a247020f35d3ce480f106605fdd704cfddb7513f0b52d7f63646c6e5be4d9
+size 353519
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_008.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_008.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..1283974fe3e90e53e8e459b54b94852a9420a678
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_008.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:711df5b60e0ced8b818a49f32e6ca516193346a3b4ce63bb2188d3802f62aace
+size 343334
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_009.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_009.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..4b302b8307affc7d776b3dc8f042791c3b7c5c53
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_009.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ab88f7973f2fee383ba852f55d46912aaee7ad849a21a46cdaedb8e957046c6f
+size 204924
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_010.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_010.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..e8f7cdbf945bf4ff508c0f2582244a5dc0f0dead
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4333A1E1AE5B90482FB1125369C59B_video_dashinit_scene_010.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5ed2910d6e2e6b4159ba3290053526fa12573082a18dddd38f3d2eebc50ce15f
+size 144843
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_001.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_001.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..4ead1ba0979abddb704ca6ffce2db07f62304936
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_001.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b73792168a06223614b33179844a458bd8389f426980414f47ea0389f57cba2
+size 1138637
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_002.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_002.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..4e74dd50e8ae6229f617ef640fe247c659d9d739
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_002.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:85bcdde3e764e65ca3260b20c17918581dfce3acec4eb04e4813a2ac53e16587
+size 566912
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_003.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_003.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..79b5800d124d5aa74d18756c0c21e082a252fd88
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_003.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:baa62fca4618f79cb7be3b2158509fc97f2f9e7f82991300b6e19b42df217a4d
+size 544288
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_004.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_004.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..406a73841fd5b1f9fe8c660743b50405b5225681
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_004.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:102282c000ac3021d2c6749d0164663e936a6e07655d56c1850015fdb148a292
+size 915749
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_005.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_005.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..d5e2e4c8dad5b38730a8a330dc0ba02dc7b0218d
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_005.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:677c65cb2eb4dc4bcbab485c0d9c21eec605fcb81ece23fc93545ed495180c79
+size 1653275
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_006.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_006.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..ef2a4d34037723849bab9809fb0a1a8f551b4719
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_006.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f8297e4e79175c5029cb0021039466d6368f30d1d4409106e3acfd8a953e5c58
+size 820349
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_007.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_007.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..6f24bf80817a72aba8261d68887a789cccd5c4f1
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_2B4E29561A51827AF04D3C92BDED06B5_video_dashinit_scene_007.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dc97a361411f3180cb4589903566191e0ac3140856101b51beef3f71b1fec858
+size 813675
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_001.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_001.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..7c4d93a169291ecbf17dab826ad4e5821abc01a6
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_001.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f4f03c0afab306a75821fe4813057e29096f7675ec996cf4183331b1349e746
+size 324818
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_002.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_002.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..dcbf346ac8bdea03942797459ca30725ddaf7659
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_002.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b1a7875fc230877dc96276d2d7657133ab425b9528f364bbe146922301e8191d
+size 730156
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_003.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_003.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..8b4a5b2c4a3e84562babf86c4b34f58a7816973b
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_003.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9f1dc7bb83d826085cdd4d2ab288f05d4f0e1bedb198b1a1a822dd77563180d8
+size 439700
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_004.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_004.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..7e814745dc727cd9c420c41c0ff29bb4ef350af1
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_004.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b6f4281fed8eb79ec922038da4544ce46be6cc360703b2383f2cbfc88345a3f3
+size 302739
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_005.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_005.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..800d7a56b24efdadc6406740463f1f960b4794fc
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_005.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c106b4019465de406077d42afeb010aa1de43fcb3603707b66cd59b9f8945f6b
+size 561379
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_006.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_006.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..8608f5430aa5ac1b08b140220c73f696c8f31174
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_3547B2AC7669AF7E434D64AFDC9A6C9F_video_dashinit_scene_006.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4d3c5ace6a08ea56c81716b6f30286124d683eb6c3b39d0d6398c3d8d094d991
+size 382204
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_001.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_001.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..6d096e441eaacb0e454968902113c098470db7d1
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_001.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:744efec1722396dc5143dbf7c0bcf82c6a0ba9e84856a23f805e3714435bc92a
+size 3038574
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_002.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_002.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..fcd6142c3ef88e37083cfc4016ac033bda1bc8f4
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_6F4E14515715C2DB44E4959237066298_video_dashinit_scene_002.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:28aa5290f27ed1932b6a38b6d724c2ba4f026c3ac65d05dd012e5f2b52d66809
+size 2173169
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_7944DC0BB36028CA2D36023098F3029C_video_dashinit.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_7944DC0BB36028CA2D36023098F3029C_video_dashinit.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..97be4c6e0f5e8c45b973c73b9fa063bfecb37b50
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_7944DC0BB36028CA2D36023098F3029C_video_dashinit.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:746b1ad01affae459d6aac7fefa1088fb7d7a21d5d9e9b4a04c7731b781e9bfc
+size 4166090
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_001.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_001.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..83af318ce009ec8673dba5c6b05671c5d57030ad
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_001.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0a2b8e7985767b31c524c994fd9c9fd33051157b00c9aab3cb2a397592bd61f9
+size 485941
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_002.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_002.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..718be4014f5093f68d667416d1328cb5afde54d4
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_002.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a746a0cd1404a122c5bf3ddddff42d4eb53fec8ec4de924ec9a01338a34e8e6e
+size 489915
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_003.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_003.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..94df5adca967b27f57756a78d8fb60cef1feed19
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_003.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:db938b641d85013311027904c60c677304c97d8068e57ccd7dc4c16d1a835c5b
+size 418039
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_004.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_004.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..64cd1c9d5511ee5b4e160aad5f225a0837a1636e
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_004.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:98ef580dbb51cc18696454c3085a23e92c0d9d81bd952bf9c8fa702fcae0e9a4
+size 178086
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_005.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_005.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..ab1dd724e329879ce074d5deb23f240f1bfd85b4
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_005.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5280d3fabab8f340dfbec7a6a38f1ef935729f4bcf487a9d1c9962bd59da0e8b
+size 456687
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_006.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_006.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..88601696d7d6117509b86f1996117f59e1d39448
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_006.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:938cce3a14599b31f019392b60cef471a0772976ac9ad0e8f1ba99e170b5b79a
+size 272429
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_007.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_007.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..9b4d95b523d5d07c974029cbaa252b0d096a3aca
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_007.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2de0949df664c4c620df3b104303683e36ddd020ba5dde44c4a137021ae18345
+size 395515
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_008.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_008.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..4eee0a00d43b2957ecfa6689f9e57021ba02c9c2
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_008.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:571da24fa531e235fb6a5165db7c62f88c326cc2c2e74e1b6a84d989aa95791d
+size 190197
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_009.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_009.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..bc12c0e4640edd52ff1c02032d6ae8584db4e9ba
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_009.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:20b73a994dd292b3ca6b3ac3e9a6b33d749ae707a36c21ef0b0b4041545eb076
+size 298354
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_010.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_010.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..887811d582e81468591ff6f6e9e9f8d68cd98f93
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_010.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ae135ac6c0e734f8ea3d3164ba5ef89f8c282ff1e9661b8f376a9b9eb52925b9
+size 616945
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_011.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_011.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..c5457e78ecbd772892b608dc9046be1f65f13da4
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_011.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84926ba00331659433a0d58d7f5e845111767d646e1ae3f092769d55018202cf
+size 329815
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_012.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_012.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..45999d00a21191ef025d14b28c69658036f7a154
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_012.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cda09142f9fdc01c204068726f46b42e6e3c470e798bc031af412a78a484fd8f
+size 316734
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_013.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_013.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..d5ee91ef606edc7aeeee61c9f802d2c4b966ffa4
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_013.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:37952ba40f722803f050c5f1439fc726c758fbc17cd517e635d3edc76460e915
+size 414335
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_014.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_014.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..a318303a2b6374f0769b37a86f6294711c0df3fe
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_014.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a54dbb901ce3fa5a2a185c78bcf1e50dcd99d12a318931464394909dea4c17c1
+size 399673
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_015.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_015.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..e5ae76c278703cea5f160dc13c719ee57d48894c
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_015.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:283836d35460ca1fd3df50f11accbeb6a704c1903e58bbd3ce3ac78a7eaf2d58
+size 250265
diff --git a/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_016.mp4 b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_016.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..92572ab0ebd95c3c50ab189f8b2abb58dc82193d
--- /dev/null
+++ b/tools/beatsyncer/clips/Snapinsta.app_video_E544C6323B8E4CAB778F010CB59E0D88_video_dashinit_scene_016.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3aa7163a6fc8bef96ad94d92ff68d40a94ae3314d1614517a2193408b6184bfc
+size 512814
diff --git a/tools/beatsyncer/clips/ai-generated-8079926_1280.png b/tools/beatsyncer/clips/ai-generated-8079926_1280.png
new file mode 100644
index 0000000000000000000000000000000000000000..0b8b78978ee4d1bc939c68ee80c9d5db3f21b198
--- /dev/null
+++ b/tools/beatsyncer/clips/ai-generated-8079926_1280.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:17833b1d83e2e696579216bc9f8b9c2fac95faf0575b340416ead9e266dcfa91
+size 1961454
diff --git a/tools/beatsyncer/clips/ai-generated-8601539_1280.jpg b/tools/beatsyncer/clips/ai-generated-8601539_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..50ecec428cfd4b631466171c563a50f6291dfebe
--- /dev/null
+++ b/tools/beatsyncer/clips/ai-generated-8601539_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3906d8cf7e9550fbd3b05c68fe765f27a8bfcceabad9331be437c4fdedbe1797
+size 216003
diff --git a/tools/beatsyncer/clips/ai-generated-9151678_1280.jpg b/tools/beatsyncer/clips/ai-generated-9151678_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..4fd05ffa33baa906a078510b0bc421dda7582ac3
--- /dev/null
+++ b/tools/beatsyncer/clips/ai-generated-9151678_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:603dd2be0b27599561ed4800cfd847b27d0a66b25bbc4b0a5c9016dacdcf65a1
+size 137560
diff --git a/tools/beatsyncer/clips/antique-car-7577214_1280.jpg b/tools/beatsyncer/clips/antique-car-7577214_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..cba4a4ee1491d4b7b24dfc3f0cf78507ddac98b3
--- /dev/null
+++ b/tools/beatsyncer/clips/antique-car-7577214_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:090d0179cbb679234bce2e1bd521fc8c7466a991a794a3cec200b51041e777af
+size 167880
diff --git a/tools/beatsyncer/clips/automobile-1850862_1280.jpg b/tools/beatsyncer/clips/automobile-1850862_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..6bf84daadfa52d7ed96cc77b59bfbdd2c76175cd
--- /dev/null
+++ b/tools/beatsyncer/clips/automobile-1850862_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7de07654b576da6f108b950af43322bd47588f507d5186b3f49efbe572ba7296
+size 333222
diff --git a/tools/beatsyncer/clips/bmw-6236208_1280.jpg b/tools/beatsyncer/clips/bmw-6236208_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..38034616d1012662626e6a5170b58f58d1427228
--- /dev/null
+++ b/tools/beatsyncer/clips/bmw-6236208_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bac47669253ed1ad483da83dd8af68626351aab886f0f7e5a089be11c011111b
+size 159486
diff --git a/tools/beatsyncer/clips/car-171415_1280.jpg b/tools/beatsyncer/clips/car-171415_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..64a218bb0a5680285b61cc2c06006aa53d26c8ff
--- /dev/null
+++ b/tools/beatsyncer/clips/car-171415_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:393d00a235e920e0438e4c898f95b4fcdcf7b2fee91eeb79e97958c4b1a368b6
+size 251656
diff --git a/tools/beatsyncer/clips/car-219813_1280.jpg b/tools/beatsyncer/clips/car-219813_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..35a6b9873e63fd879e9febebaef75585e2be47a9
--- /dev/null
+++ b/tools/beatsyncer/clips/car-219813_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:df1590bd4f0be0b115dd8ac7fab20c5e419dbe54488bc6a043e1996a513e6d17
+size 296910
diff --git a/tools/beatsyncer/clips/car-5548243_1280.jpg b/tools/beatsyncer/clips/car-5548243_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d344dc9c67201d0fd35847c0ebf1ceefa9f8ac76
--- /dev/null
+++ b/tools/beatsyncer/clips/car-5548243_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2e235172f175245abb0c1230635f6087d89c8ef3b7a6a864cc8f7a66d4897b4e
+size 184266
diff --git a/tools/beatsyncer/clips/car-5548244_1280.jpg b/tools/beatsyncer/clips/car-5548244_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..dc1d55bb327564488890bfd0e903444bc14da257
--- /dev/null
+++ b/tools/beatsyncer/clips/car-5548244_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:82ab09060e1f89241514e65fab9ce7fcc4e04896b4dc3189cec0e912d4f3aa5f
+size 174652
diff --git a/tools/beatsyncer/clips/car-6306695_1280.jpg b/tools/beatsyncer/clips/car-6306695_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..07fac9c2aa50635f6311464b6ce08567d623ae42
--- /dev/null
+++ b/tools/beatsyncer/clips/car-6306695_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b7189b1652e1ae61c159980e0afdaf5bc4081e52ee1c69a28f6324ef6dc5dc79
+size 350032
diff --git a/tools/beatsyncer/clips/car-6483720_1280.jpg b/tools/beatsyncer/clips/car-6483720_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..6da63490218334e5f14d88ee1bf01ffac79487ea
--- /dev/null
+++ b/tools/beatsyncer/clips/car-6483720_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4362ec001e7980d4bda7648187337321803667fb58f22bd7e0f55f8b546bc948
+size 378868
diff --git a/tools/beatsyncer/clips/car-6589240_1280.jpg b/tools/beatsyncer/clips/car-6589240_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9480b7333edc811d6f2b6c64f88f4861d88572bf
--- /dev/null
+++ b/tools/beatsyncer/clips/car-6589240_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8a0a61b9220b07d35ba65322a7e198c7d939bfdfaf67ccf3ac5a4c7da1c17367
+size 303340
diff --git a/tools/beatsyncer/clips/car-6658749_1280.jpg b/tools/beatsyncer/clips/car-6658749_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..2a49a2e350f3f15bcf2bd266d8d70bc972bc79b0
--- /dev/null
+++ b/tools/beatsyncer/clips/car-6658749_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5271536a3da5173f6110b0af11250bfc6ae827d4cfe6397d96b7002198c179d0
+size 311605
diff --git a/tools/beatsyncer/clips/car-7139944_1280.jpg b/tools/beatsyncer/clips/car-7139944_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9a0e0bbfc3b86687fc0ea1e0c9f393fd6f5f03ff
--- /dev/null
+++ b/tools/beatsyncer/clips/car-7139944_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3602f55c14ff323ecd8b53d903cbfb9900932e70cd5f7511506405be32ec98dc
+size 243122
diff --git a/tools/beatsyncer/clips/car-7203265_1280(1).jpg b/tools/beatsyncer/clips/car-7203265_1280(1).jpg
new file mode 100644
index 0000000000000000000000000000000000000000..1e45abb631974c67bf21cdac042b1588abf2317a
--- /dev/null
+++ b/tools/beatsyncer/clips/car-7203265_1280(1).jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b90ff21aac0e87ee4e68fdce47d3e383105128bbfa134fe60a11cb4a75b0d9c
+size 136040
diff --git a/tools/beatsyncer/clips/car-7203265_1280.jpg b/tools/beatsyncer/clips/car-7203265_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..1e45abb631974c67bf21cdac042b1588abf2317a
--- /dev/null
+++ b/tools/beatsyncer/clips/car-7203265_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b90ff21aac0e87ee4e68fdce47d3e383105128bbfa134fe60a11cb4a75b0d9c
+size 136040
diff --git a/tools/beatsyncer/clips/car-7635511_1280.jpg b/tools/beatsyncer/clips/car-7635511_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..88f71dae0d5134e5d7e33807502d6ca61e5e902d
Binary files /dev/null and b/tools/beatsyncer/clips/car-7635511_1280.jpg differ
diff --git a/tools/beatsyncer/clips/car-7725834_1280.jpg b/tools/beatsyncer/clips/car-7725834_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..c732701b0bc8df21ee706d3c7f1d88a81d99fd5e
--- /dev/null
+++ b/tools/beatsyncer/clips/car-7725834_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d362cf273be65e838ecc50eafe7653628f2508179afe3bc96f5b95c5305776cb
+size 226572
diff --git a/tools/beatsyncer/clips/car-7830737_1280.jpg b/tools/beatsyncer/clips/car-7830737_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..4780022581f7ced16c75af319fbab1f6fdd3665d
--- /dev/null
+++ b/tools/beatsyncer/clips/car-7830737_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:071455b19109f32af2f6f942ead5bd3949664c6d918d4d2821f2de40c1044347
+size 160709
diff --git a/tools/beatsyncer/clips/car-8141820_1280.jpg b/tools/beatsyncer/clips/car-8141820_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..47ccd2fbe6734a178d5c198074ece2aa49a483ca
--- /dev/null
+++ b/tools/beatsyncer/clips/car-8141820_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3e186cab540d27d66a31017dd7c9390e4145d74cc178610c318a475e956c3d61
+size 251941
diff --git a/tools/beatsyncer/clips/fashion-1399346_1280.jpg b/tools/beatsyncer/clips/fashion-1399346_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..42e01cbf458799aaa448ba783f6d401b8087e8f7
--- /dev/null
+++ b/tools/beatsyncer/clips/fashion-1399346_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f4b0963f51cd7760cdbca11ac6013b626c077fdc0fbaa8fa0a3f0d0c155e5d04
+size 294769
diff --git a/tools/beatsyncer/clips/mercedes-benz-7383931_1280.jpg b/tools/beatsyncer/clips/mercedes-benz-7383931_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..05ba148e7b782ba90ac0e8e3070da181984fb274
--- /dev/null
+++ b/tools/beatsyncer/clips/mercedes-benz-7383931_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:861b90101d7287e06fbcf241d2da21081df57bfbda76257a39fda2414bf30968
+size 255212
diff --git a/tools/beatsyncer/clips/speedy-car-8391554_1280.jpg b/tools/beatsyncer/clips/speedy-car-8391554_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..ae4bfc026629374bb912ecf9cb1f7f093fdf5f50
--- /dev/null
+++ b/tools/beatsyncer/clips/speedy-car-8391554_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e39c754e0e77354d74d935f50247f082b901d6b14b1a74634ccc9a7d7af94a3f
+size 185268
diff --git a/tools/beatsyncer/clips/sport-4155825_1280.jpg b/tools/beatsyncer/clips/sport-4155825_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9e84558e3772b4d47bc7f041396e49e8ccb3638a
--- /dev/null
+++ b/tools/beatsyncer/clips/sport-4155825_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7203fbc33cd3a72c032380d1780bdef5379c47738097a4ce767c8a887fd9fd7a
+size 271345
diff --git a/tools/beatsyncer/clips/vintage-car-7300881_1280.jpg b/tools/beatsyncer/clips/vintage-car-7300881_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..1cea38a7af6b29b83742049e2a6492cc6955f7e1
--- /dev/null
+++ b/tools/beatsyncer/clips/vintage-car-7300881_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3557b663e9c98f3241f9ae52afdd5baa93bfdc49714dd8391a1045a78129633f
+size 325573
diff --git a/tools/beatsyncer/clips/vintage-rolls-royce-7414656_1280.jpg b/tools/beatsyncer/clips/vintage-rolls-royce-7414656_1280.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..508d6d6a02239b1f14ac282119a978ecb3f22f94
--- /dev/null
+++ b/tools/beatsyncer/clips/vintage-rolls-royce-7414656_1280.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:454595c576969698435dbdb0daafde92ec7c28339d5c124fedd18dafd2805bf8
+size 170516
diff --git a/tools/beatsyncer/tmp/input_audio.mp3 b/tools/beatsyncer/tmp/input_audio.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..0de13507dc5ffc3a53a4484faf0733964d8a505e
--- /dev/null
+++ b/tools/beatsyncer/tmp/input_audio.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3e8e47b4ac39e602e39d2a3c38d4509d11a113f742e0fbe594810468c0507577
+size 406090
diff --git a/tools/image_background_remover/__init__.py b/tools/image_background_remover/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4dd1cb1145c8a4910c8b71990bf16ef93863b4ac
--- /dev/null
+++ b/tools/image_background_remover/__init__.py
@@ -0,0 +1 @@
+# Image Background Remover package
diff --git a/tools/image_background_remover/__pycache__/__init__.cpython-311.pyc b/tools/image_background_remover/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0f7d0aba83c085f95624aceb00aad99b237fda7f
Binary files /dev/null and b/tools/image_background_remover/__pycache__/__init__.cpython-311.pyc differ
diff --git a/tools/image_background_remover/__pycache__/__init__.cpython-312.pyc b/tools/image_background_remover/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6ced405ddd57b0549092a6bdcf0057893c0ff76d
Binary files /dev/null and b/tools/image_background_remover/__pycache__/__init__.cpython-312.pyc differ
diff --git a/tools/image_background_remover/__pycache__/image_background_remover_tool.cpython-311.pyc b/tools/image_background_remover/__pycache__/image_background_remover_tool.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c1dd7544719de8a12b24ba3467e51ef4b01c3848
Binary files /dev/null and b/tools/image_background_remover/__pycache__/image_background_remover_tool.cpython-311.pyc differ
diff --git a/tools/image_background_remover/__pycache__/image_background_remover_tool.cpython-312.pyc b/tools/image_background_remover/__pycache__/image_background_remover_tool.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3ae18ddac61f09e3443b4e8ef91ac0eadd8765f3
Binary files /dev/null and b/tools/image_background_remover/__pycache__/image_background_remover_tool.cpython-312.pyc differ
diff --git a/tools/image_background_remover/__pycache__/image_background_remover_ui.cpython-311.pyc b/tools/image_background_remover/__pycache__/image_background_remover_ui.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3e0579b9a83ae7ec3f4e2845d6ae7e03de7aee4b
Binary files /dev/null and b/tools/image_background_remover/__pycache__/image_background_remover_ui.cpython-311.pyc differ
diff --git a/tools/image_background_remover/__pycache__/image_background_remover_ui.cpython-312.pyc b/tools/image_background_remover/__pycache__/image_background_remover_ui.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..94bf1abd4dc745a347de4196bf7a305f9176dd53
Binary files /dev/null and b/tools/image_background_remover/__pycache__/image_background_remover_ui.cpython-312.pyc differ
diff --git a/tools/image_background_remover/image_background_remover_tool.py b/tools/image_background_remover/image_background_remover_tool.py
new file mode 100644
index 0000000000000000000000000000000000000000..1981cf203ccba5ac0dec859b260930d5cb583e92
--- /dev/null
+++ b/tools/image_background_remover/image_background_remover_tool.py
@@ -0,0 +1,45 @@
+# Image Background Remover Tool
+# Version: 1.0.0
+# Author: Isaac
+
+from rembg import remove
+from PIL import Image
+import os
+import tempfile
+
+class ImageBackgroundRemover:
+ """
+ A tool that removes the background from images using the rembg library.
+ """
+
+ def __init__(self):
+ self.name = "Image Background Remover"
+ self.version = "1.0.0"
+
+ def process(self, input_image):
+ """
+ Process the input image and remove its background.
+
+ Args:
+ input_image: The input image file
+
+ Returns:
+ Path to the processed image with background removed
+ """
+ try:
+ # Create a temporary directory to store the output
+ temp_dir = tempfile.mkdtemp()
+ output_path = os.path.join(temp_dir, "output.png")
+
+ # Open the input image
+ img = Image.open(input_image)
+
+ # Remove the background
+ output = remove(img)
+
+ # Save the result
+ output.save(output_path)
+
+ return output_path
+ except Exception as e:
+ raise Exception(f"Failed to process image: {str(e)}")
diff --git a/tools/image_background_remover/image_background_remover_ui.py b/tools/image_background_remover/image_background_remover_ui.py
new file mode 100644
index 0000000000000000000000000000000000000000..8fb770fd7d90a0bf0d702592209f8770897d9ae7
--- /dev/null
+++ b/tools/image_background_remover/image_background_remover_ui.py
@@ -0,0 +1,55 @@
+# Image Background Remover UI
+# Version: 1.0.0
+# Author: Isaac
+
+import gradio as gr
+from tools.image_background_remover.image_background_remover_tool import ImageBackgroundRemover
+
+class ImageBackgroundRemoverUI:
+ """
+ UI for the Image Background Remover tool.
+ """
+
+ def __init__(self):
+ self.name = "Image Background Remover"
+ self.tool = ImageBackgroundRemover()
+
+ def process_input(self, input_image):
+ """Process the input image and return the result"""
+ try:
+ result = self.tool.process(input_image)
+ return result
+ except Exception as e:
+ return None
+
+ def create_ui(self):
+ """Create the Gradio UI for this tool"""
+ with gr.Column():
+ gr.Markdown(f"# {self.name}")
+ gr.Markdown("Upload an image to remove its background")
+
+ input_image = gr.Image(type="filepath", label="Input Image")
+ process_btn = gr.Button("Remove Background", variant="primary")
+ output_image = gr.Image(label="Result")
+
+ process_btn.click(
+ fn=self.process_input,
+ inputs=[input_image],
+ outputs=[output_image]
+ )
+
+def run_app():
+ """Run the Gradio app"""
+ ui = ImageBackgroundRemoverUI()
+ app = gr.Interface(
+ fn=ui.process_input,
+ inputs=gr.Image(type="filepath", label="Input Image"),
+ outputs=gr.Image(label="Result"),
+ title=ui.name,
+ description="Upload an image to remove its background",
+ examples=["examples/sample1.jpg", "examples/sample2.jpg"] if gr.os.path.exists("examples") else None,
+ )
+ app.launch()
+
+if __name__ == "__main__":
+ run_app()