Spaces:
Paused
Paused
File size: 5,804 Bytes
7f15dbc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | """
parse_notes.py — Nemotron Parse wrapper for structured extraction
from check-in notes.
Uses NVIDIA's Nemotron-Parse (<1B params) to extract emotions,
themes, entities, and sentiment from the player's daily check-in
note. This unlocks the NVIDIA Nemotron sponsor prize.
Because this runs in the same HF Space as MiniCPM (the main LLM),
we load Nemotron-Parse as a secondary model for structured extraction
only — not for generation. The model is tiny enough (<1B) that it
adds minimal GPU memory pressure alongside MiniCPM 2.5B.
Usage:
from parse_notes import extract_note_insights
insights = extract_note_insights("I'm exhausted from overworking")
# -> { "sentiment": "negative", "emotions": ["exhaustion"],
# "themes": ["burnout", "work"], "entities": [] }
HF Space env config:
Set NEMOTRON_PARSE_MODEL=nvidia/Nemotron-Parse-H-Base-v1
(or omit for default)
See: https://huggingface.co/nvidia/Nemotron-Parse-H-Base-v1
"""
from __future__ import annotations
import json
import logging
import os
from dataclasses import dataclass, field
from typing import Optional
logger = logging.getLogger(__name__)
# ─── Types ────────────────────────────────────────────────────────────────────
@dataclass
class NoteInsights:
sentiment: str # positive | negative | neutral | mixed
emotions: list[str] = field(default_factory=list)
themes: list[str] = field(default_factory=list)
entities: list[str] = field(default_factory=list)
intensity: float = 0.0 # 0.0 to 1.0
# ─── Extraction via Nemotron-Parse ────────────────────────────────────────────
DEFAULT_MODEL = "nvidia/Nemotron-Parse-H-Base-v1"
_PIPELINE = None
def _get_pipeline():
global _PIPELINE
if _PIPELINE is None:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = os.environ.get(
"NEMOTRON_PARSE_MODEL", DEFAULT_MODEL
)
logger.info("Loading Nemotron-Parse: %s", model_name)
tokenizer = AutoTokenizer.from_pretrained(
model_name, trust_remote_code=True
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
trust_remote_code=True,
torch_dtype=torch.float16,
device_map="auto",
)
_PIPELINE = {"model": model, "tokenizer": tokenizer}
logger.info("Nemotron-Parse loaded")
return _PIPELINE["model"], _PIPELINE["tokenizer"]
_EXTRACTION_PROMPT = """\
Extract structured insights from this journal note.
Return valid JSON with these fields:
- "sentiment": "positive" | "negative" | "neutral" | "mixed"
- "emotions": list of emotion words present (e.g. ["anxiety", "hope"])
- "themes": list of thematic keywords (e.g. ["work", "relationships", "health"])
- "entities": list of specific people, places, or things mentioned
- "intensity": float 0.0 to 1.0 describing emotional intensity
Note: {note}
JSON:
"""
def extract_note_insights(note: str) -> Optional[NoteInsights]:
if not note or not note.strip():
return None
try:
model, tokenizer = _get_pipeline()
prompt = _EXTRACTION_PROMPT.format(note=note.strip())
inputs = tokenizer(prompt, return_tensors="pt")
outputs = model.generate(
**inputs,
max_new_tokens=128,
temperature=0.1,
do_sample=False,
)
decoded = tokenizer.decode(
outputs[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True,
).strip()
# Strip any trailing conversational fluff
if "{" in decoded:
decoded = decoded[decoded.index("{"):decoded.rindex("}")+1]
data = json.loads(decoded)
return NoteInsights(
sentiment=data.get("sentiment", "neutral"),
emotions=data.get("emotions", []),
themes=data.get("themes", []),
entities=data.get("entities", []),
intensity=float(data.get("intensity", 0.0)),
)
except Exception as exc:
logger.warning("Nemotron-Parse extraction failed: %s", exc)
return None
# ─── Simple keyword fallback (no model needed) ───────────────────────────────
def _keyword_sentiment(note: str) -> str:
negative_words = {
"tired", "exhausted", "sad", "angry", "frustrated", "anxious",
"worried", "scared", "alone", "stuck", "overwhelmed", "burnout",
}
positive_words = {
"happy", "grateful", "hopeful", "excited", "proud", "peaceful",
"joyful", "loved", "inspired", "motivated", "alive",
}
words = set(note.lower().split())
pos = len(words & positive_words)
neg = len(words & negative_words)
if pos > neg:
return "positive"
if neg > pos:
return "negative"
if pos == 0 and neg == 0:
return "neutral"
return "mixed"
def fast_insights(note: str) -> Optional[NoteInsights]:
if not note or not note.strip():
return None
return NoteInsights(
sentiment=_keyword_sentiment(note),
emotions=list({
w for w in note.lower().split()
if w in {
"tired", "exhausted", "sad", "angry", "frustrated",
"anxious", "worried", "scared", "happy", "grateful",
"hopeful", "excited", "proud", "peaceful", "joyful",
"loved", "inspired", "motivated", "alive", "hopeful",
}
}),
themes=[],
entities=[],
intensity=0.5,
)
|