#![ allow( clippy::trivially_copy_pass_by_ref ) ]
use api_huggingface::
{
Client,
environment::HuggingFaceEnvironmentImpl,
components::
{
input::InferenceParameters,
},
secret::Secret,
};
use std::{ collections::HashMap, time::Instant };
use serde::{ Serialize, Deserialize };
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize ) ]
pub enum SentimentCategory
{
VeryPositive,
Positive,
Neutral,
Negative,
VeryNegative,
}
impl SentimentCategory
{
#[ must_use ]
pub fn name( &self ) -> &'static str
{
match self
{
SentimentCategory::VeryPositive => "Very Positive",
SentimentCategory::Positive => "Positive",
SentimentCategory::Neutral => "Neutral",
SentimentCategory::Negative => "Negative",
SentimentCategory::VeryNegative => "Very Negative",
}
}
#[ must_use ]
pub fn score_range( &self ) -> ( f32, f32 )
{
match self
{
SentimentCategory::VeryPositive => ( 0.8, 1.0 ),
SentimentCategory::Positive => ( 0.6, 0.8 ),
SentimentCategory::Neutral => ( 0.4, 0.6 ),
SentimentCategory::Negative => ( 0.2, 0.4 ),
SentimentCategory::VeryNegative => ( 0.0, 0.2 ),
}
}
#[ must_use ]
pub fn polarity( &self ) -> f32
{
match self
{
SentimentCategory::VeryPositive => 1.0,
SentimentCategory::Positive => 0.5,
SentimentCategory::Neutral => 0.0,
SentimentCategory::Negative => -0.5,
SentimentCategory::VeryNegative => -1.0,
}
}
#[ must_use ]
pub fn preferred_model() -> &'static str
{
"cardiffnlp/twitter-roberta-base-sentiment-latest"
}
#[ must_use ]
pub fn from_score( score : f32 ) -> Self
{
if score >= 0.8 { SentimentCategory::VeryPositive }
else if score >= 0.6 { SentimentCategory::Positive }
else if score >= 0.4 { SentimentCategory::Neutral }
else if score >= 0.2 { SentimentCategory::Negative }
else { SentimentCategory::VeryNegative }
}
}
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize ) ]
pub enum EmotionalTone
{
Joy,
Sadness,
Anger,
Fear,
Surprise,
Disgust,
Trust,
Anticipation,
}
impl EmotionalTone
{
#[ must_use ]
pub fn name( &self ) -> &'static str
{
match self
{
EmotionalTone::Joy => "Joy",
EmotionalTone::Sadness => "Sadness",
EmotionalTone::Anger => "Anger",
EmotionalTone::Fear => "Fear",
EmotionalTone::Surprise => "Surprise",
EmotionalTone::Disgust => "Disgust",
EmotionalTone::Trust => "Trust",
EmotionalTone::Anticipation => "Anticipation",
}
}
#[ must_use ]
pub fn sentiment_bias( &self ) -> SentimentCategory
{
match self
{
EmotionalTone::Joy => SentimentCategory::VeryPositive,
EmotionalTone::Trust | EmotionalTone::Anticipation => SentimentCategory::Positive,
EmotionalTone::Surprise => SentimentCategory::Neutral,
EmotionalTone::Sadness | EmotionalTone::Fear => SentimentCategory::Negative,
EmotionalTone::Anger | EmotionalTone::Disgust => SentimentCategory::VeryNegative,
}
}
#[ must_use ]
pub fn all_tones() -> Vec< EmotionalTone >
{
vec![
EmotionalTone::Joy, EmotionalTone::Sadness, EmotionalTone::Anger, EmotionalTone::Fear,
EmotionalTone::Surprise, EmotionalTone::Disgust, EmotionalTone::Trust, EmotionalTone::Anticipation
]
}
}
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize ) ]
pub enum ContentCategory
{
Safe,
Questionable,
Harmful,
Spam,
HateSpeech,
Violence,
}
impl ContentCategory
{
#[ must_use ]
pub fn name( &self ) -> &'static str
{
match self
{
ContentCategory::Safe => "Safe",
ContentCategory::Questionable => "Questionable",
ContentCategory::Harmful => "Harmful",
ContentCategory::Spam => "Spam",
ContentCategory::HateSpeech => "Hate Speech",
ContentCategory::Violence => "Violence",
}
}
#[ must_use ]
pub fn severity_level( &self ) -> u8
{
match self
{
ContentCategory::Safe => 1,
ContentCategory::Questionable => 2,
ContentCategory::Spam => 3,
ContentCategory::Harmful => 4,
ContentCategory::HateSpeech | ContentCategory::Violence => 5,
}
}
#[ must_use ]
pub fn should_block( &self ) -> bool
{
matches!( self, ContentCategory::Harmful | ContentCategory::HateSpeech | ContentCategory::Violence )
}
#[ must_use ]
pub fn preferred_model() -> &'static str
{
"unitary/toxic-bert"
}
}
#[ derive( Debug, Clone ) ]
pub struct SentimentResult
{
pub text : String,
pub sentiment : SentimentCategory,
pub confidence : f32,
pub sentiment_score : f32,
pub emotional_tones : Vec< ( EmotionalTone, f32 ) >,
pub content_assessment : ContentModerationResult,
pub processing_time_ms : u64,
}
#[ derive( Debug, Clone ) ]
pub struct ContentModerationResult
{
pub category : ContentCategory,
pub confidence : f32,
pub toxicity_score : f32,
pub flags : Vec< String >,
pub recommendation : ModerationAction,
}
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash ) ]
pub enum ModerationAction
{
Allow,
Review,
Block,
Flag,
}
impl ModerationAction
{
#[ must_use ]
pub fn name( &self ) -> &'static str
{
match self
{
ModerationAction::Allow => "Allow",
ModerationAction::Review => "Review",
ModerationAction::Block => "Block",
ModerationAction::Flag => "Flag",
}
}
#[ must_use ]
pub fn severity( &self ) -> u8
{
match self
{
ModerationAction::Allow => 1,
ModerationAction::Review => 2,
ModerationAction::Flag => 3,
ModerationAction::Block => 4,
}
}
}
#[ derive( Debug, Clone ) ]
pub struct BatchSentimentRequest
{
pub texts : Vec< String >,
pub include_emotional_analysis : bool,
pub include_moderation : bool,
pub batch_options : BatchOptions,
}
#[ derive( Debug, Clone ) ]
pub struct BatchOptions
{
pub max_batch_size : usize,
pub parallel_processing : bool,
pub progress_interval : Option< usize >,
pub confidence_threshold : f32,
}
impl Default for BatchOptions
{
fn default() -> Self
{
Self
{
max_batch_size : 20,
parallel_processing : true,
progress_interval : Some( 10 ),
confidence_threshold : 0.5,
}
}
}
#[ derive( Debug, Clone ) ]
pub struct SentimentStatistics
{
pub total_count : usize,
pub sentiment_distribution : HashMap< SentimentCategory, usize >,
pub average_sentiment_score : f32,
pub sentiment_score_std_dev : f32,
pub top_emotional_tones : Vec< ( EmotionalTone, f32 ) >,
pub moderation_summary : ModerationStatistics,
pub performance_metrics : PerformanceMetrics,
}
#[ derive( Debug, Clone ) ]
pub struct ModerationStatistics
{
pub category_distribution : HashMap< ContentCategory, usize >,
pub average_toxicity_score : f32,
pub blocked_count : usize,
pub flagged_count : usize,
pub common_flags : Vec< ( String, usize ) >,
}
#[ derive( Debug, Clone ) ]
pub struct PerformanceMetrics
{
pub average_processing_time : f64,
pub total_processing_time : u64,
pub throughput : f64,
pub memory_usage_mb : f32,
}
#[ derive( Debug, Clone ) ]
pub struct SentimentAnalysisPlatform
{
client : Client< HuggingFaceEnvironmentImpl >,
config : PlatformConfig,
analysis_history : Vec< SentimentResult >,
performance_stats : PerformanceMetrics,
}
#[ derive( Debug, Clone ) ]
pub struct PlatformConfig
{
pub sentiment_model : String,
pub moderation_model : String,
pub confidence_threshold : f32,
pub enable_emotional_analysis : bool,
pub enable_content_moderation : bool,
pub max_text_length : usize,
}
impl Default for PlatformConfig
{
fn default() -> Self
{
Self
{
sentiment_model : SentimentCategory::preferred_model().to_string(),
moderation_model : ContentCategory::preferred_model().to_string(),
confidence_threshold : 0.6,
enable_emotional_analysis : true,
enable_content_moderation : true,
max_text_length : 512,
}
}
}
impl SentimentAnalysisPlatform
{
#[ must_use ]
pub fn new( client : Client< HuggingFaceEnvironmentImpl > ) -> Self
{
Self
{
client,
config : PlatformConfig::default(),
analysis_history : Vec::new(),
performance_stats : PerformanceMetrics
{
average_processing_time : 0.0,
total_processing_time : 0,
throughput : 0.0,
memory_usage_mb : 0.0,
},
}
}
#[ must_use ]
pub fn with_config( client : Client< HuggingFaceEnvironmentImpl >, config : PlatformConfig ) -> Self
{
Self
{
client,
config,
analysis_history : Vec::new(),
performance_stats : PerformanceMetrics
{
average_processing_time : 0.0,
total_processing_time : 0,
throughput : 0.0,
memory_usage_mb : 0.0,
},
}
}
pub async fn analyze_sentiment( &mut self, text : &str ) -> Result< SentimentResult, Box< dyn std::error::Error > >
{
let start_time = Instant::now();
if text.len() > self.config.max_text_length
{
return Err( format!( "Text length {} exceeds maximum {}", text.len(), self.config.max_text_length ).into() );
}
let sentiment_prompt = Self::build_sentiment_prompt( text );
let params = InferenceParameters::new()
.with_max_new_tokens( 50 )
.with_temperature( 0.1 ) .with_top_p( 0.8 );
let response = self.client.inference().create_with_parameters(
&sentiment_prompt,
&self.config.sentiment_model,
params
).await?;
let sentiment_text = response.extract_text_or_default( "neutral" );
#[ allow( clippy::cast_possible_truncation ) ]
let processing_time = start_time.elapsed().as_millis() as u64;
let ( sentiment, confidence, sentiment_score ) = Self::parse_sentiment_response( &sentiment_text );
let emotional_tones = if self.config.enable_emotional_analysis
{
Self::analyze_emotional_tones( text )
}
else
{
Vec::new()
};
let content_assessment = if self.config.enable_content_moderation
{
Self::moderate_content( text )
}
else
{
ContentModerationResult
{
category : ContentCategory::Safe,
confidence : 1.0,
toxicity_score : 0.0,
flags : Vec::new(),
recommendation : ModerationAction::Allow,
}
};
let result = SentimentResult
{
text : text.to_string(),
sentiment,
confidence,
sentiment_score,
emotional_tones,
content_assessment,
processing_time_ms : processing_time,
};
self.update_performance_stats( processing_time );
self.analysis_history.push( result.clone() );
Ok( result )
}
pub async fn analyze_batch( &mut self, request : &BatchSentimentRequest ) -> Result< Vec< Result< SentimentResult, Box< dyn std::error::Error > > >, Box< dyn std::error::Error > >
{
let mut results = Vec::new();
let batch_size = request.batch_options.max_batch_size.min( request.texts.len() );
for ( chunk_idx, chunk ) in request.texts.chunks( batch_size ).enumerate()
{
let mut chunk_results = Vec::new();
if request.batch_options.parallel_processing
{
for text in chunk
{
let result = self.analyze_sentiment( text ).await;
chunk_results.push( result );
}
}
else
{
for text in chunk
{
let result = self.analyze_sentiment( text ).await;
chunk_results.push( result );
}
}
if let Some( interval ) = request.batch_options.progress_interval
{
if ( chunk_idx + 1 ) % interval == 0
{
println!( "Processed {chunkplus} batches of {batch_size} texts", chunkplus = chunk_idx + 1 );
}
}
results.extend( chunk_results );
}
Ok( results )
}
#[ must_use ]
pub fn generate_statistics( &self ) -> SentimentStatistics
{
let mut sentiment_distribution = HashMap::new();
let mut total_sentiment_score = 0.0;
let mut total_toxicity_score = 0.0;
let mut emotional_tone_counts : HashMap< EmotionalTone, f32 > = HashMap::new();
let mut category_distribution = HashMap::new();
let mut flag_counts : HashMap< String, usize > = HashMap::new();
let mut blocked_count = 0;
let mut flagged_count = 0;
for result in &self.analysis_history
{
*sentiment_distribution.entry( result.sentiment ).or_insert( 0 ) += 1;
total_sentiment_score += result.sentiment_score;
for ( tone, intensity ) in &result.emotional_tones
{
*emotional_tone_counts.entry( *tone ).or_insert( 0.0 ) += intensity;
}
*category_distribution.entry( result.content_assessment.category ).or_insert( 0 ) += 1;
total_toxicity_score += result.content_assessment.toxicity_score;
for flag in &result.content_assessment.flags
{
*flag_counts.entry( flag.clone() ).or_insert( 0 ) += 1;
}
if result.content_assessment.recommendation == ModerationAction::Block
{
blocked_count += 1;
}
if result.content_assessment.recommendation == ModerationAction::Flag
{
flagged_count += 1;
}
}
let total_count = self.analysis_history.len();
let average_sentiment_score = if total_count > 0 { total_sentiment_score / total_count as f32 } else { 0.0 };
let average_toxicity_score = if total_count > 0 { total_toxicity_score / total_count as f32 } else { 0.0 };
let sentiment_score_std_dev = if total_count > 1
{
let variance = self.analysis_history.iter()
.map( | result | ( result.sentiment_score - average_sentiment_score ).powi( 2 ) )
.sum::< f32 >() / ( total_count - 1 ) as f32;
variance.sqrt()
}
else
{
0.0
};
let mut top_emotional_tones : Vec< ( EmotionalTone, f32 ) > = emotional_tone_counts.into_iter().collect();
top_emotional_tones.sort_by( | a, b | b.1.partial_cmp( &a.1 ).unwrap_or( core::cmp::Ordering::Equal ) );
top_emotional_tones.truncate( 5 );
let mut common_flags : Vec< ( String, usize ) > = flag_counts.into_iter().collect();
common_flags.sort_by_key( | ( _, count ) | core::cmp::Reverse( *count ) );
common_flags.truncate( 5 );
SentimentStatistics
{
total_count,
sentiment_distribution,
average_sentiment_score,
sentiment_score_std_dev,
top_emotional_tones,
moderation_summary : ModerationStatistics
{
category_distribution,
average_toxicity_score,
blocked_count,
flagged_count,
common_flags,
},
performance_metrics : self.performance_stats.clone(),
}
}
pub fn clear_history( &mut self )
{
self.analysis_history.clear();
self.performance_stats = PerformanceMetrics
{
average_processing_time : 0.0,
total_processing_time : 0,
throughput : 0.0,
memory_usage_mb : 0.0,
};
}
#[ must_use ]
pub fn get_history( &self ) -> &Vec< SentimentResult >
{
&self.analysis_history
}
fn build_sentiment_prompt( text : &str ) -> String
{
format!(
"Analyze the sentiment of the following text and classify it as very positive, positive, neutral, negative, or very negative:\n\nText : {text}\n\nSentiment:"
)
}
fn parse_sentiment_response( response : &str ) -> ( SentimentCategory, f32, f32 )
{
let response_lower = response.trim().to_lowercase();
let ( sentiment, base_confidence ) = if response_lower.contains( "very positive" ) || response_lower.contains( "excellent" ) || response_lower.contains( "amazing" )
{
( SentimentCategory::VeryPositive, 0.9 )
}
else if response_lower.contains( "positive" ) || response_lower.contains( "good" ) || response_lower.contains( "nice" )
{
( SentimentCategory::Positive, 0.8 )
}
else if response_lower.contains( "neutral" ) || response_lower.contains( "okay" )
{
( SentimentCategory::Neutral, 0.7 )
}
else if response_lower.contains( "negative" ) || response_lower.contains( "bad" )
{
( SentimentCategory::Negative, 0.8 )
}
else if response_lower.contains( "very negative" ) || response_lower.contains( "terrible" ) || response_lower.contains( "awful" )
{
( SentimentCategory::VeryNegative, 0.9 )
}
else
{
( SentimentCategory::Neutral, 0.5 )
};
let confidence = ( base_confidence * ( 0.8 + ( response.len() as f32 / 100.0 ).min( 0.2 ) ) ).min( 1.0 );
let sentiment_score = sentiment.polarity() * 0.5 + 0.5;
( sentiment, confidence, sentiment_score )
}
fn analyze_emotional_tones( text : &str ) -> Vec< ( EmotionalTone, f32 ) >
{
let mut tones = Vec::new();
let text_lower = text.to_lowercase();
if text_lower.contains( "happy" ) || text_lower.contains( "joy" ) || text_lower.contains( "excited" ) || text_lower.contains( "wonderful" )
{
tones.push( ( EmotionalTone::Joy, 0.8 ) );
}
if text_lower.contains( "sad" ) || text_lower.contains( "depressed" ) || text_lower.contains( "disappointed" )
{
tones.push( ( EmotionalTone::Sadness, 0.7 ) );
}
if text_lower.contains( "angry" ) || text_lower.contains( "furious" ) || text_lower.contains( "hate" )
{
tones.push( ( EmotionalTone::Anger, 0.8 ) );
}
if text_lower.contains( "scared" ) || text_lower.contains( "afraid" ) || text_lower.contains( "worried" )
{
tones.push( ( EmotionalTone::Fear, 0.7 ) );
}
if tones.is_empty()
{
tones.push( ( EmotionalTone::Trust, 0.3 ) );
}
tones
}
fn moderate_content( text : &str ) -> ContentModerationResult
{
let text_lower = text.to_lowercase();
let mut flags = Vec::new();
let mut toxicity_score : f32 = 0.0;
if text_lower.contains( "hate" ) || text_lower.contains( "stupid" ) || text_lower.contains( "idiot" )
{
flags.push( "potential_hate_speech".to_string() );
toxicity_score += 0.3;
}
if text_lower.contains( "kill" ) || text_lower.contains( "violence" ) || text_lower.contains( "hurt" )
{
flags.push( "violence_threat".to_string() );
toxicity_score += 0.4;
}
if text_lower.contains( "spam" ) || text_lower.contains( "click here" ) || text_lower.contains( "buy now" )
{
flags.push( "promotional_content".to_string() );
toxicity_score += 0.2;
}
let ( category, recommendation ) = if toxicity_score >= 0.7
{
if flags.iter().any( | f | f.contains( "violence" ) )
{
( ContentCategory::Violence, ModerationAction::Block )
}
else if flags.iter().any( | f | f.contains( "hate" ) )
{
( ContentCategory::HateSpeech, ModerationAction::Block )
}
else
{
( ContentCategory::Harmful, ModerationAction::Flag )
}
}
else if toxicity_score >= 0.4
{
( ContentCategory::Questionable, ModerationAction::Review )
}
else if toxicity_score >= 0.2
{
if flags.iter().any( | f | f.contains( "promotional" ) )
{
( ContentCategory::Spam, ModerationAction::Review )
}
else
{
( ContentCategory::Questionable, ModerationAction::Allow )
}
}
else
{
( ContentCategory::Safe, ModerationAction::Allow )
};
let confidence = if flags.is_empty() { 0.9 } else { 0.7 + ( flags.len() as f32 * 0.1 ).min( 0.2 ) };
ContentModerationResult
{
category,
confidence,
toxicity_score : toxicity_score.min( 1.0 ),
flags,
recommendation,
}
}
fn update_performance_stats( &mut self, processing_time : u64 )
{
let history_count = self.analysis_history.len() as u64;
self.performance_stats.total_processing_time += processing_time;
self.performance_stats.average_processing_time =
self.performance_stats.total_processing_time as f64 / ( history_count + 1 ) as f64;
if self.performance_stats.total_processing_time > 0
{
self.performance_stats.throughput =
( history_count + 1 ) as f64 * 1000.0 / self.performance_stats.total_processing_time as f64;
}
self.performance_stats.memory_usage_mb = ( history_count + 1 ) as f32 * 0.1; }
}
fn create_test_client() -> Option< Client< HuggingFaceEnvironmentImpl > >
{
let api_key = super::get_api_key_for_testing()?;
let secret = Secret::new( api_key );
let env = HuggingFaceEnvironmentImpl::build( secret, None ).ok()?;
Client::build( env ).ok()
}
fn create_sample_texts() -> Vec< String >
{
vec![
"I absolutely love this product! It's amazing and works perfectly.".to_string(),
"This is okay, nothing special but not bad either.".to_string(),
"I hate this so much, it's terrible and doesn't work at all.".to_string(),
"The weather is nice today.".to_string(),
"I'm so excited about the upcoming vacation!".to_string(),
"I'm really worried about the exam tomorrow.".to_string(),
"This movie was incredibly boring and disappointing.".to_string(),
"Thank you so much for your help, I really appreciate it.".to_string(),
"Click here to buy now! Amazing deal, don't miss out!".to_string(),
"I'm feeling sad and depressed lately.".to_string(),
]
}