maulana-m commited on
Commit
5891917
·
1 Parent(s): 15a9e23

initial commit

Browse files
app.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from spotlight.core.downloader import Downloader
2
+ from spotlight.core.service import SpotlightService
3
+ from spotlight.core.llm import GeminiApi
4
+ from spotlight.core.dto import SpotlightRequest
5
+ from spotlight.core.exceptions import VideoUrlInvalidError
6
+ import asyncio
7
+ import gradio as gr
8
+ import json
9
+
10
+ CSS = """
11
+ .spotlight-item {
12
+ display: flex;
13
+ flex-direction: column; /* Stack topic name above the video */
14
+ align-items: center; /* Center the content horizontally */
15
+ border: 1px solid #ddd;
16
+ padding: 15px; /* Increased padding */
17
+ margin-bottom: 25px; /* Increased margin for spacing */
18
+ border-radius: 8px;
19
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
20
+ width: 350px; /* Set a fixed width for consistency */
21
+ text-align: center; /* Center the text in the topic name */
22
+ }
23
+
24
+ .spotlight-topic {
25
+ font-size: 1.2em;
26
+ font-weight: bold;
27
+ margin-bottom: 10px; /* Increased margin */
28
+ color: #28598a; /* A nicer color */
29
+ word-wrap: break-word; /* Handle long topic names */
30
+ }
31
+
32
+ .spotlight-video {
33
+ /* Add some margin around the iframe, if needed */
34
+ margin-bottom: 5px;
35
+ }
36
+
37
+ /* Optional: If you want the spotlight items to display in a row */
38
+ .gradio-container {
39
+ display: flex; /* Use flexbox to arrange items in a row */
40
+ flex-wrap: wrap; /* Allow items to wrap to the next line if they don't fit */
41
+ justify-content: center; /* Distribute items evenly */
42
+ }
43
+ """
44
+
45
+ spotlight_service = SpotlightService(
46
+ _downloader=Downloader(),
47
+ llm=GeminiApi()
48
+ )
49
+
50
+ async def run_splotlight(video_url, lang):
51
+ try:
52
+ request = SpotlightRequest(
53
+ video_url=video_url,
54
+ lang=lang
55
+ )
56
+ except VideoUrlInvalidError as e:
57
+ raise gr.Error("Video Url is invalid")
58
+ spotlights = await spotlight_service.run(request)
59
+
60
+
61
+ html_output = ""
62
+ for row in spotlights:
63
+ html_content = f"""
64
+ <div class="spotlight-item">
65
+ <div class="spotlight-topic">{row["topic_name"]}</div>
66
+ <div class="spotlight-video">
67
+ <iframe width="320" height="180" src="{row["embed_url"]}" frameborder="0" allowfullscreen></iframe>
68
+ </div>
69
+ </div>
70
+ """
71
+ html_output += html_content
72
+
73
+ return html_output
74
+
75
+
76
+
77
+ with gr.Blocks(css=CSS) as demo:
78
+ video_url = gr.Textbox(label="Enter youtube url")
79
+ lang = gr.Textbox(label="Language")
80
+ run_button = gr.Button("Run", variant="primary")
81
+ output_html = gr.HTML(label="Output")
82
+
83
+ run_button.click(run_splotlight, [video_url, lang], [output_html])
84
+
85
+ demo.queue(max_size=10).launch()
requirements.txt ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile pyproject.toml -o requirements.txt
3
+ aiofiles==24.1.0
4
+ # via gradio
5
+ annotated-types==0.7.0
6
+ # via pydantic
7
+ anyio==4.9.0
8
+ # via
9
+ # google-genai
10
+ # gradio
11
+ # httpx
12
+ # starlette
13
+ audioop-lts==0.2.1
14
+ # via gradio
15
+ cachetools==5.5.2
16
+ # via google-auth
17
+ certifi==2025.4.26
18
+ # via
19
+ # httpcore
20
+ # httpx
21
+ # requests
22
+ charset-normalizer==3.4.2
23
+ # via requests
24
+ click==8.1.8
25
+ # via
26
+ # typer
27
+ # uvicorn
28
+ fastapi==0.115.12
29
+ # via
30
+ # spotlight-segment (pyproject.toml)
31
+ # gradio
32
+ ffmpy==0.5.0
33
+ # via gradio
34
+ filelock==3.18.0
35
+ # via huggingface-hub
36
+ fsspec==2025.5.1
37
+ # via
38
+ # gradio-client
39
+ # huggingface-hub
40
+ google-auth==2.40.2
41
+ # via google-genai
42
+ google-genai==1.16.1
43
+ # via spotlight-segment (pyproject.toml)
44
+ gradio==5.31.0
45
+ # via spotlight-segment (pyproject.toml)
46
+ gradio-client==1.10.1
47
+ # via gradio
48
+ groovy==0.1.2
49
+ # via gradio
50
+ h11==0.16.0
51
+ # via
52
+ # httpcore
53
+ # uvicorn
54
+ hf-xet==1.1.2
55
+ # via huggingface-hub
56
+ httpcore==1.0.9
57
+ # via httpx
58
+ httpx==0.28.1
59
+ # via
60
+ # google-genai
61
+ # gradio
62
+ # gradio-client
63
+ # safehttpx
64
+ huggingface-hub==0.32.0
65
+ # via
66
+ # gradio
67
+ # gradio-client
68
+ idna==3.10
69
+ # via
70
+ # anyio
71
+ # httpx
72
+ # requests
73
+ jinja2==3.1.6
74
+ # via gradio
75
+ markdown-it-py==3.0.0
76
+ # via rich
77
+ markupsafe==3.0.2
78
+ # via
79
+ # gradio
80
+ # jinja2
81
+ mdurl==0.1.2
82
+ # via markdown-it-py
83
+ numpy==2.2.6
84
+ # via
85
+ # gradio
86
+ # pandas
87
+ orjson==3.10.18
88
+ # via gradio
89
+ packaging==25.0
90
+ # via
91
+ # gradio
92
+ # gradio-client
93
+ # huggingface-hub
94
+ pandas==2.2.3
95
+ # via gradio
96
+ pillow==11.2.1
97
+ # via gradio
98
+ pyasn1==0.6.1
99
+ # via
100
+ # pyasn1-modules
101
+ # rsa
102
+ pyasn1-modules==0.4.2
103
+ # via google-auth
104
+ pydantic==2.11.5
105
+ # via
106
+ # spotlight-segment (pyproject.toml)
107
+ # fastapi
108
+ # google-genai
109
+ # gradio
110
+ # pydantic-settings
111
+ pydantic-core==2.33.2
112
+ # via pydantic
113
+ pydantic-settings==2.9.1
114
+ # via spotlight-segment (pyproject.toml)
115
+ pydub==0.25.1
116
+ # via gradio
117
+ pygments==2.19.1
118
+ # via rich
119
+ python-dateutil==2.9.0.post0
120
+ # via pandas
121
+ python-dotenv==1.1.0
122
+ # via pydantic-settings
123
+ python-multipart==0.0.20
124
+ # via gradio
125
+ pytz==2025.2
126
+ # via pandas
127
+ pyyaml==6.0.2
128
+ # via
129
+ # gradio
130
+ # huggingface-hub
131
+ requests==2.32.3
132
+ # via
133
+ # google-genai
134
+ # huggingface-hub
135
+ rich==14.0.0
136
+ # via typer
137
+ rsa==4.9.1
138
+ # via google-auth
139
+ ruff==0.11.11
140
+ # via gradio
141
+ safehttpx==0.1.6
142
+ # via gradio
143
+ semantic-version==2.10.0
144
+ # via gradio
145
+ shellingham==1.5.4
146
+ # via typer
147
+ six==1.17.0
148
+ # via python-dateutil
149
+ sniffio==1.3.1
150
+ # via anyio
151
+ starlette==0.46.2
152
+ # via
153
+ # fastapi
154
+ # gradio
155
+ tomlkit==0.13.2
156
+ # via gradio
157
+ tqdm==4.67.1
158
+ # via huggingface-hub
159
+ typer==0.15.4
160
+ # via gradio
161
+ typing-extensions==4.13.2
162
+ # via
163
+ # fastapi
164
+ # google-genai
165
+ # gradio
166
+ # gradio-client
167
+ # huggingface-hub
168
+ # pydantic
169
+ # pydantic-core
170
+ # typer
171
+ # typing-inspection
172
+ typing-inspection==0.4.1
173
+ # via
174
+ # pydantic
175
+ # pydantic-settings
176
+ tzdata==2025.2
177
+ # via pandas
178
+ urllib3==2.4.0
179
+ # via requests
180
+ uvicorn==0.34.2
181
+ # via
182
+ # spotlight-segment (pyproject.toml)
183
+ # gradio
184
+ websockets==15.0.1
185
+ # via
186
+ # google-genai
187
+ # gradio-client
188
+ yt-dlp==2025.5.22
189
+ # via spotlight-segment (pyproject.toml)
spotlight/__init__.py ADDED
File without changes
spotlight/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (164 Bytes). View file
 
