@danhachuel/thunderbolt 0.4.17 → 0.4.19

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
@@ -34,7 +34,7 @@ def display_version(version: str) -> str:
34
34
 
35
35
  APP_VERSION_LABEL = display_version(APP_VERSION)
36
36
 
37
- from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, retry_task_with_current_settings, set_channel_defaults, transition_task, update_channel, update_channel_video
37
+ from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, retry_task_with_current_settings, set_channel_defaults, stop_task_by_user, transition_task, update_channel, update_channel_video
38
38
  from hermes_ui.drafts import list_drafts, save_draft
39
39
  from hermes_ui.automation_worker import load_worker_status
40
40
  from hermes_ui.pipeline_worker import load_pipeline_worker_status, recover_stale_tasks, STALE_TASK_SECONDS, WORKER_HEARTBEAT_TIMEOUT_SECONDS
@@ -3778,8 +3778,12 @@ def _render_video_task_state(task: dict[str, Any]) -> None:
3778
3778
  state = str(task.get("state") or "unknown").strip().lower()
3779
3779
  progress = _video_task_progress(task)
3780
3780
  st.caption("Estado")
3781
- st.write(state or "")
3782
- st.caption(VIDEO_TASK_STATE_LABELS.get(state, state.replace("_", " ").capitalize() or "Desconhecido"))
3781
+ if state == "blocked" and task.get("stop_reason") == "user":
3782
+ st.write("Stoped by User")
3783
+ st.caption("Parado manualmente pelo utilizador.")
3784
+ else:
3785
+ st.write(state or "—")
3786
+ st.caption(VIDEO_TASK_STATE_LABELS.get(state, state.replace("_", " ").capitalize() or "Desconhecido"))
3783
3787
  st.progress(progress, text=f"{progress}%")
3784
3788
  helper_status = str(task.get("video_helper_status") or "").strip()
3785
3789
  if state == "doing" and helper_status:
@@ -4240,7 +4244,7 @@ def render_automation():
4240
4244
  st.rerun()
4241
4245
  with stop_col:
4242
4246
  if st.button("Stop", key=f"automation_stop_{task['id']}", use_container_width=True, disabled=state != "doing"):
4243
- transition_task(task["id"], "blocked")
4247
+ stop_task_by_user(task["id"])
4244
4248
  st.rerun()
4245
4249
  with delete_col:
4246
4250
  confirm_delete_key = f"automation_confirm_delete_{task['id']}"
