anurag-deo commited on
Commit
5ca2d95
·
verified ·
1 Parent(s): 9961ce0

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +309 -5
  2. app.py +33 -0
  3. app/__init__.py +0 -0
  4. app/__pycache__/__init__.cpython-310.pyc +0 -0
  5. app/agents/__init__.py +0 -0
  6. app/agents/__pycache__/__init__.cpython-310.pyc +0 -0
  7. app/agents/__pycache__/base_agent.cpython-310.pyc +0 -0
  8. app/agents/__pycache__/blog_generator.cpython-310.pyc +0 -0
  9. app/agents/__pycache__/paper_analyzer.cpython-310.pyc +0 -0
  10. app/agents/__pycache__/poster_generator.cpython-310.pyc +0 -0
  11. app/agents/__pycache__/poster_layout_analyzer.cpython-310.pyc +0 -0
  12. app/agents/__pycache__/presentation_generator.cpython-310.pyc +0 -0
  13. app/agents/__pycache__/presentation_planner.cpython-310.pyc +0 -0
  14. app/agents/__pycache__/presentation_visual_analyzer.cpython-310.pyc +0 -0
  15. app/agents/__pycache__/tikz_diagram_generator.cpython-310.pyc +0 -0
  16. app/agents/__pycache__/tldr_generator.cpython-310.pyc +0 -0
  17. app/agents/base_agent.py +23 -0
  18. app/agents/blog_generator.py +221 -0
  19. app/agents/paper_analyzer.py +87 -0
  20. app/agents/poster_generator.py +268 -0
  21. app/agents/poster_layout_analyzer.py +166 -0
  22. app/agents/presentation_generator.py +337 -0
  23. app/agents/presentation_planner.py +209 -0
  24. app/agents/presentation_visual_analyzer.py +316 -0
  25. app/agents/publisher.py +6 -0
  26. app/agents/tikz_diagram_generator.py +162 -0
  27. app/agents/tldr_generator.py +172 -0
  28. app/config/__init__.py +0 -0
  29. app/config/__pycache__/__init__.cpython-310.pyc +0 -0
  30. app/config/__pycache__/settings.cpython-310.pyc +0 -0
  31. app/config/settings.py +61 -0
  32. app/database/__init__.py +0 -0
  33. app/database/database.py +26 -0
  34. app/database/models.py +48 -0
  35. app/main.py +563 -0
  36. app/models/__init__.py +0 -0
  37. app/models/__pycache__/__init__.cpython-310.pyc +0 -0
  38. app/models/__pycache__/schemas.cpython-310.pyc +0 -0
  39. app/models/schemas.py +101 -0
  40. app/services/__init__.py +0 -0
  41. app/services/__pycache__/__init__.cpython-310.pyc +0 -0
  42. app/services/__pycache__/blog_image_service.cpython-310.pyc +0 -0
  43. app/services/__pycache__/devto_service.cpython-310.pyc +0 -0
  44. app/services/__pycache__/image_service.cpython-310.pyc +0 -0
  45. app/services/__pycache__/llm_service.cpython-310.pyc +0 -0
  46. app/services/__pycache__/pdf_service.cpython-310.pyc +0 -0
  47. app/services/__pycache__/pdf_to_image_service.cpython-310.pyc +0 -0
  48. app/services/__pycache__/presentation_pdf_to_image_service.cpython-310.pyc +0 -0
  49. app/services/blog_image_service.py +256 -0
  50. app/services/devto_service.py +133 -0
README.md CHANGED
@@ -1,12 +1,316 @@
1
  ---
2
  title: ScholarShare
3
- emoji: 🔥
4
- colorFrom: gray
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 5.33.0
8
  app_file: app.py
9
  pinned: false
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: ScholarShare
3
+ emoji: 📚
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 5.32.0
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: AI-powered academic paper analysis and content generation
12
+ suggested_hardware: t4-small
13
+ suggested_storage: small
14
  ---
15
 