spotlight/api/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from spotlight.api.routers import segments
2
+ from fastapi import FastAPI
3
+ from fastapi.exceptions import RequestValidationError
4
+ from spotlight.api.error_handlers import (
5
+ request_validation_handler,
6
+ bad_request_exception_handler,
7
+ subtitle_not_found_handler
8
+ )
9
+ from spotlight.core.exceptions import VideoUrlInvalidError, SubtitleNotFoundError
10
+
11
+
12
+ app = FastAPI()
13
+ app.include_router(segments.router)
14
+ app.exception_handler(RequestValidationError)(request_validation_handler)
15
+ app.exception_handler(VideoUrlInvalidError)(bad_request_exception_handler)
16
+ app.exception_handler(SubtitleNotFoundError)(subtitle_not_found_handler)
spotlight/api/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (945 Bytes). View file
 
spotlight/api/__pycache__/error_handlers.cpython-313.pyc ADDED
Binary file (2.97 kB). View file
 
spotlight/api/error_handlers.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request
2
+ from fastapi.exceptions import RequestValidationError
3
+ from starlette.exceptions import HTTPException
4
+ from fastapi.responses import JSONResponse
5
+ from typing import Optional, List
6
+ import re
7
+ import http
8
+ import logging
9
+
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def _camel_to_snake(name):
15
+ name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
16
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).upper()
17
+
18
+
19
+ def error_response(error: Exception, error_code: int = 500, descriptions: Optional[List] = None):
20
+ if descriptions is None:
21
+ descriptions = []
22
+
23
+ status = http.HTTPStatus(error_code).name
24
+
25
+ if isinstance(error, HTTPException):
26
+ name = error.detail.replace(" ", "")
27
+ error_code = error.status_code
28
+ else:
29
+ name = error.__class__.__name__
30
+
31
+ error_message = [
32
+ {"field": desc[0], "message": desc[1]} for desc in descriptions
33
+ ]
34
+
35
+ return JSONResponse(
36
+ dict(
37
+ status=status,
38
+ error=_camel_to_snake(name),
39
+ code=error_code,
40
+ descriptions=error_message,
41
+ ),
42
+ status_code=error_code,
43
+ )
44
+
45
+
46
+ async def bad_request_exception_handler(_, error: Exception):
47
+ return error_response(error, 400)
48
+
49
+
50
+ async def subtitle_not_found_handler(_, error: Exception):
51
+ return error_response(error, 404)
52
+
53
+
54
+ async def request_validation_handler(_: Request, error: RequestValidationError):
55
+ validation_errors = [(".".join(str(x) for x in e["loc"]), e["type"])
56
+ for e in error.errors()]
57
+ return error_response(error, 400, validation_errors)
spotlight/api/routers/__pycache__/segments.cpython-313.pyc ADDED
Binary file (1.15 kB). View file
 
