@danhachuel/thunderbolt 0.7.57 → 0.7.59

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
@@ -1578,7 +1578,7 @@ def render_channel_video_defaults(channel: dict) -> None:
1578
1578
  source = channel_video_source_value(channel.get("default_video_source") or channel.get("style_wide"))
1579
1579
  aspect_ratio = str(channel.get("default_video_aspect_ratio") or channel.get("video_aspect_ratio") or "Landscape 16:9")
1580
1580
  encoder = str(channel.get("default_video_encoder") or VIDEO_ENCODER_OPTIONS[0])
1581
- maximum_clip_duration = int(channel.get("default_maximum_clip_duration") or 10)
1581
+ maximum_clip_duration = int(channel.get("default_maximum_clip_duration") or 5)
1582
1582
  videos_per_run = int(channel.get("default_videos_per_run") or 1)
1583
1583
  concatenation_mode = str(channel.get("default_video_concatenation_mode") or VIDEO_CONCATENATION_OPTIONS[0])
1584
1584
  transition_mode = str(channel.get("default_video_transition_mode") or VIDEO_TRANSITION_OPTIONS[0])
@@ -97,6 +97,12 @@ def create_channel(name: str, url: str = "", metadata: dict[str, Any] | None = N
97
97
  }
98
98
  if metadata:
99
99
  channel.update({k: v for k, v in metadata.items() if k in channel and k != "id"})
100
+ if str(channel.get("platform") or "").strip().casefold() == "youtube":
101
+ channel.setdefault("default_maximum_clip_duration", 5)
102
+ channel.setdefault("default_video_aspect_ratio", "Landscape 16:9")
103
+ channel.setdefault("default_enable_subtitles", True)
104
+ channel.setdefault("default_background_music_source", "Sem música")
105
+ channel.setdefault("average_video_time", "20:00")
100
106
  channels = read_json("channels.json", [])
101
107
  channels.append(channel)
102
108
  write_json("channels.json", channels)
@@ -208,10 +214,15 @@ def create_tasks_for_batch(batch: dict[str, Any]) -> list[dict[str, Any]]:
208
214
  "default_subtitle_font_size": 60,
209
215
  "default_subtitle_outline": "#000000",
210
216
  "default_subtitle_outline_width": 1.5,
211
- "default_background_music_source": "Sem música",
212
- "default_background_music_volume": "20%",
217
+ "default_background_music_source": "Sem música",
218
+ "default_background_music_volume": "20%",
213
219
  }.items()
214
220
  }
221
+ if str(channel.get("platform") or "").strip().casefold() == "youtube":
222
+ channel_generation_defaults.update({
223
+ "maximum_clip_duration": channel.get("default_maximum_clip_duration", 5),
224
+ "video_aspect_ratio": channel.get("default_video_aspect_ratio", "Landscape 16:9"),
225
+ })
215
226
  configured_generation_settings = options.get("generation_settings") if isinstance(options.get("generation_settings"), dict) else {}
216
227
  payload_generation_settings = payload.get("generation_settings") if isinstance(payload.get("generation_settings"), dict) else {}
217
228
  generation_settings = {**channel_generation_defaults, **configured_generation_settings, **payload_generation_settings}
@@ -519,6 +530,9 @@ def _refresh_task_channel_video_settings(task: dict[str, Any]) -> None:
519
530
  }
520
531
  for setting_name, (channel_key, default) in channel_defaults.items():
521
532
  generation_settings[setting_name] = channel.get(channel_key, default)
533
+ if str(channel.get("platform") or "").strip().casefold() == "youtube":
534
+ generation_settings["maximum_clip_duration"] = channel.get("default_maximum_clip_duration", 5)
535
+ generation_settings["video_aspect_ratio"] = channel.get("default_video_aspect_ratio", "Landscape 16:9")
522
536
  task["generation_settings"] = generation_settings
523
537
  task["voice"] = channel.get("default_voice") or channel.get("voice") or task.get("voice", "")
