@danhachuel/thunderbolt 0.6.80 → 0.6.82

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.
@@ -28,7 +28,6 @@ from hermes_ui.media_generation import (
28
28
  generate_motion_control_video,
29
29
  generate_ugc_product_video,
30
30
  generate_video_for_card,
31
- upload_kie_file,
32
31
  validate_motion_control_file,
33
32
  )
34
33
  from hermes_ui.creative_generation import generate_ugc_segment_prompts
@@ -116,6 +115,25 @@ def _provider_label(card: Mapping[str, Any]) -> str:
116
115
  return f"{definition.label} · {model}"
117
116
 
118
117
 
118
+ def _content_metadata(item: Mapping[str, Any]) -> dict[str, Any]:
119
+ raw = item.get("metadata") if isinstance(item.get("metadata"), Mapping) else item.get("metadata_json")
120
+ if isinstance(raw, Mapping):
121
+ return dict(raw)
122
+ try:
123
+ parsed = json.loads(str(raw or "{}"))
124
+ except (TypeError, json.JSONDecodeError):
125
+ return {}
126
+ return dict(parsed) if isinstance(parsed, Mapping) else {}
127
+
128
+
129
+ def _ugc_media_records(repository: Any) -> list[dict[str, Any]]:
130
+ try:
131
+ records = repository.list_content("", limit=200)
132
+ except Exception:
133
+ return []
134
+ return [item for item in records if _content_metadata(item).get("workflow") == "ugc_products"]
135
+
136
+
119
137
  def render_ai_influencers_api_status(settings: dict[str, Any]) -> None:
120
138
  """Show the effective backend status after selector and credentials are loaded."""
121
139
  status = backend_status(settings)
@@ -349,8 +367,12 @@ def _workflow_owner(repository: Any) -> str:
349
367
  return ensure_standalone_content_owner(repository)
350
368
 
351
369
 
352
- def _workflow_provider_cards(settings: Mapping[str, Any]) -> list[dict[str, Any]]:
353
- return [card for card in media_cards_for_pool(settings, "video") if str(card.get("provider") or "").strip().lower() == "kie_ai"]
370
+ def _workflow_provider_cards(settings: Mapping[str, Any], *, provider: str | None = "kie_ai") -> list[dict[str, Any]]:
371
+ cards = media_cards_for_pool(settings, "video")
372
+ if provider is None:
373
+ return cards
374
+ normalized_provider = str(provider).strip().lower()
375
+ return [card for card in cards if str(card.get("provider") or "").strip().lower() == normalized_provider]
354
376
 
355
377
 
356
378
  def render_motion_control(settings: dict[str, Any]) -> None:
@@ -421,77 +443,156 @@ def render_motion_control(settings: dict[str, Any]) -> None:
421
443
 
422
444
 
423
445
  def render_ugc_products(settings: dict[str, Any]) -> None:
424
- """Render the local product-image to VEO3 workflow without Telegram or social publishing."""
446
+ """Create portrait UGC photos or videos from a product and a saved character."""
425
447
  st.title("UGC Products")
426
- st.caption("Crie um anúncio UGC a partir de uma imagem de produto e de um roteiro local. O workflow gera dois clips VEO3 de 8 segundos e junta-os com FFmpeg no storage local.")
448
+ st.caption("Crie fotos ou vídeos UGC em formato vertical 9:16 para TikTok e Instagram usando um produto e um personagem cadastrados.")
427
449
  repository = _repository(settings)
428
450
  if repository is None:
429
451
  return
