dejanseo commited on
Commit
8e2aea6
·
verified ·
1 Parent(s): a19db4b

Upload app.py

Browse files
Files changed (1) hide show
  1. 74/app.py +299 -0
74/app.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import base64
3
+ import time
4
+ import numpy as np
5
+ import sentencepiece as spm
6
+ from ai_edge_litert.interpreter import Interpreter
7
+ from selenium import webdriver
8
+ from selenium.webdriver.chrome.service import Service as ChromeService
9
+ from selenium.webdriver.chrome.options import Options as ChromeOptions
10
+ import common_quality_data_pb2 as apc_pb2
11
+ import os
12
+
13
+ # --- Paths ---
14
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
15
+ EMBEDDER_PATH = os.path.join(BASE_DIR, "passage_embedder", "model.tflite")
16
+ CLASSIFIER_PATH = os.path.join(BASE_DIR, "shopping_classifier", "model.tflite")
17
+ SPM_PATH = os.path.join(BASE_DIR, "passage_embedder", "sentencepiece.model")
18
+ CHROME_CANARY = os.path.expandvars(
19
+ r"%LOCALAPPDATA%\Google\Chrome SxS\Application\chrome.exe"
20
+ )
21
+
22
+ INPUT_WINDOW_SIZE = 64
23
+ EMBEDDING_DIM = 768
24
+ MAX_WORDS_PER_PASSAGE = 100
25
+ MIN_WORDS_PER_PASSAGE = 5
26
+ MAX_PASSAGES = 10
27
+
28
+
29
+ # --- Load models once ---
30
+ @st.cache_resource
31
+ def load_sp():
32
+ sp = spm.SentencePieceProcessor()
33
+ sp.Load(SPM_PATH)
34
+ return sp
35
+
36
+
37
+ @st.cache_resource
38
+ def load_embedder():
39
+ interp = Interpreter(model_path=EMBEDDER_PATH)
40
+ interp.allocate_tensors()
41
+ return interp
42
+
43
+
44
+ @st.cache_resource
45
+ def load_classifier():
46
+ interp = Interpreter(model_path=CLASSIFIER_PATH)
47
+ interp.allocate_tensors()
48
+ return interp
49
+
50
+
51
+ # --- Text extraction from AnnotatedPageContent proto ---
52
+ def extract_text_from_node(node):
53
+ """Recursively extract text items from ContentNode tree."""
54
+ items = []
55
+ attrs = node.content_attributes
56
+ if attrs.HasField("text_data"):
57
+ text = attrs.text_data.text_content.strip()
58
+ if text:
59
+ items.append(text)
60
+ elif attrs.HasField("table_data"):
61
+ text = attrs.table_data.table_name.strip()
62
+ if text:
63
+ items.append(text)
64
+ elif attrs.HasField("image_data"):
65
+ text = attrs.image_data.image_caption.strip()
66
+ if text:
67
+ items.append(text)
68
+ for child in node.children_nodes:
69
+ items.extend(extract_text_from_node(child))
70
+ return items
71
+
72
+
73
+ def chunk_passages(text_items, max_words=MAX_WORDS_PER_PASSAGE,
74
+ min_words=MIN_WORDS_PER_PASSAGE, max_passages=MAX_PASSAGES):
75
+ """Greedy word-count chunking matching Chrome's algorithm."""
76
+ passages = []
77
+ current = []
78
+ current_word_count = 0
79
+
80
+ for item in text_items:
81
+ words = item.split()
82
+ item_word_count = len(words)
83
+
84
+ if item_word_count < min_words:
85
+ current.append(item)
86
+ current_word_count += item_word_count
87
+ else:
88
+ if current_word_count + item_word_count > max_words and current:
89
+ passages.append(" ".join(current))
90
+ current = [item]
91
+ current_word_count = item_word_count
92
+ else:
93
+ current.append(item)
94
+ current_word_count += item_word_count
95
+
96
+ if current_word_count >= max_words:
97
+ passages.append(" ".join(current))
98
+ current = []
99
+ current_word_count = 0
100
+
101
+ if len(passages) >= max_passages:
102
+ break
103
+
104
+ if current and len(passages) < max_passages:
105
+ passages.append(" ".join(current))
106
+
107
+ return passages[:max_passages]
108
+
109
+
110
+ # --- Tokenization ---
111
+ def tokenize(sp, text):
112
+ """SentencePiece encode, append EOS if room, resize to INPUT_WINDOW_SIZE."""
113
+ token_ids = sp.Encode(text)
114
+ if len(token_ids) < INPUT_WINDOW_SIZE:
115
+ token_ids.append(sp.eos_id())
116
+ token_ids = token_ids[:INPUT_WINDOW_SIZE]
117
+ # Zero-pad
118
+ token_ids += [0] * (INPUT_WINDOW_SIZE - len(token_ids))
119
+ return np.array(token_ids, dtype=np.int32).reshape(1, INPUT_WINDOW_SIZE)
120
+
121
+
122
+ # --- Embedding ---
123
+ def embed(interp, token_ids):
124
+ """Run passage embedder: int32[1,64] -> float32[1,768]."""
125
+ input_details = interp.get_input_details()
126
+ output_details = interp.get_output_details()
127
+ interp.set_tensor(input_details[0]["index"], token_ids)
128
+ interp.invoke()
129
+ return interp.get_tensor(output_details[0]["index"]).copy()
130
+
131
+
132
+ # --- Classification ---
133
+ def classify(interp, input_vector):
134
+ """Run shopping classifier: float32[1,1536] -> float32[1,1]."""
135
+ input_details = interp.get_input_details()
136
+ output_details = interp.get_output_details()
137
+ interp.set_tensor(input_details[0]["index"], input_vector)
138
+ interp.invoke()
139
+ return float(interp.get_tensor(output_details[0]["index"])[0][0])
140
+
141
+
142
+ # --- CDP page extraction ---
143
+ def fetch_page_content(url):
144
+ """Use Chrome Canary + Selenium CDP to get AnnotatedPageContent."""
145
+ options = ChromeOptions()
146
+ options.binary_location = CHROME_CANARY
147
+ options.add_argument("--headless=new")
148
+ options.add_argument("--disable-gpu")
149
+ options.add_argument("--no-sandbox")
150
+
151
+ driver = webdriver.Chrome(options=options)
152
+ try:
153
+ driver.get(url)
154
+ # Wait for content to settle (Chrome uses 5s delay)
155
+ time.sleep(5)
156
+
157
+ # Try AnnotatedPageContent via CDP
158
+ apc_data = None
159
+ try:
160
+ result = driver.execute_cdp_cmd(
161
+ "Page.getAnnotatedPageContent",
162
+ {"includeActionableInformation": True},
163
+ )
164
+ apc_data = base64.b64decode(result["content"])
165
+ except Exception as e:
166
+ st.warning(f"CDP AnnotatedPageContent failed: {e}")
167
+
168
+ # Fallback: get title and innerText
169
+ title = driver.title
170
+ inner_text = driver.execute_script("return document.body.innerText")
171
+ page_url = driver.current_url
172
+ finally:
173
+ driver.quit()
174
+
175
+ return apc_data, title, page_url, inner_text
176
+
177
+
178
+ def process_apc(apc_data):
179
+ """Parse AnnotatedPageContent proto and extract title, url, text items."""
180
+ apc = apc_pb2.AnnotatedPageContent()
181
+ apc.ParseFromString(apc_data)
182
+
183
+ title = apc.main_frame_data.title
184
+ url = apc.main_frame_data.url
185
+ text_items = extract_text_from_node(apc.root_node)
186
+
187
+ return title, url, text_items
188
+
189
+
190
+ def process_fallback(title, url, inner_text):
191
+ """Fallback: split innerText into text items by lines."""
192
+ lines = [line.strip() for line in inner_text.split("\n") if line.strip()]
193
+ return title, url, lines
194
+
195
+
196
+ # --- Full pipeline ---
197
+ def run_pipeline(title, url, text_items, sp, embedder, classifier):
198
+ """Run the full embedding + classification pipeline."""
199
+ # 1. Create passages
200
+ passages = chunk_passages(text_items)
201
+
202
+ # 2. Embed title + url
203
+ title_url_text = f"{title} - {url}"
204
+ title_url_tokens = tokenize(sp, title_url_text)
205
+ title_url_emb = embed(embedder, title_url_tokens) # [1, 768]
206
+
207
+ # 3. Embed passages and mean-pool
208
+ if passages:
209
+ passage_embeddings = []
210
+ for passage in passages:
211
+ tokens = tokenize(sp, passage)
212
+ emb = embed(embedder, tokens)
213
+ passage_embeddings.append(emb[0])
214
+ # Mean pooling
215
+ mean_pooled = np.mean(passage_embeddings, axis=0, keepdims=True) # [1, 768]
216
+ else:
217
+ mean_pooled = np.zeros((1, EMBEDDING_DIM), dtype=np.float32)
218
+
219
+ # 4. Concatenate: [title_url(768) | passages_mean(768)] = [1, 1536]
220
+ input_vector = np.concatenate([title_url_emb, mean_pooled], axis=1).astype(np.float32)
221
+
222
+ # 5. Classify
223
+ score = classify(classifier, input_vector)
224
+
225
+ return score, passages
226
+
227
+
228
+ # --- Streamlit UI ---
229
+ st.set_page_config(page_title="Shopping Classifier", layout="wide")
230
+
231
+ st.html("""
232
+ <style>
233
+ .stButton > button[kind="primary"] {
234
+ background-color: #2e7d32;
235
+ border-color: #2e7d32;
236
+ }
237
+ .stButton > button[kind="primary"]:hover {
238
+ background-color: #1b5e20;
239
+ border-color: #1b5e20;
240
+ }
241
+ </style>
242
+ """)
243
+ st.subheader("Shopping Page Classifier")
244
+ #st.caption("Using Chrome's OPTIMIZATION_TARGET_SHOPPING_CLASSIFIER model")
245
+
246
+ url = st.text_input("Enter URL", placeholder="https://www.amazon.com/dp/B0...")
247
+
248
+ if st.button("Classify", type="primary") and url:
249
+ sp = load_sp()
250
+ embedder = load_embedder()
251
+ classifier = load_classifier()
252
+
253
+ with st.spinner("Loading page in Chrome Canary..."):
254
+ apc_data, fallback_title, page_url, inner_text = fetch_page_content(url)
255
+
256
+ # Process page content
257
+ used_method = None
258
+ if apc_data:
259
+ try:
260
+ title, resolved_url, text_items = process_apc(apc_data)
261
+ used_method = "CDP AnnotatedPageContent"
262
+ except Exception as e:
263
+ st.warning(f"Proto parse failed: {e}, falling back to innerText")
264
+ title, resolved_url, text_items = process_fallback(
265
+ fallback_title, page_url, inner_text
266
+ )
267
+ used_method = "innerText fallback"
268
+ else:
269
+ title, resolved_url, text_items = process_fallback(
270
+ fallback_title, page_url, inner_text
271
+ )
272
+ used_method = "innerText fallback"
273
+
274
+ with st.spinner("Running inference..."):
275
+ score, passages = run_pipeline(
276
+ title, resolved_url, text_items, sp, embedder, classifier
277
+ )
278
+
279
+ # --- Results ---
280
+ threshold = 0.5
281
+ is_shopping = score >= threshold
282
+ col1, col2 = st.columns(2)
283
+ with col1:
284
+ st.metric("Score", f"{score:.4f}")
285
+ with col2:
286
+ if is_shopping:
287
+ st.success(f"SHOPPING PAGE (>= {threshold})")
288
+ else:
289
+ st.info(f"NOT SHOPPING (< {threshold})")
290
+
291
+ # Details
292
+ with st.expander("Details"):
293
+ st.write(f"**Method:** {used_method}")
294
+ st.write(f"**Title:** {title}")
295
+ st.write(f"**URL:** {resolved_url}")
296
+ st.write(f"**Text items extracted:** {len(text_items)}")
297
+ st.write(f"**Passages created:** {len(passages)}")
298
+ passages_json = {f"passage_{i+1}": p for i, p in enumerate(passages)}
299
+ st.json(passages_json)