524
538
  task["thumbnail_blueprint_id"] = (
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from typing import Any, Mapping
8
8
 
9
9
  from .provider_routing import route_llm_json
10
- from .storage import BLUEPRINTS, SEED_THUMBNAIL_BLUEPRINTS, atomic_write, list_blueprint_files, load_blueprint_file
10
+ from .storage import BLUEPRINTS, SEED_BLUEPRINTS, SEED_THUMBNAIL_BLUEPRINTS, atomic_write, list_blueprint_files, load_blueprint_file
11
11
 
12
12
  HORIZONTAL_GENERIC_THUMBNAIL_BLUEPRINT_ID = "Youtube_Generic_Thumbnail_Blueprint"
13
13
  VERTICAL_GENERIC_THUMBNAIL_BLUEPRINT_ID = "Tiktok_Generic_Thumbnail_Blueprint"
@@ -72,7 +72,7 @@ def thumbnail_blueprint_for_channel(channel: Mapping[str, Any], format_value: An
72
72
  script_id = str(channel.get("default_blueprint_id") or channel.get("blueprint_id") or "").strip()
73
73
  pairs = _pair_state()
74
74
  return resolve_thumbnail_blueprint(
75
- pairs.get(script_id, "") or _generic_thumbnail_blueprint_id(format_value, channel.get("platform"))
75
+ _paired_thumbnail_id(script_id, pairs) or _generic_thumbnail_blueprint_id(format_value, channel.get("platform"))
76
76
  )
77
77
 
78
78
 
@@ -118,6 +118,9 @@ def thumbnail_aspect_ratio_for_channel_task(
118
118
  if not selected and direct and direct not in generic_ids:
119
119
  selected = resolve_thumbnail_blueprint(direct)
120
120
  if selected.get("content") and direct not in generic_ids:
121
+ explicit_ratio = _explicit_blueprint_aspect_ratio(selected.get("content"))
122
+ if explicit_ratio:
123
+ return explicit_ratio
121
124
  return "9:16" if _contains_vertical_rules(selected.get("content")) else "16:9"
122
125
 
123
126
  platform = str(channel.get("platform") or task.get("platform") or "").strip().casefold()
@@ -130,21 +133,60 @@ def _contains_vertical_rules(value: Any) -> bool:
130
133
  return bool(re.search(r"\b9\s*:\s*16\b|\bvertical\b|\bportrait\b", str(value or ""), flags=re.IGNORECASE))
131
134
 
132
135
 
136
+ def _explicit_blueprint_aspect_ratio(value: Any) -> str:
137
+ """Read the authoritative FORMAT/QUALITY ratio before contextual wording.
138
+
139
+ Finance and other landscape blueprints may mention a vertical third or
140
+ vertical positioning in their composition rules. Those words must not
141
+ override an explicit ``Aspect ratio: 16:9`` declaration.
142
+ """
143
+ content = str(value or "")
144
+ format_sections = re.findall(
145
+ r"(?is)(?:FORMAT\s*(?:&|AND)?\s*QUALITY|FORMATO\s*(?:E|&)\s*QUALIDADE)(.*?)(?=\n#{1,6}\s|\Z)",
146
+ content,
147
+ )
148
+ search_area = "\n".join(format_sections) if format_sections else content
149
+ ratio_match = re.search(r"\b(16\s*:\s*9|9\s*:\s*16)\b", search_area)
150
+ if not ratio_match:
151
+ return ""
152
+ return "16:9" if ratio_match.group(1).replace(" ", "") == "16:9" else "9:16"
153
+
154
+
155
+ def _normalised_pair_key(value: Any) -> str:
156
+ return re.sub(r"[^a-z0-9]+", " ", str(value or "").casefold()).strip()
157
+
158
+
159
+ def _paired_thumbnail_id(blueprint_id: Any, pairs: Mapping[str, str]) -> str:
160
+ wanted = str(blueprint_id or "").strip()
161
+ if not wanted:
162
+ return ""
163
+ if wanted in pairs:
164
+ return str(pairs[wanted] or "")
165
+ wanted_key = _normalised_pair_key(wanted)
166
+ for key, value in pairs.items():
167
+ if _normalised_pair_key(key) == wanted_key:
168
+ return str(value or "")
169
+ return ""
170
+
171
+
133
172
  def thumbnail_blueprint_for_blueprint(blueprint_id: Any, format_value: Any = "") -> dict[str, Any]:
134
173
  """Resolve the visual pair for a script Blueprint, falling back to Generic."""
135
- paired_id = _pair_state().get(str(blueprint_id or "").strip(), "")
174
+ paired_id = _paired_thumbnail_id(blueprint_id, _pair_state())
136
175
  if isinstance(paired_id, list):
137
176
  paired_id = paired_id[0] if paired_id else ""
138
177
  return resolve_thumbnail_blueprint(paired_id or _generic_thumbnail_blueprint_id(format_value))
139
178
 
140
179
 
141
180
  def _pair_state() -> dict[str, str]:
142
- path = BLUEPRINTS / "thumbnail_blueprint_pairs.json"
143
- try:
144
- value = __import__("json").loads(path.read_text(encoding="utf-8"))
145
- return {str(k): str(v) for k, v in value.items()} if isinstance(value, dict) else {}
146
- except (OSError, ValueError, TypeError):
147
- return {}
181
+ result: dict[str, str] = {}
182
+ for path in (SEED_BLUEPRINTS / "thumbnail_blueprint_pairs.json", BLUEPRINTS / "thumbnail_blueprint_pairs.json"):
183
+ try:
184
+ value = __import__("json").loads(path.read_text(encoding="utf-8"))
185
+ except (OSError, ValueError, TypeError):
186
+ continue
187
+ if isinstance(value, dict):
188
+ result.update({str(k): str(v) for k, v in value.items()})
189
+ return result
148
190
 
149
191
 
150
192
  def thumbnail_blueprint_associations() -> dict[str, str]:
@@ -39,6 +39,9 @@ def infer_thumbnail_aspect_ratio(
39
39
  ) -> str:
40
40
  """Resolve the requested thumbnail orientation without trusting provider defaults."""
41
41
  blueprint_content = str((blueprint or {}).get("content") or "")
42
+ explicit_ratio = _explicit_blueprint_aspect_ratio(blueprint_content)
43
+ if explicit_ratio:
44
+ return explicit_ratio
42
45
  source = f"{blueprint_content}\n{str(prompt or '')}"
43
46
  if re.search(r"\b9\s*:\s*16\b|\bvertical\b|\bportrait\b", source, flags=re.IGNORECASE):
44
47
  return "9:16"
@@ -47,6 +50,19 @@ def infer_thumbnail_aspect_ratio(
47
50
  return fallback if fallback in {"16:9", "9:16"} else DEFAULT_ASPECT_RATIO
48
51
 
49
52
 
53
+ def _explicit_blueprint_aspect_ratio(value: Any) -> str:
54
+ content = str(value or "")
55
+ sections = re.findall(
56
+ r"(?is)(?:FORMAT\s*(?:&|AND)?\s*QUALITY|FORMATO\s*(?:E|&)\s*QUALIDADE)(.*?)(?=\n#{1,6}\s|\Z)",
57
+ content,
58
+ )
59
+ area = "\n".join(sections) if sections else content
60
+ match = re.search(r"\b(16\s*:\s*9|9\s*:\s*16)\b", area)
61
+ if not match:
62
+ return ""
63
+ return "16:9" if match.group(1).replace(" ", "") == "16:9" else "9:16"
64
+
65
+
50
66
  def normalize_thumbnail_bytes(image_bytes: bytes, aspect_ratio: str = DEFAULT_ASPECT_RATIO) -> bytes:
51
67
  """Return a non-distorted JPEG in the requested thumbnail orientation."""
52
68
  try:
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
  WORDS_PER_MINUTE = 150
9
- DEFAULT_AVERAGE_VIDEO_TIME = "12:00"
9
+ DEFAULT_AVERAGE_VIDEO_TIME = "20:00"
10
10
  DEFAULT_TIKTOK_AVERAGE_VIDEO_TIME = "01:20"
11
11
  AVERAGE_VIDEO_TIME_KEY = "average_video_time"
12
12
  AVERAGE_VIDEO_WORD_COUNT_KEY = "average_video_word_count"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.57",
3
+ "version": "0.7.59",
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",