@danhachuel/thunderbolt 0.6.82 → 0.6.83

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.
@@ -457,104 +457,103 @@ def render_ugc_products(settings: dict[str, Any]) -> None:
457
457
 
458
458
  with media_tab:
459
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.",
460
+ influencers = [{"id": "", "name": "Sem personagem", "bio": ""}]
461
+ influencer_options = _influencer_options(influencers)
462
+ selected_influencer = st.selectbox(
463
+ "Personagem (opcional)",
464
+ influencer_options,
465
+ format_func=lambda value: _influencer_label(influencers, value),
466
+ key="ugc_products_character",
467
+ )
468
+ selected_character = next(item for item in influencers if str(item.get("id")) == selected_influencer)
469
+ try:
470
+ character_assets = repository.list_assets(selected_influencer)
471
+ except Exception:
472
+ character_assets = []
473
+ character_images = [item for item in character_assets if str(item.get("asset_type") or "") == "image"]
474
+ product_name = st.text_input("Nome do produto", key="ugc_products_name", placeholder="Ex.: Sérum facial hidratante")
475
+ 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…")
476
+ product_uploads = st.file_uploader(
477
+ "Imagem do produto (fotos/vídeos)",
478
+ type=["jpg", "jpeg", "png", "webp", "mp4", "mov", "webm"],
479
+ accept_multiple_files=True,
480
+ key="ugc_products_inputs",
481
+ help="Anexe uma ou mais referências do produto. Para geração, a primeira imagem será usada como referência visual principal.",
482
+ )
483
+ generation_mode = st.radio("Geração", ["Gerar Foto", "Gerar Video"], horizontal=True, key="ugc_products_generation_mode")
484
+ 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")
485
+ image_cards, image_options = _provider_options(settings, "image")
486
+ video_cards = _workflow_provider_cards(settings, provider=None)
487
+ video_options = [str(card.get("id") or "") for card in video_cards]
488
+ 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 ""
489
+ 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 ""
490
+ 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.")
491
+ generate_script = st.button("Gerar roteiro com IA", use_container_width=True, key="ugc_products_generate_script")
492
+ generate = st.button("Gerar Foto" if generation_mode == "Gerar Foto" else "Gerar Video", type="primary", use_container_width=True, key="ugc_products_generate")
493
+ if generate_script:
494
+ context = "\n".join(
495
+ item for item in [
496
+ f"Produto: {product_name.strip()}" if product_name.strip() else "",
497
+ f"Informação/link: {product_info.strip()}" if product_info.strip() else "",
498
+ f"Personagem: {selected_character.get('name') or selected_influencer}",
499
+ f"Biografia do personagem: {selected_character.get('bio') or ''}",
500
+ ] if item
483
501
  )
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.")
502
+ prompts = generate_ugc_segment_prompts(settings, context or "Criar uma demonstração UGC natural do produto.")
503
+ st.session_state["ugc_products_script"] = "\n---\n".join(prompts)
504
+ st.rerun()
505
+ if generate:
506
+ if not product_name.strip():
507
+ st.error("Informe o nome do produto.")
508
+ elif not product_info.strip() and not product_uploads:
509
+ st.error("Informe os dados/link do produto ou anexe pelo menos uma mídia.")
510
+ elif not script.strip():
511
+ st.error("Escreva o roteiro ou use o botão Gerar roteiro com IA.")
512
+ elif generation_mode == "Gerar Foto" and not image_provider_id:
513
+ st.error("Active um provider de imagem antes de gerar a foto.")
514
+ elif generation_mode == "Gerar Video" and not video_provider_id:
515
+ st.error("Active um provider de vídeo antes de gerar o vídeo.")
516
+ else:
517
+ image_upload = next((item for item in product_uploads or [] if Path(str(getattr(item, "name", ""))).suffix.lower() in {".jpg", ".jpeg", ".png", ".webp"}), None)
518
+ if image_upload is None:
519
+ st.error("Anexe pelo menos uma imagem do produto para usar como referência visual.")
517
520
  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.")
521
+ try:
522
+ product_path = _store_uploaded_file(image_upload, "ugc-products-inputs")
523
+ character_path = _local_asset_path(character_images[0]) if character_images else None
524
+ prompts = [part.strip() for part in script.split("\n---\n") if part.strip()]
525
+ if len(prompts) != 2:
526
+ prompts = generate_ugc_segment_prompts(settings, script)
527
+ 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)
528
+ character_name = str(selected_character.get("name") or selected_influencer)
529
+ 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]
530
+ owner_id = selected_influencer or _workflow_owner(repository)
531
+ record = repository.create_content({
532
+ "influencer_id": owner_id,
533
+ "content_type": "image" if generation_mode == "Gerar Foto" else "video",
534
+ "prompt": script,
535
+ "caption": f"{product_name.strip()} + {character_name}",
536
+ "provider": card.get("provider") if card else "",
537
+ "model": str(card.get("model") or "") if card else "",
538
+ "platform": "TikTok, Instagram",
539
+ "state": "running",
540
+ "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},
541
+ })
542
+ with st.spinner("A gerar a mídia UGC em formato vertical 9:16…"):
543
+ if generation_mode == "Gerar Foto":
544
+ image_card = {**card, "aspect_ratio": "9:16"}
545
+ output = generate_image_for_card(settings, image_card, enriched_prompts[0], topic=product_name.strip(), reference_image=product_path)
546
+ task_ids = []
547
+ else:
548
+ 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]
549
+ task_ids = []
550
+ repository.update_content(record["id"], {"state": "completed", "artifact_path": str(output), "provider_request_id": ",".join(task_ids)})
551
+ st.success("Mídia UGC criada e guardada em Midias Prontas.")
552
+ except Exception as exc:
553
+ repository.update_content(record["id"], {"state": "failed", "error": str(exc)[:1000]})
554
+ st.error(f"Não foi possível gerar a mídia UGC: {exc}")
555
+ if not image_cards and not video_cards:
556
+ st.warning("Configure pelo menos um provider de imagem ou vídeo em Configuração API > API Keys > Imagem e Video IA.")
558
557
 
559
558
  with ready_tab:
560
559
  st.subheader("Midias Prontas")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.82",
3
+ "version": "0.6.83",
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",