feat(llm): add TwelveLabs Pegasus as an optional content-aware clipping backend#165
Conversation
…ng backend Pegasus reasons over the actual video (visuals + audio) instead of only the ASR transcript, so it can pick highlight segments even when the transcript is ambiguous. Wired in as an opt-in 'pegasus' model prefix in llm_inference; it returns segments in the existing 'N. [start-end] text' format so the AI Clip pipeline works unchanged. Adds twelvelabs to requirements and a key-gated test (skips without TWELVELABS_API_KEY).
There was a problem hiding this comment.
Code Review
This pull request integrates TwelveLabs Pegasus as an optional video-understanding LLM backend for content-aware clipping in FunClip, updating the UI, requirements, and documentation accordingly. The review feedback highlights a critical timestamp format mismatch between Pegasus's seconds-based output and FunClip's expected SRT format, suggesting a conversion helper to resolve it. Additionally, the reviewer recommends wrapping the Pegasus inference call in a try-except block for better error handling in the UI and adding a corresponding unit test for the timestamp conversion logic.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| def call_twelvelabs_pegasus(apikey, | ||
| video, | ||
| model="pegasus1.5", | ||
| prompt=None, | ||
| max_tokens=2048): | ||
| """Run TwelveLabs Pegasus content-aware analysis over a video. | ||
|
|
||
| Unlike the ASR-text based LLM backends, Pegasus reasons over the actual | ||
| video (visuals + audio), which lets it pick highlight segments even when | ||
| the transcript alone is ambiguous. Returns Pegasus' text response, which | ||
| is expected to follow the "N. [start-end] text" segment format. | ||
|
|
||
| Args: | ||
| apikey: TwelveLabs API key (https://twelvelabs.io, free tier available). | ||
| video: a public video URL or a local video file path. | ||
| model: Pegasus model name, e.g. "pegasus1.5". | ||
| prompt: optional extra instruction appended to the default prompt. | ||
| max_tokens: max tokens for the Pegasus response. | ||
| """ | ||
| from twelvelabs import TwelveLabs | ||
|
|
||
| client = TwelveLabs(api_key=apikey or os.environ.get("TWELVELABS_API_KEY")) | ||
| video_context = _resolve_video_context(client, video) | ||
|
|
||
| full_prompt = PEGASUS_SYSTEM_PROMPT | ||
| if prompt is not None and len(prompt.strip()): | ||
| full_prompt = full_prompt + "\n\n" + prompt.strip() | ||
|
|
||
| response = client.analyze( | ||
| model_name=model, | ||
| video=video_context, | ||
| prompt=full_prompt, | ||
| max_tokens=max_tokens, | ||
| ) | ||
| logging.info("TwelveLabs Pegasus inference done.") | ||
| return response.data |
There was a problem hiding this comment.
Critical Bug: Timestamp Format Mismatch
There is a critical format mismatch between the output of the Pegasus model and the expectations of FunClip's extract_timestamps() function:
PEGASUS_SYSTEM_PROMPTinstructs the model to return timestamps in seconds (e.g.,[12.5-15.0]).extract_timestamps()infunclip/utils/trans_utils.pyuses a strict regular expression\[(\d{2}:\d{2}:\d{2},\d{2,3})\s*-\s*(\d{2}:\d{2}:\d{2},\d{2,3})\]which only matches standard SRT timestamp formats (e.g.,[00:00:12,500-00:00:15,000]).
Because of this, extract_timestamps() will always return an empty list for Pegasus's output, and the clipping pipeline will fail to extract any segments.
To fix this without breaking other backends, we can intercept the Pegasus response text and convert any seconds-based timestamps (e.g., [12.5-15.0]) into the standard [HH:MM:SS,mmm-HH:MM:SS,mmm] format before returning it. Additionally, we should explicitly validate the presence of the API key to prevent generic errors.
def _convert_seconds_to_hhmmss_mmm(seconds_str):
try:
seconds = float(seconds_str)
except ValueError:
return seconds_str
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
millis = int(round((seconds - int(seconds)) * 1000))
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
def _convert_response_timestamps(text):
import re
if not isinstance(text, str):
return text
def repl(match):
start_seq = match.group(1)
end_seq = match.group(2)
return f"[{_convert_seconds_to_hhmmss_mmm(start_seq)}-{_convert_seconds_to_hhmmss_mmm(end_seq)}]"
return re.sub(r'\[(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\]', repl, text)
def call_twelvelabs_pegasus(apikey,
video,
model="pegasus1.5",
prompt=None,
max_tokens=2048):
"""Run TwelveLabs Pegasus content-aware analysis over a video.
Unlike the ASR-text based LLM backends, Pegasus reasons over the actual
video (visuals + audio), which lets it pick highlight segments even when
the transcript alone is ambiguous. Returns Pegasus' text response, which
is expected to follow the "N. [start-end] text" segment format.
Args:
apikey: TwelveLabs API key (https://twelvelabs.io, free tier available).
video: a public video URL or a local video file path.
model: Pegasus model name, e.g. "pegasus1.5".
prompt: optional extra instruction appended to the default prompt.
max_tokens: max tokens for the Pegasus response.
"""
from twelvelabs import TwelveLabs
api_key = apikey or os.environ.get("TWELVELABS_API_KEY")
if not api_key:
raise ValueError("TwelveLabs API key is required. Please provide it via the UI or set the TWELVELABS_API_KEY environment variable.")
client = TwelveLabs(api_key=api_key)
video_context = _resolve_video_context(client, video)
full_prompt = PEGASUS_SYSTEM_PROMPT
if prompt is not None and len(prompt.strip()):
full_prompt = full_prompt + "\n\n" + prompt.strip()
response = client.analyze(
model_name=model,
video=video_context,
prompt=full_prompt,
max_tokens=max_tokens,
)
logging.info("TwelveLabs Pegasus inference done.")
return _convert_response_timestamps(response.data)| if model.startswith('pegasus'): | ||
| # TwelveLabs Pegasus reasons over the actual video (visuals + audio) | ||
| # rather than the ASR transcript, so it needs the video source. | ||
| if video_input is None: | ||
| logging.error("Pegasus requires a video input; please upload a video first.") | ||
| return "Please upload a video before running Pegasus inference." | ||
| return call_twelvelabs_pegasus(apikey, video_input, model=model, prompt=system_content) |
There was a problem hiding this comment.
Robustness: Add Exception Handling for Pegasus Inference
Unlike standard text-based LLM APIs, the Pegasus pipeline involves video asset uploads, processing, and polling, which have more potential failure modes (e.g., network issues, invalid files, or API key errors). Wrapping the call in a try-except block ensures that any exceptions are caught and a friendly error message is returned to the Gradio UI instead of crashing the thread.
| if model.startswith('pegasus'): | |
| # TwelveLabs Pegasus reasons over the actual video (visuals + audio) | |
| # rather than the ASR transcript, so it needs the video source. | |
| if video_input is None: | |
| logging.error("Pegasus requires a video input; please upload a video first.") | |
| return "Please upload a video before running Pegasus inference." | |
| return call_twelvelabs_pegasus(apikey, video_input, model=model, prompt=system_content) | |
| if model.startswith('pegasus'): | |
| # TwelveLabs Pegasus reasons over the actual video (visuals + audio) | |
| # rather than the ASR transcript, so it needs the video source. | |
| if video_input is None: | |
| logging.error("Pegasus requires a video input; please upload a video first.") | |
| return "Please upload a video before running Pegasus inference." | |
| try: | |
| return call_twelvelabs_pegasus(apikey, video_input, model=model, prompt=system_content) | |
| except Exception as e: | |
| logging.error(f"Pegasus inference failed: {e}") | |
| return f"Error running Pegasus inference: {e}" |
| def test_default_prompt_requests_parseable_segment_format(self): | ||
| self.assertIn("[start_time-end_time]", PEGASUS_SYSTEM_PROMPT) |
There was a problem hiding this comment.
Testing: Add Unit Test for Timestamp Conversion
To ensure the new timestamp conversion logic works correctly and handles edge cases (such as single-digit seconds, decimals, and multiple segments), we should add a dedicated unit test.
| def test_default_prompt_requests_parseable_segment_format(self): | |
| self.assertIn("[start_time-end_time]", PEGASUS_SYSTEM_PROMPT) | |
| def test_default_prompt_requests_parseable_segment_format(self): | |
| self.assertIn("[start_time-end_time]", PEGASUS_SYSTEM_PROMPT) | |
| def test_timestamp_conversion(self): | |
| from llm.twelvelabs_api import _convert_response_timestamps | |
| raw_response = "1. [12.5-15.0] description\n2. [120.45-135] another segment" | |
| expected = "1. [00:00:12,500-00:00:15,000] description\n2. [00:02:00,450-00:02:15,000] another segment" | |
| self.assertEqual(_convert_response_timestamps(raw_response), expected) |
|
Thanks @mohit-twelvelabs — this is a clean, well-scoped contribution. 🙌 I reviewed and verified it locally:
The value is real: the existing backends reason over the ASR transcript only, while Pegasus reasons over the actual video (visuals + audio), so content-aware clipping can use cues a transcript can't capture. Merging — thanks for adding it! |
Hi! I'm Mohit, I work at TwelveLabs (@mohit-twelvelabs).
What this adds
An optional content-aware clipping backend for the LLM step, powered by TwelveLabs Pegasus (a video understanding model).
The existing LLM backends (qwen/gpt/g4f/deepseek) reason over the ASR transcript only. Pegasus reasons over the actual video — visuals and audio — so it can pick good highlight segments even when the transcript alone is ambiguous (action shots, scene changes, on-screen events, music, etc.).
It plugs into the same
llm_inferencedispatch via a newpegasusmodel prefix and returns segments in FunClip's existingN. [start-end] textformat, so the 'AI Clip' button andextract_timestamps()work unchanged.Why it helps FunClip
FunClip's smart-clipping is currently transcript-bound. Pegasus extends "LLM-based clipping" to genuinely multimodal clip selection without changing the UX: same flow, same output format, just a richer signal for choosing what's worth keeping.
Opt-in / non-breaking
funclip/llm/twelvelabs_api.py; no existing backend touched.pegasus1.5added to the model dropdown; the default model is unchanged.twelvelabsis only imported lazily inside the function, so the new dep is only needed if you actually select Pegasus.How it was tested
TWELVELABS_API_KEY) that runs Pegasus over a public sample video and asserts text is returned — verified locally end to end againsttwelvelabs==1.2.8; the model returned correctly-formatted segments parseable byextract_timestamps().You can grab a free API key at https://twelvelabs.io — there's a generous free tier.