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