This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. .env.example +0 -6
  2. .gitattributes +0 -2
  3. .gitignore +0 -1
  4. .idea/.gitignore +3 -0
  5. .idea/inspectionProfiles/Project_Default.xml +14 -0
  6. .idea/inspectionProfiles/profiles_settings.xml +6 -0
  7. .idea/misc.xml +7 -0
  8. .idea/modules.xml +8 -0
  9. .idea/slide-deck-ai.iml +10 -0
  10. .idea/vcs.xml +6 -0
  11. .streamlit/config.toml +0 -10
  12. README.md +14 -100
  13. app.py +211 -488
  14. clarifai_grpc_helper.py +71 -0
  15. examples/example_04.json +0 -3
  16. file_embeddings/embeddings.npy +0 -3
  17. file_embeddings/icons.npy +0 -3
  18. global_config.py +14 -177
  19. helpers/__init__.py +0 -0
  20. helpers/file_manager.py +0 -40
  21. helpers/icons_embeddings.py +0 -166
  22. helpers/image_search.py +0 -148
  23. helpers/llm_helper.py +0 -257
  24. helpers/pptx_helper.py +0 -1084
  25. helpers/text_helper.py +0 -83
  26. icons/png128/0-circle.png +0 -0
  27. icons/png128/1-circle.png +0 -0
  28. icons/png128/123.png +0 -0
  29. icons/png128/2-circle.png +0 -0
  30. icons/png128/3-circle.png +0 -0
  31. icons/png128/4-circle.png +0 -0
  32. icons/png128/5-circle.png +0 -0
  33. icons/png128/6-circle.png +0 -0
  34. icons/png128/7-circle.png +0 -0
  35. icons/png128/8-circle.png +0 -0
  36. icons/png128/9-circle.png +0 -0
  37. icons/png128/activity.png +0 -0
  38. icons/png128/airplane.png +0 -0
  39. icons/png128/alarm.png +0 -0
  40. icons/png128/alien-head.png +0 -0
  41. icons/png128/alphabet.png +0 -0
  42. icons/png128/amazon.png +0 -0
  43. icons/png128/amritsar-golden-temple.png +0 -0
  44. icons/png128/amsterdam-canal.png +0 -0
  45. icons/png128/amsterdam-windmill.png +0 -0
  46. icons/png128/android.png +0 -0
  47. icons/png128/angkor-wat.png +0 -0
  48. icons/png128/apple.png +0 -0
  49. icons/png128/archive.png +0 -0
  50. icons/png128/argentina-obelisk.png +0 -0
.env.example DELETED
@@ -1,6 +0,0 @@
1
- # Example .env file for SlideDeck AI
2
- # Add your API keys and configuration values here
3
-
4
- # OpenRouter API key (if using OpenRouter as a provider)
5
-
6
- OPENROUTER_API_KEY=your-openrouter-api-key
 
 
 
 
 
 
 
.gitattributes CHANGED
@@ -33,5 +33,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- *.pptx filter=lfs diff=lfs merge=lfs -text
37
- pptx_templates/Minimalist_sales_pitch.pptx filter=lfs diff=lfs merge=lfs -text
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
.gitignore CHANGED
@@ -144,4 +144,3 @@ dmypy.json
144
  # Cython debug symbols
145
  cython_debug/
146
 
147
- .idea
 
144
  # Cython debug symbols
145
  cython_debug/
146
 
 
.idea/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
.idea/inspectionProfiles/Project_Default.xml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
5
+ <option name="ignoredPackages">
6
+ <value>
7
+ <list size="1">
8
+ <item index="0" class="java.lang.String" itemvalue="numpy" />
9
+ </list>
10
+ </value>
11
+ </option>
12
+ </inspection_tool>
13
+ </profile>
14
+ </component>
.idea/inspectionProfiles/profiles_settings.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
.idea/misc.xml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.10 (slide-deck-ai)" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (slide-deck-ai)" project-jdk-type="Python SDK" />
7
+ </project>
.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/slide-deck-ai.iml" filepath="$PROJECT_DIR$/.idea/slide-deck-ai.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
.idea/slide-deck-ai.iml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/venv" />
6
+ </content>
7
+ <orderEntry type="inheritedJdk" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ </module>
.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
.streamlit/config.toml DELETED
@@ -1,10 +0,0 @@
1
- [server]
2
- runOnSave = true
3
- headless = false
4
- maxUploadSize = 2
5
-
6
- [browser]
7
- gatherUsageStats = false
8
-
9
- [theme]
10
- base = "dark"
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🏢
4
  colorFrom: yellow
5
  colorTo: green
6
  sdk: streamlit
7
- sdk_version: 1.44.1
8
  app_file: app.py
9
  pinned: false
10
  license: mit
@@ -16,122 +16,36 @@ We spend a lot of time on creating the slides and organizing our thoughts for an
16
  With SlideDeck AI, co-create slide decks on any topic with Generative Artificial Intelligence.
17
  Describe your topic and let SlideDeck AI generate a PowerPoint slide deck for you—it's as simple as that!
18
 
