Spaces:
Running on Zero
Running on Zero
Download curation_demo.py from Brianferrell787/range: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/spaces/Brianferrell787/range/resolve/main/curation_demo.py
- Command line
-
hf download hf://spaces/Brianferrell787/range/curation_demo.py
-
curl -L -o curation_demo.py https://huggingface.co/spaces/Brianferrell787/range/resolve/main/curation_demo.py
10.6 kB
| """Small, bounded generic taxonomy demo; no model calls or filesystem input.""" | |
| import json | |
| from time import perf_counter | |
| import gradio as gr | |
| from range_curate import CurationEngine, CurationTaxonomy | |
| TAXONOMY = '''version: 1 | |
| name: Clinical research example | |
| rules: | |
| - label: clinical_trials | |
| include: [randomized trial, phase 3 trial] | |
| exclude: [mouse model] | |
| - label: oncology | |
| include: [lung cancer, cancer immunotherapy] | |
| exclude: [] | |
| ''' | |
| DOCUMENTS = json.dumps([ | |
| 'A randomized trial evaluated a treatment for lung cancer.', | |
| 'A mouse model was discussed alongside a randomized trial.', | |
| 'A city council approved new bicycle parking.' | |
| ], indent=2) | |
| GUIDE = '''## Build a curated dataset with your own taxonomy | |
| **Python package coming soon.** You can try the hosted demo now or build from source. | |
| [Read the code README for installation, taxonomy YAML, and streaming-batch examples](https://huggingface.co/spaces/Brianferrell787/range/blob/main/PACKAGE_README.md). | |
| 1. **Define inclusion.** Write down what belongs, what does not, and ambiguous cases. | |
| 2. **Name your labels.** Add phrases and spelling variants; use clear, stable labels. | |
| 3. **Test a small labeled sample.** Include positive cases, near misses, exclusions, | |
| abbreviations, and documents with multiple topics. Review false positives and | |
| false negatives before processing the full collection. | |
| 4. **Freeze and run.** Save the YAML, taxonomy fingerprint, package version, and | |
| input identity. Use bounded batches for large collections; save retained row IDs. | |
| 5. **Review and iterate.** Compare revisions on a separate held-out sample. | |
| The generic API keeps a label when **any include phrase** matches and **no exclude | |
| phrase** for that label matches anywhere in the document. Labels are independent. | |
| Offsets refer to the original text, in Unicode code points. | |
| This is normalized literal matching: it does not infer negation, diagnoses, intent, | |
| or scientific validity. The biomedical wording is a toy example, not a validated | |
| biomedical taxonomy. | |
| ### Ask an AI agent to help draft your taxonomy | |
| Copy this prompt into your own agent, then review its output: | |
| > Help me draft a RANGE version-1 taxonomy for [dataset purpose]. First clarify | |
| > inclusion, exclusion, and ambiguous cases. Use only keys version, name, rules; | |
| > each rule has label, include, and optional exclude. Phrase lists contain strings. | |
| > Suggest aliases and collision risks. Explain that exclusions veto a label across | |
| > the whole document. Provide separate synthetic positive, negative, and borderline | |
| > examples with expected labels. Do not claim clinical validity or measured accuracy. | |
| > Keep proposed rules separate from approved rules. Do not use evaluation documents | |
| > to tune the taxonomy. Return YAML and a review checklist; do not execute it. | |
| Agents help author and test rules; no agent or model runs inside this demo. | |
| Use representative, appropriately shareable examples and human/domain review. | |
| ### Turn a text collection into a dataset you can use | |
| Search helps you find material for a question. Curation answers a different question: | |
| **Which documents belong in the dataset I want to build?** | |
| Start with your collection and define what qualifies: topics, phrases, labels, and | |
| exclusions. RANGE applies those rules across your text, records keep/drop decisions, | |
| and shows the matched evidence. Save the selected records for analysis, model | |
| training, evaluation, or a later enrichment stage. | |
| For example, a research team could use a reviewed taxonomy to select clinical-trial | |
| reports from a mixed document collection. The result is a reusable dataset with a | |
| record of why each document was selected. Save the taxonomy version and input IDs | |
| so you can rerun the same policy or compare it with a revised one. | |
| RANGE is built around **bulk selection, explicit rules, and traceable decisions**. | |
| It does not generate answers or rank documents by semantic similarity. | |
| ### Optional semantic enrichment: future work | |
| NER, GLiNER and other models could contribute entity observations or review signals. | |
| They are not integrated here. Their model versions, thresholds, uncertainty and | |
| costs need separate validation and benchmarks. A lexical prefilter can lose relevant | |
| documents; it is not automatically a recall-preserving gate for a semantic stage. | |
| The General Economy tabs demonstrate one specialized taxonomy and its market | |
| relationships. Those relationships and historical benchmark numbers are not | |
| requirements or performance promises for your custom taxonomy. | |
| ''' | |
| def processing_timing(elapsed, count): | |
| """Describe this CPU invocation, not a sustained-throughput benchmark.""" | |
| milliseconds = elapsed * 1000 | |
| duration = f'{milliseconds:,.3f} ms' if milliseconds >= 0.001 else '<0.001 ms' | |
| rate = count / elapsed if elapsed > 0 else None | |
| headline = f'Processed {count:,} record{"s" if count != 1 else ""} in {duration} on CPU' | |
| if count > 1 and rate is not None: | |
| headline += f' · {rate:,.0f} records/s for this batch' | |
| scope = ('Includes matching, decision evidence and engine setup when needed. ' | |
| 'Excludes input validation, queue/network wait and display rendering. ' | |
| 'Small demo runs are not sustained bulk benchmarks.') | |
| return {'processing_ms': milliseconds, 'records_per_second': rate, | |
| 'backend': 'cpu', 'timing_scope': scope}, f'**{headline}**\n\n{scope}' | |
| def curate_custom(taxonomy_yaml, documents_json): | |
| for value, limit, byte_limit, name in [(taxonomy_yaml, 12000, 48000, 'Taxonomy'), | |
| (documents_json, 202000, 810000, 'Documents')]: | |
| if not isinstance(value, str) or len(value) > limit or len(value.encode('utf8')) > byte_limit: | |
| raise gr.Error(f'{name} input exceeds the demo limit.') | |
| try: | |
| # Hosted input uses a small, alias-free YAML subset; local API accepts normal safe YAML. | |
| import yaml | |
| if any(isinstance(t, (yaml.tokens.AliasToken, yaml.tokens.AnchorToken)) for t in yaml.scan(taxonomy_yaml)): | |
| raise ValueError('YAML anchors and aliases are not supported in this demo') | |
| taxonomy = CurationTaxonomy.from_yaml(taxonomy_yaml) | |
| phrases = [p for r in taxonomy.rules for p in (*r.include, *r.exclude)] | |
| if len(taxonomy.rules) > 32 or len(phrases) > 256 or any(len(p) > 160 for p in phrases): | |
| raise ValueError('Use at most32 labels,256 phrases,160 characters per phrase') | |
| docs = json.loads(documents_json) | |
| if not isinstance(docs, list) or not 1 <= len(docs) <= 100 or any(not isinstance(d, str) for d in docs): | |
| raise ValueError('Provide a JSON array containing1–100 text strings') | |
| if any(len(d) > 20000 or len(d.encode('utf8')) > 80000 for d in docs) or sum(map(len, docs)) > 200000 or sum(len(d.encode('utf8')) for d in docs) > 800000: | |
| raise ValueError('Document size exceeds the per-document or batch limit') | |
| started = perf_counter() | |
| engine = CurationEngine(taxonomy, workers=1) | |
| rows = engine.screen_batch(docs, evidence=True, max_evidence=50) | |
| elapsed = perf_counter() - started | |
| except (ValueError, TypeError, yaml.YAMLError, RecursionError) as exc: | |
| raise gr.Error('Invalid taxonomy or documents. Check the documented schema and size limits.') from exc | |
| timing, timing_text = processing_timing(elapsed, len(rows)) | |
| try: | |
| range_seconds, basic_seconds, matches = engine._scanner.compare_matching(docs) | |
| except ValueError: | |
| timing_text += ('\n\nMatching comparison unavailable: the match limit was exceeded ' | |
| 'or the two matchers did not agree. Curation results are still shown.') | |
| else: | |
| timing['matching_comparison'] = { | |
| 'range_ms': range_seconds * 1000, 'basic_search_ms': basic_seconds * 1000, | |
| 'verified_matches': matches, 'repeats': 5, 'statistic': 'median', | |
| 'scope': 'Prepared-text matching, boundary checks and identical match-position output; CPU, one thread', | |
| } | |
| timing_text = ( | |
| '### Search time on your documents\n\n' | |
| '| Matcher | Search time |\n| --- | ---: |\n' | |
| f'| RANGE | {range_seconds * 1000:,.6f} ms |\n' | |
| f'| Basic search (one phrase at a time) | {basic_seconds * 1000:,.6f} ms |\n\n' | |
| f'**{matches:,} identical matches verified across {len(docs):,} records.** ' | |
| 'Both run in native code on one CPU thread, using the same prepared text, ' | |
| 'phrases and word-boundary rules. Times include finding matches and collecting ' | |
| 'their positions; they exclude normalization, matcher setup, labels and evidence formatting. ' | |
| 'Median of five warm runs with alternating order. Small-input timings vary; ' | |
| 'this is a basic substring-search baseline, not an indexed search engine.\n\n' | |
| + timing_text | |
| ) | |
| return [[r['row_index'], r['keep'], ', '.join(r['labels'])] for r in rows], { | |
| **timing, | |
| 'documents': len(rows), 'kept': sum(r['keep'] for r in rows), | |
| 'taxonomy_fingerprint': taxonomy.fingerprint, | |
| 'note': 'Rule matches only. Evidence capped at 50 spans per document; decisions examine all matches.'}, rows, timing_text | |
| def build_curation_tabs(): | |
| with gr.Tab('Curate your text'): | |
| gr.Markdown('### Your taxonomy. Your curated dataset. At speed.\nEdit this illustrative taxonomy or replace it with your own domain. This bounded demo accepts up to 100 documents; the package supports streaming batches.') | |
| with gr.Row(): | |
| tax = gr.Code(value=TAXONOMY, language='yaml', label='Your taxonomy (YAML)', lines=13) | |
| docs = gr.Textbox(value=DOCUMENTS, label='Documents (JSON array of text strings)', lines=13, max_length=202000) | |
| run = gr.Button('Curate dataset', variant='primary') | |
| timing = gr.Markdown() | |
| table = gr.Dataframe(headers=['row_index', 'keep', 'labels'], interactive=False, label='Selection decisions') | |
| metrics = gr.JSON(label='Curation summary') | |
| evidence = gr.JSON(label='Matched evidence and original offsets') | |
| run.click(curate_custom, [tax, docs], [table, metrics, evidence, timing], api_name='curate', concurrency_limit=1, concurrency_id='range-cpu') | |
| with gr.Tab('Build your taxonomy'): | |
| gr.Markdown(GUIDE) | |