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 -10
  2. .gitattributes +0 -2
  3. .github/workflows/codeql.yml +0 -98
  4. .gitignore +0 -1
  5. .idea/.gitignore +3 -0
  6. .idea/inspectionProfiles/Project_Default.xml +14 -0
  7. .idea/inspectionProfiles/profiles_settings.xml +6 -0
  8. .idea/misc.xml +7 -0
  9. .idea/modules.xml +8 -0
  10. .idea/slide-deck-ai.iml +10 -0
  11. .idea/vcs.xml +6 -0
  12. .streamlit/config.toml +0 -10
  13. README.md +14 -113
  14. app.py +213 -560
  15. clarifai_grpc_helper.py +71 -0
  16. examples/example_04.json +0 -3
  17. file_embeddings/embeddings.npy +0 -3
  18. file_embeddings/icons.npy +0 -3
  19. global_config.py +14 -193
  20. helpers/__init__.py +0 -0
  21. helpers/file_manager.py +0 -73
  22. helpers/icons_embeddings.py +0 -166
  23. helpers/image_search.py +0 -148
  24. helpers/llm_helper.py +0 -259
  25. helpers/pptx_helper.py +0 -1129
  26. helpers/text_helper.py +0 -83
  27. icons/png128/0-circle.png +0 -0
  28. icons/png128/1-circle.png +0 -0
  29. icons/png128/123.png +0 -0
  30. icons/png128/2-circle.png +0 -0
  31. icons/png128/3-circle.png +0 -0
  32. icons/png128/4-circle.png +0 -0
  33. icons/png128/5-circle.png +0 -0
  34. icons/png128/6-circle.png +0 -0
  35. icons/png128/7-circle.png +0 -0
  36. icons/png128/8-circle.png +0 -0
  37. icons/png128/9-circle.png +0 -0
  38. icons/png128/activity.png +0 -0
  39. icons/png128/airplane.png +0 -0
  40. icons/png128/alarm.png +0 -0
  41. icons/png128/alien-head.png +0 -0
  42. icons/png128/alphabet.png +0 -0
  43. icons/png128/amazon.png +0 -0
  44. icons/png128/amritsar-golden-temple.png +0 -0
  45. icons/png128/amsterdam-canal.png +0 -0
  46. icons/png128/amsterdam-windmill.png +0 -0
  47. icons/png128/android.png +0 -0
  48. icons/png128/angkor-wat.png +0 -0
  49. icons/png128/apple.png +0 -0
  50. icons/png128/archive.png +0 -0
.env.example DELETED
@@ -1,10 +0,0 @@
1
- # Example .env file for SlideDeck AI
2
- # Add your API keys and configuration values here
3
-
4
- PEXEL_API_KEY=your-pexel-key-for-images
5
-
6
- TOGETHER_API_KEY=your-together-ai-key
7
- OPENROUTER_API_KEY=your-openrouter-api-key
8
-
9
- RUN_IN_OFFLINE_MODE=true-or-false
10
- DEFAULT_MODEL_INDEX=3
 
 
 
 
 
 
 
 
 
 
 
.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
 
 
.github/workflows/codeql.yml DELETED
@@ -1,98 +0,0 @@
1
- # For most projects, this workflow file will not need changing; you simply need
2
- # to commit it to your repository.
3
- #
4
- # You may wish to alter this file to override the set of languages analyzed,
5
- # or to provide custom queries or build logic.
6
- #
7
- # ******** NOTE ********
8
- # We have attempted to detect the languages in your repository. Please check
9
- # the `language` matrix defined below to confirm you have the correct set of
10
- # supported CodeQL languages.
11
- #
12
- name: "CodeQL Advanced"
13
-
14
- on:
15
- push:
16
- branches: [ "main" ]
17
- pull_request:
18
- branches: [ "main" ]
19
- schedule:
20
- - cron: '35 12 * * 6'
21
-
22
- jobs:
23
- analyze:
24
- name: Analyze (${{ matrix.language }})
25
- # Runner size impacts CodeQL analysis time. To learn more, please see:
26
- # - https://gh.io/recommended-hardware-resources-for-running-codeql
27
- # - https://gh.io/supported-runners-and-hardware-resources
28
- # - https://gh.io/using-larger-runners (GitHub.com only)
29
- # Consider using larger runners or machines with greater resources for possible analysis time improvements.
30
- runs-on: ubuntu-latest
31
- permissions:
32
- # required for all workflows
33
- security-events: write
34
-
35
- # required to fetch internal or private CodeQL packs
36
- packages: read
37
-
38
- # only required for workflows in private repositories
39
- actions: read
40
- contents: read
41
-
42
- strategy:
43
- fail-fast: false
44
- matrix:
45
- include:
46
- - language: python
47
- build-mode: none
48
- # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
49
- # Use `c-cpp` to analyze code written in C, C++ or both
50
- # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
51
- # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
52
- # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
53
- # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
54
- # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
55
- # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
56
- steps:
57
- - name: Checkout repository
58
- uses: actions/checkout@v4
59
-
60
- # Add any setup steps before running the `github/codeql-action/init` action.
61
- # This includes steps like installing compilers or runtimes (`actions/setup-node`
62
- # or others). This is typically only required for manual builds.
63
- # - name: Setup runtime (example)
64
- # uses: actions/setup-example@v1
65
-
66
- # Initializes the CodeQL tools for scanning.
67
- - name: Initialize CodeQL
68
- uses: github/codeql-action/init@v3
69
- with:
70
- languages: ${{ matrix.language }}
71
- build-mode: ${{ matrix.build-mode }}
72
- # If you wish to specify custom queries, you can do so here or in a config file.
73
- # By default, queries listed here will override any specified in a config file.
74
- # Prefix the list here with "+" to use these queries and those in the config file.
75
-
76
- # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
77
- # queries: security-extended,security-and-quality
78
-
79
- # If the analyze step fails for one of the languages you are analyzing with
80
- # "We were unable to automatically build your code", modify the matrix above
81
- # to set the build mode to "manual" for that language. Then modify this step
82
- # to build your code.
83
- # ℹ️ Command-line programs to run using the OS shell.
84
- # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
85
- - if: matrix.build-mode == 'manual'
86
- shell: bash
87
- run: |
88
- echo 'If you are using a "manual" build mode for one or more of the' \
89
- 'languages you are analyzing, replace this with the commands to build' \
90
- 'your code, for example:'
91
- echo ' make bootstrap'
92
- echo ' make release'
93
- exit 1
94
-
95
- - name: Perform CodeQL Analysis
96
- uses: github/codeql-action/analyze@v3
97
- with:
98
- category: "/language:${{matrix.language}}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore CHANGED
@@ -144,4 +144,3 @@ dmypy.json
144
  # Cython debug symbols
145
  cython_debug/
146
 
