@danhachuel/thunderbolt 0.6.72 → 0.6.74

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
@@ -4969,6 +4969,21 @@ def _task_artifact_path(task: dict[str, Any], *names: str) -> Path | None:
4969
4969
  return None
4970
4970
 
4971
4971
 
4972
+ def _download_title(task: dict[str, Any]) -> str:
4973
+ """Return a filesystem-safe title for automation downloads."""
4974
+ raw_title = str(task.get("title") or task.get("topic") or task.get("name") or "Vídeo").strip()
4975
+ cleaned = re.sub(r'[<>:"/\\|?*\\x00-\\x1f]+', "_", raw_title)
4976
+ cleaned = " ".join(cleaned.split()).strip(" ._")
4977
+ return cleaned or "Vídeo"
4978
+
4979
+
4980
+ def _automation_download_name(prefix: str, task: dict[str, Any], path: Path | None, fallback_suffix: str) -> str:
4981
+ suffix = path.suffix if path and path.suffix else fallback_suffix
4982
+ if not suffix.startswith("."):
4983
+ suffix = f".{suffix}"
4984
+ return f"{prefix}_{_download_title(task)}{suffix}"
4985
+
4986
+
4972
4987
  def _is_music_task(task: dict[str, Any]) -> bool:
4973
4988
  """Identify music pipeline tasks without conflating them with ordinary video tasks."""
4974
4989
  return bool(task.get("music_mode")) or str(task.get("style_wide") or task.get("style") or "").strip().casefold() in {"music", "música"}
@@ -5508,27 +5523,99 @@ def _render_tiktok_automation_cards():
5508
5523
  for task in tiktok_tasks:
5509
5524
  task_id = str(task["id"])
5510
5525
  with st.container(border=True):
5511
- cols = st.columns([2.4, 1.4, 1.1, 1.6])
5512
- with cols[0]:
5513
- thumbnail_path = _task_thumbnail_path(task)
5526
+ task_cols = st.columns([2.25, 1.55, 1.05, 2.15], gap="small")
5527
+ thumbnail_path = _task_thumbnail_path(task)
5528
+ script_path = _task_artifact_path(task, "script")
5529
+ video_path = _task_artifact_path(task, "video")
5530
+ thumbnail_prompt_path = _task_artifact_path(task, "thumbnail_prompt_json")
5531
+ thumbnail_prompt = str(task.get("thumbnail_prompt") or "").strip()
5532
+ with task_cols[0]:
5514
5533
  if thumbnail_path:
5515
- st.image(str(thumbnail_path), width=150, caption="Thumbnail")
5534
+ st.image(str(thumbnail_path), width=180, caption="Thumbnail")
5535
+ else:
5536
+ st.caption("Thumbnail ainda não pronta")
5516
5537
  st.write(f"**{task.get('title') or task.get('topic') or 'Vídeo TikTok'}**")
5517
5538
  st.caption(f"{task.get('channel_name') or 'Canal TikTok'} · {task_id}")
5518
- with cols[1]:
5539
+ thumbnail_download_col, prompt_download_col = st.columns(2, gap="small")
5540
+ with thumbnail_download_col:
5541
+ st.download_button(
5542
+ "Baixar Thumbnail 9:16",
5543
+ data=thumbnail_path.read_bytes() if thumbnail_path else b"",
5544
+ file_name=_automation_download_name("Thumbnail9:16", task, thumbnail_path, ".png"),
5545
+ mime="image/png",
5546
+ key=f"tiktok_automation_download_thumbnail_{task_id}",
5547
+ use_container_width=True,
5548
+ disabled=thumbnail_path is None,
5549
+ )
5550
+ with prompt_download_col:
5551
+ prompt_data = thumbnail_prompt_path.read_bytes() if thumbnail_prompt_path else thumbnail_prompt.encode("utf-8")
5552
+ st.download_button(
5553
+ "Baixar Thumbnail Prompt",
5554
+ data=prompt_data,
5555
+ file_name=_automation_download_name("Thumbnail-Prompt", task, thumbnail_prompt_path, ".txt"),
5556
+ mime="application/json" if thumbnail_prompt_path else "text/plain",
5557
+ key=f"tiktok_automation_download_thumbnail_prompt_{task_id}",
5558
+ use_container_width=True,
5559
+ disabled=thumbnail_prompt_path is None and not thumbnail_prompt,
5560
+ )
5561
+ with task_cols[1]:
5519
5562
  _render_video_task_state(task)
