diff --git a/freevideo_engine/agent/cast.py b/freevideo_engine/agent/cast.py index 6b2a2e21..3e7cfcb6 100644 --- a/freevideo_engine/agent/cast.py +++ b/freevideo_engine/agent/cast.py @@ -25,7 +25,7 @@ ORIGINS = ('user', 'agent', 'builtin') LABELS = schema.LABELS EDITABLE = ('name', 'description', 'images', 'voice') -MAX_IMAGES = 6 +MAX_IMAGES = schema.MAX_CAST_IMAGES MAX_CANDIDATES = 6 MAX_RECEIPTS = 64 VOICE_SECONDS = (2, 15) @@ -40,7 +40,7 @@ CLOSE_UP = dict(aspect='1:1', seconds=4) SCENE = dict(aspect='16:9', seconds=6) -# The sheet of every view a shot carries for each person, put together here from the card's own pictures: up to six +# The sheet of every view a continuing shot carries for a single person, put together here from the card's own pictures: up to six # equal cells (three to a row) on a light grey ground, each picture scaled whole and centred, no text. SHEET_VERSION = 1 SHEET_ORDER = ('front', 'three_quarter', 'side', 'back', 'face') # then any other label, in card order @@ -140,6 +140,12 @@ def _plain(item): return None +def _caption(row): + """A requested picture's caption on one line, as picture fields ({} when it has none).""" + text = ' '.join(str(row.get('caption') or '').split()) + return {'caption': text} if text else {} + + def _check(record, stem): card = record['card'] if (card['id'] != stem or not CARD.fullmatch(stem) or card['kind'] not in KINDS @@ -161,7 +167,8 @@ def _check(record, stem): if not isinstance(card[key], list) or len(card[key]) > limit: raise ValueError('Invalid pictures') for row in card[key]: - if not isinstance(row['label'], str) or not isinstance(row['media']['file'], str): + if (not isinstance(row['label'], str) or not isinstance(row['media']['file'], str) + or not isinstance(row.get('caption', ''), str)): raise ValueError('Invalid picture') unknown_labels |= row['label'] not in LABELS if card['voice'] is not None: @@ -383,7 +390,7 @@ def create(self, value, *, by='builtin', ref=None, made_for=None, local=True, cl images = [] for index, row in enumerate(value.get('images', [])): entry = self._picture_at(identity, row['media'], by, index) - images.append(dict(entry, label=row.get('label', 'other'))) + images.append(dict(entry, label=row.get('label', 'other'), **_caption(row))) voice = self._voice(identity, value['voice']) if value.get('voice') else None with self.changed: digest, replay = self._receipt('cast:create', value, ref, client) @@ -461,7 +468,7 @@ def update(self, identity, value, *, by='builtin', ref=None, revision=None, loca for key, rows in changes.items(): if key == 'images': resolved['images'] = [entry if entry is not None else dict(self._picture_at(identity, row['media'], by, index), - label=row.get('label', 'other')) for index, (row, entry) in enumerate(rows)] + label=row.get('label', 'other'), **_caption(row)) for index, (row, entry) in enumerate(rows)] elif key == 'voice': resolved['voice'] = None if rows is None else self._voice(identity, rows) else: @@ -510,16 +517,19 @@ def _changes(self, card, value, by): entry.pop('label_guess', None) if by == 'user': entry.pop('generated', None) # The user's label makes it the user's picture. + if 'caption' in row: # Left out, the caption stays; "" removes it. + entry.pop('caption', None) + entry.update(_caption(row)) else: fresh = True rows.append((row, entry)) - current = [(r['media']['file'], r['label']) for r in card['images']] - proposed = [(e['media']['file'], e['label']) for _, e in rows if e is not None] + current = [(r['media']['file'], r['label'], r.get('caption')) for r in card['images']] + proposed = [(e['media']['file'], e['label'], e.get('caption')) for _, e in rows if e is not None] if fresh or proposed != current: changes['images'] = rows if by != 'user' and card['edited_by_user']['images']: # The user's pictures may only be joined by candidates the server made; never replaced. - untouched = all((f, l) in proposed for f, l in current) and len(kept) == len(current) + untouched = all(row in proposed for row in current) and len(kept) == len(current) if fresh or not untouched or not added: raise self._by_user(card, 'images') if 'voice' in value: diff --git a/freevideo_engine/agent/drama.py b/freevideo_engine/agent/drama.py index ac0b32b9..58a28dca 100644 --- a/freevideo_engine/agent/drama.py +++ b/freevideo_engine/agent/drama.py @@ -60,8 +60,8 @@ def __init__(self, problems): CAPTIONED = 3 # the score of a take with unwanted captions CARD = re.compile(r'c[0-9a-f]{12}') # characters..card names a cast library card # How a shot carries its cast cards. Change these after the reference tests; nothing else needs to. -# Each person is two pictures: one sheet of every view (made by the cast library when the card has none), then -# the face close-up. A continuing shot with several people has no room for two each: it carries one picture a person. +# A fresh shot carries every picture of each person, the one standing for the person last. A continuing shot has room +# for three pictures: one person carries the sheet of every view then the face, several people one picture each. FACE_ORDER = ('face', 'front', 'three_quarter') # the one picture standing for a person, then any other label, in card order MAX_VOICES = 3 # voice clips per shot, the model limit MAX_REFERENCES = 32 @@ -266,7 +266,8 @@ def prompt(play, shot): def subject_definitions(subjects): """The prompt prefix tying cast cards to their reference tags; the one place its wording lives. - subjects: [{'name', 'description', 'pictures': (first, last), a list of such pairs, or None, 'audio': number or None}]. + subjects: [{'name', 'description', 'pictures': (first, last), a list of such pairs, or None, 'captions': [(picture, + text)], 'audio': number or None}]. The page writes the same prompt from the same wording (web/cast_mention.js); tests/fixtures/subject_definitions.json holds the cases both sides must reproduce exactly.""" parts = [] @@ -282,6 +283,10 @@ def subject_definitions(subjects): for a, b in ranges) single = len(ranges) == 1 and ranges[0][0] == ranges[0][1] tags.append('%s show%s its appearance' % (pictures, 's' if single else '')) + for picture, caption in subject.get('captions') or (): + caption = (caption or '').strip().rstrip('.') + if caption: + tags.append(' shows %s' % (picture, caption)) if subject.get('audio'): tags.append('