PopovDanil commited on
Commit
51aa39f
·
1 Parent(s): 2cf8356
Files changed (6) hide show
  1. app/cleaner.py +427 -0
  2. app/main.py +9 -1
  3. app/schemas.py +4 -1
  4. app/settings.py +189 -0
  5. requirements.txt +13 -1
  6. settings.py +0 -0
app/cleaner.py ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import glob
2
+ import os
3
+ import re
4
+ import time
5
+ from multiprocessing import Pool, cpu_count
6
+ from pathlib import Path
7
+
8
+ from langchain.agents import create_agent
9
+ from langchain_community.chat_models import ChatLlamaCpp
10
+ from langchain_core.messages import HumanMessage, SystemMessage
11
+ from langchain_core.runnables import Runnable, RunnableLambda, chain
12
+ from langchain_core.tools import tool
13
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
14
+ from langchain_text_splitters import RecursiveCharacterTextSplitter, TextSplitter
15
+ from pydantic import BaseModel, Field
16
+ from settings import settings
17
+
18
+
19
+ @tool
20
+ def remove_brackets_content(text: str) -> str:
21
+ """
22
+ Remove all content inside square brackets [],
23
+ round brackets () and curly brackets {}.
24
+ Useful for removing sound descriptions, speaker labels,
25
+ stage directions like [applause], (laughs), {music}.
26
+ """
27
+ text = re.sub(r'\[.*?\]', '', text) # [applause]
28
+ text = re.sub(r'\(.*?\)', '', text) # (laughs)
29
+ text = re.sub(r'\{.*?\}', '', text) # {music}
30
+ return text.strip()
31
+
32
+
33
+ @tool
34
+ def remove_non_alphabetic(text: str) -> str:
35
+ """
36
+ Remove all non-alphabetic characters except spaces.
37
+ Keeps only letters A-Z, a-z and whitespace.
38
+ Useful for stripping punctuation, numbers, special symbols.
39
+ """
40
+ text = re.sub(r'[^a-zA-Z\s]', '', text)
41
+ return text.strip()
42
+
43
+
44
+ @tool
45
+ def remove_newlines(text: str) -> str:
46
+ """
47
+ Remove newline characters and replace them with spaces.
48
+ Merges multi-line subtitle blocks into single lines.
49
+ """
50
+ text = text.replace('\n', ' ')
51
+ text = text.replace('\r', ' ')
52
+ text = re.sub(r' +', ' ', text) # collapse multiple spaces
53
+ return text.strip()
54
+
55
+
56
+ @tool
57
+ def remove_dialog_punctuation(text: str) -> str:
58
+ """
59
+ Remove dialog-specific punctuation: dashes at line start (- text),
60
+ ellipsis (...), double dashes (--), quotation marks,
61
+ and excessive punctuation used in subtitles.
62
+ """
63
+ text = re.sub(r'^\s*-+\s*', '', text, flags=re.MULTILINE) # leading dashes
64
+ text = re.sub(r'\.{2,}', '', text) # ellipsis ...
65
+ text = re.sub(r'-{2,}', '', text) # double dash --
66
+ text = re.sub(r'["""\'\'\']+', '', text) # quotes
67
+ text = re.sub(r'[!?,;:]+', '', text) # dialog punctuation
68
+ return text.strip()
69
+
70
+
71
+ @tool
72
+ def remove_timestamps(text: str) -> str:
73
+ """
74
+ Remove SRT/VTT subtitle timestamps.
75
+ Handles formats like:
76
+ - 00:01:23,456 --> 00:01:25,789 (SRT)
77
+ - 00:01:23.456 --> 00:01:25.789 (VTT)
78
+ Also removes bare sequence numbers (1, 2, 3...) used in SRT files.
79
+ """
80
+ # SRT timestamps
81
+ text = re.sub(
82
+ r'\d{2}:\d{2}:\d{2}[.,]\d{3}\s*-->\s*\d{2}:\d{2}:\d{2}[.,]\d{3}',
83
+ '', text
84
+ )
85
+ # VTT cue identifiers
86
+ text = re.sub(r'^\s*\d+\s*$', '', text, flags=re.MULTILINE)
87
+ # WEBVTT header
88
+ text = re.sub(r'WEBVTT.*\n?', '', text)
89
+ return text.strip()
90
+
91
+
92
+ @tool
93
+ def remove_speaker_labels(text: str) -> str:
94
+ """
95
+ Remove speaker labels commonly found in subtitles.
96
+ Handles formats like:
97
+ - JOHN: text
98
+ - John: text
99
+ - [JOHN]: text
100
+ - <v John> text (VTT format)
101
+ """
102
+ text = re.sub(r'^[A-Z][A-Z\s]{1,20}:\s*', '', text, flags=re.MULTILINE) # JOHN:
103
+ text = re.sub(r'^\w[\w\s]{1,20}:\s*', '', text, flags=re.MULTILINE) # John:
104
+ text = re.sub(r'<v\s+[^>]+>', '', text) # <v John>
105
+ return text.strip()
106
+
107
+
108
+ @tool
109
+ def remove_html_tags(text: str) -> str:
110
+ """
111
+ Remove HTML/XML tags commonly found in subtitles.
112
+ Handles: <i>, <b>, <u>, <font color="">, <c.colorname> etc.
113
+ Used in SRT and VTT files for styling.
114
+ """
115
+ text = re.sub(r'<[^>]+>', '', text)
116
+ return text.strip()
117
+
118
+
119
+ @tool
120
+ def normalize_whitespace(text: str) -> str:
121
+ """
122
+ Normalize all whitespace: collapse multiple spaces into one,
123
+ strip leading/trailing spaces from each line,
124
+ remove empty lines.
125
+ Final cleanup step — use after all other tools.
126
+ """
127
+ lines = text.split('\n')
128
+ lines = [re.sub(r' +', ' ', line).strip() for line in lines]
129
+ lines = [line for line in lines if line] # remove empty
130
+ return ' '.join(lines)
131
+
132
+
133
+ @tool
134
+ def lowercase_text(text: str) -> str:
135
+ """
136
+ Convert all text to lowercase.
137
+ Recommended for sentiment analysis preprocessing
138
+ to ensure uniform token representation.
139
+ """
140
+ return text.lower()
141
+
142
+
143
+ @tool
144
+ def remove_filler_words(text: str) -> str:
145
+ """
146
+ Remove common spoken filler words that add noise for sentiment analysis.
147
+ Removes: um, uh, hmm, ah, oh, er, erm, hm, gonna, wanna, gotta etc.
148
+ """
149
+ fillers = r'\b(um+|uh+|hmm+|hm+|ah+|oh+|er+|erm+|gonna|wanna|gotta|kinda|sorta|like|okay|ok|yeah|yep|nope)\b'
150
+ text = re.sub(fillers, '', text, flags=re.IGNORECASE)
151
+ text = re.sub(r' +', ' ', text)
152
+ return text.strip()
153
+
154
+
155
+ class FormattedResponse(BaseModel):
156
+ """Cleaned text"""
157
+ CLEANED_TEXT: str = Field(description="The CLEANED subtitle text")
158
+
159
+ @chain
160
+ def clean_subtitle(text: str) -> FormattedResponse:
161
+ """Deterministic cleaning pipeline — no LLM needed."""
162
+ text = remove_timestamps.invoke(text)
163
+ text = remove_brackets_content.invoke(text)
164
+ text = remove_html_tags.invoke(text)
165
+ text = remove_speaker_labels.invoke(text)
166
+ text = remove_dialog_punctuation.invoke(text)
167
+ text = remove_newlines.invoke(text)
168
+ text = remove_non_alphabetic.invoke(text)
169
+ text = remove_filler_words.invoke(text)
170
+ text = lowercase_text.invoke(text)
171
+ text = normalize_whitespace.invoke(text)
172
+ return FormattedResponse(CLEANED_TEXT=text)
173
+
174
+ class PreprocessingAgent:
175
+ """
176
+ Agent for preprocessing.
177
+
178
+ Pipeline:
179
+ 1. Dumping system prompt and subtitles into chat format
180
+ 2. Passing this chat to agent, which can call tools
181
+ 3. Passing agent's output to LLM to extract cleaned text
182
+
183
+ Second LLM is used, since single agent will struggle dealing with heavy prompt,
184
+ tools and requirements for structured output
185
+ """
186
+ def __init__(self):
187
+
188
+ self.use_hugging_face = settings.preprocessor.use_hugging_face # Flag for using local or external LLM
189
+ self.output_path = settings.preprocessor.output_path
190
+ self.num_workers = cpu_count() # If using external model several workers can be used in parallel
191
+
192
+ self.prompt ="""Your goal is to clean raw subtitle text step by step using available tools.
193
+ ## Input text format:
194
+ The user will provide the subtitle text marked as SUBTITLE_TEXT.
195
+
196
+ ## Recommended cleaning pipeline (follow this order):
197
+ 1. remove_timestamps — strip SRT/VTT timing info
198
+ 2. remove_brackets_content — remove [sound], (laughter), {{music}}
199
+ 3. remove_html_tags — strip <i>, <b>, <font> tags
200
+ 4. remove_speaker_labels — remove JOHN:, John:, <v John>
201
+ 5. remove_dialog_punctuation — remove ---, ..., quotes, !?;:,
202
+ 6. remove_newlines — merge lines into single text
203
+ 7. remove_non_alphabetic — keep only letters and spaces
204
+ 8. remove_filler_words — remove um, uh, gonna, wanna...
205
+ 9. lowercase_text — convert to lowercase
206
+ 10. normalize_whitespace — final cleanup of spaces
207
+
208
+ Apply ALL steps unless the user specifies otherwise.
209
+ After cleaning, return the final cleaned text clearly labeled as:
210
+
211
+ CLEANED_TEXT: <result>
212
+ """
213
+
214
+ def clean_text(text: str) -> str:
215
+ return clean_subtitle.invoke(text).CLEANED_TEXT
216
+
217
+
218
+ def _setup_pipeline(self, tools: list) -> Runnable:
219
+ """
220
+ Initializes the pipeline
221
+
222
+ Args:
223
+ tools (list): list of available tools
224
+
225
+ Returns:
226
+ Runnable: Runnable pipeline
227
+ """
228
+ if self.use_hugging_face:
229
+ endpoint = HuggingFaceEndpoint(
230
+ repo_id=settings.preprocessor.model,
231
+ task='text-generation',
232
+ do_sample=False,
233
+ temperature=0.05,
234
+ max_new_tokens=16_000,
235
+ streaming=False
236
+ )
237
+
238
+ llm = ChatHuggingFace(llm=endpoint)
239
+ structured_llm = llm.with_structured_output(FormattedResponse, method='json_mode')
240
+ else:
241
+ llm = ChatLlamaCpp(
242
+ model_path=settings.preprocessor.local_model_path,
243
+ n_ctx=4096,
244
+ n_gpu_layers=-1,
245
+ n_batch=512,
246
+ max_tokens=16_000,
247
+ n_threads=cpu_count() - 1,
248
+ temperature=0.05,
249
+ verbose=True,
250
+ f16_kv=True
251
+ )
252
+ structured_llm = llm.with_structured_output(FormattedResponse)
253
+
254
+ agent = create_agent(llm, tools, system_prompt=SystemMessage(self.prompt))
255
+
256
+ pipeline = (
257
+ RunnableLambda(lambda text: {
258
+ 'messages': [HumanMessage(f'Clean the following text:\n\nSUBTITLE_TEXT:\n{text}')]
259
+ })
260
+ | agent
261
+ | RunnableLambda(lambda result: f"Return this cleaned text in the required format:\n\n{result['messages'][-1].content}")
262
+ | structured_llm
263
+ )
264
+
265
+ return pipeline
266
+
267
+
268
+ def _init_tools(self) -> list:
269
+ """
270
+ Provides the list of available tools. This method is required due to
271
+ function serialization problem (when using several workers)
272
+
273
+ Returns:
274
+ list: tools
275
+ """
276
+ return [
277
+ remove_timestamps,
278
+ remove_brackets_content,
279
+ remove_html_tags,
280
+ remove_speaker_labels,
281
+ remove_dialog_punctuation,
282
+ remove_newlines,
283
+ remove_non_alphabetic,
284
+ remove_filler_words,
285
+ lowercase_text,
286
+ normalize_whitespace,
287
+ ]
288
+
289
+
290
+ def invoke(self, pipeline: Runnable, splitter: TextSplitter, text: str) -> str | None:
291
+ """
292
+ Preprocesses the text. Additionally, splits it to chunks to avoid
293
+ context overflow
294
+
295
+ Args:
296
+ pipeline (Runnable): pipeline for preprocessing
297
+ splitter (TextSplitter): text splitter. Defaults to RecursiveCharacterSplitter
298
+ text (str): subtitles
299
+
300
+ Returns:
301
+ str | None: cleaned text
302
+ """
303
+ texts = splitter.split_text(text)
304
+
305
+ ready_parts = []
306
+ for splitted_text in texts:
307
+ response = pipeline.invoke(splitted_text)
308
+
309
+ # Some HuggingFace models struggles with formatted response
310
+ # So we iterate over all keys and try to find ANY text
311
+ if self.use_hugging_face:
312
+ for key in response.keys():
313
+ if 'text' in key.lower():
314
+ ready_parts.append(response[key].strip())
315
+ break
316
+ else:
317
+ # Local models have a bit different schema, so they always will have this
318
+ ready_parts.append(response.CLEANED_TEXT.strip())
319
+
320
+ if not len(ready_parts):
321
+ return None
322
+
323
+ return ' '.join(ready_parts).strip()
324
+
325
+
326
+ def analyze_file(self, pipeline: Runnable, splitter: TextSplitter, input_path: Path) -> None:
327
+ """
328
+ Loads file with subtitles, cleans it, and saves back
329
+
330
+ Args:
331
+ pipeline (Runnable): preprocessing pipeline
332
+ splitter (_type_): text splitter
333
+ input_path (Path): path to subtitles
334
+ """
335
+ with open(input_path, 'r', encoding='utf-8') as f:
336
+ text = f.read()
337
+
338
+ print(f'Processing {input_path.name}')
339
+ start_time = time.time()
340
+
341
+ content = self.invoke(pipeline, splitter, text) # cleaning logic
342
+
343
+ self.output_path.mkdir(parents=True, exist_ok=True)
344
+
345
+ with open(
346
+ os.path.join(self.output_path, f'{input_path.stem}.csv'), "w"
347
+ ) as file:
348
+ file.write(content)
349
+
350
+ elapsed = time.time() - start_time
351
+
352
+ print(f'Saved to {self.output_path} in {elapsed:.2f}s')
353
+
354
+
355
+ def _select_filenames(self) -> list[Path]:
356
+ """
357
+ Collects all the files with subtitles.
358
+
359
+ Returns:
360
+ list[Path]: list with path for each file
361
+ """
362
+ names = []
363
+
364
+ for filepath in glob.glob(
365
+ os.path.join(settings.preprocessor.input_path, '*.txt')
366
+ ):
367
+ names.append(Path(filepath))
368
+
369
+ return names
370
+
371
+
372
+ def _start_worker(self, files: list[Path], worker_id: int = 0) -> None:
373
+ """
374
+ Starts worker. Each worker has its own pipeline, set of tools, and
375
+ text splitter
376
+
377
+ Args:
378
+ files (list[Path]): list of files to be preprocessed
379
+ """
380
+ print(f'Worker {worker_id} is preparing')
381
+
382
+ # Note: When using multiprocessing, each process will serialize
383
+ # self and any other referenced object, but langchain tools are not
384
+ # serializable. So separate method for tools is required
385
+ tools = self._init_tools()
386
+ pipeline = self._setup_pipeline(tools)
387
+ splitter = RecursiveCharacterTextSplitter(
388
+ chunk_size=settings.preprocessor.chunk_size,
389
+ chunk_overlap=0,
390
+ length_function=len
391
+ )
392
+
393
+ print(f'Worker {worker_id} is starting preprocessing')
394
+
395
+ for file in files:
396
+ self.analyze_file(pipeline, splitter, file)
397
+
398
+ print(f'Worker {worker_id} is finishing')
399
+
400
+
401
+ def start_preprocessing(self) -> None:
402
+ """
403
+ Starts preprocessing.
404
+ """
405
+ offset = settings.scraper.offset
406
+ files = self._select_filenames()
407
+
408
+ if self.use_hugging_face:
409
+ # Map files to workers
410
+ chunk_size = (len(files) + self.num_workers - 1) // self.num_workers
411
+ file_mapping = [
412
+ files[i + offset : i + offset + chunk_size]
413
+ for i in range(0, len(files), chunk_size)
414
+ ]
415
+
416
+ # Collect worker's args
417
+ worker_args = [
418
+ (files, i + 1)
419
+ for i, files in enumerate(file_mapping)
420
+ ]
421
+
422
+ # Each worker - separate process
423
+ with Pool(processes=self.num_workers) as p:
424
+ p.starmap(self._start_worker, worker_args)
425
+ else:
426
+ files = files[offset:]
427
+ self._start_worker(files, 1)
app/main.py CHANGED
@@ -1,7 +1,9 @@
 
