@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
|
|
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])
|
package/hermes_ui/domain.py
CHANGED
|
@@ -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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 = "
|
|
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