@danhachuel/thunderbolt 0.7.53 → 0.7.55

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.
Files changed (2) hide show
  1. package/app/main.py +146 -40
  2. package/package.json +1 -1
package/app/main.py CHANGED
@@ -1572,6 +1572,84 @@ def render_channel_audio_subtitle_defaults(channel: dict) -> None:
1572
1572
  st.rerun()
1573
1573
 
1574
1574
 
1575
+ def render_channel_video_defaults(channel: dict) -> None:
1576
+ """Render and persist the video defaults used by this channel."""
1577
+ channel_id = str(channel.get("id") or "")
1578
+ source = channel_video_source_value(channel.get("default_video_source") or channel.get("style_wide"))
1579
+ aspect_ratio = str(channel.get("default_video_aspect_ratio") or channel.get("video_aspect_ratio") or "Landscape 16:9")
1580
+ encoder = str(channel.get("default_video_encoder") or VIDEO_ENCODER_OPTIONS[0])
1581
+ maximum_clip_duration = int(channel.get("default_maximum_clip_duration") or 10)
1582
+ videos_per_run = int(channel.get("default_videos_per_run") or 1)
1583
+ concatenation_mode = str(channel.get("default_video_concatenation_mode") or VIDEO_CONCATENATION_OPTIONS[0])
1584
+ transition_mode = str(channel.get("default_video_transition_mode") or VIDEO_TRANSITION_OPTIONS[0])
1585
+ with st.expander("Configurações de vídeo", expanded=False):
1586
+ video_cols = st.columns(2)
1587
+ with video_cols[0]:
1588
+ selected_source = st.selectbox(
1589
+ "Fonte do vídeo",
1590
+ WIDE_STYLE_OPTIONS,
1591
+ index=WIDE_STYLE_OPTIONS.index(source) if source in WIDE_STYLE_OPTIONS else 0,
1592
+ key=f"channel_default_video_source_{channel_id}",
1593
+ )
1594
+ selected_aspect_ratio = st.selectbox(
1595
+ "Proporção do vídeo",
1596
+ CHANNEL_ASPECT_RATIO_OPTIONS,
1597
+ index=CHANNEL_ASPECT_RATIO_OPTIONS.index(aspect_ratio) if aspect_ratio in CHANNEL_ASPECT_RATIO_OPTIONS else 0,
1598
+ key=f"channel_default_video_aspect_ratio_{channel_id}",
1599
+ )
1600
+ selected_concatenation = st.selectbox(
1601
+ "Video Concatenation Mode",
1602
+ VIDEO_CONCATENATION_OPTIONS,
1603
+ index=VIDEO_CONCATENATION_OPTIONS.index(concatenation_mode) if concatenation_mode in VIDEO_CONCATENATION_OPTIONS else 0,
1604
+ key=f"channel_default_video_concatenation_{channel_id}",
1605
+ )
1606
+ selected_transition = st.selectbox(
1607
+ "Video Transition Mode",
1608
+ VIDEO_TRANSITION_OPTIONS,
1609
+ index=VIDEO_TRANSITION_OPTIONS.index(transition_mode) if transition_mode in VIDEO_TRANSITION_OPTIONS else 0,
1610
+ key=f"channel_default_video_transition_{channel_id}",
1611
+ )
1612
+ with video_cols[1]:
1613
+ selected_maximum_clip_duration = st.selectbox(
1614
+ "Maximum Clip Duration (seconds)",
1615
+ [3, 5, 8, 10, 15],
1616
+ index=[3, 5, 8, 10, 15].index(maximum_clip_duration) if maximum_clip_duration in [3, 5, 8, 10, 15] else 3,
1617
+ key=f"channel_default_maximum_clip_duration_{channel_id}",
1618
+ )
1619
+ selected_videos_per_run = st.selectbox(
1620
+ "Videos per Run",
1621
+ list(range(1, 11)),
1622
+ index=videos_per_run - 1 if 1 <= videos_per_run <= 10 else 0,
1623
+ key=f"channel_default_videos_per_run_{channel_id}",
1624
+ )
1625
+ selected_encoder = st.selectbox(
1626
+ "Video Encoder",
1627
+ VIDEO_ENCODER_OPTIONS,
1628
+ index=VIDEO_ENCODER_OPTIONS.index(encoder) if encoder in VIDEO_ENCODER_OPTIONS else 0,
1629
+ key=f"channel_default_video_encoder_{channel_id}",
1630
+ )
1631
+ match_visuals = st.checkbox(
1632
+ "Match Visuals to Script Order",
1633
+ value=bool(channel.get("default_match_visuals_to_script_order", False)),
1634
+ key=f"channel_default_match_visuals_{channel_id}",
1635
+ )
1636
+ if st.button("Guardar configurações de vídeo", type="primary", key=f"save_channel_video_defaults_{channel_id}", width="stretch"):
1637
+ update_channel(channel_id, {
1638
+ "default_video_source": channel_video_source_storage(selected_source),
1639
+ "default_video_aspect_ratio": selected_aspect_ratio,
1640
+ "default_video_concatenation_mode": selected_concatenation,
1641
+ "default_video_transition_mode": selected_transition,
1642
+ "default_maximum_clip_duration": int(selected_maximum_clip_duration),
1643
+ "default_videos_per_run": int(selected_videos_per_run),
1644
+ "default_video_encoder": selected_encoder,
1645
+ "default_match_visuals_to_script_order": bool(match_visuals),
1646
+ "style_wide": channel_video_source_storage(selected_source),
1647
+ "video_aspect_ratio": selected_aspect_ratio,
1648
+ })
1649
+ st.success("Default de vídeo guardado para este canal.")
1650
+ st.rerun()
1651
+
1652
+
1575
1653
  def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], youtube_account_labels: dict[str, str], youtube_accounts_by_id: dict[str, dict[str, Any]]) -> None:
