lokesh341 commited on
Commit
7c94bbc
ยท
verified ยท
1 Parent(s): b033288

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +206 -170
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # app.py - Enhanced PDF & Video Generator (HD Video with Names Below Images)
2
 
3
  import gradio as gr
4
  from fpdf import FPDF
@@ -6,237 +6,273 @@ import os
6
  import tempfile
7
  import re
8
  from datetime import datetime
9
- from moviepy.editor import ImageClip, concatenate_videoclips, TextClip, CompositeVideoClip
10
- from PIL import Image # For image processing if needed
11
 
12
- class SimplePDF(FPDF):
 
 
 
 
13
  def footer(self):
14
  self.set_y(-15)
15
  self.set_font('Arial', 'I', 8)
16
- self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C')
17
 
18
- def clean_text(text, max_length=100):
19
- """Clean text for PDF and video compatibility"""
20
  if not text:
21
- return ""
 
 
22
  replacements = {
23
  'โ€“': '-', 'โ€”': '-', 'โ€œ': '"', 'โ€': '"', 'โ€˜': "'", 'โ€™': "'",
24
- 'โ€ข': '-', 'ยฐ': 'ยฐ', 'โ„ข': 'TM', 'ยฎ': 'R', 'ยฉ': 'C',
25
- 'โ‚ฌ': 'EUR', 'ยฃ': 'GBP', 'ยฅ': 'JPY'
26
  }
 
27
  for old, new in replacements.items():
28
- text = text.replace(old, new)
29
- text = re.sub(r'[^\w\s\-.,!?\(\)\[\]:;0-9ยฐ]', '', text)
30
- text = ' '.join(text.split())[:max_length]
31
- return text.strip()
 
 
32
 
33
- def generate_outputs(title, plan, names, image_files, duration_per_slide):
34
- # Validation
35
  if not title or not title.strip():
36
- return None, None, "โŒ Title required!"
37
 
38
  names_list = [n.strip() for n in names.split(',') if n.strip()]
39
 
40
  if not names_list:
41
- return None, None, "โŒ At least one name needed!"
 
 
 
42
 
43
  if not image_files:
44
- return None, None, "โŒ Images required!"
45
 
46
  if len(names_list) != len(image_files):
47
- return None, None, f"โŒ {len(names_list)} names vs {len(image_files)} images"
48
-
49
- # Limit to 30 items max
50
- if len(names_list) > 30:
51
- return None, None, "โŒ Max 30 items allowed!"
52
 
53
  try:
54
- # PDF Generation
55
- pdf = SimplePDF()
56
- pdf.set_margins(15, 15, 15)
57
 
58
- clean_title = clean_text(title)
59
- clean_plan = clean_text(plan, 400)
 
60
 
 
61
  pdf.add_page()
62
- pdf.set_font('Arial', 'B', 20)
63
- pdf.cell(0, 15, clean_title, ln=1, align='C')
 
64
 
65
- if clean_plan:
66
- pdf.ln(10)
67
- pdf.set_font('Arial', '', 11)
68
- pdf.multi_cell(0, 7, clean_plan)
69
 
