@danhachuel/thunderbolt 0.6.65 → 0.6.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/main.py CHANGED
@@ -116,7 +116,7 @@ from hermes_ui.update_manager import check_version, restart_current_process, upd
116
116
 
117
117
  from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
118
118
  from hermes_ui.script_generation import generate_script_document
119
- from hermes_ui.video_length import DEFAULT_AVERAGE_VIDEO_TIME, channel_video_length, length_generation_settings, words_from_channel_time
119
+ from hermes_ui.video_length import DEFAULT_AVERAGE_VIDEO_TIME, channel_video_length, channel_video_time_value, length_generation_settings, words_from_channel_time
120
120
  from hermes_ui.voice_preview import DEFAULT_SAMPLE, load_preview_file, synthesize_preview
121
121
  from hermes_ui.elevenlabs_voices import ElevenLabsVoicesError, cached_personal_voices, fetch_personal_voices, personal_voice_options
122
122
  from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
@@ -2374,10 +2374,10 @@ def render_tiktok_channels():
2374
2374
  prompt_file = TIKTOK_PROMPT_MASTERS / str(channel.get("default_prompt_master") or channel.get("prompt_master") or "")
2375
2375
  prompt_content = load_prompt_master_file(prompt_file) if prompt_file.is_file() else ""
2376
2376
  calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel, prompt_master=prompt_content)
2377
- average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or DEFAULT_AVERAGE_VIDEO_TIME), key=f"tiktok_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
2377
+ average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"tiktok_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
2378
2378
  if st.button("Guardar tempo", key=f"save_tiktok_channel_average_video_time_{channel_id}", use_container_width=True):
2379
2379
  if not valid_hhmm(average_video_time):
2380
- st.error("Use o formato HH:MM, por exemplo 00:02.")
2380
+ st.error("Use o formato MM:SS, por exemplo 12:00.")
2381
2381
  else:
2382
2382
  update_channel(channel_id, {"average_video_time": average_video_time.strip() or DEFAULT_AVERAGE_VIDEO_TIME, "average_video_word_count": words_from_channel_time(average_video_time)})
2383
2383
  st.success("Tempo médio do canal TikTok guardado.")
@@ -3073,10 +3073,10 @@ def render_channels():
3073
3073
  st.metric("Visualizações", _format_channel_count(channel.get("view_count")))
3074
3074
  with header_cols[5]:
3075
3075
  calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel)
3076
- average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or DEFAULT_AVERAGE_VIDEO_TIME), key=f"youtube_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
3076
+ average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"youtube_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
3077
3077
  if st.button("Guardar tempo", key=f"save_youtube_channel_average_video_time_{channel_id}", use_container_width=True):
3078
3078
  if not valid_hhmm(average_video_time):
3079
- st.error("Use o formato HH:MM, por exemplo 00:02.")
3079
+ st.error("Use o formato MM:SS, por exemplo 12:00.")
3080
3080
  else:
3081
3081
  update_channel(channel_id, {"average_video_time": average_video_time.strip() or DEFAULT_AVERAGE_VIDEO_TIME, "average_video_word_count": words_from_channel_time(average_video_time)})
3082
3082
  st.success("Tempo médio do canal YouTube guardado.")
@@ -5465,7 +5465,7 @@ def render_tiktok_automation():
5465
5465
  prompt_file = TIKTOK_PROMPT_MASTERS / str(channel.get("default_prompt_master") or channel.get("prompt_master") or "")
5466
5466
  prompt_content = load_prompt_master_file(prompt_file) if prompt_file.is_file() else ""
5467
5467
  calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel, prompt_master=prompt_content)
5468
- average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or DEFAULT_AVERAGE_VIDEO_TIME), key=f"tiktok_average_video_time_{channel_id}", help=f"Referência média: {calculated_words or 0} palavras · fonte: {calculated_source}.")
5468
+ average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"tiktok_average_video_time_{channel_id}", help=f"Referência média: {calculated_words or 0} palavras · fonte: {calculated_source}.")
5469
5469
  default_cols = st.columns([1.0, 1.0, 1.35, 1.45, 1.55, 1.0], gap="small")
5470
5470
  with default_cols[0]:
5471
5471
  st.markdown("**Idioma do roteiro**")