430
- video_cards = [card for card in _workflow_provider_cards(settings) if str(card.get("model") or "").strip().lower() in {"", "veo3", "veo3_fast", "veo3_lite"}]
431
- if not video_cards:
432
- st.warning("Active e configure um cartão KIE AI com modelo VEO3 compatível no pool de vídeo em Configuração API > API Keys > Imagem e Video IA.")
433
- return
434
- image_cards, _ = _provider_options(settings, "image")
435
- image_cards = [card for card in image_cards if str(card.get("provider") or "").strip().lower() == "nano_banana"]
436
- with st.form("influencer_ugc_products_form"):
437
- product_upload = st.file_uploader("Imagem do produto", type=["jpg", "jpeg", "png", "webp"], key="ugc_products_image")
438
- script = st.text_area("Roteiro de vídeo", height=180, placeholder="Escreva a demonstração, falas e acções. Para controlar os dois clips manualmente, separe-os com uma linha contendo ---.", key="ugc_products_script")
439
- improve_image = st.checkbox("Melhorar a imagem com o provider de imagem activo (opcional)", value=False, key="ugc_products_improve_image")
440
- image_provider_id = ""
441
- if improve_image and image_cards:
442
- image_provider_options = [str(card.get("id") or "") for card in image_cards]
443
- image_provider_id = st.selectbox("Provider de melhoria da imagem", image_provider_options, format_func=lambda value: _provider_label(next(card for card in image_cards if str(card.get("id")) == value)), key="ugc_products_image_provider")
444
- elif improve_image:
445
- st.info("Não existe um provider activo no pool de imagem; será usada a imagem original.")
446
- card_options = [str(card.get("id") or "") for card in video_cards]
447
- provider_id = st.selectbox("Provider VEO3", card_options, format_func=lambda value: _provider_label(next(card for card in video_cards if str(card.get("id")) == value)), key="ugc_products_video_provider")
448
- generate = st.form_submit_button("Gerar UGC Product", type="primary", use_container_width=True)
449
- if generate:
450
- if product_upload is None:
451
- st.error("Seleccione a imagem do produto.")
452
- return
453
- card = next(card for card in video_cards if str(card.get("id")) == provider_id)
454
- try:
455
- product_path = _store_uploaded_file(product_upload, "ugc-products-inputs")
456
- image_path = product_path
457
- if improve_image and image_provider_id:
458
- image_card = next(card for card in image_cards if str(card.get("id")) == image_provider_id)
459
- with st.spinner("A melhorar a imagem do produto…"):
460
- image_path = generate_image_for_card(settings, image_card, "Melhorar a apresentação fotográfica do mesmo produto sem alterar a embalagem, logótipo, cores, forma ou objectos visíveis. Fundo comercial limpo e luz natural.", topic="UGC Product", reference_image=product_path)
461
- prompts = generate_ugc_segment_prompts(settings, script)
462
- owner_id = _workflow_owner(repository)
463
- record = repository.create_content(
464
- {
465
- "influencer_id": owner_id,
466
- "content_type": "video",
467
- "prompt": script,
468
- "caption": "",
469
- "provider": card.get("provider"),
470
- "model": str(card.get("model") or "veo3_fast"),
471
- "platform": "",
472
- "state": "running",
473
- "metadata": {"workflow": "ugc_products", "product_image_path": str(product_path), "effective_image_path": str(image_path), "segment_prompts": prompts, "telegram": False, "social_publish": False},
474
- }
475
- )
476
- with st.spinner("A enviar a imagem para KIE, a gerar os dois clips VEO3 e a juntá-los localmente…"):
477
- try:
478
- image_url = upload_kie_file(image_path, card, upload_path="thunderbolt/ugc-products")
479
- output, task_ids = generate_ugc_product_video(settings, card, image_url=image_url, prompts=prompts)
480
- repository.update_content(record["id"], {"state": "completed", "artifact_path": str(output), "provider_request_id": ",".join(task_ids), "metadata": {"workflow": "ugc_products", "product_image_path": str(product_path), "effective_image_path": str(image_path), "segment_prompts": prompts, "task_ids": task_ids, "telegram": False, "social_publish": False}})
481
- record_notification("influencer_content_completed", "UGC Products concluído", "O vídeo UGC Product foi gerado, unido e guardado localmente.", metadata={"content_id": record["id"], "workflow": "ugc_products", "provider": card.get("provider")}, dedupe_key=f"influencer-workflow:{record['id']}:completed")
482
- st.success("UGC Product concluído. O vídeo final está disponível no histórico local abaixo.")
483
- with st.expander("Prompts dos dois segmentos", expanded=False):
484
- for index, prompt_item in enumerate(prompts, start=1):
485
- st.markdown(f"**Segmento {index}**")
486
- st.code(prompt_item)
487
- except Exception as exc:
488
- repository.update_content(record["id"], {"state": "failed", "error": str(exc)[:1000]})
489
- record_notification("influencer_content_failed", "UGC Products falhou", f"A criação UGC Products falhou: {str(exc)[:240]}", metadata={"content_id": record["id"], "workflow": "ugc_products", "provider": card.get("provider")}, dedupe_key=f"influencer-workflow:{record['id']}:failed")
490
- st.error(f"Não foi possível gerar o UGC Product: {exc}")
491
- except (MediaGenerationError, ValueError, OSError) as exc:
492
- st.error(str(exc))
493
- _render_content_history(repository, STANDALONE_CONTENT_INFLUENCER_ID)
452
+ media_tab, ready_tab = st.tabs(["Criar UGC", "Midias Prontas"])
453
+ try:
454
+ influencers = [item for item in repository.list_influencers() if str(item.get("id") or "") != STANDALONE_CONTENT_INFLUENCER_ID]
455
+ except Exception:
456
+ influencers = []
494
457
 
