feat: Twelve Labs Bundle (#7837)
* feat: add twelve labs components * fix: fix uv.lock for twelve labs components
This commit is contained in:
parent
63e70a54e9
commit
327c0fd791
17 changed files with 4038 additions and 1715 deletions
|
|
@ -123,6 +123,7 @@ dependencies = [
|
|||
"langchain-ibm>=0.3.8",
|
||||
"opik>=1.6.3",
|
||||
"openai>=1.68.2",
|
||||
"twelvelabs>=0.4.7",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
|
|
|||
17
src/backend/base/langflow/components/twelvelabs/__init__.py
Normal file
17
src/backend/base/langflow/components/twelvelabs/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from .convert_astra_results import ConvertAstraToTwelveLabs
|
||||
from .pegasus_index import PegasusIndexVideo
|
||||
from .split_video import SplitVideoComponent
|
||||
from .text_embeddings import TwelveLabsTextEmbeddingsComponent
|
||||
from .twelvelabs_pegasus import TwelveLabsPegasus
|
||||
from .video_embeddings import TwelveLabsVideoEmbeddingsComponent
|
||||
from .video_file import VideoFileComponent
|
||||
|
||||
__all__ = [
|
||||
"ConvertAstraToTwelveLabs",
|
||||
"PegasusIndexVideo",
|
||||
"SplitVideoComponent",
|
||||
"TwelveLabsPegasus",
|
||||
"TwelveLabsTextEmbeddingsComponent",
|
||||
"TwelveLabsVideoEmbeddingsComponent",
|
||||
"VideoFileComponent",
|
||||
]
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
from typing import Any
|
||||
|
||||
from langflow.custom import Component
|
||||
from langflow.io import HandleInput, Output
|
||||
from langflow.schema import Data
|
||||
from langflow.schema.message import Message
|
||||
|
||||
|
||||
class ConvertAstraToTwelveLabs(Component):
|
||||
"""Convert AstraDB search results to TwelveLabs Pegasus inputs."""
|
||||
|
||||
display_name = "Convert AstraDB to Pegasus Input"
|
||||
description = "Converts AstraDB search results to inputs compatible with TwelveLabs Pegasus."
|
||||
icon = "TwelveLabs"
|
||||
name = "ConvertAstraToTwelveLabs"
|
||||
documentation = "https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md"
|
||||
|
||||
inputs = [
|
||||
HandleInput(
|
||||
name="astra_results",
|
||||
display_name="AstraDB Results",
|
||||
input_types=["Data"],
|
||||
info="Search results from AstraDB component",
|
||||
required=True,
|
||||
is_list=True,
|
||||
)
|
||||
]
|
||||
|
||||
outputs = [
|
||||
Output(
|
||||
name="index_id",
|
||||
display_name="Index ID",
|
||||
type_=Message,
|
||||
method="get_index_id",
|
||||
),
|
||||
Output(
|
||||
name="video_id",
|
||||
display_name="Video ID",
|
||||
type_=Message,
|
||||
method="get_video_id",
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._video_id = None
|
||||
self._index_id = None
|
||||
|
||||
def build(self, **kwargs: Any) -> None: # noqa: ARG002 - Required for parent class compatibility
|
||||
"""Process the AstraDB results and extract TwelveLabs index information."""
|
||||
if not self.astra_results:
|
||||
return
|
||||
|
||||
# Convert to list if single item
|
||||
results = self.astra_results if isinstance(self.astra_results, list) else [self.astra_results]
|
||||
|
||||
# Try to extract index information from metadata
|
||||
for doc in results:
|
||||
if not isinstance(doc, Data):
|
||||
continue
|
||||
|
||||
# Get the metadata, handling the nested structure
|
||||
metadata = {}
|
||||
if hasattr(doc, "metadata") and isinstance(doc.metadata, dict):
|
||||
# Handle nested metadata using .get() method
|
||||
metadata = doc.metadata.get("metadata", doc.metadata)
|
||||
|
||||
# Extract index_id and video_id
|
||||
self._index_id = metadata.get("index_id")
|
||||
self._video_id = metadata.get("video_id")
|
||||
|
||||
# If we found both, we can stop searching
|
||||
if self._index_id and self._video_id:
|
||||
break
|
||||
|
||||
def get_video_id(self) -> Message:
|
||||
"""Return the extracted video ID as a Message."""
|
||||
self.build()
|
||||
return Message(text=self._video_id if self._video_id else "")
|
||||
|
||||
def get_index_id(self) -> Message:
|
||||
"""Return the extracted index ID as a Message."""
|
||||
self.build()
|
||||
return Message(text=self._index_id if self._index_id else "")
|
||||
311
src/backend/base/langflow/components/twelvelabs/pegasus_index.py
Normal file
311
src/backend/base/langflow/components/twelvelabs/pegasus_index.py
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
from twelvelabs import TwelveLabs
|
||||
|
||||
from langflow.custom import Component
|
||||
from langflow.inputs import DataInput, DropdownInput, SecretStrInput, StrInput
|
||||
from langflow.io import Output
|
||||
from langflow.schema import Data
|
||||
|
||||
|
||||
class TwelveLabsError(Exception):
|
||||
"""Base exception for Twelve Labs errors."""
|
||||
|
||||
|
||||
class IndexCreationError(TwelveLabsError):
|
||||
"""Error raised when there's an issue with an index."""
|
||||
|
||||
|
||||
class TaskError(TwelveLabsError):
|
||||
"""Error raised when a task fails."""
|
||||
|
||||
|
||||
class TaskTimeoutError(TwelveLabsError):
|
||||
"""Error raised when a task times out."""
|
||||
|
||||
|
||||
class PegasusIndexVideo(Component):
|
||||
"""Indexes videos using Twelve Labs Pegasus API and adds the video ID to metadata."""
|
||||
|
||||
display_name = "Twelve Labs Pegasus Index Video"
|
||||
description = "Index videos using Twelve Labs and add the video_id to metadata."
|
||||
icon = "TwelveLabs"
|
||||
name = "TwelveLabsPegasusIndexVideo"
|
||||
documentation = "https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md"
|
||||
|
||||
inputs = [
|
||||
DataInput(
|
||||
name="videodata",
|
||||
display_name="Video Data",
|
||||
info="Video Data objects (from VideoFile or SplitVideo)",
|
||||
is_list=True,
|
||||
required=True,
|
||||
),
|
||||
SecretStrInput(
|
||||
name="api_key", display_name="Twelve Labs API Key", info="Enter your Twelve Labs API Key.", required=True
|
||||
),
|
||||
DropdownInput(
|
||||
name="model_name",
|
||||
display_name="Model",
|
||||
info="Pegasus model to use for indexing",
|
||||
options=["pegasus1.2"],
|
||||
value="pegasus1.2",
|
||||
advanced=False,
|
||||
),
|
||||
StrInput(
|
||||
name="index_name",
|
||||
display_name="Index Name",
|
||||
info="Name of the index to use. If the index doesn't exist, it will be created.",
|
||||
required=False,
|
||||
),
|
||||
StrInput(
|
||||
name="index_id",
|
||||
display_name="Index ID",
|
||||
info="ID of an existing index to use. If provided, index_name will be ignored.",
|
||||
required=False,
|
||||
),
|
||||
]
|
||||
|
||||
outputs = [
|
||||
Output(
|
||||
display_name="Indexed Data", name="indexed_data", method="index_videos", output_types=["Data"], is_list=True
|
||||
),
|
||||
]
|
||||
|
||||
def _get_or_create_index(self, client: TwelveLabs) -> tuple[str, str]:
|
||||
"""Get existing index or create new one.
|
||||
|
||||
Returns (index_id, index_name).
|
||||
"""
|
||||
# First check if index_id is provided and valid
|
||||
if hasattr(self, "index_id") and self.index_id:
|
||||
try:
|
||||
index = client.index.retrieve(id=self.index_id)
|
||||
except (ValueError, KeyError) as e:
|
||||
if not hasattr(self, "index_name") or not self.index_name:
|
||||
error_msg = "Invalid index ID provided and no index name specified for fallback"
|
||||
raise IndexCreationError(error_msg) from e
|
||||
else:
|
||||
return self.index_id, index.name
|
||||
|
||||
# If index_name is provided, try to find it
|
||||
if hasattr(self, "index_name") and self.index_name:
|
||||
try:
|
||||
# List all indexes and find by name
|
||||
indexes = client.index.list()
|
||||
for idx in indexes:
|
||||
if idx.name == self.index_name:
|
||||
return idx.id, idx.name
|
||||
|
||||
# If we get here, index wasn't found - create it
|
||||
index = client.index.create(
|
||||
name=self.index_name,
|
||||
models=[
|
||||
{
|
||||
"name": self.model_name if hasattr(self, "model_name") else "pegasus1.2",
|
||||
"options": ["visual", "audio"],
|
||||
}
|
||||
],
|
||||
)
|
||||
except (ValueError, KeyError) as e:
|
||||
error_msg = f"Error with index name {self.index_name}"
|
||||
raise IndexCreationError(error_msg) from e
|
||||
else:
|
||||
return index.id, index.name
|
||||
|
||||
# If we get here, neither index_id nor index_name was provided
|
||||
error_msg = "Either index_name or index_id must be provided"
|
||||
raise IndexCreationError(error_msg)
|
||||
|
||||
def on_task_update(self, task: Any, video_path: str) -> None:
|
||||
"""Callback for task status updates.
|
||||
|
||||
Updates the component status with the current task status.
|
||||
"""
|
||||
video_name = Path(video_path).name
|
||||
status_msg = f"Indexing {video_name}... Status: {task.status}"
|
||||
self.status = status_msg
|
||||
|
||||
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=5, max=60), reraise=True)
|
||||
def _check_task_status(
|
||||
self,
|
||||
client: TwelveLabs,
|
||||
task_id: str,
|
||||
video_path: str,
|
||||
) -> Any:
|
||||
"""Check task status once.
|
||||
|
||||
Makes a single API call to check the status of a task.
|
||||
"""
|
||||
task = client.task.retrieve(id=task_id)
|
||||
self.on_task_update(task, video_path)
|
||||
return task
|
||||
|
||||
def _wait_for_task_completion(
|
||||
self, client: TwelveLabs, task_id: str, video_path: str, max_retries: int = 120, sleep_time: int = 10
|
||||
) -> Any:
|
||||
"""Wait for task completion with timeout and improved error handling.
|
||||
|
||||
Polls the task status until completion or timeout.
|
||||
"""
|
||||
retries = 0
|
||||
consecutive_errors = 0
|
||||
max_consecutive_errors = 5
|
||||
video_name = Path(video_path).name
|
||||
|
||||
while retries < max_retries:
|
||||
try:
|
||||
self.status = f"Checking task status for {video_name} (attempt {retries + 1})"
|
||||
task = self._check_task_status(client, task_id, video_path)
|
||||
|
||||
if task.status == "ready":
|
||||
self.status = f"Indexing for {video_name} completed successfully!"
|
||||
return task
|
||||
if task.status == "failed":
|
||||
error_msg = f"Task failed for {video_name}: {getattr(task, 'error', 'Unknown error')}"
|
||||
self.status = error_msg
|
||||
raise TaskError(error_msg)
|
||||
if task.status == "error":
|
||||
error_msg = f"Task encountered an error for {video_name}: {getattr(task, 'error', 'Unknown error')}"
|
||||
self.status = error_msg
|
||||
raise TaskError(error_msg)
|
||||
|
||||
time.sleep(sleep_time)
|
||||
retries += 1
|
||||
elapsed_time = retries * sleep_time
|
||||
self.status = f"Indexing {video_name}... {elapsed_time}s elapsed"
|
||||
|
||||
except (ValueError, KeyError) as e:
|
||||
consecutive_errors += 1
|
||||
error_msg = f"Error checking task status for {video_name}: {e!s}"
|
||||
self.status = error_msg
|
||||
|
||||
if consecutive_errors >= max_consecutive_errors:
|
||||
too_many_errors = f"Too many consecutive errors checking task status for {video_name}"
|
||||
raise TaskError(too_many_errors) from e
|
||||
|
||||
time.sleep(sleep_time * (2**consecutive_errors))
|
||||
continue
|
||||
|
||||
timeout_msg = f"Timeout waiting for indexing of {video_name} after {max_retries * sleep_time} seconds"
|
||||
self.status = timeout_msg
|
||||
raise TaskTimeoutError(timeout_msg)
|
||||
|
||||
def _upload_video(self, client: TwelveLabs, video_path: str, index_id: str) -> str:
|
||||
"""Upload a single video and return its task ID.
|
||||
|
||||
Uploads a video file to the specified index and returns the task ID.
|
||||
"""
|
||||
video_name = Path(video_path).name
|
||||
with Path(video_path).open("rb") as video_file:
|
||||
self.status = f"Uploading {video_name} to index {index_id}..."
|
||||
task = client.task.create(index_id=index_id, file=video_file)
|
||||
task_id = task.id
|
||||
self.status = f"Upload complete for {video_name}. Task ID: {task_id}"
|
||||
return task_id
|
||||
|
||||
def index_videos(self) -> list[Data]:
|
||||
"""Indexes each video and adds the video_id to its metadata."""
|
||||
if not self.videodata:
|
||||
self.status = "No video data provided."
|
||||
return []
|
||||
|
||||
if not self.api_key:
|
||||
error_msg = "Twelve Labs API Key is required"
|
||||
raise IndexCreationError(error_msg)
|
||||
|
||||
if not (hasattr(self, "index_name") and self.index_name) and not (hasattr(self, "index_id") and self.index_id):
|
||||
error_msg = "Either index_name or index_id must be provided"
|
||||
raise IndexCreationError(error_msg)
|
||||
|
||||
client = TwelveLabs(api_key=self.api_key)
|
||||
indexed_data_list: list[Data] = []
|
||||
|
||||
# Get or create the index
|
||||
try:
|
||||
index_id, index_name = self._get_or_create_index(client)
|
||||
self.status = f"Using index: {index_name} (ID: {index_id})"
|
||||
except IndexCreationError as e:
|
||||
self.status = f"Failed to get/create Twelve Labs index: {e!s}"
|
||||
raise
|
||||
|
||||
# First, validate all videos and create a list of valid ones
|
||||
valid_videos: list[tuple[Data, str]] = []
|
||||
for video_data_item in self.videodata:
|
||||
if not isinstance(video_data_item, Data):
|
||||
self.status = f"Skipping invalid data item: {video_data_item}"
|
||||
continue
|
||||
|
||||
video_info = video_data_item.data
|
||||
if not isinstance(video_info, dict):
|
||||
self.status = f"Skipping item with invalid data structure: {video_info}"
|
||||
continue
|
||||
|
||||
video_path = video_info.get("text")
|
||||
if not video_path or not isinstance(video_path, str):
|
||||
self.status = f"Skipping item with missing or invalid video path: {video_info}"
|
||||
continue
|
||||
|
||||
if not Path(video_path).exists():
|
||||
self.status = f"Video file not found, skipping: {video_path}"
|
||||
continue
|
||||
|
||||
valid_videos.append((video_data_item, video_path))
|
||||
|
||||
if not valid_videos:
|
||||
self.status = "No valid videos to process."
|
||||
return []
|
||||
|
||||
# Upload all videos first and collect their task IDs
|
||||
upload_tasks: list[tuple[Data, str, str]] = [] # (data_item, video_path, task_id)
|
||||
for data_item, video_path in valid_videos:
|
||||
try:
|
||||
task_id = self._upload_video(client, video_path, index_id)
|
||||
upload_tasks.append((data_item, video_path, task_id))
|
||||
except (ValueError, KeyError) as e:
|
||||
self.status = f"Failed to upload {video_path}: {e!s}"
|
||||
continue
|
||||
|
||||
# Now check all tasks in parallel using a thread pool
|
||||
with ThreadPoolExecutor(max_workers=min(10, len(upload_tasks))) as executor:
|
||||
futures = []
|
||||
for data_item, video_path, task_id in upload_tasks:
|
||||
future = executor.submit(self._wait_for_task_completion, client, task_id, video_path)
|
||||
futures.append((data_item, video_path, future))
|
||||
|
||||
# Process results as they complete
|
||||
for data_item, video_path, future in futures:
|
||||
try:
|
||||
completed_task = future.result()
|
||||
if completed_task.status == "ready":
|
||||
video_id = completed_task.video_id
|
||||
video_name = Path(video_path).name
|
||||
self.status = f"Video {video_name} indexed successfully. Video ID: {video_id}"
|
||||
|
||||
# Add video_id to the metadata
|
||||
video_info = data_item.data
|
||||
if "metadata" not in video_info:
|
||||
video_info["metadata"] = {}
|
||||
elif not isinstance(video_info["metadata"], dict):
|
||||
self.status = f"Warning: Overwriting non-dict metadata for {video_path}"
|
||||
video_info["metadata"] = {}
|
||||
|
||||
video_info["metadata"].update(
|
||||
{"video_id": video_id, "index_id": index_id, "index_name": index_name}
|
||||
)
|
||||
|
||||
updated_data_item = Data(data=video_info)
|
||||
indexed_data_list.append(updated_data_item)
|
||||
except (TaskError, TaskTimeoutError) as e:
|
||||
self.status = f"Failed to process {video_path}: {e!s}"
|
||||
|
||||
if not indexed_data_list:
|
||||
self.status = "No videos were successfully indexed."
|
||||
else:
|
||||
self.status = f"Finished indexing {len(indexed_data_list)}/{len(self.videodata)} videos."
|
||||
|
||||
return indexed_data_list
|
||||
291
src/backend/base/langflow/components/twelvelabs/split_video.py
Normal file
291
src/backend/base/langflow/components/twelvelabs/split_video.py
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import hashlib
|
||||
import math
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from langflow.custom import Component
|
||||
from langflow.inputs import BoolInput, DropdownInput, HandleInput, IntInput
|
||||
from langflow.schema import Data
|
||||
from langflow.template import Output
|
||||
|
||||
|
||||
class SplitVideoComponent(Component):
|
||||
"""A component that splits a video into multiple clips of specified duration using FFmpeg."""
|
||||
|
||||
display_name = "Split Video"
|
||||
description = "Split a video into multiple clips of specified duration."
|
||||
icon = "TwelveLabs"
|
||||
name = "SplitVideo"
|
||||
documentation = "https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md"
|
||||
|
||||
inputs = [
|
||||
HandleInput(
|
||||
name="videodata",
|
||||
display_name="Video Data",
|
||||
info="Input video data from VideoFile component",
|
||||
required=True,
|
||||
input_types=["Data"],
|
||||
),
|
||||
IntInput(
|
||||
name="clip_duration",
|
||||
display_name="Clip Duration (seconds)",
|
||||
info="Duration of each clip in seconds",
|
||||
required=True,
|
||||
value=30,
|
||||
),
|
||||
DropdownInput(
|
||||
name="last_clip_handling",
|
||||
display_name="Last Clip Handling",
|
||||
info=(
|
||||
"How to handle the final clip when it would be shorter than the specified duration:\n"
|
||||
"- Truncate: Skip the final clip entirely if it's shorter than the specified duration\n"
|
||||
"- Overlap Previous: Start the final clip earlier to maintain full duration, "
|
||||
"overlapping with previous clip\n"
|
||||
"- Keep Short: Keep the final clip at its natural length, even if shorter than specified duration"
|
||||
),
|
||||
options=["Truncate", "Overlap Previous", "Keep Short"],
|
||||
value="Overlap Previous",
|
||||
required=True,
|
||||
),
|
||||
BoolInput(
|
||||
name="include_original",
|
||||
display_name="Include Original Video",
|
||||
info="Whether to include the original video in the output",
|
||||
value=False,
|
||||
),
|
||||
]
|
||||
|
||||
outputs = [
|
||||
Output(
|
||||
name="clips",
|
||||
display_name="Video Clips",
|
||||
method="process",
|
||||
output_types=["Data"],
|
||||
),
|
||||
]
|
||||
|
||||
def get_video_duration(self, video_path: str) -> float:
|
||||
"""Get video duration using FFmpeg."""
|
||||
try:
|
||||
# Validate video path to prevent shell injection
|
||||
if not isinstance(video_path, str) or any(c in video_path for c in ";&|`$(){}[]<>*?!#~"):
|
||||
error_msg = "Invalid video path"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=noprint_wrappers=1:nokey=1",
|
||||
video_path,
|
||||
]
|
||||
result = subprocess.run( # noqa: S603
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False, # Explicitly set shell=False for security
|
||||
)
|
||||
if result.returncode != 0:
|
||||
error_msg = f"FFprobe error: {result.stderr}"
|
||||
raise RuntimeError(error_msg)
|
||||
return float(result.stdout.strip())
|
||||
except Exception as e:
|
||||
self.log(f"Error getting video duration: {e!s}", "ERROR")
|
||||
raise
|
||||
|
||||
def get_output_dir(self, video_path: str) -> str:
|
||||
"""Create a unique output directory for clips based on video name and timestamp."""
|
||||
# Get the video filename without extension
|
||||
path_obj = Path(video_path)
|
||||
base_name = path_obj.stem
|
||||
|
||||
# Create a timestamp
|
||||
timestamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
# Create a unique hash from the video path
|
||||
path_hash = hashlib.sha256(video_path.encode()).hexdigest()[:8]
|
||||
|
||||
# Create the output directory path
|
||||
output_dir = Path(path_obj.parent) / f"clips_{base_name}_{timestamp}_{path_hash}"
|
||||
|
||||
# Create the directory if it doesn't exist
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return str(output_dir)
|
||||
|
||||
def process_video(self, video_path: str, clip_duration: int, *, include_original: bool) -> list[Data]:
|
||||
"""Process video and split it into clips using FFmpeg."""
|
||||
try:
|
||||
# Get video duration
|
||||
total_duration = self.get_video_duration(video_path)
|
||||
|
||||
# Calculate number of clips (ceiling to include partial clip)
|
||||
num_clips = math.ceil(total_duration / clip_duration)
|
||||
self.log(
|
||||
f"Total duration: {total_duration}s, Clip duration: {clip_duration}s, Number of clips: {num_clips}"
|
||||
)
|
||||
|
||||
# Create output directory for clips
|
||||
output_dir = self.get_output_dir(video_path)
|
||||
|
||||
# Get original video info
|
||||
path_obj = Path(video_path)
|
||||
original_filename = path_obj.name
|
||||
original_name = path_obj.stem
|
||||
|
||||
# List to store all video paths (including original if requested)
|
||||
video_paths: list[Data] = []
|
||||
|
||||
# Add original video if requested
|
||||
if include_original:
|
||||
original_data: dict[str, Any] = {
|
||||
"text": video_path,
|
||||
"metadata": {
|
||||
"source": video_path,
|
||||
"type": "video",
|
||||
"clip_index": -1, # -1 indicates original video
|
||||
"duration": int(total_duration), # Convert to int
|
||||
"original_video": {
|
||||
"name": original_name,
|
||||
"filename": original_filename,
|
||||
"path": video_path,
|
||||
"duration": int(total_duration), # Convert to int
|
||||
"total_clips": int(num_clips),
|
||||
"clip_duration": int(clip_duration),
|
||||
},
|
||||
},
|
||||
}
|
||||
video_paths.append(Data(data=original_data))
|
||||
|
||||
# Split video into clips
|
||||
for i in range(int(num_clips)): # Convert num_clips to int for range
|
||||
start_time = float(i * clip_duration) # Convert to float for time calculations
|
||||
end_time = min(float((i + 1) * clip_duration), total_duration)
|
||||
duration = end_time - start_time
|
||||
|
||||
# Handle last clip if it's shorter
|
||||
if i == int(num_clips) - 1 and duration < clip_duration: # Convert num_clips to int for comparison
|
||||
if self.last_clip_handling == "Truncate":
|
||||
# Skip if the last clip would be too short
|
||||
continue
|
||||
if self.last_clip_handling == "Overlap Previous" and i > 0:
|
||||
# Start from earlier to make full duration
|
||||
start_time = total_duration - clip_duration
|
||||
duration = clip_duration
|
||||
# For "Keep Short", we use the original start_time and duration
|
||||
|
||||
# Skip if duration is too small (less than 1 second)
|
||||
if duration < 1:
|
||||
continue
|
||||
|
||||
# Generate output path
|
||||
output_path = Path(output_dir) / f"clip_{i:03d}.mp4"
|
||||
output_path_str = str(output_path)
|
||||
|
||||
try:
|
||||
# Use FFmpeg to split the video
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
video_path,
|
||||
"-ss",
|
||||
str(start_time),
|
||||
"-t",
|
||||
str(duration),
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-y", # Overwrite output file if it exists
|
||||
output_path_str,
|
||||
]
|
||||
|
||||
result = subprocess.run( # noqa: S603
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False, # Explicitly set shell=False for security
|
||||
)
|
||||
if result.returncode != 0:
|
||||
error_msg = f"FFmpeg error: {result.stderr}"
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# Create timestamp string for metadata
|
||||
start_min = int(start_time // 60)
|
||||
start_sec = int(start_time % 60)
|
||||
end_min = int(end_time // 60)
|
||||
end_sec = int(end_time % 60)
|
||||
timestamp_str = f"{start_min:02d}:{start_sec:02d} - {end_min:02d}:{end_sec:02d}"
|
||||
|
||||
# Create Data object for the clip
|
||||
clip_data: dict[str, Any] = {
|
||||
"text": output_path_str,
|
||||
"metadata": {
|
||||
"source": video_path,
|
||||
"type": "video",
|
||||
"clip_index": i,
|
||||
"start_time": float(start_time),
|
||||
"end_time": float(end_time),
|
||||
"duration": float(duration),
|
||||
"original_video": {
|
||||
"name": original_name,
|
||||
"filename": original_filename,
|
||||
"path": video_path,
|
||||
"duration": int(total_duration),
|
||||
"total_clips": int(num_clips),
|
||||
"clip_duration": int(clip_duration),
|
||||
},
|
||||
"clip": {
|
||||
"index": i,
|
||||
"total": int(num_clips),
|
||||
"duration": float(duration),
|
||||
"start_time": float(start_time),
|
||||
"end_time": float(end_time),
|
||||
"timestamp": timestamp_str,
|
||||
},
|
||||
},
|
||||
}
|
||||
video_paths.append(Data(data=clip_data))
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error processing clip {i}: {e!s}", "ERROR")
|
||||
raise
|
||||
|
||||
self.log(f"Created {len(video_paths)} clips in {output_dir}")
|
||||
except Exception as e:
|
||||
self.log(f"Error processing video: {e!s}", "ERROR")
|
||||
raise
|
||||
else:
|
||||
return video_paths
|
||||
|
||||
def process(self) -> list[Data]:
|
||||
"""Process the input video and return a list of Data objects containing the clips."""
|
||||
try:
|
||||
# Get the input video path from the previous component
|
||||
if not hasattr(self, "videodata") or not isinstance(self.videodata, list) or len(self.videodata) != 1:
|
||||
error_msg = "Please provide exactly one video"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
video_path = self.videodata[0].data.get("text")
|
||||
if not video_path or not Path(video_path).exists():
|
||||
error_msg = "Invalid video path"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Validate video path to prevent shell injection
|
||||
if not isinstance(video_path, str) or any(c in video_path for c in ";&|`$(){}[]<>*?!#~"):
|
||||
error_msg = "Invalid video path contains unsafe characters"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Process the video
|
||||
return self.process_video(video_path, self.clip_duration, include_original=self.include_original)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error in split video component: {e!s}", "ERROR")
|
||||
raise
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
from twelvelabs import TwelveLabs
|
||||
|
||||
from langflow.base.embeddings.model import LCEmbeddingsModel
|
||||
from langflow.field_typing import Embeddings
|
||||
from langflow.io import DropdownInput, FloatInput, IntInput, SecretStrInput
|
||||
|
||||
|
||||
class TwelveLabsTextEmbeddings(Embeddings):
|
||||
def __init__(self, api_key: str, model: str) -> None:
|
||||
self.client = TwelveLabs(api_key=api_key)
|
||||
self.model = model
|
||||
|
||||
def embed_documents(self, texts: list[str]) -> list[list[float]]:
|
||||
all_embeddings: list[list[float]] = []
|
||||
for text in texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
result = self.client.embed.create(model_name=self.model, text=text)
|
||||
|
||||
if result.text_embedding and result.text_embedding.segments:
|
||||
for segment in result.text_embedding.segments:
|
||||
all_embeddings.append([float(x) for x in segment.embeddings_float])
|
||||
break # Only take first segment for now
|
||||
|
||||
return all_embeddings
|
||||
|
||||
def embed_query(self, text: str) -> list[float]:
|
||||
result = self.client.embed.create(model_name=self.model, text=text)
|
||||
|
||||
if result.text_embedding and result.text_embedding.segments:
|
||||
return [float(x) for x in result.text_embedding.segments[0].embeddings_float]
|
||||
return []
|
||||
|
||||
|
||||
class TwelveLabsTextEmbeddingsComponent(LCEmbeddingsModel):
|
||||
display_name = "Twelve Labs Text Embeddings"
|
||||
description = "Generate embeddings using Twelve Labs text embedding models."
|
||||
icon = "TwelveLabs"
|
||||
name = "TwelveLabsTextEmbeddings"
|
||||
documentation = "https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md"
|
||||
|
||||
inputs = [
|
||||
SecretStrInput(name="api_key", display_name="Twelve Labs API Key", value="TWELVELABS_API_KEY", required=True),
|
||||
DropdownInput(
|
||||
name="model",
|
||||
display_name="Model",
|
||||
advanced=False,
|
||||
options=["Marengo-retrieval-2.7"],
|
||||
value="Marengo-retrieval-2.7",
|
||||
),
|
||||
IntInput(name="max_retries", display_name="Max Retries", value=3, advanced=True),
|
||||
FloatInput(name="request_timeout", display_name="Request Timeout", advanced=True),
|
||||
]
|
||||
|
||||
def build_embeddings(self) -> Embeddings:
|
||||
return TwelveLabsTextEmbeddings(api_key=self.api_key, model=self.model)
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
from twelvelabs import TwelveLabs
|
||||
|
||||
from langflow.custom import Component
|
||||
from langflow.field_typing.range_spec import RangeSpec
|
||||
from langflow.inputs import DataInput, DropdownInput, MessageInput, MultilineInput, SecretStrInput, SliderInput
|
||||
from langflow.io import Output
|
||||
from langflow.schema.message import Message
|
||||
|
||||
|
||||
class TaskError(Exception):
|
||||
"""Error raised when a task fails."""
|
||||
|
||||
|
||||
class TaskTimeoutError(Exception):
|
||||
"""Error raised when a task times out."""
|
||||
|
||||
|
||||
class IndexCreationError(Exception):
|
||||
"""Error raised when there's an issue with an index."""
|
||||
|
||||
|
||||
class ApiRequestError(Exception):
|
||||
"""Error raised when an API request fails."""
|
||||
|
||||
|
||||
class VideoValidationError(Exception):
|
||||
"""Error raised when video validation fails."""
|
||||
|
||||
|
||||
class TwelveLabsPegasus(Component):
|
||||
display_name = "Twelve Labs Pegasus"
|
||||
description = "Chat with videos using Twelve Labs Pegasus API."
|
||||
icon = "TwelveLabs"
|
||||
name = "TwelveLabsPegasus"
|
||||
documentation = "https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md"
|
||||
|
||||
inputs = [
|
||||
DataInput(name="videodata", display_name="Video Data", info="Video Data", is_list=True),
|
||||
SecretStrInput(
|
||||
name="api_key", display_name="Twelve Labs API Key", info="Enter your Twelve Labs API Key.", required=True
|
||||
),
|
||||
MessageInput(
|
||||
name="video_id",
|
||||
display_name="Pegasus Video ID",
|
||||
info="Enter a Video ID for a previously indexed video.",
|
||||
),
|
||||
MessageInput(
|
||||
name="index_name",
|
||||
display_name="Index Name",
|
||||
info="Name of the index to use. If the index doesn't exist, it will be created.",
|
||||
required=False,
|
||||
),
|
||||
MessageInput(
|
||||
name="index_id",
|
||||
display_name="Index ID",
|
||||
info="ID of an existing index to use. If provided, index_name will be ignored.",
|
||||
required=False,
|
||||
),
|
||||
DropdownInput(
|
||||
name="model_name",
|
||||
display_name="Model",
|
||||
info="Pegasus model to use for indexing",
|
||||
options=["pegasus1.2"],
|
||||
value="pegasus1.2",
|
||||
advanced=False,
|
||||
),
|
||||
MultilineInput(
|
||||
name="message",
|
||||
display_name="Prompt",
|
||||
info="Message to chat with the video.",
|
||||
required=True,
|
||||
),
|
||||
SliderInput(
|
||||
name="temperature",
|
||||
display_name="Temperature",
|
||||
value=0.7,
|
||||
range_spec=RangeSpec(min=0, max=1, step=0.01),
|
||||
info=(
|
||||
"Controls randomness in responses. Lower values are more deterministic, "
|
||||
"higher values are more creative."
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
outputs = [
|
||||
Output(
|
||||
display_name="Message",
|
||||
name="response",
|
||||
method="process_video",
|
||||
type_=Message,
|
||||
),
|
||||
Output(
|
||||
display_name="Video ID",
|
||||
name="processed_video_id",
|
||||
method="get_video_id",
|
||||
type_=Message,
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._task_id: str | None = None
|
||||
self._video_id: str | None = None
|
||||
self._index_id: str | None = None
|
||||
self._index_name: str | None = None
|
||||
self._message: str | None = None
|
||||
|
||||
def _get_or_create_index(self, client: TwelveLabs) -> tuple[str, str]:
|
||||
"""Get existing index or create new one.
|
||||
|
||||
Returns (index_id, index_name).
|
||||
"""
|
||||
# First check if index_id is provided and valid
|
||||
if hasattr(self, "_index_id") and self._index_id:
|
||||
try:
|
||||
index = client.index.retrieve(id=self._index_id)
|
||||
self.log(f"Found existing index with ID: {self._index_id}")
|
||||
except (ValueError, KeyError) as e:
|
||||
self.log(f"Error retrieving index with ID {self._index_id}: {e!s}", "WARNING")
|
||||
else:
|
||||
return self._index_id, index.name
|
||||
|
||||
# If index_name is provided, try to find it
|
||||
if hasattr(self, "_index_name") and self._index_name:
|
||||
try:
|
||||
# List all indexes and find by name
|
||||
indexes = client.index.list()
|
||||
for idx in indexes:
|
||||
if idx.name == self._index_name:
|
||||
self.log(f"Found existing index: {self._index_name} (ID: {idx.id})")
|
||||
return idx.id, idx.name
|
||||
|
||||
# If we get here, index wasn't found - create it
|
||||
self.log(f"Creating new index: {self._index_name}")
|
||||
index = client.index.create(
|
||||
name=self._index_name,
|
||||
models=[
|
||||
{
|
||||
"name": self.model_name if hasattr(self, "model_name") else "pegasus1.2",
|
||||
"options": ["visual", "audio"],
|
||||
}
|
||||
],
|
||||
)
|
||||
except (ValueError, KeyError) as e:
|
||||
self.log(f"Error with index name {self._index_name}: {e!s}", "ERROR")
|
||||
error_message = f"Error with index name {self._index_name}"
|
||||
raise IndexCreationError(error_message) from e
|
||||
else:
|
||||
return index.id, index.name
|
||||
|
||||
# If neither is provided, create a new index with timestamp
|
||||
try:
|
||||
index_name = f"index_{int(time.time())}"
|
||||
self.log(f"Creating new index: {index_name}")
|
||||
index = client.index.create(
|
||||
name=index_name,
|
||||
models=[
|
||||
{
|
||||
"name": self.model_name if hasattr(self, "model_name") else "pegasus1.2",
|
||||
"options": ["visual", "audio"],
|
||||
}
|
||||
],
|
||||
)
|
||||
except (ValueError, KeyError) as e:
|
||||
self.log(f"Failed to create new index: {e!s}", "ERROR")
|
||||
error_message = "Failed to create new index"
|
||||
raise IndexCreationError(error_message) from e
|
||||
else:
|
||||
return index.id, index.name
|
||||
|
||||
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10), reraise=True)
|
||||
async def _make_api_request(self, method: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
"""Make API request with retry logic.
|
||||
|
||||
Retries failed requests with exponential backoff.
|
||||
"""
|
||||
try:
|
||||
return await method(*args, **kwargs)
|
||||
except (ValueError, KeyError) as e:
|
||||
self.log(f"API request failed: {e!s}", "ERROR")
|
||||
error_message = "API request failed"
|
||||
raise ApiRequestError(error_message) from e
|
||||
|
||||
def wait_for_task_completion(
|
||||
self, client: TwelveLabs, task_id: str, max_retries: int = 120, sleep_time: int = 5
|
||||
) -> Any:
|
||||
"""Wait for task completion with timeout and improved error handling.
|
||||
|
||||
Polls the task status until completion or timeout.
|
||||
"""
|
||||
retries = 0
|
||||
consecutive_errors = 0
|
||||
max_consecutive_errors = 3
|
||||
|
||||
while retries < max_retries:
|
||||
try:
|
||||
self.log(f"Checking task status (attempt {retries + 1})")
|
||||
result = client.task.retrieve(id=task_id)
|
||||
consecutive_errors = 0 # Reset error counter on success
|
||||
|
||||
if result.status == "ready":
|
||||
self.log("Task completed successfully!")
|
||||
return result
|
||||
if result.status == "failed":
|
||||
error_msg = f"Task failed with status: {result.status}"
|
||||
self.log(error_msg, "ERROR")
|
||||
raise TaskError(error_msg)
|
||||
if result.status == "error":
|
||||
error_msg = f"Task encountered an error: {getattr(result, 'error', 'Unknown error')}"
|
||||
self.log(error_msg, "ERROR")
|
||||
raise TaskError(error_msg)
|
||||
|
||||
time.sleep(sleep_time)
|
||||
retries += 1
|
||||
status_msg = f"Processing video... {retries * sleep_time}s elapsed"
|
||||
self.status = status_msg
|
||||
self.log(status_msg)
|
||||
|
||||
except (ValueError, KeyError) as e:
|
||||
consecutive_errors += 1
|
||||
error_msg = f"Error checking task status: {e!s}"
|
||||
self.log(error_msg, "WARNING")
|
||||
|
||||
if consecutive_errors >= max_consecutive_errors:
|
||||
too_many_errors = "Too many consecutive errors"
|
||||
raise TaskError(too_many_errors) from e
|
||||
|
||||
time.sleep(sleep_time * 2)
|
||||
continue
|
||||
|
||||
timeout_msg = f"Timeout after {max_retries * sleep_time} seconds"
|
||||
self.log(timeout_msg, "ERROR")
|
||||
raise TaskTimeoutError(timeout_msg)
|
||||
|
||||
def validate_video_file(self, filepath: str) -> tuple[bool, str]:
|
||||
"""Validate video file using ffprobe.
|
||||
|
||||
Returns (is_valid, error_message).
|
||||
"""
|
||||
# Ensure filepath is a string and doesn't contain shell metacharacters
|
||||
if not isinstance(filepath, str) or any(c in filepath for c in ";&|`$(){}[]<>*?!#~"):
|
||||
return False, "Invalid filepath"
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"stream=codec_type,codec_name",
|
||||
"-of",
|
||||
"default=nw=1",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_format",
|
||||
filepath,
|
||||
]
|
||||
|
||||
# Use subprocess with a list of arguments to avoid shell injection
|
||||
# We need to skip the S603 warning here as we're taking proper precautions
|
||||
# with input validation and using shell=False
|
||||
result = subprocess.run( # noqa: S603
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False, # Explicitly set shell=False for security
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return False, f"FFprobe error: {result.stderr}"
|
||||
|
||||
probe_data = json.loads(result.stdout)
|
||||
|
||||
has_video = any(stream.get("codec_type") == "video" for stream in probe_data.get("streams", []))
|
||||
|
||||
if not has_video:
|
||||
return False, "No video stream found in file"
|
||||
|
||||
self.log(f"Video validation successful: {json.dumps(probe_data, indent=2)}")
|
||||
except subprocess.SubprocessError as e:
|
||||
return False, f"FFprobe process error: {e!s}"
|
||||
except json.JSONDecodeError as e:
|
||||
return False, f"FFprobe output parsing error: {e!s}"
|
||||
except (ValueError, OSError) as e:
|
||||
return False, f"Validation error: {e!s}"
|
||||
else:
|
||||
return True, ""
|
||||
|
||||
def on_task_update(self, task: Any) -> None:
|
||||
"""Callback for task status updates.
|
||||
|
||||
Updates the component status with the current task status.
|
||||
"""
|
||||
self.status = f"Processing video... Status: {task.status}"
|
||||
self.log(self.status)
|
||||
|
||||
def process_video(self) -> Message:
|
||||
"""Process video using Pegasus and generate response if message is provided.
|
||||
|
||||
Handles video indexing and question answering using the Twelve Labs API.
|
||||
"""
|
||||
# Check and initialize inputs
|
||||
if hasattr(self, "index_id") and self.index_id:
|
||||
self._index_id = self.index_id.text if hasattr(self.index_id, "text") else self.index_id
|
||||
|
||||
if hasattr(self, "index_name") and self.index_name:
|
||||
self._index_name = self.index_name.text if hasattr(self.index_name, "text") else self.index_name
|
||||
|
||||
if hasattr(self, "video_id") and self.video_id:
|
||||
self._video_id = self.video_id.text if hasattr(self.video_id, "text") else self.video_id
|
||||
|
||||
if hasattr(self, "message") and self.message:
|
||||
self._message = self.message.text if hasattr(self.message, "text") else self.message
|
||||
|
||||
try:
|
||||
# If we have a message and already processed video, use existing video_id
|
||||
if self._message and self._video_id and self._video_id != "":
|
||||
self.status = f"Have video id: {self._video_id}"
|
||||
|
||||
client = TwelveLabs(api_key=self.api_key)
|
||||
|
||||
self.status = f"Processing query (w/ video ID): {self._video_id} {self._message}"
|
||||
self.log(self.status)
|
||||
|
||||
response = client.generate.text(
|
||||
video_id=self._video_id,
|
||||
prompt=self._message,
|
||||
temperature=self.temperature,
|
||||
)
|
||||
return Message(text=response.data)
|
||||
|
||||
# Otherwise process new video
|
||||
if not self.videodata or not isinstance(self.videodata, list) or len(self.videodata) != 1:
|
||||
return Message(text="Please provide exactly one video")
|
||||
|
||||
video_path = self.videodata[0].data.get("text")
|
||||
if not video_path or not Path(video_path).exists():
|
||||
return Message(text="Invalid video path")
|
||||
|
||||
if not self.api_key:
|
||||
return Message(text="No API key provided")
|
||||
|
||||
client = TwelveLabs(api_key=self.api_key)
|
||||
|
||||
# Get or create index
|
||||
try:
|
||||
index_id, index_name = self._get_or_create_index(client)
|
||||
self.status = f"Using index: {index_name} (ID: {index_id})"
|
||||
self.log(f"Using index: {index_name} (ID: {index_id})")
|
||||
self._index_id = index_id
|
||||
self._index_name = index_name
|
||||
except IndexCreationError as e:
|
||||
return Message(text=f"Failed to get/create index: {e}")
|
||||
|
||||
with Path(video_path).open("rb") as video_file:
|
||||
task = client.task.create(index_id=self._index_id, file=video_file)
|
||||
self._task_id = task.id
|
||||
|
||||
# Wait for processing to complete
|
||||
task.wait_for_done(sleep_interval=5, callback=self.on_task_update)
|
||||
|
||||
if task.status != "ready":
|
||||
return Message(text=f"Processing failed with status {task.status}")
|
||||
|
||||
# Store video_id for future use
|
||||
self._video_id = task.video_id
|
||||
|
||||
# Generate response if message provided
|
||||
if self._message:
|
||||
self.status = f"Processing query: {self._message}"
|
||||
self.log(self.status)
|
||||
|
||||
response = client.generate.text(
|
||||
video_id=self._video_id,
|
||||
prompt=self._message,
|
||||
temperature=self.temperature,
|
||||
)
|
||||
return Message(text=response.data)
|
||||
|
||||
success_msg = (
|
||||
f"Video processed successfully. You can now ask questions about the video. Video ID: {self._video_id}"
|
||||
)
|
||||
return Message(text=success_msg)
|
||||
|
||||
except (ValueError, KeyError, IndexCreationError, TaskError, TaskTimeoutError) as e:
|
||||
self.log(f"Error: {e!s}", "ERROR")
|
||||
# Clear stored IDs on error
|
||||
self._video_id = None
|
||||
self._index_id = None
|
||||
self._task_id = None
|
||||
return Message(text=f"Error: {e!s}")
|
||||
|
||||
def get_video_id(self) -> Message:
|
||||
"""Return the video ID of the processed video as a Message.
|
||||
|
||||
Returns an empty string if no video has been processed.
|
||||
"""
|
||||
video_id = self._video_id or ""
|
||||
return Message(text=video_id)
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from twelvelabs import TwelveLabs
|
||||
|
||||
from langflow.base.embeddings.model import LCEmbeddingsModel
|
||||
from langflow.field_typing import Embeddings
|
||||
from langflow.io import DropdownInput, IntInput, SecretStrInput
|
||||
|
||||
|
||||
class TwelveLabsVideoEmbeddings(Embeddings):
|
||||
def __init__(self, api_key: str, model_name: str = "Marengo-retrieval-2.7") -> None:
|
||||
self.client = TwelveLabs(api_key=api_key)
|
||||
self.model_name = model_name
|
||||
|
||||
def _wait_for_task_completion(self, task_id: str) -> Any:
|
||||
while True:
|
||||
result = self.client.embed.task.retrieve(id=task_id)
|
||||
if result.status == "ready":
|
||||
return result
|
||||
time.sleep(5)
|
||||
|
||||
def embed_documents(self, texts: list[str]) -> list[list[float]]:
|
||||
embeddings: list[list[float]] = []
|
||||
for text in texts:
|
||||
video_path = text.page_content if hasattr(text, "page_content") else str(text)
|
||||
result = self.embed_video(video_path)
|
||||
|
||||
# First try to use video embedding, then fall back to clip embedding if available
|
||||
if result["video_embedding"] is not None:
|
||||
embeddings.append(cast(list[float], result["video_embedding"]))
|
||||
elif result["clip_embeddings"] and len(result["clip_embeddings"]) > 0:
|
||||
embeddings.append(cast(list[float], result["clip_embeddings"][0]))
|
||||
else:
|
||||
# If neither is available, raise an error
|
||||
error_msg = "No embeddings were generated for the video"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
return embeddings
|
||||
|
||||
def embed_query(self, text: str) -> list[float]:
|
||||
video_path = text.page_content if hasattr(text, "page_content") else str(text)
|
||||
result = self.embed_video(video_path)
|
||||
|
||||
# First try to use video embedding, then fall back to clip embedding if available
|
||||
if result["video_embedding"] is not None:
|
||||
return cast(list[float], result["video_embedding"])
|
||||
if result["clip_embeddings"] and len(result["clip_embeddings"]) > 0:
|
||||
return cast(list[float], result["clip_embeddings"][0])
|
||||
# If neither is available, raise an error
|
||||
error_msg = "No embeddings were generated for the video"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
def embed_video(self, video_path: str) -> dict[str, list[float] | list[list[float]]]:
|
||||
file_path = Path(video_path)
|
||||
with file_path.open("rb") as video_file:
|
||||
task = self.client.embed.task.create(
|
||||
model_name=self.model_name,
|
||||
video_file=video_file,
|
||||
video_embedding_scopes=["video", "clip"],
|
||||
)
|
||||
|
||||
result = self._wait_for_task_completion(task.id)
|
||||
|
||||
video_embedding: dict[str, list[float] | list[list[float]]] = {
|
||||
"video_embedding": [], # Initialize as empty list instead of None
|
||||
"clip_embeddings": [],
|
||||
}
|
||||
|
||||
if hasattr(result.video_embedding, "segments") and result.video_embedding.segments:
|
||||
for seg in result.video_embedding.segments:
|
||||
# Check for embeddings_float attribute (this is the correct attribute name)
|
||||
if hasattr(seg, "embeddings_float") and seg.embedding_scope == "video":
|
||||
# Convert to list of floats
|
||||
video_embedding["video_embedding"] = [float(x) for x in seg.embeddings_float]
|
||||
|
||||
return video_embedding
|
||||
|
||||
|
||||
class TwelveLabsVideoEmbeddingsComponent(LCEmbeddingsModel):
|
||||
display_name = "Twelve Labs Video Embeddings"
|
||||
description = "Generate embeddings from videos using Twelve Labs video embedding models."
|
||||
name = "TwelveLabsVideoEmbeddings"
|
||||
icon = "TwelveLabs"
|
||||
documentation = "https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md"
|
||||
inputs = [
|
||||
SecretStrInput(name="api_key", display_name="API Key", required=True),
|
||||
DropdownInput(
|
||||
name="model_name",
|
||||
display_name="Model",
|
||||
advanced=False,
|
||||
options=["Marengo-retrieval-2.7"],
|
||||
value="Marengo-retrieval-2.7",
|
||||
),
|
||||
IntInput(name="request_timeout", display_name="Request Timeout", advanced=True),
|
||||
]
|
||||
|
||||
def build_embeddings(self) -> Embeddings:
|
||||
return TwelveLabsVideoEmbeddings(api_key=self.api_key, model_name=self.model_name)
|
||||
179
src/backend/base/langflow/components/twelvelabs/video_file.py
Normal file
179
src/backend/base/langflow/components/twelvelabs/video_file.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
from pathlib import Path
|
||||
|
||||
from langflow.base.data import BaseFileComponent
|
||||
from langflow.io import FileInput
|
||||
from langflow.schema import Data
|
||||
|
||||
|
||||
class VideoFileComponent(BaseFileComponent):
|
||||
"""Handles loading and processing of video files.
|
||||
|
||||
This component supports processing video files in common video formats.
|
||||
"""
|
||||
|
||||
display_name = "Video File"
|
||||
description = "Load a video file in common video formats."
|
||||
icon = "TwelveLabs"
|
||||
name = "VideoFile"
|
||||
documentation = "https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md"
|
||||
|
||||
VALID_EXTENSIONS = [
|
||||
# Common video formats
|
||||
"mp4",
|
||||
"avi",
|
||||
"mov",
|
||||
"mkv",
|
||||
"webm",
|
||||
"flv",
|
||||
"wmv",
|
||||
"mpg",
|
||||
"mpeg",
|
||||
"m4v",
|
||||
"3gp",
|
||||
"3g2",
|
||||
"m2v",
|
||||
# Professional video formats
|
||||
"mxf",
|
||||
"dv",
|
||||
"vob",
|
||||
# Additional video formats
|
||||
"ogv",
|
||||
"rm",
|
||||
"rmvb",
|
||||
"amv",
|
||||
"divx",
|
||||
"m2ts",
|
||||
"mts",
|
||||
"ts",
|
||||
"qt",
|
||||
"yuv",
|
||||
"y4m",
|
||||
]
|
||||
|
||||
inputs = [
|
||||
FileInput(
|
||||
display_name="Video File",
|
||||
name="file_path",
|
||||
file_types=[
|
||||
# Common video formats
|
||||
"mp4",
|
||||
"avi",
|
||||
"mov",
|
||||
"mkv",
|
||||
"webm",
|
||||
"flv",
|
||||
"wmv",
|
||||
"mpg",
|
||||
"mpeg",
|
||||
"m4v",
|
||||
"3gp",
|
||||
"3g2",
|
||||
"m2v",
|
||||
# Professional video formats
|
||||
"mxf",
|
||||
"dv",
|
||||
"vob",
|
||||
# Additional video formats
|
||||
"ogv",
|
||||
"rm",
|
||||
"rmvb",
|
||||
"amv",
|
||||
"divx",
|
||||
"m2ts",
|
||||
"mts",
|
||||
"ts",
|
||||
"qt",
|
||||
"yuv",
|
||||
"y4m",
|
||||
],
|
||||
required=True,
|
||||
info="Upload a video file in any common video format supported by ffmpeg",
|
||||
),
|
||||
]
|
||||
|
||||
outputs = [
|
||||
*BaseFileComponent._base_outputs,
|
||||
]
|
||||
|
||||
def process_files(self, file_list: list[BaseFileComponent.BaseFile]) -> list[BaseFileComponent.BaseFile]:
|
||||
"""Process video files."""
|
||||
self.log(f"DEBUG: Processing video files: {len(file_list)}")
|
||||
|
||||
if not file_list:
|
||||
msg = "No files to process."
|
||||
raise ValueError(msg)
|
||||
|
||||
processed_files = []
|
||||
for file in file_list:
|
||||
try:
|
||||
file_path = str(file.path)
|
||||
self.log(f"DEBUG: Processing video file: {file_path}")
|
||||
|
||||
# Verify file exists
|
||||
file_path_obj = Path(file_path)
|
||||
if not file_path_obj.exists():
|
||||
error_msg = f"Video file not found: {file_path}"
|
||||
raise FileNotFoundError(error_msg)
|
||||
|
||||
# Verify extension
|
||||
if not file_path.lower().endswith(tuple(self.VALID_EXTENSIONS)):
|
||||
error_msg = f"Invalid file type. Expected: {', '.join(self.VALID_EXTENSIONS)}"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Create a dictionary instead of a Document
|
||||
doc_data = {"text": file_path, "metadata": {"source": file_path, "type": "video"}}
|
||||
|
||||
# Pass the dictionary to Data
|
||||
file.data = Data(data=doc_data)
|
||||
|
||||
self.log(f"DEBUG: Created data: {doc_data}")
|
||||
processed_files.append(file)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error processing video file: {e!s}", "ERROR")
|
||||
raise
|
||||
|
||||
return processed_files
|
||||
|
||||
def load_files(self) -> list[Data]:
|
||||
"""Load video files and return a list of Data objects."""
|
||||
try:
|
||||
self.log("DEBUG: Starting video file load")
|
||||
if not hasattr(self, "file_path") or not self.file_path:
|
||||
self.log("DEBUG: No video file path provided")
|
||||
return []
|
||||
|
||||
self.log(f"DEBUG: Loading video from path: {self.file_path}")
|
||||
|
||||
# Verify file exists
|
||||
file_path_obj = Path(self.file_path)
|
||||
if not file_path_obj.exists():
|
||||
self.log(f"DEBUG: Video file not found at path: {self.file_path}")
|
||||
return []
|
||||
|
||||
# Verify file size
|
||||
file_size = file_path_obj.stat().st_size
|
||||
self.log(f"DEBUG: Video file size: {file_size} bytes")
|
||||
|
||||
# Create a proper Data object with the video path
|
||||
video_data = {
|
||||
"text": self.file_path,
|
||||
"metadata": {"source": self.file_path, "type": "video", "size": file_size},
|
||||
}
|
||||
|
||||
self.log(f"DEBUG: Created video data: {video_data}")
|
||||
result = [Data(data=video_data)]
|
||||
|
||||
# Log the result to verify it's a proper Data object
|
||||
self.log("DEBUG: Returning list with Data objects")
|
||||
except (FileNotFoundError, PermissionError, OSError) as e:
|
||||
self.log(f"DEBUG: File error in video load_files: {e!s}", "ERROR")
|
||||
return []
|
||||
except ImportError as e:
|
||||
self.log(f"DEBUG: Import error in video load_files: {e!s}", "ERROR")
|
||||
return []
|
||||
except (ValueError, TypeError) as e:
|
||||
self.log(f"DEBUG: Value or type error in video load_files: {e!s}", "ERROR")
|
||||
return []
|
||||
else:
|
||||
return result
|
||||
4
src/frontend/package-lock.json
generated
4
src/frontend/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "langflow",
|
||||
"version": "1.4.1",
|
||||
"version": "1.3.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "langflow",
|
||||
"version": "1.4.1",
|
||||
"version": "1.3.4",
|
||||
"dependencies": {
|
||||
"@chakra-ui/number-input": "^2.1.2",
|
||||
"@headlessui/react": "^2.0.4",
|
||||
|
|
|
|||
38
src/frontend/src/icons/TwelveLabs/TL-Symbol.svg
Normal file
38
src/frontend/src/icons/TwelveLabs/TL-Symbol.svg
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 204 146.6">
|
||||
<!-- Generator: Adobe Illustrator 29.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #1d1c1b;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="st0" x="43.9" y="50.3" width="64.3" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" y="50.3" width="35.3" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="124.1" y="50.3" width="40.3" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="129.9" y="37.8" width="34.5" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="168.9" y="37.8" width="27.3" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="157.3" y="25" width="31.1" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="167.1" y="12.5" width="9.2" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="74.3" y="112.6" width="15.9" height="9" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="101.8" y="112.6" width="10.4" height="9" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="117" y="112.6" width="28" height="9" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="131" y="100.1" width="11.6" height="9" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="52.4" y="112.6" width="9.2" height="9" rx="2.6" ry="2.6"/>
|
||||
<path class="st0" d="M94.7,127.7c0-1.4,1.1-2.6,2.6-2.6h4c1.4,0,2.6,1.1,2.6,2.6v3.9c0,1.4-1.1,2.6-2.6,2.6h-4c-1.4,0-2.6-1.1-2.6-2.6v-3.9Z"/>
|
||||
<rect class="st0" x="85.8" y="137.6" width="8.7" height="9" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="120.4" width="11.4" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="55.8" y="37.8" width="29" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="109.7" y="12.5" width="17.6" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="98.8" y="25" width="28.5" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="187.4" y="50.3" width="16.6" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="30.6" y="62.8" width="82.1" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="105.1" y="87.8" width="32.1" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="43.9" y="75.3" width="104.3" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="27.9" y="87.8" width="38.8" height="8.7" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="63.3" y="100.1" width="12.7" height="9" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="108.1" y="100.1" width="13.7" height="9" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="39.8" y="100.1" width="12.9" height="9" rx="2.6" ry="2.6"/>
|
||||
<rect class="st0" x="124.1" y="62.8" width="33.1" height="8.7" rx="2.6" ry="2.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
249
src/frontend/src/icons/TwelveLabs/TwelveLabsLogo.jsx
Normal file
249
src/frontend/src/icons/TwelveLabs/TwelveLabsLogo.jsx
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
const SvgTwelveLogo = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 204 146.6"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="43.9"
|
||||
y="50.3"
|
||||
width="64.3"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
y="50.3"
|
||||
width="35.3"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="124.1"
|
||||
y="50.3"
|
||||
width="40.3"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="129.9"
|
||||
y="37.8"
|
||||
width="34.5"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="168.9"
|
||||
y="37.8"
|
||||
width="27.3"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="157.3"
|
||||
y="25"
|
||||
width="31.1"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="167.1"
|
||||
y="12.5"
|
||||
width="9.2"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="74.3"
|
||||
y="112.6"
|
||||
width="15.9"
|
||||
height="9"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="101.8"
|
||||
y="112.6"
|
||||
width="10.4"
|
||||
height="9"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="117"
|
||||
y="112.6"
|
||||
width="28"
|
||||
height="9"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="131"
|
||||
y="100.1"
|
||||
width="11.6"
|
||||
height="9"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="52.4"
|
||||
y="112.6"
|
||||
width="9.2"
|
||||
height="9"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M94.7,127.7c0-1.4,1.1-2.6,2.6-2.6h4c1.4,0,2.6,1.1,2.6,2.6v3.9c0,1.4-1.1,2.6-2.6,2.6h-4c-1.4,0-2.6-1.1-2.6-2.6v-3.9Z"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="85.8"
|
||||
y="137.6"
|
||||
width="8.7"
|
||||
height="9"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="120.4"
|
||||
width="11.4"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="55.8"
|
||||
y="37.8"
|
||||
width="29"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="109.7"
|
||||
y="12.5"
|
||||
width="17.6"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="98.8"
|
||||
y="25"
|
||||
width="28.5"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="187.4"
|
||||
y="50.3"
|
||||
width="16.6"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="30.6"
|
||||
y="62.8"
|
||||
width="82.1"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="105.1"
|
||||
y="87.8"
|
||||
width="32.1"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="43.9"
|
||||
y="75.3"
|
||||
width="104.3"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="27.9"
|
||||
y="87.8"
|
||||
width="38.8"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="63.3"
|
||||
y="100.1"
|
||||
width="12.7"
|
||||
height="9"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="108.1"
|
||||
y="100.1"
|
||||
width="13.7"
|
||||
height="9"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="39.8"
|
||||
y="100.1"
|
||||
width="12.9"
|
||||
height="9"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
x="124.1"
|
||||
y="62.8"
|
||||
width="33.1"
|
||||
height="8.7"
|
||||
rx="2.6"
|
||||
ry="2.6"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SvgTwelveLogo;
|
||||
9
src/frontend/src/icons/TwelveLabs/index.tsx
Normal file
9
src/frontend/src/icons/TwelveLabs/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgTwelveLogo from "./TwelveLabsLogo";
|
||||
|
||||
export const TwelveLabsIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <SvgTwelveLogo ref={ref} {...props} />;
|
||||
});
|
||||
230
src/frontend/src/icons/eagerIconImports.ts
Normal file
230
src/frontend/src/icons/eagerIconImports.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
import { AgentQLIcon } from "@/icons/AgentQL";
|
||||
import { AIMLIcon } from "@/icons/AIML";
|
||||
import { AirbyteIcon } from "@/icons/Airbyte";
|
||||
import { AnthropicIcon } from "@/icons/Anthropic";
|
||||
import { ApifyIcon, ApifyWhiteIcon } from "@/icons/Apify";
|
||||
import { ArizeIcon } from "@/icons/Arize";
|
||||
import { ArXivIcon } from "@/icons/ArXiv";
|
||||
import { AssemblyAIIcon } from "@/icons/AssemblyAI";
|
||||
import { AstraDBIcon } from "@/icons/AstraDB";
|
||||
import { AthenaIcon } from "@/icons/athena/index";
|
||||
import { AWSIcon } from "@/icons/AWS";
|
||||
import { AWSInvertedIcon } from "@/icons/AWSInverted";
|
||||
import { AzureIcon } from "@/icons/Azure";
|
||||
import { BingIcon } from "@/icons/Bing";
|
||||
import { BotMessageSquareIcon } from "@/icons/BotMessageSquare";
|
||||
import { BWPythonIcon } from "@/icons/BW python";
|
||||
import { CassandraIcon } from "@/icons/Cassandra";
|
||||
import { ChromaIcon } from "@/icons/ChromaIcon";
|
||||
import { ClickhouseIcon } from "@/icons/Clickhouse";
|
||||
import { CloudflareIcon } from "@/icons/Cloudflare";
|
||||
import { CohereIcon } from "@/icons/Cohere";
|
||||
import { ComposioIcon } from "@/icons/Composio";
|
||||
import { ConfluenceIcon } from "@/icons/Confluence";
|
||||
import { CouchbaseIcon } from "@/icons/Couchbase";
|
||||
import { CrewAiIcon } from "@/icons/CrewAI";
|
||||
import { DeepSeekIcon } from "@/icons/DeepSeek";
|
||||
import { DropboxIcon } from "@/icons/Dropbox";
|
||||
import { DuckDuckGoIcon } from "@/icons/DuckDuckGo";
|
||||
import { ElasticsearchIcon } from "@/icons/ElasticsearchStore";
|
||||
import { EvernoteIcon } from "@/icons/Evernote";
|
||||
import { ExaIcon } from "@/icons/Exa";
|
||||
import { FBIcon } from "@/icons/FacebookMessenger";
|
||||
import { FirecrawlIcon } from "@/icons/Firecrawl";
|
||||
import { freezeAllIcon } from "@/icons/freezeAll";
|
||||
import { GitBookIcon } from "@/icons/GitBook";
|
||||
import { GitLoaderIcon } from "@/icons/GitLoader";
|
||||
import { GleanIcon } from "@/icons/Glean";
|
||||
import { GlobeOkIcon } from "@/icons/globe-ok";
|
||||
import { GmailIcon } from "@/icons/gmail";
|
||||
import { GoogleIcon } from "@/icons/Google";
|
||||
import { GoogleDriveIcon } from "@/icons/GoogleDrive";
|
||||
import { GoogleGenerativeAIIcon } from "@/icons/GoogleGenerativeAI";
|
||||
import {
|
||||
GradientInfinity,
|
||||
GradientSave,
|
||||
GradientUngroup,
|
||||
} from "@/icons/GradientSparkles";
|
||||
import { GridHorizontalIcon } from "@/icons/GridHorizontal";
|
||||
import { GroqIcon } from "@/icons/Groq";
|
||||
import { HackerNewsIcon } from "@/icons/hackerNews";
|
||||
import { HCDIcon } from "@/icons/HCD";
|
||||
import { HomeAssistantIcon } from "@/icons/HomeAssistant";
|
||||
import { HuggingFaceIcon } from "@/icons/HuggingFace";
|
||||
import { WatsonxAiIcon } from "@/icons/IBMWatsonx";
|
||||
import { IcosaIcon } from "@/icons/Icosa";
|
||||
import { IFixIcon } from "@/icons/IFixIt";
|
||||
import { JSIcon } from "@/icons/JSicon";
|
||||
import { LangChainIcon } from "@/icons/LangChain";
|
||||
import { LangwatchIcon } from "@/icons/Langwatch";
|
||||
import { LMStudioIcon } from "@/icons/LMStudio";
|
||||
import { MaritalkIcon } from "@/icons/Maritalk";
|
||||
import { Mem0 } from "@/icons/Mem0";
|
||||
import { MetaIcon } from "@/icons/Meta";
|
||||
import { MidjourneyIcon } from "@/icons/Midjorney";
|
||||
import { MilvusIcon } from "@/icons/Milvus";
|
||||
import { MistralIcon } from "@/icons/mistral";
|
||||
import { MongoDBIcon } from "@/icons/MongoDB";
|
||||
import { NeedleIcon } from "@/icons/Needle";
|
||||
import { NotDiamondIcon } from "@/icons/NotDiamond";
|
||||
import { NotionIcon } from "@/icons/Notion";
|
||||
import { NovitaIcon } from "@/icons/Novita";
|
||||
import { NvidiaIcon } from "@/icons/Nvidia";
|
||||
import { OlivyaIcon } from "@/icons/Olivya";
|
||||
import { OllamaIcon } from "@/icons/Ollama";
|
||||
import { OneDriveIcon } from "@/icons/OneDrive";
|
||||
import { OpenAiIcon } from "@/icons/OpenAi";
|
||||
import { OpenRouterIcon } from "@/icons/OpenRouter";
|
||||
import { OpenSearch } from "@/icons/OpenSearch";
|
||||
import { PerplexityIcon } from "@/icons/Perplexity";
|
||||
import { PineconeIcon } from "@/icons/Pinecone";
|
||||
import { PostgresIcon } from "@/icons/Postgres";
|
||||
import { PythonIcon } from "@/icons/Python";
|
||||
import { QDrantIcon } from "@/icons/QDrant";
|
||||
import { QianFanChatIcon } from "@/icons/QianFanChat";
|
||||
import { RedisIcon } from "@/icons/Redis";
|
||||
import { SambaNovaIcon } from "@/icons/SambaNova";
|
||||
import { ScrapeGraph } from "@/icons/ScrapeGraphAI";
|
||||
import { SearchAPIIcon } from "@/icons/SearchAPI";
|
||||
import { SearchHybridIcon } from "@/icons/SearchHybrid";
|
||||
import { SearchLexicalIcon } from "@/icons/SearchLexical";
|
||||
import { SearchVectorIcon } from "@/icons/SearchVector";
|
||||
import { SearxIcon } from "@/icons/Searx";
|
||||
import { SerperIcon } from "@/icons/Serper";
|
||||
import { SerpSearchIcon } from "@/icons/SerpSearch";
|
||||
import { ShareIcon } from "@/icons/Share";
|
||||
import { Share2Icon } from "@/icons/Share2";
|
||||
import { SlackIcon } from "@/icons/Slack";
|
||||
import { SpiderIcon } from "@/icons/Spider";
|
||||
import { Streamlit } from "@/icons/Streamlit";
|
||||
import { SupabaseIcon } from "@/icons/supabase";
|
||||
import { TavilyIcon } from "@/icons/Tavily";
|
||||
import { ThumbDownIconCustom, ThumbUpIconCustom } from "@/icons/thumbs";
|
||||
import { TwelveLabsIcon } from "@/icons/TwelveLabs";
|
||||
import { UnstructuredIcon } from "@/icons/Unstructured";
|
||||
import { UpstashSvgIcon } from "@/icons/Upstash";
|
||||
import { VectaraIcon } from "@/icons/VectaraIcon";
|
||||
import { VertexAIIcon } from "@/icons/VertexAI";
|
||||
import { WeaviateIcon } from "@/icons/Weaviate";
|
||||
import { WikipediaIcon } from "@/icons/Wikipedia";
|
||||
import { WolframIcon } from "@/icons/Wolfram";
|
||||
import { XAIIcon } from "@/icons/xAI";
|
||||
import { YouTubeSvgIcon as YouTubeIcon } from "@/icons/Youtube";
|
||||
import { ZepMemoryIcon } from "@/icons/ZepMemory";
|
||||
|
||||
// Export the eagerly loaded icons map
|
||||
export const eagerIconsMapping = {
|
||||
"AI/ML": AIMLIcon,
|
||||
AgentQL: AgentQLIcon,
|
||||
Airbyte: AirbyteIcon,
|
||||
Anthropic: AnthropicIcon,
|
||||
Apify: ApifyIcon,
|
||||
ApifyWhite: ApifyWhiteIcon,
|
||||
ArXiv: ArXivIcon,
|
||||
Arize: ArizeIcon,
|
||||
AssemblyAI: AssemblyAIIcon,
|
||||
AstraDB: AstraDBIcon,
|
||||
Athena: AthenaIcon,
|
||||
AWS: AWSIcon,
|
||||
AWSInverted: AWSInvertedIcon,
|
||||
Azure: AzureIcon,
|
||||
Bing: BingIcon,
|
||||
BotMessageSquare: BotMessageSquareIcon,
|
||||
BWPython: BWPythonIcon,
|
||||
Cassandra: CassandraIcon,
|
||||
Chroma: ChromaIcon,
|
||||
Clickhouse: ClickhouseIcon,
|
||||
Cloudflare: CloudflareIcon,
|
||||
Cohere: CohereIcon,
|
||||
Composio: ComposioIcon,
|
||||
Confluence: ConfluenceIcon,
|
||||
Couchbase: CouchbaseIcon,
|
||||
CrewAI: CrewAiIcon,
|
||||
DeepSeek: DeepSeekIcon,
|
||||
Dropbox: DropboxIcon,
|
||||
DuckDuckGo: DuckDuckGoIcon,
|
||||
ElasticsearchStore: ElasticsearchIcon,
|
||||
Evernote: EvernoteIcon,
|
||||
Exa: ExaIcon,
|
||||
FacebookMessenger: FBIcon,
|
||||
Firecrawl: FirecrawlIcon,
|
||||
FreezeAll: freezeAllIcon,
|
||||
GitBook: GitBookIcon,
|
||||
GitLoader: GitLoaderIcon,
|
||||
Glean: GleanIcon,
|
||||
GlobeOk: GlobeOkIcon,
|
||||
Google: GoogleIcon,
|
||||
GoogleDrive: GoogleDriveIcon,
|
||||
GoogleGenerativeAI: GoogleGenerativeAIIcon,
|
||||
Gmail: GmailIcon,
|
||||
GradientInfinity: GradientInfinity,
|
||||
GradientSave: GradientSave,
|
||||
GradientUngroup: GradientUngroup,
|
||||
GridHorizontal: GridHorizontalIcon,
|
||||
Groq: GroqIcon,
|
||||
HackerNews: HackerNewsIcon,
|
||||
HCD: HCDIcon,
|
||||
HomeAssistant: HomeAssistantIcon,
|
||||
HuggingFace: HuggingFaceIcon,
|
||||
Icosa: IcosaIcon,
|
||||
IFixIt: IFixIcon,
|
||||
javascript: JSIcon,
|
||||
LangChain: LangChainIcon,
|
||||
Langwatch: LangwatchIcon,
|
||||
LMStudio: LMStudioIcon,
|
||||
Maritalk: MaritalkIcon,
|
||||
Mem0: Mem0,
|
||||
Meta: MetaIcon,
|
||||
Midjourney: MidjourneyIcon,
|
||||
Milvus: MilvusIcon,
|
||||
Mistral: MistralIcon,
|
||||
MongoDB: MongoDBIcon,
|
||||
Needle: NeedleIcon,
|
||||
NotDiamond: NotDiamondIcon,
|
||||
Notion: NotionIcon,
|
||||
Novita: NovitaIcon,
|
||||
NVIDIA: NvidiaIcon,
|
||||
Olivya: OlivyaIcon,
|
||||
Ollama: OllamaIcon,
|
||||
OneDrive: OneDriveIcon,
|
||||
OpenAI: OpenAiIcon,
|
||||
OpenRouter: OpenRouterIcon,
|
||||
OpenSearch: OpenSearch,
|
||||
Perplexity: PerplexityIcon,
|
||||
Pinecone: PineconeIcon,
|
||||
Postgres: PostgresIcon,
|
||||
Python: PythonIcon,
|
||||
QDrant: QDrantIcon,
|
||||
QianFanChat: QianFanChatIcon,
|
||||
Redis: RedisIcon,
|
||||
SambaNova: SambaNovaIcon,
|
||||
ScrapeGraph: ScrapeGraph,
|
||||
SearchAPI: SearchAPIIcon,
|
||||
SearchLexical: SearchLexicalIcon,
|
||||
SearchHybrid: SearchHybridIcon,
|
||||
SearchVector: SearchVectorIcon,
|
||||
Searx: SearxIcon,
|
||||
SerpSearch: SerpSearchIcon,
|
||||
Serper: SerperIcon,
|
||||
Share: ShareIcon,
|
||||
Share2: Share2Icon,
|
||||
Slack: SlackIcon,
|
||||
Spider: SpiderIcon,
|
||||
Streamlit: Streamlit,
|
||||
Supabase: SupabaseIcon,
|
||||
Tavily: TavilyIcon,
|
||||
ThumbDownCustom: ThumbDownIconCustom,
|
||||
ThumbUpCustom: ThumbUpIconCustom,
|
||||
TwelveLabs: TwelveLabsIcon,
|
||||
Unstructured: UnstructuredIcon,
|
||||
Upstash: UpstashSvgIcon,
|
||||
Vectara: VectaraIcon,
|
||||
VertexAI: VertexAIIcon,
|
||||
WatsonxAI: WatsonxAiIcon,
|
||||
Weaviate: WeaviateIcon,
|
||||
Wikipedia: WikipediaIcon,
|
||||
Wolfram: WolframIcon,
|
||||
xAI: XAIIcon,
|
||||
YouTube: YouTubeIcon,
|
||||
ZepMemory: ZepMemoryIcon,
|
||||
};
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { TwelveLabsIcon } from "./TwelveLabs";
|
||||
|
||||
// Export the lazy loading mapping for icons
|
||||
export const lazyIconsMapping = {
|
||||
"AI/ML": () =>
|
||||
|
|
@ -258,6 +260,10 @@ export const lazyIconsMapping = {
|
|||
import("@/icons/thumbs").then((mod) => ({
|
||||
default: mod.ThumbUpIconCustom,
|
||||
})),
|
||||
TwelveLabs: () =>
|
||||
import("@/icons/TwelveLabs").then((mod) => ({
|
||||
default: mod.TwelveLabsIcon,
|
||||
})),
|
||||
TwitterX: () =>
|
||||
import("@/icons/Twitter X").then((mod) => ({
|
||||
default: mod.TwitterXIcon,
|
||||
|
|
|
|||
|
|
@ -265,6 +265,7 @@ export const SIDEBAR_BUNDLES = [
|
|||
{ display_name: "Mem0", name: "mem0", icon: "Mem0" },
|
||||
{ display_name: "Youtube", name: "youtube", icon: "YouTube" },
|
||||
{ display_name: "ScrapeGraph AI", name: "scrapegraph", icon: "ScrapeGraph" },
|
||||
{ display_name: "Twelve Labs", name: "twelvelabs", icon: "TwelveLabs" },
|
||||
{
|
||||
display_name: "Home Assistant",
|
||||
name: "homeassistant",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue