Skip to content

Commit 45150df

Browse files
authored
feat: add Gemini text-to-speech service
feat: add Gemini text-to-speech service
1 parent 3d27b2e commit 45150df

9 files changed

Lines changed: 795 additions & 114 deletions

File tree

docs/source/api.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ Speech services
4646
:members:
4747
:show-inheritance:
4848

49+
.. automodule:: manim_voiceover.services.gemini
50+
:members:
51+
:show-inheritance:
52+
4953

5054
Defaults
5155
~~~~~~~~

docs/source/services.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ Manim Voiceover defines the :py:class:`~~base.SpeechService` class for adding ne
4242
- No
4343
- No
4444
- It's a free API subsidized by Google, so there is a likelihood it may stop working in the future.
45+
* - :py:class:`~gemini.GeminiService`
46+
- Very good, human-like
47+
- No
48+
- Yes
49+
- Requires a Gemini API key or Google Cloud ADC, and Python 3.10 or newer.
4550
* - :py:class:`~openai.OpenAIService`
4651
- Very good, human-like
4752
- No
@@ -118,6 +123,39 @@ Install Manim Voiceover with the ``gtts`` extra in order to use :py:class:`~gtts
118123
119124
Refer to the `example usage <https://github.com/ManimCommunity/manim-voiceover/blob/main/examples/gtts-example.py>`__ to get started.
120125

126+
:py:class:`~gemini.GeminiService`
127+
*********************************
128+
129+
`Gemini text-to-speech <https://ai.google.dev/gemini-api/docs/speech-generation>`__ provides controllable text-to-speech through the Google Gen AI SDK. It requires an internet connection and Python 3.10 or newer.
130+
131+
Install Manim Voiceover with the ``gemini`` extra in order to use :py:class:`~gemini.GeminiService`:
132+
133+
.. code:: sh
134+
135+
pip install "manim-voiceover[gemini]"
136+
137+
For Gemini Developer API authentication, create a file called ``.env``
138+
that contains your API key in the same directory where you call Manim.
139+
140+
.. code:: sh
141+
142+
GEMINI_API_KEY="..." # insert the API key here
143+
144+
Gemini uses API-key authentication by default:
145+
146+
.. code:: python
147+
148+
self.set_speech_service(GeminiService(voice="Kore"))
149+
150+
For Google Cloud Vertex AI authentication, use Application Default
151+
Credentials and set ``auth_mode="adc"``:
152+
153+
.. code:: python
154+
155+
self.set_speech_service(
156+
GeminiService(voice="Kore", auth_mode="adc", project="my-project-id")
157+
)
158+
121159
:py:class:`~openai.OpenAIService`
122160
*************************************
123161
`OpenAI <https://platform.openai.com/docs/api-reference/audio/createSpeech/>`__ provides a text-to-speech service. It is through an API, so it requires an internet connection to work. It also requires an API key to use. Register for one `here <https://platform.openai.com/>`__.

examples/voiceover-demo.py

Lines changed: 91 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,48 @@
11
from manim import *
2-
import pygments.styles as code_styles
32
from manim_voiceover import VoiceoverScene
43

5-
from manim_voiceover.services.azure import AzureService
4+
from manim_voiceover.services.gemini import GeminiService
65

7-
code_style = code_styles.get_style_by_name("one-dark")
6+
code_style = "one-dark"
7+
8+
SCENE_HEADER_LINES = (1, 2)
9+
SET_SERVICE_LINE = 3
10+
GEMINI_SERVICE_LINE = 4
11+
GEMINI_VOICE_LINE = 5
12+
GEMINI_AUTH_LINE = 6
13+
SERVICE_CLOSE_LINES = (7, 8)
14+
CIRCLE_SETUP_LINES = (9, 10)
15+
VOICEOVER_CONTEXT_LINE = 11
16+
VOICEOVER_PLAY_LINE = 12
17+
SHIFT_CONTEXT_LINE = 14
18+
SHIFT_DURATION_LINE = 15
19+
20+
21+
def demo_code_block(code_string):
22+
return Code(
23+
code_string=code_string,
24+
add_line_numbers=False,
25+
formatter_style=code_style,
26+
background="window",
27+
language="python",
28+
paragraph_config={"font": "Menlo"},
29+
)
30+
31+
32+
def code_lines(code_block, start, end=None):
33+
if end is None:
34+
return code_block.code_lines[start - 1]
35+
return code_block.code_lines[start - 1 : end]
36+
37+
38+
def code_line_range(code_block, line_range):
39+
return code_lines(code_block, line_range[0], line_range[1])
840

