@danhachuel/thunderbolt 0.5.93 → 0.5.95

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
@@ -124,6 +124,7 @@ from integrations.youtube_direct_credentials import delete_credentials_document,
124
124
  from integrations.session_info_health import check_account_session_info_health
125
125
  from integrations.youtube_session_manager import renew_account_session
126
126
  from integrations.youtube_batch import account_key as youtube_batch_account_key, account_status as youtube_batch_account_status, authorize_account as authorize_youtube_batch_account, delete_account_token as delete_youtube_batch_token, list_my_channels as list_youtube_batch_channels, loopback_redirect_uri
127
+ from integrations.youtube_growth_api import find_account_for_channel as find_youtube_growth_account
127
128
  from integrations.local_runtime import MoneyPrinterRuntime
128
129
  from integrations.moneyprinter_config import sync_moneyprinter_config
129
130
  from integrations.openai_model_discovery import DEFAULT_NVIDIA_NIM_BASE_URL
@@ -1496,21 +1497,6 @@ def render_growth_youtube():
1496
1497
  st.caption("Auditoria pública baseada nos 3 pilares críticos e nas métricas secundárias do agente Growth.")
1497
1498
  growth_settings = read_json("settings.json", {})
1498
1499
  youtube_data_api_ready = bool(str(growth_settings.get("youtube_api_key") or os.getenv("YOUTUBE_API_KEY") or "").strip())
1499
- growth_accounts = growth_settings.get("youtube_batch_accounts", [])
1500
- analytics_accounts_ready = False
1501
- if isinstance(growth_accounts, list):
1502
- for growth_account in growth_accounts:
1503
- if isinstance(growth_account, dict) and youtube_batch_account_status(growth_account, STORAGE).ok:
1504
- analytics_accounts_ready = True
1505
- break
1506
- st.markdown("**Estado das APIs de Growth**")
1507
- api_status_cols = st.columns(2, gap="small")
1508
- with api_status_cols[0]:
1509
- st.caption("YouTube Data API v3: Metadados públicos")
1510
- _api_status_badge("Configured" if youtube_data_api_ready else "Missing configuration", "ready" if youtube_data_api_ready else "missing")
1511
- with api_status_cols[1]:
1512
- st.caption("YouTube Analytics API: Métricas internas")
1513
- _api_status_badge("Configured" if analytics_accounts_ready else "Missing configuration", "ready" if analytics_accounts_ready else "missing")
1514
1500
  channels = [item for item in read_json("channels.json", []) if isinstance(item, dict) and item.get("active", True) and is_youtube_channel_record(item)]
1515
1501
  if not channels:
1516
1502
  st.info("Cadastre pelo menos um canal YouTube em Canais YouTube para iniciar a análise.")
@@ -1526,6 +1512,16 @@ def render_growth_youtube():
1526
1512
  with action_status_col:
1527
1513
  if st.session_state.get("growth_analysis_status") == "complete":
1528
1514
  st.markdown('<div style="padding-top:9px;color:#22c55e;font-weight:700;white-space:nowrap;">✓ Análise concluída</div>', unsafe_allow_html=True)
1515
+ matched_growth_account = find_youtube_growth_account(selected, growth_settings)
1516
+ analytics_accounts_ready = bool(matched_growth_account and youtube_batch_account_status(matched_growth_account, STORAGE).ok)
1517
+ st.markdown("**Estado das APIs de Growth**")
1518
+ api_status_cols = st.columns(2, gap="small")
1519
+ with api_status_cols[0]:
1520
+ st.caption("YouTube Data API v3: Metadados públicos")
1521
+ _api_status_badge("Configured" if youtube_data_api_ready else "Missing configuration", "ready" if youtube_data_api_ready else "missing")
1522
+ with api_status_cols[1]:
1523
+ st.caption("YouTube Analytics API: Métricas internas")
1524
+ _api_status_badge("Configured" if analytics_accounts_ready else "Missing configuration", "ready" if analytics_accounts_ready else "missing")
1529
1525
  if analyse_clicked:
1530
1526
  settings = read_json("settings.json", {})
1531
1527
  llm_settings, _ = ensure_llm_provider_cards(settings)
