@danhachuel/thunderbolt 0.4.42 → 0.4.44
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 +188 -15
- package/hermes_ui/automation_worker.py +3 -1
- package/hermes_ui/domain.py +2 -1
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -937,6 +937,7 @@ def render_video_generation_settings(
|
|
|
937
937
|
*,
|
|
938
938
|
current_language: str = "",
|
|
939
939
|
channel: dict[str, Any] | None = None,
|
|
940
|
+
default_aspect_ratio: str = "Landscape 16:9",
|
|
940
941
|
generate_content_callback: Any | None = None,
|
|
941
942
|
save_draft_callback: Any | None = None,
|
|
942
943
|
sections: set[str] | None = None,
|
|
@@ -1048,7 +1049,10 @@ def render_video_generation_settings(
|
|
|
1048
1049
|
settings["match_visuals_to_script_order"] = st.checkbox("Match Visuals to Script Order", value=False, key=f"{prefix}_match_visuals")
|
|
1049
1050
|
settings["video_transition_mode"] = st.selectbox("Video Transition Mode", VIDEO_TRANSITION_OPTIONS, key=f"{prefix}_video_transition")
|
|
1050
1051
|
with video_cols[1]:
|
|
1051
|
-
|
|
1052
|
+
aspect_options = ["Portrait 9:16", "Landscape 16:9", "Square 1:1"]
|
|
1053
|
+
if f"{prefix}_video_aspect_ratio" not in st.session_state:
|
|
1054
|
+
st.session_state[f"{prefix}_video_aspect_ratio"] = default_aspect_ratio
|
|
1055
|
+
settings["video_aspect_ratio"] = st.selectbox("Proporção do vídeo", aspect_options, index=aspect_options.index(st.session_state[f"{prefix}_video_aspect_ratio"]) if st.session_state[f"{prefix}_video_aspect_ratio"] in aspect_options else 0, key=f"{prefix}_video_aspect_ratio")
|
|
1052
1056
|
settings["maximum_clip_duration"] = st.selectbox("Maximum Clip Duration (seconds)", [3, 5, 8, 10, 15], key=f"{prefix}_maximum_clip_duration")
|
|
1053
1057
|
settings["videos_per_run"] = st.selectbox("Videos per Run", list(range(1, 11)), key=f"{prefix}_videos_per_run")
|
|
1054
1058
|
settings["video_encoder"] = st.selectbox("Video Encoder", VIDEO_ENCODER_OPTIONS, key=f"{prefix}_video_encoder")
|
|
@@ -1840,6 +1844,137 @@ def render_tiktok_prompt_masters():
|
|
|
1840
1844
|
st.error(str(exc))
|
|
1841
1845
|
|
|
1842
1846
|
|
|
1847
|
+
def format_metric_number(value: Any) -> str:
|
|
1848
|
+
if value in (None, ""):
|
|
1849
|
+
return "—"
|
|
1850
|
+
try:
|
|
1851
|
+
return f"{int(value):,}".replace(",", ".")
|
|
1852
|
+
except (TypeError, ValueError):
|
|
1853
|
+
return str(value)
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
def _tiktok_channel_records() -> list[dict[str, Any]]:
|
|
1857
|
+
return [channel for channel in read_json("channels.json", []) if isinstance(channel, dict) and channel.get("platform") == "tiktok"]
|
|
1858
|
+
|
|
1859
|
+
|
|
1860
|
+
def _tiktok_prompt_options() -> tuple[list[str], dict[str, str]]:
|
|
1861
|
+
files = list_prompt_master_files()
|
|
1862
|
+
labels = {path.name: get_display_name("prompt_masters", path, path.stem) for path in files}
|
|
1863
|
+
return [""] + [path.name for path in files], {"": "Sem Prompt Master padrão", **labels}
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
def render_tiktok_channels():
|
|
1867
|
+
st.title("Canais Tiktok")
|
|
1868
|
+
st.caption("Canais de Shorts em formato Portrait 9:16. A pesquisa usa exclusivamente a página pública do TikTok, sem API.")
|
|
1869
|
+
settings = read_json("settings.json", {})
|
|
1870
|
+
channels = _tiktok_channel_records()
|
|
1871
|
+
import_tab, spreadsheet_tab, manual_tab = render_localized_tabs(["Importar do Tiktok", "Canais em lote Planilha", "Cadastro manual"])
|
|
1872
|
+
prompt_ids, prompt_labels = _tiktok_prompt_options()
|
|
1873
|
+
with import_tab:
|
|
1874
|
+
st.caption("A pesquisa consulta exclusivamente a página pública do TikTok. Não existe método alternativo nem utilização de API.")
|
|
1875
|
+
source = st.text_input("URL pública ou @handle", placeholder="https://www.tiktok.com/@conta", key="tiktok_channel_source")
|
|
1876
|
+
lookup_cols = st.columns([1, 1])
|
|
1877
|
+
with lookup_cols[0]:
|
|
1878
|
+
if st.button("Buscar no Tiktok", type="primary", use_container_width=True, key="tiktok_channel_lookup"):
|
|
1879
|
+
result = fetch_public_tiktok_profile(source)
|
|
1880
|
+
st.session_state["tiktok_channel_import"] = {"ok": result.ok, "message": result.message, "data": result.data}
|
|
1881
|
+
with lookup_cols[1]:
|
|
1882
|
+
if st.button("Limpar importação", use_container_width=True, key="tiktok_channel_clear"):
|
|
1883
|
+
for key in ("tiktok_channel_import", "tiktok_import_language", "tiktok_import_niche", "tiktok_import_prompt", "tiktok_import_description"):
|
|
1884
|
+
st.session_state.pop(key, None)
|
|
1885
|
+
st.rerun()
|
|
1886
|
+
imported_state = st.session_state.get("tiktok_channel_import", {})
|
|
1887
|
+
if imported_state.get("message"):
|
|
1888
|
+
(st.success if imported_state.get("ok") else st.warning)(imported_state["message"])
|
|
1889
|
+
imported = imported_state.get("data") if imported_state.get("ok") else {}
|
|
1890
|
+
if imported:
|
|
1891
|
+
st.caption("Dados encontrados na página pública. Reveja os cartões e edite os campos antes de guardar.")
|
|
1892
|
+
with st.container(border=True):
|
|
1893
|
+
profile_cols = st.columns([0.8, 2.2, 1, 1, 1])
|
|
1894
|
+
with profile_cols[0]:
|
|
1895
|
+
avatar = imported.get("avatar_url") or imported.get("avatar")
|
|
1896
|
+
if avatar:
|
|
1897
|
+
st.image(avatar, width=82)
|
|
1898
|
+
else:
|
|
1899
|
+
st.markdown("### TT")
|
|
1900
|
+
with profile_cols[1]:
|
|
1901
|
+
st.markdown(f"### {imported.get('name') or imported.get('username') or 'Perfil TikTok'}")
|
|
1902
|
+
st.write(imported.get("handle") or f"@{imported.get('username', '')}")
|
|
1903
|
+
st.caption(imported.get("public_url") or imported.get("url") or "Página pública")
|
|
1904
|
+
with profile_cols[2]:
|
|
1905
|
+
st.metric("Seguidores", format_metric_number(imported.get("subscriber_count")))
|
|
1906
|
+
with profile_cols[3]:
|
|
1907
|
+
st.metric("Curtidas", format_metric_number(imported.get("likes_count")))
|
|
1908
|
+
with profile_cols[4]:
|
|
1909
|
+
st.metric("Vídeos", format_metric_number(imported.get("video_count")))
|
|
1910
|
+
if imported.get("bio"):
|
|
1911
|
+
st.caption(f"Descrição pública: {imported['bio']}")
|
|
1912
|
+
with st.container(border=True):
|
|
1913
|
+
st.markdown("**Dados do canal a cadastrar**")
|
|
1914
|
+
data_cols = st.columns(3)
|
|
1915
|
+
with data_cols[0]:
|
|
1916
|
+
st.write(f"**Nome:** {imported.get('name') or imported.get('username') or '—'}")
|
|
1917
|
+
st.write(f"**Handle:** {imported.get('handle') or '—'}")
|
|
1918
|
+
with data_cols[1]:
|
|
1919
|
+
st.write(f"**URL:** {imported.get('public_url') or imported.get('url') or '—'}")
|
|
1920
|
+
st.write(f"**Origem:** {imported.get('metrics_source') or 'tiktok_public_page'}")
|
|
1921
|
+
with data_cols[2]:
|
|
1922
|
+
st.write("**Formato:** Portrait 9:16")
|
|
1923
|
+
st.write("**Consulta:** Página pública")
|
|
1924
|
+
with st.form("tiktok_channel_import_form"):
|
|
1925
|
+
name = st.text_input("Nome canal", value=str(imported.get("name") or imported.get("username") or ""))
|
|
1926
|
+
url = st.text_input("URL canal", value=str(imported.get("public_url") or imported.get("url") or ""))
|
|
1927
|
+
handle = st.text_input("Handle canal", value=str(imported.get("handle") or ""))
|
|
1928
|
+
language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_import_language")
|
|
1929
|
+
niche = st.text_input("Nicho", key="tiktok_import_niche")
|
|
1930
|
+
prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_import_prompt")
|
|
1931
|
+
description = st.text_area("Descrição", value=str(imported.get("bio") or ""), key="tiktok_import_description")
|
|
1932
|
+
if st.form_submit_button("Cadastrar canal TikTok", type="primary"):
|
|
1933
|
+
channel = create_channel(name.strip(), url.strip(), {"platform": "tiktok", "handle": handle.strip(), "language": language, "niche": niche.strip(), "description": description.strip(), "default_prompt_master": prompt, "prompt_master": prompt, "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "metrics_source": "tiktok_public_page", "subscriber_count": imported.get("subscriber_count"), "video_count": imported.get("video_count")})
|
|
1934
|
+
update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt})
|
|
1935
|
+
st.session_state.pop("tiktok_channel_import", None)
|
|
1936
|
+
st.success(f"Canal {channel['name']} cadastrado.")
|
|
1937
|
+
st.rerun()
|
|
1938
|
+
with spreadsheet_tab:
|
|
1939
|
+
uploaded = st.file_uploader("Upload da planilha de canais TikTok", type=["xlsx", "xls"], key="tiktok_channel_spreadsheet")
|
|
1940
|
+
if uploaded and st.button("Ler e cadastrar planilha TikTok", type="primary", key="tiktok_read_sheet"):
|
|
1941
|
+
rows, warnings = parse_channel_workbook(uploaded.getvalue(), uploaded.name)
|
|
1942
|
+
for warning in warnings:
|
|
1943
|
+
st.warning(warning)
|
|
1944
|
+
created = 0
|
|
1945
|
+
for row in rows:
|
|
1946
|
+
candidate = {"name": row.get("name", ""), "handle": row.get("handle", ""), "url": row.get("url", "")}
|
|
1947
|
+
if find_duplicate_channel(candidate, channels):
|
|
1948
|
+
continue
|
|
1949
|
+
created_channel = create_channel(str(row.get("name") or row.get("handle") or "TikTok"), str(row.get("url") or ""), {"platform": "tiktok", "handle": row.get("handle", ""), "language": language_code(row.get("language") or "pt"), "niche": row.get("niche", ""), "description": row.get("description", ""), "default_prompt_master": row.get("prompt_master", ""), "prompt_master": row.get("prompt_master", ""), "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "automation_time": row.get("automation_time") or "00:00"})
|
|
1950
|
+
update_channel(created_channel["id"], {"platform": "tiktok"})
|
|
1951
|
+
created += 1
|
|
1952
|
+
st.success(f"{created} canal(is) TikTok cadastrado(s).")
|
|
1953
|
+
st.rerun()
|
|
1954
|
+
st.caption("A planilha usa os campos de URL, nome, handle, idioma, nicho, Prompt Master, descrição e horário.")
|
|
1955
|
+
with manual_tab:
|
|
1956
|
+
with st.form("tiktok_channel_manual_form"):
|
|
1957
|
+
url = st.text_input("URL canal", placeholder="https://www.tiktok.com/@conta", key="tiktok_manual_channel_url")
|
|
1958
|
+
name = st.text_input("Nome canal", key="tiktok_manual_channel_name")
|
|
1959
|
+
handle = st.text_input("Handle canal", key="tiktok_manual_channel_handle")
|
|
1960
|
+
language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_manual_channel_language")
|
|
1961
|
+
niche = st.text_input("Nicho", key="tiktok_manual_channel_niche")
|
|
1962
|
+
prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_manual_channel_prompt")
|
|
1963
|
+
description = st.text_area("Descrição", key="tiktok_manual_channel_description")
|
|
1964
|
+
if st.form_submit_button("Guardar canal manual", type="primary"):
|
|
1965
|
+
reference = normalize_tiktok_reference(url or handle)
|
|
1966
|
+
channel = create_channel(name.strip() or reference["username"], reference["url"], {"platform": "tiktok", "handle": reference["handle"], "language": language, "niche": niche.strip(), "description": description.strip(), "default_prompt_master": prompt, "prompt_master": prompt, "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16"})
|
|
1967
|
+
update_channel(channel["id"], {"platform": "tiktok"})
|
|
1968
|
+
st.success("Canal TikTok cadastrado.")
|
|
1969
|
+
st.rerun()
|
|
1970
|
+
st.divider()
|
|
1971
|
+
st.subheader(f"Canais TikTok cadastrados ({len(channels)})")
|
|
1972
|
+
if channels:
|
|
1973
|
+
st.dataframe([{k: c.get(v, "") for k, v in {"URL canal": "url", "Nome canal": "name", "Handle canal": "handle", "Idioma": "language", "Nicho": "niche", "Prompt Master": "default_prompt_master", "Proporção": "video_aspect_ratio", "Activo": "active", "Descrição": "description"}.items()} for c in channels], use_container_width=True, hide_index=True, height=360)
|
|
1974
|
+
else:
|
|
1975
|
+
st.info("Ainda não existem canais TikTok cadastrados.")
|
|
1976
|
+
|
|
1977
|
+
|
|
1843
1978
|
def render_channels():
|
|
1844
1979
|
st.title("Canais Youtube")
|
|
1845
1980
|
st.caption("Escolha entre importar dados públicos do YouTube ou preencher o canal manualmente.")
|
|
@@ -2575,25 +2710,29 @@ def render_video_from_draft() -> None:
|
|
|
2575
2710
|
st.success(f"Tarefa criada a partir de {record.get('title') or 'Roteiro sem título'}: {tasks[0].get('id') if tasks else '—'}.")
|
|
2576
2711
|
|
|
2577
2712
|
|
|
2578
|
-
def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "new_video"):
|
|
2713
|
+
def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "new_video", *, channel_platform: str = "youtube", fixed_aspect_ratio: str | None = None):
|
|
2579
2714
|
st.title(page_title)
|
|
2580
|
-
tab_labels = ["Criar vídeo"] + (["Gerar de Rascunho"] if page_title
|
|
2715
|
+
tab_labels = ["Criar vídeo"] + (["Gerar de Rascunho"] if page_title in {"Criação de Vídeos", "Criação de Shorts"} else [])
|
|
2581
2716
|
tabs = render_localized_tabs(tab_labels)
|
|
2582
2717
|
create_tab = tabs[0]
|
|
2583
2718
|
draft_tab = tabs[1] if len(tabs) > 1 else None
|
|
2584
2719
|
with create_tab:
|
|
2585
|
-
all_channels = [c for c in read_json("channels.json", []) if isinstance(c, dict)]
|
|
2720
|
+
all_channels = [c for c in read_json("channels.json", []) if isinstance(c, dict) and ((channel_platform == "tiktok" and c.get("platform") == "tiktok") or (channel_platform != "tiktok" and c.get("platform", "youtube") != "tiktok"))]
|
|
2586
2721
|
active_channels = [c for c in all_channels if c.get("active", True)]
|
|
2587
2722
|
if not all_channels:
|
|
2588
2723
|
st.warning("Cadastre pelo menos um canal antes de criar vídeos.")
|
|
2589
2724
|
else:
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2725
|
+
if fixed_aspect_ratio:
|
|
2726
|
+
mode = "single"
|
|
2727
|
+
st.info("Os Shorts são sempre criados para um canal TikTok específico e em formato Portrait 9:16.")
|
|
2728
|
+
else:
|
|
2729
|
+
mode_label = st.radio(
|
|
2730
|
+
"Modo de criação",
|
|
2731
|
+
["Canal específico", "Lote no mesmo canal", "Lote geral"],
|
|
2732
|
+
horizontal=True,
|
|
2733
|
+
key=f"{prefix}_mode",
|
|
2734
|
+
)
|
|
2735
|
+
mode = {"Canal específico": "single", "Lote no mesmo canal": "same_channel", "Lote geral": "general"}[mode_label]
|
|
2597
2736
|
selected_one: dict[str, Any] | None = None
|
|
2598
2737
|
legacy_language = st.session_state.get("video_language") or read_json("settings.json", {}).get("video_language")
|
|
2599
2738
|
if str(legacy_language or "").strip().casefold() in {"music", "00 – apenas música de fundo (sem falas)", "00 - apenas música de fundo (sem falas)"}:
|
|
@@ -2644,6 +2783,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2644
2783
|
generation_settings = render_video_generation_settings(
|
|
2645
2784
|
prefix,
|
|
2646
2785
|
current_language=str(st.session_state.get("video_language") or ""),
|
|
2786
|
+
default_aspect_ratio=fixed_aspect_ratio or "Landscape 16:9",
|
|
2647
2787
|
save_draft_callback=lambda: _save_pipeline_draft_callback(
|
|
2648
2788
|
prefix,
|
|
2649
2789
|
"music" if page_title == "Criação de Músicas" else "video",
|
|
@@ -2666,6 +2806,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2666
2806
|
prefix,
|
|
2667
2807
|
current_language=str(st.session_state.get("video_language") or ""),
|
|
2668
2808
|
channel=selected_one,
|
|
2809
|
+
default_aspect_ratio=fixed_aspect_ratio or "Landscape 16:9",
|
|
2669
2810
|
generate_content_callback=lambda: _generate_video_content_callback(
|
|
2670
2811
|
prefix,
|
|
2671
2812
|
selected_one,
|
|
@@ -2684,6 +2825,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2684
2825
|
generation_settings = render_video_generation_settings(
|
|
2685
2826
|
prefix,
|
|
2686
2827
|
current_language=str(st.session_state.get("video_language") or ""),
|
|
2828
|
+
default_aspect_ratio=fixed_aspect_ratio or "Landscape 16:9",
|
|
2687
2829
|
save_draft_callback=lambda: _save_pipeline_draft_callback(
|
|
2688
2830
|
prefix,
|
|
2689
2831
|
"music" if page_title == "Criação de Músicas" else "video",
|
|
@@ -4445,6 +4587,31 @@ def render_thumbnails():
|
|
|
4445
4587
|
st.error(str(exc))
|
|
4446
4588
|
|
|
4447
4589
|
|
|
4590
|
+
def render_tiktok_automation():
|
|
4591
|
+
st.title("Automação Tiktok")
|
|
4592
|
+
st.caption("Fila exclusiva de Shorts TikTok em Portrait 9:16, usando Prompt Master em vez de Blueprint.")
|
|
4593
|
+
channels = _tiktok_channel_records()
|
|
4594
|
+
prompt_ids, prompt_labels = _tiktok_prompt_options()
|
|
4595
|
+
if not channels:
|
|
4596
|
+
st.info("Cadastre primeiro um canal TikTok.")
|
|
4597
|
+
return
|
|
4598
|
+
for channel in channels:
|
|
4599
|
+
channel_id = str(channel["id"])
|
|
4600
|
+
with st.container(border=True):
|
|
4601
|
+
st.write(f"**{channel.get('name', 'Sem nome')}** · {channel.get('handle', '')}")
|
|
4602
|
+
enabled = st.toggle("Automação ligada", value=bool(channel.get("automation_on", False)), key=f"tiktok_automation_on_{channel_id}")
|
|
4603
|
+
schedule_time = st.text_input("Horário diário (HH:MM)", value=channel.get("automation_time", "00:00"), key=f"tiktok_automation_time_{channel_id}")
|
|
4604
|
+
prompt = st.selectbox("Prompt Master padrão", prompt_ids, index=prompt_ids.index(channel.get("default_prompt_master", "")) if channel.get("default_prompt_master", "") in prompt_ids else 0, format_func=lambda item: prompt_labels.get(item, item), key=f"tiktok_automation_prompt_{channel_id}")
|
|
4605
|
+
st.caption("Formato da fila: Portrait 9:16 · Pool de vídeo: TikTok")
|
|
4606
|
+
if st.button("Guardar automação TikTok", key=f"tiktok_automation_save_{channel_id}", type="primary"):
|
|
4607
|
+
if not valid_hhmm(schedule_time):
|
|
4608
|
+
st.error("Use o formato HH:MM, por exemplo 08:30.")
|
|
4609
|
+
else:
|
|
4610
|
+
update_channel(channel_id, {"automation_on": bool(enabled), "automation_time": schedule_time.strip(), "default_prompt_master": prompt, "prompt_master": prompt, "platform": "tiktok", "video_aspect_ratio": "Portrait 9:16"})
|
|
4611
|
+
st.success("Automação TikTok guardada.")
|
|
4612
|
+
st.rerun()
|
|
4613
|
+
|
|
4614
|
+
|
|
4448
4615
|
def render_automation():
|
|
4449
4616
|
st.title("Automação Youtube")
|
|
4450
4617
|
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.")
|
|
@@ -4459,7 +4626,7 @@ def render_automation():
|
|
|
4459
4626
|
st.caption(f"Última verificação do worker: {last_tick}")
|
|
4460
4627
|
if worker_status.get("last_error"):
|
|
4461
4628
|
st.error(f"Último erro do worker: {worker_status['last_error']}")
|
|
4462
|
-
channels = read_json("channels.json", [])
|
|
4629
|
+
channels = [channel for channel in read_json("channels.json", []) if isinstance(channel, dict) and channel.get("platform", "youtube") != "tiktok"]
|
|
4463
4630
|
if not channels:
|
|
4464
4631
|
st.info("Nenhum canal cadastrado para configurar.")
|
|
4465
4632
|
for channel in channels:
|
|
@@ -7409,6 +7576,7 @@ def render_pipeline():
|
|
|
7409
7576
|
def main():
|
|
7410
7577
|
pipeline_video_items = [
|
|
7411
7578
|
("Criação de Vídeos", ":material/add_circle:", "Criação de Vídeos"),
|
|
7579
|
+
("Criação de Shorts", ":material/phone_android:", "Criação de Shorts"),
|
|
7412
7580
|
("Backlog Vídeos", ":material/video_library:", "Backlog Vídeos"),
|
|
7413
7581
|
("Roteiros", ":material/article:", "Roteiros"),
|
|
7414
7582
|
("Thumbnails", ":material/image:", "Thumbnails"),
|
|
@@ -7416,6 +7584,7 @@ def main():
|
|
|
7416
7584
|
]
|
|
7417
7585
|
channel_profile_items = [
|
|
7418
7586
|
("Canais YouTube", ":material/ondemand_video:", "Canais YouTube"),
|
|
7587
|
+
("Canais Tiktok", ":material/music_video:", "Canais Tiktok"),
|
|
7419
7588
|
("Blueprints Youtube", ":material/library_books:", "Blueprints Youtube"),
|
|
7420
7589
|
("Thumbnail Blueprints", ":material/image:", "Thumbnail Blueprints"),
|
|
7421
7590
|
("Brandings Youtube", ":material/brush:", "Brandings Youtube"),
|
|
@@ -7463,6 +7632,7 @@ def main():
|
|
|
7463
7632
|
]
|
|
7464
7633
|
automation_items = [
|
|
7465
7634
|
("Automação Youtube", ":material/schedule:", "Automação Youtube"),
|
|
7635
|
+
("Automação Tiktok", ":material/schedule:", "Automação Tiktok"),
|
|
7466
7636
|
]
|
|
7467
7637
|
edition_items = [
|
|
7468
7638
|
("Limpador de Metadados", ":material/edit_note:", "Limpador de Metadados"),
|
|
@@ -7496,11 +7666,11 @@ def main():
|
|
|
7496
7666
|
"Configurações": settings_items,
|
|
7497
7667
|
}
|
|
7498
7668
|
nav_paths = {
|
|
7499
|
-
"Início": "/inicio", "Automação": "/automacao", "Automação Youtube": "/automacao/youtube",
|
|
7669
|
+
"Início": "/inicio", "Automação": "/automacao", "Automação Youtube": "/automacao/youtube", "Automação Tiktok": "/automacao/tiktok",
|
|
7500
7670
|
"Niche Finder": "/niche-finder", "Niche Finder Kaggle": "/niche-finder/kaggle", "Niche Finder Apify": "/niche-finder/apify",
|
|
7501
|
-
"Pipeline Vídeos": "/pipeline-videos", "Criação de Vídeos": "/pipeline-videos/criacao", "Backlog Vídeos": "/pipeline-videos/backlog", "Roteiros": "/pipeline-videos/roteiros", "Thumbnails": "/pipeline-videos/thumbnails", "Upload": "/pipeline-videos/upload",
|
|
7671
|
+
"Pipeline Vídeos": "/pipeline-videos", "Criação de Vídeos": "/pipeline-videos/criacao", "Criação de Shorts": "/pipeline-videos/shorts", "Backlog Vídeos": "/pipeline-videos/backlog", "Roteiros": "/pipeline-videos/roteiros", "Thumbnails": "/pipeline-videos/thumbnails", "Upload": "/pipeline-videos/upload",
|
|
7502
7672
|
"Pipeline Música": "/pipeline-musica", "Criação de Músicas": "/pipeline-musica/criacao", "Music Backlog": "/pipeline-musica/backlog", "Vozes Personalizadas": "/pipeline-musica/vozes-personalizadas", "Upload Música": "/pipeline-musica/upload",
|
|
7503
|
-
"Canais/Perfis (Vídeos)": "/canais-perfis-videos", "Canais YouTube": "/canais-perfis-videos/canais-youtube", "Blueprints Youtube": "/canais-perfis-videos/blueprints-youtube", "Thumbnail Blueprints": "/canais-perfis-videos/thumbnail-blueprints", "Brandings Youtube": "/canais-perfis-videos/brandings-youtube", "Contas TikTok": "/canais-perfis-videos/contas-tiktok", "Prompt Masters": "/canais-perfis-videos/prompt-masters", "Facebook Pages": "/canais-perfis-videos/facebook-pages",
|
|
7673
|
+
"Canais/Perfis (Vídeos)": "/canais-perfis-videos", "Canais YouTube": "/canais-perfis-videos/canais-youtube", "Canais Tiktok": "/canais-perfis-videos/canais-tiktok", "Blueprints Youtube": "/canais-perfis-videos/blueprints-youtube", "Thumbnail Blueprints": "/canais-perfis-videos/thumbnail-blueprints", "Brandings Youtube": "/canais-perfis-videos/brandings-youtube", "Contas TikTok": "/canais-perfis-videos/contas-tiktok", "Prompt Masters": "/canais-perfis-videos/prompt-masters", "Facebook Pages": "/canais-perfis-videos/facebook-pages",
|
|
7504
7674
|
"AI Influencers": "/ai-influencers", "Personagens": "/ai-influencers/personagens", "Geração de Conteúdo IA": "/ai-influencers/geracao-conteudo", "Motion Control": "/ai-influencers/motion-control", "UGC Products": "/ai-influencers/ugc-products", "Redes Sociais": "/ai-influencers/redes-sociais",
|
|
7505
7675
|
"Edição": "/edicao", "Limpador de Metadados": "/edicao/limpador-metadados", "Cortes": "/edicao/cortes", "Editor Python": "/edicao/editor-python", "Download Mídia": "/edicao/download-midia",
|
|
7506
7676
|
"Growth": "/growth", "Analista Growth Youtube": "/growth/youtube", "Analista Growth Tiktok": "/growth/tiktok", "Analista Growth Instagram": "/growth/instagram", "Analista Facebook Pages": "/growth/facebook-pages", "Analista Bilibili": "/growth/bilibili",
|
|
@@ -7576,6 +7746,7 @@ def main():
|
|
|
7576
7746
|
"Início": render_dashboard,
|
|
7577
7747
|
"Pipeline Vídeos": render_pipeline,
|
|
7578
7748
|
"Criação de Vídeos": render_new_video,
|
|
7749
|
+
"Criação de Shorts": lambda: render_new_video("Criação de Shorts", "new_shorts", channel_platform="tiktok", fixed_aspect_ratio="Portrait 9:16"),
|
|
7579
7750
|
"Backlog Vídeos": render_videos,
|
|
7580
7751
|
"Criação de Músicas": render_music_creation,
|
|
7581
7752
|
"Music Backlog": render_music_backlog,
|
|
@@ -7589,9 +7760,11 @@ def main():
|
|
|
7589
7760
|
"Brandings Youtube": render_youtube_brandings,
|
|
7590
7761
|
"Prompt Masters": render_tiktok_prompt_masters,
|
|
7591
7762
|
"Canais YouTube": render_channels,
|
|
7763
|
+
"Canais Tiktok": render_tiktok_channels,
|
|
7592
7764
|
"Contas TikTok": render_tiktok_accounts,
|
|
7593
7765
|
"Facebook Pages": lambda: render_edit_placeholder("Facebook Pages", ""),
|
|
7594
7766
|
"Automação Youtube": render_automation,
|
|
7767
|
+
"Automação Tiktok": render_tiktok_automation,
|
|
7595
7768
|
"Niche Finder Kaggle": render_niche_finder,
|
|
7596
7769
|
"Tutorial Kaggle": lambda: render_niche_tutorial("kaggle"),
|
|
7597
7770
|
"Niche Finder Apify": render_niche_finder_apify,
|
|
@@ -246,7 +246,9 @@ def _create_channel_batch(channel: dict[str, Any], when: datetime) -> dict[str,
|
|
|
246
246
|
topic, payload = _pending_payload(channel)
|
|
247
247
|
options = {
|
|
248
248
|
"language": payload.get("language") or channel.get("language") or "pt",
|
|
249
|
-
"format": "wide",
|
|
249
|
+
"format": "portrait" if channel.get("platform") == "tiktok" else "wide",
|
|
250
|
+
"platform": channel.get("platform", "youtube"),
|
|
251
|
+
"queue": "tiktok_shorts" if channel.get("platform") == "tiktok" else "youtube",
|
|
250
252
|
"style_wide": style_wide,
|
|
251
253
|
"style_ia": channel.get("style_ia") or "",
|
|
252
254
|
"material_source": payload.get("material_source") or "",
|
package/hermes_ui/domain.py
CHANGED
|
@@ -169,11 +169,12 @@ def create_tasks_for_batch(batch: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
169
169
|
"creation_mode": batch.get("mode", "single"),
|
|
170
170
|
"channel_id": channel_id,
|
|
171
171
|
"channel_name": channel.get("name", "Canal"),
|
|
172
|
+
"platform": channel.get("platform", "youtube"),
|
|
172
173
|
"topic": topic,
|
|
173
174
|
"title": title,
|
|
174
175
|
"topic_source": topic_source,
|
|
175
176
|
"language": payload.get("language", options.get("language", channel.get("language", "Português"))),
|
|
176
|
-
"format": payload.get("format", options.get("format", "wide")),
|
|
177
|
+
"format": payload.get("format", options.get("format", "portrait" if channel.get("platform") == "tiktok" else "wide")),
|
|
177
178
|
"style_wide": payload.get("style_wide", options.get("style_wide", channel.get("style_wide", "pexels"))),
|
|
178
179
|
"material_source": payload.get("material_source", options.get("material_source", channel.get("material_source", ""))),
|
|
179
180
|
"style_ia": payload.get("style_ia", options.get("style_ia", "")),
|
package/package.json
CHANGED