458
+ with media_tab:
459
+ if not influencers:
460
+ st.info("Cadastre pelo menos um personagem na aba Personagens antes de criar UGC.")
461
+ else:
462
+ influencer_options = _influencer_options(influencers)
463
+ selected_influencer = st.selectbox(
464
+ "Personagem",
465
+ influencer_options,
466
+ format_func=lambda value: _influencer_label(influencers, value),
467
+ key="ugc_products_character",
468
+ )
469
+ selected_character = next(item for item in influencers if str(item.get("id")) == selected_influencer)
470
+ try:
471
+ character_assets = repository.list_assets(selected_influencer)
472
+ except Exception:
473
+ character_assets = []
474
+ character_images = [item for item in character_assets if str(item.get("asset_type") or "") == "image"]
475
+ product_name = st.text_input("Nome do produto", key="ugc_products_name", placeholder="Ex.: Sérum facial hidratante")
476
+ product_info = st.text_area("Informação do Produto ou link do produto", key="ugc_products_info", height=110, placeholder="Descreva benefícios, características, oferta ou cole o link do produto…")
477
+ product_uploads = st.file_uploader(
478
+ "Imagem do produto (fotos/vídeos)",
479
+ type=["jpg", "jpeg", "png", "webp", "mp4", "mov", "webm"],
480
+ accept_multiple_files=True,
481
+ key="ugc_products_inputs",
482
+ help="Anexe uma ou mais referências do produto. Para geração, a primeira imagem será usada como referência visual principal.",
483
+ )
484
+ generation_mode = st.radio("Geração", ["Gerar Foto", "Gerar Video"], horizontal=True, key="ugc_products_generation_mode")
485
+ duration = st.number_input("Duração de cada clip (segundos)", min_value=2, max_value=30, value=8, step=1, key="ugc_products_duration", disabled=generation_mode != "Gerar Video")
486
+ image_cards, image_options = _provider_options(settings, "image")
487
+ video_cards = _workflow_provider_cards(settings, provider=None)
488
+ video_options = [str(card.get("id") or "") for card in video_cards]
489
+ image_provider_id = st.selectbox("Provider / modelo de imagem", image_options, format_func=lambda value: _provider_label(next(card for card in image_cards if str(card.get("id")) == value)), key="ugc_products_image_provider") if image_cards else ""
490
+ video_provider_id = st.selectbox("Provider / modelo de vídeo", video_options, format_func=lambda value: _provider_label(next(card for card in video_cards if str(card.get("id")) == value)), key="ugc_products_video_provider") if video_cards else ""
491
+ script = st.text_area("Roteiro de vídeo (UGC)", height=150, key="ugc_products_script", placeholder="O roteiro gerado pela IA aparecerá aqui; também pode editá-lo antes de gerar.")
492
+ generate_script = st.button("Gerar roteiro com IA", use_container_width=True, key="ugc_products_generate_script")
493
+ generate = st.button("Gerar Foto" if generation_mode == "Gerar Foto" else "Gerar Video", type="primary", use_container_width=True, key="ugc_products_generate")
494
+ if generate_script:
495
+ context = "\n".join(
496
+ item for item in [
497
+ f"Produto: {product_name.strip()}" if product_name.strip() else "",
498
+ f"Informação/link: {product_info.strip()}" if product_info.strip() else "",
499
+ f"Personagem: {selected_character.get('name') or selected_influencer}",
500
+ f"Biografia do personagem: {selected_character.get('bio') or ''}",
501
+ ] if item
502
+ )
503
+ prompts = generate_ugc_segment_prompts(settings, context or "Criar uma demonstração UGC natural do produto.")
504
+ st.session_state["ugc_products_script"] = "\n---\n".join(prompts)
505
+ st.rerun()
506
+ if generate:
507
+ if not product_name.strip():
508
+ st.error("Informe o nome do produto.")
509
+ elif not product_info.strip() and not product_uploads:
510
+ st.error("Informe os dados/link do produto ou anexe pelo menos uma mídia.")
511
+ elif not script.strip():
512
+ st.error("Escreva o roteiro ou use o botão Gerar roteiro com IA.")
513
+ elif generation_mode == "Gerar Foto" and not image_provider_id:
514
+ st.error("Active um provider de imagem antes de gerar a foto.")
515
+ elif generation_mode == "Gerar Video" and not video_provider_id:
516
+ st.error("Active um provider de vídeo antes de gerar o vídeo.")
517
+ else:
518
+ image_upload = next((item for item in product_uploads or [] if Path(str(getattr(item, "name", ""))).suffix.lower() in {".jpg", ".jpeg", ".png", ".webp"}), None)
519
+ if image_upload is None:
520
+ st.error("Anexe pelo menos uma imagem do produto para usar como referência visual.")
521
+ else:
522
+ try:
523
+ product_path = _store_uploaded_file(image_upload, "ugc-products-inputs")
524
+ character_path = _local_asset_path(character_images[0]) if character_images else None
525
+ prompts = [part.strip() for part in script.split("\n---\n") if part.strip()]
526
+ if len(prompts) != 2:
527
+ prompts = generate_ugc_segment_prompts(settings, script)
528
+ card = next((item for item in (image_cards if generation_mode == "Gerar Foto" else video_cards) if str(item.get("id")) == (image_provider_id if generation_mode == "Gerar Foto" else video_provider_id)), None)
529
+ character_name = str(selected_character.get("name") or selected_influencer)
530
+ enriched_prompts = [f"Personagem: {character_name}. Produto: {product_name.strip()}. Informação: {product_info.strip()}. {item} Formato vertical 9:16, enquadramento para telemóvel, TikTok e Instagram." for item in prompts]
531
+ owner_id = selected_influencer
532
+ record = repository.create_content({
533
+ "influencer_id": owner_id,
534
+ "content_type": "image" if generation_mode == "Gerar Foto" else "video",
535
+ "prompt": script,
536
+ "caption": f"{product_name.strip()} + {character_name}",
537
+ "provider": card.get("provider") if card else "",
538
+ "model": str(card.get("model") or "") if card else "",
539
+ "platform": "TikTok, Instagram",
540
+ "state": "running",
541
+ "metadata": {"workflow": "ugc_products", "product_name": product_name.strip(), "product_info": product_info.strip(), "character_name": character_name, "character_asset_path": str(character_path or ""), "product_image_path": str(product_path), "generation_mode": generation_mode, "duration_seconds": int(duration), "aspect_ratio": "9:16", "segment_prompts": enriched_prompts, "telegram": False, "social_publish": False},
542
+ })
543
+ with st.spinner("A gerar a mídia UGC em formato vertical 9:16…"):
544
+ if generation_mode == "Gerar Foto":
545
+ image_card = {**card, "aspect_ratio": "9:16"}
546
+ output = generate_image_for_card(settings, image_card, enriched_prompts[0], topic=product_name.strip(), reference_image=product_path)
547
+ task_ids = []
548
+ else:
549
+ output = generate_ugc_product_video(settings, card, image_url=_image_input({"stored_path": str(product_path), "mime_type": mimetypes.guess_type(product_path.name)[0] or "image/jpeg"}), prompts=enriched_prompts, duration=int(duration))[0]
550
+ task_ids = []
551
+ repository.update_content(record["id"], {"state": "completed", "artifact_path": str(output), "provider_request_id": ",".join(task_ids)})
552
+ st.success("Mídia UGC criada e guardada em Midias Prontas.")
553
+ except Exception as exc:
554
+ repository.update_content(record["id"], {"state": "failed", "error": str(exc)[:1000]})
555
+ st.error(f"Não foi possível gerar a mídia UGC: {exc}")
556
+ if not image_cards and not video_cards:
557
+ st.warning("Configure pelo menos um provider de imagem ou vídeo em Configuração API > API Keys > Imagem e Video IA.")
558
+
559
+ with ready_tab:
560
+ st.subheader("Midias Prontas")
561
+ records = _ugc_media_records(repository)
562
+ if not records:
563
+ st.info("Ainda não existem mídias UGC prontas.")
564
+ for item in records:
565
+ metadata = _content_metadata(item)
566
+ artifact = Path(str(item.get("artifact_path") or ""))
567
+ content_id = str(item.get("id") or "content")
568
+ title = str(item.get("caption") or f"{metadata.get('product_name') or 'Produto'} + {metadata.get('character_name') or 'Personagem'}")
569
+ with st.container(border=True):
570
+ preview_col, detail_col, edit_col, download_col = st.columns([1.2, 2.7, 0.35, 0.9])
571
+ with preview_col:
572
+ if artifact.is_file() and str(item.get("content_type") or "") == "image":
573
+ st.image(str(artifact), use_container_width=True)
574
+ elif artifact.is_file():
575
+ st.video(str(artifact))
576
+ else:
577
+ st.caption("Artefacto indisponível")
578
+ with detail_col:
579
+ st.write(f"**{title}**")
580
+ st.caption(f"{metadata.get('product_name') or 'Produto'} · {metadata.get('character_name') or 'Personagem'} · {metadata.get('generation_mode') or 'UGC'} · 9:16")
581
+ st.caption(f"Criado: {item.get('created_at') or '—'}")
582
+ with edit_col:
583
+ if st.button("", icon=":material/edit:", help="Renomear mídia", key=f"ugc_ready_rename_{content_id}"):
584
+ st.session_state[f"ugc_ready_editing_{content_id}"] = True
585
+ with download_col:
586
+ if artifact.is_file() and str(item.get("state") or "") == "completed":
587
+ mime = mimetypes.guess_type(artifact.name)[0] or ("image/png" if str(item.get("content_type") or "") == "image" else "video/mp4")
588
+ st.download_button("Download", data=artifact.read_bytes(), file_name=artifact.name, mime=mime, key=f"ugc_ready_download_{content_id}", use_container_width=True)
589
+ if st.session_state.get(f"ugc_ready_editing_{content_id}"):
590
+ with st.form(f"ugc_ready_rename_form_{content_id}"):
591
+ renamed = st.text_input("Nome da mídia", value=title, key=f"ugc_ready_name_{content_id}")
592
+ if st.form_submit_button("Guardar nome"):
593
+ repository.update_content(content_id, {"caption": renamed.strip() or title})
594
+ st.session_state.pop(f"ugc_ready_editing_{content_id}", None)
595
+ st.rerun()
495
596
 
496
597
  def render_ai_influencer_content(settings: dict[str, Any]) -> None:
497
598
  st.title("Geração de Conteúdo IA")
@@ -584,7 +584,7 @@ def _video_endpoint(card: Mapping[str, Any]) -> str:
584
584
  raise MediaGenerationError(f"O provider {card.get('provider')} não tem endpoint de vídeo configurado.")
585
585
 
586
586
 
587
- def _video_request(card: dict[str, Any], prompt: str, image_url: str = "") -> Any:
587
+ def _video_request(card: dict[str, Any], prompt: str, image_url: str = "", duration: int | None = None, aspect_ratio: str | None = None) -> Any:
588
588
  style = str(card.get("api_style") or media_provider_definition(card.get("provider")).api_style)
589
589
  endpoint = _video_endpoint(card)
590
590
  body: dict[str, Any] = {
@@ -596,20 +596,26 @@ def _video_request(card: dict[str, Any], prompt: str, image_url: str = "") -> An
596
596
  size=str(card.get("video_size") or ""),
597
597
  ),
598
598
  }
599
+ if aspect_ratio:
600
+ body["aspect_ratio"] = str(aspect_ratio)
599
601
  if image_url:
600
602
  body["image_url"] = image_url
603
+ if duration is not None:
604
+ body["duration"] = max(1, int(duration))
601
605
  if style == "openrouter":
