pycaps-ai 0.2.1__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.
Files changed (185) hide show
  1. pycaps/__init__.py +13 -0
  2. pycaps/ai/__init__.py +5 -0
  3. pycaps/ai/gpt.py +35 -0
  4. pycaps/ai/llm.py +11 -0
  5. pycaps/ai/llm_provider.py +16 -0
  6. pycaps/animation/__init__.py +31 -0
  7. pycaps/animation/animation.py +11 -0
  8. pycaps/animation/builtin/__init__.py +18 -0
  9. pycaps/animation/builtin/preset/__init__.py +21 -0
  10. pycaps/animation/builtin/preset/fade_in.py +17 -0
  11. pycaps/animation/builtin/preset/fade_out.py +19 -0
  12. pycaps/animation/builtin/preset/pop_in.py +22 -0
  13. pycaps/animation/builtin/preset/pop_in_bounce.py +20 -0
  14. pycaps/animation/builtin/preset/pop_out.py +22 -0
  15. pycaps/animation/builtin/preset/slide_in.py +25 -0
  16. pycaps/animation/builtin/preset/slide_out.py +26 -0
  17. pycaps/animation/builtin/preset/zoom_in.py +24 -0
  18. pycaps/animation/builtin/preset/zoom_out.py +25 -0
  19. pycaps/animation/builtin/primitive/__init__.py +11 -0
  20. pycaps/animation/builtin/primitive/fade_in_primitive.py +6 -0
  21. pycaps/animation/builtin/primitive/pop_in_primitive.py +64 -0
  22. pycaps/animation/builtin/primitive/slide_in_primitive.py +54 -0
  23. pycaps/animation/builtin/primitive/zoom_in_primitive.py +64 -0
  24. pycaps/animation/definitions.py +22 -0
  25. pycaps/animation/element_animator.py +52 -0
  26. pycaps/animation/preset_animation.py +16 -0
  27. pycaps/animation/primitive_animation.py +89 -0
  28. pycaps/api/__init__.py +3 -0
  29. pycaps/api/api_key_service.py +21 -0
  30. pycaps/api/api_sender.py +53 -0
  31. pycaps/api/emoji_in_segments_api.py +42 -0
  32. pycaps/api/pycaps_tagger_api.py +24 -0
  33. pycaps/bootstrap.py +35 -0
  34. pycaps/cli/__init__.py +1 -0
  35. pycaps/cli/cli.py +23 -0
  36. pycaps/cli/config_cli.py +25 -0
  37. pycaps/cli/preview_styles_cli.py +30 -0
  38. pycaps/cli/render_cli.py +103 -0
  39. pycaps/cli/template_cli.py +39 -0
  40. pycaps/common/__init__.py +43 -0
  41. pycaps/common/config_service.py +50 -0
  42. pycaps/common/element_container.py +48 -0
  43. pycaps/common/models.py +298 -0
  44. pycaps/common/types.py +63 -0
  45. pycaps/effect/__init__.py +22 -0
  46. pycaps/effect/clip/__init__.py +9 -0
  47. pycaps/effect/clip/animate_segment_emojis_effect.py +105 -0
  48. pycaps/effect/clip/clip_effect.py +6 -0
  49. pycaps/effect/clip/typewriting_effect.py +54 -0
  50. pycaps/effect/effect.py +5 -0
  51. pycaps/effect/sound/__init__.py +5 -0
  52. pycaps/effect/sound/builtin_sound.py +42 -0
  53. pycaps/effect/sound/presets/click-light.mp3 +0 -0
  54. pycaps/effect/sound/presets/click.mp3 +0 -0
  55. pycaps/effect/sound/presets/ding-long.mp3 +0 -0
  56. pycaps/effect/sound/presets/ding-short.mp3 +0 -0
  57. pycaps/effect/sound/presets/ding.mp3 +0 -0
  58. pycaps/effect/sound/presets/glitch-static.mp3 +0 -0
  59. pycaps/effect/sound/presets/glitch.mp3 +0 -0
  60. pycaps/effect/sound/presets/heart-beat.mp3 +0 -0
  61. pycaps/effect/sound/presets/hit-intense.mp3 +0 -0
  62. pycaps/effect/sound/presets/hit-strong.mp3 +0 -0
  63. pycaps/effect/sound/presets/pop-2.mp3 +0 -0
  64. pycaps/effect/sound/presets/pop.mp3 +0 -0
  65. pycaps/effect/sound/presets/slide-paper.mp3 +0 -0
  66. pycaps/effect/sound/presets/swoosh.mp3 +0 -0
  67. pycaps/effect/sound/presets/whoosh-2.mp3 +0 -0
  68. pycaps/effect/sound/presets/whoosh-deep.mp3 +0 -0
  69. pycaps/effect/sound/presets/whoosh.mp3 +0 -0
  70. pycaps/effect/sound/sound.py +15 -0
  71. pycaps/effect/sound/sound_effect.py +74 -0
  72. pycaps/effect/text/__init__.py +14 -0
  73. pycaps/effect/text/emoji_in_segment_effect.py +88 -0
  74. pycaps/effect/text/emoji_in_segment_getter.py +34 -0
  75. pycaps/effect/text/emoji_in_segment_llm_getter.py +38 -0
  76. pycaps/effect/text/emoji_in_word_effect.py +43 -0
  77. pycaps/effect/text/modify_words_effect.py +26 -0
  78. pycaps/effect/text/remove_punctuation_marks_effect.py +41 -0
  79. pycaps/effect/text/text_effect.py +4 -0
  80. pycaps/layout/__init__.py +19 -0
  81. pycaps/layout/definitions.py +61 -0
  82. pycaps/layout/layout_updater.py +55 -0
  83. pycaps/layout/layout_utils.py +38 -0
  84. pycaps/layout/line_splitter.py +75 -0
  85. pycaps/layout/positions_calculator.py +105 -0
  86. pycaps/layout/word_size_calculator.py +19 -0
  87. pycaps/logger.py +50 -0
  88. pycaps/pipeline/__init__.py +9 -0
  89. pycaps/pipeline/caps_pipeline.py +290 -0
  90. pycaps/pipeline/caps_pipeline_builder.py +137 -0
  91. pycaps/pipeline/json_config_loader.py +231 -0
  92. pycaps/pipeline/json_schema.py +181 -0
  93. pycaps/pipeline/subtitle_data_service.py +14 -0
  94. pycaps/renderer/__init__.py +13 -0
  95. pycaps/renderer/css_subtitle_renderer.py +296 -0
  96. pycaps/renderer/letter_size_cache.py +26 -0
  97. pycaps/renderer/pictex_subtitle_renderer.py +147 -0
  98. pycaps/renderer/playwright_screenshot_capturer.py +38 -0
  99. pycaps/renderer/previewer/__init__.py +3 -0
  100. pycaps/renderer/previewer/css_subtitle_previewer.py +52 -0
  101. pycaps/renderer/previewer/previewer.html +340 -0
  102. pycaps/renderer/rendered_image_cache.py +35 -0
  103. pycaps/renderer/renderer_page.py +80 -0
  104. pycaps/renderer/subtitle_renderer.py +36 -0
  105. pycaps/selector/__init__.py +11 -0
  106. pycaps/selector/tag_based_selector.py +17 -0
  107. pycaps/selector/time_event_selector.py +72 -0
  108. pycaps/selector/word_clip_selector.py +31 -0
  109. pycaps/tag/__init__.py +11 -0
  110. pycaps/tag/definitions.py +19 -0
  111. pycaps/tag/tag_condition.py +113 -0
  112. pycaps/tag/tagger/__init__.py +7 -0
  113. pycaps/tag/tagger/ai_tagger.py +28 -0
  114. pycaps/tag/tagger/external_llm_tagger.py +76 -0
  115. pycaps/tag/tagger/semantic_tagger.py +114 -0
  116. pycaps/tag/tagger/structure_tagger.py +51 -0
  117. pycaps/template/__init__.py +17 -0
  118. pycaps/template/builtin_template.py +10 -0
  119. pycaps/template/constants.py +3 -0
  120. pycaps/template/local_template.py +8 -0
  121. pycaps/template/preset/classic/pycaps.template.json +3 -0
  122. pycaps/template/preset/classic/styles.css +10 -0
  123. pycaps/template/preset/default/pycaps.template.json +16 -0
  124. pycaps/template/preset/default/resources/black.ttf +0 -0
  125. pycaps/template/preset/default/styles.css +16 -0
  126. pycaps/template/preset/explosive/pycaps.template.json +66 -0
  127. pycaps/template/preset/explosive/resources/black.ttf +0 -0
  128. pycaps/template/preset/explosive/styles.css +44 -0
  129. pycaps/template/preset/fast/pycaps.template.json +21 -0
  130. pycaps/template/preset/fast/styles.css +15 -0
  131. pycaps/template/preset/hype/pycaps.template.json +65 -0
  132. pycaps/template/preset/hype/resources/komika.ttf +0 -0
  133. pycaps/template/preset/hype/styles.css +26 -0
  134. pycaps/template/preset/line-focus/pycaps.template.json +29 -0
  135. pycaps/template/preset/line-focus/resources/black.ttf +0 -0
  136. pycaps/template/preset/line-focus/styles.css +32 -0
  137. pycaps/template/preset/minimalist/pycaps.template.json +33 -0
  138. pycaps/template/preset/minimalist/styles.css +26 -0
  139. pycaps/template/preset/model/main.py +0 -0
  140. pycaps/template/preset/model/preview.hash +4 -0
  141. pycaps/template/preset/model/pycaps.template.json +0 -0
  142. pycaps/template/preset/model/styles.css +0 -0
  143. pycaps/template/preset/neo-minimal/pycaps.template.json +44 -0
  144. pycaps/template/preset/neo-minimal/styles.css +46 -0
  145. pycaps/template/preset/retro-gaming/pycaps.template.json +48 -0
  146. pycaps/template/preset/retro-gaming/resources/PressStart2P.ttf +0 -0
  147. pycaps/template/preset/retro-gaming/styles.css +29 -0
  148. pycaps/template/preset/vibrant/pycaps.template.json +57 -0
  149. pycaps/template/preset/vibrant/resources/black.ttf +0 -0
  150. pycaps/template/preset/vibrant/styles.css +34 -0
  151. pycaps/template/preset/word-focus/pycaps.template.json +29 -0
  152. pycaps/template/preset/word-focus/resources/black.ttf +0 -0
  153. pycaps/template/preset/word-focus/styles.css +22 -0
  154. pycaps/template/template.py +14 -0
  155. pycaps/template/template_factory.py +17 -0
  156. pycaps/template/template_loader.py +31 -0
  157. pycaps/template/template_service.py +21 -0
  158. pycaps/transcriber/__init__.py +23 -0
  159. pycaps/transcriber/base_transcriber.py +18 -0
  160. pycaps/transcriber/editor/__init__.py +3 -0
  161. pycaps/transcriber/editor/editor.html +731 -0
  162. pycaps/transcriber/editor/transcription_editor.py +54 -0
  163. pycaps/transcriber/google_audio_transcriber.py +103 -0
  164. pycaps/transcriber/preview_transcriber.py +29 -0
  165. pycaps/transcriber/splitter/__init__.py +12 -0
  166. pycaps/transcriber/splitter/base_segment_splitter.py +12 -0
  167. pycaps/transcriber/splitter/limit_by_chars_splitter.py +87 -0
  168. pycaps/transcriber/splitter/limit_by_words_splitter.py +45 -0
  169. pycaps/transcriber/splitter/split_into_sentences_splitter.py +33 -0
  170. pycaps/transcriber/transcript_format.py +9 -0
  171. pycaps/transcriber/transcript_loader.py +383 -0
  172. pycaps/transcriber/whisper_audio_transcriber.py +88 -0
  173. pycaps/utils/__init__.py +7 -0
  174. pycaps/utils/script_utils.py +30 -0
  175. pycaps/utils/time_utils.py +3 -0
  176. pycaps/video/__init__.py +7 -0
  177. pycaps/video/audio_utils.py +29 -0
  178. pycaps/video/subtitle_clips_generator.py +103 -0
  179. pycaps/video/video_generator.py +128 -0
  180. pycaps_ai-0.2.1.dist-info/METADATA +225 -0
  181. pycaps_ai-0.2.1.dist-info/RECORD +185 -0
  182. pycaps_ai-0.2.1.dist-info/WHEEL +5 -0
  183. pycaps_ai-0.2.1.dist-info/entry_points.txt +2 -0
  184. pycaps_ai-0.2.1.dist-info/licenses/LICENSE +21 -0
  185. pycaps_ai-0.2.1.dist-info/top_level.txt +1 -0
