@danhachuel/thunderbolt 0.6.70 → 0.6.72
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 +160 -59
- package/hermes_ui/music.py +1 -1
- package/hermes_ui/pipeline_worker.py +1 -1
- package/hermes_ui/python_editor.py +1 -1
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -1168,7 +1168,7 @@ def render_video_generation_settings(
|
|
|
1168
1168
|
st.caption("Carregue uma narração pronta; o ficheiro será usado directamente pelo MoneyPrinterTurbo.")
|
|
1169
1169
|
uploaded_voiceover = st.file_uploader(
|
|
1170
1170
|
"Ficheiro de narração",
|
|
1171
|
-
type=["mp3", "wav", "m4a", "aac", "flac", "ogg"],
|
|
1171
|
+
type=["mp3", "mpeg", "wav", "m4a", "aac", "flac", "ogg"],
|
|
1172
1172
|
key=f"{prefix}_voiceover_upload",
|
|
1173
1173
|
)
|
|
1174
1174
|
if uploaded_voiceover is not None and st.button("Guardar áudio de narração", key=f"{prefix}_voiceover_store", use_container_width=True):
|
|
@@ -3522,7 +3522,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3522
3522
|
else:
|
|
3523
3523
|
st.warning("Ainda não existem músicas em storage/music. Escolha Carregar ficheiro ou Criar via Suno API.")
|
|
3524
3524
|
elif music_source == "Carregar ficheiro":
|
|
3525
|
-
uploaded_music = st.file_uploader("Carregar música", type=["mp3", "wav", "m4a", "aac", "flac", "ogg"], key=f"{prefix}_music_upload")
|
|
3525
|
+
uploaded_music = st.file_uploader("Carregar música", type=["mp3", "mpeg", "wav", "m4a", "aac", "flac", "ogg"], key=f"{prefix}_music_upload")
|
|
3526
3526
|
if uploaded_music and st.button("Guardar música local", key=f"{prefix}_music_store", use_container_width=True):
|
|
3527
3527
|
try:
|
|
3528
3528
|
stored_music = store_music_file(uploaded_music.name, uploaded_music.getvalue())
|
|
@@ -5105,73 +5105,174 @@ def render_videos():
|
|
|
5105
5105
|
st.rerun()
|
|
5106
5106
|
|
|
5107
5107
|
|
|
5108
|
-
def
|
|
5109
|
-
"""
|
|
5110
|
-
st.subheader("Music Backlog")
|
|
5111
|
-
st.caption("Fila independente de geração de áudio por Suno AI ou Google Lyria. Não inclui tarefas, worker ou progresso de vídeo.")
|
|
5112
|
-
st.caption(f"As músicas são guardadas em `{STORAGE / 'music'}`.")
|
|
5108
|
+
def _music_backlog_records() -> list[dict[str, Any]]:
|
|
5109
|
+
"""Combine generated music tasks with audio files imported into storage/music."""
|
|
5113
5110
|
tasks = list_music_tasks()
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
st.info(f"Geração musical em execução · {len(active)} tarefa(s) de áudio.")
|
|
5117
|
-
if not tasks:
|
|
5118
|
-
st.info("Nenhuma música criada.")
|
|
5119
|
-
return
|
|
5120
|
-
known_states = ["to_do", "doing", "blocked", "done", "failed", "cancelled"]
|
|
5121
|
-
extra_states = sorted({str(task.get("state") or "unknown") for task in tasks if str(task.get("state") or "unknown") not in known_states})
|
|
5122
|
-
state_filter = st.selectbox("Filtrar por estado", ["Todos", *known_states, *extra_states], key="music_backlog_state_filter")
|
|
5111
|
+
records: list[dict[str, Any]] = []
|
|
5112
|
+
registered_paths: set[str] = set()
|
|
5123
5113
|
for task in tasks:
|
|
5124
|
-
|
|
5114
|
+
record = dict(task)
|
|
5115
|
+
record["source_type"] = "created"
|
|
5116
|
+
audio_path = Path(str(record.get("audio_path") or "")) if record.get("audio_path") else None
|
|
5117
|
+
if audio_path:
|
|
5118
|
+
registered_paths.add(str(audio_path.resolve()))
|
|
5119
|
+
records.append(record)
|
|
5120
|
+
for music_file in list_music_files():
|
|
5121
|
+
if str(music_file.resolve()) in registered_paths:
|
|
5125
5122
|
continue
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5123
|
+
records.append({
|
|
5124
|
+
"id": f"imported_{hashlib.sha1(str(music_file.resolve()).encode('utf-8')).hexdigest()[:12]}",
|
|
5125
|
+
"title": get_display_name("music", music_file, music_file.stem),
|
|
5126
|
+
"provider": "imported",
|
|
5127
|
+
"stage": "imported",
|
|
5128
|
+
"state": "done",
|
|
5129
|
+
"progress": 100,
|
|
5130
|
+
"audio_path": str(music_file),
|
|
5131
|
+
"source_type": "imported",
|
|
5132
|
+
"created_at": datetime.fromtimestamp(music_file.stat().st_mtime, timezone.utc).isoformat(timespec="seconds"),
|
|
5133
|
+
})
|
|
5134
|
+
return records
|
|
5135
|
+
|
|
5136
|
+
|
|
5137
|
+
def _music_lyrics_records() -> list[dict[str, Any]]:
|
|
5138
|
+
"""Return persisted music-lyrics documents that still exist on disk."""
|
|
5139
|
+
records: list[dict[str, Any]] = []
|
|
5140
|
+
for document in list_script_documents():
|
|
5141
|
+
if str(document.get("document_type") or "").strip() != "music_lyrics":
|
|
5142
|
+
continue
|
|
5143
|
+
path = Path(str(document.get("path") or ""))
|
|
5144
|
+
if path.is_file():
|
|
5145
|
+
records.append({**document, "path": str(path), "title": get_display_name("music_lyrics", path, str(document.get("title") or path.stem))})
|
|
5146
|
+
return records
|
|
5147
|
+
|
|
5148
|
+
|
|
5149
|
+
def _render_music_file_card(record: dict[str, Any]) -> None:
|
|
5150
|
+
music_path = Path(str(record.get("audio_path") or ""))
|
|
5151
|
+
record_id = str(record.get("id") or music_path.name)
|
|
5152
|
+
title = get_display_name("music", music_path, str(record.get("title") or music_path.stem)) if music_path else str(record.get("title") or "Música sem título")
|
|
5153
|
+
with st.container(border=True):
|
|
5154
|
+
cols = st.columns([2.2, 1, 1, 1.2, 1.8])
|
|
5155
|
+
with cols[0]:
|
|
5156
|
+
st.write(f"**{title}**")
|
|
5157
|
+
source_type = "Música criada" if str(record.get("source_type") or "") == "created" else "Música importada"
|
|
5158
|
+
st.caption(source_type)
|
|
5159
|
+
if str(record.get("provider") or "").casefold() not in {"", "imported"}:
|
|
5160
|
+
provider = "Google Lyria" if str(record.get("provider") or "").casefold() == "lyria" else "Suno AI"
|
|
5161
|
+
st.caption(f"Provider: {provider}")
|
|
5162
|
+
st.caption(record_id)
|
|
5163
|
+
if music_path.is_file():
|
|
5164
|
+
st.download_button(
|
|
5165
|
+
"Descarregar música",
|
|
5166
|
+
data=music_path.read_bytes(),
|
|
5167
|
+
file_name=music_path.name,
|
|
5168
|
+
mime=mimetypes.guess_type(music_path.name)[0] or "audio/mpeg",
|
|
5169
|
+
key=f"music_backlog_download_{record_id}",
|
|
5170
|
+
use_container_width=True,
|
|
5171
|
+
)
|
|
5172
|
+
edit_key = _render_library_name_editor("music", music_path, title)
|
|
5173
|
+
with st.columns([0.2, 0.8])[0]:
|
|
5174
|
+
_render_card_pencil(edit_key)
|
|
5175
|
+
else:
|
|
5176
|
+
st.caption(f"Música registada: {music_path}")
|
|
5177
|
+
with cols[1]:
|
|
5178
|
+
st.caption("Tipo")
|
|
5179
|
+
st.write("Áudio")
|
|
5180
|
+
with cols[2]:
|
|
5181
|
+
stage = str(record.get("stage") or "")
|
|
5182
|
+
st.write({"music_generation": "Geração musical", "completed": "Concluída", "failed": "Falha", "imported": "Importada"}.get(stage, "Na fila"))
|
|
5183
|
+
with cols[3]:
|
|
5184
|
+
state = str(record.get("state") or "unknown").strip().lower()
|
|
5185
|
+
progress = _video_task_progress(record)
|
|
5186
|
+
st.caption("Estado")
|
|
5187
|
+
st.write(state or "—")
|
|
5188
|
+
st.caption(VIDEO_TASK_STATE_LABELS.get(state, state.replace("_", " ").capitalize() or "Desconhecido"))
|
|
5189
|
+
st.progress(progress, text=f"{progress}%")
|
|
5190
|
+
if record.get("error"):
|
|
5191
|
+
st.error(str(record.get("error") or "")[:500])
|
|
5192
|
+
with cols[4]:
|
|
5193
|
+
if str(record.get("source_type") or "") == "created":
|
|
5194
|
+
state = str(record.get("state") or "")
|
|
5164
5195
|
start_col, stop_col = st.columns(2)
|
|
5165
5196
|
with start_col:
|
|
5166
|
-
if st.button("Start", key=f"music_backlog_start_{
|
|
5167
|
-
run_music_task(
|
|
5197
|
+
if st.button("Start", key=f"music_backlog_start_{record_id}", use_container_width=True, disabled=state not in {"to_do", "blocked", "failed"}):
|
|
5198
|
+
run_music_task(record_id, read_json("settings.json", {}))
|
|
5168
5199
|
st.rerun()
|
|
5169
5200
|
with stop_col:
|
|
5170
|
-
if st.button("Stop", key=f"music_backlog_stop_{
|
|
5171
|
-
transition_music_task(
|
|
5201
|
+
if st.button("Stop", key=f"music_backlog_stop_{record_id}", use_container_width=True, disabled=state != "doing"):
|
|
5202
|
+
transition_music_task(record_id, "blocked")
|
|
5172
5203
|
st.rerun()
|
|
5173
5204
|
|
|
5174
5205
|
|
|
5206
|
+
def _render_lyrics_card(record: dict[str, Any]) -> None:
|
|
5207
|
+
path = Path(str(record.get("path") or ""))
|
|
5208
|
+
if not path.is_file():
|
|
5209
|
+
return
|
|
5210
|
+
record_id = str(record.get("id") or hashlib.sha1(str(path.resolve()).encode("utf-8")).hexdigest()[:12])
|
|
5211
|
+
title = str(record.get("title") or path.stem)
|
|
5212
|
+
content = read_script_document(record)
|
|
5213
|
+
with st.container(border=True):
|
|
5214
|
+
title_col, download_col, edit_col = st.columns([3.2, 1.3, 0.6])
|
|
5215
|
+
with title_col:
|
|
5216
|
+
st.write(f"**{title}**")
|
|
5217
|
+
st.caption("Lyrics")
|
|
5218
|
+
with download_col:
|
|
5219
|
+
st.download_button("Descarregar lyrics", data=path.read_bytes(), file_name=path.name, mime="text/markdown", key=f"music_lyrics_download_{record_id}", use_container_width=True)
|
|
5220
|
+
with edit_col:
|
|
5221
|
+
edit_key = _render_library_name_editor("music_lyrics", path, title)
|
|
5222
|
+
_render_card_pencil(edit_key)
|
|
5223
|
+
with st.expander("Ver lyrics", expanded=False):
|
|
5224
|
+
st.code(content or path.read_text(encoding="utf-8"), language="markdown")
|
|
5225
|
+
|
|
5226
|
+
|
|
5227
|
+
def render_music_backlog() -> None:
|
|
5228
|
+
"""List generated and imported music, plus locally stored lyrics."""
|
|
5229
|
+
st.subheader("Music Backlog")
|
|
5230
|
+
st.caption("Fila independente de geração e importação de áudio por Suno AI ou Google Lyria. Não inclui tarefas, worker ou progresso de vídeo.")
|
|
5231
|
+
st.caption(f"As músicas são guardadas em `{STORAGE / 'music'}`.")
|
|
5232
|
+
music_tab, lyrics_tab = st.tabs(["Músicas", "Lyrics"])
|
|
5233
|
+
with music_tab:
|
|
5234
|
+
with st.form("music_backlog_upload_form", clear_on_submit=True):
|
|
5235
|
+
uploaded_music = st.file_uploader("Adicionar músicas à pasta acima", type=["mp3", "mpeg", "wav", "m4a", "aac", "flac", "ogg"], accept_multiple_files=True, key="music_backlog_upload")
|
|
5236
|
+
upload_music = st.form_submit_button("Guardar músicas", type="primary", use_container_width=True)
|
|
5237
|
+
if upload_music and uploaded_music:
|
|
5238
|
+
for uploaded in uploaded_music:
|
|
5239
|
+
store_music_file(uploaded.name, uploaded.getvalue())
|
|
5240
|
+
st.success(f"{len(uploaded_music)} música(s) guardada(s) em `{STORAGE / 'music'}`.")
|
|
5241
|
+
st.rerun()
|
|
5242
|
+
records = _music_backlog_records()
|
|
5243
|
+
active = [record for record in records if str(record.get("state") or "") == "doing"]
|
|
5244
|
+
if active:
|
|
5245
|
+
st.info(f"Geração musical em execução · {len(active)} tarefa(s) de áudio.")
|
|
5246
|
+
if not records:
|
|
5247
|
+
st.info("Nenhuma música criada ou importada.")
|
|
5248
|
+
else:
|
|
5249
|
+
known_states = ["to_do", "doing", "blocked", "done", "failed", "cancelled"]
|
|
5250
|
+
extra_states = sorted({str(record.get("state") or "unknown") for record in records if str(record.get("state") or "unknown") not in known_states})
|
|
5251
|
+
state_filter = st.selectbox("Filtrar por estado", ["Todos", *known_states, *extra_states], key="music_backlog_state_filter")
|
|
5252
|
+
for record in records:
|
|
5253
|
+
if state_filter != "Todos" and record.get("state") != state_filter:
|
|
5254
|
+
continue
|
|
5255
|
+
_render_music_file_card(record)
|
|
5256
|
+
with lyrics_tab:
|
|
5257
|
+
st.caption(f"As lyrics são guardadas em `{script_storage_path()}` no formato Markdown local do Thunderbolt.")
|
|
5258
|
+
with st.form("music_backlog_lyrics_upload_form", clear_on_submit=True):
|
|
5259
|
+
uploaded_lyrics = st.file_uploader("Adicionar lyrics", type=["md", "txt"], key="music_backlog_lyrics_upload")
|
|
5260
|
+
upload_lyrics = st.form_submit_button("Guardar lyrics", use_container_width=True)
|
|
5261
|
+
if upload_lyrics and uploaded_lyrics:
|
|
5262
|
+
content = uploaded_lyrics.getvalue().decode("utf-8", errors="replace").strip()
|
|
5263
|
+
if content:
|
|
5264
|
+
save_script_document({"title": Path(uploaded_lyrics.name).stem, "document_type": "music_lyrics", "content": content})
|
|
5265
|
+
st.success("Lyrics guardadas.")
|
|
5266
|
+
st.rerun()
|
|
5267
|
+
else:
|
|
5268
|
+
st.warning("O ficheiro de lyrics está vazio.")
|
|
5269
|
+
lyrics = _music_lyrics_records()
|
|
5270
|
+
if not lyrics:
|
|
5271
|
+
st.info("Nenhuma lyrics guardada.")
|
|
5272
|
+
for record in lyrics:
|
|
5273
|
+
_render_lyrics_card(record)
|
|
5274
|
+
|
|
5275
|
+
|
|
5175
5276
|
def _thumbnail_editor_context(record: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
5176
5277
|
"""Resolve the persisted task channel/Blueprint without requiring either one to remain registered."""
|
|
5177
5278
|
channel_id = str(record.get("channel_id") or "").strip()
|
package/hermes_ui/music.py
CHANGED
|
@@ -13,7 +13,7 @@ import requests
|
|
|
13
13
|
from . import storage
|
|
14
14
|
from .notifications import record_notification
|
|
15
15
|
|
|
16
|
-
MUSIC_EXTENSIONS = {".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg"}
|
|
16
|
+
MUSIC_EXTENSIONS = {".mp3", ".mpeg", ".wav", ".m4a", ".aac", ".flac", ".ogg"}
|
|
17
17
|
VOICEOVER_EXTENSIONS = set(MUSIC_EXTENSIONS)
|
|
18
18
|
|
|
19
19
|
|
|
@@ -775,7 +775,7 @@ def _mpt_percent(value: Any, fallback: float = 1.0) -> str:
|
|
|
775
775
|
|
|
776
776
|
def _valid_audio_artifact(value: Any) -> Path | None:
|
|
777
777
|
path = _valid_artifact_path(value)
|
|
778
|
-
if path is None or path.suffix.lower() not in {
|
|
778
|
+
if path is None or path.suffix.lower() not in {'.mp3', '.mpeg', '.wav', '.m4a', '.aac', '.flac', '.ogg'}:
|
|
779
779
|
return None
|
|
780
780
|
return path
|
|
781
781
|
|
|
@@ -16,7 +16,7 @@ from .metadata_cleaner import VIDEO_EXTENSIONS as METADATA_VIDEO_EXTENSIONS
|
|
|
16
16
|
from .metadata_cleaner import _resolve_ffmpeg
|
|
17
17
|
|
|
18
18
|
VIDEO_EXTENSIONS = {f".{extension.lower().lstrip('.')}" for extension in METADATA_VIDEO_EXTENSIONS}
|
|
19
|
-
AUDIO_EXTENSIONS = {".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg"}
|
|
19
|
+
AUDIO_EXTENSIONS = {".mp3", ".mpeg", ".wav", ".m4a", ".aac", ".flac", ".ogg"}
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class PythonEditorError(RuntimeError):
|
package/package.json
CHANGED