1
  from fastapi import FastAPI
2
 
 
3
  from .model import get_model
4
- from .schemas import EmotionResponse, SubtitleRequest
5
 
6
  app = FastAPI(title="Emotion Analyzer API")
7
 
@@ -19,4 +21,10 @@ async def analyze(req: SubtitleRequest):
19
 
20
  return EmotionResponse(
21
  data=df.to_dict(orient="records")
 
 
 
 
 
 
22
  )
 
1
+
2
  from fastapi import FastAPI
3
 
4
+ from .cleaner import PreprocessingAgent
5
  from .model import get_model
6
+ from .schemas import CleanResponse, EmotionResponse, SubtitleRequest
7
 
8
  app = FastAPI(title="Emotion Analyzer API")
9
 
 
21
 
22
  return EmotionResponse(
23
  data=df.to_dict(orient="records")
24
+ )
25
+
26
+ @app.post('/clean', response_model=CleanResponse)
27
+ async def clean(req: SubtitleRequest):
28
+ return CleanResponse(
29
+ text=PreprocessingAgent.clean_text(req.text)
30
  )
app/schemas.py CHANGED
@@ -6,4 +6,7 @@ class SubtitleRequest(BaseModel):
6
 
7
 
8
  class EmotionResponse(BaseModel):
9
- data: list[dict]
 
 
 
 
6
 
