Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
ea7dc28
1
Parent(s): 62f0a86
Sync from GitHub 568a6a97b4e0075c2edef972568daa49f4f256c0
Browse files- frontend/app.py +20 -0
- src/artifacts/podcast_generator.py +23 -0
- src/artifacts/tts_adapter.py +31 -3
frontend/app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
import os
|
|
|
|
| 3 |
from typing import Any
|
| 4 |
|
| 5 |
import requests
|
|
@@ -693,6 +694,12 @@ elif page == "Notebooks":
|
|
| 693 |
if ok and isinstance(artifact_result, list):
|
| 694 |
artifacts = artifact_result
|
| 695 |
if artifacts:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
st.dataframe(artifacts, use_container_width=True)
|
| 697 |
artifact_options = {
|
| 698 |
f"{a['id']} - {a.get('type', 'unknown')} - {a.get('status', '')}": a
|
|
@@ -756,6 +763,19 @@ elif page == "Notebooks":
|
|
| 756 |
st.info(f"Podcast status: {artifact_status}")
|
| 757 |
else:
|
| 758 |
st.info("Select an artifact to preview.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
else:
|
| 760 |
st.info("No artifacts generated yet.")
|
| 761 |
else:
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
import os
|
| 3 |
+
import time
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
import requests
|
|
|
|
| 694 |
if ok and isinstance(artifact_result, list):
|
| 695 |
artifacts = artifact_result
|
| 696 |
if artifacts:
|
| 697 |
+
auto_refresh_key = f"auto_refresh_artifacts_{selected_notebook_id}"
|
| 698 |
+
auto_refresh = st.checkbox(
|
| 699 |
+
"Auto-refresh while artifacts are processing",
|
| 700 |
+
value=bool(st.session_state.get(auto_refresh_key, True)),
|
| 701 |
+
key=auto_refresh_key,
|
| 702 |
+
)
|
| 703 |
st.dataframe(artifacts, use_container_width=True)
|
| 704 |
artifact_options = {
|
| 705 |
f"{a['id']} - {a.get('type', 'unknown')} - {a.get('status', '')}": a
|
|
|
|
| 763 |
st.info(f"Podcast status: {artifact_status}")
|
| 764 |
else:
|
| 765 |
st.info("Select an artifact to preview.")
|
| 766 |
+
|
| 767 |
+
in_flight = sum(
|
| 768 |
+
1
|
| 769 |
+
for a in artifacts
|
| 770 |
+
if str(a.get("status", "")).lower() in {"pending", "processing"}
|
| 771 |
+
)
|
| 772 |
+
if auto_refresh and in_flight > 0:
|
| 773 |
+
st.caption(
|
| 774 |
+
f"{in_flight} artifact(s) still processing. "
|
| 775 |
+
"Refreshing in 4 seconds..."
|
| 776 |
+
)
|
| 777 |
+
time.sleep(4)
|
| 778 |
+
st.rerun()
|
| 779 |
else:
|
| 780 |
st.info("No artifacts generated yet.")
|
| 781 |
else:
|
src/artifacts/podcast_generator.py
CHANGED
|
@@ -426,6 +426,26 @@ IMPORTANT:
|
|
| 426 |
- Make it sound like a real conversation, not a lecture
|
| 427 |
"""
|
| 428 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
def _synthesize_segments(
|
| 430 |
self,
|
| 431 |
script: List[Dict[str, str]],
|
|
@@ -477,6 +497,9 @@ IMPORTANT:
|
|
| 477 |
)
|
| 478 |
self._last_tts_errors.append(error_detail)
|
| 479 |
print(f" ⚠️ Failed {error_detail}")
|
|
|
|
|
|
|
|
|
|
| 480 |
continue
|
| 481 |
|
| 482 |
return audio_files
|
|
|
|
| 426 |
- Make it sound like a real conversation, not a lecture
|
| 427 |
"""
|
| 428 |
|
| 429 |
+
@staticmethod
|
| 430 |
+
def _is_fatal_tts_error(exc: Exception) -> bool:
|
| 431 |
+
"""
|
| 432 |
+
Detect provider/configuration errors where retrying further segments is pointless.
|
| 433 |
+
"""
|
| 434 |
+
text = " ".join(str(exc).lower().split())
|
| 435 |
+
fatal_markers = [
|
| 436 |
+
"voice_not_found",
|
| 437 |
+
"no compatible elevenlabs synthesis method found",
|
| 438 |
+
"invalid_api_key",
|
| 439 |
+
"unauthorized",
|
| 440 |
+
"authentication",
|
| 441 |
+
"forbidden",
|
| 442 |
+
"insufficient_credits",
|
| 443 |
+
"quota",
|
| 444 |
+
"status_code: 401",
|
| 445 |
+
"status_code: 403",
|
| 446 |
+
]
|
| 447 |
+
return any(marker in text for marker in fatal_markers)
|
| 448 |
+
|
| 449 |
def _synthesize_segments(
|
| 450 |
self,
|
| 451 |
script: List[Dict[str, str]],
|
|
|
|
| 497 |
)
|
| 498 |
self._last_tts_errors.append(error_detail)
|
| 499 |
print(f" ⚠️ Failed {error_detail}")
|
| 500 |
+
if self._is_fatal_tts_error(e):
|
| 501 |
+
print(" ⛔ Fatal TTS configuration/provider error detected. Stopping remaining segments.")
|
| 502 |
+
break
|
| 503 |
continue
|
| 504 |
|
| 505 |
return audio_files
|
src/artifacts/tts_adapter.py
CHANGED
|
@@ -12,6 +12,20 @@ load_dotenv()
|
|
| 12 |
# TTS Provider type
|
| 13 |
TTSProvider = Literal["openai", "elevenlabs", "edge"]
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
class TTSAdapter(ABC):
|
| 17 |
"""Base class for TTS providers."""
|
|
@@ -76,13 +90,27 @@ class ElevenLabsTTS(TTSAdapter):
|
|
| 76 |
|
| 77 |
def _load_voice_aliases(self) -> dict[str, str]:
|
| 78 |
"""Best-effort map of configured voice names to voice IDs."""
|
|
|
|
|
|
|
|
|
|
| 79 |
try:
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
voices = getattr(response, "voices", response)
|
| 82 |
except Exception:
|
| 83 |
-
return
|
| 84 |
|
| 85 |
-
aliases: dict[str, str] = {}
|
| 86 |
for voice in voices or []:
|
| 87 |
if isinstance(voice, dict):
|
| 88 |
name = voice.get("name")
|
|
|
|
| 12 |
# TTS Provider type
|
| 13 |
TTSProvider = Literal["openai", "elevenlabs", "edge"]
|
| 14 |
|
| 15 |
+
# Common ElevenLabs preset voice name -> voice_id mapping.
|
| 16 |
+
# This allows env values like "Rachel"/"Antoni" to work with SDK methods that require voice_id.
|
| 17 |
+
ELEVENLABS_PRESET_VOICE_IDS: dict[str, str] = {
|
| 18 |
+
"rachel": "21m00Tcm4TlvDq8ikWAM",
|
| 19 |
+
"domi": "AZnzlk1XvdvUeBnXmlld",
|
| 20 |
+
"bella": "EXAVITQu4vr4xnSDxMaL",
|
| 21 |
+
"antoni": "ErXwobaYiN019PkySvjV",
|
| 22 |
+
"elli": "MF3mGyEYCl7XYWbV9V6O",
|
| 23 |
+
"josh": "TxGEqnHWrfWFTfGW9XjX",
|
| 24 |
+
"arnold": "VR6AewLTigWG4xSOukaG",
|
| 25 |
+
"adam": "pNInz6obpgDQGcFmaJgB",
|
| 26 |
+
"sam": "yoZ06aMxZJJ28mfd3POQ",
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
|
| 30 |
class TTSAdapter(ABC):
|
| 31 |
"""Base class for TTS providers."""
|
|
|
|
| 90 |
|
| 91 |
def _load_voice_aliases(self) -> dict[str, str]:
|
| 92 |
"""Best-effort map of configured voice names to voice IDs."""
|
| 93 |
+
aliases: dict[str, str] = dict(ELEVENLABS_PRESET_VOICE_IDS)
|
| 94 |
+
|
| 95 |
+
# First try the latest SDK shape.
|
| 96 |
try:
|
| 97 |
+
voices_api = getattr(self.client, "voices", None)
|
| 98 |
+
if voices_api is None:
|
| 99 |
+
return aliases
|
| 100 |
+
|
| 101 |
+
if hasattr(voices_api, "get_all"):
|
| 102 |
+
response = voices_api.get_all()
|
| 103 |
+
elif hasattr(voices_api, "search"):
|
| 104 |
+
response = voices_api.search()
|
| 105 |
+
elif hasattr(voices_api, "list"):
|
| 106 |
+
response = voices_api.list()
|
| 107 |
+
else:
|
| 108 |
+
return aliases
|
| 109 |
+
|
| 110 |
voices = getattr(response, "voices", response)
|
| 111 |
except Exception:
|
| 112 |
+
return aliases
|
| 113 |
|
|
|
|
| 114 |
for voice in voices or []:
|
| 115 |
if isinstance(voice, dict):
|
| 116 |
name = voice.get("name")
|