Geraldine commited on
Commit
98fc0a1
·
verified ·
1 Parent(s): 1dc0c1c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +286 -286
app.py CHANGED
@@ -1,286 +1,286 @@
1
- import dash
2
- from dash import dcc, html, Input, Output, State, ctx
3
- import dash_bootstrap_components as dbc
4
- import plotly.express as px
5
- import pandas as pd
6
- import numpy as np
7
- import umap
8
- import hdbscan
9
- import sklearn.feature_extraction.text as text
10
- from dash.exceptions import PreventUpdate
11
- import os
12
- from dotenv import load_dotenv
13
- import helpers
14
- import lancedb
15
- from omeka_s_api_client import OmekaSClient, OmekaSClientError
16
- from lancedb_client import LanceDBManager
17
-
18
- # Load .env for credentials
19
- load_dotenv()
20
- _DEFAULT_PARSE_METADATA = (
21
- 'dcterms:identifier','dcterms:type','dcterms:title', 'dcterms:description',
22
- 'dcterms:creator','dcterms:publisher','dcterms:date','dcterms:spatial',
23
- 'dcterms:format','dcterms:provenance','dcterms:subject','dcterms:medium',
24
- 'bibo:annotates','bibo:content', 'bibo:locator', 'bibo:owner'
25
- )
26
-
27
- app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
28
- app.config.suppress_callback_exceptions = True
29
- server = app.server
30
- manager = LanceDBManager()
31
-
32
- french_stopwords = text.ENGLISH_STOP_WORDS.union([
33
- "alors", "au", "aucuns", "aussi", "autre", "avant", "avec", "avoir", "bon",
34
- "car", "ce", "cela", "ces", "ceux", "chaque", "ci", "comme", "comment", "dans",
35
- "des", "du", "dedans", "dehors", "depuis", "devrait", "doit", "donc", "dos",
36
- "début", "elle", "elles", "en", "encore", "essai", "est", "et", "eu", "fait",
37
- "faites", "fois", "font", "hors", "ici", "il", "ils", "je", "juste", "la", "le",
38
- "les", "leur", "là", "ma", "maintenant", "mais", "mes", "mine", "moins", "mon",
39
- "mot", "même", "ni", "nommés", "notre", "nous", "nouveaux", "ou", "où", "par",
40
- "parce", "parole", "pas", "personnes", "peut", "peu", "pièce", "plupart", "pour",
41
- "pourquoi", "quand", "que", "quel", "quelle", "quelles", "quels", "qui", "sa",
42
- "sans", "ses", "seulement", "si", "sien", "son", "sont", "sous", "soyez", "sujet",
43
- "sur", "ta", "tandis", "tellement", "tels", "tes", "ton", "tous", "tout", "trop",
44
- "très", "tu", "valeur", "voie", "voient", "vont", "votre", "vous", "vu", "ça",
45
- "étaient", "état", "étions", "été", "être"
46
- ])
47
-
48
- # -------------------- Layout --------------------
49
- app.layout = dbc.Container([
50
- html.H2("🌍 Omeka S UMAP Explorer", className="text-center mt-4"),
51
- html.Hr(),
52
-
53
- # Input controls
54
- dbc.Row([
55
- dbc.Col([
56
- html.H5("🔍 From Omeka S"),
57
- dcc.Input(id="api-url", value="https://your-omeka-instance.org", type="text", className="form-control"),
58
- dbc.Button("Load Item Sets", id="load-sets", color="secondary", className="mt-2"),
59
- dcc.Dropdown(id="items-sets-dropdown", placeholder="Select a collection"),
60
- dcc.Input(id="table-name", value="my_table", type="text", className="form-control mt-2", placeholder="New table name"),
61
- dbc.Button("Process Omeka Collection", id="load-data", color="primary", className="mt-2"),
62
- ], md=4),
63
-
64
- dbc.Col([
65
- html.H5("📁 From LanceDB"),
66
- dbc.Button("Load Existing Tables", id="load-tables", color="info"),
67
- dcc.Dropdown(id="db-tables-dropdown", placeholder="Select an existing table"),
68
- dbc.Button("Display Table", id="load-data-db", color="success", className="mt-2"),
69
- ], md=4),
70
-
71
- dbc.Col([
72
- html.H5("🔎 Query Tool (coming soon)"),
73
- dbc.Input(placeholder="Type a search query...", type="text", disabled=True),
74
- ], md=4),
75
- ], className="mb-4"),
76
-
77
- # Main plot area and metadata side panel
78
- dbc.Row([
79
- dbc.Col(
80
- dcc.Graph(id="umap-graph", style={"height": "700px"}),
81
- md=8
82
- ),
83
- dbc.Col(
84
- html.Div(id="point-details", style={
85
- "padding": "15px",
86
- "borderLeft": "1px solid #ccc",
87
- "height": "700px",
88
- "overflowY": "auto"
89
- }),
90
- md=4
91
- ),
92
- ]),
93
-
94
- # Status/info
95
- html.Div(id="status", className="mt-3"),
96
-
97
- dcc.Store(id="omeka-client-config", storage_type="session")
98
- ], fluid=True)
99
-
100
- # -------------------- Callbacks --------------------
101
-
102
- @app.callback(
103
- Output("items-sets-dropdown", "options"),
104
- Output("omeka-client-config", "data"),
105
- Input("load-sets", "n_clicks"),
106
- State("api-url", "value"),
107
- prevent_initial_call=True
108
- )
109
- def load_item_sets(n, base_url):
110
- client = OmekaSClient(base_url, "...", "...", 50)
111
- try:
112
- item_sets = client.list_all_item_sets()
113
- options = [{"label": s.get('dcterms:title', [{}])[0].get('@value', 'N/A'), "value": s["o:id"]} for s in item_sets]
114
- return options, {
115
- "base_url": base_url,
116
- "key_identity": "...",
117
- "key_credential": "...",
118
- "default_per_page": 50
119
- }
120
- except Exception as e:
121
- return dash.no_update, dash.no_update
122
-
123
- @app.callback(
124
- Output("db-tables-dropdown", "options"),
125
- Input("load-tables", "n_clicks"),
126
- prevent_initial_call=True
127
- )
128
- def list_tables(n):
129
- return [{"label": t, "value": t} for t in manager.list_tables()]
130
-
131
- @app.callback(
132
- Output("umap-graph", "figure"),
133
- Output("status", "children"),
134
- Input("load-data", "n_clicks"), # From Omeka S
135
- Input("load-data-db", "n_clicks"), # From DB table
136
- State("items-sets-dropdown", "value"),
137
- State("omeka-client-config", "data"),
138
- State("table-name", "value"),
139
- State("db-tables-dropdown", "value"),
140
- prevent_initial_call=True
141
- )
142
- def handle_data_loading(n_clicks_omeka, n_clicks_db, item_set_id, client_config, table_name, db_table):
143
- triggered_id = ctx.triggered_id
144
- print(triggered_id)
145
-
146
- if triggered_id == "load-data": # Omeka S case
147
- if not client_config:
148
- raise PreventUpdate
149
-
150
- client = OmekaSClient(
151
- base_url=client_config["base_url"],
152
- key_identity=client_config["key_identity"],
153
- key_credential=client_config["key_credential"]
154
- )
155
-
156
- df_omeka = harvest_omeka_items(client, item_set_id=item_set_id)
157
- items = df_omeka.to_dict(orient="records")
158
- records_with_text = [helpers.add_concatenated_text_field_exclude_keys(item, keys_to_exclude=['id','images_urls'], text_field_key='text', pair_separator=' - ') for item in items]
159
- df = helpers.prepare_df_atlas(pd.DataFrame(records_with_text), id_col='id', images_col='images_urls')
160
-
161
- text_embed = helpers.generate_text_embed(df['text'].tolist())
162
- img_embed = helpers.generate_img_embed(df['images_urls'].tolist())
163
- embeddings = np.concatenate([text_embed, img_embed], axis=1)
164
- df["embeddings"] = embeddings.tolist()
165
-
166
- reducer = umap.UMAP(n_neighbors=15, min_dist=0.1, metric="cosine")
167
- umap_embeddings = reducer.fit_transform(embeddings)
168
- df["umap_embeddings"] = umap_embeddings.tolist()
169
-
170
- clusterer = hdbscan.HDBSCAN(min_cluster_size=10)
171
- cluster_labels = clusterer.fit_predict(umap_embeddings)
172
- df["Cluster"] = cluster_labels
173
-
174
- vectorizer = text.TfidfVectorizer(max_features=1000, stop_words=list(french_stopwords), lowercase=True)
175
- tfidf_matrix = vectorizer.fit_transform(df["text"].astype(str).tolist())
176
- top_words = []
177
- for label in sorted(df["Cluster"].unique()):
178
- if label == -1:
179
- top_words.append("Noise")
180
- continue
181
- mask = (df["Cluster"] == label).to_numpy().nonzero()[0]
182
- cluster_docs = tfidf_matrix[mask]
183
- mean_tfidf = cluster_docs.mean(axis=0)
184
- mean_tfidf = np.asarray(mean_tfidf).flatten()
185
- top_indices = mean_tfidf.argsort()[::-1][:5]
186
- terms = [vectorizer.get_feature_names_out()[i] for i in top_indices]
187
- top_words.append(", ".join(terms))
188
- cluster_name_map = {label: name for label, name in zip(sorted(df["Cluster"].unique()), top_words)}
189
- df["Topic"] = df["Cluster"].map(cluster_name_map)
190
-
191
- manager.initialize_table(table_name)
192
- manager.add_entry(table_name, df.to_dict(orient="records"))
193
-
194
- elif triggered_id == "load-data-db": # Load existing LanceDB table
195
- if not db_table:
196
- raise PreventUpdate
197
- items = manager.get_content_table(db_table)
198
- df = pd.DataFrame(items)
199
- df = df.dropna(axis=1, how='all')
200
- df = df.fillna('')
201
- #umap_embeddings = np.array(df["umap_embeddings"].tolist())
202
-
203
- else:
204
- raise PreventUpdate
205
-
206
- # Plotting
207
- return create_umap_plot(df)
208
-
209
-
210
- @app.callback(
211
- Output("point-details", "children"),
212
- Input("umap-graph", "clickData")
213
- )
214
- def show_point_details(clickData):
215
- if not clickData:
216
- return html.Div("🖱️ Click a point to see more details.", style={"color": "#888"})
217
- img_url, title, desc = clickData["points"][0]["customdata"]
218
- return html.Div([
219
- html.H4(title),
220
- html.Img(src=img_url, style={"maxWidth": "100%", "marginBottom": "10px"}),
221
- html.P(desc or "No description available.")
222
- ])
223
-
224
- # -------------------- Utility --------------------
225
-
226
- def harvest_omeka_items(client, item_set_id=None, per_page=50):
227
- """
228
- Fetch and parse items from Omeka S.
229
- Args:
230
- client: OmekaSClient instance
231
- item_set_id: ID of the item set to fetch items from (optional)
232
- per_page: Number of items to fetch per page (default: 50)
233
- Returns:
234
- DataFrame containing parsed item data
235
- """
236
- print("\n--- Fetching and Parsing Multiple Items by colection---")
237
- try:
238
- # Fetch first 5 items
239
- items_list = client.list_all_items(item_set_id=item_set_id, per_page=per_page)
240
- print(items_list)
241
- print(f"Fetched {len(items_list)} items.")
242
-
243
- parsed_items_list = []
244
- for item_raw in items_list:
245
- if 'o:media' in item_raw:
246
- parsed = client.digest_item_data(item_raw, prefixes=_DEFAULT_PARSE_METADATA)
247
- if parsed: # Only add if parsing was successful
248
- # Add media
249
- medias_id = [x["o:id"] for x in item_raw["o:media"]]
250
- medias_list = []
251
- for media_id in medias_id:
252
- media = client.get_media(media_id)
253
- if "image" in media["o:media_type"]:
254
- medias_list.append(media.get('o:original_url'))
255
- if medias_list: # Only append if there are image URLs
256
- parsed["images_urls"] = medias_list
257
- parsed_items_list.append(parsed)
258
- print(f"Successfully parsed {len(parsed_items_list)} items.")
259
-
260
- print(f"Successfully parsed {len(parsed_items_list)} items.")
261
- # Note: List columns (like dcterms:title) might need further handling in Pandas
262
- print("\nDataFrame from parsed items:")
263
- return pd.DataFrame(parsed_items_list)
264
- except OmekaSClientError as e:
265
- print(f"Error fetching/parsing multiple items: {e}")
266
- except Exception as e:
267
- print(f"An unexpected error occurred during multi-item parsing: {e}")
268
-
269
- def create_umap_plot(df):
270
- coords = np.array(df["umap_embeddings"].tolist())
271
- fig = px.scatter(
272
- df, x=coords[:, 0], y=coords[:, 1],
273
- color="Topic",
274
- custom_data=["images_urls", "Title", "Description"],
275
- hover_data=None,
276
- title="UMAP Projection with HDBSCAN Topics"
277
- )
278
- fig.update_traces(
279
- marker=dict(size=8, line=dict(width=1, color="DarkSlateGrey")),
280
- hovertemplate="<b>%{customdata[1]}</b><br><img src='%{customdata[0]}' height='150'><extra></extra>"
281
- )
282
- fig.update_layout(height=700, margin=dict(t=30, b=30, l=30, r=30))
283
- return fig, f"Loaded {len(df)} items and projected into 2D."
284
-
285
- if __name__ == "__main__":
286
- app.run(debug=True)
 
