File size: 8,001 Bytes
3a1d713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88b1ea9
 
 
 
 
 
 
 
3a1d713
 
 
 
 
 
 
 
 
 
5411f5f
 
 
 
bb4c28d
 
 
88b1ea9
726d852
 
 
 
0b5c981
 
88b1ea9
3a1d713
 
 
 
 
88b1ea9
 
 
 
 
 
 
 
 
 
bb4c28d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a1d713
88b1ea9
726d852
 
 
 
bb4c28d
 
 
88b1ea9
3a1d713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88b1ea9
0b5c981
88b1ea9
 
5411f5f
bb4c28d
726d852
3a1d713
88b1ea9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b5c981
 
88b1ea9
 
5411f5f
726d852
88b1ea9
3a1d713
 
 
 
 
 
 
 
 
bb4c28d
 
 
 
 
 
 
 
 
 
 
 
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
"""Bundled example corpora, so a visitor can run CCR without their own data.

Every entry is a file in sample_data/ plus the provenance a researcher needs to
cite it. The citation travels into run metadata (see jobs.py), because results
produced on someone else's corpus have to carry the attribution into whatever
the researcher publishes - a demo dataset is still data with authors.

Adding one: drop the CSV in sample_data/, add an entry here, and say in
`preprocessing` exactly what was done to it. A test checks every file exists,
parses, and has the declared text column.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass
from pathlib import Path

SAMPLES_DIR = Path(__file__).resolve().parents[2] / "sample_data"


@dataclass(frozen=True)
class ExampleCorpus:
    """A corpus offered in Step 1.

    Small ones ship in sample_data/ (`filename`); large ones live in object
    storage under the `examples/` prefix (`storage_key`), because a 38 MB file
    in the repo would ride in the Docker image and the Space repo on every
    deploy. Exactly one of the two is set.
    """

    id: str
    name: str
    filename: str
    text_column: str
    language: str
    n_rows: int
    description: str
    citation: str
    source_url: str
    preprocessing: str
    # The corpus's OWN licence, which is not the platform's. A researcher has to
    # know the terms they are working under before they analyse someone else's
    # data, so it is shown wherever the corpus is described.
    license: str = ""
    # First in the list and selected on load. NOT a claim that it is 'the'
    # corpus: the PI's ask was for CAMEL to be available and ready to use.
    preselected: bool = False
    storage_key: str = ""
    # Optional mark for the dataset, served from sample_data/ at /samples.
    # Shown in the info dialog, where it identifies THIS dataset - not in the
    # picker row, which lists several and would imply they share a source.
    logo: str = ""
    # One line under the name in the sample-data list, so a researcher can judge
    # fit before running. The row already shows the count and language.
    detail: str = ""

    @property
    def path(self) -> Path:
        return SAMPLES_DIR / self.filename

    @property
    def bundled(self) -> bool:
        return not self.storage_key

    def available(self) -> bool:
        """Bundled files are checked on disk; stored ones are assumed present -
        a HEAD on every listing would add a network round trip to page load,
        and a missing object surfaces as a clear 404 on selection instead."""
        return self.path.exists() if self.bundled else True

    def blocked_reason(self, row_ceiling: int) -> str:
        """Why this instance cannot run this corpus, or "" if it can.

        A corpus larger than CCR_MAX_ROWS is refused at ingest, so offering it
        as a working option produces a dead end. Saying so up front, and why,
        beats an error after the click - and it corrects itself the moment the
        deployment raises the limit.
        """
        if not self.available():
            return "Not loaded on this instance."
        if row_ceiling and self.n_rows > row_ceiling:
            return (
                f"Too large for this instance: {self.n_rows:,} texts against a "
                f"{row_ceiling:,}-row limit."
            )
        return ""

    def public(self, row_ceiling: int = 0) -> dict:
        data = asdict(self)
        data["bundled"] = self.bundled
        data["logo_url"] = (
            f"/samples/{self.logo}" if self.logo and (SAMPLES_DIR / self.logo).exists() else ""
        )
        data.pop("logo", None)
        blocked = self.blocked_reason(row_ceiling)
        data["usable"] = not blocked
        data["blocked_reason"] = blocked
        data.pop("storage_key", None)  # internal location, not the user's business
        return data


EXAMPLES: list[ExampleCorpus] = [
    ExampleCorpus(
        id="camel_sample",
        name="CAMEL corpus (sample)",
        filename="camel_sample.csv",
        text_column="text",
        language="en",
        n_rows=999,
        description=(
            "The Cultural and Moral Expressions in Language (CAMEL) corpus consists of "
            "over 57 thousand texts taken from several platforms and annotated for 25 "
            "different cultural and moral constructs. This is a 999-text sample drawn "
            "from it, keeping the platform mix of the full corpus (Reddit, Twitter, "
            "IMDB, news, Wikipedia, and others). The full corpus, including the "
            "annotations, is on Hugging Face."
        ),
        citation=(
            "Zewail, A., Setia, A., Mohammadsadegh, R., Seker, F., Hajian, A., Sosa, H., "
            "Morhayim, L., Reddy, S., & Atari, M. (2026). The Cultural and Moral "
            "Expressions in Language (CAMEL) corpus [Preprint]."
        ),
        source_url="https://huggingface.co/datasets/Culture-and-Morality-Lab/CAMEL_Dataset",
        preprocessing=(
            "Usernames were removed by the corpus authors. For this sample: texts drawn "
            "proportionally by source platform with a fixed seed "
            "(scripts/build_camel_sample.py), whitespace trimmed, and only the text and "
            "source_platform columns kept. Text is otherwise verbatim."
        ),
        detail=(
            "Median 25 words, mean 69. Runs in seconds on any model. "
            "Best for a first look at how CCR works."
        ),
        license="CC BY-NC 4.0 (Creative Commons Attribution-NonCommercial 4.0). Free for non-commercial research with attribution; see the dataset page for the full terms.",
        preselected=True,
        logo="camel_logo.png",
    ),
    ExampleCorpus(
        id="camel_full",
        name="CAMEL corpus (full)",
        filename="",
        storage_key="examples/camel_full.csv",
        text_column="text",
        language="en",
        n_rows=57174,
        description=(
            "The complete Cultural and Moral Expressions in Language (CAMEL) corpus: "
            "57,174 texts from Reddit, Twitter, IMDB, news, Wikipedia, the Internet "
            "Archive and other sources, annotated by the lab for 25 cultural and moral "
            "constructs. The annotation columns come with it, so CCR scores can be "
            "compared against the human ratings."
        ),
        citation=(
            "Zewail, A., Setia, A., Mohammadsadegh, R., Seker, F., Hajian, A., Sosa, H., "
            "Morhayim, L., Reddy, S., & Atari, M. (2026). The Cultural and Moral "
            "Expressions in Language (CAMEL) corpus [Preprint]."
        ),
        source_url="https://huggingface.co/datasets/Culture-and-Morality-Lab/CAMEL_Dataset",
        preprocessing=(
            "Usernames were removed by the corpus authors. Otherwise the corpus exactly "
            "as published: all 64 columns, text verbatim, no sampling."
        ),
        detail=(
            "Median 26 words, up to 1,770. Expect a few minutes on MiniLM and "
            "considerably longer on the large models; pick MiniLM unless you have a "
            "reason not to."
        ),
        license="CC BY-NC 4.0 (Creative Commons Attribution-NonCommercial 4.0). Free for non-commercial research with attribution; see the dataset page for the full terms.",
        logo="camel_logo.png",
    ),
]

BY_ID = {e.id: e for e in EXAMPLES}


def get(example_id: str) -> ExampleCorpus | None:
    return BY_ID.get(example_id)


def listed(row_ceiling: int = 0) -> list[dict]:
    """Examples this instance has, preselected one first.

    Entries too large for the row ceiling are still listed but marked unusable
    with a reason, rather than hidden: a corpus quietly vanishing is harder to
    explain than one that says why it is unavailable.
    """
    return [
        e.public(row_ceiling)
        for e in sorted(EXAMPLES, key=lambda e: not e.preselected)
        if e.available()
    ]