pelican-nlp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. pelican_nlp/__init__.py +9 -0
  2. pelican_nlp/core/__init__.py +5 -0
  3. pelican_nlp/core/audio_document.py +20 -0
  4. pelican_nlp/core/corpus.py +296 -0
  5. pelican_nlp/core/document.py +226 -0
  6. pelican_nlp/core/subject.py +30 -0
  7. pelican_nlp/extraction/__init__.py +2 -0
  8. pelican_nlp/extraction/acoustic_feature_extraction.py +71 -0
  9. pelican_nlp/extraction/distance_from_randomness.py +109 -0
  10. pelican_nlp/extraction/extract_embeddings.py +57 -0
  11. pelican_nlp/extraction/extract_logits.py +102 -0
  12. pelican_nlp/extraction/language_model.py +71 -0
  13. pelican_nlp/extraction/semantic_similarity.py +60 -0
  14. pelican_nlp/extraction/test_documents/test_features.csv +4 -0
  15. pelican_nlp/extraction/test_documents/wallace_1.15_3.txt +1 -0
  16. pelican_nlp/extraction/test_documents/wallace_1.1_3.txt +1 -0
  17. pelican_nlp/extraction/test_documents/wallace_1_4.txt +1 -0
  18. pelican_nlp/main.py +211 -0
  19. pelican_nlp/metrics_statistics/embeddings_metrics_statistics.py +34 -0
  20. pelican_nlp/preprocessing/LPDS.py +77 -0
  21. pelican_nlp/preprocessing/__init__.py +7 -0
  22. pelican_nlp/preprocessing/pipeline.py +50 -0
  23. pelican_nlp/preprocessing/speaker_diarization.py +33 -0
  24. pelican_nlp/preprocessing/text_cleaner.py +224 -0
  25. pelican_nlp/preprocessing/text_importer.py +42 -0
  26. pelican_nlp/preprocessing/text_normalizer.py +24 -0
  27. pelican_nlp/preprocessing/text_tokenizer.py +43 -0
  28. pelican_nlp/sample_configuration_files/config_discourse.yml +103 -0
  29. pelican_nlp/sample_configuration_files/config_fluency.yml +108 -0
  30. pelican_nlp/sample_configuration_files/config_general.yml +131 -0
  31. pelican_nlp/utils/__init__.py +3 -0
  32. pelican_nlp/utils/csv_functions.py +193 -0
  33. pelican_nlp/utils/sample_usage.py +17 -0
  34. pelican_nlp/utils/setup_functions.py +93 -0
  35. pelican_nlp-0.1.0.dist-info/METADATA +146 -0
  36. pelican_nlp-0.1.0.dist-info/RECORD +39 -0
  37. pelican_nlp-0.1.0.dist-info/WHEEL +5 -0
  38. pelican_nlp-0.1.0.dist-info/licenses/LICENSE +400 -0
  39. pelican_nlp-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ In der trüben Kulisse Londons, die von einem unaufhörlich niederprasselnden Regen fast erdrückt wurde, befand sich die berüchtigte Seemannstaverne "Zum Lachenden Krake" in Wapping's dunkler Gasse. Ihr Ruf war ebenso zwielichtig wie die Stammgäste, die sich dort in trister Regelmäßigkeit einfanden. In einer dunklen Ecke des Gasthauses saß ein Mann, dessen finstere Erscheinung kaum ins Licht drang. Sein Name war Ryder Sparrow, ein Name, der durch das East End raunte wie ein geflüstertes Geheimnis. Er war ein Detektiv, bislang vergeblich darum bemüht, unseren mysteriösen Antagonisten - das "Schwarze Phantom" - dingfest zu machen. Der Fall des Schwarzen Phantoms war ein Mysterium, das nicht nur Scotland Yard, sondern ganz England in Atem hielt. Dieser unbekannte Dramatiker der Dunkelheit verübte seine Straftaten im Schutze der Nacht und ließ der Polizei nichts anderes als zertrümmerte Fensterrahmen und leere Tresore vor. Sein brillantester Coup, der beraubte Globe Plaza Verarbeitungstresor, hing wie ein Damoklesschwert über Ryders Karriere, denn es war wohlbekannt, dass der melancholische Detektiv bereits mehrere Fälle durch dessen nicht fassbare Handschrift it bohämmernden Details verbinden konnte. Ryder hob seinen Blick zum hartgesottenen Barmann, einer Koryphäe und ungeschlagener Bewahrer aller Gerüchte die über die Theke gingen. "Hammon, was hört man über das Phantom?", fragte Ryder mit einem zugleich einzigen Ausdruck aus Hoffnung und Müdigkeit. Hammon wischte mit seinem Küchentuch über die fleckige Bar und nagelte Ryder mit einem Blick fest, der besagte, dass man mehr erhalten würde, als was Worte ausdrücken können, wenn er sprach. "Ryder, der Wind weht von Thief Lane her. Die Zwillinge Mallory werden heute Nacht ihre Karten im Kopf stechen; die könnten mehr wissen." Hammon schnappte das Tuch mit einem kräftigen zweimaligen Zucken ein. "Such sie bei der Mitternachtsschicht in Dockseide." Ryder wusste, dass der Abend von einem Licht durchtränkt war, das nur metaphorisch seine Schritte beleuchten konnte, und mit dem raschen Schulterzug seines Trenchcoats entschied er, diesem oder jenem Weg nachzugehen. Die Docks waren umweht von Lagermief und nasser Holzstöße; der Zustand schnappte gierig nach jedem, der eine noch unverboschene Nase durch den Nebel streckte. Die Mallory-Zwillinge, Fred und George, waren bekannte Namen – flüssig in Lug und Trug, und ihre Informationen entsprachen dem Klang klirrender Münzen mehr denn einem Gefühl roher Realität. Fred Mallory blitzte Ryder mit einem feinen Nicken entgegen - sie begriffen sich auf einer gemeinsamen stillen Talfahrt durch das Meer der moralischen Verpackung. "Das Phantom?", erwiderte Fred, bevor Ryder eine Arbeitwichse beginnen könnte. "Vielleicht lag's im Tain Memorial Auktionshaus diets Komm, mehr kann ich nicht sagen..." Ryders Augenmerk verharrte länger als er sollte - war dies eine verdeckte Gönnung wichtige Subtilitäten eines gesichtslosen Phantasmas? Das Fingerdümpeln der alten Taschenuhr in seiner Westentasche erinnerte ihn daran, dass Zeit längst nicht neutral zwischen Wahrheit und Verrat verharrte. Er wandte sich um, seinen Beinen ein zusätzliches Scheit Selbstzerstreuung gestattend. Zur dunklen bewölkten Stunde desjenigen Morgens, klopfte Regen entlang Londons Straßen, wobei Ryder Sparrow sicheren Tritt fassen wollte auf der Triangulierung dünnerweise gestauchter Spuren – signifikant das throndsüchtige Auge seiner Stillseelsword. Die Auktionschauser zeugten wabenaufgeschlossenen Mitgesellen - aber frei wurden Quilten unter deckschützen Vereinsmeiereien, ihre sanfthellbrisischen Optionen abwehrverfangen. Drinnen kurz beleuchtet verneurten erhellsgehend Sa