19
- ## Star History
20
-
21
- [![Star History Chart](https://api.star-history.com/svg?repos=barun-saha/slide-deck-ai&type=Date)](https://star-history.com/#barun-saha/slide-deck-ai&Date)
22
-
23
 
24
  # Process
25
 
26
  SlideDeck AI works in the following way:
27
 
28
- 1. Given a topic description, it uses a Large Language Model (LLM) to generate the *initial* content of the slides.
29
  The output is generated as structured JSON data based on a pre-defined schema.
30
- 2. Next, it uses the keywords from the JSON output to search and download a few images with a certain probability.
31
- 3. Subsequently, it uses the `python-pptx` library to generate the slides,
32
  based on the JSON data from the previous step.
33
- A user can choose from a set of pre-defined presentation templates.
34
- 4. At this stage onward, a user can provide additional instructions to *refine* the content.
35
- For example, one can ask to add another slide or modify an existing slide.
36
- A history of instructions is maintained.
37
- 5. Every time SlideDeck AI generates a PowerPoint presentation, a download button is provided.
38
- Clicking on the button will download the file.
39
-
40
- In addition, SlideDeck AI can also create a presentation based on PDF files.
41
-
42
-
43
- # Summary of the LLMs
44
-
45
- SlideDeck AI allows the use of different LLMs from six online providers—Azure OpenAI, Hugging Face, Google, Cohere, Together AI, and OpenRouter. Most of these service providers offer generous free usage of relevant LLMs without requiring any billing information.
46
-
47
- Based on several experiments, SlideDeck AI generally recommends the use of Mistral NeMo, Gemini Flash, and GPT-4o to generate the slide decks.
48
-
49
- The supported LLMs offer different styles of content generation. Use one of the following LLMs along with relevant API keys/access tokens, as appropriate, to create the content of the slide deck:
50
 
51
- | LLM | Provider (code) | Requires API key | Characteristics |
52
- |:---------------------------------| :------- |:-------------------------------------------------------------------------------------------------------------------------|:-------------------------|
53
- | Mistral 7B Instruct v0.2 | Hugging Face (`hf`) | Mandatory; [get here](https://huggingface.co/settings/tokens) | Faster, shorter content |
54
- | Mistral NeMo Instruct 2407 | Hugging Face (`hf`) | Mandatory; [get here](https://huggingface.co/settings/tokens) | Slower, longer content |
55
- | Gemini 2.0 Flash | Google Gemini API (`gg`) | Mandatory; [get here](https://aistudio.google.com/apikey) | Faster, longer content |
56
- | Gemini 2.0 Flash Lite | Google Gemini API (`gg`) | Mandatory; [get here](https://aistudio.google.com/apikey) | Fastest, longer content |
57
- | GPT | Azure OpenAI (`az`) | Mandatory; [get here](https://ai.azure.com/resource/playground) NOTE: You need to have your subscription/billing set up | Faster, longer content |
58
- | Command R+ | Cohere (`co`) | Mandatory; [get here](https://dashboard.cohere.com/api-keys) | Shorter, simpler content |
59
- | Gemini-2.0-flash-001 | OpenRouter (`or`) | Mandatory; [get here](https://openrouter.ai/settings/keys) | Faster, longer content |
60
- | GPT-3.5 Turbo | OpenRouter (`or`) | Mandatory; [get here](https://openrouter.ai/settings/keys) | Faster, longer content |
61
- | Llama 3.3 70B Instruct Turbo | Together AI (`to`) | Mandatory; [get here](https://api.together.ai/settings/api-keys) | Detailed, slower |
62
- | Llama 3.1 8B Instruct Turbo 128K | Together AI (`to`) | Mandatory; [get here](https://api.together.ai/settings/api-keys) | Shorter |
63
-
64
- **IMPORTANT**: SlideDeck AI does **NOT** store your API keys/tokens or transmit them elsewhere. If you provide your API key, it is only used to invoke the relevant LLM to generate contents. That's it! This is an
65
- Open-Source project, so feel free to audit the code and convince yourself.
66
-
67
- In addition, offline LLMs provided by Ollama can be used. Read below to know more.
68
-
69
-
70
- # Icons
71
-
72
- SlideDeck AI uses a subset of icons from [bootstrap-icons-1.11.3](https://github.com/twbs/icons)
73
- (MIT license) in the slides. A few icons from [SVG Repo](https://www.svgrepo.com/)
74
- (CC0, MIT, and Apache licenses) are also used.
75
 
76
 
77
  # Local Development
78
 
79
- SlideDeck AI uses LLMs via different providers, such as Hugging Face, Google, and Gemini.
80
- To run this project by yourself, you need to provide the `HUGGINGFACEHUB_API_TOKEN` API key,
81
- for example, in a `.env` file. Alternatively, you can provide the access token in the app's user interface itself (UI). For other LLM providers, the API key can only be specified in the UI. For image search, the `PEXEL_API_KEY` should be made available as an environment variable.
82
- Visit the respective websites to obtain the API keys.
83
-
84
- ## Offline LLMs Using Ollama
85
-
86
- SlideDeck AI allows the use of offline LLMs to generate the contents of the slide decks. This is typically suitable for individuals or organizations who would like to use self-hosted LLMs for privacy concerns, for example.
87
-
88
- Offline LLMs are made available via Ollama. Therefore, a pre-requisite here is to have [Ollama installed](https://ollama.com/download) on the system and the desired [LLM](https://ollama.com/search) pulled locally.
89
-
90
- In addition, the `RUN_IN_OFFLINE_MODE` environment variable needs to be set to `True` to enable the offline mode. This, for example, can be done using a `.env` file or from the terminal. The typical steps to use SlideDeck AI in offline mode (in a `bash` shell) are as follows:
91
-
92
- ```bash
93
- # Install Git Large File Storage (LFS)
94
- sudo apt install git-lfs
95
- git lfs install
96
-
97
- ollama list # View locally available LLMs
98
- export RUN_IN_OFFLINE_MODE=True # Enable the offline mode to use Ollama
99
- git clone https://github.com/barun-saha/slide-deck-ai.git
100
- cd slide-deck-ai
101
- git lfs pull # Pull the PPTX template files
102
-
103
- python -m venv venv # Create a virtual environment
104
- source venv/bin/activate # On a Linux system
105
- pip install -r requirements.txt
106
-
107
- streamlit run ./app.py # Run the application
108
- ```
109
-
110
- The `.env` file should be created inside the `slide-deck-ai` directory.
111
-
112
- The UI is similar to the online mode. However, rather than selecting an LLM from a list, one has to write the name of the Ollama model to be used in a textbox. There is no API key asked here.
113
-
114
- The online and offline modes are mutually exclusive. So, setting `RUN_IN_OFFLINE_MODE` to `False` will make SlideDeck AI use the online LLMs (i.e., the "original mode."). By default, `RUN_IN_OFFLINE_MODE` is set to `False`.
115
-
116
- Finally, the focus is on using offline LLMs, not going completely offline. So, Internet connectivity would still be required to fetch the images from Pexels.
117
 
118
 
119
  # Live Demo
120
 
121
- - [SlideDeck AI](https://huggingface.co/spaces/barunsaha/slide-deck-ai) on Hugging Face Spaces
122
- - [Demo video](https://youtu.be/QvAKzNKtk9k) of the chat interface on YouTube
123
- - Demo video on [using Azure OpenAI](https://youtu.be/oPbH-z3q0Mw)
124
 
125
 
126
  # Award
127
 
128
- SlideDeck AI has won the 3rd Place in the [Llama 2 Hackathon with Clarifai](https://lablab.ai/event/llama-2-hackathon-with-clarifai) in 2023.
129
-
130
-
131
- # Contributors
132
-
133
- SlideDeck AI welcomes the very first community contribution from [Srinivasan Ragothaman](https://github.com/rsrini7), who added OpenRouter support and API keys mapping from the `.env` file. Thank you!
134
-
135
- [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors)
136
-
137
-
 
4
  colorFrom: yellow
5
  colorTo: green
6
  sdk: streamlit
7
+ sdk_version: 1.26.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
 
16
  With SlideDeck AI, co-create slide decks on any topic with Generative Artificial Intelligence.
17
  Describe your topic and let SlideDeck AI generate a PowerPoint slide deck for you—it's as simple as that!
18
 
19
+ SlideDeck AI is powered by [Mistral 7B Instruct](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.1).
20
+ Originally, it was built using the Llama 2 API provided by Clarifai.
 
 
21
 
22
  # Process
23
 
24
  SlideDeck AI works in the following way:
25
 
26
+ 1. Given a topic description, it uses Mistral 7B Instruct to generate the outline/contents of the slides.
27
  The output is generated as structured JSON data based on a pre-defined schema.
28
+ 2. Subsequently, it uses the `python-pptx` library to generate the slides,
 
29
  based on the JSON data from the previous step.
30
+ Here, a user can choose from a set of three pre-defined presentation templates.
31
+ 3. In addition, it uses Metaphor to fetch Web pages related to the topic.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
+ 4. ~~Finally, it uses Stable Diffusion 2 to generate an image, based on the title and each slide heading.~~
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
 
36
  # Local Development
37
 
38
+ SlideDeck AI uses [Mistral 7B Instruct](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.1)
39
+ via the Hugging Face Inference API.
40
+ To run this project by yourself, you need to provide the `HUGGINGFACEHUB_API_TOKEN` and `METAPHOR_API_KEY` API keys,
41
+ for example, in a `.env` file. Visit the respective websites to obtain the keys.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
44
  # Live Demo
45
 
46
+ [SlideDeck AI](https://huggingface.co/spaces/barunsaha/slide-deck-ai)
 
 
47
 
48
 
49
  # Award
50
 
51
+ SlideDeck AI has won the 3rd Place in the [Llama 2 Hackathon with Clarifai](https://lablab.ai/event/llama-2-hackathon-with-clarifai).
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,586 +1,310 @@
1
- """
2
- Streamlit app containing the UI and the application logic.
3
- """
4
- import datetime
5
- import logging
6
- import os
7
  import pathlib
8
- import random
9
  import tempfile
10
- from typing import List, Union
11
 
12
- import httpx
13
- import huggingface_hub
14
  import json5
15
- import ollama
16
- import requests
17
  import streamlit as st
18
- from dotenv import load_dotenv
19
- from langchain_community.chat_message_histories import StreamlitChatMessageHistory
20
- from langchain_core.messages import HumanMessage
21
- from langchain_core.prompts import ChatPromptTemplate
22
 
23
- import global_config as gcfg
24
- import helpers.file_manager as filem
25
  from global_config import GlobalConfig
26
- from helpers import llm_helper, pptx_helper, text_helper
27
-
28
- load_dotenv()
29
 
30
- RUN_IN_OFFLINE_MODE = os.getenv('RUN_IN_OFFLINE_MODE', 'False').lower() == 'true'
31
 
 
 
32
 
33
- @st.cache_data
34
- def _load_strings() -> dict:
35
- """
36
- Load various strings to be displayed in the app.
37
- :return: The dictionary of strings.
38
- """
39
 
40
- with open(GlobalConfig.APP_STRINGS_FILE, 'r', encoding='utf-8') as in_file:
41
- return json5.loads(in_file.read())
 
 
42
 
43
 
44
  @st.cache_data
45
- def _get_prompt_template(is_refinement: bool) -> str:
46
- """
47
- Return a prompt template.
48
-
49
- :param is_refinement: Whether this is the initial or refinement prompt.
50
- :return: The prompt template as f-string.
51
  """
 
52
 
53
- if is_refinement:
54
- with open(GlobalConfig.REFINEMENT_PROMPT_TEMPLATE, 'r', encoding='utf-8') as in_file:
55
- template = in_file.read()
56
- else:
57
- with open(GlobalConfig.INITIAL_PROMPT_TEMPLATE, 'r', encoding='utf-8') as in_file:
58
- template = in_file.read()
59
-
60
- return template
61
-
62
-
63
- def are_all_inputs_valid(
64
- user_prompt: str,
65
- selected_provider: str,
66
- selected_model: str,
67
- user_key: str,
68
- azure_deployment_url: str = '',
69
- azure_endpoint_name: str = '',
70
- azure_api_version: str = '',
71
- ) -> bool:
72
- """
73
- Validate user input and LLM selection.
74
-
75
- :param user_prompt: The prompt.
76
- :param selected_provider: The LLM provider.
77
- :param selected_model: Name of the model.
78
- :param user_key: User-provided API key.
79
- :param azure_deployment_url: Azure OpenAI deployment URL.
80
- :param azure_endpoint_name: Azure OpenAI model endpoint.
81
- :param azure_api_version: Azure OpenAI API version.
82
- :return: `True` if all inputs "look" OK; `False` otherwise.
83
  """
84
 
85
- if not text_helper.is_valid_prompt(user_prompt):
86
- handle_error(
87
- 'Not enough information provided!'
88
- ' Please be a little more descriptive and type a few words'
89
- ' with a few characters :)',
90
- False
91
- )
92
- return False
93
-
94
- if not selected_provider or not selected_model:
95
- handle_error('No valid LLM provider and/or model name found!', False)
96
- return False
97
-
98
- if not llm_helper.is_valid_llm_provider_model(
99
- selected_provider, selected_model, user_key,
100
- azure_endpoint_name, azure_deployment_url, azure_api_version
101
- ):
102
- handle_error(
103
- 'The LLM settings do not look correct. Make sure that an API key/access token'
104
- ' is provided if the selected LLM requires it. An API key should be 6-94 characters'
105
- ' long, only containing alphanumeric characters, hyphens, and underscores.\n\n'
106
- 'If you are using Azure OpenAI, make sure that you have provided the additional and'
107
- ' correct configurations.',
108
- False
109
- )
110
- return False
111
-
112
- return True
113
 
114
 
115
- def handle_error(error_msg: str, should_log: bool):
 
116
  """
117
- Display an error message in the app.
118
 
119
- :param error_msg: The error message to be displayed.
120
- :param should_log: If `True`, log the message.
121
  """
122
 
123
- if should_log:
124
- logger.error(error_msg)
125
-
126
- st.error(error_msg)
127
 
128
 
129
- def reset_api_key():
130
- """
131
- Clear API key input when a different LLM is selected from the dropdown list.
132
  """
 
133
 
134
- st.session_state.api_key_input = ''
135
-
136
-
137
- APP_TEXT = _load_strings()
138
-
139
- # Session variables
140
- CHAT_MESSAGES = 'chat_messages'
141
- DOWNLOAD_FILE_KEY = 'download_file_name'
142
- IS_IT_REFINEMENT = 'is_it_refinement'
143
- ADDITIONAL_INFO = 'additional_info'
144
-
145
-
146
- logger = logging.getLogger(__name__)
147
-
148
- texts = list(GlobalConfig.PPTX_TEMPLATE_FILES.keys())
149
- captions = [GlobalConfig.PPTX_TEMPLATE_FILES[x]['caption'] for x in texts]
150
 
151
- with st.sidebar:
152
- # The PPT templates
153
- pptx_template = st.sidebar.radio(
154
- '1: Select a presentation template:',
155
- texts,
156
- captions=captions,
157
- horizontal=True
158
  )
159
 
160
- if RUN_IN_OFFLINE_MODE:
161
- llm_provider_to_use = st.text_input(
162
- label='2: Enter Ollama model name to use (e.g., mistral:v0.2):',
163
- help=(
164
- 'Specify a correct, locally available LLM, found by running `ollama list`, for'
165
- ' example `mistral:v0.2` and `mistral-nemo:latest`. Having an Ollama-compatible'
166
- ' and supported GPU is strongly recommended.'
167
- )
168
- )
169
- api_key_token: str = ''
170
- azure_endpoint: str = ''
171
- azure_deployment: str = ''
172
- api_version: str = ''
173
- else:
174
- # The online LLMs
175
- llm_provider_to_use = st.sidebar.selectbox(
176
- label='2: Select a suitable LLM to use:\n\n(Gemini and Mistral-Nemo are recommended)',
177
- options=[f'{k} ({v["description"]})' for k, v in GlobalConfig.VALID_MODELS.items()],
178
- index=GlobalConfig.DEFAULT_MODEL_INDEX,
179
- help=GlobalConfig.LLM_PROVIDER_HELP,
180
- on_change=reset_api_key
181
- ).split(' ')[0]
182
-
183
- # --- Automatically fetch API key from .env if available ---
184
- provider_match = GlobalConfig.PROVIDER_REGEX.match(llm_provider_to_use)
185
- selected_provider = provider_match.group(1) if provider_match else llm_provider_to_use
186
- env_key_name = GlobalConfig.PROVIDER_ENV_KEYS.get(selected_provider)
187
- default_api_key = os.getenv(env_key_name, "") if env_key_name else ""
188
-
189
- # Always sync session state to env value if needed (auto-fill on provider change)
190
- if default_api_key and st.session_state.get('api_key_input', None) != default_api_key:
191
- st.session_state['api_key_input'] = default_api_key
192
-
193
- api_key_token = st.text_input(
194
- label=(
195
- '3: Paste your API key/access token:\n\n'
196
- '*Mandatory* for all providers.'
197
- ),
198
- key='api_key_input',
199
- type='password',
200
- disabled=bool(default_api_key),
201
- )
202
-
203
- # Additional configs for Azure OpenAI
204
- with st.expander('**Azure OpenAI-specific configurations**'):
205
- azure_endpoint = st.text_input(
206
- label=(
207
- '4: Azure endpoint URL, e.g., https://example.openai.azure.com/.\n\n'
208
- '*Mandatory* for Azure OpenAI (only).'
209
- )
210
- )
211
- azure_deployment = st.text_input(
212
- label=(
213
- '5: Deployment name on Azure OpenAI:\n\n'
214
- '*Mandatory* for Azure OpenAI (only).'
215
- ),
216
- )
217
- api_version = st.text_input(
218
- label=(
219
- '6: API version:\n\n'
220
- '*Mandatory* field. Change based on your deployment configurations.'
221
- ),
222
- value='2024-05-01-preview',
223
- )
224
 
225
 
226
  def build_ui():
227
  """
228
- Display the input elements for content generation.
229
  """
230
 
 
 
231
  st.title(APP_TEXT['app_name'])
232
  st.subheader(APP_TEXT['caption'])
233
  st.markdown(
234
- '![Visitors](https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fhuggingface.co%2Fspaces%2Fbarunsaha%2Fslide-deck-ai&countColor=%23263759)' # noqa: E501
 
 
 
 
 
235
  )
236
 
237
- today = datetime.date.today()
238
- if today.month == 1 and 1 <= today.day <= 15:
239
- st.success(
240
- (
241
- 'Wishing you a happy and successful New Year!'
242
- ' It is your appreciation that keeps SlideDeck AI going.'
243
- f' May you make some great slide decks in {today.year} ✨'
244
- ),
245
- icon='🎆'
 
 
246
  )
247
 
248
- with st.expander('Usage Policies and Limitations'):
249
- st.text(APP_TEXT['tos'] + '\n\n' + APP_TEXT['tos2'])
250
 
251
- set_up_chat_ui()
 
 
 
 
 
252
 
 
 
253
 
254
- def set_up_chat_ui():
255
- """
256
- Prepare the chat interface and related functionality.
257
- """
258
 
259
- with st.expander('Usage Instructions'):
260
- st.markdown(GlobalConfig.CHAT_USAGE_INSTRUCTIONS)
 
 
261
 
262
- st.info(APP_TEXT['like_feedback'])
263
- st.chat_message('ai').write(random.choice(APP_TEXT['ai_greetings']))
264
 
265
- history = StreamlitChatMessageHistory(key=CHAT_MESSAGES)
266
- prompt_template = ChatPromptTemplate.from_template(
267
- _get_prompt_template(
268
- is_refinement=_is_it_refinement()
269
- )
 
 
270
  )
271
 
272
- # Since Streamlit app reloads at every interaction, display the chat history
273
- # from the save session state
274
- for msg in history.messages:
275
- st.chat_message(msg.type).code(msg.content, language='json')
276
-
277
- if prompt := st.chat_input(
278
- placeholder=APP_TEXT['chat_placeholder'],
279
- max_chars=GlobalConfig.LLM_MODEL_MAX_INPUT_LENGTH,
280
- accept_file=True,
281
- file_type=['pdf', ],
282
- ):
283
- prompt_text = prompt.text or ''
284
- if prompt['files']:
285
- # Apparently, Streamlit stores uploaded files in memory and clears on browser close
286
- # https://docs.streamlit.io/knowledge-base/using-streamlit/where-file-uploader-store-when-deleted
287
- st.session_state[ADDITIONAL_INFO] = filem.get_pdf_contents(prompt['files'][0])
288
- print(f'{prompt["files"]=}')
289
-
290
- provider, llm_name = llm_helper.get_provider_model(
291
- llm_provider_to_use,
292
- use_ollama=RUN_IN_OFFLINE_MODE
293
- )
294
 
295
- user_key = api_key_token.strip()
296
- az_deployment = azure_deployment.strip()
297
- az_endpoint = azure_endpoint.strip()
298
- api_ver = api_version.strip()
299
 
300
- if not are_all_inputs_valid(
301
- prompt_text, provider, llm_name, user_key,
302
- az_deployment, az_endpoint, api_ver
303
- ):
304
- return
305
 
306
- logger.info(
307
- 'User input: %s | #characters: %d | LLM: %s',
308
- prompt_text, len(prompt_text), llm_name
309
- )
310
- st.chat_message('user').write(prompt_text)
311
-
312
- if _is_it_refinement():
313
- user_messages = _get_user_messages()
314
- user_messages.append(prompt_text)
315
- list_of_msgs = [
316
- f'{idx + 1}. {msg}' for idx, msg in enumerate(user_messages)
317
- ]
318
- formatted_template = prompt_template.format(
319
- **{
320
- 'instructions': '\n'.join(list_of_msgs),
321
- 'previous_content': _get_last_response(),
322
- 'additional_info': st.session_state.get(ADDITIONAL_INFO, ''),
323
- }
324
- )
325
- else:
326
- formatted_template = prompt_template.format(
327
- **{
328
- 'question': prompt_text,
329
- 'additional_info': st.session_state.get(ADDITIONAL_INFO, ''),
330
- }
331
- )
332
-
333
- progress_bar = st.progress(0, 'Preparing to call LLM...')
334
- response = ''
335
 
336
- try:
337
- llm = llm_helper.get_langchain_llm(
338
- provider=provider,
339
- model=llm_name,
340
- max_new_tokens=gcfg.get_max_output_tokens(llm_provider_to_use),
341
- api_key=user_key,
342
- azure_endpoint_url=az_endpoint,
343
- azure_deployment_name=az_deployment,
344
- azure_api_version=api_ver,
345
- )
346
-
347
- if not llm:
348
- handle_error(
349
- 'Failed to create an LLM instance! Make sure that you have selected the'
350
- ' correct model from the dropdown list and have provided correct API key'
351
- ' or access token.',
352
- False
353
- )
354
- return
355
 
356
- for chunk in llm.stream(formatted_template):
357
- if isinstance(chunk, str):
358
- response += chunk
359
- else:
360
- content = getattr(chunk, 'content', None)
361
- if content is not None:
362
- response += content
363
- else:
364
- response += str(chunk)
365
-
366
- # Update the progress bar with an approx progress percentage
367
- progress_bar.progress(
368
- min(
369
- len(response) / gcfg.get_max_output_tokens(llm_provider_to_use),
370
- 0.95
371
- ),
372
- text='Streaming content...this might take a while...'
373
- )
374
- except (httpx.ConnectError, requests.exceptions.ConnectionError):
375
- handle_error(
376
- 'A connection error occurred while streaming content from the LLM endpoint.'
377
- ' Unfortunately, the slide deck cannot be generated. Please try again later.'
378
- ' Alternatively, try selecting a different LLM from the dropdown list. If you are'
379
- ' using Ollama, make sure that Ollama is already running on your system.',
380
- True
381
- )
382
- return
383
- except huggingface_hub.errors.ValidationError as ve:
384
- handle_error(
385
- f'An error occurred while trying to generate the content: {ve}'
386
- '\nPlease try again with a significantly shorter input text.',
387
- True
388
- )
389
- return
390
- except ollama.ResponseError:
391
- handle_error(
392
- f'The model `{llm_name}` is unavailable with Ollama on your system.'
393
- f' Make sure that you have provided the correct LLM name or pull it using'
394
- f' `ollama pull {llm_name}`. View LLMs available locally by running `ollama list`.',
395
- True
396
- )
397
- return
398
- except Exception as ex:
399
- _msg = str(ex)
400
- if 'payment required' in _msg.lower():
401
- handle_error(
402
- 'The available inference quota has exhausted.'
403
- ' Please use your own Hugging Face access token. Paste your token in'
404
- ' the input field on the sidebar to the left.'
405
- '\n\nDon\'t have a token? Get your free'
406
- ' [HF access token](https://huggingface.co/settings/tokens) now'
407
- ' and start creating your slide deck! For gated models, you may need to'
408
- ' visit the model\'s page and accept the terms or service.'
409
- '\n\nAlternatively, choose a different LLM and provider from the list.',
410
- should_log=True
411
  )
412
  else:
413
- handle_error(
414
- f'An unexpected error occurred while generating the content: {_msg}'
415
- '\n\nPlease try again later, possibly with different inputs.'
416
- ' Alternatively, try selecting a different LLM from the dropdown list.'
417
- ' If you are using Azure OpenAI, Cohere, Gemini, or Together AI models, make'
418
- ' sure that you have provided a correct API key.'
419
- ' Read **[how to get free LLM API keys](https://github.com/barun-saha/slide-deck-ai?tab=readme-ov-file#summary-of-the-llms)**.',
420
- True
421
  )
422
- return
423
 
424
- history.add_user_message(prompt_text)
425
- history.add_ai_message(response)
426
 
427
- # The content has been generated as JSON
428
- # There maybe trailing ``` at the end of the response -- remove them
429
- # To be careful: ``` may be part of the content as well when code is generated
430
- response = text_helper.get_clean_json(response)
431
- logger.info(
432
- 'Cleaned JSON length: %d', len(response)
433
- )
434
 
435
- # Now create the PPT file
436
- progress_bar.progress(
437
- GlobalConfig.LLM_PROGRESS_MAX,
438
- text='Finding photos online and generating the slide deck...'
439
- )
440
- progress_bar.progress(1.0, text='Done!')
441
- st.chat_message('ai').code(response, language='json')
442
 
443
- if path := generate_slide_deck(response):
444
- _display_download_button(path)
445
-
446
- logger.info(
447
- '#messages in history / 2: %d',
448
- len(st.session_state[CHAT_MESSAGES]) / 2
449
- )
450
 
451
 
452
- def generate_slide_deck(json_str: str) -> Union[pathlib.Path, None]:
453
  """
454
- Create a slide deck and return the file path. In case there is any error creating the slide
455
- deck, the path may be to an empty file.
456
 
457
- :param json_str: The content in *valid* JSON format.
458
- :return: The path to the .pptx file or `None` in case of error.
 
459
  """
460
 
461
- try:
462
- parsed_data = json5.loads(json_str)
463
- except ValueError:
464
- handle_error(
465
- 'Encountered error while parsing JSON...will fix it and retry',
466
- True
467
- )
468
- try:
469
- parsed_data = json5.loads(text_helper.fix_malformed_json(json_str))
470
- except ValueError:
471
- handle_error(
472
- 'Encountered an error again while fixing JSON...'
473
- 'the slide deck cannot be created, unfortunately ☹'
474
- '\nPlease try again later.',
475
- True
476
- )
477
- return None
478
- except RecursionError:
479
- handle_error(
480
- 'Encountered a recursion error while parsing JSON...'
481
- 'the slide deck cannot be created, unfortunately ☹'
482
- '\nPlease try again later.',
483
- True
484
- )
485
- return None
486
- except Exception:
487
- handle_error(
488
- 'Encountered an error while parsing JSON...'
489
- 'the slide deck cannot be created, unfortunately ☹'
490
- '\nPlease try again later.',
491
- True
492
- )
493
- return None
494
-
495
- if DOWNLOAD_FILE_KEY in st.session_state:
496
- path = pathlib.Path(st.session_state[DOWNLOAD_FILE_KEY])
497
- else:
498
- temp = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
499
- path = pathlib.Path(temp.name)
500
- st.session_state[DOWNLOAD_FILE_KEY] = str(path)
501
-
502
- if temp:
503
- temp.close()
504
 
505
  try:
506
- logger.debug('Creating PPTX file: %s...', st.session_state[DOWNLOAD_FILE_KEY])
507
- pptx_helper.generate_powerpoint_presentation(
508
- parsed_data,
509
- slides_template=pptx_template,
510
- output_file_path=path
511
- )
512
  except Exception as ex:
513
- st.error(APP_TEXT['content_generation_error'])
514
- logger.error('Caught a generic exception: %s', str(ex))
515
-
516
- return path
517
-
518
-
519
- def _is_it_refinement() -> bool:
520
- """
521
- Whether it is the initial prompt or a refinement.
522
-
523
- :return: True if it is the initial prompt; False otherwise.
524
- """
525
 
526
- if IS_IT_REFINEMENT in st.session_state:
527
- return True
528
 
529
- if len(st.session_state[CHAT_MESSAGES]) >= 2:
530
- # Prepare for the next call
531
- st.session_state[IS_IT_REFINEMENT] = True
532
- return True
533
 
534
- return False
535
 
536
 
537
- def _get_user_messages() -> List[str]:
538
  """
539
- Get a list of user messages submitted until now from the session state.
540
 
541
- :return: The list of user messages.
 
 
 
542
  """
543
 
544
- return [
545
- msg.content for msg in st.session_state[CHAT_MESSAGES] if isinstance(msg, HumanMessage)
546
- ]
547
 
 
 
 
 
 
548
 
549
- def _get_last_response() -> str:
550
- """
551
- Get the last response generated by AI.
552
 
553
- :return: The response text.
554
- """
 
 
 
 
 
 
555
 
556
- return st.session_state[CHAT_MESSAGES][-1].content
 
557
 
 
558
 
559
- def _display_messages_history(view_messages: st.expander):
 
560
  """
561
- Display the history of messages.
562
 
563
- :param view_messages: The list of AI and Human messages.
564
  """
565
 
566
- with view_messages:
567
- view_messages.json(st.session_state[CHAT_MESSAGES])
 
 
 
568
 
 
 
569
 
570
- def _display_download_button(file_path: pathlib.Path):
571
- """
572
- Display a download button to download a slide deck.
573
 
574
- :param file_path: The path of the .pptx file.
575
- """
576
 
577
- with open(file_path, 'rb') as download_file:
578
- st.download_button(
579
- 'Download PPTX file ⬇️',
580
- data=download_file,
581
- file_name='Presentation.pptx',
582
- key=datetime.datetime.now()
583
- )
 
 
 
 
 
584
 
585
 
586
  def main():
@@ -593,4 +317,3 @@ def main():
593
 
594
  if __name__ == '__main__':
595
  main()
596
-
 
 
 
 
 
 
 
1
  import pathlib
2
+ import logging
3
  import tempfile
4
+ from typing import List, Tuple
5
 
 
 
6
  import json5
7
+ import metaphor_python as metaphor
 
8
  import streamlit as st
 
 
 
 
9
 
10
+ import llm_helper
11
+ import pptx_helper
12
  from global_config import GlobalConfig
 
 
 
13
 
 
14
 
15
+ APP_TEXT = json5.loads(open(GlobalConfig.APP_STRINGS_FILE, 'r', encoding='utf-8').read())
16
+ GB_CONVERTER = 2 ** 30
17
 
 
 
 
 
 
 
18
 
19
+ logging.basicConfig(
20
+ level=GlobalConfig.LOG_LEVEL,
21
+ format='%(asctime)s - %(message)s',
22
+ )
23
 
24
 
25
  @st.cache_data
26
+ def get_contents_wrapper(text: str) -> str:
 
 
 
 
 
27
  """
28
+ Fetch and cache the slide deck contents on a topic by calling an external API.
29
 
30
+ :param text: The presentation topic
31
+ :return: The slide deck contents or outline in JSON format
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  """
33
 
34
+ logging.info('LLM call because of cache miss...')
35
+ return llm_helper.generate_slides_content(text).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
 
38
+ @st.cache_resource
39
+ def get_metaphor_client_wrapper() -> metaphor.Metaphor:
40
  """
41
+ Create a Metaphor client for semantic Web search.
42
 
43
+ :return: Metaphor instance
 
44
  """
45
 
46
+ return metaphor.Metaphor(api_key=GlobalConfig.METAPHOR_API_KEY)
 
 
 
47
 
48
 
49
+ @st.cache_data
50
+ def get_web_search_results_wrapper(text: str) -> List[Tuple[str, str]]:
 
51
  """
52
+ Fetch and cache the Web search results on a given topic.
53
 
54
+ :param text: The topic
55
+ :return: A list of (title, link) tuples
56
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ results = []
59
+ search_results = get_metaphor_client_wrapper().search(
60
+ text,
61
+ use_autoprompt=True,
62
+ num_results=5
 
 
63
  )
64
 
65
+ for a_result in search_results.results:
66
+ results.append((a_result.title, a_result.url))
67
+
68
+ return results
69
+
70
+
71
+ # def get_disk_used_percentage() -> float:
72
+ # """
73
+ # Compute the disk usage.
74
+ #
75
+ # :return: Percentage of the disk space currently used
76
+ # """
77
+ #
78
+ # total, used, free = shutil.disk_usage(__file__)
79
+ # total = total // GB_CONVERTER
80
+ # used = used // GB_CONVERTER
81
+ # free = free // GB_CONVERTER
82
+ # used_perc = 100.0 * used / total
83
+ #
84
+ # logging.debug(f'Total: {total} GB\n'
85
+ # f'Used: {used} GB\n'
86
+ # f'Free: {free} GB')
87
+ #
88
+ # logging.debug('\n'.join(os.listdir()))
89
+ #
90
+ # return used_perc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
 
93
  def build_ui():
94
  """
95
+ Display the input elements for content generation. Only covers the first step.
96
  """
97
 
98
+ # get_disk_used_percentage()
99
+
100
  st.title(APP_TEXT['app_name'])
101
  st.subheader(APP_TEXT['caption'])
102
  st.markdown(
103
+ 'Powered by'
104
+ ' [Mistral-7B-Instruct-v0.2](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2).'
105
+ )
106
+ st.markdown(
107
+ '*If the JSON is generated or parsed incorrectly, try again later by making minor changes'
108
+ ' to the input text.*'
109
  )
110
 
111
+ with st.form('my_form'):
112
+ # Topic input
113
+ try:
114
+ with open(GlobalConfig.PRELOAD_DATA_FILE, 'r', encoding='utf-8') as in_file:
115
+ preload_data = json5.loads(in_file.read())
116
+ except (FileExistsError, FileNotFoundError):
117
+ preload_data = {'topic': '', 'audience': ''}
118
+
119
+ topic = st.text_area(
120
+ APP_TEXT['input_labels'][0],
121
+ value=preload_data['topic']
122
  )
123
 
124
+ texts = list(GlobalConfig.PPTX_TEMPLATE_FILES.keys())
125
+ captions = [GlobalConfig.PPTX_TEMPLATE_FILES[x]['caption'] for x in texts]
126
 
127
+ pptx_template = st.radio(
128
+ 'Select a presentation template:',
129
+ texts,
130
+ captions=captions,
131
+ horizontal=True
132
+ )
133
 
134
+ st.divider()
135
+ submit = st.form_submit_button('Generate slide deck')
136
 
137
+ if submit:
138
+ # st.write(f'Clicked {time.time()}')
139
+ st.session_state.submitted = True
 
140
 
141
+ # https://github.com/streamlit/streamlit/issues/3832#issuecomment-1138994421
142
+ if 'submitted' in st.session_state:
143
+ progress_text = 'Generating the slides...give it a moment'
144
+ progress_bar = st.progress(0, text=progress_text)
145
 
146
+ topic_txt = topic.strip()
147
+ generate_presentation(topic_txt, pptx_template, progress_bar)
148
 
149
+ st.divider()
150
+ st.text(APP_TEXT['tos'])
151
+ st.text(APP_TEXT['tos2'])
152
+
153
+ st.markdown(
154
+ '![Visitors]'
155
+ '(https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fhuggingface.co%2Fspaces%2Fbarunsaha%2Fslide-deck-ai&countColor=%23263759)'
156
  )
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ def generate_presentation(topic: str, pptx_template: str, progress_bar):
160
+ """
161
+ Process the inputs to generate the slides.
 
162
 
163
+ :param topic: The presentation topic based on which contents are to be generated
164
+ :param pptx_template: The PowerPoint template name to be used
165
+ :param progress_bar: Progress bar from the page
166
+ :return:
167
+ """
168
 
169
+ topic_length = len(topic)
170
+ logging.debug('Input length:: topic: %s', topic_length)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
+ if topic_length >= 10:
173
+ logging.debug('Topic: %s', topic)
174
+ target_length = min(topic_length, GlobalConfig.LLM_MODEL_MAX_INPUT_LENGTH)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
+ try:
177
+ # Step 1: Generate the contents in JSON format using an LLM
178
+ json_str = process_slides_contents(topic[:target_length], progress_bar)
179
+ logging.debug('Truncated topic: %s', topic[:target_length])
180
+ logging.debug('Length of JSON: %d', len(json_str))
181
+
182
+ # Step 2: Generate the slide deck based on the template specified
183
+ if len(json_str) > 0:
184
+ st.info(
185
+ 'Tip: The generated content doesn\'t look so great?'
186
+ ' Need alternatives? Just change your description text and try again.',
187
+ icon="💡️"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  )
189
  else:
190
+ st.error(
191
+ 'Unfortunately, JSON generation failed, so the next steps would lead'
192
+ ' to nowhere. Try again or come back later.'
 
 
 
 
 
193
  )
194
+ return
195
 
196
+ all_headers = generate_slide_deck(json_str, pptx_template, progress_bar)
 
197
 
198
+ # Step 3: Bonus stuff: Web references and AI art
199
+ show_bonus_stuff(all_headers)
 
 
 
 
 
200
 
201
+ except ValueError as ve:
202
+ st.error(f'Unfortunately, an error occurred: {ve}! '
203
+ f'Please change the text, try again later, or report it, sharing your inputs.')
 
 
 
 
204
 
205
+ else:
206
+ st.error('Not enough information provided! Please be little more descriptive :)')
 
 
 
 
 
207
 
208
 
209
+ def process_slides_contents(text: str, progress_bar: st.progress) -> str:
210
  """
211
+ Convert given text into structured data and display. Update the UI.
 
212
 
213
+ :param text: The topic description for the presentation
214
+ :param progress_bar: Progress bar for this step
215
+ :return: The contents as a JSON-formatted string
216
  """
217
 
218
+ json_str = ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
  try:
221
+ logging.info('Calling LLM for content generation on the topic: %s', text)
222
+ json_str = get_contents_wrapper(text)
 
 
 
 
223
  except Exception as ex:
224
+ st.error(
225
+ f'An exception occurred while trying to convert to JSON. It could be because of heavy'
226
+ f' traffic or something else. Try doing it again or try again later.'
227
+ f'\nError message: {ex}'
228
+ )
 
 
 
 
 
 
 
229
 
230
+ progress_bar.progress(50, text='Contents generated')
 
231
 
232
+ with st.expander('The generated contents (in JSON format)'):
233
+ st.code(json_str, language='json')
 
 
234
 
235
+ return json_str
236
 
237
 
238
+ def generate_slide_deck(json_str: str, pptx_template: str, progress_bar) -> List:
239
  """
240
+ Create a slide deck.
241
 
242
+ :param json_str: The contents in JSON format
243
+ :param pptx_template: The PPTX template name
244
+ :param progress_bar: Progress bar
245
+ :return: A list of all slide headers and the title
246
  """
247
 
248
+ progress_text = 'Creating the slide deck...give it a moment'
249
+ progress_bar.progress(75, text=progress_text)
 
250
 
251
+ # # Get a unique name for the file to save -- use the session ID
252
+ # ctx = st_sr.get_script_run_ctx()
253
+ # session_id = ctx.session_id
254
+ # timestamp = time.time()
255
+ # output_file_name = f'{session_id}_{timestamp}.pptx'
256
 
257
+ temp = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
258
+ path = pathlib.Path(temp.name)
 
259
 
260
+ logging.info('Creating PPTX file...')
261
+ all_headers = pptx_helper.generate_powerpoint_presentation(
262
+ json_str,
263
+ as_yaml=False,
264
+ slides_template=pptx_template,
265
+ output_file_path=path
266
+ )
267
+ progress_bar.progress(100, text='Done!')
268
 
269
+ with open(path, 'rb') as f:
270
+ st.download_button('Download PPTX file', f, file_name='Presentation.pptx')
271
 
272
+ return all_headers
273
 
274
+
275
+ def show_bonus_stuff(ppt_headers: List[str]):
276
  """
277
+ Show bonus stuff for the presentation.
278
 
279
+ :param ppt_headers: A list of the slide headings.
280
  """
281
 
282
+ # Use the presentation title and the slide headers to find relevant info online
283
+ logging.info('Calling Metaphor search...')
284
+ ppt_text = ' '.join(ppt_headers)
285
+ search_results = get_web_search_results_wrapper(ppt_text)
286
+ md_text_items = []
287
 
288
+ for (title, link) in search_results:
289
+ md_text_items.append(f'[{title}]({link})')
290
 
291
+ with st.expander('Related Web references'):
292
+ st.markdown('\n\n'.join(md_text_items))
 
293
 
294
+ logging.info('Done!')
 
295
 
296
+ # # Avoid image generation. It costs time and an API call, so just limit to the text generation.
297
+ # with st.expander('AI-generated image on the presentation topic'):
298
+ # logging.info('Calling SDXL for image generation...')
299
+ # # img_empty.write('')
300
+ # # img_text.write(APP_TEXT['image_info'])
301
+ # image = get_ai_image_wrapper(ppt_text)
302
+ #
303
+ # if len(image) > 0:
304
+ # image = base64.b64decode(image)
305
+ # st.image(image, caption=ppt_text)
306
+ # st.info('Tip: Right-click on the image to save it.', icon="💡️")
307
+ # logging.info('Image added')
308
 
309
 
310
  def main():
 
317
 
318
  if __name__ == '__main__':
319
  main()
 
clarifai_grpc_helper.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from clarifai_grpc.channel.clarifai_channel import ClarifaiChannel
2
+ from clarifai_grpc.grpc.api import resources_pb2, service_pb2, service_pb2_grpc
3
+ from clarifai_grpc.grpc.api.status import status_code_pb2
4
+
5
+ from global_config import GlobalConfig
6
+
7
+
8
+ CHANNEL = ClarifaiChannel.get_grpc_channel()
9
+ STUB = service_pb2_grpc.V2Stub(CHANNEL)
10
+
11
+ METADATA = (
12
+ ('authorization', 'Key ' + GlobalConfig.CLARIFAI_PAT),
13
+ )
14
+
15
+ USER_DATA_OBJECT = resources_pb2.UserAppIDSet(
16
+ user_id=GlobalConfig.CLARIFAI_USER_ID,
17
+ app_id=GlobalConfig.CLARIFAI_APP_ID
18
+ )
19
+
20
+ RAW_TEXT = '''You are a helpful, intelligent chatbot. Create the slides for a presentation on the given topic. Include main headings for each slide, detailed bullet points for each slide. Add relevant content to each slide. Do not output any blank line.
21
+
22
+ Topic:
23
+ Talk about AI, covering what it is and how it works. Add its pros, cons, and future prospects. Also, cover its job prospects.
24
+ '''
25
+
26
+
27
+ def get_text_from_llm(prompt: str) -> str:
28
+ post_model_outputs_response = STUB.PostModelOutputs(
29
+ service_pb2.PostModelOutputsRequest(
30
+ user_app_id=USER_DATA_OBJECT, # The userDataObject is created in the overview and is required when using a PAT
31
+ model_id=GlobalConfig.CLARIFAI_MODEL_ID,
32
+ # version_id=MODEL_VERSION_ID, # This is optional. Defaults to the latest model version
33
+ inputs=[
34
+ resources_pb2.Input(
35
+ data=resources_pb2.Data(
36
+ text=resources_pb2.Text(
37
+ raw=prompt
38
+ )
39
+ )
40
+ )
41
+ ]
42
+ ),
43
+ metadata=METADATA
44
+ )
45
+
46
+ if post_model_outputs_response.status.code != status_code_pb2.SUCCESS:
47
+ print(post_model_outputs_response.status)
48
+ raise Exception(f"Post model outputs failed, status: {post_model_outputs_response.status.description}")
49
+
50
+ # Since we have one input, one output will exist here
51
+ output = post_model_outputs_response.outputs[0]
52
+
53
+ # print("Completion:\n")
54
+ # print(output.data.text.raw)
55
+
56
+ return output.data.text.raw
57
+
58
+
59
+ if __name__ == '__main__':
60
+ topic = ('Talk about AI, covering what it is and how it works.'
61
+ ' Add its pros, cons, and future prospects.'
62
+ ' Also, cover its job prospects.'
63
+ )
64
+ print(topic)
65
+
66
+ with open(GlobalConfig.SLIDES_TEMPLATE_FILE, 'r') as in_file:
67
+ prompt_txt = in_file.read()
68
+ prompt_txt = prompt_txt.replace('{topic}', topic)
69
+ response_txt = get_text_from_llm(prompt_txt)
70
+
71
+ print('Output:\n', response_txt)
examples/example_04.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "topic": "12 slides on a basic tutorial on Python along with examples"
3
- }
 
 
 
 
file_embeddings/embeddings.npy DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:64a1ba79b20c81ba7ed6604468736f74ae89813fe378191af1d8574c008b3ab5
3
- size 326784
 
 
 
 
file_embeddings/icons.npy DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:ce5ce4c86bb213915606921084b3516464154edcae12f4bc708d62c6bd7acebb
3
- size 51168
 
 
 
 
global_config.py CHANGED
@@ -1,9 +1,4 @@
1
- """
2
- A set of configurations used by the app.
3
- """
4
- import logging
5
  import os
6
- import re
7
 
8
  from dataclasses import dataclass
9
  from dotenv import load_dotenv
@@ -14,190 +9,32 @@ load_dotenv()
14
 
15
  @dataclass(frozen=True)
16
  class GlobalConfig:
17
- """
18
- A data class holding the configurations.
19
- """
 
 
20
 
21
- PROVIDER_COHERE = 'co'
22
- PROVIDER_GOOGLE_GEMINI = 'gg'
23
- PROVIDER_HUGGING_FACE = 'hf'
24
- PROVIDER_AZURE_OPENAI = 'az'
25
- PROVIDER_OLLAMA = 'ol'
26
- PROVIDER_OPENROUTER = 'or'
27
- PROVIDER_TOGETHER_AI = 'to'
28
- VALID_PROVIDERS = {
29
- PROVIDER_COHERE,
30
- PROVIDER_GOOGLE_GEMINI,
31
- PROVIDER_HUGGING_FACE,
32
- PROVIDER_OLLAMA,
33
- PROVIDER_TOGETHER_AI,
34
- PROVIDER_AZURE_OPENAI,
35
- PROVIDER_OPENROUTER,
36
- }
37
- PROVIDER_ENV_KEYS = {
38
- PROVIDER_COHERE: "COHERE_API_KEY",
39
- PROVIDER_GOOGLE_GEMINI: "GOOGLE_API_KEY",
40
- PROVIDER_HUGGING_FACE: "HUGGINGFACEHUB_API_TOKEN",
41
- PROVIDER_AZURE_OPENAI: "AZURE_OPENAI_API_KEY",
42
- PROVIDER_OPENROUTER: "OPENROUTER_API_KEY",
43
- PROVIDER_TOGETHER_AI: "TOGETHER_API_KEY",
44
- }
45
- PROVIDER_REGEX = re.compile(r'\[(.*?)\]')
46
- VALID_MODELS = {
47
- '[az]azure/open-ai': {
48
- 'description': 'faster, detailed',
49
- 'max_new_tokens': 8192,
50
- 'paid': True,
51
- },
52
- '[co]command-r-08-2024': {
53
- 'description': 'simpler, slower',
54
- 'max_new_tokens': 4096,
55
- 'paid': True,
56
- },
57
- '[gg]gemini-2.0-flash': {
58
- 'description': 'fast, detailed',
59
- 'max_new_tokens': 8192,
60
- 'paid': True,
61
- },
62
- '[gg]gemini-2.0-flash-lite': {
63
- 'description': 'fastest, detailed',
64
- 'max_new_tokens': 8192,
65
- 'paid': True,
66
- },
67
- '[hf]mistralai/Mistral-7B-Instruct-v0.2': {
68
- 'description': 'faster, shorter',
69
- 'max_new_tokens': 8192,
70
- 'paid': False,
71
- },
72
- '[hf]mistralai/Mistral-Nemo-Instruct-2407': {
73
- 'description': 'longer response',
74
- 'max_new_tokens': 8192,
75
- 'paid': False,
76
- },
77
- '[or]google/gemini-2.0-flash-001': {
78
- 'description': 'Google Gemini-2.0-flash-001 (via OpenRouter)',
79
- 'max_new_tokens': 8192,
80
- 'paid': True,
81
- },
82
- '[or]openai/gpt-3.5-turbo': {
83
- 'description': 'OpenAI GPT-3.5 Turbo (via OpenRouter)',
84
- 'max_new_tokens': 4096,
85
- 'paid': True,
86
- },
87
- '[to]meta-llama/Llama-3.3-70B-Instruct-Turbo': {
88
- 'description': 'detailed, slower',
89
- 'max_new_tokens': 4096,
90
- 'paid': True,
91
- },
92
- '[to]meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo-128K': {
93
- 'description': 'shorter, faster',
94
- 'max_new_tokens': 4096,
95
- 'paid': True,
96
- }
97
- }
98
- LLM_PROVIDER_HELP = (
99
- 'LLM provider codes:\n\n'
100
- '- **[az]**: Azure OpenAI\n'
101
- '- **[co]**: Cohere\n'
102
- '- **[gg]**: Google Gemini API\n'
103
- '- **[hf]**: Hugging Face Inference API\n'
104
- '- **[or]**: OpenRouter\n\n'
105
- '- **[to]**: Together AI\n'
106
- '[Find out more](https://github.com/barun-saha/slide-deck-ai?tab=readme-ov-file#summary-of-the-llms)'
107
- )
108
- DEFAULT_MODEL_INDEX = int(os.environ.get('DEFAULT_MODEL_INDEX', '4'))
109
- LLM_MODEL_TEMPERATURE = 0.2
110
- MAX_PAGE_COUNT = 50
111
- LLM_MODEL_MAX_INPUT_LENGTH = 1000 # characters
112
 
113
  LOG_LEVEL = 'DEBUG'
114
- COUNT_TOKENS = False
115
  APP_STRINGS_FILE = 'strings.json'
116
  PRELOAD_DATA_FILE = 'examples/example_02.json'
117
- INITIAL_PROMPT_TEMPLATE = 'prompts/initial_template_v4_two_cols_img.txt'
118
- REFINEMENT_PROMPT_TEMPLATE = 'prompts/refinement_template_v4_two_cols_img.txt'
119
-
120
- LLM_PROGRESS_MAX = 90
121
- ICONS_DIR = 'icons/png128/'
122
- TINY_BERT_MODEL = 'gaunernst/bert-mini-uncased'
123
- EMBEDDINGS_FILE_NAME = 'file_embeddings/embeddings.npy'
124
- ICONS_FILE_NAME = 'file_embeddings/icons.npy'
125
 
126
  PPTX_TEMPLATE_FILES = {
127
- 'Basic': {
128
  'file': 'pptx_templates/Blank.pptx',
129
- 'caption': 'A good start (Uses [photos](https://unsplash.com/photos/AFZ-qBPEceA) by [cetteup](https://unsplash.com/@cetteup?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) on [Unsplash](https://unsplash.com/photos/a-foggy-forest-filled-with-lots-of-trees-d3ci37Gcgxg?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)) 🟧'
130
  },
131
  'Ion Boardroom': {
132
  'file': 'pptx_templates/Ion_Boardroom.pptx',
133
- 'caption': 'Make some bold decisions 🟥'
134
- },
135
- 'Minimalist Sales Pitch': {
136
- 'file': 'pptx_templates/Minimalist_sales_pitch.pptx',
137
- 'caption': 'In high contrast ⬛'
138
  },
139
  'Urban Monochrome': {
140
  'file': 'pptx_templates/Urban_monochrome.pptx',
141
- 'caption': 'Marvel in a monochrome dream'
142
- },
143
  }
144
-
145
- # This is a long text, so not incorporated as a string in `strings.json`
146
- CHAT_USAGE_INSTRUCTIONS = (
147
- 'Briefly describe your topic of presentation in the textbox provided below. For example:\n'
148
- '- Make a slide deck on AI.'
149
- '\n\n'
150
- 'Subsequently, you can add follow-up instructions, e.g.:\n'
151
- '- Can you add a slide on GPUs?'
152
- '\n\n'
153
- ' You can also ask it to refine any particular slide, e.g.:\n'
154
- '- Make the slide with title \'Examples of AI\' a bit more descriptive.'
155
- '\n\n'
156
- 'Finally, click on the download button at the bottom to download the slide deck.'
157
- ' See this [demo video](https://youtu.be/QvAKzNKtk9k) for a brief walkthrough.\n\n'
158
- 'Remember, the conversational interface is meant to (and will) update yor *initial*/'
159
- '*previous* slide deck. If you want to create a new slide deck on a different topic,'
160
- ' start a new chat session by reloading this page.'
161
- '\n\nSlideDeck AI can algo generate a presentation based on a PDF file. You can upload'
162
- ' a PDF file using the chat widget. Only a single file and up to max 50 pages will be'
163
- ' considered. For PDF-based slide deck generation, LLMs with large context windows, such'
164
- ' as Gemini, GPT, and Mistral-Nemo, are recommended. Note: images from the PDF files will'
165
- ' not be used.'
166
- '\n\nAlso, note that the uploaded file might disappear from the page after click.'
167
- ' You do not need to upload the same file again to continue'
168
- ' the interaction and refining—the contents of the PDF file will be retained in the'
169
- ' same interactive session.'
170
- '\n\nCurrently, paid or *free-to-use* LLMs from six different providers are supported.'
171
- ' A [summary of the supported LLMs]('
172
- 'https://github.com/barun-saha/slide-deck-ai/blob/main/README.md#summary-of-the-llms)'
173
- ' is available for reference. SlideDeck AI does **NOT** store your API keys.'
174
- '\n\nSlideDeck AI does not have access to the Web, apart for searching for images relevant'
175
- ' to the slides. Photos are added probabilistically; transparency needs to be changed'
176
- ' manually, if required.\n\n'
177
- '[SlideDeck AI](https://github.com/barun-saha/slide-deck-ai) is an Open-Source project,'
178
- ' released under the'
179
- ' [MIT license](https://github.com/barun-saha/slide-deck-ai?tab=MIT-1-ov-file#readme).'
180
- '\n\n---\n\n'
181
- '© Copyright 2023-2025 Barun Saha.\n\n'
182
- )
183
-
184
-
185
- logging.basicConfig(
186
- level=GlobalConfig.LOG_LEVEL,
187
- format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
188
- datefmt='%Y-%m-%d %H:%M:%S'
189
- )
190
-
191
-
192
- def get_max_output_tokens(llm_name: str) -> int:
193
- """
194
- Get the max output tokens value configured for an LLM. Return a default value if not configured.
195
-
196
- :param llm_name: The name of the LLM.
197
- :return: Max output tokens or a default count.
198
- """
199
-
200
- try:
201
- return GlobalConfig.VALID_MODELS[llm_name]['max_new_tokens']
202
- except KeyError:
203
- return 2048
 
 
 
 
 
1
  import os
 
2
 
3
  from dataclasses import dataclass
4
  from dotenv import load_dotenv
 
9
 
10
  @dataclass(frozen=True)
11
  class GlobalConfig:
12
+ HF_LLM_MODEL_NAME = 'mistralai/Mistral-7B-Instruct-v0.2'
13
+ LLM_MODEL_TEMPERATURE: float = 0.2
14
+ LLM_MODEL_MIN_OUTPUT_LENGTH: int = 50
15
+ LLM_MODEL_MAX_OUTPUT_LENGTH: int = 2000
16
+ LLM_MODEL_MAX_INPUT_LENGTH: int = 300
17
 
18
+ HUGGINGFACEHUB_API_TOKEN = os.environ.get('HUGGINGFACEHUB_API_TOKEN', '')
19
+ METAPHOR_API_KEY = os.environ.get('METAPHOR_API_KEY', '')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  LOG_LEVEL = 'DEBUG'
 
22
  APP_STRINGS_FILE = 'strings.json'
23
  PRELOAD_DATA_FILE = 'examples/example_02.json'
24
+ SLIDES_TEMPLATE_FILE = 'langchain_templates/template_combined.txt'
25
+ JSON_TEMPLATE_FILE = 'langchain_templates/text_to_json_template_02.txt'
 
 
 
 
 
 
26
 
27
  PPTX_TEMPLATE_FILES = {
28
+ 'Blank': {
29
  'file': 'pptx_templates/Blank.pptx',
30
+ 'caption': 'A good start'
31
  },
32
  'Ion Boardroom': {
33
  'file': 'pptx_templates/Ion_Boardroom.pptx',
34
+ 'caption': 'Make some bold decisions'
 
 
 
 
35
  },
36
  'Urban Monochrome': {
37
  'file': 'pptx_templates/Urban_monochrome.pptx',
38
+ 'caption': 'Marvel in a monochrome dream'
39
+ }
40
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helpers/__init__.py DELETED
File without changes
helpers/file_manager.py DELETED
@@ -1,40 +0,0 @@
1
- """
2
- File manager helper to work with uploaded files.
3
- """
4
- import logging
5
- import os
6
- import sys
7
-
8
- import streamlit as st
9
- from pypdf import PdfReader
10
-
11
- sys.path.append('..')
12
- sys.path.append('../..')
13
-
14
- from global_config import GlobalConfig
15
-
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- def get_pdf_contents(
21
- pdf_file: st.runtime.uploaded_file_manager.UploadedFile,
22
- max_pages: int = GlobalConfig.MAX_PAGE_COUNT
23
- ) -> str:
24
- """
25
- Extract the text contents from a PDF file.
26
-
27
- :param pdf_file: The uploaded PDF file.
28
- :param max_pages: The max no. of pages to extract contents from.
29
- :return: The contents.
30
- """
31
-
32
- reader = PdfReader(pdf_file)
33
- n_pages = min(max_pages, len(reader.pages))
34
- text = ''
35
-
36
- for page in range(n_pages):
37
- page = reader.pages[page]
38
- text += page.extract_text()
39
-
40
- return text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helpers/icons_embeddings.py DELETED
@@ -1,166 +0,0 @@
1
- """
2
- Generate and save the embeddings of a pre-defined list of icons.
3
- Compare them with keywords embeddings to find most relevant icons.
4
- """
5
- import os
6
- import pathlib
7
- import sys
8
- from typing import List, Tuple
9
-
10
- import numpy as np
11
- from sklearn.metrics.pairwise import cosine_similarity
12
- from transformers import BertTokenizer, BertModel
13
-
14
- sys.path.append('..')
15
- sys.path.append('../..')
16
-
17
- from global_config import GlobalConfig
18
-
19
-
20
- tokenizer = BertTokenizer.from_pretrained(GlobalConfig.TINY_BERT_MODEL)
21
- model = BertModel.from_pretrained(GlobalConfig.TINY_BERT_MODEL)
22
-
23
-
24
- def get_icons_list() -> List[str]:
25
- """
26
- Get a list of available icons.
27
-
28
- :return: The icons file names.
29
- """
30
-
31
- items = pathlib.Path('../' + GlobalConfig.ICONS_DIR).glob('*.png')
32
- items = [
33
- os.path.basename(str(item)).removesuffix('.png') for item in items
34
- ]
35
-
36
- return items
37
-
38
-
39
- def get_embeddings(texts) -> np.ndarray:
40
- """
41
- Generate embeddings for a list of texts using a pre-trained language model.
42
-
43
- :param texts: A string or a list of strings to be converted into embeddings.
44
- :type texts: Union[str, List[str]]
45
- :return: A NumPy array containing the embeddings for the input texts.
46
- :rtype: numpy.ndarray
47
-
48
- :raises ValueError: If the input is not a string or a list of strings, or if any element
49
- in the list is not a string.
50
-
51
- Example usage:
52
- >>> keyword = 'neural network'
53
- >>> file_names = ['neural_network_icon.png', 'data_analysis_icon.png', 'machine_learning.png']
54
- >>> keyword_embeddings = get_embeddings(keyword)
55
- >>> file_name_embeddings = get_embeddings(file_names)
56
- """
57
-
58
- inputs = tokenizer(texts, return_tensors='pt', padding=True, max_length=128, truncation=True)
59
- outputs = model(**inputs)
60
-
61
- return outputs.last_hidden_state.mean(dim=1).detach().numpy()
62
-
63
-
64
- def save_icons_embeddings():
65
- """
66
- Generate and save the embeddings for the icon file names.
67
- """
68
-
69
- file_names = get_icons_list()
70
- print(f'{len(file_names)} icon files available...')
71
- file_name_embeddings = get_embeddings(file_names)
72
- print(f'file_name_embeddings.shape: {file_name_embeddings.shape}')
73
-
74
- # Save embeddings to a file
75
- np.save(GlobalConfig.EMBEDDINGS_FILE_NAME, file_name_embeddings)
76
- np.save(GlobalConfig.ICONS_FILE_NAME, file_names) # Save file names for reference
77
-
78
-
79
- def load_saved_embeddings() -> Tuple[np.ndarray, np.ndarray]:
80
- """
81
- Load precomputed embeddings and icons file names.
82
-
83
- :return: The embeddings and the icon file names.
84
- """
85
-
86
- file_name_embeddings = np.load(GlobalConfig.EMBEDDINGS_FILE_NAME)
87
- file_names = np.load(GlobalConfig.ICONS_FILE_NAME)
88
-
89
- return file_name_embeddings, file_names
90
-
91
-
92
- def find_icons(keywords: List[str]) -> List[str]:
93
- """
94
- Find relevant icon file names for a list of keywords.
95
-
96
- :param keywords: The list of one or more keywords.
97
- :return: A list of the file names relevant for each keyword.
98
- """
99
-
100
- keyword_embeddings = get_embeddings(keywords)
101
- file_name_embeddings, file_names = load_saved_embeddings()
102
-
103
- # Compute similarity
104
- similarities = cosine_similarity(keyword_embeddings, file_name_embeddings)
105
- icon_files = file_names[np.argmax(similarities, axis=-1)]
106
-
107
- return icon_files
108
-
109
-
110
- def main():
111
- """
112
- Example usage.
113
- """
114
-
115
- # Run this again if icons are to be added/removed
116
- save_icons_embeddings()
117
-
118
- keywords = [
119
- 'deep learning',
120
- '',
121
- 'recycling',
122
- 'handshake',
123
- 'Ferry',
124
- 'rain drop',
125
- 'speech bubble',
126
- 'mental resilience',
127
- 'turmeric',
128
- 'Art',
129
- 'price tag',
130
- 'Oxygen',
131
- 'oxygen',
132
- 'Social Connection',
133
- 'Accomplishment',
134
- 'Python',
135
- 'XML',
136
- 'Handshake',
137
- ]
138
- icon_files = find_icons(keywords)
139
- print(
140
- f'The relevant icon files are:\n'
141
- f'{list(zip(keywords, icon_files))}'
142
- )
143
-
144
- # BERT tiny:
145
- # [('deep learning', 'deep-learning'), ('', '123'), ('recycling', 'refinery'),
146
- # ('handshake', 'dash-circle'), ('Ferry', 'cart'), ('rain drop', 'bucket'),
147
- # ('speech bubble', 'globe'), ('mental resilience', 'exclamation-triangle'),
148
- # ('turmeric', 'kebab'), ('Art', 'display'), ('price tag', 'bug-fill'),
149
- # ('Oxygen', 'radioactive')]
150
-
151
- # BERT mini
152
- # [('deep learning', 'deep-learning'), ('', 'compass'), ('recycling', 'tools'),
153
- # ('handshake', 'bandaid'), ('Ferry', 'cart'), ('rain drop', 'trash'),
154
- # ('speech bubble', 'image'), ('mental resilience', 'recycle'), ('turmeric', 'linkedin'),
155
- # ('Art', 'book'), ('price tag', 'card-image'), ('Oxygen', 'radioactive')]
156
-
157
- # BERT small
158
- # [('deep learning', 'deep-learning'), ('', 'gem'), ('recycling', 'tools'),
159
- # ('handshake', 'handbag'), ('Ferry', 'truck'), ('rain drop', 'bucket'),
160
- # ('speech bubble', 'strategy'), ('mental resilience', 'deep-learning'),
161
- # ('turmeric', 'flower'),
162
- # ('Art', 'book'), ('price tag', 'hotdog'), ('Oxygen', 'radioactive')]
163
-
164
-
165
- if __name__ == '__main__':
166
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helpers/image_search.py DELETED
@@ -1,148 +0,0 @@
1
- """
2
- Search photos using Pexels API.
3
- """
4
- import logging
5
- import os
6
- import random
7
- from io import BytesIO
8
- from typing import Union, Tuple, Literal
9
- from urllib.parse import urlparse, parse_qs
10
-
11
- import requests
12
- from dotenv import load_dotenv
13
-
14
-
15
- load_dotenv()
16
-
17
-
18
- REQUEST_TIMEOUT = 12
19
- MAX_PHOTOS = 3
20
-
21
-
22
- # Only show errors
23
- logging.getLogger('urllib3').setLevel(logging.ERROR)
24
- # Disable all child loggers of urllib3, e.g. urllib3.connectionpool
25
- # logging.getLogger('urllib3').propagate = True
26
-
27
-
28
-
29
- def search_pexels(
30
- query: str,
31
- size: Literal['small', 'medium', 'large'] = 'medium',
32
- per_page: int = MAX_PHOTOS
33
- ) -> dict:
34
- """
35
- Searches for images on Pexels using the provided query.
36
-
37
- This function sends a GET request to the Pexels API with the specified search query
38
- and authorization header containing the API key. It returns the JSON response from the API.
39
-
40
- [2024-08-31] Note:
41
- `curl` succeeds but API call via Python `requests` fail. Apparently, this could be due to
42
- Cloudflare (or others) blocking the requests, perhaps identifying as Web-scraping. So,
43
- changing the user-agent to Firefox.
44
- https://stackoverflow.com/a/74674276/147021
45
- https://stackoverflow.com/a/51268523/147021
46
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox#linux
47
-
48
- :param query: The search query for finding images.
49
- :param size: The size of the images: small, medium, or large.
50
- :param per_page: No. of results to be displayed per page.
51
- :return: The JSON response from the Pexels API containing search results.
52
- :raises requests.exceptions.RequestException: If the request to the Pexels API fails.
53
- """
54
-
55
- url = 'https://api.pexels.com/v1/search'
56
- headers = {
57
- 'Authorization': os.getenv('PEXEL_API_KEY'),
58
- 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0',
59
- }
60
- params = {
61
- 'query': query,
62
- 'size': size,
63
- 'page': 1,
64
- 'per_page': per_page
65
- }
66
- response = requests.get(url, headers=headers, params=params, timeout=REQUEST_TIMEOUT)
67
- response.raise_for_status() # Ensure the request was successful
68
-
69
- return response.json()
70
-
71
-
72
- def get_photo_url_from_api_response(
73
- json_response: dict
74
- ) -> Tuple[Union[str, None], Union[str, None]]:
75
- """
76
- Return a randomly chosen photo from a Pexels search API response. In addition, also return
77
- the original URL of the page on Pexels.
78
-
79
- :param json_response: The JSON response.
80
- :return: The selected photo URL and page URL or `None`.
81
- """
82
-
83
- page_url = None
84
- photo_url = None
85
-
86
- if 'photos' in json_response:
87
- photos = json_response['photos']
88
-
89
- if photos:
90
- photo_idx = random.choice(list(range(MAX_PHOTOS)))
91
- photo = photos[photo_idx]
92
-
93
- if 'url' in photo:
94
- page_url = photo['url']
95
-
96
- if 'src' in photo:
97
- if 'large' in photo['src']:
98
- photo_url = photo['src']['large']
99
- elif 'original' in photo['src']:
100
- photo_url = photo['src']['original']
101
-
102
- return photo_url, page_url
103
-
104
-
105
- def get_image_from_url(url: str) -> BytesIO:
106
- """
107
- Fetches an image from the specified URL and returns it as a BytesIO object.
108
-
109
- This function sends a GET request to the provided URL, retrieves the image data,
110
- and wraps it in a BytesIO object, which can be used like a file.
111
-
112
- :param url: The URL of the image to be fetched.
113
- :return: A BytesIO object containing the image data.
114
- :raises requests.exceptions.RequestException: If the request to the URL fails.
115
- """
116
-
117
- headers = {
118
- 'Authorization': os.getenv('PEXEL_API_KEY'),
119
- 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0',
120
- }
121
- response = requests.get(url, headers=headers, stream=True, timeout=REQUEST_TIMEOUT)
122
- response.raise_for_status()
123
- image_data = BytesIO(response.content)
124
-
125
- return image_data
126
-
127
-
128
- def extract_dimensions(url: str) -> Tuple[int, int]:
129
- """
130
- Extracts the height and width from the URL parameters.
131
-
132
- :param url: The URL containing the image dimensions.
133
- :return: A tuple containing the width and height as integers.
134
- """
135
- parsed_url = urlparse(url)
136
- query_params = parse_qs(parsed_url.query)
137
- width = int(query_params.get('w', [0])[0])
138
- height = int(query_params.get('h', [0])[0])
139
-
140
- return width, height
141
-
142
-
143
- if __name__ == '__main__':
144
- print(
145
- search_pexels(
146
- query='people'
147
- )
148
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helpers/llm_helper.py DELETED
@@ -1,257 +0,0 @@
1
- """
2
- Helper functions to access LLMs.
3
- """
4
- import logging
5
- import re
6
- import sys
7
- import urllib3
8
- from typing import Tuple, Union
9
-
10
- import requests
11
- from requests.adapters import HTTPAdapter
12
- from urllib3.util import Retry
13
- from langchain_core.language_models import BaseLLM, BaseChatModel
14
- import os
15
-
16
- sys.path.append('..')
17
-
18
- from global_config import GlobalConfig
19
-
20
-
21
- LLM_PROVIDER_MODEL_REGEX = re.compile(r'\[(.*?)\](.*)')
22
- OLLAMA_MODEL_REGEX = re.compile(r'[a-zA-Z0-9._:-]+$')
23
- # 94 characters long, only containing alphanumeric characters, hyphens, and underscores
24
- API_KEY_REGEX = re.compile(r'^[a-zA-Z0-9_-]{6,94}$')
25
- REQUEST_TIMEOUT = 35
26
- OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1'
27
-
28
-
29
- logger = logging.getLogger(__name__)
30
- logging.getLogger('httpx').setLevel(logging.WARNING)
31
- logging.getLogger('httpcore').setLevel(logging.WARNING)
32
- logging.getLogger('openai').setLevel(logging.ERROR)
33
-
34
- retries = Retry(
35
- total=5,
36
- backoff_factor=0.25,
37
- backoff_jitter=0.3,
38
- status_forcelist=[502, 503, 504],
39
- allowed_methods={'POST'},
40
- )
41
- adapter = HTTPAdapter(max_retries=retries)
42
- http_session = requests.Session()
43
- http_session.mount('https://', adapter)
44
- http_session.mount('http://', adapter)
45
-
46
-
47
- def get_provider_model(provider_model: str, use_ollama: bool) -> Tuple[str, str]:
48
- """
49
- Parse and get LLM provider and model name from strings like `[provider]model/name-version`.
50
-
51
- :param provider_model: The provider, model name string from `GlobalConfig`.
52
- :param use_ollama: Whether Ollama is used (i.e., running in offline mode).
53
- :return: The provider and the model name; empty strings in case no matching pattern found.
54
- """
55
-
56
- provider_model = provider_model.strip()
57
-
58
- if use_ollama:
59
- match = OLLAMA_MODEL_REGEX.match(provider_model)
60
- if match:
61
- return GlobalConfig.PROVIDER_OLLAMA, match.group(0)
62
- else:
63
- match = LLM_PROVIDER_MODEL_REGEX.match(provider_model)
64
-
65
- if match:
66
- inside_brackets = match.group(1)
67
- outside_brackets = match.group(2)
68
- return inside_brackets, outside_brackets
69
-
70
- return '', ''
71
-
72
-
73
- def is_valid_llm_provider_model(
74
- provider: str,
75
- model: str,
76
- api_key: str,
77
- azure_endpoint_url: str = '',
78
- azure_deployment_name: str = '',
79
- azure_api_version: str = '',
80
- ) -> bool:
81
- """
82
- Verify whether LLM settings are proper.
83
- This function does not verify whether `api_key` is correct. It only confirms that the key has
84
- at least five characters. Key verification is done when the LLM is created.
85
-
86
- :param provider: Name of the LLM provider.
87
- :param model: Name of the model.
88
- :param api_key: The API key or access token.
89
- :param azure_endpoint_url: Azure OpenAI endpoint URL.
90
- :param azure_deployment_name: Azure OpenAI deployment name.
91
- :param azure_api_version: Azure OpenAI API version.
92
- :return: `True` if the settings "look" OK; `False` otherwise.
93
- """
94
-
95
- if not provider or not model or provider not in GlobalConfig.VALID_PROVIDERS:
96
- return False
97
-
98
- if not api_key:
99
- return False
100
-
101
- if api_key and API_KEY_REGEX.match(api_key) is None:
102
- return False
103
-
104
- if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
105
- valid_url = urllib3.util.parse_url(azure_endpoint_url)
106
- all_status = all(
107
- [azure_api_version, azure_deployment_name, str(valid_url)]
108
- )
109
- return all_status
110
-
111
- return True
112
-
113
-
114
- def get_langchain_llm(
115
- provider: str,
116
- model: str,
117
- max_new_tokens: int,
118
- api_key: str = '',
119
- azure_endpoint_url: str = '',
120
- azure_deployment_name: str = '',
121
- azure_api_version: str = '',
122
- ) -> Union[BaseLLM, BaseChatModel, None]:
123
- """
124
- Get an LLM based on the provider and model specified.
125
-
126
- :param provider: The LLM provider. Valid values are `hf` for Hugging Face.
127
- :param model: The name of the LLM.
128
- :param max_new_tokens: The maximum number of tokens to generate.
129
- :param api_key: API key or access token to use.
130
- :param azure_endpoint_url: Azure OpenAI endpoint URL.
131
- :param azure_deployment_name: Azure OpenAI deployment name.
132
- :param azure_api_version: Azure OpenAI API version.
133
- :return: An instance of the LLM or Chat model; `None` in case of any error.
134
- """
135
-
136
- if provider == GlobalConfig.PROVIDER_HUGGING_FACE:
137
- from langchain_community.llms.huggingface_endpoint import HuggingFaceEndpoint
138
-
139
- logger.debug('Getting LLM via HF endpoint: %s', model)
140
- return HuggingFaceEndpoint(
141
- repo_id=model,
142
- max_new_tokens=max_new_tokens,
143
- top_k=40,
144
- top_p=0.95,
145
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
146
- repetition_penalty=1.03,
147
- streaming=True,
148
- huggingfacehub_api_token=api_key,
149
- return_full_text=False,
150
- stop_sequences=['</s>'],
151
- )
152
-
153
- if provider == GlobalConfig.PROVIDER_GOOGLE_GEMINI:
154
- from google.generativeai.types.safety_types import HarmBlockThreshold, HarmCategory
155
- from langchain_google_genai import GoogleGenerativeAI
156
-
157
- logger.debug('Getting LLM via Google Gemini: %s', model)
158
- return GoogleGenerativeAI(
159
- model=model,
160
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
161
- # max_tokens=max_new_tokens,
162
- timeout=None,
163
- max_retries=2,
164
- google_api_key=api_key,
165
- safety_settings={
166
- HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT:
167
- HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
168
- HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
169
- HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
170
- HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT:
171
- HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
172
- }
173
- )
174
-
175
- if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
176
- from langchain_openai import AzureChatOpenAI
177
-
178
- logger.debug('Getting LLM via Azure OpenAI: %s', model)
179
-
180
- # The `model` parameter is not used here; `azure_deployment` points to the desired name
181
- return AzureChatOpenAI(
182
- azure_deployment=azure_deployment_name,
183
- api_version=azure_api_version,
184
- azure_endpoint=azure_endpoint_url,
185
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
186
- # max_tokens=max_new_tokens,
187
- timeout=None,
188
- max_retries=1,
189
- api_key=api_key,
190
- )
191
-
192
- if provider == GlobalConfig.PROVIDER_OPENROUTER:
193
- # Use langchain-openai's ChatOpenAI for OpenRouter
194
- from langchain_openai import ChatOpenAI
195
-
196
- logger.debug('Getting LLM via OpenRouter: %s', model)
197
- openrouter_api_key = api_key
198
-
199
- return ChatOpenAI(
200
- base_url=OPENROUTER_BASE_URL,
201
- openai_api_key=openrouter_api_key,
202
- model_name=model,
203
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
204
- max_tokens=max_new_tokens,
205
- streaming=True,
206
- )
207
-
208
- if provider == GlobalConfig.PROVIDER_COHERE:
209
- from langchain_cohere.llms import Cohere
210
-
211
- logger.debug('Getting LLM via Cohere: %s', model)
212
- return Cohere(
213
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
214
- max_tokens=max_new_tokens,
215
- timeout_seconds=None,
216
- max_retries=2,
217
- cohere_api_key=api_key,
218
- streaming=True,
219
- )
220
-
221
- if provider == GlobalConfig.PROVIDER_TOGETHER_AI:
222
- from langchain_together import Together
223
-
224
- logger.debug('Getting LLM via Together AI: %s', model)
225
- return Together(
226
- model=model,
227
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
228
- together_api_key=api_key,
229
- max_tokens=max_new_tokens,
230
- top_k=40,
231
- top_p=0.90,
232
- )
233
-
234
- if provider == GlobalConfig.PROVIDER_OLLAMA:
235
- from langchain_ollama.llms import OllamaLLM
236
-
237
- logger.debug('Getting LLM via Ollama: %s', model)
238
- return OllamaLLM(
239
- model=model,
240
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
241
- num_predict=max_new_tokens,
242
- format='json',
243
- streaming=True,
244
- )
245
-
246
- return None
247
-
248
-
249
- if __name__ == '__main__':
250
- inputs = [
251
- '[co]Cohere',
252
- '[hf]mistralai/Mistral-7B-Instruct-v0.2',
253
- '[gg]gemini-1.5-flash-002'
254
- ]
255
-
256
- for text in inputs:
257
- print(get_provider_model(text, use_ollama=False))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helpers/pptx_helper.py DELETED
@@ -1,1084 +0,0 @@
1
- """
2
- A set of functions to create a PowerPoint slide deck.
3
- """
4
- import logging
5
- import os
6
- import pathlib
7
- import random
8
- import re
9
- import sys
10
- import tempfile
11
- from typing import List, Tuple, Optional
12
-
13
- import json5
14
- import pptx
15
- from dotenv import load_dotenv
16
- from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
17
- from pptx.shapes.placeholder import PicturePlaceholder, SlidePlaceholder
18
-
19
- sys.path.append('..')
20
- sys.path.append('../..')
21
-
22
- import helpers.icons_embeddings as ice
23
- import helpers.image_search as ims
24
- from global_config import GlobalConfig
25
-
26
-
27
- load_dotenv()
28
-
29
-
30
- # English Metric Unit (used by PowerPoint) to inches
31
- EMU_TO_INCH_SCALING_FACTOR = 1.0 / 914400
32
- INCHES_3 = pptx.util.Inches(3)
33
- INCHES_2 = pptx.util.Inches(2)
34
- INCHES_1_5 = pptx.util.Inches(1.5)
35
- INCHES_1 = pptx.util.Inches(1)
36
- INCHES_0_8 = pptx.util.Inches(0.8)
37
- INCHES_0_9 = pptx.util.Inches(0.9)
38
- INCHES_0_5 = pptx.util.Inches(0.5)
39
- INCHES_0_4 = pptx.util.Inches(0.4)
40
- INCHES_0_3 = pptx.util.Inches(0.3)
41
- INCHES_0_2 = pptx.util.Inches(0.2)
42
-
43
- STEP_BY_STEP_PROCESS_MARKER = '>> '
44
- ICON_BEGINNING_MARKER = '[['
45
- ICON_END_MARKER = ']]'
46
-
47
- ICON_SIZE = INCHES_0_8
48
- ICON_BG_SIZE = INCHES_1
49
-
50
- IMAGE_DISPLAY_PROBABILITY = 1 / 3.0
51
- FOREGROUND_IMAGE_PROBABILITY = 0.8
52
-
53
- SLIDE_NUMBER_REGEX = re.compile(r"^slide[ ]+\d+:", re.IGNORECASE)
54
- ICONS_REGEX = re.compile(r"\[\[(.*?)\]\]\s*(.*)")
55
- BOLD_ITALICS_PATTERN = re.compile(r'(\*\*(.*?)\*\*|\*(.*?)\*)')
56
-
57
- ICON_COLORS = [
58
- pptx.dml.color.RGBColor.from_string('800000'), # Maroon
59
- pptx.dml.color.RGBColor.from_string('6A5ACD'), # SlateBlue
60
- pptx.dml.color.RGBColor.from_string('556B2F'), # DarkOliveGreen
61
- pptx.dml.color.RGBColor.from_string('2F4F4F'), # DarkSlateGray
62
- pptx.dml.color.RGBColor.from_string('4682B4'), # SteelBlue
63
- pptx.dml.color.RGBColor.from_string('5F9EA0'), # CadetBlue
64
- ]
65
-
66
-
67
- logger = logging.getLogger(__name__)
68
- logging.getLogger('PIL.PngImagePlugin').setLevel(logging.ERROR)
69
-
70
-
71
- def remove_slide_number_from_heading(header: str) -> str:
72
- """
73
- Remove the slide number from a given slide header.
74
-
75
- :param header: The header of a slide.
76
- :return: The header without slide number.
77
- """
78
-
79
- if SLIDE_NUMBER_REGEX.match(header):
80
- idx = header.find(':')
81
- header = header[idx + 1:]
82
-
83
- return header
84
-
85
-
86
- def add_bulleted_items(text_frame: pptx.text.text.TextFrame, flat_items_list: list):
87
- """
88
- Add a list of texts as bullet points and apply formatting.
89
-
90
- :param text_frame: The text frame where text is to be displayed.
91
- :param flat_items_list: The list of items to be displayed.
92
- """
93
-
94
- for idx, an_item in enumerate(flat_items_list):
95
- if idx == 0:
96
- paragraph = text_frame.paragraphs[0] # First paragraph for title text
97
- else:
98
- paragraph = text_frame.add_paragraph()
99
- paragraph.level = an_item[1]
100
-
101
- format_text(paragraph, an_item[0].removeprefix(STEP_BY_STEP_PROCESS_MARKER))
102
-
103
-
104
- def format_text(frame_paragraph, text):
105
- """
106
- Apply bold and italic formatting while preserving the original word order
107
- without duplication.
108
- """
109
-
110
- matches = list(BOLD_ITALICS_PATTERN.finditer(text))
111
- last_index = 0 # Track position in the text
112
- # Group 0: Full match (e.g., **bold** or *italic*)
113
- # Group 1: The outer parentheses (captures either bold or italic match, because of |)
114
- # Group 2: The bold text inside **bold**
115
- # Group 3: The italic text inside *italic*
116
- for match in matches:
117
- start, end = match.span()
118
- # Add unformatted text before the formatted section
119
- if start > last_index:
120
- run = frame_paragraph.add_run()
121
- run.text = text[last_index:start]
122
-
123
- # Extract formatted text
124
- if match.group(2): # Bold
125
- run = frame_paragraph.add_run()
126
- run.text = match.group(2)
127
- run.font.bold = True
128
- elif match.group(3): # Italics
129
- run = frame_paragraph.add_run()
130
- run.text = match.group(3)
131
- run.font.italic = True
132
-
133
- last_index = end # Update position
134
-
135
- # Add any remaining unformatted text
136
- if last_index < len(text):
137
- run = frame_paragraph.add_run()
138
- run.text = text[last_index:]
139
-
140
-
141
- def generate_powerpoint_presentation(
142
- parsed_data: dict,
143
- slides_template: str,
144
- output_file_path: pathlib.Path
145
- ) -> List:
146
- """
147
- Create and save a PowerPoint presentation file containing the content in JSON format.
148
-
149
- :param parsed_data: The presentation content as parsed JSON data.
150
- :param slides_template: The PPTX template to use.
151
- :param output_file_path: The path of the PPTX file to save as.
152
- :return: A list of presentation title and slides headers.
153
- """
154
-
155
- presentation = pptx.Presentation(GlobalConfig.PPTX_TEMPLATE_FILES[slides_template]['file'])
156
- slide_width_inch, slide_height_inch = _get_slide_width_height_inches(presentation)
157
-
158
- # The title slide
159
- title_slide_layout = presentation.slide_layouts[0]
160
- slide = presentation.slides.add_slide(title_slide_layout)
161
- title = slide.shapes.title
162
- subtitle = slide.placeholders[1]
163
- title.text = parsed_data['title']
164
- logger.info(
165
- 'PPT title: %s | #slides: %d | template: %s',
166
- title.text, len(parsed_data['slides']),
167
- GlobalConfig.PPTX_TEMPLATE_FILES[slides_template]['file']
168
- )
169
- subtitle.text = 'by Myself and SlideDeck AI :)'
170
- all_headers = [title.text, ]
171
-
172
- # Add content in a loop
173
- for a_slide in parsed_data['slides']:
174
- try:
175
- is_processing_done = _handle_icons_ideas(
176
- presentation=presentation,
177
- slide_json=a_slide,
178
- slide_width_inch=slide_width_inch,
179
- slide_height_inch=slide_height_inch
180
- )
181
-
182
- if not is_processing_done:
183
- is_processing_done = _handle_table(
184
- presentation=presentation,
185
- slide_json=a_slide,
186
- slide_width_inch=slide_width_inch,
187
- slide_height_inch=slide_height_inch
188
- )
189
-
190
- if not is_processing_done:
191
- is_processing_done = _handle_double_col_layout(
192
- presentation=presentation,
193
- slide_json=a_slide,
194
- slide_width_inch=slide_width_inch,
195
- slide_height_inch=slide_height_inch
196
- )
197
-
198
- if not is_processing_done:
199
- is_processing_done = _handle_step_by_step_process(
200
- presentation=presentation,
201
- slide_json=a_slide,
202
- slide_width_inch=slide_width_inch,
203
- slide_height_inch=slide_height_inch
204
- )
205
-
206
- if not is_processing_done:
207
- _handle_default_display(
208
- presentation=presentation,
209
- slide_json=a_slide,
210
- slide_width_inch=slide_width_inch,
211
- slide_height_inch=slide_height_inch
212
- )
213
-
214
- except Exception:
215
- # In case of any unforeseen error, try to salvage what is available
216
- continue
217
-
218
- # The thank-you slide
219
- last_slide_layout = presentation.slide_layouts[0]
220
- slide = presentation.slides.add_slide(last_slide_layout)
221
- title = slide.shapes.title
222
- title.text = 'Thank you!'
223
-
224
- presentation.save(output_file_path)
225
-
226
- return all_headers
227
-
228
-
229
- def get_flat_list_of_contents(items: list, level: int) -> List[Tuple]:
230
- """
231
- Flatten a (hierarchical) list of bullet points to a single list containing each item and
232
- its level.
233
-
234
- :param items: A bullet point (string or list).
235
- :param level: The current level of hierarchy.
236
- :return: A list of (bullet item text, hierarchical level) tuples.
237
- """
238
-
239
- flat_list = []
240
-
241
- for item in items:
242
- if isinstance(item, str):
243
- flat_list.append((item, level))
244
- elif isinstance(item, list):
245
- flat_list = flat_list + get_flat_list_of_contents(item, level + 1)
246
-
247
- return flat_list
248
-
249
-
250
- def get_slide_placeholders(
251
- slide: pptx.slide.Slide,
252
- layout_number: int,
253
- is_debug: bool = False
254
- ) -> List[Tuple[int, str]]:
255
- """
256
- Return the index and name (lower case) of all placeholders present in a slide, except
257
- the title placeholder.
258
-
259
- A placeholder in a slide is a place to add content. Each placeholder has a name and an index.
260
- This index is NOT a list index, rather a set of keys used to look up a dict. So, `idx` is
261
- non-contiguous. Also, the title placeholder of a slide always has index 0. User-added
262
- placeholder get indices assigned starting from 10.
263
-
264
- With user-edited or added placeholders, their index may be difficult to track. This function
265
- returns the placeholders name as well, which could be useful to distinguish between the
266
- different placeholder.
267
-
268
- :param slide: The slide.
269
- :param layout_number: The layout number used by the slide.
270
- :param is_debug: Whether to print debugging statements.
271
- :return: A list containing placeholders (idx, name) tuples, except the title placeholder.
272
- """
273
-
274
- if is_debug:
275
- print(
276
- f'Slide layout #{layout_number}:'
277
- f' # of placeholders: {len(slide.shapes.placeholders)} (including the title)'
278
- )
279
-
280
- placeholders = [
281
- (shape.placeholder_format.idx, shape.name.lower()) for shape in slide.shapes.placeholders
282
- ]
283
- placeholders.pop(0) # Remove the title placeholder
284
-
285
- if is_debug:
286
- print(placeholders)
287
-
288
- return placeholders
289
-
290
-
291
- def _handle_default_display(
292
- presentation: pptx.Presentation,
293
- slide_json: dict,
294
- slide_width_inch: float,
295
- slide_height_inch: float
296
- ):
297
- """
298
- Display a list of text in a slide.
299
-
300
- :param presentation: The presentation object.
301
- :param slide_json: The content of the slide as JSON data.
302
- :param slide_width_inch: The width of the slide in inches.
303
- :param slide_height_inch: The height of the slide in inches.
304
- """
305
-
306
- status = False
307
-
308
- if 'img_keywords' in slide_json:
309
- if random.random() < IMAGE_DISPLAY_PROBABILITY:
310
- if random.random() < FOREGROUND_IMAGE_PROBABILITY:
311
- status = _handle_display_image__in_foreground(
312
- presentation,
313
- slide_json,
314
- slide_width_inch,
315
- slide_height_inch
316
- )
317
- else:
318
- status = _handle_display_image__in_background(
319
- presentation,
320
- slide_json,
321
- slide_width_inch,
322
- slide_height_inch
323
- )
324
-
325
- if status:
326
- return
327
-
328
- # Image display failed, so display only text
329
- bullet_slide_layout = presentation.slide_layouts[1]
330
- slide = presentation.slides.add_slide(bullet_slide_layout)
331
-
332
- shapes = slide.shapes
333
- title_shape = shapes.title
334
-
335
- try:
336
- body_shape = shapes.placeholders[1]
337
- except KeyError:
338
- placeholders = get_slide_placeholders(slide, layout_number=1)
339
- body_shape = shapes.placeholders[placeholders[0][0]]
340
-
341
- title_shape.text = remove_slide_number_from_heading(slide_json['heading'])
342
- text_frame = body_shape.text_frame
343
-
344
- # The bullet_points may contain a nested hierarchy of JSON arrays
345
- # In some scenarios, it may contain objects (dictionaries) because the LLM generated so
346
- # ^ The second scenario is not covered
347
- flat_items_list = get_flat_list_of_contents(slide_json['bullet_points'], level=0)
348
- add_bulleted_items(text_frame, flat_items_list)
349
-
350
- _handle_key_message(
351
- the_slide=slide,
352
- slide_json=slide_json,
353
- slide_height_inch=slide_height_inch,
354
- slide_width_inch=slide_width_inch
355
- )
356
-
357
-
358
- def _handle_display_image__in_foreground(
359
- presentation: pptx.Presentation(),
360
- slide_json: dict,
361
- slide_width_inch: float,
362
- slide_height_inch: float
363
- ) -> bool:
364
- """
365
- Create a slide with text and image using a picture placeholder layout. If not image keyword is
366
- available, it will add only text to the slide.
367
-
368
- :param presentation: The presentation object.
369
- :param slide_json: The content of the slide as JSON data.
370
- :param slide_width_inch: The width of the slide in inches.
371
- :param slide_height_inch: The height of the slide in inches.
372
- :return: True if the side has been processed.
373
- """
374
-
375
- img_keywords = slide_json['img_keywords'].strip()
376
- slide = presentation.slide_layouts[8] # Picture with Caption
377
- slide = presentation.slides.add_slide(slide)
378
- placeholders = None
379
-
380
- title_placeholder = slide.shapes.title
381
- title_placeholder.text = remove_slide_number_from_heading(slide_json['heading'])
382
-
383
- try:
384
- pic_col: PicturePlaceholder = slide.shapes.placeholders[1]
385
- except KeyError:
386
- placeholders = get_slide_placeholders(slide, layout_number=8)
387
- pic_col = None
388
- for idx, name in placeholders:
389
- if 'picture' in name:
390
- pic_col: PicturePlaceholder = slide.shapes.placeholders[idx]
391
-
392
- try:
393
- text_col: SlidePlaceholder = slide.shapes.placeholders[2]
394
- except KeyError:
395
- text_col = None
396
- if not placeholders:
397
- placeholders = get_slide_placeholders(slide, layout_number=8)
398
-
399
- for idx, name in placeholders:
400
- if 'content' in name:
401
- text_col: SlidePlaceholder = slide.shapes.placeholders[idx]
402
-
403
- flat_items_list = get_flat_list_of_contents(slide_json['bullet_points'], level=0)
404
- add_bulleted_items(text_col.text_frame, flat_items_list)
405
-
406
- if not img_keywords:
407
- # No keywords, so no image search and addition
408
- return True
409
-
410
- try:
411
- photo_url, page_url = ims.get_photo_url_from_api_response(
412
- ims.search_pexels(query=img_keywords, size='medium')
413
- )
414
-
415
- if photo_url:
416
- pic_col.insert_picture(
417
- ims.get_image_from_url(photo_url)
418
- )
419
-
420
- _add_text_at_bottom(
421
- slide=slide,
422
- slide_width_inch=slide_width_inch,
423
- slide_height_inch=slide_height_inch,
424
- text='Photo provided by Pexels',
425
- hyperlink=page_url
426
- )
427
- except Exception as ex:
428
- logger.error(
429
- '*** Error occurred while running adding image to slide: %s',
430
- str(ex)
431
- )
432
-
433
- return True
434
-
435
-
436
- def _handle_display_image__in_background(
437
- presentation: pptx.Presentation(),
438
- slide_json: dict,
439
- slide_width_inch: float,
440
- slide_height_inch: float
441
- ) -> bool:
442
- """
443
- Add a slide with text and an image in the background. It works just like
444
- `_handle_default_display()` but with a background image added. If not image keyword is
445
- available, it will add only text to the slide.
446
-
447
- :param presentation: The presentation object.
448
- :param slide_json: The content of the slide as JSON data.
449
- :param slide_width_inch: The width of the slide in inches.
450
- :param slide_height_inch: The height of the slide in inches.
451
- :return: True if the slide has been processed.
452
- """
453
-
454
- img_keywords = slide_json['img_keywords'].strip()
455
-
456
- # Add a photo in the background, text in the foreground
457
- slide = presentation.slides.add_slide(presentation.slide_layouts[1])
458
- title_shape = slide.shapes.title
459
-
460
- try:
461
- body_shape = slide.shapes.placeholders[1]
462
- except KeyError:
463
- placeholders = get_slide_placeholders(slide, layout_number=1)
464
- # Layout 1 usually has two placeholders, including the title
465
- body_shape = slide.shapes.placeholders[placeholders[0][0]]
466
-
467
- title_shape.text = remove_slide_number_from_heading(slide_json['heading'])
468
-
469
- flat_items_list = get_flat_list_of_contents(slide_json['bullet_points'], level=0)
470
- add_bulleted_items(body_shape.text_frame, flat_items_list)
471
-
472
- if not img_keywords:
473
- # No keywords, so no image search and addition
474
- return True
475
-
476
- try:
477
- photo_url, page_url = ims.get_photo_url_from_api_response(
478
- ims.search_pexels(query=img_keywords, size='large')
479
- )
480
-
481
- if photo_url:
482
- picture = slide.shapes.add_picture(
483
- image_file=ims.get_image_from_url(photo_url),
484
- left=0,
485
- top=0,
486
- width=pptx.util.Inches(slide_width_inch),
487
- )
488
-
489
- _add_text_at_bottom(
490
- slide=slide,
491
- slide_width_inch=slide_width_inch,
492
- slide_height_inch=slide_height_inch,
493
- text='Photo provided by Pexels',
494
- hyperlink=page_url
495
- )
496
-
497
- # Move picture to background
498
- # https://github.com/scanny/python-pptx/issues/49#issuecomment-137172836
499
- slide.shapes._spTree.remove(picture._element)
500
- slide.shapes._spTree.insert(2, picture._element)
501
- except Exception as ex:
502
- logger.error(
503
- '*** Error occurred while running adding image to the slide background: %s',
504
- str(ex)
505
- )
506
-
507
- return True
508
-
509
-
510
- def _handle_icons_ideas(
511
- presentation: pptx.Presentation(),
512
- slide_json: dict,
513
- slide_width_inch: float,
514
- slide_height_inch: float
515
- ):
516
- """
517
- Add a slide with some icons and text.
518
- If no suitable icons are found, the step numbers are shown.
519
-
520
- :param presentation: The presentation object.
521
- :param slide_json: The content of the slide as JSON data.
522
- :param slide_width_inch: The width of the slide in inches.
523
- :param slide_height_inch: The height of the slide in inches.
524
- :return: True if the slide has been processed.
525
- """
526
-
527
- if 'bullet_points' in slide_json and slide_json['bullet_points']:
528
- items = slide_json['bullet_points']
529
-
530
- # Ensure that it is a single list of strings without any sub-list
531
- for step in items:
532
- if not isinstance(step, str) or not step.startswith(ICON_BEGINNING_MARKER):
533
- return False
534
-
535
- slide_layout = presentation.slide_layouts[5]
536
- slide = presentation.slides.add_slide(slide_layout)
537
- slide.shapes.title.text = remove_slide_number_from_heading(slide_json['heading'])
538
-
539
- n_items = len(items)
540
- text_box_size = INCHES_2
541
-
542
- # Calculate the total width of all pictures and the spacing
543
- total_width = n_items * ICON_SIZE
544
- spacing = (pptx.util.Inches(slide_width_inch) - total_width) / (n_items + 1)
545
- top = INCHES_3
546
-
547
- icons_texts = [
548
- (match.group(1), match.group(2)) for match in [
549
- ICONS_REGEX.search(item) for item in items
550
- ]
551
- ]
552
- fallback_icon_files = ice.find_icons([item[0] for item in icons_texts])
553
-
554
- for idx, item in enumerate(icons_texts):
555
- icon, accompanying_text = item
556
- icon_path = f'{GlobalConfig.ICONS_DIR}/{icon}.png'
557
-
558
- if not os.path.exists(icon_path):
559
- logger.warning(
560
- 'Icon not found: %s...using fallback icon: %s',
561
- icon, fallback_icon_files[idx]
562
- )
563
- icon_path = f'{GlobalConfig.ICONS_DIR}/{fallback_icon_files[idx]}.png'
564
-
565
- left = spacing + idx * (ICON_SIZE + spacing)
566
- # Calculate the center position for alignment
567
- center = left + ICON_SIZE / 2
568
-
569
- # Add a rectangle shape with a fill color (background)
570
- # The size of the shape is slightly bigger than the icon, so align the icon position
571
- shape = slide.shapes.add_shape(
572
- MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
573
- center - INCHES_0_5,
574
- top - (ICON_BG_SIZE - ICON_SIZE) / 2,
575
- INCHES_1, INCHES_1
576
- )
577
- shape.fill.solid()
578
- shape.shadow.inherit = False
579
-
580
- # Set the icon's background shape color
581
- shape.fill.fore_color.rgb = shape.line.color.rgb = random.choice(ICON_COLORS)
582
- # Add the icon image on top of the colored shape
583
- slide.shapes.add_picture(icon_path, left, top, height=ICON_SIZE)
584
-
585
- # Add a text box below the shape
586
- text_box = slide.shapes.add_shape(
587
- MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
588
- left=center - text_box_size / 2, # Center the text box horizontally
589
- top=top + ICON_SIZE + INCHES_0_2,
590
- width=text_box_size,
591
- height=text_box_size
592
- )
593
- text_frame = text_box.text_frame
594
- text_frame.word_wrap = True
595
- text_frame.paragraphs[0].alignment = pptx.enum.text.PP_ALIGN.CENTER
596
- format_text(text_frame.paragraphs[0], accompanying_text)
597
-
598
- # Center the text vertically
599
- text_frame.vertical_anchor = pptx.enum.text.MSO_ANCHOR.MIDDLE
600
- text_box.fill.background() # No fill
601
- text_box.line.fill.background() # No line
602
- text_box.shadow.inherit = False
603
-
604
- # Set the font color based on the theme
605
- for paragraph in text_frame.paragraphs:
606
- for run in paragraph.runs:
607
- run.font.color.theme_color = pptx.enum.dml.MSO_THEME_COLOR.TEXT_2
608
-
609
- _add_text_at_bottom(
610
- slide=slide,
611
- slide_width_inch=slide_width_inch,
612
- slide_height_inch=slide_height_inch,
613
- text='More icons available in the SlideDeck AI repository',
614
- hyperlink='https://github.com/barun-saha/slide-deck-ai/tree/main/icons/png128'
615
- )
616
-
617
- return True
618
-
619
- return False
620
-
621
-
622
- def _add_text_at_bottom(
623
- slide: pptx.slide.Slide,
624
- slide_width_inch: float,
625
- slide_height_inch: float,
626
- text: str,
627
- hyperlink: Optional[str] = None,
628
- target_height: Optional[float] = 0.5
629
- ):
630
- """
631
- Add arbitrary text to a textbox positioned near the lower left side of a slide.
632
-
633
- :param slide: The slide.
634
- :param slide_width_inch: The width of the slide.
635
- :param slide_height_inch: The height of the slide.
636
- :param target_height: the target height of the box in inches (optional).
637
- :param text: The text to be added
638
- :param hyperlink: The hyperlink to be added to the text (optional).
639
- """
640
-
641
- footer = slide.shapes.add_textbox(
642
- left=INCHES_1,
643
- top=pptx.util.Inches(slide_height_inch - target_height),
644
- width=pptx.util.Inches(slide_width_inch),
645
- height=pptx.util.Inches(target_height)
646
- )
647
-
648
- paragraph = footer.text_frame.paragraphs[0]
649
- run = paragraph.add_run()
650
- run.text = text
651
- run.font.size = pptx.util.Pt(10)
652
- run.font.underline = False
653
-
654
- if hyperlink:
655
- run.hyperlink.address = hyperlink
656
-
657
-
658
- def _handle_double_col_layout(
659
- presentation: pptx.Presentation(),
660
- slide_json: dict,
661
- slide_width_inch: float,
662
- slide_height_inch: float
663
- ) -> bool:
664
- """
665
- Add a slide with a double column layout for comparison.
666
-
667
- :param presentation: The presentation object.
668
- :param slide_json: The content of the slide as JSON data.
669
- :param slide_width_inch: The width of the slide in inches.
670
- :param slide_height_inch: The height of the slide in inches.
671
- :return: True if double col layout has been added; False otherwise.
672
- """
673
-
674
- if 'bullet_points' in slide_json and slide_json['bullet_points']:
675
- double_col_content = slide_json['bullet_points']
676
-
677
- if double_col_content and (
678
- len(double_col_content) == 2
679
- ) and isinstance(double_col_content[0], dict) and isinstance(double_col_content[1], dict):
680
- slide = presentation.slide_layouts[4]
681
- slide = presentation.slides.add_slide(slide)
682
- placeholders = None
683
-
684
- shapes = slide.shapes
685
- title_placeholder = shapes.title
686
- title_placeholder.text = remove_slide_number_from_heading(slide_json['heading'])
687
-
688
- try:
689
- left_heading, right_heading = shapes.placeholders[1], shapes.placeholders[3]
690
- except KeyError:
691
- # For manually edited/added master slides, the placeholder idx numbers in the dict
692
- # will be different (>= 10)
693
- left_heading, right_heading = None, None
694
- placeholders = get_slide_placeholders(slide, layout_number=4)
695
-
696
- for idx, name in placeholders:
697
- if 'text placeholder' in name:
698
- if not left_heading:
699
- left_heading = shapes.placeholders[idx]
700
- elif not right_heading:
701
- right_heading = shapes.placeholders[idx]
702
-
703
- try:
704
- left_col, right_col = shapes.placeholders[2], shapes.placeholders[4]
705
- except KeyError:
706
- left_col, right_col = None, None
707
- if not placeholders:
708
- placeholders = get_slide_placeholders(slide, layout_number=4)
709
-
710
- for idx, name in placeholders:
711
- if 'content placeholder' in name:
712
- if not left_col:
713
- left_col = shapes.placeholders[idx]
714
- elif not right_col:
715
- right_col = shapes.placeholders[idx]
716
-
717
- left_col_frame, right_col_frame = left_col.text_frame, right_col.text_frame
718
-
719
- if 'heading' in double_col_content[0] and left_heading:
720
- left_heading.text = double_col_content[0]['heading']
721
- if 'bullet_points' in double_col_content[0]:
722
- flat_items_list = get_flat_list_of_contents(
723
- double_col_content[0]['bullet_points'], level=0
724
- )
725
-
726
- if not left_heading:
727
- left_col_frame.text = double_col_content[0]['heading']
728
-
729
- add_bulleted_items(left_col_frame, flat_items_list)
730
-
731
- if 'heading' in double_col_content[1] and right_heading:
732
- right_heading.text = double_col_content[1]['heading']
733
- if 'bullet_points' in double_col_content[1]:
734
- flat_items_list = get_flat_list_of_contents(
735
- double_col_content[1]['bullet_points'], level=0
736
- )
737
-
738
- if not right_heading:
739
- right_col_frame.text = double_col_content[1]['heading']
740
-
741
- add_bulleted_items(right_col_frame, flat_items_list)
742
-
743
- _handle_key_message(
744
- the_slide=slide,
745
- slide_json=slide_json,
746
- slide_height_inch=slide_height_inch,
747
- slide_width_inch=slide_width_inch
748
- )
749
-
750
- return True
751
-
752
- return False
753
-
754
-
755
- def _handle_step_by_step_process(
756
- presentation: pptx.Presentation,
757
- slide_json: dict,
758
- slide_width_inch: float,
759
- slide_height_inch: float
760
- ) -> bool:
761
- """
762
- Add shapes to display a step-by-step process in the slide, if available.
763
-
764
- :param presentation: The presentation object.
765
- :param slide_json: The content of the slide as JSON data.
766
- :param slide_width_inch: The width of the slide in inches.
767
- :param slide_height_inch: The height of the slide in inches.
768
- :return True if this slide has a step-by-step process depiction added; False otherwise.
769
- """
770
-
771
- if 'bullet_points' in slide_json and slide_json['bullet_points']:
772
- steps = slide_json['bullet_points']
773
-
774
- no_marker_count = 0.0
775
- n_steps = len(steps)
776
-
777
- # Ensure that it is a single list of strings without any sub-list
778
- for step in steps:
779
- if not isinstance(step, str):
780
- return False
781
-
782
- # In some cases, one or two steps may not begin with >>, e.g.:
783
- # {
784
- # "heading": "Step-by-Step Process: Creating a Legacy",
785
- # "bullet_points": [
786
- # "Identify your unique talents and passions",
787
- # ">> Develop your skills and knowledge",
788
- # ">> Create meaningful work",
789
- # ">> Share your work with the world",
790
- # ">> Continuously learn and adapt"
791
- # ],
792
- # "key_message": ""
793
- # },
794
- #
795
- # Use a threshold, e.g., at most 20%
796
- if not step.startswith(STEP_BY_STEP_PROCESS_MARKER):
797
- no_marker_count += 1
798
-
799
- slide_header = slide_json['heading'].lower()
800
- if (no_marker_count / n_steps > 0.25) and not (
801
- ('step-by-step' in slide_header) or ('step by step' in slide_header)
802
- ):
803
- return False
804
-
805
- if n_steps < 3 or n_steps > 6:
806
- # Two steps -- probably not a process
807
- # More than 5--6 steps -- would likely cause a visual clutter
808
- return False
809
-
810
- bullet_slide_layout = presentation.slide_layouts[1]
811
- slide = presentation.slides.add_slide(bullet_slide_layout)
812
- shapes = slide.shapes
813
- shapes.title.text = remove_slide_number_from_heading(slide_json['heading'])
814
-
815
- if 3 <= n_steps <= 4:
816
- # Horizontal display
817
- height = INCHES_1_5
818
- width = pptx.util.Inches(slide_width_inch / n_steps - 0.01)
819
- top = pptx.util.Inches(slide_height_inch / 2)
820
- left = pptx.util.Inches((slide_width_inch - width.inches * n_steps) / 2 + 0.05)
821
-
822
- for step in steps:
823
- shape = shapes.add_shape(MSO_AUTO_SHAPE_TYPE.CHEVRON, left, top, width, height)
824
- text_frame = shape.text_frame
825
- text_frame.clear()
826
- paragraph = text_frame.paragraphs[0]
827
- paragraph.alignment = pptx.enum.text.PP_ALIGN.LEFT
828
- format_text(paragraph, step.removeprefix(STEP_BY_STEP_PROCESS_MARKER))
829
- left += width - INCHES_0_4
830
- elif 4 < n_steps <= 6:
831
- # Vertical display
832
- height = pptx.util.Inches(0.65)
833
- top = pptx.util.Inches(slide_height_inch / 4)
834
- left = INCHES_1 # slide_width_inch - width.inches)
835
-
836
- # Find the close to median width, based on the length of each text, to be set
837
- # for the shapes
838
- width = pptx.util.Inches(slide_width_inch * 2 / 3)
839
- lengths = [len(step) for step in steps]
840
- font_size_20pt = pptx.util.Pt(20)
841
- widths = sorted(
842
- [
843
- min(
844
- pptx.util.Inches(font_size_20pt.inches * a_len),
845
- width
846
- ) for a_len in lengths
847
- ]
848
- )
849
- width = widths[len(widths) // 2]
850
-
851
- for step in steps:
852
- shape = shapes.add_shape(MSO_AUTO_SHAPE_TYPE.PENTAGON, left, top, width, height)
853
- text_frame = shape.text_frame
854
- text_frame.clear()
855
- paragraph = text_frame.paragraphs[0]
856
- paragraph.alignment = pptx.enum.text.PP_ALIGN.LEFT
857
- format_text(paragraph, step.removeprefix(STEP_BY_STEP_PROCESS_MARKER))
858
- top += height + INCHES_0_3
859
- left += INCHES_0_5
860
-
861
- return True
862
-
863
-
864
- def _handle_table(
865
- presentation: pptx.Presentation,
866
- slide_json: dict,
867
- slide_width_inch: float,
868
- slide_height_inch: float
869
- ) -> bool:
870
- """
871
- Add a table to a slide, if available.
872
-
873
- :param presentation: The presentation object.
874
- :param slide_json: The content of the slide as JSON data.
875
- :param slide_width_inch: The width of the slide in inches.
876
- :param slide_height_inch: The height of the slide in inches.
877
- :return True if this slide has a step-by-step process depiction added; False otherwise.
878
- """
879
-
880
- if 'table' not in slide_json or not slide_json['table']:
881
- return False
882
-
883
- headers = slide_json['table'].get('headers', [])
884
- rows = slide_json['table'].get('rows', [])
885
- bullet_slide_layout = presentation.slide_layouts[1]
886
- slide = presentation.slides.add_slide(bullet_slide_layout)
887
- shapes = slide.shapes
888
- shapes.title.text = remove_slide_number_from_heading(slide_json['heading'])
889
- left = slide.placeholders[1].left
890
- top = slide.placeholders[1].top
891
- width = slide.placeholders[1].width
892
- height = slide.placeholders[1].height
893
- table = slide.shapes.add_table(len(rows) + 1, len(headers), left, top, width, height).table
894
-
895
- # Set headers
896
- for col_idx, header_text in enumerate(headers):
897
- table.cell(0, col_idx).text = header_text
898
- table.cell(0, col_idx).text_frame.paragraphs[
899
- 0].font.bold = True # Make header bold
900
-
901
- # Fill in rows
902
- for row_idx, row_data in enumerate(rows, start=1):
903
- for col_idx, cell_text in enumerate(row_data):
904
- table.cell(row_idx, col_idx).text = cell_text
905
-
906
- return True
907
-
908
-
909
- def _handle_key_message(
910
- the_slide: pptx.slide.Slide,
911
- slide_json: dict,
912
- slide_width_inch: float,
913
- slide_height_inch: float
914
- ):
915
- """
916
- Add a shape to display the key message in the slide, if available.
917
-
918
- :param the_slide: The slide to be processed.
919
- :param slide_json: The content of the slide as JSON data.
920
- :param slide_width_inch: The width of the slide in inches.
921
- :param slide_height_inch: The height of the slide in inches.
922
- """
923
-
924
- if 'key_message' in slide_json and slide_json['key_message']:
925
- height = pptx.util.Inches(1.6)
926
- width = pptx.util.Inches(slide_width_inch / 2.3)
927
- top = pptx.util.Inches(slide_height_inch - height.inches - 0.1)
928
- left = pptx.util.Inches((slide_width_inch - width.inches) / 2)
929
- shape = the_slide.shapes.add_shape(
930
- MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
931
- left=left,
932
- top=top,
933
- width=width,
934
- height=height
935
- )
936
- format_text(shape.text_frame.paragraphs[0], slide_json['key_message'])
937
-
938
-
939
- def _get_slide_width_height_inches(presentation: pptx.Presentation) -> Tuple[float, float]:
940
- """
941
- Get the dimensions of a slide in inches.
942
-
943
- :param presentation: The presentation object.
944
- :return: The width and the height.
945
- """
946
-
947
- slide_width_inch = EMU_TO_INCH_SCALING_FACTOR * presentation.slide_width
948
- slide_height_inch = EMU_TO_INCH_SCALING_FACTOR * presentation.slide_height
949
-
950
- return slide_width_inch, slide_height_inch
951
-
952
-
953
- if __name__ == '__main__':
954
- _JSON_DATA = '''
955
- {
956
- "title": "AI Applications: Transforming Industries",
957
- "slides": [
958
- {
959
- "heading": "Introduction to AI Applications",
960
- "bullet_points": [
961
- "Artificial Intelligence (AI) is *transforming* various industries",
962
- "AI applications range from simple decision-making tools to complex systems",
963
- "AI can be categorized into types: Rule-based, Instance-based, and Model-based"
964
- ],
965
- "key_message": "AI is a broad field with diverse applications and categories",
966
- "img_keywords": "AI, transformation, industries, decision-making, categories"
967
- },
968
- {
969
- "heading": "AI in Everyday Life",
970
- "bullet_points": [
971
- "**Virtual assistants** like Siri, Alexa, and Google Assistant",
972
- "**Recommender systems** in Netflix, Amazon, and Spotify",
973
- "**Fraud detection** in banking and *credit card* transactions"
974
- ],
975
- "key_message": "AI is integrated into our daily lives through various services",
976
- "img_keywords": "virtual assistants, recommender systems, fraud detection"
977
- },
978
- {
979
- "heading": "AI in Healthcare",
980
- "bullet_points": [
981
- "Disease diagnosis and prediction using machine learning algorithms",
982
- "Personalized medicine and drug discovery",
983
- "AI-powered robotic surgeries and remote patient monitoring"
984
- ],
985
- "key_message": "AI is revolutionizing healthcare with improved diagnostics and patient care",
986
- "img_keywords": "healthcare, disease diagnosis, personalized medicine, robotic surgeries"
987
- },
988
- {
989
- "heading": "AI in Key Industries",
990
- "bullet_points": [
991
- {
992
- "heading": "Retail",
993
- "bullet_points": [
994
- "Inventory management and demand forecasting",
995
- "Customer segmentation and targeted marketing",
996
- "AI-driven chatbots for customer service"
997
- ]
998
- },
999
- {
1000
- "heading": "Finance",
1001
- "bullet_points": [
1002
- "Credit scoring and risk assessment",
1003
- "Algorithmic trading and portfolio management",
1004
- "AI for detecting money laundering and cyber fraud"
1005
- ]
1006
- }
1007
- ],
1008
- "key_message": "AI is transforming retail and finance with improved operations and decision-making",
1009
- "img_keywords": "retail, finance, inventory management, credit scoring, algorithmic trading"
1010
- },
1011
- {
1012
- "heading": "AI in Education",
1013
- "bullet_points": [
1014
- "Personalized learning paths and adaptive testing",
1015
- "Intelligent tutoring systems for skill development",
1016
- "AI for predicting student performance and dropout rates"
1017
- ],
1018
- "key_message": "AI is personalizing education and improving student outcomes",
1019
- },
1020
- {
1021
- "heading": "Step-by-Step: AI Development Process",
1022
- "bullet_points": [
1023
- ">> **Step 1:** Define the problem and objectives",
1024
- ">> **Step 2:** Collect and preprocess data",
1025
- ">> **Step 3:** Select and train the AI model",
1026
- ">> **Step 4:** Evaluate and optimize the model",
1027
- ">> **Step 5:** Deploy and monitor the AI system"
1028
- ],
1029
- "key_message": "Developing AI involves a structured process from problem definition to deployment",
1030
- "img_keywords": ""
1031
- },
1032
- {
1033
- "heading": "AI Icons: Key Aspects",
1034
- "bullet_points": [
1035
- "[[brain]] Human-like *intelligence* and decision-making",
1036
- "[[robot]] Automation and physical *tasks*",
1037
- "[[]] Data processing and cloud computing",
1038
- "[[lightbulb]] Insights and *predictions*",
1039
- "[[globe2]] Global connectivity and *impact*"
1040
- ],
1041
- "key_message": "AI encompasses various aspects, from human-like intelligence to global impact",
1042
- "img_keywords": "AI aspects, intelligence, automation, data processing, global impact"
1043
- },
1044
- {
1045
- "heading": "AI vs. ML vs. DL: A Tabular Comparison",
1046
- "table": {
1047
- "headers": ["Feature", "AI", "ML", "DL"],
1048
- "rows": [
1049
- ["Definition", "Creating intelligent agents", "Learning from data", "Deep neural networks"],
1050
- ["Approach", "Rule-based, expert systems", "Algorithms, statistical models", "Deep neural networks"],
1051
- ["Data Requirements", "Varies", "Large datasets", "Massive datasets"],
1052
- ["Complexity", "Varies", "Moderate", "High"],
1053
- ["Computational Cost", "Low to Moderate", "Moderate", "High"],
1054
- ["Examples", "Chess, recommendation systems", "Spam filters, image recognition", "Image recognition, NLP"]
1055
- ]
1056
- },
1057
- "key_message": "This table provides a concise comparison of the key features of AI, ML, and DL.",
1058
- "img_keywords": "AI, ML, DL, comparison, table, features"
1059
- },
1060
- {
1061
- "heading": "Conclusion: Embracing AI's Potential",
1062
- "bullet_points": [
1063
- "AI is transforming industries and improving lives",
1064
- "Ethical considerations are crucial for responsible AI development",
1065
- "Invest in AI education and workforce development",
1066
- "Call to action: Explore AI applications and contribute to shaping its future"
1067
- ],
1068
- "key_message": "AI offers *immense potential*, and we must embrace it responsibly",
1069
- "img_keywords": "AI transformation, ethical considerations, AI education, future of AI"
1070
- }
1071
- ]
1072
- }'''
1073
-
1074
- temp = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
1075
- path = pathlib.Path(temp.name)
1076
-
1077
- generate_powerpoint_presentation(
1078
- json5.loads(_JSON_DATA),
1079
- output_file_path=path,
1080
- slides_template='Basic'
1081
- )
1082
- print(f'File path: {path}')
1083
-
1084
- temp.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helpers/text_helper.py DELETED
@@ -1,83 +0,0 @@
1
- """
2
- Utility functions to help with text processing.
3
- """
4
- import json_repair as jr
5
-
6
-
7
- def is_valid_prompt(prompt: str) -> bool:
8
- """
9
- Verify whether user input satisfies the concerned constraints.
10
-
11
- :param prompt: The user input text.
12
- :return: True if all criteria are satisfied; False otherwise.
13
- """
14
-
15
- if len(prompt) < 7 or ' ' not in prompt:
16
- return False
17
-
18
- return True
19
-
20
-
21
- def get_clean_json(json_str: str) -> str:
22
- """
23
- Attempt to clean a JSON response string from the LLM by removing ```json at the beginning and
24
- trailing ``` and any text beyond that.
25
- CAUTION: May not be always accurate.
26
-
27
- :param json_str: The input string in JSON format.
28
- :return: The "cleaned" JSON string.
29
- """
30
-
31
- response_cleaned = json_str
32
-
33
- if json_str.startswith('```json'):
34
- json_str = json_str[7:]
35
-
36
- while True:
37
- idx = json_str.rfind('```') # -1 on failure
38
-
39
- if idx <= 0:
40
- break
41
-
42
- # In the ideal scenario, the character before the last ``` should be
43
- # a new line or a closing bracket
44
- prev_char = json_str[idx - 1]
45
-
46
- if (prev_char == '}') or (prev_char == '\n' and json_str[idx - 2] == '}'):
47
- response_cleaned = json_str[:idx]
48
-
49
- json_str = json_str[:idx]
50
-
51
- return response_cleaned
52
-
53
-
54
- def fix_malformed_json(json_str: str) -> str:
55
- """
56
- Try and fix the syntax error(s) in a JSON string.
57
-
58
- :param json_str: The input JSON string.
59
- :return: The fixed JSOn string.
60
- """
61
-
62
- return jr.repair_json(json_str, skip_json_loads=True)
63
-
64
-
65
- if __name__ == '__main__':
66
- JSON1 = '''{
67
- "key": "value"
68
- }
69
- '''
70
- JSON2 = '''["Reason": "Regular updates help protect against known vulnerabilities."]'''
71
- JSON3 = '''["Reason" Regular updates help protect against known vulnerabilities."]'''
72
- JSON4 = '''
73
- {"bullet_points": [
74
- ">> Write without stopping or editing",
75
- >> Set daily writing goals and stick to them,
76
- ">> Allow yourself to make mistakes"
77
- ],}
78
- '''
79
-
80
- print(fix_malformed_json(JSON1))
81
- print(fix_malformed_json(JSON2))
82
- print(fix_malformed_json(JSON3))
83
- print(fix_malformed_json(JSON4))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
icons/png128/0-circle.png DELETED
Binary file (4.1 kB)
 
icons/png128/1-circle.png DELETED
Binary file (3.45 kB)
 
icons/png128/123.png DELETED
Binary file (2.5 kB)
 
icons/png128/2-circle.png DELETED
Binary file (4.01 kB)
 
icons/png128/3-circle.png DELETED
Binary file (4.24 kB)
 
icons/png128/4-circle.png DELETED
Binary file (3.74 kB)
 
icons/png128/5-circle.png DELETED
Binary file (4.12 kB)
 
icons/png128/6-circle.png DELETED
Binary file (4.37 kB)
 
icons/png128/7-circle.png DELETED
Binary file (3.78 kB)
 
icons/png128/8-circle.png DELETED
Binary file (4.43 kB)
 
icons/png128/9-circle.png DELETED
Binary file (4.44 kB)
 
icons/png128/activity.png DELETED
Binary file (1.38 kB)
 
icons/png128/airplane.png DELETED
Binary file (2.09 kB)
 
icons/png128/alarm.png DELETED
Binary file (4.08 kB)
 
icons/png128/alien-head.png DELETED
Binary file (4.73 kB)
 
icons/png128/alphabet.png DELETED
Binary file (2.44 kB)
 
icons/png128/amazon.png DELETED
Binary file (3.56 kB)
 
icons/png128/amritsar-golden-temple.png DELETED
Binary file (4.44 kB)
 
icons/png128/amsterdam-canal.png DELETED
Binary file (3.32 kB)
 
icons/png128/amsterdam-windmill.png DELETED
Binary file (2.67 kB)
 
icons/png128/android.png DELETED
Binary file (2.24 kB)
 
icons/png128/angkor-wat.png DELETED
Binary file (2.64 kB)
 
icons/png128/apple.png DELETED
Binary file (2.4 kB)
 
icons/png128/archive.png DELETED
Binary file (1.27 kB)
 
icons/png128/argentina-obelisk.png DELETED
Binary file (1.39 kB)