Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.1.0
4.3.0
1 change: 1 addition & 0 deletions bot/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class ClipType(Enum):
SELECTED = "selected"
ADJUSTED = "adjusted"
SINGLE = "single"
TIKTAK = "tiktak"


@dataclass
Expand Down
2 changes: 2 additions & 0 deletions bot/factory/subscribed_permission_level_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
SendClipHandler,
SerialContextHandler,
SnapClipHandler,
TikTakHandler,
TranscriptionHandler,
)
from bot.middlewares import (
Expand Down Expand Up @@ -80,6 +81,7 @@ def _create_handler_classes(self) -> List[Type[BotMessageHandler]]:
SendClipHandler,
SerialContextHandler,
SnapClipHandler,
TikTakHandler,
TranscriptionHandler,
]

Expand Down
39 changes: 31 additions & 8 deletions bot/handlers/not_sending_videos/semantic_search_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@
get_no_query_provided_message,
)
from bot.search.filter_applicator import FilterApplicator
from bot.search.semantic_segments_finder import SemanticSearchMode
from bot.utils.constants import EpisodeMetadataKeys
from bot.search.semantic_segments_finder import (
SemanticSearchMode,
SemanticSegmentsFinder,
)
from bot.settings import settings
from bot.utils.constants import (
EpisodeMetadataKeys,
SegmentKeys,
)


class SemanticSearchHandler(SemanticBotHandler):
Expand Down Expand Up @@ -88,12 +95,14 @@ async def _handle_semantic_results(

unique = self._deduplicate_semantic_results(results, mode)

if mode != SemanticSearchMode.EPISODE:
await DatabaseManager.insert_last_search(
chat_id=self._message.get_chat_id(),
quote=query,
segments=json.dumps(unique),
)
if mode == SemanticSearchMode.EPISODE:
unique = await self.__enrich_episode_results_with_clip_times(unique, active_series)

await DatabaseManager.insert_last_search(
chat_id=self._message.get_chat_id(),
quote=query,
segments=json.dumps(unique),
)

response = self.__format_response(unique, query, mode)

Expand All @@ -105,6 +114,20 @@ async def _handle_semantic_results(
),
)

async def __enrich_episode_results_with_clip_times(
self, episodes: list, active_series: str,
) -> list:
episode_ids = [ep.get("episode_id") for ep in episodes if ep.get("episode_id")]
first_times = await SemanticSegmentsFinder.fetch_first_dialogue_times(
episode_ids, active_series, self._logger,
)
for ep in episodes:
episode_id = ep.get("episode_id", "")
first_time = first_times.get(episode_id, 0.0)
ep[SegmentKeys.START_TIME] = first_time
ep[SegmentKeys.END_TIME] = first_time + settings.SENSODCINEK_PREVIEW_DURATION_SEC
return episodes

@staticmethod
def __format_response(unique: list, query: str, mode: SemanticSearchMode) -> str:
if mode == SemanticSearchMode.FRAMES:
Expand Down
1 change: 1 addition & 0 deletions bot/handlers/sending_videos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
from bot.handlers.sending_videos.semantic_clip_handler import SemanticClipHandler
from bot.handlers.sending_videos.send_clip_handler import SendClipHandler
from bot.handlers.sending_videos.snap_clip_handler import SnapClipHandler
from bot.handlers.sending_videos.tiktak_handler import TikTakHandler
119 changes: 119 additions & 0 deletions bot/handlers/sending_videos/tiktak_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import json
import logging
from typing import List

from bot.database.database_manager import DatabaseManager
from bot.database.models import (
ClipType,
LastClip,
)
from bot.handlers.bot_message_handler import BotMessageHandler
from bot.responses.sending_videos.tiktak_handler_responses import (
get_no_last_clip_message,
get_tiktak_compiled_note,
get_tiktak_no_detections_note,
get_tiktak_success_log,
)
from bot.settings import settings
from bot.utils.constants import (
EpisodeMetadataKeys,
SegmentKeys,
)
from bot.video.tiktak_processor import TikTakProcessor


class TikTakHandler(BotMessageHandler):
def get_commands(self) -> List[str]:
return ["tiktak", "tt"]

async def _do_handle(self) -> None:
msg = self._message
chat_id = msg.get_chat_id()

last_clip = await DatabaseManager.get_last_clip_by_chat_id(chat_id)
if not last_clip:
return await self._reply_error(get_no_last_clip_message())

if last_clip.clip_type == ClipType.COMPILED and last_clip.compiled_clip:
return await self.__handle_compiled(last_clip)

return await self.__handle_single(last_clip)

async def __handle_compiled(self, last_clip: LastClip) -> None:
output = await TikTakProcessor.process_compiled(
last_clip.compiled_clip,
self._logger,
)
await self._responder.send_markdown(get_tiktak_compiled_note())
await self._responder.send_video(output, duration=None)
await DatabaseManager.insert_last_clip(
chat_id=self._message.get_chat_id(),
segment=json.loads(last_clip.segment) if isinstance(last_clip.segment, str) else last_clip.segment,
compiled_clip=None,
clip_type=ClipType.TIKTAK,
adjusted_start_time=last_clip.adjusted_start_time,
adjusted_end_time=last_clip.adjusted_end_time,
is_adjusted=last_clip.is_adjusted,
)
return await self._log_system_message(
logging.INFO,
get_tiktak_success_log(self._message.get_username()),
)

async def __handle_single(self, last_clip: LastClip) -> None:
segment = json.loads(last_clip.segment) if isinstance(last_clip.segment, str) else last_clip.segment
video_path = segment.get(SegmentKeys.VIDEO_PATH)
start_time = last_clip.adjusted_start_time or float(segment.get(SegmentKeys.START_TIME, 0))
end_time = last_clip.adjusted_end_time or float(segment.get(SegmentKeys.END_TIME, 0))

episode_metadata = segment.get(EpisodeMetadataKeys.EPISODE_METADATA, {})
season = episode_metadata.get(EpisodeMetadataKeys.SEASON)
episode_number = episode_metadata.get(EpisodeMetadataKeys.EPISODE_NUMBER)
series_name = episode_metadata.get(EpisodeMetadataKeys.SERIES_NAME, "")

had_detections = self.__has_detections(
series_name, season, episode_number, settings.TIKTAK_DETECTION_DIR,
)

output = await TikTakProcessor.process_single(
video_path=video_path,
start_time=start_time,
end_time=end_time,
season=season or 0,
episode_number=episode_number or 0,
series_name=series_name,
detection_dir=settings.TIKTAK_DETECTION_DIR,
logger=self._logger,
)

if not had_detections:
await self._responder.send_markdown(get_tiktak_no_detections_note())

duration = end_time - start_time
await self._responder.send_video(output, duration=duration)

await DatabaseManager.insert_last_clip(
chat_id=self._message.get_chat_id(),
segment=segment,
compiled_clip=None,
clip_type=ClipType.TIKTAK,
adjusted_start_time=start_time,
adjusted_end_time=end_time,
is_adjusted=last_clip.is_adjusted,
)
return await self._log_system_message(
logging.INFO,
get_tiktak_success_log(self._message.get_username()),
)

@staticmethod
def __has_detections(
series_name: str,
season: int,
episode_number: int,
detection_dir: str,
) -> bool:
if season is None or episode_number is None:
return False
det_path = TikTakProcessor._detection_file_path(series_name, season, episode_number, detection_dir)
return det_path is not None and det_path.exists()
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
)

