1use 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#[derive(Debug, Clone, Copy, PartialEq)]
41pub enum ParaphraseStrategy {
42 Synonym,
44 Restructure,
46 BackTranslation,
48 Hybrid,
50}
51
52#[derive(Debug, Clone)]
54pub struct ParaphraseConfig {
55 pub num_variations: usize,
57 pub strategy: ParaphraseStrategy,
59 pub preserve_entities: bool,
61 pub min_similarity: f32,
63 pub max_replacement_ratio: f32,
65 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#[derive(Debug, Clone)]
84pub struct ParaphraseResult {
85 pub text: String,
87 pub similarity: f32,
89 pub strategy_used: ParaphraseStrategy,
91 pub replacements: Vec<(String, String)>,
93}
94
95pub 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 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 pub fn with_word2vec(mut self, model: Word2Vec) -> Self {
116 self.word2vec = Some(model);
117 self
118 }
119
120 pub fn with_tokenizer(mut self, tokenizer: Box<dyn Tokenizer>) -> Self {
122 self.tokenizer = tokenizer;
123 self
124 }
125
126 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 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 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 let paraphrase_lower = paraphrase_result.text.to_lowercase();
172 if !seen.contains(¶phrase_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 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 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 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 for &idx in candidates.iter() {
209 if replaced_count >= max_replacements {
210 break;
211 }
212
213 let word = &tokens[idx];
214
215 if word.len() <= 2 || !word.chars().any(|c| c.is_alphabetic()) {
217 continue;
218 }
219
220 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, ¶phrased_text);
230
231 Ok(ParaphraseResult {
232 text: paraphrased_text,
233 similarity,
234 strategy_used: ParaphraseStrategy::Synonym,
235 replacements,
236 })
237 }
238
239 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 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 fn find_synonym(&self, word: &str) -> Result<Option<String>> {
267 let word_lower = word.to_lowercase();
268
269 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 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 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 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 fn pattern_passive_to_active(&self, text: &str) -> String {
326 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 fn pattern_clause_reorder(&self, text: &str) -> String {
347 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 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 fn pattern_adverb_movement(&self, text: &str) -> String {
382 let tokens: Vec<&str> = text.split_whitespace().collect();
384 if tokens.len() < 3 {
385 return text.to_string();
386 }
387
388 for (i, token) in tokens.iter().enumerate() {
390 if token.ends_with("ly") && i > 0 {
391 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 fn apply_backtranslation_patterns(&self, text: &str) -> Result<String> {
404 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 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 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 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 fn pattern_number_variation(&self, text: &str) -> String {
477 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 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 new_tokens[i] = format!("{}s", token);
489 break;
490 }
491 }
492
493 new_tokens.join(" ")
494 }
495
496 fn calculate_similarity(&self, text1: &str, text2: &str) -> f32 {
498 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 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 fn build_default_synonym_map() -> HashMap<String, Vec<String>> {
544 let mut map = HashMap::new();
545
546 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}