@@ -0,0 +1 @@
1
+ In der trüben Kulisse Londons, die von einem unaufhörlich niederprasselnden Regen fast erdrückt wurde, befand sich die berüchtigte Seemannstaverne "Zum grünen Papagei" in einem der dunkleren Winkel von Limehouse. Es war ein namentlich kaum beachtetes lokal, das doch in den geheimen Gassen des Verbrechens eine bedeutende Rolle spielte. Der grüne Papagei, der hinter der Theke thronte – eine prächtige, doch seltsam bedrohlich wirkende Statue –, bot einem ganz bestimmten Besucher jedes Mal ein unbehagliches Gefühl. Inspector George Hastings vom Scotland Yard trat an diesem düsteren Abend in den schummrigen Raum. Die Geräusche des Ortes – das gedämpfte Gemurmel der Gäste, das gelegentliche Klingeln von Münzen auf Holz und das plötzliche, schallende Lachen – fanden einen Drang in seine Gehörgänge, als wären sie Nachrichten einer längst im Verborgenen gefangenen Welt. Hastings schob sich mit dem gewohnten, unaufdringlichen Selbst bewussten Tritt durch die wenigen Schatten der Anwesenden und fixierte sein Interesse auf Seamus O'Reilly, den Barbesitzer. Ein irischer Einwanderer, dessen Vergangenheit in einem anrüchigen Schleier gehüllt war – O'Reilly war it bohnenlangjähriger Beobachtung und noch längerer Misstrauen. Seine großflächigen, rauen Hände waren ein Zeugnis der Arbeit, doch sein scharfes, neugieriges Auge verriet einen Geist, der nie zur Ruhe zu kommen schien. "Was kann ich für einen Hüter des Gesetzes tun?", brummte O'Reilly und stützte sich auf die polierte Theke. Der Lichtschimmer der sparsamen Beleuchtung zeigte das eine unregelmäßige Sammlung von Narben, die sein Gesicht durchzogen – Zeichen der zahlreichen Wirtshausschlägereien, in die er früher verwickelt gewesen war. "Nicht heute." Hastings verstärkte seinen Unterton freundlicher Höflichkeit. "Nicht als einen obligatorischen Mittwochabendbesuch, zweimal im Monat." "Twice im Mond, ja." Ein lodernder Funken erwachte in O'Reillys Augen, dann erlosch er so schnell wieder, wie er gekommen war. "Wegen der Papageistatue, richtig?" Diese Frage stellen hatte O’Reilly nichts wissend entblättern sollen, doch es war der Auftakt, den Hastings benötigt hatte. Bevor es jedoch zu einer tiefer gehenden Konversation kommen konnte, näherte sich eine weitere Gestalt seinem Platz an der Theke – ein dürrer Mann in einem makellosen Anzug, dessen präzise gefaltetes Einstecktuch sich auffällig von der erdrückenden Neutralität seines dunklen Kostüms abhob. Das rotsilkene Stück Stoff vermittelte ihm die Aura eines unruhigen Ozeans, begraben unter einer kalten See. "Mr. Parker", verkündete O'Reilly mit insgespieltem Respekt, der dem Neuankömmling zur Schau Stellung eines Mannes gerecht wurde, der es gewöhnt war zu erwarten, dass die Welt vor ihm in Sicherheit geht. "Oder sollte ich Sie 'Sir Thomas' nennen?" Thomas Parker réponse in einem amüsierten Ton. "Gerade hier, in diesem Raum, reicht 'Mr. Parker' aus." Je länger er sprach, desto deutlicher wurde für Hastings klar, dass Parker nicht das kompakte Rätsel war, das er vorgab zu sein. Man sagte sich, er stamme aus einer Familie der gehobeneren Londoner Gesellschaft, deren Ansichten über die dunkleren Ecken der Stadt wohl nur auf dem Papier existierten. Mit einem unverbindlichen Nicken verabschiedete sich Parker schnell und ließ die Bar hinter sich, wie jemand, der nichts weiteres erwartete. Ein signifikantes Bedürfnis, das oft eine Stillsegnung war, rührte sich bei Hastings in Auffassung zum Aufmerksamkeitsregel von O’Reillys Barbesucher heraus. Er witterte den Beginn von etwas Komplexem – einem Weben, gleich einer Spinnenqualität, das bislang nur die Umrisse einer Falle
@@ -0,0 +1 @@
1
+ In den dichten Nebelschwaden Londons, die wie ein undurchschaubares Tuch über den Straßen schwebten, lagerten Geheimnisse, die nur auf ihre Entdeckung warteten. Im Herzen der Stadt, in den engen Gassen, wo das Gaslicht schummrig auf das Pflaster fiel, war ein Verbrechen geschehen, das die Bürger in Angst und Schrecken versetzte. Inspektor William Thorn, ein Mann von mittlerem Wuchs und durchdringendem Blick, stolzierte durch den nebelverhangenen Innenhof der Scotland Yard Headquarters. Der kalte Wind spielte mit seinen Mantelschößen, während er sich schnellstmöglich den Weg zum Büro von Kommissar Gordon bahnte. Die nächtlichen Straßen säumten Gestalten, die aus den Schatten traten und wieder hineinglitten, als Thorn den vertrauten Pfad entlangging. Im Büro empfing ihn Kommissar Gordon, ein hagerer Mann mit grimmiger Miene, der trotz seiner streng wirkenden Gesichtszüge eine väterliche Wärme ausstrahlte. Er saß hinter seinem massiven Schreibtisch, der mit Berichten und Karten überhäuft war, und seine blauen Augen fixierten Thorn, als dieser eintrat. „William“, begann Gordon ohne Umschweife in seiner gewohnt tiefen Stimme, „wir haben einen neuen Fall, der unsere ganze Aufmerksamkeit erfordert.“ Der Inspektor zog sich den Stuhl heran und nahm Platz. „Was ist geschehen, Sir?“ fragte er, während seine Gedanken bereits die verschiedenen Möglichkeiten durchspielten. „Gestern Nacht wurde in einem verlassenen Lagerhaus an der Themse die Leiche von Lord Harold Davenport gefunden. Es gibt bisher keine Hinweise, die uns auf das Motiv oder den Täter schließen lassen“, erklärte Gordon knapp und übergab Thorn eine Mappe mit den bisherigen Ermittlungen. Thorn öffnete die Mappe und überflog die ersten Seiten. Lord Davenport war bekannt für seinen exzentrischen Lebensstil, der ihn in die unterschiedlichsten Kreise Londons führte – von der vornehmen Gesellschaft bis hin zu den zwielichtigen Unterweltgestalten. Seine Verstrickungen in dubiose Geschäfte waren ebenso berüchtigt wie seine Fähigkeit, stets ungeschoren davonzukommen. Diese Tatsache allein machte seine Ermordung zu einem merkwürdigen Rätsel; wer immer ihn umgebracht hatte, musste ein dringendes Motiv gehabt haben. „Alle seine nahen Kontakte verbergen etwas“, merkte Gordon an und lehnte sich zurück. „Die Polizei fand ihn mit einem blutroten Seidentuch um den Hals geknotet – ein Symbol, das wir bisher nicht einordnen können.“ Thorn nickte, seine Gedanken rasten. „Und was ist mit seinem Personal, oder etwaigen Geschäftspartnern?“ „Einige der Bediensteten wurden befragt, doch alle scheinen nichts Bemerkenswertes zu wissen“, antwortete der Kommissar mit einem Seufzen. „Es gibt jedoch Gerüchte über eine mysteriöse Frau, die ihn in den Tagen vor seinem Tod regelmäßig besucht haben soll.“ Ein weiterer Hinweis, der eher zu Verwirrung als zur Klarheit beitrug. Thorn erhob sich, entschlossen die Ermittlungen aufzunehmen. Seine erste Station war das Herrenhaus der Davenports, das in einem noblen Viertel lag und dennoch in seiner düsteren Pracht ein gewisses Unbehagen auszudrücken schien. Das Geisterhafte, das über dem Anwesen lag, wurde noch verstärkt durch das fahlen Mondlicht, das sich in den hohen Fenstern spiegelte. Keine Bewegung signalisierten Leben, nichts, außer einem einsamen Diener, der Thorn die Tür öffnete. „Inspektor Thorn“, stellte dieser sich knapp vor und schritt mit dem Bediensteten die kahle Halle entlang hinein in die opulent eingerichtete Bibliothek. Lady Elizabeth Davenport, die Witwe des verstorbenen Lords, empfing ihn. Sie war eine schöne Frau mittleren Alters mit einer eleganten Haltung, die jedoch eine gewisse müde Traurigkeit nicht verbergen konnte. Ihr schwarzes Trauerkleid ließ sie geisterhaft erhaben wirken, und ihre Augen schimmerten unruhig. „Vielen Dank, dass Sie mich empfangen, Lady Davenport“, begann Thorn, während er sich setzte
pelican_nlp/main.py ADDED
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pelican Project
4
+ ===============
5
+
6
+ Pelican is a tool developed to enable consistent and reproducible language processing.
7
+ Main entry point for the Pelican project handling document processing and metric extraction.
8
+
9
+ Author: Yves Pauli
10
+ Created: 2024-01-10
11
+ Version: 1.0.0
12
+
13
+ Copyright (c) 2024 Yves Pauli
14
+ License: Attribution-NonCommercial 4.0 International
15
+ All rights reserved.
16
+ """
17
+
18
+ from pathlib import Path
19
+ from typing import Dict, List
20
+ import torch.cuda
21
+ import sys
22
+
23
+ from pelican_nlp.core import Corpus
24
+ from pelican_nlp.utils.setup_functions import subject_instantiator, load_config, remove_previous_derivative_dir
25
+ from pelican_nlp.preprocessing import LPDS
26
+
27
+ # Constants
28
+ # DEFAULT_CONFIG_PATH = 'configuration_files/config_morteza.yml'
29
+
30
+ class Pelican:
31
+
32
+ """Main class for the Pelican project handling document processing and metric extraction."""
33
+
34
+ def __init__(self, config_path: str = None, dev_mode: bool = True) -> None:
35
+ self.dev_mode = dev_mode
36
+
37
+ # If no config path is provided, use the default config from package
38
+ if config_path is None:
39
+ package_dir = Path(__file__).parent
40
+ default_config = package_dir / 'configuration_files' / 'config_fluency.yml'
41
+ if default_config.exists():
42
+ config_path = str(default_config)
43
+ print(f"Using default configuration file: {config_path}")
44
+ else:
45
+ sys.exit('Error: Default configuration file not found in package.')
46
+
47
+ # Verify the provided path is a YAML file
48
+ elif not config_path.endswith(('.yml', '.yaml')):
49
+ sys.exit('Error: Configuration file must be a YAML file (*.yml or *.yaml)')
50
+
51
+ self.config = load_config(config_path)
52
+ self.project_path = Path(self.config['PATH_TO_PROJECT_FOLDER'])
53
+ self.path_to_subjects = self.project_path / 'subjects'
54
+ self.output_directory = self.project_path / 'derivatives'
55
+ self.task = self.config['task_name']
56
+
57
+ # Add test configuration, TESTS NOT YET IMPLEMENTED
58
+ self.test_config = {
59
+ 'run_all': True, # Run all tests by default
60
+ 'test_paths': ['tests'], # Default test directory
61
+ 'markers': [], # Specific test markers to run
62
+ 'skip_slow': True, # Skip slow tests by default
63
+ }
64
+
65
+ if not self.path_to_subjects.is_dir():
66
+ sys.exit('Error: Could not find subjects directory; check folder structure.')
67
+
68
+ def run(self) -> None:
69
+ """Execute the main processing pipeline."""
70
+ self._clear_gpu_memory()
71
+
72
+ '''
73
+ #run unittests in dev_mode; not yet implemented
74
+ if self.dev_mode:
75
+ self._run_tests()
76
+ '''
77
+
78
+ self._handle_output_directory()
79
+
80
+ # Check/Create LPDS
81
+ self._LPDS()
82
+
83
+ # Instantiate all subjects
84
+ subjects = subject_instantiator(self.config)
85
+
86
+ # Process each corpus
87
+ for corpus_name in self.config['corpus_names']:
88
+ self._process_corpus(corpus_name, subjects)
89
+
90
+ def _process_corpus(self, corpus_name: str, subjects: List) -> None:
91
+
92
+ """Process a single corpus including preprocessing and metric extraction."""
93
+ print(f'Processing corpus: {corpus_name}')
94
+
95
+ corpus_documents = self._identify_corpus_files(subjects, corpus_name)
96
+ corpus = Corpus(corpus_name, corpus_documents[corpus_name], self.config)
97
+
98
+ for document in corpus_documents[corpus_name]:
99
+ document.corpus_name = corpus_name
100
+
101
+ if self.config['input_file']=='text':
102
+ corpus.preprocess_all_documents()
103
+ print(f'Corpus {corpus_name} is preprocessed')
104
+
105
+ self._extract_metrics(corpus)
106
+
107
+ if self.config['create_aggregation_of_results']:
108
+ corpus.create_corpus_results_consolidation_csv()
109
+
110
+ if self.config['output_document_information']:
111
+ corpus.create_document_information_csv()
112
+
113
+
114
+ elif self.config['input_file']=='audio':
115
+ if self.config['opensmile_feature_extraction']:
116
+ corpus.extract_opensmile_features()
117
+
118
+ if self.config['prosogram_extraction']:
119
+ corpus.extract_prosogram()
120
+
121
+ del corpus
122
+
123
+
124
+ def _LPDS(self):
125
+ """Initialize LPDS and create derivative directory"""
126
+ lpds = LPDS(self.project_path, self.config['multiple_sessions'])
127
+ lpds.LPDS_checker()
128
+ lpds.derivative_dir_creator()
129
+
130
+ def _extract_metrics(self, corpus: Corpus) -> None:
131
+ """Extract specified metrics from the corpus."""
132
+ metric = self.config['metric_to_extract']
133
+ if metric == 'logits':
134
+ print('Extracting logits...')
135
+ corpus.extract_logits()
136
+ elif metric == 'embeddings':
137
+ print('Extracting embeddings...')
138
+ corpus.extract_embeddings()
139
+ else:
140
+ raise ValueError(f"Unsupported metric: {metric}")
141
+
142
+ self._clear_gpu_memory()
143
+
144
+ def _identify_corpus_files(self, subjects: List, corpus: str) -> Dict:
145
+ """Identify and group files belonging to a specific corpus."""
146
+ corpus_dict = {corpus: []}
147
+ for subject in subjects:
148
+ for document in subject.documents:
149
+ name = Path(document.name)
150
+ document.extension = name.suffix
151
+ # Split by both '_' and '.' to get all parts
152
+ parts = name.stem.replace('.', '_').split('_')
153
+ # Check if corpus name appears in any part
154
+ if corpus in parts:
155
+ corpus_dict[corpus].append(document)
156
+ return corpus_dict
157
+
158
+ def _handle_output_directory(self) -> None:
159
+ """Handle the output directory based on dev mode."""
160
+ if self.dev_mode:
161
+ remove_previous_derivative_dir(self.output_directory)
162
+ elif self.output_directory.exists():
163
+ self._prompt_for_continuation()
164
+
165
+ def _run_tests(self) -> None:
166
+ # Run unittests to test implemented functions... not yet in use
167
+ """Run test suite in development mode with configurable options."""
168
+ import pytest
169
+ print("Running tests in development mode...")
170
+
171
+ # Build pytest arguments
172
+ pytest_args = ["-v", "--no-header"]
173
+
174
+ # Add test paths
175
+ pytest_args.extend(self.test_config['test_paths'])
176
+
177
+ # Add markers if specified
178
+ for marker in self.test_config['markers']:
179
+ pytest_args.extend(["-m", marker])
180
+
181
+ # Skip slow tests if configured
182
+ if self.test_config['skip_slow']:
183
+ pytest_args.extend(["-m", "not slow"])
184
+
185
+ # Run pytest with constructed arguments
186
+ result = pytest.main(pytest_args)
187
+
188
+ # Handle test results
189
+ if result != 0:
190
+ print("Tests failed. Aborting execution.")
191
+ sys.exit(1)
192
+
193
+ print("All tests passed. Continuing with execution.\n")
194
+
195
+ @staticmethod
196
+ def _prompt_for_continuation() -> None:
197
+ """Prompt user for continuation if output directory exists."""
198
+ print('Warning: An output directory already exists. Continuing might invalidate previously computed results.')
199
+ confirm = input("Do you want to continue? Type 'yes' to proceed: ").strip().lower()
200
+ if confirm not in ('yes', 'y'):
201
+ print("Operation aborted.")
202
+ sys.exit(0)
203
+
204
+ @staticmethod
205
+ def _clear_gpu_memory() -> None:
206
+ if torch.cuda.is_available():
207
+ torch.cuda.empty_cache()
208
+
209
+
210
+ if __name__ == '__main__':
211
+ Pelican().run()
@@ -0,0 +1,34 @@
1
+ import numpy as np
2
+ from itertools import combinations
3
+ from scipy.spatial.distance import pdist, squareform
4
+
5
+ class embeddings_metrics_statistics():
6
+ @staticmethod
7
+ def pairwise_similarities(embeddings, metric_function=None):
8
+ """Compute pairwise similarities between embeddings."""
9
+ distance_matrix = pdist(embeddings, metric='cosine')
10
+ similarity_matrix = 1 - squareform(distance_matrix)
11
+ return similarity_matrix
12
+
13
+ @staticmethod
14
+ def compute_window_statistics(similarities, window_size, aggregation_functions=[np.mean]):
15
+ """Compute aggregated statistics over a given window size."""
16
+ num_tokens = similarities.shape[0]
17
+ stats = {}
18
+
19
+ for start in range(0, num_tokens, window_size):
20
+ end = min(start + window_size, num_tokens)
21
+ window_similarities = similarities[start:end, start:end]
22
+ window_values = window_similarities[np.triu_indices_from(window_similarities, k=1)]
23
+
24
+ for func in aggregation_functions:
25
+ key = f'{func.__name__}_window_{window_size}'
26
+ stats.setdefault(key, []).append(func(window_values))
27
+
28
+ return {key: np.mean(values) for key, values in stats.items()}
29
+
30
+
31
+ @staticmethod
32
+ def aggregate_window(window_values, aggregation_functions=[np.mean]):
33
+ """Aggregates window values using specified functions."""
34
+ return {func.__name__: func(window_values) for func in aggregation_functions}
@@ -0,0 +1,77 @@
1
+ import re
2
+ import os
3
+
4
+ class LPDS:
5
+ def __init__(self, project_folder, multiple_sessions):
6
+ self.project_folder = project_folder
7
+ self.multiple_sessions = multiple_sessions
8
+ self.subjects_folder = os.path.join(self.project_folder, "subjects")
9
+ self.subject_folders = [f for f in os.listdir(self.subjects_folder) if
10
+ os.path.isdir(os.path.join(self.subjects_folder, f))]
11
+
12
+ def LPDS_checker(self):
13
+ # Check if the main project folder exists
14
+ if not os.path.isdir(self.project_folder):
15
+ raise FileNotFoundError(f"Project folder '{self.project_folder}' does not exist.")
16
+
17
+ # Check for required files in the project folder
18
+ suggested_files = ["dataset_description.json", "README", "CHANGES", "participants.tsv"]
19
+ for file in suggested_files:
20
+ if not os.path.isfile(os.path.join(self.project_folder, file)):
21
+ print(f"Warning: Missing suggested file '{file}' in the project folder.")
22
+
23
+ # Check for the 'subjects' folder
24
+ if not os.path.isdir(self.subjects_folder):
25
+ raise FileNotFoundError("Error: The 'subjects' folder is missing in the project folder.")
26
+
27
+ # Check if there is at least one subfolder in 'subjects', ideally named 'sub-01'
28
+ if not self.subject_folders:
29
+ raise FileNotFoundError("Error: No subject subfolders found in the 'subjects' folder.")
30
+ if 'sub-01' not in self.subject_folders:
31
+ print("Warning: Ideally, subject folders should follow the naming convention 'sub-x'.")
32
+
33
+ # Iterate through subject subfolders
34
+ for subject_folder in self.subject_folders:
35
+ subject_path = os.path.join(self.subjects_folder, subject_folder)
36
+
37
+ # Check for session folders if project has sessions
38
+ if self.multiple_sessions:
39
+ session_folders = [f for f in os.listdir(subject_path) if
40
+ os.path.isdir(os.path.join(subject_path, f))]
41
+ if not session_folders:
42
+ print(f"Warning: No session folders found in '{subject_folder}'.")
43
+ if 'ses-01' not in session_folders:
44
+ print(f"Warning: Ideally, the session folders should follow the naming convention 'ses-x'.")
45
+
46
+ # Check for optional subject_metadata file
47
+ metadata_file = os.path.join(subject_path, "subject_metadata")
48
+ if not os.path.isfile(metadata_file):
49
+ #print(f"Note: Optional 'subject_metadata' file is missing in '{subject_folder}'.")
50
+ continue
51
+
52
+ session_folders = subject_folder
53
+
54
+ # Iterate through current level folders (subjects or sessions)
55
+ for session_folder in session_folders:
56
+ session_path = os.path.join(subject_path, session_folder)
57
+ task_folders = [f for f in os.listdir(session_path) if os.path.isdir(os.path.join(session_path, f))]
58
+
59
+ # Check for tasks inside session folder
60
+ for task_folder in task_folders:
61
+ task_path = os.path.join(session_path, task_folder)
62
+ task_files = [f for f in os.listdir(task_path) if os.path.isfile(os.path.join(task_path, f))]
63
+
64
+ # Check naming convention for files in the task folder
65
+ for file in task_files:
66
+ if self.multiple_sessions:
67
+ pattern = fr"^{subject_folder}_{session_folder}_{task_folder}.*"
68
+ else:
69
+ pattern = fr"^{subject_folder}_{task_folder}.*"
70
+ if not re.match(pattern, file):
71
+ print(f"Warning: File '{file}' in '{task_folder}' does not follow the LPDS naming conventions")
72
+
73
+ def derivative_dir_creator(self):
74
+ # Create the 'derivatives' folder if it doesn't exist
75
+ derivatives_folder = os.path.join(self.project_folder, "derivatives")
76
+ if not os.path.exists(derivatives_folder):
77
+ os.makedirs(derivatives_folder)
@@ -0,0 +1,7 @@
1
+ # Import the core classes and functions for easier access
2
+ from .text_importer import TextImporter
3
+ from .text_cleaner import TextCleaner
4
+ from .text_tokenizer import TextTokenizer
5
+ from .text_normalizer import TextNormalizer
6
+ from .pipeline import TextPreprocessingPipeline
7
+ from .LPDS import LPDS
@@ -0,0 +1,50 @@
1
+ from pelican_nlp.preprocessing.text_tokenizer import TextTokenizer
2
+ from pelican_nlp.preprocessing.text_cleaner import TextCleaner, FluencyCleaner
3
+ from pelican_nlp.preprocessing.text_normalizer import TextNormalizer
4
+
5
+ class TextPreprocessingPipeline:
6
+ """Pipeline for text preprocessing operations."""
7
+
8
+ def __init__(self, config):
9
+ """Initialize pipeline with configuration.
10
+
11
+ Args:
12
+ config: Dictionary of configuration options
13
+ """
14
+ self.config = config
15
+ self.pipeline_options = config['pipeline_options']
16
+ self.cleaner = None
17
+ self.normalizer = None
18
+ self.tokenizer = None
19
+
20
+ def process_document(self, document):
21
+ """Process a document through configured pipeline steps.
22
+
23
+ Args:
24
+ document: Document object to process
25
+ """
26
+ print('Processing document (pipeline.py)')
27
+
28
+ for option, enabled in self.pipeline_options.items():
29
+ if enabled:
30
+ processor = getattr(self, f"_{option}")
31
+ processor(document)
32
+
33
+ def _clean_text(self, document):
34
+ """Clean document text."""
35
+ self.cleaner = TextCleaner(self.config['cleaning_options'])
36
+ document.clean_text(self.cleaner)
37
+
38
+ def _tokenize_text(self, document):
39
+ """Tokenize document text."""
40
+ self.tokenizer = TextTokenizer(self.config['tokenization_options'])
41
+ document.tokenize_text(self.tokenizer)
42
+
43
+ def _normalize_text(self, document):
44
+ """Normalize document text."""
45
+ self.normalizer = TextNormalizer(self.config['normalization_options'])
46
+ document.normalize_text(self.normalizer)
47
+
48
+ def _quality_check(self, document):
49
+ """Placeholder for quality check implementation."""
50
+ pass
@@ -0,0 +1,33 @@
1
+ import re
2
+ from collections import Counter
3
+
4
+ class TextDiarizer:
5
+ def __init__(self, method=None):
6
+ self.method = method
7
+
8
+
9
+ def speaker_tag_identification(self, text, num_speakers, exclude_speakers=None):
10
+ if exclude_speakers is None:
11
+ exclude_tags = []
12
+
13
+ # Regular expression to capture potential speaker tags
14
+ potential_tags = re.findall(r'^(\w+):', text, re.MULTILINE)
15
+
16
+ # Count occurrences of potential speaker tags
17
+ tag_counts = Counter(potential_tags)
18
+
19
+ # Filter likely speaker tags based on frequency and exclusion list
20
+ speaker_tags = [tag for tag, count in tag_counts.most_common(num_speakers) if tag not in exclude_speakers]
21
+
22
+ return speaker_tags
23
+
24
+ @staticmethod
25
+ def parse_speaker(text, speaker_tag, keep_speakertag=False):
26
+
27
+ pattern = rf"{re.escape(speaker_tag)}:\s*(.*?)(?=\n\s*\w+:|\Z)"
28
+ matches = re.findall(pattern, text, re.DOTALL)
29
+
30
+ if keep_speakertag:
31
+ return [f"{speaker_tag}: {match.strip()}" for match in matches]
32
+ else:
33
+ return [match.strip() for match in matches]
@@ -0,0 +1,224 @@
1
+ import string
2
+ import re
3
+ from collections import Counter
4
+
5
+ class TextCleaner:
6
+ def __init__(self, options=None):
7
+ self.options = options
8
+
9
+ def clean(self, document, text, characters_to_remove=None):
10
+ """Clean text based on configured options.
11
+
12
+ Args:
13
+ document: Document object containing metadata
14
+ text: Text to clean
15
+ characters_to_remove: Optional string of characters to remove
16
+ Returns:
17
+ Cleaned text string
18
+ """
19
+ if self.options.get('remove_timestamps', True):
20
+ text = remove_timestamps(text, self.options.get('timestamp_pattern_example'))
21
+
22
+ if characters_to_remove: # Simplified condition
23
+ text = _remove_special_characters(text, characters_to_remove)
24
+
25
+ # Consolidate conditional blocks for better readability
26
+ cleaning_operations = [
27
+ ('fluency_task', lambda: self.clean_fluency_transcripts(document, text)),
28
+ ('remove_punctuation', lambda: remove_punctuation(text)),
29
+ ('lowercase', lambda: lowercase(text))
30
+ ]
31
+
32
+ for option, operation in cleaning_operations:
33
+ if self.options.get(option):
34
+ text = operation()
35
+
36
+ if self.options.get('general_cleaning', True):
37
+ replacements = [
38
+ (r'/', ''),
39
+ (r'\s+([?.!,"])', r'\1'),
40
+ (r'\n\s*\n', '\n'),
41
+ (r'\\', ''),
42
+ (' +', ' ')
43
+ ]
44
+ text = self._apply_replacements(text, replacements)
45
+
46
+ return text.strip()
47
+
48
+ @staticmethod
49
+ def _apply_replacements(text, replacements):
50
+ """Apply a list of regex replacements to text."""
51
+ for old, new in replacements:
52
+ text = re.sub(old, new, text)
53
+ return text
54
+
55
+ def clean_fluency_transcripts(self, document, content):
56
+ fluencyCleaner = FluencyCleaner(self.options)
57
+ return fluencyCleaner.cleanFluency(document, content)
58
+
59
+
60
+ class FluencyCleaner:
61
+
62
+ def __init__(self, options=None):
63
+ self.options = options
64
+
65
+ def cleanFluency(self, document, content):
66
+ """Clean fluency task transcripts.
67
+
68
+ Args:
69
+ document: Document object containing metadata
70
+ content: Text content to clean
71
+ Returns:
72
+ Cleaned text string
73
+ """
74
+ word_splitter = self.options['word_splitter']
75
+ # First split by word_splitter if defined
76
+ if word_splitter:
77
+ words = content.split(word_splitter)
78
+ # Further split each chunk by commas
79
+ temp_words = []
80
+ for chunk in words:
81
+ temp_words.extend(chunk.split(','))
82
+ words = temp_words
83
+ else:
84
+ # If no word_splitter defined, just split by commas
85
+ words = content.split(',')
86
+
87
+ self.count_duplicates_and_hyphenated(document, words)
88
+
89
+ content = re.sub(r'\s+', '', content).strip()
90
+ # Apply the same splitting logic for the cleaned content
91
+ if word_splitter:
92
+ words = []
93
+ for chunk in content.split(word_splitter):
94
+ words.extend(chunk.split(','))
95
+ else:
96
+ words = content.split(',')
97
+ words = [word for word in words if word]
98
+
99
+ word_counter = Counter(words)
100
+ document.fluency_duplicate_count = sum(count - 1 for count in word_counter.values() if count > 1)
101
+
102
+ # Apply cleaning operations based on options
103
+ if self.options['remove_hyphens']:
104
+ words = [word.replace('-', '') for word in words]
105
+ if self.options['remove_duplicates']:
106
+ words = self.remove_duplicates(words)
107
+ if self.options['lowercase']:
108
+ words = lowercase(words)
109
+
110
+ return ' '.join(words)
111
+
112
+ @staticmethod
113
+ def remove_duplicates(words):
114
+
115
+ # Remove duplicate words while preserving order
116
+ word_counter = Counter(words)
117
+ seen = set()
118
+ cleaned_words = []
119
+
120
+ for word in words:
121
+ if word in seen and word_counter[word] > 1:
122
+ word_counter[word] -= 1
123
+ else:
124
+ cleaned_words.append(word)
125
+ seen.add(word)
126
+
127
+ return cleaned_words
128
+
129
+ @staticmethod
130
+ def count_duplicates_and_hyphenated(document, words):
131
+ word_counter = Counter(words)
132
+
133
+ document.number_of_duplicates = sum(count - 1 for count in word_counter.values() if count > 1)
134
+ document.number_of_hyphenated_words = sum(1 for word in words if '-' in word)
135
+
136
+
137
+ def _remove_brackets_and_bracketcontent(text):
138
+ replacements = [
139
+ (r'\(.*?\)', ''),
140
+ (r'\<.*?\>', ''),
141
+ (r'\[.*?\]', ''),
142
+ (r'\{.*?\}', '')
143
+ ]
144
+ for old, new in replacements:
145
+ text = re.sub(old, new, text)
146
+ return text
147
+
148
+ def remove_timestamps(text, example):
149
+ #removes timestamps with specified pattern
150
+ pattern = _detect_timestamp_pattern(example)
151
+ return re.sub(pattern, '', text)
152
+
153
+ def _detect_timestamp_pattern(example):
154
+ pattern = re.escape(example) # Escape special characters
155
+ pattern = re.sub(r'\d', r'\\d', pattern) # Replace digits with \d
156
+ return pattern
157
+
158
+ def lowercase(text):
159
+ if isinstance(text, str):
160
+ return text.lower()
161
+ elif isinstance(text, list):
162
+ return [token.lower() for token in text]
163
+ else:
164
+ raise ValueError("Input to _lowercase must be either a string or a list of tokens")
165
+
166
+ def remove_punctuation(text):
167
+ if isinstance(text, str):
168
+ return text.translate(str.maketrans('', '', string.punctuation))
169
+ elif isinstance(text, list):
170
+ return [token.translate(str.maketrans('', '', string.punctuation)) for token in text]
171
+ else:
172
+ raise ValueError("Input to _remove_punctuation must be either a string or a list of tokens")
173
+
174
+ def _remove_special_characters(text, characters_to_remove):
175
+ return text.translate(str.maketrans('', '', characters_to_remove))
176
+
177
+ def remove_speaker_tags(text, speaker_tags):
178
+ pattern = re.compile(r'^(?:' + '|'.join(re.escape(tag) for tag in speaker_tags) + r'):\s*', re.MULTILINE)
179
+ return re.sub(pattern, '', text)
180
+
181
+ def clean_subword_token_RoBERTa(token):
182
+ """Clean RoBERTa subword tokens.
183
+
184
+ Args:
185
+ token: String token to clean
186
+ Returns:
187
+ Cleaned token or None if token should be ignored
188
+ """
189
+ # Special character mappings
190
+ char_mappings = {
191
+ '√§': 'ä',
192
+ '√º': 'ü',
193
+ '√∂': 'ö',
194
+ '√í': 'í'
195
+ }
196
+
197
+ # Remove subword boundary marker
198
+ clean_token = token.replace("▁", "")
199
+
200
+ # Replace special characters
201
+ for old, new in char_mappings.items():
202
+ clean_token = clean_token.replace(old, new)
203
+
204
+ # Remove bracketed content
205
+ clean_token = re.sub(r"\[.*?\]|\(.*?\)", "", clean_token)
206
+
207
+ # Keep only letters, numbers, and hyphens
208
+ clean_token = re.sub(r"[^A-Za-z0-9\u00C0-\u017F\-]", "", clean_token)
209
+
210
+ return None if clean_token.isdigit() else clean_token.strip()
211
+
212
+
213
+ def clean_token_generic(token):
214
+ """Generic token cleaning method for non-RoBERTa models.
215
+ This is a placeholder that should be implemented based on specific model needs.
216
+
217
+ Args:
218
+ token: String token to clean
219
+ Returns:
220
+ Cleaned token or None if token should be ignored
221
+ """
222
+ # TODO: Implement proper cleaning logic for other models
223
+ # For now, just return the token as is if it's not empty or None
224
+ return token.strip() if token and token.strip() else None