Spaces:
Running
Running
Commit ·
25bbb06
1
Parent(s): 974fbb9
ok
Browse files- app/api/deps.py +5 -0
- app/api/v1/convert.py +40 -5
- app/models/domain.py +18 -3
- app/services/text_cleaner_service.py +154 -0
- requirements.txt +6 -1
app/api/deps.py
CHANGED
|
@@ -9,9 +9,14 @@ from app.services.database_service import DatabaseService
|
|
| 9 |
from app.services.embeddings_service import EmbeddingService
|
| 10 |
from app.services.extraction_service import ExtractionService
|
| 11 |
from app.services.ocr_service import OCRService
|
|
|
|
| 12 |
from app.services.web_search_service import WebSearchService
|
| 13 |
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def get_auth_service() -> AuthService:
|
| 16 |
return AuthService()
|
| 17 |
|
|
|
|
| 9 |
from app.services.embeddings_service import EmbeddingService
|
| 10 |
from app.services.extraction_service import ExtractionService
|
| 11 |
from app.services.ocr_service import OCRService
|
| 12 |
+
from app.services.text_cleaner_service import TextCleanerService
|
| 13 |
from app.services.web_search_service import WebSearchService
|
| 14 |
|
| 15 |
|
| 16 |
+
def get_text_cleaner_service() -> TextCleanerService:
|
| 17 |
+
return TextCleanerService()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
def get_auth_service() -> AuthService:
|
| 21 |
return AuthService()
|
| 22 |
|
app/api/v1/convert.py
CHANGED
|
@@ -15,13 +15,15 @@ from app.config import get_settings
|
|
| 15 |
from app.api.deps import (
|
| 16 |
get_converter_service,
|
| 17 |
get_extraction_service,
|
|
|
|
| 18 |
require_auth,
|
| 19 |
)
|
| 20 |
from app.core.logger import get_logger
|
| 21 |
-
from app.models.domain import ConversionError
|
| 22 |
from app.models.schemas import ConversionMetadata, ConversionResponse, UrlRequest
|
| 23 |
from app.services.converter_service import ConverterService
|
| 24 |
from app.services.extraction_service import ExtractionService
|
|
|
|
| 25 |
|
| 26 |
router = APIRouter()
|
| 27 |
_logger = get_logger(__name__)
|
|
@@ -52,17 +54,24 @@ async def _build_response(
|
|
| 52 |
raw_data: Optional[bytes] = None,
|
| 53 |
mappings: Optional[Dict[str, Dict[str, Any]]] = None,
|
| 54 |
extraction_service: ExtractionService = None,
|
|
|
|
|
|
|
| 55 |
) -> ConversionResponse:
|
| 56 |
json_content: Optional[Any] = None
|
| 57 |
error_message: Optional[str] = None
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
if return_json and filename and extraction_service:
|
| 60 |
loop = asyncio.get_running_loop()
|
| 61 |
json_result = await loop.run_in_executor(
|
| 62 |
_thread_pool,
|
| 63 |
extraction_service.extract_structured,
|
| 64 |
filename,
|
| 65 |
-
|
| 66 |
mappings,
|
| 67 |
raw_data,
|
| 68 |
)
|
|
@@ -71,13 +80,30 @@ async def _build_response(
|
|
| 71 |
else:
|
| 72 |
json_content = json_result
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
return ConversionResponse(
|
| 75 |
success=True,
|
| 76 |
time_ms=round(result.duration_ms, 3),
|
| 77 |
-
content=
|
| 78 |
return_json=return_json,
|
| 79 |
json_content=json_content,
|
| 80 |
-
metadata=
|
| 81 |
error_message=error_message,
|
| 82 |
)
|
| 83 |
|
|
@@ -109,10 +135,12 @@ async def convert_file(
|
|
| 109 |
file: Annotated[UploadFile, File(description="File to convert")],
|
| 110 |
plain_text: bool = Form(False),
|
| 111 |
return_json: bool = Form(False),
|
|
|
|
| 112 |
mappings: Optional[str] = Form(None, description="JSON string with field mappings"),
|
| 113 |
token: str = Depends(require_auth),
|
| 114 |
converter_service: ConverterService = Depends(get_converter_service),
|
| 115 |
extraction_service: ExtractionService = Depends(get_extraction_service),
|
|
|
|
| 116 |
):
|
| 117 |
if file is None:
|
| 118 |
raise HTTPException(status_code=400, detail={"success": False, "message": "No file provided."})
|
|
@@ -137,9 +165,14 @@ async def convert_file(
|
|
| 137 |
_raise_for_error(outcome)
|
| 138 |
|
| 139 |
_logger.info("Conversion successful for %s", file.filename)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
if plain_text:
|
| 141 |
from fastapi.responses import PlainTextResponse
|
| 142 |
-
return PlainTextResponse(
|
| 143 |
|
| 144 |
response = await _build_response(
|
| 145 |
outcome,
|
|
@@ -148,6 +181,8 @@ async def convert_file(
|
|
| 148 |
raw_data=raw,
|
| 149 |
mappings=parsed_mappings,
|
| 150 |
extraction_service=extraction_service,
|
|
|
|
|
|
|
| 151 |
)
|
| 152 |
_logger.info("Request completed for %s", file.filename)
|
| 153 |
return response
|
|
|
|
| 15 |
from app.api.deps import (
|
| 16 |
get_converter_service,
|
| 17 |
get_extraction_service,
|
| 18 |
+
get_text_cleaner_service,
|
| 19 |
require_auth,
|
| 20 |
)
|
| 21 |
from app.core.logger import get_logger
|
| 22 |
+
from app.models.domain import ConversionError, count_tokens
|
| 23 |
from app.models.schemas import ConversionMetadata, ConversionResponse, UrlRequest
|
| 24 |
from app.services.converter_service import ConverterService
|
| 25 |
from app.services.extraction_service import ExtractionService
|
| 26 |
+
from app.services.text_cleaner_service import TextCleanerService
|
| 27 |
|
| 28 |
router = APIRouter()
|
| 29 |
_logger = get_logger(__name__)
|
|
|
|
| 54 |
raw_data: Optional[bytes] = None,
|
| 55 |
mappings: Optional[Dict[str, Dict[str, Any]]] = None,
|
| 56 |
extraction_service: ExtractionService = None,
|
| 57 |
+
clean_content: bool = False,
|
| 58 |
+
text_cleaner_service: TextCleanerService = None,
|
| 59 |
) -> ConversionResponse:
|
| 60 |
json_content: Optional[Any] = None
|
| 61 |
error_message: Optional[str] = None
|
| 62 |
|
| 63 |
+
content = result.markdown
|
| 64 |
+
if clean_content and text_cleaner_service:
|
| 65 |
+
loop = asyncio.get_running_loop()
|
| 66 |
+
content = await loop.run_in_executor(_thread_pool, text_cleaner_service.clean, content)
|
| 67 |
+
|
| 68 |
if return_json and filename and extraction_service:
|
| 69 |
loop = asyncio.get_running_loop()
|
| 70 |
json_result = await loop.run_in_executor(
|
| 71 |
_thread_pool,
|
| 72 |
extraction_service.extract_structured,
|
| 73 |
filename,
|
| 74 |
+
content,
|
| 75 |
mappings,
|
| 76 |
raw_data,
|
| 77 |
)
|
|
|
|
| 80 |
else:
|
| 81 |
json_content = json_result
|
| 82 |
|
| 83 |
+
if clean_content and content != result.markdown:
|
| 84 |
+
lines = content.splitlines()
|
| 85 |
+
words = content.split()
|
| 86 |
+
import hashlib
|
| 87 |
+
metadata = ConversionMetadata(
|
| 88 |
+
source=result.source,
|
| 89 |
+
char_count=len(content),
|
| 90 |
+
word_count=len(words),
|
| 91 |
+
line_count=len(lines),
|
| 92 |
+
file_size_bytes=result.file_size_bytes,
|
| 93 |
+
mime_type=result.mime_type,
|
| 94 |
+
content_hash=hashlib.sha256(content.encode()).hexdigest(),
|
| 95 |
+
token_estimate=max(1, count_tokens(content)),
|
| 96 |
+
)
|
| 97 |
+
else:
|
| 98 |
+
metadata = _build_metadata(result)
|
| 99 |
+
|
| 100 |
return ConversionResponse(
|
| 101 |
success=True,
|
| 102 |
time_ms=round(result.duration_ms, 3),
|
| 103 |
+
content=content,
|
| 104 |
return_json=return_json,
|
| 105 |
json_content=json_content,
|
| 106 |
+
metadata=metadata,
|
| 107 |
error_message=error_message,
|
| 108 |
)
|
| 109 |
|
|
|
|
| 135 |
file: Annotated[UploadFile, File(description="File to convert")],
|
| 136 |
plain_text: bool = Form(False),
|
| 137 |
return_json: bool = Form(False),
|
| 138 |
+
clean_content: bool = Form(False),
|
| 139 |
mappings: Optional[str] = Form(None, description="JSON string with field mappings"),
|
| 140 |
token: str = Depends(require_auth),
|
| 141 |
converter_service: ConverterService = Depends(get_converter_service),
|
| 142 |
extraction_service: ExtractionService = Depends(get_extraction_service),
|
| 143 |
+
text_cleaner_service: TextCleanerService = Depends(get_text_cleaner_service),
|
| 144 |
):
|
| 145 |
if file is None:
|
| 146 |
raise HTTPException(status_code=400, detail={"success": False, "message": "No file provided."})
|
|
|
|
| 165 |
_raise_for_error(outcome)
|
| 166 |
|
| 167 |
_logger.info("Conversion successful for %s", file.filename)
|
| 168 |
+
|
| 169 |
+
content = outcome.markdown
|
| 170 |
+
if clean_content:
|
| 171 |
+
content = await loop.run_in_executor(_thread_pool, text_cleaner_service.clean, content)
|
| 172 |
+
|
| 173 |
if plain_text:
|
| 174 |
from fastapi.responses import PlainTextResponse
|
| 175 |
+
return PlainTextResponse(content)
|
| 176 |
|
| 177 |
response = await _build_response(
|
| 178 |
outcome,
|
|
|
|
| 181 |
raw_data=raw,
|
| 182 |
mappings=parsed_mappings,
|
| 183 |
extraction_service=extraction_service,
|
| 184 |
+
clean_content=clean_content,
|
| 185 |
+
text_cleaner_service=text_cleaner_service,
|
| 186 |
)
|
| 187 |
_logger.info("Request completed for %s", file.filename)
|
| 188 |
return response
|
app/models/domain.py
CHANGED
|
@@ -1,8 +1,23 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
import hashlib
|
| 4 |
from dataclasses import dataclass, field
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
@dataclass(frozen=True)
|
|
@@ -20,7 +35,7 @@ class ConversionResult:
|
|
| 20 |
|
| 21 |
@property
|
| 22 |
def token_estimate(self) -> int:
|
| 23 |
-
return max(1, self.
|
| 24 |
|
| 25 |
|
| 26 |
@dataclass(frozen=True)
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
from dataclasses import dataclass, field
|
| 4 |
+
|
| 5 |
+
import tiktoken
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
_ENCODING_CACHE: dict[str, tiktoken.Encoding] = {}
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _get_encoding(name: str = "o200k_base") -> tiktoken.Encoding:
|
| 12 |
+
if name not in _ENCODING_CACHE:
|
| 13 |
+
_ENCODING_CACHE[name] = tiktoken.get_encoding(name)
|
| 14 |
+
return _ENCODING_CACHE[name]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def count_tokens(text: str, encoding_name: str = "o200k_base") -> int:
|
| 18 |
+
if not text:
|
| 19 |
+
return 0
|
| 20 |
+
return len(_get_encoding(encoding_name).encode(text, disallowed_special=()))
|
| 21 |
|
| 22 |
|
| 23 |
@dataclass(frozen=True)
|
|
|
|
| 35 |
|
| 36 |
@property
|
| 37 |
def token_estimate(self) -> int:
|
| 38 |
+
return max(1, count_tokens(self.markdown))
|
| 39 |
|
| 40 |
|
| 41 |
@dataclass(frozen=True)
|
app/services/text_cleaner_service.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import html
|
| 3 |
+
from cleantext import clean
|
| 4 |
+
from text_unidecode import unidecode
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def clean_text(
|
| 8 |
+
text: str,
|
| 9 |
+
*,
|
| 10 |
+
# Basic cleanup
|
| 11 |
+
normalize_whitespace: bool = True,
|
| 12 |
+
remove_newlines: bool = True,
|
| 13 |
+
strip: bool = True,
|
| 14 |
+
# Character handling
|
| 15 |
+
to_lowercase: bool = False,
|
| 16 |
+
remove_punctuation: bool = False,
|
| 17 |
+
# Content removal
|
| 18 |
+
remove_urls: bool = False,
|
| 19 |
+
remove_emails: bool = False,
|
| 20 |
+
remove_phone_numbers: bool = False,
|
| 21 |
+
remove_numbers: bool = False,
|
| 22 |
+
remove_digits: bool = False,
|
| 23 |
+
# Escape sequences
|
| 24 |
+
fix_escape_sequences: bool = False,
|
| 25 |
+
remove_html_entities: bool = True,
|
| 26 |
+
# Special characters
|
| 27 |
+
remove_currency_symbols: bool = True,
|
| 28 |
+
remove_emoji: bool = True,
|
| 29 |
+
normalize_unicode: bool = True,
|
| 30 |
+
# Custom replacements
|
| 31 |
+
custom_replacements: dict | None = None,
|
| 32 |
+
) -> str:
|
| 33 |
+
"""
|
| 34 |
+
Robust generic string cleaner using validated cleantext API parameters
|
| 35 |
+
and text_unidecode for superior ASCII transliteration.
|
| 36 |
+
|
| 37 |
+
Parameters
|
| 38 |
+
----------
|
| 39 |
+
text : Input string to clean.
|
| 40 |
+
normalize_whitespace : Normalize all whitespace variants to single space.
|
| 41 |
+
remove_newlines : Remove line breaks (\\n, \\r, etc.).
|
| 42 |
+
strip : Strip leading/trailing whitespace.
|
| 43 |
+
to_lowercase : Convert text to lowercase.
|
| 44 |
+
remove_punctuation : Remove punctuation characters.
|
| 45 |
+
remove_urls : Remove URLs (http/https/www).
|
| 46 |
+
remove_emails : Remove email addresses.
|
| 47 |
+
remove_phone_numbers : Remove phone numbers.
|
| 48 |
+
remove_numbers : Remove standalone numbers.
|
| 49 |
+
remove_digits : Remove all digit characters.
|
| 50 |
+
fix_escape_sequences : Fix literal escape sequences (\\\\n, \\\\t, etc.).
|
| 51 |
+
remove_html_entities : Decode HTML entities (& → &).
|
| 52 |
+
remove_currency_symbols: Remove $, €, £, ¥, etc.
|
| 53 |
+
remove_emoji : Remove emoji characters.
|
| 54 |
+
normalize_unicode : Apply unicode fix + transliterate to ASCII via unidecode.
|
| 55 |
+
custom_replacements : Dict of {exact_string: replacement} applied first.
|
| 56 |
+
|
| 57 |
+
Returns
|
| 58 |
+
-------
|
| 59 |
+
str : Cleaned string.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
# ── Guard: handle None / non-string input safely ─────────────────────────
|
| 63 |
+
if text is None:
|
| 64 |
+
return ""
|
| 65 |
+
if not isinstance(text, str):
|
| 66 |
+
text = str(text)
|
| 67 |
+
if not text.strip():
|
| 68 |
+
return ""
|
| 69 |
+
|
| 70 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 71 |
+
# STEP 1 ── Custom replacements (exact string match, applied first)
|
| 72 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 73 |
+
if custom_replacements:
|
| 74 |
+
for target, replacement in custom_replacements.items():
|
| 75 |
+
text = text.replace(target, replacement)
|
| 76 |
+
|
| 77 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 78 |
+
# STEP 2 ── Fix literal escape sequences BEFORE any other processing
|
| 79 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 80 |
+
if fix_escape_sequences:
|
| 81 |
+
LITERAL_ESCAPE_MAP = [
|
| 82 |
+
("\\n", " "),
|
| 83 |
+
("\\t", " "),
|
| 84 |
+
("\\r", " "),
|
| 85 |
+
("\\v", " "),
|
| 86 |
+
("\\f", " "),
|
| 87 |
+
("\\a", ""),
|
| 88 |
+
("\\b", ""),
|
| 89 |
+
("\\\\", " "),
|
| 90 |
+
("\\/", "/"),
|
| 91 |
+
("\\'", "'"),
|
| 92 |
+
('\\"', '"'),
|
| 93 |
+
]
|
| 94 |
+
for literal, replacement in LITERAL_ESCAPE_MAP:
|
| 95 |
+
text = text.replace(literal, replacement)
|
| 96 |
+
|
| 97 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 98 |
+
# STEP 3 ── Decode HTML entities (& → &, < → <, ' → ')
|
| 99 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 100 |
+
if remove_html_entities:
|
| 101 |
+
text = html.unescape(text)
|
| 102 |
+
|
| 103 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 104 |
+
# STEP 4 ── Core cleaning via cleantext (validated API parameters only)
|
| 105 |
+
# We disable cleantext's to_ascii because text_unidecode
|
| 106 |
+
# handles transliteration far more robustly in STEP 5.
|
| 107 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 108 |
+
text = clean(
|
| 109 |
+
text,
|
| 110 |
+
fix_unicode=True, # Fix mojibake/encoding errors
|
| 111 |
+
to_ascii=False, # Defer to text_unidecode
|
| 112 |
+
lower=to_lowercase,
|
| 113 |
+
normalize_whitespace=normalize_whitespace,
|
| 114 |
+
no_line_breaks=remove_newlines,
|
| 115 |
+
strip_lines=strip,
|
| 116 |
+
no_urls=remove_urls,
|
| 117 |
+
no_emails=remove_emails,
|
| 118 |
+
no_phone_numbers=remove_phone_numbers,
|
| 119 |
+
no_numbers=remove_numbers,
|
| 120 |
+
no_digits=remove_digits,
|
| 121 |
+
no_currency_symbols=remove_currency_symbols,
|
| 122 |
+
no_punct=remove_punctuation,
|
| 123 |
+
no_emoji=remove_emoji,
|
| 124 |
+
replace_with_url="",
|
| 125 |
+
replace_with_email="",
|
| 126 |
+
replace_with_phone_number="",
|
| 127 |
+
replace_with_number="",
|
| 128 |
+
replace_with_digit="",
|
| 129 |
+
replace_with_currency_symbol="",
|
| 130 |
+
replace_with_punct="",
|
| 131 |
+
lang="en",
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 135 |
+
# STEP 5 ── Robust ASCII transliteration via text_unidecode
|
| 136 |
+
# Converts remaining non-ASCII (accents, cyrillic, greek, etc.)
|
| 137 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 138 |
+
if normalize_unicode:
|
| 139 |
+
text = unidecode(text)
|
| 140 |
+
|
| 141 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 142 |
+
# STEP 6 ── Post-clean whitespace tidy-up
|
| 143 |
+
# Removal + transliteration may leave stray multi-spaces
|
| 144 |
+
# ────────────────────────────────────────────────────────────────────────
|
| 145 |
+
text = re.sub(r" {2,}", " ", text)
|
| 146 |
+
if strip:
|
| 147 |
+
text = text.strip()
|
| 148 |
+
|
| 149 |
+
return text
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class TextCleanerService:
|
| 153 |
+
def clean(self, text: str, **kwargs) -> str:
|
| 154 |
+
return clean_text(text, **kwargs)
|
requirements.txt
CHANGED
|
@@ -23,6 +23,7 @@ torch==2.12.1
|
|
| 23 |
einops
|
| 24 |
spacy>=3.7.0
|
| 25 |
phonenumbers>=8.13.0
|
|
|
|
| 26 |
|
| 27 |
# Async database drivers
|
| 28 |
aiomysql>=0.3.2
|
|
@@ -35,4 +36,8 @@ scrapling[all]>=0.4.0
|
|
| 35 |
# Chat / AI
|
| 36 |
redis>=5.0.0
|
| 37 |
defusedxml>=0.7.1
|
| 38 |
-
chardet>=5.2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
einops
|
| 24 |
spacy>=3.7.0
|
| 25 |
phonenumbers>=8.13.0
|
| 26 |
+
tiktoken>=0.9.0
|
| 27 |
|
| 28 |
# Async database drivers
|
| 29 |
aiomysql>=0.3.2
|
|
|
|
| 36 |
# Chat / AI
|
| 37 |
redis>=5.0.0
|
| 38 |
defusedxml>=0.7.1
|
| 39 |
+
chardet>=5.2.0
|
| 40 |
+
|
| 41 |
+
# Text processing
|
| 42 |
+
clean-text>=0.6.0
|
| 43 |
+
Unidecode>=1.3.8
|