spotlight/api/routers/segments.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.routing import APIRouter
2
+ from fastapi import Request
3
+ from fastapi.responses import JSONResponse
4
+ from spotlight.core.downloader import Downloader
5
+ from spotlight.core.service import SpotlightService
6
+ from spotlight.core.llm import GeminiApi
7
+ from spotlight.core.dto import SpotlightRequest
8
+
9
+
10
+ router = APIRouter()
11
+
12
+ spotlight_service = SpotlightService(
13
+ _downloader=Downloader(),
14
+ llm=GeminiApi()
15
+ )
16
+
17
+
18
+ @router.post("/segments")
19
+ async def segments(request: Request, spotlight_request: SpotlightRequest):
20
+ response = await spotlight_service.run(spotlight_request)
21
+
22
+ return JSONResponse(
23
+ dict(data=response, status="success")
24
+ )
spotlight/cli/__init__.py ADDED
File without changes
spotlight/cli/main.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from spotlight.core.downloader import Downloader
2
+ from spotlight.core.service import SpotlightService
3
+ from spotlight.core.llm import GeminiApi
4
+ from spotlight.core.dto import SpotlightRequest
5
+ import click
6
+ import asyncio
7
+
8
+
9
+ spotlight_service = SpotlightService(
10
+ _downloader=Downloader(),
11
+ llm=GeminiApi()
12
+ )
13
+
14
+
15
+ @click.command()
16
+ @click.option("--video_url", help="video youtube link")
17
+ @click.option("--lang", help="language output")
18
+ def run(video_url, lang):
19
+
20
+ """Spotlight segment: Cli program to extract important segments for any youtube video"""
21
+
22
+ async def main(video_url, lang):
23
+ request = SpotlightRequest(
24
+ video_url=video_url,
25
+ lang=lang
26
+ )
27
+ output = await spotlight_service.run(request)
28
+ print(output)
29
+
30
+ if video_url is None and lang is None:
31
+ click.echo(click.get_current_context().get_help())
32
+ else:
33
+ asyncio.run(main(video_url, lang))
34
+
35
+
36
+ if __name__ == '__main__':
37
+ run()
spotlight/core/__pycache__/config.cpython-313.pyc ADDED
Binary file (923 Bytes). View file
 
