2626)
2727from functions_visio import build_visio_page_markdown , parse_vsdx_pages
2828from functions_content import *
29+ from functions_office_media import extract_office_embedded_images_with_diagnostics
2930from functions_content_understanding import analyze_image_with_content_understanding
3031from functions_settings import *
3132from functions_search import *
@@ -311,6 +312,29 @@ def _analyze_single_embedded_image(image_path, extraction_engine, image_extracti
311312 ).strip ()
312313
313314
315+ def _describe_embedded_image_skips (diagnostics ):
316+ """Render skip counts as a short, admin-readable explanation."""
317+ reasons = diagnostics .get ('skipped_reasons' ) or {}
318+ if not reasons :
319+ return ''
320+ friendly = {
321+ 'below_minimum_pixels' : 'too small' ,
322+ 'below_minimum_bytes' : 'too small' ,
323+ 'duplicate_image' : 'duplicates' ,
324+ 'unsupported_format' : 'unsupported format' ,
325+ 'unreadable_image' : 'unreadable' ,
326+ 'unreadable_or_oversized' : 'unreadable or oversized' ,
327+ 'per_document_cap_reached' : 'over the per-document cap' ,
328+ 'write_failed' : 'could not be written' ,
329+ 'unsafe_entry_name' : 'unsafe file name' ,
330+ }
331+ parts = []
332+ for reason , count in sorted (reasons .items (), key = lambda item : - item [1 ]):
333+ label = friendly .get (reason , reason .replace ('_' , ' ' ))
334+ parts .append (f"{ count } { label } " )
335+ return ", " .join (parts )
336+
337+
314338def _build_office_embedded_image_chunks (
315339 file_path ,
316340 settings ,
@@ -347,14 +371,39 @@ def _build_office_embedded_image_chunks(
347371
348372 try :
349373 temp_image_dir = tempfile .mkdtemp (prefix = 'office_images_' )
350- embedded_images = extract_office_embedded_images (
374+ embedded_images , image_diagnostics = extract_office_embedded_images_with_diagnostics (
351375 file_path ,
352376 temp_image_dir ,
353377 min_pixels = min_pixels ,
354378 max_images = max_images ,
355379 )
356380
381+ candidate_count = image_diagnostics .get ('candidates' , 0 )
357382 if not embedded_images :
383+ # Say so explicitly. Otherwise "no images in this file" and "images found but all
384+ # skipped" look identical in the workspace log, which is the common confusion.
385+ if candidate_count :
386+ skip_summary = _describe_embedded_image_skips (image_diagnostics )
387+ update_callback (
388+ status = (
389+ f"Found { candidate_count } embedded image(s), none analyzable"
390+ + (f" ({ skip_summary } )" if skip_summary else "" )
391+ ),
392+ office_embedded_image_count = 0 ,
393+ office_embedded_image_candidates = candidate_count ,
394+ office_embedded_image_skipped = image_diagnostics .get ('skipped' , 0 ),
395+ )
396+ log_event (
397+ f"[OFFICE_EMBEDDED_IMAGES] { os .path .basename (file_path )} : "
398+ f"{ candidate_count } candidate(s), none analyzable. { image_diagnostics } " ,
399+ level = logging .WARNING ,
400+ )
401+ else :
402+ update_callback (
403+ status = "No embedded images found in this document" ,
404+ office_embedded_image_count = 0 ,
405+ office_embedded_image_candidates = 0 ,
406+ )
358407 return [], 0 , extraction_engine
359408
360409 engine_label = (
@@ -364,10 +413,13 @@ def _build_office_embedded_image_chunks(
364413 )
365414 total_images = len (embedded_images )
366415 update_callback (
367- status = f"Analyzing { total_images } embedded image(s) with { engine_label } ..."
416+ status = f"Analyzing { total_images } of { candidate_count } embedded image(s) with { engine_label } ..."
368417 )
369418
370419 for image_index , embedded_image in enumerate (embedded_images , start = 1 ):
420+ update_callback (
421+ status = f"Analyzing embedded image { image_index } of { total_images } with { engine_label } ..."
422+ )
371423 try :
372424 analysis_text = _analyze_single_embedded_image (
373425 embedded_image ['path' ],
@@ -380,9 +432,18 @@ def _build_office_embedded_image_chunks(
380432 f"[OFFICE_EMBEDDED_IMAGES] Failed to analyze { embedded_image .get ('name' )} : { image_error } " ,
381433 level = logging .WARNING ,
382434 )
383- continue
384-
385- if not analysis_text :
435+ analysis_text = ''
436+
437+ # Text drawn inside a vector diagram is recovered during rasterization, so a figure
438+ # still contributes searchable labels even when the engine returns nothing.
439+ embedded_text = str (embedded_image .get ('embedded_text' ) or '' ).strip ()
440+ body_parts = []
441+ if analysis_text :
442+ body_parts .append (analysis_text )
443+ if embedded_text and embedded_text not in analysis_text :
444+ body_parts .append (f"Text labels in this figure:\n { embedded_text } " )
445+
446+ if not body_parts :
386447 continue
387448
388449 location_label = ''
@@ -396,13 +457,19 @@ def _build_office_embedded_image_chunks(
396457 )
397458 chunks .append ({
398459 'page_number' : starting_page_number + len (chunks ),
399- 'content' : f"{ heading } \n \n { analysis_text } " ,
460+ 'content' : f"{ heading } \n \n " + " \n \n " . join ( body_parts ) ,
400461 })
401462 analyzed_count += 1
402463
403464 if analyzed_count :
465+ skip_summary = _describe_embedded_image_skips (image_diagnostics )
404466 update_callback (
405- status = f"Analyzed { analyzed_count } embedded image(s) with { engine_label } ."
467+ status = (
468+ f"Analyzed { analyzed_count } of { candidate_count } embedded image(s) with { engine_label } ."
469+ + (f" Skipped: { skip_summary } ." if skip_summary else "" )
470+ ),
471+ office_embedded_image_candidates = candidate_count ,
472+ office_embedded_image_skipped = image_diagnostics .get ('skipped' , 0 ),
406473 )
407474 except Exception as embedded_image_error :
408475 log_event (
0 commit comments