objectionengine2 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,22 @@
1
+ class Comment:
2
+ def __init__(self,
3
+ user_id: str = None,
4
+ user_name: str = 'Prosecutor',
5
+ text_content: str = '...',
6
+ evidence_path: str = None,
7
+ score: float = 0
8
+ ):
9
+ """
10
+ All arguments are optional
11
+ :param str user_id: An unique identifier to tell users appart
12
+ :param str evidence_path: Path pointing to an image to be used as evidence
13
+ :param str user_name: Name of the user
14
+ :param str text_content: Text to display
15
+ :param float score: Positiveness of a commentary (negative below zero, positive above zero)
16
+ """
17
+ self.user_name = user_name
18
+ self.user_id = user_id
19
+ self.text_content = text_content
20
+ self.score = score
21
+ self.evidence_path = evidence_path
22
+ self.effective_user_id = self.user_id or self.user_name
@@ -0,0 +1,9 @@
1
+ from typing import Optional, List
2
+ from objectionengine2.font_constants import FONT_ARRAY
3
+ from objectionengine2.font_tools import get_font_score
4
+
5
+ def is_renderable(text: str, font_array: Optional[List[dict]] = None) -> bool:
6
+ score = 0
7
+ for font in FONT_ARRAY if font_array is None else font_array:
8
+ score = max(score, get_font_score(font, text))
9
+ return score >= len(text)
@@ -0,0 +1,18 @@
1
+ """Per-user cache paths for downloaded Objection Engine data."""
2
+
3
+ from pathlib import Path
4
+
5
+ from platformdirs import user_cache_path
6
+
7
+
8
+ def cache_path() -> Path:
9
+ """Return the platform's cache directory for objectionengine2."""
10
+ return user_cache_path("objectionengine2", appauthor=False)
11
+
12
+
13
+ def assets_path() -> Path:
14
+ return cache_path() / "assets"
15
+
16
+
17
+ def models_path() -> Path:
18
+ return cache_path() / "models"
@@ -0,0 +1,72 @@
1
+ from objectionengine2.parse_tags import DialogueAction
2
+
3
+
4
+ def compose_gavel_slam(
5
+ num_slams: int = 3,
6
+ delay_between_slams: float = 0.17,
7
+ finish_wait_time: float = 0.766,
8
+ ):
9
+ if num_slams <= 0:
10
+ return []
11
+
12
+ if num_slams == 1:
13
+ return [
14
+ DialogueAction("hidebox", 0),
15
+ DialogueAction("cut gavel", 0),
16
+ DialogueAction("gavel 0", 0),
17
+ DialogueAction("wait 0.3", 0),
18
+ DialogueAction("gavel 1", 0),
19
+ DialogueAction("wait 0.04", 0),
20
+ DialogueAction("gavel 2", 0),
21
+ DialogueAction("wait 0.04", 0),
22
+ DialogueAction("gavel 3", 0),
23
+ DialogueAction("sound gavel", 0),
24
+ DialogueAction("shake 3 0.2", 0),
25
+ DialogueAction(f"wait {finish_wait_time}", 0),
26
+ ]
27
+
28
+ commands = [
29
+ DialogueAction("hidebox", 0),
30
+ DialogueAction("cut gavel", 0),
31
+ DialogueAction("gavel 0", 0),
32
+ DialogueAction("wait 0.300", 0),
33
+ DialogueAction("gavel 1", 0),
34
+ DialogueAction("wait 0.04", 0),
35
+ DialogueAction("gavel 3", 0),
36
+ DialogueAction("sound gavel", 0),
37
+ DialogueAction("shake 3 0.2", 0),
38
+ DialogueAction(f"wait {delay_between_slams}", 0),
39
+ ]
40
+
41
+ # First and last slams have different duration
42
+ for _ in range(num_slams - 2):
43
+ commands.extend(
44
+ [
45
+ DialogueAction("gavel 2", 0),
46
+ DialogueAction("wait 0.04", 0),
47
+ DialogueAction("gavel 1", 0),
48
+ DialogueAction("wait 0.17", 0),
49
+ DialogueAction("gavel 3", 0),
50
+ DialogueAction("sound gavel", 0),
51
+ DialogueAction("shake 3 0.2", 0),
52
+ DialogueAction(f"wait {delay_between_slams}", 0),
53
+ ]
54
+ )
55
+
56
+ # Last slam
57
+ commands.extend(
58
+ [
59
+ DialogueAction("gavel 2", 0),
60
+ DialogueAction("wait 0.04", 0),
61
+ DialogueAction("gavel 1", 0),
62
+ DialogueAction("wait 0.17", 0),
63
+ DialogueAction("gavel 2", 0),
64
+ DialogueAction("wait 0.04", 0),
65
+ DialogueAction("gavel 3", 0),
66
+ DialogueAction("sound gavel", 0),
67
+ DialogueAction("shake 3 0.2", 0),
68
+ DialogueAction(f"wait {finish_wait_time}", 0),
69
+ ]
70
+ )
71
+
72
+ return commands
@@ -0,0 +1,56 @@
1
+ from objectionengine2.parse_tags import DialogueAction
2
+
3
+ def compose_verdict(
4
+ text: str,
5
+ slam_group: str = "letter",
6
+ color: str = "white",
7
+ slam_delay: float = 0.0,
8
+ finish_wait_time: float = 2.0,
9
+ ):
10
+ if slam_group not in ["letter", "word"]:
11
+ raise ValueError(
12
+ f'slam_group "{slam_group}" is not valid; must be "letter" or "word"'
13
+ )
14
+ if color not in ["black", "white"]:
15
+ raise ValueError(f'color "{color}" is not valid; must be "black" or "white"')
16
+
17
+ output = []
18
+ output.append(DialogueAction(f'verdict set "{text}" {color}', 0))
19
+
20
+ slam_delay_command = f"wait {slam_delay}"
21
+ if slam_group == "letter":
22
+ for i in range(len(text)):
23
+ output.extend(
24
+ [
25
+ DialogueAction(f"verdict show {i}", 0),
26
+ DialogueAction("wait 0.2", 0),
27
+ DialogueAction("sound guilty", 0),
28
+ DialogueAction("shake 4 0.1", 0),
29
+ ]
30
+ )
31
+ if i != len(text) - 1:
32
+ output.append(DialogueAction(slam_delay_command, 0))
33
+
34
+ elif slam_group == "word":
35
+ for i in range(len(text)):
36
+ output.append(DialogueAction(f"verdict show {i}", 0))
37
+ if text[i].isspace():
38
+ output.extend(
39
+ [
40
+ DialogueAction("wait 0.2", 0),
41
+ DialogueAction("sound guilty", 0),
42
+ DialogueAction("shake 4 0.1", 0),
43
+ DialogueAction(slam_delay_command, 0),
44
+ ]
45
+ )
46
+ output.extend(
47
+ [
48
+ DialogueAction("wait 0.2", 0),
49
+ DialogueAction("sound guilty", 0),
50
+ DialogueAction("shake 4 0.1", 0),
51
+ ]
52
+ )
53
+
54
+ output.append(DialogueAction(f"wait {finish_wait_time}", 0))
55
+ output.append(DialogueAction("verdict clear", 0))
56
+ return output
@@ -0,0 +1,37 @@
1
+ from enum import IntEnum
2
+ from objectionengine2.loading import ASSETS_FOLDER
3
+
4
+ class TextType(IntEnum):
5
+ DIALOGUE = 0
6
+ NAME = 1
7
+
8
+ FONT_ARRAY = [
9
+ # AA-Like > Pixel > Generic
10
+ {'path': f'{ASSETS_FOLDER}/textbox/font/igiari-cyrillic/igiari-cyrillic.ttf'},
11
+ # AA-like, Latin, hiragana, katakana, (part of) cyrillic
12
+ {'path': f'{ASSETS_FOLDER}/textbox/font/igiari/Igiari.ttf'},
13
+ # AA-like, Latin, hiragana, katakana, (part of) cyrillic
14
+ {'path': f'{ASSETS_FOLDER}/textbox/font/igiari/Galmuri11.ttf'},
15
+ # Pixel, Kanji, Hiragana, Katakana
16
+ {'path': f'{ASSETS_FOLDER}/textbox/font/igiari/jackeyfont.ttf'},
17
+ # Arabic
18
+ {'path': f'{ASSETS_FOLDER}/textbox/font/igiari/arabic-1.ttf', 'size': 12, 'offset': {TextType.NAME: (0, -5)}, 'rtl': True},
19
+ # Pixel-font, Hebrew
20
+ {'path': f'{ASSETS_FOLDER}/textbox/font/igiari/STANRG__.ttf'},
21
+ # Generic
22
+ {'path': f'{ASSETS_FOLDER}/textbox/font/igiari/NotoSans-Regular.ttf'},
23
+ # Pixel font, Arabic
24
+ {'path': f'{ASSETS_FOLDER}/textbox/font/igiari/bitsy-font-with-arabic.ttf', 'size': 10, 'rtl': True},
25
+ ]
26
+
27
+ NAMETAG_FONT_ARRAY = [
28
+ {'path': f'{ASSETS_FOLDER}/textbox/font/ace-name/ace-name.ttf', 'size': 8}
29
+ ] + FONT_ARRAY
30
+
31
+ TEXT_COLORS = {
32
+ "red": (240, 112, 56),
33
+ "blue": (104, 192, 240),
34
+ "green": (0, 240, 0)
35
+ }
36
+
37
+ MAX_LINE_WIDTH = 220
@@ -0,0 +1,98 @@
1
+ from .font_constants import FONT_ARRAY
2
+ from PIL import ImageFont
3
+ from typing import List, Dict, Union
4
+ from textwrap import wrap
5
+ import spacy
6
+
7
+ nlp = spacy.blank("xx")
8
+ nlp.add_pipe('sentencizer')
9
+
10
+
11
+ def split_sentences(text: str) -> list[str]:
12
+ """Split dialogue without requiring Polyglot language data."""
13
+ sentences = [span.text.strip() for span in nlp(text).sents]
14
+ return [sentence for sentence in sentences if sentence] or [text]
15
+
16
+ try:
17
+ from fontTools.ttLib import TTFont
18
+ except:
19
+ from fonttools.ttLib import TTFont
20
+
21
+ def get_text_width(text, font_size = 15, font = None):
22
+ if font is None:
23
+ font = get_best_font(text, FONT_ARRAY)
24
+ font_path = font['path']
25
+ font_obj = ImageFont.truetype(font_path, font_size)
26
+ return font_obj.getlength(text)
27
+
28
+ def get_font_score(font, text):
29
+ font_path = font['path']
30
+ font = TTFont(font_path)
31
+
32
+ # We check all chars for presence on the font
33
+ valid_chars = 0
34
+ for char in text:
35
+ # We check if the char is in any table of the font
36
+ for table in font['cmap'].tables:
37
+ if ord(char) in table.cmap:
38
+ valid_chars += 1
39
+ break
40
+ return valid_chars
41
+
42
+ def get_best_font(text, font_array):
43
+ best_font = font_array[-1]
44
+ best_font_points = 0
45
+ text = text.replace('\n', '').replace('\r', '').replace('\t', '').replace('\u200B', '')
46
+ for font in font_array:
47
+ pts = get_font_score(font, text)
48
+ if pts > best_font_points:
49
+ best_font_points = pts
50
+ best_font = font
51
+ if best_font_points >= len(text):
52
+ return font
53
+ print(f'WARNING. NO OPTIMAL FONT FOUND, font: {best_font}, font score: {best_font_points}/{len(text)}, text \"{text}\"\n')
54
+ return best_font
55
+
56
+
57
+ def fit_words_within_width(words: Union[list[str], str], font: ImageFont.FreeTypeFont, insert_space: bool):
58
+ new_text = ""
59
+ space = " " if insert_space else ""
60
+ for word in words:
61
+ last_sentence = new_text.split("\n")[-1] + word + space
62
+ if font.getlength(text=last_sentence) >= 240:
63
+ if new_text.split("\n")[-1] != "":
64
+ new_text += "\n"
65
+ new_text += fit_words_within_width(word, font, False) + space
66
+ else:
67
+ new_text += word + space
68
+
69
+ return new_text
70
+
71
+ def split_str_into_newlines(text: str, font_path, font_size):
72
+ font = ImageFont.truetype(font_path, font_size)
73
+ words = text.split(" ")
74
+ return fit_words_within_width(words, font, True)
75
+
76
+
77
+ def split_with_joined_sentences(text: str):
78
+ """
79
+ """
80
+ tokens = nlp(text)
81
+ sentences = [sent.text.strip() for sent in tokens.sents]
82
+ joined_sentences = []
83
+ i = 0
84
+ while i < len(sentences):
85
+ sentence = sentences[i]
86
+ if len(sentence) > 85: # Long sentences should be wrapped to multiple shorter lines
87
+ text_chunks = [chunk for chunk in wrap(sentence, 85)]
88
+ joined_sentences = [*joined_sentences, *text_chunks]
89
+ i += 1
90
+ else:
91
+ if i + 1 < len(sentences) and len(f"{sentence} {sentences[i+1]}") <= 85: # Maybe we can join two different sentences
92
+ joined_sentences.append(sentence + " " + sentences[i+1])
93
+ i += 2
94
+ else:
95
+ joined_sentences.append(sentence)
96
+ i += 1
97
+
98
+ return joined_sentences
@@ -0,0 +1,80 @@
1
+ from objectionengine2.loading import ASSETS_FOLDER, _print_note
2
+ from objectionengine2.MovieKit import ImageObject, SceneObject
3
+ from os.path import join, exists
4
+
5
+ GAVEL_SLAM_PATH = join(ASSETS_FOLDER, "gavel_slam")
6
+
7
+ """
8
+ https://www.youtube.com/watch?v=XBR35H_ya4g as reference
9
+ Single slam at 9:53
10
+ 0 - 9 frames (0.300s)
11
+ 1 - 1 frame (0.033s)
12
+ 2 - 1 frame (0.033s)
13
+ 3 - 23 frames (0.766s)
14
+
15
+ # Triple slam at 11:07
16
+ 0 - 9 frames (0.300s)
17
+ 1 - 1 frame (0.033s)
18
+ 3 - 5 frames (0.166s)
19
+ 2 - 1 frame (0.033s)
20
+ 1 - 5 frames (0.166s)
21
+ 3 - 5 frames (0.166s)
22
+ 2 - 1 frame (0.033s)
23
+ 1 - 5 frames (0.166s)
24
+ 2 - 1 frame (0.033s)
25
+ 3 - 23 frames (0.766s)
26
+
27
+ NOTE: using 0.033 for to indicate 1 frame didn't seem to work (it would simply
28
+ skip the frame) - instead, I rounded up fo 0.04.
29
+ """
30
+
31
+ class GavelSlamObject(SceneObject):
32
+ def __init__(self, parent: SceneObject = None, name: str = "", pos: tuple[int, int, int] = (0, 0, 0)):
33
+ super().__init__(parent, name, pos)
34
+ if not exists(GAVEL_SLAM_PATH):
35
+ _print_note(
36
+ f"Gavel slam assets not found at {GAVEL_SLAM_PATH}"
37
+ )
38
+ return
39
+
40
+ self.bg_img = ImageObject(
41
+ parent=self,
42
+ name="Gavel Slam BG",
43
+ pos=(0, 0, 11),
44
+ filepath=join(GAVEL_SLAM_PATH, "gavel_slam_bg.png"),
45
+ )
46
+
47
+ self.block_img = ImageObject(
48
+ parent=self,
49
+ name="Gavel Slam Block",
50
+ pos=(0, 0, 11),
51
+ filepath=join(GAVEL_SLAM_PATH, "gavel_slam_block.png"),
52
+ )
53
+
54
+ self.gavel_frames = [
55
+ ImageObject(
56
+ parent=self,
57
+ name="Gavel Frame 1",
58
+ pos=(0, 0, 11),
59
+ filepath=join(GAVEL_SLAM_PATH, "gavel_slam_gavel_1.png"),
60
+ ),
61
+ ImageObject(
62
+ parent=self,
63
+ name="Gavel Frame 2",
64
+ pos=(0, 0, 11),
65
+ filepath=join(GAVEL_SLAM_PATH, "gavel_slam_gavel_2.png"),
66
+ ),
67
+ ImageObject(
68
+ parent=self,
69
+ name="Gavel Frame 3",
70
+ pos=(0, 0, 11),
71
+ filepath=join(GAVEL_SLAM_PATH, "gavel_slam_gavel_3.png"),
72
+ ),
73
+ ]
74
+ self.set_gavel_frame(0)
75
+
76
+ def set_gavel_frame(self, frame: int = 0):
77
+ for f in self.gavel_frames: f.hide()
78
+ if frame <= 0 or frame > len(self.gavel_frames):
79
+ return
80
+ self.gavel_frames[frame - 1].show()
@@ -0,0 +1,179 @@
1
+ from objectionengine2.loading import ASSETS_FOLDER, _print_note
2
+ from objectionengine2.math_helpers import lerp
3
+ from .MovieKit import SceneObject
4
+ from PIL import Image, ImageDraw, ImageFont
5
+ from os.path import exists
6
+
7
+ VERDICT_FONT_SIZE = 70
8
+ VERDICT_FONT_START_SIZE_RATIO = 8 / 3
9
+ VERDICT_STROKE_WIDTH_RATIO = 1 / 14
10
+ VERDICT_STROKE_WIDTH = int(VERDICT_FONT_SIZE * VERDICT_STROKE_WIDTH_RATIO)
11
+
12
+ VERDICT_MAX_WIDTH = 230
13
+ VERDICT_SLAM_SPEED = 8.0
14
+
15
+ VERDICT_COLORS = {
16
+ "black": {"fill": (0, 0, 0), "stroke": (255, 255, 255)},
17
+ "white": {"fill": (255, 255, 255), "stroke": (0, 0, 0)},
18
+ }
19
+
20
+ VERDICT_FONT_PATH = f"{ASSETS_FOLDER}/verdict/DFMinchoStd-W12.otf"
21
+
22
+
23
+ class JudgeVerdictTextObject(SceneObject):
24
+ def __init__(self, parent: SceneObject = None, name: str = ""):
25
+ super().__init__(parent, name, (0, 0, 10))
26
+ self.font = None
27
+
28
+ # If the verdict font isn't available, then don't crash, but don't
29
+ # render anything
30
+ if not exists(VERDICT_FONT_PATH):
31
+ _print_note(
32
+ f"Verdict font not found at {VERDICT_FONT_PATH}, so verdict text will not be rendered"
33
+ )
34
+ return
35
+
36
+ self.font = ImageFont.truetype(VERDICT_FONT_PATH, VERDICT_FONT_SIZE)
37
+ self._text = ""
38
+ self.clear()
39
+
40
+ def get_text_bbox(self, text):
41
+ if self.font is None:
42
+ return (0, 0)
43
+
44
+ bb = self.font.getbbox(text)
45
+ x1, y1, x2, y2 = bb
46
+
47
+ padding = VERDICT_STROKE_WIDTH
48
+ x1 -= padding
49
+ y1 -= padding
50
+ x2 += padding
51
+ y2 += padding
52
+ h = y2 - y1
53
+ w = x2 - x1
54
+
55
+ return (w, h)
56
+
57
+ def clear(self):
58
+ if self.font is None:
59
+ return
60
+
61
+ self.set_text("")
62
+
63
+ def set_text(self, text: str, text_color: str = None):
64
+ if self.font is None:
65
+ return
66
+
67
+ self._text = text
68
+
69
+ w, h = self.get_text_bbox(self._text)
70
+
71
+ x_squish = min(1.0, VERDICT_MAX_WIDTH / w)
72
+ chars = []
73
+ for i, c in enumerate(self._text):
74
+ next_char = "" if i == len(self._text) - 1 else self._text[i + 1]
75
+ next_char_width = self.font.getlength(next_char)
76
+ char_width = self.font.getlength(c + next_char) - next_char_width
77
+
78
+ # For each character, make an image and store it along with the character
79
+ # it holds and its getlength value
80
+ char_img = Image.new("RGBA", self.get_text_bbox(c), (255, 0, 255, 0))
81
+ char_img_ctx = ImageDraw.Draw(char_img)
82
+
83
+ if text_color not in VERDICT_COLORS:
84
+ text_color = "black"
85
+ args = {
86
+ "xy": (VERDICT_STROKE_WIDTH, VERDICT_STROKE_WIDTH),
87
+ "text": c,
88
+ "fill": VERDICT_COLORS[text_color]["fill"],
89
+ "stroke_width": VERDICT_STROKE_WIDTH,
90
+ "stroke_fill": VERDICT_COLORS[text_color]["stroke"],
91
+ "font": self.font,
92
+ "anchor": "lt",
93
+ }
94
+ char_img_ctx.text(**args)
95
+
96
+ # Squish character on X axis if the whole string will be too wide
97
+ char_img = char_img.resize(
98
+ (int(char_img.width * x_squish), char_img.height)
99
+ )
100
+ true_char_width = int(char_width * x_squish)
101
+
102
+ # Get bounding box stuff!
103
+ # From https://github.com/python-pillow/Pillow/issues/3921#issuecomment-533085656
104
+ bottom_1 = self.font.getsize(self._text[i])[1]
105
+ right, bottom_2 = self.font.getsize(self._text[: i + 1])
106
+ bottom = bottom_1 if bottom_1 < bottom_2 else bottom_2
107
+ width, height = self.font.getmask(c).size
108
+ top = bottom - height
109
+ left = right - width
110
+
111
+ rect = (left, top, right, bottom)
112
+
113
+ chars.append(
114
+ {
115
+ "img": char_img,
116
+ "char": c,
117
+ "width": true_char_width,
118
+ "rect": rect,
119
+ "scale": VERDICT_FONT_START_SIZE_RATIO,
120
+ "visible": False,
121
+ }
122
+ )
123
+
124
+ width_sum = sum([i["width"] for i in chars])
125
+ self._characters = {"chars": chars, "width_sum": width_sum, "height": h}
126
+
127
+ def show_index(self, index: int):
128
+ if self.font is None:
129
+ return
130
+
131
+ self._characters["chars"][index]["visible"] = True
132
+
133
+ def update(self, delta):
134
+ if self.font is None:
135
+ return
136
+
137
+ for character in self._characters["chars"]:
138
+ if not character["visible"]:
139
+ continue
140
+ character["scale"] = max(
141
+ 1.0, character["scale"] - delta * VERDICT_SLAM_SPEED
142
+ )
143
+
144
+ def render(self, img: Image.Image, ctx: ImageDraw.ImageDraw):
145
+ if self.font is None:
146
+ return
147
+
148
+ width_so_far = -int(self._characters["width_sum"] / 2) - VERDICT_STROKE_WIDTH
149
+ for character in self._characters["chars"]:
150
+ if not character["visible"]:
151
+ continue
152
+ char_img: Image.Image = character["img"]
153
+ char_width = character["width"]
154
+
155
+ x = width_so_far
156
+ y = (
157
+ character["rect"][1]
158
+ - int(self._characters["height"] / 2)
159
+ - VERDICT_STROKE_WIDTH
160
+ )
161
+ w = char_img.width
162
+ h = char_img.height
163
+
164
+ scale = character["scale"]
165
+ scaled_w = int(char_img.width * scale)
166
+ scaled_h = int(char_img.height * scale)
167
+
168
+ # https://math.stackexchange.com/a/3055687
169
+ scaled_x = x + int(w / 2) - int(scaled_w / 2)
170
+ scaled_y = y + int(h / 2) - int(scaled_h / 2)
171
+ scaled_char_img = char_img.resize(
172
+ (scaled_w, scaled_h), resample=Image.Resampling.NEAREST
173
+ )
174
+
175
+ img.alpha_composite(
176
+ scaled_char_img,
177
+ (scaled_x + int(img.width / 2), scaled_y + int(img.height / 2)),
178
+ )
179
+ width_so_far += char_width