Spaces:
Running on Zero
Running on Zero
File size: 7,220 Bytes
68c1777 | 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 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | """Dataset upload → parse → validate → clean → chat-format conversion.
Runs entirely on CPU (spec P7); PDF/DOCX treated as untrusted input with
config-driven limits (spec §4.2). Token counts are fast estimates (chars/4)
unless a cached tokenizer is available."""
import csv
import hashlib
import io
import json
import pathlib
from src.config_loader import get_configs
class DatasetError(Exception):
pass
def _limits():
return get_configs().limits.get("upload", {})
def _check_size(path: pathlib.Path):
mb = path.stat().st_size / 1e6
if mb > _limits().get("max_file_mb", 50):
raise DatasetError(f"File is {mb:.0f} MB — limit is {_limits().get('max_file_mb')} MB.")
def _extract_pdf(path):
from pypdf import PdfReader # lazy (spec §12)
try:
reader = PdfReader(str(path))
if reader.is_encrypted:
raise DatasetError("Encrypted PDFs are not accepted.")
cap = _limits().get("max_extracted_chars", 5_000_000)
out, total = [], 0
for page in reader.pages:
t = page.extract_text() or ""
total += len(t)
if total > cap:
raise DatasetError(f"Extracted text exceeds {cap} characters.")
out.append(t)
return "\n\n".join(out)
except DatasetError:
raise
except Exception as e: # noqa: BLE001
raise DatasetError(f"PDF could not be parsed safely: {type(e).__name__}") from e
def _extract_docx(path):
import docx # lazy
try:
d = docx.Document(str(path))
text = "\n".join(p.text for p in d.paragraphs)
if len(text) > _limits().get("max_extracted_chars", 5_000_000):
raise DatasetError("Extracted text exceeds the configured limit.")
return text
except DatasetError:
raise
except Exception as e: # noqa: BLE001
raise DatasetError(f"DOCX could not be parsed safely: {type(e).__name__}") from e
def _rows_from_structured(path: pathlib.Path):
suffix = path.suffix.lower()
text = path.read_text(errors="replace")
if suffix == ".csv":
return list(csv.DictReader(io.StringIO(text)))
if suffix == ".jsonl":
return [json.loads(l) for l in text.splitlines() if l.strip()]
if suffix == ".json":
data = json.loads(text)
if isinstance(data, dict):
data = data.get("data") or data.get("rows") or [data]
return data
raise DatasetError(f"Unsupported structured format {suffix}")
FIELD_GUESSES = {
"instruction": ["instruction", "question", "prompt", "input_text", "query"],
"input": ["input", "context", "passage"],
"output": ["output", "answer", "response", "completion", "target", "label"],
}
def _guess_fields(row: dict):
keys = {k.lower(): k for k in row}
got = {}
for role, cands in FIELD_GUESSES.items():
for c in cands:
if c in keys:
got[role] = keys[c]
break
return got
def _chunk_text(text, target=1500):
paras = [p.strip() for p in text.split("\n\n") if len(p.strip()) > 60]
chunks, buf = [], ""
for p in paras:
if len(buf) + len(p) > target and buf:
chunks.append(buf.strip())
buf = p
else:
buf += "\n\n" + p
if len(buf.strip()) > 200:
chunks.append(buf.strip())
return chunks
def prepare(file_path: str, system_prompt: str = "") -> tuple[list[dict], dict]:
"""Returns (records, summary). records = [{"messages": [...]}, ...]"""
path = pathlib.Path(file_path)
_check_size(path)
suffix = path.suffix.lower()
lim = _limits()
if suffix in (".csv", ".json", ".jsonl"):
rows = _rows_from_structured(path)
if not rows:
raise DatasetError("No rows found in the file.")
fields = _guess_fields(rows[0])
if "output" not in fields or ("instruction" not in fields and "input" not in fields):
raise DatasetError(
f"Could not identify instruction/output columns. Found: {list(rows[0].keys())}. "
f"Rename columns to one of {FIELD_GUESSES['instruction']} + {FIELD_GUESSES['output']}."
)
records, has_refs = [], True
for r in rows:
user = str(r.get(fields.get("instruction", ""), "")).strip()
ctx = str(r.get(fields.get("input", ""), "")).strip() if "input" in fields else ""
out = str(r.get(fields["output"], "")).strip()
if not (user or ctx) or not out:
continue
content = f"{user}\n\n{ctx}".strip()
msgs = ([{"role": "system", "content": system_prompt}] if system_prompt else [])
msgs += [{"role": "user", "content": content}, {"role": "assistant", "content": out}]
records.append({"messages": msgs})
elif suffix in (".txt", ".pdf", ".docx"):
text = (path.read_text(errors="replace") if suffix == ".txt"
else _extract_pdf(path) if suffix == ".pdf" else _extract_docx(path))
chunks = _chunk_text(text)
if not chunks:
raise DatasetError("No usable text extracted.")
records = [{"messages": (
[{"role": "system", "content": system_prompt}] if system_prompt else []) + [
{"role": "user", "content": "Continue writing in the style and subject of this document excerpt:\n\n"
+ c[: len(c) // 2]},
{"role": "assistant", "content": c[len(c) // 2:]},
]} for c in chunks]
has_refs = False
else:
raise DatasetError(f"Unsupported file type {suffix}. Accepted: CSV, JSON, JSONL, TXT, PDF, DOCX.")
# clean: dedupe + empty filter
seen, cleaned, dupes = set(), [], 0
for r in records:
key = hashlib.sha1(json.dumps(r, sort_keys=True).encode()).hexdigest()
if key in seen:
dupes += 1
continue
seen.add(key)
cleaned.append(r)
if len(cleaned) > lim.get("max_samples", 100_000):
raise DatasetError(f"{len(cleaned)} samples exceed the limit {lim.get('max_samples')}.")
lengths = [sum(len(m["content"]) for m in r["messages"]) for r in cleaned]
est_tokens = int(sum(lengths) / 4)
if est_tokens > lim.get("max_total_tokens", 20_000_000):
raise DatasetError(f"Estimated {est_tokens} tokens exceed the limit.")
summary = {
"samples": len(cleaned),
"duplicates_removed": dupes,
"est_tokens": est_tokens,
"avg_tokens_per_sample": round(est_tokens / max(len(cleaned), 1), 1),
"avg_chars": round(sum(lengths) / max(len(lengths), 1), 1),
"has_reference_answers": has_refs,
"source_file": path.name,
"fingerprint": hashlib.sha256(json.dumps(cleaned[:200], sort_keys=True).encode()).hexdigest()[:16],
}
return cleaned, summary
def save_jsonl(records: list[dict], path: pathlib.Path):
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
for r in records:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
def load_jsonl(path: pathlib.Path) -> list[dict]:
return [json.loads(l) for l in path.read_text().splitlines() if l.strip()]
|