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

1import re 

2 

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) 

9 

10 

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] 

19 

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 

27 

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 

38 

39 chunk = remaining[:cut].strip() 

40 if chunk: 

41 parts.append(chunk) 

42 remaining = remaining[cut:].strip() 

43 

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)) 

49 

50 return [part for part in parts if part][:max_parts] 

51 

52 

53def extract_multi_reply_parts(answer_text: str, max_parts: int = 2) -> list[str]: 

54 if not answer_text: 

55 return [] 

56 

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] 

63 

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] 

67 

68 

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) 

73 

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 "" 

81 

82 candidate = candidate_match.group("title").strip(" ?!.:,;") 

83 if not candidate: 

84 return "" 

85 

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 "" 

97 

98 return normalize_text_for_scene(candidate, max_len=80) 

99 

100 

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 )