File size: 5,420 Bytes
0a9384a
 
faae576
0a9384a
faae576
fa0883e
0a9384a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ff56c6
0a9384a
faae576
0a9384a
5ff56c6
faae576
 
0a9384a
faae576
0a9384a
faae576
 
0a9384a
faae576
 
 
 
e3ef8d2
fa0883e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3ef8d2
fa0883e
 
 
0a9384a
e3ef8d2
faae576
 
 
 
e3ef8d2
 
faae576
e3ef8d2
 
 
faae576
e3ef8d2
faae576
 
 
 
 
 
e3ef8d2
 
faae576
 
 
0a9384a
faae576
0a9384a
e3ef8d2
0a9384a
faae576
0a9384a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3ef8d2
 
fa0883e
 
 
 
e3ef8d2
fa0883e
 
 
 
 
 
faae576
1ac9c9a
fa0883e
5ff56c6
 
faae576
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
import os, re, difflib, traceback
from typing import List, Tuple
import gradio as gr
from huggingface_hub import hf_hub_download
from ctransformers import AutoModelForCausalLM

# ---------------- Auto-pick a valid GGUF ----------------
CANDIDATES: Tuple[Tuple[str, str], ...] = (
    ("bartowski/Llama-3.2-3B-Instruct-GGUF", "Llama-3.2-3B-Instruct-Q8_0.gguf"),
    ("bartowski/Llama-3.2-3B-Instruct-GGUF", "Llama-3.2-3B-Instruct-Q6_K_L.gguf"),
    ("bartowski/Llama-3.2-3B-Instruct-GGUF", "Llama-3.2-3B-Instruct-Q5_K_M.gguf"),
    ("bartowski/Llama-3.2-3B-Instruct-GGUF", "Llama-3.2-3B-Instruct-Q4_0.gguf"),
)

def resolve_model_file() -> str:
    last_err = None
    for repo, fname in CANDIDATES:
        try:
            path = hf_hub_download(repo_id=repo, filename=fname)
            print(f"[Humanizer] Using {repo} :: {fname}")
            return path
        except Exception as e:
            last_err = e
            print(f"[Humanizer] Could not get {repo}/{fname}: {e}")
    raise RuntimeError(f"Failed to download any GGUF. Last error: {last_err}")

MODEL_TYPE = "llama"
_llm = None

def load_model():
    global _llm
    if _llm is None:
        file_path = resolve_model_file()
        _llm = AutoModelForCausalLM.from_pretrained(
            file_path,           # direct path to the .gguf we just downloaded
            model_type=MODEL_TYPE,
            gpu_layers=0,
            context_length=4096, # safer on free CPU
        )
    return _llm

# ---------------- Protect / restore ----------------
SENTINEL_OPEN, SENTINEL_CLOSE = "§§KEEP_OPEN§§", "§§KEEP_CLOSE§§"
URL_RE  = re.compile(r'(https?://\S+)')
CODE_RE = re.compile(r'`{1,3}[\s\S]*?`{1,3}')
CITE_RE = re.compile(r'\[(?:[^\]]+?)\]|\(\d{4}\)|\[\d+(?:-\d+)?\]')
NUM_RE  = re.compile(r'\b\d[\d,.\-/]*\b')

def protect(text: str):
    protected = []
    def wrap(m):
        protected.append(m.group(0))
        return f"{SENTINEL_OPEN}{len(protected)-1}{SENTINEL_CLOSE}"
    text = CODE_RE.sub(wrap, text)
    text = URL_RE.sub(wrap, text)
    text = CITE_RE.sub(wrap, text)
    text = NUM_RE.sub(wrap, text)
    return text, protected

def restore(text: str, protected: List[str]):
    def unwrap(m): return protected[int(m.group(1))]
    text = re.sub(rf"{SENTINEL_OPEN}(\d+){SENTINEL_CLOSE}", unwrap, text)
    return text.replace(SENTINEL_OPEN, "").replace(SENTINEL_CLOSE, "")

# ---------------- Prompting ----------------
SYSTEM = (
    "You are an expert editor. Humanize the user's text: improve flow, vary sentence length, "
    "split run-ons, replace stiff phrasing with natural alternatives, and preserve meaning. "
    "Do NOT alter anything wrapped by §§KEEP_OPEN§§<id>§§KEEP_CLOSE§§ (citations, URLs, numbers, code). "
    "Keep the requested tone and region. No em dashes—use simple punctuation."
)

def build_prompt(text: str, tone: str, region: str, level: str, intensity: int) -> str:
    user = (
        f"Tone: {tone}. Region: {region} English. Reading level: {level}. "
        f"Humanization intensity: {intensity} (10 strongest).\n\n"
        f"Rewrite this text. Keep markers intact:\n\n{text}"
    )
    return (
        "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n"
        f"{SYSTEM}\n"
        "<|eot_id|><|start_header_id|>user<|end_header_id|>\n"
        f"{user}\n"
        "<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n"
    )

def diff_ratio(a: str, b: str) -> float:
    return difflib.SequenceMatcher(None, a, b).ratio()

def generate_once(prompt: str, temperature: float, max_new: int = 384) -> str:
    llm = load_model()
    return llm(prompt, temperature=temperature, top_p=0.95, max_new_tokens=max_new, stop=["<|eot_id|>"]).strip()

# ---------------- Main ----------------
def humanize_core(text: str, tone: str, region: str, level: str, intensity: int):
    try:
        protected_text, bag = protect(text)
        prompt = build_prompt(protected_text, tone, region, level, intensity)

        draft = generate_once(prompt, temperature=0.35)
        if diff_ratio(protected_text, draft) > 0.97:
            draft = generate_once(prompt, temperature=0.9)

        draft = draft.replace("—", "-")
        final = restore(draft, bag)

        for i, span in enumerate(bag):
            marker = f"{SENTINEL_OPEN}{i}{SENTINEL_CLOSE}"
            if marker in protected_text and span not in final:
                final = final.replace(marker, span)
        return final
    except Exception:
        return "ERROR:\n" + traceback.format_exc()

# ---------------- Gradio UI (REST at /api/predict/) ----------------
def ui_humanize(text, tone, region, level, intensity):
    return humanize_core(text, tone, region, level, int(intensity))

demo = gr.Interface(
    fn=ui_humanize,
    inputs=[
        gr.Textbox(lines=12, label="Input text"),
        gr.Dropdown(["professional","casual","academic","friendly","persuasive"], value="professional", label="Tone"),
        gr.Dropdown(["US","UK","KE"], value="US", label="Region"),
        gr.Dropdown(["general","simple","advanced"], value="general", label="Reading level"),
        gr.Slider(1, 10, value=6, step=1, label="Humanization intensity"),
    ],
    outputs=gr.Textbox(label="Humanized"),
    title="NoteCraft Humanizer (Llama-3.2-3B-Instruct)",
    description="REST: POST /api/predict/ with { data: [text,tone,region,level,intensity] }",
).queue()

if __name__ == "__main__":
    demo.launch()