5520
- with cols[2]:
5563
+ with task_cols[2]:
5521
5564
  st.caption("Plataforma")
5522
5565
  st.write("TikTok")
5523
5566
  st.caption("Portrait 9:16")
5524
- with cols[3]:
5567
+ with task_cols[3]:
5525
5568
  state = str(task.get("state") or "")
5526
- if st.button("Start", key=f"tiktok_automation_start_{task_id}", disabled=state not in {"to_do", "blocked", "failed"}, use_container_width=True):
5527
- _start_pipeline_task(task_id, state)
5528
- st.rerun()
5529
- if st.button("Stop", key=f"tiktok_automation_stop_{task_id}", disabled=state != "doing", use_container_width=True):
5530
- stop_task_by_user(task_id)
5531
- st.rerun()
5569
+ start_col, stop_col, delete_col = st.columns(3)
5570
+ with start_col:
5571
+ if st.button("Start", key=f"tiktok_automation_start_{task_id}", use_container_width=True, disabled=state not in {"to_do", "blocked", "failed"}):
5572
+ if _start_pipeline_task(task_id, state):
5573
+ st.rerun()
5574
+ with stop_col:
5575
+ if st.button("Stop", key=f"tiktok_automation_stop_{task_id}", use_container_width=True, disabled=state != "doing"):
5576
+ stop_task_by_user(task_id)
5577
+ st.rerun()
5578
+ script_download_col, video_download_col = st.columns(2, gap="small")
5579
+ with script_download_col:
5580
+ st.download_button(
5581
+ "Baixar Roteiro",
5582
+ data=script_path.read_bytes() if script_path else b"",
5583
+ file_name=_automation_download_name("Script", task, script_path, ".md"),
5584
+ mime="text/markdown",
5585
+ key=f"tiktok_automation_download_script_{task_id}",
5586
+ use_container_width=True,
5587
+ disabled=script_path is None,
5588
+ )
5589
+ with video_download_col:
5590
+ st.download_button(
5591
+ "Baixar Vídeo9:16",
5592
+ data=video_path.read_bytes() if video_path else b"",
5593
+ file_name=_automation_download_name("Vídeo9:16", task, video_path, ".mp4"),
5594
+ mime="video/mp4",
5595
+ key=f"tiktok_automation_download_video_{task_id}",
5596
+ use_container_width=True,
5597
+ disabled=video_path is None,
5598
+ )
5599
+ with delete_col:
5600
+ confirm_delete_key = f"tiktok_automation_confirm_delete_{task_id}"
5601
+ if st.button("Apagar", key=f"tiktok_automation_delete_{task_id}", use_container_width=True, disabled=state == "doing"):
5602
+ st.session_state[confirm_delete_key] = True
5603
+ st.rerun()
5604
+ if st.session_state.get(confirm_delete_key):
5605
+ st.warning("Remover este vídeo da fila? Os ficheiros de artefactos serão preservados.")
5606
+ confirm_col, cancel_col = st.columns(2)
5607
+ with confirm_col:
5608
+ if st.button("Confirmar", key=f"tiktok_automation_confirm_delete_button_{task_id}", use_container_width=True, type="primary"):
5609
+ try:
5610
+ delete_task(task["id"])
5611
+ st.session_state.pop(confirm_delete_key, None)
5612
+ st.rerun()
5613
+ except ValueError as exc:
5614
+ st.error(str(exc))
5615
+ with cancel_col:
5616
+ if st.button("Cancelar", key=f"tiktok_automation_cancel_delete_{task_id}", use_container_width=True):
5617
+ st.session_state.pop(confirm_delete_key, None)
5618
+ st.rerun()
5532
5619
 
5533
5620
 
5534
5621
  def render_tiktok_automation():