16
+ # 🎓 ScholarShare - AI-Powered Research Dissemination Platform
17
+
18
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
19
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
20
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
21
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/charliermarsh/ruff)
22
+
23
+ **ScholarShare** is an innovative platform designed to bridge the gap between complex academic research and broader public understanding. It leverages cutting-edge AI to transform dense research papers into accessible and engaging content formats, including blog posts, social media updates, and conference posters. Our goal is to empower researchers to maximize the impact of their work and foster a more informed society. 🚀
24
+
25
+ ## ✨ Features
26
+
27
+ * 📄 **Multi-Format Paper Ingestion:** Upload PDFs, provide URLs (e.g., arXiv links), or paste raw text.
28
+ * 🧠 **In-Depth AI Analysis:** Extracts key information: title, authors, abstract, methodology, findings, results, conclusion, complexity, and technical terms.
29
+ * 📝 **Automated Blog Generation:** Creates beginner-friendly blog posts from research papers, complete with title, content, tags, and estimated reading time.
30
+ * 📱 **Social Media Content Creation:** Generates platform-specific content (LinkedIn, Twitter, Facebook, Instagram) including text posts and relevant images.
31
+ * 🎨 **Academic Poster Generation:** Produces LaTeX-based conference posters with customizable templates (IEEE, ACM, Nature) and orientations (landscape, portrait).
32
+ * 📊 **Presentation Generation:** Creates Beamer LaTeX presentations with customizable templates (academic, corporate, minimal) and adjustable slide counts (8-20 slides).
33
+ * 🚀 **Direct Publishing (DEV.to):** Seamlessly publish generated blog content to DEV.to as drafts or immediately.
34
+ * 📥 **Downloadable Outputs:** All generated content (analysis summaries, blog posts, LaTeX code, PDFs) can be easily downloaded.
35
+ * 🌐 **User-Friendly Interface:** Built with Gradio for an intuitive and interactive experience.
36
+
37
+ ## 🛠️ Tech Stack
38
+
39
+ * 🐍 **Backend:** Python
40
+ * 🤖 **AI/ML:** Various LLM services (via `app.services.llm_service`)
41
+ * 🖼️ **Web Framework/UI:** Gradio (`gradio`, `gradio-pdf`)
42
+ * 📄 **PDF Processing:** `pdf_service` (details depend on implementation, e.g., PyMuPDF, pdfminer)
43
+ * 📜 **LaTeX Compilation:** (Assumed, for poster generation, e.g., `pdflatex` via `poster_service`)
44
+ * 🔗 **API Integration:** DEV.to API (via `devto_service`)
45
+ * 📦 **Packaging:** Poetry (implied by `pyproject.toml` and `uv.lock`)
46
+
47
+ ## 🌊 Architecture & Workflow
48
+
49
+ ScholarShare processes research papers through a series of AI agents and services to generate various content formats.
50
+
51
+ ```mermaid
52
+ graph TD
53
+ %% Nodes
54
+ A[User Input: PDF/URL/Text] --> B{Paper Processing Service}
55
+ B -- Extracted Content --> C[Paper Analyzer Agent]
56
+ C -- Analysis Data --> D{Core Analysis Object}
57
+
58
+ D --> E[Blog Generator Agent]
59
+ E -- Blog Data --> F[Blog Output: Markdown / DEV.to]
60
+
61
+ D --> G[TLDR / Social Media Agent]
62
+ G -- Social Media Data --> H[Social Media Posts & Images]
63
+
64
+ D --> I[Poster Generator Agent]
65
+ I -- Poster Data & Template --> J[Poster Output: LaTeX / PDF]
66
+
67
+ subgraph "User Interface (Gradio)"
68
+ direction LR
69
+ K[Upload/Input Section] --> A
70
+ F --> L[Blog Display & Publish]
71
+ H --> M[Social Media Display]
72
+ J --> N[Poster Display & Download]
73
+ end
74
+
75
+ %% Node Styles: Better readability on dark background
76
+ style A fill:#ffffff,stroke:#000,stroke-width:2px,color:#000
77
+ style B fill:#d0e1ff,stroke:#000,stroke-width:2px,color:#000
78
+ style C fill:#d0e1ff,stroke:#000,stroke-width:2px,color:#000
79
+ style D fill:#ffff99,stroke:#000,stroke-width:2px,color:#000
80
+ style E fill:#e1f5c4,stroke:#000,stroke-width:2px,color:#000
81
+ style G fill:#e1f5c4,stroke:#000,stroke-width:2px,color:#000
82
+ style I fill:#e1f5c4,stroke:#000,stroke-width:2px,color:#000
83
+ style F fill:#cceeff,stroke:#000,stroke-width:2px,color:#000
84
+ style H fill:#cceeff,stroke:#000,stroke-width:2px,color:#000
85
+ style J fill:#cceeff,stroke:#000,stroke-width:2px,color:#000
86
+ style K fill:#dddddd,stroke:#000,stroke-width:2px,color:#000
87
+ style L fill:#eeeeee,stroke:#000,stroke-width:2px,color:#000
88
+ style M fill:#eeeeee,stroke:#000,stroke-width:2px,color:#000
89
+ style N fill:#eeeeee,stroke:#000,stroke-width:2px,color:#000
90
+
91
+ ```
92
+
93
+ ## 📁 Project Structure
94
+
95
+ A high-level overview of the ScholarShare project directory.
96
+
97
+ ```mermaid
98
+ graph TD
99
+ R[ScholarShare Root]
100
+ R --> F1[scholarshare/]
101
+ R --> F2[1706.03762v7.pdf]
102
+ R --> F3[README.md]
103
+ R --> F4[requirements.txt]
104
+ R --> F5[pyproject.toml]
105
+ R --> F6[Dockerfile]
106
+ R --> F7[docker-compose.yml]
107
+
108
+ F1 --> S1[main.py Gradio App]
109
+ F1 --> S2[app/]
110
+ F1 --> S3[data/]
111
+ F1 --> S4[outputs/]
112
+ F1 --> S5[parsed_pdf_content.txt]
113
+
114
+ S2 --> A1[agents/]
115
+ S2 --> A2[config/]
116
+ S2 --> A3[database/]
117
+ S2 --> A4[models/]
118
+ S2 --> A5[services/]
119
+ S2 --> A6[templates/]
120
+ S2 --> A7[utils/]
121
+
122
+ A1 --> AG1[paper_analyzer.py]
123
+ A1 --> AG2[blog_generator.py]
124
+ A1 --> AG3[tldr_generator.py]
125
+ A1 --> AG4[poster_generator.py]
126
+
127
+ A5 --> SV1[pdf_service.py]
128
+ A5 --> SV2[llm_service.py]
129
+ A5 --> SV3[devto_service.py]
130
+ A5 --> SV4[poster_service.py]
131
+
132
+ %% Light styles for dark mode visibility
133
+ style R fill:#ffffff,stroke:#000,stroke-width:2px,color:#000
134
+ style F1 fill:#e8f0fe,stroke:#000,stroke-width:1.5px,color:#000
135
+ style F2 fill:#f9f9f9,stroke:#000,stroke-width:1px,color:#000
136
+ style F3 fill:#f9f9f9,stroke:#000,stroke-width:1px,color:#000
137
+ style F4 fill:#f9f9f9,stroke:#000,stroke-width:1px,color:#000
138
+ style F5 fill:#f9f9f9,stroke:#000,stroke-width:1px,color:#000
139
+ style F6 fill:#f9f9f9,stroke:#000,stroke-width:1px,color:#000
140
+ style F7 fill:#f9f9f9,stroke:#000,stroke-width:1px,color:#000
141
+ style S1 fill:#f1f8e9,stroke:#000,stroke-width:1px,color:#000
142
+ style S2 fill:#fff3e0,stroke:#000,stroke-width:1px,color:#000
143
+ style S3 fill:#f1f8e9,stroke:#000,stroke-width:1px,color:#000
144
+ style S4 fill:#f1f8e9,stroke:#000,stroke-width:1px,color:#000
145
+ style S5 fill:#f1f8e9,stroke:#000,stroke-width:1px,color:#000
146
+ style A1 fill:#ffe0b2,stroke:#000,stroke-width:0.5px,color:#000
147
+ style A2 fill:#ffe0b2,stroke:#000,stroke-width:0.5px,color:#000
148
+ style A3 fill:#ffe0b2,stroke:#000,stroke-width:0.5px,color:#000
149
+ style A4 fill:#ffe0b2,stroke:#000,stroke-width:0.5px,color:#000
150
+ style A5 fill:#ffe0b2,stroke:#000,stroke-width:0.5px,color:#000
151
+ style A6 fill:#ffe0b2,stroke:#000,stroke-width:0.5px,color:#000
152
+ style A7 fill:#ffe0b2,stroke:#000,stroke-width:0.5px,color:#000
153
+ style AG1 fill:#fff,stroke:#000,stroke-width:0.5px,color:#000
154
+ style AG2 fill:#fff,stroke:#000,stroke-width:0.5px,color:#000
155
+ style AG3 fill:#fff,stroke:#000,stroke-width:0.5px,color:#000
156
+ style AG4 fill:#fff,stroke:#000,stroke-width:0.5px,color:#000
157
+ style SV1 fill:#fff,stroke:#000,stroke-width:0.5px,color:#000
158
+ style SV2 fill:#fff,stroke:#000,stroke-width:0.5px,color:#000
159
+ style SV3 fill:#fff,stroke:#000,stroke-width:0.5px,color:#000
160
+ style SV4 fill:#fff,stroke:#000,stroke-width:0.5px,color:#000
161
+
162
+ ```
163
+
164
+ ## 🚀 Getting Started
165
+
166
+ ### 📋 Prerequisites
167
+
168
+ * Python 3.10+
169
+ * Poetry (for dependency management - recommended) or pip
170
+ * Access to a LaTeX distribution (e.g., TeX Live, MiKTeX) for poster generation.
171
+ * (Optional) Docker 🐳
172
+
173
+ ### ⚙️ Installation
174
+
175
+ 1. **Clone the repository:**
176
+ ```bash
177
+ git clone https://github.com/your-username/ScholarShare.git # Replace with actual repo URL
178
+ cd ScholarShare
179
+ ```
180
+
181
+ 2. **Set up environment variables:**
182
+ Create a `.env` file in the `scholarshare/app/config/` directory or directly in `scholarshare/` if `settings.py` is configured to look there.
183
+ Populate it with necessary API keys and configurations (e.g., `OPENAI_API_KEY`, `DEVTO_API_KEY`).
184
+ Example `scholarshare/app/config/.env` (or `scholarshare/.env`):
185
+ ```env
186
+ OPENAI_API_KEY="your_openai_api_key"
187
+ DEVTO_API_KEY="your_devto_api_key"
188
+ # Other settings from settings.py
189
+ HOST="0.0.0.0"
190
+ PORT=7860
191
+ DEBUG=True
192
+ ```
193
+ *(Ensure `settings.py` loads these, e.g., using `python-dotenv`)*
194
+
195
+ 3. **Install dependencies:**
196
+
197
+ * **Using Poetry (recommended):**
198
+ ```bash
199
+ poetry install
200
+ ```
201
+
202
+ * **Using pip and `requirements.txt`:**
203
+ ```bash
204
+ pip install -r requirements.txt
205
+ ```
206
+ *(Note: `requirements.txt` might need to be generated from `pyproject.toml` if not kept up-to-date: `poetry export -f requirements.txt --output requirements.txt --without-hashes`)*
207
+
208
+ 4. **Ensure output directories exist:**
209
+ The application creates these, but you can pre-create them:
210
+ ```bash
211
+ mkdir -p scholarshare/outputs/posters
212
+ mkdir -p scholarshare/outputs/blogs
213
+ mkdir -p scholarshare/data
214
+ ```
215
+
216
+ ### ▶️ Running the Application
217
+
218
+ * **Using Poetry:**
219
+ ```bash
220
+ cd scholarshare
221
+ poetry run python main.py
222
+ ```
223
+
224
+ * **Using Python directly:**
225
+ ```bash
226
+ cd scholarshare
227
+ python main.py
228
+ ```
229
+
230
+ The application will typically be available at `http://localhost:7860` or `http://0.0.0.0:7860`.
231
+
232
+ ### 🐳 Running with Docker (if `Dockerfile` and `docker-compose.yml` are configured)
233
+
234
+ 1. **Build the Docker image:**
235
+ ```bash
236
+ docker-compose build
237
+ ```
238
+ 2. **Run the container:**
239
+ ```bash
240
+ docker-compose up
241
+ ```
242
+ The application should be accessible as configured in `docker-compose.yml`.
243
+
244
+ ## 📖 Usage
245
+
246
+ 1. **Navigate to the "Paper Input & Analysis" Tab:**
247
+ * **Upload PDF:** Click "Upload PDF Paper" and select your research paper.
248
+ * **Enter URL:** Paste a direct link to a PDF (e.g., an arXiv abstract page URL might work if the service can resolve it to a PDF, or a direct PDF link).
249
+ * **Paste Text:** Copy and paste the raw text content of your paper.
250
+ 2. **Analyze Paper:** Click the "🔍 Analyze Paper" button. Wait for the status to show "✅ Paper processed successfully!". The analysis summary will appear.
251
+ 3. **Generate Blog Content:**
252
+ * Go to the "📝 Blog Generation" tab.
253
+ * Click "✍️ Generate Blog Content". The generated blog post will appear.
254
+ * You can download it as Markdown.
255
+ 4. **Generate Social Media Content:**
256
+ * Go to the "📱 Social Media Content" tab.
257
+ * Click "📱 Generate Social Content". Content for LinkedIn, Twitter, Facebook, and Instagram will be generated, along with associated images if applicable.
258
+ 5. **Generate Poster:**
259
+ * Go to the "🎨 Poster Generation" tab.
260
+ * Select a "Poster Template Style" (e.g., IEEE, ACM).
261
+ * Select "Poster Orientation" (landscape or portrait).
262
+ * Click "🎨 Generate Poster". A PDF preview and LaTeX code will be displayed. You can download both.
263
+ 6. **Generate Presentation:**
264
+ * Go to the "📊 Presentation Generation" tab.
265
+ * Select a "Presentation Template Style" (academic, corporate, minimal).
266
+ * Adjust the "Number of Slides" (8-20 slides).
267
+ * Click "📊 Generate Presentation". A PDF preview and Beamer LaTeX code will be displayed. You can download both.
268
+ 7. **Publish to DEV.to:**
269
+ * Go to the "🚀 Publishing" tab (ensure blog content is generated first).
270
+ * Click "💾 Save as Draft" or "🚀 Publish Now". The status of the publication will be shown.
271
+
272
+ ## 🖼️ Screenshots / Demo
273
+
274
+ *(Placeholder: Add screenshots of the Gradio interface for each tab and feature. A GIF demonstrating the workflow would be excellent here.)*
275
+
276
+ **Example: Paper Input Tab**
277
+ `[Image of Paper Input Tab]`
278
+
279
+ **Example: Blog Generation Tab**
280
+ `[Image of Blog Generation Tab]`
281
+
282
+ **Example: Poster Preview**
283
+ `[Image of Poster Preview]`
284
+
285
+ ## 🤝 Contributing
286
+
287
+ Contributions are welcome! Whether it's bug fixes, feature enhancements, or documentation improvements, please feel free to:
288
+
289
+ 1. **Fork the repository.**
290
+ 2. **Create a new branch:** `git checkout -b feature/your-feature-name` or `bugfix/issue-number`.
291
+ 3. **Make your changes.** Ensure your code follows the project's style guidelines (e.g., run `black .` for formatting).
292
+ 4. **Write tests** for new features or bug fixes if applicable.
293
+ 5. **Commit your changes:** `git commit -m "feat: Describe your feature"` or `fix: Describe your fix`.
294
+ 6. **Push to the branch:** `git push origin feature/your-feature-name`.
295
+ 7. **Open a Pull Request** against the `main` (or `develop`) branch.
296
+
297
+ Please provide a clear description of your changes in the PR.
298
+
299
+ ## 📜 License
300
+
301
+ This project is licensed under the **MIT License**. See the [LICENSE](LICENSE.md) file for details.
302
+ *(Note: You'll need to create a `LICENSE.md` file with the MIT license text if it doesn't exist.)*
303
+
304
+ ## 📞 Contact & Support
305
+
306
+ * **Issues:** If you encounter any bugs or have feature requests, please [open an issue](https://github.com/your-username/ScholarShare/issues) on GitHub. <!-- Replace with actual repo URL -->
307
+ * **Maintainer:** [Your Name/Organization] - [[email protected]] <!-- Update with actual contact -->
308
+
309
+ ## 🙏 Acknowledgements
310
+
311
+ * The [Gradio](https://www.gradio.app/) team for the easy-to-use UI framework.
312
+ * Providers of the LLM services used for content generation.
313
+ * The open-source community for the various libraries and tools that make this project possible.
314
+
315
+ ---
316
+ *This README was generated with assistance from an AI coding agent.*
app.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Entry point for Hugging Face Spaces deployment.
4
+ This file is required by HF Spaces and should be named 'app.py' in the root directory.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Add the current directory to Python path for imports
12
+ sys.path.insert(0, str(Path(__file__).parent))
13
+
14
+ # Import and run the main application
15
+ from main import create_interface
16
+
17
+ if __name__ == "__main__":
18
+ # Create output directories
19
+ Path("outputs/posters").mkdir(parents=True, exist_ok=True)
20
+ Path("outputs/blogs").mkdir(parents=True, exist_ok=True)
21
+ Path("outputs/presentations").mkdir(parents=True, exist_ok=True)
22
+ Path("data").mkdir(parents=True, exist_ok=True)
23
+
24
+ # Create the Gradio interface
25
+ app = create_interface()
26
+
27
+ # Launch with Hugging Face Spaces compatible settings
28
+ app.launch(
29
+ server_name="0.0.0.0",
30
+ server_port=7860, # HF Spaces uses port 7860
31
+ share=False,
32
+ debug=False
33
+ )
app/__init__.py ADDED
File without changes
app/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (156 Bytes). View file
 
app/agents/__init__.py ADDED
File without changes
app/agents/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (163 Bytes). View file
 
app/agents/__pycache__/base_agent.cpython-310.pyc ADDED
Binary file (1.24 kB). View file
 
app/agents/__pycache__/blog_generator.cpython-310.pyc ADDED
Binary file (6.61 kB). View file
 
app/agents/__pycache__/paper_analyzer.cpython-310.pyc ADDED
Binary file (3.55 kB). View file
 
app/agents/__pycache__/poster_generator.cpython-310.pyc ADDED
Binary file (8.59 kB). View file
 
app/agents/__pycache__/poster_layout_analyzer.cpython-310.pyc ADDED
Binary file (5.5 kB). View file
 
app/agents/__pycache__/presentation_generator.cpython-310.pyc ADDED
Binary file (10.3 kB). View file
 
app/agents/__pycache__/presentation_planner.cpython-310.pyc ADDED
Binary file (6.59 kB). View file
 
app/agents/__pycache__/presentation_visual_analyzer.cpython-310.pyc ADDED
Binary file (9.64 kB). View file
 
app/agents/__pycache__/tikz_diagram_generator.cpython-310.pyc ADDED
Binary file (5.99 kB). View file
 
app/agents/__pycache__/tldr_generator.cpython-310.pyc ADDED
Binary file (5.62 kB). View file
 
app/agents/base_agent.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict
3
+
4
+ from app.services.llm_service import llm_service
5
+
6
+
7
+ class BaseAgent(ABC):
8
+ def __init__(self, name: str, model_type: str = "light"):
9
+ self.name = name
10
+ self.model_type = model_type
11
+ self.llm_service = llm_service
12
+
13
+ @abstractmethod
14
+ async def process(self, input_data: Any) -> Dict[str, Any]:
15
+ """Process input and return results"""
16
+
17
+ async def generate_response(self, messages: list, temperature: float = 0.7) -> str:
18
+ """Generate LLM response"""
19
+ return await self.llm_service.generate_completion(
20
+ messages=messages,
21
+ model_type=self.model_type,
22
+ temperature=temperature,
23
+ )
app/agents/blog_generator.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.agents.base_agent import BaseAgent
2
+ from app.models.schemas import BlogContent, PaperAnalysis
3
+ from app.services.blog_image_service import blog_image_service
4
+
5
+
6
+ class BlogGeneratorAgent(BaseAgent):
7
+ def __init__(self):
8
+ super().__init__("BlogGenerator", model_type="light")
9
+
10
+ async def process(self, analysis: PaperAnalysis) -> BlogContent:
11
+ """Generate beginner-friendly blog content from paper analysis"""
12
+ blog_prompt = f"""
13
+ You are an expert computer scientist specializes in the area of machine learning. Transform this research paper analysis into an engaging, highly technical yet beginner-friendly blog post.
14
+
15
+ Paper Analysis:
16
+ Title: {analysis.title}
17
+ Authors: {", ".join(analysis.authors)}
18
+ Abstract: {analysis.abstract}
19
+ Key Findings: {", ".join(analysis.key_findings)}
20
+ Methodology: {analysis.methodology}
21
+ Results: {analysis.results}
22
+ Conclusion: {analysis.conclusion}
23
+ Complexity Level: {analysis.complexity_level}
24
+
25
+ Create a blog post that:
26
+ 1. Use technical language and explain complex concepts in simple terms
27
+ 2. Uses analogies to explain technical terms
28
+ 3. Has an engaging introduction that hooks the reader
29
+ 4. Clearly explains the significance of the research
30
+ 5. Includes practical implications of the findings
31
+ 6. Is optimized for SEO with proper structure and headings
32
+ 7. Must explain the key findings and methodology in great detail.
33
+ 8. Use markdown formatting for code snippets, equations, and figures
34
+
35
+ Structure the blog post with:
36
+ - Catchy title
37
+ - Engaging introduction
38
+ - Main sections with clear headings
39
+ - Conclusion with key takeaways
40
+
41
+ Make it interesting for a general audience while maintaining scientific accuracy.
42
+ Don't include any other information except the blog post content. No additional headers or ending text in the response.
43
+ """
44
+
45
+ messages = [
46
+ {
47
+ "role": "system",
48
+ "content": "You are an expert computer scientist expert in machine learning and artificial intelligence also you are expert in blog writing, who excels at making complex research accessible to everyone.",
49
+ },
50
+ {"role": "user", "content": blog_prompt},
51
+ ]
52
+
53
+ response = await self.generate_response(messages, temperature=0.7)
54
+
55
+ # Extract title, content, and tags
56
+ title = self._extract_title(response)
57
+ content = self._clean_content(response)
58
+ tags = self._extract_tags(response, analysis)
59
+ meta_description = self._generate_meta_description(analysis)
60
+ reading_time = self._calculate_reading_time(content)
61
+
62
+ # Generate and embed images into the blog content
63
+ try:
64
+ print("Generating images for blog post...")
65
+ images = await blog_image_service.generate_blog_images(analysis, content)
66
+ if images:
67
+ print(f"Generated {len(images)} images successfully")
68
+ content = await blog_image_service.embed_images_in_content(
69
+ content, images
70
+ )
71
+ # Update reading time to account for images
72
+ reading_time = (
73
+ self._calculate_reading_time(content) + 1
74
+ ) # Add 1 minute for images
75
+ else:
76
+ print("No images were generated")
77
+ except Exception as e:
78
+ print(f"Failed to generate images for blog: {e}")
79
+ # Continue without images if generation fails
80
+
81
+ # Save the blog content in a file
82
+ with open(f"{title}.md", "w") as f:
83
+ f.write(content)
84
+
85
+ return BlogContent(
86
+ title=title,
87
+ content=content,
88
+ tags=tags,
89
+ meta_description=meta_description,
90
+ reading_time=reading_time,
91
+ )
92
+
93
+ def _extract_title(self, content: str) -> str:
94
+ """Extract title from blog content"""
95
+ lines = content.split("\n")
96
+ for line in lines:
97
+ if line.strip().startswith("#") and not line.strip().startswith("##"):
98
+ return line.strip().replace("#", "").strip()
99
+ return "Research Insights: Latest Findings"
100
+
101
+ def _clean_content(self, content: str) -> str:
102
+ """Clean and format blog content"""
103
+ # Remove title from content if it's duplicated
104
+ lines = content.split("\n")
105
+ cleaned_lines = []
106
+ title_found = False
107
+
108
+ for line in lines:
109
+ if (
110
+ line.strip().startswith("#")
111
+ and not line.strip().startswith("##")
112
+ and not title_found
113
+ ):
114
+ title_found = True
115
+ continue
116
+ cleaned_lines.append(line)
117
+
118
+ return "\n".join(cleaned_lines).strip()
119
+
120
+ def _extract_tags(self, content: str, analysis: PaperAnalysis) -> list:
121
+ """Extract relevant tags for the blog post"""
122
+ base_tags = ["research", "science", "academic"]
123
+
124
+ # Add complexity-based tags
125
+ if analysis.complexity_level == "beginner":
126
+ base_tags.extend(["beginners", "explained"])
127
+ elif analysis.complexity_level == "advanced":
128
+ base_tags.extend(["advanced", "technical"])
129
+
130
+ # Add field-specific tags based on content
131
+ content_lower = content.lower()
132
+ field_tags = {
133
+ "ai": [
134
+ "ai",
135
+ "machinelearning",
136
+ "artificialintelligence",
137
+ "deeplearning",
138
+ "neuralnetworks",
139
+ "automation",
140
+ "computervision",
141
+ "nlp",
142
+ "generativeai",
143
+ ],
144
+ "machine learning": [
145
+ "machinelearning",
146
+ "ml",
147
+ "datascience",
148
+ "supervisedlearning",
149
+ "unsupervisedlearning",
150
+ "reinforcementlearning",
151
+ "featureengineering",
152
+ "modeloptimization",
153
+ ],
154
+ "computer science": [
155
+ "computerscience",
156
+ "programming",
157
+ "technology",
158
+ "softwareengineering",
159
+ "algorithms",
160
+ "datastructures",
161
+ "computationaltheory",
162
+ "systemsdesign",
163
+ ],
164
+ "data science": [
165
+ "datascience",
166
+ "bigdata",
167
+ "datamining",
168
+ "datavisualization",
169
+ "statistics",
170
+ "predictiveanalytics",
171
+ "dataengineering",
172
+ ],
173
+ "cybersecurity": [
174
+ "cybersecurity",
175
+ "infosec",
176
+ "ethicalhacking",
177
+ "cryptography",
178
+ "networksecurity",
179
+ "applicationsecurity",
180
+ "securityengineering",
181
+ ],
182
+ "software development": [
183
+ "softwaredevelopment",
184
+ "coding",
185
+ "devops",
186
+ "agile",
187
+ "testing",
188
+ "debugging",
189
+ "versioncontrol",
190
+ "softwarearchitecture",
191
+ ],
192
+ "cloud computing": [
193
+ "cloudcomputing",
194
+ "aws",
195
+ "azure",
196
+ "googlecloud",
197
+ "serverless",
198
+ "containers",
199
+ "kubernetes",
200
+ "cloudsecurity",
201
+ ],
202
+ }
203
+
204
+ for field, tags in field_tags.items():
205
+ if field in content_lower:
206
+ base_tags.extend(tags[:2])
207
+ break
208
+
209
+ return list(set(base_tags))[:10] # Limit to 10 tags
210
+
211
+ def _generate_meta_description(self, analysis: PaperAnalysis) -> str:
212
+ """Generate SEO meta description"""
213
+ if analysis.key_findings:
214
+ finding = analysis.key_findings[0]
215
+ return f"Discover how {finding[:100]}... Latest research insights explained in simple terms."
216
+ return f"Explore the latest research findings from {analysis.title[:50]}... explained for everyone."
217
+
218
+ def _calculate_reading_time(self, content: str) -> int:
219
+ """Calculate estimated reading time in minutes"""
220
+ word_count = len(content.split())
221
+ return max(1, word_count // 200) # Assuming 200 words per minute
app/agents/paper_analyzer.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+
4
+ from app.agents.base_agent import BaseAgent
5
+ from app.models.schemas import PaperAnalysis, PaperInput
6
+
7
+
8
+ class PaperAnalyzerAgent(BaseAgent):
9
+ def __init__(self):
10
+ super().__init__("PaperAnalyzer", model_type="light")
11
+
12
+ async def process(self, input_data: PaperInput) -> PaperAnalysis:
13
+ """Analyze research paper and extract key information"""
14
+ analysis_prompt = f"""
15
+ You are an expert research paper analyzer. Analyze the following research paper content and extract key information in JSON format.
16
+
17
+ Paper Content:
18
+ {input_data.content}
19
+
20
+ Please provide a detailed analysis in the following JSON structure:
21
+ {{
22
+ "title": "Paper title",
23
+ "authors": ["Author 1", "Author 2"],
24
+ "abstract": "Paper abstract",
25
+ "key_findings": ["Finding 1", "Finding 2", "Finding 3"],
26
+ "methodology": "Detailed description of methodology",
27
+ "results": "Summary of key results",
28
+ "conclusion": "Main conclusions",
29
+ "complexity_level": "beginner/intermediate/advanced",
30
+ "technical_terms": ["term1", "term2"],
31
+ "figures_tables": [
32
+ {{"type": "figure", "description": "Description", "content": "Content if available"}},
33
+ {{"type": "table", "description": "Description", "content": "Content if available"}}
34
+ ]
35
+ }}
36
+
37
+ Focus on extracting the most important and impactful findings. Be thorough but concise.
38
+ """
39
+
40
+ messages = [
41
+ {
42
+ "role": "system",
43
+ "content": "You are an expert research paper analyzer with deep knowledge across multiple academic fields.",
44
+ },
45
+ {"role": "user", "content": analysis_prompt},
46
+ ]
47
+
48
+ response = await self.generate_response(messages, temperature=0.3)
49
+ # print(f"Raw response: {response}") # Debugging line
50
+
51
+ try:
52
+ # Extract JSON from response by removing markdown code blocks
53
+ # Remove ```json at the start and ``` at the end
54
+ cleaned_response = re.sub(r'^```json\s*', '', response, flags=re.MULTILINE)
55
+ cleaned_response = re.sub(r'\s*```$', '', cleaned_response, flags=re.MULTILINE)
56
+ cleaned_response = cleaned_response.strip()
57
+ print(f"Cleaned response: {cleaned_response}") # Debugging line
58
+
59
+ # Extract JSON from cleaned response
60
+ json_match = re.search(r"\{.*\}", cleaned_response, re.DOTALL)
61
+ if json_match:
62
+ analysis_data = json.loads(json_match.group())
63
+ return PaperAnalysis(**analysis_data)
64
+ raise ValueError("No valid JSON found in response")
65
+ except Exception:
66
+ # Fallback parsing
67
+ return self._fallback_parse(response, input_data.content)
68
+
69
+ def _fallback_parse(self, response: str, content: str) -> PaperAnalysis:
70
+ """Fallback parsing if JSON extraction fails"""
71
+ lines = content.split("\n")
72
+ title = next(
73
+ (line.strip() for line in lines if len(line.strip()) > 10), "Untitled Paper"
74
+ )
75
+
76
+ return PaperAnalysis(
77
+ title=title,
78
+ authors=["Unknown Author"],
79
+ abstract=content[:500] + "..." if len(content) > 500 else content,
80
+ key_findings=["Analysis in progress"],
81
+ methodology="To be analyzed",
82
+ results="To be analyzed",
83
+ conclusion="To be analyzed",
84
+ complexity_level="intermediate",
85
+ technical_terms=[],
86
+ figures_tables=[],
87
+ )
app/agents/poster_generator.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import tempfile
4
+
5
+ from app.agents.base_agent import BaseAgent
6
+ from app.agents.poster_layout_analyzer import PosterLayoutAnalyzerAgent
7
+ from app.models.schemas import PaperAnalysis, PosterContent
8
+ from app.services.pdf_to_image_service import pdf_to_image_service
9
+
10
+
11
+ class PosterGeneratorAgent(BaseAgent):
12
+ def __init__(self):
13
+ super().__init__("PosterGenerator", model_type="coding")
14
+ self.template_dir = "app/templates/poster_templates"
15
+ self.layout_analyzer = PosterLayoutAnalyzerAgent()
16
+ self.max_fix_attempts = 2 # Maximum attempts to fix layout issues
17
+
18
+ async def process(
19
+ self,
20
+ analysis: PaperAnalysis,
21
+ template_type: str = "ieee",
22
+ orientation: str = "landscape",
23
+ ) -> PosterContent:
24
+ """Generate academic conference poster"""
25
+ # tikzdocumentation
26
+ tikzdocumentation = ""
27
+ with open(
28
+ os.path.join(self.template_dir, "tikzposter.md"),
29
+ encoding="utf-8",
30
+ ) as f:
31
+ tikzdocumentation = f.read()
32
+ # Generate LaTeX content
33
+ latex_prompt = f"""
34
+ Generate LaTeX code for an academic conference poster using the {template_type} style.
35
+ The poster should be suitable for a {orientation} orientation and include the following sections:
36
+
37
+ Paper Details:
38
+ Title: {analysis.title}
39
+ Authors: {", ".join(analysis.authors)}
40
+ Abstract: {analysis.abstract}
41
+ Methodology: {analysis.methodology}
42
+ Results: {analysis.results}
43
+ Conclusion: {analysis.conclusion}
44
+ Key Findings: {", ".join(analysis.key_findings)}
45
+
46
+ Requirements:
47
+ - Use tikzposter or beamerposter package
48
+ - Professional academic layout
49
+ - Clear sections: Abstract, Methodology, Results, Conclusion
50
+ - Include space for figures/tables
51
+ - Use appropriate fonts and colors for {template_type} style
52
+ - Make it visually appealing and readable
53
+
54
+ Generate complete LaTeX code that can be compiled directly.
55
+ Use the tikzposter package for a modern academic poster design.
56
+ The poster should be structured with clear sections and headings.
57
+ Poster should be aesthetically pleasing with good theme and color choices. Given the documentation below, try to make the poster visually appealing and professional.
58
+ MAKE SURE THAT NONE OF THE SECTIONS ARE EMPTY OR MISSING OR GOES OUT OF THE POSTER.
59
+ For you reference here is the documentation for the tikzposter package:
60
+ {tikzdocumentation}
61
+
62
+ Make sure you give your ouptut just a tex code block starting with ```latex and ending with ```.
63
+ Do not include any other text or explanations.
64
+ Here is an example of a simple poster template:
65
+ ```latex
66
+ # Your code goes here
67
+ ```
68
+
69
+ """
70
+
71
+ messages = [
72
+ {
73
+ "role": "system",
74
+ "content": "You are a LaTeX expert specializing in academic poster design. Generate clean, compilable LaTeX code.",
75
+ },
76
+ {"role": "user", "content": latex_prompt},
77
+ ]
78
+
79
+ latex_code = await self.generate_response(messages, temperature=0.3)
80
+ latex_code = self._clean_latex_code(latex_code)
81
+
82
+ # Compile to PDF and analyze layout
83
+ pdf_path, final_latex_code = await self._compile_and_analyze_poster(
84
+ latex_code,
85
+ analysis.title,
86
+ analysis,
87
+ )
88
+
89
+ return PosterContent(
90
+ template_type=template_type,
91
+ title=analysis.title,
92
+ authors=", ".join(analysis.authors),
93
+ abstract=analysis.abstract,
94
+ methodology=analysis.methodology,
95
+ results=analysis.results,
96
+ conclusion=analysis.conclusion,
97
+ figures=[], # Will be populated if figures are detected
98
+ latex_code=final_latex_code,
99
+ pdf_path=pdf_path,
100
+ )
101
+
102
+ def _clean_latex_code(self, latex_code: str) -> str:
103
+ """Clean and validate LaTeX code"""
104
+ # Remove markdown code block markers if present
105
+ latex_code = latex_code.replace("```latex", "").replace("```", "")
106
+
107
+ # Ensure document structure
108
+ if "\\documentclass" not in latex_code:
109
+ latex_code = "\\documentclass[a0paper,portrait]{tikzposter}\n" + latex_code
110
+
111
+ if "\\begin{document}" not in latex_code:
112
+ latex_code += "\n\\begin{document}\n\\maketitle\n\\end{document}"
113
+
114
+ return latex_code.strip()
115
+
116
+ async def _compile_latex(self, latex_code: str, title: str) -> str:
117
+ """Compile LaTeX to PDF"""
118
+ try:
119
+ with tempfile.TemporaryDirectory() as temp_dir:
120
+ # Create LaTeX file
121
+ tex_file = os.path.join(temp_dir, "poster.tex")
122
+ with open(tex_file, "w", encoding="utf-8") as f:
123
+ f.write(latex_code)
124
+
125
+ # Compile with pdflatex
126
+ result = subprocess.run(
127
+ ["pdflatex", "-interaction=nonstopmode", "poster.tex"],
128
+ cwd=temp_dir,
129
+ capture_output=True,
130
+ text=True,
131
+ check=False,
132
+ )
133
+
134
+ pdf_file = os.path.join(temp_dir, "poster.pdf")
135
+
136
+ if os.path.exists(pdf_file):
137
+ # Move to outputs directory
138
+ output_dir = "outputs/posters"
139
+ os.makedirs(output_dir, exist_ok=True)
140
+
141
+ safe_title = "".join(
142
+ c for c in title if c.isalnum() or c in (" ", "-", "_")
143
+ ).rstrip()
144
+ output_path = os.path.join(output_dir, f"{safe_title}_poster.pdf")
145
+
146
+ import shutil
147
+
148
+ shutil.copy2(pdf_file, output_path)
149
+ return output_path
150
+ # Compilation failed, try with simpler template
151
+ return await self._generate_fallback_poster(latex_code, title)
152
+
153
+ except Exception as e:
154
+ print(f"LaTeX compilation error: {e}")
155
+ return await self._generate_fallback_poster(latex_code, title)
156
+
157
+ async def _generate_fallback_poster(self, latex_code: str, title: str) -> str:
158
+ """Generate a simple fallback poster if LaTeX compilation fails"""
159
+ try:
160
+ # Create a simple HTML poster as fallback
161
+ html_content = f"""
162
+ <!DOCTYPE html>
163
+ <html>
164
+ <head>
165
+ <title>{title}</title>
166
+ <style>
167
+ body {{ font-family: Arial, sans-serif; margin: 20px; }}
168
+ .poster {{ width: 800px; margin: 0 auto; }}
169
+ .title {{ font-size: 24px; font-weight: bold; text-align: center; margin-bottom: 20px; }}
170
+ .section {{ margin: 20px 0; padding: 15px; border: 1px solid #ccc; }}
171
+ .section h3 {{ color: #333; }}
172
+ </style>
173
+ </head>
174
+ <body>
175
+ <div class="poster">
176
+ <div class="title">{title}</div>
177
+ <div class="section">
178
+ <h3>LaTeX Code Generated</h3>
179
+ <pre>{latex_code[:500]}...</pre>
180
+ </div>
181
+ <div class="section">
182
+ <p>Note: PDF compilation failed. LaTeX code is available for manual compilation.</p>
183
+ </div>
184
+ </div>
185
+ </body>
186
+ </html>
187
+ """
188
+
189
+ output_dir = "outputs/posters"
190
+ os.makedirs(output_dir, exist_ok=True)
191
+
192
+ safe_title = "".join(
193
+ c for c in title if c.isalnum() or c in (" ", "-", "_")
194
+ ).rstrip()
195
+ output_path = os.path.join(output_dir, f"{safe_title}_poster.html")
196
+
197
+ with open(output_path, "w", encoding="utf-8") as f:
198
+ f.write(html_content)
199
+
200
+ return output_path
201
+
202
+ except Exception as e:
203
+ print(f"Fallback poster generation error: {e}")
204
+ return "outputs/posters/poster_generation_failed.txt"
205
+
206
+ async def _compile_and_analyze_poster(
207
+ self,
208
+ latex_code: str,
209
+ title: str,
210
+ analysis: PaperAnalysis,
211
+ ) -> tuple[str, str]:
212
+ """
213
+ Compile LaTeX to PDF and analyze layout, fixing issues if needed.
214
+
215
+ Returns:
216
+ tuple: (pdf_path, final_latex_code)
217
+
218
+ """
219
+ current_latex = latex_code
220
+ attempt = 0
221
+
222
+ while attempt <= self.max_fix_attempts:
223
+ # Compile to PDF
224
+ pdf_path = await self._compile_latex(current_latex, title)
225
+
226
+ if not pdf_path:
227
+ print(f"PDF compilation failed on attempt {attempt + 1}")
228
+ break
229
+
230
+ # Convert PDF to image for analysis (optimized resolution for vision model)
231
+ poster_image_path = await pdf_to_image_service.convert_pdf_to_image(
232
+ pdf_path,
233
+ max_width=800, # Optimized for vision model token costs
234
+ )
235
+
236
+ if not poster_image_path:
237
+ print("Could not convert PDF to image for analysis")
238
+ return pdf_path, current_latex
239
+
240
+ # Analyze poster layout
241
+ (
242
+ fits_properly,
243
+ analysis_message,
244
+ fixed_latex,
245
+ ) = await self.layout_analyzer.analyze_poster_layout(
246
+ poster_image_path,
247
+ current_latex,
248
+ title,
249
+ )
250
+
251
+ print(f"Poster layout analysis (attempt {attempt + 1}): {analysis_message}")
252
+
253
+ if fits_properly:
254
+ print("Poster layout is satisfactory!")
255
+ return pdf_path, current_latex
256
+
257
+ if fixed_latex and attempt < self.max_fix_attempts:
258
+ print(f"Applying layout fixes (attempt {attempt + 1})...")
259
+ current_latex = self._clean_latex_code(fixed_latex)
260
+ attempt += 1
261
+ else:
262
+ print(
263
+ "Max fix attempts reached or no fix available. Using current version.",
264
+ )
265
+ return pdf_path, current_latex
266
+
267
+ # If we reach here, return the last attempt
268
+ return pdf_path, current_latex
app/agents/poster_layout_analyzer.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ from typing import Optional, Tuple
4
+
5
+ from app.agents.base_agent import BaseAgent
6
+
7
+
8
+ class PosterLayoutAnalyzerAgent(BaseAgent):
9
+ """Agent to analyze poster layout and provide fixes for content that doesn't fit properly"""
10
+
11
+ def __init__(self):
12
+ super().__init__("PosterLayoutAnalyzer", model_type="coding")
13
+
14
+ async def analyze_poster_layout(
15
+ self,
16
+ poster_image_path: str,
17
+ latex_code: str,
18
+ paper_title: str,
19
+ ) -> Tuple[bool, str, Optional[str]]:
20
+ """
21
+ Analyze poster layout from image and provide fixes if content doesn't fit
22
+
23
+ Returns:
24
+ - bool: Whether the poster fits properly
25
+ - str: Analysis message
26
+ - Optional[str]: Fixed LaTeX code if needed
27
+
28
+ """
29
+ try:
30
+ # Convert image to base64 for vision model
31
+ image_base64 = await self._image_to_base64(poster_image_path)
32
+
33
+ if not image_base64:
34
+ return False, "Could not process poster image", None
35
+
36
+ # Create analysis prompt
37
+ analysis_prompt = f"""
38
+ You are an expert in academic poster design and LaTeX formatting.
39
+ Analyze this generated poster image and determine if all content fits properly within the page boundaries.
40
+
41
+ Paper Title: {paper_title}
42
+
43
+ Look for the following issues:
44
+ 1. Text or content that extends beyond page margins
45
+ 2. Overlapping text or sections
46
+ 3. Cut-off content at the bottom or sides
47
+ 4. Sections that are too cramped or unreadable
48
+ 5. Any content that appears to be missing or truncated
49
+
50
+ Based on your analysis, provide:
51
+ 1. A clear assessment of whether the poster fits properly (YES/NO)
52
+ 2. Specific issues you identify (if any)
53
+ 3. Recommendations for fixing layout issues
54
+
55
+ Be detailed and specific about what you observe in the image.
56
+ """
57
+
58
+ messages = [
59
+ {
60
+ "role": "system",
61
+ "content": "You are an expert academic poster design analyst. Analyze poster layouts for proper formatting and content fit.",
62
+ },
63
+ {
64
+ "role": "user",
65
+ "content": [
66
+ {
67
+ "type": "text",
68
+ "text": analysis_prompt,
69
+ },
70
+ {
71
+ "type": "image_url",
72
+ "image_url": {
73
+ "url": f"data:image/png;base64,{image_base64}",
74
+ },
75
+ },
76
+ ],
77
+ },
78
+ ]
79
+
80
+ analysis_response = await self.generate_response(messages, temperature=0.3)
81
+
82
+ # Check if poster needs fixing
83
+ needs_fixing = (
84
+ "NO" in analysis_response.upper()
85
+ and "fits properly" in analysis_response
86
+ )
87
+ fits_properly = not needs_fixing
88
+
89
+ if needs_fixing:
90
+ # Generate fixed LaTeX code
91
+ fixed_latex = await self._generate_fixed_latex(
92
+ latex_code, analysis_response
93
+ )
94
+ return fits_properly, analysis_response, fixed_latex
95
+ return fits_properly, analysis_response, None
96
+
97
+ except Exception as e:
98
+ return False, f"Error analyzing poster layout: {e!s}", None
99
+
100
+ async def _image_to_base64(self, image_path: str) -> Optional[str]:
101
+ """Convert image to base64 string"""
102
+ try:
103
+ if not os.path.exists(image_path):
104
+ print(f"Image file not found: {image_path}")
105
+ return None
106
+
107
+ with open(image_path, "rb") as image_file:
108
+ image_data = image_file.read()
109
+ base64_string = base64.b64encode(image_data).decode("utf-8")
110
+ return base64_string
111
+ except Exception as e:
112
+ print(f"Error converting image to base64: {e}")
113
+ return None
114
+
115
+ async def _generate_fixed_latex(self, original_latex: str, analysis: str) -> str:
116
+ """Generate fixed LaTeX code based on analysis"""
117
+ fix_prompt = f"""
118
+ Based on the poster layout analysis below, fix the LaTeX code to ensure all content fits properly within the page.
119
+
120
+ Analysis of issues found:
121
+ {analysis}
122
+
123
+ Original LaTeX Code:
124
+ {original_latex}
125
+
126
+ Please provide a corrected version of the LaTeX code that addresses the layout issues.
127
+ Focus on:
128
+ 1. Reducing font sizes if text is too large
129
+ 2. Adjusting section spacing and margins
130
+ 3. Making content more concise if needed
131
+ 4. Optimizing layout structure
132
+ 5. Ensuring proper tikzposter block sizing and positioning
133
+ 6. Using appropriate column layouts
134
+ 7. Adjusting text wrapping and line spacing
135
+
136
+ Provide ONLY the corrected LaTeX code starting with \\documentclass and ending with \\end{{document}}.
137
+ Do not include any explanations or markdown formatting.
138
+ MAKE SURE THE ENTIRE POSTER CONTENT FITS PROPERLY ON THE PAGE.
139
+ """
140
+
141
+ messages = [
142
+ {
143
+ "role": "system",
144
+ "content": "You are a LaTeX expert specializing in fixing academic poster layouts. Generate clean, compilable LaTeX code that fits properly on the page.",
145
+ },
146
+ {
147
+ "role": "user",
148
+ "content": fix_prompt,
149
+ },
150
+ ]
151
+
152
+ fixed_latex = await self.generate_response(messages, temperature=0.2)
153
+
154
+ # Clean the response
155
+ fixed_latex = fixed_latex.replace("```latex", "").replace("```", "").strip()
156
+
157
+ return fixed_latex
158
+
159
+ async def process(self, input_data):
160
+ """Implementation of abstract method from BaseAgent"""
161
+ # This method can be used for batch processing if needed
162
+ return await self.analyze_poster_layout(
163
+ input_data.get("image_path"),
164
+ input_data.get("latex_code"),
165
+ input_data.get("title"),
166
+ )
app/agents/presentation_generator.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import tempfile
3
+ from pathlib import Path
4
+
5
+ from app.agents.base_agent import BaseAgent
6
+ from app.agents.presentation_planner import PresentationPlannerAgent
7
+ from app.agents.presentation_visual_analyzer import PresentationVisualAnalyzerAgent
8
+ from app.agents.tikz_diagram_generator import TikzDiagramAgent
9
+ from app.models.schemas import PaperAnalysis, PresentationContent, PresentationPlan
10
+ from app.services.presentation_pdf_to_image_service import (
11
+ presentation_pdf_to_image_service,
12
+ )
13
+
14
+
15
+ class PresentationGeneratorAgent(BaseAgent):
16
+ """Main agent for generating complete research presentations using Beamer"""
17
+
18
+ def __init__(self):
19
+ super().__init__("PresentationGenerator", model_type="coding")
20
+ self.template_dir = "app/templates/presentation_templates"
21
+ self.planner = PresentationPlannerAgent()
22
+ self.diagram_generator = TikzDiagramAgent()
23
+ self.visual_analyzer = PresentationVisualAnalyzerAgent()
24
+ self.max_iterations = 1
25
+
26
+ async def process(
27
+ self,
28
+ analysis: PaperAnalysis,
29
+ template_type: str = "academic",
30
+ max_slides: int = 15,
31
+ ) -> PresentationContent:
32
+ """Generate complete presentation from paper analysis"""
33
+ try:
34
+ # Step 1: Plan the presentation structure
35
+ print("Planning presentation structure...")
36
+ plan = await self.planner.process(analysis, max_slides)
37
+
38
+ # Step 2: Generate TikZ diagrams if needed
39
+ print("Generating diagrams...")
40
+ diagrams = []
41
+ if plan.suggested_diagrams:
42
+ diagrams = await self.diagram_generator.process(
43
+ plan.suggested_diagrams,
44
+ analysis,
45
+ )
46
+
47
+ # Step 3: Generate Beamer LaTeX code
48
+ print("Generating Beamer presentation...")
49
+ latex_code = await self._generate_beamer_presentation(
50
+ analysis,
51
+ plan,
52
+ diagrams,
53
+ template_type,
54
+ )
55
+
56
+ # Step 4: Compile and analyze presentation
57
+ print("Compiling and analyzing presentation...")
58
+ pdf_path, final_latex = await self._compile_and_analyze_presentation(
59
+ latex_code,
60
+ analysis.title,
61
+ plan,
62
+ )
63
+
64
+ return PresentationContent(
65
+ title=analysis.title,
66
+ authors=", ".join(analysis.authors),
67
+ institution=None, # Could be extracted from paper if available
68
+ date=None,
69
+ template_type=template_type,
70
+ slides=plan.slides,
71
+ tikz_diagrams=diagrams,
72
+ latex_code=final_latex,
73
+ pdf_path=pdf_path,
74
+ total_slides=plan.total_slides,
75
+ )
76
+
77
+ except Exception as e:
78
+ print(f"Error generating presentation: {e}")
79
+ raise
80
+
81
+ async def _generate_beamer_presentation(
82
+ self,
83
+ analysis: PaperAnalysis,
84
+ plan: PresentationPlan,
85
+ diagrams: list,
86
+ template_type: str,
87
+ ) -> str:
88
+ """Generate complete Beamer LaTeX code"""
89
+ # Create diagram code mapping
90
+ diagram_codes = {d.diagram_id: d.tikz_code for d in diagrams}
91
+
92
+ beamer_prompt = f"""
93
+ Generate a complete Beamer LaTeX presentation based on this research paper analysis and slide plan.
94
+
95
+ Paper Information:
96
+ Title: {analysis.title}
97
+ Authors: {", ".join(analysis.authors)}
98
+ Abstract: {analysis.abstract}
99
+
100
+ Presentation Plan:
101
+ Total Slides: {plan.total_slides}
102
+ Style: {plan.presentation_style}
103
+ Template: {template_type}
104
+
105
+ Slide Details:
106
+ {self._format_slide_plan(plan.slides)}
107
+
108
+ Available TikZ Diagrams:
109
+ {self._format_diagram_info(diagrams)}
110
+
111
+ Requirements:
112
+ 1. Use Beamer document class with appropriate theme
113
+ 2. Include all necessary packages (tikz, graphicx, etc.)
114
+ 3. Create professional academic presentation
115
+ 4. Use consistent formatting and styling
116
+ 5. Include proper section breaks and navigation
117
+ 6. Integrate TikZ diagrams where appropriate
118
+ 7. Use appropriate font sizes and spacing
119
+ 8. Follow Beamer best practices
120
+ 9. Choose the color scheme such that all the text are readable and visually appealing
121
+ 10. If there are too many content on a slide, split it into multiple slides
122
+ 11. Ensure all diagrams are properly labeled and referenced in the text
123
+ 12. No content should go out of the slide boundaries
124
+
125
+ Template Style Guidelines:
126
+ - Academic: Clean, professional, blue color scheme
127
+ - Corporate: Modern, sleek, with company-like styling
128
+ - Minimal: Simple, clean, minimal visual elements
129
+
130
+ Generate complete LaTeX code starting with \\documentclass{{beamer}} and ending with \\end{{document}}.
131
+ Include all slide content, proper formatting, and TikZ diagrams.
132
+ Make sure each slide is well-designed and informative.
133
+ """
134
+
135
+ messages = [
136
+ {
137
+ "role": "system",
138
+ "content": "You are a Beamer LaTeX expert specializing in creating professional academic presentations. Generate clean, compilable code.",
139
+ },
140
+ {"role": "user", "content": beamer_prompt},
141
+ ]
142
+
143
+ latex_code = await self.generate_response(messages, temperature=0.3)
144
+ return self._clean_latex_code(latex_code)
145
+
146
+ def _format_slide_plan(self, slides) -> str:
147
+ """Format slide plan for the prompt"""
148
+ formatted = ""
149
+ for slide in slides:
150
+ formatted += f"""
151
+ Slide {slide.slide_number}: {slide.title}
152
+ Type: {slide.slide_type}
153
+ Content: {slide.content}
154
+ TikZ Diagrams: {", ".join(slide.tikz_diagrams) if slide.tikz_diagrams else "None"}
155
+ Notes: {slide.notes or "None"}
156
+ ---"""
157
+ return formatted
158
+
159
+ def _format_diagram_info(self, diagrams) -> str:
160
+ """Format diagram information for the prompt"""
161
+ if not diagrams:
162
+ return "No diagrams available"
163
+
164
+ formatted = ""
165
+ for diagram in diagrams:
166
+ formatted += f"""
167
+ {diagram.diagram_id}: {diagram.title}
168
+ Description: {diagram.description}
169
+ Type: {diagram.diagram_type}
170
+ ---"""
171
+ return formatted
172
+
173
+ def _clean_latex_code(self, latex_code: str) -> str:
174
+ """Clean and validate Beamer LaTeX code"""
175
+ # Remove markdown code blocks if present
176
+ latex_code = latex_code.replace("```latex", "").replace("```", "")
177
+
178
+ # Ensure document structure
179
+ if "\\documentclass" not in latex_code:
180
+ latex_code = "\\documentclass{beamer}\n" + latex_code
181
+
182
+ if "\\begin{document}" not in latex_code:
183
+ latex_code += "\n\\begin{document}\n\\end{document}"
184
+
185
+ return latex_code.strip()
186
+
187
+ async def _compile_latex(self, latex_code: str, title: str) -> str:
188
+ """Compile Beamer LaTeX to PDF"""
189
+ try:
190
+ with tempfile.TemporaryDirectory() as temp_dir:
191
+ # Clean title for filename
192
+ safe_title = "".join(
193
+ c for c in title if c.isalnum() or c in (" ", "-", "_")
194
+ ).rstrip()
195
+ safe_title = safe_title.replace(" ", "_")[:50]
196
+
197
+ # Write LaTeX file
198
+ tex_file = Path(temp_dir) / f"{safe_title}_presentation.tex"
199
+ tex_file.write_text(latex_code, encoding="utf-8")
200
+
201
+ # Compile with pdflatex (multiple passes for references)
202
+ for _ in range(2):
203
+ result = subprocess.run(
204
+ ["pdflatex", "-interaction=nonstopmode", str(tex_file)],
205
+ cwd=temp_dir,
206
+ capture_output=True,
207
+ text=True,
208
+ timeout=60,
209
+ check=False,
210
+ )
211
+
212
+ pdf_file = tex_file.with_suffix(".pdf")
213
+ if pdf_file.exists():
214
+ # Copy to outputs directory
215
+ output_path = Path("outputs/presentations")
216
+ output_path.mkdir(exist_ok=True)
217
+
218
+ final_pdf = output_path / f"{safe_title}_presentation.pdf"
219
+ final_pdf.write_bytes(pdf_file.read_bytes())
220
+
221
+ return str(final_pdf)
222
+ print(f"PDF compilation failed. LaTeX output: {result.stdout}")
223
+ print(f"LaTeX errors: {result.stderr}")
224
+ return None
225
+
226
+ except Exception as e:
227
+ print(f"Error compiling presentation: {e}")
228
+ return None
229
+
230
+ async def _compile_and_analyze_presentation(
231
+ self,
232
+ latex_code: str,
233
+ title: str,
234
+ plan: PresentationPlan,
235
+ ) -> tuple[str, str]:
236
+ """Compile presentation and analyze visual quality"""
237
+ current_latex = latex_code
238
+ iteration = 0
239
+
240
+ while iteration < self.max_iterations:
241
+ # Compile to PDF
242
+ pdf_path = await self._compile_latex(current_latex, title)
243
+
244
+ if not pdf_path:
245
+ print(f"PDF compilation failed on iteration {iteration + 1}")
246
+ break
247
+
248
+ # Analyze at least half of the slides for comprehensive quality assessment
249
+ print(f"Analyzing presentation quality (iteration {iteration + 1})...")
250
+
251
+ # Calculate pages to analyze (at least half, minimum 1)
252
+ pages_to_analyze = max(1, plan.total_slides // 2)
253
+ if pages_to_analyze < plan.total_slides // 2:
254
+ pages_to_analyze = plan.total_slides // 2 + 1
255
+
256
+ print(f"Analyzing {pages_to_analyze} out of {plan.total_slides} slides...")
257
+
258
+ # Analyze multiple slides for comprehensive assessment
259
+ all_analyses = []
260
+ improvement_suggestions = []
261
+ slides_analyzed = 0
262
+ quality_threshold = (
263
+ 0.7 # Consider presentation good if 70% of slides are good
264
+ )
265
+
266
+ for page_num in range(1, min(pages_to_analyze + 1, plan.total_slides + 1)):
267
+ slide_image_path = (
268
+ await presentation_pdf_to_image_service.convert_pdf_to_image(
269
+ pdf_path,
270
+ page_number=page_num,
271
+ max_width=1024,
272
+ )
273
+ )
274
+
275
+ if slide_image_path:
276
+ (
277
+ is_good,
278
+ analysis,
279
+ improved_latex,
280
+ ) = await self.visual_analyzer.analyze_presentation_layout(
281
+ slide_image_path,
282
+ current_latex,
283
+ page_num,
284
+ plan.total_slides,
285
+ )
286
+
287
+ all_analyses.append((page_num, is_good, analysis))
288
+ if improved_latex and not is_good:
289
+ improvement_suggestions.append(improved_latex)
290
+
291
+ slides_analyzed += 1
292
+ print(
293
+ f"Slide {page_num}: {'✅ Good' if is_good else '❌ Needs improvement'}"
294
+ )
295
+ else:
296
+ print(f"Could not generate image for slide {page_num}")
297
+
298
+ if slides_analyzed == 0:
299
+ print("Could not analyze any slides - using current version")
300
+ return pdf_path, current_latex
301
+
302
+ # Determine overall quality based on analyzed slides
303
+ good_slides = sum(1 for _, is_good, _ in all_analyses if is_good)
304
+ quality_ratio = good_slides / slides_analyzed
305
+
306
+ print(
307
+ f"Quality assessment: {good_slides}/{slides_analyzed} slides are good ({quality_ratio:.1%})"
308
+ )
309
+
310
+ # Consider presentation good if quality meets threshold
311
+ if quality_ratio >= quality_threshold:
312
+ print("Overall presentation quality is satisfactory!")
313
+ return pdf_path, current_latex
314
+
315
+ # Apply improvements if available and not at max iterations
316
+ if improvement_suggestions and iteration < self.max_iterations - 1:
317
+ print(f"Applying visual improvements (iteration {iteration + 1})...")
318
+ # Use the most recent improvement suggestion
319
+ current_latex = self._clean_latex_code(improvement_suggestions[-1])
320
+ iteration += 1
321
+ else:
322
+ print("Max iterations reached or no improvements available.")
323
+ return pdf_path, current_latex
324
+
325
+ return pdf_path, current_latex
326
+
327
+ async def create_template_variants(self, analysis: PaperAnalysis) -> dict:
328
+ """Create presentations with different templates for comparison"""
329
+ templates = ["academic", "corporate", "minimal"]
330
+ presentations = {}
331
+
332
+ for template in templates:
333
+ print(f"Generating {template} template presentation...")
334
+ presentation = await self.process(analysis, template_type=template)
335
+ presentations[template] = presentation
336
+
337
+ return presentations
app/agents/presentation_planner.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.agents.base_agent import BaseAgent
2
+ from app.models.schemas import PaperAnalysis, PresentationPlan, SlideContent
3
+
4
+
5
+ class PresentationPlannerAgent(BaseAgent):
6
+ """Agent to plan presentation structure and content based on paper analysis"""
7
+
8
+ def __init__(self):
9
+ super().__init__("PresentationPlanner", model_type="heavy")
10
+
11
+ async def process(
12
+ self, analysis: PaperAnalysis, max_slides: int = 15
13
+ ) -> PresentationPlan:
14
+ """Create a detailed plan for the presentation slides"""
15
+ planning_prompt = f"""
16
+ You are an expert presentation planner specializing in academic research presentations.
17
+ Create a detailed slide-by-slide plan for a {max_slides}-slide presentation based on this research paper analysis.
18
+
19
+ Paper Analysis:
20
+ Title: {analysis.title}
21
+ Authors: {", ".join(analysis.authors)}
22
+ Abstract: {analysis.abstract}
23
+ Key Findings: {", ".join(analysis.key_findings)}
24
+ Methodology: {analysis.methodology}
25
+ Results: {analysis.results}
26
+ Conclusion: {analysis.conclusion}
27
+ Complexity Level: {analysis.complexity_level}
28
+ Technical Terms: {", ".join(analysis.technical_terms)}
29
+
30
+ Create a presentation plan with the following structure:
31
+ 1. Title slide
32
+ 2. Agenda/Outline
33
+ 3. Introduction/Background (1-2 slides)
34
+ 4. Problem Statement/Motivation (1 slide)
35
+ 5. Methodology (2-3 slides)
36
+ 6. Results (3-4 slides)
37
+ 7. Key Findings (1-2 slides)
38
+ 8. Implications/Discussion (1-2 slides)
39
+ 9. Conclusion (1 slide)
40
+ 10. Future Work/Q&A (1 slide)
41
+
42
+ For each slide, provide:
43
+ - Slide number
44
+ - Title
45
+ - Detailed content description (what should be on the slide)
46
+ - Slide type (title, content, image, diagram, conclusion)
47
+ - Any speaker notes
48
+ - Suggestions for tikz diagrams if applicable
49
+
50
+ Also suggest specific diagrams that would enhance the presentation:
51
+ - Flowcharts for methodology
52
+ - Architecture diagrams for systems
53
+ - Comparison charts for results
54
+ - Timeline diagrams for processes
55
+ - Graphs for statistical data
56
+
57
+ Try to put the content in the slides in a way that is engaging and informative, while also being concise.
58
+ Don't put too much text on each slide; focus on key points and visuals.
59
+ Understand that the slides are small and too much text will overflow from the slides making them look cluttered.
60
+
61
+
62
+ Respond in this JSON format:
63
+ {{
64
+ "total_slides": {max_slides},
65
+ "slides": [
66
+ {{
67
+ "slide_number": 1,
68
+ "title": "Slide Title",
69
+ "content": "Detailed content description",
70
+ "slide_type": "title|content|image|diagram|conclusion",
71
+ "notes": "Speaker notes or additional information",
72
+ "tikz_diagrams": ["description of tikz diagram if needed"]
73
+ }}
74
+ ],
75
+ "suggested_diagrams": ["List of diagrams to create"],
76
+ "presentation_style": "academic"
77
+ }}
78
+
79
+ Make the presentation engaging, well-structured, and appropriate for an academic audience.
80
+ Ensure content flows logically and each slide has a clear purpose.
81
+ """
82
+
83
+ messages = [
84
+ {
85
+ "role": "system",
86
+ "content": "You are an expert academic presentation planner with deep knowledge of effective presentation design and research communication.",
87
+ },
88
+ {"role": "user", "content": planning_prompt},
89
+ ]
90
+
91
+ response = await self.generate_response(messages, temperature=0.4)
92
+
93
+ try:
94
+ # Clean and parse JSON response
95
+ import json
96
+ import re
97
+
98
+ # Remove markdown code blocks if present
99
+ cleaned_response = re.sub(r"^```json\s*", "", response, flags=re.MULTILINE)
100
+ cleaned_response = re.sub(
101
+ r"\s*```$", "", cleaned_response, flags=re.MULTILINE
102
+ )
103
+ cleaned_response = cleaned_response.strip()
104
+
105
+ # Extract JSON from cleaned response
106
+ json_match = re.search(r"\{.*\}", cleaned_response, re.DOTALL)
107
+ if json_match:
108
+ plan_data = json.loads(json_match.group())
109
+
110
+ # Convert slides data to SlideContent objects
111
+ slides = []
112
+ for slide_data in plan_data.get("slides", []):
113
+ slides.append(SlideContent(**slide_data))
114
+
115
+ return PresentationPlan(
116
+ total_slides=plan_data.get("total_slides", max_slides),
117
+ slides=slides,
118
+ suggested_diagrams=plan_data.get("suggested_diagrams", []),
119
+ presentation_style=plan_data.get("presentation_style", "academic"),
120
+ )
121
+ raise ValueError("No valid JSON found in response")
122
+
123
+ except Exception as e:
124
+ print(f"Error parsing presentation plan: {e}")
125
+ # Fallback plan
126
+ return self._create_fallback_plan(analysis, max_slides)
127
+
128
+ def _create_fallback_plan(
129
+ self, analysis: PaperAnalysis, max_slides: int
130
+ ) -> PresentationPlan:
131
+ """Create a basic fallback presentation plan"""
132
+ slides = [
133
+ SlideContent(
134
+ slide_number=1,
135
+ title=analysis.title,
136
+ content=f"Authors: {', '.join(analysis.authors)}",
137
+ slide_type="title",
138
+ notes="Title slide with paper title and authors",
139
+ ),
140
+ SlideContent(
141
+ slide_number=2,
142
+ title="Agenda",
143
+ content="Presentation outline: Introduction, Methodology, Results, Conclusion",
144
+ slide_type="content",
145
+ notes="Overview of presentation structure",
146
+ ),
147
+ SlideContent(
148
+ slide_number=3,
149
+ title="Introduction",
150
+ content=analysis.abstract[:300] + "..."
151
+ if len(analysis.abstract) > 300
152
+ else analysis.abstract,
153
+ slide_type="content",
154
+ notes="Introduction based on paper abstract",
155
+ ),
156
+ SlideContent(
157
+ slide_number=4,
158
+ title="Methodology",
159
+ content=analysis.methodology[:400] + "..."
160
+ if len(analysis.methodology) > 400
161
+ else analysis.methodology,
162
+ slide_type="content",
163
+ notes="Research methodology and approach",
164
+ ),
165
+ SlideContent(
166
+ slide_number=5,
167
+ title="Key Findings",
168
+ content="\n".join(
169
+ [f"• {finding}" for finding in analysis.key_findings[:3]]
170
+ ),
171
+ slide_type="content",
172
+ notes="Main research findings",
173
+ ),
174
+ SlideContent(
175
+ slide_number=6,
176
+ title="Results",
177
+ content=analysis.results[:400] + "..."
178
+ if len(analysis.results) > 400
179
+ else analysis.results,
180
+ slide_type="content",
181
+ notes="Research results and outcomes",
182
+ ),
183
+ SlideContent(
184
+ slide_number=7,
185
+ title="Conclusion",
186
+ content=analysis.conclusion[:300] + "..."
187
+ if len(analysis.conclusion) > 300
188
+ else analysis.conclusion,
189
+ slide_type="conclusion",
190
+ notes="Conclusions and implications",
191
+ ),
192
+ SlideContent(
193
+ slide_number=8,
194
+ title="Thank You",
195
+ content="Questions & Discussion",
196
+ slide_type="conclusion",
197
+ notes="Q&A slide",
198
+ ),
199
+ ]
200
+
201
+ return PresentationPlan(
202
+ total_slides=len(slides),
203
+ slides=slides,
204
+ suggested_diagrams=[
205
+ "Research methodology flowchart",
206
+ "Results comparison chart",
207
+ ],
208
+ presentation_style="academic",
209
+ )
app/agents/presentation_visual_analyzer.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ from typing import Optional, Tuple
4
+
5
+ from app.agents.base_agent import BaseAgent
6
+
7
+
8
+ class PresentationVisualAnalyzerAgent(BaseAgent):
9
+ """Agent to analyze presentation layout and visual elements for quality assurance"""
10
+
11
+ def __init__(self):
12
+ super().__init__("PresentationVisualAnalyzer", model_type="light")
13
+
14
+ async def analyze_presentation_layout(
15
+ self,
16
+ presentation_image_path: str,
17
+ latex_code: str,
18
+ slide_number: int,
19
+ total_slides: int,
20
+ ) -> Tuple[bool, str, Optional[str]]:
21
+ """
22
+ Analyze presentation slide layout and provide feedback
23
+
24
+ Returns:
25
+ tuple: (is_layout_good, analysis_feedback, suggested_fixes)
26
+
27
+ """
28
+ try:
29
+ # Convert image to base64 for vision model
30
+ image_base64 = await self._image_to_base64(presentation_image_path)
31
+ if not image_base64:
32
+ return False, "Could not process presentation image", None
33
+
34
+ analysis_prompt = f"""
35
+ Analyze this presentation slide (slide {slide_number} of {total_slides}) for layout quality and visual appeal.
36
+
37
+ Look for the following aspects:
38
+ 1. Text readability and font sizes
39
+ 2. Content overflow or text cut-off
40
+ 3. Proper spacing and margins
41
+ 4. Visual hierarchy and organization
42
+ 5. Color scheme and contrast
43
+ 6. Diagram/image clarity and positioning
44
+ 7. Overall professional appearance
45
+ 8. Slide balance and composition
46
+
47
+ Specific issues to check:
48
+ - Is all text clearly visible and readable?
49
+ - Are there any overlapping elements?
50
+ - Is the content well-organized and not cramped?
51
+ - Are diagrams and images properly sized and positioned?
52
+ - Is the slide visually appealing and professional?
53
+ - Does the slide follow good presentation design principles?
54
+
55
+ Provide a detailed assessment including:
56
+ 1. Overall quality rating (Excellent/Good/Fair/Poor)
57
+ 2. Specific issues identified (if any)
58
+ 3. Recommendations for improvement
59
+ 4. Whether the slide needs to be regenerated
60
+
61
+ There are a lot of cases where the sentences are not fitting in the slide, it looks like it is the end of the sentence, but some part of the sentence is cut off. Make sure to find those cases and provide feedback on them. THESE FEEDBACKS SHOULD BE WRITTEN IN CAPITAL.
62
+
63
+ Be thorough in your analysis and provide actionable feedback.
64
+ """
65
+
66
+ messages = [
67
+ {
68
+ "role": "system",
69
+ "content": "You are an expert presentation design analyst with deep knowledge of visual design principles and academic presentation standards.",
70
+ },
71
+ {
72
+ "role": "user",
73
+ "content": [
74
+ {
75
+ "type": "text",
76
+ "text": analysis_prompt,
77
+ },
78
+ {
79
+ "type": "image_url",
80
+ "image_url": {
81
+ "url": f"data:image/png;base64,{image_base64}",
82
+ },
83
+ },
84
+ ],
85
+ },
86
+ ]
87
+
88
+ analysis_response = await self.generate_response(messages, temperature=0.3)
89
+
90
+ # Determine if layout needs improvement
91
+ needs_improvement = any(
92
+ keyword in analysis_response.lower()
93
+ for keyword in [
94
+ "poor",
95
+ "fair",
96
+ "cramped",
97
+ "overflow",
98
+ "cut-off",
99
+ "overlapping",
100
+ "unreadable",
101
+ "too small",
102
+ "needs improvement",
103
+ "regenerate",
104
+ ]
105
+ )
106
+
107
+ layout_is_good = not needs_improvement
108
+
109
+ if needs_improvement:
110
+ # Generate specific fixes
111
+ fixed_latex = await self._generate_layout_fixes(
112
+ latex_code, analysis_response
113
+ )
114
+ return layout_is_good, analysis_response, fixed_latex
115
+
116
+ return layout_is_good, analysis_response, None
117
+
118
+ except Exception as e:
119
+ return False, f"Error analyzing presentation layout: {e!s}", None
120
+
121
+ async def _image_to_base64(self, image_path: str) -> Optional[str]:
122
+ """Convert image to base64 string"""
123
+ try:
124
+ if not os.path.exists(image_path):
125
+ print(f"Presentation image not found: {image_path}")
126
+ return None
127
+
128
+ with open(image_path, "rb") as image_file:
129
+ image_data = image_file.read()
130
+ base64_string = base64.b64encode(image_data).decode("utf-8")
131
+ return base64_string
132
+ except Exception as e:
133
+ print(f"Error converting presentation image to base64: {e}")
134
+ return None
135
+
136
+ async def _generate_layout_fixes(self, original_latex: str, analysis: str) -> str:
137
+ """Generate improved LaTeX code based on visual analysis"""
138
+ fix_prompt = f"""
139
+ Based on the presentation layout analysis below, improve the Beamer LaTeX code to fix visual and layout issues.
140
+
141
+ Visual Analysis Feedback:
142
+ {analysis}
143
+
144
+ Original LaTeX Code:
145
+ {original_latex}
146
+
147
+ Please provide an improved version that addresses the identified issues. Focus on:
148
+ 1. Improving text readability (font sizes, spacing)
149
+ 2. Fixing content overflow or cut-off issues
150
+ 3. Better spacing and margins
151
+ 4. Improved visual hierarchy
152
+ 5. Better positioning of elements
153
+ 6. Professional color scheme
154
+ 7. Proper slide layout and composition
155
+
156
+ Beamer-specific improvements:
157
+ - Use appropriate frame options for content fitting
158
+ - Adjust font sizes with \\tiny, \\small, \\large, etc.
159
+ - Use proper column layouts for better organization
160
+ - Apply appropriate themes and color schemes
161
+ - Use proper spacing commands (\\vspace, \\hspace)
162
+ - Ensure TikZ diagrams fit properly on slides
163
+
164
+ Provide ONLY the corrected Beamer LaTeX code.
165
+ Do not include explanations or markdown formatting.
166
+ """
167
+
168
+ messages = [
169
+ {
170
+ "role": "system",
171
+ "content": "You are a Beamer LaTeX expert specializing in presentation layout optimization and visual design improvements.",
172
+ },
173
+ {
174
+ "role": "user",
175
+ "content": fix_prompt,
176
+ },
177
+ ]
178
+
179
+ fixed_latex = await self.generate_response(messages, temperature=0.2)
180
+
181
+ # Clean the response
182
+ fixed_latex = fixed_latex.replace("```latex", "").replace("```", "").strip()
183
+
184
+ return fixed_latex
185
+
186
+ async def analyze_diagram_quality(
187
+ self,
188
+ diagram_image_path: str,
189
+ tikz_code: str,
190
+ ) -> Tuple[bool, str, Optional[str]]:
191
+ """Analyze the quality of TikZ diagrams in presentations"""
192
+ try:
193
+ image_base64 = await self._image_to_base64(diagram_image_path)
194
+ if not image_base64:
195
+ return False, "Could not process diagram image", None
196
+
197
+ diagram_prompt = """
198
+ Analyze this TikZ diagram for clarity, readability, and visual appeal in a presentation context.
199
+
200
+ Check for:
201
+ 1. Clarity and readability of text and labels
202
+ 2. Appropriate sizing for presentation slides
203
+ 3. Good color choices and contrast
204
+ 4. Proper spacing between elements
205
+ 5. Professional appearance
206
+ 6. Clear visual hierarchy
207
+ 7. Effective use of shapes and arrows
208
+
209
+ Provide feedback on:
210
+ - Overall diagram quality
211
+ - Specific improvements needed
212
+ - Whether the diagram enhances understanding
213
+ - Recommendations for better visual design
214
+ """
215
+
216
+ messages = [
217
+ {
218
+ "role": "system",
219
+ "content": "You are an expert in diagram design and TikZ visualization for academic presentations.",
220
+ },
221
+ {
222
+ "role": "user",
223
+ "content": [
224
+ {
225
+ "type": "text",
226
+ "text": diagram_prompt,
227
+ },
228
+ {
229
+ "type": "image_url",
230
+ "image_url": {
231
+ "url": f"data:image/png;base64,{image_base64}",
232
+ },
233
+ },
234
+ ],
235
+ },
236
+ ]
237
+
238
+ analysis = await self.generate_response(messages, temperature=0.3)
239
+
240
+ needs_improvement = any(
241
+ keyword in analysis.lower()
242
+ for keyword in [
243
+ "poor",
244
+ "unclear",
245
+ "hard to read",
246
+ "too small",
247
+ "cramped",
248
+ "needs improvement",
249
+ "confusing",
250
+ "messy",
251
+ ]
252
+ )
253
+
254
+ is_good = not needs_improvement
255
+
256
+ if needs_improvement:
257
+ improved_tikz = await self._improve_tikz_diagram(tikz_code, analysis)
258
+ return is_good, analysis, improved_tikz
259
+
260
+ return is_good, analysis, None
261
+
262
+ except Exception as e:
263
+ return False, f"Error analyzing diagram: {e!s}", None
264
+
265
+ async def _improve_tikz_diagram(self, original_tikz: str, analysis: str) -> str:
266
+ """Improve TikZ diagram code based on analysis"""
267
+ improvement_prompt = f"""
268
+ Improve this TikZ diagram code based on the visual analysis feedback.
269
+
270
+ Analysis Feedback:
271
+ {analysis}
272
+
273
+ Original TikZ Code:
274
+ {original_tikz}
275
+
276
+ Create an improved version that:
277
+ - Has better readability and clarity
278
+ - Uses appropriate sizing for presentations
279
+ - Has good color choices and contrast
280
+ - Is well-organized and professional
281
+ - Follows TikZ best practices
282
+
283
+ Provide only the improved TikZ code within tikzpicture environment.
284
+ """
285
+
286
+ messages = [
287
+ {
288
+ "role": "system",
289
+ "content": "You are a TikZ expert focused on creating clear, professional diagrams for presentations.",
290
+ },
291
+ {
292
+ "role": "user",
293
+ "content": improvement_prompt,
294
+ },
295
+ ]
296
+
297
+ improved_tikz = await self.generate_response(messages, temperature=0.2)
298
+
299
+ # Clean the response
300
+ improved_tikz = (
301
+ improved_tikz.replace("```latex", "")
302
+ .replace("```tikz", "")
303
+ .replace("```", "")
304
+ .strip()
305
+ )
306
+
307
+ return improved_tikz
308
+
309
+ async def process(self, input_data):
310
+ """Implementation of abstract method from BaseAgent"""
311
+ return await self.analyze_presentation_layout(
312
+ input_data.get("image_path"),
313
+ input_data.get("latex_code"),
314
+ input_data.get("slide_number", 1),
315
+ input_data.get("total_slides", 1),
316
+ )
app/agents/publisher.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from app.agents.base_agent import BaseAgent
2
+
3
+
4
+ class PublisherAgent(BaseAgent):
5
+ def __init__(self):
6
+ super().__init__("Publisher", model_type="light")
app/agents/tikz_diagram_generator.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.agents.base_agent import BaseAgent
2
+ from app.models.schemas import PaperAnalysis, TikzDiagram
3
+
4
+
5
+ class TikzDiagramAgent(BaseAgent):
6
+ """Agent to create TikZ-based diagrams and infographics for presentations"""
7
+
8
+ def __init__(self):
9
+ super().__init__("TikzDiagramGenerator", model_type="coding")
10
+
11
+ async def process(
12
+ self, diagram_descriptions: list[str], analysis: PaperAnalysis
13
+ ) -> list[TikzDiagram]:
14
+ """Generate TikZ diagrams based on descriptions and paper content"""
15
+ diagrams = []
16
+ for i, description in enumerate(diagram_descriptions):
17
+ diagram = await self._create_single_diagram(description, analysis, i + 1)
18
+ if diagram:
19
+ diagrams.append(diagram)
20
+
21
+ return diagrams
22
+
23
+ async def _create_single_diagram(
24
+ self, description: str, analysis: PaperAnalysis, diagram_num: int
25
+ ) -> TikzDiagram:
26
+ """Create a single TikZ diagram"""
27
+ tikz_prompt = f"""
28
+ Create a TikZ diagram for a research presentation based on this description and paper content.
29
+
30
+ Diagram Description: {description}
31
+
32
+ Paper Context:
33
+ Title: {analysis.title}
34
+ Methodology: {analysis.methodology}
35
+ Key Findings: {", ".join(analysis.key_findings)}
36
+ Results: {analysis.results}
37
+ Technical Terms: {", ".join(analysis.technical_terms)}
38
+
39
+ Generate clean, professional TikZ code that:
40
+ 1. Is suitable for academic presentations
41
+ 2. Uses appropriate colors and styling
42
+ 3. Is well-commented for clarity
43
+ 4. Fits within slide dimensions
44
+ 5. Uses clear, readable fonts
45
+ 6. Follows TikZ best practices
46
+
47
+ Common diagram types to consider:
48
+ - Flowcharts for processes/methodology
49
+ - Block diagrams for system architecture
50
+ - Graphs and charts for data visualization
51
+ - Timeline diagrams for sequential processes
52
+ - Comparison diagrams for before/after scenarios
53
+ - Network diagrams for connections/relationships
54
+
55
+ Provide ONLY the TikZ code within a tikzpicture environment, without document structure.
56
+ Use appropriate TikZ libraries like shapes, arrows, positioning, etc.
57
+
58
+ Example format:
59
+ \\begin{{tikzpicture}}[node distance=2cm, auto]
60
+ % Your TikZ code here
61
+ \\end{{tikzpicture}}
62
+
63
+ Make the diagram informative, visually appealing, and directly relevant to the research content.
64
+ """
65
+
66
+ messages = [
67
+ {
68
+ "role": "system",
69
+ "content": "You are a TikZ expert specializing in creating academic diagrams and visualizations. Generate clean, professional TikZ code.",
70
+ },
71
+ {"role": "user", "content": tikz_prompt},
72
+ ]
73
+
74
+ response = await self.generate_response(messages, temperature=0.3)
75
+
76
+ # Clean the TikZ code
77
+ tikz_code = self._clean_tikz_code(response)
78
+
79
+ # Determine diagram type from description
80
+ diagram_type = self._determine_diagram_type(description)
81
+
82
+ return TikzDiagram(
83
+ diagram_id=f"diagram_{diagram_num}",
84
+ title=f"Diagram {diagram_num}",
85
+ description=description,
86
+ tikz_code=tikz_code,
87
+ diagram_type=diagram_type,
88
+ )
89
+
90
+ def _clean_tikz_code(self, code: str) -> str:
91
+ """Clean and validate TikZ code"""
92
+ # Remove markdown code blocks if present
93
+ code = code.replace("```latex", "").replace("```tikz", "").replace("```", "")
94
+
95
+ # Ensure tikzpicture environment
96
+ if "\\begin{tikzpicture}" not in code:
97
+ # Wrap content in tikzpicture if not present
98
+ code = f"\\begin{{tikzpicture}}[node distance=2cm, auto]\n{code}\n\\end{{tikzpicture}}"
99
+
100
+ return code.strip()
101
+
102
+ def _determine_diagram_type(self, description: str) -> str:
103
+ """Determine diagram type from description"""
104
+ description_lower = description.lower()
105
+
106
+ if any(
107
+ word in description_lower
108
+ for word in ["flow", "process", "step", "workflow"]
109
+ ):
110
+ return "flowchart"
111
+ if any(
112
+ word in description_lower
113
+ for word in ["architecture", "system", "structure", "framework"]
114
+ ):
115
+ return "architecture"
116
+ if any(
117
+ word in description_lower
118
+ for word in ["timeline", "sequence", "chronological", "time"]
119
+ ):
120
+ return "timeline"
121
+ if any(
122
+ word in description_lower
123
+ for word in ["comparison", "vs", "versus", "compare", "before", "after"]
124
+ ):
125
+ return "comparison"
126
+ if any(
127
+ word in description_lower
128
+ for word in ["graph", "chart", "plot", "data", "statistics"]
129
+ ):
130
+ return "graph"
131
+ if any(
132
+ word in description_lower
133
+ for word in ["network", "connection", "relationship", "link"]
134
+ ):
135
+ return "network"
136
+ return "general"
137
+
138
+ async def create_methodology_flowchart(
139
+ self, analysis: PaperAnalysis
140
+ ) -> TikzDiagram:
141
+ """Create a specific methodology flowchart"""
142
+ return await self._create_single_diagram(
143
+ f"Methodology flowchart showing the research process: {analysis.methodology}",
144
+ analysis,
145
+ 1,
146
+ )
147
+
148
+ async def create_results_comparison(self, analysis: PaperAnalysis) -> TikzDiagram:
149
+ """Create a results comparison diagram"""
150
+ return await self._create_single_diagram(
151
+ f"Results comparison chart based on: {analysis.results}",
152
+ analysis,
153
+ 2,
154
+ )
155
+
156
+ async def create_architecture_diagram(self, analysis: PaperAnalysis) -> TikzDiagram:
157
+ """Create a system architecture diagram"""
158
+ return await self._create_single_diagram(
159
+ f"System architecture diagram for the proposed approach in: {analysis.methodology}",
160
+ analysis,
161
+ 3,
162
+ )
app/agents/tldr_generator.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.agents.base_agent import BaseAgent
2
+ from app.models.schemas import PaperAnalysis, TLDRContent
3
+ from app.services.image_service import image_service
4
+
5
+
6
+ class TLDRGeneratorAgent(BaseAgent):
7
+ def __init__(self):
8
+ super().__init__("TLDRGenerator", model_type="light")
9
+
10
+ async def process(self, analysis: PaperAnalysis) -> TLDRContent:
11
+ """Generate platform-specific social media content"""
12
+ # LinkedIn Post
13
+ linkedin_prompt = f"""
14
+ Create a professional LinkedIn post about this research:
15
+
16
+ Title: {analysis.title}
17
+ Key Findings: {", ".join(analysis.key_findings)}
18
+ Methodology: {analysis.methodology}
19
+
20
+ Requirements:
21
+ - Professional tone
22
+ - 1200 characters max
23
+ - Include relevant hashtags
24
+ - Encourage engagement
25
+ - Highlight practical implications
26
+ Make sure to include the key findings and methodology in bullet fashion.
27
+ Don't include any other information except the post content. No additional headers or ending text in the response.
28
+ """
29
+
30
+ # Twitter Thread
31
+ twitter_prompt = f"""
32
+ Create a Twitter thread (3-5 tweets) about this research:
33
+
34
+ Title: {analysis.title}
35
+ Key Findings: {", ".join(analysis.key_findings)}
36
+ Methodology: {analysis.methodology}
37
+
38
+ Requirements:
39
+ - Each tweet max 280 characters
40
+ - Start with hook
41
+ - Include thread numbers (1/n)
42
+ - End with call to action
43
+
44
+ Make sure to include the key findings and methodology.
45
+ Don't include any other information except the thread content. No additional headers or ending text in the response.
46
+ """
47
+
48
+ # Facebook Post
49
+ facebook_prompt = f"""
50
+ Create an engaging Facebook post about this research:
51
+
52
+ Title: {analysis.title}
53
+ Key Finding: {analysis.key_findings if analysis.key_findings else analysis.conclusion}
54
+ Methodology: {analysis.methodology}
55
+
56
+ Requirements:
57
+ - Conversational tone
58
+ - Ask questions to encourage comments
59
+ - Include emojis appropriately
60
+ - 500 characters max
61
+
62
+ Make sure to include the key findings and methodology.
63
+ Don't include any other information except the post content. No additional headers or ending text in the response.
64
+ """
65
+
66
+ # Instagram Caption
67
+ instagram_prompt = f"""
68
+ Create an Instagram caption about this research:
69
+
70
+ Title: {analysis.title}
71
+ Key Finding: {analysis.key_findings if analysis.key_findings else analysis.conclusion}
72
+ Methodology: {analysis.methodology}
73
+
74
+ Requirements:
75
+ - Visual-friendly language
76
+ - Include relevant emojis
77
+ - Use line breaks for readability
78
+ - Include hashtags
79
+ - 2200 characters max
80
+
81
+ Make sure to include the key findings and methodology.
82
+ Don't include any other information except the caption content. No additional headers or ending text in the response.
83
+ """
84
+
85
+ # Generate all content
86
+ linkedin_post = await self._generate_platform_content(linkedin_prompt)
87
+ twitter_thread = await self._generate_twitter_thread(twitter_prompt)
88
+ facebook_post = await self._generate_platform_content(facebook_prompt)
89
+ instagram_caption = await self._generate_platform_content(instagram_prompt)
90
+ hashtags = self._generate_hashtags(analysis)
91
+
92
+ # Generate images for all platforms
93
+ try:
94
+ images = await image_service.generate_all_social_images(analysis)
95
+ except Exception as e:
96
+ print(f"Error generating images: {e!s}")
97
+ images = {
98
+ "linkedin": None,
99
+ "twitter": None,
100
+ "facebook": None,
101
+ "instagram": None,
102
+ }
103
+
104
+ return TLDRContent(
105
+ linkedin_post=linkedin_post,
106
+ twitter_thread=twitter_thread,
107
+ facebook_post=facebook_post,
108
+ instagram_caption=instagram_caption,
109
+ hashtags=hashtags,
110
+ linkedin_image=images.get("linkedin"),
111
+ twitter_image=images.get("twitter"),
112
+ facebook_image=images.get("facebook"),
113
+ instagram_image=images.get("instagram"),
114
+ )
115
+
116
+ async def _generate_platform_content(self, prompt: str) -> str:
117
+ """Generate content for a specific platform"""
118
+ messages = [
119
+ {
120
+ "role": "system",
121
+ "content": "You are a social media expert who creates engaging, platform-specific content.",
122
+ },
123
+ {"role": "user", "content": prompt},
124
+ ]
125
+ return await self.generate_response(messages, temperature=0.8)
126
+
127
+ async def _generate_twitter_thread(self, prompt: str) -> list:
128
+ """Generate Twitter thread as list of tweets"""
129
+ response = await self._generate_platform_content(prompt)
130
+
131
+ # Split into individual tweets
132
+ tweets = []
133
+ lines = response.split("\n")
134
+ current_tweet = ""
135
+
136
+ for line in lines:
137
+ line = line.strip()
138
+ if line and (
139
+ line.startswith(("1/", "2/", "3/", "4/", "5/"))
140
+ or line.startswith(("1.", "2.", "3.", "4.", "5."))
141
+ ):
142
+ if current_tweet:
143
+ tweets.append(current_tweet.strip())
144
+ current_tweet = line
145
+ elif line:
146
+ current_tweet += " " + line
147
+
148
+ if current_tweet:
149
+ tweets.append(current_tweet.strip())
150
+
151
+ return tweets[:5] # Limit to 5 tweets
152
+
153
+ def _generate_hashtags(self, analysis: PaperAnalysis) -> list:
154
+ """Generate relevant hashtags"""
155
+ hashtags = ["#research", "#science", "#academic"]
156
+
157
+ # Add field-specific hashtags
158
+ title_lower = analysis.title.lower()
159
+ if "ai" in title_lower or "artificial intelligence" in title_lower:
160
+ hashtags.extend(["#AI", "#MachineLearning", "#Technology"])
161
+ elif "machine learning" in title_lower:
162
+ hashtags.extend(["#MachineLearning", "#DataScience", "#AI"])
163
+ elif "computer" in title_lower:
164
+ hashtags.extend(["#ComputerScience", "#Technology", "#Programming"])
165
+ elif "biology" in title_lower or "medical" in title_lower:
166
+ hashtags.extend(["#Biology", "#Medicine", "#Healthcare"])
167
+ elif "physics" in title_lower:
168
+ hashtags.extend(["#Physics", "#Science"])
169
+ elif "chemistry" in title_lower:
170
+ hashtags.extend(["#Chemistry", "#Science"])
171
+
172
+ return hashtags[:10]
app/config/__init__.py ADDED
File without changes
app/config/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (163 Bytes). View file
 
app/config/__pycache__/settings.cpython-310.pyc ADDED
Binary file (1.92 kB). View file
 
app/config/settings.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ try:
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+ except ImportError:
8
+ # dotenv not available, use environment variables directly
9
+ pass
10
+
11
+
12
+ class Settings:
13
+ # LLM Configuration
14
+ MISTRAL_API_KEY: str = os.getenv("MISTRAL_API_KEY", "")
15
+
16
+ # Model Configuration
17
+ HEAVY_MODEL_PROVIDER: str = os.getenv("HEAVY_MODEL_PROVIDER", "openai")
18
+ HEAVY_MODEL_NAME: str = os.getenv("HEAVY_MODEL_NAME", "gpt-4-turbo")
19
+ HEAVY_MODEL_API_KEY: str = os.getenv("HEAVY_MODEL_API_KEY", "")
20
+ HEAVY_MODEL_BASE_URL: str | None = os.getenv("HEAVY_MODEL_BASE_URL")
21
+
22
+ LIGHT_MODEL_PROVIDER: str = os.getenv("LIGHT_MODEL_PROVIDER", "openai")
23
+ LIGHT_MODEL_NAME: str = os.getenv("LIGHT_MODEL_NAME", "gpt-3.5-turbo")
24
+ LIGHT_MODEL_API_KEY: str = os.getenv("LIGHT_MODEL_API_KEY", "")
25
+ LIGHT_MODEL_BASE_URL: str | None = os.getenv("LIGHT_MODEL_BASE_URL")
26
+
27
+ CODING_MODEL_PROVIDER: str = os.getenv("CODING_MODEL_PROVIDER", "openai")
28
+ CODING_MODEL_NAME: str = os.getenv("CODING_MODEL_NAME", "gpt-4-1106-preview")
29
+ CODING_MODEL_API_KEY: str = os.getenv("CODING_MODEL_API_KEY", "")
30
+ CODING_MODEL_BASE_URL: str | None = os.getenv(
31
+ "CODING_MODEL_BASE_URL",
32
+ "https://api.openai.com/v1/chat/completions",
33
+ )
34
+
35
+ # Image Generation
36
+ IMAGE_GEN_API_KEY: str = os.getenv("IMAGE_GEN_API_KEY", "")
37
+ IMAGE_GEN_BASE_URL: str | None = os.getenv(
38
+ "IMAGE_GEN_BASE_URL",
39
+ "https://api.openai.com/v1/images/generations",
40
+ )
41
+ IMAGE_GEN_MODEL: str = os.getenv("IMAGE_GEN_MODEL", "dall-e-3")
42
+ IMAGE_GEN_IMAGE_SIZE: str = os.getenv("IMAGE_GEN_IMAGE_SIZE", "1024x1024")
43
+ IMAGE_GEN_IMAGE_QUALITY: str = os.getenv("IMAGE_GEN_IMAGE_QUALITY", "standard")
44
+ IMAGE_GEN_IMAGE_STYLE: str = os.getenv("IMAGE_GEN_IMAGE_STYLE", "vivid")
45
+
46
+ # DeepInfra API for blog images
47
+ DEEPINFRA_API_KEY: str = os.getenv("DEEPINFRA_API_KEY", "")
48
+
49
+ # API Keys
50
+ DEVTO_API_KEY: str = os.getenv("DEVTO_API_KEY", "")
51
+
52
+ # Database
53
+ DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./scholarshare.db")
54
+
55
+ # Application Settings
56
+ DEBUG: bool = os.getenv("DEBUG", "True").lower() == "true"
57
+ HOST: str = os.getenv("HOST", "0.0.0.0")
58
+ PORT: int = int(os.getenv("PORT", "7860"))
59
+
60
+
61
+ settings = Settings()
app/database/__init__.py ADDED
File without changes
app/database/database.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import sessionmaker
3
+
4
+ from app.config.settings import settings
5
+ from app.database.models import Base
6
+
7
+ engine = create_engine(settings.DATABASE_URL)
8
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
9
+
10
+
11
+ def create_tables():
12
+ """Create all tables"""
13
+ Base.metadata.create_all(bind=engine)
14
+
15
+
16
+ def get_db():
17
+ """Get database session"""
18
+ db = SessionLocal()
19
+ try:
20
+ yield db
21
+ finally:
22
+ db.close()
23
+
24
+
25
+ # Initialize database
26
+ create_tables()
app/database/models.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import JSON, Boolean, Column, DateTime, Integer, String, Text
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.sql import func
4
+
5
+ Base = declarative_base()
6
+
7
+
8
+ class Paper(Base):
9
+ __tablename__ = "papers"
10
+
11
+ id = Column(Integer, primary_key=True, index=True)
12
+ title = Column(String(500), nullable=False)
13
+ authors = Column(JSON) # List of authors
14
+ abstract = Column(Text)
15
+ content = Column(Text)
16
+ source_type = Column(String(50)) # pdf, url, text
17
+ source_url = Column(String(500))
18
+ complexity_level = Column(String(50))
19
+ technical_terms = Column(JSON)
20
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
21
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
22
+
23
+
24
+ class GeneratedContent(Base):
25
+ __tablename__ = "generated_content"
26
+
27
+ id = Column(Integer, primary_key=True, index=True)
28
+ paper_id = Column(Integer, nullable=False)
29
+ content_type = Column(String(50)) # blog, poster, social
30
+ platform = Column(String(50)) # devto, linkedin, twitter, etc.
31
+ title = Column(String(500))
32
+ content = Column(Text)
33
+ metadata = Column(JSON)
34
+ is_published = Column(Boolean, default=False)
35
+ published_url = Column(String(500))
36
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
37
+
38
+
39
+ class PublishingLog(Base):
40
+ __tablename__ = "publishing_logs"
41
+
42
+ id = Column(Integer, primary_key=True, index=True)
43
+ content_id = Column(Integer, nullable=False)
44
+ platform = Column(String(50))
45
+ status = Column(String(50)) # success, failed, pending
46
+ response_data = Column(JSON)
47
+ error_message = Column(Text)
48
+ published_at = Column(DateTime(timezone=True), server_default=func.now())
app/main.py ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ import gradio as gr
6
+
7
+ from app.agents.blog_generator import BlogGeneratorAgent
8
+ from app.agents.paper_analyzer import PaperAnalyzerAgent
9
+ from app.agents.poster_generator import PosterGeneratorAgent
10
+ from app.agents.tldr_generator import TLDRGeneratorAgent
11
+ from app.config.settings import settings
12
+ from app.models.schemas import PaperInput
13
+ from app.services.devto_service import devto_service
14
+ from app.services.pdf_service import pdf_service
15
+
16
+ # Initialize agents
17
+ paper_analyzer = PaperAnalyzerAgent()
18
+ blog_generator = BlogGeneratorAgent()
19
+ tldr_generator = TLDRGeneratorAgent()
20
+ poster_generator = PosterGeneratorAgent()
21
+
22
+ # Global state - Consider refactoring to avoid globals if possible
23
+ current_analysis: Optional[dict] = None
24
+ current_blog: Optional[dict] = None
25
+ current_tldr: Optional[dict] = None
26
+ current_poster: Optional[dict] = None
27
+
28
+
29
+ async def process_paper(pdf_file, url_input, text_input, progress=None):
30
+ """Process paper from various input sources."""
31
+ global current_analysis
32
+ if progress is None:
33
+ progress = gr.Progress()
34
+
35
+ try:
36
+ progress(0.1, desc="Processing input...")
37
+
38
+ # Determine input source and extract content
39
+ if pdf_file is not None:
40
+ progress(0.2, desc="Parsing PDF...")
41
+ # Replaced open() with Path.open() and async read
42
+ pdf_path = Path(pdf_file.name)
43
+ pdf_content = await asyncio.to_thread(pdf_path.read_bytes)
44
+ # TODO: Uncomment when PDF parsing is implemented
45
+ # parsed_data = pdf_service.parse_pdf(pdf_content)
46
+ # content = parsed_data["text"]
47
+ # Read the PDF content directly from file parsed_pdf_content.txt
48
+ with open("parsed_pdf_content.txt", encoding="utf-8") as f:
49
+ content = f.read()
50
+ source_type = "pdf"
51
+ elif url_input and url_input.strip():
52
+ progress(0.2, desc="Fetching from URL...")
53
+ parsed_data = await pdf_service.parse_url(url_input.strip())
54
+ content = parsed_data["text"]
55
+ source_type = "url"
56
+ elif text_input and text_input.strip():
57
+ content = text_input.strip()
58
+ source_type = "text"
59
+ else:
60
+ return "❌ Please provide a PDF file, URL, or text input.", "", "", ""
61
+
62
+ if not content or not content.strip():
63
+ return "❌ No content could be extracted from the input.", "", "", ""
64
+
65
+ # Create paper input
66
+ paper_input = PaperInput(content=content, source_type=source_type)
67
+
68
+ # Analyze paper
69
+ progress(0.4, desc="Analyzing paper...")
70
+ current_analysis = await paper_analyzer.process(paper_input)
71
+ print(current_analysis) # Debugging line
72
+
73
+ # Generate preview content
74
+ progress(0.7, desc="Generating previews...")
75
+
76
+ analysis_summary = f"""
77
+ # Paper Analysis Summary
78
+
79
+ ## **Title:** {current_analysis.title}
80
+
81
+ ## **Authors:** {", ".join(current_analysis.authors)}
82
+
83
+ ## **Abstract:**
84
+ {current_analysis.abstract}
85
+
86
+ ## **Methodology:**
87
+ {current_analysis.methodology}
88
+
89
+ ## **Key Findings:**
90
+ {chr(10).join([f"• {finding}" for finding in current_analysis.key_findings])}
91
+
92
+ ## **Results:**
93
+ {current_analysis.results}
94
+
95
+ ## **Conclusion:**
96
+ {current_analysis.conclusion}
97
+
98
+ ## **Complexity Level:** {current_analysis.complexity_level.title()}
99
+
100
+ ## **Technical Terms:**
101
+ {", ".join(current_analysis.technical_terms) if current_analysis.technical_terms else "None identified"}
102
+ """
103
+
104
+ progress(1.0, desc="Complete!")
105
+
106
+ return (
107
+ "✅ Paper processed successfully!",
108
+ analysis_summary,
109
+ "Ready to generate blog content",
110
+ gr.DownloadButton(visible=True),
111
+ )
112
+
113
+ except Exception as e:
114
+ # Consider more specific exception handling
115
+ return (
116
+ f"❌ Error processing paper: {e!s}",
117
+ "",
118
+ "",
119
+ gr.DownloadButton(visible=False),
120
+ )
121
+
122
+
123
+ async def generate_blog_content(progress=None):
124
+ """Generate blog content from analysis."""
125
+ global current_analysis, current_blog
126
+ if progress is None:
127
+ progress = gr.Progress()
128
+
129
+ if not current_analysis:
130
+ return "❌ Please process a paper first.", gr.update(
131
+ visible=False,
132
+ interactive=False,
133
+ )
134
+
135
+ try:
136
+ progress(0.1, desc="Starting blog generation...")
137
+ await asyncio.sleep(0.5) # Give time for progress bar to show
138
+
139
+ progress(0.3, desc="Generating blog content...")
140
+ await asyncio.sleep(0.3) # Allow UI to update
141
+
142
+ current_blog = await blog_generator.process(current_analysis)
143
+
144
+ progress(0.8, desc="Formatting blog content...")
145
+ await asyncio.sleep(0.3) # Allow UI to update
146
+
147
+ blog_preview = f"""# {current_blog.title}
148
+
149
+ {current_blog.content}
150
+
151
+ **Tags:** {", ".join(current_blog.tags)}
152
+ **Reading Time:** {current_blog.reading_time} minutes"""
153
+
154
+ progress(1.0, desc="Blog content generated!")
155
+ return blog_preview, gr.DownloadButton(visible=True)
156
+
157
+ except Exception as e:
158
+ # Consider more specific exception handling
159
+ return f"❌ Error generating blog: {e!s}", gr.DownloadButton(visible=False)
160
+
161
+
162
+ async def generate_social_content(progress=None):
163
+ """Generate social media content from analysis."""
164
+ global current_analysis, current_tldr
165
+ if progress is None:
166
+ progress = gr.Progress()
167
+
168
+ if not current_analysis:
169
+ return "❌ Please process a paper first.", "", "", ""
170
+
171
+ try:
172
+ progress(0.3, desc="Generating social media content...")
173
+ current_tldr = await tldr_generator.process(current_analysis)
174
+
175
+ progress(1.0, desc="Social content generated!")
176
+
177
+ return (
178
+ current_tldr.linkedin_post,
179
+ "\n\n".join(
180
+ [
181
+ f"Tweet {i + 1}: {tweet}"
182
+ for i, tweet in enumerate(current_tldr.twitter_thread)
183
+ ],
184
+ ),
185
+ current_tldr.facebook_post,
186
+ current_tldr.instagram_caption,
187
+ )
188
+
189
+ except Exception as e:
190
+ # Consider more specific exception handling
191
+ error_msg = f"❌ Error generating social content: {e!s}"
192
+ return error_msg, error_msg, error_msg, error_msg
193
+
194
+
195
+ async def generate_poster_content(template_type, progress=None):
196
+ """Generate poster content from analysis."""
197
+ global current_analysis, current_poster
198
+ if progress is None:
199
+ progress = gr.Progress()
200
+
201
+ if not current_analysis:
202
+ return "❌ Please process a paper first.", ""
203
+
204
+ try:
205
+ progress(0.3, desc="Generating poster...")
206
+ current_poster = await poster_generator.process(current_analysis, template_type)
207
+
208
+ progress(0.8, desc="Compiling LaTeX...")
209
+
210
+ poster_info = f"""
211
+ **Poster Generated Successfully!**
212
+
213
+ **Template:** {template_type.upper()}
214
+ **PDF Path:** {current_poster.pdf_path if current_poster.pdf_path else "Compilation in progress..."}
215
+
216
+ **LaTeX Code Preview:**
217
+ ```
218
+ {current_poster.latex_code[:300]}...
219
+ ```
220
+ """
221
+
222
+ progress(1.0, desc="Poster ready!")
223
+ return poster_info, current_poster.latex_code
224
+
225
+ except Exception as e:
226
+ # Consider more specific exception handling
227
+ return f"❌ Error generating poster: {e!s}", ""
228
+
229
+
230
+ async def publish_to_devto(publish_now):
231
+ """Publish blog content to DEV.to."""
232
+ global current_blog
233
+
234
+ if not current_blog:
235
+ return "❌ Please generate blog content first."
236
+
237
+ try:
238
+ result = await devto_service.publish_article(current_blog, publish_now)
239
+
240
+ if result["success"]:
241
+ status = "Published" if result.get("published") else "Saved as Draft"
242
+ return f"✅ Article {status} successfully!\nURL: {result.get('url', 'N/A')}"
243
+ # Removed unnecessary else
244
+ return f"❌ Publication failed: {result.get('error', 'Unknown error')}"
245
+
246
+ except Exception as e:
247
+ # Consider more specific exception handling
248
+ return f"❌ Error publishing to DEV.to: {e!s}"
249
+
250
+
251
+ def publish_draft():
252
+ """Sync wrapper for publishing as draft."""
253
+ try:
254
+ loop = asyncio.get_event_loop()
255
+ if loop.is_running():
256
+ # If loop is already running, create a task
257
+ import concurrent.futures
258
+
259
+ with concurrent.futures.ThreadPoolExecutor() as executor:
260
+ future = executor.submit(asyncio.run, publish_to_devto(False))
261
+ return future.result()
262
+ else:
263
+ return loop.run_until_complete(publish_to_devto(False))
264
+ except RuntimeError:
265
+ # If no event loop, create one
266
+ return asyncio.run(publish_to_devto(False))
267
+
268
+
269
+ def publish_now():
270
+ """Sync wrapper for publishing immediately."""
271
+ try:
272
+ loop = asyncio.get_event_loop()
273
+ if loop.is_running():
274
+ # If loop is already running, create a task
275
+ import concurrent.futures
276
+
277
+ with concurrent.futures.ThreadPoolExecutor() as executor:
278
+ future = executor.submit(asyncio.run, publish_to_devto(True))
279
+ return future.result()
280
+ else:
281
+ return loop.run_until_complete(publish_to_devto(True))
282
+ except RuntimeError:
283
+ # If no event loop, create one
284
+ return asyncio.run(publish_to_devto(True))
285
+
286
+
287
+ async def download_analysis_summary():
288
+ """Generate downloadable analysis summary as markdown file."""
289
+ global current_analysis
290
+
291
+ if not current_analysis:
292
+ return None
293
+
294
+ try:
295
+ # Create comprehensive analysis summary
296
+ markdown_content = f"""# Paper Analysis Summary
297
+
298
+ ## Title
299
+ {current_analysis.title}
300
+
301
+ ## Authors
302
+ {", ".join(current_analysis.authors)}
303
+
304
+ ## Abstract
305
+ {current_analysis.abstract}
306
+
307
+ ## Methodology
308
+ {current_analysis.methodology}
309
+
310
+ ## Key Findings
311
+ {chr(10).join([f"- {finding}" for finding in current_analysis.key_findings])}
312
+
313
+ ## Results
314
+ {current_analysis.results}
315
+
316
+ ## Conclusion
317
+ {current_analysis.conclusion}
318
+
319
+ ## Complexity Level
320
+ {current_analysis.complexity_level.title()}
321
+
322
+ ## Technical Terms
323
+ {", ".join(current_analysis.technical_terms) if current_analysis.technical_terms else "None identified"}
324
+
325
+ ## Figures and Tables
326
+ {chr(10).join([f"- {fig.get('description', 'Figure/Table')}: {fig.get('caption', 'No caption')}" for fig in current_analysis.figures_tables]) if current_analysis.figures_tables else "None identified"}
327
+
328
+ ---
329
+ *Generated by ScholarShare - AI Research Dissemination Platform*
330
+ """
331
+
332
+ # Save to outputs directory
333
+ output_path = Path("outputs/analysis_summary.md")
334
+ output_path.write_text(markdown_content, encoding="utf-8")
335
+
336
+ return str(output_path)
337
+
338
+ except Exception:
339
+ return None
340
+
341
+
342
+ async def download_blog_markdown():
343
+ """Generate downloadable blog content as markdown file."""
344
+ if not current_blog:
345
+ return None
346
+
347
+ try:
348
+ # Create comprehensive blog markdown
349
+ markdown_content = f"""# {current_blog.title}
350
+
351
+ {current_blog.content}
352
+
353
+ ---
354
+
355
+ **Tags:** {", ".join(current_blog.tags)}
356
+ **Reading Time:** {current_blog.reading_time} minutes
357
+ **Meta Description:** {current_blog.meta_description}
358
+
359
+ ---
360
+ *Generated by ScholarShare - AI Research Dissemination Platform*
361
+ """
362
+
363
+ # Save to outputs directory
364
+ output_path = Path("outputs/blogs/blog_content.md")
365
+ output_path.write_text(markdown_content, encoding="utf-8")
366
+
367
+ return str(output_path)
368
+
369
+ except OSError:
370
+ return None
371
+
372
+
373
+ # Create Gradio Interface
374
+ def create_interface():
375
+ with gr.Blocks(
376
+ title="ScholarShare - AI Research Dissemination",
377
+ theme=gr.themes.Soft(),
378
+ ) as app:
379
+ gr.Markdown("""
380
+ # 🎓 ScholarShare - AI-Powered Research Dissemination Platform
381
+
382
+ Transform complex research papers into accessible, multi-format content for broader audience engagement.
383
+ """)
384
+
385
+ with gr.Tab("📄 Paper Input & Analysis"):
386
+ gr.Markdown("## Upload and Analyze Research Paper")
387
+
388
+ with gr.Row():
389
+ with gr.Column():
390
+ pdf_input = gr.File(
391
+ label="Upload PDF Paper",
392
+ file_types=[".pdf"],
393
+ type="filepath",
394
+ )
395
+ url_input = gr.Textbox(
396
+ label="Or Enter Paper URL (arXiv, etc.)",
397
+ placeholder="https://arxiv.org/pdf/...",
398
+ )
399
+ text_input = gr.Textbox(
400
+ label="Or Paste Paper Text",
401
+ lines=5,
402
+ placeholder="Paste your research paper content here...",
403
+ )
404
+
405
+ process_btn = gr.Button("🔍 Analyze Paper", variant="primary")
406
+
407
+ with gr.Column():
408
+ status_output = gr.Textbox(label="Status", interactive=False)
409
+ analysis_output = gr.Markdown(
410
+ label="Paper Analysis",
411
+ max_height=400,
412
+ show_copy_button=True,
413
+ )
414
+ download_analysis_btn = gr.DownloadButton(
415
+ label="📥 Download Analysis as Markdown",
416
+ visible=False,
417
+ )
418
+
419
+ with gr.Tab("📝 Blog Generation"):
420
+ gr.Markdown("## Generate Beginner-Friendly Blog Content")
421
+
422
+ with gr.Row():
423
+ with gr.Column():
424
+ blog_status = gr.Textbox(label="Blog Status", interactive=False)
425
+ generate_blog_btn = gr.Button(
426
+ "✍️ Generate Blog Content",
427
+ variant="primary",
428
+ )
429
+ download_blog_btn = gr.DownloadButton(
430
+ label="📥 Download Blog as Markdown",
431
+ visible=False, # Changed from True to False initially
432
+ )
433
+
434
+ with gr.Column():
435
+ blog_output = gr.Markdown(
436
+ label="Generated Blog Content",
437
+ max_height=400,
438
+ show_copy_button=True,
439
+ )
440
+
441
+ with gr.Tab("📱 Social Media Content"):
442
+ gr.Markdown("## Generate Platform-Specific Social Media Content")
443
+
444
+ with gr.Row():
445
+ generate_social_btn = gr.Button(
446
+ "📱 Generate Social Content",
447
+ variant="primary",
448
+ )
449
+
450
+ with gr.Row():
451
+ with gr.Column():
452
+ linkedin_output = gr.Textbox(label="LinkedIn Post", lines=5)
453
+ twitter_output = gr.Textbox(label="Twitter Thread", lines=5)
454
+
455
+ with gr.Column():
456
+ facebook_output = gr.Textbox(label="Facebook Post", lines=5)
457
+ instagram_output = gr.Textbox(label="Instagram Caption", lines=5)
458
+
459
+ with gr.Tab("🎨 Poster Generation"):
460
+ gr.Markdown("## Generate Academic Conference Poster")
461
+
462
+ with gr.Row():
463
+ with gr.Column():
464
+ template_dropdown = gr.Dropdown(
465
+ choices=["ieee", "acm", "nature"],
466
+ value="ieee",
467
+ label="Poster Template Style",
468
+ )
469
+ generate_poster_btn = gr.Button(
470
+ "🎨 Generate Poster",
471
+ variant="primary",
472
+ )
473
+
474
+ with gr.Column():
475
+ poster_output = gr.Markdown(label="Poster Information")
476
+
477
+ with gr.Row():
478
+ latex_output = gr.Code(label="LaTeX Code", language="latex")
479
+
480
+ with gr.Tab("🚀 Publishing"):
481
+ gr.Markdown("## Publish Content to Platforms")
482
+
483
+ with gr.Column():
484
+ gr.Markdown("### DEV.to Publishing")
485
+ with gr.Row():
486
+ publish_draft_btn = gr.Button("💾 Save as Draft")
487
+ publish_now_btn = gr.Button("🚀 Publish Now", variant="primary")
488
+
489
+ publish_status = gr.Textbox(
490
+ label="Publishing Status",
491
+ interactive=False,
492
+ )
493
+
494
+ # Event handlers
495
+ process_btn.click(
496
+ fn=process_paper,
497
+ inputs=[pdf_input, url_input, text_input],
498
+ outputs=[
499
+ status_output,
500
+ analysis_output,
501
+ blog_status,
502
+ download_analysis_btn,
503
+ ],
504
+ )
505
+
506
+ download_analysis_btn.click(
507
+ fn=download_analysis_summary,
508
+ outputs=[download_analysis_btn],
509
+ )
510
+
511
+ generate_blog_btn.click(
512
+ fn=generate_blog_content,
513
+ outputs=[blog_output, download_blog_btn],
514
+ )
515
+
516
+ download_blog_btn.click(
517
+ fn=download_blog_markdown,
518
+ outputs=[download_blog_btn],
519
+ )
520
+
521
+ generate_social_btn.click(
522
+ fn=generate_social_content,
523
+ outputs=[
524
+ linkedin_output,
525
+ twitter_output,
526
+ facebook_output,
527
+ instagram_output,
528
+ ],
529
+ )
530
+
531
+ generate_poster_btn.click(
532
+ fn=generate_poster_content,
533
+ inputs=[template_dropdown],
534
+ outputs=[poster_output, latex_output],
535
+ )
536
+
537
+ publish_draft_btn.click(
538
+ fn=publish_draft,
539
+ outputs=[publish_status],
540
+ )
541
+
542
+ publish_now_btn.click(
543
+ fn=publish_now,
544
+ outputs=[publish_status],
545
+ )
546
+
547
+ return app
548
+
549
+
550
+ if __name__ == "__main__":
551
+ # Create output directories using pathlib
552
+ Path("outputs/posters").mkdir(parents=True, exist_ok=True)
553
+ Path("outputs/blogs").mkdir(parents=True, exist_ok=True)
554
+ Path("data").mkdir(parents=True, exist_ok=True)
555
+
556
+ # Launch the application
557
+ app_instance = create_interface()
558
+ app_instance.launch(
559
+ server_name=settings.HOST,
560
+ server_port=settings.PORT,
561
+ debug=settings.DEBUG,
562
+ share=False,
563
+ )
app/models/__init__.py ADDED
File without changes
app/models/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (163 Bytes). View file
 
app/models/__pycache__/schemas.cpython-310.pyc ADDED
Binary file (3.43 kB). View file
 
app/models/schemas.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class PaperInput(BaseModel):
9
+ content: str
10
+ source_type: str # "pdf", "url", "text"
11
+ metadata: dict[str, Any] | None = None
12
+
13
+
14
+ class PaperAnalysis(BaseModel):
15
+ title: str
16
+ authors: list[str]
17
+ abstract: str
18
+ key_findings: list[str]
19
+ methodology: str
20
+ results: str
21
+ conclusion: str
22
+ complexity_level: str
23
+ technical_terms: list[str]
24
+ figures_tables: list[dict[str, Any]]
25
+
26
+
27
+ class BlogContent(BaseModel):
28
+ title: str
29
+ content: str
30
+ tags: list[str]
31
+ meta_description: str
32
+ reading_time: int
33
+
34
+
35
+ class TLDRContent(BaseModel):
36
+ linkedin_post: str
37
+ twitter_thread: list[str]
38
+ facebook_post: str
39
+ instagram_caption: str
40
+ hashtags: list[str]
41
+ linkedin_image: str | None = None
42
+ twitter_image: str | None = None
43
+ facebook_image: str | None = None
44
+ instagram_image: str | None = None
45
+
46
+
47
+ class PosterContent(BaseModel):
48
+ template_type: str
49
+ title: str
50
+ authors: str
51
+ abstract: str
52
+ methodology: str
53
+ results: str
54
+ conclusion: str
55
+ figures: list[str]
56
+ latex_code: str
57
+ pdf_path: str | None = None
58
+
59
+
60
+ class PublishResult(BaseModel):
61
+ platform: str
62
+ success: bool
63
+ url: str | None = None
64
+ error: str | None = None
65
+
66
+
67
+ class SlideContent(BaseModel):
68
+ slide_number: int
69
+ title: str
70
+ content: str
71
+ slide_type: str # "title", "content", "image", "diagram", "conclusion"
72
+ notes: str | None = None
73
+ tikz_diagrams: list[str] = [] # LaTeX tikz code for diagrams
74
+
75
+
76
+ class TikzDiagram(BaseModel):
77
+ diagram_id: str
78
+ title: str
79
+ description: str
80
+ tikz_code: str
81
+ diagram_type: str # "flowchart", "graph", "architecture", "timeline", "comparison"
82
+
83
+
84
+ class PresentationPlan(BaseModel):
85
+ total_slides: int
86
+ slides: list[SlideContent]
87
+ suggested_diagrams: list[str] # Descriptions of diagrams to create
88
+ presentation_style: str # "academic", "corporate", "minimal"
89
+
90
+
91
+ class PresentationContent(BaseModel):
92
+ title: str
93
+ authors: str
94
+ institution: str | None = None
95
+ date: str | None = None
96
+ template_type: str
97
+ slides: list[SlideContent]
98
+ tikz_diagrams: list[TikzDiagram]
99
+ latex_code: str
100
+ pdf_path: str | None = None
101
+ total_slides: int
app/services/__init__.py ADDED
File without changes
app/services/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (165 Bytes). View file
 
app/services/__pycache__/blog_image_service.cpython-310.pyc ADDED
Binary file (8.33 kB). View file
 
app/services/__pycache__/devto_service.cpython-310.pyc ADDED
Binary file (3.44 kB). View file
 
app/services/__pycache__/image_service.cpython-310.pyc ADDED
Binary file (7.65 kB). View file
 
app/services/__pycache__/llm_service.cpython-310.pyc ADDED
Binary file (1.97 kB). View file
 
app/services/__pycache__/pdf_service.cpython-310.pyc ADDED
Binary file (3.85 kB). View file
 
app/services/__pycache__/pdf_to_image_service.cpython-310.pyc ADDED
Binary file (3.75 kB). View file
 
app/services/__pycache__/presentation_pdf_to_image_service.cpython-310.pyc ADDED
Binary file (5.28 kB). View file
 
app/services/blog_image_service.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import base64
3
+ import os
4
+ import re
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+ import requests
10
+
11
+ from app.models.schemas import PaperAnalysis
12
+
13
+
14
+ class BlogImageService:
15
+ """Service for generating and managing images for blog posts"""
16
+
17
+ def __init__(self):
18
+ self.deepinfra_model = "black-forest-labs/FLUX-1-dev"
19
+ self.output_dir = Path("outputs/images/blog")
20
+ self.output_dir.mkdir(parents=True, exist_ok=True)
21
+ self.upload_api_key = (
22
+ "6d207e02198a847aa98d0a2a901485a5" # FreeImage.host API key
23
+ )
24
+
25
+ async def generate_blog_images(
26
+ self, analysis: PaperAnalysis, content: str
27
+ ) -> List[str]:
28
+ """Generate multiple images for a blog post and return markdown image tags"""
29
+ try:
30
+ # Generate image prompts based on the blog content
31
+ image_prompts = await self._generate_image_prompts(analysis, content)
32
+
33
+ # Generate images using DeepInfra
34
+ image_urls = await self._generate_images_async(image_prompts)
35
+
36
+ # Create markdown image tags with proper formatting
37
+ markdown_images = []
38
+ for i, url in enumerate(image_urls):
39
+ if url and url != "No image URL found":
40
+ caption = self._generate_image_caption(analysis, i)
41
+ markdown_images.append(f"![{caption}]({url})")
42
+
43
+ return markdown_images
44
+
45
+ except Exception as e:
46
+ print(f"Error generating blog images: {e}")
47
+ return []
48
+
49
+ async def _generate_image_prompts(
50
+ self, analysis: PaperAnalysis, content: str
51
+ ) -> List[str]:
52
+ """Generate image prompts based on the research paper and blog content"""
53
+ from app.services.llm_service import LLMService
54
+
55
+ llm_service = LLMService()
56
+
57
+ prompt_generation_text = f"""
58
+ You are an expert in creating visual prompts for AI image generation to enhance blog posts about research papers.
59
+
60
+ Research Paper Details:
61
+ Title: {analysis.title}
62
+ Abstract: {analysis.abstract[:300]}...
63
+ Key Findings: {", ".join(analysis.key_findings[:3])}
64
+ Methodology: {analysis.methodology[:200]}...
65
+
66
+ Blog Content Preview: {content[:500]}...
67
+
68
+ Create 2-3 detailed visual prompts for FLUX image generation that would enhance this blog post.
69
+ Each prompt should:
70
+ 1. Be scientifically accurate and relevant to the research
71
+ 2. Create visually appealing, professional diagrams or illustrations
72
+ 3. Help explain complex concepts through visual metaphors
73
+ 4. Be suitable for a blog audience (clear, engaging, informative)
74
+
75
+ Generate prompts for:
76
+ 1. A main concept illustration (abstract/conceptual)
77
+ 2. A methodology visualization (process/workflow)
78
+ 3. A results/findings/use-case illustration (if applicable)
79
+
80
+ Each prompt should be highly detailed, incorporating the elements that needed along with the details like colors, style, textures, background details, and also mention the correct position of the elements in the image.
81
+
82
+ Return only the prompts, one per line, without numbering or additional text.
83
+ """
84
+
85
+ messages = [
86
+ {
87
+ "role": "system",
88
+ "content": "You are an expert visual designer specializing in scientific and educational imagery for blog posts.",
89
+ },
90
+ {"role": "user", "content": prompt_generation_text},
91
+ ]
92
+
93
+ response = await llm_service.generate_completion(
94
+ messages=messages,
95
+ model_type="light",
96
+ temperature=0.7,
97
+ )
98
+
99
+ # Parse the response to extract individual prompts
100
+ prompts = [prompt.strip() for prompt in response.split("\n") if prompt.strip()]
101
+
102
+ # Enhance prompts with style guidelines
103
+ enhanced_prompts = []
104
+ for prompt in prompts[:3]: # Limit to 3 images
105
+ enhanced_prompt = f"{prompt}, professional scientific illustration, clean modern design, educational diagram style, high quality, detailed"
106
+ enhanced_prompts.append(enhanced_prompt)
107
+
108
+ return enhanced_prompts
109
+
110
+ async def _generate_images_async(self, prompts: List[str]) -> List[str]:
111
+ """Generate images asynchronously using DeepInfra API"""
112
+ loop = asyncio.get_event_loop()
113
+
114
+ # Use ThreadPoolExecutor to handle blocking requests
115
+ with ThreadPoolExecutor(max_workers=3) as executor:
116
+ futures = [
117
+ loop.run_in_executor(executor, self._fetch_image_url, prompt)
118
+ for prompt in prompts
119
+ ]
120
+
121
+ results = await asyncio.gather(*futures, return_exceptions=True)
122
+
123
+ # Process results and upload images
124
+ upload_futures = []
125
+ for result in results:
126
+ if isinstance(result, str) and result != "No image URL found":
127
+ upload_futures.append(
128
+ loop.run_in_executor(
129
+ executor, self._process_and_upload_image, result
130
+ ),
131
+ )
132
+
133
+ if upload_futures:
134
+ uploaded_urls = await asyncio.gather(
135
+ *upload_futures, return_exceptions=True
136
+ )
137
+ return [
138
+ url if not isinstance(url, Exception) else "No image URL found"
139
+ for url in uploaded_urls
140
+ ]
141
+
142
+ return []
143
+
144
+ def _fetch_image_url(self, prompt: str) -> str:
145
+ """Fetch image from DeepInfra API (synchronous)"""
146
+ url = f"https://api.deepinfra.com/v1/inference/{self.deepinfra_model}"
147
+ headers = {
148
+ "Content-Type": "application/json",
149
+ "Authorization": f"bearer {os.getenv('DEEPINFRA_API_KEY')}",
150
+ }
151
+ payload = {
152
+ "prompt": prompt,
153
+ "num_inference_steps": 20,
154
+ "width": 1024,
155
+ "height": 768, # Better aspect ratio for blog images
156
+ }
157
+
158
+ try:
159
+ response = requests.post(url, json=payload, headers=headers, timeout=60)
160
+ if response.status_code == 200:
161
+ response_data = response.json()
162
+ images = response_data.get("images", [])
163
+ return images[0] if images else "No image URL found"
164
+ print(f"Request failed: {response.status_code}, {response.text}")
165
+ return "No image URL found"
166
+ except Exception as e:
167
+ print(f"An error occurred: {e!s}")
168
+ return "No image URL found"
169
+
170
+ def _process_and_upload_image(self, base64_string: str) -> str:
171
+ """Process base64 image and upload to hosting service (synchronous)"""
172
+ try:
173
+ # Save image locally first
174
+ image_data = base64.b64decode(base64_string.split(",")[-1])
175
+ temp_filename = f"temp_blog_image_{hash(base64_string) % 1000000}.png"
176
+ temp_path = self.output_dir / temp_filename
177
+
178
+ with open(temp_path, "wb") as image_file:
179
+ image_file.write(image_data)
180
+
181
+ # Upload to hosting service
182
+ url = "https://freeimage.host/api/1/upload"
183
+
184
+ with open(temp_path, "rb") as image_file:
185
+ base64_image = base64.b64encode(image_file.read()).decode("utf-8")
186
+
187
+ payload = {
188
+ "key": self.upload_api_key,
189
+ "action": "upload",
190
+ "source": base64_image,
191
+ "format": "json",
192
+ }
193
+
194
+ response = requests.post(url, data=payload, timeout=60)
195
+
196
+ if response.status_code == 200:
197
+ response_data = response.json()
198
+ hosted_url = response_data.get("image", {}).get(
199
+ "url", "No image URL found"
200
+ )
201
+
202
+ # Clean up temp file
203
+ try:
204
+ temp_path.unlink()
205
+ except:
206
+ pass
207
+
208
+ return hosted_url
209
+ print(f"Upload failed: {response.status_code}, {response.text}")
210
+ return "No image URL found"
211
+
212
+ except Exception as e:
213
+ print(f"Error processing/uploading image: {e!s}")
214
+ return "No image URL found"
215
+
216
+ def _generate_image_caption(self, analysis: PaperAnalysis, image_index: int) -> str:
217
+ """Generate appropriate captions for images"""
218
+ captions = [
219
+ f"Conceptual illustration of {analysis.title[:50]}...",
220
+ f"Methodology visualization for {analysis.title[:50]}...",
221
+ f"Key findings illustration from {analysis.title[:50]}...",
222
+ ]
223
+
224
+ if image_index < len(captions):
225
+ return captions[image_index]
226
+ return f"Research illustration from {analysis.title[:50]}..."
227
+
228
+ async def embed_images_in_content(self, content: str, images: List[str]) -> str:
229
+ """Embed images into blog content at appropriate locations"""
230
+ if not images:
231
+ return content
232
+
233
+ # Split content into sections (by headers)
234
+ sections = re.split(r"(^#{1,3}\s+.*$)", content, flags=re.MULTILINE)
235
+
236
+ # Insert images at strategic points
237
+ enhanced_content = []
238
+ image_index = 0
239
+
240
+ for i, section in enumerate(sections):
241
+ enhanced_content.append(section)
242
+
243
+ # Add image after introduction (first section after first header)
244
+ if (
245
+ (i == 2 and image_index < len(images))
246
+ or (i == len(sections) // 2 and image_index < len(images))
247
+ or (i == len(sections) - 3 and image_index < len(images))
248
+ ):
249
+ enhanced_content.append(f"\n\n{images[image_index]}\n\n")
250
+ image_index += 1
251
+
252
+ return "".join(enhanced_content)
253
+
254
+
255
+ # Global instance
256
+ blog_image_service = BlogImageService()
app/services/devto_service.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from typing import Any, Dict
3
+
4
+ import requests
5
+
6
+ from app.config.settings import settings
7
+ from app.models.schemas import BlogContent
8
+
9
+
10
+ class DevToService:
11
+ def __init__(self):
12
+ self.api_key = settings.DEVTO_API_KEY
13
+ self.base_url = "https://dev.to/api"
14
+
15
+ async def publish_article(
16
+ self,
17
+ blog_content: BlogContent,
18
+ publish_now: bool = False,
19
+ ) -> Dict[str, Any]:
20
+ """Publish article to DEV.to"""
21
+
22
+ def _sync_publish():
23
+ try:
24
+ # Check if API key is configured
25
+ if not self.api_key:
26
+ return {
27
+ "success": False,
28
+ "error": "DEV.to API key is not configured. Please set the DEVTO_API_KEY environment variable.",
29
+ }
30
+
31
+ headers = {
32
+ "api-key": self.api_key,
33
+ "Content-Type": "application/json",
34
+ }
35
+
36
+ # Ensure tags is a list of strings
37
+ tags = blog_content.tags if isinstance(blog_content.tags, list) else []
38
+ # DEV.to has a limit of 4 tags max
39
+ tags = tags[:4] if len(tags) > 4 else tags
40
+
41
+ article_data = {
42
+ "article": {
43
+ "title": blog_content.title,
44
+ "body_markdown": blog_content.content,
45
+ "published": publish_now,
46
+ "tags": tags,
47
+ "description": blog_content.meta_description,
48
+ },
49
+ }
50
+
51
+ # Only add series if it's not empty
52
+ if hasattr(blog_content, "series") and blog_content.series:
53
+ article_data["article"]["series"] = blog_content.series
54
+
55
+ print("=== DEV.to Publish Debug Info ===")
56
+ print(f"API Key present: {bool(self.api_key)}")
57
+ print(f"API Key length: {len(self.api_key) if self.api_key else 0}")
58
+ print(f"Title: {blog_content.title}")
59
+ print(f"Tags: {tags}")
60
+ print(f"Content length: {len(blog_content.content)}")
61
+ print(f"Description: {blog_content.meta_description}")
62
+ print("Article data structure:")
63
+ print(article_data)
64
+
65
+ response = requests.post(
66
+ f"{self.base_url}/articles",
67
+ json=article_data,
68
+ headers=headers,
69
+ timeout=30, # Add timeout
70
+ )
71
+ response.raise_for_status()
72
+ result = response.json()
73
+
74
+ return {
75
+ "success": True,
76
+ "url": result.get("url"),
77
+ "id": result.get("id"),
78
+ "published": result.get("published", False),
79
+ }
80
+
81
+ except requests.exceptions.HTTPError as e:
82
+ # Get the detailed error response from DEV.to
83
+ error_details = "Unknown error"
84
+ if hasattr(e, "response") and e.response is not None:
85
+ try:
86
+ error_details = e.response.json()
87
+ except:
88
+ error_details = e.response.text
89
+
90
+ print(f"DEV.to API Error: {e}")
91
+ print(f"Error details: {error_details}")
92
+
93
+ return {
94
+ "success": False,
95
+ "error": f"DEV.to API Error: {e!s} - Details: {error_details}",
96
+ }
97
+ except Exception as e:
98
+ print(f"General error: {e}")
99
+ return {
100
+ "success": False,
101
+ "error": str(e),
102
+ }
103
+
104
+ # Run the synchronous function in a thread pool
105
+ loop = asyncio.get_event_loop()
106
+ return await loop.run_in_executor(None, _sync_publish)
107
+
108
+ async def get_my_articles(self, per_page: int = 10) -> Dict[str, Any]:
109
+ """Get user's published articles"""
110
+
111
+ def _sync_get_articles():
112
+ try:
113
+ headers = {
114
+ "api-key": self.api_key,
115
+ }
116
+
117
+ response = requests.get(
118
+ f"{self.base_url}/articles/me/published?per_page={per_page}",
119
+ headers=headers,
120
+ timeout=30, # Add timeout
121
+ )
122
+ response.raise_for_status()
123
+ result = response.json()
124
+ return {"success": True, "articles": result}
125
+ except Exception as e:
126
+ return {"success": False, "error": str(e)}
127
+
128
+ # Run the synchronous function in a thread pool
129
+ loop = asyncio.get_event_loop()
130
+ return await loop.run_in_executor(None, _sync_get_articles)
131
+
132
+
133
+ devto_service = DevToService()