@danhachuel/thunderbolt 0.3.98 → 0.3.100
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/influencers_ui.py +32 -17
- package/app/main.py +31 -1
- package/hermes_ui/api_key_tests.py +17 -0
- package/package.json +1 -1
package/app/influencers_ui.py
CHANGED
|
@@ -6,6 +6,7 @@ import base64
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import io
|
|
8
8
|
import json
|
|
9
|
+
import mimetypes
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any, Mapping
|
|
11
12
|
|
|
@@ -294,24 +295,38 @@ def _render_content_history(repository: Any, influencer_id: str = "") -> None:
|
|
|
294
295
|
if not records:
|
|
295
296
|
return
|
|
296
297
|
st.subheader("Conteúdos gerados")
|
|
297
|
-
rows = []
|
|
298
298
|
for item in records:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
299
|
+
content_id = str(item.get("id") or "content")
|
|
300
|
+
content_type = str(item.get("content_type") or "").strip().lower()
|
|
301
|
+
artifact = Path(str(item.get("artifact_path") or ""))
|
|
302
|
+
state = str(item.get("state") or "")
|
|
303
|
+
type_label = "Imagem" if content_type == "image" else "Vídeo" if content_type == "video" else "Conteúdo"
|
|
304
|
+
with st.container(border=True):
|
|
305
|
+
preview_col, detail_col = st.columns([1.25, 2.75])
|
|
306
|
+
with preview_col:
|
|
307
|
+
if artifact.is_file() and content_type == "image":
|
|
308
|
+
st.image(str(artifact), caption=f"{type_label} gerada", use_container_width=True)
|
|
309
|
+
elif artifact.is_file() and content_type == "video":
|
|
310
|
+
st.video(str(artifact))
|
|
311
|
+
else:
|
|
312
|
+
st.caption("Artefacto indisponível")
|
|
313
|
+
with detail_col:
|
|
314
|
+
st.write(f"**{type_label} · {CONTENT_STATES.get(state, state or '—')}**")
|
|
315
|
+
st.caption(f"Provider: {item.get('provider') or '—'} · {item.get('model') or '—'}")
|
|
316
|
+
st.caption(f"Plataforma: {item.get('platform') or '—'} · Criado: {item.get('created_at') or '—'}")
|
|
317
|
+
if item.get("error"):
|
|
318
|
+
st.error(str(item.get("error") or "")[:700])
|
|
319
|
+
if artifact.is_file() and state == "completed" and content_type in {"image", "video"}:
|
|
320
|
+
fallback_mime = "image/png" if content_type == "image" else "video/mp4"
|
|
321
|
+
mime = mimetypes.guess_type(artifact.name)[0] or fallback_mime
|
|
322
|
+
st.download_button(
|
|
323
|
+
f"Descarregar {type_label.casefold()}",
|
|
324
|
+
data=artifact.read_bytes(),
|
|
325
|
+
file_name=artifact.name,
|
|
326
|
+
mime=mime,
|
|
327
|
+
key=f"influencer_content_download_{content_type}_{content_id}",
|
|
328
|
+
use_container_width=True,
|
|
329
|
+
)
|
|
315
330
|
|
|
316
331
|
|
|
317
332
|
def _store_uploaded_file(uploaded: Any, folder: str) -> Path:
|
package/app/main.py
CHANGED
|
@@ -2726,6 +2726,11 @@ def render_music_creation():
|
|
|
2726
2726
|
st.error(str(exc))
|
|
2727
2727
|
|
|
2728
2728
|
|
|
2729
|
+
def render_custom_music_voices() -> None:
|
|
2730
|
+
"""Reserved view for future reusable custom music-voice blueprints."""
|
|
2731
|
+
st.title("Vozes Personalizadas, Funcionará como um Blueprint")
|
|
2732
|
+
|
|
2733
|
+
|
|
2729
2734
|
def render_scripts():
|
|
2730
2735
|
st.title("Roteiros")
|
|
2731
2736
|
st.caption("Produza e guarde roteiros de vídeos ou letras de músicas a partir dos Blueprints do Thunderbolt.")
|
|
@@ -3844,7 +3849,7 @@ def render_music_backlog() -> None:
|
|
|
3844
3849
|
music_file = Path(music_path)
|
|
3845
3850
|
st.success("Música pronta; pode continuar para o destino configurado.")
|
|
3846
3851
|
st.download_button(
|
|
3847
|
-
"Descarregar música
|
|
3852
|
+
"Descarregar música",
|
|
3848
3853
|
data=music_file.read_bytes(),
|
|
3849
3854
|
file_name=music_file.name,
|
|
3850
3855
|
mime="audio/mpeg",
|
|
@@ -3921,6 +3926,14 @@ def render_thumbnails():
|
|
|
3921
3926
|
image_path = record.get("image_path")
|
|
3922
3927
|
if image_path and image_path.is_file():
|
|
3923
3928
|
st.image(str(image_path), use_container_width=True)
|
|
3929
|
+
st.download_button(
|
|
3930
|
+
"Descarregar thumbnail",
|
|
3931
|
+
data=image_path.read_bytes(),
|
|
3932
|
+
file_name=image_path.name,
|
|
3933
|
+
mime="image/jpeg" if image_path.suffix.lower() in {".jpg", ".jpeg"} else "image/png",
|
|
3934
|
+
key=f"thumbnail_download_{task_id}_{record['variant_index']}",
|
|
3935
|
+
use_container_width=True,
|
|
3936
|
+
)
|
|
3924
3937
|
else:
|
|
3925
3938
|
st.markdown("### Sem imagem")
|
|
3926
3939
|
st.caption("Imagem ainda não gerada")
|
|
@@ -6242,6 +6255,21 @@ def render_settings():
|
|
|
6242
6255
|
saved_lyria_model = str(settings.get("lyria_model") or lyria_models[0])
|
|
6243
6256
|
lyria_model = st.selectbox("Modelo Google Lyria", lyria_models, index=lyria_models.index(saved_lyria_model) if saved_lyria_model in lyria_models else 0, key="settings_lyria_model")
|
|
6244
6257
|
|
|
6258
|
+
def save_google_lyria() -> None:
|
|
6259
|
+
settings.update({"lyria_api_key": lyria_api_key.strip(), "lyria_model": lyria_model})
|
|
6260
|
+
write_json("settings.json", settings)
|
|
6261
|
+
|
|
6262
|
+
if st.form_submit_button("Guardar Google Lyria", type="primary", use_container_width=True, key="save_google_lyria"):
|
|
6263
|
+
save_google_lyria()
|
|
6264
|
+
st.success("Google Lyria guardado.")
|
|
6265
|
+
_render_api_test_control(
|
|
6266
|
+
settings,
|
|
6267
|
+
"voice:google_lyria",
|
|
6268
|
+
lambda: test_voice_provider("google_lyria", {"lyria_api_key": lyria_api_key, "lyria_model": lyria_model}),
|
|
6269
|
+
widget_key="api_test_voice_google_lyria",
|
|
6270
|
+
persist_callback=save_google_lyria,
|
|
6271
|
+
)
|
|
6272
|
+
|
|
6245
6273
|
with st.expander("Publicação através do Upload-Post", expanded=False):
|
|
6246
6274
|
upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
|
|
6247
6275
|
upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
|
|
@@ -6960,6 +6988,7 @@ def main():
|
|
|
6960
6988
|
music_items = [
|
|
6961
6989
|
("Criação de Músicas", ":material/music_note:", "Criação de Músicas"),
|
|
6962
6990
|
("Music Backlog", ":material/queue_music:", "Music Backlog"),
|
|
6991
|
+
("Vozes Personalizadas, Funcionará como um Blueprint", ":material/record_voice_over:", "Vozes Personalizadas, Funcionará como um Blueprint"),
|
|
6963
6992
|
("Upload Música", ":material/library_music:", "Upload Música"),
|
|
6964
6993
|
]
|
|
6965
6994
|
models_ai_items = [
|
|
@@ -7083,6 +7112,7 @@ def main():
|
|
|
7083
7112
|
"Backlog Vídeos": render_videos,
|
|
7084
7113
|
"Criação de Músicas": render_music_creation,
|
|
7085
7114
|
"Music Backlog": render_music_backlog,
|
|
7115
|
+
"Vozes Personalizadas, Funcionará como um Blueprint": render_custom_music_voices,
|
|
7086
7116
|
"Upload Música": render_music_upload,
|
|
7087
7117
|
"Roteiros": render_scripts,
|
|
7088
7118
|
"Thumbnails": render_thumbnails,
|
|
@@ -128,6 +128,20 @@ def test_nano_banana_credentials(api_key: str, model: str) -> dict[str, Any]:
|
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def test_google_lyria_credentials(api_key: str, model: str) -> dict[str, Any]:
|
|
132
|
+
"""Validate access to the selected Lyria model without requesting audio generation."""
|
|
133
|
+
api_key = str(api_key or "").strip()
|
|
134
|
+
model = str(model or "").strip()
|
|
135
|
+
if not api_key:
|
|
136
|
+
return _missing("Introduza a Google Lyria API key antes de testar.")
|
|
137
|
+
if not model:
|
|
138
|
+
return _result("missing", "Seleccione o modelo Google Lyria antes de testar.")
|
|
139
|
+
return _get(
|
|
140
|
+
f"https://generativelanguage.googleapis.com/v1beta/models/{quote(model.removeprefix('models/'), safe='')}",
|
|
141
|
+
headers={"x-goog-api-key": api_key},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
131
145
|
def test_media_provider_card(card: Mapping[str, Any]) -> dict[str, Any]:
|
|
132
146
|
"""Run a bounded, non-generative check for an image/video provider card."""
|
|
133
147
|
source = dict(card) if isinstance(card, Mapping) else {}
|
|
@@ -318,6 +332,8 @@ def test_voice_provider(provider: str, settings: dict[str, Any]) -> dict[str, An
|
|
|
318
332
|
return test_openai_compatible_voice_credentials("Sonilo", settings.get("sonilo_api_key", ""), settings.get("sonilo_base_url", ""))
|
|
319
333
|
if provider == "suno":
|
|
320
334
|
return test_suno_credentials(settings.get("suno_api_key", ""), settings.get("suno_api_base_url", ""), settings.get("suno_api_endpoint", ""))
|
|
335
|
+
if provider == "google_lyria":
|
|
336
|
+
return test_google_lyria_credentials(settings.get("lyria_api_key", ""), settings.get("lyria_model", ""))
|
|
321
337
|
return _unsupported("Este provider de voz não tem diagnóstico remoto configurado.")
|
|
322
338
|
|
|
323
339
|
|
|
@@ -325,6 +341,7 @@ __all__ = [
|
|
|
325
341
|
"test_apify_credentials",
|
|
326
342
|
"test_azure_speech_credentials",
|
|
327
343
|
"test_elevenlabs_credentials",
|
|
344
|
+
"test_google_lyria_credentials",
|
|
328
345
|
"test_innertube_api_key",
|
|
329
346
|
"test_kaggle_credentials",
|
|
330
347
|
"test_influencer_database",
|
package/package.json
CHANGED