diff --git a/freevideo_engine/adaptive.py b/freevideo_engine/adaptive.py index 6fa6011f..37d9a9b8 100644 --- a/freevideo_engine/adaptive.py +++ b/freevideo_engine/adaptive.py @@ -660,11 +660,13 @@ def complete_observation(metrics, canvas, output, artifacts, telemetry): from .validation import _retained_array_header, finite_number expected = canvas['frames'] continuation = metrics.get('continuation') - output_frames = expected + from .continuation import validate_hide_tail + hidden = validate_hide_tail(metrics.get('hide_tail_frames', 0)) + output_frames = expected - hidden if continuation is not None: from .continuation import OVERLAP_FRAMES if (not isinstance(continuation, dict) or continuation.get('overlap_frames') != OVERLAP_FRAMES - or continuation.get('new_frames') != expected - OVERLAP_FRAMES): + or continuation.get('new_frames') != expected - hidden - OVERLAP_FRAMES): raise ValueError('Invalid continuation output geometry') output_frames = continuation['new_frames'] preview = metrics.get('preview') is True diff --git a/freevideo_engine/agent/cast.py b/freevideo_engine/agent/cast.py index 7ed1f999..1b32163d 100644 --- a/freevideo_engine/agent/cast.py +++ b/freevideo_engine/agent/cast.py @@ -98,6 +98,7 @@ def view(card): if card.get('voice'): value['voice'] = dict(deepcopy(card['voice']), media=media_view(card['voice']['media']), clip=assets.view(card['voice']['clip']['file'])) + value['generate_error'] = errors.with_reason(value.get('generate_error')) return value @@ -112,7 +113,8 @@ def row_view(card): return dict(id=card['id'], kind=card['kind'], name=card['name'], description=card['description'], origin=card['origin'], revision=card['revision'], updated_at=card['updated_at'], images=len(card['images']), voice=card['voice'] is not None, - generating=card.get('generating') is not None, thumbnail=thumbnail(card)) + generating=card.get('generating') is not None, thumbnail=thumbnail(card), + generate_error=errors.with_reason(card.get('generate_error'))) def _plain(item): @@ -729,7 +731,7 @@ def _land_image(self, identity, generating, hidden, rows): code = (row.get('error') or {}).get('code') if row else 'not_found' code = code or (row['status'] if row else 'not_found') detail = errors.image_failure(code, (row.get('error') or {}).get('report_id') if row else None) - failure = failure or dict(code=code or row['status'], job=job, message=( + failure = failure or dict(code=code, job=job, error_zh=detail['reason']['zh'], message=( 'The generation was cancelled.' if row and row['status'] == 'cancelled' else detail['reason']['en']), **detail) continue @@ -764,7 +766,8 @@ def _land(self, identity, generating, hidden, rows): face = job == generating.get('face_job') if row is None or row['status'] != 'succeeded': code = (row.get('error') or {}).get('code') if row else 'not_found' - failure = failure or dict(code=code or row['status'], job=job, message=( + failure = failure or dict(code=code or row['status'], job=job, + error_zh=errors.failure_reason((row.get('error') or dict(code=code or row['status'])) if row else dict(code=code)), message=( 'The generation was cancelled.' if row and row['status'] == 'cancelled' else 'The reference clip did not finish.')) continue diff --git a/freevideo_engine/agent/comfy.py b/freevideo_engine/agent/comfy.py index 96deda12..4d7f3201 100644 --- a/freevideo_engine/agent/comfy.py +++ b/freevideo_engine/agent/comfy.py @@ -101,6 +101,13 @@ def capability_snapshot(self, query): cache = getattr(self.server, '_freevideo_capabilities', None) return cache.get(_query(query)) if hasattr(cache, 'get') else dict(UNAVAILABLE) + def prepared_precision(self): + """'int8' or 'fp8', the family of the model setup prepared for this machine; None while unknown.""" + cache = getattr(self.server, '_freevideo_capabilities', None) + snapshot = getattr(cache, 'snapshot', None) + saved = snapshot[0].get('prepared_format', 'int8') if snapshot else None + return next((p for p in ('int8', 'fp8') if isinstance(saved, str) and saved.startswith(p)), None) + def output_summary(self, relative): from .jobs import library_id from ..comfy_library import _report diff --git a/freevideo_engine/agent/drama.py b/freevideo_engine/agent/drama.py index 8d1e1154..a9ce60e7 100644 --- a/freevideo_engine/agent/drama.py +++ b/freevideo_engine/agent/drama.py @@ -62,7 +62,17 @@ def __init__(self, problems): 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. PICTURE_ORDER = ('sheet', 'front', 'three_quarter', 'face') # then any other label, in card order -PICTURES_EACH = 3 +PICTURES_EACH = 6 +PICTURES_LEAST = 3 # What a person carried before six: a device that cannot hold more still shoots with these. +# Six pictures cost extra time on every step. A class of machine starts from six only when it was measured at 25% +# extra or less on every step, cold and warm: (card memory tier in GiB, people in the shot, weight precision). +# Everything else, and everything unmeasured, starts from three. Only Linux is measured. Two people went 45-79% over +# at 8 to 24 GiB, so only one-person classes can ever be listed. No class has passed yet. +SIX_PICTURES_CLASSES = frozenset() +SIX_PICTURES_TIERS_GIB = (8, 10, 12, 16, 20, 24, 32) # measured card sizes; a card takes the largest one it reaches +SIX_PICTURES_SYSTEMS = ('Linux',) +TIER_SLACK_GIB = .5 # A "24 GB" card reports a little under 24 GiB. +CONTINUED_ORDER = ('face', 'front', 'three_quarter') # a continuing shot's picture for each person MAX_VOICES = 3 # voice clips per shot, the model limit MAX_REFERENCES = 32 @@ -150,16 +160,16 @@ def validate(function, *args, **kwargs): validate(specs._int, shot[field], path + '/' + field, low, high) if 'seconds' in shot: validate(specs._number, shot['seconds'], path + '/seconds', 1.625, 60.) - modes = int(shot.get('first') is not None) + int(bool(shot.get('continue'))) + int('references' in shot) - if modes > 1: - problem(path, 'first, continue and references are mutually exclusive') + # A continuing shot starts from the previous one and may add reference pictures, never a first frame. + if shot.get('first') is not None and (shot.get('continue') or shot.get('references')): + problem(path, 'first is exclusive with continue and references') # Continuing a shot, or one longer than one 15 s block, needs two-pass text generation. seconds = shot.get('seconds', play.get('seconds')) long = isinstance(seconds, (int, float)) and not isinstance(seconds, bool) and seconds > 15 if ((shot.get('continue') or long) and play.get('quality') in specs.QUALITY and not specs.QUALITY[play['quality']][0] and all(p['path'] != '/quality' for p in problems)): problem('/quality', '"light" (two-pass) when a shot continues another or is longer than 15 s') - if long and (shot.get('first') is not None or 'references' in shot): + if long and (shot.get('first') is not None or shot.get('references') and not shot.get('continue')): problem(path + '/seconds', 'at most 15 with a first frame or references') elif long and not shot.get('continue') and _carded(play, shot): # Cast cards travel as references, which one generation of at most 15 s carries. @@ -171,9 +181,13 @@ def validate(function, *args, **kwargs): refs = shot['references'] if not isinstance(refs, list) or len(refs) > 32: problem(path + '/references', 'must be a list of at most 32 items') + elif shot.get('continue') and len(refs) > specs.CONTINUED_PICTURES: + problem(path + '/references', 'at most %d pictures when the shot continues' % specs.CONTINUED_PICTURES) if isinstance(refs, list): for j, ref in enumerate(refs): - validate(specs.media_item, ref, path + '/references/%d' % j) + validate(specs.media_item, ref, path + '/references/%d' % j, image=bool(shot.get('continue'))) + if shot.get('continue') and not specs.still_picture(ref): + problem(path + '/references/%d' % j, 'must be a picture when the shot continues') if 'cast' in shot: if not isinstance(shot['cast'], list): problem(path + '/cast', 'must be a list of character names') @@ -261,7 +275,9 @@ 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) or None, 'audio': number or None}].""" + subjects: [{'name', 'description', 'pictures': (first, last), a list of such pairs, or None, '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 = [] for number, subject in enumerate(subjects, 1): text = ' is %s' % (number, subject['name']) @@ -269,9 +285,12 @@ def subject_definitions(subjects): text += (': %s.' % description) if description else '.' tags = [] if subject.get('pictures'): - first, last = subject['pictures'] - pictures = '' % first if first == last else ' to ' % (first, last) - tags.append('%s show%s its appearance' % (pictures, 's' if first == last else '')) + # One (first, last) pair, or several when the person's pictures are not side by side. + ranges = [subject['pictures']] if isinstance(subject['pictures'][0], int) else subject['pictures'] + pictures = ', '.join('' % a if a == b else ' to ' % (a, b) + 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 '')) if subject.get('audio'): tags.append('