7
 
8
  class EmotionResponse(BaseModel):
9
+ data: list[dict]
10
+
11
+ class CleanResponse(BaseModel):
12
+ text: str
app/settings.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from dotenv import load_dotenv
5
+ from pydantic import BaseModel, Field
6
+
7
+ load_dotenv() # for manual use (please, ensure that there is .env file in the same folder)
8
+
9
+ BASE_DIR = Path(__file__).resolve().parent
10
+
11
+
12
+ class ScraperSettings(BaseModel):
13
+ url: str = Field(
14
+ default='https://subslikescript.com',
15
+ description="Base URL for scraping"
16
+ )
17
+
18
+ output_folder: str = Field(
19
+ default=os.path.join(BASE_DIR, 'data'),
20
+ description="Directory where scraped files will be saved"
21
+ )
22
+
23
+ # Do not set False if using Docker
24
+ headless: bool = Field(
25
+ default=False,
26
+ description="Run browser in headless mode (disable UI)"
27
+ )
28
+
29
+ delay: int = Field(
30
+ default=3,
31
+ description="Delay between requests in seconds"
32
+ )
33
+
34
+ num_workers: int = Field(
35
+ default=4,
36
+ description="Number of concurrent workers"
37
+ )
38
+
39
+ offset: int = Field(
40
+ default=386,
41
+ description="End page for the first worker at the last run"
42
+ )
43
+
44
+
45
+ class EmotionAnalyzerSettings(BaseModel):
46
+ input_path: str = Field(
47
+ default='./data',
48
+ description="Path to preprocessed data"
49
+ )
50
+
51
+ output_path: str = Field(
52
+ default='./emotion_analysis/embeddings',
53
+ description="Path to files with embeddings"
54
+ )
55
+
56
+
57
+ class PreprocessorSettings(BaseModel):
58
+ use_hugging_face: bool = Field(
59
+ default=False,
60
+ description="Flag to use propitiate HuggingFace model"
61
+ )
62
+
63
+ local_model_path: str = Field(
64
+ default='./emotion_analysis/weights/mistral_prep/model.gguf',
65
+ description="Path to file with local model"
66
+ )
67
+
68
+ model: str = Field(
69
+ default='Qwen/Qwen2.5-7B-Instruct',
70
+ description="Model name"
71
+ )
72
+
73
+ input_path: str = Field(
74
+ default='./data',
75
+ description="Path to raw subtitles"
76
+ )
77
+
78
+ output_path: str = Field(
79
+ default=Path('./preprocessing/ready_data'),
80
+ description="Path to folder with preprocessed data"
81
+ )
82
+
83
+ chunk_size: int = Field(
84
+ default=4096,
85
+ description="Each text is divided into chunks since " \
86
+ "entire sequence can not fit into context"
87
+ )
88
+
89
+ offset: int = Field(
90
+ default=0,
91
+ description="Determines how much first files will be skipped "
92
+ "(in case some have already been preprocessed)"
93
+ )
94
+
95
+
96
+ class DBSettings(BaseModel):
97
+ db_url: str = Field(
98
+ default=os.environ['DB_URL'],
99
+ description="DB connection string"
100
+ )
101
+
102
+
103
+ class GraphSettings(BaseModel):
104
+ max_depth: int = Field(
105
+ default=5,
106
+ description="Maximum graph depth (excluding the highest node (root))"
107
+ )
108
+
109
+ min_samples_leaf: int = Field(
110
+ default=6,
111
+ description="Minimum number of movies in each leaf"
112
+ )
113
+
114
+ max_nodes: int = Field(
115
+ default=800,
116
+ description="Maximum number of nodes in the graph (important for KMeans)"
117
+ )
118
+
119
+ target_leaf_size: int = Field(
120
+ default=50,
121
+ description="Desired number of movies in each leaf"
122
+ )
123
+
124
+ # Important - with current algorithm min/max_fanout can be violated for very small
125
+ # number of nodes. To understand the reason check the algorithm description
126
+ # in ./clustering/graph_creator.py
127
+ min_fanout: int = Field(
128
+ default=3,
129
+ description="Minimal number of successors for each node"
130
+ )
131
+
132
+ max_fanout: int = Field(
133
+ default=8,
134
+ description="Maximum number of successors for each node"
135
+ )
136
+
137
+
138
+ class NameCreatorSettings(BaseModel):
139
+ model_path: str = Field(
140
+ default='./emotion_analysis/weights/qwen/model.gguf',
141
+ description="Path to model"
142
+ )
143
+
144
+
145
+ class APISettings(BaseModel):
146
+ app_path: str = Field(
147
+ default='api.api:app',
148
+ description='Path to file with FastAPI app'
149
+ )
150
+
151
+ host: str = Field(
152
+ default='0.0.0.0',
153
+ )
154
+
155
+ port: int = Field(
156
+ default=int(os.environ['API_PORT']),
157
+ )
158
+
159
+
160
+ class MoviesValidatorSettings(BaseModel):
161
+ path_to_files: str = Field(
162
+ default='./movies_for_validation'
163
+ )
164
+
165
+
166
+ class AdminPanelSettings(BaseModel):
167
+ login: str = Field(
168
+ default=os.environ['ADMIN_LOGIN']
169
+ )
170
+
171
+ password: str = Field(
172
+ default=os.environ['ADMIN_PASSWORD']
173
+ )
174
+
175
+
176
+ class Settings(BaseModel):
177
+ scraper: ScraperSettings = ScraperSettings()
178
+ emotion_analyzer: EmotionAnalyzerSettings = EmotionAnalyzerSettings()
179
+ preprocessor: PreprocessorSettings = PreprocessorSettings()
180
+ name_creator: NameCreatorSettings = NameCreatorSettings()
181
+ movie_validator: MoviesValidatorSettings = MoviesValidatorSettings()
182
+ admin: AdminPanelSettings = AdminPanelSettings()
183
+ db: DBSettings = DBSettings()
184
+ graph: GraphSettings = GraphSettings()
185
+ api: APISettings = APISettings()
186
+ base_dir: str = BASE_DIR
187
+
188
+
189
+ settings = Settings()
requirements.txt CHANGED
@@ -6,4 +6,16 @@ pandas
6
  numpy
7
  peft
8
  accelerate
9
- bitsandbytes
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  numpy
7
  peft
8
  accelerate
9
+ bitsandbytes
10
+ langchain==1.2.10
11
+ langchain-classic==1.0.1
12
+ langchain-community==0.4.1
13
+ langchain-core==1.2.13
14
+ langchain-huggingface==1.2.0
15
+ langchain-text-splitters==1.1.0
16
+ langgraph==1.0.8
17
+ langgraph-checkpoint==4.0.0
18
+ langgraph-prebuilt==1.0.7
19
+ langgraph-sdk==0.3.6
20
+ langsmith==0.7.3
21
+ dotenv
settings.py DELETED
File without changes