Spaces:
Running
Running
File size: 16,294 Bytes
87e5c9c 77d5469 2d980d5 87e5c9c 7e0dde7 8312087 77d5469 2d980d5 77d5469 8312087 9d26661 8312087 77d5469 62a2921 8312087 87e5c9c c1cba4f 471b053 9d26661 77d5469 ca983bc 77d5469 8312087 9e8f29e 80098ed 9e8f29e 80098ed 62a2921 80098ed 62a2921 80098ed 9e8f29e 80098ed 62a2921 ca983bc 80098ed 62a2921 2d980d5 7e0dde7 2d980d5 7e0dde7 2d980d5 7e0dde7 8312087 87e5c9c 079d1ca 77d5469 34de38e 7e0dde7 34de38e 77d5469 079d1ca 7e0dde7 87e5c9c 7e0dde7 87e5c9c 9e8f29e 87e5c9c 9e8f29e 87e5c9c 9e8f29e 87e5c9c 3ea8fe3 87e5c9c 7e0dde7 87e5c9c 925dd67 7452863 925dd67 3ea8fe3 87e5c9c 3ea8fe3 87e5c9c 34de38e 079d1ca 7e0dde7 895976b c430753 895976b c430753 7e0dde7 2d980d5 e9ed1f2 c9456c2 e9ed1f2 471b053 e9ed1f2 471b053 c9456c2 471b053 77d5469 e9ed1f2 77d5469 e9ed1f2 471b053 e9ed1f2 471b053 e9ed1f2 471b053 e9ed1f2 471b053 e9ed1f2 471b053 e9ed1f2 471b053 e9ed1f2 471b053 e9ed1f2 471b053 77d5469 471b053 c9456c2 471b053 03e9034 7e0dde7 34de38e 03e9034 34de38e 77d5469 32939cb 34de38e e219aa1 34de38e c9456c2 55b49e6 471b053 32939cb 471b053 77d5469 34de38e 03e9034 34de38e 03e9034 34de38e 32939cb 34de38e e219aa1 03e9034 34de38e 03e9034 e219aa1 03e9034 34de38e |
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 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 |
"""
utils.py - Utility functions for the project.
"""
import logging
import os
import re
import string
import subprocess
from collections import defaultdict, deque
from datetime import datetime, timedelta
from itertools import combinations, islice
from pathlib import Path
from typing import List
logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
level=logging.INFO,
)
import torch
from natsort import natsorted
from nltk.tokenize import WhitespaceTokenizer, sent_tokenize, word_tokenize
from rapidfuzz import fuzz
STOPWORDS = set(
"a about above after again all also am an and any are aren't as at back be because been before being below between both but by can't cannot could couldn't did didn't do does doesn't doing don't down during each few for from further had hadn't has hasn't have haven't having he'd he'll he's hence her here here's hers herself him himself his how how's however i'd i'll i'm i've if in into is isn't it's its itself just let's me more moreover most mustn't my myself new nor now of off on once only or other ought our ours ourselves out over own really same shan't she'd she'll she's should shouldn't so some such than that that's the their theirs them themselves then there there's therefore these they they'd they'll they're they've this those through thus to too under until up use used using very was wasn't we we'd we'll we're we've were weren't what what's when when's where where's which while who who's whom why why's with won't would wouldn't you'd you'll you're you've your yours yourself yourselves".split()
)
def contraction_aware_tokenize(text: str) -> List[str]:
"""contraction_aware_tokenize - merges words containing apostrophes as one token."""
# Tokenize the text using the WhitespaceTokenizer
tokenizer = WhitespaceTokenizer()
tokens = tokenizer.tokenize(text)
merged_tokens = []
merged_token = ""
for token in tokens:
if re.search(r"\w+'\w+", token):
# Token contains an apostrophe, merge with previous token
merged_token += token
else:
# no apostrophe, add previous merged token (if any) and current
if merged_token:
merged_tokens.append(merged_token)
merged_token = ""
merged_tokens.append(token)
# Add the last merged token (if any)
if merged_token:
merged_tokens.append(merged_token)
return merged_tokens
def remove_stopwords(
text: str, stopwords: List[str] = STOPWORDS, contraction_tokenize: bool = True
) -> str:
"""
remove_stopwords - Remove stopwords from text.
:param str text: input text
:param List[str] stopwords: list of stopwords, defaults to STOPWORDS
:param bool contraction_tokenize: use custom apostrophe tokenizer, defaults to True
:return str: text with stopwords removed
"""
lines = text.split("\n")
filtered_lines = []
def fix_commas(text: str) -> str:
"""fixes commas in text to have a space after them"""
spaced_text = text.replace(",", ", ")
return spaced_text.replace(" ", " ").strip()
for line in lines:
sentences = sent_tokenize(line)
filtered_sentences = []
for sentence in sentences:
# Add space around punctuations for the regex to work correctly, only if they are followed by a letter
sentence_with_spaces = re.sub(r"([.,!?])(\w)", r"\1 \2", sentence[:-1])
words = (
contraction_aware_tokenize(sentence_with_spaces)
if contraction_tokenize
else word_tokenize(sentence_with_spaces)
)
filtered_words = []
for word in words:
if word.lower() not in stopwords:
filtered_words.append(word)
filtered_sentence = " ".join(filtered_words)
# Restore original spaces around punctuation marks
filtered_sentence = re.sub(r"([.,!?])\s*", r"\1", filtered_sentence)
filtered_sentences.append(filtered_sentence + sentence[-1])
filtered_line = " ".join(filtered_sentences)
# Replace multiple consecutive whitespaces with a single space
filtered_line = re.sub(r"\s+", " ", filtered_line)
filtered_line = fix_commas(filtered_line.strip())
filtered_lines.append(filtered_line)
filtered_text = "\n".join(filtered_lines)
return filtered_text
def remove_stagnant_files(
freq: str = "hourly",
search_path: str = ".",
substring="DocSumm",
remove_suffix=".txt",
):
"""
remove_stagnant_files - Remove files that have not been modified in a certain amount of time.
:param str freq: frequency of file removal, defaults to "hourly"
:param str search_path: location to search for files, defaults to "."
:param str substring: substring to search for in file names, defaults to "DocSumm"
:param str remove_suffix: suffix of files to remove, defaults to ".txt"
:raises ValueError: if freq is not one of "hourly", "daily", or "weekly"
"""
current_time = datetime.now()
search_path = Path(search_path)
if freq == "hourly":
time_threshold = current_time - timedelta(hours=1)
elif freq == "daily":
time_threshold = current_time - timedelta(days=1)
elif freq == "weekly":
time_threshold = current_time - timedelta(weeks=1)
else:
raise ValueError(
"Invalid frequency. Supported values are 'hourly', 'daily', and 'weekly'."
)
files_to_remove = []
potential_files = [
f for f in search_path.iterdir() if f.is_file() and f.suffix == remove_suffix
]
logging.info(f"Found {len(potential_files)} files.")
for candidate in potential_files:
if (
candidate.is_file()
and substring in candidate.name
and candidate.stat().st_mtime < time_threshold.timestamp()
):
files_to_remove.append(candidate)
logging.debug(f"File {candidate} last modified at {candidate.stat().st_mtime}")
logging.info(f"Removing {len(files_to_remove)} files.")
for file_path in files_to_remove:
file_path.unlink()
logging.debug(f"Removed files: {files_to_remove}")
def compare_model_size(model_name: str, threshold: int = 500) -> bool:
"""
compare_model_size - compare string representations of model size to a threshold
:param str model_name: the model name to compare
:param int threshold: the threshold to compare against in millions, defaults to 500
:return: True if the model size is greater than the threshold, False or None otherwise
"""
pattern = r"(\d+)(M|G|k|b)?" # param regex
matches = re.findall(pattern, model_name)
if not matches:
return None
# Extract the parameter count and unit
parameter_count, unit = matches[-1]
parameter_count = int(parameter_count)
# Convert to the standard form (M for million, G for billion, k for thousand)
if unit == "G" or unit == "b":
parameter_count *= 1000
elif unit == "M":
pass
elif unit == "k":
parameter_count /= 1000
else:
return None # Unknown
return parameter_count > threshold
def validate_pytorch2(torch_version: str = None) -> bool:
"""
validate_pytorch2 - validate that the PyTorch version is 2.0 or greater
:param str torch_version: the PyTorch version to validate, defaults to None
:return: True if the PyTorch version is 2.0 or greater, False otherwise
"""
torch_version = torch.__version__ if torch_version is None else torch_version
pattern = r"^2\.\d+(\.\d+)*"
return True if re.match(pattern, torch_version) else False
def get_timestamp(detailed=False) -> str:
"""
get_timestamp - get a timestamp for the current time
:param bool detailed: whether to include seconds and microseconds, defaults to False
:return: str, the timestamp
"""
return (
datetime.now().strftime("%b%d%Y_%H%M%S%f")
if detailed
else datetime.now().strftime("%b%d%Y_%H")
)
def truncate_word_count(text: str, max_words=1024) -> dict:
"""
truncate_word_count - truncate a text to a maximum number of words
:param str text: the text to truncate
:param int max_words: the maximum number of words to keep, defaults to 1024
:return: dict, the processed text
"""
words = contraction_aware_tokenize(str(text))
processed = {}
if len(words) > max_words:
processed["was_truncated"] = True
processed["processed_text"] = " ".join(words[:max_words])
else:
processed["was_truncated"] = False
processed["processed_text"] = text
return processed
def load_examples(src, filetypes=[".txt", ".pdf"]):
"""
load_examples - a helper function for the gradio module to load examples
:param str src: the path to the examples
"""
src = Path(src)
src.mkdir(exist_ok=True)
pdf_url = (
"https://www.dropbox.com/s/y92xy7o5qb88yij/all_you_need_is_attention.pdf?dl=1"
)
subprocess.run(["wget", pdf_url, "-O", src / "all_you_need_is_attention.pdf"])
examples = [f for f in src.iterdir() if f.suffix in filetypes]
examples = natsorted(examples)
# load the examples into a list
text_examples = []
for example in examples:
with open(example, "r") as f:
text = f.read()
text_examples.append([text, "base", 2, 1024, 0.7, 3.5, 3])
return text_examples
def load_example_filenames(example_path: str or Path):
"""
load_example_filenames - a helper function for the gradio module to load examples
Returns:
dict, the examples (filename:full path)
"""
example_path = Path(example_path)
# load the examples into a list
examples = {f.name: f for f in example_path.glob("*.txt")}
return examples
def textlist2html(text_batches: List[str]) -> str:
"""textlist2html - convert a list of text summaries into a single HTML string"""
# Step 1: Generate each summary batch as a string of HTML
formatted_batches = [
f"""
<div style="
margin-bottom: 20px;
font-size: 18px;
line-height: 1.5em;
color: #333;
">
<h2 style="font-size: 22px; color: #555;">Batch {i}:</h2>
<p style="white-space: pre-line;">{s}</p>
</div>
"""
for i, s in enumerate(text_batches, start=1)
]
# Step 2: Join all the summary batches together into one string
joined_batches = "".join(formatted_batches)
# Step 3: Wrap the summary string in a larger div with background color, border, and padding
text_html_block = f"""
<div style="
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
">
{joined_batches}
</div>
"""
return text_html_block
def extract_batches(html_string: str, pattern=None, flags=None) -> list:
"""
Extract batches of text from an HTML string.
Args:
html_string (str): The HTML string to extract batches from.
pattern (str, optional): The regular expression pattern to use. Defaults to a pattern that matches batches in the format provided.
flags (int, optional): The flags to use with the regular expression. Defaults to re.DOTALL.
Returns:
list: A list of dictionaries where each dictionary represents a batch and has 'title' and 'content' keys.
"""
# Set default pattern if none provided
if pattern is None:
pattern = r'<h2 style="font-size: 22px; color: #555;">(.*?)</h2>\s*<p style="white-space: pre-line;">(.*?)</p>'
# Set default flags if none provided
if flags is None:
flags = re.DOTALL
try:
# Find all matches in the string
matches = re.findall(pattern, html_string, flags)
# Convert matches to a list of dictionaries
batches = [
{"title": title.strip(), "content": content.strip()}
for title, content in matches
]
return batches
except re.error as e:
logging.error(f"An error occurred while trying to extract batches: {e}")
return []
def extract_keywords(
text: str, num_keywords: int = 3, window_size: int = 5, kw_max_len: int = 20
) -> List[str]:
"""
Extracts keywords from a text using a simplified TextRank algorithm.
Args:
text: The text to extract keywords from.
num_keywords: The number of keywords to extract. Default: 3
window_size: The number of words considered for co-occurrence. Default: 5
kw_max_len: The maximum length of a keyword (truncate longer keywords to max). Default: 20
Returns:
A list of strings, where each string is a keyword extracted from the input text.
"""
logger = logging.getLogger(__name__)
# Remove stopwords and tokenize the text into words
words = [
word
for word in re.findall(r"\b\w{3,}\b", text.lower())
if word not in STOPWORDS
]
# Create a graph of word co-occurrences within a moving window of words
cooccur = defaultdict(lambda: defaultdict(int))
deque_words = deque(maxlen=window_size)
for word in words:
for w1, w2 in combinations(deque_words, 2):
cooccur[w1][w2] += 1
cooccur[w2][w1] += 1
deque_words.append(word)
# Assign scores to words using a simplified TextRank algorithm
scores = defaultdict(float)
for _ in range(10):
new_scores = defaultdict(float)
for word, co_words in cooccur.items():
new_scores[word] = 0.15 + 0.85 * sum(
cooccur[word][other] / sum(cooccur[other].values()) * scores[other]
for other in co_words
)
scores = new_scores
# Sort the words by score and return the top num_keywords keywords
keywords = sorted(scores, key=scores.get, reverse=True)[:num_keywords]
logger.debug(f"All keywords: {keywords}")
# Use fuzzy matching to remove similar keywords
final_keywords = []
for keyword in keywords:
if not any(fuzz.ratio(keyword, other) > 70 for other in final_keywords):
final_keywords.append(keyword[:kw_max_len])
logger.debug(f"Keywords (max len. {kw_max_len}):\t{final_keywords}")
return final_keywords
def saves_summary(
summarize_output, outpath: str or Path = None, add_signature=True, **kwargs
) -> Path:
"""
saves_summary - save the summary generated from summarize_via_tokenbatches() to a text file
summarize_output: output from summarize_via_tokenbatches()
outpath: path to the output file
add_signature: whether to add a signature to the output file
kwargs: additional keyword arguments to include in the output file
"""
logger = logging.getLogger(__name__)
sum_text = [f"{s['summary'][0]}\n" for s in summarize_output]
sum_scores = [f"\n - {round(s['summary_score'],4)}" for s in summarize_output]
scores_text = "\n".join(sum_scores)
full_summary = "\n".join(sum_text)
keywords = "_".join(extract_keywords(full_summary, kw_max_len=4))
logger.debug(f"kw:\t{keywords}")
outpath = (
Path.cwd() / f"DocSumm_{keywords}_{get_timestamp()}.txt"
if outpath is None
else Path(outpath)
)
logger.info(f"Saving summary to:\t{outpath.name}")
with open(
outpath,
"w",
encoding="utf-8",
) as fo:
fo.writelines(full_summary)
fo.write("\n\n")
if add_signature:
fo.write("\n\n---\n\n")
fo.write("Generated with the Document Summarization space :)\n\n")
fo.write("https://hf.co/spaces/pszemraj/document-summarization\n\n")
with open(
outpath,
"a",
encoding="utf-8",
) as fo:
fo.write("\n")
fo.write(f"## Section Scores:\n\n")
fo.writelines(scores_text)
fo.write("\n\n")
fo.write(f"Date: {get_timestamp()}\n\n")
if kwargs:
fo.write("---\n\n")
fo.write("## Parameters:\n\n")
for key, value in kwargs.items():
fo.write(f"{key}: {value}\n")
return outpath
|