Add Write+Share permission and voice selection
Permission System Upgrade: - Added SHARE permission level to database enum - Three permission tiers now: * READ: View and chat only * WRITE: View, chat, add documents, generate podcasts * SHARE (Write+Share): All of WRITE + can share with others - Only owners can grant SHARE permission - Users with SHARE can add more collaborators Voice Selection for Podcasts: - Added 8 popular ElevenLabs voices to choose from - Speaker 1 (Analytical): Brian, Adam, Charlie, Daniel - Speaker 2 (Explanatory): Sarah, Bella, Charlotte, Emily - Voice IDs stored in task parameters - Passed through to audio generation - Default: Brian + Sarah (original voices) Voice Options: - Brian (Default Male) - nPczCjzI2devNBz1zQrb - Sarah (Default Female) - Xb7hH8MSUJpSbSDYk0k2 - Adam (Deep Male) - pNInz6obpgDQGcFmaJgB - Bella (Friendly Female) - EXAVITQu4vr4xnSDxMaL - Charlie (Casual Male) - IKne3meq5aSn9XLyUdCD - Charlotte (Professional Female) - XB0fDUnXU5powFXDhCwa - Daniel (British Male) - onwK4e9ZLuTAKqWW03F9 - Emily (Warm Female) - LcfcDJNUP1GQjkzn1xUU UI Updates: - Voice selection dropdowns in podcast generation - Helpful descriptions for each speaker role - Permission level shown as "Write + Share" in UI - Help text explains each permission tier - Share button visible for users with SHARE permission Technical: - Updated audio.py to accept voice parameters - Updated background_tasks.py to pass voices - Database enum extended - Permission checks updated throughout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ec608b15fa
commit
030cd2ef30
5 changed files with 90 additions and 15 deletions
16
server.log
16
server.log
|
|
@ -60,3 +60,19 @@ INFO:mcp.server.lowlevel.server:Warning: PydanticDeprecatedSince20: The `parse_o
|
|||
INFO: 127.0.0.1:59500 - "DELETE /mcp HTTP/1.1" 307 Temporary Redirect
|
||||
INFO:mcp.server.streamable_http:Terminating session: b505497d41044d28bb59b2684753a6fc
|
||||
INFO: 127.0.0.1:59500 - "DELETE /mcp/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:60605 - "POST /mcp HTTP/1.1" 307 Temporary Redirect
|
||||
INFO:mcp.server.streamable_http_manager:Created new transport with session ID: 1bb3384b1c43444e8c5092b7426d62a4
|
||||
INFO: 127.0.0.1:60605 - "POST /mcp/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:60608 - "GET /mcp HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:60609 - "POST /mcp HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:60608 - "GET /mcp/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:60609 - "POST /mcp/ HTTP/1.1" 202 Accepted
|
||||
INFO: 127.0.0.1:60611 - "POST /mcp HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:60611 - "POST /mcp/ HTTP/1.1" 200 OK
|
||||
INFO:mcp.server.lowlevel.server:Processing request of type CallToolRequest
|
||||
INFO:httpx:HTTP Request: POST https://api.cloud.llamaindex.ai/api/v1/pipelines/884e242c-86dd-4824-8347-e6dfb91d98dc/retrieve "HTTP/1.1 200 OK"
|
||||
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/responses "HTTP/1.1 200 OK"
|
||||
INFO:mcp.server.lowlevel.server:Warning: PydanticDeprecatedSince20: The `parse_obj` method is deprecated; use `model_validate` instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.11/migration/
|
||||
INFO: 127.0.0.1:60625 - "DELETE /mcp HTTP/1.1" 307 Temporary Redirect
|
||||
INFO:mcp.server.streamable_http:Terminating session: 1bb3384b1c43444e8c5092b7426d62a4
|
||||
INFO: 127.0.0.1:60625 - "DELETE /mcp/ HTTP/1.1" 200 OK
|
||||
|
|
|
|||
|
|
@ -83,19 +83,23 @@ class PodcastGenerator(BaseModel):
|
|||
)
|
||||
return MultiTurnConversation.model_validate_json(response.message.content)
|
||||
|
||||
async def _conversation_audio(self, conversation: MultiTurnConversation) -> str:
|
||||
async def _conversation_audio(self, conversation: MultiTurnConversation, voice1_id: str = None, voice2_id: str = None) -> str:
|
||||
# Default voices if not specified
|
||||
voice1_id = voice1_id or "nPczCjzI2devNBz1zQrb"
|
||||
voice2_id = voice2_id or "Xb7hH8MSUJpSbSDYk0k2"
|
||||
|
||||
files: List[str] = []
|
||||
for turn in conversation.conversation:
|
||||
if turn.speaker == "speaker1":
|
||||
speech_iterator = self.client.text_to_speech.convert(
|
||||
voice_id="nPczCjzI2devNBz1zQrb",
|
||||
voice_id=voice1_id,
|
||||
text=turn.content,
|
||||
output_format="mp3_22050_32",
|
||||
model_id="eleven_turbo_v2_5",
|
||||
)
|
||||
else:
|
||||
speech_iterator = self.client.text_to_speech.convert(
|
||||
voice_id="Xb7hH8MSUJpSbSDYk0k2",
|
||||
voice_id=voice2_id,
|
||||
text=turn.content,
|
||||
output_format="mp3_22050_32",
|
||||
model_id="eleven_turbo_v2_5",
|
||||
|
|
@ -127,9 +131,9 @@ class PodcastGenerator(BaseModel):
|
|||
|
||||
return output_path
|
||||
|
||||
async def create_conversation(self, file_transcript: str):
|
||||
async def create_conversation(self, file_transcript: str, voice1_id: str = None, voice2_id: str = None):
|
||||
conversation = await self._conversation_script(file_transcript=file_transcript)
|
||||
podcast_file = await self._conversation_audio(conversation=conversation)
|
||||
podcast_file = await self._conversation_audio(conversation=conversation, voice1_id=voice1_id, voice2_id=voice2_id)
|
||||
return podcast_file
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -217,6 +217,8 @@ async def execute_podcast_task(task_id: int):
|
|||
target_length = params.get('target_length', 10)
|
||||
custom_theme = params.get('custom_theme')
|
||||
custom_prompt = params.get('custom_prompt')
|
||||
voice1_id = params.get('voice1_id')
|
||||
voice2_id = params.get('voice2_id')
|
||||
|
||||
try:
|
||||
documents = get_notebook_documents(notebook_id)
|
||||
|
|
@ -246,8 +248,12 @@ async def execute_podcast_task(task_id: int):
|
|||
# Generate script
|
||||
script = await generate_podcast_script_from_outline(outline, combined_content)
|
||||
|
||||
# Generate audio
|
||||
audio_file = await PODCAST_GEN.create_conversation(file_transcript=script)
|
||||
# Generate audio with selected voices
|
||||
audio_file = await PODCAST_GEN.create_conversation(
|
||||
file_transcript=script,
|
||||
voice1_id=voice1_id,
|
||||
voice2_id=voice2_id
|
||||
)
|
||||
|
||||
# Save to notebook
|
||||
save_notebook_podcast(notebook_id, audio_file)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|||
class PermissionLevel(enum.Enum):
|
||||
READ = "read"
|
||||
WRITE = "write"
|
||||
SHARE = "share" # Write + Share permissions
|
||||
ADMIN = "admin"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -78,9 +78,10 @@ if not is_owner and not is_shared:
|
|||
finally:
|
||||
db.close()
|
||||
|
||||
# Determine read-only mode
|
||||
# Determine permissions
|
||||
is_read_only = is_shared and permission == PermissionLevel.READ
|
||||
can_edit = is_owner or (is_shared and permission in [PermissionLevel.WRITE, PermissionLevel.ADMIN])
|
||||
can_edit = is_owner or (is_shared and permission in [PermissionLevel.WRITE, PermissionLevel.SHARE, PermissionLevel.ADMIN])
|
||||
can_share = is_owner or (is_shared and permission in [PermissionLevel.SHARE, PermissionLevel.ADMIN])
|
||||
|
||||
# Header
|
||||
col_header1, col_header2 = st.columns([3, 1])
|
||||
|
|
@ -120,7 +121,7 @@ if can_edit:
|
|||
st.rerun()
|
||||
|
||||
with col3:
|
||||
if is_owner and st.button("📤 Share Notebook", use_container_width=True):
|
||||
if can_share and st.button("📤 Share Notebook", use_container_width=True):
|
||||
st.session_state.sharing_notebook_id = notebook.id
|
||||
st.rerun()
|
||||
|
||||
|
|
@ -184,8 +185,8 @@ if st.session_state.get("adding_docs_to_notebook") == notebook.id:
|
|||
|
||||
st.markdown("---")
|
||||
|
||||
# Share notebook section (owner only)
|
||||
if is_owner and st.session_state.get("sharing_notebook_id") == notebook.id:
|
||||
# Share notebook section (owner or users with SHARE permission)
|
||||
if can_share and st.session_state.get("sharing_notebook_id") == notebook.id:
|
||||
st.subheader("📤 Share Notebook")
|
||||
|
||||
# Show existing shares first
|
||||
|
|
@ -216,10 +217,20 @@ if is_owner and st.session_state.get("sharing_notebook_id") == notebook.id:
|
|||
# Add new share
|
||||
st.markdown("**Add new collaborator:**")
|
||||
share_email = st.text_input("Enter email to share with:")
|
||||
|
||||
# Only owners can grant SHARE permission
|
||||
if is_owner:
|
||||
permission_options = [PermissionLevel.READ, PermissionLevel.WRITE, PermissionLevel.SHARE]
|
||||
help_text = "Read: View only | Write: View + Edit + Add Docs | Share: Write + Can share with others"
|
||||
else:
|
||||
permission_options = [PermissionLevel.READ, PermissionLevel.WRITE]
|
||||
help_text = "Read: View only | Write: View + Edit + Add Docs"
|
||||
|
||||
permission = st.selectbox(
|
||||
"Permission Level:",
|
||||
options=[PermissionLevel.READ, PermissionLevel.WRITE],
|
||||
format_func=lambda x: x.value.capitalize()
|
||||
options=permission_options,
|
||||
format_func=lambda x: "Write + Share" if x == PermissionLevel.SHARE else x.value.capitalize(),
|
||||
help=help_text
|
||||
)
|
||||
|
||||
col_share1, col_share2 = st.columns(2)
|
||||
|
|
@ -308,6 +319,41 @@ if st.session_state.get("generating_podcast") == notebook.id:
|
|||
help="Specific instructions for how to structure the conversation"
|
||||
)
|
||||
|
||||
# Voice selection
|
||||
st.markdown("---")
|
||||
st.markdown("**🎙️ Voice Selection:**")
|
||||
|
||||
# Popular ElevenLabs voices
|
||||
VOICE_OPTIONS = {
|
||||
"Brian (Default Male)": "nPczCjzI2devNBz1zQrb",
|
||||
"Sarah (Default Female)": "Xb7hH8MSUJpSbSDYk0k2",
|
||||
"Adam (Deep Male)": "pNInz6obpgDQGcFmaJgB",
|
||||
"Bella (Friendly Female)": "EXAVITQu4vr4xnSDxMaL",
|
||||
"Charlie (Casual Male)": "IKne3meq5aSn9XLyUdCD",
|
||||
"Charlotte (Professional Female)": "XB0fDUnXU5powFXDhCwa",
|
||||
"Daniel (British Male)": "onwK4e9ZLuTAKqWW03F9",
|
||||
"Emily (Warm Female)": "LcfcDJNUP1GQjkzn1xUU"
|
||||
}
|
||||
|
||||
col_v1, col_v2 = st.columns(2)
|
||||
with col_v1:
|
||||
voice1_name = st.selectbox(
|
||||
"Speaker 1 (Analytical):",
|
||||
options=list(VOICE_OPTIONS.keys()),
|
||||
index=0,
|
||||
help="Usually asks questions and probes deeper"
|
||||
)
|
||||
voice1_id = VOICE_OPTIONS[voice1_name]
|
||||
|
||||
with col_v2:
|
||||
voice2_name = st.selectbox(
|
||||
"Speaker 2 (Explanatory):",
|
||||
options=list(VOICE_OPTIONS.keys()),
|
||||
index=1,
|
||||
help="Usually explains concepts and provides context"
|
||||
)
|
||||
voice2_id = VOICE_OPTIONS[voice2_name]
|
||||
|
||||
col_p1, col_p2 = st.columns(2)
|
||||
with col_p1:
|
||||
if st.button("Generate in Background", type="primary", key="generate_podcast_btn"):
|
||||
|
|
@ -316,7 +362,9 @@ if st.session_state.get("generating_podcast") == notebook.id:
|
|||
params = {
|
||||
'target_length': target_length,
|
||||
'custom_theme': custom_theme if custom_theme else None,
|
||||
'custom_prompt': custom_prompt if custom_prompt else None
|
||||
'custom_prompt': custom_prompt if custom_prompt else None,
|
||||
'voice1_id': voice1_id,
|
||||
'voice2_id': voice2_id
|
||||
}
|
||||
|
||||
# Start background task
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue