awacke1 commited on
Commit
1251848
·
verified ·
1 Parent(s): a686a3f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +139 -268
app.py CHANGED
@@ -1,288 +1,159 @@
1
- #!/usr/bin/env python
2
- # app.py
3
-
4
  import io
5
  import os
6
  import re
7
- import base64
8
- import glob
9
- import logging
10
- import random
11
- import shutil
12
- import time
13
- import zipfile
14
- import json
15
- import asyncio
16
-
17
- from pathlib import Path
18
  from datetime import datetime
19
- from typing import Any, List, Dict, Optional
20
 
21
  import pandas as pd
22
- import pytz
23
  import streamlit as st
24
- import aiofiles
25
- import requests
26
-
27
- from PIL import Image, ImageDraw, UnidentifiedImageError
28
  from reportlab.pdfgen import canvas
29
  from reportlab.lib.utils import ImageReader
30
- from reportlab.lib.pagesizes import letter
31
- import fitz # PyMuPDF
32
-
33
- from huggingface_hub import InferenceClient
34
- from huggingface_hub.utils import RepositoryNotFoundError, GatedRepoError
35
-
36
- # Optional AI/ML imports
37
- try:
38
- import torch
39
- from transformers import (
40
- AutoModelForCausalLM,
41
- AutoTokenizer,
42
- AutoProcessor,
43
- AutoModelForVision2Seq,
44
- pipeline
45
- )
46
- _transformers_available = True
47
- except ImportError:
48
- _transformers_available = False
49
-
50
- try:
51
- from diffusers import StableDiffusionPipeline
52
- _diffusers_available = True
53
- except ImportError:
54
- _diffusers_available = False
55
 
56
- # --- Page Configuration ---
57
  st.set_page_config(
58
- page_title="Vision & Layout Titans (HF) 🚀🖼️",
59
- page_icon="🤖",
60
  layout="wide",
61
  initial_sidebar_state="expanded",
62
- menu_items={
63
- 'Get Help': 'https://huggingface.co/docs',
64
- 'About': "Combined App: Image→PDF Layout + HF AI Tools 🌌"
65
- }
66
  )
67
 
68
- # --- Logging Setup ---
69
- logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
70
- logger = logging.getLogger(__name__)
71
- log_records: List[logging.LogRecord] = []
72
- class LogCaptureHandler(logging.Handler):
73
- def emit(self, record):
74
- log_records.append(record)
75
- logger.addHandler(LogCaptureHandler())
76
-
77
- # --- Constants & Defaults ---
78
- HF_TOKEN = os.getenv("HF_TOKEN")
79
- DEFAULT_PROVIDER = "hf-inference"
80
- FEATURED_MODELS_LIST = [
81
- "meta-llama/Meta-Llama-3.1-8B-Instruct",
82
- "mistralai/Mistral-7B-Instruct-v0.3",
83
- "google/gemma-2-9b-it",
84
- "Qwen/Qwen2-7B-Instruct",
85
- "microsoft/Phi-3-mini-4k-instruct",
86
- "HuggingFaceH4/zephyr-7b-beta",
87
- "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
88
- "HuggingFaceTB/SmolLM-1.7B-Instruct"
89
- ]
90
-
91
- # --- Session State Initialization ---
92
- def _init_state(key: str, default: Any):
93
- if key not in st.session_state:
94
- st.session_state[key] = default
95
-
96
- for k, v in {
97
- 'layout_snapshots': [],
98
- 'layout_new_uploads': [],
99
- 'layout_last_capture': None,
100
- 'history': [],
101
- 'processing': {},
102
- 'asset_checkboxes': {},
103
- 'downloaded_pdfs': {},
104
- 'unique_counter': 0,
105
- 'cam0_file': None,
106
- 'cam1_file': None,
107
- 'characters': [],
108
- 'char_form_reset_key': 0,
109
- 'gallery_size': 10,
110
- 'hf_inference_client': None,
111
- 'hf_provider': DEFAULT_PROVIDER,
112
- 'hf_custom_key': "",
113
- 'hf_selected_api_model': FEATURED_MODELS_LIST[0],
114
- 'hf_custom_api_model': "",
115
- 'local_models': {},
116
- 'selected_local_model_path': None,
117
- 'gen_max_tokens': 512,
118
- 'gen_temperature': 0.7,
119
- 'gen_top_p': 0.95,
120
- 'gen_frequency_penalty': 0.0,
121
- 'gen_seed': -1
122
- }.items():
123
- _init_state(k, v)
124
-
125
- # --- Utility Functions ---
126
- def generate_filename(seq: str, ext: str = "png") -> str:
127
- ts = time.strftime('%Y%m%d_%H%M%S')
128
- safe = re.sub(r'[^\w\-]+', '_', seq)
129
- return f"{safe}_{ts}.{ext}"
130
-
131
- def clean_stem(fn: str) -> str:
132
- return os.path.splitext(os.path.basename(fn))[0].replace('-', ' ').replace('_', ' ').title()
133
-
134
- def get_download_link(path: str, mime: str, label: str = "Download") -> str:
135
- if not os.path.exists(path): return f"{label} (not found)"
136
- data = open(path,'rb').read()
137
- b64 = base64.b64encode(data).decode()
138
- return f'<a href="data:{mime};base64,{b64}" download="{os.path.basename(path)}">{label}</a>'
139
-
140
- def get_gallery_files(types: List[str] = ['png','jpg','jpeg','pdf','md','txt']) -> List[str]:
141
- files = set()
142
- for ext in types:
143
- files.update(glob.glob(f"*.{ext}"))
144
- files.update(glob.glob(f"*.{ext.upper()}"))
145
- return sorted(files)
146
-
147
- # Delete with rerun
148
- def delete_asset(path: str):
149
- try:
150
- os.remove(path)
151
- st.session_state['asset_checkboxes'].pop(path, None)
152
- if path in st.session_state['layout_snapshots']:
153
- st.session_state['layout_snapshots'].remove(path)
154
- st.toast(f"Deleted {os.path.basename(path)}", icon="✅")
155
- except OSError as e:
156
- st.error(f"Delete failed: {e}")
157
- st.rerun()
158
 
159
- # Sidebar gallery updater
160
- def update_gallery():
161
- st.sidebar.markdown("### Asset Gallery 📸📖")
162
- files = get_gallery_files()
163
- if not files:
164
- st.sidebar.info("No assets.")
165
- return
166
- st.sidebar.caption(f"Found {len(files)} assets.")
167
- for f in files[:st.session_state['gallery_size']]:
168
- name = os.path.basename(f)
169
- ext = os.path.splitext(f)[1].lower()
170
- st.sidebar.markdown(f"**{name}**")
171
- with st.sidebar.expander("Preview", expanded=False):
172
- try:
173
- if ext in ['.png','.jpg','.jpeg']:
174
- st.image(Image.open(f), use_container_width=True)
175
- elif ext == '.pdf':
176
- doc = fitz.open(f)
177
- if doc.page_count:
178
- pix = doc[0].get_pixmap(matrix=fitz.Matrix(0.5,0.5))
179
- img = Image.frombytes('RGB',[pix.width,pix.height],pix.samples)
180
- st.image(img, use_container_width=True)
181
- doc.close()
182
- else:
183
- txt = Path(f).read_text(errors='ignore')
184
- st.code(txt[:200]+'…')
185
- except:
186
- st.warning("Preview error")
187
- c1,c2,c3 = st.sidebar.columns(3)
188
- sel = st.session_state['asset_checkboxes'].get(f, False)
189
- c1.checkbox("Select", value=sel, key=f"cb_{f}")
190
- st.session_state['asset_checkboxes'][f] = st.session_state.get(f"cb_{f}")
191
- mime = {'png':'image/png','jpg':'image/jpeg','jpeg':'image/jpeg','pdf':'application/pdf','md':'text/markdown','txt':'text/plain'}.get(ext[1:], 'application/octet-stream')
192
- with open(f,'rb') as fp:
193
- c2.download_button("📥", data=fp, file_name=name, mime=mime, key=f"dl_{f}")
194
- c3.button("🗑️", key=f"del_{f}", on_click=delete_asset, args=(f,))
195
- st.sidebar.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
- # --- PDF Snapshot & Generation ---
198
- async def process_pdf_snapshot(path: str, mode: str='single', resF: float=2.0) -> List[str]:
199
- status = st.empty()
200
- status.text("Snapshot start...")
201
- out_files: List[str] = []
202
  try:
