videopython 0.33.2__tar.gz → 0.33.4__tar.gz
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.
- videopython-0.33.4/PKG-INFO +133 -0
- videopython-0.33.4/README.md +84 -0
- {videopython-0.33.2 → videopython-0.33.4}/pyproject.toml +3 -1
- videopython-0.33.4/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- videopython-0.33.4/src/videopython/base/fonts/LICENSE_DEJAVU +99 -0
- videopython-0.33.4/src/videopython/base/fonts/__init__.py +58 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/base/image_text.py +22 -22
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/base/transcription.py +93 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/editing/effects.py +2 -6
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/editing/transcription_overlay.py +27 -1
- videopython-0.33.2/PKG-INFO +0 -258
- videopython-0.33.2/README.md +0 -209
- {videopython-0.33.2 → videopython-0.33.4}/.gitignore +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/LICENSE +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/__init__.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/__init__.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/_device.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/__init__.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/config.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/dubber.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/expressiveness.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/loudness.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/models.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/pipeline.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/quality.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/remux.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/generation/audio.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/generation/image.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/generation/qwen3.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/generation/translation.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/generation/video.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/transforms.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/understanding/audio.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/understanding/faces.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/understanding/image.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/understanding/separation.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/understanding/temporal.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/video_analysis/__init__.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/video_analysis/analyzer.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/video_analysis/models.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/video_analysis/sampling.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/ai/video_analysis/stages.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/audio/audio.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/base/__init__.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/base/description.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/base/video.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/editing/operation.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/editing/streaming.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/editing/video_edit.py +0 -0
- {videopython-0.33.2 → videopython-0.33.4}/src/videopython/py.typed +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: videopython
|
|
3
|
+
Version: 0.33.4
|
|
4
|
+
Summary: Minimal video generation and processing library.
|
|
5
|
+
Project-URL: Homepage, https://videopython.com
|
|
6
|
+
Project-URL: Repository, https://github.com/bartwojtowicz/videopython/
|
|
7
|
+
Project-URL: Documentation, https://videopython.com
|
|
8
|
+
Author-email: Bartosz Wójtowicz <bartoszwojtowicz@outlook.com>, Bartosz Rudnikowicz <bartoszrudnikowicz840@gmail.com>, Piotr Pukisz <piotr.pukisz@gmail.com>
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,editing,generation,movie,opencv,python,shorts,video,videopython
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: <3.14,>=3.10
|
|
20
|
+
Requires-Dist: numpy>=1.25.2
|
|
21
|
+
Requires-Dist: opencv-python-headless>=4.9.0.80
|
|
22
|
+
Requires-Dist: pillow>=12.1.1
|
|
23
|
+
Requires-Dist: pydantic>=2.8.0
|
|
24
|
+
Requires-Dist: tqdm>=4.66.3
|
|
25
|
+
Provides-Extra: ai
|
|
26
|
+
Requires-Dist: accelerate>=0.29.2; extra == 'ai'
|
|
27
|
+
Requires-Dist: chatterbox-tts>=0.1.7; extra == 'ai'
|
|
28
|
+
Requires-Dist: demucs>=4.0.0; extra == 'ai'
|
|
29
|
+
Requires-Dist: diffusers>=0.30.0; extra == 'ai'
|
|
30
|
+
Requires-Dist: hf-transfer>=0.1.9; extra == 'ai'
|
|
31
|
+
Requires-Dist: imagehash>=4.3; extra == 'ai'
|
|
32
|
+
Requires-Dist: llama-cpp-python>=0.3.0; extra == 'ai'
|
|
33
|
+
Requires-Dist: numba>=0.61.0; extra == 'ai'
|
|
34
|
+
Requires-Dist: ollama>=0.4.5; extra == 'ai'
|
|
35
|
+
Requires-Dist: openai-whisper>=20240930; extra == 'ai'
|
|
36
|
+
Requires-Dist: pyannote-audio>=4.0.0; extra == 'ai'
|
|
37
|
+
Requires-Dist: pyloudnorm>=0.1.1; extra == 'ai'
|
|
38
|
+
Requires-Dist: qwen-vl-utils>=0.0.10; extra == 'ai'
|
|
39
|
+
Requires-Dist: scikit-learn>=1.3.0; extra == 'ai'
|
|
40
|
+
Requires-Dist: scipy>=1.10.0; extra == 'ai'
|
|
41
|
+
Requires-Dist: sentencepiece>=0.1.99; extra == 'ai'
|
|
42
|
+
Requires-Dist: silero-vad>=5.1; extra == 'ai'
|
|
43
|
+
Requires-Dist: torch>=2.8.0; extra == 'ai'
|
|
44
|
+
Requires-Dist: torchaudio>=2.8.0; extra == 'ai'
|
|
45
|
+
Requires-Dist: transformers>=5.2.0; extra == 'ai'
|
|
46
|
+
Requires-Dist: transnetv2-pytorch>=1.0.5; extra == 'ai'
|
|
47
|
+
Requires-Dist: ultralytics>=8.0.0; extra == 'ai'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# videopython
|
|
51
|
+
|
|
52
|
+
[](https://pypi.org/project/videopython/)
|
|
53
|
+
[](https://pypi.org/project/videopython/)
|
|
54
|
+
[](LICENSE)
|
|
55
|
+
|
|
56
|
+
Minimal, LLM-friendly Python library for programmatic video editing, processing, and AI video workflows.
|
|
57
|
+
|
|
58
|
+
Full documentation: [videopython.com](https://videopython.com)
|
|
59
|
+
|
|
60
|
+
> **Disclaimer:** This project started as a hand-written hobby project, but most of the code is now produced by LLM agents. Humans still drive direction, approve changes, and own design decisions.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Install FFmpeg first (macOS: brew install ffmpeg | Debian: apt-get install ffmpeg)
|
|
66
|
+
pip install videopython # core video/audio editing
|
|
67
|
+
pip install "videopython[ai]" # + local AI features (GPU recommended)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Python `>=3.10, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use.
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
### JSON editing plans
|
|
75
|
+
|
|
76
|
+
A `VideoEdit` is a multi-segment plan, defined as a dict (or JSON), validated and executed against the source files:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from videopython.editing import VideoEdit
|
|
80
|
+
|
|
81
|
+
edit = VideoEdit.from_dict({
|
|
82
|
+
"segments": [{
|
|
83
|
+
"source": "raw.mp4",
|
|
84
|
+
"start": 10.0,
|
|
85
|
+
"end": 20.0,
|
|
86
|
+
"operations": [
|
|
87
|
+
{"op": "resize", "width": 1080, "height": 1920},
|
|
88
|
+
{"op": "color_adjust", "saturation": 1.15, "contrast": 1.05},
|
|
89
|
+
{"op": "fade", "mode": "in", "duration": 0.5},
|
|
90
|
+
],
|
|
91
|
+
}],
|
|
92
|
+
})
|
|
93
|
+
edit.validate() # dry-run via metadata, no frames loaded
|
|
94
|
+
edit.run_to_file("output.mp4") # streams ffmpeg decode → effects → encode
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`run_to_file()` streams ffmpeg decode → per-frame effects → encode, so memory stays bounded even for hour-long sources. Use `edit.run()` to get a `Video` back in memory instead.
|
|
98
|
+
|
|
99
|
+
### AI generation
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from videopython.ai import TextToImage, ImageToVideo, TextToSpeech
|
|
103
|
+
|
|
104
|
+
image = TextToImage().generate_image("A cinematic mountain sunrise")
|
|
105
|
+
video = ImageToVideo().generate_video(image=image)
|
|
106
|
+
audio = TextToSpeech().generate_audio("Welcome to videopython.")
|
|
107
|
+
video.add_audio(audio).save("ai_video.mp4")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## LLM & AI Agent Integration
|
|
111
|
+
|
|
112
|
+
Every operation is a Pydantic model whose fields ARE the JSON wire format. `VideoEdit.json_schema()` returns a JSON Schema with a discriminated union over every registered `Operation` — pass it straight to Anthropic tool use, OpenAI function calling, or any structured-output API. Then `edit.validate()` dry-runs the plan via metadata before any frames are loaded, so a failed LLM output can be fed back as an error and retried cheaply.
|
|
113
|
+
|
|
114
|
+
See the [LLM Integration Guide](https://videopython.com/guides/llm-integration/) for end-to-end examples, validation error loops, and operation discovery patterns.
|
|
115
|
+
|
|
116
|
+
## Features
|
|
117
|
+
|
|
118
|
+
- **`videopython.base`** — `Video`, `VideoMetadata`, `FrameIterator`, `ImageText`, `Transcription`, and shared result types (`BoundingBox`, `FaceTrack`, `SceneBoundary`, ...). No AI dependencies.
|
|
119
|
+
- **`videopython.audio`** — `Audio` with overlay, concat, normalize, time-stretch, silence detection, segment classification.
|
|
120
|
+
- **`videopython.editing`** — `Operation`/`Effect` foundation, `VideoEdit` plan runner with JSON Schema + streaming execution. Transforms (cut, resize, crop, fps, speed, reverse, freeze, silence removal) and effects (blur, zoom, color grading, vignette, Ken Burns, fade, overlays, animated subtitles).
|
|
121
|
+
- **`videopython.ai`** *(install with `[ai]`)* — generation (`TextToVideo`, `ImageToVideo`, `TextToImage`, `TextToSpeech`, `TextToMusic`), understanding (`AudioToText`, `AudioClassifier`, `SceneVLM`, `FaceTracker`, `SemanticSceneDetector`), `FaceTrackingCrop` transform, and the full-pipeline `VideoAnalyzer`.
|
|
122
|
+
- **`videopython.ai.dubbing`** — `VideoDubber` for voice-cloned revoicing with timing sync.
|
|
123
|
+
|
|
124
|
+
## Examples
|
|
125
|
+
|
|
126
|
+
- [Social Media Clip](https://videopython.com/examples/social-clip/)
|
|
127
|
+
- [AI-Generated Video](https://videopython.com/examples/ai-video/)
|
|
128
|
+
- [Auto-Subtitles](https://videopython.com/examples/auto-subtitles/)
|
|
129
|
+
- [Processing Large Videos](https://videopython.com/examples/large-videos/)
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
See [`DEVELOPMENT.md`](DEVELOPMENT.md) for local setup, testing, and contribution workflow.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# videopython
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/videopython/)
|
|
4
|
+
[](https://pypi.org/project/videopython/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Minimal, LLM-friendly Python library for programmatic video editing, processing, and AI video workflows.
|
|
8
|
+
|
|
9
|
+
Full documentation: [videopython.com](https://videopython.com)
|
|
10
|
+
|
|
11
|
+
> **Disclaimer:** This project started as a hand-written hobby project, but most of the code is now produced by LLM agents. Humans still drive direction, approve changes, and own design decisions.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Install FFmpeg first (macOS: brew install ffmpeg | Debian: apt-get install ffmpeg)
|
|
17
|
+
pip install videopython # core video/audio editing
|
|
18
|
+
pip install "videopython[ai]" # + local AI features (GPU recommended)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Python `>=3.10, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### JSON editing plans
|
|
26
|
+
|
|
27
|
+
A `VideoEdit` is a multi-segment plan, defined as a dict (or JSON), validated and executed against the source files:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from videopython.editing import VideoEdit
|
|
31
|
+
|
|
32
|
+
edit = VideoEdit.from_dict({
|
|
33
|
+
"segments": [{
|
|
34
|
+
"source": "raw.mp4",
|
|
35
|
+
"start": 10.0,
|
|
36
|
+
"end": 20.0,
|
|
37
|
+
"operations": [
|
|
38
|
+
{"op": "resize", "width": 1080, "height": 1920},
|
|
39
|
+
{"op": "color_adjust", "saturation": 1.15, "contrast": 1.05},
|
|
40
|
+
{"op": "fade", "mode": "in", "duration": 0.5},
|
|
41
|
+
],
|
|
42
|
+
}],
|
|
43
|
+
})
|
|
44
|
+
edit.validate() # dry-run via metadata, no frames loaded
|
|
45
|
+
edit.run_to_file("output.mp4") # streams ffmpeg decode → effects → encode
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`run_to_file()` streams ffmpeg decode → per-frame effects → encode, so memory stays bounded even for hour-long sources. Use `edit.run()` to get a `Video` back in memory instead.
|
|
49
|
+
|
|
50
|
+
### AI generation
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from videopython.ai import TextToImage, ImageToVideo, TextToSpeech
|
|
54
|
+
|
|
55
|
+
image = TextToImage().generate_image("A cinematic mountain sunrise")
|
|
56
|
+
video = ImageToVideo().generate_video(image=image)
|
|
57
|
+
audio = TextToSpeech().generate_audio("Welcome to videopython.")
|
|
58
|
+
video.add_audio(audio).save("ai_video.mp4")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## LLM & AI Agent Integration
|
|
62
|
+
|
|
63
|
+
Every operation is a Pydantic model whose fields ARE the JSON wire format. `VideoEdit.json_schema()` returns a JSON Schema with a discriminated union over every registered `Operation` — pass it straight to Anthropic tool use, OpenAI function calling, or any structured-output API. Then `edit.validate()` dry-runs the plan via metadata before any frames are loaded, so a failed LLM output can be fed back as an error and retried cheaply.
|
|
64
|
+
|
|
65
|
+
See the [LLM Integration Guide](https://videopython.com/guides/llm-integration/) for end-to-end examples, validation error loops, and operation discovery patterns.
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
- **`videopython.base`** — `Video`, `VideoMetadata`, `FrameIterator`, `ImageText`, `Transcription`, and shared result types (`BoundingBox`, `FaceTrack`, `SceneBoundary`, ...). No AI dependencies.
|
|
70
|
+
- **`videopython.audio`** — `Audio` with overlay, concat, normalize, time-stretch, silence detection, segment classification.
|
|
71
|
+
- **`videopython.editing`** — `Operation`/`Effect` foundation, `VideoEdit` plan runner with JSON Schema + streaming execution. Transforms (cut, resize, crop, fps, speed, reverse, freeze, silence removal) and effects (blur, zoom, color grading, vignette, Ken Burns, fade, overlays, animated subtitles).
|
|
72
|
+
- **`videopython.ai`** *(install with `[ai]`)* — generation (`TextToVideo`, `ImageToVideo`, `TextToImage`, `TextToSpeech`, `TextToMusic`), understanding (`AudioToText`, `AudioClassifier`, `SceneVLM`, `FaceTracker`, `SemanticSceneDetector`), `FaceTrackingCrop` transform, and the full-pipeline `VideoAnalyzer`.
|
|
73
|
+
- **`videopython.ai.dubbing`** — `VideoDubber` for voice-cloned revoicing with timing sync.
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
- [Social Media Clip](https://videopython.com/examples/social-clip/)
|
|
78
|
+
- [AI-Generated Video](https://videopython.com/examples/ai-video/)
|
|
79
|
+
- [Auto-Subtitles](https://videopython.com/examples/auto-subtitles/)
|
|
80
|
+
- [Processing Large Videos](https://videopython.com/examples/large-videos/)
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
See [`DEVELOPMENT.md`](DEVELOPMENT.md) for local setup, testing, and contribution workflow.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "videopython"
|
|
3
|
-
version = "0.33.
|
|
3
|
+
version = "0.33.4"
|
|
4
4
|
description = "Minimal video generation and processing library."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
|
|
@@ -186,9 +186,11 @@ build-backend = "hatchling.build"
|
|
|
186
186
|
|
|
187
187
|
[tool.hatch.build.targets.wheel]
|
|
188
188
|
packages = ["src/videopython"]
|
|
189
|
+
artifacts = ["src/videopython/base/fonts/*.ttf", "src/videopython/base/fonts/LICENSE_DEJAVU"]
|
|
189
190
|
|
|
190
191
|
[tool.hatch.build.targets.sdist]
|
|
191
192
|
include = ["src/videopython", "src/videopython/py.typed"]
|
|
193
|
+
artifacts = ["src/videopython/base/fonts/*.ttf", "src/videopython/base/fonts/LICENSE_DEJAVU"]
|
|
192
194
|
|
|
193
195
|
[tool.pytest.ini_options]
|
|
194
196
|
pythonpath = ["src/"]
|
|
Binary file
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
|
|
2
|
+
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
|
|
3
|
+
|
|
4
|
+
Bitstream Vera Fonts Copyright
|
|
5
|
+
------------------------------
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
|
|
8
|
+
a trademark of Bitstream, Inc.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of the fonts accompanying this license ("Fonts") and associated
|
|
12
|
+
documentation files (the "Font Software"), to reproduce and distribute the
|
|
13
|
+
Font Software, including without limitation the rights to use, copy, merge,
|
|
14
|
+
publish, distribute, and/or sell copies of the Font Software, and to permit
|
|
15
|
+
persons to whom the Font Software is furnished to do so, subject to the
|
|
16
|
+
following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright and trademark notices and this permission notice shall
|
|
19
|
+
be included in all copies of one or more of the Font Software typefaces.
|
|
20
|
+
|
|
21
|
+
The Font Software may be modified, altered, or added to, and in particular
|
|
22
|
+
the designs of glyphs or characters in the Fonts may be modified and
|
|
23
|
+
additional glyphs or characters may be added to the Fonts, only if the fonts
|
|
24
|
+
are renamed to names not containing either the words "Bitstream" or the word
|
|
25
|
+
"Vera".
|
|
26
|
+
|
|
27
|
+
This License becomes null and void to the extent applicable to Fonts or Font
|
|
28
|
+
Software that has been modified and is distributed under the "Bitstream
|
|
29
|
+
Vera" names.
|
|
30
|
+
|
|
31
|
+
The Font Software may be sold as part of a larger software package but no
|
|
32
|
+
copy of one or more of the Font Software typefaces may be sold by itself.
|
|
33
|
+
|
|
34
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
35
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
|
36
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
|
37
|
+
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
|
38
|
+
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
|
|
39
|
+
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
|
40
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
|
41
|
+
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
|
|
42
|
+
FONT SOFTWARE.
|
|
43
|
+
|
|
44
|
+
Except as contained in this notice, the names of Gnome, the Gnome
|
|
45
|
+
Foundation, and Bitstream Inc., shall not be used in advertising or
|
|
46
|
+
otherwise to promote the sale, use or other dealings in this Font Software
|
|
47
|
+
without prior written authorization from the Gnome Foundation or Bitstream
|
|
48
|
+
Inc., respectively. For further information, contact: fonts at gnome dot
|
|
49
|
+
org.
|
|
50
|
+
|
|
51
|
+
Arev Fonts Copyright
|
|
52
|
+
------------------------------
|
|
53
|
+
|
|
54
|
+
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
|
|
55
|
+
|
|
56
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
57
|
+
a copy of the fonts accompanying this license ("Fonts") and
|
|
58
|
+
associated documentation files (the "Font Software"), to reproduce
|
|
59
|
+
and distribute the modifications to the Bitstream Vera Font Software,
|
|
60
|
+
including without limitation the rights to use, copy, merge, publish,
|
|
61
|
+
distribute, and/or sell copies of the Font Software, and to permit
|
|
62
|
+
persons to whom the Font Software is furnished to do so, subject to
|
|
63
|
+
the following conditions:
|
|
64
|
+
|
|
65
|
+
The above copyright and trademark notices and this permission notice
|
|
66
|
+
shall be included in all copies of one or more of the Font Software
|
|
67
|
+
typefaces.
|
|
68
|
+
|
|
69
|
+
The Font Software may be modified, altered, or added to, and in
|
|
70
|
+
particular the designs of glyphs or characters in the Fonts may be
|
|
71
|
+
modified and additional glyphs or characters may be added to the
|
|
72
|
+
Fonts, only if the fonts are renamed to names not containing either
|
|
73
|
+
the words "Tavmjong Bah" or the word "Arev".
|
|
74
|
+
|
|
75
|
+
This License becomes null and void to the extent applicable to Fonts
|
|
76
|
+
or Font Software that has been modified and is distributed under the
|
|
77
|
+
"Tavmjong Bah Arev" names.
|
|
78
|
+
|
|
79
|
+
The Font Software may be sold as part of a larger software package but
|
|
80
|
+
no copy of one or more of the Font Software typefaces may be sold by
|
|
81
|
+
itself.
|
|
82
|
+
|
|
83
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
84
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
85
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
86
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
|
|
87
|
+
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
88
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
89
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
90
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
91
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
92
|
+
|
|
93
|
+
Except as contained in this notice, the name of Tavmjong Bah shall not
|
|
94
|
+
be used in advertising or otherwise to promote the sale, use or other
|
|
95
|
+
dealings in this Font Software without prior written authorization
|
|
96
|
+
from Tavmjong Bah. For further information, contact: tavmjong @ free
|
|
97
|
+
. fr.
|
|
98
|
+
|
|
99
|
+
$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Bundled default font and graceful font loading.
|
|
2
|
+
|
|
3
|
+
Text operations let callers omit a font path. This module provides a
|
|
4
|
+
reliable resolution chain so rendering never hard-fails on a missing or
|
|
5
|
+
unreadable font:
|
|
6
|
+
|
|
7
|
+
1. The explicit ``font_filename`` if given and loadable.
|
|
8
|
+
2. The bundled DejaVu Sans (broad Unicode coverage).
|
|
9
|
+
3. PIL's built-in font (always available).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from importlib.resources import as_file, files
|
|
15
|
+
|
|
16
|
+
from PIL import ImageFont
|
|
17
|
+
|
|
18
|
+
__all__ = ["DEFAULT_FONT_FILENAME", "load_font"]
|
|
19
|
+
|
|
20
|
+
DEFAULT_FONT_FILENAME = "DejaVuSans.ttf"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _try_truetype(path: str, font_size: int) -> ImageFont.FreeTypeFont | None:
|
|
24
|
+
try:
|
|
25
|
+
return ImageFont.truetype(path, font_size)
|
|
26
|
+
except (OSError, ValueError):
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_font(font_filename: str | None, font_size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
|
31
|
+
"""Load a font, falling back gracefully when one is unavailable.
|
|
32
|
+
|
|
33
|
+
Resolution order: the given ``font_filename`` -> the bundled DejaVu
|
|
34
|
+
Sans -> PIL's built-in bitmap font. Never raises for a missing or
|
|
35
|
+
unreadable font, so callers may pass ``None`` to mean "use the
|
|
36
|
+
default".
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
font_filename: Path to a ``.ttf``/``.otf`` file, or ``None``.
|
|
40
|
+
font_size: Font size in points.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A loaded PIL font object.
|
|
44
|
+
"""
|
|
45
|
+
if font_filename:
|
|
46
|
+
font = _try_truetype(font_filename, font_size)
|
|
47
|
+
if font is not None:
|
|
48
|
+
return font
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
with as_file(files(__package__).joinpath(DEFAULT_FONT_FILENAME)) as bundled:
|
|
52
|
+
font = _try_truetype(str(bundled), font_size)
|
|
53
|
+
if font is not None:
|
|
54
|
+
return font
|
|
55
|
+
except (FileNotFoundError, ModuleNotFoundError):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
return ImageFont.load_default(font_size)
|
|
@@ -16,6 +16,7 @@ import numpy as np
|
|
|
16
16
|
from PIL import Image, ImageDraw, ImageFont
|
|
17
17
|
|
|
18
18
|
from videopython.base.exceptions import OutOfBoundsError
|
|
19
|
+
from videopython.base.fonts import load_font
|
|
19
20
|
|
|
20
21
|
__all__ = ["ImageText", "TextAlign", "AnchorPoint"]
|
|
21
22
|
|
|
@@ -106,7 +107,7 @@ class ImageText:
|
|
|
106
107
|
# PIL uses (width, height), so we reverse for Image.new
|
|
107
108
|
self.image = Image.new(mode, (image_size[1], image_size[0]), color=background)
|
|
108
109
|
self._draw = ImageDraw.Draw(self.image)
|
|
109
|
-
self._font_cache: dict[tuple[str, int], ImageFont.FreeTypeFont] = {}
|
|
110
|
+
self._font_cache: dict[tuple[str | None, int], ImageFont.FreeTypeFont | ImageFont.ImageFont] = {}
|
|
110
111
|
|
|
111
112
|
@property
|
|
112
113
|
def img_array(self) -> np.ndarray:
|
|
@@ -119,7 +120,7 @@ class ImageText:
|
|
|
119
120
|
raise ValueError("Filename cannot be empty")
|
|
120
121
|
self.image.save(filename)
|
|
121
122
|
|
|
122
|
-
def _fit_font_width(self, text: str, font: str, max_width: int) -> int:
|
|
123
|
+
def _fit_font_width(self, text: str, font: str | None, max_width: int) -> int:
|
|
123
124
|
"""
|
|
124
125
|
Find the maximum font size where the text width is less than or equal to max_width.
|
|
125
126
|
|
|
@@ -150,7 +151,7 @@ class ImageText:
|
|
|
150
151
|
raise ValueError(f"Max width {max_width} is too small for any font size!")
|
|
151
152
|
return max_font_size
|
|
152
153
|
|
|
153
|
-
def _fit_font_height(self, text: str, font: str, max_height: int) -> int:
|
|
154
|
+
def _fit_font_height(self, text: str, font: str | None, max_height: int) -> int:
|
|
154
155
|
"""
|
|
155
156
|
Find the maximum font size where the text height is less than or equal to max_height.
|
|
156
157
|
|
|
@@ -184,7 +185,7 @@ class ImageText:
|
|
|
184
185
|
def _get_font_size(
|
|
185
186
|
self,
|
|
186
187
|
text: str,
|
|
187
|
-
font: str,
|
|
188
|
+
font: str | None,
|
|
188
189
|
max_width: int | None = None,
|
|
189
190
|
max_height: int | None = None,
|
|
190
191
|
) -> int:
|
|
@@ -333,7 +334,7 @@ class ImageText:
|
|
|
333
334
|
def write_text(
|
|
334
335
|
self,
|
|
335
336
|
text: str,
|
|
336
|
-
font_filename: str,
|
|
337
|
+
font_filename: str | None,
|
|
337
338
|
xy: PositionType,
|
|
338
339
|
font_size: int | None = 11,
|
|
339
340
|
font_border_size: int = 0,
|
|
@@ -368,9 +369,6 @@ class ImageText:
|
|
|
368
369
|
if not text:
|
|
369
370
|
raise ValueError("Text cannot be empty")
|
|
370
371
|
|
|
371
|
-
if not font_filename:
|
|
372
|
-
raise ValueError("Font filename cannot be empty")
|
|
373
|
-
|
|
374
372
|
if font_size is not None and font_size <= 0:
|
|
375
373
|
raise ValueError("Font size must be positive")
|
|
376
374
|
|
|
@@ -405,12 +403,16 @@ class ImageText:
|
|
|
405
403
|
self._draw.text((x, y), text, font=font, fill=color)
|
|
406
404
|
return text_dimensions
|
|
407
405
|
|
|
408
|
-
def _get_font(self, font_filename: str, font_size: int) -> ImageFont.FreeTypeFont:
|
|
406
|
+
def _get_font(self, font_filename: str | None, font_size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
|
409
407
|
"""
|
|
410
408
|
Get a font object, using cache if available.
|
|
411
409
|
|
|
410
|
+
Resolves via :func:`videopython.base.fonts.load_font`, so a missing,
|
|
411
|
+
unreadable, or ``None`` ``font_filename`` gracefully falls back to
|
|
412
|
+
the bundled default font instead of raising.
|
|
413
|
+
|
|
412
414
|
Args:
|
|
413
|
-
font_filename: Path to the font file
|
|
415
|
+
font_filename: Path to the font file, or None for the default.
|
|
414
416
|
font_size: Size of the font in points
|
|
415
417
|
|
|
416
418
|
Returns:
|
|
@@ -418,13 +420,10 @@ class ImageText:
|
|
|
418
420
|
"""
|
|
419
421
|
key = (font_filename, font_size)
|
|
420
422
|
if key not in self._font_cache:
|
|
421
|
-
|
|
422
|
-
self._font_cache[key] = ImageFont.truetype(font_filename, font_size)
|
|
423
|
-
except (OSError, IOError) as e:
|
|
424
|
-
raise ValueError(f"Error loading font '{font_filename}': {str(e)}")
|
|
423
|
+
self._font_cache[key] = load_font(font_filename, font_size)
|
|
425
424
|
return self._font_cache[key]
|
|
426
425
|
|
|
427
|
-
def get_text_dimensions(self, font_filename: str, font_size: int, text: str) -> tuple[int, int]:
|
|
426
|
+
def get_text_dimensions(self, font_filename: str | None, font_size: int, text: str) -> tuple[int, int]:
|
|
428
427
|
"""
|
|
429
428
|
Return dimensions (width, height) of the rendered text.
|
|
430
429
|
|
|
@@ -455,7 +454,11 @@ class ImageText:
|
|
|
455
454
|
raise ValueError(f"Error measuring text: {str(e)}")
|
|
456
455
|
|
|
457
456
|
def _get_font_baseline_offset(
|
|
458
|
-
self,
|
|
457
|
+
self,
|
|
458
|
+
base_font_filename: str | None,
|
|
459
|
+
base_font_size: int,
|
|
460
|
+
highlight_font_filename: str | None,
|
|
461
|
+
highlight_font_size: int,
|
|
459
462
|
) -> int:
|
|
460
463
|
"""
|
|
461
464
|
Calculate the vertical offset needed to align baselines of different fonts and sizes.
|
|
@@ -497,7 +500,7 @@ class ImageText:
|
|
|
497
500
|
def _split_lines_by_width(
|
|
498
501
|
self,
|
|
499
502
|
text: str,
|
|
500
|
-
font_filename: str,
|
|
503
|
+
font_filename: str | None,
|
|
501
504
|
font_size: int,
|
|
502
505
|
box_width: int,
|
|
503
506
|
) -> list[str]:
|
|
@@ -566,7 +569,7 @@ class ImageText:
|
|
|
566
569
|
def write_text_box(
|
|
567
570
|
self,
|
|
568
571
|
text: str,
|
|
569
|
-
font_filename: str,
|
|
572
|
+
font_filename: str | None,
|
|
570
573
|
xy: PositionType,
|
|
571
574
|
box_width: int | float | None = None,
|
|
572
575
|
font_size: int = 11,
|
|
@@ -615,9 +618,6 @@ class ImageText:
|
|
|
615
618
|
if not text:
|
|
616
619
|
raise ValueError("Text cannot be empty")
|
|
617
620
|
|
|
618
|
-
if not font_filename:
|
|
619
|
-
raise ValueError("Font filename cannot be empty")
|
|
620
|
-
|
|
621
621
|
if font_size <= 0:
|
|
622
622
|
raise ValueError("Font size must be positive")
|
|
623
623
|
|
|
@@ -831,7 +831,7 @@ class ImageText:
|
|
|
831
831
|
def _write_line_with_highlight(
|
|
832
832
|
self,
|
|
833
833
|
line: str,
|
|
834
|
-
font_filename: str,
|
|
834
|
+
font_filename: str | None,
|
|
835
835
|
font_size: int,
|
|
836
836
|
font_border_size: int,
|
|
837
837
|
text_color: RGBColor,
|
|
@@ -6,6 +6,13 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
__all__ = ["Transcription", "TranscriptionSegment", "TranscriptionWord"]
|
|
8
8
|
|
|
9
|
+
# Sentence-ending punctuation used by ``Transcription.capitalize_sentences``.
|
|
10
|
+
_SENTENCE_TERMINATORS = (".", "!", "?", "…")
|
|
11
|
+
|
|
12
|
+
# Trailing characters stripped before checking for a sentence terminator
|
|
13
|
+
# (closing quotes/brackets and whitespace), so ``end."`` still ends a sentence.
|
|
14
|
+
_TRAILING_WRAPPERS = "\"')]}»”’ "
|
|
15
|
+
|
|
9
16
|
|
|
10
17
|
@dataclass
|
|
11
18
|
class TranscriptionWord:
|
|
@@ -279,6 +286,92 @@ class Transcription:
|
|
|
279
286
|
|
|
280
287
|
return Transcription(segments=standardized_segments, language=self.language)
|
|
281
288
|
|
|
289
|
+
def capitalize_sentences(self) -> Transcription:
|
|
290
|
+
"""Return a new Transcription with sentence-start capitalization.
|
|
291
|
+
|
|
292
|
+
The first letter of the first spoken word and of every word that
|
|
293
|
+
follows sentence-ending punctuation (``.``, ``!``, ``?``, ``…``) is
|
|
294
|
+
upper-cased. Remaining characters are left untouched, so acronyms and
|
|
295
|
+
proper nouns from the source transcription are preserved. Timing,
|
|
296
|
+
speaker, and language are carried through unchanged.
|
|
297
|
+
|
|
298
|
+
Abbreviation detection is intentionally not attempted: a token like
|
|
299
|
+
``"U.S."`` is treated as a sentence end. This heuristic is adequate
|
|
300
|
+
for burned-in subtitles and avoids a brittle abbreviation list.
|
|
301
|
+
"""
|
|
302
|
+
capitalized_segments: list[TranscriptionSegment] = []
|
|
303
|
+
start_of_sentence = True
|
|
304
|
+
|
|
305
|
+
for segment in self.segments:
|
|
306
|
+
new_words: list[TranscriptionWord] = []
|
|
307
|
+
for word in segment.words:
|
|
308
|
+
token = word.word
|
|
309
|
+
if start_of_sentence:
|
|
310
|
+
idx = next((i for i, ch in enumerate(token) if ch.isalpha()), None)
|
|
311
|
+
if idx is not None:
|
|
312
|
+
token = token[:idx] + token[idx].upper() + token[idx + 1 :]
|
|
313
|
+
start_of_sentence = False
|
|
314
|
+
if token.rstrip(_TRAILING_WRAPPERS).endswith(_SENTENCE_TERMINATORS):
|
|
315
|
+
start_of_sentence = True
|
|
316
|
+
new_words.append(TranscriptionWord(start=word.start, end=word.end, word=token, speaker=word.speaker))
|
|
317
|
+
|
|
318
|
+
capitalized_segments.append(
|
|
319
|
+
TranscriptionSegment(
|
|
320
|
+
start=segment.start,
|
|
321
|
+
end=segment.end,
|
|
322
|
+
text=" ".join(w.word for w in new_words),
|
|
323
|
+
words=new_words,
|
|
324
|
+
speaker=segment.speaker,
|
|
325
|
+
avg_logprob=segment.avg_logprob,
|
|
326
|
+
no_speech_prob=segment.no_speech_prob,
|
|
327
|
+
compression_ratio=segment.compression_ratio,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return Transcription(segments=capitalized_segments, language=self.language)
|
|
332
|
+
|
|
333
|
+
def chunk_segments(self, max_words: int) -> Transcription:
|
|
334
|
+
"""Return a new Transcription splitting each segment into smaller cues.
|
|
335
|
+
|
|
336
|
+
Each segment is split into consecutive groups of at most ``max_words``
|
|
337
|
+
words, using that group's own first/last word timings. Unlike
|
|
338
|
+
:meth:`standardize_segments`, words are never merged across the
|
|
339
|
+
original segments, so silence gaps between segments are preserved and
|
|
340
|
+
subtitles do not linger over pauses. Speaker, confidence, and language
|
|
341
|
+
metadata are carried through unchanged.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
max_words: Maximum number of words per output segment.
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
ValueError: If ``max_words`` is not positive.
|
|
348
|
+
"""
|
|
349
|
+
if max_words <= 0:
|
|
350
|
+
raise ValueError("max_words must be positive")
|
|
351
|
+
|
|
352
|
+
chunked_segments: list[TranscriptionSegment] = []
|
|
353
|
+
for segment in self.segments:
|
|
354
|
+
words = segment.words
|
|
355
|
+
if not words:
|
|
356
|
+
chunked_segments.append(segment)
|
|
357
|
+
continue
|
|
358
|
+
for i in range(0, len(words), max_words):
|
|
359
|
+
group = words[i : i + max_words]
|
|
360
|
+
chunked_segments.append(
|
|
361
|
+
TranscriptionSegment(
|
|
362
|
+
start=group[0].start,
|
|
363
|
+
end=group[-1].end,
|
|
364
|
+
text=" ".join(w.word for w in group),
|
|
365
|
+
words=list(group),
|
|
366
|
+
speaker=segment.speaker,
|
|
367
|
+
avg_logprob=segment.avg_logprob,
|
|
368
|
+
no_speech_prob=segment.no_speech_prob,
|
|
369
|
+
compression_ratio=segment.compression_ratio,
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return Transcription(segments=chunked_segments, language=self.language)
|
|
374
|
+
|
|
282
375
|
def slice(self, start: float, end: float) -> Transcription | None:
|
|
283
376
|
"""Return a new Transcription containing only words within the time range.
|
|
284
377
|
|
|
@@ -24,6 +24,7 @@ from pydantic import Field, PrivateAttr, model_validator
|
|
|
24
24
|
from tqdm import tqdm
|
|
25
25
|
|
|
26
26
|
from videopython.base.description import BoundingBox
|
|
27
|
+
from videopython.base.fonts import load_font
|
|
27
28
|
from videopython.editing.operation import Effect
|
|
28
29
|
|
|
29
30
|
if TYPE_CHECKING:
|
|
@@ -643,12 +644,7 @@ class TextOverlay(Effect):
|
|
|
643
644
|
return self
|
|
644
645
|
|
|
645
646
|
def _get_font(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
|
646
|
-
|
|
647
|
-
return ImageFont.truetype(self.font_filename, self.font_size)
|
|
648
|
-
try:
|
|
649
|
-
return ImageFont.truetype("DejaVuSans.ttf", self.font_size)
|
|
650
|
-
except OSError:
|
|
651
|
-
return ImageFont.load_default()
|
|
647
|
+
return load_font(self.font_filename, self.font_size)
|
|
652
648
|
|
|
653
649
|
def _wrap_text(self, text: str, font: ImageFont.FreeTypeFont | ImageFont.ImageFont, max_px: int) -> str:
|
|
654
650
|
lines: list[str] = []
|