941

1042
class VoiceoverDemo(VoiceoverScene):
1143
def construct(self):
12-
# Initialize speech synthesis using Azure's TTS API
13-
self.set_speech_service(
14-
AzureService(
15-
voice="en-US-AriaNeural",
16-
style="newscast-casual", # global_speed=1.15
17-
)
18-
)
44+
# Initialize speech synthesis using Gemini's TTS API
45+
self.set_speech_service(GeminiService(voice="Kore", auth_mode="adc"))
1946
banner = ManimBanner().scale(0.5)
2047

2148
with self.voiceover(text="Hey Manim Community!"):
@@ -31,20 +58,15 @@ def construct(self):
3158
self.wait(tracker.get_remaining_duration(buff=-1))
3259
self.play(FadeOut(banner))
3360

34-
demo_code = Code(
35-
code='''tracker = self.add_voiceover_text(
61+
demo_code = demo_code_block(
62+
'''tracker = self.add_voiceover_text(
3663
"""AI generated voices have become realistic
3764
enough for use in most content. Using neural
3865
text-to-speech frees you from the painstaking
3966
process of recording and manually syncing
4067
audio to your video."""
4168
)
42-
self.play(Write(demo_code), run_time=tracker.duration)''',
43-
insert_line_no=False,
44-
style=code_style,
45-
background="window",
46-
font="Consolas",
47-
language="python",
69+
self.play(Write(demo_code), run_time=tracker.duration)'''
4870
).rescale_to_fit(12, 0)
4971

