@danhachuel/thunderbolt 0.2.63 → 0.2.64
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 +79 -29
- package/hermes_ui/storage.py +48 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -22,7 +22,7 @@ except (OSError, json.JSONDecodeError):
|
|
|
22
22
|
|
|
23
23
|
from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, pipeline_summary, set_channel_defaults, transition_task, update_channel, update_channel_video
|
|
24
24
|
from hermes_ui.automation_worker import load_worker_status
|
|
25
|
-
from hermes_ui.storage import BLUEPRINTS, STORAGE, TIKTOK_PROMPT_MASTERS, ensure_storage, list_blueprint_files, list_prompt_master_files, load_blueprint_file, load_prompt_master_file, now, read_json, write_json
|
|
25
|
+
from hermes_ui.storage import BLUEPRINTS, STORAGE, TIKTOK_PROMPT_MASTERS, ensure_storage, get_display_name, list_blueprint_files, list_prompt_master_files, load_blueprint_file, load_prompt_master_file, now, read_json, set_display_name, write_json
|
|
26
26
|
from app.modules.niche_finder.apify import ApifyError, DEFAULT_ACTOR_ID, abort_actor_run, build_actor_input, get_dataset_items, normalize_video_items, start_actor_run, wait_for_actor_run
|
|
27
27
|
from app.modules.niche_finder.core import NicheAnalysisError, run_niche_analysis
|
|
28
28
|
from app.modules.niche_finder.data_loader import DatasetError, download_kaggle_dataset
|
|
@@ -204,6 +204,41 @@ def card(label: str, value: str | int, note: str = ""):
|
|
|
204
204
|
st.markdown(f'<div class="content-card"><div class="content-label">{label}</div><div class="content-value">{value}</div><div class="small-muted">{note}</div></div>', unsafe_allow_html=True)
|
|
205
205
|
|
|
206
206
|
|
|
207
|
+
def _library_card_key(kind: str, path: Path) -> str:
|
|
208
|
+
return hashlib.sha1(f"{kind}:{path.resolve()}".encode("utf-8")).hexdigest()[:12]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _render_library_name_editor(kind: str, path: Path, current_name: str) -> str:
|
|
212
|
+
"""Render the inline name editor while keeping the physical filename unchanged."""
|
|
213
|
+
edit_key = f"rename_{kind}_{_library_card_key(kind, path)}"
|
|
214
|
+
if st.session_state.get(edit_key):
|
|
215
|
+
with st.form(f"{edit_key}_form", border=False):
|
|
216
|
+
edited_name = st.text_input("Nome de apresentação", value=current_name, max_chars=120, key=f"{edit_key}_input")
|
|
217
|
+
save_col, cancel_col = st.columns(2)
|
|
218
|
+
with save_col:
|
|
219
|
+
save_name = st.form_submit_button("Guardar nome", type="primary", use_container_width=True)
|
|
220
|
+
with cancel_col:
|
|
221
|
+
cancel_name = st.form_submit_button("Cancelar", use_container_width=True)
|
|
222
|
+
if save_name:
|
|
223
|
+
try:
|
|
224
|
+
set_display_name(kind, path, edited_name)
|
|
225
|
+
st.session_state.pop(edit_key, None)
|
|
226
|
+
st.success("Nome actualizado.")
|
|
227
|
+
st.rerun()
|
|
228
|
+
except ValueError as exc:
|
|
229
|
+
st.error(str(exc))
|
|
230
|
+
if cancel_name:
|
|
231
|
+
st.session_state.pop(edit_key, None)
|
|
232
|
+
st.rerun()
|
|
233
|
+
return edit_key
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _render_card_pencil(edit_key: str) -> None:
|
|
237
|
+
if st.button("✏️", help="Editar nome de apresentação", key=f"pencil_{edit_key}", type="tertiary", use_container_width=True):
|
|
238
|
+
st.session_state[edit_key] = True
|
|
239
|
+
st.rerun()
|
|
240
|
+
|
|
241
|
+
|
|
207
242
|
def channel_options() -> list[dict]:
|
|
208
243
|
return [c for c in read_json("channels.json", []) if c.get("active", True)]
|
|
209
244
|
|
|
@@ -214,7 +249,7 @@ def blueprint_catalog() -> list[tuple[str, str]]:
|
|
|
214
249
|
try:
|
|
215
250
|
data = load_blueprint_file(path)
|
|
216
251
|
identifier = str(data.get("id") or path.stem)
|
|
217
|
-
label = str(data.get("name") or path.stem)
|
|
252
|
+
label = get_display_name("blueprints", path, str(data.get("name") or data.get("title") or path.stem))
|
|
218
253
|
options.append((identifier, label))
|
|
219
254
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
220
255
|
continue
|
|
@@ -231,11 +266,11 @@ def blueprint_for_channel(channel: dict) -> dict[str, Any]:
|
|
|
231
266
|
data = load_blueprint_file(path)
|
|
232
267
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
233
268
|
continue
|
|
234
|
-
identifiers = {str(data.get("id") or ""), path.stem, str(data.get("name") or "")}
|
|
269
|
+
identifiers = {str(data.get("id") or ""), path.stem, str(data.get("name") or ""), get_display_name("blueprints", path, str(data.get("name") or path.stem))}
|
|
235
270
|
if blueprint_id in identifiers:
|
|
236
271
|
resolved = dict(data)
|
|
237
272
|
resolved.setdefault("id", blueprint_id)
|
|
238
|
-
resolved
|
|
273
|
+
resolved["name"] = get_display_name("blueprints", path, str(data.get("name") or path.stem))
|
|
239
274
|
return resolved
|
|
240
275
|
return {"id": blueprint_id, "name": blueprint_id}
|
|
241
276
|
|
|
@@ -665,14 +700,21 @@ def render_blueprints():
|
|
|
665
700
|
if not files:
|
|
666
701
|
st.info("Ainda não existem blueprints na pasta local.")
|
|
667
702
|
for path in files:
|
|
668
|
-
if search and search.lower() not in path.name.lower():
|
|
669
|
-
continue
|
|
670
703
|
try:
|
|
671
704
|
data = load_blueprint_file(path)
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
705
|
+
fallback_title = str(data.get("channel_name") or data.get("name") or data.get("title") or path.stem)
|
|
706
|
+
title = get_display_name("blueprints", path, fallback_title)
|
|
707
|
+
if search and search.lower() not in f"{title}\n{path.name}".lower():
|
|
708
|
+
continue
|
|
709
|
+
card_key = _library_card_key("blueprints", path)
|
|
710
|
+
header_cols = st.columns([0.93, 0.07], vertical_alignment="center")
|
|
711
|
+
with header_cols[0]:
|
|
712
|
+
with st.expander(f"{title} — {path.relative_to(BLUEPRINTS)}"):
|
|
713
|
+
st.caption(f"Ficheiro: {path}")
|
|
714
|
+
st.json(data)
|
|
715
|
+
with header_cols[1]:
|
|
716
|
+
_render_card_pencil(f"rename_blueprints_{card_key}")
|
|
717
|
+
_render_library_name_editor("blueprints", path, title)
|
|
676
718
|
except Exception as exc:
|
|
677
719
|
with st.expander(f"Inválido — {path.name}"):
|
|
678
720
|
st.error(str(exc))
|
|
@@ -752,7 +794,8 @@ def render_tiktok_prompt_masters():
|
|
|
752
794
|
content = load_prompt_master_file(path)
|
|
753
795
|
except (OSError, ValueError):
|
|
754
796
|
content = ""
|
|
755
|
-
|
|
797
|
+
display_heading = get_display_name("prompt_masters", path, next((line.lstrip("#").strip() for line in content.splitlines() if line.startswith("#")), path.stem))
|
|
798
|
+
if search and search.lower() not in f"{display_heading}\n{path.name}\n{content}".lower():
|
|
756
799
|
continue
|
|
757
800
|
visible_files.append(path)
|
|
758
801
|
if not visible_files:
|
|
@@ -760,24 +803,31 @@ def render_tiktok_prompt_masters():
|
|
|
760
803
|
for path in visible_files:
|
|
761
804
|
try:
|
|
762
805
|
content = load_prompt_master_file(path)
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
with
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
st.
|
|
779
|
-
|
|
780
|
-
|
|
806
|
+
fallback_heading = next((line.lstrip("#").strip() for line in content.splitlines() if line.startswith("#")), path.stem)
|
|
807
|
+
heading = get_display_name("prompt_masters", path, fallback_heading)
|
|
808
|
+
card_key = _library_card_key("prompt_masters", path)
|
|
809
|
+
header_cols = st.columns([0.93, 0.07], vertical_alignment="center")
|
|
810
|
+
with header_cols[0]:
|
|
811
|
+
with st.expander(f"{heading} — {path.name}", expanded=False):
|
|
812
|
+
st.caption(f"Ficheiro TikTok: `{path}`")
|
|
813
|
+
edited_content = st.text_area("Conteúdo Markdown", value=content, height=360, key=f"tiktok_prompt_master_editor_{path.stem}")
|
|
814
|
+
prompt_cols = st.columns(3)
|
|
815
|
+
with prompt_cols[0]:
|
|
816
|
+
if st.button("Guardar alterações", type="primary", use_container_width=True, key=f"save_prompt_master_{path.stem}"):
|
|
817
|
+
path.write_text(edited_content.rstrip() + "\n", encoding="utf-8")
|
|
818
|
+
st.success("Prompt Master actualizado.")
|
|
819
|
+
st.rerun()
|
|
820
|
+
with prompt_cols[1]:
|
|
821
|
+
st.download_button("Descarregar", data=content.encode("utf-8"), file_name=path.name, mime="text/markdown", use_container_width=True, key=f"download_prompt_master_{path.stem}")
|
|
822
|
+
with prompt_cols[2]:
|
|
823
|
+
if st.button("Apagar", use_container_width=True, key=f"delete_prompt_master_{path.stem}"):
|
|
824
|
+
path.unlink(missing_ok=True)
|
|
825
|
+
st.success("Prompt Master removido da biblioteca TikTok.")
|
|
826
|
+
st.rerun()
|
|
827
|
+
st.markdown(content)
|
|
828
|
+
with header_cols[1]:
|
|
829
|
+
_render_card_pencil(f"rename_prompt_masters_{card_key}")
|
|
830
|
+
_render_library_name_editor("prompt_masters", path, heading)
|
|
781
831
|
except (OSError, ValueError) as exc:
|
|
782
832
|
with st.expander(f"Ficheiro inválido — {path.name}"):
|
|
783
833
|
st.error(str(exc))
|
package/hermes_ui/storage.py
CHANGED
|
@@ -24,6 +24,7 @@ DEFAULTS: dict[str, Any] = {
|
|
|
24
24
|
"queues.json": {"niche": [], "blueprint": [], "brand": [], "script": [], "title": [], "thumbnail": [], "video": [], "edit": [], "upload": []},
|
|
25
25
|
"batches.json": [],
|
|
26
26
|
"uploads.json": [],
|
|
27
|
+
"display_names.json": {"blueprints": {}, "prompt_masters": {}},
|
|
27
28
|
"niche_apify_runs.json": [],
|
|
28
29
|
"metadata_edits.json": [],
|
|
29
30
|
"python_editor_edits.json": [],
|
|
@@ -306,6 +307,53 @@ def append_json(name: str, item: dict[str, Any]) -> dict[str, Any]:
|
|
|
306
307
|
return item
|
|
307
308
|
|
|
308
309
|
|
|
310
|
+
def _display_name_key(kind: str, path: Path) -> str:
|
|
311
|
+
"""Return a stable storage-relative key without renaming the physical file."""
|
|
312
|
+
resolved = path.resolve()
|
|
313
|
+
if kind == "blueprints":
|
|
314
|
+
root = BLUEPRINTS.resolve()
|
|
315
|
+
try:
|
|
316
|
+
return resolved.relative_to(root).as_posix()
|
|
317
|
+
except ValueError as exc:
|
|
318
|
+
raise ValueError("O ficheiro não pertence ao storage de Blueprints.") from exc
|
|
319
|
+
if kind == "prompt_masters":
|
|
320
|
+
root = TIKTOK_PROMPT_MASTERS.resolve()
|
|
321
|
+
try:
|
|
322
|
+
return resolved.relative_to(root).as_posix()
|
|
323
|
+
except ValueError as exc:
|
|
324
|
+
raise ValueError("O ficheiro não pertence ao storage de Prompt Masters.") from exc
|
|
325
|
+
raise ValueError(f"Tipo de biblioteca inválido: {kind}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_display_name(kind: str, path: Path, fallback: str) -> str:
|
|
329
|
+
names = read_json("display_names.json", {"blueprints": {}, "prompt_masters": {}})
|
|
330
|
+
if not isinstance(names, dict):
|
|
331
|
+
return fallback
|
|
332
|
+
entries = names.get(kind, {})
|
|
333
|
+
if not isinstance(entries, dict):
|
|
334
|
+
return fallback
|
|
335
|
+
value = str(entries.get(_display_name_key(kind, path)) or "").strip()
|
|
336
|
+
return value or fallback
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def set_display_name(kind: str, path: Path, name: str) -> str:
|
|
340
|
+
clean_name = " ".join(str(name).split()).strip()
|
|
341
|
+
if not clean_name:
|
|
342
|
+
raise ValueError("Informe um nome para a biblioteca.")
|
|
343
|
+
if len(clean_name) > 120:
|
|
344
|
+
raise ValueError("O nome deve ter no máximo 120 caracteres.")
|
|
345
|
+
names = read_json("display_names.json", {"blueprints": {}, "prompt_masters": {}})
|
|
346
|
+
if not isinstance(names, dict):
|
|
347
|
+
names = {"blueprints": {}, "prompt_masters": {}}
|
|
348
|
+
entries = names.get(kind)
|
|
349
|
+
if not isinstance(entries, dict):
|
|
350
|
+
entries = {}
|
|
351
|
+
names[kind] = entries
|
|
352
|
+
entries[_display_name_key(kind, path)] = clean_name
|
|
353
|
+
write_json("display_names.json", names)
|
|
354
|
+
return clean_name
|
|
355
|
+
|
|
356
|
+
|
|
309
357
|
def list_blueprint_files() -> list[Path]:
|
|
310
358
|
ensure_storage()
|
|
311
359
|
return sorted(BLUEPRINTS.rglob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
|
package/package.json
CHANGED