Files

296 lines
11 KiB
Python

import os
import fitz # PyMuPDF
import torch
from PIL import Image
import numpy as np
import folder_paths
from .utils import strip_path
from docx import Document
class DocumentLoaderNode:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and f.lower().endswith((".pdf", ".txt", ".doc", ".docx"))]
return {"required": {"file_path": (sorted(files),)}}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("parsed_text",)
FUNCTION = "load_document"
CATEGORY = "document_processing"
def load_document(self, file_path):
full_path = folder_paths.get_annotated_filepath(strip_path(file_path))
_, ext = os.path.splitext(full_path)
if ext.lower() == '.pdf':
return (self.parse_pdf(full_path),)
elif ext.lower() in ['.txt', '.doc', '.docx']:
return (self.parse_text(full_path),)
else:
raise ValueError(f"Unsupported file type: {ext}")
def parse_pdf(self, file_path):
text = ""
with fitz.open(file_path) as doc:
for page in doc:
text += page.get_text()
return text
def parse_text(self, file_path):
_, ext = os.path.splitext(file_path)
if ext.lower() == '.txt':
with open(file_path, 'r', encoding='utf-8') as file:
return file.read()
elif ext.lower() in ['.doc', '.docx']:
doc = Document(file_path)
return "\n".join([para.text for para in doc.paragraphs])
@classmethod
def IS_CHANGED(cls, file_path):
full_path = folder_paths.get_annotated_filepath(strip_path(file_path))
return full_path
@classmethod
def VALIDATE_INPUTS(cls, file_path):
full_path = folder_paths.get_annotated_filepath(strip_path(file_path))
return os.path.isfile(full_path)
class PDFToImageNode:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and f.lower().endswith(".pdf")]
return {
"required": {
"file_path": (sorted(files),),
"start_page": ("INT", {"default": 1, "min": 1, "max": 10000}),
"end_page": ("INT", {"default": 1, "min": 1, "max": 10000}),
"dpi": ("INT", {"default": 300, "min": 72, "max": 600}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "pdf_to_images"
CATEGORY = "document_processing"
OUTPUT_IS_LIST = (True,)
def pdf_to_images(self, file_path, start_page, end_page, dpi):
full_path = folder_paths.get_annotated_filepath(strip_path(file_path))
doc = fitz.open(full_path)
num_pages = len(doc)
start_page = max(1, min(start_page, num_pages))
end_page = max(start_page, min(end_page, num_pages))
images = []
for page_num in range(start_page - 1, end_page):
page = doc.load_page(page_num)
pix = page.get_pixmap(matrix=fitz.Matrix(dpi/72, dpi/72))
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
img_np = np.array(img).astype(np.float32) / 255.0
img_tensor = torch.from_numpy(img_np)[None,]
images.append(img_tensor)
doc.close()
return (images,)
@classmethod
def IS_CHANGED(cls, file_path, start_page, end_page, dpi):
full_path = folder_paths.get_annotated_filepath(strip_path(file_path))
return full_path
@classmethod
def VALIDATE_INPUTS(cls, file_path, start_page, end_page, dpi):
full_path = folder_paths.get_annotated_filepath(strip_path(file_path))
return os.path.isfile(full_path)
class PDFPageSplitterNode:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and f.lower().endswith(".pdf")]
return {
"required": {
"file_path": (sorted(files),),
"page_numbers": ("STRING", {"default": "1,2,3"}),
"dpi": ("INT", {"default": 300, "min": 72, "max": 600}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("selected_pages",)
FUNCTION = "pdf_to_selected_images"
CATEGORY = "document_processing"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
def pdf_to_selected_images(self, file_path, page_numbers, dpi):
full_path = folder_paths.get_annotated_filepath(strip_path(file_path))
doc = fitz.open(full_path)
num_pages = len(doc)
# Parse page numbers from input string
try:
selected_pages = [int(p.strip()) for p in page_numbers.split(',') if p.strip()]
selected_pages = [p for p in selected_pages if 1 <= p <= num_pages]
except ValueError:
raise ValueError("Invalid page numbers. Please provide comma-separated integers.")
images = []
for page_num in selected_pages:
page = doc.load_page(page_num - 1) # PDF pages are 0-indexed
pix = page.get_pixmap(matrix=fitz.Matrix(dpi/72, dpi/72))
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
img_np = np.array(img).astype(np.float32) / 255.0
img_tensor = torch.from_numpy(img_np)[None,]
images.append(img_tensor)
doc.close()
return (images,)
@classmethod
def IS_CHANGED(cls, file_path, page_numbers, dpi):
full_path = folder_paths.get_annotated_filepath(strip_path(file_path))
return full_path
@classmethod
def VALIDATE_INPUTS(cls, file_path, page_numbers, dpi):
full_path = folder_paths.get_annotated_filepath(strip_path(file_path))
if not os.path.isfile(full_path):
return False
try:
[int(p.strip()) for p in page_numbers.split(',') if p.strip()]
except ValueError:
return False
return True
class ImageSelectorNode:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"indexes": ("STRING", {"default": "0"}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "select_images"
CATEGORY = "image_processing"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
def select_images(self, images, indexes):
try:
index_list = [int(idx.strip()) for idx in indexes.split(',')]
except ValueError:
raise ValueError("Invalid indexes. Please provide comma-separated integers.")
selected_images = []
for idx in index_list:
if idx < 0 or idx >= len(images):
raise ValueError(f"Index {idx} is out of range. Available images: {len(images)}")
selected_images.append(images[idx])
return (selected_images,)
@classmethod
def IS_CHANGED(cls, images, indexes):
return indexes
class TextChunkerNode:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"multiline": True}),
"chunk_size": ("INT", {"default": 1000, "min": 1, "max": 10000}),
"chunk_method": (["words", "characters"],),
"respect_word_boundaries": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text_chunks",)
FUNCTION = "chunk_text"
CATEGORY = "document_processing"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
def chunk_text(self, text, chunk_size, chunk_method, respect_word_boundaries):
if not text:
return (["No text provided"],)
chunks = []
if chunk_method == "words":
words = text.split()
current_chunk = []
word_count = 0
for word in words:
current_chunk.append(word)
word_count += 1
if word_count >= chunk_size:
chunks.append(" ".join(current_chunk))
current_chunk = []
word_count = 0
if current_chunk:
chunks.append(" ".join(current_chunk))
else: # characters
if respect_word_boundaries:
words = text.split()
current_chunk = []
char_count = 0
for word in words:
if char_count + len(word) > chunk_size and current_chunk:
chunks.append(" ".join(current_chunk))
current_chunk = []
char_count = 0
current_chunk.append(word)
char_count += len(word) + 1 # +1 for space
if current_chunk:
chunks.append(" ".join(current_chunk))
else:
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
return (chunks,)
class ChunkRouterNode:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"chunks": ("STRING", {"forceInput": True}),
"indices": ("INT", {"forceInput": True}),
"selected_index": ("INT", {"default": 0, "min": 0, "max": 1000}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("selected_chunk",)
FUNCTION = "route_chunk"
CATEGORY = "document_processing"
def route_chunk(self, chunks, indices, selected_index):
if selected_index < 0 or selected_index >= len(chunks):
raise ValueError(f"Selected index {selected_index} is out of range. Available chunks: {len(chunks)}")
return (chunks[selected_index],)
# Update NODE_CLASS_MAPPINGS and NODE_DISPLAY_NAME_MAPPINGS
NODE_CLASS_MAPPINGS = {
"DocumentLoader": DocumentLoaderNode,
"PDFToImage": PDFToImageNode,
"PDFPageSplitter": PDFPageSplitterNode,
"ImageSelector": ImageSelectorNode,
"TextChunker": TextChunkerNode,
"ChunkRouter": ChunkRouterNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DocumentLoader": "Document Loader",
"PDFToImage": "PDF to Image (Multi-Page)",
"PDFPageSplitter": "PDF Page Splitter",
"ImageSelector": "Image Selector",
"TextChunker": "Text Chunker",
"ChunkRouter": "Chunk Router"
}