@danhachuel/thunderbolt 0.4.42 → 0.4.43
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 +138 -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,87 @@ def render_tiktok_prompt_masters():
|
|
|
1840
1844
|
st.error(str(exc))
|
|
1841
1845
|
|
|
1842
1846
|
|
|
1847
|
+
def _tiktok_channel_records() -> list[dict[str, Any]]:
|
|
1848
|
+
return [channel for channel in read_json("channels.json", []) if isinstance(channel, dict) and channel.get("platform") == "tiktok"]
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
def _tiktok_prompt_options() -> tuple[list[str], dict[str, str]]:
|
|
1852
|
+
files = list_prompt_master_files()
|
|
1853
|
+
labels = {path.name: get_display_name("prompt_masters", path, path.stem) for path in files}
|
|
1854
|
+
return [""] + [path.name for path in files], {"": "Sem Prompt Master padrão", **labels}
|
|
1855
|
+
|
|
1856
|
+
|
|
1857
|
+
def render_tiktok_channels():
|
|
1858
|
+
st.title("Canais Tiktok")
|
|
1859
|
+
st.caption("Canais de Shorts em formato Portrait 9:16. A pesquisa usa exclusivamente a página pública do TikTok, sem API.")
|
|
1860
|
+
settings = read_json("settings.json", {})
|
|
1861
|
+
channels = _tiktok_channel_records()
|
|
1862
|
+
import_tab, spreadsheet_tab, manual_tab = render_localized_tabs(["Importar do Tiktok", "Canais em lote Planilha", "Cadastro manual"])
|
|
1863
|
+
prompt_ids, prompt_labels = _tiktok_prompt_options()
|
|
1864
|
+
with import_tab:
|
|
1865
|
+
source = st.text_input("URL pública ou @handle", placeholder="https://www.tiktok.com/@conta", key="tiktok_channel_source")
|
|
1866
|
+
if st.button("Buscar no Tiktok", type="primary", use_container_width=True, key="tiktok_channel_lookup"):
|
|
1867
|
+
result = fetch_public_tiktok_profile(source)
|
|
1868
|
+
st.session_state["tiktok_channel_import"] = {"ok": result.ok, "message": result.message, "data": result.data}
|
|
1869
|
+
imported_state = st.session_state.get("tiktok_channel_import", {})
|
|
1870
|
+
if imported_state.get("message"):
|
|
1871
|
+
(st.success if imported_state.get("ok") else st.warning)(imported_state["message"])
|
|
1872
|
+
imported = imported_state.get("data") if imported_state.get("ok") else {}
|
|
1873
|
+
if imported:
|
|
1874
|
+
with st.form("tiktok_channel_import_form"):
|
|
1875
|
+
name = st.text_input("Nome canal", value=str(imported.get("name") or imported.get("username") or ""))
|
|
1876
|
+
url = st.text_input("URL canal", value=str(imported.get("public_url") or imported.get("url") or ""))
|
|
1877
|
+
handle = st.text_input("Handle canal", value=str(imported.get("handle") or ""))
|
|
1878
|
+
language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_import_language")
|
|
1879
|
+
niche = st.text_input("Nicho", key="tiktok_import_niche")
|
|
1880
|
+
prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_import_prompt")
|
|
1881
|
+
description = st.text_area("Descrição", value=str(imported.get("bio") or ""), key="tiktok_import_description")
|
|
1882
|
+
if st.form_submit_button("Cadastrar canal TikTok", type="primary"):
|
|
1883
|
+
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")})
|
|
1884
|
+
update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt})
|
|
1885
|
+
st.session_state.pop("tiktok_channel_import", None)
|
|
1886
|
+
st.success(f"Canal {channel['name']} cadastrado.")
|
|
1887
|
+
st.rerun()
|
|
1888
|
+
with spreadsheet_tab:
|
|
1889
|
+
uploaded = st.file_uploader("Upload da planilha de canais TikTok", type=["xlsx", "xls"], key="tiktok_channel_spreadsheet")
|
|
1890
|
+
if uploaded and st.button("Ler e cadastrar planilha TikTok", type="primary", key="tiktok_read_sheet"):
|
|
1891
|
+
rows, warnings = parse_channel_workbook(uploaded.getvalue(), uploaded.name)
|
|
1892
|
+
for warning in warnings:
|
|
1893
|
+
st.warning(warning)
|
|
1894
|
+
created = 0
|
|
1895
|
+
for row in rows:
|
|
1896
|
+
candidate = {"name": row.get("name", ""), "handle": row.get("handle", ""), "url": row.get("url", "")}
|
|
1897
|
+
if find_duplicate_channel(candidate, channels):
|
|
1898
|
+
continue
|
|
1899
|
+
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"})
|
|
1900
|
+
update_channel(created_channel["id"], {"platform": "tiktok"})
|
|
1901
|
+
created += 1
|
|
1902
|
+
st.success(f"{created} canal(is) TikTok cadastrado(s).")
|
|
1903
|
+
st.rerun()
|
|
1904
|
+
st.caption("A planilha usa os campos de URL, nome, handle, idioma, nicho, Prompt Master, descrição e horário.")
|
|
1905
|
+
with manual_tab:
|
|
1906
|
+
with st.form("tiktok_channel_manual_form"):
|
|
1907
|
+
url = st.text_input("URL canal", placeholder="https://www.tiktok.com/@conta", key="tiktok_manual_channel_url")
|
|
1908
|
+
name = st.text_input("Nome canal", key="tiktok_manual_channel_name")
|
|
1909
|
+
handle = st.text_input("Handle canal", key="tiktok_manual_channel_handle")
|
|
1910
|
+
language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_manual_channel_language")
|
|
1911
|
+
niche = st.text_input("Nicho", key="tiktok_manual_channel_niche")
|
|
1912
|
+
prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_manual_channel_prompt")
|
|
1913
|
+
description = st.text_area("Descrição", key="tiktok_manual_channel_description")
|
|
1914
|
+
if st.form_submit_button("Guardar canal manual", type="primary"):
|
|
1915
|
+
reference = normalize_tiktok_reference(url or handle)
|
|
1916
|
+
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"})
|
|
1917
|
+
update_channel(channel["id"], {"platform": "tiktok"})
|
|
1918
|
+
st.success("Canal TikTok cadastrado.")
|
|
1919
|
+
st.rerun()
|
|
1920
|
+
st.divider()
|
|
1921
|
+
st.subheader(f"Canais TikTok cadastrados ({len(channels)})")
|
|
1922
|
+
if channels:
|
|
1923
|
+
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)
|
|
1924
|
+
else:
|
|
1925
|
+
st.info("Ainda não existem canais TikTok cadastrados.")
|
|
1926
|
+
|
|
1927
|
+
|
|
1843
1928
|
def render_channels():
|
|
1844
1929
|
st.title("Canais Youtube")
|
|
1845
1930
|
st.caption("Escolha entre importar dados públicos do YouTube ou preencher o canal manualmente.")
|
|
@@ -2575,25 +2660,29 @@ def render_video_from_draft() -> None:
|
|
|
2575
2660
|
st.success(f"Tarefa criada a partir de {record.get('title') or 'Roteiro sem título'}: {tasks[0].get('id') if tasks else '—'}.")
|
|
2576
2661
|
|
|
2577
2662
|
|
|
2578
|
-
def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "new_video"):
|
|
2663
|
+
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
2664
|
st.title(page_title)
|
|
2580
|
-
tab_labels = ["Criar vídeo"] + (["Gerar de Rascunho"] if page_title
|
|
2665
|
+
tab_labels = ["Criar vídeo"] + (["Gerar de Rascunho"] if page_title in {"Criação de Vídeos", "Criação de Shorts"} else [])
|
|
2581
2666
|
tabs = render_localized_tabs(tab_labels)
|
|
2582
2667
|
create_tab = tabs[0]
|
|
2583
2668
|
draft_tab = tabs[1] if len(tabs) > 1 else None
|
|
2584
2669
|
with create_tab:
|
|
2585
|
-
all_channels = [c for c in read_json("channels.json", []) if isinstance(c, dict)]
|
|
2670
|
+
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
2671
|
active_channels = [c for c in all_channels if c.get("active", True)]
|
|
2587
2672
|
if not all_channels:
|
|
2588
2673
|
st.warning("Cadastre pelo menos um canal antes de criar vídeos.")
|
|
2589
2674
|
else:
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2675
|
+
if fixed_aspect_ratio:
|
|
2676
|
+
mode = "single"
|
|
2677
|
+
st.info("Os Shorts são sempre criados para um canal TikTok específico e em formato Portrait 9:16.")
|
|
2678
|
+
else:
|
|
2679
|
+
mode_label = st.radio(
|
|
2680
|
+
"Modo de criação",
|
|
2681
|
+
["Canal específico", "Lote no mesmo canal", "Lote geral"],
|
|
2682
|
+
horizontal=True,
|
|
2683
|
+
key=f"{prefix}_mode",
|
|
2684
|
+
)
|
|
2685
|
+
mode = {"Canal específico": "single", "Lote no mesmo canal": "same_channel", "Lote geral": "general"}[mode_label]
|
|
2597
2686
|
selected_one: dict[str, Any] | None = None
|
|
2598
2687
|
legacy_language = st.session_state.get("video_language") or read_json("settings.json", {}).get("video_language")
|
|
2599
2688
|
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 +2733,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2644
2733
|
generation_settings = render_video_generation_settings(
|
|
2645
2734
|
prefix,
|
|
2646
2735
|
current_language=str(st.session_state.get("video_language") or ""),
|
|
2736
|
+
default_aspect_ratio=fixed_aspect_ratio or "Landscape 16:9",
|
|
2647
2737
|
save_draft_callback=lambda: _save_pipeline_draft_callback(
|
|
2648
2738
|
prefix,
|
|
2649
2739
|
"music" if page_title == "Criação de Músicas" else "video",
|
|
@@ -2666,6 +2756,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2666
2756
|
prefix,
|
|
2667
2757
|
current_language=str(st.session_state.get("video_language") or ""),
|
|
2668
2758
|
channel=selected_one,
|
|
2759
|
+
default_aspect_ratio=fixed_aspect_ratio or "Landscape 16:9",
|
|
2669
2760
|
generate_content_callback=lambda: _generate_video_content_callback(
|
|
2670
2761
|
prefix,
|
|
2671
2762
|
selected_one,
|
|
@@ -2684,6 +2775,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2684
2775
|
generation_settings = render_video_generation_settings(
|
|
2685
2776
|
prefix,
|
|
2686
2777
|
current_language=str(st.session_state.get("video_language") or ""),
|
|
2778
|
+
default_aspect_ratio=fixed_aspect_ratio or "Landscape 16:9",
|
|
2687
2779
|
save_draft_callback=lambda: _save_pipeline_draft_callback(
|
|
2688
2780
|
prefix,
|
|
2689
2781
|
"music" if page_title == "Criação de Músicas" else "video",
|
|
@@ -4445,6 +4537,31 @@ def render_thumbnails():
|
|
|
4445
4537
|
st.error(str(exc))
|
|
4446
4538
|
|
|
4447
4539
|
|
|
4540
|
+
def render_tiktok_automation():
|
|
4541
|
+
st.title("Automação Tiktok")
|
|
4542
|
+
st.caption("Fila exclusiva de Shorts TikTok em Portrait 9:16, usando Prompt Master em vez de Blueprint.")
|
|
4543
|
+
channels = _tiktok_channel_records()
|
|
4544
|
+
prompt_ids, prompt_labels = _tiktok_prompt_options()
|
|
4545
|
+
if not channels:
|
|
4546
|
+
st.info("Cadastre primeiro um canal TikTok.")
|
|
4547
|
+
return
|
|
4548
|
+
for channel in channels:
|
|
4549
|
+
channel_id = str(channel["id"])
|
|
4550
|
+
with st.container(border=True):
|
|
4551
|
+
st.write(f"**{channel.get('name', 'Sem nome')}** · {channel.get('handle', '')}")
|
|
4552
|
+
enabled = st.toggle("Automação ligada", value=bool(channel.get("automation_on", False)), key=f"tiktok_automation_on_{channel_id}")
|
|
4553
|
+
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}")
|
|
4554
|
+
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}")
|
|
4555
|
+
st.caption("Formato da fila: Portrait 9:16 · Pool de vídeo: TikTok")
|
|
4556
|
+
if st.button("Guardar automação TikTok", key=f"tiktok_automation_save_{channel_id}", type="primary"):
|
|
4557
|
+
if not valid_hhmm(schedule_time):
|
|
4558
|
+
st.error("Use o formato HH:MM, por exemplo 08:30.")
|
|
4559
|
+
else:
|
|
4560
|
+
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"})
|
|
4561
|
+
st.success("Automação TikTok guardada.")
|
|
4562
|
+
st.rerun()
|
|
4563
|
+
|
|
4564
|
+
|
|
4448
4565
|
def render_automation():
|
|
4449
4566
|
st.title("Automação Youtube")
|
|
4450
4567
|
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 +4576,7 @@ def render_automation():
|
|
|
4459
4576
|
st.caption(f"Última verificação do worker: {last_tick}")
|
|
4460
4577
|
if worker_status.get("last_error"):
|
|
4461
4578
|
st.error(f"Último erro do worker: {worker_status['last_error']}")
|
|
4462
|
-
channels = read_json("channels.json", [])
|
|
4579
|
+
channels = [channel for channel in read_json("channels.json", []) if isinstance(channel, dict) and channel.get("platform", "youtube") != "tiktok"]
|
|
4463
4580
|
if not channels:
|
|
4464
4581
|
st.info("Nenhum canal cadastrado para configurar.")
|
|
4465
4582
|
for channel in channels:
|
|
@@ -7409,6 +7526,7 @@ def render_pipeline():
|
|
|
7409
7526
|
def main():
|
|
7410
7527
|
pipeline_video_items = [
|
|
7411
7528
|
("Criação de Vídeos", ":material/add_circle:", "Criação de Vídeos"),
|
|
7529
|
+
("Criação de Shorts", ":material/phone_android:", "Criação de Shorts"),
|
|
7412
7530
|
("Backlog Vídeos", ":material/video_library:", "Backlog Vídeos"),
|
|
7413
7531
|
("Roteiros", ":material/article:", "Roteiros"),
|
|
7414
7532
|
("Thumbnails", ":material/image:", "Thumbnails"),
|
|
@@ -7416,6 +7534,7 @@ def main():
|
|
|
7416
7534
|
]
|
|
7417
7535
|
channel_profile_items = [
|
|
7418
7536
|
("Canais YouTube", ":material/ondemand_video:", "Canais YouTube"),
|
|
7537
|
+
("Canais Tiktok", ":material/music_video:", "Canais Tiktok"),
|
|
7419
7538
|
("Blueprints Youtube", ":material/library_books:", "Blueprints Youtube"),
|
|
7420
7539
|
("Thumbnail Blueprints", ":material/image:", "Thumbnail Blueprints"),
|
|
7421
7540
|
("Brandings Youtube", ":material/brush:", "Brandings Youtube"),
|
|
@@ -7463,6 +7582,7 @@ def main():
|
|
|
7463
7582
|
]
|
|
7464
7583
|
automation_items = [
|
|
7465
7584
|
("Automação Youtube", ":material/schedule:", "Automação Youtube"),
|
|
7585
|
+
("Automação Tiktok", ":material/schedule:", "Automação Tiktok"),
|
|
7466
7586
|
]
|
|
7467
7587
|
edition_items = [
|
|
7468
7588
|
("Limpador de Metadados", ":material/edit_note:", "Limpador de Metadados"),
|
|
@@ -7496,11 +7616,11 @@ def main():
|
|
|
7496
7616
|
"Configurações": settings_items,
|
|
7497
7617
|
}
|
|
7498
7618
|
nav_paths = {
|
|
7499
|
-
"Início": "/inicio", "Automação": "/automacao", "Automação Youtube": "/automacao/youtube",
|
|
7619
|
+
"Início": "/inicio", "Automação": "/automacao", "Automação Youtube": "/automacao/youtube", "Automação Tiktok": "/automacao/tiktok",
|
|
7500
7620
|
"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",
|
|
7621
|
+
"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
7622
|
"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",
|
|
7623
|
+
"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
7624
|
"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
7625
|
"Edição": "/edicao", "Limpador de Metadados": "/edicao/limpador-metadados", "Cortes": "/edicao/cortes", "Editor Python": "/edicao/editor-python", "Download Mídia": "/edicao/download-midia",
|
|
7506
7626
|
"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 +7696,7 @@ def main():
|
|
|
7576
7696
|
"Início": render_dashboard,
|
|
7577
7697
|
"Pipeline Vídeos": render_pipeline,
|
|
7578
7698
|
"Criação de Vídeos": render_new_video,
|
|
7699
|
+
"Criação de Shorts": lambda: render_new_video("Criação de Shorts", "new_shorts", channel_platform="tiktok", fixed_aspect_ratio="Portrait 9:16"),
|
|
7579
7700
|
"Backlog Vídeos": render_videos,
|
|
7580
7701
|
"Criação de Músicas": render_music_creation,
|
|
7581
7702
|
"Music Backlog": render_music_backlog,
|
|
@@ -7589,9 +7710,11 @@ def main():
|
|
|
7589
7710
|
"Brandings Youtube": render_youtube_brandings,
|
|
7590
7711
|
"Prompt Masters": render_tiktok_prompt_masters,
|
|
7591
7712
|
"Canais YouTube": render_channels,
|
|
7713
|
+
"Canais Tiktok": render_tiktok_channels,
|
|
7592
7714
|
"Contas TikTok": render_tiktok_accounts,
|
|
7593
7715
|
"Facebook Pages": lambda: render_edit_placeholder("Facebook Pages", ""),
|
|
7594
7716
|
"Automação Youtube": render_automation,
|
|
7717
|
+
"Automação Tiktok": render_tiktok_automation,
|
|
7595
7718
|
"Niche Finder Kaggle": render_niche_finder,
|
|
7596
7719
|
"Tutorial Kaggle": lambda: render_niche_tutorial("kaggle"),
|
|
7597
7720
|
"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