602
- body["aspect_ratio"] = str(card.get("aspect_ratio") or INTERNAL_VIDEO_ASPECT_RATIO)
606
+ body["aspect_ratio"] = str(aspect_ratio or card.get("aspect_ratio") or INTERNAL_VIDEO_ASPECT_RATIO)
603
607
  body["resolution"] = str(card.get("video_size") or INTERNAL_VIDEO_SIZE)
604
608
  return requests.post(endpoint, headers=_headers(card), json=body, timeout=180)
605
609
  if style == "kie":
606
610
  input_payload: dict[str, Any] = {
607
611
  "prompt": body["prompt"],
608
- "aspect_ratio": str(card.get("aspect_ratio") or INTERNAL_VIDEO_ASPECT_RATIO),
612
+ "aspect_ratio": str(aspect_ratio or card.get("aspect_ratio") or INTERNAL_VIDEO_ASPECT_RATIO),
609
613
  "resolution": str(card.get("video_size") or INTERNAL_VIDEO_SIZE),
610
614
  }
611
615
  if image_url:
612
616
  input_payload["imageUrls"] = [image_url]
617
+ if duration is not None:
618
+ input_payload["duration"] = max(1, int(duration))
613
619
  return requests.post(endpoint, headers=_headers(card), json={"model": _model(card), "input": input_payload}, timeout=180)