147
- .idea.DS_Store
 
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,135 +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 several online providers—Azure OpenAI, 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
- | Gemini 2.0 Flash | Google Gemini API (`gg`) | Mandatory; [get here](https://aistudio.google.com/apikey) | Faster, longer content |
54
- | Gemini 2.0 Flash Lite | Google Gemini API (`gg`) | Mandatory; [get here](https://aistudio.google.com/apikey) | Fastest, longer content |
55
- | Gemini 2.5 Flash | Google Gemini API (`gg`) | Mandatory; [get here](https://aistudio.google.com/apikey) | Faster, longer content |
56
- | Gemini 2.5 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
- | DeepSeek V3-0324 | Together AI (`to`) | Mandatory; [get here](https://api.together.ai/settings/api-keys) | Slower, medium-length |
62
- | Llama 3.3 70B Instruct Turbo | Together AI (`to`) | Mandatory; [get here](https://api.together.ai/settings/api-keys) | Slower, detailed |
63
- | Llama 3.1 8B Instruct Turbo 128K | Together AI (`to`) | Mandatory; [get here](https://api.together.ai/settings/api-keys) | Faster, shorter |
64
 
65
- **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
66
- Open-Source project, so feel free to audit the code and convince yourself.
67
-
68
- In addition, offline LLMs provided by Ollama can be used. Read below to know more.
69
-
70
-
71
- # Icons
72
-
73
- SlideDeck AI uses a subset of icons from [bootstrap-icons-1.11.3](https://github.com/twbs/icons)
74
- (MIT license) in the slides. A few icons from [SVG Repo](https://www.svgrepo.com/)
75
- (CC0, MIT, and Apache licenses) are also used.
76
 
77
 
78
  # Local Development
79
 
80
- SlideDeck AI uses LLMs via different providers. To run this project by yourself, you need to use an appropriate API key, for example, in a `.env` file.
81
- Alternatively, you can provide the access token in the app's user interface itself (UI).
82
-
83
- ## Offline LLMs Using Ollama
84
-
85
- 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.
86
-
87
- 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. You should choose a model to use based on your hardware capacity. However, if you have no GPU, [gemma3:1b](https://ollama.com/library/gemma3:1b) can be a suitable model to run only on CPU.
88
-
89
- 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:
90
-
91
- ```bash
92
- # Environment initialization, especially on Debian
93
- sudo apt update -y
94
- sudo apt install python-is-python3 -y
95
- sudo apt install git -y
96
- # Change the package name based on the Python version installed: python -V
97
- sudo apt install python3.11-venv -y
98
-
99
- # Install Git Large File Storage (LFS)
100
- sudo apt install git-lfs -y
101
- git lfs install
102
-
103
- ollama list # View locally available LLMs
104
- export RUN_IN_OFFLINE_MODE=True # Enable the offline mode to use Ollama
105
- git clone https://github.com/barun-saha/slide-deck-ai.git
106
- cd slide-deck-ai
107
- git lfs pull # Pull the PPTX template files
108
-
109
- python -m venv venv # Create a virtual environment
110
- source venv/bin/activate # On a Linux system
111
- pip install -r requirements.txt
112
-
113
- streamlit run ./app.py # Run the application
114
- ```
115
-
116
- 💡If you have cloned the repository locally but cannot open and view the PPTX templates, you may need to run `git lfs pull` to download the template files. Without this, although content generation will work, the slide deck cannot be created.
117
-
118
- The `.env` file should be created inside the `slide-deck-ai` directory.
119
-
120
- 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.
121
-
122
- 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`.
123
-
124
- 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.
125
 
126
 
127
  # Live Demo
128
 
129
- - [SlideDeck AI](https://huggingface.co/spaces/barunsaha/slide-deck-ai) on Hugging Face Spaces
130
- - [Demo video](https://youtu.be/QvAKzNKtk9k) of the chat interface on YouTube
131
- - Demo video on [using Azure OpenAI](https://youtu.be/oPbH-z3q0Mw)
132
 
133
 
134
  # Award
135
 
136
- 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.
137
-
138
-
139
- # Contributors
140
-
141
- SlideDeck AI is glad to have the following community contributions:
142
- - [Srinivasan Ragothaman](https://github.com/rsrini7): added OpenRouter support and API keys mapping from the `.env` file.
143
- - [Aditya](https://github.com/AdiBak): added support for page range selection for PDF files.
144
- - [Sagar Bharatbhai Bharadia](https://github.com/sagarbharadia17): added support for Gemini 2.5 Flash Lite and Gemini 2.5 Flash LLMs.
145
-
146
- Thank you all for your contributions!
147
-
148
- [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors)
149
-
150
-
 
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,657 +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
- 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 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 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
- 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
- def reset_chat_history():
138
- """
139
- Clear the chat history and related session state variables.
140
  """
141
- # Clear session state variables using pop with None default
142
- st.session_state.pop(CHAT_MESSAGES, None)
143
- st.session_state.pop(IS_IT_REFINEMENT, None)
144
- st.session_state.pop(ADDITIONAL_INFO, None)
145
- st.session_state.pop(PDF_FILE_KEY, None)
146
-
147
- # Remove previously generated temp PPTX file
148
- temp_pptx_path = st.session_state.pop(DOWNLOAD_FILE_KEY, None)
149
- if temp_pptx_path:
150
- pptx_path = pathlib.Path(temp_pptx_path)
151
- if pptx_path.exists() and pptx_path.is_file():
152
- pptx_path.unlink()
153
-
154
-
155
- APP_TEXT = _load_strings()
156
-
157
- # Session variables
158
- CHAT_MESSAGES = 'chat_messages'
159
- DOWNLOAD_FILE_KEY = 'download_file_name'
160
- IS_IT_REFINEMENT = 'is_it_refinement'
161
- ADDITIONAL_INFO = 'additional_info'
162
- PDF_FILE_KEY = 'pdf_file'
163
-
164
-
165
- logger = logging.getLogger(__name__)
166
-
167
- texts = list(GlobalConfig.PPTX_TEMPLATE_FILES.keys())
168
- captions = [GlobalConfig.PPTX_TEMPLATE_FILES[x]['caption'] for x in texts]
169
-
170
-
171
- with st.sidebar:
172
- # New Chat button at the top of sidebar
173
- col1, col2, col3 = st.columns([.17, 0.8, .1])
174
- with col2:
175
- if st.button('New Chat 💬', help='Start a new conversation', key='new_chat_button'):
176
- reset_chat_history() # Reset the chat history when the button is clicked
177
-
178
- # The PPT templates
179
- pptx_template = st.sidebar.radio(
180
- '1: Select a presentation template:',
181
- texts,
182
- captions=captions,
183
- horizontal=True
184
- )
185
-
186
- if RUN_IN_OFFLINE_MODE:
187
- llm_provider_to_use = st.text_input(
188
- label='2: Enter Ollama model name to use (e.g., gemma3:1b):',
189
- help=(
190
- 'Specify a correct, locally available LLM, found by running `ollama list`, for'
191
- ' example, `gemma3:1b`, `mistral:v0.2`, and `mistral-nemo:latest`. Having an'
192
- ' Ollama-compatible and supported GPU is strongly recommended.'
193
- )
194
- )
195
- api_key_token: str = ''
196
- azure_endpoint: str = ''
197
- azure_deployment: str = ''
198
- api_version: str = ''
199
- else:
200
- # The online LLMs
201
- llm_provider_to_use = st.sidebar.selectbox(
202
- label='2: Select a suitable LLM to use:\n\n(Gemini and Mistral-Nemo are recommended)',
203
- options=[f'{k} ({v["description"]})' for k, v in GlobalConfig.VALID_MODELS.items()],
204
- index=GlobalConfig.DEFAULT_MODEL_INDEX,
205
- help=GlobalConfig.LLM_PROVIDER_HELP,
206
- on_change=reset_api_key
207
- ).split(' ')[0]
208
-
209
- # --- Automatically fetch API key from .env if available ---
210
- provider_match = GlobalConfig.PROVIDER_REGEX.match(llm_provider_to_use)
211
- selected_provider = provider_match.group(1) if provider_match else llm_provider_to_use
212
- env_key_name = GlobalConfig.PROVIDER_ENV_KEYS.get(selected_provider)
213
- default_api_key = os.getenv(env_key_name, "") if env_key_name else ""
214
-
215
- # Always sync session state to env value if needed (autofill on provider change)
216
- if default_api_key and st.session_state.get('api_key_input', None) != default_api_key:
217
- st.session_state['api_key_input'] = default_api_key
218
-
219
- api_key_token = st.text_input(
220
- label=(
221
- '3: Paste your API key/access token:\n\n'
222
- '*Mandatory* for all providers.'
223
- ),
224
- key='api_key_input',
225
- type='password',
226
- disabled=bool(default_api_key),
227
- )
228
 
229
- # Additional configs for Azure OpenAI
230
- with st.expander('**Azure OpenAI-specific configurations**'):
231
- azure_endpoint = st.text_input(
232
- label=(
233
- '4: Azure endpoint URL, e.g., https://example.openai.azure.com/.\n\n'
234
- '*Mandatory* for Azure OpenAI (only).'
235
- )
236
- )
237
- azure_deployment = st.text_input(
238
- label=(
239
- '5: Deployment name on Azure OpenAI:\n\n'
240
- '*Mandatory* for Azure OpenAI (only).'
241
- ),
242
- )
243
- api_version = st.text_input(
244
- label=(
245
- '6: API version:\n\n'
246
- '*Mandatory* field. Change based on your deployment configurations.'
247
- ),
248
- value='2024-05-01-preview',
249
- )
250
-
251
- # Make slider with initial values
252
- page_range_slider = st.slider(
253
- 'Specify a page range for the uploaded PDF file (if any):',
254
- 1, GlobalConfig.MAX_ALLOWED_PAGES,
255
- [1, GlobalConfig.MAX_ALLOWED_PAGES]
256
  )
257
- st.session_state['page_range_slider'] = page_range_slider
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
 
260
  def build_ui():
261
  """
262
- Display the input elements for content generation.
263
  """
264
 
 
 
265
  st.title(APP_TEXT['app_name'])
266
  st.subheader(APP_TEXT['caption'])
267
  st.markdown(
268
- '![Visitors](https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fhuggingface.co%2Fspaces%2Fbarunsaha%2Fslide-deck-ai&countColor=%23263759)' # noqa: E501
 
 
 
 
 
269
  )
270
 
271
- today = datetime.date.today()
272
- if today.month == 1 and 1 <= today.day <= 15:
273
- st.success(
274
- (
275
- 'Wishing you a happy and successful New Year!'
276
- ' It is your appreciation that keeps SlideDeck AI going.'
277
- f' May you make some great slide decks in {today.year} ✨'
278
- ),
279
- icon='🎆'
 
 
280
  )
281
 
282
- with st.expander('Usage Policies and Limitations'):
283
- st.text(APP_TEXT['tos'] + '\n\n' + APP_TEXT['tos2'])
284
 
285
- set_up_chat_ui()
 
 
 
 
 
286
 
 
 
287
 
288
- def set_up_chat_ui():
289
- """
290
- Prepare the chat interface and related functionality.
291
- """
292
- # Set start and end page
293
- st.session_state['start_page'] = st.session_state['page_range_slider'][0]
294
- st.session_state['end_page'] = st.session_state['page_range_slider'][1]
295
 
296
- with st.expander('Usage Instructions'):
297
- st.markdown(GlobalConfig.CHAT_USAGE_INSTRUCTIONS)
 
 
298
 
299
- st.info(APP_TEXT['like_feedback'])
300
- st.chat_message('ai').write(random.choice(APP_TEXT['ai_greetings']))
301
 
302
- history = StreamlitChatMessageHistory(key=CHAT_MESSAGES)
303
- prompt_template = ChatPromptTemplate.from_template(
304
- _get_prompt_template(
305
- is_refinement=_is_it_refinement()
306
- )
307
- )
308
 
309
- # Since Streamlit app reloads at every interaction, display the chat history
310
- # from the save session state
311
- for msg in history.messages:
312
- st.chat_message(msg.type).code(msg.content, language='json')
313
-
314
- # Chat input at the bottom
315
- prompt = st.chat_input(
316
- placeholder=APP_TEXT['chat_placeholder'],
317
- max_chars=GlobalConfig.LLM_MODEL_MAX_INPUT_LENGTH,
318
- accept_file=True,
319
- file_type=['pdf', ],
320
  )
321
 
322
- if prompt:
323
- prompt_text = prompt.text or ''
324
- if prompt['files']:
325
- # Store uploaded pdf in session state
326
- uploaded_pdf = prompt['files'][0]
327
- st.session_state[PDF_FILE_KEY] = uploaded_pdf
328
- # Apparently, Streamlit stores uploaded files in memory and clears on browser close
329
- # https://docs.streamlit.io/knowledge-base/using-streamlit/where-file-uploader-store-when-deleted
330
-
331
- # Check if pdf file is uploaded
332
- # (we can use the same file if the user doesn't upload a new one)
333
- if PDF_FILE_KEY in st.session_state:
334
- # Get validated page range
335
- (
336
- st.session_state['start_page'],
337
- st.session_state['end_page']
338
- ) = filem.validate_page_range(
339
- st.session_state[PDF_FILE_KEY],
340
- st.session_state['start_page'],
341
- st.session_state['end_page']
342
- )
343
- # Show sidebar text for page selection and file name
344
- with st.sidebar:
345
- if st.session_state['end_page'] is None: # If the PDF has only one page
346
- st.text(
347
- f'Extracting page {st.session_state["start_page"]} in'
348
- f' {st.session_state["pdf_file"].name}'
349
- )
350
- else:
351
- st.text(
352
- f'Extracting pages {st.session_state["start_page"]} to'
353
- f' {st.session_state["end_page"]} in {st.session_state["pdf_file"].name}'
354
- )
355
-
356
- # Get pdf contents
357
- st.session_state[ADDITIONAL_INFO] = filem.get_pdf_contents(
358
- st.session_state[PDF_FILE_KEY],
359
- (st.session_state['start_page'], st.session_state['end_page'])
360
- )
361
- provider, llm_name = llm_helper.get_provider_model(
362
- llm_provider_to_use,
363
- use_ollama=RUN_IN_OFFLINE_MODE
364
- )
365
 
366
- user_key = api_key_token.strip()
367
- az_deployment = azure_deployment.strip()
368
- az_endpoint = azure_endpoint.strip()
369
- api_ver = api_version.strip()
370
 
371
- if not are_all_inputs_valid(
372
- prompt_text, provider, llm_name, user_key,
373
- az_deployment, az_endpoint, api_ver
374
- ):
375
- return
376
 
377
- logger.info(
378
- 'User input: %s | #characters: %d | LLM: %s',
379
- prompt_text, len(prompt_text), llm_name
380
- )
381
- st.chat_message('user').write(prompt_text)
382
-
383
- if _is_it_refinement():
384
- user_messages = _get_user_messages()
385
- user_messages.append(prompt_text)
386
- list_of_msgs = [
387
- f'{idx + 1}. {msg}' for idx, msg in enumerate(user_messages)
388
- ]
389
- formatted_template = prompt_template.format(
390
- **{
391
- 'instructions': '\n'.join(list_of_msgs),
392
- 'previous_content': _get_last_response(),
393
- 'additional_info': st.session_state.get(ADDITIONAL_INFO, ''),
394
- }
395
- )
396
- else:
397
- formatted_template = prompt_template.format(
398
- **{
399
- 'question': prompt_text,
400
- 'additional_info': st.session_state.get(ADDITIONAL_INFO, ''),
401
- }
402
- )
403
-
404
- progress_bar = st.progress(0, 'Preparing to call LLM...')
405
- response = ''
406
 
407
- try:
408
- llm = llm_helper.get_langchain_llm(
409
- provider=provider,
410
- model=llm_name,
411
- max_new_tokens=gcfg.get_max_output_tokens(llm_provider_to_use),
412
- api_key=user_key,
413
- azure_endpoint_url=az_endpoint,
414
- azure_deployment_name=az_deployment,
415
- azure_api_version=api_ver,
416
- )
417
-
418
- if not llm:
419
- handle_error(
420
- 'Failed to create an LLM instance! Make sure that you have selected the'
421
- ' correct model from the dropdown list and have provided correct API key'
422
- ' or access token.',
423
- False
424
- )
425
- return
426
 
427
- for chunk in llm.stream(formatted_template):
428
- if isinstance(chunk, str):
429
- response += chunk
430
- else:
431
- content = getattr(chunk, 'content', None)
432
- if content is not None:
433
- response += content
434
- else:
435
- response += str(chunk)
436
-
437
- # Update the progress bar with an approx progress percentage
438
- progress_bar.progress(
439
- min(
440
- len(response) / gcfg.get_max_output_tokens(llm_provider_to_use),
441
- 0.95
442
- ),
443
- text='Streaming content...this might take a while...'
444
- )
445
- except (httpx.ConnectError, requests.exceptions.ConnectionError):
446
- handle_error(
447
- 'A connection error occurred while streaming content from the LLM endpoint.'
448
- ' Unfortunately, the slide deck cannot be generated. Please try again later.'
449
- ' Alternatively, try selecting a different LLM from the dropdown list. If you are'
450
- ' using Ollama, make sure that Ollama is already running on your system.',
451
- True
452
- )
453
- return
454
- except huggingface_hub.errors.ValidationError as ve:
455
- handle_error(
456
- f'An error occurred while trying to generate the content: {ve}'
457
- '\nPlease try again with a significantly shorter input text.',
458
- True
459
- )
460
- return
461
- except ollama.ResponseError:
462
- handle_error(
463
- f'The model `{llm_name}` is unavailable with Ollama on your system.'
464
- f' Make sure that you have provided the correct LLM name or pull it using'
465
- f' `ollama pull {llm_name}`. View LLMs available locally by running `ollama list`.',
466
- True
467
- )
468
- return
469
- except Exception as ex:
470
- _msg = str(ex)
471
- if 'payment required' in _msg.lower():
472
- handle_error(
473
- 'The available inference quota has exhausted.'
474
- ' Please use your own Hugging Face access token. Paste your token in'
475
- ' the input field on the sidebar to the left.'
476
- '\n\nDon\'t have a token? Get your free'
477
- ' [HF access token](https://huggingface.co/settings/tokens) now'
478
- ' and start creating your slide deck! For gated models, you may need to'
479
- ' visit the model\'s page and accept the terms or service.'
480
- '\n\nAlternatively, choose a different LLM and provider from the list.',
481
- should_log=True
482
  )
483
  else:
484
- handle_error(
485
- f'An unexpected error occurred while generating the content: {_msg}'
486
- '\n\nPlease try again later, possibly with different inputs.'
487
- ' Alternatively, try selecting a different LLM from the dropdown list.'
488
- ' If you are using Azure OpenAI, Cohere, Gemini, or Together AI models, make'
489
- ' sure that you have provided a correct API key.'
490
- ' Read **[how to get free LLM API keys](https://github.com/barun-saha/slide-deck-ai?tab=readme-ov-file#summary-of-the-llms)**.',
491
- True
492
  )
493
- return
494
-
495
- history.add_user_message(prompt_text)
496
- history.add_ai_message(response)
497
 
498
- # The content has been generated as JSON
499
- # There maybe trailing ``` at the end of the response -- remove them
500
- # To be careful: ``` may be part of the content as well when code is generated
501
- response = text_helper.get_clean_json(response)
502
- logger.info(
503
- 'Cleaned JSON length: %d', len(response)
504
- )
505
 
506
- # Now create the PPT file
507
- progress_bar.progress(
508
- GlobalConfig.LLM_PROGRESS_MAX,
509
- text='Finding photos online and generating the slide deck...'
510
- )
511
- progress_bar.progress(1.0, text='Done!')
512
- st.chat_message('ai').code(response, language='json')
513
 
514
- if path := generate_slide_deck(response):
515
- _display_download_button(path)
 
516
 
517
- logger.info(
518
- '#messages in history / 2: %d',
519
- len(st.session_state[CHAT_MESSAGES]) / 2
520
- )
521
 
522
 
523
- def generate_slide_deck(json_str: str) -> Union[pathlib.Path, None]:
524
  """
525
- Create a slide deck and return the file path. In case there is any error creating the slide
526
- deck, the path may be to an empty file.
527
 
528
- :param json_str: The content in *valid* JSON format.
529
- :return: The path to the .pptx file or `None` in case of error.
 
530
  """
531
 
532
- try:
533
- parsed_data = json5.loads(json_str)
534
- except ValueError:
535
- handle_error(
536
- 'Encountered error while parsing JSON...will fix it and retry',
537
- True
538
- )
539
- try:
540
- parsed_data = json5.loads(text_helper.fix_malformed_json(json_str))
541
- except ValueError:
542
- handle_error(
543
- 'Encountered an error again while fixing JSON...'
544
- 'the slide deck cannot be created, unfortunately ☹'
545
- '\nPlease try again later.',
546
- True
547
- )
548
- return None
549
- except RecursionError:
550
- handle_error(
551
- 'Encountered a recursion error while parsing JSON...'
552
- 'the slide deck cannot be created, unfortunately ☹'
553
- '\nPlease try again later.',
554
- True
555
- )
556
- return None
557
- except Exception:
558
- handle_error(
559
- 'Encountered an error while parsing JSON...'
560
- 'the slide deck cannot be created, unfortunately ☹'
561
- '\nPlease try again later.',
562
- True
563
- )
564
- return None
565
-
566
- if DOWNLOAD_FILE_KEY in st.session_state:
567
- path = pathlib.Path(st.session_state[DOWNLOAD_FILE_KEY])
568
- else:
569
- temp = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
570
- path = pathlib.Path(temp.name)
571
- st.session_state[DOWNLOAD_FILE_KEY] = str(path)
572
-
573
- if temp:
574
- temp.close()
575
 
576
  try:
577
- logger.debug('Creating PPTX file: %s...', st.session_state[DOWNLOAD_FILE_KEY])
578
- pptx_helper.generate_powerpoint_presentation(
579
- parsed_data,
580
- slides_template=pptx_template,
581
- output_file_path=path
582
- )
583
  except Exception as ex:
584
- st.error(APP_TEXT['content_generation_error'])
585
- logger.error('Caught a generic exception: %s', str(ex))
586
-
587
- return path
588
-
589
-
590
- def _is_it_refinement() -> bool:
591
- """
592
- Whether it is the initial prompt or a refinement.
593
-
594
- :return: True if it is the initial prompt; False otherwise.
595
- """
596
-
597
- if IS_IT_REFINEMENT in st.session_state:
598
- return True
599
 
600
- if len(st.session_state[CHAT_MESSAGES]) >= 2:
601
- # Prepare for the next call
602
- st.session_state[IS_IT_REFINEMENT] = True
603
- return True
604
 
605
- return False
 
606
 
 
607
 
608
- def _get_user_messages() -> List[str]:
609
- """
610
- Get a list of user messages submitted until now from the session state.
611
 
612
- :return: The list of user messages.
613
  """
 
614
 
615
- return [
616
- msg.content for msg in st.session_state[CHAT_MESSAGES] if isinstance(msg, HumanMessage)
617
- ]
618
-
619
-
620
- def _get_last_response() -> str:
621
  """
622
- Get the last response generated by AI.
623
 
624
- :return: The response text.
625
- """
626
 
627
- return st.session_state[CHAT_MESSAGES][-1].content
 
 
 
 
628
 
 
 
629
 
630
- def _display_messages_history(view_messages: st.expander):
631
- """
632
- Display the history of messages.
 
 
 
 
 
633
 
634
- :param view_messages: The list of AI and Human messages.
635
- """
636
 
637
- with view_messages:
638
- view_messages.json(st.session_state[CHAT_MESSAGES])
639
 
640
 
641
- def _display_download_button(file_path: pathlib.Path):
642
  """
643
- Display a download button to download a slide deck.
644
 
645
- :param file_path: The path of the .pptx file.
646
  """
647
 
648
- with open(file_path, 'rb') as download_file:
649
- st.download_button(
650
- 'Download PPTX file ⬇️',
651
- data=download_file,
652
- file_name='Presentation.pptx',
653
- key=datetime.datetime.now()
654
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
655
 
656
 
657
  def main():
 
 
 
 
 
 
 
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():
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,206 +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
- '[gg]gemini-2.5-flash': {
68
- 'description': 'fast, detailed',
69
- 'max_new_tokens': 8192,
70
- 'paid': True,
71
- },
72
- '[gg]gemini-2.5-flash-lite': {
73
- 'description': 'fastest, detailed',
74
- 'max_new_tokens': 8192,
75
- 'paid': True,
76
- },
77
- # '[hf]mistralai/Mistral-7B-Instruct-v0.2': {
78
- # 'description': 'faster, shorter',
79
- # 'max_new_tokens': 8192,
80
- # 'paid': False,
81
- # },
82
- # '[hf]mistralai/Mistral-Nemo-Instruct-2407': {
83
- # 'description': 'longer response',
84
- # 'max_new_tokens': 8192,
85
- # 'paid': False,
86
- # },
87
- '[or]google/gemini-2.0-flash-001': {
88
- 'description': 'Google Gemini-2.0-flash-001 (via OpenRouter)',
89
- 'max_new_tokens': 8192,
90
- 'paid': True,
91
- },
92
- '[or]openai/gpt-3.5-turbo': {
93
- 'description': 'OpenAI GPT-3.5 Turbo (via OpenRouter)',
94
- 'max_new_tokens': 4096,
95
- 'paid': True,
96
- },
97
- '[to]deepseek-ai/DeepSeek-V3': {
98
- 'description': 'slower, medium',
99
- 'max_new_tokens': 8192,
100
- 'paid': True,
101
- },
102
- '[to]meta-llama/Llama-3.3-70B-Instruct-Turbo': {
103
- 'description': 'slower, detailed',
104
- 'max_new_tokens': 4096,
105
- 'paid': True,
106
- },
107
- '[to]meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo-128K': {
108
- 'description': 'faster, shorter',
109
- 'max_new_tokens': 4096,
110
- 'paid': True,
111
- }
112
- }
113
- LLM_PROVIDER_HELP = (
114
- 'LLM provider codes:\n\n'
115
- '- **[az]**: Azure OpenAI\n'
116
- '- **[co]**: Cohere\n'
117
- '- **[gg]**: Google Gemini API\n'
118
- # '- **[hf]**: Hugging Face Inference API\n'
119
- '- **[or]**: OpenRouter\n\n'
120
- '- **[to]**: Together AI\n\n'
121
- '[Find out more](https://github.com/barun-saha/slide-deck-ai?tab=readme-ov-file#summary-of-the-llms)'
122
- )
123
- DEFAULT_MODEL_INDEX = int(os.environ.get('DEFAULT_MODEL_INDEX', '4'))
124
- LLM_MODEL_TEMPERATURE = 0.2
125
- MAX_PAGE_COUNT = 50
126
- MAX_ALLOWED_PAGES = 150
127
- LLM_MODEL_MAX_INPUT_LENGTH = 1000 # characters
128
 
129
  LOG_LEVEL = 'DEBUG'
130
- COUNT_TOKENS = False
131
  APP_STRINGS_FILE = 'strings.json'
132
  PRELOAD_DATA_FILE = 'examples/example_02.json'
133
- INITIAL_PROMPT_TEMPLATE = 'prompts/initial_template_v4_two_cols_img.txt'
134
- REFINEMENT_PROMPT_TEMPLATE = 'prompts/refinement_template_v4_two_cols_img.txt'
135
-
136
- LLM_PROGRESS_MAX = 90
137
- ICONS_DIR = 'icons/png128/'
138
- TINY_BERT_MODEL = 'gaunernst/bert-mini-uncased'
139
- EMBEDDINGS_FILE_NAME = 'file_embeddings/embeddings.npy'
140
- ICONS_FILE_NAME = 'file_embeddings/icons.npy'
141
 
142
  PPTX_TEMPLATE_FILES = {
143
- 'Basic': {
144
  'file': 'pptx_templates/Blank.pptx',
145
- '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)) 🟧'
146
  },
147
  'Ion Boardroom': {
148
  'file': 'pptx_templates/Ion_Boardroom.pptx',
149
- 'caption': 'Make some bold decisions 🟥'
150
- },
151
- 'Minimalist Sales Pitch': {
152
- 'file': 'pptx_templates/Minimalist_sales_pitch.pptx',
153
- 'caption': 'In high contrast ⬛'
154
  },
155
  'Urban Monochrome': {
156
  'file': 'pptx_templates/Urban_monochrome.pptx',
157
- 'caption': 'Marvel in a monochrome dream'
158
- },
159
  }
160
-
161
- # This is a long text, so not incorporated as a string in `strings.json`
162
- CHAT_USAGE_INSTRUCTIONS = (
163
- 'Briefly describe your topic of presentation in the textbox provided below. For example:\n'
164
- '- Make a slide deck on AI.'
165
- '\n\n'
166
- 'Subsequently, you can add follow-up instructions, e.g.:\n'
167
- '- Can you add a slide on GPUs?'
168
- '\n\n'
169
- ' You can also ask it to refine any particular slide, e.g.:\n'
170
- '- Make the slide with title \'Examples of AI\' a bit more descriptive.'
171
- '\n\n'
172
- 'Finally, click on the download button at the bottom to download the slide deck.'
173
- ' See this [demo video](https://youtu.be/QvAKzNKtk9k) for a brief walkthrough.\n\n'
174
- 'Remember, the conversational interface is meant to (and will) update yor *initial*/'
175
- '*previous* slide deck. If you want to create a new slide deck on a different topic,'
176
- ' start a new chat session by reloading this page.'
177
- '\n\nSlideDeck AI can algo generate a presentation based on a PDF file. You can upload'
178
- ' a PDF file using the chat widget. Only a single file and up to max 50 pages will be'
179
- ' considered. For PDF-based slide deck generation, LLMs with large context windows, such'
180
- ' as Gemini, GPT, and Mistral-Nemo, are recommended. Note: images from the PDF files will'
181
- ' not be used.'
182
- '\n\nAlso, note that the uploaded file might disappear from the page after click.'
183
- ' You do not need to upload the same file again to continue'
184
- ' the interaction and refining—the contents of the PDF file will be retained in the'
185
- ' same interactive session.'
186
- '\n\nCurrently, paid or *free-to-use* LLMs from six different providers are supported.'
187
- ' A [summary of the supported LLMs]('
188
- 'https://github.com/barun-saha/slide-deck-ai/blob/main/README.md#summary-of-the-llms)'
189
- ' is available for reference. SlideDeck AI does **NOT** store your API keys.'
190
- '\n\nSlideDeck AI does not have access to the Web, apart for searching for images relevant'
191
- ' to the slides. Photos are added probabilistically; transparency needs to be changed'
192
- ' manually, if required.\n\n'
193
- '[SlideDeck AI](https://github.com/barun-saha/slide-deck-ai) is an Open-Source project,'
194
- ' released under the'
195
- ' [MIT license](https://github.com/barun-saha/slide-deck-ai?tab=MIT-1-ov-file#readme).'
196
- '\n\n---\n\n'
197
- '© Copyright 2023-2025 Barun Saha.\n\n'
198
- )
199
-
200
-
201
- logging.basicConfig(
202
- level=GlobalConfig.LOG_LEVEL,
203
- format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
204
- datefmt='%Y-%m-%d %H:%M:%S'
205
- )
206
-
207
-
208
- def get_max_output_tokens(llm_name: str) -> int:
209
- """
210
- Get the max output tokens value configured for an LLM. Return a default value if not configured.
211
-
212
- :param llm_name: The name of the LLM.
213
- :return: Max output tokens or a default count.
214
- """
215
-
216
- try:
217
- return GlobalConfig.VALID_MODELS[llm_name]['max_new_tokens']
218
- except KeyError:
219
- 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,73 +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
- page_range: tuple[int, int]) -> str:
23
- """
24
- Extract the text contents from a PDF file.
25
-
26
- :param pdf_file: The uploaded PDF file.
27
- :param page_range: The range of pages to extract contents from.
28
- :return: The contents.
29
- """
30
-
31
- reader = PdfReader(pdf_file)
32
-
33
- start, end = page_range # Set start and end per the range (user-specified values)
34
-
35
- text = ''
36
-
37
- if end is None:
38
- # If end is None (where PDF has only 1 page or start = end), extract start
39
- end = start
40
-
41
- # Get the text from the specified page range
42
- for page_num in range(start - 1, end):
43
- text += reader.pages[page_num].extract_text()
44
-
45
-
46
- return text
47
-
48
- def validate_page_range(pdf_file: st.runtime.uploaded_file_manager.UploadedFile,
49
- start:int, end:int) -> tuple[int, int]:
50
- """
51
- Validate the page range.
52
-
53
- :param pdf_file: The uploaded PDF file.
54
- :param start: The start page
55
- :param end: The end page
56
- :return: The validated page range tuple
57
- """
58
- n_pages = len(PdfReader(pdf_file).pages)
59
-
60
- # Set start to max of 1 or specified start (whichever's higher)
61
- start = max(1, start)
62
-
63
- # Set end to min of pdf length or specified end (whichever's lower)
64
- end = min(n_pages, end)
65
-
66
- if start > end: # If the start is higher than the end, make it 1
67
- start = 1
68
-
69
- if start == end:
70
- # If start = end (including when PDF is 1 page long), set end to None
71
- return start, None
72
-
73
- return start, end
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,259 +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 provider != GlobalConfig.PROVIDER_OLLAMA:
99
- # No API key is required for offline Ollama models
100
- if not api_key:
101
- return False
102
-
103
- if api_key and API_KEY_REGEX.match(api_key) is None:
104
- return False
105
-
106
- if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
107
- valid_url = urllib3.util.parse_url(azure_endpoint_url)
108
- all_status = all(
109
- [azure_api_version, azure_deployment_name, str(valid_url)]
110
- )
111
- return all_status
112
-
113
- return True
114
-
115
-
116
- def get_langchain_llm(
117
- provider: str,
118
- model: str,
119
- max_new_tokens: int,
120
- api_key: str = '',
121
- azure_endpoint_url: str = '',
122
- azure_deployment_name: str = '',
123
- azure_api_version: str = '',
124
- ) -> Union[BaseLLM, BaseChatModel, None]:
125
- """
126
- Get an LLM based on the provider and model specified.
127
-
128
- :param provider: The LLM provider. Valid values are `hf` for Hugging Face.
129
- :param model: The name of the LLM.
130
- :param max_new_tokens: The maximum number of tokens to generate.
131
- :param api_key: API key or access token to use.
132
- :param azure_endpoint_url: Azure OpenAI endpoint URL.
133
- :param azure_deployment_name: Azure OpenAI deployment name.
134
- :param azure_api_version: Azure OpenAI API version.
135
- :return: An instance of the LLM or Chat model; `None` in case of any error.
136
- """
137
-
138
- if provider == GlobalConfig.PROVIDER_HUGGING_FACE:
139
- from langchain_community.llms.huggingface_endpoint import HuggingFaceEndpoint
140
-
141
- logger.debug('Getting LLM via HF endpoint: %s', model)
142
- return HuggingFaceEndpoint(
143
- repo_id=model,
144
- max_new_tokens=max_new_tokens,
145
- top_k=40,
146
- top_p=0.95,
147
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
148
- repetition_penalty=1.03,
149
- streaming=True,
150
- huggingfacehub_api_token=api_key,
151
- return_full_text=False,
152
- stop_sequences=['</s>'],
153
- )
154
-
155
- if provider == GlobalConfig.PROVIDER_GOOGLE_GEMINI:
156
- from google.generativeai.types.safety_types import HarmBlockThreshold, HarmCategory
157
- from langchain_google_genai import GoogleGenerativeAI
158
-
159
- logger.debug('Getting LLM via Google Gemini: %s', model)
160
- return GoogleGenerativeAI(
161
- model=model,
162
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
163
- # max_tokens=max_new_tokens,
164
- timeout=None,
165
- max_retries=2,
166
- google_api_key=api_key,
167
- safety_settings={
168
- HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT:
169
- HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
170
- HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
171
- HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
172
- HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT:
173
- HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
174
- }
175
- )
176
-
177
- if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
178
- from langchain_openai import AzureChatOpenAI
179
-
180
- logger.debug('Getting LLM via Azure OpenAI: %s', model)
181
-
182
- # The `model` parameter is not used here; `azure_deployment` points to the desired name
183
- return AzureChatOpenAI(
184
- azure_deployment=azure_deployment_name,
185
- api_version=azure_api_version,
186
- azure_endpoint=azure_endpoint_url,
187
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
188
- # max_tokens=max_new_tokens,
189
- timeout=None,
190
- max_retries=1,
191
- api_key=api_key,
192
- )
193
-
194
- if provider == GlobalConfig.PROVIDER_OPENROUTER:
195
- # Use langchain-openai's ChatOpenAI for OpenRouter
196
- from langchain_openai import ChatOpenAI
197
-
198
- logger.debug('Getting LLM via OpenRouter: %s', model)
199
- openrouter_api_key = api_key
200
-
201
- return ChatOpenAI(
202
- base_url=OPENROUTER_BASE_URL,
203
- openai_api_key=openrouter_api_key,
204
- model_name=model,
205
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
206
- max_tokens=max_new_tokens,
207
- streaming=True,
208
- )
209
-
210
- if provider == GlobalConfig.PROVIDER_COHERE:
211
- from langchain_cohere.llms import Cohere
212
-
213
- logger.debug('Getting LLM via Cohere: %s', model)
214
- return Cohere(
215
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
216
- max_tokens=max_new_tokens,
217
- timeout_seconds=None,
218
- max_retries=2,
219
- cohere_api_key=api_key,
220
- streaming=True,
221
- )
222
-
223
- if provider == GlobalConfig.PROVIDER_TOGETHER_AI:
224
- from langchain_together import Together
225
-
226
- logger.debug('Getting LLM via Together AI: %s', model)
227
- return Together(
228
- model=model,
229
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
230
- together_api_key=api_key,
231
- max_tokens=max_new_tokens,
232
- top_k=40,
233
- top_p=0.90,
234
- )
235
-
236
- if provider == GlobalConfig.PROVIDER_OLLAMA:
237
- from langchain_ollama.llms import OllamaLLM
238
-
239
- logger.debug('Getting LLM via Ollama: %s', model)
240
- return OllamaLLM(
241
- model=model,
242
- temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
243
- num_predict=max_new_tokens,
244
- format='json',
245
- streaming=True,
246
- )
247
-
248
- return None
249
-
250
-
251
- if __name__ == '__main__':
252
- inputs = [
253
- '[co]Cohere',
254
- '[hf]mistralai/Mistral-7B-Instruct-v0.2',
255
- '[gg]gemini-1.5-flash-002'
256
- ]
257
-
258
- for text in inputs:
259
- print(get_provider_model(text, use_ollama=False))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helpers/pptx_helper.py DELETED
@@ -1,1129 +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
- logger.error(
217
- 'An error occurred while processing a slide...continuing with the next one',
218
- exc_info=True
219
- )
220
- continue
221
-
222
- # The thank-you slide
223
- last_slide_layout = presentation.slide_layouts[0]
224
- slide = presentation.slides.add_slide(last_slide_layout)
225
- title = slide.shapes.title
226
- title.text = 'Thank you!'
227
-
228
- presentation.save(output_file_path)
229
-
230
- return all_headers
231
-
232
-
233
- def get_flat_list_of_contents(items: list, level: int) -> List[Tuple]:
234
- """
235
- Flatten a (hierarchical) list of bullet points to a single list containing each item and
236
- its level.
237
-
238
- :param items: A bullet point (string or list).
239
- :param level: The current level of hierarchy.
240
- :return: A list of (bullet item text, hierarchical level) tuples.
241
- """
242
-
243
- flat_list = []
244
-
245
- for item in items:
246
- if isinstance(item, str):
247
- flat_list.append((item, level))
248
- elif isinstance(item, list):
249
- flat_list = flat_list + get_flat_list_of_contents(item, level + 1)
250
-
251
- return flat_list
252
-
253
-
254
- def get_slide_placeholders(
255
- slide: pptx.slide.Slide,
256
- layout_number: int,
257
- is_debug: bool = False
258
- ) -> List[Tuple[int, str]]:
259
- """
260
- Return the index and name (lower case) of all placeholders present in a slide, except
261
- the title placeholder.
262
-
263
- A placeholder in a slide is a place to add content. Each placeholder has a name and an index.
264
- This index is NOT a list index, rather a set of keys used to look up a dict. So, `idx` is
265
- non-contiguous. Also, the title placeholder of a slide always has index 0. User-added
266
- placeholder get indices assigned starting from 10.
267
-
268
- With user-edited or added placeholders, their index may be difficult to track. This function
269
- returns the placeholders name as well, which could be useful to distinguish between the
270
- different placeholder.
271
-
272
- :param slide: The slide.
273
- :param layout_number: The layout number used by the slide.
274
- :param is_debug: Whether to print debugging statements.
275
- :return: A list containing placeholders (idx, name) tuples, except the title placeholder.
276
- """
277
-
278
- if is_debug:
279
- print(
280
- f'Slide layout #{layout_number}:'
281
- f' # of placeholders: {len(slide.shapes.placeholders)} (including the title)'
282
- )
283
-
284
- placeholders = [
285
- (shape.placeholder_format.idx, shape.name.lower()) for shape in slide.shapes.placeholders
286
- ]
287
- placeholders.pop(0) # Remove the title placeholder
288
-
289
- if is_debug:
290
- print(placeholders)
291
-
292
- return placeholders
293
-
294
-
295
- def _handle_default_display(
296
- presentation: pptx.Presentation,
297
- slide_json: dict,
298
- slide_width_inch: float,
299
- slide_height_inch: float
300
- ):
301
- """
302
- Display a list of text in a slide.
303
-
304
- :param presentation: The presentation object.
305
- :param slide_json: The content of the slide as JSON data.
306
- :param slide_width_inch: The width of the slide in inches.
307
- :param slide_height_inch: The height of the slide in inches.
308
- """
309
-
310
- status = False
311
-
312
- if 'img_keywords' in slide_json:
313
- if random.random() < IMAGE_DISPLAY_PROBABILITY:
314
- if random.random() < FOREGROUND_IMAGE_PROBABILITY:
315
- status = _handle_display_image__in_foreground(
316
- presentation,
317
- slide_json,
318
- slide_width_inch,
319
- slide_height_inch
320
- )
321
- else:
322
- status = _handle_display_image__in_background(
323
- presentation,
324
- slide_json,
325
- slide_width_inch,
326
- slide_height_inch
327
- )
328
-
329
- if status:
330
- return
331
-
332
- # Image display failed, so display only text
333
- bullet_slide_layout = presentation.slide_layouts[1]
334
- slide = presentation.slides.add_slide(bullet_slide_layout)
335
-
336
- shapes = slide.shapes
337
- title_shape = shapes.title
338
-
339
- try:
340
- body_shape = shapes.placeholders[1]
341
- except KeyError:
342
- placeholders = get_slide_placeholders(slide, layout_number=1)
343
- body_shape = shapes.placeholders[placeholders[0][0]]
344
-
345
- title_shape.text = remove_slide_number_from_heading(slide_json['heading'])
346
- text_frame = body_shape.text_frame
347
-
348
- # The bullet_points may contain a nested hierarchy of JSON arrays
349
- # In some scenarios, it may contain objects (dictionaries) because the LLM generated so
350
- # ^ The second scenario is not covered
351
- flat_items_list = get_flat_list_of_contents(slide_json['bullet_points'], level=0)
352
- add_bulleted_items(text_frame, flat_items_list)
353
-
354
- _handle_key_message(
355
- the_slide=slide,
356
- slide_json=slide_json,
357
- slide_height_inch=slide_height_inch,
358
- slide_width_inch=slide_width_inch
359
- )
360
-
361
-
362
- def _handle_display_image__in_foreground(
363
- presentation: pptx.Presentation,
364
- slide_json: dict,
365
- slide_width_inch: float,
366
- slide_height_inch: float
367
- ) -> bool:
368
- """
369
- Create a slide with text and image using a picture placeholder layout. If not image keyword is
370
- available, it will add only text to the slide.
371
-
372
- :param presentation: The presentation object.
373
- :param slide_json: The content of the slide as JSON data.
374
- :param slide_width_inch: The width of the slide in inches.
375
- :param slide_height_inch: The height of the slide in inches.
376
- :return: True if the side has been processed.
377
- """
378
-
379
- img_keywords = slide_json['img_keywords'].strip()
380
- slide = presentation.slide_layouts[8] # Picture with Caption
381
- slide = presentation.slides.add_slide(slide)
382
- placeholders = None
383
-
384
- title_placeholder = slide.shapes.title
385
- title_placeholder.text = remove_slide_number_from_heading(slide_json['heading'])
386
-
387
- try:
388
- pic_col: PicturePlaceholder = slide.shapes.placeholders[1]
389
- except KeyError:
390
- placeholders = get_slide_placeholders(slide, layout_number=8)
391
- pic_col = None
392
- for idx, name in placeholders:
393
- if 'picture' in name:
394
- pic_col: PicturePlaceholder = slide.shapes.placeholders[idx]
395
-
396
- try:
397
- text_col: SlidePlaceholder = slide.shapes.placeholders[2]
398
- except KeyError:
399
- text_col = None
400
- if not placeholders:
401
- placeholders = get_slide_placeholders(slide, layout_number=8)
402
-
403
- for idx, name in placeholders:
404
- if 'content' in name:
405
- text_col: SlidePlaceholder = slide.shapes.placeholders[idx]
406
-
407
- flat_items_list = get_flat_list_of_contents(slide_json['bullet_points'], level=0)
408
- add_bulleted_items(text_col.text_frame, flat_items_list)
409
-
410
- if not img_keywords:
411
- # No keywords, so no image search and addition
412
- return True
413
-
414
- try:
415
- photo_url, page_url = ims.get_photo_url_from_api_response(
416
- ims.search_pexels(query=img_keywords, size='medium')
417
- )
418
-
419
- if photo_url:
420
- pic_col.insert_picture(
421
- ims.get_image_from_url(photo_url)
422
- )
423
-
424
- _add_text_at_bottom(
425
- slide=slide,
426
- slide_width_inch=slide_width_inch,
427
- slide_height_inch=slide_height_inch,
428
- text='Photo provided by Pexels',
429
- hyperlink=page_url
430
- )
431
- except Exception as ex:
432
- logger.error(
433
- '*** Error occurred while running adding image to slide: %s',
434
- str(ex)
435
- )
436
-
437
- return True
438
-
439
-
440
- def _handle_display_image__in_background(
441
- presentation: pptx.Presentation,
442
- slide_json: dict,
443
- slide_width_inch: float,
444
- slide_height_inch: float
445
- ) -> bool:
446
- """
447
- Add a slide with text and an image in the background. It works just like
448
- `_handle_default_display()` but with a background image added. If not image keyword is
449
- available, it will add only text to the slide.
450
-
451
- :param presentation: The presentation object.
452
- :param slide_json: The content of the slide as JSON data.
453
- :param slide_width_inch: The width of the slide in inches.
454
- :param slide_height_inch: The height of the slide in inches.
455
- :return: True if the slide has been processed.
456
- """
457
-
458
- img_keywords = slide_json['img_keywords'].strip()
459
-
460
- # Add a photo in the background, text in the foreground
461
- slide = presentation.slides.add_slide(presentation.slide_layouts[1])
462
- title_shape = slide.shapes.title
463
-
464
- try:
465
- body_shape = slide.shapes.placeholders[1]
466
- except KeyError:
467
- placeholders = get_slide_placeholders(slide, layout_number=1)
468
- # Layout 1 usually has two placeholders, including the title
469
- body_shape = slide.shapes.placeholders[placeholders[0][0]]
470
-
471
- title_shape.text = remove_slide_number_from_heading(slide_json['heading'])
472
- flat_items_list = get_flat_list_of_contents(slide_json['bullet_points'], level=0)
473
- add_bulleted_items(body_shape.text_frame, flat_items_list)
474
-
475
- if not img_keywords:
476
- # No keywords, so no image search and addition
477
- return True
478
-
479
- try:
480
- photo_url, page_url = ims.get_photo_url_from_api_response(
481
- ims.search_pexels(query=img_keywords, size='large')
482
- )
483
-
484
- if photo_url:
485
- picture = slide.shapes.add_picture(
486
- image_file=ims.get_image_from_url(photo_url),
487
- left=0,
488
- top=0,
489
- width=pptx.util.Inches(slide_width_inch),
490
- )
491
-
492
- try:
493
- # Find all blip elements to handle potential multiple instances
494
- blip_elements = picture._element.xpath('.//a:blip')
495
- if not blip_elements:
496
- logger.warning(
497
- 'No blip element found in the picture. Transparency cannot be applied.'
498
- )
499
- return True
500
-
501
- for blip in blip_elements:
502
- # Add transparency to the image through the blip properties
503
- alpha_mod = blip.makeelement(
504
- '{http://schemas.openxmlformats.org/drawingml/2006/main}alphaModFix'
505
- )
506
- # Opacity value between 0-100000
507
- alpha_mod.set('amt', '50000') # 50% opacity
508
-
509
- # Check if alphaModFix already exists to avoid duplicates
510
- existing_alpha_mod = blip.find(
511
- '{http://schemas.openxmlformats.org/drawingml/2006/main}alphaModFix'
512
- )
513
- if existing_alpha_mod is not None:
514
- blip.remove(existing_alpha_mod)
515
-
516
- blip.append(alpha_mod)
517
- logger.debug('Added transparency to blip element: %s', blip.xml)
518
-
519
- except Exception as ex:
520
- logger.error(
521
- 'Failed to apply transparency to the image: %s. Continuing without it.',
522
- str(ex)
523
- )
524
-
525
- _add_text_at_bottom(
526
- slide=slide,
527
- slide_width_inch=slide_width_inch,
528
- slide_height_inch=slide_height_inch,
529
- text='Photo provided by Pexels',
530
- hyperlink=page_url
531
- )
532
-
533
- # Move picture to background
534
- try:
535
- slide.shapes._spTree.remove(picture._element)
536
- slide.shapes._spTree.insert(2, picture._element)
537
- except Exception as ex:
538
- logger.error(
539
- 'Failed to move image to background: %s. Image will remain in foreground.',
540
- str(ex)
541
- )
542
-
543
- return True
544
-
545
- except Exception as ex:
546
- logger.error(
547
- '*** Error occurred while adding image to the slide background: %s',
548
- str(ex)
549
- )
550
- return True
551
-
552
- return True
553
-
554
-
555
- def _handle_icons_ideas(
556
- presentation: pptx.Presentation,
557
- slide_json: dict,
558
- slide_width_inch: float,
559
- slide_height_inch: float
560
- ):
561
- """
562
- Add a slide with some icons and text.
563
- If no suitable icons are found, the step numbers are shown.
564
-
565
- :param presentation: The presentation object.
566
- :param slide_json: The content of the slide as JSON data.
567
- :param slide_width_inch: The width of the slide in inches.
568
- :param slide_height_inch: The height of the slide in inches.
569
- :return: True if the slide has been processed.
570
- """
571
-
572
- if 'bullet_points' in slide_json and slide_json['bullet_points']:
573
- items = slide_json['bullet_points']
574
-
575
- # Ensure that it is a single list of strings without any sub-list
576
- for step in items:
577
- if not isinstance(step, str) or not step.startswith(ICON_BEGINNING_MARKER):
578
- return False
579
-
580
- slide_layout = presentation.slide_layouts[5]
581
- slide = presentation.slides.add_slide(slide_layout)
582
- slide.shapes.title.text = remove_slide_number_from_heading(slide_json['heading'])
583
-
584
- n_items = len(items)
585
- text_box_size = INCHES_2
586
-
587
- # Calculate the total width of all pictures and the spacing
588
- total_width = n_items * ICON_SIZE
589
- spacing = (pptx.util.Inches(slide_width_inch) - total_width) / (n_items + 1)
590
- top = INCHES_3
591
-
592
- icons_texts = [
593
- (match.group(1), match.group(2)) for match in [
594
- ICONS_REGEX.search(item) for item in items
595
- ]
596
- ]
597
- fallback_icon_files = ice.find_icons([item[0] for item in icons_texts])
598
-
599
- for idx, item in enumerate(icons_texts):
600
- icon, accompanying_text = item
601
- icon_path = f'{GlobalConfig.ICONS_DIR}/{icon}.png'
602
-
603
- if not os.path.exists(icon_path):
604
- logger.warning(
605
- 'Icon not found: %s...using fallback icon: %s',
606
- icon, fallback_icon_files[idx]
607
- )
608
- icon_path = f'{GlobalConfig.ICONS_DIR}/{fallback_icon_files[idx]}.png'
609
-
610
- left = spacing + idx * (ICON_SIZE + spacing)
611
- # Calculate the center position for alignment
612
- center = left + ICON_SIZE / 2
613
-
614
- # Add a rectangle shape with a fill color (background)
615
- # The size of the shape is slightly bigger than the icon, so align the icon position
616
- shape = slide.shapes.add_shape(
617
- MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
618
- center - INCHES_0_5,
619
- top - (ICON_BG_SIZE - ICON_SIZE) / 2,
620
- INCHES_1, INCHES_1
621
- )
622
- shape.fill.solid()
623
- shape.shadow.inherit = False
624
-
625
- # Set the icon's background shape color
626
- shape.fill.fore_color.rgb = shape.line.color.rgb = random.choice(ICON_COLORS)
627
- # Add the icon image on top of the colored shape
628
- slide.shapes.add_picture(icon_path, left, top, height=ICON_SIZE)
629
-
630
- # Add a text box below the shape
631
- text_box = slide.shapes.add_shape(
632
- MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
633
- left=center - text_box_size / 2, # Center the text box horizontally
634
- top=top + ICON_SIZE + INCHES_0_2,
635
- width=text_box_size,
636
- height=text_box_size
637
- )
638
- text_frame = text_box.text_frame
639
- text_frame.word_wrap = True
640
- text_frame.paragraphs[0].alignment = pptx.enum.text.PP_ALIGN.CENTER
641
- format_text(text_frame.paragraphs[0], accompanying_text)
642
-
643
- # Center the text vertically
644
- text_frame.vertical_anchor = pptx.enum.text.MSO_ANCHOR.MIDDLE
645
- text_box.fill.background() # No fill
646
- text_box.line.fill.background() # No line
647
- text_box.shadow.inherit = False
648
-
649
- # Set the font color based on the theme
650
- for paragraph in text_frame.paragraphs:
651
- for run in paragraph.runs:
652
- run.font.color.theme_color = pptx.enum.dml.MSO_THEME_COLOR.TEXT_2
653
-
654
- _add_text_at_bottom(
655
- slide=slide,
656
- slide_width_inch=slide_width_inch,
657
- slide_height_inch=slide_height_inch,
658
- text='More icons available in the SlideDeck AI repository',
659
- hyperlink='https://github.com/barun-saha/slide-deck-ai/tree/main/icons/png128'
660
- )
661
-
662
- return True
663
-
664
- return False
665
-
666
-
667
- def _add_text_at_bottom(
668
- slide: pptx.slide.Slide,
669
- slide_width_inch: float,
670
- slide_height_inch: float,
671
- text: str,
672
- hyperlink: Optional[str] = None,
673
- target_height: Optional[float] = 0.5
674
- ):
675
- """
676
- Add arbitrary text to a textbox positioned near the lower left side of a slide.
677
-
678
- :param slide: The slide.
679
- :param slide_width_inch: The width of the slide.
680
- :param slide_height_inch: The height of the slide.
681
- :param target_height: the target height of the box in inches (optional).
682
- :param text: The text to be added
683
- :param hyperlink: The hyperlink to be added to the text (optional).
684
- """
685
-
686
- footer = slide.shapes.add_textbox(
687
- left=INCHES_1,
688
- top=pptx.util.Inches(slide_height_inch - target_height),
689
- width=pptx.util.Inches(slide_width_inch),
690
- height=pptx.util.Inches(target_height)
691
- )
692
-
693
- paragraph = footer.text_frame.paragraphs[0]
694
- run = paragraph.add_run()
695
- run.text = text
696
- run.font.size = pptx.util.Pt(10)
697
- run.font.underline = False
698
-
699
- if hyperlink:
700
- run.hyperlink.address = hyperlink
701
-
702
-
703
- def _handle_double_col_layout(
704
- presentation: pptx.Presentation,
705
- slide_json: dict,
706
- slide_width_inch: float,
707
- slide_height_inch: float
708
- ) -> bool:
709
- """
710
- Add a slide with a double column layout for comparison.
711
-
712
- :param presentation: The presentation object.
713
- :param slide_json: The content of the slide as JSON data.
714
- :param slide_width_inch: The width of the slide in inches.
715
- :param slide_height_inch: The height of the slide in inches.
716
- :return: True if double col layout has been added; False otherwise.
717
- """
718
-
719
- if 'bullet_points' in slide_json and slide_json['bullet_points']:
720
- double_col_content = slide_json['bullet_points']
721
-
722
- if double_col_content and (
723
- len(double_col_content) == 2
724
- ) and isinstance(double_col_content[0], dict) and isinstance(double_col_content[1], dict):
725
- slide = presentation.slide_layouts[4]
726
- slide = presentation.slides.add_slide(slide)
727
- placeholders = None
728
-
729
- shapes = slide.shapes
730
- title_placeholder = shapes.title
731
- title_placeholder.text = remove_slide_number_from_heading(slide_json['heading'])
732
-
733
- try:
734
- left_heading, right_heading = shapes.placeholders[1], shapes.placeholders[3]
735
- except KeyError:
736
- # For manually edited/added master slides, the placeholder idx numbers in the dict
737
- # will be different (>= 10)
738
- left_heading, right_heading = None, None
739
- placeholders = get_slide_placeholders(slide, layout_number=4)
740
-
741
- for idx, name in placeholders:
742
- if 'text placeholder' in name:
743
- if not left_heading:
744
- left_heading = shapes.placeholders[idx]
745
- elif not right_heading:
746
- right_heading = shapes.placeholders[idx]
747
-
748
- try:
749
- left_col, right_col = shapes.placeholders[2], shapes.placeholders[4]
750
- except KeyError:
751
- left_col, right_col = None, None
752
- if not placeholders:
753
- placeholders = get_slide_placeholders(slide, layout_number=4)
754
-
755
- for idx, name in placeholders:
756
- if 'content placeholder' in name:
757
- if not left_col:
758
- left_col = shapes.placeholders[idx]
759
- elif not right_col:
760
- right_col = shapes.placeholders[idx]
761
-
762
- left_col_frame, right_col_frame = left_col.text_frame, right_col.text_frame
763
-
764
- if 'heading' in double_col_content[0] and left_heading:
765
- left_heading.text = double_col_content[0]['heading']
766
- if 'bullet_points' in double_col_content[0]:
767
- flat_items_list = get_flat_list_of_contents(
768
- double_col_content[0]['bullet_points'], level=0
769
- )
770
-
771
- if not left_heading:
772
- left_col_frame.text = double_col_content[0]['heading']
773
-
774
- add_bulleted_items(left_col_frame, flat_items_list)
775
-
776
- if 'heading' in double_col_content[1] and right_heading:
777
- right_heading.text = double_col_content[1]['heading']
778
- if 'bullet_points' in double_col_content[1]:
779
- flat_items_list = get_flat_list_of_contents(
780
- double_col_content[1]['bullet_points'], level=0
781
- )
782
-
783
- if not right_heading:
784
- right_col_frame.text = double_col_content[1]['heading']
785
-
786
- add_bulleted_items(right_col_frame, flat_items_list)
787
-
788
- _handle_key_message(
789
- the_slide=slide,
790
- slide_json=slide_json,
791
- slide_height_inch=slide_height_inch,
792
- slide_width_inch=slide_width_inch
793
- )
794
-
795
- return True
796
-
797
- return False
798
-
799
-
800
- def _handle_step_by_step_process(
801
- presentation: pptx.Presentation,
802
- slide_json: dict,
803
- slide_width_inch: float,
804
- slide_height_inch: float
805
- ) -> bool:
806
- """
807
- Add shapes to display a step-by-step process in the slide, if available.
808
-
809
- :param presentation: The presentation object.
810
- :param slide_json: The content of the slide as JSON data.
811
- :param slide_width_inch: The width of the slide in inches.
812
- :param slide_height_inch: The height of the slide in inches.
813
- :return True if this slide has a step-by-step process depiction added; False otherwise.
814
- """
815
-
816
- if 'bullet_points' in slide_json and slide_json['bullet_points']:
817
- steps = slide_json['bullet_points']
818
-
819
- no_marker_count = 0.0
820
- n_steps = len(steps)
821
-
822
- # Ensure that it is a single list of strings without any sub-list
823
- for step in steps:
824
- if not isinstance(step, str):
825
- return False
826
-
827
- # In some cases, one or two steps may not begin with >>, e.g.:
828
- # {
829
- # "heading": "Step-by-Step Process: Creating a Legacy",
830
- # "bullet_points": [
831
- # "Identify your unique talents and passions",
832
- # ">> Develop your skills and knowledge",
833
- # ">> Create meaningful work",
834
- # ">> Share your work with the world",
835
- # ">> Continuously learn and adapt"
836
- # ],
837
- # "key_message": ""
838
- # },
839
- #
840
- # Use a threshold, e.g., at most 20%
841
- if not step.startswith(STEP_BY_STEP_PROCESS_MARKER):
842
- no_marker_count += 1
843
-
844
- slide_header = slide_json['heading'].lower()
845
- if (no_marker_count / n_steps > 0.25) and not (
846
- ('step-by-step' in slide_header) or ('step by step' in slide_header)
847
- ):
848
- return False
849
-
850
- if n_steps < 3 or n_steps > 6:
851
- # Two steps -- probably not a process
852
- # More than 5--6 steps -- would likely cause a visual clutter
853
- return False
854
-
855
- bullet_slide_layout = presentation.slide_layouts[1]
856
- slide = presentation.slides.add_slide(bullet_slide_layout)
857
- shapes = slide.shapes
858
- shapes.title.text = remove_slide_number_from_heading(slide_json['heading'])
859
-
860
- if 3 <= n_steps <= 4:
861
- # Horizontal display
862
- height = INCHES_1_5
863
- width = pptx.util.Inches(slide_width_inch / n_steps - 0.01)
864
- top = pptx.util.Inches(slide_height_inch / 2)
865
- left = pptx.util.Inches((slide_width_inch - width.inches * n_steps) / 2 + 0.05)
866
-
867
- for step in steps:
868
- shape = shapes.add_shape(MSO_AUTO_SHAPE_TYPE.CHEVRON, left, top, width, height)
869
- text_frame = shape.text_frame
870
- text_frame.clear()
871
- paragraph = text_frame.paragraphs[0]
872
- paragraph.alignment = pptx.enum.text.PP_ALIGN.LEFT
873
- format_text(paragraph, step.removeprefix(STEP_BY_STEP_PROCESS_MARKER))
874
- left += width - INCHES_0_4
875
- elif 4 < n_steps <= 6:
876
- # Vertical display
877
- height = pptx.util.Inches(0.65)
878
- top = pptx.util.Inches(slide_height_inch / 4)
879
- left = INCHES_1 # slide_width_inch - width.inches)
880
-
881
- # Find the close to median width, based on the length of each text, to be set
882
- # for the shapes
883
- width = pptx.util.Inches(slide_width_inch * 2 / 3)
884
- lengths = [len(step) for step in steps]
885
- font_size_20pt = pptx.util.Pt(20)
886
- widths = sorted(
887
- [
888
- min(
889
- pptx.util.Inches(font_size_20pt.inches * a_len),
890
- width
891
- ) for a_len in lengths
892
- ]
893
- )
894
- width = widths[len(widths) // 2]
895
-
896
- for step in steps:
897
- shape = shapes.add_shape(MSO_AUTO_SHAPE_TYPE.PENTAGON, left, top, width, height)
898
- text_frame = shape.text_frame
899
- text_frame.clear()
900
- paragraph = text_frame.paragraphs[0]
901
- paragraph.alignment = pptx.enum.text.PP_ALIGN.LEFT
902
- format_text(paragraph, step.removeprefix(STEP_BY_STEP_PROCESS_MARKER))
903
- top += height + INCHES_0_3
904
- left += INCHES_0_5
905
-
906
- return True
907
-
908
-
909
- def _handle_table(
910
- presentation: pptx.Presentation,
911
- slide_json: dict,
912
- slide_width_inch: float,
913
- slide_height_inch: float
914
- ) -> bool:
915
- """
916
- Add a table to a slide, if available.
917
-
918
- :param presentation: The presentation object.
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
- :return True if this slide has a step-by-step process depiction added; False otherwise.
923
- """
924
-
925
- if 'table' not in slide_json or not slide_json['table']:
926
- return False
927
-
928
- headers = slide_json['table'].get('headers', [])
929
- rows = slide_json['table'].get('rows', [])
930
- bullet_slide_layout = presentation.slide_layouts[1]
931
- slide = presentation.slides.add_slide(bullet_slide_layout)
932
- shapes = slide.shapes
933
- shapes.title.text = remove_slide_number_from_heading(slide_json['heading'])
934
- left = slide.placeholders[1].left
935
- top = slide.placeholders[1].top
936
- width = slide.placeholders[1].width
937
- height = slide.placeholders[1].height
938
- table = slide.shapes.add_table(len(rows) + 1, len(headers), left, top, width, height).table
939
-
940
- # Set headers
941
- for col_idx, header_text in enumerate(headers):
942
- table.cell(0, col_idx).text = header_text
943
- table.cell(0, col_idx).text_frame.paragraphs[
944
- 0].font.bold = True # Make header bold
945
-
946
- # Fill in rows
947
- for row_idx, row_data in enumerate(rows, start=1):
948
- for col_idx, cell_text in enumerate(row_data):
949
- table.cell(row_idx, col_idx).text = cell_text
950
-
951
- return True
952
-
953
-
954
- def _handle_key_message(
955
- the_slide: pptx.slide.Slide,
956
- slide_json: dict,
957
- slide_width_inch: float,
958
- slide_height_inch: float
959
- ):
960
- """
961
- Add a shape to display the key message in the slide, if available.
962
-
963
- :param the_slide: The slide to be processed.
964
- :param slide_json: The content of the slide as JSON data.
965
- :param slide_width_inch: The width of the slide in inches.
966
- :param slide_height_inch: The height of the slide in inches.
967
- """
968
-
969
- if 'key_message' in slide_json and slide_json['key_message']:
970
- height = pptx.util.Inches(1.6)
971
- width = pptx.util.Inches(slide_width_inch / 2.3)
972
- top = pptx.util.Inches(slide_height_inch - height.inches - 0.1)
973
- left = pptx.util.Inches((slide_width_inch - width.inches) / 2)
974
- shape = the_slide.shapes.add_shape(
975
- MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
976
- left=left,
977
- top=top,
978
- width=width,
979
- height=height
980
- )
981
- format_text(shape.text_frame.paragraphs[0], slide_json['key_message'])
982
-
983
-
984
- def _get_slide_width_height_inches(presentation: pptx.Presentation) -> Tuple[float, float]:
985
- """
986
- Get the dimensions of a slide in inches.
987
-
988
- :param presentation: The presentation object.
989
- :return: The width and the height.
990
- """
991
-
992
- slide_width_inch = EMU_TO_INCH_SCALING_FACTOR * presentation.slide_width
993
- slide_height_inch = EMU_TO_INCH_SCALING_FACTOR * presentation.slide_height
994
-
995
- return slide_width_inch, slide_height_inch
996
-
997
-
998
- if __name__ == '__main__':
999
- _JSON_DATA = '''
1000
- {
1001
- "title": "AI Applications: Transforming Industries",
1002
- "slides": [
1003
- {
1004
- "heading": "Introduction to AI Applications",
1005
- "bullet_points": [
1006
- "Artificial Intelligence (AI) is *transforming* various industries",
1007
- "AI applications range from simple decision-making tools to complex systems",
1008
- "AI can be categorized into types: Rule-based, Instance-based, and Model-based"
1009
- ],
1010
- "key_message": "AI is a broad field with diverse applications and categories",
1011
- "img_keywords": "AI, transformation, industries, decision-making, categories"
1012
- },
1013
- {
1014
- "heading": "AI in Everyday Life",
1015
- "bullet_points": [
1016
- "**Virtual assistants** like Siri, Alexa, and Google Assistant",
1017
- "**Recommender systems** in Netflix, Amazon, and Spotify",
1018
- "**Fraud detection** in banking and *credit card* transactions"
1019
- ],
1020
- "key_message": "AI is integrated into our daily lives through various services",
1021
- "img_keywords": "virtual assistants, recommender systems, fraud detection"
1022
- },
1023
- {
1024
- "heading": "AI in Healthcare",
1025
- "bullet_points": [
1026
- "Disease diagnosis and prediction using machine learning algorithms",
1027
- "Personalized medicine and drug discovery",
1028
- "AI-powered robotic surgeries and remote patient monitoring"
1029
- ],
1030
- "key_message": "AI is revolutionizing healthcare with improved diagnostics and patient care",
1031
- "img_keywords": "healthcare, disease diagnosis, personalized medicine, robotic surgeries"
1032
- },
1033
- {
1034
- "heading": "AI in Key Industries",
1035
- "bullet_points": [
1036
- {
1037
- "heading": "Retail",
1038
- "bullet_points": [
1039
- "Inventory management and demand forecasting",
1040
- "Customer segmentation and targeted marketing",
1041
- "AI-driven chatbots for customer service"
1042
- ]
1043
- },
1044
- {
1045
- "heading": "Finance",
1046
- "bullet_points": [
1047
- "Credit scoring and risk assessment",
1048
- "Algorithmic trading and portfolio management",
1049
- "AI for detecting money laundering and cyber fraud"
1050
- ]
1051
- }
1052
- ],
1053
- "key_message": "AI is transforming retail and finance with improved operations and decision-making",
1054
- "img_keywords": "retail, finance, inventory management, credit scoring, algorithmic trading"
1055
- },
1056
- {
1057
- "heading": "AI in Education",
1058
- "bullet_points": [
1059
- "Personalized learning paths and adaptive testing",
1060
- "Intelligent tutoring systems for skill development",
1061
- "AI for predicting student performance and dropout rates"
1062
- ],
1063
- "key_message": "AI is personalizing education and improving student outcomes",
1064
- },
1065
- {
1066
- "heading": "Step-by-Step: AI Development Process",
1067
- "bullet_points": [
1068
- ">> **Step 1:** Define the problem and objectives",
1069
- ">> **Step 2:** Collect and preprocess data",
1070
- ">> **Step 3:** Select and train the AI model",
1071
- ">> **Step 4:** Evaluate and optimize the model",
1072
- ">> **Step 5:** Deploy and monitor the AI system"
1073
- ],
1074
- "key_message": "Developing AI involves a structured process from problem definition to deployment",
1075
- "img_keywords": ""
1076
- },
1077
- {
1078
- "heading": "AI Icons: Key Aspects",
1079
- "bullet_points": [
1080
- "[[brain]] Human-like *intelligence* and decision-making",
1081
- "[[robot]] Automation and physical *tasks*",
1082
- "[[]] Data processing and cloud computing",
1083
- "[[lightbulb]] Insights and *predictions*",
1084
- "[[globe2]] Global connectivity and *impact*"
1085
- ],
1086
- "key_message": "AI encompasses various aspects, from human-like intelligence to global impact",
1087
- "img_keywords": "AI aspects, intelligence, automation, data processing, global impact"
1088
- },
1089
- {
1090
- "heading": "AI vs. ML vs. DL: A Tabular Comparison",
1091
- "table": {
1092
- "headers": ["Feature", "AI", "ML", "DL"],
1093
- "rows": [
1094
- ["Definition", "Creating intelligent agents", "Learning from data", "Deep neural networks"],
1095
- ["Approach", "Rule-based, expert systems", "Algorithms, statistical models", "Deep neural networks"],
1096
- ["Data Requirements", "Varies", "Large datasets", "Massive datasets"],
1097
- ["Complexity", "Varies", "Moderate", "High"],
1098
- ["Computational Cost", "Low to Moderate", "Moderate", "High"],
1099
- ["Examples", "Chess, recommendation systems", "Spam filters, image recognition", "Image recognition, NLP"]
1100
- ]
1101
- },
1102
- "key_message": "This table provides a concise comparison of the key features of AI, ML, and DL.",
1103
- "img_keywords": "AI, ML, DL, comparison, table, features"
1104
- },
1105
- {
1106
- "heading": "Conclusion: Embracing AI's Potential",
1107
- "bullet_points": [
1108
- "AI is transforming industries and improving lives",
1109
- "Ethical considerations are crucial for responsible AI development",
1110
- "Invest in AI education and workforce development",
1111
- "Call to action: Explore AI applications and contribute to shaping its future"
1112
- ],
1113
- "key_message": "AI offers *immense potential*, and we must embrace it responsibly",
1114
- "img_keywords": "AI transformation, ethical considerations, AI education, future of AI"
1115
- }
1116
- ]
1117
- }'''
1118
-
1119
- temp = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
1120
- path = pathlib.Path(temp.name)
1121
-
1122
- generate_powerpoint_presentation(
1123
- json5.loads(_JSON_DATA),
1124
- output_file_path=path,
1125
- slides_template='Basic'
1126
- )
1127
- print(f'File path: {path}')
1128
-
1129
- 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)