70
- pdf.ln(10)
71
- pdf.set_font('Arial', 'I', 9)
72
- pdf.cell(0, 8, f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", ln=1, align='C')
73
- pdf.cell(0, 8, f"Participants: {len(names_list)}", ln=1, align='C')
74
 
 
75
  for i, (name, img_path) in enumerate(zip(names_list, image_files)):
76
  pdf.add_page()
77
- clean_name = clean_text(name, 40)
78
- pdf.set_font('Arial', 'B', 16)
79
- pdf.cell(0, 12, clean_name.upper(), ln=1, align='C')
80
 
81
- pdf.set_font('Arial', '', 11)
82
- pdf.cell(0, 8, f"ID: {i+1}/{len(names_list)}", ln=1, align='C')
83
 
84
- image_added = False
85
- if os.path.exists(img_path):
86
- try:
87
- img_width = 100
88
- x_pos = (pdf.w - img_width) / 2
89
- pdf.image(img_path, x=x_pos, y=40, w=img_width)
90
- image_added = True
91
- except:
92
- pass
93
-
94
- if not image_added:
95
- pdf.set_font('Arial', '', 12)
96
- pdf.cell(0, 10, "[IMAGE PLACEHOLDER]", ln=1, align='C')
97
-
98
- pdf_path = f"pdf_{int(datetime.now().timestamp())}.pdf"
99
- pdf.output(pdf_path)
100
-
101
- # Video Generation - Full HD 1920x1080
102
- clips = []
103
- hd_width, hd_height = 1920, 1080
104
- text_height = 150 # Space for name below image
105
-
106
- for i, (name, img_path) in enumerate(zip(names_list, image_files)):
107
- clean_name = clean_text(name, 40)
108
 
109
- # Load image
110
- if os.path.exists(img_path):
111
- try:
112
- # Create image clip
113
- img_clip = ImageClip(img_path).resize(height=hd_height - text_height - 50) # Resize to fit above text
114
- img_clip = img_clip.set_position(("center", 25)) # Position at top with margin
 
 
115
 
116
- # Text clip for name below
117
- text_clip = TextClip(
118
- f"{clean_name.upper()}\nID: {i+1}",
119
- fontsize=80, color='white', font='Arial-Bold',
120
- size=(hd_width, text_height), bg_color='black', method='caption'
121
- ).set_position(("center", hd_height - text_height - 25))
122
 
123
- # Composite
124
- composite = CompositeVideoClip([img_clip, text_clip], size=(hd_width, hd_height))
125
- composite = composite.set_duration(duration_per_slide)
126
- clips.append(composite)
127
- except Exception as e:
128
- # Fallback placeholder clip
129
- text_clip = TextClip(
130
- f"{clean_name.upper()}\nID: {i+1}\n[Image Not Available]",
131
- fontsize=60, color='white', bg_color='black'
132
- ).set_duration(duration_per_slide)
133
- clips.append(text_clip)
134
- else:
135
- # Placeholder if no image
136
- text_clip = TextClip(
137
- f"{clean_name.upper()}\nID: {i+1}\n[No Image]",
138
- fontsize=60, color='white', bg_color='black'
139
- ).set_duration(duration_per_slide)
140
- clips.append(text_clip)
141
-
142
- # Concatenate all clips into video
143
- video = concatenate_videoclips(clips, method="compose")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
- # Add title intro slide if title
146
- if clean_title:
147
- title_clip = TextClip(
148
- clean_title.upper(),
149
- fontsize=100, color='white', bg_color='blue',
150
- size=(hd_width, hd_height)
151
- ).set_duration(3) # 3 sec intro
152
- video = concatenate_videoclips([title_clip, video])
153
 
154
- video_path = f"video_{int(datetime.now().timestamp())}.mp4"
155
- video.write_videofile(video_path, fps=24, codec='libx264', audio=False) # HD silent video
 
 
 
 
 
 
156
 
157
- success_msg = f"โœ… Generated!\n๐Ÿ“„ PDF & ๐ŸŽฅ HD Video (1920x1080)\n๐Ÿ‘ฅ {len(names_list)} items (IDs 1 to {len(names_list)})"
158
- return pdf_path, video_path, success_msg
159
 
160
  except Exception as e:
161
- return None, None, f"โŒ Error: {str(e)}"
162
 
163
- # Enhanced UI/UX with Better Layout & Instructions
164
  with gr.Blocks(
165
- theme=gr.themes.Soft(primary_hue="blue", secondary_hue="gray", neutral_hue="slate"),
 
 
 
 
 
166
  css="""
167
- .gradio-container {max-width: 1200px; margin: auto; padding: 20px;}
168
- .input-label {font-weight: bold; font-size: 1.2em;}
169
- .output-section {margin-top: 20px; padding: 15px; background: #f0f4f8; border-radius: 10px;}
170
- .status {font-size: 1.1em; color: #333;}
 
 
 
171
  """
172
  ) as demo:
173
- gr.Markdown(
174
- """
175
- # ๐Ÿš€ HD PDF & Video Generator
176
- **Create Professional PDF & Full HD Video Slideshow**
177
-
178
- ### Instructions:
179
- - **Title**: Document/Video title (required)
180
- - **Plan**: Optional description
181
- - **Names**: Comma-separated (e.g., "John Doe, Jane Smith") - Max 30
182
- - **Images**: Upload matching images (uploaded below names input for UX)
183
- - **Slide Duration**: Seconds per slide in video
184
- - Video: Full HD 1920x1080, name below image, IDs 1 to N
185
- - Click Generate for PDF & Video downloads
186
- """
187
- )
188
 
189
- with gr.Row(equal_height=True):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  with gr.Column(scale=2):
191
  title_input = gr.Textbox(
192
- label="๐Ÿ“› Title (Required)",
193
- placeholder="Enter title here...",
194
- lines=1,
195
- elem_classes="input-label"
196
- )
197
- plan_input = gr.Textbox(
198
- label="๐Ÿ“‹ Plan/Description (Optional)",
199
- placeholder="Enter details here...",
200
- lines=4
201
  )
 
202
  names_input = gr.Textbox(
203
- label="๐Ÿ‘ฅ Names (Comma-Separated, Required)",
204
- placeholder="Name1, Name2, Name3 (up to 30)",
205
- lines=3
 
206
  )
207
- images_input = gr.File(
208
- label="๐Ÿ–ผ๏ธ Upload Images (Match Names Count)",
209
- file_count="multiple",
210
- file_types=["image"]
 
 
211
  )
212
 
213
  with gr.Column(scale=1):
214
- duration_input = gr.Slider(
215
- minimum=1,
216
- maximum=30,
217
- step=1,
218
- value=5,
219
- label="โฑ๏ธ Duration per Slide (Seconds)"
220
  )
221
- generate_btn = gr.Button("๐Ÿš€ Generate PDF & Video", variant="primary", size="lg")
222
- clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear All", variant="secondary")
223
 
224
- with gr.Row(elem_classes="output-section"):
225
- pdf_output = gr.File(label="๐Ÿ“„ Download PDF")
226
- video_output = gr.File(label="๐ŸŽฅ Download HD Video (MP4)")
 
227
 
228
- status_output = gr.Markdown("Ready to generate...", elem_classes="status")
 
 
 
 
 
 
 
 
229
 
230
- # Event handlers
231
  generate_btn.click(
232
- fn=generate_outputs,
233
- inputs=[title_input, plan_input, names_input, images_input, duration_input],
234
- outputs=[pdf_output, video_output, status_output]
235
  )
236
 
237
  clear_btn.click(
238
- fn=lambda: ("", "", "", None, 5, None, None, "Cleared!"),
239
- outputs=[title_input, plan_input, names_input, images_input, duration_input, pdf_output, video_output, status_output]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  )
241
 
242
  if __name__ == "__main__":
 
1
+ # app.py - PDF Generator ONLY (No MoviePy - HF Spaces Compatible)
2
 
3
  import gradio as gr
4
  from fpdf import FPDF
 
6
  import tempfile
7
  import re
8
  from datetime import datetime
9
+ from pathlib import Path
10
+ import io
11
 
12
+ class ProfessionalPDF(FPDF):
13
+ def header(self):
14
+ self.set_font('Arial', 'B', 12)
15
+ self.cell(0, 10, 'Professional Document', 0, 1, 'C')
16
+
17
  def footer(self):
18
  self.set_y(-15)
19
  self.set_font('Arial', 'I', 8)
20
+ self.cell(0, 10, f'Page {self.page_no()} | Generated {datetime.now().strftime("%Y-%m-%d")}', 0, 0, 'C')
21
 
22
+ def clean_text_for_pdf(text, max_length=100):
23
+ """Clean text to avoid PDF encoding issues"""
24
  if not text:
25
+ return "Untitled"
26
+
27
+ # Replace problematic characters
28
  replacements = {
29
  'โ€“': '-', 'โ€”': '-', 'โ€œ': '"', 'โ€': '"', 'โ€˜': "'", 'โ€™': "'",
30
+ 'โ€ข': '*', 'ยฐ': 'deg', 'โ„ข': 'TM', 'ยฎ': 'R', 'ยฉ': 'C',
31
+ 'โ‚ฌ': 'EUR', 'ยฃ': 'GBP', 'ยฅ': 'JPY', '\n': ' '
32
  }
33
+
34
  for old, new in replacements.items():
35
+ text = str(text).replace(old, new)
36
+
37
+ # Remove remaining special chars
38
+ text = re.sub(r'[^\w\s\-.,!?\(\)\[\]:;0-9]', '', text)
39
+ text = re.sub(r'\s+', ' ', text) # Normalize whitespace
40
+ return text.strip()[:max_length]
41
 
42
+ def generate_pdf(title, plan, names, image_files):
43
+ # Fast validation
44
  if not title or not title.strip():
45
+ return None, "โŒ Title is required!"
46
 
47
  names_list = [n.strip() for n in names.split(',') if n.strip()]
48
 
49
  if not names_list:
50
+ return None, "โŒ Enter at least one name!"
51
+
52
+ if len(names_list) > 30:
53
+ return None, "โŒ Maximum 30 names allowed!"
54
 
55
  if not image_files:
56
+ return None, "โŒ Upload images (matching names count)!"
57
 
58
  if len(names_list) != len(image_files):
59
+ return None, f"โŒ Mismatch: {len(names_list)} names, {len(image_files)} images"
 
 
 
 
60
 
61
  try:
62
+ pdf = ProfessionalPDF()
63
+ pdf.set_margins(20, 20, 20)
64
+ pdf.set_auto_page_break(auto=True, margin=20)
65
 
66
+ # Clean inputs
67
+ clean_title = clean_text_for_pdf(title, 80)
68
+ clean_plan = clean_text_for_pdf(plan, 800)
69
 
70
+ # COVER PAGE
71
  pdf.add_page()
72
+ pdf.set_font('Arial', 'B', 28)
73
+ pdf.set_text_color(0, 51, 102) # Dark blue
74
+ pdf.cell(0, 20, clean_title, ln=1, align='C')
75
 
76
+ pdf.ln(15)
77
+ pdf.set_font('Arial', '', 14)
78
+ pdf.set_text_color(0, 0, 0)
79
+ pdf.multi_cell(0, 10, clean_plan)
80
 
81
+ pdf.ln(20)
82
+ pdf.set_font('Arial', 'B', 12)
83
+ pdf.cell(0, 10, f"TOTAL PARTICIPANTS: {len(names_list)}", ln=1, align='C')
84
+ pdf.cell(0, 10, f"GENERATED: {datetime.now().strftime('%Y-%m-%d %H:%M')}", ln=1, align='C')
85
 
86
+ # INDIVIDUAL PAGES (Image UP, Name DOWN)
87
  for i, (name, img_path) in enumerate(zip(names_list, image_files)):
88
  pdf.add_page()
 
 
 
89
 
90
+ # Clean name
91
+ clean_name = clean_text_for_pdf(name, 50)
92
 
93
+ # HEADER WITH ID
94
+ pdf.set_font('Arial', 'B', 18)
95
+ pdf.set_text_color(51, 51, 153) # Blue
96
+ pdf.cell(0, 15, f"PARTICIPANT #{i+1}", ln=1, align='C')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ # IMAGE SECTION (UPPER PART)
99
+ pdf.ln(10)
100
+ try:
101
+ if os.path.exists(img_path):
102
+ # Calculate centered position
103
+ img_width = 140
104
+ page_width = pdf.w - 40 # Account for margins
105
+ x_pos = (page_width - img_width) / 2 + 20
106
 
107
+ pdf.image(img_path, x=x_pos, y=pdf.get_y(), w=img_width, h=180)
 
 
 
 
 
108
 
109
+ # Image border
110
+ pdf.set_draw_color(200, 200, 200)
111
+ pdf.line(x_pos, pdf.get_y(), x_pos + img_width, pdf.get_y())
112
+ pdf.line(x_pos + img_width, pdf.get_y(), x_pos + img_width, pdf.get_y() + 180)
113
+ pdf.line(x_pos + img_width, pdf.get_y() + 180, x_pos, pdf.get_y() + 180)
114
+ pdf.line(x_pos, pdf.get_y() + 180, x_pos, pdf.get_y())
115
+
116
+ else:
117
+ # Placeholder rectangle
118
+ img_width = 140
119
+ x_pos = (pdf.w - img_width) / 2
120
+ pdf.set_fill_color(240, 240, 240)
121
+ pdf.rect(x_pos, pdf.get_y(), img_width, 180, 'F')
122
+ pdf.set_font('Arial', '', 12)
123
+ pdf.set_xy(x_pos + 10, pdf.get_y() + 80)
124
+ pdf.multi_cell(img_width - 20, 10, "IMAGE\nNOT\nFOUND", align='C')
125
+
126
+ except Exception:
127
+ # Fallback placeholder
128
+ img_width = 140
129
+ x_pos = (pdf.w - img_width) / 2
130
+ pdf.set_fill_color(240, 240, 240)
131
+ pdf.rect(x_pos, pdf.get_y(), img_width, 180, 'F')
132
+ pdf.set_font('Arial', '', 12)
133
+ pdf.set_xy(x_pos + 10, pdf.get_y() + 80)
134
+ pdf.multi_cell(img_width - 20, 10, "IMAGE\nERROR", align='C')
135
+
136
+ # NAME SECTION (LOWER PART - BELOW IMAGE)
137
+ pdf.ln(200) # Move below image area
138
+ pdf.set_font('Arial', 'B', 22)
139
+ pdf.set_text_color(0, 0, 0)
140
+ pdf.cell(0, 20, clean_name.upper(), ln=1, align='C')
141
+
142
+ pdf.set_font('Arial', '', 12)
143
+ pdf.set_text_color(100, 100, 100)
144
+ pdf.cell(0, 10, f"Participant ID: {i+1} of {len(names_list)}", ln=1, align='C')
145
+ pdf.cell(0, 10, f"Generated: {datetime.now().strftime('%d/%m/%Y')}", ln=1, align='C')
146
 
147
+ # Output to file directly
148
+ timestamp = int(datetime.now().timestamp())
149
+ pdf_path = f"professional_pdf_{timestamp}.pdf"
150
+ pdf.output(pdf_path)
 
 
 
 
151
 
152
+ success_msg = f"""
153
+ โœ… **PDF Generated Successfully!**
154
+ ๐Ÿ“„ **Title**: {clean_title[:50]}...
155
+ ๐Ÿ‘ฅ **{len(names_list)} Participants** (IDs 1-{len(names_list)})
156
+ ๐Ÿ“‘ **{len(names_list) + 1} Pages** (Cover + Participants)
157
+ ๐Ÿ–ผ๏ธ **Images**: UP | **Names**: DOWN layout
158
+ ๐Ÿ’พ Ready for download!
159
+ """
160
 
161
+ return pdf_path, success_msg
 
162
 
163
  except Exception as e:
164
+ return None, f"โŒ Error: {str(e)}\nPlease check your inputs and try again."
165
 
166
+ # Professional UI/UX with Full HD View Support
167
  with gr.Blocks(
168
+ title="Professional PDF Generator",
169
+ theme=gr.themes.Soft(
170
+ primary_hue="blue",
171
+ secondary_hue="gray",
172
+ neutral_hue="stone"
173
+ ),
174
  css="""
175
+ .gradio-container {max-width: 1400px !important; margin: auto;}
176
+ .header {background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; text-align: center;}
177
+ .input-section {background: #f8fafc; padding: 20px; border-radius: 10px; margin: 10px 0; border-left: 4px solid #3b82f6;}
178
+ .status-success {background: #dcfce7; color: #166534; padding: 15px; border-radius: 8px; border-left: 4px solid #22c55e;}
179
+ .status-error {background: #fef2f2; color: #dc2626; padding: 15px; border-radius: 8px; border-left: 4px solid #ef4444;}
180
+ .btn-primary {background: linear-gradient(45deg, #3b82f6, #1d4ed8) !important;}
181
+ .output-zone {background: #f1f5f9; padding: 20px; border-radius: 10px; margin-top: 20px;}
182
  """
183
  ) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
+ # Header
186
+ with gr.Row():
187
+ gr.Markdown(
188
+ """
189
+ <div class="header">
190
+ <h1>๐Ÿ“„ Professional PDF Generator</h1>
191
+ <p><strong>Image UP โ€ข Name DOWN โ€ข HD Quality โ€ข 1-30 Participants</strong></p>
192
+ </div>
193
+ """,
194
+ elem_classes="header"
195
+ )
196
+
197
+ gr.Markdown("""
198
+ ### ๐Ÿš€ How to Use:
199
+ 1. **Title**: Enter document title
200
+ 2. **Names**: Comma-separated (max 30): "John Doe, Jane Smith"
201
+ 3. **Images**: Upload matching images
202
+ 4. **Layout**: Each page = Large image (UP) + Name/ID (DOWN)
203
+ 5. **Output**: Professional PDF with cover page
204
+ """)
205
+
206
+ # Inputs
207
+ with gr.Row():
208
  with gr.Column(scale=2):
209
  title_input = gr.Textbox(
210
+ label="๐Ÿ“› Document Title *",
211
+ placeholder="Enter your document title...",
212
+ lines=2,
213
+ elem_classes="input-section"
 
 
 
 
 
214
  )
215
+
216
  names_input = gr.Textbox(
217
+ label="๐Ÿ‘ฅ Names (Comma Separated) *",
218
+ placeholder="John Doe, Jane Smith, Bob Wilson (Max 30)",
219
+ lines=4,
220
+ elem_classes="input-section"
221
  )
222
+
223
+ plan_input = gr.Textbox(
224
+ label="๐Ÿ“ Description/Plan (Optional)",
225
+ placeholder="Add description, plan, or notes for cover page...",
226
+ lines=3,
227
+ elem_classes="input-section"
228
  )
229
 
230
  with gr.Column(scale=1):
231
+ images_input = gr.File(
232
+ label="๐Ÿ–ผ๏ธ Upload Images *",
233
+ file_count="multiple",
234
+ file_types=[".jpg", ".jpeg", ".png", ".webp"],
235
+ elem_classes="input-section"
 
236
  )
 
 
237
 
238
+ # Controls
239
+ with gr.Row():
240
+ generate_btn = gr.Button("๐Ÿš€ Generate Professional PDF", variant="primary", size="lg")
241
+ clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear All", variant="secondary")
242
 
243
+ # Outputs
244
+ with gr.Row(elem_classes="output-zone"):
245
+ pdf_output = gr.File(label="๐Ÿ“ฅ Download Professional PDF")
246
+
247
+ status_output = gr.Markdown("Ready to generate professional PDF...", elem_classes="status-success")
248
+
249
+ # Events
250
+ def clear_all():
251
+ return "", "", "", None, "Form cleared! Ready to create new PDF."
252
 
 
253
  generate_btn.click(
254
+ fn=generate_pdf,
255
+ inputs=[title_input, plan_input, names_input, images_input],
256
+ outputs=[pdf_output, status_output]
257
  )
258
 
259
  clear_btn.click(
260
+ fn=clear_all,
261
+ outputs=[title_input, plan_input, names_input, images_input, status_output]
262
+ )
263
+
264
+ # Real-time name counter
265
+ def count_names(names):
266
+ names_list = [n.strip() for n in names.split(',') if n.strip()]
267
+ count = len(names_list)
268
+ if count > 30:
269
+ return f"โš ๏ธ {count} names (MAX 30 allowed!)"
270
+ return f"โœ… {count} names ready"
271
+
272
+ names_input.change(
273
+ fn=count_names,
274
+ inputs=[names_input],
275
+ outputs=[status_output]
276
  )
277
 
278
  if __name__ == "__main__":