kwabs22 Claude Opus 5.5 commited on
Commit
6287002
·
1 Parent(s): 5b66adc

Add a pronoun rule option: most recent object or last clause's subject

Browse files

translate_to_scene and extend_scene take pronouns="recent" (default, the
user's choice) or "subject": the subject of the most recent clause, else
the most recent object. In "the cat sat on the table. the apple lay beside
it", recent gives the table and subject the cat. The rule is kept in the
ledger, so extend_scene follows it unless given another; the reference
question records which rule chose it. The UI has an "'it' means" selector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Files changed (4) hide show
  1. SPEC.md +13 -7
  2. app.py +14 -6
  3. vli/grammar.py +23 -8
  4. vli/pipeline.py +4 -4
SPEC.md CHANGED
@@ -113,11 +113,17 @@ on, `sentences` lists them). Within a sentence or across sentences:
113
  the one already mentioned" (default: the same cat), alternatives include "say
114
  'another cat' for a new one".
115
  - **"a/an/another <noun>"** always adds a new item.
116
- - **Pronouns** (it, he, she, they, him, her, them) refer to the most recent object,
117
- leaving out the clause's own subject when the pronoun is its object ("the apple lay
118
- beside it" can't mean the apple). Surfaced as "'it' = the table?", with the other
119
- objects as alternatives, most recent first. A pronoun with nothing before it is an
120
- error.
 
 
 
 
 
 
121
  - Each reference question carries `binds: {token, to}`; relations record which token
122
  named each end (`tokens: {subject, object}`), so a reference can be rebound.
123
  - **`bind`** (a `refine_scene` change): `{"id": "q10", "bind": "n1"}` rebinds a
@@ -131,9 +137,9 @@ on, `sentences` lists them). Within a sentence or across sentences:
131
  | Tool | In → out |
132
  |---|---|
133
  | `check_words` | sentence, primitives → per-word status, missing words, completions |
134
- | `translate_to_scene` | sentence, primitives, seed, html → ledger, PNG, GLB, mini HTML file, mini HTML text |
135
  | `refine_scene` | ledger, changes, seed, html → ledger, PNG, GLB, mini HTML file, mini HTML text |
136
- | `extend_scene` | ledger, sentence, primitives, seed, html → ledger, PNG, GLB, mini HTML file, mini HTML text |
137
  | `simplify_item` | ledger, item id, html, rings, samples → report, rung PNGs, rung GLBs, mini HTML file, mini HTML text |
138
  | `papercraft_rings` | ledger, item id, rings, samples, height_cm → pieces report, PNG per A4 page (to look at), SVG per A4 page (mm, to print) |
139
 
 
113
  the one already mentioned" (default: the same cat), alternatives include "say
114
  'another cat' for a new one".
115
  - **"a/an/another <noun>"** always adds a new item.
116
+ - **Pronouns** (it, he, she, they, him, her, them) refer back by a rule, `pronouns`
117
+ on `translate_to_scene`/`extend_scene`, kept in the ledger:
118
+ - `"recent"` (default): the most recent object.
119
+ - `"subject"`: the subject of the most recent clause, else the most recent object.
120
+ Closer to how readers take "it": in "the cat sat on the table. the apple lay
121
+ beside it", `recent` gives the table, `subject` the cat.
122
+
123
+ Either way the clause's own subject is left out when the pronoun is its object ("the
124
+ apple lay beside it" can't mean the apple), and the choice is surfaced as "'it' = the
125
+ cat?" with the other objects as alternatives, most recent first. A pronoun with
126
+ nothing before it is an error.
127
  - Each reference question carries `binds: {token, to}`; relations record which token
128
  named each end (`tokens: {subject, object}`), so a reference can be rebound.
129
  - **`bind`** (a `refine_scene` change): `{"id": "q10", "bind": "n1"}` rebinds a
 
137
  | Tool | In → out |
138
  |---|---|