pycaps/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .pipeline import CapsPipeline, CapsPipelineBuilder, JsonConfigLoader
2
+ from .renderer import CssSubtitleRenderer, PictexSubtitleRenderer
3
+ from .transcriber import WhisperAudioTranscriber, GoogleAudioTranscriber, AudioTranscriber, LimitByWordsSplitter, LimitByCharsSplitter, SplitIntoSentencesSplitter, TranscriptFormat, load_transcription
4
+ from .effect import *
5
+ from .animation import *
6
+ from .selector import WordClipSelector
7
+ from .tag import TagCondition, BuiltinTag, TagConditionFactory, SemanticTagger
8
+ from .common import *
9
+ from .layout.definitions import *
10
+ from .ai import LlmProvider
11
+ from .template import TemplateLoader, TemplateFactory, DEFAULT_TEMPLATE_NAME
12
+
13
+ __version__ = "0.2.1"
pycaps/ai/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .gpt import Gpt
2
+ from .llm import Llm
3
+ from .llm_provider import LlmProvider
4
+
5
+ __all__ = ["Gpt", "Llm", "LlmProvider"]
pycaps/ai/gpt.py ADDED
@@ -0,0 +1,35 @@
1
+ from pycaps.ai.llm import Llm
2
+ import os
3
+
4
+ class Gpt(Llm):
5
+
6
+ OPENAI_API_KEY_NAME = "PYCAPS_OPENAI_API_KEY"
7
+
8
+ def __init__(self):
9
+ self._client = None
10
+
11
+ def send_message(self, prompt: str, model: str = "gpt-4.1-mini") -> str:
12
+ return self._get_client().responses.create(model=model, input=prompt).output_text
13
+
14
+ def is_enabled(self) -> bool:
15
+ return os.getenv(self.OPENAI_API_KEY_NAME) is not None
16
+
17
+ def _get_client(self):
18
+ try:
19
+ from openai import OpenAI
20
+
21
+ if self._client:
22
+ return self._client
23
+
24
+ self._client = OpenAI(api_key=os.getenv(self.OPENAI_API_KEY_NAME))
25
+ return self._client
26
+ except ImportError:
27
+ raise ImportError(
28
+ "OpenAI API not found. "
29
+ "Please install it with: pip install openai"
30
+ )
31
+ except Exception as e:
32
+ raise RuntimeError(
33
+ f"Error initializing OpenAI client: {e}\n\n"
34
+ "Please ensure you have authenticated correctly via PYCAPS_OPENAI_API_KEY."
35
+ )
pycaps/ai/llm.py ADDED
@@ -0,0 +1,11 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class Llm(ABC):
4
+
5
+ @abstractmethod
6
+ def send_message(self, message: str, model: str) -> str:
7
+ pass
8
+
9
+ @abstractmethod
10
+ def is_enabled(self) -> bool:
11
+ pass
@@ -0,0 +1,16 @@
1
+ from typing import Optional
2
+ from .llm import Llm
3
+ from .gpt import Gpt
4
+
5
+ class LlmProvider:
6
+ _llm: Optional[Llm] = None
7
+
8
+ @staticmethod
9
+ def get() -> Llm:
10
+ if LlmProvider._llm is None:
11
+ LlmProvider._llm = Gpt()
12
+ return LlmProvider._llm
13
+
14
+ @staticmethod
15
+ def set(llm: Llm):
16
+ LlmProvider._llm = llm
@@ -0,0 +1,31 @@
1
+ # src/pycaps/animator/__init__.py
2
+
3
+ from .builtin import *
4
+ from .definitions import Transformer, OvershootConfig, Direction
5
+ from .primitive_animation import PrimitiveAnimation
6
+ from .preset_animation import PresetAnimation
7
+ from .animation import Animation
8
+ from .element_animator import ElementAnimator
9
+
10
+ __all__ = [
11
+ "Animation",
12
+ "PrimitiveAnimation",
13
+ "PresetAnimation",
14
+ "ElementAnimator",
15
+ "Transformer",
16
+ "OvershootConfig",
17
+ "Direction",
18
+ "SlideInPrimitive",
19
+ "ZoomInPrimitive",
20
+ "PopInPrimitive",
21
+ "FadeInPrimitive",
22
+ "FadeIn",
23
+ "FadeOut",
24
+ "PopIn",
25
+ "PopOut",
26
+ "PopInBounce",
27
+ "SlideIn",
28
+ "SlideOut",
29
+ "ZoomIn",
30
+ "ZoomOut",
31
+ ]
@@ -0,0 +1,11 @@
1
+ from pycaps.common import WordClip, ElementType
2
+ from abc import ABC, abstractmethod
3
+
4
+ class Animation(ABC):
5
+ def __init__(self, duration: float, delay: float = 0.0) -> None:
6
+ self._duration: float = duration
7
+ self._delay: float = delay
8
+
9
+ @abstractmethod
10
+ def run(self, clip: WordClip, offset: float, what: ElementType) -> None:
11
+ pass
@@ -0,0 +1,18 @@
1
+ from .primitive import *
2
+ from .preset import *
3
+
4
+ __all__ = [
5
+ "FadeIn",
6
+ "FadeOut",
7
+ "SlideIn",
8
+ "SlideOut",
9
+ "ZoomIn",
10
+ "ZoomOut",
11
+ "PopIn",
12
+ "PopOut",
13
+ "PopInBounce",
14
+ "FadeInPrimitive",
15
+ "PopInPrimitive",
16
+ "ZoomInPrimitive",
17
+ "SlideInPrimitive",
18
+ ]
@@ -0,0 +1,21 @@
1
+ from .fade_in import FadeIn
2
+ from .fade_out import FadeOut
3
+ from .pop_in import PopIn
4
+ from .pop_out import PopOut
5
+ from .pop_in_bounce import PopInBounce
6
+ from .slide_in import SlideIn
7
+ from .slide_out import SlideOut
8
+ from .zoom_in import ZoomIn
9
+ from .zoom_out import ZoomOut
10
+
11
+ __all__ = [
12
+ "FadeIn",
13
+ "FadeOut",
14
+ "PopIn",
15
+ "PopOut",
16
+ "PopInBounce",
17
+ "SlideIn",
18
+ "SlideOut",
19
+ "ZoomIn",
20
+ "ZoomOut",
21
+ ]
@@ -0,0 +1,17 @@
1
+ from ...animation import Animation
2
+ from ...preset_animation import PresetAnimation
3
+ from ..primitive import FadeInPrimitive
4
+ from typing import List
5
+
6
+ class FadeIn(PresetAnimation):
7
+
8
+ def __init__(self, duration: float = 0.2, delay: float = 0.0):
9
+ super().__init__(duration, delay)
10
+
11
+ def _build_animations(self) -> List[Animation]:
12
+ return [
13
+ FadeInPrimitive(
14
+ duration=self._duration,
15
+ delay=self._delay,
16
+ )
17
+ ]
@@ -0,0 +1,19 @@
1
+ from ...animation import Animation
2
+ from ...preset_animation import PresetAnimation
3
+ from typing import List
4
+ from ...definitions import Transformer
5
+ from ..primitive import FadeInPrimitive
6
+
7
+ class FadeOut(PresetAnimation):
8
+
9
+ def __init__(self, duration: float = 0.2, delay: float = 0.0):
10
+ super().__init__(duration, delay)
11
+
12
+ def _build_animations(self) -> List[Animation]:
13
+ return [
14
+ FadeInPrimitive(
15
+ duration=self._duration,
16
+ delay=self._delay,
17
+ transformer=Transformer.INVERT
18
+ )
19
+ ]
@@ -0,0 +1,22 @@
1
+ from ...animation import Animation
2
+ from ...preset_animation import PresetAnimation
3
+ from ..primitive import PopInPrimitive, FadeInPrimitive
4
+ from typing import List
5
+ from ...definitions import OvershootConfig
6
+
7
+ class PopIn(PresetAnimation):
8
+
9
+ def __init__(self, duration: float = 0.3, delay: float = 0.0):
10
+ super().__init__(duration, delay)
11
+
12
+ def _build_animations(self) -> List[Animation]:
13
+ return [
14
+ PopInPrimitive(
15
+ duration=self._duration,
16
+ delay=self._delay,
17
+ overshoot=OvershootConfig(),
18
+ min_scale=0.5,
19
+ init_scale=0.5
20
+ ),
21
+ FadeInPrimitive(duration=self._duration, delay=self._delay)
22
+ ]
@@ -0,0 +1,20 @@
1
+ from ...animation import Animation
2
+ from ...preset_animation import PresetAnimation
3
+ from ..primitive import PopInPrimitive, FadeInPrimitive
4
+ from typing import List
5
+ from ...definitions import OvershootConfig
6
+
7
+ class PopInBounce(PresetAnimation):
8
+
9
+ def __init__(self, duration: float = 0.4, delay: float = 0.0):
10
+ super().__init__(duration, delay)
11
+
12
+ def _build_animations(self) -> List[Animation]:
13
+ return [
14
+ PopInPrimitive(
15
+ duration=self._duration,
16
+ delay=self._delay,
17
+ overshoot=OvershootConfig()
18
+ ),
19
+ FadeInPrimitive(duration=self._duration*0.2, delay=self._delay)
20
+ ]
@@ -0,0 +1,22 @@
1
+ from ...animation import Animation
2
+ from ...preset_animation import PresetAnimation
3
+ from ..primitive import PopInPrimitive, FadeInPrimitive
4
+ from typing import List
5
+ from ...definitions import Transformer
6
+
7
+ class PopOut(PresetAnimation):
8
+
9
+ def __init__(self, duration: float = 0.2, delay: float = 0.0):
10
+ super().__init__(duration, delay)
11
+
12
+ def _build_animations(self) -> List[Animation]:
13
+ return [
14
+ PopInPrimitive(
15
+ duration=self._duration,
16
+ delay=self._delay,
17
+ transformer=Transformer.INVERT,
18
+ init_scale=0.2,
19
+ min_scale=0.2
20
+ ),
21
+ FadeInPrimitive(duration=self._duration, delay=self._delay, transformer=Transformer.INVERT)
22
+ ]
@@ -0,0 +1,25 @@
1
+ from ...animation import Animation
2
+ from ...preset_animation import PresetAnimation
3
+ from ..primitive import SlideInPrimitive, FadeInPrimitive
4
+ from typing import List
5
+ from ...definitions import Direction, OvershootConfig
6
+
7
+ class SlideIn(PresetAnimation):
8
+
9
+ def __init__(self, direction: Direction = Direction.LEFT, duration: float = 0.3, delay: float = 0.0) -> None:
10
+ super().__init__(duration, delay)
11
+ self._direction: Direction = direction
12
+
13
+ def _build_animations(self) -> List[Animation]:
14
+ return [
15
+ SlideInPrimitive(
16
+ duration=self._duration,
17
+ delay=self._delay,
18
+ direction=self._direction,
19
+ overshoot=OvershootConfig()
20
+ ),
21
+ FadeInPrimitive(
22
+ duration=self._duration,
23
+ delay=self._delay,
24
+ )
25
+ ]
@@ -0,0 +1,26 @@
1
+ from ...animation import Animation
2
+ from ...preset_animation import PresetAnimation
3
+ from ..primitive import SlideInPrimitive, FadeInPrimitive
4
+ from typing import List
5
+ from ...definitions import Transformer, Direction
6
+
7
+ class SlideOut(PresetAnimation):
8
+
9
+ def __init__(self, direction: Direction = Direction.RIGHT, duration: float = 0.3, delay: float = 0.0) -> None:
10
+ super().__init__(duration, delay)
11
+ self._direction: Direction = direction
12
+
13
+ def _build_animations(self) -> List[Animation]:
14
+ return [
15
+ SlideInPrimitive(
16
+ duration=self._duration,
17
+ delay=self._delay,
18
+ direction=self._direction,
19
+ transformer=Transformer.INVERT
20
+ ),
21
+ FadeInPrimitive(
22
+ duration=self._duration,
23
+ delay=self._delay,
24
+ transformer=Transformer.INVERT
25
+ )
26
+ ]
@@ -0,0 +1,24 @@
1
+ from ...animation import Animation
2
+ from ...preset_animation import PresetAnimation
3
+ from ..primitive import ZoomInPrimitive, FadeInPrimitive
4
+ from typing import List
5
+ from ...definitions import OvershootConfig, Transformer
6
+
7
+ class ZoomIn(PresetAnimation):
8
+
9
+ def __init__(self, duration: float = 0.3, delay: float = 0.0):
10
+ super().__init__(duration, delay)
11
+
12
+ def _build_animations(self) -> List[Animation]:
13
+ return [
14
+ ZoomInPrimitive(
15
+ duration=self._duration,
16
+ delay=self._delay,
17
+ overshoot=OvershootConfig(),
18
+ transformer=Transformer.EASE_OUT
19
+ ),
20
+ FadeInPrimitive(
21
+ duration=self._duration*0.5,
22
+ delay=self._delay,
23
+ )
24
+ ]
@@ -0,0 +1,25 @@
1
+ from ...animation import Animation
2
+ from ...preset_animation import PresetAnimation
3
+ from ..primitive import ZoomInPrimitive, FadeInPrimitive
4
+ from typing import List
5
+ from ...definitions import Transformer
6
+
7
+ class ZoomOut(PresetAnimation):
8
+
9
+ def __init__(self, duration: float = 0.3, delay: float = 0.0):
10
+ super().__init__(duration, delay)
11
+
12
+ def _build_animations(self) -> List[Animation]:
13
+ return [
14
+ ZoomInPrimitive(
15
+ duration=self._duration,
16
+ delay=self._delay,
17
+ init_scale=0.2,
18
+ transformer=Transformer.INVERT
19
+ ),
20
+ FadeInPrimitive(
21
+ duration=self._duration,
22
+ delay=self._delay,
23
+ transformer=Transformer.INVERT
24
+ )
25
+ ]
@@ -0,0 +1,11 @@
1
+ from .fade_in_primitive import FadeInPrimitive
2
+ from .pop_in_primitive import PopInPrimitive
3
+ from .zoom_in_primitive import ZoomInPrimitive
4
+ from .slide_in_primitive import SlideInPrimitive
5
+
6
+ __all__ = [
7
+ "FadeInPrimitive",
8
+ "PopInPrimitive",
9
+ "ZoomInPrimitive",
10
+ "SlideInPrimitive",
11
+ ]
@@ -0,0 +1,6 @@
1
+ from ...primitive_animation import PrimitiveAnimation
2
+ from pycaps.common import WordClip
3
+
4
+ class FadeInPrimitive(PrimitiveAnimation):
5
+ def _apply_animation(self, clip: WordClip, offset: float) -> None:
6
+ self._apply_opacity(clip, offset, lambda t: t)
@@ -0,0 +1,64 @@
1
+ from typing import Tuple, Callable, Optional
2
+ from ...definitions import Transformer, OvershootConfig
3
+ from ...primitive_animation import PrimitiveAnimation
4
+ from pycaps.common import WordClip
5
+ from pycaps.layout import LayoutUtils
6
+
7
+ class PopInPrimitive(PrimitiveAnimation):
8
+ def __init__(
9
+ self,
10
+ duration: float,
11
+ delay: float = 0.0,
12
+ transformer: Callable[[float], float] = Transformer.LINEAR,
13
+ init_scale: float = 0.7,
14
+ min_scale: float = 0.3,
15
+ min_scale_at: float = 0.5,
16
+ overshoot: Optional[OvershootConfig] = None,
17
+ ) -> None:
18
+ super().__init__(duration, delay, transformer)
19
+ self._init_scale: float = init_scale
20
+ self._min_scale: float = min_scale
21
+ self._min_scale_at: float = min_scale_at
22
+ self._overshoot: Optional[OvershootConfig] = overshoot
23
+
24
+ if self._overshoot is not None and self._min_scale_at >= self._overshoot.peak_at:
25
+ raise ValueError("min_scale_at must be less than overshoot.peak_at")
26
+
27
+ def _apply_animation(self, clip: WordClip, offset: float) -> None:
28
+ group_center = LayoutUtils.get_clip_container_center(clip, self._what)
29
+ word_original_width = clip.layout.size.width
30
+ word_original_height = clip.layout.size.height
31
+ word_final_center_x = clip.layout.position.x + word_original_width / 2
32
+ word_final_center_y = clip.layout.position.y + word_original_height / 2
33
+
34
+ def get_size_factor(t: float) -> float:
35
+ peak_at = self._overshoot.peak_at if self._overshoot is not None else 1.0
36
+ overshoot_scale = 1 + self._overshoot.amount if self._overshoot is not None else 1.0
37
+
38
+ if t < self._min_scale_at:
39
+ progress = t / self._min_scale_at
40
+ return self._init_scale + (self._min_scale - self._init_scale) * progress
41
+ elif self._min_scale_at < t < peak_at:
42
+ progress = (t - self._min_scale_at) / (peak_at - self._min_scale_at)
43
+ return self._min_scale + (overshoot_scale - self._min_scale) * progress
44
+ elif peak_at < t < 1.0:
45
+ progress = (t - peak_at) / (1.0 - peak_at) if peak_at != 1.0 else 1.0
46
+ return overshoot_scale + (1.0 - overshoot_scale) * progress
47
+ else:
48
+ return 1.0
49
+
50
+ def get_position(t: float) -> Tuple[float, float]:
51
+ scale = get_size_factor(t)
52
+ current_width = word_original_width * scale
53
+ current_height = word_original_height * scale
54
+
55
+ current_center_x = group_center[0] + (word_final_center_x - group_center[0]) * t
56
+ current_center_y = group_center[1] + (word_final_center_y - group_center[1]) * t
57
+
58
+ final_x = current_center_x - (current_width / 2)
59
+ final_y = current_center_y - (current_height / 2)
60
+
61
+ return (final_x, final_y)
62
+
63
+ self._apply_size(clip, offset, get_size_factor)
64
+ self._apply_position(clip, offset, get_position)
@@ -0,0 +1,54 @@
1
+ from ...primitive_animation import PrimitiveAnimation
2
+ from ...definitions import Direction, OvershootConfig, Transformer
3
+ from pycaps.common import WordClip
4
+ from typing import Tuple, Callable, Optional
5
+
6
+ class SlideInPrimitive(PrimitiveAnimation):
7
+ def __init__(
8
+ self,
9
+ duration: float,
10
+ delay: float = 0.0,
11
+ transformer: Callable[[float], float] = Transformer.LINEAR,
12
+ direction: Direction = Direction.LEFT,
13
+ distance: float = 100,
14
+ overshoot: Optional[OvershootConfig] = None,
15
+ ) -> None:
16
+ super().__init__(duration, delay, transformer)
17
+ self._direction: Direction = direction
18
+ self._distance: float = distance
19
+ self._overshoot: Optional[OvershootConfig] = overshoot
20
+
21
+ def _apply_animation(self, clip: WordClip, offset: float) -> None:
22
+ final_pos = clip.layout.position
23
+
24
+ def get_displacement(t: float) -> float:
25
+ if self._overshoot is None:
26
+ return self._distance * (t-1)
27
+
28
+ overshoot_distance = self._distance * self._overshoot.amount
29
+ if t < self._overshoot.peak_at:
30
+ progress = t / self._overshoot.peak_at
31
+ start_offset = -(self._distance)
32
+ target_offset = overshoot_distance
33
+ return start_offset + (target_offset - start_offset) * progress
34
+ else:
35
+ progress = (t - self._overshoot.peak_at) / (1.0 - self._overshoot.peak_at)
36
+ start_offset = overshoot_distance
37
+ target_offset = 0
38
+ return start_offset + (target_offset - start_offset) * progress
39
+
40
+ def get_position(t: float) -> Tuple[float, float]:
41
+ current_displacement = get_displacement(t)
42
+
43
+ if self._direction == Direction.LEFT:
44
+ return final_pos.x + current_displacement, final_pos.y
45
+ elif self._direction == Direction.RIGHT:
46
+ return final_pos.x - current_displacement, final_pos.y
47
+ elif self._direction == Direction.UP:
48
+ return final_pos.x, final_pos.y + current_displacement
49
+ elif self._direction == Direction.DOWN:
50
+ return final_pos.x, final_pos.y - current_displacement
51
+ return final_pos.x, final_pos.y
52
+
53
+
54
+ self._apply_position(clip, offset, get_position)
@@ -0,0 +1,64 @@
1
+ from pycaps.common import WordClip
2
+ from pycaps.layout import LayoutUtils
3
+ from typing import Tuple, Callable, Optional
4
+ from ...definitions import Transformer, OvershootConfig
5
+ from ...primitive_animation import PrimitiveAnimation
6
+
7
+ # TODO: it has an error while size is being animated, it's probably a precision error (because of using floats for the size in the scale)
8
+ # To notice it, you need to use a color background for a whole line, and run this animation over the line.
9
+ class ZoomInPrimitive(PrimitiveAnimation):
10
+
11
+ def __init__(
12
+ self,
13
+ duration: float,
14
+ delay: float = 0.0,
15
+ transformer: Callable[[float], float] = Transformer.LINEAR,
16
+ init_scale: float = 0.5,
17
+ overshoot: Optional[OvershootConfig] = None,
18
+ ) -> None:
19
+ super().__init__(duration, delay, transformer)
20
+ self._init_scale: float = init_scale
21
+ self._overshoot: Optional[OvershootConfig] = overshoot
22
+
23
+ def _apply_animation(self, clip: WordClip, offset: float) -> None:
24
+ group_center = LayoutUtils.get_clip_container_center(clip, self._what)
25
+ word_final_center = (
26
+ clip.layout.position.x + clip.layout.size.width / 2,
27
+ clip.layout.position.y + clip.layout.size.height / 2
28
+ )
29
+ relative_pos_vector = (
30
+ word_final_center[0] - group_center[0],
31
+ word_final_center[1] - group_center[1]
32
+ )
33
+
34
+ def get_size_factor(t: float) -> float:
35
+ if self._overshoot is None:
36
+ return self._init_scale + (1.0 - self._init_scale) * t
37
+
38
+ if t < self._overshoot.peak_at:
39
+ progress = t / self._overshoot.peak_at
40
+ start_offset = self._init_scale
41
+ target_offset = 1 + self._overshoot.amount
42
+ return start_offset + (target_offset - start_offset) * progress
43
+
44
+ progress = (t - self._overshoot.peak_at) / (1.0 - self._overshoot.peak_at)
45
+ start_offset = 1 + self._overshoot.amount
46
+ target_offset = 1
47
+ return start_offset - (start_offset - target_offset) * progress
48
+
49
+ def get_position(t: float) -> Tuple[float, float]:
50
+ progress = get_size_factor(t)
51
+
52
+ current_width = clip.layout.size.width * progress
53
+ current_height = clip.layout.size.height * progress
54
+
55
+ current_center_x = group_center[0] + (relative_pos_vector[0] * progress)
56
+ current_center_y = group_center[1] + (relative_pos_vector[1] * progress)
57
+
58
+ final_x = current_center_x - (current_width / 2)
59
+ final_y = current_center_y - (current_height / 2)
60
+
61
+ return (final_x, final_y)
62
+
63
+ self._apply_position(clip, offset, get_position)
64
+ self._apply_size(clip, offset, get_size_factor)
@@ -0,0 +1,22 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+ class Direction(str, Enum):
6
+ LEFT = "left"
7
+ RIGHT = "right"
8
+ UP = "up"
9
+ DOWN = "down"
10
+
11
+ class OvershootConfig(BaseModel):
12
+ model_config = ConfigDict(frozen=True)
13
+
14
+ amount: float = 0.1
15
+ peak_at: float = 0.7
16
+
17
+ class Transformer:
18
+ LINEAR = lambda t: t
19
+ EASE_IN = lambda t: t**2
20
+ EASE_OUT = lambda t: 1 - (1 - t)**2
21
+ EASE_IN_OUT = lambda t: t**2 * (3 - 2 * t)
22
+ INVERT = lambda t: 1 - t
@@ -0,0 +1,52 @@
1
+ from typing import Optional, List
2
+ from pycaps.common import ElementType, EventType, Document, WordClip
3
+ from pycaps.tag import TagCondition
4
+ from pycaps.selector import WordClipSelector
5
+ from .animation import Animation
6
+
7
+ class ElementAnimator:
8
+
9
+ def __init__(self, animation: Animation, when: EventType, what: ElementType, tag_condition: Optional[TagCondition] = None) -> None:
10
+ self._animation: Animation = animation
11
+ self._when: EventType = when
12
+ self._what: ElementType = what
13
+ self._tag_condition: Optional[TagCondition] = tag_condition
14
+
15
+ def run(self, document: Document) -> None:
16
+ clips = self._filter_clips(document)
17
+ for clip in clips:
18
+ offset = self.__get_time_offset(clip)
19
+ self._animation.run(clip, offset, self._what)
20
+
21
+ def _filter_clips(self, document: Document) -> List[WordClip]:
22
+ selector = WordClipSelector().filter_by_time(self._when, self._what, self._animation._duration, self._animation._delay)
23
+ if self._tag_condition:
24
+ selector = selector.filter_by_tag(self._tag_condition)
25
+ return selector.select(document)
26
+
27
+ def __get_time_offset(self, clip: WordClip) -> float:
28
+ if self._when == EventType.ON_NARRATION_STARTS:
29
+ return self.__get_on_start_offset(clip)
30
+ elif self._when == EventType.ON_NARRATION_ENDS:
31
+ return self.__get_on_end_offset(clip)
32
+
33
+ def __get_on_start_offset(self, clip: WordClip) -> float:
34
+ start_time = 0
35
+ if self._what == ElementType.WORD:
36
+ start_time = clip.get_word().time.start
37
+ elif self._what == ElementType.LINE:
38
+ start_time = clip.get_line().time.start
39
+ elif self._what == ElementType.SEGMENT:
40
+ start_time = clip.get_segment().time.start
41
+
42
+ return clip.media_clip.start - start_time - self._animation._delay
43
+
44
+ def __get_on_end_offset(self, clip: WordClip) -> float:
45
+ end_time = 0
46
+ if self._what == ElementType.WORD:
47
+ end_time = clip.get_word().time.end
48
+ elif self._what == ElementType.LINE:
49
+ end_time = clip.get_line().time.end
50
+ elif self._what == ElementType.SEGMENT:
51
+ end_time = clip.get_segment().time.end
52
+ return -(end_time - self._animation._duration - self._animation._delay - clip.media_clip.start)