@@ -5489,7 +5489,7 @@ def render_tiktok_automation():
5489
5489
  if not valid_hhmm(schedule_time):
5490
5490
  st.error("Use o formato HH:MM, por exemplo 08:30.")
5491
5491
  elif not valid_hhmm(average_video_time):
5492
- st.error("O Tempo Medio de Video deve estar no formato HH:MM, por exemplo 00:02.")
5492
+ st.error("O Tempo Medio de Video deve estar no formato MM:SS, por exemplo 12:00.")
5493
5493
  else:
5494
5494
  avatar_url = _tiktok_avatar_url(channel)
5495
5495
  update_channel(channel_id, {"automation_on": bool(enabled), "automation_time": schedule_time.strip(), "average_video_time": average_video_time.strip() or DEFAULT_AVERAGE_VIDEO_TIME, "average_video_word_count": words_from_channel_time(average_video_time), "default_prompt_master": prompt, "prompt_master": prompt, "platform": "tiktok", "format": automation_format, "video_aspect_ratio": "Portrait 9:16", "style_wide": "portrait", "avatar_url": avatar_url, "thumbnail_url": avatar_url})
@@ -5653,7 +5653,7 @@ def render_automation():
5653
5653
  schedule_time = st.text_input("Horário (HH:MM)", value=channel.get("automation_time", "00:00"), key=f"automation_time_{channel_id}")
5654
5654
  with header_cols[7]:
5655
5655
  calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel)
5656
- average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or DEFAULT_AVERAGE_VIDEO_TIME), key=f"average_video_time_{channel_id}", help=f"Referência média: {calculated_words or 0} palavras · fonte: {calculated_source}.")
5656
+ average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"average_video_time_{channel_id}", help=f"Referência média: {calculated_words or 0} palavras · fonte: {calculated_source}.")
5657
5657
 
5658
5658
  control_cols = st.columns([1.8, 1.8, 1.35, 1.2], gap="small")
5659
5659
  with control_cols[0]:
@@ -5680,7 +5680,7 @@ def render_automation():
5680
5680
  if not valid_hhmm(schedule_time):
5681
5681
  st.error("Use o formato HH:MM, por exemplo 08:30.")
5682
5682
  elif not valid_hhmm(average_video_time):
5683
- st.error("O Tempo Medio de Video deve estar no formato HH:MM, por exemplo 00:02.")
5683
+ st.error("O Tempo Medio de Video deve estar no formato MM:SS, por exemplo 12:00.")
5684
5684
  else:
