Skip to content

Commit 39bc683

Browse files
author
Xiaoyi
committed
add lite mode to support audio-focused youtube video
1 parent afa10cc commit 39bc683

6 files changed

Lines changed: 116 additions & 26 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ This repository contains the official implementation of the paper [Deep Video Di
99
![image](https://github.com/user-attachments/assets/ac1c7f0a-3c10-4c4c-88d1-7bfe0e2010e1)
1010

1111

12+
## Update
13+
14+
- **2025/07/16**: Add `lite_mode` to enable a lightweight version of the agent that uses only subtitles. Good for Youtube podcast analysis!
15+
- **2025/07/14**: Support OpenAI API and Azure OpenAI API.
16+
- **2025/07/08**: Initial release of the Deep Video Discovery codebase.
17+
1218
## Introduction
1319

1420
**Deep Video Discovery (DVD)** is a deep-research style question answering agent designed for understanding extra-long videos. Leveraging the powerful capabilities of large language models (LLMs), DVD effectively interprets and processes extensive video content to answer complex user queries.

dvd/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
AOAI_EMBEDDING_LARGE_DIM = 3072
2525

2626
# ------------------ agent and tool setting ------------------ #
27+
LITE_MODE = True # if True, only leverage srt subtitle, no pixel downloaded or pixel captioning
2728
GLOBAL_BROWSE_TOPK = 300
2829
OVERWRITE_CLIP_SEARCH_TOPK = 0 # 0 means no overwrite and let agent decide
2930

dvd/dvd_core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ def finish(answer: A[str, D("Answer to the user's question.")]) -> None:
2626
class DVDCoreAgent:
2727
def __init__(self, video_db_path, video_caption_path, max_iterations):
2828
self.tools = [frame_inspect_tool, clip_search_tool, global_browse_tool, finish]
29+
if config.LITE_MODE:
30+
self.tools.remove(frame_inspect_tool)
2931
self.name_to_function_map = {tool.__name__: tool for tool in self.tools}
3032
self.function_schemas = [
3133
{"function": as_json_schema(func), "type": "function"}

dvd/frame_caption.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ def _caption_clip(task: Tuple[str, Dict], caption_ckpt_folder) -> Tuple[str, dic
240240
try:
241241
assert isinstance(resp, str), f"Response must be a JSON string instead of {type(resp)}:{resp}."
242242
parsed = json.loads(resp)
243+
parsed["clip_description"] += f"\n\nTranscript during this video clip: {transcript}." # add transcript to description
244+
resp = json.dumps(parsed)
243245
with open(os.path.join(caption_ckpt_folder, f"{timestamp}.json"), "w") as f:
244246
f.write(resp)
245247
return timestamp, parsed
@@ -329,6 +331,25 @@ def process_video(
329331
json.dump(frame_captions, f, indent=4)
330332

331333

334+
def process_video_lite(
335+
output_caption_folder: str,
336+
subtitle_file_path: str,
337+
):
338+
"""
339+
Process video in LITE_MODE using SRT subtitles.
340+
"""
341+
captions = parse_srt_to_dict(subtitle_file_path)
342+
frame_captions = {}
343+
for key, text in captions.items():
344+
frame_captions[key] = {
345+
"caption": f"\n\nTranscript during this video clip: {text}.",
346+
}
347+
frame_captions["subject_registry"] = {}
348+
with open(
349+
os.path.join(output_caption_folder, "captions.json"), "w"
350+
) as f:
351+
json.dump(frame_captions, f, indent=4)
352+
332353
# --------------------------------------------------------------------------- #
333354
# main #
334355
# --------------------------------------------------------------------------- #

dvd/video_utils.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,51 @@ def load_video(
9999
)
100100
shutil.copy2(subtitle_source, subtitle_destination)
101101

102-
return os.path.abspath(destination_path)
102+
def download_srt_subtitle(video_url: str, output_path: str):
103+
"""Downloads an SRT subtitle from a YouTube URL."""
104+
if not _is_youtube_url(video_url):
105+
raise ValueError("Provided URL is not a valid YouTube link.")
106+
107+
output_dir = os.path.dirname(output_path)
108+
os.makedirs(output_dir, exist_ok=True)
109+
110+
ydl_opts = {
111+
'writesubtitles': True,
112+
'subtitleslangs': ['en'],
113+
'subtitlesformat': 'srt',
114+
'skip_download': True,
115+
'outtmpl': os.path.join(output_dir, '%(id)s.%(ext)s'),
116+
}
117+
118+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
119+
info = ydl.extract_info(video_url, download=False)
120+
video_id = info['id']
121+
ydl.download([video_url])
122+
123+
# Locate the downloaded subtitle file (yt-dlp names them as <id>.<lang>.srt)
124+
downloaded_subtitle_path = None
125+
for f in os.listdir(output_dir):
126+
if f.startswith(video_id) and f.endswith(".srt"):
127+
downloaded_subtitle_path = os.path.join(output_dir, f)
128+
break
129+
130+
if downloaded_subtitle_path:
131+
shutil.move(downloaded_subtitle_path, output_path)
132+
else:
133+
# Try auto-generated subtitles
134+
ydl_opts['writeautomaticsub'] = True
135+
with yt_dlp.YoutubeDL(ydl_opts) as ydl_auto:
136+
ydl_auto.download([video_url])
137+
138+
if os.path.exists(os.path.join(output_dir, f"{video_id}.en.vtt")):
139+
# yt-dlp might download as .vtt and convert, check for final .srt
140+
for f in os.listdir(output_dir):
141+
if f.startswith(video_id) and f.endswith('.srt'):
142+
shutil.move(os.path.join(output_dir, f), output_path)
143+
return
144+
145+
raise FileNotFoundError(f"Could not find SRT subtitle for {video_url}")
103146

104-
# ------------------- Not found -------------------
105-
raise FileNotFoundError(f"Video source '{video_source}' not found or is not a valid URL.")
106147

107148
def decode_video_to_frames(video_path: str) -> str:
108149
"""

local_run.py

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import os
33
import argparse
44
from dvd.dvd_core import DVDCoreAgent
5-
from dvd.video_utils import load_video, decode_video_to_frames
6-
from dvd.frame_caption import process_video
5+
from dvd.video_utils import load_video, decode_video_to_frames, download_srt_subtitle
6+
from dvd.frame_caption import process_video, process_video_lite
77
from dvd.utils import extract_answer
88

99
def main():
@@ -27,31 +27,50 @@ def main():
2727
frames_dir = os.path.join(config.VIDEO_DATABASE_FOLDER, video_id, "frames")
2828
captions_dir = os.path.join(config.VIDEO_DATABASE_FOLDER, video_id, "captions")
2929
video_db_path = os.path.join(config.VIDEO_DATABASE_FOLDER, video_id, "database.json")
30+
srt_path = os.path.join(config.VIDEO_DATABASE_FOLDER, video_id, "subtitles.srt")
3031

31-
# Download video
32-
if not os.path.exists(video_path):
33-
print(f"Downloading video from {video_url} to {video_path}...")
34-
load_video(video_url, video_path)
35-
print("Video downloaded.")
32+
if config.LITE_MODE:
33+
print("Running in LITE_MODE.")
34+
if not os.path.exists(srt_path):
35+
print(f"Downloading SRT subtitle for {video_url} to {srt_path}...")
36+
try:
37+
download_srt_subtitle(video_url, srt_path)
38+
print("SRT subtitle downloaded.")
39+
except Exception as e:
40+
print(f"Error downloading subtitle: {e}")
41+
print("Please turn off LITE_MODE and try again.")
42+
return
43+
else:
44+
print(f"SRT subtitle already exists at {srt_path}.")
45+
46+
# In LITE_MODE, we use srt as caption file
47+
process_video_lite(captions_dir, srt_path)
48+
caption_file = os.path.join(captions_dir, "captions.json")
3649
else:
37-
print(f"Video already exists at {video_path}.")
50+
# Download video
51+
if not os.path.exists(video_path):
52+
print(f"Downloading video from {video_url} to {video_path}...")
53+
load_video(video_url, video_path)
54+
print("Video downloaded.")
55+
else:
56+
print(f"Video already exists at {video_path}.")
3857

39-
# Decode video to frames
40-
if not os.path.exists(frames_dir) or not os.listdir(frames_dir):
41-
print(f"Decoding video to frames in {frames_dir}...")
42-
decode_video_to_frames(video_path)
43-
print("Video decoded.")
44-
else:
45-
print(f"Frames already exist in {frames_dir}.")
58+
# Decode video to frames
59+
if not os.path.exists(frames_dir) or not os.listdir(frames_dir):
60+
print(f"Decoding video to frames in {frames_dir}...")
61+
decode_video_to_frames(video_path)
62+
print("Video decoded.")
63+
else:
64+
print(f"Frames already exist in {frames_dir}.")
4665

47-
# Get captions
48-
caption_file = os.path.join(captions_dir, "captions.json")
49-
if not os.path.exists(caption_file):
50-
print("Processing video to get captions...")
51-
process_video(frames_dir, captions_dir)
52-
print("Captions generated.")
53-
else:
54-
print(f"Captions already exist at {caption_file}.")
66+
# Get captions
67+
caption_file = os.path.join(captions_dir, "captions.json")
68+
if not os.path.exists(caption_file):
69+
print("Processing video to get captions...")
70+
process_video(frames_dir, captions_dir)
71+
print("Captions generated.")
72+
else:
73+
print(f"Captions already exist at {caption_file}.")
5574

5675
# Initialize DVDCoreAgent
5776
print("Initializing DVDCoreAgent...")

0 commit comments

Comments
 (0)