1576
1654
  channel_id = str(channel["id"])
1577
1655
  blueprint_ids, blueprint_labels, current_blueprint, voice_options, current_voice = channel_default_options(channel)
@@ -3326,16 +3404,33 @@ def render_channels():
3326
3404
  channels = [channel for channel in read_json("channels.json", []) if is_youtube_channel_record(channel)]
3327
3405
  st.divider()
3328
3406
  st.subheader(f"Canais Youtube cadastrados ({len(channels)})")
3329
- youtube_channel_search = st.text_input(
3330
- "Pesquisar nomes dos canais YouTube",
3331
- key="youtube_registered_channels_search",
3332
- placeholder="Digite o nome do canal",
3333
- )
3407
+ filter_cols = st.columns([1, 1], gap="small")
3408
+ with filter_cols[0]:
3409
+ youtube_channel_search = st.text_input(
3410
+ "Pesquisar nomes dos canais YouTube",
3411
+ key="youtube_registered_channels_search",
3412
+ placeholder="Digite o nome do canal",
3413
+ )
3414
+ with filter_cols[1]:
3415
+ youtube_country_options = [""] + list(COUNTRY_OPTIONS)
3416
+ youtube_country_filter = st.selectbox(
3417
+ "Filtrar por País",
3418
+ youtube_country_options,
3419
+ format_func=lambda value: value or "Todos os países",
3420
+ key="youtube_registered_country_filter",
3421
+ )
3422
+ if st.button("Filtrar", type="primary", width="content", key="youtube_registered_country_filter_apply"):
3423
+ st.session_state["youtube_registered_country_filter_applied"] = youtube_country_filter
3424
+ applied_country = str(st.session_state.get("youtube_registered_country_filter_applied") or "").strip()
3334
3425
  visible_registered_channels = [
3335
3426
  channel for channel in channels
3336
3427
  if not youtube_channel_search.strip()
3337
3428
  or youtube_channel_search.strip().casefold() in str(channel.get("name") or "").casefold()
3338
3429
  ]
