@danhachuel/thunderbolt 0.7.31 → 0.7.33
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 +70 -17
- package/hermes_ui/cuts.py +4 -0
- package/hermes_ui/python_editor.py +2 -0
- package/package.json +1 -1
- package/seed/blueprints/thumbnail_blueprint_pairs.json +5 -5
- /package/seed/blueprints/thumbnails/{FINANCE_Thumbnail_Blueprint.md → FINANCE USA_Thumbnail_Blueprint.md} +0 -0
package/app/main.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
3
|
import io
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
4
6
|
|
|
5
7
|
os.environ["PYTHONIOENCODING"] = "utf-8"
|
|
6
8
|
os.environ["PYTHONUTF8"] = "1"
|
|
@@ -633,6 +635,32 @@ def _library_card_key(kind: str, path: Path) -> str:
|
|
|
633
635
|
return hashlib.sha1(f"{kind}:{path.resolve()}".encode("utf-8")).hexdigest()[:12]
|
|
634
636
|
|
|
635
637
|
|
|
638
|
+
def _video_thumbnail(path: Path) -> bytes | None:
|
|
639
|
+
"""Create a small cached preview frame for a local video card."""
|
|
640
|
+
if not path.is_file():
|
|
641
|
+
return None
|
|
642
|
+
thumbnail_dir = STORAGE / "ui_cache" / "video_thumbnails"
|
|
643
|
+
thumbnail_dir.mkdir(parents=True, exist_ok=True)
|
|
644
|
+
target = thumbnail_dir / f"{hashlib.sha1(str(path.resolve()).encode('utf-8')).hexdigest()}.jpg"
|
|
645
|
+
try:
|
|
646
|
+
if target.is_file() and target.stat().st_mtime >= path.stat().st_mtime:
|
|
647
|
+
return target.read_bytes()
|
|
648
|
+
ffmpeg = shutil.which("ffmpeg")
|
|
649
|
+
if not ffmpeg:
|
|
650
|
+
return None
|
|
651
|
+
completed = subprocess.run(
|
|
652
|
+
[ffmpeg, "-y", "-hide_banner", "-loglevel", "error", "-ss", "00:00:01", "-i", str(path), "-frames:v", "1", "-vf", "scale=640:-2", str(target)],
|
|
653
|
+
capture_output=True,
|
|
654
|
+
check=False,
|
|
655
|
+
timeout=20,
|
|
656
|
+
)
|
|
657
|
+
if completed.returncode == 0 and target.is_file():
|
|
658
|
+
return target.read_bytes()
|
|
659
|
+
except (OSError, subprocess.SubprocessError):
|
|
660
|
+
return None
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
|
|
636
664
|
def _render_library_name_editor(kind: str, path: Path, current_name: str) -> str:
|
|
637
665
|
"""Render the inline name editor while keeping the physical filename unchanged."""
|
|
638
666
|
edit_key = f"rename_{kind}_{_library_card_key(kind, path)}"
|
|
@@ -4833,12 +4861,7 @@ def render_media_download():
|
|
|
4833
4861
|
|
|
4834
4862
|
def render_cuts():
|
|
4835
4863
|
st.title("Cortes")
|
|
4836
|
-
st.caption("Crie clips verticais, quadrados ou horizontais a partir de vídeos longos,
|
|
4837
|
-
|
|
4838
|
-
st.markdown(
|
|
4839
|
-
"<div class='tb-cuts-hero'><div class='tb-cuts-kicker'>01 · CLIP GENERATOR</div><h2>Create Viral Shorts</h2><p>Escolha um vídeo longo, defina o formato e gere clips locais sem sobrescrever a fonte.</p></div>",
|
|
4840
|
-
unsafe_allow_html=True,
|
|
4841
|
-
)
|
|
4864
|
+
st.caption("Crie clips verticais, quadrados ou horizontais a partir de vídeos longos, sem sobrescrever a fonte.")
|
|
4842
4865
|
|
|
4843
4866
|
source_path = None
|
|
4844
4867
|
source_tab, url_tab, generated_tab, folder_tab = render_localized_tabs(["Upload ficheiro", "URL de vídeo", "Vídeos gerados", "Pasta local"])
|
|
@@ -4870,14 +4893,27 @@ def render_cuts():
|
|
|
4870
4893
|
with generated_tab:
|
|
4871
4894
|
generated_paths = list_cut_generated_videos(read_json("tasks.json", []))
|
|
4872
4895
|
if not generated_paths:
|
|
4873
|
-
st.info("Ainda não existem vídeos
|
|
4896
|
+
st.info("Ainda não existem vídeos prontos na pipeline.")
|
|
4874
4897
|
else:
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4898
|
+
st.caption(f"{len(generated_paths)} vídeo(s) pronto(s) disponível(is).")
|
|
4899
|
+
for index, generated_path in enumerate(generated_paths):
|
|
4900
|
+
with st.expander(generated_path.stem, expanded=False):
|
|
4901
|
+
card_cols = st.columns([1.35, 2.4, 1.1])
|
|
4902
|
+
with card_cols[0]:
|
|
4903
|
+
thumbnail = _video_thumbnail(generated_path)
|
|
4904
|
+
if thumbnail:
|
|
4905
|
+
st.image(thumbnail, width="stretch")
|
|
4906
|
+
else:
|
|
4907
|
+
st.video(str(generated_path))
|
|
4908
|
+
with card_cols[1]:
|
|
4909
|
+
st.markdown(f"**{generated_path.stem}**")
|
|
4910
|
+
st.caption(f"Vídeo pronto · {generated_path.suffix.lstrip('.').upper()}")
|
|
4911
|
+
st.caption(f"{generated_path.stat().st_size / (1024 * 1024):.2f} MB")
|
|
4912
|
+
with card_cols[2]:
|
|
4913
|
+
if st.button("Usar vídeo", key=f"cuts_use_generated_{index}_{_library_card_key('cuts', generated_path)}", type="primary", width="stretch"):
|
|
4914
|
+
st.session_state["cuts_source_path"] = str(generated_path)
|
|
4915
|
+
st.session_state["cuts_source_label"] = f"Pipeline · {generated_path.stem}"
|
|
4916
|
+
st.rerun()
|
|
4881
4917
|
with folder_tab:
|
|
4882
4918
|
folder_value = st.text_input("Pasta de vídeos", value=str(STORAGE / "videos"), key="cuts_video_folder")
|
|
4883
4919
|
folder_paths = list_cut_video_files(folder_value)
|
|
@@ -5028,11 +5064,28 @@ def render_python_editor():
|
|
|
5028
5064
|
tasks = read_json("tasks.json", [])
|
|
5029
5065
|
generated_paths = list_generated_videos(tasks)
|
|
5030
5066
|
if not generated_paths:
|
|
5031
|
-
st.info("Ainda não existem vídeos
|
|
5067
|
+
st.info("Ainda não existem vídeos prontos nos artefactos da pipeline.")
|
|
5032
5068
|
else:
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5069
|
+
st.caption("Seleccione um vídeo pronto na pipeline.")
|
|
5070
|
+
for index, generated_path in enumerate(generated_paths):
|
|
5071
|
+
with st.expander(generated_path.stem, expanded=False):
|
|
5072
|
+
card_cols = st.columns([1.35, 2.4, 1.1])
|
|
5073
|
+
with card_cols[0]:
|
|
5074
|
+
thumbnail = _video_thumbnail(generated_path)
|
|
5075
|
+
if thumbnail:
|
|
5076
|
+
st.image(thumbnail, width="stretch")
|
|
5077
|
+
else:
|
|
5078
|
+
st.video(str(generated_path))
|
|
5079
|
+
with card_cols[1]:
|
|
5080
|
+
st.markdown(f"**{generated_path.stem}**")
|
|
5081
|
+
st.caption(f"Vídeo pronto · {generated_path.suffix.lstrip('.').upper()}")
|
|
5082
|
+
with card_cols[2]:
|
|
5083
|
+
if st.button("Editar vídeo", key=f"python_use_generated_{index}_{_library_card_key('python', generated_path)}", type="primary", width="stretch"):
|
|
5084
|
+
st.session_state["python_editor_source_path"] = str(generated_path)
|
|
5085
|
+
st.rerun()
|
|
5086
|
+
stored_generated = Path(str(st.session_state.get("python_editor_source_path") or ""))
|
|
5087
|
+
if stored_generated.is_file() and stored_generated in generated_paths:
|
|
5088
|
+
source_path = stored_generated
|
|
5036
5089
|
elif source_mode == "Pasta local":
|
|
5037
5090
|
default_folder = str(STORAGE / "videos")
|
|
5038
5091
|
folder_value = st.text_input("Pasta de vídeos", value=st.session_state.get("python_editor_folder", default_folder), key="python_editor_folder")
|
package/hermes_ui/cuts.py
CHANGED
|
@@ -121,6 +121,10 @@ def list_video_files(folder: str | Path) -> list[Path]:
|
|
|
121
121
|
def list_generated_videos(tasks: Iterable[dict[str, Any]]) -> list[Path]:
|
|
122
122
|
paths: dict[str, Path] = {}
|
|
123
123
|
for task in tasks:
|
|
124
|
+
# Um caminho pode existir enquanto a tarefa ainda está a ser processada;
|
|
125
|
+
# só artefactos explicitamente prontos devem entrar nesta biblioteca.
|
|
126
|
+
if "video_ready" in task and not bool(task.get("video_ready")):
|
|
127
|
+
continue
|
|
124
128
|
artifacts = task.get("artifacts") or {}
|
|
125
129
|
value = artifacts.get("video")
|
|
126
130
|
if isinstance(value, str) and value.strip():
|
|
@@ -96,6 +96,8 @@ def list_video_files(folder: str | Path) -> list[Path]:
|
|
|
96
96
|
def list_generated_videos(tasks: list[dict[str, Any]]) -> list[Path]:
|
|
97
97
|
paths: dict[str, Path] = {}
|
|
98
98
|
for task in tasks:
|
|
99
|
+
if "video_ready" in task and not bool(task.get("video_ready")):
|
|
100
|
+
continue
|
|
99
101
|
artifacts = task.get("artifacts") or {}
|
|
100
102
|
value = artifacts.get("video")
|
|
101
103
|
if isinstance(value, str) and value.strip():
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"BLUEPRINTCANALMILITAR": "Militar_Thumbnail_Blueprint",
|
|
3
3
|
"MILITAR": "Militar_Thumbnail_Blueprint",
|
|
4
|
-
"blueprintcanalfinanças": "
|
|
5
|
-
"blueprintcanalfinancas": "
|
|
6
|
-
"BLUEPRINT CANAL FINANÇAS": "
|
|
7
|
-
"FINANÇAS": "
|
|
8
|
-
"FINANCAS": "
|
|
4
|
+
"blueprintcanalfinanças": "FINANCE USA_Thumbnail_Blueprint",
|
|
5
|
+
"blueprintcanalfinancas": "FINANCE USA_Thumbnail_Blueprint",
|
|
6
|
+
"BLUEPRINT CANAL FINANÇAS": "FINANCE USA_Thumbnail_Blueprint",
|
|
7
|
+
"FINANÇAS": "FINANCE USA_Thumbnail_Blueprint",
|
|
8
|
+
"FINANCAS": "FINANCE USA_Thumbnail_Blueprint"
|
|
9
9
|
}
|
|
File without changes
|