@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
|
-
"
|
|
280
|
+
"h264_nvenc",
|
|
281
281
|
"-preset",
|
|
282
|
-
"
|
|
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":
|
|
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(
|
|
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":
|
|
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", "
|
|
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", "
|
|
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
|
|
package/hermes_ui/storage.py
CHANGED
|
@@ -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