203
- doc = fitz.open(path)
204
- mat = fitz.Matrix(resF,resF)
205
- cnt = {'single':1,'twopage':2,'allpages':len(doc)}.get(mode,1)
206
- for i in range(min(cnt,len(doc))):
207
- s = time.time()
208
- page = doc[i]
209
- pix = page.get_pixmap(matrix=mat)
210
- base = os.path.splitext(os.path.basename(path))[0]
211
- fname = generate_filename(f"{base}_pg{i+1}_{mode}","png")
212
- await asyncio.to_thread(pix.save, fname)
213
- out_files.append(fname)
214
- status.text(f"Saved {fname} ({int(time.time()-s)}s)")
215
- doc.close()
216
- status.success(f"Snapshot done: {len(out_files)} files")
217
- except Exception as e:
218
- status.error(f"Snapshot error: {e}")
219
- for f in out_files:
220
- if os.path.exists(f): os.remove(f)
221
- out_files = []
222
- return out_files
223
-
224
- from reportlab.lib.pagesizes import letter
225
-
226
- def make_image_sized_pdf(sources: List[Any]) -> Optional[bytes]:
227
- # dedupe
228
- seen, uniq = set(), []
229
- for s in sources:
230
- key = s if isinstance(s,str) else getattr(s,'name',None)
231
- if key and key not in seen:
232
- seen.add(key)
233
- uniq.append(s)
234
- if not uniq:
235
- st.warning("No images for PDF")
236
- return None
237
- buf = io.BytesIO()
238
- c = canvas.Canvas(buf, pagesize=letter)
239
- status = st.empty()
240
- for idx,s in enumerate(uniq,1):
241
- try:
242
- img = Image.open(s) if isinstance(s,str) else Image.open(s)
243
- w,h = img.size
244
- cap = 30
245
- c.setPageSize((w,h+cap))
246
- c.drawImage(ImageReader(img),0,cap,w,h,mask='auto')
247
- cap_txt = clean_stem(s if isinstance(s,str) else s.name)
248
- c.setFont('Helvetica',12)
249
- c.drawCentredString(w/2,cap/2,cap_txt)
250
- c.setFont('Helvetica',8)
251
- c.drawRightString(w-10,10,str(idx))
252
- c.showPage()
253
- status.text(f"Page {idx}/{len(uniq)} added")
254
- except Exception as e:
255
- status.error(f"Error page {idx}: {e}")
256
  c.save()
257
- buf.seek(0)
258
- return buf.getvalue()
259
-
260
- # --- HF Inference Client ---
261
- def get_hf_client() -> Optional[InferenceClient]:
262
- provider = st.session_state['hf_provider']
263
- token = st.session_state['hf_custom_key'].strip() or HF_TOKEN
264
- if provider!='hf-inference' and not token:
265
- st.error(f"Provider {provider} needs token")
266
- return None
267
- client = st.session_state['hf_inference_client']
268
- if not client:
269
- st.session_state['hf_inference_client'] = InferenceClient(token=token, provider=provider)
270
- return st.session_state['hf_inference_client']
271
-
272
- # --- HF Processing ---
273
- def process_text_hf(text: str, prompt: str, use_api: bool) -> str:
274
- stp = st.empty(); stp.text("Processing...")
275
- msgs = [{"role":"system","content":"You are an assistant."},
276
- {"role":"user","content":f"{prompt}\n\n{text}"}]
277
- out = ""
278
- if use_api:
279
- client = get_hf_client()
280
- if not client: return "Client error"
281
- model = st.session_state['hf_custom_api_model'] or st.session_state['hf_selected_api_model']
282
  try:
