@danhachuel/thunderbolt 0.6.70 → 0.6.71
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 +158 -57
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -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", "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/package.json
CHANGED