Spaces:
Sleeping
Sleeping
File size: 10,361 Bytes
02dc410 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
import base64
import re
import io
from PIL import Image, ImageDraw, ImageFont
class ReadmeProcessor:
def __init__(self):
"""
Initializes the ReadmeProcessor.
"""
self.image_frames_base64 = []
# Try to load a default font for annotation, fall back to a generic one if not found
try:
self.font = ImageFont.truetype("arial.ttf", 24) # Increased font size for 840x840 image
except IOError:
self.font = ImageFont.load_default()
self.text_color = (255, 0, 0) # Red color for text (R, G, B)
self.target_image_size = (240, 240) # New fixed size for all images
def _parse_image_references(self, text: str) -> list[int]:
"""
Parses a given text to find all image reference blocks like '[Image X, Y, Z]'
or '[Image A]' and returns a flat, sorted list of unique 0-indexed image numbers.
Duplicate image references within the same paragraph (across multiple blocks or within one)
are handled. The list is sorted by the original image index to ensure "12345..." order.
Args:
text: The paragraph text to parse.
Returns:
A sorted list of unique 0-indexed integers representing image numbers.
Returns an empty list if no valid references are found.
"""
unique_image_indices = set()
matches = re.findall(r'\[Image\s*((?:\d+\s*(?:,\s*\d+\s*)*))\]', text)
for match_str in matches:
for num_str in match_str.split(','):
try:
img_num = int(num_str.strip()) - 1 # Adjust to 0-indexed
if 0 <= img_num < len(self.image_frames_base64):
unique_image_indices.add(img_num)
else:
print(f"Warning: Image index {img_num + 1} out of bounds. Skipping in block '{match_str}'.")
except ValueError:
print(f"Warning: Could not parse image number from '{num_str.strip()}' in block '{match_str}'. Skipping.")
# Return a sorted list of unique indices to maintain order (1, 2, 3, ...)
return sorted(list(unique_image_indices))
def _process_single_image_for_display(self, image_index: int) -> str | None:
"""
Loads an image, resizes it to the target_image_size, annotates it with its
1-indexed display number, and returns its base64 encoding.
Args:
image_index: The 0-indexed position of the image in self.image_frames_base64.
Returns:
A base64 encoded string of the resized and annotated image in PNG format,
or None if an error occurs.
"""
try:
img_data_b64 = self.image_frames_base64[image_index]
img = Image.open(io.BytesIO(base64.b64decode(img_data_b64))).convert("RGB")
# Resize the image to the fixed target size (840x840)
img = img.resize(self.target_image_size, Image.LANCZOS)
draw = ImageDraw.Draw(img)
# Use 1-indexed number for display
text_to_draw = f"{image_index + 1}"
# Get text bounding box for precise positioning
try:
# Use textbbox for PIL 9.2.0+
bbox = draw.textbbox((0, 0), text_to_draw, font=self.font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
except AttributeError:
# Fallback for older PIL versions (less accurate)
text_width, text_height = draw.textsize(text_to_draw, font=self.font)
# Position text at the top-left corner with a small padding
padding = 10 # Increased padding for larger image
text_x = padding
text_y = padding
draw.text((text_x, text_y), text_to_draw, font=self.font, fill=self.text_color)
buffered = io.BytesIO()
img.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode('utf-8')
except (IndexError, TypeError, ValueError, IOError) as e:
print(f"Error processing image at index {image_index} for annotation: {e}. Skipping.")
return None
def _get_image_markdown_tag(self, base64_image_data: str, alt_text: str = "Annotated Image") -> str:
"""
Generates a Markdown image tag from base64 data.
"""
return f""
def process_readme(self, readme_text: str, image_frames_base64: list[str]) -> str:
"""
Processes the input README text, finds unique image references in each paragraph,
resizes and annotates each image, and embeds them as individual Markdown images
below the respective paragraph. Images are ordered by their original index,
and displayed with a maximum of two images per line.
Args:
readme_text: The full README text content.
image_frames_base64: A list of base64 encoded strings, representing image frames.
Returns:
The processed README text with embedded images.
"""
processed_lines = []
self.image_frames_base64 = image_frames_base64
paragraphs = readme_text.split('\n\n')
for paragraph in paragraphs:
processed_lines.append(paragraph) # Add the original paragraph
# Get all unique, sorted image indices for this paragraph
unique_image_indices = self._parse_image_references(paragraph)
if unique_image_indices:
image_tags_for_paragraph = []
for img_idx in unique_image_indices:
annotated_b64 = self._process_single_image_for_display(img_idx)
if annotated_b64:
image_tags_for_paragraph.append(self._get_image_markdown_tag(annotated_b64, f"Image {img_idx + 1}"))
else:
print(f"Could not process image {img_idx + 1} for paragraph: '{paragraph[:50]}...'")
# Arrange image tags with max 2 per line
if image_tags_for_paragraph:
processed_lines.append("\n") # Add a newline before image block
for i in range(0, len(image_tags_for_paragraph), 2):
row_images = image_tags_for_paragraph[i:i+2]
# Join with a space to place images side-by-side in Markdown
#comment
# processed_lines.append(" ".join(row_images))
# Add an empty string (newline) after each row of images,
# to ensure the next row or paragraph starts on a new line.
# This avoids issues if the last row only has one image.
processed_lines.append("")
# Ensure there's always an empty line after a paragraph block
# This handles cases where there were no images for the paragraph
# or if the last image row already added a newline.
if processed_lines and processed_lines[-1] != "":
processed_lines.append("")
return "\n".join(processed_lines).strip()
# --- Example Usage ---
if __name__ == "__main__":
# Create some dummy base64 image data for demonstration
def create_dummy_image_b64(width, height, color):
img = Image.new('RGB', (width, height), color)
buffered = io.BytesIO()
img.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode('utf-8')
# Simulate 6 image frames with varying original sizes (they will all be resized to 840x840)
dummy_frames = [
create_dummy_image_b64(150, 80, (255, 200, 200)), # Light Red
create_dummy_image_b64(220, 160, (200, 255, 200)), # Light Green
create_dummy_image_b64(180, 140, (200, 200, 255)), # Light Blue
create_dummy_image_b64(250, 170, (255, 255, 200)), # Light Yellow
create_dummy_image_b64(190, 155, (200, 255, 255)), # Light Cyan
create_dummy_image_b64(210, 145, (255, 200, 255)) # Light Magenta
]
# Your example README text
readme_content = """
The earliest verifiable publication of this video is on November 13, 2023. The video was posted to the Telegram channel @SerajSat at 11:03 AM [0]. This represents the first known instance of the video's circulation based on the provided sources.
The visual information within the keyframes shows a daytime scene with diffuse lighting, likely due to heavy smoke, dust, or fog, which obscures clear shadows for a more precise time-of-day analysis [Image 1, 1, 2, 6]. The presence of numerous Palestine Red Crescent Society (PRCS) ambulances gathered outside a building suggests an emergency situation [Image 3].
Given that the video was published on November 13, 2023, it is highly probable that the event depicted was filmed on or very shortly before that date and time. This conclusion is further supported by additional visual cues [Image 4, 5].
"""
# Initialize the processor with dummy frames
processor = ReadmeProcessor()
# Process the README content
processed_readme = processor.process_readme(readme_content, dummy_frames)
# Print the result (this would typically be saved to a .md file or displayed)
print("--- Processed README Content ---")
print(processed_readme)
# Save the processed README content to a file
with open("processed_readme.md", "w") as f:
f.write(processed_readme)
# Example with no image references
print("\n--- Processed README Content (No Images) ---")
no_image_readme = "This paragraph has no images. Another paragraph follows."
processed_no_image_readme = processor.process_readme(no_image_readme, dummy_frames)
print(processed_no_image_readme)
# Example with out-of-bounds image reference
print("\n--- Processed README Content (Out-of-bounds Image) ---")
out_of_bounds_readme = "This paragraph references an image that doesn't exist [Image 99]."
processed_out_of_bounds_readme = processor.process_readme(out_of_bounds_readme, dummy_frames)
print(processed_out_of_bounds_readme) |