feat: Twelve Labs Bundle (#7837)

* feat: add twelve labs components

* fix: fix uv.lock for twelve labs components
This commit is contained in:
Will 2025-05-13 11:56:29 -07:00 • committed by GitHub
commit 327c0fd791
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 4038 additions and 1715 deletions

View file

@ -123,6 +123,7 @@ dependencies = [
"langchain-ibm>=0.3.8",
"opik>=1.6.3",
"openai>=1.68.2",
"twelvelabs>=0.4.7",
]
[dependency-groups]

View 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",
]

View file

@ -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 "")

View 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

View 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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View 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

View file

@ -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",

View 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

View 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;

View 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} />;
});

View 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,
};

View file

@ -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,

View file

@ -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",

3768
uv.lock generated

File diff suppressed because it is too large Load diff