@@ -359,6 +359,8 @@ def transition_task(task_id: str, state: str | None = None, stage: str | None =
359
359
  previous_state = str(task.get("state") or "")
360
360
  if state:
361
361
  task["state"] = state
362
+ if state != "blocked":
363
+ task.pop("stop_reason", None)
362
364
  if stage:
363
365
  if stage not in STAGES and stage not in LEGACY_STAGES:
364
366
  raise ValueError(f"Etapa inválida: {stage}")
@@ -375,6 +377,21 @@ def transition_task(task_id: str, state: str | None = None, stage: str | None =
375
377
  return updated
376
378
 
377
379
 
380
+ def stop_task_by_user(task_id: str) -> dict[str, Any] | None:
381
+ """Stop a running video task and retain the user-originated reason for the UI."""
382
+ task = transition_task(task_id, "blocked")
383
+ if task is None:
384
+ return None
385
+ tasks = read_json("tasks.json", [])
386
+ for persisted in tasks:
387
+ if persisted.get("id") == task_id:
388
+ persisted["stop_reason"] = "user"
389
+ persisted["updated_at"] = now()
390
+ write_json("tasks.json", tasks)
391
+ return persisted
392
+ return task
393
+
394
+
378
395
  def retry_task_with_current_settings(task_id: str) -> dict[str, Any] | None:
379
396
  """Queue a failed or blocked task without persisting API credentials or provider snapshots.
380
397
 
@@ -18,7 +18,7 @@ from integrations.upload_routing import upload_with_default_route
18
18
  from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_title_and_keywords, generate_thumbnail_prompt, generate_topic_for_channel
19
19
  from hermes_ui.script_documents import save_script_document
20
20
  from hermes_ui.script_generation import generate_script_document
21
- from hermes_ui.storage import STORAGE, atomic_write, ensure_storage, read_json, write_json
21
+ from hermes_ui.storage import STORAGE, atomic_write, ensure_storage, get_display_name, list_blueprint_files, load_blueprint_file, read_json, write_json
22
22
  from hermes_ui.llm_providers import active_llm_card, provider_definition
23
23
  from hermes_ui.media_generation import MediaGenerationError, _append_generation_constraints, generate_image_from_pool, generate_video_from_pool
24
24
  from hermes_ui.media_providers import FULL_IA_VIDEO_PROVIDER_CODES, media_cards_for_pool, media_provider_definition
@@ -273,11 +273,29 @@ def _channel_for_task(task: dict[str, Any]) -> dict[str, Any]:
273
273
 
274
274
 
275
275
  def _blueprint_for_channel(channel: dict[str, Any]) -> dict[str, Any]:
276
- blueprint_id = str(channel.get("default_blueprint_id") or channel.get("blueprint_id") or "")
277
- blueprints = read_json("blueprints.json", [])
278
- if not isinstance(blueprints, list):
276
+ """Resolve the Blueprint assigned to a channel from the persisted library files."""
277
+ blueprint_id = str(channel.get("default_blueprint_id") or channel.get("blueprint_id") or "").strip()
278
+ blueprint_name = ""
279
+ if not blueprint_id and not blueprint_name:
279
280
  return {}
280
- return next((item for item in blueprints if isinstance(item, dict) and str(item.get("id")) == blueprint_id), {})
281
+ for path in list_blueprint_files():
282
+ try:
283
+ data = load_blueprint_file(path)
284
+ except (OSError, ValueError, json.JSONDecodeError):
285
+ continue
286
+ display_name = get_display_name("blueprints", path, str(data.get("name") or data.get("title") or path.stem))
287
+ identifiers = {
288
+ str(data.get("id") or "").strip(),
289
+ path.stem,
290
+ str(data.get("name") or "").strip(),
291
+ display_name,
292
+ }
293
+ if blueprint_id in identifiers or blueprint_name in identifiers:
294
+ resolved = dict(data)
295
+ resolved.setdefault("id", blueprint_id or path.stem)
296
+ resolved["name"] = display_name
297
+ return resolved
298
+ return {"id": blueprint_id, "name": blueprint_name or blueprint_id}
281
299
 
282
300
 
283
301
  def _keywords(topic: str, title: str, niche: str = "") -> list[str]:
@@ -1209,6 +1227,8 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
1209
1227
  channel = _channel_for_task(task)
1210
1228
  settings = _settings()
1211
1229
  blueprint = _blueprint_for_channel(channel)
1230
+ if not blueprint and (task.get("blueprint_id") or task.get("blueprint_name")):
1231
+ blueprint = {"id": str(task.get("blueprint_id") or ""), "name": str(task.get("blueprint_name") or task.get("blueprint_id") or "")}
1212
1232
  route = _normalise_video_route(task, settings)
1213
1233
  topic = str(task.get("topic") or "").strip()
1214
1234
  if route != "music" and (not topic or str(task.get("topic_source") or "") in {"auto", "llm_pending"}):
@@ -43,7 +43,8 @@ def save_script_document(document: dict[str, Any]) -> dict[str, Any]:
43
43
  f"title: {title}",
44
44
  f"language: {str(document.get('language') or '')}",
45
45
  f"channel: {str(document.get('channel_name') or '')}",
46
- f"blueprint: {str(document.get('blueprint_name') or '')}",
46
+ f"blueprint_id: {str(document.get('blueprint_id') or '')}",
47
+ f"blueprint: {str(document.get('blueprint_name') or document.get('blueprint_id') or '')}",
47
48
  f"created_at: {created_at}",
48
49
  "---",
49
50
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.17",
3
+ "version": "0.4.19",
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",