Download core/writer_utils.py from deeme/long: direct link, hf CLI and curl.
- Browser
- Download file 8.01 kB
-
https://huggingface.co/spaces/deeme/long/resolve/main/core/writer_utils.py
- Command line
-
hf download hf://spaces/deeme/long/core/writer_utils.py
-
curl -L -o writer_utils.py https://huggingface.co/spaces/deeme/long/resolve/main/core/writer_utils.py
8.01 kB
| import uuid | |
| # 定义了用于Wirter yield的数据类型,同时也是前端展示的“关键点”消息 | |
| class KeyPointMsg(dict): | |
| def __init__(self, title='', subtitle='', prompt_name=''): | |
| super().__init__() | |
| if not title and not subtitle and prompt_name: | |
| pass | |
| elif title and subtitle and not prompt_name: | |
| pass | |
| else: | |
| raise ValueError('Either title and subtitle or prompt_name must be provided') | |
| self.update({ | |
| 'id': str(uuid.uuid4()), | |
| 'title': title, | |
| 'subtitle': subtitle, | |
| 'prompt_name': prompt_name, | |
| 'finished': False | |
| }) | |
| def set_finished(self): | |
| assert not self['finished'], 'finished flag is already set' | |
| self['finished'] = True | |
| return self # 返回self,方便链式调用 | |
| def is_finished(self): | |
| return self['finished'] | |
| def is_prompt(self): | |
| return bool(self.prompt_name) | |
| def is_title(self): | |
| return bool(self.title) | |
| def id(self): | |
| return self['id'] | |
| def title(self): | |
| return self['title'] | |
| def subtitle(self): | |
| return self['subtitle'] | |
| def prompt_name(self): | |
| prompt_name = self['prompt_name'] | |
| if len(prompt_name) >= 10: | |
| return prompt_name[:10] + '...' | |
| return prompt_name | |
| import re | |
| from difflib import Differ | |
| # 后续考虑采用现成的库实现,目前逻辑过于繁琐,而且太慢了 | |
| def detect_max_edit_span(a, b): | |
| diff = Differ().compare(a, b) | |
| l = 0 | |
| r = 0 | |
| flag_count_l = True | |
| for tag in diff: | |
| if tag.startswith(' '): | |
| if flag_count_l: | |
| l += 1 | |
| else: | |
| r += 1 | |
| else: | |
| flag_count_l = False | |
| r = 0 | |
| return l, -r | |
| def split_text_by_separators(text, separators, keep_separators=True): | |
| """ | |
| 将文本按指定的分隔符分割为段落 | |
| Args: | |
| text: 要分割的文本 | |
| separators: 分隔符列表 | |
| keep_separators: 是否在结果中保留分隔符,默认为True | |
| Returns: | |
| 包含分割后段落的列表 | |
| """ | |
| pattern = f'({"|".join(map(re.escape, separators))}+)' | |
| chunks = re.split(pattern, text) | |
| paragraphs = [] | |
| current_para = [] | |
| for i in range(0, len(chunks), 2): | |
| content = chunks[i] | |
| separator = chunks[i + 1] if i + 1 < len(chunks) else '' | |
| current_para.append(content) | |
| if keep_separators and separator: | |
| current_para.append(separator) | |
| if content.strip(): | |
| paragraphs.append(''.join(current_para)) | |
| current_para = [] | |
| return paragraphs | |
| def split_text_into_paragraphs(text, keep_separators=True): | |
| return split_text_by_separators(text, ['\n'], keep_separators) | |
| def split_text_into_sentences(text, keep_separators=True): | |
| return split_text_by_separators(text, ['\n', '。', '?', '!', ';'], keep_separators) | |
| def run_and_echo_yield_func(func, *args, **kwargs): | |
| echo_text = "" | |
| all_messages = [] | |
| for messages in func(*args, **kwargs): | |
| all_messages.append(messages) | |
| new_echo_text = "\n".join(f"{msg['role']}:\n{msg['content']}" for msg in messages) | |
| if new_echo_text.startswith(echo_text): | |
| delta_echo_text = new_echo_text[len(echo_text):] | |
| else: | |
| echo_text = "" | |
| print('\n--------------------------------') | |
| delta_echo_text = new_echo_text | |
| print(delta_echo_text, end="") | |
| echo_text = echo_text + delta_echo_text | |
| return all_messages | |
| def run_yield_func(func, *args, **kwargs): | |
| gen = func(*args, **kwargs) | |
| try: | |
| while True: | |
| next(gen) | |
| except StopIteration as e: | |
| return e.value | |
| def split_text_into_chunks(text, max_chunk_size, min_chunk_n, min_chunk_size=1, max_chunk_n=1000): | |
| def split_paragraph(para): | |
| mid = len(para) // 2 | |
| split_pattern = r'[。?;]' | |
| split_points = [m.end() for m in re.finditer(split_pattern, para)] | |
| if not split_points: | |
| raise Exception("没有找到分割点!") | |
| closest_point = min(split_points, key=lambda x: abs(x - mid)) | |
| if not para[:closest_point].strip() or not para[closest_point:].strip(): | |
| raise Exception("没有找到分割点!") | |
| return para[:closest_point], para[closest_point:] | |
| paragraphs = split_text_into_paragraphs(text) | |
| assert max_chunk_n >= 1, "max_chunk_n必须大于等于1" | |
| assert sum(len(p) for p in paragraphs) >= min_chunk_size, f"分割时,输入的文本长度小于要求的min_chunk_size:{min_chunk_size}" | |
| count = 0 # 防止死循环 | |
| while len(paragraphs) > max_chunk_n or min(len(p) for p in paragraphs) < min_chunk_size: | |
| assert (count:=count+1) < 1000, "分割进入死循环!" | |
| # 找出相邻chunks中和最小的两个进行合并 | |
| min_sum = float('inf') | |
| min_i = 0 | |
| for i in range(len(paragraphs) - 1): | |
| curr_sum = len(paragraphs[i]) + len(paragraphs[i + 1]) | |
| if curr_sum < min_sum: | |
| min_sum = curr_sum | |
| min_i = i | |
| # 合并这两个chunks | |
| paragraphs[min_i:min_i + 2] = [''.join(paragraphs[min_i:min_i + 2])] | |
| while len(paragraphs) < min_chunk_n or max(len(p) for p in paragraphs) > max_chunk_size: | |
| assert (count:=count+1) < 1000, "分割进入死循环!" | |
| longest_para_i = max(range(len(paragraphs)), key=lambda i: len(paragraphs[i])) | |
| part1, part2 = split_paragraph(paragraphs[longest_para_i]) | |
| if len(part1) < min_chunk_size or len(part2) < min_chunk_size or len(paragraphs) + 1 > max_chunk_n: | |
| raise Exception("没有找到合适的分割点!") | |
| paragraphs[longest_para_i:longest_para_i+1] = [part1, part2] | |
| return paragraphs | |
| def test_split_text_into_chunks(): | |
| # Test case 1: Simple paragraph splitting | |
| text1 = "这是第一段。这是第二段。这是第三段。" | |
| result1 = split_text_into_chunks(text1, max_chunk_size=10, min_chunk_n=3) | |
| print("Test 1 result:", result1) | |
| assert len(result1) == 3, f"Expected 3 chunks, got {len(result1)}" | |
| # Test case 2: Long paragraph splitting | |
| text2 = "这是一个很长的段落,包含了很多句子。它应该被分割成多个小块。这里有一些标点符号,比如句号。还有问号?以及分号;这些都可以用来分割文本。" | |
| result2 = split_text_into_chunks(text2, max_chunk_size=20, min_chunk_n=4) | |
| print("Test 2 result:", result2) | |
| assert len(result2) >= 4, f"Expected at least 4 chunks, got {len(result2)}" | |
| assert all(len(chunk) <= 20 for chunk in result2), "Some chunks are longer than max_chunk_size" | |
| # Test case 3: Text with newlines | |
| text3 = "第一段。\n\n第二段。\n第三段。\n\n第四段很长,需要被分割。这是第四段的继续。" | |
| result3 = split_text_into_chunks(text3, max_chunk_size=15, min_chunk_n=5) | |
| print("Test 3 result:", result3) | |
| assert len(result3) >= 5, f"Expected at least 5 chunks, got {len(result3)}" | |
| assert all(len(chunk) <= 15 for chunk in result3), "Some chunks are longer than max_chunk_size" | |
| print("All tests passed!") | |
| if __name__ == "__main__": | |
| print(detect_max_edit_span("我吃西红柿", "我不喜欢吃西红柿")) | |
| print(detect_max_edit_span("我吃西红柿", "不喜欢吃西红柿")) | |
| print(detect_max_edit_span("我吃西红柿", "我不喜欢吃")) | |
| print(detect_max_edit_span("我吃西红柿", "你不喜欢吃西瓜")) | |
| test_split_text_into_chunks() | |