Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- README.md +309 -5
- app.py +33 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-310.pyc +0 -0
- app/agents/__init__.py +0 -0
- app/agents/__pycache__/__init__.cpython-310.pyc +0 -0
- app/agents/__pycache__/base_agent.cpython-310.pyc +0 -0
- app/agents/__pycache__/blog_generator.cpython-310.pyc +0 -0
- app/agents/__pycache__/paper_analyzer.cpython-310.pyc +0 -0
- app/agents/__pycache__/poster_generator.cpython-310.pyc +0 -0
- app/agents/__pycache__/poster_layout_analyzer.cpython-310.pyc +0 -0
- app/agents/__pycache__/presentation_generator.cpython-310.pyc +0 -0
- app/agents/__pycache__/presentation_planner.cpython-310.pyc +0 -0
- app/agents/__pycache__/presentation_visual_analyzer.cpython-310.pyc +0 -0
- app/agents/__pycache__/tikz_diagram_generator.cpython-310.pyc +0 -0
- app/agents/__pycache__/tldr_generator.cpython-310.pyc +0 -0
- app/agents/base_agent.py +23 -0
- app/agents/blog_generator.py +221 -0
- app/agents/paper_analyzer.py +87 -0
- app/agents/poster_generator.py +268 -0
- app/agents/poster_layout_analyzer.py +166 -0
- app/agents/presentation_generator.py +337 -0
- app/agents/presentation_planner.py +209 -0
- app/agents/presentation_visual_analyzer.py +316 -0
- app/agents/publisher.py +6 -0
- app/agents/tikz_diagram_generator.py +162 -0
- app/agents/tldr_generator.py +172 -0
- app/config/__init__.py +0 -0
- app/config/__pycache__/__init__.cpython-310.pyc +0 -0
- app/config/__pycache__/settings.cpython-310.pyc +0 -0
- app/config/settings.py +61 -0
- app/database/__init__.py +0 -0
- app/database/database.py +26 -0
- app/database/models.py +48 -0
- app/main.py +563 -0
- app/models/__init__.py +0 -0
- app/models/__pycache__/__init__.cpython-310.pyc +0 -0
- app/models/__pycache__/schemas.cpython-310.pyc +0 -0
- app/models/schemas.py +101 -0
- app/services/__init__.py +0 -0
- app/services/__pycache__/__init__.cpython-310.pyc +0 -0
- app/services/__pycache__/blog_image_service.cpython-310.pyc +0 -0
- app/services/__pycache__/devto_service.cpython-310.pyc +0 -0
- app/services/__pycache__/image_service.cpython-310.pyc +0 -0
- app/services/__pycache__/llm_service.cpython-310.pyc +0 -0
- app/services/__pycache__/pdf_service.cpython-310.pyc +0 -0
- app/services/__pycache__/pdf_to_image_service.cpython-310.pyc +0 -0
- app/services/__pycache__/presentation_pdf_to_image_service.cpython-310.pyc +0 -0
- app/services/blog_image_service.py +256 -0
- app/services/devto_service.py +133 -0
README.md
CHANGED
@@ -1,12 +1,316 @@
|
|
1 |
---
|
2 |
title: ScholarShare
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
-
sdk_version: 5.
|
8 |
app_file: app.py
|
9 |
pinned: false
|
|
|
|
|
|
|
|
|
10 |
---
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
[](https://www.python.org/downloads/)
|
19 |
+
[](https://opensource.org/licenses/MIT)
|
20 |
+
[](http://makeapullrequest.com)
|
21 |
+
[](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"")
|
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()
|