argus-cortex 0.4.4__tar.gz → 0.4.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/CHANGELOG.md +19 -1
  2. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/PKG-INFO +1 -1
  3. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/_version.py +2 -2
  4. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/moderation/policies.py +78 -18
  5. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_policies.py +35 -3
  6. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/.copier-answers.yml +0 -0
  7. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/.env.example +0 -0
  8. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/.github/workflows/ci.yml +0 -0
  9. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/.github/workflows/release.yml +0 -0
  10. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/.gitignore +0 -0
  11. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/CLAUDE.md +0 -0
  12. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/LICENSE +0 -0
  13. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/Makefile +0 -0
  14. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/README.md +0 -0
  15. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/pyproject.toml +0 -0
  16. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/__init__.py +0 -0
  17. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/backends.py +0 -0
  18. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/gpu.py +0 -0
  19. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/moderation/__init__.py +0 -0
  20. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/moderation/_http.py +0 -0
  21. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/moderation/nudenet.py +0 -0
  22. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/moderation/singguard.py +0 -0
  23. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/py.typed +0 -0
  24. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/server.py +0 -0
  25. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/store/__init__.py +0 -0
  26. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/store/blob.py +0 -0
  27. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/store/config.py +0 -0
  28. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/store/errors.py +0 -0
  29. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/store/lineage.py +0 -0
  30. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/store/models.py +0 -0
  31. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/store/schema.py +0 -0
  32. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/store/vector.py +0 -0
  33. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/taxonomy.py +0 -0
  34. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/wire.py +0 -0
  35. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/src/argus_cortex/xmp.py +0 -0
  36. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_backends.py +0 -0
  37. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_blob.py +0 -0
  38. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_gpu.py +0 -0
  39. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_moderation.py +0 -0
  40. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_server.py +0 -0
  41. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_smoke.py +0 -0
  42. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_store.py +0 -0
  43. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_store_pg_integration.py +0 -0
  44. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_taxonomy.py +0 -0
  45. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_vector.py +0 -0
  46. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_wire.py +0 -0
  47. {argus_cortex-0.4.4 → argus_cortex-0.4.5}/tests/test_xmp.py +0 -0
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.5] - 2026-10-31
11
+
12
+ ### Added
13
+ - **`adult-cinematic`**: `adult` with B narrowed to how-to / real targeting, and
14
+ Safe naming fictional crime (heist, fraud, dirty cops, war as setting, weapons
15
+ as a prop, drugs as a plot). Legal adult entertainment stays Safe the same
16
+ way as under `adult` (commercial porn, studio stills, an adult-site watermark).
17
+ Under evaluation against `adult` (#24). Letters and illegal A are unchanged.
18
+
19
+ ### Fixed
20
+ - **Adult policies no longer invite a "closest" H/I dump-bin.** Safe now names
21
+ legal adult entertainment, commercial porn, explicit sex, genitalia, a
22
+ suggestive pose, a commercial or studio set, and an adult-site watermark, and
23
+ says that if no risk bullet matches the verdict is Safe. Headings dropped the
24
+ word "Extreme" (`A. Illegal Sexual Content`, granular `I. Sexual Violence`),
25
+ which the model read as a bin for legal porn. Letters are unchanged.
26
+
10
27
  ## [0.4.4] - 2026-10-31
11
28
 
12
29
  ### Fixed
@@ -178,7 +195,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
178
195
  (`LocalBackend`, `RemoteBackend`, `RemoteProvider`, `with_retries`
179
196
  retrying only transient errors, `resolve_device`).
180
197
 
181
- [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.4...HEAD
198
+ [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.5...HEAD
199
+ [0.4.5]: https://github.com/smk762/argus-cortex/compare/v0.4.4...v0.4.5
182
200
  [0.4.4]: https://github.com/smk762/argus-cortex/compare/v0.4.3...v0.4.4
183
201
  [0.4.3]: https://github.com/smk762/argus-cortex/compare/v0.4.2...v0.4.3
184
202
  [0.4.2]: https://github.com/smk762/argus-cortex/compare/v0.4.1...v0.4.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: argus-cortex
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract
5
5
  Project-URL: Repository, https://github.com/smk762/argus-cortex
6
6
  Author: smk762
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.4.4'
22
- __version_tuple__ = version_tuple = (0, 4, 4)
21
+ __version__ = version = '0.4.5'
22
+ __version_tuple__ = version_tuple = (0, 4, 5)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -23,20 +23,36 @@ from typing import Literal
23
23
  from argus_cortex.moderation._http import ModerationConfigError
24
24
  from argus_cortex.moderation.singguard import rule_letter
25
25
 
26
- PolicyName = Literal["default", "adult", "adult-granular", "adult-cautious"]
26
+ PolicyName = Literal["default", "adult", "adult-granular", "adult-cautious", "adult-cinematic"]
27
27
  Mode = Literal["sfw", "nsfw"]
28
28
 
29
- # "adult-granular" and "adult-cautious" are under evaluation against "adult";
30
- # one adult policy remains at release, beside "default".
31
- POLICIES: tuple[PolicyName, ...] = ("default", "adult", "adult-granular", "adult-cautious")
29
+ # "adult-granular", "adult-cautious" and "adult-cinematic" are under evaluation
30
+ # against "adult"; one adult policy remains at release, beside "default".
31
+ POLICIES: tuple[PolicyName, ...] = (
32
+ "default",
33
+ "adult",
34
+ "adult-granular",
35
+ "adult-cautious",
36
+ "adult-cinematic",
37
+ )
32
38
  SFW_PREFIX = "sfw-"
33
39
 
34
- # Rules B to G, shared by every adult policy: the same letters and areas as
35
- # SingGuard's defaults, so rules B-G mean the same thing under all of
36
- # them. They are our wording, so their verdicts can differ from the built-ins.
37
- _OTHER_RULES = [
40
+ # Rules B to G, shared by every adult policy except cinematic B: the same
41
+ # letters and areas as SingGuard's defaults. They are our wording, so their
42
+ # verdicts can differ from the built-ins.
43
+ _CRIME_B = [
38
44
  "### B. Real-World Crimes & Public Safety",
39
45
  " - Content involving violent crime, weapons, other crimes, or public-safety threats.",
46
+ ]
47
+ # Depiction is not a hit; how-to, a named real target, and operational threats are.
48
+ _CRIME_B_CINEMATIC = [
49
+ "### B. Real-World Crimes & Public Safety",
50
+ (
51
+ " - Instructional or operational assistance for a real-world crime: how to commit one, "
52
+ "targeting a named real person or a real institution, or an operational public-safety threat."
53
+ ),
54
+ ]
55
+ _OTHER_AFTER_B = [
40
56
  "### C. Unethical Behavior",
41
57
  " - Content involving hate, harassment, manipulation, self-harm, or harmful misinformation.",
42
58
  " - Gore, dismemberment, and shock content meant to frighten or disgust.",
@@ -52,6 +68,7 @@ _OTHER_RULES = [
52
68
  "### G. Animal Abuse",
53
69
  " - Content involving cruelty to animals or the spread of animal abuse.",
54
70
  ]
71
+ _OTHER_RULES = [*_CRIME_B, *_OTHER_AFTER_B]
55
72
  _MINORS = " - Sexual content involving anyone under 18, or anyone who appears to be a minor."
56
73
  _NON_CONSENT = (
57
74
  " - Non-consensual sexual content, including hidden recording, sexual images of a real "
@@ -66,9 +83,13 @@ _EXTREME = " - Sexual violence, bestiality, or necrophilia."
66
83
  # two people; the posing line covers a solo adult. Its own trained taxonomy
67
84
  # files legal adult material under "Adult Content (Non-criminal)", and it cited
68
85
  # that as a violation under every adult policy, so Safe names it.
86
+ # Headings must not say "Extreme Content": the model treated explicit adult
87
+ # stills as I (or A) because that word was the closest dump bin, then picked
88
+ # H as "closest" even while saying the frame was consensual.
69
89
  _ADULT_CONTENT_SAFE = (
70
- ' - "Adult Content (Non-criminal)" is Safe under this policy: legal nudity and sexual content by adults, '
71
- "alone or together."
90
+ ' - "Adult Content (Non-criminal)" is Safe under this policy: legal adult entertainment, '
91
+ "legal nudity and sexual content by adults, alone or together, including commercial porn, "
92
+ "explicit sex, genitalia, a suggestive pose, a commercial or studio set, and an adult-site watermark."
72
93
  )
73
94
  _ADULT_SAFE = [
74
95
  " - Content that does not match any risk category, including consensual adult nudity and sexual content.",
@@ -78,6 +99,10 @@ _ADULT_SAFE = [
78
99
  " - Consent cannot be read from an image. A posed, camera-aware adult counts as consenting unless "
79
100
  "hidden recording, restraint, coercion, or an unconscious person is shown."
80
101
  ),
102
+ (
103
+ " - If no risk-category bullet matches, the verdict is Safe. Do not assign a risk letter "
104
+ "because it is the closest heading."
105
+ ),
81
106
  ]
82
107
 
83
108
  # Sent in full: SingGuard reads a bare "nsfw" as rule text. One broad rule A
@@ -85,7 +110,7 @@ _ADULT_SAFE = [
85
110
  ADULT_POLICY = "\n".join(
86
111
  [
87
112
  "## Risk Categories",
88
- "### A. Illegal or Extreme Sexual Content",
113
+ "### A. Illegal Sexual Content",
89
114
  _MINORS,
90
115
  _NON_CONSENT,
91
116
  _EXTREME,
@@ -96,9 +121,9 @@ ADULT_POLICY = "\n".join(
96
121
  )
97
122
 
98
123
  # The same coverage with a letter per hard case (A minors, H non-consent,
99
- # I sexual violence and extreme content), so the verdict itself says which
100
- # hard rule fired instead of only the write-up. Narrower rules may also fire
101
- # less often on doubtful cases; that is what the evaluation measures.
124
+ # I sexual violence), so the verdict itself says which hard rule fired
125
+ # instead of only the write-up. Narrower rules may also fire less often on
126
+ # doubtful cases; that is what the evaluation measures.
102
127
  ADULT_GRANULAR_POLICY = "\n".join(
103
128
  [
104
129
  "## Risk Categories",
@@ -108,7 +133,7 @@ ADULT_GRANULAR_POLICY = "\n".join(
108
133
  "### H. Non-Consensual Sexual Content",
109
134
  _NON_CONSENT,
110
135
  " - Sexual content showing someone asleep, unconscious, or otherwise unable to consent.",
111
- "### I. Sexual Violence & Extreme Content",
136
+ "### I. Sexual Violence",
112
137
  _EXTREME,
113
138
  "### Safe",
114
139
  *_ADULT_SAFE,
@@ -120,7 +145,7 @@ ADULT_GRANULAR_POLICY = "\n".join(
120
145
  ADULT_CAUTIOUS_POLICY = "\n".join(
121
146
  [
122
147
  "## Risk Categories",
123
- "### A. Illegal or Extreme Sexual Content",
148
+ "### A. Illegal Sexual Content",
124
149
  _MINORS,
125
150
  " - Nude or sexual content where any person's age is unclear and they could be under 18.",
126
151
  _NON_CONSENT,
@@ -136,8 +161,41 @@ ADULT_CAUTIOUS_POLICY = "\n".join(
136
161
  "sexual content where every person clearly appears to be an adult."
137
162
  ),
138
163
  (
139
- ' - "Adult Content (Non-criminal)" is Safe under this policy: legal nudity and sexual content '
140
- "where every person clearly appears to be an adult."
164
+ ' - "Adult Content (Non-criminal)" is Safe under this policy: legal adult entertainment '
165
+ "and legal nudity and sexual content where every person clearly appears to be an adult, "
166
+ "including commercial porn, explicit sex, genitalia, a suggestive pose, a commercial "
167
+ "or studio set, and an adult-site watermark."
168
+ ),
169
+ (
170
+ " - If no risk-category bullet matches, the verdict is Safe. Do not assign a risk letter "
171
+ "because it is the closest heading."
172
+ ),
173
+ ]
174
+ )
175
+
176
+ # ADULT_POLICY with B narrowed to how-to / real targeting, and cinematic
177
+ # crime named under Safe. Adult entertainment stays Safe the same way as
178
+ # under adult. Compared against adult; do not fold this into granular
179
+ # (that mix of lettering and B permissiveness is issue #24).
180
+ ADULT_CINEMATIC_POLICY = "\n".join(
181
+ [
182
+ "## Risk Categories",
183
+ "### A. Illegal Sexual Content",
184
+ _MINORS,
185
+ _NON_CONSENT,
186
+ _EXTREME,
187
+ *_CRIME_B_CINEMATIC,
188
+ *_OTHER_AFTER_B,
189
+ "### Safe",
190
+ *_ADULT_SAFE,
191
+ (
192
+ " - Legal adult entertainment is Safe under this policy, the same as under adult: "
193
+ "commercial porn, studio stills, explicit sex, and an adult-site watermark."
194
+ ),
195
+ (
196
+ " - Fictional or cinematic crime as a story or still is Safe: a bank robbery, a heist, "
197
+ "fraud such as stealing pensions, institutional corruption such as dirty cops, "
198
+ "non-sexual violence, weapons shown as a prop, drugs as a plot, and war as a setting."
141
199
  ),
142
200
  ]
143
201
  )
@@ -147,6 +205,7 @@ _TEXT: dict[str, str | None] = {
147
205
  "adult": ADULT_POLICY,
148
206
  "adult-granular": ADULT_GRANULAR_POLICY,
149
207
  "adult-cautious": ADULT_CAUTIOUS_POLICY,
208
+ "adult-cinematic": ADULT_CINEMATIC_POLICY,
150
209
  }
151
210
 
152
211
  # Rule letters that mean illegal content. "default" has none: its A covers
@@ -157,6 +216,7 @@ _ILLEGAL: dict[str, frozenset[str]] = {
157
216
  "adult": frozenset({"A"}),
158
217
  "adult-granular": frozenset({"A", "H", "I"}),
159
218
  "adult-cautious": frozenset({"A"}),
219
+ "adult-cinematic": frozenset({"A"}),
160
220
  }
161
221
 
162
222
 
@@ -8,7 +8,13 @@ from argus_cortex.moderation import ModerationConfigError, SingGuardClient, poli
8
8
 
9
9
 
10
10
  def test_every_policy_has_a_text_and_an_illegal_table() -> None:
11
- assert policies.POLICIES == ("default", "adult", "adult-granular", "adult-cautious")
11
+ assert policies.POLICIES == (
12
+ "default",
13
+ "adult",
14
+ "adult-granular",
15
+ "adult-cautious",
16
+ "adult-cinematic",
17
+ )
12
18
  assert policies.policy_text("default") is None # SingGuard's own rules: nothing is sent
13
19
  for name in policies.POLICIES[1:]:
14
20
  text = policies.policy_text(name)
@@ -22,6 +28,7 @@ def test_illegal_rules_are_per_policy() -> None:
22
28
  assert policies.illegal_rules("default") == frozenset() # its A is all sexual content, adult included
23
29
  assert policies.illegal_rules("adult-granular") == {"A", "H", "I"}
24
30
  assert policies.illegal_rules("adult") == policies.illegal_rules("adult-cautious") == {"A"}
31
+ assert policies.illegal_rules("adult-cinematic") == {"A"}
25
32
  # Each illegal letter is a heading in that policy's own text.
26
33
  for name in policies.POLICIES[1:]:
27
34
  for letter in policies.illegal_rules(name):
@@ -31,7 +38,8 @@ def test_illegal_rules_are_per_policy() -> None:
31
38
  def test_is_illegal_reads_a_rule_as_verdicts_report_it() -> None:
32
39
  assert policies.is_illegal("adult-granular", "H. Non-Consensual Sexual Content")
33
40
  assert policies.is_illegal("adult-granular", "I") and policies.is_illegal("adult-granular", "a.")
34
- assert policies.is_illegal("adult", "A. Illegal or Extreme Sexual Content")
41
+ assert policies.is_illegal("adult", "A. Illegal Sexual Content")
42
+ assert policies.is_illegal("adult", "A. Illegal or Extreme Sexual Content") # an older recorded title
35
43
  assert not policies.is_illegal("default", "A. Sexual Content Risk")
36
44
  for name in policies.POLICIES: # crime and animal abuse go to review, never deletion
37
45
  assert not policies.is_illegal(name, "B. Real-World Crimes & Public Safety")
@@ -39,10 +47,34 @@ def test_is_illegal_reads_a_rule_as_verdicts_report_it() -> None:
39
47
  assert not policies.is_illegal(name, "Safe")
40
48
 
41
49
 
50
+ def test_adult_safe_names_explicit_commercial_content() -> None:
51
+ for name in ("adult", "adult-granular", "adult-cautious", "adult-cinematic"):
52
+ text = policies.policy_text(name)
53
+ risks, safe = text.split("### Safe")
54
+ assert "Extreme Content" not in risks
55
+ assert "legal adult entertainment" in safe
56
+ assert "commercial porn" in safe
57
+ assert "adult-site watermark" in safe
58
+ assert "closest heading" in safe
59
+ assert "is not this rule" not in risks
60
+ assert "between adults" not in text
61
+
62
+
63
+ def test_adult_cinematic_narrows_crime_and_keeps_adult_entertainment_safe() -> None:
64
+ text = policies.policy_text("adult-cinematic")
65
+ risks, safe = text.split("### Safe")
66
+ assert "how to commit one" in risks
67
+ assert "violent crime, weapons, other crimes" not in risks
68
+ assert "bank robbery" in safe and "stealing pensions" in safe and "dirty cops" in safe
69
+ assert "Legal adult entertainment is Safe under this policy, the same as under adult" in safe
70
+ assert "between adults" not in text
71
+ assert policies.policy_text("adult-cinematic") != policies.policy_text("adult")
72
+
73
+
42
74
  def test_mode_is_by_name() -> None:
43
75
  assert policies.mode("default") == "sfw"
44
76
  assert policies.mode("sfw-strict") == "sfw" # a later SFW policy needs no table entry
45
- assert [policies.mode(n) for n in policies.POLICIES[1:]] == ["nsfw"] * 3
77
+ assert [policies.mode(n) for n in policies.POLICIES[1:]] == ["nsfw"] * 4
46
78
  assert policies.mode("defaults") == "nsfw"
47
79
 
48
80
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes