Coverage for bot/byte_semantics_reply.py: 85%
62 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-25 12:30 -0300
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-25 12:30 -0300
1import re
3from bot.byte_semantics_base import compact_message, format_chat_reply, normalize_text_for_scene
4from bot.byte_semantics_constants import (
5 MAX_CHAT_MESSAGE_LENGTH,
6 MOVIE_TITLE_QUOTE_PATTERN,
7 MULTIPART_SEPARATOR,
8)
11def split_text_for_chat(
12 text: str, max_len: int = MAX_CHAT_MESSAGE_LENGTH, max_parts: int = 2
13) -> list[str]:
14 clean_text = (text or "").strip()
15 if not clean_text:
16 return []
17 if len(clean_text) <= max_len:
18 return [clean_text]
20 parts: list[str] = []
21 remaining = clean_text
22 while remaining and len(parts) < max_parts:
23 if len(remaining) <= max_len:
24 parts.append(remaining.strip())
25 remaining = ""
26 break
28 cut = remaining.rfind("\n", 0, max_len + 1)
29 if cut < int(max_len * 0.5):
30 sentence_cuts = [
31 remaining.rfind(symbol, 0, max_len + 1) for symbol in (". ", "? ", "! ")
32 ]
33 cut = max(sentence_cuts)
34 if cut < int(max_len * 0.5):
35 cut = remaining.rfind(" ", 0, max_len + 1)
36 if cut <= 0:
37 cut = max_len
39 chunk = remaining[:cut].strip()
40 if chunk:
41 parts.append(chunk)
42 remaining = remaining[cut:].strip()
44 if remaining:
45 if parts:
46 parts[-1] = compact_message(f"{parts[-1]} {remaining}", max_len=max_len)
47 else:
48 parts.append(compact_message(remaining, max_len=max_len))
50 return [part for part in parts if part][:max_parts]
53def extract_multi_reply_parts(answer_text: str, max_parts: int = 2) -> list[str]:
54 if not answer_text:
55 return []
57 raw_parts = [
58 segment.strip() for segment in answer_text.split(MULTIPART_SEPARATOR) if segment.strip()
59 ]
60 if len(raw_parts) >= 2:
61 normalized = [format_chat_reply(part) for part in raw_parts[:max_parts]]
62 return [part for part in normalized if part]
64 chunks = split_text_for_chat(answer_text, max_len=MAX_CHAT_MESSAGE_LENGTH, max_parts=max_parts)
65 normalized = [format_chat_reply(chunk) for chunk in chunks]
66 return [part for part in normalized if part]
69def extract_movie_title(prompt: str) -> str:
70 quote_match = MOVIE_TITLE_QUOTE_PATTERN.search(prompt or "")
71 if quote_match:
72 return normalize_text_for_scene(quote_match.group("title"), max_len=80)
74 candidate_match = re.search(
75 r"ficha\s*t[eé]cnica(?:\s+(?:do|da|de)\s+filme)?(?:\s+(?:do|da|de))?\s+(?P<title>.+)$",
76 prompt or "",
77 re.IGNORECASE,
78 )
79 if not candidate_match:
80 return ""
82 candidate = candidate_match.group("title").strip(" ?!.:,;")
83 if not candidate:
84 return ""
86 lowered = candidate.lower()
87 generic_fragments = (
88 "que estamos vendo",
89 "que estamos assistindo",
90 "que estamos assistindo hoje",
91 "agora",
92 )
93 if any(fragment in lowered for fragment in generic_fragments):
94 return ""
95 if lowered in {"filme", "hoje", "agora"}:
96 return ""
98 return normalize_text_for_scene(candidate, max_len=80)
101def build_movie_fact_sheet_query(movie_title: str) -> str:
102 return (
103 f"Monte uma ficha tecnica objetiva do filme '{movie_title}' para chat Twitch. "
104 "Responda em no maximo 4 linhas, sem markdown, com alta densidade e sem enrolacao. "
105 "Formato obrigatorio: "
106 "1) Titulo | Ano | Pais. "
107 "2) Direcao | Genero | Duracao. "
108 "3) Elenco principal (ate 3 nomes). "
109 "4) Nota media | Premios-chave (ou N/D). "
110 "Se nao houver confianca em algum dado, escreva N/D."
111 )