139
  | `check_words` | sentence, primitives → per-word status, missing words, completions |
140
+ | `translate_to_scene` | sentence, primitives, seed, html, pronouns → ledger, PNG, GLB, mini HTML file, mini HTML text |
141
  | `refine_scene` | ledger, changes, seed, html → ledger, PNG, GLB, mini HTML file, mini HTML text |
142
+ | `extend_scene` | ledger, sentence, primitives, seed, html, pronouns → ledger, PNG, GLB, mini HTML file, mini HTML text |
143
  | `simplify_item` | ledger, item id, html, rings, samples → report, rung PNGs, rung GLBs, mini HTML file, mini HTML text |
144
  | `papercraft_rings` | ledger, item id, rings, samples, height_cm → pieces report, PNG per A4 page (to look at), SVG per A4 page (mm, to print) |
145
 
app.py CHANGED
@@ -116,7 +116,7 @@ def check_words(sentence: str, primitives_json: str = "{}") -> str:
116
 
117
 
118
  def translate_to_scene(sentence: str, primitives_json: str = "{}", seed: str = "0",
119
- html: str = "url") -> tuple[str, str, str, str, str]:
120
  """Translate an English sentence into a 3D scene.
121
 
122
  Produces the serial ledger (tokens in reading order, the items each word commits,
@@ -132,12 +132,16 @@ def translate_to_scene(sentence: str, primitives_json: str = "{}", seed: str = "
132
  html: "url" (default) returns the mini HTML as a file URL only; "inline" also returns
133
  its text (thousands of tokens: ask for it when building on the page); "skip" omits it.
134
  Iterate on the ledger and picture; the ledger can always re-create the page.
 
 
 
135
 
136
  Returns:
137
  (ledger JSON string, rendered PNG, GLB scene file, mini HTML file, mini HTML text or "").
138
  """
139
  try:
140
- *rest, page = pipeline.translate(sentence, _json(primitives_json, "primitives_json", {}), _seed(seed))
 
141
  return (*rest, *_html_out(page, html))
142
  except Exception as e:
143
  _fail(e)