from bot.responses.bot_response import BotResponse
from bot.utils.constants import EpisodeMetadataKeys
from bot.utils.constants import (
EpisodeMetadataKeys,
SegmentKeys,
)
from bot.utils.functions import (
convert_number_to_emoji,
format_segment,
Expand Down Expand Up @@ -83,8 +86,16 @@ def format_semantic_episodes_response(
else:
ep_fmt = f"S{str(season).zfill(2)}E{str(episode_num).zfill(2)}"

start_time = ep.get(SegmentKeys.START_TIME)
if start_time is not None:
minutes = int(start_time) // 60
seconds = int(start_time) % 60
clip_info = f" | klip od {minutes}:{seconds:02d}"
else:
clip_info = ""

line = (
f"{convert_number_to_emoji(i)} | 📺 {ep_fmt}\n"
f"{convert_number_to_emoji(i)} | 📺 {ep_fmt}{clip_info}\n"
f" 👉 {title}"
)
episode_lines.append(line)
Expand Down
23 changes: 23 additions & 0 deletions bot/responses/sending_videos/tiktak_handler_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from bot.responses.bot_response import BotResponse


def get_no_last_clip_message() -> str:
return BotResponse.error("BRAK KLIPU", "Najpierw wyszukaj klip, a potem użyj /tiktak")


def get_tiktak_success_log(username: str) -> str:
return f"TikTak clip generated for user '{username}'"


def get_tiktak_compiled_note() -> str:
return BotResponse.warning(
"KOMPILACJA",
"Dla kompilacji zastosowano statyczne kadrowanie centralne (brak danych detekcji).",
)


def get_tiktak_no_detections_note() -> str:
return BotResponse.warning(
"BRAK DETEKCJI",
"Nie znaleziono danych o osobach - zastosowano kadrowanie centralne.",
)
43 changes: 43 additions & 0 deletions bot/search/semantic_segments_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,49 @@ def deduplicate_segments(segments: List[Dict[str, Any]]) -> List[Dict[str, Any]]
unique.append(seg)
return unique

@staticmethod
async def fetch_first_dialogue_times(
episode_ids: List[str],
series_name: str,
logger: logging.Logger,
) -> Dict[str, float]:
if not episode_ids:
return {}
es = await ElasticSearchManager.connect_to_elasticsearch(logger)
index = f"{series_name}{ElasticsearchIndexSuffixes.TEXT_SEGMENTS}"
query = {
"query": {"terms": {"episode_id": episode_ids}},
"size": 0,
"aggs": {
"per_episode": {
"terms": {"field": "episode_id", "size": len(episode_ids)},
"aggs": {
"first_segment": {
"top_hits": {
"sort": [{"start_time": {"order": "asc"}}],
"size": 1,
"_source": ["start_time"],
},
},
},
},
},
}
try:
response = await es.search(index=index, body=query)
except Exception: # pylint: disable=broad-except
await log_system_message(logging.WARNING, "Failed to fetch first dialogue times.", logger)
return {}

result: Dict[str, float] = {}
buckets = response.get("aggregations", {}).get("per_episode", {}).get("buckets", [])
for bucket in buckets:
episode_id = bucket.get("key")
hits = bucket.get("first_segment", {}).get("hits", {}).get("hits", [])
if episode_id and hits:
result[episode_id] = hits[0]["_source"].get("start_time", 0.0)
return result

@staticmethod
def deduplicate_episodes(episodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
seen = set()
Expand Down
3 changes: 3 additions & 0 deletions bot/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ class Settings(BaseSettings):
REST_API_APP_PATH: str = Field("bot.platforms.rest_runner:app")
DISABLE_RATE_LIMITING: bool = Field(False)

TIKTAK_DETECTION_DIR: str = Field("")
SENSODCINEK_PREVIEW_DURATION_SEC: int = Field(30)

VLLM_HOST: str = Field("http://localhost:11435")
VLLM_EMBEDDINGS_MODEL: str = Field("qwen3vl-embed")
VLLM_TIMEOUT_SECONDS: int = Field(30)
Expand Down
Loading
Loading