Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Speech services
:members:
:show-inheritance:

.. automodule:: manim_voiceover.services.gemini
:members:
:show-inheritance:


Defaults
~~~~~~~~
Expand Down
38 changes: 38 additions & 0 deletions docs/source/services.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ Manim Voiceover defines the :py:class:`~~base.SpeechService` class for adding ne
- No
- No
- It's a free API subsidized by Google, so there is a likelihood it may stop working in the future.
* - :py:class:`~gemini.GeminiService`
- Very good, human-like
- No
- Yes
- Requires a Gemini API key or Google Cloud ADC, and Python 3.10 or newer.
* - :py:class:`~openai.OpenAIService`
- Very good, human-like
- No
Expand Down Expand Up @@ -118,6 +123,39 @@ Install Manim Voiceover with the ``gtts`` extra in order to use :py:class:`~gtts

Refer to the `example usage <https://github.com/ManimCommunity/manim-voiceover/blob/main/examples/gtts-example.py>`__ to get started.

:py:class:`~gemini.GeminiService`
*********************************

`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.

Install Manim Voiceover with the ``gemini`` extra in order to use :py:class:`~gemini.GeminiService`:

.. code:: sh

pip install "manim-voiceover[gemini]"

For Gemini Developer API authentication, create a file called ``.env``
that contains your API key in the same directory where you call Manim.

.. code:: sh

GEMINI_API_KEY="..." # insert the API key here

Gemini uses API-key authentication by default:

.. code:: python

self.set_speech_service(GeminiService(voice="Kore"))

For Google Cloud Vertex AI authentication, use Application Default
Credentials and set ``auth_mode="adc"``:

.. code:: python

self.set_speech_service(
GeminiService(voice="Kore", auth_mode="adc", project="my-project-id")
)

:py:class:`~openai.OpenAIService`
*************************************
`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/>`__.
Expand Down
202 changes: 91 additions & 111 deletions examples/voiceover-demo.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
from manim import *
import pygments.styles as code_styles
from manim_voiceover import VoiceoverScene

from manim_voiceover.services.azure import AzureService
from manim_voiceover.services.gemini import GeminiService

code_style = code_styles.get_style_by_name("one-dark")
code_style = "one-dark"

SCENE_HEADER_LINES = (1, 2)
SET_SERVICE_LINE = 3
GEMINI_SERVICE_LINE = 4
GEMINI_VOICE_LINE = 5
GEMINI_AUTH_LINE = 6
SERVICE_CLOSE_LINES = (7, 8)
CIRCLE_SETUP_LINES = (9, 10)
VOICEOVER_CONTEXT_LINE = 11
VOICEOVER_PLAY_LINE = 12
SHIFT_CONTEXT_LINE = 14
SHIFT_DURATION_LINE = 15


def demo_code_block(code_string):
return Code(
code_string=code_string,
add_line_numbers=False,
formatter_style=code_style,
background="window",
language="python",
paragraph_config={"font": "Menlo"},
)


def code_lines(code_block, start, end=None):
if end is None:
return code_block.code_lines[start - 1]
return code_block.code_lines[start - 1 : end]


def code_line_range(code_block, line_range):
return code_lines(code_block, line_range[0], line_range[1])


class VoiceoverDemo(VoiceoverScene):
def construct(self):
# Initialize speech synthesis using Azure's TTS API
self.set_speech_service(
AzureService(
voice="en-US-AriaNeural",
style="newscast-casual", # global_speed=1.15
)
)
# Initialize speech synthesis using Gemini's TTS API
self.set_speech_service(GeminiService(voice="Kore", auth_mode="adc"))
banner = ManimBanner().scale(0.5)

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

demo_code = Code(
code='''tracker = self.add_voiceover_text(
demo_code = demo_code_block(
'''tracker = self.add_voiceover_text(
"""AI generated voices have become realistic
enough for use in most content. Using neural
text-to-speech frees you from the painstaking
process of recording and manually syncing
audio to your video."""
)
self.play(Write(demo_code), run_time=tracker.duration)''',
insert_line_no=False,
style=code_style,
background="window",
font="Consolas",
language="python",
self.play(Write(demo_code), run_time=tracker.duration)'''
).rescale_to_fit(12, 0)

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

demo_code2 = Code(
code="""class VoiceoverDemo(VoiceoverScene):
demo_code2 = demo_code_block(
"""class VoiceoverDemo(VoiceoverScene):
def construct(self):
self.set_speech_service(
AzureService(
voice="en-US-AriaNeural",
style="newscast-casual",
global_speed=1.15
GeminiService(
voice="Kore",
auth_mode="adc",
)
)
circle = Circle()
Expand All @@ -96,64 +117,51 @@ def construct(self):
self.play(Create(circle))

with self.voiceover(text="Let's shift it to the left 2 units.") as tracker:
self.play(circle.animate.shift(2 * LEFT), run_time=tracker.duration)""",
insert_line_no=False,
style=code_style,
background="window",
font="Consolas",
language="python",
self.play(circle.animate.shift(2 * LEFT), run_time=tracker.duration)"""
).rescale_to_fit(12, 0)

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

with self.voiceover(
text="First, we create a scene using the Voiceover Scene class from the plugin."
):
self.play(FadeIn(demo_code2.code[:2]))
with self.voiceover(text="First, we create a scene using the Voiceover Scene class from the plugin."):
self.play(FadeIn(code_line_range(demo_code2, SCENE_HEADER_LINES)))

with self.voiceover(
text="Then, we initialize the voiceover by setting the appropriate speech synthesizer."
):
self.play(FadeIn(demo_code2.code[2]))
with self.voiceover(text="Then, we initialize the voiceover by setting the appropriate speech synthesizer."):
self.play(FadeIn(code_lines(demo_code2, SET_SERVICE_LINE)))

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

with self.voiceover(
text="We use the English speaking neural voice called Aria."
):
self.play(FadeIn(demo_code2.code[4]))
with self.voiceover(text="We use the prebuilt Gemini voice called Kore."):
self.play(FadeIn(code_lines(demo_code2, GEMINI_VOICE_LINE)))

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

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

with self.voiceover(
text="""With the configuration out of the way, it is time to animate."""
):
with self.voiceover(text="""With the configuration out of the way, it is time to animate."""):
pass

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

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

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

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

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

demo_code3 = Code(
code="""class VoiceoverDemo(VoiceoverScene):
demo_code3 = demo_code_block(
"""class VoiceoverDemo(VoiceoverScene):
def construct(self):
self.set_speech_service(
AzureService(
voice="en-US-AriaNeural",
style="newscast-casual",
global_speed=1.15
GeminiService(
voice="Kore",
auth_mode="adc",
)
)
# self.set_speech_service(
# StitcherService("my_voice_recording.mp3")
# )
""",
insert_line_no=False,
style=code_style,
background="window",
font="Consolas",
language="python",
"""
).scale(0.85)

demo_code4 = (
Code(
code="""class VoiceoverDemo(VoiceoverScene):
demo_code_block(
"""class VoiceoverDemo(VoiceoverScene):
def construct(self):
# self.set_speech_service(
# AzureService(
# voice="en-US-AriaNeural",
# style="newscast-casual",
# global_speed=1.15
# GeminiService(
# voice="Kore",
# auth_mode="adc",
# )
# )
# self.set_speech_service(
# StitcherService("my_voice_recording.mp3")
# )
""",
insert_line_no=False,
style=code_style,
background="window",
font="Consolas",
language="python",
"""
)
.scale(0.85)
.align_to(demo_code3, LEFT)
)

demo_code5 = (
Code(
code="""class VoiceoverDemo(VoiceoverScene):
demo_code_block(
"""class VoiceoverDemo(VoiceoverScene):
def construct(self):
# self.set_speech_service(
# AzureService(
# voice="en-US-AriaNeural",
# style="newscast-casual",
# global_speed=1.15
# GeminiService(
# voice="Kore",
# auth_mode="adc",
# )
# )
self.set_speech_service(
StitcherService("my_voice_recording.mp3")
)
""",
insert_line_no=False,
style=code_style,
background="window",
font="Consolas",
language="python",
"""
)
.scale(0.85)
.align_to(demo_code3, LEFT)
Expand All @@ -258,31 +248,21 @@ def construct(self):
self.wait()
self.play(FadeOut(text1, text2, arrow))

with self.voiceover(
text="To do that, you record an MP3 of the final text of your video."
):
with self.voiceover(text="To do that, you record an MP3 of the final text of your video."):
self.play(FadeIn(demo_code3))

with self.voiceover(
text="""Manim-voiceover then splits your audio automatically and replaces the AI generated voice with your real recording."""
):
self.play(FadeOut(demo_code3.code), FadeIn(demo_code4.code))
self.play(FadeOut(demo_code4.code), FadeIn(demo_code5.code))
self.play(FadeOut(demo_code3.code_lines), FadeIn(demo_code4.code_lines))
self.play(FadeOut(demo_code4.code_lines), FadeIn(demo_code5.code_lines))

self.wait(2)

with self.voiceover(
text="""Manim-voiceover makes it much easier to do voiceovers for Manim projects."""
):
self.play(FadeOut(demo_code5.code, demo_code3.background_mobject))
with self.voiceover(text="""Manim-voiceover makes it much easier to do voiceovers for Manim projects."""):
self.play(FadeOut(demo_code5.code_lines, demo_code3.background))

with self.voiceover(
text="Visit the GitHub repo to start using it in your project."
):
self.play(
FadeIn(
Tex(r"\texttt{https://github.com/ManimCommunity/manim-voiceover}")
)
)
with self.voiceover(text="Visit the GitHub repo to start using it in your project."):
self.play(FadeIn(Tex(r"\texttt{https://github.com/ManimCommunity/manim-voiceover}")))

self.wait(5)
Loading