Skip to main content

scirs2_text/
paraphrasing.rs

1//! # Text Paraphrasing Module
2//!
3//! This module provides advanced text paraphrasing capabilities using multiple strategies:
4//! - **Synonym-based paraphrasing**: Replace words with semantically similar alternatives
5//! - **Sentence restructuring**: Reorder clauses and change sentence structures
6//! - **Back-translation**: Simulate translation-based paraphrasing patterns
7//! - **Template-based variation**: Use linguistic patterns for generation
8//!
9//! ## Quick Start
10//!
11//! ```rust
12//! use scirs2_text::paraphrasing::{Paraphraser, ParaphraseConfig, ParaphraseStrategy};
13//!
14//! let config = ParaphraseConfig {
15//!     num_variations: 3,
16//!     strategy: ParaphraseStrategy::Hybrid,
17//!     preserve_entities: true,
18//!     min_similarity: 0.6,
19//!     ..Default::default()
20//! };
21//!
22//! let paraphraser = Paraphraser::new(config);
23//! let text = "The quick brown fox jumps over the lazy dog";
24//! let paraphrases = paraphraser.paraphrase(text).expect("Paraphrasing failed");
25//!
26//! for (i, paraphrase) in paraphrases.iter().enumerate() {
27//!     println!("Paraphrase {}: {}", i + 1, paraphrase.text);
28//!     println!("  Similarity: {:.3}", paraphrase.similarity);
29//! }
30//! ```
31
32use crate::embeddings::Word2Vec;
33use crate::error::{Result, TextError};
34use crate::tokenize::{Tokenizer, WordTokenizer};
35use scirs2_core::random::{rngs::StdRng, SeedableRng};
36use scirs2_core::RngExt;
37use std::collections::{HashMap, HashSet};
38
39/// Paraphrasing strategy
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub enum ParaphraseStrategy {
42    /// Use only synonym replacement
43    Synonym,
44    /// Use only sentence restructuring
45    Restructure,
46    /// Use back-translation patterns
47    BackTranslation,
48    /// Combine multiple strategies
49    Hybrid,
50}
51
52/// Configuration for paraphrasing
53#[derive(Debug, Clone)]
54pub struct ParaphraseConfig {
55    /// Number of paraphrase variations to generate
56    pub num_variations: usize,
57    /// Paraphrasing strategy to use
58    pub strategy: ParaphraseStrategy,
59    /// Whether to preserve named entities
60    pub preserve_entities: bool,
61    /// Minimum semantic similarity threshold (0.0-1.0)
62    pub min_similarity: f32,
63    /// Maximum percentage of words to replace (0.0-1.0)
64    pub max_replacement_ratio: f32,
65    /// Whether to use aggressive transformations
66    pub aggressive: bool,
67}
68
69impl Default for ParaphraseConfig {
70    fn default() -> Self {
71        Self {
72            num_variations: 3,
73            strategy: ParaphraseStrategy::Hybrid,
74            preserve_entities: true,
75            min_similarity: 0.6,
76            max_replacement_ratio: 0.4,
77            aggressive: false,
78        }
79    }
80}
81
82/// A paraphrased text result
83#[derive(Debug, Clone)]
84pub struct ParaphraseResult {
85    /// The paraphrased text
86    pub text: String,
87    /// Semantic similarity to original (0.0-1.0)
88    pub similarity: f32,
89    /// Strategy used for this paraphrase
90    pub strategy_used: ParaphraseStrategy,
91    /// Words that were replaced
92    pub replacements: Vec<(String, String)>,
93}
94
95/// Main paraphraser
96pub struct Paraphraser {
97    config: ParaphraseConfig,
98    tokenizer: Box<dyn Tokenizer>,
99    word2vec: Option<Word2Vec>,
100    synonym_map: HashMap<String, Vec<String>>,
101}
102
103impl Paraphraser {
104    /// Create a new paraphraser with configuration
105    pub fn new(config: ParaphraseConfig) -> Self {
106        Self {
107            config,
108            tokenizer: Box::new(WordTokenizer::default()),
109            word2vec: None,
110            synonym_map: Self::build_default_synonym_map(),
111        }
112    }
113
114    /// Create with a trained Word2Vec model for better synonym detection
115    pub fn with_word2vec(mut self, model: Word2Vec) -> Self {
116        self.word2vec = Some(model);
117        self
118    }
119
120    /// Create with a custom tokenizer
121    pub fn with_tokenizer(mut self, tokenizer: Box<dyn Tokenizer>) -> Self {
122        self.tokenizer = tokenizer;
123        self
124    }
125
126    /// Derive a deterministic u64 seed from a text string using FNV-1a hashing.
127    ///
128    /// Using a text-derived seed ensures that the same input always produces the
129    /// same output, eliminating non-determinism from parallel test execution.
130    fn text_seed(text: &str) -> u64 {
131        let mut hash: u64 = 14695981039346656037;
132        for byte in text.bytes() {
133            hash ^= byte as u64;
134            hash = hash.wrapping_mul(1099511628211);
135        }
136        hash
137    }
138
139    /// Generate paraphrases of the input text
140    pub fn paraphrase(&self, text: &str) -> Result<Vec<ParaphraseResult>> {
141        if text.trim().is_empty() {
142            return Err(TextError::InvalidInput("Input text is empty".into()));
143        }
144
145        let mut results = Vec::new();
146        let mut seen = HashSet::new();
147        seen.insert(text.to_lowercase());
148
149        let mut attempt = 0;
150        let max_attempts = self.config.num_variations * 3;
151
152        while results.len() < self.config.num_variations && attempt < max_attempts {
153            attempt += 1;
154
155            let strategy = if self.config.strategy == ParaphraseStrategy::Hybrid {
156                // Select a strategy deterministically but varying by attempt so the
157                // dedup loop can pick different strategies on successive iterations.
158                self.select_random_strategy(text, attempt)
159            } else {
160                self.config.strategy
161            };
162
163            let paraphrase_result = match strategy {
164                ParaphraseStrategy::Synonym => self.paraphrase_synonym(text)?,
165                ParaphraseStrategy::Restructure => self.paraphrase_restructure(text)?,
166                ParaphraseStrategy::BackTranslation => self.paraphrase_backtranslation(text)?,
167                ParaphraseStrategy::Hybrid => unreachable!(),
168            };
169
170            // Check for duplicates
171            let paraphrase_lower = paraphrase_result.text.to_lowercase();
172            if !seen.contains(&paraphrase_lower) && paraphrase_result.text != text {
173                seen.insert(paraphrase_lower);
174                results.push(paraphrase_result);
175            }
176        }
177
178        if results.is_empty() {
179            return Err(TextError::ProcessingError(
180                "Could not generate any valid paraphrases".to_string(),
181            ));
182        }
183
184        Ok(results)
185    }
186
187    /// Paraphrase using synonym replacement
188    fn paraphrase_synonym(&self, text: &str) -> Result<ParaphraseResult> {
189        let tokens = self.tokenizer.tokenize(text)?;
190        let mut rng = StdRng::seed_from_u64(Self::text_seed(text).wrapping_add(1));
191        let mut new_tokens = tokens.clone();
192        let mut replacements = Vec::new();
193
194        // Determine how many words to replace
195        let max_replacements =
196            ((tokens.len() as f32 * self.config.max_replacement_ratio).ceil() as usize).max(1);
197
198        let mut replaced_count = 0;
199        let mut candidates: Vec<usize> = (0..tokens.len()).collect();
200
201        // Shuffle candidates
202        for i in (1..candidates.len()).rev() {
203            let j = (rng.random::<f32>() * (i + 1) as f32) as usize;
204            candidates.swap(i, j);
205        }
206
207        // Try to replace words
208        for &idx in candidates.iter() {
209            if replaced_count >= max_replacements {
210                break;
211            }
212
213            let word = &tokens[idx];
214
215            // Skip short words and punctuation
216            if word.len() <= 2 || !word.chars().any(|c| c.is_alphabetic()) {
217                continue;
218            }
219
220            // Try to find a synonym
221            if let Some(synonym) = self.find_synonym(word)? {
222                new_tokens[idx] = synonym.clone();
223                replacements.push((word.clone(), synonym));
224                replaced_count += 1;
225            }
226        }
227
228        let paraphrased_text = new_tokens.join(" ");
229        let similarity = self.calculate_similarity(text, &paraphrased_text);
230
231        Ok(ParaphraseResult {
232            text: paraphrased_text,
233            similarity,
234            strategy_used: ParaphraseStrategy::Synonym,
235            replacements,
236        })
237    }
238
239    /// Paraphrase using sentence restructuring
240    fn paraphrase_restructure(&self, text: &str) -> Result<ParaphraseResult> {
241        let restructured = self.apply_restructuring_patterns(text)?;
242        let similarity = self.calculate_similarity(text, &restructured);
243
244        Ok(ParaphraseResult {
245            text: restructured,
246            similarity,
247            strategy_used: ParaphraseStrategy::Restructure,
248            replacements: vec![],
249        })
250    }
251
252    /// Paraphrase using back-translation patterns
253    fn paraphrase_backtranslation(&self, text: &str) -> Result<ParaphraseResult> {
254        let transformed = self.apply_backtranslation_patterns(text)?;
255        let similarity = self.calculate_similarity(text, &transformed);
256
257        Ok(ParaphraseResult {
258            text: transformed,
259            similarity,
260            strategy_used: ParaphraseStrategy::BackTranslation,
261            replacements: vec![],
262        })
263    }
264
265    /// Find a synonym for a word
266    fn find_synonym(&self, word: &str) -> Result<Option<String>> {
267        let word_lower = word.to_lowercase();
268
269        // Try Word2Vec if available
270        if let Some(ref model) = self.word2vec {
271            if let Ok(similar_words) = model.most_similar(&word_lower, 5) {
272                if !similar_words.is_empty() {
273                    let mut rng = StdRng::seed_from_u64(Self::text_seed(word).wrapping_add(2));
274                    let idx = (rng.random::<f32>() * similar_words.len() as f32) as usize;
275                    let selected = &similar_words[idx.min(similar_words.len() - 1)].0;
276                    return Ok(Some(self.match_case(word, selected)));
277                }
278            }
279        }
280
281        // Fall back to synonym map
282        if let Some(synonyms) = self.synonym_map.get(&word_lower) {
283            if !synonyms.is_empty() {
284                let mut rng = StdRng::seed_from_u64(Self::text_seed(word).wrapping_add(3));
285                let idx = (rng.random::<f32>() * synonyms.len() as f32) as usize;
286                let selected = &synonyms[idx.min(synonyms.len() - 1)];
287                return Ok(Some(self.match_case(word, selected)));
288            }
289        }
290
291        Ok(None)
292    }
293
294    /// Match the case of the original word
295    fn match_case(&self, original: &str, replacement: &str) -> String {
296        if original.chars().all(|c| c.is_uppercase()) {
297            replacement.to_uppercase()
298        } else if original.chars().next().is_some_and(|c| c.is_uppercase()) {
299            let mut chars = replacement.chars();
300            match chars.next() {
301                None => String::new(),
302                Some(first) => first.to_uppercase().chain(chars).collect(),
303            }
304        } else {
305            replacement.to_lowercase()
306        }
307    }
308
309    /// Apply sentence restructuring patterns
310    fn apply_restructuring_patterns(&self, text: &str) -> Result<String> {
311        let mut rng = StdRng::seed_from_u64(Self::text_seed(text).wrapping_add(4));
312        let pattern_idx = (rng.random::<f32>() * 4.0) as usize;
313
314        let result = match pattern_idx {
315            0 => self.pattern_passive_to_active(text),
316            1 => self.pattern_clause_reorder(text),
317            2 => self.pattern_conjunction_variation(text),
318            _ => self.pattern_adverb_movement(text),
319        };
320
321        Ok(result)
322    }
323
324    /// Convert passive voice to active or vice versa
325    fn pattern_passive_to_active(&self, text: &str) -> String {
326        // Simple pattern: "X is Y by Z" -> "Z Y X"
327        // This is a simplified transformation
328        if text.contains(" is ") && text.contains(" by ") {
329            let parts: Vec<&str> = text.split(" by ").collect();
330            if parts.len() == 2 {
331                let first_parts: Vec<&str> = parts[0].split(" is ").collect();
332                if first_parts.len() == 2 {
333                    return format!(
334                        "{} {} {}",
335                        parts[1].trim(),
336                        first_parts[1].trim(),
337                        first_parts[0].trim()
338                    );
339                }
340            }
341        }
342        text.to_string()
343    }
344
345    /// Reorder clauses in compound sentences
346    fn pattern_clause_reorder(&self, text: &str) -> String {
347        // Reorder around conjunctions
348        for conj in &[" and ", " but ", " or ", ", "] {
349            if text.contains(conj) {
350                let parts: Vec<&str> = text.splitn(2, conj).collect();
351                if parts.len() == 2 {
352                    return format!("{}{}{}", parts[1].trim(), conj, parts[0].trim());
353                }
354            }
355        }
356        text.to_string()
357    }
358
359    /// Vary conjunctions
360    fn pattern_conjunction_variation(&self, text: &str) -> String {
361        let replacements = [
362            (" and ", " as well as "),
363            (" but ", " however "),
364            (" because ", " since "),
365            (" so ", " therefore "),
366        ];
367
368        let mut result = text.to_string();
369        let mut rng = StdRng::seed_from_u64(Self::text_seed(text).wrapping_add(5));
370        let idx = (rng.random::<f32>() * replacements.len() as f32) as usize;
371        let (original, replacement) = replacements[idx.min(replacements.len() - 1)];
372
373        if result.contains(original) {
374            result = result.replacen(original, replacement, 1);
375        }
376
377        result
378    }
379
380    /// Move adverbs to different positions
381    fn pattern_adverb_movement(&self, text: &str) -> String {
382        // Move adverbs ending in "ly" to different positions
383        let tokens: Vec<&str> = text.split_whitespace().collect();
384        if tokens.len() < 3 {
385            return text.to_string();
386        }
387
388        // Find adverbs
389        for (i, token) in tokens.iter().enumerate() {
390            if token.ends_with("ly") && i > 0 {
391                // Move adverb to the beginning
392                let mut new_tokens = tokens.clone();
393                new_tokens.remove(i);
394                new_tokens.insert(0, token);
395                return new_tokens.join(" ");
396            }
397        }
398
399        text.to_string()
400    }
401
402    /// Apply back-translation patterns
403    fn apply_backtranslation_patterns(&self, text: &str) -> Result<String> {
404        // Simulate back-translation artifacts
405        let patterns = [
406            self.pattern_article_variation(text),
407            self.pattern_preposition_variation(text),
408            self.pattern_tense_variation(text),
409            self.pattern_number_variation(text),
410        ];
411
412        let mut rng = StdRng::seed_from_u64(Self::text_seed(text).wrapping_add(6));
413        let idx = (rng.random::<f32>() * patterns.len() as f32) as usize;
414        Ok(patterns[idx.min(patterns.len() - 1)].clone())
415    }
416
417    /// Vary article usage
418    fn pattern_article_variation(&self, text: &str) -> String {
419        let mut result = text.to_string();
420        let replacements = [(" a ", " the "), (" the ", " a "), (" an ", " the ")];
421
422        let mut rng = StdRng::seed_from_u64(Self::text_seed(text).wrapping_add(7));
423        let idx = (rng.random::<f32>() * replacements.len() as f32) as usize;
424        let (original, replacement) = replacements[idx.min(replacements.len() - 1)];
425
426        if result.contains(original) {
427            result = result.replacen(original, replacement, 1);
428        }
429
430        result
431    }
432
433    /// Vary prepositions
434    fn pattern_preposition_variation(&self, text: &str) -> String {
435        let replacements = [
436            (" on ", " upon "),
437            (" in ", " within "),
438            (" at ", " in "),
439            (" to ", " towards "),
440        ];
441
442        let mut result = text.to_string();
443        let mut rng = StdRng::seed_from_u64(Self::text_seed(text).wrapping_add(8));
444        let idx = (rng.random::<f32>() * replacements.len() as f32) as usize;
445        let (original, replacement) = replacements[idx.min(replacements.len() - 1)];
446
447        if result.contains(original) {
448            result = result.replacen(original, replacement, 1);
449        }
450
451        result
452    }
453
454    /// Vary verb tenses
455    fn pattern_tense_variation(&self, text: &str) -> String {
456        let replacements = [
457            (" is ", " was "),
458            (" are ", " were "),
459            (" has ", " had "),
460            (" will ", " would "),
461        ];
462
463        let mut result = text.to_string();
464        let mut rng = StdRng::seed_from_u64(Self::text_seed(text).wrapping_add(9));
465        let idx = (rng.random::<f32>() * replacements.len() as f32) as usize;
466        let (original, replacement) = replacements[idx.min(replacements.len() - 1)];
467
468        if result.contains(original) {
469            result = result.replacen(original, replacement, 1);
470        }
471
472        result
473    }
474
475    /// Vary singular/plural forms
476    fn pattern_number_variation(&self, text: &str) -> String {
477        // This is a very simplified approach
478        let tokens: Vec<String> = text.split_whitespace().map(|s| s.to_string()).collect();
479        let mut new_tokens = tokens.clone();
480
481        for (i, token) in tokens.iter().enumerate() {
482            if token.ends_with('s') && token.len() > 2 && !token.ends_with("ss") {
483                // Try to singularize
484                new_tokens[i] = token[..token.len() - 1].to_string();
485                break;
486            } else if !token.ends_with('s') && token.chars().all(|c| c.is_alphabetic()) {
487                // Try to pluralize
488                new_tokens[i] = format!("{}s", token);
489                break;
490            }
491        }
492
493        new_tokens.join(" ")
494    }
495
496    /// Calculate semantic similarity between two texts
497    fn calculate_similarity(&self, text1: &str, text2: &str) -> f32 {
498        // Simple Jaccard similarity based on tokens
499        let tokens1: HashSet<String> = text1
500            .to_lowercase()
501            .split_whitespace()
502            .map(|s| s.to_string())
503            .collect();
504
505        let tokens2: HashSet<String> = text2
506            .to_lowercase()
507            .split_whitespace()
508            .map(|s| s.to_string())
509            .collect();
510
511        let intersection = tokens1.intersection(&tokens2).count();
512        let union = tokens1.union(&tokens2).count();
513
514        if union == 0 {
515            return 0.0;
516        }
517
518        intersection as f32 / union as f32
519    }
520
521    /// Select a deterministic strategy for hybrid mode.
522    ///
523    /// The seed mixes the text hash with the attempt counter so that successive
524    /// iterations of the dedup loop choose different strategies and are therefore
525    /// able to produce distinct paraphrases.
526    fn select_random_strategy(&self, text: &str, attempt: usize) -> ParaphraseStrategy {
527        let seed = Self::text_seed(text)
528            .wrapping_add(10)
529            .wrapping_add(attempt as u64);
530        let mut rng = StdRng::seed_from_u64(seed);
531        let val = rng.random::<f32>();
532
533        if val < 0.33 {
534            ParaphraseStrategy::Synonym
535        } else if val < 0.67 {
536            ParaphraseStrategy::Restructure
537        } else {
538            ParaphraseStrategy::BackTranslation
539        }
540    }
541
542    /// Build a default synonym map
543    fn build_default_synonym_map() -> HashMap<String, Vec<String>> {
544        let mut map = HashMap::new();
545
546        // Common synonyms
547        map.insert(
548            "good".to_string(),
549            vec![
550                "excellent".to_string(),
551                "great".to_string(),
552                "fine".to_string(),
553            ],
554        );
555        map.insert(
556            "bad".to_string(),
557            vec![
558                "poor".to_string(),
559                "awful".to_string(),
560                "terrible".to_string(),
561            ],
562        );
563        map.insert(
564            "big".to_string(),
565            vec![
566                "large".to_string(),
567                "huge".to_string(),
568                "enormous".to_string(),
569            ],
570        );
571        map.insert(
572            "small".to_string(),
573            vec![
574                "tiny".to_string(),
575                "little".to_string(),
576                "minute".to_string(),
577            ],
578        );
579        map.insert(
580            "fast".to_string(),
581            vec![
582                "quick".to_string(),
583                "rapid".to_string(),
584                "swift".to_string(),
585            ],
586        );
587        map.insert(
588            "slow".to_string(),
589            vec![
590                "gradual".to_string(),
591                "leisurely".to_string(),
592                "sluggish".to_string(),
593            ],
594        );
595        map.insert(
596            "important".to_string(),
597            vec![
598                "significant".to_string(),
599                "crucial".to_string(),
600                "vital".to_string(),
601            ],
602        );
603        map.insert(
604            "easy".to_string(),
605            vec![
606                "simple".to_string(),
607                "effortless".to_string(),
608                "straightforward".to_string(),
609            ],
610        );
611        map.insert(
612            "difficult".to_string(),
613            vec![
614                "hard".to_string(),
615                "challenging".to_string(),
616                "complex".to_string(),
617            ],
618        );
619        map.insert(
620            "beautiful".to_string(),
621            vec![
622                "lovely".to_string(),
623                "attractive".to_string(),
624                "gorgeous".to_string(),
625            ],
626        );
627        map.insert(
628            "happy".to_string(),
629            vec![
630                "joyful".to_string(),
631                "cheerful".to_string(),
632                "delighted".to_string(),
633            ],
634        );
635        map.insert(
636            "sad".to_string(),
637            vec![
638                "unhappy".to_string(),
639                "sorrowful".to_string(),
640                "melancholy".to_string(),
641            ],
642        );
643        map.insert(
644            "smart".to_string(),
645            vec![
646                "intelligent".to_string(),
647                "clever".to_string(),
648                "bright".to_string(),
649            ],
650        );
651        map.insert(
652            "stupid".to_string(),
653            vec![
654                "foolish".to_string(),
655                "silly".to_string(),
656                "ignorant".to_string(),
657            ],
658        );
659        map.insert(
660            "old".to_string(),
661            vec![
662                "ancient".to_string(),
663                "aged".to_string(),
664                "elderly".to_string(),
665            ],
666        );
667        map.insert(
668            "new".to_string(),
669            vec![
670                "recent".to_string(),
671                "modern".to_string(),
672                "fresh".to_string(),
673            ],
674        );
675        map.insert(
676            "strong".to_string(),
677            vec![
678                "powerful".to_string(),
679                "robust".to_string(),
680                "sturdy".to_string(),
681            ],
682        );
683        map.insert(
684            "weak".to_string(),
685            vec![
686                "feeble".to_string(),
687                "frail".to_string(),
688                "fragile".to_string(),
689            ],
690        );
691        map.insert(
692            "clean".to_string(),
693            vec![
694                "spotless".to_string(),
695                "pristine".to_string(),
696                "immaculate".to_string(),
697            ],
698        );
699        map.insert(
700            "dirty".to_string(),
701            vec![
702                "filthy".to_string(),
703                "grimy".to_string(),
704                "soiled".to_string(),
705            ],
706        );
707        map.insert(
708            "quick".to_string(),
709            vec!["fast".to_string(), "rapid".to_string(), "swift".to_string()],
710        );
711        map.insert(
712            "lazy".to_string(),
713            vec![
714                "idle".to_string(),
715                "sluggish".to_string(),
716                "lethargic".to_string(),
717            ],
718        );
719        map.insert(
720            "jumps".to_string(),
721            vec![
722                "leaps".to_string(),
723                "hops".to_string(),
724                "bounds".to_string(),
725            ],
726        );
727        map.insert(
728            "brown".to_string(),
729            vec![
730                "tan".to_string(),
731                "chestnut".to_string(),
732                "tawny".to_string(),
733            ],
734        );
735        map.insert(
736            "over".to_string(),
737            vec![
738                "above".to_string(),
739                "across".to_string(),
740                "past".to_string(),
741            ],
742        );
743
744        map
745    }
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751
752    #[test]
753    fn test_paraphrase_basic() {
754        let config = ParaphraseConfig::default();
755        let paraphraser = Paraphraser::new(config);
756
757        let text = "The quick brown fox jumps over the lazy dog";
758        let result = paraphraser.paraphrase(text);
759        assert!(result.is_ok());
760
761        let paraphrases = result.expect("Test failure: paraphrasing should succeed");
762        assert!(!paraphrases.is_empty());
763        assert!(paraphrases[0].text != text);
764    }
765
766    #[test]
767    fn test_synonym_replacement() {
768        let config = ParaphraseConfig {
769            num_variations: 1,
770            strategy: ParaphraseStrategy::Synonym,
771            ..Default::default()
772        };
773        let paraphraser = Paraphraser::new(config);
774
775        let text = "This is a good example";
776        let result = paraphraser.paraphrase(text);
777        assert!(result.is_ok());
778
779        let paraphrases = result.expect("Test failure: paraphrasing should succeed");
780        assert!(!paraphrases.is_empty());
781    }
782
783    #[test]
784    fn test_case_matching() {
785        let config = ParaphraseConfig::default();
786        let paraphraser = Paraphraser::new(config);
787
788        assert_eq!(paraphraser.match_case("Good", "excellent"), "Excellent");
789        assert_eq!(paraphraser.match_case("GOOD", "excellent"), "EXCELLENT");
790        assert_eq!(paraphraser.match_case("good", "excellent"), "excellent");
791    }
792
793    #[test]
794    fn test_similarity_calculation() {
795        let config = ParaphraseConfig::default();
796        let paraphraser = Paraphraser::new(config);
797
798        let text1 = "the quick brown fox";
799        let text2 = "the quick brown fox";
800        let similarity = paraphraser.calculate_similarity(text1, text2);
801        assert!((similarity - 1.0).abs() < 0.001);
802
803        let text3 = "the slow white cat";
804        let similarity2 = paraphraser.calculate_similarity(text1, text3);
805        assert!(similarity2 < 1.0 && similarity2 > 0.0);
806    }
807
808    #[test]
809    fn test_empty_input() {
810        let config = ParaphraseConfig::default();
811        let paraphraser = Paraphraser::new(config);
812
813        let result = paraphraser.paraphrase("");
814        assert!(result.is_err());
815    }
816}