283
- resp = client.chat_completion(
284
- model=model,
285
- messages=msgs,
286
- max_tokens=st.session_state['gen_max_tokens'],
287
- temperature=st.session
288
- ]}]}
 
 
 
 
 
 
 
 
1
  import io
2
  import os
3
  import re
 
 
 
 
 
 
 
 
 
 
 
4
  from datetime import datetime
5
+ from collections import Counter
6
 
7
  import pandas as pd
 
8
  import streamlit as st
9
+ from PIL import Image
 
 
 
10
  from reportlab.pdfgen import canvas
11
  from reportlab.lib.utils import ImageReader
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ # --- App Configuration ----------------------------------
14
  st.set_page_config(
15
+ page_title="Image PDF Comic Layout",
 
16
  layout="wide",
17
  initial_sidebar_state="expanded",
 
 
 
 
18
  )
19
 
20
+ st.title("🖼️ Image PDF • Full-Page & Custom Layout Generator")
21
+ st.markdown(
22
+ "Upload images, filter by orientation, reorder visually, and generate a captioned PDF where each page matches its image's dimensions."
23
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ # --- Sidebar: Page Settings -----------------------------
26
+ st.sidebar.header("1️⃣ Page Aspect Ratio & Size")
27
+ ratio_map = {
28
+ "4:3 (Landscape)": (4, 3),
29
+ "16:9 (Landscape)": (16, 9),
30
+ "1:1 (Square)": (1, 1),
31
+ "2:3 (Portrait)": (2, 3),
32
+ "9:16 (Portrait)": (9, 16),
33
+ }
34
+ ratio_choice = st.sidebar.selectbox(
35
+ "Preset Ratio", list(ratio_map.keys()) + ["Custom…"]
36
+ )
37
+ if ratio_choice != "Custom…":
38
+ rw, rh = ratio_map[ratio_choice]
39
+ else:
40
+ rw = st.sidebar.number_input("Custom Width Ratio", min_value=1, value=4)
41
+ rh = st.sidebar.number_input("Custom Height Ratio", min_value=1, value=3)
42
+ BASE_WIDTH_PT = st.sidebar.slider(
43
+ "Base Page Width (pt)", min_value=400, max_value=1200, value=800, step=100
44
+ )
45
+ page_width = BASE_WIDTH_PT
46
+ page_height = int(BASE_WIDTH_PT * (rh / rw))
47
+ st.sidebar.markdown(f"**Preview page size:** {page_width}×{page_height} pt")
48
+
49
+ # --- Main: Upload, Filter & Reorder -------------------
50
+ st.header("2️⃣ Upload, Filter & Reorder Images")
51
+ uploaded = st.file_uploader(
52
+ "📂 Select PNG/JPG images", type=["png","jpg","jpeg"], accept_multiple_files=True
53
+ )
54
+ ordered_files = []
55
+ if uploaded:
56
+ records = []
57
+ for idx, f in enumerate(uploaded):
58
+ im = Image.open(f)
59
+ w, h = im.size
60
+ ar = round(w / h, 2)
61
+ orient = "Square" if 0.9 <= ar <= 1.1 else ("Landscape" if ar > 1.1 else "Portrait")
62
+ records.append({
63
+ "filename": f.name,
64
+ "width": w,
65
+ "height": h,
66
+ "aspect_ratio": ar,
67
+ "orientation": orient,
68
+ "order": idx,
69
+ })
70
+ df = pd.DataFrame(records)
71
+ dims = st.sidebar.multiselect(
72
+ "Include orientations:", options=["Landscape","Portrait","Square"],
73
+ default=["Landscape","Portrait","Square"]
74
+ )
75
+ df = df[df["orientation"].isin(dims)].reset_index(drop=True)
76
 
77
+ st.markdown("#### Image Metadata & Order")
78
+ st.dataframe(df.style.format({"aspect_ratio": "{:.2f}"}), use_container_width=True)
79
+ st.markdown("*Drag rows or edit the `order` column to set PDF page sequence.*")
 
 
80
  try:
81
+ edited = st.experimental_data_editor(df, num_rows="fixed", use_container_width=True)
82
+ ordered_df = edited
83
+ except Exception:
84
+ edited = st.data_editor(
85
+ df,
86
+ column_config={
87
+ "order": st.column_config.NumberColumn(
88
+ "Order", min_value=0, max_value=len(df)-1
89
+ )
90
+ },
91
+ hide_index=True,
92
+ use_container_width=True,
93
+ )
94
+ ordered_df = edited.sort_values("order").reset_index(drop=True)
95
+
96
+ name2file = {f.name: f for f in uploaded}
97
+ ordered_files = [name2file[n] for n in ordered_df["filename"] if n in name2file]
98
+
99
+ # --- Utility: Clean stems -------------------------------
100
+ def clean_stem(fn: str) -> str:
101
+ stem = os.path.splitext(fn)[0]
102
+ return stem.replace("-", " ").replace("_", " ")
103
+
104
+ # --- PDF Creation: Image-sized + Captions --------------
105
+ def make_image_sized_pdf(images):
106
+ buffer = io.BytesIO()
107
+ c = canvas.Canvas(buffer)
108
+ for idx, f in enumerate(images, start=1):
109
+ im = Image.open(f)
110
+ iw, ih = im.size
111
+ # Caption height
112
+ cap_h = 20
113
+ page_w, page_h = iw, ih + cap_h
114
+ c.setPageSize((page_w, page_h))
115
+ # Draw image
116
+ c.drawImage(ImageReader(im), 0, cap_h, iw, ih, preserveAspectRatio=True, mask='auto')
117
+ # Draw caption
118
+ caption = clean_stem(f.name)
119
+ c.setFont("Helvetica", 12)
120
+ c.drawCentredString(page_w/2, cap_h/2, caption)
121
+ # Page number
122
+ c.setFont("Helvetica", 8)
123
+ c.drawRightString(page_w - 10, 10, str(idx))
124
+ c.showPage()
 
 
 
 
 
 
 
 
 
125
  c.save()
126
+ buffer.seek(0)
127
+ return buffer.getvalue()
128
+
129
+ # --- Generate & Download -------------------------------
130
+ st.header("3️⃣ Generate & Download PDF with Captions")
131
+ if st.button("🖋️ Generate Captioned PDF"):
132
+ if not ordered_files:
133
+ st.warning("Upload and reorder at least one image.")
134
+ else:
135
+ # Timestamp + weekday
136
+ now = datetime.now()
137
+ prefix = now.strftime("%Y-%m%d-%I%M%p") + "-" + now.strftime("%a").upper()
138
+ # Build filename from ordered stems
139
+ stems = [clean_stem(f.name) for f in ordered_files]
140
+ basename = " - ".join(stems)
141
+ fname = f"{prefix}-{basename}.pdf"
142
+ pdf_bytes = make_image_sized_pdf(ordered_files)
143
+ st.success(f"✅ PDF ready: **{fname}**")
144
+ st.download_button(
145
+ "⬇️ Download PDF", data=pdf_bytes,
146
+ file_name=fname, mime="application/pdf"
147
+ )
148
+ st.markdown("#### Preview of Page 1")
 
 
149
  try:
150
+ import fitz
151
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
152
+ pix = doc.load_page(0).get_pixmap(matrix=fitz.Matrix(1.5,1.5))
153
+ st.image(pix.tobytes(), use_container_width=True)
154
+ except Exception:
155
+ st.info("Install `pymupdf` (`fitz`) for preview.")
156
+
157
+ # --- Footer ------------------------------------------------
158
+ st.sidebar.markdown("---")
159
+ st.sidebar.markdown("Built by Aaron C. Wacker • Senior AI Engineer")