@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.
Files changed (2) hide show
  1. package/app/main.py +158 -57
  2. 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 render_music_backlog() -> None:
5109
- """Render the independent audio-only queue; it never reads the video pipeline."""
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
- active = [task for task in tasks if str(task.get("state") or "") == "doing"]
5115
- if active:
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
- if state_filter != "Todos" and task.get("state") != state_filter:
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
- with st.container(border=True):
5127
- cols = st.columns([2.2, 1, 1, 1.2, 1.8])
5128
- with cols[0]:
5129
- st.write(f"**{task.get('title') or 'Música sem título'}**")
5130
- st.caption(f"Provider: {'Google Lyria' if str(task.get('provider') or '').casefold() == 'lyria' else 'Suno AI'}")
5131
- st.caption(str(task.get('id') or ''))
5132
- music_path = str(task.get("audio_path") or "").strip()
5133
- if music_path and Path(music_path).is_file():
5134
- music_file = Path(music_path)
5135
- st.success("Música pronta; pode continuar para o destino configurado.")
5136
- st.download_button(
5137
- "Descarregar música",
5138
- data=music_file.read_bytes(),
5139
- file_name=music_file.name,
5140
- mime="audio/mpeg",
5141
- key=f"music_backlog_download_{task['id']}",
5142
- use_container_width=True,
5143
- )
5144
- elif music_path:
5145
- st.caption(f"Música registada: {music_path}")
5146
- else:
5147
- st.caption("A música será disponibilizada quando a etapa de geração terminar.")
5148
- with cols[1]:
5149
- st.caption("Tipo")
5150
- st.write("Áudio")
5151
- with cols[2]:
5152
- st.write({"music_generation": "Geração musical", "completed": "Concluída", "failed": "Falha"}.get(str(task.get("stage") or ""), "Na fila"))
5153
- with cols[3]:
5154
- state = str(task.get("state") or "unknown").strip().lower()
5155
- progress = _video_task_progress(task)
5156
- st.caption("Estado")
5157
- st.write(state or "—")
5158
- st.caption(VIDEO_TASK_STATE_LABELS.get(state, state.replace("_", " ").capitalize() or "Desconhecido"))
5159
- st.progress(progress, text=f"{progress}%")
5160
- if task.get("error"):
5161
- st.error(str(task.get("error") or "")[:500])
5162
- with cols[4]:
5163
- state = str(task.get("state") or "")
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_{task['id']}", use_container_width=True, disabled=state not in {"to_do", "blocked", "failed"}):
5167
- run_music_task(str(task["id"]), read_json("settings.json", {}))
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_{task['id']}", use_container_width=True, disabled=state != "doing"):
5171
- transition_music_task(str(task["id"]), "blocked")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.70",
3
+ "version": "0.6.71",
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",