1
+ import dash
2
+ from dash import dcc, html, Input, Output, State, ctx
3
+ import dash_bootstrap_components as dbc
4
+ import plotly.express as px
5
+ import pandas as pd
6
+ import numpy as np
7
+ import umap
8
+ import hdbscan
9
+ import sklearn.feature_extraction.text as text
10
+ from dash.exceptions import PreventUpdate
11
+ import os
12
+ from dotenv import load_dotenv
13
+ import helpers
14
+ import lancedb
15
+ from omeka_s_api_client import OmekaSClient, OmekaSClientError
16
+ from lancedb_client import LanceDBManager
17
+
18
+ # Load .env for credentials
19
+ load_dotenv()
20
+ _DEFAULT_PARSE_METADATA = (
21
+ 'dcterms:identifier','dcterms:type','dcterms:title', 'dcterms:description',
22
+ 'dcterms:creator','dcterms:publisher','dcterms:date','dcterms:spatial',
23
+ 'dcterms:format','dcterms:provenance','dcterms:subject','dcterms:medium',
24
+ 'bibo:annotates','bibo:content', 'bibo:locator', 'bibo:owner'
25
+ )
26
+
27
+ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
28
+ app.config.suppress_callback_exceptions = True
29
+ server = app.server
30
+ manager = LanceDBManager()
31
+
32
+ french_stopwords = text.ENGLISH_STOP_WORDS.union([
33
+ "alors", "au", "aucuns", "aussi", "autre", "avant", "avec", "avoir", "bon",
34
+ "car", "ce", "cela", "ces", "ceux", "chaque", "ci", "comme", "comment", "dans",
35
+ "des", "du", "dedans", "dehors", "depuis", "devrait", "doit", "donc", "dos",
36
+ "début", "elle", "elles", "en", "encore", "essai", "est", "et", "eu", "fait",
37
+ "faites", "fois", "font", "hors", "ici", "il", "ils", "je", "juste", "la", "le",
38
+ "les", "leur", "là", "ma", "maintenant", "mais", "mes", "mine", "moins", "mon",
39
+ "mot", "même", "ni", "nommés", "notre", "nous", "nouveaux", "ou", "où", "par",
40
+ "parce", "parole", "pas", "personnes", "peut", "peu", "pièce", "plupart", "pour",
41
+ "pourquoi", "quand", "que", "quel", "quelle", "quelles", "quels", "qui", "sa",
42
+ "sans", "ses", "seulement", "si", "sien", "son", "sont", "sous", "soyez", "sujet",
43
+ "sur", "ta", "tandis", "tellement", "tels", "tes", "ton", "tous", "tout", "trop",
44
+ "très", "tu", "valeur", "voie", "voient", "vont", "votre", "vous", "vu", "ça",
45
+ "étaient", "état", "étions", "été", "être"
46
+ ])
47
+
48
+ # -------------------- Layout --------------------
49
+ app.layout = dbc.Container([
50
+ html.H2("🌍 Omeka S UMAP Explorer", className="text-center mt-4"),
51
+ html.Hr(),
52
+
53
+ # Input controls
54
+ dbc.Row([
55
+ dbc.Col([
56
+ html.H5("🔍 From Omeka S"),
57
+ dcc.Input(id="api-url", value="https://your-omeka-instance.org", type="text", className="form-control"),
58
+ dbc.Button("Load Item Sets", id="load-sets", color="secondary", className="mt-2"),
59
+ dcc.Dropdown(id="items-sets-dropdown", placeholder="Select a collection"),
60
+ dcc.Input(id="table-name", value="my_table", type="text", className="form-control mt-2", placeholder="New table name"),
61
+ dbc.Button("Process Omeka Collection", id="load-data", color="primary", className="mt-2"),
62
+ ], md=4),
63
+
64
+ dbc.Col([
65
+ html.H5("📁 From LanceDB"),
66
+ dbc.Button("Load Existing Tables", id="load-tables", color="info"),
67
+ dcc.Dropdown(id="db-tables-dropdown", placeholder="Select an existing table"),
68
+ dbc.Button("Display Table", id="load-data-db", color="success", className="mt-2"),
69
+ ], md=4),
70
+
71
+ dbc.Col([
72
+ html.H5("🔎 Query Tool (coming soon)"),
73
+ dbc.Input(placeholder="Type a search query...", type="text", disabled=True),
74
+ ], md=4),
75
+ ], className="mb-4"),
76
+
77
+ # Main plot area and metadata side panel
78
+ dbc.Row([
79
+ dbc.Col(
80
+ dcc.Graph(id="umap-graph", style={"height": "700px"}),
81
+ md=8
82
+ ),
83
+ dbc.Col(
84
+ html.Div(id="point-details", style={
85
+ "padding": "15px",
86
+ "borderLeft": "1px solid #ccc",
87
+ "height": "700px",
88
+ "overflowY": "auto"
89
+ }),
90
+ md=4
91
+ ),
92
+ ]),
93
+
94
+ # Status/info
95
+ html.Div(id="status", className="mt-3"),
96
+
97
+ dcc.Store(id="omeka-client-config", storage_type="session")
98
+ ], fluid=True)
99
+
100
+ # -------------------- Callbacks --------------------
101
+
102
+ @app.callback(
103
+ Output("items-sets-dropdown", "options"),
104
+ Output("omeka-client-config", "data"),
105
+ Input("load-sets", "n_clicks"),
106
+ State("api-url", "value"),
107
+ prevent_initial_call=True
108
+ )
109
+ def load_item_sets(n, base_url):
110
+ client = OmekaSClient(base_url, "...", "...", 50)
111
+ try:
112
+ item_sets = client.list_all_item_sets()
113
+ options = [{"label": s.get('dcterms:title', [{}])[0].get('@value', 'N/A'), "value": s["o:id"]} for s in item_sets]
114
+ return options, {
115
+ "base_url": base_url,
116
+ "key_identity": "...",
117
+ "key_credential": "...",
118
+ "default_per_page": 50
119
+ }
120
+ except Exception as e:
121
+ return dash.no_update, dash.no_update
122
+
123
+ @app.callback(
124
+ Output("db-tables-dropdown", "options"),
125
+ Input("load-tables", "n_clicks"),
126
+ prevent_initial_call=True
127
+ )
128
+ def list_tables(n):
129
+ return [{"label": t, "value": t} for t in manager.list_tables()]
130
+
131
+ @app.callback(
132
+ Output("umap-graph", "figure"),
133
+ Output("status", "children"),
134
+ Input("load-data", "n_clicks"), # From Omeka S
135
+ Input("load-data-db", "n_clicks"), # From DB table
136
+ State("items-sets-dropdown", "value"),
137
+ State("omeka-client-config", "data"),
138
+ State("table-name", "value"),
139
+ State("db-tables-dropdown", "value"),
140
+ prevent_initial_call=True
141
+ )
142
+ def handle_data_loading(n_clicks_omeka, n_clicks_db, item_set_id, client_config, table_name, db_table):
143
+ triggered_id = ctx.triggered_id
144
+ print(triggered_id)
145
+
146
+ if triggered_id == "load-data": # Omeka S case
147
+ if not client_config:
148
+ raise PreventUpdate
149
+
150
+ client = OmekaSClient(
151
+ base_url=client_config["base_url"],
152
+ key_identity=client_config["key_identity"],
153
+ key_credential=client_config["key_credential"]
154
+ )
155
+
156
+ df_omeka = harvest_omeka_items(client, item_set_id=item_set_id)
157
+ items = df_omeka.to_dict(orient="records")
158
+ records_with_text = [helpers.add_concatenated_text_field_exclude_keys(item, keys_to_exclude=['id','images_urls'], text_field_key='text', pair_separator=' - ') for item in items]
159
+ df = helpers.prepare_df_atlas(pd.DataFrame(records_with_text), id_col='id', images_col='images_urls')
160
+
161
+ text_embed = helpers.generate_text_embed(df['text'].tolist())
162
+ img_embed = helpers.generate_img_embed(df['images_urls'].tolist())
163
+ embeddings = np.concatenate([text_embed, img_embed], axis=1)
164
+ df["embeddings"] = embeddings.tolist()
165
+
166
+ reducer = umap.UMAP(n_neighbors=15, min_dist=0.1, metric="cosine")
167
+ umap_embeddings = reducer.fit_transform(embeddings)
168
+ df["umap_embeddings"] = umap_embeddings.tolist()
169
+
170
+ clusterer = hdbscan.HDBSCAN(min_cluster_size=10)
171
+ cluster_labels = clusterer.fit_predict(umap_embeddings)
172
+ df["Cluster"] = cluster_labels
173
+
174
+ vectorizer = text.TfidfVectorizer(max_features=1000, stop_words=list(french_stopwords), lowercase=True)
175
+ tfidf_matrix = vectorizer.fit_transform(df["text"].astype(str).tolist())
176
+ top_words = []
177
+ for label in sorted(df["Cluster"].unique()):
178
+ if label == -1:
179
+ top_words.append("Noise")
180
+ continue
181
+ mask = (df["Cluster"] == label).to_numpy().nonzero()[0]
182
+ cluster_docs = tfidf_matrix[mask]
183
+ mean_tfidf = cluster_docs.mean(axis=0)
184
+ mean_tfidf = np.asarray(mean_tfidf).flatten()
185
+ top_indices = mean_tfidf.argsort()[::-1][:5]
186
+ terms = [vectorizer.get_feature_names_out()[i] for i in top_indices]
187
+ top_words.append(", ".join(terms))
188
+ cluster_name_map = {label: name for label, name in zip(sorted(df["Cluster"].unique()), top_words)}
189
+ df["Topic"] = df["Cluster"].map(cluster_name_map)
190
+
191
+ manager.initialize_table(table_name)
192
+ manager.add_entry(table_name, df.to_dict(orient="records"))
193
+
194
+ elif triggered_id == "load-data-db": # Load existing LanceDB table
195
+ if not db_table:
196
+ raise PreventUpdate
197
+ items = manager.get_content_table(db_table)
198
+ df = pd.DataFrame(items)
199
+ df = df.dropna(axis=1, how='all')
200
+ df = df.fillna('')
201
+ #umap_embeddings = np.array(df["umap_embeddings"].tolist())
202
+
203
+ else:
204
+ raise PreventUpdate
205
+
206
+ # Plotting
207
+ return create_umap_plot(df)
208
+
209
+
210
+ @app.callback(
211
+ Output("point-details", "children"),
212
+ Input("umap-graph", "clickData")
213
+ )
214
+ def show_point_details(clickData):
215
+ if not clickData:
216
+ return html.Div("🖱️ Click a point to see more details.", style={"color": "#888"})
217
+ img_url, title, desc = clickData["points"][0]["customdata"]
218
+ return html.Div([
219
+ html.H4(title),
220
+ html.Img(src=img_url, style={"maxWidth": "100%", "marginBottom": "10px"}),
221
+ html.P(desc or "No description available.")
222
+ ])
223
+
224
+ # -------------------- Utility --------------------
225
+
226
+ def harvest_omeka_items(client, item_set_id=None, per_page=50):
227
+ """
228
+ Fetch and parse items from Omeka S.
229
+ Args:
230
+ client: OmekaSClient instance
231
+ item_set_id: ID of the item set to fetch items from (optional)
232
+ per_page: Number of items to fetch per page (default: 50)
233
+ Returns:
234
+ DataFrame containing parsed item data
235
+ """
236
+ print("\n--- Fetching and Parsing Multiple Items by colection---")
237
+ try:
238
+ # Fetch first 5 items
239
+ items_list = client.list_all_items(item_set_id=item_set_id, per_page=per_page)
240
+ print(items_list)
241
+ print(f"Fetched {len(items_list)} items.")
242
+
243
+ parsed_items_list = []
244
+ for item_raw in items_list:
245
+ if 'o:media' in item_raw:
246
+ parsed = client.digest_item_data(item_raw, prefixes=_DEFAULT_PARSE_METADATA)
247
+ if parsed: # Only add if parsing was successful
248
+ # Add media
249
+ medias_id = [x["o:id"] for x in item_raw["o:media"]]
250
+ medias_list = []
251
+ for media_id in medias_id:
252
+ media = client.get_media(media_id)
253
+ if "image" in media["o:media_type"]:
254
+ medias_list.append(media.get('o:original_url'))
255
+ if medias_list: # Only append if there are image URLs
256
+ parsed["images_urls"] = medias_list
257
+ parsed_items_list.append(parsed)
258
+ print(f"Successfully parsed {len(parsed_items_list)} items.")
259
+
260
+ print(f"Successfully parsed {len(parsed_items_list)} items.")
261
+ # Note: List columns (like dcterms:title) might need further handling in Pandas
262
+ print("\nDataFrame from parsed items:")
263
+ return pd.DataFrame(parsed_items_list)
264
+ except OmekaSClientError as e:
265
+ print(f"Error fetching/parsing multiple items: {e}")
266
+ except Exception as e:
267
+ print(f"An unexpected error occurred during multi-item parsing: {e}")
268
+
269
+ def create_umap_plot(df):
270
+ coords = np.array(df["umap_embeddings"].tolist())
271
+ fig = px.scatter(
272
+ df, x=coords[:, 0], y=coords[:, 1],
273
+ color="Topic",
274
+ custom_data=["images_urls", "Title", "Description"],
275
+ hover_data=None,
276
+ title="UMAP Projection with HDBSCAN Topics"
277
+ )
278
+ fig.update_traces(
279
+ marker=dict(size=8, line=dict(width=1, color="DarkSlateGrey")),
280
+ hovertemplate="<b>%{customdata[1]}</b><br><img src='%{customdata[0]}' height='150'><extra></extra>"
281
+ )
282
+ fig.update_layout(height=700, margin=dict(t=30, b=30, l=30, r=30))
283
+ return fig, f"Loaded {len(df)} items and projected into 2D."
284
+
285
+ if __name__ == "__main__":
286
+ app.run(debug=True, port=7860)