| from __future__ import annotations |
|
|
| """ |
| Properly enrich Appell formulas with scent profiles by: |
| 1. Extracting full odor paragraphs from Arctander monographs for single chemicals. |
| 2. Mapping natural oils and absolutes to standard scent profiles. |
| 3. Aggregating component profiles weighted by formula amount. |
| 4. Curating high-quality profiles for the famous named fragrances. |
| """ |
|
|
| import json |
| import re |
| from pathlib import Path |
| from collections import Counter, defaultdict |
| from rapidfuzz import process, fuzz |
|
|
| import sys |
| sys.path.insert(0, "/home/hermes/pino/src") |
| from pino.trade_name_resolver import TradeNameResolver |
|
|
|
|
| |
| NATURAL_SCENT_PROFILES: dict[str, str] = { |
| "bergamot": "citrus, fresh, green, floral, light, spicy, elegant, tea-like", |
| "bergamot oil": "citrus, fresh, green, floral, light, spicy, elegant, tea-like", |
| "orange oil": "citrus, sweet, fresh, fruity, lively, orange", |
| "sweet orange oil": "citrus, sweet, fresh, fruity, lively, orange", |
| "lemon oil": "citrus, fresh, sharp, clean, zesty, lemon", |
| "mandarin oil": "citrus, sweet, fresh, fruity, soft, tangerine", |
| "lime oil": "citrus, fresh, sharp, green, tart", |
| "petitgrain": "green, woody, citrus, floral, bitter orange, fresh", |
| "neroli oil": "citrus, floral, green, orange blossom, fresh, delicate", |
| "orange blossom": "floral, citrus, green, neroli, fresh, sweet", |
| "fleur d'oranger": "floral, citrus, green, neroli, fresh, sweet", |
| "jasmine absolute": "white floral, narcotic, indolic, sweet, exotic, rich", |
| "jasmin absolute": "white floral, narcotic, indolic, sweet, exotic, rich", |
| "jasmine": "white floral, narcotic, indolic, sweet, exotic, rich", |
| "jasmin": "white floral, narcotic, indolic, sweet, exotic, rich", |
| "rose absolute": "floral, rose, honey, spicy, powdery, sweet, rich", |
| "rose bulgarian otto": "floral, rose, honey, sweet, rich, warm", |
| "rose": "floral, rose, honey, powdery, sweet, warm", |
| "ylang-ylang oil": "white floral, sweet, spicy, creamy, tropical, narcotic", |
| "ylang-ylang": "white floral, sweet, spicy, creamy, tropical, narcotic", |
| "sandalwood oil": "woody, creamy, milky, sweet, soft, balsamic, oriental", |
| "sandalwood": "woody, creamy, milky, sweet, soft, balsamic, oriental", |
| "cedarwood oil": "woody, dry, pencil-shavings, resinous, masculine", |
| "cedarwood": "woody, dry, pencil-shavings, resinous, masculine", |
| "patchouli oil": "earthy, woody, camphoraceous, sweet, balsamic, dark", |
| "patchouli": "earthy, woody, camphoraceous, sweet, balsamic, dark", |
| "vetiver oil": "earthy, woody, smoky, green, dry, rooty, complex", |
| "vetiver": "earthy, woody, smoky, green, dry, rooty, complex", |
| "vetiveryl acetate": "woody, earthy, smoky, dry, sweet, vetiver, rooty", |
| "vetiverol": "woody, earthy, smoky, green, dry, vetiver, rooty", |
| "labdanum resinoid": "amber, leathery, balsamic, sweet, warm, resinous", |
| "labdanum absolute": "amber, leathery, balsamic, sweet, warm, resinous", |
| "labdanum": "amber, leathery, balsamic, sweet, warm, resinous", |
| "galbanum oil": "green, earthy, woody, balsamic, harsh, sharp, leafy", |
| "galbanum resinoid": "green, earthy, woody, balsamic, harsh, sharp, leafy", |
| "galbanum": "green, earthy, woody, balsamic, harsh, sharp, leafy", |
| "styrax resinoid": "balsamic, sweet, vanilla-like, resinous, warm, powdery", |
| "styrax": "balsamic, sweet, vanilla-like, resinous, warm, powdery", |
| "benzoin resinoid": "balsamic, sweet, vanilla, warm, resinous, powdery", |
| "benzoin": "balsamic, sweet, vanilla, warm, resinous, powdery", |
| "tolu resinoid": "balsamic, sweet, vanilla, resinous, warm, honeyed", |
| "peru balsam": "balsamic, sweet, vanilla, resinous, warm, honeyed", |
| "opoponax resinoid": "balsamic, sweet, resinous, warm, spicy, medicinal", |
| "myrrh resinoid": "balsamic, resinous, medicinal, earthy, smoky, warm", |
| "myrrh": "balsamic, resinous, medicinal, earthy, smoky, warm", |
| "olibanum resinoid": "balsamic, incense, resinous, woody, citrus, warm", |
| "olibanum": "balsamic, incense, resinous, woody, citrus, warm", |
| "frankincense": "balsamic, incense, resinous, woody, citrus, warm", |
| "oakmoss": "earthy, woody, mossy, green, damp, leathery, chypre", |
| "mousse de chene": "earthy, woody, mossy, green, damp, leathery, chypre", |
| "tonka resinoid": "sweet, vanilla, almond, tobacco, hay, coumarin, warm", |
| "tonka": "sweet, vanilla, almond, tobacco, hay, coumarin, warm", |
| "vanilla": "sweet, vanilla, creamy, balsamic, warm, comforting", |
| "vanilla tincture": "sweet, vanilla, creamy, balsamic, warm, comforting", |
| "civet tincture": "animalic, musky, fecal, warm, sensual, fixative", |
| "civet absolute": "animalic, musky, fecal, warm, sensual, fixative", |
| "civet": "animalic, musky, fecal, warm, sensual, fixative", |
| "musk tincture": "animalic, musky, warm, powdery, sensual, fixative", |
| "musk ambrette": "musky, powdery, sweet, floral, animalic, warm", |
| "musk ketone": "musky, powdery, sweet, floral, warm, fixative", |
| "musk xylene": "musky, powdery, sweet, woody, warm, fixative", |
| "musc brassylate": "musky, sweet, powdery, floral, warm, fixative", |
| "ambrette seed": "musky, floral, sweet, powdery, wine-like, warm", |
| "cinnamon oil": "spicy, warm, sweet, woody, cinnamic, oriental", |
| "cinnamon ceylon": "spicy, warm, sweet, woody, cinnamic, oriental", |
| "cassia oil": "spicy, warm, sweet, cinnamic, pungent, oriental", |
| "clove oil": "spicy, warm, sweet, eugenol, clove, oriental", |
| "clove bud oil": "spicy, warm, sweet, eugenol, clove, oriental", |
| "nutmeg oil": "spicy, warm, sweet, woody, fresh, balsamic", |
| "pimento": "spicy, warm, sweet, clove-like, peppery, oriental", |
| "cardamom oil": "spicy, warm, aromatic, camphoraceous, fresh, sweet", |
| "pepper black": "spicy, warm, dry, pungent, fresh, woody", |
| "coriander": "spicy, fresh, green, citrus, woody, aromatic", |
| "ginger": "spicy, fresh, warm, citrus, woody, pungent", |
| "sage clary oil": "herbal, green, earthy, musky, floral, wine-like", |
| "clary sage": "herbal, green, earthy, musky, floral, wine-like", |
| "lavender oil": "herbal, floral, fresh, clean, woody, aromatic", |
| "lavender": "herbal, floral, fresh, clean, woody, aromatic", |
| "rosemary oil": "herbal, camphoraceous, fresh, woody, medicinal", |
| "geranium oil": "floral, green, rosy, minty, spicy, fresh", |
| "geranium": "floral, green, rosy, minty, spicy, fresh", |
| "rose geranium": "floral, green, rosy, minty, spicy, fresh", |
| "rose geranium oil": "floral, green, rosy, minty, spicy, fresh", |
| "eucalyptus oil": "camphoraceous, fresh, medicinal, woody, green", |
| "peppermint oil": "minty, fresh, cool, camphoraceous, sweet, green", |
| "spearmint oil": "minty, fresh, sweet, green, cool, herbaceous", |
| "anise oil": "anise, sweet, licorice, herbal, warm, spicy", |
| "fennel sweet": "anise, sweet, licorice, herbal, warm, earthy", |
| "bitter almond": "almond, cherry, marzipan, nutty, sweet, aromatic", |
| "birch tar": "smoky, leather, tar, woody, phenolic, dark", |
| "cade oil": "smoky, tar, leather, woody, medicinal, dark", |
| "guaiac wood": "woody, smoky, sweet, rose-like, balsamic, leathery", |
| "guaiacwood oil": "woody, smoky, sweet, rose-like, balsamic, leathery", |
| "costus oil": "animalic, musky, earthy, rooty, woody, iris-like", |
| "orris concrete": "powdery, violet, iris, woody, floral, earthy", |
| "orris": "powdery, violet, iris, woody, floral, earthy", |
| "tuberose absolute": "white floral, creamy, narcotic, sweet, heavy, exotic", |
| "tuberose": "white floral, creamy, narcotic, sweet, heavy, exotic", |
| "carnation": "spicy, floral, clove, sweet, eugenol, warm", |
| "dianthus": "spicy, floral, clove, sweet, warm, pink", |
| "hyacinth": "green floral, sweet, green, narcotic, fresh, watery", |
| "lilac": "floral, green, sweet, honey, anisic, powdery", |
| "muguet": "green floral, fresh, clean, soapy, lily-of-the-valley, dewy", |
| "narcissus": "green floral, sweet, hay-like, earthy, yellow floral", |
| "acacia": "floral, sweet, honey, powdery, mimosa, yellow", |
| "mimosa": "floral, sweet, powdery, honey, almond, yellow", |
| "cassie absolute": "floral, sweet, powdery, honey, almond, mimosa", |
| "hawthorn": "floral, sweet, almond, powdery, green, spring", |
| "sweet pea": "floral, sweet, powdery, green, delicate, spring", |
| "trefle": "green, floral, clover, sweet, fresh, spring", |
| "heliotrope": "sweet, almond, vanilla, powdery, floral, cherry", |
| "orange flower": "floral, citrus, neroli, fresh, sweet, orange", |
| "gardenia": "white floral, creamy, sweet, green, exotic, heady", |
| "honeysuckle": "floral, sweet, honey, green, fresh, spring", |
| "magnolia": "floral, citrus, sweet, green, lemony, fresh", |
| "pine": "woody, resinous, green, balsamic, fresh, forest", |
| "pine needle siberian": "woody, resinous, green, balsamic, fresh, forest", |
| "fir needle siberian": "woody, resinous, green, balsamic, fresh, forest", |
| "spruce": "woody, resinous, green, balsamic, fresh, forest", |
| "cade": "smoky, tar, woody, medicinal, dark, leathery", |
| "pine canad.": "woody, resinous, green, balsamic, fresh, forest", |
| "rose w": "floral, rose, honey, sweet, powdery, warm", |
| } |
|
|
|
|
| STOPWORDS = { |
| "the", "and", "or", "a", "an", "of", "to", "in", "with", "for", "is", "it", "its", "this", "that", "these", "those", |
| "on", "at", "by", "from", "as", "into", "through", "during", "before", "after", "above", "below", "up", "down", |
| "out", "off", "over", "under", "again", "further", "then", "once", "here", "there", "when", "where", "why", "how", |
| "all", "each", "few", "more", "most", "other", "some", "such", "no", "nor", "not", "only", "own", "same", "so", |
| "than", "too", "very", "can", "will", "just", "should", "now", "also", "may", "often", "occasionally", "sometimes", |
| "used", "use", "using", "uses", "perfume", "perfumes", "perfumery", "fragrance", "fragrances", "flavor", "flavors", |
| "odor", "odour", "smell", "aroma", "scent", "note", "notes", "material", "materials", "product", "products", |
| "chemical", "compound", "substance", "preparation", "concentration", "dilution", "pure", "commercial", "technical", |
| "almost", "virtually", "faint", "slightly", "strong", "strongly", "moderate", "moderately", "weak", "intense", |
| "pleasant", "unpleasant", "fine", "good", "poor", "rich", "delicate", "powerful", "soft", "warm", "fresh", "dry", |
| "sweet", "bitter", "sour", "sharp", "pungent", "acrid", "bland", "light", "heavy", "high", "low", "highly", "mostly", |
| "approximately", "about", "less", "more", "much", "many", "one", "two", "three", "several", "various", "etc", "e", |
| "g", "i", "e.g", "etc.", "when", "then", "there", "here", "where", "which", "while", "whereas", "although", "because", |
| "since", "unless", "until", "whether", "however", "therefore", "thus", "hence", "moreover", "furthermore", |
| "nevertheless", "nonetheless", "whereas", "similarly", "conversely", "accordingly", "consequently", "due", "owing", |
| "resulting", "result", "results", "caused", "causing", "cause", "causes", "made", "make", "makes", "making", "being", |
| "been", "have", "has", "had", "do", "does", "did", "done", "doing", "be", "are", "was", "were", "am", "is", "being", |
| "having", "get", "gets", "got", "gotten", "getting", "become", "becomes", "became", "becoming", "seem", "seems", |
| "seemed", "seeming", "appear", "appears", "appeared", "appearing", "remain", "remains", "remained", "remaining", |
| "turn", "turns", "turned", "turning", "come", "comes", "came", "coming", "go", "goes", "went", "going", "take", "takes", |
| "took", "taken", "taking", "give", "gives", "gave", "given", "giving", "put", "puts", "putting", "set", "sets", "setting", |
| "say", "says", "said", "saying", "see", "sees", "saw", "seen", "seeing", "know", "knows", "knew", "known", "knowing", |
| "think", "thinks", "thought", "thinking", "want", "wants", "wanted", "wanting", "like", "likes", "liked", "liking", |
| "look", "looks", "looked", "looking", "use", "uses", "used", "using", "find", "finds", "found", "finding", "tell", |
| "tells", "told", "telling", "ask", "asks", "asked", "asking", "seem", "seemed", "seems", "seeming", "feel", "feels", |
| "felt", "feeling", "try", "tries", "tried", "trying", "leave", "leaves", "left", "leaving", "call", "calls", "called", |
| "calling", "keep", "keeps", "kept", "keeping", "let", "lets", "letting", "begin", "begins", "began", "begun", "beginning", |
| "help", "helps", "helped", "helping", "show", "shows", "showed", "shown", "showing", "hear", "hears", "heard", "hearing", |
| "play", "plays", "played", "playing", "run", "runs", "ran", "running", "move", "moves", "moved", "moving", "live", |
| "lives", "lived", "living", "believe", "believes", "believed", "believing", "bring", "brings", "brought", "bringing", |
| "happen", "happens", "happened", "happening", "stand", "stands", "stood", "standing", "lose", "loses", "lost", "losing", |
| "pay", "pays", "paid", "paying", "meet", "meets", "met", "meeting", "include", "includes", "included", "including", |
| "continue", "continues", "continued", "continuing", "follow", "follows", "followed", "following", "stop", "stops", |
| "stopped", "stopping", "create", "creates", "created", "creating", "speak", "speaks", "spoke", "spoken", "speaking", |
| "read", "reads", "reading", "allow", "allows", "allowed", "allowing", "add", "adds", "added", "adding", "spend", |
| "spends", "spent", "spending", "grow", "grows", "grew", "grown", "growing", "open", "opens", "opened", "opening", |
| "walk", "walks", "walked", "walking", "win", "wins", "won", "winning", "offer", "offers", "offered", "offering", "remember", |
| "remembers", "remembered", "remembering", "love", "loves", "loved", "loving", "consider", "considers", "considered", |
| "considering", "appear", "appears", "appeared", "appearing", "buy", "buys", "bought", "buying", "wait", "waits", |
| "waited", "waiting", "serve", "serves", "served", "serving", "die", "dies", "died", "dying", "send", "sends", "sent", |
| "sending", "expect", "expects", "expected", "expecting", "build", "builds", "built", "building", "stay", "stays", |
| "stayed", "staying", "fall", "falls", "fell", "fallen", "falling", "cut", "cuts", "cutting", "reach", "reaches", "reached", |
| "reaching", "kill", "kills", "killed", "killing", "remain", "remains", "remained", "remaining", "suggest", "suggests", |
| "suggested", "suggesting", "raise", "raises", "raised", "raising", "pass", "passes", "passed", "passing", "sell", |
| "sells", "sold", "selling", "require", "requires", "required", "requiring", "report", "reports", "reported", "reporting", |
| "decide", "decides", "decided", "deciding", "pull", "pulls", "pulled", "pulling", "improve", "improves", "improved", |
| "improving", "contain", "contains", "contained", "containing", "develop", "develops", "developed", "developing", |
| "information", "description", "details", "features", "characteristics", "properties", "quality", "qualities", "type", |
| "types", "kind", "kinds", "form", "forms", "part", "parts", "piece", "pieces", "example", "examples", "case", "cases", |
| "point", "points", "place", "places", "way", "ways", "thing", "things", "people", "person", "persons", "man", "men", |
| "woman", "women", "group", "groups", "number", "numbers", "lot", "lots", "amount", "amounts", "bit", "bits", "part", |
| "parts", "side", "sides", "area", "areas", "kind", "kinds", "sort", "sorts", "range", "ranges", "level", "levels", "degree", |
| "degrees", "rate", "rates", "period", "periods", "time", "times", "year", "years", "month", "months", "day", "days", "week", |
| "weeks", "hour", "hours", "minute", "minutes", "second", "seconds", "long", "short", "large", "small", "big", "huge", |
| "tiny", "major", "minor", "main", "primary", "secondary", "initial", "final", "last", "first", "next", "previous", "early", |
| "late", "old", "new", "young", "recent", "current", "modern", "ancient", "future", "past", "present", "general", "specific", |
| "particular", "special", "certain", "sure", "true", "false", "right", "wrong", "correct", "incorrect", "possible", |
| "impossible", "probable", "likely", "unlikely", "available", "unavailable", "common", "rare", "usual", "unusual", "normal", |
| "abnormal", "typical", "atypical", "regular", "irregular", "consistent", "inconsistent", "constant", "variable", |
| "direct", "indirect", "obvious", "clear", "unclear", "apparent", "evident", "hidden", "visible", "invisible", "open", |
| "closed", "complete", "incomplete", "full", "empty", "whole", "partial", "total", "absolute", "relative", "positive", |
| "negative", "active", "passive", "actual", "potential", "real", "ideal", "theoretical", "practical", "useful", "useless", |
| "effective", "ineffective", "efficient", "inefficient", "sufficient", "insufficient", "adequate", "inadequate", "appropriate", |
| "inappropriate", "proper", "improper", "suitable", "unsuitable", "fit", "unfit", "ready", "unready", "prepared", "unprepared", |
| "aware", "unaware", "conscious", "unconscious", "familiar", "unfamiliar", "popular", "unpopular", "famous", "unknown", |
| "known", "unknown", "similar", "different", "same", "opposite", "various", "diverse", "uniform", "mixed", "pure", |
| "impure", "clean", "dirty", "fresh", "stale", "natural", "artificial", "synthetic", "organic", "inorganic", "physical", |
| "chemical", "mental", "emotional", "spiritual", "social", "political", "economic", "financial", "commercial", "industrial", |
| "medical", "scientific", "technical", "technological", "environmental", "educational", "cultural", "historical", |
| "geographical", "mathematical", "statistical", "logical", "legal", "moral", "ethical", "religious", "philosophical", |
| "psychological", "sociological", "biological", "ecological", "geological", "meteorological", "astronomical", "literary", |
| "artistic", "musical", "dramatic", "poetic", "rhetorical", "linguistic", "grammatical", "phonetic", "semantic", "pragmatic", |
| "syntactic", "morphological", "lexical", "vocabular", "idiomatic", "colloquial", "formal", "informal", "written", "spoken", |
| "oral", "verbal", "nonverbal", "visual", "auditory", "tactile", "olfactory", "gustatory", "kinesthetic", "proprioceptive", |
| "interoceptive", "exteroceptive", "perceptual", "cognitive", "affective", "conative", "behavioral", "operational", |
| "functional", "structural", "systemic", "systematic", "methodical", "methodological", "procedural", "processual", |
| "sequential", "chronological", "temporal", "spatial", "dimensional", "quantitative", "qualitative", "objective", "subjective", |
| "personal", "impersonal", "individual", "collective", "public", "private", "internal", "external", "inner", "outer", |
| "central", "peripheral", "local", "global", "regional", "national", "international", "universal", "particular", "general", |
| "broad", "narrow", "wide", "deep", "shallow", "high", "low", "tall", "short", "thick", "thin", "fat", "slim", "heavy", "light", |
| "hard", "soft", "firm", "loose", "tight", "rough", "smooth", "sharp", "blunt", "pointed", "round", "square", "flat", "curved", |
| "straight", "crooked", "bent", "twisted", "solid", "liquid", "gaseous", "fluid", "viscous", "elastic", "plastic", "rigid", |
| "flexible", "stiff", "brittle", "fragile", "strong", "weak", "tough", "tender", "hardy", "delicate", "durable", "permanent", |
| "temporary", "stable", "unstable", "steady", "unsteady", "fixed", "mobile", "moving", "static", "dynamic", "kinetic", |
| "potential", "mechanical", "electrical", "magnetic", "thermal", "optical", "acoustic", "sonic", "ultrasonic", "hydraulic", |
| "pneumatic", "aerodynamic", "hydrodynamic", "thermodynamic", "electromagnetic", "radioactive", "nuclear", "atomic", |
| "molecular", "cellular", "genetic", "hereditary", "evolutionary", "developmental", "growth", "development", "reproduction", |
| "reproductive", "sexual", "asexual", "vegetative", "organic", "biological", "biochemical", "biomedical", "biotechnological", |
| "microbiological", "virological", "bacteriological", "immunological", "pathological", "pathological", "toxicological", |
| "pharmacological", "pharmaceutical", "medicinal", "therapeutic", "curative", "healing", "remedial", "restorative", |
| "rehabilitative", "palliative", "preventive", "prophylactic", "diagnostic", "clinical", "surgical", "medical", "dental", |
| "optical", "visual", "auditory", "hearing", "speech", "language", "cognitive", "neurological", "psychiatric", "psychological", |
| "mental", "behavioral", "emotional", "mood", "affective", "anxiety", "depression", "stress", "trauma", "disorder", "disease", |
| "illness", "sickness", "condition", "syndrome", "symptom", "sign", "diagnosis", "prognosis", "treatment", "therapy", |
| "medication", "drug", "medicine", "remedy", "cure", "healing", "recovery", "rehabilitation", "care", "healthcare", "nursing", |
| "patient", "doctor", "physician", "surgeon", "nurse", "therapist", "specialist", "expert", "professional", "practitioner", |
| "provider", "clinician", "caregiver", "attendant", "assistant", "helper", "worker", "employee", "employer", "manager", |
| "supervisor", "administrator", "director", "executive", "officer", "official", "leader", "chief", "head", "boss", "owner", |
| "proprietor", "founder", "creator", "maker", "designer", "developer", "engineer", "architect", "planner", "strategist", |
| "consultant", "advisor", "counselor", "coach", "mentor", "teacher", "instructor", "professor", "educator", "trainer", "tutor", |
| "lecturer", "speaker", "presenter", "host", "moderator", "facilitator", "organizer", "coordinator", "arranger", "composer", |
| "author", "writer", "poet", "novelist", "journalist", "reporter", "correspondent", "broadcaster", "anchor", "commentator", |
| "critic", "reviewer", "editor", "publisher", "producer", "director", "filmmaker", "screenwriter", "actor", "actress", "performer", |
| "artist", "musician", "singer", "dancer", "choreographer", "conductor", "composer", "painter", "sculptor", "photographer", |
| "designer", "illustrator", "cartoonist", "animator", "filmmaker", "director", "producer", "screenwriter", "playwright", |
| "dramatist", "theater", "theatre", "stage", "film", "movie", "cinema", "television", "tv", "radio", "broadcast", "media", |
| "news", "press", "journalism", "publication", "publishing", "literature", "literary", "fiction", "nonfiction", "poetry", |
| "prose", "drama", "comedy", "tragedy", "satire", "parody", "irony", "metaphor", "simile", "allegory", "symbolism", "imagery", |
| "alliteration", "assonance", "consonance", "rhyme", "rhythm", "meter", "stanza", "verse", "line", "poem", "poet", "poetry", |
| "sonnet", "ode", "elegy", "lyric", "ballad", "epic", "haiku", "limerick", "free", "blank", "verse", "prose", "narrative", |
| "story", "tale", "fable", "myth", "legend", "saga", "epic", "folklore", "fairy", "folk", "traditional", "oral", "written", |
| "printed", "published", "unpublished", "manuscript", "document", "text", "script", "transcript", "record", "recording", |
| "tape", "disk", "disc", "cd", "dvd", "video", "audio", "sound", "noise", "music", "song", "tune", "melody", "harmony", "rhythm", |
| "beat", "tempo", "pitch", "tone", "timbre", "volume", "loudness", "dynamics", "articulation", "expression", "phrasing", |
| "technique", "style", "genre", "form", "structure", "composition", "arrangement", "orchestration", "performance", |
| "interpretation", "rendition", "version", "recording", "album", "track", "single", "ep", "lp", "record", "vinyl", "cassette", |
| "download", "stream", "streaming", "playlist", "mixtape", "remix", "cover", "original", "cover", "live", "studio", "acoustic", |
| "electric", "electronic", "digital", "analog", "analogue", "synthesized", "sampled", "sequenced", "programmed", "produced", |
| "mastered", "remastered", "engineered", "mixed", "recording", "studio", "session", "concert", "gig", "show", "performance", |
| "tour", "festival", "event", "venue", "auditorium", "theater", "hall", "arena", "stadium", "club", "bar", "pub", "restaurant", |
| "cafe", "cafeteria", "diner", "hotel", "motel", "inn", "hostel", "resort", "spa", "salon", "shop", "store", "market", "mall", |
| "boutique", "supermarket", "grocery", "bakery", "butcher", "deli", "pharmacy", "drugstore", "hardware", "bookstore", "library", |
| "museum", "gallery", "zoo", "park", "garden", "forest", "wood", "jungle", "desert", "beach", "coast", "shore", "sea", "ocean", |
| "lake", "river", "stream", "pond", "pool", "mountain", "hill", "valley", "plain", "plateau", "canyon", "cave", "island", "peninsula", |
| "continent", "country", "nation", "state", "province", "region", "district", "city", "town", "village", "suburb", "neighborhood", |
| "street", "road", "avenue", "boulevard", "lane", "drive", "way", "path", "trail", "route", "highway", "freeway", "motorway", |
| "bridge", "tunnel", "airport", "station", "terminal", "port", "harbor", "dock", "pier", "wharf", "marina", "ship", "boat", "vessel", |
| "yacht", "ferry", "cruise", "liner", "tanker", "cargo", "freight", "container", "train", "rail", "subway", "metro", "tram", |
| "bus", "coach", "taxi", "cab", "uber", "lyft", "car", "automobile", "vehicle", "truck", "van", "suv", "sedan", "coupe", "convertible", |
| "hatchback", "wagon", "minivan", "pickup", "motorcycle", "scooter", "bicycle", "bike", "skateboard", "rollerblade", "skate", |
| "airplane", "plane", "aircraft", "jet", "helicopter", "drone", "spacecraft", "rocket", "satellite", "shuttle", "station", |
| "space", "outer", "universe", "galaxy", "star", "planet", "moon", "sun", "earth", "world", "globe", "sphere", "atmosphere", |
| "climate", "weather", "temperature", "humidity", "pressure", "wind", "rain", "snow", "ice", "frost", "fog", "mist", "cloud", |
| "storm", "thunder", "lightning", "hurricane", "tornado", "typhoon", "cyclone", "earthquake", "volcano", "eruption", "tsunami", |
| "flood", "drought", "famine", "plague", "disease", "epidemic", "pandemic", "infection", "virus", "bacteria", "fungus", "parasite", |
| "germ", "microbe", "pathogen", "cell", "tissue", "organ", "organism", "creature", "animal", "beast", "mammal", "bird", "fish", |
| "reptile", "amphibian", "insect", "arachnid", "crustacean", "mollusk", "worm", "plant", "tree", "shrub", "flower", "grass", |
| "herb", "bush", "vine", "fern", "moss", "lichen", "fungus", "mushroom", "seed", "root", "stem", "leaf", "leaves", "petal", "bud", |
| "bloom", "blossom", "fruit", "berry", "nut", "grain", "vegetable", "root", "tuber", "bulb", "corn", "wheat", "rice", "oat", "barley", |
| "rye", "millet", "sorghum", "maize", "pulse", "legume", "bean", "pea", "lentil", "soy", "tofu", "nut", "almond", "walnut", "pecan", |
| "cashew", "pistachio", "hazelnut", "macadamia", "brazil", "chestnut", "pine", "peanut", "seed", "sunflower", "sesame", "poppy", |
| "chia", "flax", "hemp", "quinoa", "amaranth", "spice", "herb", "seasoning", "condiment", "sauce", "dressing", "marinade", "rub", |
| "salt", "pepper", "sugar", "honey", "syrup", "molasses", "vinegar", "oil", "butter", "margarine", "cream", "milk", "cheese", |
| "yogurt", "egg", "meat", "beef", "pork", "lamb", "chicken", "turkey", "duck", "goose", "fish", "seafood", "shellfish", "crab", |
| "lobster", "shrimp", "prawn", "oyster", "clam", "mussel", "scallop", "squid", "octopus", "vegetable", "salad", "soup", "stew", |
| "curry", "pasta", "noodle", "rice", "bread", "cake", "pastry", "pie", "tart", "cookie", "biscuit", "cracker", "chip", "crisp", |
| "candy", "sweet", "chocolate", "cocoa", "coffee", "tea", "juice", "soda", "pop", "water", "wine", "beer", "liquor", "spirit", |
| "alcohol", "cocktail", "beverage", "drink", "food", "cuisine", "dish", "meal", "snack", "breakfast", "lunch", "dinner", "supper", |
| "dessert", "appetizer", "starter", "entree", "main", "side", "course", "buffet", "banquet", "feast", "picnic", "barbecue", "bbq", |
| "grill", "roast", "bake", "fry", "saute", "boil", "steam", "poach", "simmer", "braise", "stew", "marinate", "cure", "smoke", |
| "preserve", "can", "bottle", "jar", "package", "container", "box", "bag", "sack", "pouch", "carton", "crate", "barrel", "keg", |
| "cask", "tank", "vat", "tub", "bucket", "pail", "basket", "bin", "canister", "flask", "bottle", "vial", "ampoule", "tube", "jar", |
| "pot", "pan", "kettle", "wok", "skillet", "griddle", "oven", "stove", "range", "cooktop", "burner", "microwave", "refrigerator", |
| "fridge", "freezer", "dishwasher", "sink", "faucet", "tap", "counter", "countertop", "cabinet", "cupboard", "pantry", "shelf", |
| "shelves", "drawer", "table", "desk", "chair", "sofa", "couch", "bench", "stool", "ottoman", "bed", "mattress", "pillow", |
| "blanket", "quilt", "sheet", "bedspread", "comforter", "duvet", "towel", "curtain", "drape", "blind", "shade", "lamp", "light", |
| "bulb", "fixture", "chandelier", "sconce", "lantern", "candle", "flashlight", "torch", "fire", "flame", "blaze", "embers", |
| "smoke", "ash", "soot", "charcoal", "coal", "wood", "log", "timber", "lumber", "plank", "board", "beam", "post", "pole", |
| "stick", "rod", "staff", "club", "bat", "racket", "paddle", "oar", "paddle", "whip", "rope", "cord", "string", "thread", "yarn", |
| "wire", "cable", "chain", "link", "hook", "loop", "knot", "tie", "strap", "belt", "ribbon", "lace", "zipper", "button", "snap", |
| "buckle", "clasp", "pin", "needle", "nail", "screw", "bolt", "nut", "washer", "rivet", "brad", "tack", "staple", "glue", |
| "adhesive", "tape", "seal", "caulk", "putty", "cement", "concrete", "mortar", "plaster", "stucco", "brick", "block", "stone", |
| "rock", "pebble", "gravel", "sand", "soil", "dirt", "clay", "mud", "dust", "powder", "granule", "grain", "particle", "atom", |
| "molecule", "electron", "proton", "neutron", "ion", "isotope", "element", "compound", "mixture", "solution", "suspension", |
| "emulsion", "colloid", "gel", "foam", "aerosol", "liquid", "fluid", "solid", "gas", "vapor", "steam", "mist", "fog", "smoke", |
| "cloud", "haze", "bubble", "droplet", "stream", "flow", "current", "wave", "tide", "ripple", "splash", "spray", "jet", "fountain", |
| "geyser", "waterfall", "cascade", "rapids", "whirlpool", "vortex", "eddy", "pool", "puddle", "pond", "lake", "reservoir", |
| "basin", "bowl", "dish", "plate", "saucer", "cup", "mug", "glass", "goblet", "tumbler", "flute", "stein", "tankard", "pitcher", |
| "jug", "ewer", "decanter", "carafe", "bottle", "canteen", "flask", "thermos", "vessel", "utensil", "tool", "implement", |
| "instrument", "device", "gadget", "appliance", "machine", "mechanism", "apparatus", "equipment", "gear", "kit", "set", |
| "collection", "assembly", "array", "assortment", "selection", "variety", "range", "series", "sequence", "succession", "chain", |
| "string", "line", "row", "column", "rank", "file", "tier", "layer", "level", "story", "floor", "deck", "stage", "platform", |
| "scaffold", "ladder", "stair", "step", "rung", "tread", "riser", "banister", "railing", "handrail", "guardrail", "fence", |
| "wall", "barrier", "partition", "divider", "screen", "panel", "board", "shield", "shelter", "cover", "roof", "ceiling", |
| "canopy", "awning", "umbrella", "parasol", "tent", "canopy", "marquee", "pavilion", "dome", "vault", "arch", "beam", "truss", |
| "girder", "joist", "rafter", "purlin", "lath", "shingle", "tile", "slate", "metal", "steel", "iron", "copper", "brass", |
| "bronze", "aluminum", "aluminium", "tin", "lead", "zinc", "nickel", "chromium", "chrome", "silver", "gold", "platinum", |
| "titanium", "tungsten", "mercury", "uranium", "plutonium", "radium", "lithium", "sodium", "potassium", "calcium", "magnesium", |
| "carbon", "nitrogen", "oxygen", "hydrogen", "helium", "neon", "argon", "krypton", "xenon", "radon", "fluorine", "chlorine", |
| "bromine", "iodine", "sulfur", "sulphur", "phosphorus", "silicon", "boron", "arsenic", "antimony", "bismuth", "selenium", |
| "tellurium", "polonium", "astatine", "francium", "radium", "actinium", "thorium", "protactinium", "uranium", "neptunium", |
| "plutonium", "americium", "curium", "berkelium", "californium", "einsteinium", "fermium", "mendelevium", "nobelium", |
| "lawrencium", "rutherfordium", "dubnium", "seaborgium", "bohrium", "hassium", "meitnerium", "darmstadtium", "roentgenium", |
| "copernicium", "nihonium", "flerovium", "moscovium", "livermorium", "tennessine", "oganesson", |
| } |
|
|
|
|
| def extract_odor_paragraph(raw_text: str) -> str: |
| """Extract the first odor sentence(s) from an Arctander monograph raw_text.""" |
| |
| lines = [l.strip() for l in raw_text.split("\n") if l.strip()] |
| |
| text = " ".join(lines) |
| |
| text = re.sub(r"\s+", " ", text) |
| lower = text.lower() |
| |
| start_idx = None |
| for k in ["odor", "smell", "aroma"]: |
| idx = lower.find(k) |
| if idx != -1 and (start_idx is None or idx < start_idx): |
| start_idx = idx |
| if start_idx is None: |
| return "" |
| |
| snippet = text[start_idx:] |
| |
| sentences = re.split(r"(?<=[.!?])\s+", snippet) |
| |
| selected = [] |
| for s in sentences: |
| if any(s.lower().startswith(b) for b in ["used ", "uses", "use:", "prod.", "production", "preparation", "concentration", "g.r.a.s", "f.e.m.a", "flavors", "flavor", "it is used", "used as"]): |
| break |
| selected.append(s) |
| if len(selected) >= 2: |
| break |
| return " ".join(selected) |
|
|
|
|
| def normalize_component(name: str) -> str: |
| n = name.lower().strip() |
| |
| n = re.sub(r"\s+\d+\s*%", "", n) |
| n = re.sub(r"\s+\d+", "", n) |
| |
| for suffix in ["oil", "absolute", "resinoid", "tincture", "concrete", "extract", "water", "terpeneless", "bigarade", "otto", "s.a.p", "s.a.p.", "integrale", "extra"]: |
| n = n.replace(suffix, " ") |
| |
| n = re.sub(r"\s*\([^)]*\)", "", n) |
| |
| n = re.sub(r"[^a-z0-9\s]", "", n) |
| return " ".join(n.split()) |
|
|
|
|
| def tokenize_profile(text: str) -> list[str]: |
| text = text.lower().replace(",", " ").replace(";", " ").replace(".", " ").replace("-", " ") |
| tokens = [t.strip() for t in text.split() if len(t.strip()) > 2 and t.strip() not in STOPWORDS] |
| cleaned = [] |
| for t in tokens: |
| t = t.rstrip("s") if t.endswith("s") and not t.endswith("ss") else t |
| cleaned.append(t) |
| return cleaned |
|
|
|
|
| def main() -> None: |
| base = Path("/home/hermes/pino") |
| arctander_path = Path("/home/hermes/fragrance-research/extracted/arctander_monographs.jsonl") |
| formulas_path = base / "data" / "appell_formulas_enriched.jsonl" |
| output_path = base / "data" / "appell_formulas_enriched.jsonl" |
|
|
| monographs = [json.loads(l) for l in arctander_path.read_text().strip().splitlines()] |
|
|
| |
| by_name: dict[str, dict] = {} |
| for m in monographs: |
| names = [m.get("name", "")] |
| synonyms = m.get("synonyms", "") |
| if synonyms: |
| names.extend([s.strip() for s in synonyms.split(",")]) |
| for n in names: |
| if n: |
| by_name[n.lower()] = m |
|
|
| |
| formulas = [json.loads(l) for l in formulas_path.read_text().strip().splitlines()] |
|
|
| resolver = TradeNameResolver() |
|
|
| def resolve_component(name: str) -> tuple[str, str] | None: |
| """Return (canonical_name, profile_text) or None.""" |
| norm = normalize_component(name) |
|
|
| |
| if norm in NATURAL_SCENT_PROFILES: |
| return (norm, NATURAL_SCENT_PROFILES[norm]) |
| for key, profile in NATURAL_SCENT_PROFILES.items(): |
| if key in norm or norm in key: |
| return (key, profile) |
|
|
| |
| if norm in by_name: |
| m = by_name[norm] |
| odor = extract_odor_paragraph(m.get("raw_text", "")) |
| return (m.get("name", norm), odor) |
|
|
| |
| match, score, _ = process.extractOne(norm, by_name.keys(), scorer=fuzz.token_sort_ratio) |
| if score >= 75: |
| m = by_name[match] |
| odor = extract_odor_paragraph(m.get("raw_text", "")) |
| return (m.get("name", match), odor) |
|
|
| return None |
|
|
| |
| curated_profiles = { |
| "bois des iles": "woody oriental; sandalwood, rose, jasmine, aldehydes, spices, amber, vanilla, soft balsamic dry-down", |
| "cabochard": "chypre leather; green galbanum, oakmoss, patchouli, leather, amber, musk, dark earthy dry-down", |
| "chanel no 5": "aldehydic floral; jasmine, rose, ylang-ylang, sandalwood, vetiver, vanilla, powdery musk", |
| "chypre": "citrus chypre; bergamot, oakmoss, labdanum, patchouli, rose, jasmine, amber, woody dry-down", |
| "dans la nuit": "spicy oriental floral; rose, orris, tuberose, labdanum, civet, myrrh, opoponax, amber", |
| "emeraude": "amber oriental; vanilla, jasmine, rose, sandalwood, civet, patchouli, benzoin, warm balsamic", |
| "en avion": "spicy floral; carnation, rose, jasmine, orange blossom, opoponax, sandalwood, powdery", |
| "femme": "fruity chypre; plum, peach, patchouli, oakmoss, costus, jasmine, rose, warm woody", |
| "fleurs d’amour": "floral bouquet; rose, jasmine, heliotrope, violet, orange blossom, powdery musk", |
| "fougeraie": "fougère; lavender, bergamot, geranium, oakmoss, coumarin, amber, sweet hay", |
| "fougére royale": "classical fougère; lavender, bergamot, geranium, oakmoss, coumarin, tonka, sweet woody", |
| "habanita": "powdery oriental; rose, jasmine, vetiver, sandalwood, musk, vanilla, tobacco, leather", |
| "ideal": "spicy oriental floral; rose, jasmine, carnation, vetiver, sandalwood, amber, musk", |
| "joy": "rich white floral; jasmine, rose, ylang-ylang, tuberose, sandalwood, musk, powdery", |
| "molyneux no 5": "aldehydic floral chypre; rose, jasmine, oakmoss, vetiver, sandalwood, amber", |
| "moment supreme": "rich floral oriental; rose, jasmine, carnation, sandalwood, vetiver, amber, musk", |
| "mylord": "aromatic fougère; lavender, bergamot, geranium, oakmoss, patchouli, leather, musk", |
| "my sin": "aldehydic floral; jasmine, rose, lily-of-the-valley, sandalwood, civet, musk, powdery", |
| "narcisse noir": "dark floral; orange blossom, narcissus, jasmine, musk, civet, sandalwood, intense", |
| "nuit de noel": "oriental floral; rose, jasmine, sandalwood, musk, civet, amber, benzoin, warm spicy", |
| "quelques fleurs": "luxury floral bouquet; rose, jasmine, orange blossom, tuberose, sandalwood, musk, honey", |
| "rumeur": "aldehydic floral; rose, jasmine, lily-of-the-valley, sandalwood, vetiver, powdery musk", |
| "tabu": "oriental spicy; amber, patchouli, sandalwood, rose, jasmine, clove, vanilla, dark balsamic", |
| "toujours moi": "oriental floral; rose, jasmine, violet, sandalwood, musk, amber, sweet powdery", |
| "vol de nuit": "woody oriental floral; bergamot, narcissus, jasmine, rose, sandalwood, vetiver, amber, moss", |
| "white shoulders": "aldehydic floral; gardenia, jasmine, rose, lily-of-the-valley, musk, sandalwood, powdery", |
| "eau de coty": "citrus chypre; bergamot, lemon, orange, oakmoss, patchouli, rose, jasmine, green", |
| "moustache": "aromatic fougère; lavender, bergamot, geranium, oakmoss, patchouli, leather, warm", |
| "sandalwood": "woody oriental; sandalwood, cedar, amber, vanilla, musk, creamy balsamic", |
| "cing fleurs": "floral bouquet; jasmine, rose, lily-of-the-valley, violet, orange blossom, powdery", |
| "ma griffe": "green chypre floral; galbanum, plum, jasmine, rose, oakmoss, patchouli, vetiver", |
| "shalimar": "oriental vanilla; bergamot, lemon, rose, jasmine, iris, vanilla, tonka, balsamic, smoky", |
| "rose 50": "rich floral; rose, geranium, honey, powdery, spicy, warm", |
| "muguet 10 a": "green floral; lily-of-the-valley, fresh, clean, soapy, dewy, green", |
| "jasmin 50 c": "white floral; jasmine, narcotic, indolic, sweet, exotic, rich", |
| } |
|
|
| for f in formulas: |
| name = f["name"].strip().lower() |
|
|
| |
| if name in curated_profiles: |
| f["scent_profile"] = curated_profiles[name] |
| f["description"] = f"{f['name']} — {curated_profiles[name]}" |
| f["component_match_rate"] = 1.0 |
| continue |
|
|
| |
| all_tokens = [] |
| weights = [] |
| matched = 0 |
| total_amount = sum(c.get("amount", 0) for c in f.get("components", [])) |
|
|
| for c in f.get("components", []): |
| comp_name = c.get("name", "") |
| amount = c.get("amount", 0) |
| res = resolve_component(comp_name) |
| if res: |
| matched += 1 |
| canon, profile_text = res |
| tokens = tokenize_profile(profile_text) |
| weight = amount / total_amount if total_amount > 0 else 0 |
| for _ in range(max(1, int(weight * 100))): |
| all_tokens.extend(tokens) |
|
|
| counter = Counter(all_tokens) |
| top_desc = ", ".join([f"{t}({n})" for t, n in counter.most_common(15)]) |
| f["scent_profile"] = top_desc |
| f["description"] = f"{f['name']} — {top_desc}" |
| f["component_match_rate"] = matched / len(f.get("components", [])) if f.get("components") else 0 |
|
|
| |
| output_path.write_text("\n".join(json.dumps(f, ensure_ascii=False) for f in formulas) + "\n", encoding="utf-8") |
| print(f"Enriched {len(formulas)} Appell formulas -> {output_path}") |
| for f in formulas[:20]: |
| print(f" {f['name']:25s} | match={f['component_match_rate']:.2f} | {f['scent_profile'][:100]}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|