diff --git a/.gitignore b/.gitignore index 401453a..407a74a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,62 @@ get_helm.sh .ruff_cache data/minio eval.txt -.DS_Store \ No newline at end of file +.DS_Store + +# Ignore generated or temporary files managed by the Workbench +.project/* +!.project/spec.yaml +!.project/configpacks + +# General ignores + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Temp directories, notebooks created by jupyterlab +.ipynb_checkpoints +.Trash-*/ +.jupyter/ +.local/ + +# Python distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Workbench Project Layout +frontend/demo_outputs/*.mp3 +frontend/demo_outputs/*.json +frontend/output.log diff --git a/.project/configpacks b/.project/configpacks new file mode 100644 index 0000000..f7247e3 --- /dev/null +++ b/.project/configpacks @@ -0,0 +1,10 @@ +*defaults.ContainerUser +*bash.PreBuild +*defaults.EnvVars +*defaults.Readme +*defaults.Entrypoint +*apt.PackageManager +*bash.PreLanguage +*python.PipPackageManager +*bash.PostBuild +*jupyterlab.JupyterLab \ No newline at end of file diff --git a/.project/spec.yaml b/.project/spec.yaml new file mode 100644 index 0000000..f9c0995 --- /dev/null +++ b/.project/spec.yaml @@ -0,0 +1,133 @@ +specVersion: v2 +specMinorVersion: 2 +meta: + name: pdf-to-podcast + image: project-pdf-to-podcast + description: "" + labels: [] + createdOn: "2024-12-06T19:19:11Z" + defaultBranch: main +layout: +- path: docs/ + type: code + storage: git +- path: samples/ + type: code + storage: git +- path: services/ + type: code + storage: git +- path: shared/ + type: code + storage: git +- path: tests/ + type: code + storage: git +- path: launchable/ + type: code + storage: git +- path: frontend/ + type: code + storage: git +environment: + base: + registry: nvcr.io + image: nvidia/ai-workbench/python-basic:1.0.2 + build_timestamp: "20241001182612" + name: Python Basic + supported_architectures: [] + cuda_version: "" + description: A Python Base with Jupyterlab + entrypoint_script: "" + labels: + - ubuntu + - python3 + - jupyterlab + apps: + - name: jupyterlab + type: jupyterlab + class: webapp + start_command: jupyter lab --allow-root --port 8888 --ip 0.0.0.0 --no-browser + --NotebookApp.base_url=\$PROXY_PREFIX --NotebookApp.default_url=/lab --NotebookApp.allow_origin='*' + health_check_command: '[ \$(echo url=\$(jupyter lab list | head -n 2 | tail + -n 1 | cut -f1 -d'' '' | grep -v ''Currently'' | sed "s@/?@/lab?@g") | curl + -o /dev/null -s -w ''%{http_code}'' --config -) == ''200'' ]' + stop_command: jupyter lab stop 8888 + user_msg: "" + logfile_path: "" + timeout_seconds: 60 + icon_url: "" + webapp_options: + autolaunch: true + port: "8888" + proxy: + trim_prefix: false + url_command: jupyter lab list | head -n 2 | tail -n 1 | cut -f1 -d' ' | grep + -v 'Currently' + programming_languages: + - python3 + icon_url: https://workbench.download.nvidia.com/static/img/ai-workbench-icon-rectangle.jpg + image_version: 1.0.5 + os: linux + os_distro: ubuntu + os_distro_release: "22.04" + schema_version: v2 + user_info: + uid: "" + gid: "" + username: "" + package_managers: + - name: apt + binary_path: /usr/bin/apt + installed_packages: + - curl + - git + - git-lfs + - python3 + - gcc + - python3-dev + - python3-pip + - vim + - name: pip + binary_path: /usr/bin/pip + installed_packages: + - jupyterlab==4.2.4 + package_manager_environment: + name: "" + target: "" +execution: + apps: + - name: frontend + type: custom + class: webapp + start_command: PROXY_PREFIX=$PROXY_PREFIX python3 -m frontend + health_check_command: curl -f "http://localhost:7860/" + stop_command: pkill -f "^python3 -m frontend" + user_msg: "" + logfile_path: "" + timeout_seconds: 30 + icon_url: "" + webapp_options: + autolaunch: true + port: "7860" + proxy: + trim_prefix: false + url: http://localhost:7860/ + resources: + gpu: + requested: 0 + sharedMemoryMB: 0 + secrets: + - variable: ELEVENLABS_API_KEY + description: "" + - variable: NVIDIA_API_KEY + description: "" + mounts: + - type: project + target: /project/ + description: Project directory + options: rw + - type: volume + target: /nvwb-shared-volume/ + description: "" + options: volumeName=nvwb-shared-volume diff --git a/README.md b/README.md index 5b786c9..3241d99 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ The blueprint accepts a Target PDF and optionally multiple Context PDFs. The Tar For more information about the PDF, Agent and TTS service flows, please refer to the mermaid [diagram](docs/README.md) +| :exclamation: Important | +| :-----------------------| +| Users running this blueprint with [NVIDIA AI Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/) should skip to the quickstart section [here](https://github.com/nv-edwli/pdf-to-podcast/tree/main/workbench#quickstart)! | + ## Software Components - NVIDIA NIM microservices - Response generation (Inference) diff --git a/docker-compose.yaml b/docker-compose.yaml index eef914e..7fd7f66 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,28 @@ services: + local-nim: + image: nvcr.io/nim/meta/llama-3.1-8b-instruct:latest + runtime: nvidia + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [ gpu ] + ports: + - "8000:8000" + volumes: + - type: bind + source: ~/.cache/nim + target: /opt/nim/.cache/ + environment: + - NIM_MODEL_PROFILE=193649a2eb95e821309d6023a2cabb31489d3b690a9973c7ab5d1ff58b0aa7eb + - NGC_API_KEY=${NVIDIA_API_KEY:?Error NVIDIA_API_KEY not set} + networks: + - app-network + profiles: + - local + redis: image: redis:latest ports: @@ -16,7 +40,7 @@ services: - MINIO_ROOT_USER=minioadmin - MINIO_ROOT_PASSWORD=minioadmin volumes: - - ./data/minio:/data + - minio_data:/data command: minio server /data --console-address ":9001" networks: - app-network @@ -150,6 +174,7 @@ services: volumes: redis_data: pdf_temp: + minio_data: networks: app-network: diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/.gitkeep b/frontend/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/__init__.py b/frontend/__init__.py new file mode 100644 index 0000000..d50bcc4 --- /dev/null +++ b/frontend/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/frontend/__main__.py b/frontend/__main__.py new file mode 100644 index 0000000..b8d8eb7 --- /dev/null +++ b/frontend/__main__.py @@ -0,0 +1,273 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gradio as gr +import requests +import tempfile +import os +import base64 +import json +import sys +import ast +import re + +import uuid +from frontend.utils import email_demo, logger + +BP_INFO_MARKDOWN=""" +### Key Features + +PDF to Markdown Service + + * Extracts content from PDFs and converts it into markdown format for further processing. + +Monologue or Dialogue Creation Service + + * AI processes markdown content, enriching or structuring it to create natural and engaging audio content. + +Text-to-Speech (TTS) Service + + * Converts the processed content into high-quality speech. + +""" + +CONFIG_INSTRUCTIONS_MARKDOWN=""" +Use this editor to configure your long-reasoning agent. + +**Note:** The default configuration is for Build endpoints on the NVIDIA API Catalog. To use a local agent, ensure the compose services are running with the ``local`` profile. + +**Example**: Using a _locally_ running ``llama-3.1-8b-instruct`` NVIDIA NIM + +``` +{ + "reasoning": { + "name": "meta/llama-3.1-8b-instruct", + "api_base": "http://pdf-to-podcast-local-nim-1:8000/v1" + }, + ... +} +``` + +""" + +css = """ +#editor-title { + margin: auto; + width: 100%; + text-align: center; +} +""" + +js_func = """ +function refresh() { + const url = new URL(window.location); + if (url.searchParams.get('__theme') !== 'dark') { + url.searchParams.set('__theme', 'dark'); + window.location.href = url.href; + } +} +""" + +_CONFIG_CHANGES_JS = """ +async() => { + title = document.querySelector("div#config-toolbar p"); + if (! title.innerHTML.endsWith("๐ŸŸ ")) { title.innerHTML = title.innerHTML.slice(0,-2) + "๐ŸŸ "; }; +} +""" +_SAVE_CHANGES_JS = """ +async() => { + title = document.querySelector("div#config-toolbar p"); + if (! title.innerHTML.endsWith("๐ŸŸข")) { title.innerHTML = title.innerHTML.slice(0,-2) + "๐ŸŸข"; }; +} +""" +_SAVE_IMG = "https://media.githubusercontent.com/media/NVIDIA/nim-anywhere/main/code/frontend/_static/images/floppy.png" +_UNDO_IMG = "https://media.githubusercontent.com/media/NVIDIA/nim-anywhere/main/code/frontend/_static/images/undo.png" +_HISTORY_IMG = "https://media.githubusercontent.com/media/NVIDIA/nim-anywhere/main/code/frontend/_static/images/history.png" +_PSEUDO_FILE_NAME = "models.json ๐ŸŸข" +with open("/project/models.json", "r", encoding="UTF-8") as config_file: + _STARTING_CONFIG = config_file.read() + +sys.stdout = logger.Logger("/project/frontend/output.log") + +# Gradio Interface +with gr.Blocks(css=css, js=js_func) as demo: + gr.Markdown("# NVIDIA AI Blueprint: PDF-to-Podcast") + + with gr.Row(): + with gr.Column(scale=1): + + with gr.Tab("Full End to End Flow"): + gr.Markdown("### Upload at least one PDF file for a file to target or as context. ") + with gr.Row(): + target_files = gr.File(label="Upload target PDF", file_types=[".pdf"]) + context_files = gr.File(label="Upload context PDF", file_types=[".pdf"], file_count="multiple") + with gr.Row(): + settings = gr.CheckboxGroup( + ["Monologue Only"], label="Additional Settings", info="Customize your podcast here" + ) + with gr.Accordion("Optional: Email Details", open=False): + gr.Markdown("Enter a recipient email here to receive your generated podcast in your inbox! \n\n**Note**: Ensure `SENDER_EMAIL` and `SENDER_EMAIL_PASSWORD` are configured in AI Workbench") + recipient_email = gr.Textbox(label="Recipient email", placeholder="Enter email here") + + generate_button = gr.Button("Generate Podcast") + + with gr.Tab("Agent Configurations"): + gr.Markdown(CONFIG_INSTRUCTIONS_MARKDOWN) + with gr.Row(elem_id="config-row"): + with gr.Column(): + with gr.Group(elem_id="config-wrapper"): + with gr.Row(elem_id="config-toolbar", elem_classes=["toolbar"]): + file_title = gr.Markdown(_PSEUDO_FILE_NAME, elem_id="editor-title") + save_btn = gr.Button("", icon=_SAVE_IMG, elem_classes=["toolbar"]) + undo_btn = gr.Button("", icon=_UNDO_IMG, elem_classes=["toolbar"]) + reset_btn = gr.Button("", icon=_HISTORY_IMG, elem_classes=["toolbar"]) + with gr.Row(elem_id="config-row-box"): + editor = gr.Code( + elem_id="config-editor", + interactive=True, + language="json", + show_label=False, + container=False, + ) + + with gr.Tab("Architecture Diagram"): + gr.Markdown(BP_INFO_MARKDOWN) + gr.Image(value="frontend/static/diagram.png") + + with gr.Column(scale=1): + gr.Markdown("
") + output = gr.Textbox(label="Outputs", placeholder="Outputs will show here when executing", max_lines=20, lines=20) + output_file = gr.File(visible=False, interactive=False) + transcript_file = gr.File(visible=False, interactive=False) + history_file = gr.File(visible=False, interactive=False) + + demo.load(logger.read_logs, None, output, every=1) + + def read_chain_config() -> str: + """Read the chain config file.""" + with open("/project/models.json", "r", encoding="UTF-8") as cf: + return cf.read() + + demo.load(read_chain_config, outputs=editor) + + undo_btn.click(read_chain_config, outputs=editor) + undo_btn.click(None, js=_SAVE_CHANGES_JS) + + @reset_btn.click(outputs=editor) + def reset_demo() -> str: + """Reset the configuration to the starting config.""" + return _STARTING_CONFIG + + @save_btn.click(inputs=editor) + def save_chain_config(config_txt: str) -> None: + """Save the user's config file.""" + # validate json + try: + config_data = json.loads(config_txt) + except Exception as err: + raise SyntaxError(f"Error validating JSON syntax:\n{err}") from err + + # save configuration + with open("/project/models.json", "w", encoding="UTF-8") as cf: + cf.write(config_txt) + + save_btn.click(None, js=_SAVE_CHANGES_JS) + + # %% editor actions + editor.input(None, js=_CONFIG_CHANGES_JS) + + def validate_sender(sender): + if sender is None: + return False + regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return bool(re.match(regex, sender)) + + def get_transcript(filename, job_id): + service = os.environ["API_SERVICE_URL"] + url = f"{service}/saved_podcast/{job_id}/transcript" + params = {"userId": "test-userid"} + filepath = "/project/frontend/demo_outputs/transcript_" + filename + ".json" + + response = requests.get(url, params=params) + if response.status_code == 200: + json_data = response.json() + with open(filepath, "w") as file: + json.dump(json_data, file) + print(f"JSON data saved to {filepath}") + return filepath + else: + print(f"Error retrieving transcript: {response.status_code}") + return filepath + + def get_history(filename, job_id): + service = os.environ["API_SERVICE_URL"] + url = f"{service}/saved_podcast/{job_id}/history" + params = {"userId": "test-userid"} + filepath = "/project/frontend/demo_outputs/generation_history_" + filename + ".json" + response = requests.get(url, params=params) + + if response.status_code == 200: + json_data = response.json() + with open(filepath, "w") as file: + json.dump(json_data, file) + print(f"JSON data saved to {filepath}") + return filepath + else: + print(f"Error retrieving generation_history: {response.status_code}") + return filepath + + def generate_podcast(target, context, recipient, settings): + if target is None or len(target) == 0: + gr.Warning("Target PDF upload not detected. Please upload a target PDF file and try again. ") + return gr.update(visible=False) + + sender_email = os.environ["SENDER_EMAIL"] if "SENDER_EMAIL" in os.environ else None + + if isinstance(target, str): + target = [target] + if isinstance(context, str): + context = [context] + + base_url = os.environ["API_SERVICE_URL"] + monologue = True if "Monologue Only" in settings else False + vdb = False # True if "Vector Database" in settings else False + filename = str(uuid.uuid4()) + sender_validation = validate_sender(sender_email) + if not sender_validation and len(recipient) > 0: + gr.Warning("SENDER_EMAIL not detected or malformed. Please fix or remove recipient email and try again. You may need to restart the container for Environment Variable changes to take effect. ") + return gr.update(visible=False) + elif sender_validation and len(recipient) > 0 and "SENDER_EMAIL_PASSWORD" not in os.environ: + gr.Warning("SENDER_EMAIL_PASSWORD not detected. Please fix or remove recipient email and try again. You may need to restart the container for Environment Variable changes to take effect. ") + return gr.update(visible=False) + email = [recipient] if (sender_validation and len(recipient) > 0 and "SENDER_EMAIL_PASSWORD" in os.environ) else [filename + "@"] # delimiter + + # Generate podcast + job_id = email_demo.test_api(base_url, target, context, email, monologue, vdb) + + # Send file via email + if sender_validation and len(recipient) > 0 and "SENDER_EMAIL_PASSWORD" in os.environ: + email_demo.send_file_via_email("/project/frontend/demo_outputs/" + recipient.split('@')[0] + "-output.mp3", sender_email, recipient) + return gr.update(value="/project/frontend/demo_outputs/" + recipient.split('@')[0] + "-output.mp3", label="podcast audio", visible=True), gr.update(value=get_transcript(recipient.split('@')[0], job_id), label="podcast transcript", visible=True), gr.update(value=get_history(recipient.split('@')[0], job_id), label="generation history", visible=True) + + return gr.update(value="/project/frontend/demo_outputs/" + filename + "-output.mp3", label="podcast audio", visible=True), gr.update(value=get_transcript(filename, job_id), label="podcast transcript", visible=True), gr.update(value=get_history(filename, job_id), label="generation history", visible=True) + + generate_button.click(generate_podcast, [target_files, + context_files, + recipient_email, + settings], [output_file, transcript_file, history_file]) + +# Launch Gradio app +if __name__ == "__main__": + demo.launch(server_name="0.0.0.0", root_path=os.environ.get("PROXY_PREFIX")) diff --git a/frontend/demo_outputs/.gitkeep b/frontend/demo_outputs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/output.log b/frontend/output.log new file mode 100644 index 0000000..04f3df5 --- /dev/null +++ b/frontend/output.log @@ -0,0 +1,3 @@ +Running on local URL: http://0.0.0.0:7860 + +To create a public link, set `share=True` in `launch()`. diff --git a/frontend/shared/setup.py b/frontend/shared/setup.py new file mode 100644 index 0000000..81b05e6 --- /dev/null +++ b/frontend/shared/setup.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from setuptools import setup, find_packages + +setup( + name="shared", + version="0.1", + packages=find_packages(), + install_requires=[ + "redis", + "pydantic", + "httpx", + "requests", + "langchain-nvidia-ai-endpoints", + ], +) diff --git a/frontend/shared/shared/__init__.py b/frontend/shared/shared/__init__.py new file mode 100644 index 0000000..d50bcc4 --- /dev/null +++ b/frontend/shared/shared/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/frontend/shared/shared/api_types.py b/frontend/shared/shared/api_types.py new file mode 100644 index 0000000..f0471c2 --- /dev/null +++ b/frontend/shared/shared/api_types.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pydantic import BaseModel, Field, model_validator +from typing import Optional, Dict, List +from .pdf_types import PDFMetadata +from enum import Enum + + +class JobStatus(str, Enum): + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + + +class ServiceType(str, Enum): + PDF = "pdf" + AGENT = "agent" + TTS = "tts" + + +class StatusUpdate(BaseModel): + job_id: str + status: JobStatus + message: Optional[str] = None + service: Optional[ServiceType] = None + timestamp: Optional[float] = None + data: Optional[dict] = None + + +class StatusResponse(BaseModel): + status: str + result: Optional[str] = None + error: Optional[str] = None + message: Optional[str] = None + + +class TranscriptionParams(BaseModel): + userId: str = Field(..., description="KAS User ID") + name: str = Field(..., description="Name of the podcast") + duration: int = Field(..., description="Duration in minutes") + monologue: bool = Field( + False, description="If True, creates a single-speaker podcast" + ) + speaker_1_name: str = Field( + ..., description="Name of the speaker (or first speaker if not monologue)" + ) + speaker_2_name: Optional[str] = Field( + None, description="Name of the second speaker (not required for monologue)" + ) + voice_mapping: Dict[str, str] = Field( + ..., + description="Mapping of speaker IDs to voice IDs. For monologue, only speaker-1 is required", + example={ + "speaker-1": "iP95p4xoKVk53GoZ742B", + "speaker-2": "9BWtsMINqrJLrRacOk9x", + }, + ) + guide: Optional[str] = Field( + None, description="Optional guidance for the transcription focus and structure" + ) + vdb_task: bool = Field( + False, + description="If True, creates a VDB task when running NV-Ingest allowing for retrieval abilities", + ) + + @model_validator(mode="after") + def validate_monologue_settings(self) -> "TranscriptionParams": + if self.monologue: + # Check speaker_2_name is not provided + if self.speaker_2_name is not None: + raise ValueError( + "speaker_2_name should not be provided for monologue podcasts" + ) + + # Check voice_mapping only contains speaker-1 + if "speaker-2" in self.voice_mapping: + raise ValueError( + "voice_mapping should only contain speaker-1 for monologue podcasts" + ) + + # Check that speaker-1 is present in voice_mapping + if "speaker-1" not in self.voice_mapping: + raise ValueError("voice_mapping must contain speaker-1") + else: + # For dialogues, ensure both speakers are present + if not self.speaker_2_name: + raise ValueError("speaker_2_name is required for dialogue podcasts") + + required_speakers = {"speaker-1", "speaker-2"} + if not all(speaker in self.voice_mapping for speaker in required_speakers): + raise ValueError( + "voice_mapping must contain both speaker-1 and speaker-2 for dialogue podcasts" + ) + + return self + + +class TranscriptionRequest(TranscriptionParams): + pdf_metadata: List[PDFMetadata] + job_id: str + + +class RAGRequest(BaseModel): + query: str = Field(..., description="The search query to process") + k: int = Field(..., description="Number of results to retrieve", ge=1) + job_id: str = Field(..., description="The unique job identifier") diff --git a/frontend/shared/shared/connection.py b/frontend/shared/shared/connection.py new file mode 100644 index 0000000..9168b52 --- /dev/null +++ b/frontend/shared/shared/connection.py @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi import WebSocket, WebSocketDisconnect +from typing import Dict, Set +import redis +import ujson as json +import logging +import time +import asyncio +from collections import defaultdict +from threading import Thread +import queue + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class ConnectionManager: + def __init__(self, redis_client: redis.Redis): + self.active_connections: Dict[str, Set[WebSocket]] = defaultdict(set) + self.pubsub = None + self.message_queue = queue.Queue() + self.redis_thread = None + self.should_stop = False + self.redis_client = redis_client + + async def connect(self, websocket: WebSocket, job_id: str): + await websocket.accept() + self.active_connections[job_id].add(websocket) + logger.info( + f"New WebSocket connection for job {job_id}. Total connections: {len(self.active_connections[job_id])}" + ) + + # Start Redis listener if not already running + if self.redis_thread is None: + self.redis_thread = Thread(target=self._redis_listener) + self.redis_thread.daemon = True + self.redis_thread.start() + # Start the async message processor + asyncio.create_task(self._process_messages()) + + def disconnect(self, websocket: WebSocket, job_id: str): + if job_id in self.active_connections: + self.active_connections[job_id].remove(websocket) + if not self.active_connections[job_id]: + del self.active_connections[job_id] + logger.info( + f"WebSocket disconnected for job {job_id}. Remaining connections: {len(self.active_connections[job_id]) if job_id in self.active_connections else 0}" + ) + + def _redis_listener(self): + """Redis subscription running in a separate thread""" + try: + self.pubsub = self.redis_client.pubsub(ignore_subscribe_messages=True) + self.pubsub.subscribe("status_updates:all") + logger.info("Successfully subscribed to Redis status_updates:all channel") + + while not self.should_stop: + message = self.pubsub.get_message() + if message and message["type"] == "message" and "data" in message: + data = message["data"].decode("utf-8") + try: + job_update = json.loads(data) + logger.info( + f"Received message for job {job_update.get('job_id')}" + ) + except json.JSONDecodeError: + logger.error("Invalid JSON in Redis message") + # Put message in queue for processing regardless of logging logic error + finally: + self.message_queue.put(data) + time.sleep(0.01) # Prevent tight loop + + except Exception as e: + logger.error(f"Redis subscription error: {e}") + finally: + if self.pubsub: + self.pubsub.unsubscribe() + self.pubsub.close() + + async def _process_messages(self): + """Async task to process messages from the queue and broadcast them""" + while True: + try: + # Check queue in a non-blocking way + while not self.message_queue.empty(): + message: str = self.message_queue.get_nowait() + try: + update = json.loads(message) + job_id = update.get("job_id") + logger.info(f"Processing message for job {job_id}") + + if job_id and job_id in self.active_connections: + logger.info( + f"Broadcasting update for job {job_id} to {len(self.active_connections[job_id])} connections" + ) + await self.broadcast_to_job( + job_id, + { + "service": update.get("service"), + "status": update.get("status"), + "message": update.get("message", ""), + }, + ) + logger.info( + f"Broadcasted update for job {job_id}: {update.get('service')} - {update.get('status')}" + ) + except json.JSONDecodeError: + logger.error(f"Invalid JSON in Redis message: {message}") + except Exception as e: + logger.error(f"Error processing Redis message: {e}") + + # Small delay before next check + await asyncio.sleep(0.01) + + except Exception as e: + logger.error(f"Message processing error: {e}") + await asyncio.sleep(1) + + async def broadcast_to_job(self, job_id: str, message: dict): + """Send message to all WebSocket connections for a job""" + if job_id in self.active_connections: + disconnected = set() + for connection in self.active_connections[job_id]: + try: + await connection.send_json(message) + except WebSocketDisconnect: + disconnected.add(connection) + except Exception as e: + logger.error(f"Error sending message to WebSocket: {e}") + disconnected.add(connection) + + # Clean up disconnected clients + for connection in disconnected: + self.disconnect(connection, job_id) + + def cleanup(self): + """Cleanup resources""" + self.should_stop = True + if self.redis_thread: + self.redis_thread.join(timeout=1.0) + if self.pubsub: + self.pubsub.close() diff --git a/frontend/shared/shared/job.py b/frontend/shared/shared/job.py new file mode 100644 index 0000000..f4a3ac1 --- /dev/null +++ b/frontend/shared/shared/job.py @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from shared.api_types import ServiceType +from shared.otel import OpenTelemetryInstrumentation +import redis +import time +import ujson as json +import threading + + +class JobStatusManager: + def __init__( + self, + service_type: ServiceType, + telemetry: OpenTelemetryInstrumentation, + redis_url="redis://redis:6379", + ): + self.telemetry = telemetry + self.redis = redis.Redis.from_url(redis_url, decode_responses=False) + self.service_type = service_type + self._lock = threading.Lock() + + def create_job(self, job_id: str): + with self.telemetry.tracer.start_as_current_span("job.create_job") as span: + span.set_attribute("job_id", job_id) + update = { + "job_id": job_id, + "status": "pending", + "message": "Job created", + "service": self.service_type, + "timestamp": time.time(), + } + # Encode the update dict as JSON bytes + hset_key = f"status:{job_id}:{str(self.service_type)}" + span.set_attribute("hset_key", hset_key) + self.redis.hset( + hset_key, + mapping={k: str(v).encode() for k, v in update.items()}, + ) + self.redis.publish("status_updates:all", json.dumps(update).encode()) + + def update_status(self, job_id: str, status: str, message: str): + with self.telemetry.tracer.start_as_current_span("job.update_status") as span: + span.set_attribute("job_id", job_id) + update = { + "job_id": job_id, + "status": status, + "message": message, + "service": self.service_type, + "timestamp": time.time(), + } + # Encode the update dict as JSON bytes + hset_key = f"status:{job_id}:{str(self.service_type)}" + span.set_attribute("hset_key", hset_key) + self.redis.hset( + hset_key, + mapping={k: str(v).encode() for k, v in update.items()}, + ) + self.redis.publish("status_updates:all", json.dumps(update).encode()) + + def set_result(self, job_id: str, result: bytes): + with self.telemetry.tracer.start_as_current_span("job.set_result") as span: + span.set_attribute("job_id", job_id) + set_key = f"result:{job_id}:{str(self.service_type)}" + span.set_attribute("set_key", set_key) + self.redis.set(set_key, result) + + def set_result_with_expiration(self, job_id: str, result: bytes, ex: int): + with self.telemetry.tracer.start_as_current_span( + "job.set_result_with_expiration" + ) as span: + span.set_attribute("job_id", job_id) + set_key = f"result:{job_id}:{str(self.service_type)}" + span.set_attribute("set_key", set_key) + self.redis.set(set_key, result, ex=ex) + + def get_result(self, job_id: str): + with self.telemetry.tracer.start_as_current_span("job.get_result") as span: + span.set_attribute("job_id", job_id) + get_key = f"result:{job_id}:{str(self.service_type)}" + span.set_attribute("get_key", get_key) + result = self.redis.get(get_key) + return result if result else None + + def get_status(self, job_id: str): + with self.telemetry.tracer.start_as_current_span("job.get_status") as span: + span.set_attribute("job_id", job_id) + # Get raw bytes and decode manually + hget_key = f"status:{job_id}:{str(self.service_type)}" + span.set_attribute("hget_key", hget_key) + status = self.redis.hgetall(hget_key) + if not status: + raise ValueError("Job not found") + # Decode bytes to strings for each field + return {k.decode(): v.decode() for k, v in status.items()} + + def cleanup_old_jobs(self, max_age=3600): + current_time = time.time() + removed = 0 + pattern = f"status:*:{str(self.service_type)}" + for key in self.redis.scan_iter(match=pattern): + status = self.redis.hgetall(key) + try: + timestamp = float(status[b"timestamp"].decode()) + if timestamp < current_time - max_age: + self.redis.delete(key) + job_id = key.split(b":")[1].decode() + self.redis.delete(f"result:{job_id}:{self.service_type}") + removed += 1 + except (KeyError, ValueError): + # Handle malformed status entries + continue + return removed diff --git a/frontend/shared/shared/llmmanager.py b/frontend/shared/shared/llmmanager.py new file mode 100644 index 0000000..24c5ba9 --- /dev/null +++ b/frontend/shared/shared/llmmanager.py @@ -0,0 +1,276 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from langchain_nvidia_ai_endpoints import ChatNVIDIA +from typing import List, Dict, Any, Optional, Union +import logging +import ujson as json +from shared.otel import OpenTelemetryInstrumentation +from opentelemetry.trace.status import StatusCode +from pathlib import Path +from dataclasses import dataclass +from langchain_core.messages import AIMessage + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@dataclass +class ModelConfig: + name: str + api_base: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ModelConfig": + return cls( + name=data["name"], + api_base=data["api_base"], + ) + + +class LLMManager: + """ + A lightweight and user friendly wrapper over Langchain's ChatNVIDIA class. We use this class + to abstract away all Langchain functionalities including models, async/sync queries, + structured outputs, types, streaming and more. It also comes with OTEL telemetry out of the box + for all queries. It is specifically tailored for singular invocations. + + Configs can be overridden by providing a custom config file. Currently the defaults are + hardcoded to build.nvidia.com endpoints. + + Usage: + >>> llm_manager = LLMManager(api_key, telemetry) + >>> llm_manager.query_sync("reasoning", [{"role": "user", "content": "Hello, world!"}], "test") + """ + + DEFAULT_CONFIGS = { + "reasoning": { + "name": "meta/llama-3.1-405b-instruct", + "api_base": "https://integrate.api.nvidia.com/v1", + }, + "iteration": { + "name": "meta/llama-3.1-405b-instruct", + "api_base": "https://integrate.api.nvidia.com/v1", + }, + "json": { + "name": "meta/llama-3.1-70b-instruct", + "api_base": "https://integrate.api.nvidia.com/v1", + }, + } + + def __init__( + self, + api_key: str, + telemetry: OpenTelemetryInstrumentation, + config_path: Optional[str] = None, + ): + """ + Initialize LLMManager with telemetry + requires: OpenTelemetryInstrumentation instance for tracing + """ + try: + self.api_key = api_key + self.telemetry = telemetry + self._llm_cache: Dict[str, ChatNVIDIA] = {} + self.model_configs = self._load_configurations(config_path) + logger.info("Successfully initialized LLMManager") + except Exception as e: + logger.error(f"Failed to initialize LLMManager: {e}") + raise + + def _load_configurations( + self, config_path: Optional[str] + ) -> Dict[str, ModelConfig]: + """Load model configurations from JSON file if provided, otherwise use defaults""" + configs = self.DEFAULT_CONFIGS.copy() + if config_path: + try: + config_path = Path(config_path) + if config_path.exists(): + with config_path.open() as f: + custom_configs = json.load(f) + configs.update(custom_configs) + else: + logger.warning( + f"Config file {config_path} not found, using default configurations" + ) + except Exception as e: + logger.error(f"Error loading config file: {e}") + logger.warning("Using default configurations") + return {key: ModelConfig.from_dict(config) for key, config in configs.items()} + + def get_llm(self, model_key: str) -> ChatNVIDIA: + """Get or create a ChatNVIDIA model for the specified model key""" + if model_key not in self.model_configs: + raise ValueError(f"Unknown model key: {model_key}") + if model_key not in self._llm_cache: + config = self.model_configs[model_key] + self._llm_cache[model_key] = ChatNVIDIA( + model=config.name, + base_url=config.api_base, + nvidia_api_key=self.api_key, + max_tokens=None, + ) + return self._llm_cache[model_key] + + def query_sync( + self, + model_key: str, + messages: List[Dict[str, str]], + query_name: str, + json_schema: Optional[Dict] = None, + retries: int = 5, + ) -> Union[AIMessage, Dict[str, Any]]: + """Send a synchronous query to the specified model""" + with self.telemetry.tracer.start_as_current_span( + f"agent.query.{query_name}" + ) as span: + span.set_attribute("model_key", model_key) + span.set_attribute("retries", retries) + span.set_attribute("async", False) + + try: + llm = self.get_llm(model_key) + if json_schema: + llm = llm.with_structured_output(json_schema) + llm = llm.with_retry( + stop_after_attempt=retries, wait_exponential_jitter=True + ) + resp = llm.invoke(messages) + return resp + except Exception as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error(f"Query failed: {e}") + raise Exception( + f"Failed to get response after {retries} attempts" + ) from e + + async def query_async( + self, + model_key: str, + messages: List[Dict[str, str]], + query_name: str, + json_schema: Optional[Dict] = None, + retries: int = 5, + ) -> Union[AIMessage, Dict[str, Any]]: + """Send an asynchronous query to the specified model""" + with self.telemetry.tracer.start_as_current_span( + f"agent.query.{query_name}" + ) as span: + span.set_attribute("model_key", model_key) + span.set_attribute("retries", retries) + span.set_attribute("async", True) + + try: + llm = self.get_llm(model_key) + if json_schema: + llm = llm.with_structured_output(json_schema) + llm = llm.with_retry( + stop_after_attempt=retries, wait_exponential_jitter=True + ) + resp = await llm.ainvoke(messages) + return resp + except Exception as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error(f"Query failed: {e}") + raise Exception( + f"Failed to get response after {retries} attempts" + ) from e + + def stream_sync( + self, + model_key: str, + messages: List[Dict[str, str]], + query_name: str, + json_schema: Optional[Dict] = None, + retries: int = 5, + ) -> Union[str, Dict[str, Any]]: + """Send a synchronous streaming query to the specified model""" + with self.telemetry.tracer.start_as_current_span( + f"agent.stream.{query_name}" + ) as span: + span.set_attribute("model_key", model_key) + span.set_attribute("retries", retries) + span.set_attribute("async", False) + + try: + llm = self.get_llm(model_key) + if json_schema: + llm = llm.with_structured_output(json_schema) + llm = llm.with_retry( + stop_after_attempt=retries, wait_exponential_jitter=True + ) + + last_chunk = None + for chunk in llm.stream(messages): + # AIMessage returns content and JSON returns the dict itself + if hasattr(chunk, "content"): + last_chunk = chunk.content + else: + last_chunk = chunk + + return last_chunk + + except Exception as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error(f"Streaming query failed: {e}") + raise Exception( + f"Failed to get streaming response after {retries} attempts" + ) from e + + async def stream_async( + self, + model_key: str, + messages: List[Dict[str, str]], + query_name: str, + json_schema: Optional[Dict] = None, + retries: int = 5, + ) -> Union[str, Dict[str, Any]]: + """Send an asynchronous streaming query to the specified model""" + with self.telemetry.tracer.start_as_current_span( + f"agent.stream.{query_name}" + ) as span: + span.set_attribute("model_key", model_key) + span.set_attribute("retries", retries) + span.set_attribute("async", True) + + try: + llm = self.get_llm(model_key) + if json_schema: + llm = llm.with_structured_output(json_schema) + llm = llm.with_retry( + stop_after_attempt=retries, wait_exponential_jitter=True + ) + + last_chunk = None + async for chunk in llm.astream(messages): + # AIMessage returns content and JSON returns the dict itself + if hasattr(chunk, "content"): + last_chunk = chunk.content + else: + last_chunk = chunk + + return last_chunk + + except Exception as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error(f"Async streaming query failed: {e}") + raise Exception( + f"Failed to get streaming response after {retries} attempts" + ) from e diff --git a/frontend/shared/shared/otel.py b/frontend/shared/shared/otel.py new file mode 100644 index 0000000..c92b496 --- /dev/null +++ b/frontend/shared/shared/otel.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import Optional +import logging +import os + +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.redis import RedisInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor +from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@dataclass +class OpenTelemetryConfig: + """Configuration for OpenTelemetry setup.""" + + service_name: str + otlp_endpoint: str = "http://jaeger:4317" + enable_redis: bool = True + enable_requests: bool = True + enable_httpx: bool = True + enable_urllib3: bool = True + + +class OpenTelemetryInstrumentation: + """ + Lightweight OTEL wrapper + + Example usage: + telemetry = OpenTelemetryInstrumentation() + app = FastAPI() + telemetry.initialize(app, "api-service") + + # In code + with telemetry.tracer.start_as_current_span("operation_name") as span: + span.set_attribute("key", "value") + """ + + def __init__(self): + self._tracer: Optional[trace.Tracer] = None + self._config: Optional[OpenTelemetryConfig] = None + + @property + def tracer(self) -> trace.Tracer: + """Get the configured tracer instance.""" + if not self._tracer: + raise RuntimeError( + "OpenTelemetry has not been initialized. Call initialize() first." + ) + return self._tracer + + def initialize( + self, config: OpenTelemetryConfig, app=None + ) -> "OpenTelemetryInstrumentation": + """ + Initialize OpenTelemetry instrumentation with the given configuration. + + Args: + app: The FastAPI application instance + config: OpenTelemetryConfig instance containing configuration options + + Returns: + self for method chaining + """ + self._config = config + logger.info(f"Setting up tracing for service: {self._config.service_name}") + logger.info(f"Container ID: {os.uname().nodename}") + self._setup_tracing() + self._instrument_app(app) + return self + + def _setup_tracing(self) -> None: + """Set up the OpenTelemetry tracer provider and processors.""" + resource = Resource.create({"service.name": self._config.service_name}) + + provider = TracerProvider(resource=resource) + processor = BatchSpanProcessor( + OTLPSpanExporter(endpoint=self._config.otlp_endpoint) + ) + + provider.add_span_processor(processor) + trace.set_tracer_provider(provider) + + self._tracer = trace.get_tracer(self._config.service_name) + + def _instrument_app(self, app=None) -> None: + """Instrument the FastAPI application and optional components.""" + # Instrument FastAPI + if app: + FastAPIInstrumentor.instrument_app(app) + + # Instrument Redis if enabled + if self._config.enable_redis: + RedisInstrumentor().instrument() + + # Instrument requests library if enabled + if self._config.enable_requests: + RequestsInstrumentor().instrument() + + if self._config.enable_httpx: + HTTPXClientInstrumentor().instrument() + + if self._config.enable_urllib3: + URLLib3Instrumentor().instrument() diff --git a/frontend/shared/shared/pdf_types.py b/frontend/shared/shared/pdf_types.py new file mode 100644 index 0000000..6b5b621 --- /dev/null +++ b/frontend/shared/shared/pdf_types.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pydantic import BaseModel, Field +from typing import Optional, Union, Literal +from datetime import datetime +from enum import Enum + + +class ConversionStatus(str, Enum): + SUCCESS = "success" + FAILED = "failed" + + +class PDFConversionResult(BaseModel): + filename: str + content: str = "" + status: ConversionStatus + error: Optional[str] = None + + +class PDFMetadata(BaseModel): + filename: str + markdown: str = "" + summary: str = "" + status: ConversionStatus + type: Union[Literal["target"], Literal["context"]] + error: Optional[str] = None + created_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/frontend/shared/shared/podcast_types.py b/frontend/shared/shared/podcast_types.py new file mode 100644 index 0000000..95b4610 --- /dev/null +++ b/frontend/shared/shared/podcast_types.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pydantic import BaseModel +from typing import Optional, Dict, Literal, List + + +class SavedPodcast(BaseModel): + job_id: str + filename: str + created_at: str + size: int + transcription_params: Optional[Dict] = {} + + +class SavedPodcastWithAudio(SavedPodcast): + audio_data: str + + +class DialogueEntry(BaseModel): + text: str + speaker: Literal["speaker-1", "speaker-2"] + + +class Conversation(BaseModel): + scratchpad: str + dialogue: List[DialogueEntry] + + +class SegmentPoint(BaseModel): + description: str + + +class SegmentTopic(BaseModel): + title: str + points: List[SegmentPoint] + + +class PodcastSegment(BaseModel): + section: str + topics: List[SegmentTopic] + duration: int + references: List[str] + + +class PodcastOutline(BaseModel): + title: str + segments: List[PodcastSegment] diff --git a/frontend/shared/shared/prompt_tracker.py b/frontend/shared/shared/prompt_tracker.py new file mode 100644 index 0000000..5a8f655 --- /dev/null +++ b/frontend/shared/shared/prompt_tracker.py @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict +import time +import logging +from .storage import StorageManager +from .prompt_types import ProcessingStep, PromptTracker as PromptTrackerModel + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class PromptTracker: + """Track prompts and responses and save them to storage""" + + def __init__(self, job_id: str, user_id: str, storage_manager: StorageManager): + self.job_id = job_id + self.user_id = user_id + self.steps: Dict[str, ProcessingStep] = {} + self.storage_manager = storage_manager + + def track(self, step_name: str, prompt: str, model: str, response: str = None): + """Track a processing step""" + self.steps[step_name] = ProcessingStep( + step_name=step_name, + prompt=prompt, + response=response if response else "", + model=model, + timestamp=time.time(), + ) + if response: + self._save() + logger.info(f"Tracked step {step_name} for {self.job_id}") + + def update_result(self, step_name: str, response: str): + """Update the response for an existing step""" + if step_name in self.steps: + self.steps[step_name].response = response + self._save() + logger.info(f"Updated response for step {step_name}") + else: + logger.warning(f"Step {step_name} not found in prompt tracker") + + def _save(self): + """Save the current state to storage""" + tracker = PromptTrackerModel(steps=list(self.steps.values())) + self.storage_manager.store_file( + self.user_id, + self.job_id, + tracker.model_dump_json().encode(), + f"{self.job_id}_prompt_tracker.json", + "application/json", + ) + logger.info( + f"Stored prompt tracker for {self.job_id} in minio. Length: {len(self.steps)}" + ) diff --git a/frontend/shared/shared/prompt_types.py b/frontend/shared/shared/prompt_types.py new file mode 100644 index 0000000..1b41074 --- /dev/null +++ b/frontend/shared/shared/prompt_types.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pydantic import BaseModel +from typing import List + + +class ProcessingStep(BaseModel): + step_name: str + prompt: str + response: str + model: str + timestamp: float + + +class PromptTracker(BaseModel): + steps: List[ProcessingStep] diff --git a/frontend/shared/shared/storage.py b/frontend/shared/shared/storage.py new file mode 100644 index 0000000..c480a84 --- /dev/null +++ b/frontend/shared/shared/storage.py @@ -0,0 +1,324 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import ujson as json +import base64 +from minio import Minio +from minio.error import S3Error +from shared.api_types import TranscriptionParams +from shared.otel import OpenTelemetryInstrumentation +from opentelemetry.trace.status import StatusCode +import os +import logging +import urllib3 +from urllib3 import Retry +from urllib3.util import Timeout +from typing import Optional + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Minio config +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minio:9000") +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin") +MINIO_BUCKET_NAME = os.getenv("MINIO_BUCKET_NAME", "audio-results") + + +# TODO: use this to wrap redis as well +# TODO: wrap errors in StorageError +# TODO: implement cleanup and delete as well +class StorageManager: + def __init__(self, telemetry: OpenTelemetryInstrumentation): + """ + Initialize MinIO client and ensure bucket exists + requires: OpenTelemetryInstrumentation instance for tracing since Minio + does not have an auto otel instrumentor + """ + try: + self.telemetry: OpenTelemetryInstrumentation = telemetry + # pass in http_client for tracing + http_client = urllib3.PoolManager( + timeout=Timeout(connect=5, read=5), + maxsize=10, + retries=Retry( + total=5, backoff_factor=0.2, status_forcelist=[500, 502, 503, 504] + ), + ) + self.client = Minio( + os.getenv("MINIO_ENDPOINT", "minio:9000"), + access_key=os.getenv("MINIO_ACCESS_KEY", "minioadmin"), + secret_key=os.getenv("MINIO_SECRET_KEY", "minioadmin"), + secure=os.getenv("MINIO_SECURE", "false").lower() == "true", + http_client=http_client, + ) + + self.bucket_name = os.getenv("MINIO_BUCKET_NAME", "audio-results") + self._ensure_bucket_exists() + logger.info("Successfully initialized MinIO storage") + + except Exception as e: + logger.error(f"Failed to initialize MinIO client: {e}") + raise + + def _ensure_bucket_exists(self): + try: + if not self.client.bucket_exists(self.bucket_name): + self.client.make_bucket(self.bucket_name) + except Exception as e: + logger.error(f"Failed to ensure bucket exists: {e}") + raise + + def _get_object_path(self, user_id: str, job_id: str, filename: str) -> str: + """Generate the full object path including user isolation""" + return f"{user_id}/{job_id}/{filename}" + + def store_file( + self, + user_id: str, + job_id: str, + content: bytes, + filename: str, + content_type: str, + metadata: dict = None, + ) -> None: + """Store any file type in MinIO with metadata""" + with self.telemetry.tracer.start_as_current_span("store_file") as span: + span.set_attribute("user_id", user_id) + span.set_attribute("job_id", job_id) + span.set_attribute("filename", filename) + try: + object_name = self._get_object_path(user_id, job_id, filename) + self.client.put_object( + self.bucket_name, + object_name, + io.BytesIO(content), + length=len(content), + content_type=content_type, + metadata=metadata.model_dump() + if hasattr(metadata, "model_dump") + else metadata, + ) + except Exception as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error( + f"Failed to store file {filename} for user {user_id}, job {job_id}: {str(e)}" + ) + raise + + def store_audio( + self, + user_id: str, + job_id: str, + audio_content: bytes, + filename: str, + transcription_params: TranscriptionParams, + ): + """Store audio file with metadata in MinIO""" + with self.telemetry.tracer.start_as_current_span("store_audio") as span: + span.set_attribute("job_id", job_id) + span.set_attribute("user_id", user_id) + span.set_attribute("filename", filename) + try: + object_name = self._get_object_path(user_id, job_id, filename) + + # Convert transcription params to JSON string for metadata + params_json = json.dumps(transcription_params.model_dump()) + + # Create metadata dictionary with transcription params + metadata = {"X-Amz-Meta-Transcription-Params": params_json} + + self.client.put_object( + self.bucket_name, + object_name, + io.BytesIO(audio_content), + len(audio_content), + content_type="audio/mpeg", + metadata=metadata, + ) + logger.info( + f"Stored audio for user {user_id}, job {job_id} in MinIO as {object_name} with metadata" + ) + + except S3Error as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error(f"Failed to store audio in MinIO: {e}") + raise + + def get_podcast_audio(self, user_id: str, job_id: str) -> Optional[str]: + """Get the audio data for a specific podcast by job_id""" + with self.telemetry.tracer.start_as_current_span("get_podcast_audio") as span: + span.set_attribute("job_id", job_id) + span.set_attribute("user_id", user_id) + try: + # Find the file with matching user_id and job_id + prefix = f"{user_id}/{job_id}/" + objects = self.client.list_objects( + self.bucket_name, prefix=prefix, recursive=True + ) + + for obj in objects: + if obj.object_name.endswith(".mp3"): + span.set_attribute("audio_file", obj.object_name) + audio_data = self.client.get_object( + self.bucket_name, obj.object_name + ).read() + return base64.b64encode(audio_data).decode("utf-8") + + return None + + except Exception as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error( + f"Failed to get audio for user {user_id}, job {job_id}: {str(e)}" + ) + raise + + def get_file(self, user_id: str, job_id: str, filename: str) -> Optional[bytes]: + """Get any file from storage by user_id, job_id and filename""" + with self.telemetry.tracer.start_as_current_span("get_file") as span: + span.set_attribute("job_id", job_id) + span.set_attribute("user_id", user_id) + span.set_attribute("filename", filename) + try: + object_name = self._get_object_path(user_id, job_id, filename) + + try: + data = self.client.get_object(self.bucket_name, object_name).read() + return data + except S3Error as e: + span.set_attribute("error", str(e)) + if e.code == "NoSuchKey": + return None + raise + + except Exception as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error( + f"Failed to get file {filename} for user {user_id}, job {job_id}: {str(e)}" + ) + raise + + def delete_job_files(self, user_id: str, job_id: str) -> bool: + """Delete all files associated with a user_id and job_id""" + with self.telemetry.tracer.start_as_current_span("delete_job_files") as span: + span.set_attribute("job_id", job_id) + span.set_attribute("user_id", user_id) + try: + # List all objects with the user_id/job_id prefix + prefix = f"{user_id}/{job_id}/" + objects = self.client.list_objects( + self.bucket_name, prefix=prefix, recursive=True + ) + + # Delete each object + for obj in objects: + self.client.remove_object(self.bucket_name, obj.object_name) + logger.info(f"Deleted object: {obj.object_name}") + + return True + + except Exception as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error( + f"Failed to delete files for user {user_id}, job {job_id}: {str(e)}" + ) + return False + + def list_files_metadata(self, user_id: str = None): + """Lists metadata filtered by user_id if provided""" + with self.telemetry.tracer.start_as_current_span("list_files_metadata") as span: + try: + # If user_id is provided, use it as prefix to filter results + prefix = f"{user_id}/" if user_id else "" + span.set_attribute("user_id", user_id) + span.set_attribute("prefix", prefix) + + objects = self.client.list_objects( + self.bucket_name, prefix=prefix, recursive=True + ) + files = [] + + for obj in objects: + logger.info(f"Object: {obj.object_name}") + if obj.object_name.endswith("/"): + continue + + try: + stat = self.client.stat_object( + self.bucket_name, obj.object_name + ) + path_parts = obj.object_name.split("/") + logger.info(f"Path parts: {path_parts}") + + if not path_parts[-1].endswith(".mp3"): + continue + + # Update to handle new path structure: user_id/job_id/filename + user_id = path_parts[0] + job_id = path_parts[1] + + file_info = { + "user_id": user_id, + "job_id": job_id, + "filename": path_parts[-1], + "size": stat.size, + "created_at": obj.last_modified.isoformat(), + "path": obj.object_name, + "transcription_params": {}, + } + + if stat.metadata: + try: + params = stat.metadata.get( + "X-Amz-Meta-Transcription-Params" + ) + if params: + file_info["transcription_params"] = json.loads( + params + ) + except json.JSONDecodeError: + logger.warning( + f"Could not parse transcription params for {obj.object_name}" + ) + + files.append(file_info) + logger.info( + f"Found file: {obj.object_name}, size: {stat.size} bytes" + ) + + except Exception as e: + logger.error( + f"Error processing object {obj.object_name}: {str(e)}" + ) + continue + + files.sort(key=lambda x: x["created_at"], reverse=True) + logger.info( + f"Successfully listed {len(files)} metadata for {len(files)} files from MinIO" + ) + return files + + except Exception as e: + span.set_status(StatusCode.ERROR) + span.record_exception(e) + logger.error(f"Failed to list files from MinIO: {str(e)}") + raise diff --git a/frontend/static/diagram.png b/frontend/static/diagram.png new file mode 100644 index 0000000..249f2e0 Binary files /dev/null and b/frontend/static/diagram.png differ diff --git a/frontend/utils/email_demo.py b/frontend/utils/email_demo.py new file mode 100644 index 0000000..91bf1bd --- /dev/null +++ b/frontend/utils/email_demo.py @@ -0,0 +1,502 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests +import os +import json as json +import time +from datetime import datetime +from threading import Thread, Event +import websockets +import asyncio +from urllib.parse import urljoin +import argparse +from typing import List + +import smtplib +from email.message import EmailMessage +from email.mime.base import MIMEBase +from email import encoders + +# Add global TEST_USER_ID +TEST_USER_ID = "test-userid" + + +class StatusMonitor: + def __init__(self, base_url, job_id): + self.base_url = base_url + self.job_id = job_id + self.ws_url = self._get_ws_url(base_url) + self.stop_event = Event() + self.services = {"pdf", "agent", "tts"} + self.last_statuses = {service: None for service in self.services} + self.tts_completed = Event() + self.websocket = None + self.reconnect_delay = 1.0 + self.max_reconnect_delay = 30.0 + self.ready_event = asyncio.Event() + + def _get_ws_url(self, base_url): + """Convert HTTP URL to WebSocket URL""" + if base_url.startswith("https://"): + ws_base = "wss://" + base_url[8:] + else: + ws_base = "ws://" + base_url[7:] + return urljoin(ws_base, f"/ws/status/{self.job_id}") + + def get_time(self): + return datetime.now().strftime("%H:%M:%S") + + def start(self): + """Start the WebSocket monitoring in a separate thread""" + self.thread = Thread(target=self._run_async_loop) + self.thread.start() + + def stop(self): + """Stop the WebSocket monitoring""" + self.stop_event.set() + self.thread.join() + + def _run_async_loop(self): + """Run the asyncio event loop in a separate thread""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(self._monitor_status()) + + async def _monitor_status(self): + while not self.stop_event.is_set(): + try: + async with websockets.connect(self.ws_url) as websocket: + self.websocket = websocket + self.reconnect_delay = 1.0 + print(f"[{self.get_time()}] Connected to status WebSocket") + + while not self.stop_event.is_set(): + try: + message = await asyncio.wait_for( + websocket.recv(), timeout=30 + ) + + # Handle ready check message + try: + data = json.loads(message) + if data.get("type") == "ready_check": + await websocket.send("ready") + print( + f"[{self.get_time()}] Sent ready acknowledgment" + ) + continue + except json.JSONDecodeError: + pass + + await self._handle_message(message) + except asyncio.TimeoutError: + try: + pong_waiter = await websocket.ping() + await pong_waiter + except Exception: + break + + except websockets.exceptions.ConnectionClosed: + self.ready_event.clear() + if not self.stop_event.is_set(): + print( + f"[{self.get_time()}] WebSocket connection closed, reconnecting..." + ) + + except Exception as e: + self.ready_event.clear() + if not self.stop_event.is_set(): + print(f"[{self.get_time()}] WebSocket error: {e}, reconnecting...") + + if not self.stop_event.is_set(): + await asyncio.sleep(self.reconnect_delay) + self.reconnect_delay = min( + self.reconnect_delay * 1.5, self.max_reconnect_delay + ) + + async def _handle_message(self, message): + """Handle incoming WebSocket messages""" + try: + data = json.loads(message) + service = data.get("service") + status = data.get("status") + msg = data.get("message", "") + + if service in self.services: + current_status = f"{service}: {status} - {msg}" + if current_status != self.last_statuses[service]: + print(f"[{self.get_time()}] {current_status}") + self.last_statuses[service] = current_status + + if status == "failed": + print(f"[{self.get_time()}] Job failed in {service}: {msg}") + self.stop_event.set() + + if service == "tts" and status == "completed": + self.tts_completed.set() + self.stop_event.set() + + except json.JSONDecodeError: + print(f"[{self.get_time()}] Received invalid JSON: {message}") + except Exception as e: + print(f"[{self.get_time()}] Error processing message: {e}") + + +def get_output_with_retry(base_url: str, job_id: str, max_retries=5, retry_delay=1): + """Retry getting output with exponential backoff""" + for attempt in range(max_retries): + try: + response = requests.get( + f"{base_url}/output/{job_id}", params={"userId": TEST_USER_ID} + ) + if response.status_code == 200: + return response.content + elif response.status_code == 404: + wait_time = retry_delay * (2**attempt) + print( + f"[datetime.now().strftime('%H:%M:%S')] Output not ready yet, retrying in {wait_time:.1f}s..." + ) + time.sleep(wait_time) + continue + else: + response.raise_for_status() + except requests.RequestException as e: + print(f"[datetime.now().strftime('%H:%M:%S')] Error getting output: {e}") + if attempt == max_retries - 1: + raise + time.sleep(retry_delay * (2**attempt)) + + raise TimeoutError("Failed to get output after maximum retries") + + +def test_saved_podcasts(base_url: str, job_id: str, max_retries=5, retry_delay=5): + """Test the saved podcasts endpoints with retry logic""" + print( + f"\n[{datetime.now().strftime('%H:%M:%S')}] Testing saved podcasts endpoints..." + ) + + # Test 1: Get all saved podcasts with retry + print("\nTesting list all podcasts endpoint...") + for attempt in range(max_retries): + response = requests.get( + f"{base_url}/saved_podcasts", params={"userId": TEST_USER_ID} + ) + assert ( + response.status_code == 200 + ), f"Failed to get saved podcasts: {response.text}" + podcasts = response.json()["podcasts"] + print(f"Found {len(podcasts)} saved podcasts") + + # Check if our job_id is in the list + job_ids = [podcast["job_id"] for podcast in podcasts] + if job_id in job_ids: + print(f"Successfully found job_id {job_id} in saved podcasts list") + break + elif attempt < max_retries - 1: + wait_time = retry_delay * (2**attempt) + print( + f"Job ID not found yet, retrying in {wait_time:.1f}s... (attempt {attempt + 1}/{max_retries})" + ) + time.sleep(wait_time) + continue + else: + assert False, f"Recently created job_id {job_id} not found in saved podcasts after {max_retries} attempts" + + # Test 2: Get specific podcast metadata + print("\nTesting individual podcast metadata endpoint...") + response = requests.get( + f"{base_url}/saved_podcast/{job_id}/metadata", params={"userId": TEST_USER_ID} + ) + assert ( + response.status_code == 200 + ), f"Failed to get podcast metadata: {response.text}" + metadata = response.json() + print(f"Retrieved metadata for podcast: {metadata.get('filename', 'unknown')}") + print(f"Metadata: {json.dumps(metadata, indent=2)}") + + # Test 3: Get specific podcast audio + print("\nTesting individual podcast audio endpoint...") + response = requests.get( + f"{base_url}/saved_podcast/{job_id}/audio", params={"userId": TEST_USER_ID} + ) + assert response.status_code == 200, f"Failed to get podcast audio: {response.text}" + audio_data = response.content + print(f"Successfully retrieved audio data, size: {len(audio_data)} bytes") + + +def test_api( + base_url: str, + target_files: List[str], + context_files: List[str], + email: str, + monologue: bool = False, + vdb: bool = False, +): + voice_mapping = { + "speaker-1": "iP95p4xoKVk53GoZ742B", + } + + if not monologue: + voice_mapping["speaker-2"] = "9BWtsMINqrJLrRacOk9x" + + process_url = f"{base_url}/process_pdf" + + # Update path resolution + current_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname(current_dir) + samples_dir = os.path.join(project_root, "samples") + + # Prepare the payload with updated schema and userId + transcription_params = { + "name": "ishan-test", + "duration": 2, + "speaker_1_name": "Bob", + "voice_mapping": voice_mapping, + "guide": None, + "monologue": monologue, + "userId": TEST_USER_ID, + "vdb_task": vdb, + } + + if not monologue: + transcription_params["speaker_2_name"] = "Kate" + + print( + f"\n[{datetime.now().strftime('%H:%M:%S')}] Submitting PDFs for processing..." + ) + print(f"Using voices: {voice_mapping}") + + # Prepare multipart form data + form_data = [] + + # Process target files + for pdf_file in target_files: + if not os.path.isabs(pdf_file): + pdf_file = os.path.join(samples_dir, pdf_file) + + with open(pdf_file, "rb") as f: + content = f.read() + form_data.append( + ( + "target_files", + (os.path.basename(pdf_file), content, "application/pdf"), + ) + ) + + # Process context files + if context_files is not None: + for pdf_file in context_files: + if not os.path.isabs(pdf_file): + pdf_file = os.path.join(samples_dir, pdf_file) + + with open(pdf_file, "rb") as f: + content = f.read() + form_data.append( + ( + "context_files", + (os.path.basename(pdf_file), content, "application/pdf"), + ) + ) + + # Add transcription parameters + form_data.append(("transcription_params", (None, json.dumps(transcription_params)))) + + try: + response = requests.post(process_url, files=form_data) + + assert ( + response.status_code == 202 + ), f"Expected status code 202, but got {response.status_code}. Response: {response.text}" + job_data = response.json() + assert "job_id" in job_data, "Response missing job_id" + job_id = job_data["job_id"] + print(f"[{datetime.now().strftime('%H:%M:%S')}] Job ID received: {job_id}") + + # Step 2: Start monitoring status via WebSocket + monitor = StatusMonitor(base_url, job_id) + monitor.start() + + try: + # Wait for TTS completion or timeout + max_wait = 40 * 60 + if not monitor.tts_completed.wait(timeout=max_wait): + raise TimeoutError(f"Test timed out after {max_wait} seconds") + + # If we get here, TTS completed successfully + print( + f"\n[{datetime.now().strftime('%H:%M:%S')}] TTS processing completed, retrieving audio file..." + ) + + # Get the final output with retry logic + audio_content = get_output_with_retry(base_url, job_id) + + # Save the audio file + output_path = os.path.join("/project/frontend/demo_outputs/", str(email[0]).split('@')[0] + "-output.mp3") + with open(output_path, "wb") as f: + f.write(audio_content) + print( + f"[{datetime.now().strftime('%H:%M:%S')}] Audio file saved as '{output_path}'" + ) + + # Test saved podcasts endpoints with the newly created job_id + test_saved_podcasts(base_url, job_id) + + # Test RAG endpoint if vdb flag is enabled + if vdb: + print("\nTesting RAG endpoint...") + test_query = "What is the main topic of this document?" + rag_response = requests.post( + f"{base_url}/query_vector_db", + json={"query": test_query, "k": 3, "job_id": job_id}, + ) + assert ( + rag_response.status_code == 200 + ), f"RAG endpoint failed: {rag_response.text}" + rag_results = rag_response.json() + print(f"RAG Query: '{test_query}'") + print(f"RAG Results: {json.dumps(rag_results, indent=2)}") + + finally: + monitor.stop() + return job_id + + except Exception as e: + print(f"Error during PDF submission: {e}") + raise + +def send_file_via_email(file_location, sender_email, recipient_email): + # Email configuration + print("Configuring Sender Details") + sender_email = sender_email + email_password = os.environ['SENDER_EMAIL_PASSWORD'] + smtp_server = "smtp.gmail.com" + smtp_port = 587 + + # Create email message + print("Configuring Email Message") + msg = EmailMessage() + msg['Subject'] = '[Auto-generated] Your podcast is here!' + msg['From'] = sender_email + msg['To'] = recipient_email + msg.set_content('Your generated podcast is here! Please find the attached file.\n\nThank you for using the AI Research Assistant Blueprint powered by NVIDIA AI Workbench!') + + # Add file attachment + print("Adding podcast as attachment") + with open(file_location, 'rb') as file: + file_data = file.read() + file_name = os.path.basename(file_location) + + msg.add_attachment(file_data, maintype='application', subtype='octet-stream', filename=file_name) + + # Send email + print("Attempting to send email.") + try: + with smtplib.SMTP(smtp_server, smtp_port) as server: + server.starttls() + server.login(sender_email, email_password) + server.send_message(msg) + print(f"File '{file_name}' sent successfully to {recipient_email}") + except Exception as e: + print(f"An error occurred: {str(e)}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Process PDF files for audio conversion", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" + Examples: + # Process with target and context files + python test.py --target main.pdf --context context1.pdf context2.pdf + + # Process with only context files + python test.py --context file1.pdf file2.pdf file3.pdf + + # Process with multiple target files + python test.py --target target1.pdf target2.pdf --context context1.pdf + """, + ) + + parser.add_argument( + "--target", + nargs="+", + default=[], + help="PDF files to use as targets", + metavar="PDF", + ) + parser.add_argument( + "--context", + nargs="+", + default=[], + help="PDF files to use as context", + metavar="PDF", + ) + parser.add_argument( + "--api-url", + default=os.getenv("API_SERVICE_URL", "http://localhost:8002"), + help="API service URL (default: from API_SERVICE_URL env var or http://localhost:8002)", + ) + parser.add_argument( + "--monologue", + action="store_true", + help="Generate a monologue instead of a dialogue", + ) + parser.add_argument( + "--vdb", + action="store_true", + help="Enable Vector Database processing", + ) + parser.add_argument( + "--sender_email", + nargs="+", + default="", + help="Sender email for podcast recording (GMail)", + metavar="EMAIL", + ) + parser.add_argument( + "--recipient_email", + nargs="+", + default="", + help="Recipient email for podcast recording", + metavar="EMAIL", + ) + + args = parser.parse_args() + + # Validate that at least one file was provided + if not args.target and not args.context: + parser.error( + "At least one PDF file must be provided (either target or context)" + ) + + print(f"API URL: {args.api_url}") + print(f"Target PDF files: {args.target}") + print(f"Context PDF files: {args.context}") + print(f"Sender Email: {args.sender_email}") + print(f"Recipient Email: {args.recipient_email}") + print(f"Monologue mode: {args.monologue}") + print(f"VDB mode: {args.vdb}") + print(f"Using test user ID: {TEST_USER_ID}") + + test_api( + args.api_url, + args.target, + args.context, + args.recipient_email, + args.monologue, + args.vdb, + ) + + send_file_via_email("/project/frontend/demo_outputs/" + str(args.recipient_email[0]).split('@')[0] + "-output.mp3", str(args.sender_email[0]), str(args.recipient_email[0])) diff --git a/frontend/utils/logger.py b/frontend/utils/logger.py new file mode 100644 index 0000000..f475a81 --- /dev/null +++ b/frontend/utils/logger.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +""" A logger class for capturing stdout and displaying it to the user on the app. """ + +class Logger: + def __init__(self, filename): + self.terminal = sys.stdout + self.log = open(filename, "w") + + def write(self, message): + self.terminal.write(message) + self.log.write(message) + + def flush(self): + self.terminal.flush() + self.log.flush() + + def isatty(self): + return False + +def read_logs(): + sys.stdout.flush() + with open("/project/frontend/output.log", "r") as f: + return f.read() \ No newline at end of file diff --git a/launchable/.gitkeep b/launchable/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/launchable/PDFtoPodcast.ipynb b/launchable/PDFtoPodcast.ipynb index 1fe4570..a55691e 100644 --- a/launchable/PDFtoPodcast.ipynb +++ b/launchable/PDFtoPodcast.ipynb @@ -616,7 +616,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/models.json b/models.json index 99284e3..acab7e8 100644 --- a/models.json +++ b/models.json @@ -11,4 +11,4 @@ "name": "meta/llama-3.1-70b-instruct", "api_base": "https://integrate.api.nvidia.com/v1" } -} +} \ No newline at end of file diff --git a/postBuild.bash b/postBuild.bash new file mode 100644 index 0000000..c5143e5 --- /dev/null +++ b/postBuild.bash @@ -0,0 +1,5 @@ +#!/bin/bash +# This file contains bash commands that will be executed at the end of the container build process, +# after all system packages and programming language specific package have been installed. +# +# Note: This file may be removed if you don't need to use it diff --git a/preBuild.bash b/preBuild.bash new file mode 100644 index 0000000..e2866a1 --- /dev/null +++ b/preBuild.bash @@ -0,0 +1,5 @@ +#!/bin/bash +# This file contains bash commands that will be executed at the beginning of the container build process, +# before any system packages or programming language specific package have been installed. +# +# Note: This file may be removed if you don't need to use it diff --git a/requirements.txt b/requirements.txt index cb72513..0f28d20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,6 @@ minio httpx jinja2 ruff -ujson \ No newline at end of file +ujson +jupyterlab>3.0 +gradio==4.43.0 diff --git a/samples/.gitkeep b/samples/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/.gitkeep b/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/shared/.gitkeep b/shared/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/variables.env b/variables.env new file mode 100644 index 0000000..d5224a9 --- /dev/null +++ b/variables.env @@ -0,0 +1,8 @@ +# Set environment variables in the format KEY=VALUE, 1 per line +# This file will be sourced inside the project container when started. +# NOTE: If you change this file while the project is running, you must restart the project container for changes to take effect. + +MAX_CONCURRENT_REQUESTS=1 +API_SERVICE_URL=http://pdf-to-podcast-api-service-1:8002 +AI_WORKBENCH_FLAG=true +SENDER_EMAIL=To use email, you must also add SENDER_EMAIL_PASSWORD as a Secret! diff --git a/workbench/PDFtoPodcast.ipynb b/workbench/PDFtoPodcast.ipynb new file mode 100644 index 0000000..3984047 --- /dev/null +++ b/workbench/PDFtoPodcast.ipynb @@ -0,0 +1,415 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6b998a5c-45a6-4f05-9c7c-b0f3c4c74c17", + "metadata": {}, + "source": [ + "# PDF to Podcast NVIDIA AI Blueprint" + ] + }, + { + "cell_type": "markdown", + "id": "8a0f2acb-52d9-49dd-9676-2d58715786f5", + "metadata": {}, + "source": [ + "Ever wished you could generate podcasts from your own private data? The PDF to podcast NVIDIA AI blueprint makes it possible to build an AI research assistant that creates engaging audio outputs from PDF files. At its core, PDF to podcast is a sophisticated system that:\n", + "\n", + "- Transforms dense PDF documents into natural, engaging conversations\n", + "- Creates AI-generated podcasts with either single-speaker or two-person formats\n", + "- Uses cutting-edge language models (powered by LLama 3.1-70B NIM, LLama 3.1-8B NIM, and LLama 3.1-405B NIM) to ensure high-quality content\n", + "- Leverages ElevenLabs' voice synthesis for natural-sounding audio" + ] + }, + { + "cell_type": "markdown", + "id": "51dd4f69", + "metadata": {}, + "source": [ + "![image](data:image/jpeg;base64,/9j/4Q/+RXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABgAAAAAQAAAGAAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAA8CgAwAEAAAAAQAAAhykBgADAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9sAhAABAQEBAQECAQECAwICAgMEAwMDAwQFBAQEBAQFBgUFBQUFBQYGBgYGBgYGBwcHBwcHCAgICAgJCQkJCQkJCQkJAQEBAQICAgQCAgQJBgUGCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQn/3QAEADz/wAARCAIcA8ADASIAAhEBAxEB/8QBogAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoLEAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+foBAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKCxEAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/iiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Q/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/0f7+KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKY7pGhdyAo6k9BXgd78Vtd8WuYfhXBC1j0/ti9DG2fHH+iwoVe5H/TTdHEf4XegD37IFGRXzNJ4a1/Uv3niHxNq1y/923lSxjH+6tsiNj/edj70n/CEWn/QV1v/AMG17/8A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ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/1v7+KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA/9f+/iiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Q/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/2Q==)\n" + ] + }, + { + "cell_type": "markdown", + "id": "5af66273-f19d-4d4f-8480-31c5c03c5951", + "metadata": {}, + "source": [ + "## Features\n", + "What sets PDF to podcast apart is its flexible, microservice-driven architecture. Whether you're running on a single machine or a distributed cluster, this blueprint can be adapted to your needs and comes packed with modern tooling and observability features. **We've built this for you to edit and deploy on your own infrastructure with ease.**\n", + "\n", + "#### Observability & Monitoring\n", + "- **Jaeger Tracing ๐Ÿ”** - experience full distributed tracing built in. Watch requests flow through different services and quickly identify bottlenecks in your processing pipeline as you add your own code\n", + "- **MinIO Object Storage ๐Ÿ“ฆ** - robust, S3-compatible storage for handling PDFs and generated audio content. Perfect for scaling from development to production workloads.\n", + "- **GPU Optimized PDF processing ๐Ÿ”ฅ** - use docling for lighting fast optimized PDF processing\n", + "\n", + "#### Development Experience\n", + "- **UV Package Management ๐Ÿš€** - Lightning-fast dependency management using UV, making environment setup a breeze with `make uv`\n", + "- **Docker Compose Integration ๐Ÿณ** - one click spins up the entire stack, with smart handling of GPU resources and service dependencies.\n", + "\n", + "#### Quality and Testing\n", + "- **Automated Quality Checks โœจ** - integrated `ruff` for Python linting and formatting, ensuring consistent code quality across contributions.\n", + "- **End-to-End Testing ๐Ÿงช** - comprehensive test suite for verifying podcast generation, from PDF ingestion to final audio output." + ] + }, + { + "cell_type": "markdown", + "id": "2293d461-8cd1-48fb-bce7-dbf8515ea3f4", + "metadata": {}, + "source": [ + "## Create a podcast!\n", + "\n", + "For this example, we'll directly call the API to generate the podcast. First we write some helper functions to interact with the API\n", + "\n", + "
\n", + " Important: Ensure the docker compose services are running. In NVIDIA AI Workbench, navigate to Environment > Compose > Start. If you would like to leverage a locally-running Llama-3.1-8b-instruct model for this blueprint, ensure the \"local\" profile is selected before starting compose in AI Workbench." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61e18296-a63f-4745-9d5a-3f0148ffabe9", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import json\n", + "import time\n", + "from typing import List\n", + "from IPython.display import Audio\n", + "from pathlib import Path\n", + "import os\n", + "\n", + "BASE_URL = os.environ[\"API_SERVICE_URL\"]\n", + "\n", + "def generate_podcast(\n", + " target_pdf_paths: List[str], \n", + " name: str,\n", + " duration: int,\n", + " speaker_1_name: str,\n", + " context_pdf_paths: List[str] = None,\n", + " is_monologue: bool = False,\n", + " speaker_2_name: str = None,\n", + " guide: str = None\n", + ") -> str:\n", + " \"\"\"\n", + " Generate a podcast using the API.\n", + " \n", + " Args:\n", + " target_pdf_paths: List of paths to main PDFs to analyze\n", + " name: Name of the podcast\n", + " duration: Desired duration in minutes\n", + " speaker_1_name: Name of the first speaker\n", + " context_pdf_paths: Optional list of paths to context PDFs\n", + " is_monologue: Whether to generate a monologue\n", + " speaker_2_name: Name of second speaker (required if not monologue)\n", + " guide: Optional guidance for the podcast structure\n", + " \"\"\"\n", + " # Handle single path inputs\n", + " if isinstance(target_pdf_paths, str):\n", + " target_pdf_paths = [target_pdf_paths]\n", + " if isinstance(context_pdf_paths, str):\n", + " context_pdf_paths = [context_pdf_paths]\n", + " \n", + " files = []\n", + " \n", + " # Add all target PDFs\n", + " for pdf_path in target_pdf_paths:\n", + " content = Path(pdf_path).read_bytes()\n", + " files.append(('target_files', (Path(pdf_path).name, content, 'application/pdf')))\n", + " \n", + " # Add all context PDFs if provided\n", + " if context_pdf_paths:\n", + " for pdf_path in context_pdf_paths:\n", + " content = Path(pdf_path).read_bytes()\n", + " files.append(('context_files', (Path(pdf_path).name, content, 'application/pdf')))\n", + " \n", + " # Configure voice mapping\n", + " voice_mapping = {\n", + " \"speaker-1\": \"iP95p4xoKVk53GoZ742B\" \n", + " }\n", + " if not is_monologue:\n", + " voice_mapping[\"speaker-2\"] = \"9BWtsMINqrJLrRacOk9x\"\n", + " \n", + " # Create parameters\n", + " params = {\n", + " \"userId\": \"test-userid\",\n", + " \"name\": name,\n", + " \"duration\": duration,\n", + " \"monologue\": is_monologue,\n", + " \"speaker_1_name\": speaker_1_name,\n", + " \"voice_mapping\": voice_mapping,\n", + " \"guide\": guide,\n", + " \"vdb_task\": False\n", + " }\n", + " if not is_monologue:\n", + " params[\"speaker_2_name\"] = speaker_2_name\n", + " \n", + " response = requests.post(\n", + " f\"{BASE_URL}/process_pdf\", \n", + " files=files,\n", + " data={'transcription_params': json.dumps(params)}\n", + " )\n", + " if response.status_code != 202:\n", + " raise Exception(f\"Failed to submit podcast generation: {response.text}\")\n", + " \n", + " return response.json()['job_id']\n", + "\n", + "def get_status(job_id: str) -> dict:\n", + " \"\"\"Get the current status of all services for a job.\"\"\"\n", + " response = requests.get(f\"{BASE_URL}/status/{job_id}?userId=test-userid\")\n", + " if response.status_code != 200:\n", + " raise Exception(f\"Failed to get status: {response.text}\")\n", + " return response.json()\n", + "\n", + "def wait_for_completion(job_id: str, check_interval: int = 5, initial_delay: int = 10):\n", + " \"\"\"\n", + " Poll the status endpoint until the podcast is ready.\n", + " Shows a simplified progress view.\n", + " \"\"\"\n", + " print(f\"Waiting {initial_delay} seconds for job to initialize...\")\n", + " time.sleep(initial_delay)\n", + " \n", + " last_messages = {} # Track last message for each service to avoid duplication\n", + " \n", + " while True:\n", + " try:\n", + " statuses = get_status(job_id)\n", + " \n", + " # Check each service and only print if status changed\n", + " for service, status in statuses.items():\n", + " current_msg = status.get('message', '')\n", + " if current_msg != last_messages.get(service):\n", + " print(f\"[{service.upper()}] {current_msg}\")\n", + " last_messages[service] = current_msg\n", + " \n", + " # Check if everything is completed\n", + " all_completed = all(\n", + " status.get('status') == 'JobStatus.COMPLETED' \n", + " for status in statuses.values()\n", + " )\n", + " \n", + " if all_completed and 'tts' in statuses:\n", + " print(\"\\nPodcast generation completed!\")\n", + " return\n", + " \n", + " # Check for failures\n", + " for service, status in statuses.items():\n", + " if status.get('status') == 'JobStatus.FAILED':\n", + " raise Exception(f\"Service {service} failed: {status.get('message')}\")\n", + " \n", + " time.sleep(check_interval)\n", + " \n", + " except requests.exceptions.RequestException as e:\n", + " if \"Job not found\" in str(e):\n", + " print(\"Waiting for job to start...\")\n", + " time.sleep(check_interval)\n", + " continue\n", + " raise\n", + " except Exception as e:\n", + " print(f\"Error: {e}\")\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "id": "1ddfee58-4bd2-40ad-a2aa-0c10d14a3e23", + "metadata": {}, + "source": [ + "Next you will generate a monologue using various analyst reports on NVIDIAs most recent financial earnings. You can also add your own PDFs to this Jupyter Lab and point to them in code below. Note that context PDFs are optional and can be used to provide additional context for the generation process. Additonally, you can provide a `guide` to help guide the generation process.\n", + "\n", + "**Agent Configuration**: The NVIDIA NIM microservices used for the long agent reasoning step are configurable under ``models.json``. By default the blueprint utilizes Build API endpoints, but this is customizable. \n", + "\n", + "For example, if you would like to leverage a locally-running Llama-3.1-8b-instruct model, ensure the ``local`` profile is selected before starting compose in AI Workbench, and adjust ``models.json`` to the following:\n", + "\n", + "```json\n", + "{\r\n", + " \"reasoning\": {\r\n", + " \"name\": \"meta/llama-3.1-8b-instruct\",\r\n", + " \"api_base\": \"http://pdf-to-podcast-local-nim-1:8000/v1\"\r\n", + " },\r\n", + " ...\r\n", + "}\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfc3a2b6-8930-412d-8a6f-678fad2de97c", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " print(\"Submitting podcast generation request...\")\n", + " job_id = generate_podcast(\n", + " target_pdf_paths=[\n", + " \"../samples/investorpres-main.pdf\",\n", + " ],\n", + " context_pdf_paths=[\n", + " \"../samples/bofa-context.pdf\",\n", + " \"../samples/citi-context.pdf\"\n", + " ],\n", + " name=\"NVIDIA Earnings Analysis\",\n", + " duration=15,\n", + " speaker_1_name=\"Alex\",\n", + " is_monologue=True,\n", + " guide=\"Focus on NVIDIA's earnings and the key points driving it's growth\"\n", + " )\n", + " print(f\"Job ID: {job_id}\")\n", + " wait_for_completion(job_id)\n", + "except Exception as e:\n", + " print(f\"Error: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "a764dfff", + "metadata": {}, + "source": [ + "You can also generate a 2 person podcast by calling the same function but setting `is_monologue=False` and providing a `speaker_2_name`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2a24fa8-b024-4a79-9214-d129a6b8c382", + "metadata": {}, + "outputs": [], + "source": [ + "!curl \"pdf-to-podcast-api-service-1:8002/output/{job_id}?userId=test-userid\" --output temp_audio.mp3\n", + "Audio(\"temp_audio.mp3\")" + ] + }, + { + "cell_type": "markdown", + "id": "32d8f529-09d4-43fc-8667-bfa48fd9d029", + "metadata": {}, + "source": [ + "## Understanding Your Generated Podcast\n", + "\n", + "After generating your podcast, you have access to a couple valuable outputs that provide insights into the generation process and content. Here's what endpoints you can use to explore:\n", + "\n", + "#### 1. The Transcript\n", + "```python\n", + "/saved_podcast/{job_id}/transcript\"\n", + "```\n", + "\n", + "The transcript provides a text version of your podcast, which is valuable for:\n", + "- Quick content review without audio playback\n", + "- Creating show notes or content summaries\n", + "- Finding and quoting specific discussion points\n", + "- Making content searchable and referenceable\n", + "- Ensuring accessibility of your content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f01e8f10-6ca5-4143-9883-b479006155b8", + "metadata": {}, + "outputs": [], + "source": [ + "!curl \"pdf-to-podcast-api-service-1:8002/saved_podcast/{job_id}/transcript?userId=test-userid\"" + ] + }, + { + "cell_type": "markdown", + "id": "c4180391-05e9-4d5d-a66c-1e6dc6bb3163", + "metadata": {}, + "source": [ + "#### 2. Prompt and Generation History\n", + "```python\n", + "/saved_podcast/{job_id}/history\n", + "```\n", + "\n", + "The history reveals the AI's thought process, showing you:\n", + "- How the system analyzed and interpreted your PDFs\n", + "- Key topics and themes identified\n", + "- The structural decisions made for the conversation\n", + "- The reasoning behind content organization\n", + "- How different sections were prioritized and connected\n", + "\n", + "This is particularly useful for:\n", + "- Understanding how the AI makes decisions\n", + "- Improving future podcast generations\n", + "- Verifying content accuracy and relevance\n", + "- Fine-tuning and evals on your prompts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbe01fc0-e111-4fe0-9530-fe549e5892d0", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "!curl \"pdf-to-podcast-api-service-1:8002/saved_podcast/{job_id}/history?userId=test-userid\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5642285-2746-47fc-9b47-9f58d33301bb", + "metadata": {}, + "outputs": [], + "source": [ + "!curl \"pdf-to-podcast-api-service-1:8002/saved_podcast/{job_id}/metadata?userId=test-userid\"" + ] + }, + { + "cell_type": "markdown", + "id": "aaae77e9", + "metadata": {}, + "source": [ + "### Tools for Understanding Your Generated Podcast\n", + "\n", + "After generating your podcast, you can explore the generation process through several tools:\n", + "\n", + "#### 1. Jaeger Tracing Interface\n", + "Access Jaeger at `:16686` to:\n", + "- Visualize the complete request flow\n", + "- Debug processing bottlenecks\n", + "- Monitor service performance\n", + "- Track PDF processing and audio generation stages\n", + "\n", + "#### 2. MinIO Object Storage\n", + "Access MinIO at `:9001` with:\n", + "```\n", + "Username: minioadmin\n", + "Password: minioadmin\n", + "```\n", + "Here you can:\n", + "- Browse generated audio files\n", + "- Access intermediate processing artifacts\n", + "- View stored PDF documents\n", + "- Download or share content via presigned URLs\n", + "\n", + "#### 3. API Endpoints\n", + "You can access the API endpoint at `:8002/docs`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/workbench/README.md b/workbench/README.md new file mode 100644 index 0000000..742103f --- /dev/null +++ b/workbench/README.md @@ -0,0 +1,60 @@ +# NVIDIA AI Workbench: Introduction [![Open In AI Workbench](https://img.shields.io/badge/Open_In-AI_Workbench-76B900)](https://ngc.nvidia.com/open-ai-workbench/aHR0cHM6Ly9naXRodWIuY29tL05WSURJQS1BSS1CbHVlcHJpbnRzL3BkZi10by1wb2RjYXN0) + + + + + +

+ :arrow_down: Download AI Workbench โ€ข + :book: Read the Docs โ€ข + :open_file_folder: Explore Example Projects โ€ข + :rotating_light: Facing Issues? Let Us Know! +

+ + +## Quickstart + +If you do not NVIDIA AI Workbench installed, first complete the installation for AI Workbench [here](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/). + +Let's get started! + +1. (Optional) Fork this Project to your own GitHub namespace and copy the link. + + | :bulb: Tip | + | :-----------------------| + | We recommend forking this project to your own namespace as gives you write access for customization. You can still use this project without forking, but any changes you make will not be able to be pushed to the upstream repo as it is owned by NVIDIA. | + +2. Open the NVIDIA AI Workbench Desktop App. Select a location to work in. + +3. Clone this Project onto your desired machine by selecting **Clone Project** and providing the GitHub link. + +4. Wait for the project to build. You can expand the bottom **Building** indicator to view real-time build logs. + +5. When the build completes, set the following configurations. + + * `Environment` → `Secrets` → `Configure`. Specify the ``NVIDIA_API_KEY`` and ``ELEVENLABS_API_KEY`` Key. + * (Optional) Add a ``SENDER_EMAIL`` variable and a ``SENDER_EMAIL_PASSWORD`` secret to the project to use the email functionality on the frontend application. Gmail sender accounts are currently supported; you can create an App Password for your account [here](https://support.google.com/mail/answer/185833). + +6. Navigate to `Environment` → `Compose` and **Start** the Docker compose services. You can view progress under **Output** on the bottom left and selecting **Compose** logs from the dropdown. It may take a few minutes to pull and build the services. + + * The blueprint defaults to Build API endpoints. The services are ready when you see the following in the compose logs: + + ``` + celery-worker-1 | [2025-01-24 21:10:55,239: INFO/MainProcess] celery@ee170af41d1b ready. + ``` + + * To run the blueprint with a _locally-running_ Llama 3.1 8B Instruct NVIDIA NIM, be sure to specify the ``local`` profile from the profile dropdown before selecting **Start**. The services are ready when you see the following in the compose logs: + + ``` + local-nim-1 | INFO 2025-01-24 21:14:50.213 metrics.py:351] Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 0 reqs, Swapped: 0 reqs, Pending: 0 reqs, GPU KV cache usage: 0.0%, CPU KV cache usage: 0.0%. + ``` + + +8. (Option 1) Run the **Jupyter Notebook**. On the top right of the AI Workbench window, select **Open Jupyterlab**. Navigate to ``workbench/PDFtoPodcast.ipynb``, skip the setup sections, and get started immediately with the provided sample PDFs. + +9. (Option 2) Run the **Frontend application**. On the top right of the AI Workbench window, select **Open Frontend**. + + * Upload your own locally-stored, custom PDFs + * View and download your generated podcast locally + * Specify your agent parameters (local vs Build endpoints), + * (optional) Email your generated podcast to a recipient