@@ -5624,9 +5711,9 @@ def _render_youtube_automation_cards():
5624
5711
  thumbnail_download_col, prompt_download_col = st.columns(2, gap="small")
5625
5712
  with thumbnail_download_col:
5626
5713
  st.download_button(
5627
- "Baixar Thumbnail",
5714
+ "Baixar Thumbnail 9:16",
5628
5715
  data=thumbnail_path.read_bytes() if thumbnail_path else b"",
5629
- file_name=thumbnail_path.name if thumbnail_path else "thumbnail.png",
5716
+ file_name=_automation_download_name("Thumbnail9:16", task, thumbnail_path, ".png"),
5630
5717
  mime="image/png",
5631
5718
  key=f"automation_download_thumbnail_{task['id']}",
5632
5719
  use_container_width=True,
@@ -5637,7 +5724,7 @@ def _render_youtube_automation_cards():
5637
5724
  st.download_button(
5638
5725
  "Baixar Thumbnail Prompt",
5639
5726
  data=prompt_data,
5640
- file_name=thumbnail_prompt_path.name if thumbnail_prompt_path else "thumbnail-prompt.txt",
5727
+ file_name=_automation_download_name("Thumbnail-Prompt", task, thumbnail_prompt_path, ".txt"),
5641
5728
  mime="application/json" if thumbnail_prompt_path else "text/plain",
5642
5729
  key=f"automation_download_thumbnail_prompt_{task['id']}",
5643
5730
  use_container_width=True,
@@ -5664,7 +5751,7 @@ def _render_youtube_automation_cards():
5664
5751
  st.download_button(
5665
5752
  "Baixar Roteiro",
5666
5753
  data=script_path.read_bytes() if script_path else b"",
5667
- file_name=script_path.name if script_path else "roteiro.md",
5754
+ file_name=_automation_download_name("Script", task, script_path, ".md"),
5668
5755
  mime="text/markdown",
5669
5756
  key=f"automation_download_script_{task['id']}",
5670
5757
  use_container_width=True,
@@ -5672,9 +5759,9 @@ def _render_youtube_automation_cards():
5672
5759
  )
5673
5760
  with video_download_col:
5674
5761
  st.download_button(
5675
- "Baixar Vídeo",
5762
+ "Baixar Vídeo9:16",
5676
5763
  data=video_path.read_bytes() if video_path else b"",
5677
- file_name=video_path.name if video_path else "video.mp4",
5764
+ file_name=_automation_download_name("Vídeo9:16", task, video_path, ".mp4"),
5678
5765
  mime="video/mp4",
5679
5766
  key=f"automation_download_video_{task['id']}",
5680
5767
  use_container_width=True,
@@ -34,7 +34,7 @@ DEFAULTS: dict[str, Any] = {
34
34
  "uploads.json": [],
35
35
  "notifications.json": [],
36
36
  "media_downloads.json": [],
37
- "display_names.json": {"blueprints": {}, "prompt_masters": {}},
37
+ "display_names.json": {"blueprints": {}, "prompt_masters": {}, "music": {}, "music_lyrics": {}},
38
38
  "niche_apify_runs.json": [],
39
39
  "metadata_edits.json": [],
40
40
  "python_editor_edits.json": [],
@@ -615,6 +615,13 @@ def _display_name_key(kind: str, path: Path) -> str:
615
615
  return resolved.relative_to(root).as_posix()
616
616
  except ValueError as exc:
617
617
  raise ValueError("O ficheiro não pertence ao storage de Prompt Masters.") from exc
618
+ if kind in {"music", "music_lyrics"}:
619
+ root = (STORAGE / ("music" if kind == "music" else "scripts")).resolve()
620
+ try:
621
+ return resolved.relative_to(root).as_posix()
622
+ except ValueError as exc:
623
+ label = "música" if kind == "music" else "lyrics"
624
+ raise ValueError(f"O ficheiro não pertence ao storage de {label}.") from exc
618
625
  raise ValueError(f"Tipo de biblioteca inválido: {kind}")
619
626
 
620
627
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.72",
3
+ "version": "0.6.74",
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",