spotlight/core/__pycache__/constant.cpython-313.pyc ADDED
Binary file (2.09 kB). View file
 
spotlight/core/__pycache__/downloader.cpython-313.pyc ADDED
Binary file (3.41 kB). View file
 
spotlight/core/__pycache__/dto.cpython-313.pyc ADDED
Binary file (1.63 kB). View file
 
spotlight/core/__pycache__/exceptions.cpython-313.pyc ADDED
Binary file (874 Bytes). View file
 
spotlight/core/__pycache__/llm.cpython-313.pyc ADDED
Binary file (1.46 kB). View file
 
spotlight/core/__pycache__/service.cpython-313.pyc ADDED
Binary file (3.73 kB). View file
 
spotlight/core/config.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+
3
+
4
+ class GeneralConfig(BaseSettings):
5
+ GEMINI_API_KEY: str = ""
6
+ LLM_MODEL: str = "gemini-2.0-flash"
7
+ DEFAULT_LANGUAGE: str = "en"
8
+
9
+ class Config:
10
+ env_prefix = "GENERAL_CONFIG_"
11
+
12
+
13
+ GENERAL_CONFIG = GeneralConfig()
spotlight/core/constant.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PROMPT_TEMPLATE = """
2
+ You are an AI assistant tasked with analyzing video subtitle XML data and identifying engaging segments for viewers.
3
+
4
+ Task:
5
+ 1. Analyze the XML subtitle data: Parse the provided XML to extract subtitle text and their corresponding start and end timestamps.
6
+ 2. Identify "interesting parts": Segment the subtitles into meaningful and engaging segments. Consider the following factors when determining what makes a segment "interesting":
7
+ - Emotional impact: Segments containing humor, drama, suspense, or other strong emotional content.
8
+ - Key information: Segments that convey crucial plot points, character development, or explanations.
9
+ - Intrigue and cliffhangers: Segments that leave the viewer wanting more.
10
+ - Adjust the segment based on duration video. dont take the short duration if the video is long enough. with long video it is usually has segment that have medium - long duration (e,g > 20 second - 60 second)
11
+ - Contextual Relevance: Take the language parameter into consideration and tailor the topic titles accordingly.
12
+ 3. Generate Descriptive Topic Titles:
13
+ - For each identified segment, create a concise and compelling topic title in the specified {{language}}}. These titles should be designed to attract viewers and give them a clear idea of the segment's content.
14
+ - Focus on evocative language and avoid generic descriptions.
15
+ - DONT use language id in the output. only output topic title without additional langunge informationn
16
+
17
+ Output JSON Format: Return the segmented data in a JSON format as follows:
18
+ [
19
+ {
20
+ "topic": "Engaging Topic Title",
21
+ "start_timestamp": "00:01:30", // Example: HH:MM:SS
22
+ "end_timestamp": "00:01:45" // Example: HH:MM:SS
23
+ },
24
+ {
25
+ "topic": "Another Captivating Title",
26
+ "start_timestamp": "00:02:00",
27
+ "end_timestamp": "00:02:10"
28
+ }
29
+ // ... more segments
30
+ ]
31
+
32
+ Input:
33
+ Language: {{language}}
34
+ Subtitle XML: {{xml_subtitle}}
35
+ """
spotlight/core/downloader.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from yt_dlp import YoutubeDL
2
+ from yt_dlp.utils import DownloadError
3
+ from spotlight.core.exceptions import SubtitleNotFoundError
4
+ from spotlight.core.config import GENERAL_CONFIG
5
+ from spotlight.core.dto import VideoInfo
6
+ from typing import Dict, Any
7
+ import json
8
+ import requests
9
+ import logging
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class Downloader:
16
+ def __init__(self, params: Dict[str, Any] = {"quiet": True}):
17
+ self.client = YoutubeDL(params)
18
+
19
+ def get_video_info(self, video_url: str) -> dict:
20
+ try:
21
+ info = self.client.extract_info(video_url, download=False, process=False)
22
+ except DownloadError as e:
23
+ info = {}
24
+
25
+ return info
26
+
27
+ def save_info(self, info: Dict[str, Any], filename: str) -> None:
28
+ with open(filename, "w") as f:
29
+ f.write(json.dumps(info, indent=4))
30
+
31
+ def get_subtitle_content(self, subtitle_url: str):
32
+ response = requests.get(subtitle_url)
33
+ if response.status_code == 200:
34
+ return response.text
35
+ else:
36
+ logger.warning("Download subtitle failed")
37
+ return None
38
+
39
+ def get_subtitle_video(self, video_url: str) -> VideoInfo:
40
+ video_info = self.get_video_info(video_url)
41
+ # self.save_info(video_info, "video_info.json")
42
+ automatic_captions = video_info.get("automatic_captions", {})
43
+
44
+ # set english as default caption
45
+ # TODO customize based on input languange
46
+ origin_captions = automatic_captions.get(GENERAL_CONFIG.DEFAULT_LANGUAGE)
47
+ if origin_captions is None:
48
+ raise SubtitleNotFoundError("Subtitle not found")
49
+
50
+ subtitle_url = None
51
+ for subtitle_format in origin_captions:
52
+ if subtitle_format.get("ext") == "ttml":
53
+ subtitle_url = subtitle_format.get("url")
54
+
55
+ if subtitle_url:
56
+ subtitle_content = self.get_subtitle_content(subtitle_url)
57
+ else:
58
+ subtitle_content = None
59
+
60
+ return VideoInfo(video_id=video_info.get("id"), subtitle=subtitle_content)
spotlight/core/dto.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, field_validator, ValidationError
2
+ from spotlight.core.exceptions import VideoUrlInvalidError
3
+ import re
4
+
5
+
6
+ class SpotlightSchema(BaseModel):
7
+ topic_name: str
8
+ start_time: str
9
+ end_time: str
10
+
11
+
12
+ class VideoInfo(BaseModel):
13
+ video_id: str
14
+ subtitle: str
15
+
16
+
17
+ class SpotlightRequest(BaseModel):
18
+ video_url: str
19
+ lang: str
20
+
21
+ @field_validator("video_url")
22
+ def is_valid_youtube_url(cls, value):
23
+ pattern = r"^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.*[\w-]+\/?$"
24
+ match = re.match(pattern, value)
25
+ if not bool(match):
26
+ raise VideoUrlInvalidError("URL must be a youtube URL")
27
+ return value
spotlight/core/exceptions.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ class VideoUrlInvalidError(Exception):
2
+ def __init__(self, msg):
3
+ self.msg = msg
4
+
5
+
6
+ class SubtitleNotFoundError(Exception):
7
+ def __init__(self, msg):
8
+ self.msg = msg
spotlight/core/llm.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from spotlight.core.config import GENERAL_CONFIG
2
+ from google import genai
3
+ from google.genai import types
4
+
5
+
6
+ class GeminiApi:
7
+ def __init__(self):
8
+ self.client = genai.Client(api_key=GENERAL_CONFIG.GEMINI_API_KEY)
9
+
10
+ async def generate_completion(self, prompt: str, model: str, response_mime_type: str, response_schema=None):
11
+ config = types.GenerateContentConfig(
12
+ response_mime_type=response_mime_type,
13
+ response_schema=response_schema
14
+ )
15
+
16
+ response = await self.client.aio.models.generate_content(
17
+ model=model,
18
+ contents=prompt,
19
+ config=config
20
+ )
21
+
22
+ return response
spotlight/core/service.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from spotlight.core.downloader import Downloader
2
+ from spotlight.core.llm import GeminiApi
3
+ from spotlight.core.dto import SpotlightRequest, SpotlightSchema
4
+ from spotlight.core.constant import PROMPT_TEMPLATE
5
+ from spotlight.core.config import GENERAL_CONFIG
6
+
7
+
8
+ EMBED_URL = "https://youtube.com/embed/{id}?&start={start_time}&end={end_time}&autoplay=1"
9
+
10
+
11
+ class SpotlightService:
12
+ def __init__(self, _downloader: Downloader, llm: GeminiApi):
13
+ self.downloader = _downloader
14
+ self.llm = llm
15
+
16
+ async def run(self, splotlight_request: SpotlightRequest):
17
+ video_url = splotlight_request.video_url
18
+ lang = splotlight_request.lang
19
+
20
+ video_info = self.downloader.get_subtitle_video(video_url)
21
+ prompt = self.construct_prompt(lang, video_info.subtitle)
22
+
23
+ response_schema = list[SpotlightSchema]
24
+
25
+ response_llm = await self.llm.generate_completion(
26
+ prompt=prompt,
27
+ model=GENERAL_CONFIG.LLM_MODEL,
28
+ response_mime_type="application/json",
29
+ response_schema=response_schema
30
+ )
31
+ response = self._parse_response(response_llm, video_info.video_id)
32
+
33
+ return response
34
+
35
+ def construct_prompt(self, language: str, xml_subtitle: str) -> str:
36
+ prompt = (PROMPT_TEMPLATE
37
+ .replace("{{language}}", language)
38
+ .replace("{{xml_subtitle}}", xml_subtitle)
39
+ )
40
+
41
+ return prompt
42
+
43
+ def _parse_response(self, response_llm, video_id):
44
+ response = [x.model_dump() for x in response_llm.parsed]
45
+
46
+ for data in response:
47
+ data["embed_url"] = self._embed_timeline(video_id, data["start_time"], data["end_time"])
48
+
49
+ return response
50
+
51
+ def _embed_timeline(self, video_id: str, start_time: str, end_time: str) -> str:
52
+ def time_to_second(time_str: str):
53
+ hours, minutes, seconds = time_str.split(':')
54
+ seconds = seconds[:2]
55
+ return int(hours) * 3600 + int(minutes) * 60 + int(seconds)
56
+
57
+ embed_url = EMBED_URL.format(
58
+ id=video_id,
59
+ start_time=time_to_second(start_time),
60
+ end_time=time_to_second(end_time)
61
+ )
62
+
63
+ return embed_url