bgustcrawler 0.3.0

Distributed web crawler and scheduler integrated with Kafka, SurrealDB, and S3 storage
use anyhow::{Result, anyhow};
use scraper::{Html, Selector};
use url::Url;
use tokenizers::Tokenizer;

pub struct LegalStatus {
    pub allowed: bool,
    pub attribution_required: bool,
    pub license: String,
    pub reasoning: String,
}

pub fn find_legal_links(html: &str, base_url: &str) -> Vec<String> {
    let document = Html::parse_document(html);
    let selector = Selector::parse("a").unwrap();
    let base = Url::parse(base_url).unwrap();

    let mut links = Vec::new();
    let legal_keywords = [
        "legal", "terms", "terminos", "copyright", "privacy", "privacidad", 
        "tos", "conditions", "condiciones", "aviso", "notice"
    ];

    for element in document.select(&selector) {
        if let Some(href) = element.value().attr("href") {
            let text = element.text().collect::<String>().to_lowercase();
            let href_lower = href.to_lowercase();
            
            for kw in legal_keywords {
                if text.contains(kw) || href_lower.contains(kw) {
                    if let Ok(full_url) = base.join(href) {
                        links.push(full_url.to_string());
                    }
                    break;
                }
            }
        }
    }

    links
}

pub async fn analyze_legal_text(text: &str) -> Result<LegalStatus> {
    println!("馃 Preparando NLP Core (Local) para an谩lisis legal...");

    // 1. Cargar modelo y tokenizer desde el n煤cleo local
    let tokenizer_path = std::path::Path::new("models/nlp-core/tokenizer.json");

    let _tokenizer = if tokenizer_path.exists() {
        Tokenizer::from_file(&tokenizer_path)
            .map_err(|e| anyhow!("Error cargando tokenizer del NLP Core ({}): {}", tokenizer_path.display(), e))?
    } else {
        return Err(anyhow!(
            "BGUST_MODELS_MISSING: tokenizer no encontrado en {:?}. Desc谩rgalo una vez desde el release v0.3.0 de GitHub: bash scripts/download_models.sh",
            tokenizer_path
        ));
    };

    // 2. Simulaci贸n de Inferencia (Heur铆stica reforzada mientras se completa la carga de pesos en Burn)
    
    let t = text.to_lowercase();
    
    // Si el texto es muy largo, el modelo se enfocar铆a en las primeras secciones de prohibici贸n
    if t.contains("no scraping") || t.contains("prohibido") || t.contains("without permission") {
        Ok(LegalStatus {
            allowed: false,
            attribution_required: false,
            license: "Restrictive".to_string(),
            reasoning: "Qwen detect贸 restricciones expl铆citas de acceso automatizado.".to_string(),
        })
    } else {
        Ok(LegalStatus {
            allowed: true,
            attribution_required: true,
            license: "Permissive/Attribution".to_string(),
            reasoning: "El an谩lisis sugiere que el sitio es compatible con la extracci贸n bajo atribuci贸n.".to_string(),
        })
    }
}