@danhachuel/thunderbolt 0.3.21 → 0.3.23
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 +246 -75
- package/hermes_ui/drafts.py +33 -0
- package/hermes_ui/languages.py +76 -10
- package/hermes_ui/storage.py +1 -0
- package/hermes_ui/thumbnails.py +127 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -22,6 +22,7 @@ except (OSError, json.JSONDecodeError):
|
|
|
22
22
|
APP_VERSION = ""
|
|
23
23
|
|
|
24
24
|
from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, pipeline_summary, set_channel_defaults, transition_task, update_channel, update_channel_video
|
|
25
|
+
from hermes_ui.drafts import save_draft
|
|
25
26
|
from hermes_ui.automation_worker import load_worker_status
|
|
26
27
|
from hermes_ui.storage import BLUEPRINTS, DEFAULT_LLM_PROVIDER, MEDIA_DOWNLOADS, STORAGE, TIKTOK_PROMPT_MASTERS, ensure_storage, get_display_name, list_blueprint_files, list_prompt_master_files, load_blueprint_file, load_prompt_master_file, now, read_json, set_display_name, write_json
|
|
27
28
|
from app.modules.niche_finder.apify import ApifyError, DEFAULT_ACTOR_ID, abort_actor_run, build_actor_input, get_dataset_items, normalize_video_items, start_actor_run, wait_for_actor_run
|
|
@@ -45,6 +46,7 @@ from hermes_ui.script_documents import list_script_documents, read_script_docume
|
|
|
45
46
|
from hermes_ui.script_generation import generate_script_document
|
|
46
47
|
from hermes_ui.voice_preview import DEFAULT_SAMPLE, load_preview_file, synthesize_preview
|
|
47
48
|
from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
|
|
49
|
+
from hermes_ui.thumbnails import list_thumbnail_tasks, regenerate_thumbnail
|
|
48
50
|
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_topic_for_channel, generate_video_keywords
|
|
49
51
|
from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
|
|
50
52
|
from integrations.tiktok_public import fetch_public_tiktok_profile, normalize_tiktok_reference
|
|
@@ -520,17 +522,19 @@ def generate_video_content_for_ui(
|
|
|
520
522
|
subject: str,
|
|
521
523
|
language: str,
|
|
522
524
|
generation_settings: dict[str, Any] | None = None,
|
|
525
|
+
blueprint: dict[str, Any] | None = None,
|
|
523
526
|
) -> dict[str, Any]:
|
|
524
527
|
"""Generate the subject, script and English keywords in one MoneyPrinter-style action."""
|
|
525
528
|
subject = str(subject or "").strip()
|
|
526
529
|
topic_result: dict[str, Any] | None = None
|
|
527
530
|
if not subject:
|
|
528
|
-
|
|
531
|
+
selected_blueprint = blueprint if isinstance(blueprint, dict) else blueprint_for_channel(channel)
|
|
532
|
+
topic_result = generate_topic_for_channel(settings, channel, selected_blueprint)
|
|
529
533
|
subject = str(topic_result.get("topic") or "").strip()
|
|
530
534
|
if not subject:
|
|
531
535
|
raise CreativeGenerationError("A IA não devolveu um Video Subject válido.")
|
|
532
536
|
|
|
533
|
-
blueprint = blueprint_for_channel(channel)
|
|
537
|
+
blueprint = blueprint if isinstance(blueprint, dict) else blueprint_for_channel(channel)
|
|
534
538
|
script_result = generate_script_document(
|
|
535
539
|
settings,
|
|
536
540
|
document_type="Roteiro de vídeo",
|
|
@@ -561,7 +565,7 @@ def generate_video_content_for_ui(
|
|
|
561
565
|
}
|
|
562
566
|
|
|
563
567
|
|
|
564
|
-
def _generate_video_content_callback(prefix: str, channel: dict, fallback_language: str) -> None:
|
|
568
|
+
def _generate_video_content_callback(prefix: str, channel: dict, fallback_language: str, blueprint: dict[str, Any] | None = None) -> None:
|
|
565
569
|
"""Streamlit callback for the single button below Subject, Script and Keywords."""
|
|
566
570
|
settings = read_json("settings.json", {})
|
|
567
571
|
generation_settings = {
|
|
@@ -577,17 +581,30 @@ def _generate_video_content_callback(prefix: str, channel: dict, fallback_langua
|
|
|
577
581
|
generation_settings["video_subject"],
|
|
578
582
|
generation_settings["script_language"],
|
|
579
583
|
generation_settings=generation_settings,
|
|
584
|
+
blueprint=blueprint,
|
|
580
585
|
)
|
|
581
586
|
st.session_state[f"{prefix}_video_subject"] = result["topic"]
|
|
582
587
|
st.session_state[f"{prefix}_video_script"] = result["script"]
|
|
583
588
|
st.session_state[f"{prefix}_video_keywords"] = ", ".join(result["keywords"])
|
|
584
|
-
|
|
585
|
-
|
|
589
|
+
if prefix == "pipeline_scripts":
|
|
590
|
+
script_result = result.get("script_result") or {}
|
|
591
|
+
st.session_state["script_draft"] = {
|
|
592
|
+
"title": result["topic"],
|
|
593
|
+
"summary": str(script_result.get("summary") or ""),
|
|
594
|
+
"content": result["script"],
|
|
595
|
+
"keywords": result["keywords"],
|
|
596
|
+
}
|
|
597
|
+
st.session_state["script_draft_title"] = result["topic"]
|
|
598
|
+
st.session_state["script_draft_summary"] = str(script_result.get("summary") or "")
|
|
599
|
+
st.session_state["script_draft_content"] = result["script"]
|
|
600
|
+
st.session_state["script_draft_keywords"] = ", ".join(result["keywords"])
|
|
601
|
+
st.session_state[f"{prefix}_topic"] = result["topic"]
|
|
602
|
+
st.session_state[f"{prefix}_topic_meta"] = result.get("topic_result") or {
|
|
586
603
|
"topic": result["topic"],
|
|
587
604
|
"topic_source": result.get("topic_source", "manual"),
|
|
588
605
|
}
|
|
589
606
|
# A subject change invalidates a previously generated title/thumbnail package.
|
|
590
|
-
st.session_state.pop("
|
|
607
|
+
st.session_state.pop(f"{prefix}_creative_payload", None)
|
|
591
608
|
st.session_state[f"{prefix}_generate_content_notice"] = "Tema, roteiro e palavras-chave gerados com IA."
|
|
592
609
|
st.session_state.pop(f"{prefix}_generate_content_error", None)
|
|
593
610
|
except CreativeGenerationError as exc:
|
|
@@ -595,8 +612,57 @@ def _generate_video_content_callback(prefix: str, channel: dict, fallback_langua
|
|
|
595
612
|
st.session_state.pop(f"{prefix}_generate_content_notice", None)
|
|
596
613
|
|
|
597
614
|
|
|
598
|
-
def
|
|
599
|
-
|
|
615
|
+
def _save_pipeline_draft_callback(
|
|
616
|
+
prefix: str,
|
|
617
|
+
draft_kind: str,
|
|
618
|
+
page_title: str,
|
|
619
|
+
*,
|
|
620
|
+
channel: dict[str, Any] | None = None,
|
|
621
|
+
blueprint: dict[str, Any] | None = None,
|
|
622
|
+
document_type: str = "video_script",
|
|
623
|
+
title: str = "",
|
|
624
|
+
brief: str = "",
|
|
625
|
+
) -> None:
|
|
626
|
+
"""Persist the current editable pipeline fields as a local draft."""
|
|
627
|
+
channel = channel or {}
|
|
628
|
+
blueprint = blueprint or {}
|
|
629
|
+
is_script_draft = prefix == "pipeline_scripts"
|
|
630
|
+
subject = str(st.session_state.get(f"{prefix}_video_subject") or "").strip()
|
|
631
|
+
script = str(st.session_state.get(f"{prefix}_video_script") or "").strip()
|
|
632
|
+
keywords = str(st.session_state.get(f"{prefix}_video_keywords") or (st.session_state.get("script_draft_keywords") if is_script_draft else "") or "").strip()
|
|
633
|
+
draft_title = str((st.session_state.get("script_draft_title") if is_script_draft else "") or title or subject or page_title).strip()
|
|
634
|
+
draft_brief = str((st.session_state.get("script_brief") if is_script_draft else "") or brief or subject).strip()
|
|
635
|
+
draft_content = str((st.session_state.get("script_draft_content") if is_script_draft else "") or script).strip()
|
|
636
|
+
if not any((draft_title, draft_brief, draft_content, keywords)):
|
|
637
|
+
st.session_state[f"{prefix}_save_draft_error"] = "Preencha pelo menos um campo antes de guardar o rascunho."
|
|
638
|
+
st.session_state.pop(f"{prefix}_save_draft_notice", None)
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
record = save_draft(
|
|
642
|
+
{
|
|
643
|
+
"draft_kind": draft_kind,
|
|
644
|
+
"page": page_title,
|
|
645
|
+
"title": draft_title,
|
|
646
|
+
"brief": draft_brief,
|
|
647
|
+
"content": draft_content,
|
|
648
|
+
"video_subject": subject,
|
|
649
|
+
"video_script": script,
|
|
650
|
+
"video_keywords": keywords,
|
|
651
|
+
"summary": str((st.session_state.get("script_draft_summary") if is_script_draft else "") or "").strip(),
|
|
652
|
+
"document_type": document_type,
|
|
653
|
+
"language": str(st.session_state.get(f"{prefix}_script_language") or "").strip(),
|
|
654
|
+
"channel_id": str(channel.get("id") or ""),
|
|
655
|
+
"channel_name": str(channel.get("name") or "Documento independente"),
|
|
656
|
+
"blueprint_id": str(blueprint.get("id") or ""),
|
|
657
|
+
"blueprint_name": str(blueprint.get("name") or "SEM BLUEPRINT CONFIGURADO"),
|
|
658
|
+
}
|
|
659
|
+
)
|
|
660
|
+
st.session_state[f"{prefix}_save_draft_notice"] = f"Rascunho guardado localmente: {record['id']}."
|
|
661
|
+
st.session_state.pop(f"{prefix}_save_draft_error", None)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _video_topic_source(subject: str, prefix: str = "new_video") -> str:
|
|
665
|
+
meta = st.session_state.get(f"{prefix}_topic_meta") or {}
|
|
600
666
|
generated_topic = str(meta.get("topic") or "").strip() if isinstance(meta, dict) else ""
|
|
601
667
|
return "llm" if generated_topic and generated_topic == str(subject or "").strip() and meta.get("topic_source") == "llm" else "manual"
|
|
602
668
|
|
|
@@ -645,6 +711,7 @@ def render_video_generation_settings(
|
|
|
645
711
|
current_language: str = "",
|
|
646
712
|
channel: dict[str, Any] | None = None,
|
|
647
713
|
generate_content_callback: Any | None = None,
|
|
714
|
+
save_draft_callback: Any | None = None,
|
|
648
715
|
) -> dict[str, Any]:
|
|
649
716
|
"""Render the shared MoneyPrinter-style settings and return a serializable payload."""
|
|
650
717
|
settings: dict[str, Any] = {}
|
|
@@ -701,6 +768,20 @@ def render_video_generation_settings(
|
|
|
701
768
|
if st.session_state.get(f"{prefix}_generate_content_error"):
|
|
702
769
|
st.error(st.session_state[f"{prefix}_generate_content_error"])
|
|
703
770
|
|
|
771
|
+
if save_draft_callback is not None:
|
|
772
|
+
st.button(
|
|
773
|
+
"Salvar rascunho",
|
|
774
|
+
key=f"{prefix}_save_draft",
|
|
775
|
+
use_container_width=True,
|
|
776
|
+
type="secondary",
|
|
777
|
+
icon=":material/save:",
|
|
778
|
+
on_click=save_draft_callback,
|
|
779
|
+
)
|
|
780
|
+
if st.session_state.get(f"{prefix}_save_draft_notice"):
|
|
781
|
+
st.success(st.session_state[f"{prefix}_save_draft_notice"])
|
|
782
|
+
if st.session_state.get(f"{prefix}_save_draft_error"):
|
|
783
|
+
st.error(st.session_state[f"{prefix}_save_draft_error"])
|
|
784
|
+
|
|
704
785
|
st.markdown("### Video Settings")
|
|
705
786
|
video_cols = st.columns(2)
|
|
706
787
|
with video_cols[0]:
|
|
@@ -1667,7 +1748,7 @@ def render_channels():
|
|
|
1667
1748
|
render_channel_videos(channel)
|
|
1668
1749
|
|
|
1669
1750
|
|
|
1670
|
-
def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
1751
|
+
def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "new_video"):
|
|
1671
1752
|
st.title(page_title)
|
|
1672
1753
|
create_tab = render_localized_tabs(["Criar vídeo"])[0]
|
|
1673
1754
|
with create_tab:
|
|
@@ -1680,7 +1761,7 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1680
1761
|
"Modo de criação",
|
|
1681
1762
|
["Canal específico", "Lote no mesmo canal", "Lote geral"],
|
|
1682
1763
|
horizontal=True,
|
|
1683
|
-
key="
|
|
1764
|
+
key=f"{prefix}_mode",
|
|
1684
1765
|
)
|
|
1685
1766
|
mode = {"Canal específico": "single", "Lote no mesmo canal": "same_channel", "Lote geral": "general"}[mode_label]
|
|
1686
1767
|
selected_one: dict[str, Any] | None = None
|
|
@@ -1701,11 +1782,11 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1701
1782
|
st.caption(f"**{channel.get('name', 'Canal')}** · {status} · Blueprint: **{summary['name']}** · Voz: {summary['voice'] or 'Sem voz padrão'}")
|
|
1702
1783
|
general_context = st.text_area(
|
|
1703
1784
|
"Contexto opcional para todos os canais",
|
|
1704
|
-
value=st.session_state.get("
|
|
1705
|
-
key="
|
|
1785
|
+
value=st.session_state.get(f"{prefix}_general_context", ""),
|
|
1786
|
+
key=f"{prefix}_general_context",
|
|
1706
1787
|
placeholder="Opcional: campanha, época, evento ou restrição editorial comum. O tema final será individual por canal.",
|
|
1707
1788
|
)
|
|
1708
|
-
if st.button("Gerar tópicos individuais para todos os canais", key="
|
|
1789
|
+
if st.button("Gerar tópicos individuais para todos os canais", key=f"{prefix}_generate_general_topics", use_container_width=True):
|
|
1709
1790
|
settings = read_json("settings.json", {})
|
|
1710
1791
|
generated_topics: dict[str, dict[str, Any]] = {}
|
|
1711
1792
|
errors: list[str] = []
|
|
@@ -1719,9 +1800,9 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1719
1800
|
for error in errors:
|
|
1720
1801
|
st.error(error)
|
|
1721
1802
|
else:
|
|
1722
|
-
st.session_state["
|
|
1803
|
+
st.session_state[f"{prefix}_general_topics"] = generated_topics
|
|
1723
1804
|
st.success(f"Foram gerados {len(generated_topics)} briefings independentes.")
|
|
1724
|
-
general_topics = st.session_state.get("
|
|
1805
|
+
general_topics = st.session_state.get(f"{prefix}_general_topics", {})
|
|
1725
1806
|
if general_topics:
|
|
1726
1807
|
st.subheader("Briefings por canal")
|
|
1727
1808
|
for channel in all_channels:
|
|
@@ -1729,35 +1810,56 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1729
1810
|
if result:
|
|
1730
1811
|
st.write(f"**{channel.get('name', 'Canal')}**")
|
|
1731
1812
|
st.caption(f"{result.get('niche', '')} · {result.get('angle', '')}")
|
|
1732
|
-
st.text_area("Briefing gerado", value=result.get("topic", ""), key=f"
|
|
1813
|
+
st.text_area("Briefing gerado", value=result.get("topic", ""), key=f"{prefix}_general_topic_{channel['id']}", height=80)
|
|
1733
1814
|
generation_settings = render_video_generation_settings(
|
|
1734
|
-
|
|
1815
|
+
prefix,
|
|
1735
1816
|
current_language=str(st.session_state.get("video_language") or ""),
|
|
1817
|
+
save_draft_callback=lambda: _save_pipeline_draft_callback(
|
|
1818
|
+
prefix,
|
|
1819
|
+
"music" if page_title == "Criação de Músicas" else "video",
|
|
1820
|
+
page_title,
|
|
1821
|
+
channel=selected_one,
|
|
1822
|
+
blueprint=blueprint_for_channel(selected_one or {}),
|
|
1823
|
+
),
|
|
1736
1824
|
)
|
|
1737
1825
|
else:
|
|
1738
1826
|
if not active_channels:
|
|
1739
1827
|
st.warning("Não existem canais activos disponíveis para os modos de canal específico.")
|
|
1740
1828
|
selected = []
|
|
1741
1829
|
else:
|
|
1742
|
-
selected_one = st.selectbox("Canal", active_channels, format_func=lambda c: c["name"], key="
|
|
1830
|
+
selected_one = st.selectbox("Canal", active_channels, format_func=lambda c: c["name"], key=f"{prefix}_channel")
|
|
1743
1831
|
selected = [selected_one["id"]]
|
|
1744
1832
|
# Intentionally sits between Canal and the generation settings, as requested.
|
|
1745
1833
|
render_channel_blueprint_panel(selected_one)
|
|
1746
1834
|
generation_settings = render_video_generation_settings(
|
|
1747
|
-
|
|
1835
|
+
prefix,
|
|
1748
1836
|
current_language=str(st.session_state.get("video_language") or ""),
|
|
1749
1837
|
channel=selected_one,
|
|
1750
1838
|
generate_content_callback=lambda: _generate_video_content_callback(
|
|
1751
|
-
|
|
1839
|
+
prefix,
|
|
1752
1840
|
selected_one,
|
|
1753
1841
|
str(st.session_state.get("video_language") or "pt"),
|
|
1754
1842
|
),
|
|
1843
|
+
save_draft_callback=lambda: _save_pipeline_draft_callback(
|
|
1844
|
+
prefix,
|
|
1845
|
+
"music" if page_title == "Criação de Músicas" else "video",
|
|
1846
|
+
page_title,
|
|
1847
|
+
channel=selected_one,
|
|
1848
|
+
blueprint=blueprint_for_channel(selected_one or {}),
|
|
1849
|
+
),
|
|
1755
1850
|
)
|
|
1756
1851
|
|
|
1757
1852
|
if not generation_settings:
|
|
1758
1853
|
generation_settings = render_video_generation_settings(
|
|
1759
|
-
|
|
1854
|
+
prefix,
|
|
1760
1855
|
current_language=str(st.session_state.get("video_language") or ""),
|
|
1856
|
+
save_draft_callback=lambda: _save_pipeline_draft_callback(
|
|
1857
|
+
prefix,
|
|
1858
|
+
"music" if page_title == "Criação de Músicas" else "video",
|
|
1859
|
+
page_title,
|
|
1860
|
+
channel=selected_one,
|
|
1861
|
+
blueprint=blueprint_for_channel(selected_one or {}),
|
|
1862
|
+
),
|
|
1761
1863
|
)
|
|
1762
1864
|
wide_style_label = generation_settings["video_source"]
|
|
1763
1865
|
style_ia = generation_settings.get("style_ia", "")
|
|
@@ -1765,47 +1867,47 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1765
1867
|
music_source = ""
|
|
1766
1868
|
if wide_style_label == "Apenas Música":
|
|
1767
1869
|
st.caption("Apenas Música não gera Pexels/Pixabay nem fundo IA; o áudio musical será usado como elemento principal.")
|
|
1768
|
-
music_source = st.radio("Fonte da música", ["Ficheiro existente", "Carregar ficheiro", "Criar via Suno API"], horizontal=True, key="
|
|
1870
|
+
music_source = st.radio("Fonte da música", ["Ficheiro existente", "Carregar ficheiro", "Criar via Suno API"], horizontal=True, key=f"{prefix}_music_source")
|
|
1769
1871
|
if music_source == "Ficheiro existente":
|
|
1770
1872
|
local_music = list_music_files()
|
|
1771
1873
|
if local_music:
|
|
1772
|
-
selected_music = st.selectbox("Música local", local_music, format_func=lambda item: item.name, key="
|
|
1874
|
+
selected_music = st.selectbox("Música local", local_music, format_func=lambda item: item.name, key=f"{prefix}_music_existing")
|
|
1773
1875
|
music_path = str(selected_music)
|
|
1774
1876
|
else:
|
|
1775
1877
|
st.warning("Ainda não existem músicas em storage/music. Escolha Carregar ficheiro ou Criar via Suno API.")
|
|
1776
1878
|
elif music_source == "Carregar ficheiro":
|
|
1777
|
-
uploaded_music = st.file_uploader("Carregar música", type=["mp3", "wav", "m4a", "aac", "flac", "ogg"], key="
|
|
1778
|
-
if uploaded_music and st.button("Guardar música local", key="
|
|
1879
|
+
uploaded_music = st.file_uploader("Carregar música", type=["mp3", "wav", "m4a", "aac", "flac", "ogg"], key=f"{prefix}_music_upload")
|
|
1880
|
+
if uploaded_music and st.button("Guardar música local", key=f"{prefix}_music_store", use_container_width=True):
|
|
1779
1881
|
try:
|
|
1780
1882
|
stored_music = store_music_file(uploaded_music.name, uploaded_music.getvalue())
|
|
1781
|
-
st.session_state["
|
|
1883
|
+
st.session_state[f"{prefix}_music_path"] = str(stored_music)
|
|
1782
1884
|
st.success(f"Música guardada em `{stored_music}`")
|
|
1783
1885
|
except (OSError, ValueError) as exc:
|
|
1784
1886
|
st.error(str(exc))
|
|
1785
|
-
music_path = st.session_state.get("
|
|
1887
|
+
music_path = st.session_state.get(f"{prefix}_music_path", "")
|
|
1786
1888
|
else:
|
|
1787
|
-
suno_prompt = st.text_area("Prompt musical Suno", placeholder="Instrumental cinematográfico, calmo, sem voz...", key="
|
|
1788
|
-
suno_title = st.text_input("Título da música", value=st.session_state.get("
|
|
1789
|
-
if st.button("Solicitar música no Suno", key="
|
|
1889
|
+
suno_prompt = st.text_area("Prompt musical Suno", placeholder="Instrumental cinematográfico, calmo, sem voz...", key=f"{prefix}_suno_prompt")
|
|
1890
|
+
suno_title = st.text_input("Título da música", value=st.session_state.get(f"{prefix}_topic") or "Thunderbolt music", key=f"{prefix}_suno_title")
|
|
1891
|
+
if st.button("Solicitar música no Suno", key=f"{prefix}_suno_request", use_container_width=True):
|
|
1790
1892
|
suno_result = request_suno_generation(read_json("settings.json", {}), suno_prompt, suno_title)
|
|
1791
1893
|
(st.success if suno_result["ok"] else st.error)(suno_result["message"])
|
|
1792
1894
|
if suno_result["ok"]:
|
|
1793
1895
|
try:
|
|
1794
1896
|
generated = materialize_suno_audio(suno_result.get("data", {}), suno_title or "suno-generated.mp3")
|
|
1795
1897
|
if generated:
|
|
1796
|
-
st.session_state["
|
|
1898
|
+
st.session_state[f"{prefix}_music_path"] = str(generated)
|
|
1797
1899
|
st.success(f"Música descarregada para `{generated}`")
|
|
1798
1900
|
else:
|
|
1799
1901
|
st.info("O pedido foi aceite, mas o endpoint ainda não devolveu uma URL de áudio. Consulte o estado no serviço Suno e adicione o ficheiro quando estiver pronto.")
|
|
1800
1902
|
except (OSError, requests.RequestException, ValueError) as exc:
|
|
1801
1903
|
st.warning(f"Pedido criado, mas não foi possível descarregar o áudio: {exc}")
|
|
1802
|
-
music_path = st.session_state.get("
|
|
1904
|
+
music_path = st.session_state.get(f"{prefix}_music_path", "")
|
|
1803
1905
|
|
|
1804
1906
|
payloads: dict[str, dict[str, Any]] = {}
|
|
1805
1907
|
if mode == "general":
|
|
1806
|
-
existing_topics = st.session_state.get("
|
|
1807
|
-
payloads = dict(st.session_state.get("
|
|
1808
|
-
if st.button("Gerar títulos e thumbnails para todos os canais", key="
|
|
1908
|
+
existing_topics = st.session_state.get(f"{prefix}_general_topics", {})
|
|
1909
|
+
payloads = dict(st.session_state.get(f"{prefix}_general_payloads", {}))
|
|
1910
|
+
if st.button("Gerar títulos e thumbnails para todos os canais", key=f"{prefix}_generate_general_creative", use_container_width=True):
|
|
1809
1911
|
settings = read_json("settings.json", {})
|
|
1810
1912
|
new_payloads: dict[str, dict[str, Any]] = {}
|
|
1811
1913
|
errors: list[str] = []
|
|
@@ -1824,11 +1926,11 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1824
1926
|
for error in errors:
|
|
1825
1927
|
st.error(error)
|
|
1826
1928
|
else:
|
|
1827
|
-
st.session_state["
|
|
1828
|
-
st.session_state["
|
|
1929
|
+
st.session_state[f"{prefix}_general_topics"] = {cid: {"topic": item["topic"], "topic_source": "llm"} for cid, item in new_payloads.items()}
|
|
1930
|
+
st.session_state[f"{prefix}_general_payloads"] = new_payloads
|
|
1829
1931
|
payloads = new_payloads
|
|
1830
1932
|
st.success(f"Pacote criativo pronto para {len(new_payloads)} canais.")
|
|
1831
|
-
payloads = st.session_state.get("
|
|
1933
|
+
payloads = st.session_state.get(f"{prefix}_general_payloads", payloads)
|
|
1832
1934
|
for channel in all_channels:
|
|
1833
1935
|
payload = payloads.get(channel["id"])
|
|
1834
1936
|
if not payload:
|
|
@@ -1836,12 +1938,12 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1836
1938
|
with st.expander(f"{channel.get('name', 'Canal')} — título e thumbnail", expanded=False):
|
|
1837
1939
|
title_options = [item.get("title", "") for item in payload.get("title_candidates", []) if item.get("title")]
|
|
1838
1940
|
if title_options:
|
|
1839
|
-
selected_title = st.selectbox("Título escolhido", title_options, index=max(0, title_options.index(payload.get("title")) if payload.get("title") in title_options else 0), key=f"
|
|
1941
|
+
selected_title = st.selectbox("Título escolhido", title_options, index=max(0, title_options.index(payload.get("title")) if payload.get("title") in title_options else 0), key=f"{prefix}_general_title_{channel['id']}")
|
|
1840
1942
|
payload["title"] = selected_title
|
|
1841
1943
|
variants = payload.get("thumbnail_variants", [])
|
|
1842
1944
|
if variants:
|
|
1843
1945
|
labels = [f"{idx + 1}. {item.get('concept', 'Variante')}" for idx, item in enumerate(variants)]
|
|
1844
|
-
selected_variant_label = st.selectbox("Thumbnail escolhida", labels, key=f"
|
|
1946
|
+
selected_variant_label = st.selectbox("Thumbnail escolhida", labels, key=f"{prefix}_general_thumbnail_{channel['id']}")
|
|
1845
1947
|
variant_index = labels.index(selected_variant_label)
|
|
1846
1948
|
variant = variants[variant_index]
|
|
1847
1949
|
payload["thumbnail_variant"] = variant
|
|
@@ -1849,13 +1951,13 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1849
1951
|
payload["thumbnail_text"] = variant.get("overlay_text", "")
|
|
1850
1952
|
st.caption(f"{variant.get('composition', '')} · {variant.get('color_palette', '')}")
|
|
1851
1953
|
thumbnail_path = str(variant.get("image_path") or payload.get("thumbnail_path") or "").strip()
|
|
1852
|
-
if st.button("Gerar imagem com Nano Banana", key=f"
|
|
1954
|
+
if st.button("Gerar imagem com Nano Banana", key=f"{prefix}_general_generate_thumbnail_{channel['id']}", use_container_width=True):
|
|
1853
1955
|
try:
|
|
1854
1956
|
thumbnail_path = str(generate_thumbnail_image(read_json("settings.json", {}), variant.get("image_prompt", ""), topic=str(payload.get("topic") or ""), variant_index=variant_index))
|
|
1855
1957
|
variant["image_path"] = thumbnail_path
|
|
1856
1958
|
payload["thumbnail_path"] = thumbnail_path
|
|
1857
1959
|
payload["thumbnail_status"] = "generated"
|
|
1858
|
-
st.session_state["
|
|
1960
|
+
st.session_state[f"{prefix}_general_payloads"] = payloads
|
|
1859
1961
|
record_notification("thumbnail_generation_completed", f"Thumbnail gerada: {payload.get('title') or payload.get('topic') or 'Vídeo'}", "A thumbnail foi gerada com sucesso pelo Nano Banana.", metadata={"channel_name": channel.get("name") or "", "image_path": Path(thumbnail_path).name}, dedupe_key=f"thumbnail:{thumbnail_path}")
|
|
1860
1962
|
st.success("Thumbnail gerada com Nano Banana.")
|
|
1861
1963
|
st.rerun()
|
|
@@ -1870,7 +1972,7 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1870
1972
|
st.caption(f"Estado da thumbnail: {payload.get('thumbnail_status', 'prompt_ready')} · texto: {payload.get('thumbnail_text') or 'sem texto'}")
|
|
1871
1973
|
else:
|
|
1872
1974
|
topic_for_creative = str(generation_settings.get("video_subject") or "").strip()
|
|
1873
|
-
if st.button("Gerar títulos e thumbnails com IA", key="
|
|
1975
|
+
if st.button("Gerar títulos e thumbnails com IA", key=f"{prefix}_generate_creative", use_container_width=True):
|
|
1874
1976
|
if selected_one is None:
|
|
1875
1977
|
st.error("Seleccione primeiro um canal.")
|
|
1876
1978
|
else:
|
|
@@ -1878,33 +1980,33 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1878
1980
|
if not topic_for_creative:
|
|
1879
1981
|
topic_result = generate_topic_for_ui(read_json("settings.json", {}), selected_one)
|
|
1880
1982
|
topic_for_creative = str(topic_result["topic"]).strip()
|
|
1881
|
-
st.session_state["
|
|
1882
|
-
st.session_state["
|
|
1983
|
+
st.session_state[f"{prefix}_topic"] = topic_for_creative
|
|
1984
|
+
st.session_state[f"{prefix}_topic_meta"] = topic_result
|
|
1883
1985
|
generated = generate_creative_for_ui(
|
|
1884
1986
|
read_json("settings.json", {}),
|
|
1885
1987
|
selected_one,
|
|
1886
1988
|
topic_for_creative,
|
|
1887
|
-
topic_source=_video_topic_source(topic_for_creative),
|
|
1989
|
+
topic_source=_video_topic_source(topic_for_creative, prefix),
|
|
1888
1990
|
)
|
|
1889
1991
|
|
|
1890
|
-
st.session_state["
|
|
1992
|
+
st.session_state[f"{prefix}_creative_payload"] = generated
|
|
1891
1993
|
st.success("Tema, título e thumbnails gerados; escolha a variante antes de criar as tarefas.")
|
|
1892
1994
|
st.rerun()
|
|
1893
1995
|
except CreativeGenerationError as exc:
|
|
1894
1996
|
st.error(str(exc))
|
|
1895
|
-
payload = st.session_state.get("
|
|
1997
|
+
payload = st.session_state.get(f"{prefix}_creative_payload")
|
|
1896
1998
|
if payload:
|
|
1897
1999
|
st.subheader("Título e Thumbnail automáticos")
|
|
1898
2000
|
title_options = [item.get("title", "") for item in payload.get("title_candidates", []) if item.get("title")]
|
|
1899
2001
|
if title_options:
|
|
1900
|
-
selected_title = st.selectbox("Título escolhido", title_options, index=max(0, title_options.index(payload.get("title")) if payload.get("title") in title_options else 0), key="
|
|
2002
|
+
selected_title = st.selectbox("Título escolhido", title_options, index=max(0, title_options.index(payload.get("title")) if payload.get("title") in title_options else 0), key=f"{prefix}_title_choice")
|
|
1901
2003
|
payload["title"] = selected_title
|
|
1902
2004
|
with st.expander(f"Ver {len(title_options)} candidatos de título"):
|
|
1903
2005
|
st.dataframe(payload.get("title_candidates", []), use_container_width=True, hide_index=True)
|
|
1904
2006
|
variants = payload.get("thumbnail_variants", [])
|
|
1905
2007
|
if variants:
|
|
1906
2008
|
labels = [f"{idx + 1}. {item.get('concept', 'Variante')}" for idx, item in enumerate(variants)]
|
|
1907
|
-
selected_variant_label = st.selectbox("Thumbnail escolhida", labels, key="
|
|
2009
|
+
selected_variant_label = st.selectbox("Thumbnail escolhida", labels, key=f"{prefix}_thumbnail_choice")
|
|
1908
2010
|
variant_index = labels.index(selected_variant_label)
|
|
1909
2011
|
variant = variants[variant_index]
|
|
1910
2012
|
payload["thumbnail_variant"] = variant
|
|
@@ -1913,13 +2015,13 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1913
2015
|
st.caption(f"Composição: {variant.get('composition', '')} · Cores: {variant.get('color_palette', '')}")
|
|
1914
2016
|
st.code(variant.get("image_prompt", ""), language="text")
|
|
1915
2017
|
thumbnail_path = str(variant.get("image_path") or payload.get("thumbnail_path") or "").strip()
|
|
1916
|
-
if st.button("Gerar imagem da thumbnail com Nano Banana", key="
|
|
2018
|
+
if st.button("Gerar imagem da thumbnail com Nano Banana", key=f"{prefix}_generate_thumbnail_image", use_container_width=True):
|
|
1917
2019
|
try:
|
|
1918
2020
|
thumbnail_path = str(generate_thumbnail_image(read_json("settings.json", {}), variant.get("image_prompt", ""), topic=str(payload.get("topic") or ""), variant_index=variant_index))
|
|
1919
2021
|
variant["image_path"] = thumbnail_path
|
|
1920
2022
|
payload["thumbnail_path"] = thumbnail_path
|
|
1921
2023
|
payload["thumbnail_status"] = "generated"
|
|
1922
|
-
st.session_state["
|
|
2024
|
+
st.session_state[f"{prefix}_creative_payload"] = payload
|
|
1923
2025
|
record_notification("thumbnail_generation_completed", f"Thumbnail gerada: {payload.get('title') or payload.get('topic') or 'Vídeo'}", "A thumbnail foi gerada com sucesso pelo Nano Banana.", metadata={"channel_name": selected_one.get("name") if selected_one else "", "image_path": Path(thumbnail_path).name}, dedupe_key=f"thumbnail:{thumbnail_path}")
|
|
1924
2026
|
st.success("Thumbnail gerada com Nano Banana.")
|
|
1925
2027
|
st.rerun()
|
|
@@ -1931,9 +2033,9 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1931
2033
|
payload["thumbnail_status"] = "generated"
|
|
1932
2034
|
else:
|
|
1933
2035
|
st.info("Escolha a variante e clique em **Gerar imagem da thumbnail com Nano Banana**. A API key é configurada em Configuração API > API Keys.")
|
|
1934
|
-
st.session_state["
|
|
2036
|
+
st.session_state[f"{prefix}_creative_payload"] = payload
|
|
1935
2037
|
|
|
1936
|
-
with st.form("
|
|
2038
|
+
with st.form(f"{prefix}_form"):
|
|
1937
2039
|
quantity = st.number_input("Quantidade", min_value=1, max_value=100, value=1, disabled=mode != "same_channel")
|
|
1938
2040
|
language = generation_settings["script_language"]
|
|
1939
2041
|
fmt = generation_settings["video_format"]
|
|
@@ -1945,12 +2047,12 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1945
2047
|
st.error("Escolha, carregue ou gere uma música antes de criar o vídeo Apenas Música.")
|
|
1946
2048
|
st.stop()
|
|
1947
2049
|
if mode == "general":
|
|
1948
|
-
payloads = dict(st.session_state.get("
|
|
1949
|
-
topics = dict(st.session_state.get("
|
|
2050
|
+
payloads = dict(st.session_state.get(f"{prefix}_general_payloads", {}))
|
|
2051
|
+
topics = dict(st.session_state.get(f"{prefix}_general_topics", {}))
|
|
1950
2052
|
channels_by_id = {str(channel["id"]): channel for channel in all_channels}
|
|
1951
2053
|
payloads_need_refresh = len(payloads) != len(selected) or any(
|
|
1952
|
-
str(st.session_state.get(f"
|
|
1953
|
-
and str(st.session_state.get(f"
|
|
2054
|
+
str(st.session_state.get(f"{prefix}_general_topic_{channel_id}", "") or "").strip()
|
|
2055
|
+
and str(st.session_state.get(f"{prefix}_general_topic_{channel_id}", "") or "").strip() != str((payloads.get(channel_id) or {}).get("topic") or "").strip()
|
|
1954
2056
|
for channel_id in selected
|
|
1955
2057
|
)
|
|
1956
2058
|
if payloads_need_refresh:
|
|
@@ -1960,7 +2062,7 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1960
2062
|
with st.spinner("A gerar automaticamente um pacote criativo independente para cada canal…"):
|
|
1961
2063
|
for channel_id in selected:
|
|
1962
2064
|
channel = channels_by_id[channel_id]
|
|
1963
|
-
edited_topic = str(st.session_state.get(f"
|
|
2065
|
+
edited_topic = str(st.session_state.get(f"{prefix}_general_topic_{channel_id}", "") or "").strip()
|
|
1964
2066
|
topic_result = topics.get(channel_id) or {}
|
|
1965
2067
|
try:
|
|
1966
2068
|
if not edited_topic:
|
|
@@ -1979,8 +2081,8 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1979
2081
|
st.error("O Lote geral não foi criado porque faltou gerar o conteúdo específico de pelo menos um canal.")
|
|
1980
2082
|
else:
|
|
1981
2083
|
payloads = generated_payloads
|
|
1982
|
-
st.session_state["
|
|
1983
|
-
st.session_state["
|
|
2084
|
+
st.session_state[f"{prefix}_general_payloads"] = payloads
|
|
2085
|
+
st.session_state[f"{prefix}_general_topics"] = {cid: {"topic": payload["topic"], "topic_source": payload.get("topic_source", "llm")} for cid, payload in payloads.items()}
|
|
1984
2086
|
if len(payloads) == len(selected):
|
|
1985
2087
|
batch_topic = "Lote geral — um vídeo independente por canal"
|
|
1986
2088
|
channel_payloads = {cid: {**payload, "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings} for cid, payload in payloads.items()}
|
|
@@ -1996,14 +2098,14 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1996
2098
|
st.error("Preencha o campo Video Subject ou use o botão de geração automática abaixo das keywords.")
|
|
1997
2099
|
st.stop()
|
|
1998
2100
|
quantity_value = int(quantity if mode == "same_channel" else 1)
|
|
1999
|
-
payload = dict(st.session_state.get("
|
|
2101
|
+
payload = dict(st.session_state.get(f"{prefix}_creative_payload") or {})
|
|
2000
2102
|
if not payload.get("title") or not payload.get("thumbnail_variants"):
|
|
2001
2103
|
try:
|
|
2002
2104
|
payload = generate_creative_for_ui(
|
|
2003
2105
|
read_json("settings.json", {}),
|
|
2004
2106
|
selected_one or {},
|
|
2005
2107
|
topic_value,
|
|
2006
|
-
topic_source=_video_topic_source(topic_value),
|
|
2108
|
+
topic_source=_video_topic_source(topic_value, prefix),
|
|
2007
2109
|
)
|
|
2008
2110
|
except CreativeGenerationError as exc:
|
|
2009
2111
|
st.warning(f"Título/thumbnail automáticos pendentes: {exc} A tarefa será criada com o tópico como título e sem ficheiro de thumbnail.")
|
|
@@ -2016,7 +2118,7 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
2016
2118
|
|
|
2017
2119
|
def render_music_creation():
|
|
2018
2120
|
"""Expose the complete video-creation UI under the music-oriented navigation entry without changing the original page."""
|
|
2019
|
-
render_new_video(page_title="Criação de Músicas")
|
|
2121
|
+
render_new_video(page_title="Criação de Músicas", prefix="new_music")
|
|
2020
2122
|
|
|
2021
2123
|
|
|
2022
2124
|
def render_scripts():
|
|
@@ -2064,27 +2166,38 @@ def render_scripts():
|
|
|
2064
2166
|
st.warning("Sem Blueprint seleccionado: o documento pode ser criado, mas não terá contexto editorial de Blueprint.")
|
|
2065
2167
|
|
|
2066
2168
|
document_type = st.radio("Tipo de documento", ["Roteiro de vídeo", "Letra de música"], horizontal=True, key="script_document_type")
|
|
2067
|
-
title = st.text_input("Título", key="script_title", placeholder="Ex.: A verdade esquecida sobre…")
|
|
2068
|
-
brief = st.text_area(
|
|
2069
|
-
"Tema ou briefing",
|
|
2070
|
-
key="script_brief",
|
|
2071
|
-
height=120,
|
|
2072
|
-
placeholder="Descreva o tema, a mensagem, o conflito ou a ideia musical que o Blueprint deve orientar.",
|
|
2073
|
-
)
|
|
2074
|
-
legacy_script_language = str(st.session_state.get("script_language") or "")
|
|
2075
2169
|
script_settings = render_video_generation_settings(
|
|
2076
2170
|
"pipeline_scripts",
|
|
2077
|
-
current_language=
|
|
2171
|
+
current_language=str(st.session_state.get("script_language") or read_json("settings.json", {}).get("video_language") or "pt"),
|
|
2172
|
+
channel=selected_channel,
|
|
2173
|
+
generate_content_callback=lambda: _generate_video_content_callback(
|
|
2174
|
+
"pipeline_scripts",
|
|
2175
|
+
selected_channel or {},
|
|
2176
|
+
str(st.session_state.get("pipeline_scripts_script_language") or language_code(legacy_script_language or "pt")),
|
|
2177
|
+
selected_blueprint,
|
|
2178
|
+
),
|
|
2179
|
+
save_draft_callback=lambda: _save_pipeline_draft_callback(
|
|
2180
|
+
"pipeline_scripts",
|
|
2181
|
+
"script",
|
|
2182
|
+
"Roteiros",
|
|
2183
|
+
channel=selected_channel,
|
|
2184
|
+
blueprint=selected_blueprint,
|
|
2185
|
+
document_type="video_script" if document_type == "Roteiro de vídeo" else "music_lyrics",
|
|
2186
|
+
title=title,
|
|
2187
|
+
brief=brief,
|
|
2188
|
+
),
|
|
2078
2189
|
)
|
|
2079
2190
|
language = script_settings["script_language"]
|
|
2080
2191
|
structure_notes = script_settings["script_structure_notes"]
|
|
2192
|
+
title = str(script_settings.get("video_subject") or "").strip()
|
|
2193
|
+
brief = str(script_settings.get("video_script") or "").strip() or title
|
|
2081
2194
|
generate_col, clear_col = st.columns([1.4, 1])
|
|
2082
2195
|
with generate_col:
|
|
2083
2196
|
generate_clicked = st.button("Gerar com IA a partir do Blueprint", type="primary", use_container_width=True, key="generate_script_document")
|
|
2084
2197
|
with clear_col:
|
|
2085
2198
|
clear_clicked = st.button("Limpar rascunho", use_container_width=True, key="clear_script_document")
|
|
2086
2199
|
if clear_clicked:
|
|
2087
|
-
for key in ("script_draft", "script_draft_title", "script_draft_content", "script_draft_summary"):
|
|
2200
|
+
for key in ("script_draft", "script_draft_title", "script_draft_content", "script_draft_summary", "script_draft_keywords"):
|
|
2088
2201
|
st.session_state.pop(key, None)
|
|
2089
2202
|
st.rerun()
|
|
2090
2203
|
if generate_clicked:
|
|
@@ -2129,8 +2242,11 @@ def render_scripts():
|
|
|
2129
2242
|
st.session_state["script_draft_summary"] = str(draft.get("summary") or "")
|
|
2130
2243
|
if "script_draft_content" not in st.session_state:
|
|
2131
2244
|
st.session_state["script_draft_content"] = str(draft.get("content") or "")
|
|
2245
|
+
if "script_draft_keywords" not in st.session_state:
|
|
2246
|
+
st.session_state["script_draft_keywords"] = str(draft.get("keywords") or "")
|
|
2132
2247
|
draft_title = st.text_input("Título do rascunho", key="script_draft_title")
|
|
2133
2248
|
draft_summary = st.text_input("Resumo", key="script_draft_summary")
|
|
2249
|
+
draft_keywords = st.text_area("Palavras-chave", height=90, key="script_draft_keywords")
|
|
2134
2250
|
draft_content = st.text_area("Conteúdo guardado", height=460, key="script_draft_content")
|
|
2135
2251
|
if st.button("Guardar documento no storage", type="primary", use_container_width=True, key="save_script_document"):
|
|
2136
2252
|
try:
|
|
@@ -2139,6 +2255,7 @@ def render_scripts():
|
|
|
2139
2255
|
**draft,
|
|
2140
2256
|
"title": draft_title,
|
|
2141
2257
|
"summary": draft_summary,
|
|
2258
|
+
"keywords": draft_keywords,
|
|
2142
2259
|
"content": draft_content,
|
|
2143
2260
|
"document_type": "video_script" if document_type == "Roteiro de vídeo" else "music_lyrics",
|
|
2144
2261
|
"language": language,
|
|
@@ -2153,7 +2270,7 @@ def render_scripts():
|
|
|
2153
2270
|
except (OSError, ValueError) as exc:
|
|
2154
2271
|
st.error(f"Não foi possível guardar o documento: {exc}")
|
|
2155
2272
|
else:
|
|
2156
|
-
st.caption("Gere um rascunho com IA para o editar aqui, ou seleccione um Blueprint e preencha
|
|
2273
|
+
st.caption("Gere um rascunho com IA para o editar aqui, ou seleccione um Blueprint e preencha as configurações do tema para começar.")
|
|
2157
2274
|
|
|
2158
2275
|
with history_tab:
|
|
2159
2276
|
st.caption(f"Histórico persistente: `{script_dir}` · índice em `{STORAGE / 'state' / 'scripts.json'}`")
|
|
@@ -2900,6 +3017,58 @@ def render_videos():
|
|
|
2900
3017
|
st.rerun()
|
|
2901
3018
|
|
|
2902
3019
|
|
|
3020
|
+
def render_thumbnails():
|
|
3021
|
+
st.title("Thumbnails")
|
|
3022
|
+
st.caption("Biblioteca de thumbnails associadas às tarefas da pipeline. A refacção actualiza apenas a imagem da tarefa e preserva o histórico local.")
|
|
3023
|
+
records = list_thumbnail_tasks()
|
|
3024
|
+
if not records:
|
|
3025
|
+
st.info("Ainda não existem tarefas com thumbnail gerada ou prompt de imagem disponível.")
|
|
3026
|
+
return
|
|
3027
|
+
|
|
3028
|
+
settings = read_json("settings.json", {})
|
|
3029
|
+
for record in records:
|
|
3030
|
+
with st.container(border=True):
|
|
3031
|
+
image_col, details_col, action_col = st.columns([1.35, 2.65, 1.25])
|
|
3032
|
+
with image_col:
|
|
3033
|
+
image_path = record.get("image_path")
|
|
3034
|
+
if image_path and image_path.is_file():
|
|
3035
|
+
st.image(str(image_path), use_container_width=True)
|
|
3036
|
+
else:
|
|
3037
|
+
st.markdown("### Sem imagem")
|
|
3038
|
+
st.caption("Imagem ainda não gerada")
|
|
3039
|
+
with details_col:
|
|
3040
|
+
st.write(f"**{record['title']}**")
|
|
3041
|
+
st.caption(f"Canal: {record['channel_name']} · Tarefa: {record['task_id']}")
|
|
3042
|
+
st.caption(f"Estado: {record['status']} · Variante: {record['variant_index'] + 1}")
|
|
3043
|
+
if record["prompt"]:
|
|
3044
|
+
with st.expander("Ver prompt da thumbnail", expanded=False):
|
|
3045
|
+
st.code(record["prompt"], language="text")
|
|
3046
|
+
else:
|
|
3047
|
+
st.warning("Esta tarefa não tem prompt de imagem. Não é possível refazer a thumbnail.")
|
|
3048
|
+
with action_col:
|
|
3049
|
+
if st.button(
|
|
3050
|
+
"Refazer thumbnail",
|
|
3051
|
+
key=f"regenerate_thumbnail_{record['task_id']}",
|
|
3052
|
+
icon=":material/refresh:",
|
|
3053
|
+
use_container_width=True,
|
|
3054
|
+
disabled=not bool(record["prompt"]),
|
|
3055
|
+
):
|
|
3056
|
+
try:
|
|
3057
|
+
with st.spinner("A refazer a thumbnail…"):
|
|
3058
|
+
_task, image_path = regenerate_thumbnail(record["task_id"], settings)
|
|
3059
|
+
record_notification(
|
|
3060
|
+
"thumbnail_generation_completed",
|
|
3061
|
+
f"Thumbnail refeita: {record['title']}",
|
|
3062
|
+
"A thumbnail da tarefa foi regenerada com sucesso.",
|
|
3063
|
+
metadata={"task_id": record["task_id"], "channel_name": record["channel_name"], "image_path": str(image_path)},
|
|
3064
|
+
dedupe_key=f"thumbnail:regenerated:{record['task_id']}:{image_path}",
|
|
3065
|
+
)
|
|
3066
|
+
st.success("Thumbnail refeita com sucesso.")
|
|
3067
|
+
st.rerun()
|
|
3068
|
+
except ThumbnailGenerationError as exc:
|
|
3069
|
+
st.error(str(exc))
|
|
3070
|
+
|
|
3071
|
+
|
|
2903
3072
|
def render_automation():
|
|
2904
3073
|
st.title("Automação Youtube")
|
|
2905
3074
|
st.caption("Agendamento diário da geração por canal. O worker verifica o relógio local do computador e coloca os lotes agendados na fila.")
|
|
@@ -4548,6 +4717,7 @@ def main():
|
|
|
4548
4717
|
("Criação de Vídeos", ":material/add_circle:", "Criação de Vídeos"),
|
|
4549
4718
|
("Backlog Vídeos", ":material/video_library:", "Backlog Vídeos"),
|
|
4550
4719
|
("Roteiros", ":material/article:", "Roteiros"),
|
|
4720
|
+
("Thumbnails", ":material/image:", "Thumbnails"),
|
|
4551
4721
|
("Upload", ":material/cloud_upload:", "Upload"),
|
|
4552
4722
|
]
|
|
4553
4723
|
base_files_items = [
|
|
@@ -4682,6 +4852,7 @@ def main():
|
|
|
4682
4852
|
"Criação de Músicas": render_music_creation,
|
|
4683
4853
|
"Upload Música": lambda: render_edit_placeholder("Upload Música", ""),
|
|
4684
4854
|
"Roteiros": render_scripts,
|
|
4855
|
+
"Thumbnails": render_thumbnails,
|
|
4685
4856
|
"Upload": render_upload,
|
|
4686
4857
|
"Blueprints Youtube": render_blueprints,
|
|
4687
4858
|
"Prompt Masters": render_tiktok_prompt_masters,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .storage import now, read_json, write_json
|
|
7
|
+
|
|
8
|
+
DRAFTS_FILE = "drafts.json"
|
|
9
|
+
MAX_DRAFTS = 200
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def list_drafts() -> list[dict[str, Any]]:
|
|
13
|
+
"""Return locally persisted drafts, newest first."""
|
|
14
|
+
records = read_json(DRAFTS_FILE, [])
|
|
15
|
+
return [record for record in records if isinstance(record, dict)] if isinstance(records, list) else []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def save_draft(draft: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
"""Persist one editable pipeline draft without advancing any pipeline task."""
|
|
20
|
+
record = {
|
|
21
|
+
**draft,
|
|
22
|
+
"id": str(draft.get("id") or f"draft_{uuid.uuid4().hex[:12]}"),
|
|
23
|
+
"created_at": str(draft.get("created_at") or now()),
|
|
24
|
+
"updated_at": now(),
|
|
25
|
+
}
|
|
26
|
+
drafts = list_drafts()
|
|
27
|
+
drafts.insert(0, record)
|
|
28
|
+
write_json(DRAFTS_FILE, drafts[:MAX_DRAFTS])
|
|
29
|
+
return record
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = ["DRAFTS_FILE", "list_drafts", "save_draft"]
|
|
33
|
+
|
package/hermes_ui/languages.py
CHANGED
|
@@ -229,34 +229,34 @@ for _language_code, _api_key_values in _API_KEY_EXPANDER_TRANSLATIONS.items():
|
|
|
229
229
|
# translation path as the legacy pages.
|
|
230
230
|
UI_NAV_TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
231
231
|
"pt": {
|
|
232
|
-
"Arquivos Base": "Arquivos Base", "Pipeline Vídeos": "Pipeline Vídeos", "Canais e Perfis de Vídeos": "Canais e Perfis de Vídeos", "Canais YouTube": "Canais YouTube", "Facebook Pages": "Facebook Pages", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Backlog Vídeos", "Música": "Música", "Upload Música": "Upload Música", "Geração de Conteúdo IA": "Geração de Conteúdo IA", "Motion Control": "Motion Control", "UGC Products": "UGC Products", "Growth": "Growth", "Analista Growth Youtube": "Analista Growth Youtube", "Analista Growth Tiktok": "Analista Growth Tiktok", "Analista Growth Instagram": "Analista Growth Instagram", "Documentação": "Documentação", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Música": "Pipeline Música", "módulos disponíveis": "módulos disponíveis",
|
|
232
|
+
"Arquivos Base": "Arquivos Base", "Pipeline Vídeos": "Pipeline Vídeos", "Canais e Perfis de Vídeos": "Canais e Perfis de Vídeos", "Canais YouTube": "Canais YouTube", "Facebook Pages": "Facebook Pages", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Backlog Vídeos", "Thumbnails": "Thumbnails", "Música": "Música", "Upload Música": "Upload Música", "Geração de Conteúdo IA": "Geração de Conteúdo IA", "Motion Control": "Motion Control", "UGC Products": "UGC Products", "Growth": "Growth", "Analista Growth Youtube": "Analista Growth Youtube", "Analista Growth Tiktok": "Analista Growth Tiktok", "Analista Growth Instagram": "Analista Growth Instagram", "Documentação": "Documentação", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Música": "Pipeline Música", "módulos disponíveis": "módulos disponíveis",
|
|
233
233
|
},
|
|
234
234
|
"en": {
|
|
235
|
-
"Arquivos Base": "Base Files", "Pipeline Vídeos": "Video Pipeline", "Canais e Perfis de Vídeos": "Video Channels & Profiles", "Canais YouTube": "YouTube Channels", "Facebook Pages": "Facebook Pages", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Video Backlog", "Música": "Music", "Upload Música": "Music Upload", "Geração de Conteúdo IA": "AI Content Generation", "Motion Control": "Motion Control", "UGC Products": "UGC Products", "Growth": "Growth", "Analista Growth Youtube": "YouTube Growth Analyst", "Analista Growth Tiktok": "TikTok Growth Analyst", "Analista Growth Instagram": "Instagram Growth Analyst", "Documentação": "Documentation", "Tutorial Kaggle": "Kaggle Tutorial", "Tutorial Apify": "Apify Tutorial", "Pipeline Música": "Music Pipeline", "módulos disponíveis": "available modules",
|
|
235
|
+
"Arquivos Base": "Base Files", "Pipeline Vídeos": "Video Pipeline", "Canais e Perfis de Vídeos": "Video Channels & Profiles", "Canais YouTube": "YouTube Channels", "Facebook Pages": "Facebook Pages", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Video Backlog", "Thumbnails": "Thumbnails", "Música": "Music", "Upload Música": "Music Upload", "Geração de Conteúdo IA": "AI Content Generation", "Motion Control": "Motion Control", "UGC Products": "UGC Products", "Growth": "Growth", "Analista Growth Youtube": "YouTube Growth Analyst", "Analista Growth Tiktok": "TikTok Growth Analyst", "Analista Growth Instagram": "Instagram Growth Analyst", "Documentação": "Documentation", "Tutorial Kaggle": "Kaggle Tutorial", "Tutorial Apify": "Apify Tutorial", "Pipeline Música": "Music Pipeline", "módulos disponíveis": "available modules",
|
|
236
236
|
},
|
|
237
237
|
"zh": {
|
|
238
|
-
"Arquivos Base": "基础文件", "Pipeline Vídeos": "视频流程", "Canais e Perfis de Vídeos": "视频频道与资料", "Canais YouTube": "YouTube 频道", "Facebook Pages": "Facebook 页面", "Prompt Masters": "主提示词", "Backlog Vídeos": "视频待办", "Música": "音乐", "Upload Música": "音乐上传", "Geração de Conteúdo IA": "AI 内容生成", "Motion Control": "动作控制", "UGC Products": "UGC 产品", "Growth": "增长", "Analista Growth Youtube": "YouTube 增长分析师", "Analista Growth Tiktok": "TikTok 增长分析师", "Analista Growth Instagram": "Instagram 增长分析师", "Documentação": "文档", "Tutorial Kaggle": "Kaggle 教程", "Tutorial Apify": "Apify 教程", "Pipeline Música": "音乐流程", "módulos disponíveis": "可用模块",
|
|
238
|
+
"Arquivos Base": "基础文件", "Pipeline Vídeos": "视频流程", "Canais e Perfis de Vídeos": "视频频道与资料", "Canais YouTube": "YouTube 频道", "Facebook Pages": "Facebook 页面", "Prompt Masters": "主提示词", "Backlog Vídeos": "视频待办", "Thumbnails": "缩略图", "Música": "音乐", "Upload Música": "音乐上传", "Geração de Conteúdo IA": "AI 内容生成", "Motion Control": "动作控制", "UGC Products": "UGC 产品", "Growth": "增长", "Analista Growth Youtube": "YouTube 增长分析师", "Analista Growth Tiktok": "TikTok 增长分析师", "Analista Growth Instagram": "Instagram 增长分析师", "Documentação": "文档", "Tutorial Kaggle": "Kaggle 教程", "Tutorial Apify": "Apify 教程", "Pipeline Música": "音乐流程", "módulos disponíveis": "可用模块",
|
|
239
239
|
},
|
|
240
240
|
"de": {
|
|
241
|
-
"Arquivos Base": "Basisdateien", "Pipeline Vídeos": "Video-Pipeline", "Canais e Perfis de Vídeos": "Videokanäle und Profile", "Canais YouTube": "YouTube-Kanäle", "Facebook Pages": "Facebook-Seiten", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Video-Backlog", "Música": "Musik", "Upload Música": "Musik-Upload", "Geração de Conteúdo IA": "KI-Inhaltserstellung", "Motion Control": "Motion Control", "UGC Products": "UGC-Produkte", "Growth": "Wachstum", "Analista Growth Youtube": "YouTube-Wachstumsanalyst", "Analista Growth Tiktok": "TikTok-Wachstumsanalyst", "Analista Growth Instagram": "Instagram-Wachstumsanalyst", "Documentação": "Dokumentation", "Tutorial Kaggle": "Kaggle-Tutorial", "Tutorial Apify": "Apify-Tutorial", "Pipeline Música": "Musik-Pipeline", "módulos disponíveis": "verfügbare Module",
|
|
241
|
+
"Arquivos Base": "Basisdateien", "Pipeline Vídeos": "Video-Pipeline", "Canais e Perfis de Vídeos": "Videokanäle und Profile", "Canais YouTube": "YouTube-Kanäle", "Facebook Pages": "Facebook-Seiten", "Prompt Masters": "Prompt Masters", "Backlog Vídeos": "Video-Backlog", "Thumbnails": "Thumbnails", "Música": "Musik", "Upload Música": "Musik-Upload", "Geração de Conteúdo IA": "KI-Inhaltserstellung", "Motion Control": "Motion Control", "UGC Products": "UGC-Produkte", "Growth": "Wachstum", "Analista Growth Youtube": "YouTube-Wachstumsanalyst", "Analista Growth Tiktok": "TikTok-Wachstumsanalyst", "Analista Growth Instagram": "Instagram-Wachstumsanalyst", "Documentação": "Dokumentation", "Tutorial Kaggle": "Kaggle-Tutorial", "Tutorial Apify": "Apify-Tutorial", "Pipeline Música": "Musik-Pipeline", "módulos disponíveis": "verfügbare Module",
|
|
242
242
|
},
|
|
243
243
|
"vi": {
|
|
244
|
-
"Arquivos Base": "Tệp cơ sở", "Pipeline Vídeos": "Quy trình video", "Canais e Perfis de Vídeos": "Kênh và hồ sơ video", "Canais YouTube": "Kênh YouTube", "Facebook Pages": "Trang Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Danh sách video", "Música": "Âm nhạc", "Upload Música": "Tải nhạc lên", "Geração de Conteúdo IA": "Tạo nội dung AI", "Motion Control": "Điều khiển chuyển động", "UGC Products": "Sản phẩm UGC", "Growth": "Tăng trưởng", "Analista Growth Youtube": "Chuyên viên phân tích tăng trưởng YouTube", "Analista Growth Tiktok": "Chuyên viên phân tích tăng trưởng TikTok", "Analista Growth Instagram": "Chuyên viên phân tích tăng trưởng Instagram", "Documentação": "Tài liệu", "Tutorial Kaggle": "Hướng dẫn Kaggle", "Tutorial Apify": "Hướng dẫn Apify", "Pipeline Música": "Quy trình âm nhạc", "módulos disponíveis": "mô-đun khả dụng",
|
|
244
|
+
"Arquivos Base": "Tệp cơ sở", "Pipeline Vídeos": "Quy trình video", "Canais e Perfis de Vídeos": "Kênh và hồ sơ video", "Canais YouTube": "Kênh YouTube", "Facebook Pages": "Trang Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Danh sách video", "Thumbnails": "Ảnh thu nhỏ", "Música": "Âm nhạc", "Upload Música": "Tải nhạc lên", "Geração de Conteúdo IA": "Tạo nội dung AI", "Motion Control": "Điều khiển chuyển động", "UGC Products": "Sản phẩm UGC", "Growth": "Tăng trưởng", "Analista Growth Youtube": "Chuyên viên phân tích tăng trưởng YouTube", "Analista Growth Tiktok": "Chuyên viên phân tích tăng trưởng TikTok", "Analista Growth Instagram": "Chuyên viên phân tích tăng trưởng Instagram", "Documentação": "Tài liệu", "Tutorial Kaggle": "Hướng dẫn Kaggle", "Tutorial Apify": "Hướng dẫn Apify", "Pipeline Música": "Quy trình âm nhạc", "módulos disponíveis": "mô-đun khả dụng",
|
|
245
245
|
},
|
|
246
246
|
"tr": {
|
|
247
|
-
"Arquivos Base": "Temel Dosyalar", "Pipeline Vídeos": "Video Akışı", "Canais e Perfis de Vídeos": "Video Kanalları ve Profilleri", "Canais YouTube": "YouTube Kanalları", "Facebook Pages": "Facebook Sayfaları", "Prompt Masters": "Prompt Master'lar", "Backlog Vídeos": "Video Bekleme Listesi", "Música": "Müzik", "Upload Música": "Müzik Yükleme", "Geração de Conteúdo IA": "Yapay Zekâ İçeriği Oluşturma", "Motion Control": "Hareket Kontrolü", "UGC Products": "UGC Ürünleri", "Growth": "Büyüme", "Analista Growth Youtube": "YouTube Büyüme Analisti", "Analista Growth Tiktok": "TikTok Büyüme Analisti", "Analista Growth Instagram": "Instagram Büyüme Analisti", "Documentação": "Dokümantasyon", "Tutorial Kaggle": "Kaggle Eğitimi", "Tutorial Apify": "Apify Eğitimi", "Pipeline Música": "Müzik Akışı", "módulos disponíveis": "kullanılabilir modüller",
|
|
247
|
+
"Arquivos Base": "Temel Dosyalar", "Pipeline Vídeos": "Video Akışı", "Canais e Perfis de Vídeos": "Video Kanalları ve Profilleri", "Canais YouTube": "YouTube Kanalları", "Facebook Pages": "Facebook Sayfaları", "Prompt Masters": "Prompt Master'lar", "Backlog Vídeos": "Video Bekleme Listesi", "Thumbnails": "Küçük resimler", "Música": "Müzik", "Upload Música": "Müzik Yükleme", "Geração de Conteúdo IA": "Yapay Zekâ İçeriği Oluşturma", "Motion Control": "Hareket Kontrolü", "UGC Products": "UGC Ürünleri", "Growth": "Büyüme", "Analista Growth Youtube": "YouTube Büyüme Analisti", "Analista Growth Tiktok": "TikTok Büyüme Analisti", "Analista Growth Instagram": "Instagram Büyüme Analisti", "Documentação": "Dokümantasyon", "Tutorial Kaggle": "Kaggle Eğitimi", "Tutorial Apify": "Apify Eğitimi", "Pipeline Música": "Müzik Akışı", "módulos disponíveis": "kullanılabilir modüller",
|
|
248
248
|
},
|
|
249
249
|
"ru": {
|
|
250
|
-
"Arquivos Base": "Базовые файлы", "Pipeline Vídeos": "Конвейер видео", "Canais e Perfis de Vídeos": "Каналы и профили видео", "Canais YouTube": "Каналы YouTube", "Facebook Pages": "Страницы Facebook", "Prompt Masters": "Мастер-промпты", "Backlog Vídeos": "Очередь видео", "Música": "Музыка", "Upload Música": "Загрузка музыки", "Geração de Conteúdo IA": "Генерация контента ИИ", "Motion Control": "Управление движением", "UGC Products": "UGC-продукты", "Growth": "Рост", "Analista Growth Youtube": "Аналитик роста YouTube", "Analista Growth Tiktok": "Аналитик роста TikTok", "Analista Growth Instagram": "Аналитик роста Instagram", "Documentação": "Документация", "Tutorial Kaggle": "Руководство Kaggle", "Tutorial Apify": "Руководство Apify", "Pipeline Música": "Конвейер музыки", "módulos disponíveis": "доступные модули",
|
|
250
|
+
"Arquivos Base": "Базовые файлы", "Pipeline Vídeos": "Конвейер видео", "Canais e Perfis de Vídeos": "Каналы и профили видео", "Canais YouTube": "Каналы YouTube", "Facebook Pages": "Страницы Facebook", "Prompt Masters": "Мастер-промпты", "Backlog Vídeos": "Очередь видео", "Thumbnails": "Миниатюры", "Música": "Музыка", "Upload Música": "Загрузка музыки", "Geração de Conteúdo IA": "Генерация контента ИИ", "Motion Control": "Управление движением", "UGC Products": "UGC-продукты", "Growth": "Рост", "Analista Growth Youtube": "Аналитик роста YouTube", "Analista Growth Tiktok": "Аналитик роста TikTok", "Analista Growth Instagram": "Аналитик роста Instagram", "Documentação": "Документация", "Tutorial Kaggle": "Руководство Kaggle", "Tutorial Apify": "Руководство Apify", "Pipeline Música": "Конвейер музыки", "módulos disponíveis": "доступные модули",
|
|
251
251
|
},
|
|
252
252
|
"es": {
|
|
253
|
-
"Arquivos Base": "Archivos base", "Pipeline Vídeos": "Flujo de vídeos", "Canais e Perfis de Vídeos": "Canales y perfiles de vídeo", "Canais YouTube": "Canales de YouTube", "Facebook Pages": "Páginas de Facebook", "Prompt Masters": "Prompts maestros", "Backlog Vídeos": "Cola de vídeos", "Música": "Música", "Upload Música": "Subir música", "Geração de Conteúdo IA": "Generación de contenido con IA", "Motion Control": "Control de movimiento", "UGC Products": "Productos UGC", "Growth": "Crecimiento", "Analista Growth Youtube": "Analista de crecimiento de YouTube", "Analista Growth Tiktok": "Analista de crecimiento de TikTok", "Analista Growth Instagram": "Analista de crecimiento de Instagram", "Documentação": "Documentación", "Tutorial Kaggle": "Tutorial de Kaggle", "Tutorial Apify": "Tutorial de Apify", "Pipeline Música": "Flujo de música", "módulos disponíveis": "módulos disponibles",
|
|
253
|
+
"Arquivos Base": "Archivos base", "Pipeline Vídeos": "Flujo de vídeos", "Canais e Perfis de Vídeos": "Canales y perfiles de vídeo", "Canais YouTube": "Canales de YouTube", "Facebook Pages": "Páginas de Facebook", "Prompt Masters": "Prompts maestros", "Backlog Vídeos": "Cola de vídeos", "Thumbnails": "Miniaturas", "Música": "Música", "Upload Música": "Subir música", "Geração de Conteúdo IA": "Generación de contenido con IA", "Motion Control": "Control de movimiento", "UGC Products": "Productos UGC", "Growth": "Crecimiento", "Analista Growth Youtube": "Analista de crecimiento de YouTube", "Analista Growth Tiktok": "Analista de crecimiento de TikTok", "Analista Growth Instagram": "Analista de crecimiento de Instagram", "Documentação": "Documentación", "Tutorial Kaggle": "Tutorial de Kaggle", "Tutorial Apify": "Tutorial de Apify", "Pipeline Música": "Flujo de música", "módulos disponíveis": "módulos disponibles",
|
|
254
254
|
},
|
|
255
255
|
"id": {
|
|
256
|
-
"Arquivos Base": "File Dasar", "Pipeline Vídeos": "Alur Video", "Canais e Perfis de Vídeos": "Kanal dan Profil Video", "Canais YouTube": "Kanal YouTube", "Facebook Pages": "Halaman Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Antrean Video", "Música": "Musik", "Upload Música": "Unggah Musik", "Geração de Conteúdo IA": "Pembuatan Konten AI", "Motion Control": "Kontrol Gerakan", "UGC Products": "Produk UGC", "Growth": "Pertumbuhan", "Analista Growth Youtube": "Analis Pertumbuhan YouTube", "Analista Growth Tiktok": "Analis Pertumbuhan TikTok", "Analista Growth Instagram": "Analis Pertumbuhan Instagram", "Documentação": "Dokumentasi", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Música": "Alur Musik", "módulos disponíveis": "modul tersedia",
|
|
256
|
+
"Arquivos Base": "File Dasar", "Pipeline Vídeos": "Alur Video", "Canais e Perfis de Vídeos": "Kanal dan Profil Video", "Canais YouTube": "Kanal YouTube", "Facebook Pages": "Halaman Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Antrean Video", "Thumbnails": "Thumbnail", "Música": "Musik", "Upload Música": "Unggah Musik", "Geração de Conteúdo IA": "Pembuatan Konten AI", "Motion Control": "Kontrol Gerakan", "UGC Products": "Produk UGC", "Growth": "Pertumbuhan", "Analista Growth Youtube": "Analis Pertumbuhan YouTube", "Analista Growth Tiktok": "Analis Pertumbuhan TikTok", "Analista Growth Instagram": "Analis Pertumbuhan Instagram", "Documentação": "Dokumentasi", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Música": "Alur Musik", "módulos disponíveis": "modul tersedia",
|
|
257
257
|
},
|
|
258
258
|
"it": {
|
|
259
|
-
"Arquivos Base": "File di base", "Pipeline Vídeos": "Pipeline video", "Canais e Perfis de Vídeos": "Canali e profili video", "Canais YouTube": "Canali YouTube", "Facebook Pages": "Pagine Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Coda video", "Música": "Musica", "Upload Música": "Caricamento musica", "Geração de Conteúdo IA": "Generazione di contenuti IA", "Motion Control": "Controllo del movimento", "UGC Products": "Prodotti UGC", "Growth": "Crescita", "Analista Growth Youtube": "Analista della crescita YouTube", "Analista Growth Tiktok": "Analista della crescita TikTok", "Analista Growth Instagram": "Analista della crescita Instagram", "Documentação": "Documentazione", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Música": "Pipeline musicale", "módulos disponíveis": "moduli disponibili",
|
|
259
|
+
"Arquivos Base": "File di base", "Pipeline Vídeos": "Pipeline video", "Canais e Perfis de Vídeos": "Canali e profili video", "Canais YouTube": "Canali YouTube", "Facebook Pages": "Pagine Facebook", "Prompt Masters": "Prompt Master", "Backlog Vídeos": "Coda video", "Thumbnails": "Miniature", "Música": "Musica", "Upload Música": "Caricamento musica", "Geração de Conteúdo IA": "Generazione di contenuti IA", "Motion Control": "Controllo del movimento", "UGC Products": "Prodotti UGC", "Growth": "Crescita", "Analista Growth Youtube": "Analista della crescita YouTube", "Analista Growth Tiktok": "Analista della crescita TikTok", "Analista Growth Instagram": "Analista della crescita Instagram", "Documentação": "Documentazione", "Tutorial Kaggle": "Tutorial Kaggle", "Tutorial Apify": "Tutorial Apify", "Pipeline Música": "Pipeline musicale", "módulos disponíveis": "moduli disponibili",
|
|
260
260
|
},
|
|
261
261
|
}
|
|
262
262
|
for _language_code, _navigation_values in UI_NAV_TRANSLATIONS.items():
|
|
@@ -407,6 +407,72 @@ for _language_code, _video_generation_translation in VIDEO_GENERATION_TRANSLATIO
|
|
|
407
407
|
UI_TRANSLATIONS[_language_code].update(_video_generation_translation)
|
|
408
408
|
|
|
409
409
|
|
|
410
|
+
_PIPELINE_FEATURE_TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
411
|
+
"pt": {
|
|
412
|
+
"Thumbnails": "Thumbnails", "Refazer thumbnail": "Refazer thumbnail", "Salvar rascunho": "Salvar rascunho",
|
|
413
|
+
"Rascunho guardado localmente": "Rascunho guardado localmente", "Thumbnail refeita com sucesso.": "Thumbnail refeita com sucesso.",
|
|
414
|
+
"Ver prompt da thumbnail": "Ver prompt da thumbnail", "Sem imagem": "Sem imagem", "Imagem ainda não gerada": "Imagem ainda não gerada",
|
|
415
|
+
"Palavras-chave": "Palavras-chave", "Título do rascunho": "Título do rascunho",
|
|
416
|
+
},
|
|
417
|
+
"en": {
|
|
418
|
+
"Thumbnails": "Thumbnails", "Refazer thumbnail": "Regenerate thumbnail", "Salvar rascunho": "Save draft",
|
|
419
|
+
"Rascunho guardado localmente": "Draft saved locally", "Thumbnail refeita com sucesso.": "Thumbnail regenerated successfully.",
|
|
420
|
+
"Ver prompt da thumbnail": "View thumbnail prompt", "Sem imagem": "No image", "Imagem ainda não gerada": "Image not generated yet",
|
|
421
|
+
"Palavras-chave": "Keywords", "Título do rascunho": "Draft title",
|
|
422
|
+
},
|
|
423
|
+
"zh": {
|
|
424
|
+
"Thumbnails": "缩略图", "Refazer thumbnail": "重新生成缩略图", "Salvar rascunho": "保存草稿",
|
|
425
|
+
"Rascunho guardado localmente": "草稿已保存到本地", "Thumbnail refeita com sucesso.": "缩略图已成功重新生成。",
|
|
426
|
+
"Ver prompt da thumbnail": "查看缩略图提示词", "Sem imagem": "无图像", "Imagem ainda não gerada": "尚未生成图像",
|
|
427
|
+
"Palavras-chave": "关键词", "Título do rascunho": "草稿标题",
|
|
428
|
+
},
|
|
429
|
+
"de": {
|
|
430
|
+
"Thumbnails": "Thumbnails", "Refazer thumbnail": "Thumbnail neu generieren", "Salvar rascunho": "Entwurf speichern",
|
|
431
|
+
"Rascunho guardado localmente": "Entwurf lokal gespeichert", "Thumbnail refeita com sucesso.": "Thumbnail erfolgreich neu generiert.",
|
|
432
|
+
"Ver prompt da thumbnail": "Thumbnail-Prompt anzeigen", "Sem imagem": "Kein Bild", "Imagem ainda não gerada": "Bild noch nicht generiert",
|
|
433
|
+
"Palavras-chave": "Schlüsselwörter", "Título do rascunho": "Entwurfstitel",
|
|
434
|
+
},
|
|
435
|
+
"vi": {
|
|
436
|
+
"Thumbnails": "Ảnh thu nhỏ", "Refazer thumbnail": "Tạo lại ảnh thu nhỏ", "Salvar rascunho": "Lưu bản nháp",
|
|
437
|
+
"Rascunho guardado localmente": "Đã lưu bản nháp cục bộ", "Thumbnail refeita com sucesso.": "Đã tạo lại ảnh thu nhỏ thành công.",
|
|
438
|
+
"Ver prompt da thumbnail": "Xem prompt ảnh thu nhỏ", "Sem imagem": "Không có hình ảnh", "Imagem ainda não gerada": "Chưa tạo hình ảnh",
|
|
439
|
+
"Palavras-chave": "Từ khóa", "Título do rascunho": "Tiêu đề bản nháp",
|
|
440
|
+
},
|
|
441
|
+
"tr": {
|
|
442
|
+
"Thumbnails": "Küçük resimler", "Refazer thumbnail": "Küçük resmi yeniden oluştur", "Salvar rascunho": "Taslağı kaydet",
|
|
443
|
+
"Rascunho guardado localmente": "Taslak yerel olarak kaydedildi", "Thumbnail refeita com sucesso.": "Küçük resim başarıyla yeniden oluşturuldu.",
|
|
444
|
+
"Ver prompt da thumbnail": "Küçük resim istemini görüntüle", "Sem imagem": "Görsel yok", "Imagem ainda não gerada": "Görsel henüz oluşturulmadı",
|
|
445
|
+
"Palavras-chave": "Anahtar kelimeler", "Título do rascunho": "Taslak başlığı",
|
|
446
|
+
},
|
|
447
|
+
"ru": {
|
|
448
|
+
"Thumbnails": "Миниатюры", "Refazer thumbnail": "Создать миниатюру заново", "Salvar rascunho": "Сохранить черновик",
|
|
449
|
+
"Rascunho guardado localmente": "Черновик сохранён локально", "Thumbnail refeita com sucesso.": "Миниатюра успешно создана заново.",
|
|
450
|
+
"Ver prompt da thumbnail": "Показать промпт миниатюры", "Sem imagem": "Нет изображения", "Imagem ainda não gerada": "Изображение ещё не создано",
|
|
451
|
+
"Palavras-chave": "Ключевые слова", "Título do rascunho": "Название черновика",
|
|
452
|
+
},
|
|
453
|
+
"es": {
|
|
454
|
+
"Thumbnails": "Miniaturas", "Refazer thumbnail": "Regenerar miniatura", "Salvar rascunho": "Guardar borrador",
|
|
455
|
+
"Rascunho guardado localmente": "Borrador guardado localmente", "Thumbnail refeita com sucesso.": "Miniatura regenerada correctamente.",
|
|
456
|
+
"Ver prompt da thumbnail": "Ver prompt de la miniatura", "Sem imagem": "Sin imagen", "Imagem ainda não gerada": "Imagen aún no generada",
|
|
457
|
+
"Palavras-chave": "Palabras clave", "Título do rascunho": "Título del borrador",
|
|
458
|
+
},
|
|
459
|
+
"id": {
|
|
460
|
+
"Thumbnails": "Thumbnail", "Refazer thumbnail": "Buat ulang thumbnail", "Salvar rascunho": "Simpan draf",
|
|
461
|
+
"Rascunho guardado localmente": "Draf disimpan secara lokal", "Thumbnail refeita com sucesso.": "Thumbnail berhasil dibuat ulang.",
|
|
462
|
+
"Ver prompt da thumbnail": "Lihat prompt thumbnail", "Sem imagem": "Tidak ada gambar", "Imagem ainda não gerada": "Gambar belum dibuat",
|
|
463
|
+
"Palavras-chave": "Kata kunci", "Título do rascunho": "Judul draf",
|
|
464
|
+
},
|
|
465
|
+
"it": {
|
|
466
|
+
"Thumbnails": "Miniature", "Refazer thumbnail": "Rigenera miniatura", "Salvar rascunho": "Salva bozza",
|
|
467
|
+
"Rascunho guardado localmente": "Bozza salvata localmente", "Thumbnail refeita com sucesso.": "Miniatura rigenerata con successo.",
|
|
468
|
+
"Ver prompt da thumbnail": "Mostra prompt della miniatura", "Sem imagem": "Nessuna immagine", "Imagem ainda não gerada": "Immagine non ancora generata",
|
|
469
|
+
"Palavras-chave": "Parole chiave", "Título do rascunho": "Titolo della bozza",
|
|
470
|
+
},
|
|
471
|
+
}
|
|
472
|
+
for _language_code, _pipeline_feature_translation in _PIPELINE_FEATURE_TRANSLATIONS.items():
|
|
473
|
+
UI_TRANSLATIONS[_language_code].update(_pipeline_feature_translation)
|
|
474
|
+
|
|
475
|
+
|
|
410
476
|
_TAB_LABELS = (
|
|
411
477
|
"Blueprints", "Brandings", "Pesquisa pública", "Cadastro manual", "Contas cadastradas", "Biblioteca",
|
|
412
478
|
"Importar do YouTube", "Canais em lote gmail", "Criar vídeo", "Vídeos", "Novo roteiro/letra", "Histórico guardado",
|
package/hermes_ui/storage.py
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import uuid
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .storage import STORAGE, now, read_json, write_json
|
|
9
|
+
from .thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _as_path(value: Any) -> Path | None:
|
|
13
|
+
raw = str(value or "").strip()
|
|
14
|
+
if not raw:
|
|
15
|
+
return None
|
|
16
|
+
candidates = [Path(raw)]
|
|
17
|
+
path = Path(raw)
|
|
18
|
+
if not path.is_absolute():
|
|
19
|
+
candidates.extend([STORAGE / path, STORAGE.parent / path])
|
|
20
|
+
for candidate in candidates:
|
|
21
|
+
if candidate.is_file():
|
|
22
|
+
return candidate
|
|
23
|
+
return Path(raw)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _variant_index(task: dict[str, Any], variant: dict[str, Any], variants: list[Any]) -> int:
|
|
27
|
+
for key in ("variant_index", "index"):
|
|
28
|
+
try:
|
|
29
|
+
value = int(variant.get(key))
|
|
30
|
+
except (TypeError, ValueError):
|
|
31
|
+
continue
|
|
32
|
+
if 0 <= value < len(variants):
|
|
33
|
+
return value
|
|
34
|
+
for index, candidate in enumerate(variants):
|
|
35
|
+
if isinstance(candidate, dict) and candidate is variant:
|
|
36
|
+
return index
|
|
37
|
+
if isinstance(candidate, dict) and variant and candidate.get("image_prompt") == variant.get("image_prompt"):
|
|
38
|
+
return index
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def normalize_thumbnail_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
43
|
+
variants = task.get("thumbnail_variants") if isinstance(task.get("thumbnail_variants"), list) else []
|
|
44
|
+
variant = task.get("thumbnail_variant") if isinstance(task.get("thumbnail_variant"), dict) else {}
|
|
45
|
+
artifacts = task.get("artifacts") if isinstance(task.get("artifacts"), dict) else {}
|
|
46
|
+
image_path = variant.get("image_path") or task.get("thumbnail_path") or artifacts.get("thumbnail")
|
|
47
|
+
prompt = variant.get("image_prompt") or task.get("thumbnail_prompt") or ""
|
|
48
|
+
title = str(task.get("title") or task.get("topic") or "Vídeo sem título").strip()
|
|
49
|
+
return {
|
|
50
|
+
"task": task,
|
|
51
|
+
"task_id": str(task.get("id") or ""),
|
|
52
|
+
"title": title,
|
|
53
|
+
"topic": str(task.get("topic") or "").strip(),
|
|
54
|
+
"channel_name": str(task.get("channel_name") or "Canal sem nome").strip(),
|
|
55
|
+
"status": str(task.get("thumbnail_status") or "not_generated"),
|
|
56
|
+
"prompt": str(prompt or "").strip(),
|
|
57
|
+
"image_path": _as_path(image_path),
|
|
58
|
+
"variant": variant,
|
|
59
|
+
"variants": variants,
|
|
60
|
+
"variant_index": _variant_index(task, variant, variants),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def list_thumbnail_tasks() -> list[dict[str, Any]]:
|
|
65
|
+
tasks = read_json("tasks.json", [])
|
|
66
|
+
if not isinstance(tasks, list):
|
|
67
|
+
return []
|
|
68
|
+
records = []
|
|
69
|
+
for task in tasks:
|
|
70
|
+
if not isinstance(task, dict):
|
|
71
|
+
continue
|
|
72
|
+
record = normalize_thumbnail_task(task)
|
|
73
|
+
has_thumbnail_signal = bool(
|
|
74
|
+
record["image_path"]
|
|
75
|
+
or record["prompt"]
|
|
76
|
+
or task.get("thumbnail_status")
|
|
77
|
+
or task.get("thumbnail_variant")
|
|
78
|
+
or task.get("thumbnail_variants")
|
|
79
|
+
)
|
|
80
|
+
if record["task_id"] and has_thumbnail_signal:
|
|
81
|
+
records.append(record)
|
|
82
|
+
return records
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def regenerate_thumbnail(task_id: str, settings: dict[str, Any]) -> tuple[dict[str, Any], Path]:
|
|
86
|
+
tasks = read_json("tasks.json", [])
|
|
87
|
+
if not isinstance(tasks, list):
|
|
88
|
+
raise ThumbnailGenerationError("O índice de tarefas não está disponível.")
|
|
89
|
+
for task in tasks:
|
|
90
|
+
if not isinstance(task, dict) or str(task.get("id") or "") != str(task_id):
|
|
91
|
+
continue
|
|
92
|
+
record = normalize_thumbnail_task(task)
|
|
93
|
+
if not record["prompt"]:
|
|
94
|
+
raise ThumbnailGenerationError("Esta tarefa não tem um prompt de imagem para refazer a thumbnail.")
|
|
95
|
+
previous_image = record.get("image_path")
|
|
96
|
+
if previous_image and previous_image.is_file():
|
|
97
|
+
history_dir = STORAGE / "thumbnails" / "history"
|
|
98
|
+
history_dir.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
history_path = history_dir / f"{task_id}-{uuid.uuid4().hex[:10]}-{previous_image.name}"
|
|
100
|
+
shutil.copy2(previous_image, history_path)
|
|
101
|
+
image_path = generate_thumbnail_image(
|
|
102
|
+
settings,
|
|
103
|
+
record["prompt"],
|
|
104
|
+
topic=record["title"] or record["topic"],
|
|
105
|
+
variant_index=record["variant_index"],
|
|
106
|
+
)
|
|
107
|
+
artifacts = dict(task.get("artifacts") or {})
|
|
108
|
+
artifacts["thumbnail"] = str(image_path)
|
|
109
|
+
task["artifacts"] = artifacts
|
|
110
|
+
task["thumbnail_path"] = str(image_path)
|
|
111
|
+
task["thumbnail_status"] = "generated"
|
|
112
|
+
variant = dict(record["variant"])
|
|
113
|
+
if variant:
|
|
114
|
+
variant["image_path"] = str(image_path)
|
|
115
|
+
task["thumbnail_variant"] = variant
|
|
116
|
+
variants = task.get("thumbnail_variants")
|
|
117
|
+
if isinstance(variants, list) and 0 <= record["variant_index"] < len(variants) and isinstance(variants[record["variant_index"]], dict):
|
|
118
|
+
variants[record["variant_index"]] = {**variants[record["variant_index"]], "image_path": str(image_path)}
|
|
119
|
+
task["thumbnail_variants"] = variants
|
|
120
|
+
task["updated_at"] = now()
|
|
121
|
+
write_json("tasks.json", tasks)
|
|
122
|
+
return task, image_path
|
|
123
|
+
raise ThumbnailGenerationError(f"A tarefa {task_id} não foi encontrada.")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = ["list_thumbnail_tasks", "normalize_thumbnail_task", "regenerate_thumbnail"]
|
|
127
|
+
|
package/package.json
CHANGED