5072
tracker = self.add_voiceover_text(
@@ -80,14 +102,13 @@ def construct(self):
80102
with self.voiceover(text="I would go on, but you get the idea."):
81103
self.play(FadeOut(circle))
82104

83-
demo_code2 = Code(
84-
code="""class VoiceoverDemo(VoiceoverScene):
105+
demo_code2 = demo_code_block(
106+
"""class VoiceoverDemo(VoiceoverScene):
85107
def construct(self):
86108
self.set_speech_service(
87-
AzureService(
88-
voice="en-US-AriaNeural",
89-
style="newscast-casual",
90-
global_speed=1.15
109+
GeminiService(
110+
voice="Kore",
111+
auth_mode="adc",
91112
)
92113
)
93114
circle = Circle()
@@ -96,64 +117,51 @@ def construct(self):
96117
self.play(Create(circle))
97118
98119
with self.voiceover(text="Let's shift it to the left 2 units.") as tracker:
99-
self.play(circle.animate.shift(2 * LEFT), run_time=tracker.duration)""",
100-
insert_line_no=False,
101-
style=code_style,
102-
background="window",
103-
font="Consolas",
104-
language="python",
120+
self.play(circle.animate.shift(2 * LEFT), run_time=tracker.duration)"""
105121
).rescale_to_fit(12, 0)
106122

107123
with self.voiceover(text="Let's see how the API works!"):
108-
self.play(FadeIn(demo_code2.background_mobject))
124+
self.play(FadeIn(demo_code2.background))
109125

110-
with self.voiceover(
111-
text="First, we create a scene using the Voiceover Scene class from the plugin."
112-
):
113-
self.play(FadeIn(demo_code2.code[:2]))
126+
with self.voiceover(text="First, we create a scene using the Voiceover Scene class from the plugin."):
127+
self.play(FadeIn(code_line_range(demo_code2, SCENE_HEADER_LINES)))
114128

115-
with self.voiceover(
116-
text="Then, we initialize the voiceover by setting the appropriate speech synthesizer."
117-
):
118-
self.play(FadeIn(demo_code2.code[2]))
129+
with self.voiceover(text="Then, we initialize the voiceover by setting the appropriate speech synthesizer."):
130+
self.play(FadeIn(code_lines(demo_code2, SET_SERVICE_LINE)))
119131

120-
with self.voiceover(text="In this example, we use Azure Text-to-speech."):
121-
self.play(FadeIn(demo_code2.code[3]))
132+
with self.voiceover(text="In this example, we use Gemini text-to-speech."):
133+
self.play(FadeIn(code_lines(demo_code2, GEMINI_SERVICE_LINE)))
122134

123-
with self.voiceover(
124-
text="We use the English speaking neural voice called Aria."
125-
):
126-
self.play(FadeIn(demo_code2.code[4]))
135+
with self.voiceover(text="We use the prebuilt Gemini voice called Kore."):
136+
self.play(FadeIn(code_lines(demo_code2, GEMINI_VOICE_LINE)))
127137

128-
with self.voiceover(text='We use the style called "newscast casual".'):
129-
self.play(FadeIn(demo_code2.code[5]))
138+
with self.voiceover(text="We authenticate with Application Default Credentials."):
139+
self.play(FadeIn(code_lines(demo_code2, GEMINI_AUTH_LINE)))
130140

131141
with self.voiceover(
132-
text="""Finally, we give an option to speed up the voiceover
133-
playback fifteen percent, because the default is a bit too slow."""
142+
text="""Finally, Gemini returns audio that Manim Voiceover stores
143+
in the local voiceover cache for reuse."""
134144
):
135-
self.play(FadeIn(demo_code2.code[6:9]))
145+
self.play(FadeIn(code_line_range(demo_code2, SERVICE_CLOSE_LINES)))
136146

137-
with self.voiceover(
138-
text="""With the configuration out of the way, it is time to animate."""
139-
):
147+
with self.voiceover(text="""With the configuration out of the way, it is time to animate."""):
140148
pass
141149

142150
with self.voiceover(text="""Let's initialize the circle object."""):
143-
self.play(FadeIn(demo_code2.code[9:11]))
151+
self.play(FadeIn(code_line_range(demo_code2, CIRCLE_SETUP_LINES)))
144152

145153
with self.voiceover(
146154
text="""Then, we need to tell the scene to start narrating,
147155
by calling the function "self-dot-voiceover"."""
148156
):
149-
self.play(FadeIn(demo_code2.code[11]))
157+
self.play(FadeIn(code_lines(demo_code2, VOICEOVER_CONTEXT_LINE)))
150158

151159
with self.voiceover(
152160
text="""By wrapping our animation inside a "with-statement",
153161
we ensure that once it finishes playing, it will also wait for
154162
the voiceover playback to finish."""
155163
):
156-
self.play(FadeIn(demo_code2.code[12]))
164+
self.play(FadeIn(code_lines(demo_code2, VOICEOVER_PLAY_LINE)))
157165

158166
with self.voiceover(
159167
text="""This is extremely convenient, and let's you chain
@@ -164,79 +172,61 @@ def construct(self):
164172
with self.voiceover(
165173
text="""We just need to repeat the same pattern with self-dot-voiceover and with-statements. Here is something cool."""
166174
):
167-
self.play(FadeIn(demo_code2.code[14]))
175+
self.play(FadeIn(code_lines(demo_code2, SHIFT_CONTEXT_LINE)))
168176

169177
with self.voiceover(
170178
text="""We can retrieve the duration of the generated voiceover programmatically, and then use it to define for how long an animation should play."""
171179
):
172-
self.play(FadeIn(demo_code2.code[15]))
180+
self.play(FadeIn(code_lines(demo_code2, SHIFT_DURATION_LINE)))
173181

174-
demo_code3 = Code(
175-
code="""class VoiceoverDemo(VoiceoverScene):
182+
demo_code3 = demo_code_block(
183+
"""class VoiceoverDemo(VoiceoverScene):
176184
def construct(self):
177185
self.set_speech_service(
178-
AzureService(
179-
voice="en-US-AriaNeural",
180-
style="newscast-casual",
181-
global_speed=1.15
186+
GeminiService(
187+
voice="Kore",
188+
auth_mode="adc",
182189
)
183190
)
184191
# self.set_speech_service(
185192
# StitcherService("my_voice_recording.mp3")
186193
# )
187-
""",
188-
insert_line_no=False,
189-
style=code_style,
190-
background="window",
191-
font="Consolas",
192-
language="python",
194+
"""
193195
).scale(0.85)
194196

195197
demo_code4 = (
196-
Code(
197-
code="""class VoiceoverDemo(VoiceoverScene):
198+
demo_code_block(
199+
"""class VoiceoverDemo(VoiceoverScene):
198200
def construct(self):
199201
# self.set_speech_service(
200-
# AzureService(
201-
# voice="en-US-AriaNeural",
202-
# style="newscast-casual",
203-
# global_speed=1.15
202+
# GeminiService(
203+
# voice="Kore",
204+
# auth_mode="adc",
204205
# )
205206
# )
206207
# self.set_speech_service(
207208
# StitcherService("my_voice_recording.mp3")
208209
# )
209-
""",
210-
insert_line_no=False,
211-
style=code_style,
212-
background="window",
213-
font="Consolas",
214-
language="python",
210+
"""
215211
)
216212
.scale(0.85)
217213
.align_to(demo_code3, LEFT)
218214
)
219215

220216
demo_code5 = (
221-
Code(
222-
code="""class VoiceoverDemo(VoiceoverScene):
217+
demo_code_block(
218+
"""class VoiceoverDemo(VoiceoverScene):
223219
def construct(self):
224220
# self.set_speech_service(
225-
# AzureService(
226-
# voice="en-US-AriaNeural",
227-
# style="newscast-casual",
228-
# global_speed=1.15
221+
# GeminiService(
222+
# voice="Kore",
223+
# auth_mode="adc",
229224
# )
230225
# )
231226
self.set_speech_service(
232227
StitcherService("my_voice_recording.mp3")
233228
)
234-
""",
235-
insert_line_no=False,
236-
style=code_style,
237-
background="window",
238-
font="Consolas",
239-
language="python",
229+
"""
240230
)
241231
.scale(0.85)
242232
.align_to(demo_code3, LEFT)
@@ -258,31 +248,21 @@ def construct(self):
258248
self.wait()
259249
self.play(FadeOut(text1, text2, arrow))
260250

261-
with self.voiceover(
262-
text="To do that, you record an MP3 of the final text of your video."
263-
):
251+
with self.voiceover(text="To do that, you record an MP3 of the final text of your video."):
264252
self.play(FadeIn(demo_code3))
265253

266254
with self.voiceover(
267255
text="""Manim-voiceover then splits your audio automatically and replaces the AI generated voice with your real recording."""
268256
):
269-
self.play(FadeOut(demo_code3.code), FadeIn(demo_code4.code))
270-
self.play(FadeOut(demo_code4.code), FadeIn(demo_code5.code))
257+
self.play(FadeOut(demo_code3.code_lines), FadeIn(demo_code4.code_lines))
258+
self.play(FadeOut(demo_code4.code_lines), FadeIn(demo_code5.code_lines))
271259

272260
self.wait(2)
273261

274-
with self.voiceover(
275-
text="""Manim-voiceover makes it much easier to do voiceovers for Manim projects."""
276-
):
277-
self.play(FadeOut(demo_code5.code, demo_code3.background_mobject))
262+
with self.voiceover(text="""Manim-voiceover makes it much easier to do voiceovers for Manim projects."""):
263+
self.play(FadeOut(demo_code5.code_lines, demo_code3.background))
278264

279-
with self.voiceover(
280-
text="Visit the GitHub repo to start using it in your project."
281-
):
282-
self.play(
283-
FadeIn(
284-
Tex(r"\texttt{https://github.com/ManimCommunity/manim-voiceover}")
285-
)
286-
)
265+
with self.voiceover(text="Visit the GitHub repo to start using it in your project."):
266+
self.play(FadeIn(Tex(r"\texttt{https://github.com/ManimCommunity/manim-voiceover}")))
287267

288268
self.wait(5)

0 commit comments

Comments
 (0)