@@ -182,7 +186,7 @@ def refine_scene(ledger_json: str, changes_json: str = "[]", seed: str = "0",
182
 
183
 
184
  def extend_scene(ledger_json: str, sentence: str, primitives_json: str = "{}", seed: str = "0",
185
- html: str = "url") -> tuple[str, str, str, str, str]:
186
  """Add the next sentence of a paragraph to an existing scene.
187
 
188
  Paragraphs are sentences plus bindings. "the cat" when a cat is already in the ledger,
@@ -197,13 +201,15 @@ def extend_scene(ledger_json: str, sentence: str, primitives_json: str = "{}", s
197
  primitives_json: JSON object mapping missing words to shapes, as in translate_to_scene.
198
  seed: Picks one of the configurations that satisfy the whole ledger.
199
  html: "url" (default), "inline" or "skip", as in translate_to_scene.
 
200
 
201
  Returns:
202
  (ledger JSON string, rendered PNG, GLB scene file, mini HTML file, mini HTML text or "").
203
  """
204
  try:
205
  ledger = _json(ledger_json, "ledger_json")
206
- *rest, page = pipeline.extend(ledger, sentence, _json(primitives_json, "primitives_json", {}), _seed(seed))
 
207
  return (*rest, *_html_out(page, html))
208
  except Exception as e:
209
  _fail(e)
@@ -390,6 +396,8 @@ with gr.Blocks(title="VisualLearningIdea") as demo:
390
  with gr.Row():
391
  sentence = gr.Textbox(label="Sentence", placeholder="the cat sat and the lamp glowed", scale=4)
392
  seed = gr.Textbox(label="Configuration seed", value="0", scale=1)
 
 
393
  status = gr.Markdown()
394
  suggest = gr.Radio(label="Suggestions", choices=[], interactive=True)
395
  with gr.Row():
@@ -439,13 +447,13 @@ with gr.Blocks(title="VisualLearningIdea") as demo:
439
  .then(_suggestions, words, [status, suggest, missing], api_visibility="private")
440
  suggest.select(_complete, [sentence, suggest], sentence, api_visibility="private")
441
  use_prim.click(_use_primitive, [primitives, missing, shape], primitives, api_visibility="private")
442
- go.click(translate_to_scene, [sentence, primitives, seed, html_mode],
443
  [ledger, picture, model, scene_file, scene_html], api_name="translate_to_scene") \
444
  .then(_html_preview, scene_file, scene_preview, api_visibility="private")
445
  refine.click(refine_scene, [ledger, changes, seed, html_mode],
446
  [ledger, picture, model, scene_file, scene_html], api_name="refine_scene") \
447
  .then(_html_preview, scene_file, scene_preview, api_visibility="private")
448
- extend.click(extend_scene, [ledger, next_sentence, primitives, seed, html_mode],
449
  [ledger, picture, model, scene_file, scene_html], api_name="extend_scene") \
450
  .then(_html_preview, scene_file, scene_preview, api_visibility="private")
451
  simp.click(simplify_item, [ledger, item_id, html_mode, ring_count, ring_samples],
 
116
 
117
 
118
  def translate_to_scene(sentence: str, primitives_json: str = "{}", seed: str = "0",
119
+ html: str = "url", pronouns: str = "recent") -> tuple[str, str, str, str, str]:
120
  """Translate an English sentence into a 3D scene.
121
 
122
  Produces the serial ledger (tokens in reading order, the items each word commits,
 
132
  html: "url" (default) returns the mini HTML as a file URL only; "inline" also returns
133
  its text (thousands of tokens: ask for it when building on the page); "skip" omits it.
134
  Iterate on the ledger and picture; the ledger can always re-create the page.
135
+ pronouns: Default referent for it/he/she/they: "recent" (default) = the most recent
136
+ object; "subject" = the subject of the most recent clause. Either way the choice is
137
+ an implied question with the other objects as alternatives. Kept in the ledger.
138
 
139
  Returns:
140
  (ledger JSON string, rendered PNG, GLB scene file, mini HTML file, mini HTML text or "").
141
  """
142
  try:
143
+ *rest, page = pipeline.translate(sentence, _json(primitives_json, "primitives_json", {}), _seed(seed),
144
+ (pronouns or "recent").strip().lower())
145
  return (*rest, *_html_out(page, html))
146
  except Exception as e:
147
  _fail(e)
 
186
 
187
 
188
  def extend_scene(ledger_json: str, sentence: str, primitives_json: str = "{}", seed: str = "0",
189
+ html: str = "url", pronouns: str = "") -> tuple[str, str, str, str, str]:
190
  """Add the next sentence of a paragraph to an existing scene.
191
 
192
  Paragraphs are sentences plus bindings. "the cat" when a cat is already in the ledger,
 
201
  primitives_json: JSON object mapping missing words to shapes, as in translate_to_scene.
202
  seed: Picks one of the configurations that satisfy the whole ledger.
203
  html: "url" (default), "inline" or "skip", as in translate_to_scene.
204
+ pronouns: "recent" or "subject", as in translate_to_scene; blank = the ledger's rule.
205
 
206
  Returns:
207
  (ledger JSON string, rendered PNG, GLB scene file, mini HTML file, mini HTML text or "").
208
  """
209
  try:
210
  ledger = _json(ledger_json, "ledger_json")
211
+ *rest, page = pipeline.extend(ledger, sentence, _json(primitives_json, "primitives_json", {}), _seed(seed),
212
+ (pronouns or "").strip().lower() or None)
213
  return (*rest, *_html_out(page, html))
214
  except Exception as e:
215
  _fail(e)
 
396
  with gr.Row():
397
  sentence = gr.Textbox(label="Sentence", placeholder="the cat sat and the lamp glowed", scale=4)
398
  seed = gr.Textbox(label="Configuration seed", value="0", scale=1)
399
+ pronoun_rule = gr.Radio(label="'it' means", choices=["recent", "subject"], value="recent", scale=1,
400
+ info="recent: last object · subject: last clause's subject")
401
  status = gr.Markdown()
402
  suggest = gr.Radio(label="Suggestions", choices=[], interactive=True)
403
  with gr.Row():
 
447
  .then(_suggestions, words, [status, suggest, missing], api_visibility="private")
448
  suggest.select(_complete, [sentence, suggest], sentence, api_visibility="private")
449
  use_prim.click(_use_primitive, [primitives, missing, shape], primitives, api_visibility="private")
450
+ go.click(translate_to_scene, [sentence, primitives, seed, html_mode, pronoun_rule],
451
  [ledger, picture, model, scene_file, scene_html], api_name="translate_to_scene") \
452
  .then(_html_preview, scene_file, scene_preview, api_visibility="private")
453
  refine.click(refine_scene, [ledger, changes, seed, html_mode],
454
  [ledger, picture, model, scene_file, scene_html], api_name="refine_scene") \
455
  .then(_html_preview, scene_file, scene_preview, api_visibility="private")
456
+ extend.click(extend_scene, [ledger, next_sentence, primitives, seed, html_mode, pronoun_rule],
457
  [ledger, picture, model, scene_file, scene_html], api_name="extend_scene") \
458
  .then(_html_preview, scene_file, scene_preview, api_visibility="private")
459
  simp.click(simplify_item, [ledger, item_id, html_mode, ring_count, ring_samples],
vli/grammar.py CHANGED
@@ -17,6 +17,9 @@ from .lexicon import ASSET_TIERS, LEXICON, PRIMITIVE_SHAPES, TIERS
17
  DETERMINERS = {"the", "a", "an", "another"}
18
  NEW_DETERMINERS = {"a", "an", "another"} # always introduce a new item
19
  PRONOUNS = {"it", "he", "she", "they", "him", "her", "them"}
 
 
 
20
  CONJUNCTIONS = {"and", "then"}
21
  COPULAS = {"is", "was", "are", "were"}
22
 
@@ -158,10 +161,13 @@ def _reference_question(qid, token, word, target, others, determiner=None):
158
  "binds": {"token": token["index"], "to": target["id"]}}
159
 
160
 
161
- def build_ledger(sentence, primitives=None, prior=None):
162
  """The ledger for one sentence. With `prior` (an existing ledger), token indices
163
  continue after it and words can refer back to its objects; the result holds only
164
- the new tokens, items and questions (extend_ledger merges them)."""
 
 
 
165
  primitives = parse_primitives(primitives)
166
  tokens = tokenize(sentence, primitives)
167
  unknown = [t["text"] for t in tokens if t["kind"] == "unknown"]
@@ -176,6 +182,7 @@ def build_ledger(sentence, primitives=None, prior=None):
176
  items, unresolved = [], []
177
  by_id = {i["id"]: i for i in prior["items"]} if prior else {}
178
  objects = [i for i in prior["items"] if i["kind"] == "object"] if prior else [] # in reading order
 
179
 
180
  def add_item(item, token):
181
  items.append(item)
@@ -211,7 +218,12 @@ def build_ledger(sentence, primitives=None, prior=None):
211
  if not pool:
212
  raise ValueError(f"'{t['text']}' refers to nothing yet: name the object first")
213
  item = pool[-1] # the most recent object it can mean
214
- q = _reference_question(f"q{t['index']}", t, t["text"], item, pool[-2::-1])
 
 
 
 
 
215
  t["opens"].append(q["id"])
216
  unresolved.append(q)
217
  else:
@@ -272,6 +284,7 @@ def build_ledger(sentence, primitives=None, prior=None):
272
  raise ValueError(f"'{relation}' needs an object noun")
273
  if obj is subject:
274
  raise ValueError(f"'{verb_tok['text']}' would relate the {subject['word']} to itself")
 
275
  add_item({"id": f"r{verb_tok['index']}", "kind": "relation", "relation": relation,
276
  "subject": subject["id"], "object": obj["id"], "token": verb_tok["index"],
277
  "tokens": {"subject": subject_tok, "object": obj_tok}, "frozen": False}, verb_tok)
@@ -287,19 +300,21 @@ def build_ledger(sentence, primitives=None, prior=None):
287
  unresolved.append({"id": "q_light", "token": None, "about": "light", "question": "what light?",
288
  "default_chosen": "daylight", "implied": True,
289
  "alternatives": ["rephrase: '<noun> shone' or '<noun> glowed'"]})
290
- return {"version": 1, "sentence": sentence, "primitives": primitives,
291
  "tokens": tokens, "items": items, "unresolved": unresolved}
292
 
293
 
294
- def extend_ledger(ledger, sentence, primitives=None):
295
- """Add a sentence to a ledger: the next sentence of a paragraph."""
 
296
  prims = {**ledger.get("primitives", {}), **parse_primitives(primitives)}
297
- new = build_ledger(sentence, prims, prior=ledger)
 
298
  lit = any(i.get("emits_light") for i in ledger["items"] + new["items"])
299
  unresolved = [q for q in ledger["unresolved"] + new["unresolved"] if not (lit and q["id"] == "q_light")]
300
  return {**ledger, "sentence": ledger["sentence"].rstrip(" .") + ". " + sentence.strip(),
301
  "sentences": ledger.get("sentences", [ledger["sentence"]]) + [sentence.strip()],
302
- "primitives": prims, "tokens": ledger["tokens"] + new["tokens"],
303
  "items": ledger["items"] + new["items"], "unresolved": unresolved}
304
 
305
 
 
17
  DETERMINERS = {"the", "a", "an", "another"}
18
  NEW_DETERMINERS = {"a", "an", "another"} # always introduce a new item
19
  PRONOUNS = {"it", "he", "she", "they", "him", "her", "them"}
20
+ # What a pronoun means by default: the most recent object, or the subject of the most
21
+ # recent clause (how readers usually take "it": the cat sat on the table; it ... = the cat).
22
+ PRONOUN_RULES = ("recent", "subject")
23
  CONJUNCTIONS = {"and", "then"}
24
  COPULAS = {"is", "was", "are", "were"}
25
 
 
161
  "binds": {"token": token["index"], "to": target["id"]}}
162
 
163
 
164
+ def build_ledger(sentence, primitives=None, prior=None, pronouns="recent"):
165
  """The ledger for one sentence. With `prior` (an existing ledger), token indices
166
  continue after it and words can refer back to its objects; the result holds only
167
+ the new tokens, items and questions (extend_ledger merges them). `pronouns` picks
168
+ the default referent (PRONOUN_RULES); the other candidates stay as alternatives."""
169
+ if pronouns not in PRONOUN_RULES:
170
+ raise ValueError(f"pronouns must be one of {list(PRONOUN_RULES)}, got {pronouns!r}")
171
  primitives = parse_primitives(primitives)
172
  tokens = tokenize(sentence, primitives)
173
  unknown = [t["text"] for t in tokens if t["kind"] == "unknown"]
 
182
  items, unresolved = [], []
183
  by_id = {i["id"]: i for i in prior["items"]} if prior else {}
184
  objects = [i for i in prior["items"] if i["kind"] == "object"] if prior else [] # in reading order
185
+ subjects = [r["subject"] for r in prior["items"] if r["kind"] == "relation"] if prior else [] # clause order
186
 
187
  def add_item(item, token):
188
  items.append(item)
 
218
  if not pool:
219
  raise ValueError(f"'{t['text']}' refers to nothing yet: name the object first")
220
  item = pool[-1] # the most recent object it can mean
221
+ if pronouns == "subject":
222
+ ids = {o["id"]: o for o in pool}
223
+ item = next((ids[s] for s in reversed(subjects) if s in ids), item)
224
+ q = _reference_question(f"q{t['index']}", t, t["text"], item,
225
+ [o for o in reversed(pool) if o is not item])
226
+ q["rule"] = pronouns
227
  t["opens"].append(q["id"])
228
  unresolved.append(q)
229
  else:
 
284
  raise ValueError(f"'{relation}' needs an object noun")
285
  if obj is subject:
286
  raise ValueError(f"'{verb_tok['text']}' would relate the {subject['word']} to itself")
287
+ subjects.append(subject["id"])
288
  add_item({"id": f"r{verb_tok['index']}", "kind": "relation", "relation": relation,
289
  "subject": subject["id"], "object": obj["id"], "token": verb_tok["index"],
290
  "tokens": {"subject": subject_tok, "object": obj_tok}, "frozen": False}, verb_tok)
 
300
  unresolved.append({"id": "q_light", "token": None, "about": "light", "question": "what light?",
301
  "default_chosen": "daylight", "implied": True,
302
  "alternatives": ["rephrase: '<noun> shone' or '<noun> glowed'"]})
303
+ return {"version": 1, "sentence": sentence, "primitives": primitives, "pronouns": pronouns,
304
  "tokens": tokens, "items": items, "unresolved": unresolved}
305
 
306
 
307
+ def extend_ledger(ledger, sentence, primitives=None, pronouns=None):
308
+ """Add a sentence to a ledger: the next sentence of a paragraph. `pronouns`
309
+ defaults to the rule the ledger was built with."""
310
  prims = {**ledger.get("primitives", {}), **parse_primitives(primitives)}
311
+ pronouns = pronouns or ledger.get("pronouns", "recent")
312
+ new = build_ledger(sentence, prims, prior=ledger, pronouns=pronouns)
313
  lit = any(i.get("emits_light") for i in ledger["items"] + new["items"])
314
  unresolved = [q for q in ledger["unresolved"] + new["unresolved"] if not (lit and q["id"] == "q_light")]
315
  return {**ledger, "sentence": ledger["sentence"].rstrip(" .") + ". " + sentence.strip(),
316
  "sentences": ledger.get("sentences", [ledger["sentence"]]) + [sentence.strip()],
317
+ "primitives": prims, "pronouns": pronouns, "tokens": ledger["tokens"] + new["tokens"],
318
  "items": ledger["items"] + new["items"], "unresolved": unresolved}
319
 
320
 
vli/pipeline.py CHANGED
@@ -66,12 +66,12 @@ def outputs(ledger, seed):
66
  return json.dumps(ledger, indent=1), png, glb, html.scene_page(ledger, meshes, config)
67
 
68
 
69
- def translate(sentence, primitives=None, seed=0):
70
- return outputs(build_ledger(sentence, primitives), seed)
71
 
72
 
73
- def extend(ledger, sentence, primitives=None, seed=0):
74
- return outputs(extend_ledger(ledger, sentence, primitives), seed)
75
 
76
 
77
  def _bind(ledger, src, dst):
 
66
  return json.dumps(ledger, indent=1), png, glb, html.scene_page(ledger, meshes, config)
67
 
68
 
69
+ def translate(sentence, primitives=None, seed=0, pronouns="recent"):
70
+ return outputs(build_ledger(sentence, primitives, pronouns=pronouns), seed)
71
 
72
 
73
+ def extend(ledger, sentence, primitives=None, seed=0, pronouns=None):
74
+ return outputs(extend_ledger(ledger, sentence, primitives, pronouns), seed)
75
 
76
 
77
  def _bind(ledger, src, dst):