614
620
  if style == "heygen":
615
621
  avatar_id = str(card.get("avatar_id") or "").strip()
@@ -632,6 +638,8 @@ def _video_request(card: dict[str, Any], prompt: str, image_url: str = "") -> An
632
638
  if style == "replicate":
633
639
  image_input_key = str(card.get("image_input_key") or "image").strip() or "image"
634
640
  input_payload = {"prompt": body["prompt"]}
641
+ if duration is not None:
642
+ input_payload["duration"] = max(1, int(duration))
635
643
  if image_url:
636
644
  input_payload[image_input_key] = image_url
637
645
  return requests.post(endpoint, headers=_headers(card), json={"version": _model(card), "input": input_payload}, timeout=180)
@@ -725,6 +733,8 @@ def generate_video_for_card(
725
733
  prompt: str,
726
734
  *,
727
735
  image_url: str = "",
736
+ duration: int | None = None,
737
+ aspect_ratio: str | None = None,
728
738
  output_path: Path | None = None,
729
739
  ) -> Path:
730
740
  """Submit and resolve one video generation request."""
@@ -732,7 +742,7 @@ def generate_video_for_card(
732
742
  provider = str(card.get("provider") or "").strip().lower()
733
743
 
734
744
  def request(current: dict[str, Any]) -> Any:
735
- return _video_request(current, prompt, image_url=image_url)
745
+ return _video_request(current, prompt, image_url=image_url, duration=duration, aspect_ratio=aspect_ratio)
736
746
 
737
747
  try:
738
748
  routed = route_json_request(settings, pool=POOL_VIDEO, cards=[card], request=request)
@@ -1100,9 +1110,10 @@ def generate_ugc_product_video(
1100
1110
  *,
1101
1111
  image_url: str,
1102
1112
  prompts: list[str],
1113
+ duration: int = 8,
1103
1114
  output_path: Path | None = None,
1104
1115
  ) -> tuple[Path, list[str]]:
1105
- """Generate two 8-second KIE VEO3 clips and concatenate them locally."""
1116
+ """Generate two 8-second clips with the selected video card and concatenate them locally."""
1106
1117
  clean_prompts = [str(item or "").strip() for item in prompts if str(item or "").strip()]
1107
1118
  if len(clean_prompts) != 2:
1108
1119
  raise MediaGenerationError("UGC Products requer exactamente dois prompts de segmento.")
@@ -1111,6 +1122,6 @@ def generate_ugc_product_video(
1111
1122
  segment_paths = [destination.with_name(f"{destination.stem}-segment-{index + 1}.mp4") for index in range(2)]
1112
1123
  task_ids: list[str] = []
1113
1124
  for prompt, segment_path in zip(clean_prompts, segment_paths):
1114
- _, task_id = generate_ugc_segment(settings, card, image_url=image_url, prompt=prompt, output_path=segment_path, duration=8)
1115
- task_ids.append(task_id)
1125
+ generate_video_for_card(settings, card, prompt, image_url=image_url, duration=duration, aspect_ratio="9:16", output_path=segment_path)
1126
+ task_ids.append(segment_path.stem)
1116
1127
  return concatenate_video_files(segment_paths, destination), task_ids
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.80",
3
+ "version": "0.6.82",
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",