awacke1 commited on
Commit
08570f4
·
verified ·
1 Parent(s): a1c1447

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +290 -0
app.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import re
3
+ import os
4
+ import glob
5
+ import asyncio
6
+ import hashlib
7
+ import base64
8
+ import unicodedata
9
+ import streamlit as st
10
+ from PIL import Image
11
+ import fitz
12
+ import edge_tts
13
+ from reportlab.lib.pagesizes import A4
14
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
15
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
16
+ from reportlab.lib import colors
17
+ from reportlab.pdfbase import pdfmetrics
18
+ from reportlab.pdfbase.ttfonts import TTFont
19
+
20
+ st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
21
+
22
+ def clean_text_for_tts(text):
23
+ # Remove asterisks, pound signs, and emojis from text for audio generation.
24
+ text = re.sub(r'[#*]', '', text)
25
+ emoji_pattern = re.compile("["
26
+ u"\U0001F600-\U0001F64F" # emoticons
27
+ u"\U0001F300-\U0001F5FF" # symbols & pictographs
28
+ u"\U0001F680-\U0001F6FF" # transport & map symbols
29
+ u"\U0001F1E0-\U0001F1FF" # flags
30
+ "]+", flags=re.UNICODE)
31
+ text = emoji_pattern.sub(r'', text)
32
+ return text.strip()
33
+
34
+ def get_file_title_from_markdown(markdown_text):
35
+ # Extract first sizable line (starting with '#' and non-empty) and clean it for use as a file name.
36
+ for line in markdown_text.splitlines():
37
+ if line.strip() and line.lstrip().startswith("#"):
38
+ title = line.lstrip("#").strip()
39
+ title = re.sub(r'[^A-Za-z0-9 ]+', '', title).strip()
40
+ if title:
41
+ return title.replace(" ", "_")
42
+ return "output"
43
+
44
+ async def generate_audio(text, voice, markdown_text):
45
+ # Clean the text and generate a file name based on the markdown title.
46
+ cleaned_text = clean_text_for_tts(text)
47
+ title = get_file_title_from_markdown(markdown_text)
48
+ filename = f"{title}.mp3"
49
+ communicate = edge_tts.Communicate(cleaned_text, voice)
50
+ await communicate.save(filename)
51
+ return filename
52
+
53
+ def get_download_link(file, file_type="mp3"):
54
+ # Generate a base64 download link for a file.
55
+ with open(file, "rb") as f:
56
+ b64 = base64.b64encode(f.read()).decode()
57
+ if file_type == "mp3":
58
+ mime = "audio/mpeg"
59
+ elif file_type == "pdf":
60
+ mime = "application/pdf"
61
+ else:
62
+ mime = "application/octet-stream"
63
+ return f'<a href="data:{mime};base64,{b64}" download="{os.path.basename(file)}">Download {os.path.basename(file)}</a>'
64
+
65
+ def apply_emoji_font(text, emoji_font):
66
+ # Replace emoji characters with HTML font tags using the specified emoji font.
67
+ emoji_pattern = re.compile(
68
+ r"([\U0001F300-\U0001F5FF"
69
+ r"\U0001F600-\U0001F64F"
70
+ r"\U0001F680-\U0001F6FF"
71
+ r"\U0001F700-\U0001F77F"
72
+ r"\U0001F780-\U0001F7FF"
73
+ r"\U0001F800-\U0001F8FF"
74
+ r"\U0001F900-\U0001F9FF"
75
+ r"\U0001FA00-\U0001FA6F"
76
+ r"\U0001FA70-\U0001FAFF"
77
+ r"\u2600-\u26FF"
78
+ r"\u2700-\u27BF]+)"
79
+ )
80
+ def replace_emoji(match):
81
+ emoji = match.group(1)
82
+ emoji = unicodedata.normalize('NFC', emoji)
83
+ return f'<font face="{emoji_font}">{emoji}</font>'
84
+ segments = []
85
+ last_pos = 0
86
+ for match in emoji_pattern.finditer(text):
87
+ start, end = match.span()
88
+ if last_pos < start:
89
+ segments.append(f'<font face="{emoji_font}">{text[last_pos:start]}</font>')
90
+ segments.append(replace_emoji(match))
91
+ last_pos = end
92
+ if last_pos < len(text):
93
+ segments.append(f'<font face="{emoji_font}">{text[last_pos:]}</font>')
94
+ return ''.join(segments)
95
+
96
+ def markdown_to_pdf_content(markdown_text, render_with_bold, auto_bold_numbers):
97
+ # Convert markdown text into a list of lines with optional bold formatting.
98
+ lines = markdown_text.strip().split('\n')
99
+ pdf_content = []
100
+ number_pattern = re.compile(r'^\d+\.\s')
101
+ for line in lines:
102
+ line = line.strip()
103
+ if not line or line.startswith('# '):
104
+ continue
105
+ if render_with_bold:
106
+ line = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', line)
107
+ if auto_bold_numbers and number_pattern.match(line):
108
+ if not (line.startswith("<b>") and line.endswith("</b>")):
109
+ line = f"<b>{line}</b>"
110
+ pdf_content.append(line)
111
+ total_lines = len(pdf_content)
112
+ return pdf_content, total_lines
113
+
114
+ def create_pdf(markdown_text, base_font_size, render_with_bold, auto_bold_numbers, enlarge_numbered, num_columns, emoji_font):
115
+ buffer = io.BytesIO()
116
+ page_width = A4[0] * 2
117
+ page_height = A4[1]
118
+ doc = SimpleDocTemplate(buffer, pagesize=(page_width, page_height), leftMargin=36, rightMargin=36, topMargin=36, bottomMargin=36)
119
+ styles = getSampleStyleSheet()
120
+ spacer_height = 10
121
+ section_spacer_height = 15
122
+ pdf_content, total_lines = markdown_to_pdf_content(markdown_text, render_with_bold, auto_bold_numbers)
123
+ item_style = ParagraphStyle(
124
+ 'ItemStyle', parent=styles['Normal'], fontName="DejaVuSans",
125
+ fontSize=base_font_size, leading=base_font_size * 1.15, spaceAfter=1
126
+ )
127
+ bold_style = ParagraphStyle(
128
+ 'BoldStyle', parent=styles['Normal'], fontName="NotoEmoji-Bold",
129
+ fontSize=base_font_size, leading=base_font_size * 1.15, spaceAfter=1
130
+ )
131
+ numbered_bold_style = ParagraphStyle(
132
+ 'NumberedBoldStyle', parent=styles['Normal'], fontName="NotoEmoji-Bold",
133
+ fontSize=base_font_size + 1 if enlarge_numbered else base_font_size,
134
+ leading=(base_font_size + 1) * 1.15 if enlarge_numbered else base_font_size * 1.15, spaceAfter=1
135
+ )
136
+ section_style = ParagraphStyle(
137
+ 'SectionStyle', parent=styles['Heading2'], fontName="DejaVuSans",
138
+ textColor=colors.darkblue, fontSize=base_font_size * 1.1, leading=base_font_size * 1.32, spaceAfter=2
139
+ )
140
+ try:
141
+ available_font_files = glob.glob("*.ttf")
142
+ if not available_font_files:
143
+ st.error("No .ttf font files found in the current directory.")
144
+ return
145
+ selected_font_path = None
146
+ for f in available_font_files:
147
+ if "NotoEmoji-Bold" in f:
148
+ selected_font_path = f
149
+ break
150
+ if selected_font_path:
151
+ pdfmetrics.registerFont(TTFont("NotoEmoji-Bold", selected_font_path))
152
+ pdfmetrics.registerFont(TTFont("DejaVuSans", "DejaVuSans.ttf"))
153
+ except Exception as e:
154
+ st.error(f"Font registration error: {e}")
155
+ return
156
+ columns = [[] for _ in range(num_columns)]
157
+ lines_per_column = total_lines / num_columns if num_columns > 0 else total_lines
158
+ current_line_count = 0
159
+ current_column = 0
160
+ number_pattern = re.compile(r'^\d+\.\s')
161
+ for item in pdf_content:
162
+ if current_line_count >= lines_per_column and current_column < num_columns - 1:
163
+ current_column += 1
164
+ current_line_count = 0
165
+ columns[current_column].append(item)
166
+ current_line_count += 1
167
+ column_cells = [[] for _ in range(num_columns)]
168
+ for col_idx, column in enumerate(columns):
169
+ for item in column:
170
+ if isinstance(item, str) and item.startswith("<b>") and item.endswith("</b>"):
171
+ content = item[3:-4].strip()
172
+ if number_pattern.match(content):
173
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), numbered_bold_style))
174
+ else:
175
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), section_style))
176
+ else:
177
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(item, emoji_font), item_style))
178
+ max_cells = max(len(cells) for cells in column_cells) if column_cells else 0
179
+ for cells in column_cells:
180
+ cells.extend([Paragraph("", item_style)] * (max_cells - len(cells)))
181
+ col_width = (page_width - 72) / num_columns if num_columns > 0 else page_width - 72
182
+ table_data = list(zip(*column_cells)) if column_cells else [[]]
183
+ table = Table(table_data, colWidths=[col_width] * num_columns, hAlign='CENTER')
184
+ table.setStyle(TableStyle([
185
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
186
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
187
+ ('BACKGROUND', (0, 0), (-1, -1), colors.white),
188
+ ('GRID', (0, 0), (-1, -1), 0, colors.white),
189
+ ('LINEAFTER', (0, 0), (num_columns-1, -1), 0.5, colors.grey),
190
+ ('LEFTPADDING', (0, 0), (-1, -1), 2),
191
+ ('RIGHTPADDING', (0, 0), (-1, -1), 2),
192
+ ('TOPPADDING', (0, 0), (-1, -1), 1),
193
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 1),
194
+ ]))
195
+ story = [Spacer(1, spacer_height), table]
196
+ doc.build(story)
197
+ buffer.seek(0)
198
+ return buffer.getvalue()
199
+
200
+ def pdf_to_image(pdf_bytes):
201
+ try:
202
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
203
+ images = []
204
+ for page in doc:
205
+ pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0))
206
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
207
+ images.append(img)
208
+ doc.close()
209
+ return images
210
+ except Exception as e:
211
+ st.error(f"Failed to render PDF preview: {e}")
212
+ return None
213
+
214
+ # Auto-detect default markdown file from available .md files.
215
+ md_files = [f for f in glob.glob("*.md") if os.path.basename(f) != "README.md"]
216
+ md_options = [os.path.splitext(os.path.basename(f))[0] for f in md_files]
217
+ if md_options:
218
+ if 'markdown_content' not in st.session_state or not st.session_state.markdown_content:
219
+ with open(f"{md_options[0]}.md", "r", encoding="utf-8") as f:
220
+ st.session_state.markdown_content = f.read()
221
+ else:
222
+ st.session_state.markdown_content = ""
223
+
224
+ with st.sidebar:
225
+ st.markdown("### PDF Options")
226
+ selected_md = st.selectbox("Select Markdown File", options=md_options, index=0 if md_options else -1)
227
+ available_font_files = {os.path.splitext(os.path.basename(f))[0]: f for f in glob.glob("*.ttf")}
228
+ selected_font_name = st.selectbox("Select Emoji Font", options=list(available_font_files.keys()), index=list(available_font_files.keys()).index("NotoEmoji-Bold") if "NotoEmoji-Bold" in available_font_files else 0)
229
+ base_font_size = st.slider("Font Size (points)", min_value=6, max_value=16, value=8, step=1)
230
+ render_with_bold = st.checkbox("Render with Bold Formatting (remove ** markers)", value=True, key="render_with_bold")
231
+ auto_bold_numbers = st.checkbox("Auto Bold Numbered Lines", value=True, key="auto_bold_numbers")
232
+ enlarge_numbered = st.checkbox("Enlarge Font Size for Numbered Lines", value=True, key="enlarge_numbered")
233
+ num_columns = st.selectbox("Number of Columns", options=[1, 2, 3, 4, 5, 6], index=3)
234
+ if md_options and selected_md:
235
+ with open(f"{selected_md}.md", "r", encoding="utf-8") as f:
236
+ st.session_state.markdown_content = f.read()
237
+ edited_markdown = st.text_area("Modify the markdown content below:", value=st.session_state.markdown_content, height=300, key=f"markdown_{selected_md}_{selected_font_name}_{num_columns}")
238
+ if st.button("Update PDF"):
239
+ st.session_state.markdown_content = edited_markdown
240
+ if md_options and selected_md:
241
+ with open(f"{selected_md}.md", "w", encoding="utf-8") as f:
242
+ f.write(edited_markdown)
243
+ st.experimental_rerun()
244
+ st.download_button(label="Save Markdown", data=st.session_state.markdown_content, file_name=f"{selected_md}.md" if selected_md else "default.md", mime="text/markdown")
245
+ st.markdown("### Text-to-Speech")
246
+ VOICES = ["en-US-AriaNeural", "en-US-JennyNeural", "en-GB-SoniaNeural", "en-US-GuyNeural", "en-US-AnaNeural"]
247
+ selected_voice = st.selectbox("Select Voice for TTS", options=VOICES, index=0)
248
+ if st.button("Generate Audio"):
249
+ audio_file = asyncio.run(generate_audio(st.session_state.markdown_content, selected_voice, st.session_state.markdown_content))
250
+ st.audio(audio_file)
251
+ with open(audio_file, "rb") as f:
252
+ audio_bytes = f.read()
253
+ st.download_button("Download Audio", data=audio_bytes, file_name=os.path.basename(audio_file), mime="audio/mpeg")
254
+ if st.button("Save PDF"):
255
+ title = get_file_title_from_markdown(st.session_state.markdown_content)
256
+ pdf_filename = f"{title}.pdf"
257
+ with open(pdf_filename, "wb") as f:
258
+ f.write(pdf_bytes)
259
+ st.success(f"Saved PDF as {pdf_filename}")
260
+ st.experimental_rerun()
261
+ st.markdown("### Saved Audio Files")
262
+ mp3_files = glob.glob("*.mp3")
263
+ for mp3 in mp3_files:
264
+ st.audio(mp3)
265
+ st.markdown(get_download_link(mp3, "mp3"), unsafe_allow_html=True)
266
+ if st.button("Delete All MP3"):
267
+ for mp3 in mp3_files:
268
+ try:
269
+ os.remove(mp3)
270
+ except Exception as e:
271
+ st.error(f"Error deleting {mp3}: {e}")
272
+ st.experimental_rerun()
273
+ st.markdown("### Saved PDF Files")
274
+ pdf_files = glob.glob("*.pdf")
275
+ for pdf in pdf_files:
276
+ st.markdown(get_download_link(pdf, "pdf"), unsafe_allow_html=True)
277
+
278
+ with st.spinner("Generating PDF..."):
279
+ pdf_bytes = create_pdf(st.session_state.markdown_content, base_font_size, render_with_bold, auto_bold_numbers, enlarge_numbered, num_columns, selected_font_name)
280
+
281
+ with st.container():
282
+ pdf_images = pdf_to_image(pdf_bytes)
283
+ if pdf_images:
284
+ for img in pdf_images:
285
+ st.image(img, use_container_width=True)
286
+ else:
287
+ st.info("Download the PDF to view it locally.")
288
+
289
+ with st.sidebar:
290
+ st.download_button(label="Download PDF", data=pdf_bytes, file_name="output.pdf", mime="application/pdf")