3430
+ visible_registered_channels = [
3431
+ channel for channel in visible_registered_channels
3432
+ if not applied_country or str(channel.get("country") or "").strip() == applied_country
3433
+ ]
3339
3434
  if not channels:
3340
3435
  st.info("Nenhum canal YouTube cadastrado.")
3341
3436
  return
@@ -3392,17 +3487,34 @@ def render_channels():
3392
3487
  delete_key = f"delete_pending_{channel_id}"
3393
3488
  with st.expander("Detalhes e configuração do canal", expanded=False):
3394
3489
  calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel)
3395
- average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"youtube_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
3396
- if st.button("Guardar tempo", key=f"save_youtube_channel_average_video_time_{channel_id}", width="stretch"):
3397
- if not valid_hhmm(average_video_time):
3398
- st.error("Use o formato MM:SS, por exemplo 12:00.")
3399
- else:
3400
- update_channel(channel_id, {"average_video_time": average_video_time.strip() or DEFAULT_AVERAGE_VIDEO_TIME, "average_video_word_count": words_from_channel_time(average_video_time)})
3401
- st.success("Tempo médio do canal YouTube guardado.")
3402
- st.rerun()
3490
+ with st.expander("Tempo Medio de Video", expanded=False):
3491
+ average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"youtube_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
3492
+ if st.button("Guardar Tempo Medio de Video", key=f"save_youtube_channel_average_video_time_{channel_id}", type="primary", width="stretch"):
3493
+ if not valid_hhmm(average_video_time):
3494
+ st.error("Use o formato MM:SS, por exemplo 12:00.")
3495
+ else:
3496
+ update_channel(channel_id, {"average_video_time": average_video_time.strip() or DEFAULT_AVERAGE_VIDEO_TIME, "average_video_word_count": words_from_channel_time(average_video_time)})
3497
+ st.success("Tempo médio do canal YouTube guardado.")
3498
+ st.rerun()
3499
+ if st.session_state.get(edit_key):
3500
+ render_channel_edit_form(channel, youtube_account_ids, youtube_account_labels, youtube_accounts_by_id)
3501
+ else:
3502
+ summary = channel_blueprint_summary(channel)
3503
+ render_channel_thumbnail_blueprint_panel(channel, compact=True)
3504
+ channel_language = language_label(channel.get("language") or "pt")
3505
+ block_cols = st.columns(4, gap="small")
3506
+ with block_cols[0]:
3507
+ st.markdown(f"**Blueprint Padrão**\n\n{summary['name']}")
3508
+ with block_cols[1]:
3509
+ st.markdown(f"**Nicho**\n\n{channel_niche_label(channel)}")
3510
+ with block_cols[2]:
3511
+ st.markdown(f"**Narrador/Voz Padrão**\n\n{summary['voice'] or 'Sem voz padrão'}")
3512
+ with block_cols[3]:
3513
+ st.markdown(f"**Idioma**\n\n{channel_language}")
3514
+
3403
3515
  action_cols = st.columns(2)
3404
3516
  with action_cols[0]:
3405
- if st.button("Editar", key=f"edit_channel_button_{channel_id}", width="stretch"):
3517
+ if st.button("Editar", key=f"edit_channel_button_{channel_id}", type="primary", width="stretch"):
3406
3518
  st.session_state[edit_key] = True
3407
3519
  st.rerun()
3408
3520
  with action_cols[1]:
@@ -3425,31 +3537,6 @@ def render_channels():
3425
3537
  if st.button("Cancelar", key=f"cancel_delete_{channel_id}"):
3426
3538
  st.session_state.pop(delete_key, None)
3427
3539
  st.rerun()