5685
5685
  update_channel(channel_id, {
5686
5686
  "automation_on": bool(enabled),
@@ -6,32 +6,43 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
  WORDS_PER_MINUTE = 150
9
- DEFAULT_AVERAGE_VIDEO_TIME = "00:12"
9
+ DEFAULT_AVERAGE_VIDEO_TIME = "12:00"
10
10
  AVERAGE_VIDEO_TIME_KEY = "average_video_time"
11
11
  AVERAGE_VIDEO_WORD_COUNT_KEY = "average_video_word_count"
12
12
 
13
13
 
14
+ def channel_video_time_value(channel: dict[str, Any]) -> str:
15
+ """Return the card value, migrating only the historical twelve-minute default."""
16
+ value = str(channel.get(AVERAGE_VIDEO_TIME_KEY) or "").strip()
17
+ configured_words = str(channel.get(AVERAGE_VIDEO_WORD_COUNT_KEY) or "").strip()
18
+ if value == "00:12" and configured_words in {"", "0", "1800"}:
19
+ return DEFAULT_AVERAGE_VIDEO_TIME
20
+ return value or DEFAULT_AVERAGE_VIDEO_TIME
21
+
22
+
14
23
  def valid_hhmm(value: Any) -> bool:
24
+ """Keep the historical helper name while validating a MM:SS duration."""
15
25
  match = re.fullmatch(r"\s*(\d{1,3}):(\d{2})\s*", str(value or ""))
16
26
  if not match:
17
27
  return False
18
- hours, minutes = int(match.group(1)), int(match.group(2))
19
- return hours >= 0 and 0 <= minutes <= 59
28
+ minutes, seconds = int(match.group(1)), int(match.group(2))
29
+ return minutes >= 0 and 0 <= seconds <= 59
20
30
 
21
31
 
22
- def minutes_from_hhmm(value: Any) -> int:
32
+ def minutes_from_hhmm(value: Any) -> float:
33
+ """Convert a stored MM:SS duration to fractional minutes."""
23
34
  if not valid_hhmm(value):
24
35
  return 0
25
- hours, minutes = (int(part) for part in str(value).strip().split(":"))
26
- return hours * 60 + minutes
36
+ minutes, seconds = (int(part) for part in str(value).strip().split(":"))
37
+ return minutes + seconds / 60
27
38
 
28
39
 
29
40
  def hhmm_from_minutes(value: Any) -> str:
30
41
  try:
31
- total = max(0, int(value))
42
+ total_seconds = max(0, round(float(value) * 60))
32
43
  except (TypeError, ValueError):
33
- total = 0
34
- return f"{total // 60:02d}:{total % 60:02d}"
44
+ total_seconds = 0
45
+ return f"{total_seconds // 60:02d}:{total_seconds % 60:02d}"
35
46
 
36
47
 
37
48
  def _numbers(value: Any) -> list[int]:
@@ -91,23 +102,26 @@ def explicit_word_count(blueprint: Any = None, prompt_master: str = "") -> int:
91
102
 
92
103
 
93
104
  def channel_video_length(channel: dict[str, Any], blueprint: Any = None, prompt_master: str = "") -> tuple[int, str, str]:
94
- """Return (word_count, hh:mm, source), preferring channel override over defaults."""
95
- configured_time = minutes_from_hhmm(channel.get(AVERAGE_VIDEO_TIME_KEY))
105
+ """Return (word_count, mm:ss, source), preferring channel override over defaults."""
106
+ raw_configured_time = str(channel.get(AVERAGE_VIDEO_TIME_KEY) or "").strip()
96
107
  configured_words = int(channel.get(AVERAGE_VIDEO_WORD_COUNT_KEY) or 0) if str(channel.get(AVERAGE_VIDEO_WORD_COUNT_KEY) or "0").isdigit() else 0
108
+ if raw_configured_time == "00:12" and configured_words in {0, 1800}:
109
+ raw_configured_time = DEFAULT_AVERAGE_VIDEO_TIME
110
+ configured_time = minutes_from_hhmm(raw_configured_time)
97
111
  if configured_time:
98
- return configured_time * WORDS_PER_MINUTE, hhmm_from_minutes(configured_time), "canal"
112
+ return round(configured_time * WORDS_PER_MINUTE), hhmm_from_minutes(configured_time), "canal"
99
113
  if configured_words:
100
114
  return configured_words, hhmm_from_minutes(round(configured_words / WORDS_PER_MINUTE)), "canal"
101
115
  words = explicit_word_count(blueprint, prompt_master)
102
116
  if words:
103
117
  return words, hhmm_from_minutes(round(words / WORDS_PER_MINUTE)), "Blueprint/Prompt Master"
104
118
  default_minutes = minutes_from_hhmm(DEFAULT_AVERAGE_VIDEO_TIME)
105
- return default_minutes * WORDS_PER_MINUTE, DEFAULT_AVERAGE_VIDEO_TIME, "padrão do canal"
119
+ return round(default_minutes * WORDS_PER_MINUTE), DEFAULT_AVERAGE_VIDEO_TIME, "padrão do canal"
106
120
 
107
121
 
108
122
  def words_from_channel_time(value: Any) -> int:
109
123
  minutes = minutes_from_hhmm(value)
110
- return minutes * WORDS_PER_MINUTE if minutes else 0
124
+ return round(minutes * WORDS_PER_MINUTE) if minutes else 0
111
125
 
112
126
 
113
127
  def length_generation_settings(channel: dict[str, Any], blueprint: Any = None, prompt_master: str = "") -> dict[str, Any]:
@@ -124,6 +138,7 @@ __all__ = [
124
138
  "AVERAGE_VIDEO_TIME_KEY",
125
139
  "AVERAGE_VIDEO_WORD_COUNT_KEY",
126
140
  "DEFAULT_AVERAGE_VIDEO_TIME",
141
+ "channel_video_time_value",
127
142
  "WORDS_PER_MINUTE",
128
143
  "channel_video_length",
129
144
  "explicit_word_count",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.65",
3
+ "version": "0.6.66",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",