Merge latest upstream Long Videos
This commit is contained in:
+289
-70
@@ -66,21 +66,8 @@ HARDWARE = (
|
||||
(r"cuffs?|cuffed", "cuffs", "wrists"),
|
||||
(r"tape", "tape", "wrists"),
|
||||
)
|
||||
# Material and colour survive because they decide what the thing looks like:
|
||||
# "steel collar" must not come back as "collar" two shots later.
|
||||
# WHAT THE AUTHOR CALLED IT. This decides how much of the wording survives into
|
||||
# the guard clauses, and the guard is what every shot after the first repeats --
|
||||
# so a word missing here is a word the model stops hearing.
|
||||
#
|
||||
# It was twenty-odd words, and "a mirrored steel collar" came back as "steel
|
||||
# collar" while "a brushed nickel collar" came back as "collar". A bare "collar"
|
||||
# repeated once a shot is a bare collar, and the prior for that is a black
|
||||
# leather one -- which is exactly what was reported.
|
||||
#
|
||||
# Hyphenated compounds pass whole ("mirror-finish", "chrome-plated"), so an
|
||||
# unusual finish survives without being listed. Bare participles are NOT
|
||||
# accepted: "-ed" is a verb far more often than a modifier, and capturing one
|
||||
# would put an action into the name of the thing.
|
||||
# Preserve visual modifiers in continuity text. Hyphenated compounds pass whole;
|
||||
# arbitrary participles do not, because they are more often verbs than modifiers.
|
||||
_ADJ = (r"(?:[A-Za-z]+-[A-Za-z]+|"
|
||||
# materials
|
||||
r"steel|stainless|iron|metal|metallic|nickel|chrome|chromed|brass|"
|
||||
@@ -131,8 +118,26 @@ PART_VARIES = frozenset({"chain", "rope", "straps", "tape"})
|
||||
# outlives the beat that caused it, and the clause that says so has to be
|
||||
# writable from any later shot.
|
||||
REGION_OF = (
|
||||
# UNDERWEAR IS IN HERE TOO, on both halves of the body. The torso row has
|
||||
# listed a bra since the day it was written -- that is the report it exists
|
||||
# for, "a bra coming back on somebody topless" -- and the leg row never got
|
||||
# its counterpart, so region_of("thong") answered "". A garment that cannot
|
||||
# be placed latches no bare region, so underwear coming off said nothing
|
||||
# about the hips in that shot or in any shot after it, and an unspecified
|
||||
# region is filled by the model's own prior -- which for a hip is underwear.
|
||||
#
|
||||
# Worse, a beat saying somebody is NAKED looks each worn garment's region up
|
||||
# to take it off the body, so the one garment it could not place stayed
|
||||
# "worn" in the state while the text said she was nude. Reported as a thong
|
||||
# restored a beat after she undressed to get in the shower.
|
||||
#
|
||||
# No hardware. A chastity belt is in the layering vocabulary, but it is a
|
||||
# restraint: it is latched and held by its own mechanism, and a bare region
|
||||
# read off it would argue with that.
|
||||
(r"shorts|trousers|jeans|slacks|chinos|skirt|kilt|leggings|joggers|tights|"
|
||||
r"pantyhose|jeggings|culottes|tracksuit\s+bottoms", "legs",
|
||||
r"pantyhose|jeggings|culottes|tracksuit\s+bottoms|"
|
||||
r"panties|knickers|thong|g-?string|briefs|boxers|underwear|undies|"
|
||||
r"jockstrap|loincloth", "legs",
|
||||
"The legs are bare from the hip down"),
|
||||
(r"socks|stockings|hold-?ups|boots|shoes|trainers|sneakers|sandals|heels",
|
||||
"feet", "The feet and ankles are bare"),
|
||||
@@ -214,16 +219,34 @@ RELEASE_VERB = (
|
||||
# the engine knew a cell and a warehouse, the sampler did not, so a scene set in
|
||||
# either was a room to one reader and nowhere to the other. Same fault the
|
||||
# garment lists had, waiting to be reported.
|
||||
# MULTI-WORD ROOMS COME BEFORE BARE "room". _MOD is non-greedy, so it tries no
|
||||
# modifier first and the longest place wins -- but only if the long form is here to
|
||||
# win with. Without "locker\s+room", "heads to the locker room" read its destination
|
||||
# as "room": _MOD swallowed "locker" and the capture took what was left, so the film
|
||||
# was reported as entering "room" and the shot was told to arrive in one.
|
||||
#
|
||||
# A gym, a locker room and a court were in no list at all, which is worse than vague:
|
||||
# with the ORIGIN unknown, travel_anchor emitted nothing, so a beat walking out of a
|
||||
# gym was never told to walk and the set simply changed under the characters.
|
||||
PLACES = (r"hallway|hall|corridor|passage|landing|stairwell|staircase|stairs|"
|
||||
r"steps|bedroom|bathroom|washroom|kitchen|living\s+room|lounge|"
|
||||
r"dining\s+room|study|office|garage|basement|cellar|attic|loft|porch|"
|
||||
r"dining\s+room|locker\s+rooms?|changing\s+rooms?|dressing\s+rooms?|"
|
||||
r"waiting\s+rooms?|utility\s+rooms?|gymnasium|gym|classroom|library|"
|
||||
r"cafeteria|canteen|reception|laundry|pantry|sauna|balcony|terrace|"
|
||||
r"rooftop|elevator|court|pool|showers|shower|store|shop|studio|"
|
||||
r"study|office|garage|basement|cellar|attic|loft|porch|"
|
||||
r"veranda|garden|yard|driveway|street|alley|car\s?park|lobby|foyer|"
|
||||
r"doorway|cell|warehouse|barn|shed|van|truck|room")
|
||||
# Place words that are also ordinary verbs or everyday nouns. A reader with a
|
||||
# preposition in front of it ("in the study") can tell which sense is meant; the
|
||||
# free-text one cannot, and "she steps out", "they study the map" and "he lands
|
||||
# badly" are all commoner than the rooms they collide with.
|
||||
PLACE_ALSO_A_VERB = {"steps", "landing", "study", "lounge", "garage", "porch"}
|
||||
# "bar" and "lift" are deliberately NOT places in this file at all: bars are
|
||||
# restraint hardware here ("chained to the bars") and lifting is what happens to a
|
||||
# garment or a body, so behind a preposition they would both read as journeys.
|
||||
PLACE_ALSO_A_VERB = {"steps", "landing", "study", "lounge", "garage", "porch",
|
||||
"court", "pool", "shower", "showers", "store", "shop",
|
||||
"studio", "reception"}
|
||||
# A room is usually described, not just named -- "the tiled bathroom", "the long
|
||||
# hallway". Up to three adjectives, non-greedy so the NEAREST room still wins.
|
||||
_ROOM_MOD = (r"(?:(?!(?:of|the|an?|and|or|to|in|into|from|with|on|at|by|for|her|"
|
||||
@@ -272,15 +295,21 @@ _GAP = r"(?:\s+\S+){0,4}?\s+"
|
||||
TAKES_OFF = (r"(?:takes?|took|taking|pulls?|pulled|peels?|peeled|strips?|"
|
||||
r"stripped|shrugs?|slips?|slipped|steps?|gets?|got|kicks?|"
|
||||
r"kicked)" + _GAP + r"(?:off|out\s+of)\b"
|
||||
r"|\b(?:removes?|removed|removing|discards?|discarded|"
|
||||
r"undresses|undressed|unbuttons?|unzips?|unzipped)")
|
||||
r"|\b(?:removes?|removed|removing|discards?|discarded|sheds?|shedding|"
|
||||
r"undresses|undressed)")
|
||||
PUTS_ON = (r"(?:puts?|putting|pulls?|pulled|slips?|slipped|tugs?|tugged|"
|
||||
r"steps?|stepped|climbs?|climbed|gets?|got|wriggles?)" + _GAP +
|
||||
r"(?:on|into|back\s+on)\b"
|
||||
r"|\b(?:dresses?\s+in|dressed\s+in|buttons?|zips?\s+up|fastens?)")
|
||||
# UNZIPPING A JACKET LEAVES IT ON. These were removals, so "Owen unzips his jacket"
|
||||
# took the jacket out of every later shot and called his chest bare -- and "rolls up
|
||||
# his sleeves" a beat later rolled the sleeves of a shirt that had gone with it. They
|
||||
# open a garment; a beat that also takes it off says so ("and takes it off").
|
||||
DISPLACES = (r"(?:pulls?|pulled|pushes?|pushed|tugs?|tugged|hikes?|hiked|"
|
||||
r"rolls?|rolled|lifts?|lifted|yanks?|yanked|shoves?|shoved)"
|
||||
+ _GAP + r"(?:aside|up|down|open)\b")
|
||||
+ _GAP + r"(?:aside|up|down|open)\b"
|
||||
r"|\b(?:unzips?|unzipped|unbuttons?|unbuttoned|unfastens?|unfastened|"
|
||||
r"undoes|undid)\b")
|
||||
|
||||
# POSTURES and the _POSTURE list built from it used to live here. Nothing read
|
||||
# _POSTURE -- it was a second, dead copy of the posture vocabulary, and it had
|
||||
@@ -440,6 +469,17 @@ def _outside_speech(text):
|
||||
return _SPOKEN_SPAN.sub(" ", text or "")
|
||||
|
||||
|
||||
def staged_text(text):
|
||||
"""What a beat STAGES: not what anybody says, and not what the narration asks.
|
||||
|
||||
A narrated question is the same kind of thing as a line of speech. "Maya waits by
|
||||
the door. Will he come?" asks whether he will, which is to say he is not there --
|
||||
and the "he" read as him being present, so Will was described into the shot of
|
||||
her waiting for him. Removed for deciding who is in the shot, as speech is."""
|
||||
staged = _outside_speech(text)
|
||||
return " ".join(s for s in re.split(r"(?<=[.!?])\s+", staged) if not s.rstrip().endswith("?"))
|
||||
|
||||
|
||||
# THREE modifiers, not two: "mirrored stainless steel collar" is three words and
|
||||
# a noun, and the third was the first to be dropped.
|
||||
_HW_ONE = _rx(r"\b(" + _ADJ + r"(?:\s+" + _ADJ + r"){0,2}\s+)?("
|
||||
@@ -478,6 +518,14 @@ _GARMENT_ONE = _rx(r"\b(" + _ADJ + r"(?:\s+" + _ADJ + r"){0,2}\s+)?("
|
||||
_TAKES_OFF = _rx(r"\b" + TAKES_OFF + r"\b")
|
||||
_PUTS_ON = _rx(r"\b" + PUTS_ON + r"\b")
|
||||
_DISPLACES = _rx(r"\b" + DISPLACES + r"\b")
|
||||
# ...and the sentence can still finish the job after the garment is named. "Kate
|
||||
# unzips the denim skirt and steps out of it": the unzip opens it, the rest of the
|
||||
# sentence takes it off, and the removal verb comes after the item where the reader
|
||||
# above does not look.
|
||||
_OPENS_GARMENT = _rx(r"\b(?:unzips?|unzipped|unbuttons?|unbuttoned|unfastens?|unfastened|"
|
||||
r"undoes|undid|unhooks?|unhooked|unclasps?|unclasped)\b")
|
||||
_COMPLETES_OFF = _rx(r"\b(?:off|out\s+of|away|lets?\s+(?:it|them)\s+(?:fall|drop|slide)|"
|
||||
r"drops?\s+(?:it|them)|falls?\s+(?:to|down|away|off))\b")
|
||||
_MOVES = _rx(r"\b(?:walks?|walked|walking|goes|go|went|going|runs?|ran|running|"
|
||||
r"steps?|stepped|stepping|moves?|moved|moving|enters?|entered|"
|
||||
r"leaves?|left|leaving|crosses|crossed|crossing|climbs?|climbed|"
|
||||
@@ -595,11 +643,6 @@ def nudity_in(text):
|
||||
return out
|
||||
|
||||
|
||||
def bare_sentence(region):
|
||||
"""How to say a region is bare, or "" for one with no wording."""
|
||||
return next((s for _rx, r, s in _REGION_RX if r == region), "")
|
||||
|
||||
|
||||
def _bare_on(p, regions):
|
||||
for r in ([regions] if isinstance(regions, str) else regions):
|
||||
if r and r not in p.bare:
|
||||
@@ -653,11 +696,6 @@ def _nearest_part(parts, at, ats):
|
||||
return ""
|
||||
|
||||
|
||||
def hardware_in(text):
|
||||
"""Every piece of hardware named, as (canonical, part, as-written)."""
|
||||
return [(c, p, w) for c, p, w, _at in hardware_spans(text)]
|
||||
|
||||
|
||||
def position_spans(text):
|
||||
"""Every limb position named, as (name, at)."""
|
||||
out = []
|
||||
@@ -705,9 +743,15 @@ def place_in(text):
|
||||
|
||||
Behind a preposition, so a room has to be somewhere somebody IS. "Ana looks
|
||||
at the door" names no room -- and a door is not on the list in any case."""
|
||||
m = _PLACE_IN.search(text or "")
|
||||
text = text or ""
|
||||
m = _PLACE_IN.search(text)
|
||||
if not m:
|
||||
return ""
|
||||
clause = re.split(r"[.;!?]", text[:m.end()])[-1]
|
||||
present = re.search(r"\b(?:is|are|was|were|stands?|sits?|waits?|lies?|remains?)\b",
|
||||
clause, re.I)
|
||||
if not _MOVES.search(clause) and not present:
|
||||
return ""
|
||||
got = re.sub(r"\s+", " ", m.group(1).lower()).strip()
|
||||
# A BARE "room" NAMES NOWHERE. "Ana walks into the room" says she goes
|
||||
# inside, not which room -- and taking it as a place produced "The shot is
|
||||
@@ -730,6 +774,18 @@ def garments_in(text):
|
||||
return out
|
||||
|
||||
|
||||
_CLAUSE_BOUNDARY = re.compile(r"[,;]|\b(?:and|while)\b", re.I)
|
||||
|
||||
|
||||
def _clause_at(text, at, boundaries=None):
|
||||
"""Return the clause containing character offset `at` and its start offset."""
|
||||
if boundaries is None:
|
||||
boundaries = list(_CLAUSE_BOUNDARY.finditer(text or ""))
|
||||
lo = max((m.end() for m in boundaries if m.end() <= at), default=0)
|
||||
hi = min((m.start() for m in boundaries if m.start() > at), default=len(text))
|
||||
return text[lo:hi], lo
|
||||
|
||||
|
||||
|
||||
# Hardware, not clothing. Taking clothes off does not unlock anything, so these
|
||||
# are kept out of the garment answer -- the standing rule is that hardware is
|
||||
@@ -740,6 +796,32 @@ _NOT_CLOTHING = re.compile(
|
||||
r"straitjacket|spreader|hogtie|clamps?|clips?)$", re.I)
|
||||
_PHRASE_ONE = _rx(r"\b(?:" + GARMENT_PHRASES + r")\b")
|
||||
_WORD_ONE = _rx(r"^(?:" + GARMENT_WORDS + r")s?$")
|
||||
# The same list with NO optional plural, which is what says whether a trailing "s"
|
||||
# belongs to the word or was added to it. The vocabulary is clean on this: garments that
|
||||
# are inherently plural are listed only in the plural (boots, jeans, shorts, panties,
|
||||
# tights, leggings, socks, knickers, trousers, gloves) and the rest only in the singular
|
||||
# (skirt, vest, top), so "stem is itself a garment" is an exact test and not a guess.
|
||||
_WORD_EXACT = _rx(r"^(?:" + GARMENT_WORDS + r")$")
|
||||
|
||||
|
||||
def singular_garment(word):
|
||||
"""A garment word as the VOCABULARY spells it, so two readers cannot disagree.
|
||||
|
||||
Reported: several women take their skirts off and the skirts are back in the next
|
||||
beat. "their skirts" yields the token "skirts" while the sheet says "a denim
|
||||
skirt", and every reader downstream looks the token up in the sheet -- the scrub
|
||||
by pattern, infer_removals by entry head -- so a plural garment matched nothing
|
||||
and the removal silently did nothing at all. One woman undressing wrote "her
|
||||
skirt" and worked; the moment the subject went plural so did the garment.
|
||||
|
||||
A trailing "s" comes off only when the stem is ITSELF a garment word, which is an
|
||||
exact test here and not a guess: the vocabulary lists inherently plural garments
|
||||
only in the plural (boots, jeans, shorts, panties, tights, leggings, socks,
|
||||
knickers, trousers, gloves) and the rest only in the singular."""
|
||||
low = str(word or "").lower().strip("-")
|
||||
if low.endswith("s") and _WORD_EXACT.match(low[:-1]):
|
||||
return low[:-1]
|
||||
return low
|
||||
|
||||
|
||||
def garment_words(text):
|
||||
@@ -761,12 +843,60 @@ def garment_words(text):
|
||||
text = _PHRASE_ONE.sub(" ", text)
|
||||
for word in re.findall(r"\b[\w-]{3,}\b", text):
|
||||
low = word.lower().strip("-")
|
||||
if low in out or _NOT_CLOTHING.match(low):
|
||||
if _NOT_CLOTHING.match(low):
|
||||
continue
|
||||
if _WORD_ONE.match(low):
|
||||
if not _WORD_ONE.match(low):
|
||||
continue
|
||||
# The token the sheet wrote, not the one the beat happened to inflect. See
|
||||
# singular_garment -- shared with infer_removals so the two cannot disagree.
|
||||
low = singular_garment(low)
|
||||
if low not in out:
|
||||
out.append(low)
|
||||
return out
|
||||
|
||||
# A POSTURE DENIED IS NOT A POSTURE TAKEN.
|
||||
#
|
||||
# Reported: a woman chained by the ankles and forced into a squat stood up anyway.
|
||||
# The chain clauses were all correct -- the metal "already drawn to its full length,
|
||||
# so the position it fixes is the position that keeps" was on every shot after the
|
||||
# squat. What sat beside it was the posture latch saying she was STANDING, because
|
||||
# "She cannot stand." matched `stand` and nothing looked at the `cannot`. The latch
|
||||
# then carried that forward, so every later shot asserted, flatly and positively,
|
||||
# the one thing the chains were there to prevent. At cfg 1 a positive statement wins.
|
||||
#
|
||||
# The same shape as _in_a_request, which already suppresses a posture that is ASKED
|
||||
# for rather than taken. Attempts are here too: "tries to stand", "struggles to get
|
||||
# up", "strains to rise" are all bodies that have NOT got there, and reading them as
|
||||
# arrival is the same error in a friendlier disguise.
|
||||
#
|
||||
# A SHORT WINDOW on purpose -- the five words before the verb. The cue always sits
|
||||
# immediately in front of it ("cannot stand", "no longer able to stand"), and a wider
|
||||
# reach would let a `cannot` from a different clause silence a real posture.
|
||||
_POSTURE_DENIED = _rx(
|
||||
r"\b(?:cannot|can\s*not|can['\u2019]?t|could\s*not|could\s*n['\u2019]?t|"
|
||||
r"unable|never|not\s+able|no\s+longer\s+able|"
|
||||
r"does\s*n['\u2019]?t|does\s+not|do\s*n['\u2019]?t|did\s*n['\u2019]?t|did\s+not|"
|
||||
r"will\s+not|wo\s*n['\u2019]?t|fail(?:s|ed|ing)?|"
|
||||
r"tr(?:y|ies|ied|ying)|attempt(?:s|ed|ing)?|struggl(?:e|es|ed|ing)|"
|
||||
r"strain(?:s|ed|ing)?|fight(?:s|ing)?|want(?:s|ed)?|need(?:s|ed)?|"
|
||||
r"told|ordered|asked|begs?|begged)\b")
|
||||
|
||||
|
||||
def denied_posture(text, at):
|
||||
"""Is the posture verb at `at` negated, or only attempted, by what precedes it?"""
|
||||
before = str(text or "")[:max(0, int(at))]
|
||||
# A cue belongs to its OWN clause. Without stopping at the boundary, "McKenna
|
||||
# cannot kneel, so she sits" reached back past the comma and silenced the sitting
|
||||
# -- suppressing a posture the beat plainly states, which is the same class of
|
||||
# error in the other direction.
|
||||
cut = 0
|
||||
for _m in re.finditer(r"[,;:.!?]|\b(?:so|and|but|then|yet|while|as|before|after)\b",
|
||||
before, re.I):
|
||||
cut = _m.end()
|
||||
window = " ".join(re.findall(r"[\w'\u2019]+", before[cut:])[-5:])
|
||||
return bool(_POSTURE_DENIED.search(window))
|
||||
|
||||
|
||||
def posture_in(text):
|
||||
"""The posture this beat puts a body in. '' when it does not.
|
||||
|
||||
@@ -775,7 +905,7 @@ def posture_in(text):
|
||||
for that, but off the same vocabulary."""
|
||||
t = text or ""
|
||||
hits = sorted((m.start(), name) for name, rx in _POSTURE_OF
|
||||
for m in [rx.search(t)] if m)
|
||||
for m in rx.finditer(t) if not denied_posture(t, m.start()))
|
||||
return hits[0][1] if hits else ""
|
||||
|
||||
|
||||
@@ -820,7 +950,7 @@ _TRAILING_VERB = (r"take[sn]?|took|taking|pull(?:s|ed|ing)?|peel(?:s|ed|ing)?|"
|
||||
r"toss(?:es|ed)?|throw[s]?|threw|kick(?:s|ed|ing)?|"
|
||||
r"slide[s]?|slid|wriggle[sd]?|wiggle[sd]?")
|
||||
# ...and verbs that are a removal on their own, needing no particle.
|
||||
_UNDO_VERB = (r"remove[sd]?|removing|undress(?:es|ed)?|unzip(?:s|ped)?|"
|
||||
_UNDO_VERB = (r"remove[sd]?|removing|undress(?:es|ed)?|shed(?:s|ding)?|unzip(?:s|ped)?|"
|
||||
r"unbutton(?:s|ed)?|unhook(?:s|ed)?|unclasp(?:s|ed)?|unfasten(?:s|ed)?|"
|
||||
# Hardware comes off by being UNDONE, and these were missing: a beat
|
||||
# saying "unlocks the belt" left it described as worn for the rest of
|
||||
@@ -830,11 +960,26 @@ _UNDO_VERB = (r"remove[sd]?|removing|undress(?:es|ed)?|unzip(?:s|ped)?|"
|
||||
r"undo(?:es)?|undid")
|
||||
|
||||
|
||||
# WHERE A GARMENT ENDS UP ONCE IT IS OFF. A thing on the floor is not on a body,
|
||||
# and this is how a beat says so -- by DESTINATION, not by verb. Two readers need
|
||||
# the same list, for opposite reasons: the removal reader to call it a removal,
|
||||
# the restore reader to stop calling it one. "Lets the skirt fall" drops a lifted
|
||||
# skirt back over her legs; "lets the thong fall to the floor" is the thong coming
|
||||
# off, and the only difference between those two sentences is this list.
|
||||
FLOOR = (r"floor|ground|tiles?|tiling|lino|mat|bath\s*mat|rug|carpet|deck|boards|"
|
||||
r"concrete|grass|sand|bed|sofa|couch|chair|seat|stool|bench|basket|"
|
||||
r"hamper|laundry|pile|heap")
|
||||
TO_THE_FLOOR = (r"(?:to|on|onto|into|in)\s+(?:the|a|an|her|his|their)?\s*"
|
||||
r"(?:" + FLOOR + r")\b")
|
||||
_LANDS_OFF = _rx(r"\s*(?:fall(?:s|ing)?|drop(?:s|ping)?|land(?:s|ing)?)?\s*"
|
||||
+ TO_THE_FLOOR)
|
||||
|
||||
_DISPLACE_WAY = (r"back\s+up|back\s+down|down|up|aside|open|back|"
|
||||
r"off\s+(?:one|her|his|their)\s+shoulders?")
|
||||
_DISPLACE = re.compile(
|
||||
r"\b(?:" + _STRIP_VERB + r"|push(?:es|ed|ing)?|shove[sd]?|roll(?:s|ed|ing)?|"
|
||||
r"hitch(?:es|ed)?|hike[sd]?|open(?:s|ed)?|undo(?:es)?|unzip(?:s|ped)?|"
|
||||
r"hitch(?:es|ed)?|hike[sd]?|open(?:s|ed)?|undo(?:es)?|undid|unzip(?:s|ped)?|"
|
||||
r"unbutton(?:s|ed)?|unfasten(?:s|ed)?|unhook(?:s|ed)?|unclasp(?:s|ed)?|"
|
||||
# LIFTING A SKIRT IS DISPLACING IT, and none of these were here. Asked
|
||||
# for directly: "when the skirt has been lifted up to show the chastity
|
||||
# belt, that's when it should be shown". Lifting was not read as moving
|
||||
@@ -874,14 +1019,23 @@ def scene_name_for(head, scene):
|
||||
# a reference is pinning, so it is the worst one to describe loosely.
|
||||
item = re.sub(r"<\s*picture\s+\d+\s*>", " ", item, flags=re.I)
|
||||
item = re.sub(r"\s+", " ", item).strip()
|
||||
if not item or item.split()[-1].lower() != head:
|
||||
continue
|
||||
# Drop a leading article or possessive; they are not description.
|
||||
item = re.sub(r"^(?:a|an|the|her|his|their|its)\s+", "", item, flags=re.I)
|
||||
# The longest entry wins: a sheet that names it twice described it most
|
||||
# fully once, and the fuller name is the one worth carrying.
|
||||
if len(item) > len(best):
|
||||
best = item
|
||||
# ONE ENTRY CAN HOLD SEVERAL GARMENTS, and the name is the garment's own
|
||||
# part of it. "navy jacket over a white shirt" ends in "shirt", so the
|
||||
# whole entry came back as the shirt's name and "the navy jacket over a
|
||||
# white shirt open" was said about a shirt being unbuttoned. Split on the
|
||||
# words that join garments, and cut a "with ..." tail, which describes a
|
||||
# garment rather than naming it.
|
||||
for part in re.split(r"\s+(?:over|under|beneath|underneath|on\s+top\s+of|and)\s+",
|
||||
item, flags=re.I):
|
||||
part = re.split(r"\s+with\s+", part, flags=re.I)[0].strip()
|
||||
if not part or part.split()[-1].lower() != head:
|
||||
continue
|
||||
# Drop a leading article or possessive; they are not description.
|
||||
part = re.sub(r"^(?:a|an|the|her|his|their|its)\s+", "", part, flags=re.I)
|
||||
# The longest entry wins: a sheet that names it twice described it
|
||||
# most fully once, and the fuller name is the one worth carrying.
|
||||
if len(part) > len(best):
|
||||
best = part
|
||||
# The author's OWN capitalisation. Lowercasing turned "PVC" into "pvc" and
|
||||
# "Shiny white crop top" into all-lowercase -- a different token sequence than
|
||||
# was written, for a brand or material name that is capitalised for a reason.
|
||||
@@ -910,6 +1064,13 @@ def displaced_garments(beat, scene):
|
||||
if not way and re.match(r"\s*(?:lift|rais|hoist|gather|bunch)", m.group(0),
|
||||
re.I):
|
||||
way = "up"
|
||||
# ...and UNDOING a garment opens it. "Owen unzips his jacket" was a removal,
|
||||
# then (once it was not) nothing at all -- the jacket went back to being
|
||||
# described closed on the next shot, which is a jacket zipping itself up
|
||||
# across a cut.
|
||||
if not way and re.match(r"\s*(?:unzip|unbutton|unfasten|unhook|unclasp|undo|undid)",
|
||||
m.group(0), re.I):
|
||||
way = "open"
|
||||
if not way or not thing or thing in seen:
|
||||
continue
|
||||
# The garment has to be one the scene already dresses them in, and the head
|
||||
@@ -987,9 +1148,26 @@ def restored_garments(beat, scene):
|
||||
return []
|
||||
out, low = [], scene.lower()
|
||||
for m in _PUT_BACK_NAMED.finditer(beat):
|
||||
# ...UNLESS IT LANDS ON THE FLOOR. These verbs are the restore vocabulary
|
||||
# because that is what people write for a lifted skirt -- let fall, drop,
|
||||
# lower, let go of -- and the identical words take a garment OFF when the
|
||||
# sentence says where it lands. "Lets the thong fall to the floor" was read
|
||||
# as putting the thong back on: the author's removal, enacted backwards, and
|
||||
# from there the sheet described it as worn for the rest of the film.
|
||||
# Reported as a thong restored after she undressed. See FLOOR.
|
||||
if _LANDS_OFF.match(beat[m.end():]):
|
||||
continue
|
||||
thing = re.sub(r"\s+", " ", (m.group(1) or "")).strip().lower()
|
||||
if not thing:
|
||||
continue
|
||||
# ...AND THE CAPTURE CANNOT RUN THROUGH A PREPOSITION. The group takes
|
||||
# spaces so a sheet's "long grey skirt" comes back whole, and on "drops the
|
||||
# thong on the floor" it swallowed "thong on the floor" instead -- head
|
||||
# "floor" -- so the restore was keyed to the ROOM. The scene named a wet
|
||||
# floor, scene_name_for handed back "tiled bathroom with a wet floor", and
|
||||
# the beat was recorded as putting the bathroom back on.
|
||||
if re.search(r"\b(?:on|onto|to|into|in|at|over|under|from|with|and)\b", thing):
|
||||
continue
|
||||
head = thing.split()[-1]
|
||||
if len(head) < 3 or head not in low:
|
||||
continue
|
||||
@@ -1197,6 +1375,10 @@ def _alias_at(word, staged):
|
||||
return fallback
|
||||
|
||||
|
||||
_AUX_FOLLOWER = re.compile(r"\s+(?:I|you|he|she|we|they|it|there|this|that|anyone|someone|"
|
||||
r"everyone|anybody|somebody)\b")
|
||||
|
||||
|
||||
def names_in(beat, cast):
|
||||
"""Names this beat STAGES, in the order the sentence puts them.
|
||||
|
||||
@@ -1208,14 +1390,23 @@ def names_in(beat, cast):
|
||||
Speech-stripped, for the same reason it is everywhere else -- "McKenna, where
|
||||
are you?" is how absence gets written, and reading it as presence put a whole
|
||||
sheet entry into a shot the person is not in."""
|
||||
staged = _outside_speech(beat or "")
|
||||
staged = staged_text(beat or "")
|
||||
names = [str(n) for n in (cast or []) if n]
|
||||
hits, found = [], set()
|
||||
for n in names:
|
||||
m = re.search(r"\b" + re.escape(n) + r"\b", staged)
|
||||
if m:
|
||||
# A NAME THAT IS ALSO A WORD. "Will he come?" and "May I come in?" open a
|
||||
# sentence with the name followed by who the question is about, and read as
|
||||
# the person they staged Will and May -- a second character in a shot about
|
||||
# somebody waiting for them. A name followed straight away by a subject
|
||||
# pronoun at the start of a sentence is the verb; the next use can still be
|
||||
# the person ("Will opens the gate").
|
||||
for m in re.finditer(r"\b" + re.escape(n) + r"\b", staged):
|
||||
_opens = re.search(r"(?:^|[.!?]\s*[\"'\u201c]?)\s*$", staged[:m.start()])
|
||||
if _opens and _AUX_FOLLOWER.match(staged[m.end():]):
|
||||
continue
|
||||
hits.append((m.start(), n))
|
||||
found.add(n)
|
||||
break
|
||||
# A SHEET NAME IS OFTEN LONGER THAN WHAT THE BEATS CALL HER. "Mistress Vale"
|
||||
# on the sheet and "the Mistress" in every beat matched nothing, so her line
|
||||
# was in no shot at all and the model invented her from scratch each time.
|
||||
@@ -1415,20 +1606,35 @@ class SceneState:
|
||||
subject = who[0] if who else next(iter(list(self.people) or list(cast)
|
||||
or [""]))
|
||||
|
||||
hw = hardware_in(beat)
|
||||
applying = bool(hw) and bool(_APPLY.search(beat)) and not _RELEASE.search(beat)
|
||||
spans = hardware_spans(beat)
|
||||
garments = list(_GARMENT_ONE.finditer(beat))
|
||||
boundaries = list(_CLAUSE_BOUNDARY.finditer(beat)) if spans or garments else []
|
||||
applying = bool(spans) and bool(_APPLY.search(beat))
|
||||
releasing = bool(_RELEASE.search(beat))
|
||||
|
||||
if applying:
|
||||
wearer = _wearer(beat, who, subject)
|
||||
p = self.person(wearer)
|
||||
if applying or releasing:
|
||||
# MODIFIERS BIND TO THE NEAREST ITEM. "handcuffs her wrists behind
|
||||
# her back and locks a steel collar around her neck, chained to the
|
||||
# wall" carries two modifiers and two items; giving both modifiers
|
||||
# to both items produced handcuffs chained to a wall they were never
|
||||
# near, and a collar held behind a back.
|
||||
spans = hardware_spans(beat)
|
||||
for canon, part, written, at in spans:
|
||||
clause, lo = _clause_at(beat, at, boundaries)
|
||||
item_at = at - lo
|
||||
apply_at = max((m.start() for m in _APPLY.finditer(clause)
|
||||
if m.start() <= item_at), default=-1)
|
||||
release_at = max((m.start() for m in _RELEASE.finditer(clause)
|
||||
if m.start() <= item_at), default=-1)
|
||||
if apply_at < 0 and release_at < 0:
|
||||
continue
|
||||
local_who = names_in(clause, cast)
|
||||
wearer = _wearer(clause, local_who or who, subject)
|
||||
p = self.person(wearer)
|
||||
if release_at >= 0:
|
||||
keys = [k for k in list(p.hardware) if k[0] == canon]
|
||||
for key in keys:
|
||||
changed["released"].append((wearer, p.hardware.pop(key)))
|
||||
continue
|
||||
# KEYED BY THE PAIR. A chain on the ankles and a chain on the
|
||||
# wrists are two restraints; keyed by name alone the second
|
||||
# overwrote the first and one of them was never drawn again.
|
||||
@@ -1446,7 +1652,7 @@ class SceneState:
|
||||
# Its anchor needs no transferring either: with the tether gone from
|
||||
# the spans, the anchor binds to the nearest remaining item, which
|
||||
# is the one it was always describing.
|
||||
elif releasing:
|
||||
if releasing and not spans:
|
||||
# Whoever is actually wearing it. "The guard unlocks the handcuffs"
|
||||
# names only the agent, and taking the subject there tried to
|
||||
# release hardware from the man holding the key.
|
||||
@@ -1457,13 +1663,8 @@ class SceneState:
|
||||
# Released by NAME, whatever part it is on: an unlocking beat says
|
||||
# "unlocks the chain", not which of two chains, and matching the
|
||||
# pair left one fastened forever.
|
||||
_kinds = {c for c, _pt, _w in hw}
|
||||
named = [k for k in list(p.hardware) if k[0] in _kinds]
|
||||
if named:
|
||||
for key in named:
|
||||
changed["released"].append((wearer, p.hardware.pop(key)))
|
||||
elif re.search(r"\b(?:them|it|her|him|everything|all\s+of\s+it)\b",
|
||||
beat, re.I):
|
||||
if re.search(r"\b(?:them|it|her|him|everything|all\s+of\s+it)\b",
|
||||
beat, re.I):
|
||||
# "the guard releases her" names no item, so all of it comes off.
|
||||
while p.hardware:
|
||||
changed["released"].append((wearer, p.hardware.popitem()[1]))
|
||||
@@ -1472,12 +1673,26 @@ class SceneState:
|
||||
# to be named -- a bare "she undresses" says nothing about which garment,
|
||||
# and guessing is how a garment came off a beat before the beat that
|
||||
# took it off.
|
||||
wearer_g = _wearer(beat, who, subject) if len(who) > 1 else subject
|
||||
if wearer_g:
|
||||
p = self.person(wearer_g)
|
||||
for g in garments_in(beat):
|
||||
if subject:
|
||||
for m in garments:
|
||||
g = f"{(m.group(1) or '').strip()} {m.group(2)}".strip().lower()
|
||||
key = _garment_key(g)
|
||||
if _TAKES_OFF.search(beat):
|
||||
clause, lo = _clause_at(beat, m.start(), boundaries)
|
||||
item_at = m.start() - lo
|
||||
actions = [(x.start(), "off") for x in _TAKES_OFF.finditer(clause)
|
||||
if x.start() <= item_at]
|
||||
actions += [(x.start(), "on") for x in _PUTS_ON.finditer(clause)
|
||||
if x.start() <= item_at]
|
||||
actions += [(x.start(), "aside") for x in _DISPLACES.finditer(clause)
|
||||
if x.start() <= item_at]
|
||||
action = max(actions, default=(-1, ""))[1]
|
||||
if action == "aside" and _OPENS_GARMENT.search(clause[:item_at]):
|
||||
if _COMPLETES_OFF.search(re.split(r"[.;!?]", beat[m.end():])[0]):
|
||||
action = "off"
|
||||
local_who = names_in(clause, cast)
|
||||
wearer_g = _wearer(clause, local_who, subject)
|
||||
p = self.person(wearer_g)
|
||||
if action == "off":
|
||||
if key not in [_garment_key(x) for x in p.removed]:
|
||||
p.removed.append(g)
|
||||
changed["removed"].append((wearer_g, g))
|
||||
@@ -1485,7 +1700,7 @@ class SceneState:
|
||||
p.displaced = [x for x in p.displaced
|
||||
if _garment_key(x) != key]
|
||||
_bare_on(p, region_of(g))
|
||||
elif _PUTS_ON.search(beat):
|
||||
elif action == "on":
|
||||
if key not in [_garment_key(x) for x in p.worn]:
|
||||
p.worn.append(g)
|
||||
changed["worn"].append((wearer_g, g))
|
||||
@@ -1496,7 +1711,7 @@ class SceneState:
|
||||
# dresses is told for the rest of the film that the region is
|
||||
# bare, over the garment she just put on.
|
||||
_bare_off(p, region_of(g))
|
||||
elif _DISPLACES.search(beat):
|
||||
elif action == "aside":
|
||||
if key not in [_garment_key(x) for x in p.displaced]:
|
||||
p.displaced.append(g)
|
||||
changed["displaced"].append((wearer_g, g))
|
||||
@@ -1506,7 +1721,11 @@ class SceneState:
|
||||
# ever said what was on the chest.
|
||||
_nude = nudity_in(beat)
|
||||
if _nude:
|
||||
for n in (who or ([subject] if subject else [])):
|
||||
nude_at = min((m.start() for rx, _regions in _NUDITY_RX
|
||||
for m in rx.finditer(beat)), default=len(beat))
|
||||
located = [(abs(beat.find(n) - nude_at), n) for n in who if beat.find(n) >= 0]
|
||||
owners = [min(located)[1]] if located else ([subject] if subject else [])
|
||||
for n in owners:
|
||||
q = self.person(n)
|
||||
_bare_on(q, _nude)
|
||||
# ...and it takes the garments OFF. Saying somebody is topless
|
||||
|
||||
Reference in New Issue
Block a user