3428
- if st.session_state.get(edit_key):
3429
- render_channel_edit_form(channel, youtube_account_ids, youtube_account_labels, youtube_accounts_by_id)
3430
- else:
3431
- summary = channel_blueprint_summary(channel)
3432
- render_channel_thumbnail_blueprint_panel(channel, compact=True)
3433
- channel_language = language_label(channel.get("language") or "pt")
3434
- block_cols = st.columns(4, gap="small")
3435
- with block_cols[0]:
3436
- st.markdown(f"**Blueprint Padrão**\n\n{summary['name']}")
3437
- if st.button("Editar Blueprint", key=f"edit_prompts_{channel_id}", width="stretch"):
3438
- st.session_state[edit_key] = True
3439
- st.rerun()
3440
- with block_cols[1]:
3441
- st.markdown(f"**Nicho**\n\n{channel_niche_label(channel)}")
3442
- if st.button("Editar Nicho", key=f"edit_niche_{channel_id}", width="stretch"):
3443
- st.session_state[edit_key] = True
3444
- st.rerun()
3445
- with block_cols[2]:
3446
- st.markdown(f"**Narrador/Voz Padrão**\n\n{summary['voice'] or 'Sem voz padrão'}")
3447
- if st.button("Configurar Narrador/Voz", key=f"edit_voice_{channel_id}", width="stretch"):
3448
- st.session_state[edit_key] = True
3449
- st.rerun()
3450
- with block_cols[3]:
3451
- st.markdown(f"**Idioma**\n\n{channel_language}")
3452
-
3453
3540
  channel_account_ids = list(youtube_account_ids)
3454
3541
  current_channel_account_id = str(channel.get("google_account_id", ""))
3455
3542
  if current_channel_account_id and current_channel_account_id not in channel_account_ids:
@@ -3497,6 +3584,7 @@ def render_channels():
3497
3584
  st.success("Conta Google e DELEGATED_SESSION_ID individual do canal guardados.")
3498
3585
  st.rerun()
3499
3586
 
3587
+ render_channel_video_defaults(channel)
3500
3588
  render_channel_audio_subtitle_defaults(channel)
3501
3589
  render_channel_videos(channel)
3502
3590
 
@@ -6480,7 +6568,24 @@ def render_automation():
6480
6568
  if not channels:
6481
6569
  st.info("Nenhum canal cadastrado para configurar.")
6482
6570
  with st.expander("Canais cadastrados", expanded=False):
6483
- for channel in channels:
6571
+ automation_country_options = [""] + list(COUNTRY_OPTIONS)
6572
+ automation_filter_cols = st.columns([1, 1], gap="small")
6573
+ with automation_filter_cols[0]:
6574
+ automation_country_filter = st.selectbox(
6575
+ "Filtrar por País",
6576
+ automation_country_options,
6577
+ format_func=lambda value: value or "Todos os países",
6578
+ key="youtube_automation_country_filter",
6579
+ )
6580
+ with automation_filter_cols[1]:
6581
+ if st.button("Filtrar", type="primary", width="content", key="youtube_automation_country_filter_apply"):
6582
+ st.session_state["youtube_automation_country_filter_applied"] = automation_country_filter
6583
+ applied_automation_country = str(st.session_state.get("youtube_automation_country_filter_applied") or "").strip()
6584
+ filtered_automation_channels = [
6585
+ channel for channel in channels
6586
+ if not applied_automation_country or str(channel.get("country") or "").strip() == applied_automation_country
6587
+ ]
6588
+ for channel in filtered_automation_channels:
6484
6589
  channel_id = channel["id"]
6485
6590
  with st.container(border=True):
6486
6591
  blueprint_ids, blueprint_labels, current_blueprint, voice_options, current_voice = channel_default_options(channel)
@@ -6497,6 +6602,7 @@ def render_automation():
6497
6602
  with header_cols[1]:
6498
6603
  st.write(f"**{channel.get('name', 'Sem nome')}**")
6499
6604
  st.caption(channel.get("handle") or channel.get("url") or "sem URL")
6605
+ st.caption(f"País: {channel.get('country') or 'Não definido'}")
6500
6606
  channel_url = str(channel.get("url") or "").strip()
6501
6607
  if not channel_url:
6502
6608
  channel_id_or_handle = str(channel.get("youtube_channel_id") or channel.get("youtube_id") or channel.get("handle") or "").strip()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.53",
3
+ "version": "0.7.55",
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",