package/hermes_ui/cuts.py CHANGED
@@ -277,9 +277,11 @@ def build_clip_command(source: Path, output: Path, segment: dict[str, float], ou
277
277
  "-vf",
278
278
  _video_filter(output_format),
279
279
  "-c:v",
280
- "libx264",
280
+ "h264_nvenc",
281
281
  "-preset",
282
- "veryfast",
282
+ "p1",
283
+ "-pix_fmt",
284
+ "yuv420p",
283
285
  "-crf",
284
286
  "22",
285
287
  "-c:a",
@@ -295,12 +297,19 @@ def _run_ffmpeg(command: list[str], output: Path, *, source: Path, segment: dict
295
297
  completed = subprocess.run(command, capture_output=True, text=True, check=False)
296
298
  except OSError as exc:
297
299
  raise CutsError(f"Não foi possível iniciar FFmpeg: {exc}") from exc
300
+ executed_command = list(command)
301
+ if completed.returncode != 0 and "h264_nvenc" in executed_command:
302
+ fallback = ["libx264" if item == "h264_nvenc" else "veryfast" if item == "p1" else item for item in executed_command]
303
+ if output.exists():
304
+ output.unlink()
305
+ completed = subprocess.run(fallback, capture_output=True, text=True, check=False)
306
+ executed_command = fallback
298
307
  if completed.returncode != 0 or not output.is_file() or output.stat().st_size == 0:
299
308
  if output.exists():
300
309
  output.unlink()
301
310
  detail = (completed.stderr or completed.stdout or "erro desconhecido").strip()
302
311
  raise CutsError(f"O clip não pôde ser gerado: {detail[-1200:]}")
303
- return {"source": str(source), "output": str(output), "command": command, "segment": segment, "created_at": _now()}
312
+ return {"source": str(source), "output": str(output), "command": executed_command, "segment": segment, "created_at": _now()}
304
313
 
305
314
 
306
315
  def _write_manifest(record: dict[str, Any], run_dir: Path) -> Path:
@@ -150,10 +150,17 @@ def store_uploaded_asset(name: str, content: bytes, *, audio: bool = False) -> P
150
150
 
151
151
  def _run(command: list[str], output: Path, operation: str, source: Path, *, extra: dict[str, Any] | None = None) -> tuple[Path, dict[str, Any]]:
152
152
  output.parent.mkdir(parents=True, exist_ok=True)
153
+ executed_command = list(command)
153
154
  try:
154
- completed = subprocess.run(command, capture_output=True, text=True, check=False)
155
+ completed = subprocess.run(executed_command, capture_output=True, text=True, check=False)
155
156
  except OSError as exc:
156
157
  raise PythonEditorError(f"Não foi possível iniciar FFmpeg: {exc}") from exc
158
+ if completed.returncode != 0 and "h264_nvenc" in executed_command:
159
+ fallback = ["libx264" if item == "h264_nvenc" else "veryfast" if item == "p1" else item for item in executed_command]
160
+ if output.exists():
161
+ output.unlink()
162
+ completed = subprocess.run(fallback, capture_output=True, text=True, check=False)
163
+ executed_command = fallback
157
164
  if completed.returncode != 0 or not output.is_file() or output.stat().st_size == 0:
158
165
  if output.exists():
159
166
  output.unlink()
@@ -163,7 +170,7 @@ def _run(command: list[str], output: Path, operation: str, source: Path, *, extr
163
170
  "operation": operation,
164
171
  "source": str(source),
165
172
  "output": str(output),
166
- "command": command,
173
+ "command": executed_command,
167
174
  "created_at": _now(),
168
175
  }
169
176
  if extra:
@@ -224,7 +231,7 @@ def resize_video(source: Path, width: int, height: int, *, ffmpeg_path: str | No
224
231
  raise PythonEditorError("As dimensões do vídeo devem ser pelo menos 16x16.")
225
232
  output = _output_path(source, "redimensionado")
226
233
  scale = f"scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:(ow-iw)/2:(oh-ih)/2"
227
- command = [_ffmpeg(ffmpeg_path), "-y", "-hide_banner", "-loglevel", "error", "-i", str(source), "-vf", scale, "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-movflags", "+faststart", str(output)]
234
+ command = [_ffmpeg(ffmpeg_path), "-y", "-hide_banner", "-loglevel", "error", "-i", str(source), "-vf", scale, "-c:v", "h264_nvenc", "-preset", "p1", "-pix_fmt", "yuv420p", "-c:a", "aac", "-movflags", "+faststart", str(output)]
228
235
  return _run(command, output, "redimensionamento", source, extra={"width": width, "height": height})
229
236
 
230
237
 
@@ -234,7 +241,7 @@ def change_speed(source: Path, speed: float, *, ffmpeg_path: str | None = None)
234
241
  if speed < 0.25 or speed > 4.0:
235
242
  raise PythonEditorError("A velocidade deve estar entre 0,25x e 4x.")
236
243
  output = _output_path(source, "velocidade")
237
- command = [_ffmpeg(ffmpeg_path), "-y", "-hide_banner", "-loglevel", "error", "-i", str(source), "-filter:v", f"setpts=PTS/{speed:.6f}", "-filter:a", _atempo_chain(speed), "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-movflags", "+faststart", str(output)]
244
+ command = [_ffmpeg(ffmpeg_path), "-y", "-hide_banner", "-loglevel", "error", "-i", str(source), "-filter:v", f"setpts=PTS/{speed:.6f}", "-filter:a", _atempo_chain(speed), "-c:v", "h264_nvenc", "-preset", "p1", "-pix_fmt", "yuv420p", "-c:a", "aac", "-movflags", "+faststart", str(output)]
238
245
  return _run(command, output, "alteração de velocidade", source, extra={"speed": speed})
239
246
 
240
247
 
@@ -265,7 +265,7 @@ DEFAULTS: dict[str, Any] = {
265
265
  "sonilo_api_key": "",
266
266
  "subtitle_provider": "edge",
267
267
  "ffmpeg_path": "",
268
- "video_codec": "",
268
+ "video_codec": "h264_nvenc",
269
269
  "material_directory": "",
270
270
  "whisper_model_size": "large-v3",
271
271
  "whisper_device": "cpu",
@@ -8,6 +8,8 @@ from typing import Any
8
8
  from hermes_ui.languages import language_code
9
9
  from hermes_ui.material_sources import all_material_api_keys, selected_material_source
10
10
 
11
+ NVENC_VIDEO_CODEC = "h264_nvenc"
12
+
11
13
  try:
12
14
  import toml
13
15
  except ImportError: # pragma: no cover - installation fallback
@@ -118,6 +120,10 @@ def build_moneyprinter_config(settings: dict[str, Any], existing: dict[str, Any]
118
120
  for settings_key, config_key in app_map.items():
119
121
  if settings_key in settings:
120
122
  app[config_key] = settings[settings_key]
123
+ # The bundled video pipeline is required to use NVIDIA NVENC. The
124
+ # MoneyPrinterTurbo runtime falls back to libx264 when this encoder is not
125
+ # available on the host, preserving a playable output instead of failing.
126
+ app["video_codec"] = NVENC_VIDEO_CODEC
121
127
  # Edge TTS V1 is retained as a keyless fallback, but its default 30-second
122
128
  # stream deadline is too short for longer scripts or slow networks.
123
129
  try:
@@ -23,9 +23,9 @@ def find_account_for_channel(channel: Mapping[str, Any], settings: Mapping[str,
23
23
  channel_id = _text(channel.get("youtube_channel_id") or channel.get("id"))
24
24
  account_id = _text(channel.get("google_account_id"))
25
25
  accounts = _accounts(settings)
26
+ if account_id:
27
+ return next((account for account in accounts if _text(account.get("id")) == account_id), None)
26
28
  for account in accounts:
27
- if account_id and _text(account.get("id")) == account_id:
28
- return account
29
29
  known_channels = account.get("channels", [])
30
30
  if isinstance(known_channels, list) and any(_text(item.get("youtube_channel_id") or item.get("id")) == channel_id for item in known_channels if isinstance(item, Mapping)):
31
31
  return account
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.5.93",
3
+ "version": "0.5.95",
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",