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.
- pelican_nlp/__init__.py +9 -0
- pelican_nlp/core/__init__.py +5 -0
- pelican_nlp/core/audio_document.py +20 -0
- pelican_nlp/core/corpus.py +296 -0
- pelican_nlp/core/document.py +226 -0
- pelican_nlp/core/subject.py +30 -0
- pelican_nlp/extraction/__init__.py +2 -0
- pelican_nlp/extraction/acoustic_feature_extraction.py +71 -0
- pelican_nlp/extraction/distance_from_randomness.py +109 -0
- pelican_nlp/extraction/extract_embeddings.py +57 -0
- pelican_nlp/extraction/extract_logits.py +102 -0
- pelican_nlp/extraction/language_model.py +71 -0
- pelican_nlp/extraction/semantic_similarity.py +60 -0
- pelican_nlp/extraction/test_documents/test_features.csv +4 -0
- pelican_nlp/extraction/test_documents/wallace_1.15_3.txt +1 -0
- pelican_nlp/extraction/test_documents/wallace_1.1_3.txt +1 -0
- pelican_nlp/extraction/test_documents/wallace_1_4.txt +1 -0
- pelican_nlp/main.py +211 -0
- pelican_nlp/metrics_statistics/embeddings_metrics_statistics.py +34 -0
- pelican_nlp/preprocessing/LPDS.py +77 -0
- pelican_nlp/preprocessing/__init__.py +7 -0
- pelican_nlp/preprocessing/pipeline.py +50 -0
- pelican_nlp/preprocessing/speaker_diarization.py +33 -0
- pelican_nlp/preprocessing/text_cleaner.py +224 -0
- pelican_nlp/preprocessing/text_importer.py +42 -0
- pelican_nlp/preprocessing/text_normalizer.py +24 -0
- pelican_nlp/preprocessing/text_tokenizer.py +43 -0
- pelican_nlp/sample_configuration_files/config_discourse.yml +103 -0
- pelican_nlp/sample_configuration_files/config_fluency.yml +108 -0
- pelican_nlp/sample_configuration_files/config_general.yml +131 -0
- pelican_nlp/utils/__init__.py +3 -0
- pelican_nlp/utils/csv_functions.py +193 -0
- pelican_nlp/utils/sample_usage.py +17 -0
- pelican_nlp/utils/setup_functions.py +93 -0
- pelican_nlp-0.1.0.dist-info/METADATA +146 -0
- pelican_nlp-0.1.0.dist-info/RECORD +39 -0
- pelican_nlp-0.1.0.dist-info/WHEEL +5 -0
- pelican_nlp-0.1.0.dist-info/licenses/LICENSE +400 -0
- 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
|