@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 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.setdefault("name", str(data.get("name") or path.stem))
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
- title = data.get("channel_name") or data.get("name") or data.get("title") or path.stem
673
- with st.expander(f"{title} {path.relative_to(BLUEPRINTS)}"):
674
- st.caption(f"Ficheiro: {path}")
675
- st.json(data)
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
- if search and search.lower() not in f"{path.name}\n{content}".lower():
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
- heading = next((line.lstrip("#").strip() for line in content.splitlines() if line.startswith("#")), path.stem)
764
- with st.expander(f"{heading} — {path.name}", expanded=False):
765
- st.caption(f"Ficheiro TikTok: `{path}`")
766
- edited_content = st.text_area("Conteúdo Markdown", value=content, height=360, key=f"tiktok_prompt_master_editor_{path.stem}")
767
- prompt_cols = st.columns(3)
768
- with prompt_cols[0]:
769
- if st.button("Guardar alterações", type="primary", use_container_width=True, key=f"save_prompt_master_{path.stem}"):
770
- path.write_text(edited_content.rstrip() + "\n", encoding="utf-8")
771
- st.success("Prompt Master actualizado.")
772
- st.rerun()
773
- with prompt_cols[1]:
774
- 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}")
775
- with prompt_cols[2]:
776
- if st.button("Apagar", use_container_width=True, key=f"delete_prompt_master_{path.stem}"):
777
- path.unlink(missing_ok=True)
778
- st.success("Prompt Master removido da biblioteca TikTok.")
779
- st.rerun()
780
- st.markdown(content)
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))
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.2.63",
3
+ "version": "0.2.64",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "main": "scripts/cli.mjs",
6
6
  "type": "module",