@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 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
- settings["video_aspect_ratio"] = st.selectbox("Video Aspect Ratio", ["Portrait 9:16", "Landscape 16:9", "Square 1:1"], key=f"{prefix}_video_aspect_ratio")
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 == "Criação de Vídeos" else [])
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
- mode_label = st.radio(
2591
- "Modo de criação",
2592
- ["Canal específico", "Lote no mesmo canal", "Lote geral"],
2593
- horizontal=True,
2594
- key=f"{prefix}_mode",
2595
- )
2596
- mode = {"Canal específico": "single", "Lote no mesmo canal": "same_channel", "Lote geral": "general"}[mode_label]
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 "",
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.42",
3
+ "version": "0.4.44",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",