Audio
Pistas sincronizadas, efectos de sonido anclados, narración grabada en el editor y mezcla en la exportación de video
Música, efectos y narración sincronizados con la línea de tiempo. Declaras una pista con scene.media.audio(...) y la activas con scene.play([pista]). La vista previa la reproduce siguiendo el reloj de la escena (pausa, seek y velocidad incluidos) y la exportación a MP4 o WebM la mezcla con el video.
Pistas de audio
Archivos de sonido que empiezan en el cursor donde los activas.
scene.play([pista]) fija su inicio en el cursor absoluto de esa llamada. Rutas o tiempos inválidos lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
path | str | obligatorio | Archivo de audio; una ruta relativa usa la carpeta de assets, igual que imágenes y SVG. |
duration | float | None | None | Recorta la pista y la hace participar en la duración del lote. Sin ella, la pista suena de fondo sin alargar la línea de tiempo. |
end | float | None | None | Corta la pista en ese segundo del archivo sin alargar el lote: música de fondo que suena bajo las escenas siguientes y termina donde quieres. No se combina con duration. |
volume | float | 1.0 | Ganancia lineal. |
fade_in / fade_out | float | 0.0 | Fundidos de entrada y salida en segundos; fade_out necesita duration o end y termina justo en el corte. |
from gaanim import Scene
scene = Scene()
scene.assets.assets_dir("assets")
music = scene.media.audio("music.ogg", volume=0.35)
pop = scene.media.audio("pop.wav", duration=0.4, volume=0.8, fade_in=0.02)
scene.play([music])
scene.wait(1.5)
scene.play([pop])
scene.render()Una narración grabada con duración fija alarga el lote y termina con un fundido:
narration = scene.media.audio(
"narration.m4a",
duration=7.5,
volume=0.9,
fade_in=0.15,
fade_out=0.25,
)
scene.play([narration])Música de fondo que dura 30 s y se funde al final, mientras la escena sigue programándose desde el mismo cursor:
musica = scene.media.audio("music.ogg", end=30.0, fade_out=1.5, volume=0.5)
scene.play([musica]) # no mueve el cursor: lo que sigue suena sobre la músicascene.play.Efectos de sonido anclados
Sonidos cortos que se colocan en la línea de tiempo sin scene.play. Usan el mismo mezclador que scene.media.audio: suenan en la vista previa (con pausa, seek y velocidad) y MP4/WebM los mezclan con el video. Un seek al segundo t oye lo mismo que la reproducción continua, porque cada efecto tiene un inicio absoluto fijado al componer la escena. Ninguno alarga la línea de tiempo ni mueve el cursor. Hay tres formas de anclarlos:
scene.media.sfx(...): en un segundo absoluto.anim.sound(...): al inicio de una animación. Si la animación se mueve (undelay, su lugar en unasequenceo unstagger), el sonido se mueve con ella.Transition.*(..., sound=...): al inicio de la transición, que es el inicio del segmento al que entra.
at o volume negativos o no finitos lanzan ValueError. Para que el sonido siga a una animación que puede moverse, usa Anim.sound.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
path | str | obligatorio | Archivo de audio; una ruta relativa usa la carpeta de assets. |
at | float | None | None | Segundo absoluto de inicio; sin él, el cursor actual (scene.cursor). |
volume | float | 1.0 | Ganancia lineal. |
None — El efecto queda programado de inmediato.scene.play(title.animate.fade_in())
scene.media.sfx("whoosh.wav", at=scene.cursor)
scene.media.sfx("pop.wav", at=0.25, volume=0.6)
scene.wait(1)delay y su posición en composiciones, y suena una vez, entero, aunque la animación use repeat. Una ruta vacía, un volume negativo o no finito o un offset no finito lanzan ValueError al llamarlo; un archivo inexistente o un sonido que empezaría antes de 0 s lanzan ValueError desde scene.play, que entonces no programa nada.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
path | str | obligatorio | Archivo de audio; una ruta relativa usa la carpeta de assets. |
volume | float | 1.0 | Ganancia lineal. |
offset | float | 0.0 | Segundos desde el inicio de la animación hasta el sonido; negativo lo adelanta. |
Anim — Una copia con el efecto anclado; una segunda llamada lo reemplaza.title = scene.text("Efectos anclados", role="title")
dot = scene.geometry.circle(0.5).move_to(0, -2)
scene.play(title.animate.write().sound("typing.wav", volume=0.6))
# El pop suena 0.3 s después, con el círculo.
scene.play(dot.animate.grow_from_center().delay(0.3).sound("pop.wav"))Una transición con sonido:
scene.segment("detalle", Transition.slide(0.5, "left", sound="whoosh.wav"))
scene.wait(1)La vista previa reproduce varias pistas a la vez y aplica volume, fade_in y fade_out. MP4 usa AAC y WebM usa Opus. Las secuencias de imágenes, GIF y WebP animado rechazan las pistas porque esos formatos no transportan audio.
Audio de los videos
El audio embebido de un video sigue las mismas reglas que una pista.
clip = scene.media.video(...) solo declara el video y scene.play([clip]) activa juntos sus fotogramas y su audio; ambos se pausan, recorren y repiten con la línea de tiempo. audio=False silencia el video y volume fija su ganancia. Con video.segment(start=..., end=...), cada fragmento genera una pista finita: las pausas entre fragmentos son silenciosas aunque el último fotograma siga visible, y speed conserva el tono. Consulta Medios.
Audio que anima la escena
Los datos de una pista animan cualquier cosa. level, band y pulse devuelven un Computed del tiempo, entre 0 y 1, que se usa donde se acepta un número reactivo:
scale_to,opacity,move_toyrotate_to;- la cámara (
bind_2d) y los uniforms de los shaders; computed,scene.viz.readoutyfill_level;Updater.rotateyUpdater.wiggle;- los colores, con
Falloff.sourceydrive("fill", ...).
El archivo se analiza una sola vez: los WAV directamente y los demás formatos con FFmpeg. Cada señal consulta ese análisis donde suena la pista, según su inicio, así que un salto en la línea de tiempo o una exportación leen lo mismo que la reproducción. Antes de que la pista suene y después de que termine, las señales valen 0.
musica = scene.media.audio("assets/ritmo.ogg")
bajo = musica.band(20, 150, smoothing=0.5)
golpe = musica.pulse(decay=0.12)
logo = scene.geometry.star(5, 1, 0.45).fill(GOLD)
logo.scale_to(computed(lambda b: 0.8 + 0.5 * b, inputs=[bajo]))
logo.add_updater(Updater.wiggle(position=computed(lambda p: 0.15 * p, inputs=[golpe])))
puntos = scene.geometry.group([scene.geometry.circle(0.2).move_to(x, -2) for x in range(-3, 4)])
puntos.drive("fill", Falloff.source(bajo).gradient(BLUE, GOLD))
scene.play(musica)
scene.wait(musica.duration)
scene.render()scene.play(musica) sin duration no alarga el lote: scene.wait(musica.duration) mantiene la escena mientras suena. El ejemplo completo, examples/audio/signals.py, está en el repositorio de Gaanim; no viene con el paquete instalado.
range_db las separa. Un smoothing fuera de rango o un range_db fuera de (0, 120] lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
smoothing | float | 0.0 | En [0, 1): cada cuadro conserva esa parte del anterior, así que cerca de 1 cambia despacio. |
range_db | float | None | None | Mide en decibelios, como el oído: 0 a range_db dB por debajo de la parte fuerte y 1 en ella. Con 24, un drop se distingue de un verso más suave. |
Computed — El volumen donde suena la pista: 0 en silencio, 1 en su parte fuerte.low >= high, negativa o por encima de ese rango lanza ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
low / high | float | obligatorio | Banda de frecuencias en Hz: band(20, 150) sigue el bajo y band(2000, 8000), los platillos y las sibilantes. |
smoothing | float | 0.0 | Como en level. |
Computed — La amplitud de la banda, de 0 a 1, cada banda respecto a su propia parte fuerte.scale_to(computed(lambda p: 1 + 0.2 * p, inputs=[musica.pulse()])). Un golpe es un onset: donde empieza un sonido (un bombo, una nota, una sílaba). Sin banda salta con cualquiera, y en música electrónica los platillos lo disparan sin parar; pulse(low=40, high=120) sigue solo el bombo. Un decay no positivo, solo uno de low/high o una banda inválida lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
decay | float | 0.15 | Segundos en que el pulso cae a un tercio. |
low / high | float | None | None | Solo cuentan los golpes de esa banda, en Hz; los dos o ninguno. |
Computed — 1 en cada golpe y bajando hasta 0.tempo. Suma musica.start cuando la pista ya sonó para obtener segundos de la línea de tiempo, por ejemplo para colocar marcas o animaciones en cada golpe.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
low / high | float | None | None | Solo los golpes de esa banda, en Hz, como en pulse. |
list[float] — Segundos desde el inicio de la pista en que empieza un sonido, en orden.scene.play(musica)
golpes = [musica.start + t for t in musica.beats()]
bombos = [musica.start + t for t in musica.beats(low=40, high=120)]confidence mide cuánto se repiten los golpes en la rejilla de ese tempo, a uno, dos o cuatro pulsos: un bombo a medio tiempo, cada dos pulsos, también da un pulso claro. Si el rango no contiene el tempo de la música, se devuelve el mejor pico dentro del rango con una confianza baja, nunca uno de sus extremos. El tempo y el pulso que se siente pueden estar en proporción 4:3 o 2:1, así que conviene comparar con el tempo publicado del tema. Así scene.tempo(t.bpm, offset=musica.start + t.offset) alinea wait_until(bar=...) con la música. Sigue la speed y el offset de la pista. Sirve para música con pulso estable; con confidence por debajo de 0.3 no hay un pulso claro. No detecta secciones (drops, estribillos): el volumen confunde la entrada del bombo con un drop, así que esas marcas se colocan a mano con analysis. Un rango fuera de 0 < min_bpm < max_bpm <= 400 lanza ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
min_bpm / max_bpm | float | 60 / 200 | Rango en que se busca el tempo. |
AudioTempo — bpm, offset (segundos desde el inicio de la pista hasta su primer pulso, menos de un pulso) y confidence, de 0 a 1.scene.play(musica)
pulso = musica.tempo()
scene.tempo(pulso.bpm, offset=musica.start + pulso.offset)
scene.wait_until(bar=2)frame_rate cuadros por segundo (unos 43) y times, el segundo del archivo de cada cuadro; level, el volumen de 0 a 1, y level_db, en decibelios por debajo de la parte fuerte (0 en ella, −120 en silencio); spectrum, una fila por cuadro con la amplitud de 96 bandas logarítmicas (frequencies, en Hz), de 0 a 1 contra la parte fuerte de todo el archivo; band(low, high), la amplitud de una banda por cuadro, y onsets(low=, high=). Los tiempos son del archivo, antes del offset y la speed de la pista. Son listas de Python, listas para numpy si lo usas.AudioData — El análisis del archivo como números.datos = musica.analysis()
bombo = datos.band(40, 120)
fuertes = [t for t, v in zip(datos.times, bombo) if v > 0.9]start es el segundo de la línea de tiempo en que la pista empezó a sonar por última vez, o None antes de reproducirla. duration son los segundos que suena: su duration, o el archivo entero.drive, que es como cambian los colores: drive("fill", Falloff.source(bajo).gradient(BLUE, GOLD)). Un Computed que lee un Parameter lanza ValueError. Ver Animaciones.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
value | Computed | obligatorio | Un Computed que depende solo del tiempo: una señal de audio, scene.noise o scene.time. |
Falloff — Su valor, el mismo para todos los miembros.Visualizar el audio
Para dibujar el sonido, una pista da una señal por banda de frecuencia (spectrum) o por instante de su pasado reciente (waveform), y scene.viz.equalizer las dibuja. Cada parte se elige o se reemplaza: la forma de los elementos, su disposición y sus colores. Como son señales normales, también sirven para dibujar a mano o en un shader.
"band", igual que band, los agudos también se mueven aunque suenen menos; con "global" las barras conservan la forma real del espectro (graves altos, agudos bajos). Un rango inválido u otro normalize lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
bands | int | 32 | Bandas, de 1 a 256. |
low / high | float | 40 / None | Rango en Hz; high es por defecto la frecuencia más alta del análisis, unos 11 kHz. |
smoothing | float | 0.0 | Suavizado de cada banda, en [0, 1). |
normalize | str | "band" | "band" mide cada banda contra su propio máximo; "global", todas contra la más fuerte. |
list[Computed] — Una señal de 0 a 1 por banda, de la más grave a la más aguda.i lee la pista span * (1 - i / (points - 1)) segundos atrás, así que los valores avanzan hacia el primer punto mientras suena, como el trazo de una grabadora. Valores fuera de rango lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
points | int | 64 | Instantes, de 2 a 512. |
span | float | 2.0 | Segundos que abarca, hacia atrás desde el presente. |
low / high | float | None | None | Sigue esa banda en lugar del volumen; los dos o ninguno. |
smoothing | float | 0.0 | Suavizado, en [0, 1). |
list[Computed] — El volumen en points instantes de los últimos span segundos, el más antiguo primero.waveform sigue el volumen, de 0 a 1; samples dibuja la forma de las notas con un span de unas centésimas de segundo. Valores fuera de rango lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
points | int | 256 | Instantes, de 2 a 1024. |
span | float | 0.05 | Segundos que abarca, hacia atrás desde el presente. |
list[Computed] — La onda del sonido en esos instantes, de −1 a 1, la más antigua primero.onda = musica.samples(200, span=0.03)
osciloscopio = scene.geometry.polyline(
[(-6 + 12 * i / 199, computed(lambda v: 1.5 * v, inputs=[s])) for i, s in enumerate(onda)]
).stroke(GOLD, 0.03)
scene.play(musica)
scene.wait(3)
scene.render()ValueError; una función que no devuelve lo esperado, TypeError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
values | Sequence[ScalarSource] | obligatorio | Señales de 0 a 1: spectrum(), waveform(), Parameter, Computed o números. Lo que sale de ese rango se recorta. |
shape | str | Callable | "bar" | "bar", "capsule" (extremos redondos), "dot" (en la punta), "line" (una curva por las puntas) o "area" (esa curva rellena hasta la base). O una función shape(scene, index, count) que devuelve cualquier objeto, que se centra en su lugar, se gira a lo largo y se estira a su longitud. |
layout | str | Callable | "row" | "row" (crece hacia arriba desde la base de una caja de width × height), "mirror" (crece hacia los dos lados de una línea media) o "radial" (alrededor de un círculo de radius, desde start_angle en sentido horario, hacia fuera). O una función layout(index, count) que devuelve (x, y, ángulo): dónde empieza cada elemento y en qué dirección crece, en radianes. |
fill | Paint | list | FalloffColor | Callable | None | Una pintura, una lista que se repite, una rampa Falloff sobre el grupo (Falloff.index().gradient(...)) o fill(index, count). |
value_colors | Sequence[ColorLike] | None | None | Colorea cada elemento según su propio valor a lo largo de estos colores. Requiere señales que solo dependen del tiempo, como las de audio. |
center / width / height / radius | — | (0, 0) / 8 / 2 / 1.5 | Dónde y de qué tamaño. Cada elemento mide de min_length (0.04) a height. |
gap / thickness | float | 0.25 / None | Fracción de cada hueco que queda vacía, o un grosor fijo. |
stretch | bool | True | Con una forma propia, False la escala entera en lugar de estirarla, para iconos que deben conservar su proporción. |
Drawable — Un grupo con un miembro por valor, en orden: objetos normales que aceptan cualquier estilo, efecto o animación.import math
from gaanim import BLUE, CORAL, GOLD, Falloff, Scene
scene = Scene(frame=(16, 9), background="#0b1020")
music = scene.media.audio("assets/ritmo.ogg")
bars = scene.viz.equalizer(music.spectrum(32, smoothing=0.3), center=(-4, 1.5), width=6.5,
fill=Falloff.index().gradient(BLUE, GOLD))
ring = scene.viz.equalizer(music.spectrum(40), shape="dot", layout="radial", center=(4, 1.5),
radius=1.2, height=0.8, fill=[CORAL, GOLD])
trace = scene.viz.equalizer(music.waveform(96, span=2), shape="line", layout="mirror",
center=(0, -2.2), width=12, height=1.6)
scene.play(music)
scene.wait(music.duration)
scene.render()Las mismas señales llegan a un shader como uniforms de PostProcess.shader. Un shader no indexa campos, así que el ejemplo genera una función que elige la banda:
from gaanim import PostProcess, Scene
scene = Scene(frame=(16, 9), background="#0b1020")
music = scene.media.audio("assets/ritmo.ogg")
bands = music.spectrum(16, smoothing=0.3)
pick = "\n".join(f" if (i == {i}u) {{ return gaanim_uniforms.b{i}; }}" for i in range(16))
scene.canvas.post = PostProcess.shader(f"""
fn band(i: u32) -> f32 {{
{pick}
return 0.0;
}}
fn gaanim_post(uv: vec2<f32>, resolution: vec2<f32>, time: f32) -> vec4<f32> {{
let column = min(u32(uv.x * 16.0), 15u);
if (1.0 - uv.y < 0.3 * band(column)) {{
return vec4<f32>(0.2, 0.8, 0.95, 1.0);
}}
return gaanim_scene(uv);
}}
""", uniforms={f"b{i}": band for i, band in enumerate(bands)})
scene.play(music)
scene.wait(music.duration)
scene.render()Para más detalle, audio= le da al shader el espectrograma entero de la pista sin gastar uniforms (usa uno, gaanim_audio_time, reservado). El shader llama a:
gaanim_audio_spectrum(x, ago): el espectro enx, de 0 (20 Hz) a 1 (unos 11 kHz) en escala logarítmica, cada frecuencia de 0 a 1 contra su propio máximo, tal como sonaba haceagosegundos;gaanim_audio_level(ago): el volumen;gaanim_audio_band(low, high, ago): la media entre dos frecuencias en Hz;gaanim_audio_x(hz): dónde cae una frecuencia en la escala dex.
Leer varios ago dibuja la historia del espectro: un espectrograma que avanza mientras suena. Todas valen 0 mientras la pista no suena. Los paquetes .gaanim guardan los datos una sola vez. También sirve en shader_effect.
from gaanim import PostProcess, Scene
scene = Scene(frame=(16, 9), background="#0b1020")
music = scene.media.audio("assets/ritmo.ogg")
scene.canvas.post = PostProcess.shader("""
fn gaanim_post(uv: vec2<f32>, resolution: vec2<f32>, time: f32) -> vec4<f32> {
// Izquierda: hace 3 s; derecha: ahora. Abajo los graves.
let energy = gaanim_audio_spectrum(1.0 - uv.y, 3.0 * (1.0 - uv.x));
let heat = vec3<f32>(energy, energy * energy, 0.3 + 0.7 * energy * energy * energy);
return vec4<f32>(mix(gaanim_scene(uv).rgb, heat, energy), 1.0);
}
""", audio=music)
scene.play(music)
scene.wait(music.duration)
scene.render()Narración
Gaanim graba tu voz sin salir del editor y la sincroniza con la escena, sin editor de video externo. Hay dos formas de trabajar:
- Voz en off: la voz marca el ritmo. Cada bloque
scene.voiceover(...)tiene su propia toma y el script espera a marcas con nombre en lugar de llevar duraciones fijas. Puedes regrabar un bloque sin tocar los demás. - Toma en vivo: presentas la escena hablando. Cada
scene.stop()posterior ascene.live_take(...)se convierte en una pausa tan larga como la que hiciste al grabar.
Las tomas viven en narration/ dentro de la carpeta de assets: la de scene.assets.load_project(...) o, sin esa llamada, la del proyecto al que pertenece el script (el gaanim.toml más cercano hacia arriba). Fuera de un proyecto, o si un proyecto anterior ya tiene narration/ junto al script y no en sus assets, van junto al script: narration/<clave>.wav y, al lado, narration/<clave>.json con las marcas. Al grabar o editar esos archivos, el editor recarga la escena con los tiempos nuevos. render() y la exportación alargan el timeline para que ninguna toma quede cortada, y MP4/WebM mezclan las tomas como cualquier otra pista.
El guion
El texto que lees no tiene que vivir en el código. Escríbelo en narration/script.md con cualquier editor: cada encabezado ## clave contiene el texto de scene.voiceover("clave"), y un encabezado con el nombre de una diapositiva, sus notas del orador (ver El guion de la charla). Gaanim lo carga automáticamente; para usar otro archivo, llama antes a scene.narration_script("guion.md"). Al guardar el guion, el editor recarga la escena con las duraciones estimadas nuevas.
# Mi video sobre derivadas
## intro
Hoy vemos qué es una derivada: la pendiente de una curva en un punto.
### nota para mí: sonreír, sin prisa
## tangente
Movemos el punto por la recta y leemos su pendiente, que no cambia.
<!-- aquí respiro antes del cierre -->Un # título cierra el bloque anterior; los encabezados ### son notas para ti que no se leen, los comentarios HTML se ignoran y una línea [stop] o [stop nombre] marca dónde avanzas, y la voz no la lee. Las líneas seguidas se unen y una línea en blanco separa párrafos. Un ## vacío o repetido (sin distinguir mayúsculas) produce ValueError. El texto de un bloque se busca en este orden: el argumento text, su sección del guion y las notas del segmento; con un guion cargado, una clave sin sección emite un UserWarning. El icono del guion en la cabecera del panel crea el archivo o le añade las secciones de los bloques y las diapositivas que faltan, y las marcas de sus stops, y lo abre en el editor de GAANIM_EDITOR en lo primero que añadió. En una toma en vivo, el subtítulo del editor muestra el tramo del guion que toca decir, con ▶ y el nombre del stop donde toca avanzar (ver El prompter).
Grabar desde el editor
Abre el panel con el botón de micrófono de la barra de reproducción (o More controls → Script & narration). El panel se acopla a la derecha (ver El panel Script & narration); al final lista la toma en vivo y cada bloque de voz en off con su estado: Not recorded (duración estimada del texto) o Recorded, su nivel y de dónde sale su texto. Mientras grabas, un subtítulo abajo de la vista previa muestra el texto que toca decir y, debajo, el reloj, el medidor de nivel y las teclas.
- Record posiciona la escena al inicio del bloque y abre el micrófono predeterminado durante una cuenta atrás de tres segundos que sirve de prueba de nivel (no se guarda). Después graba mientras la escena avanza, con el texto del bloque en el subtítulo y la siguiente marca resaltada. Pulsa Espacio al decir cada marca, Enter para guardar y Esc para descartar.
- Record live take recarga la escena con sus paradas originales y graba mientras presentas. La reproducción se detiene en cada parada; → o Espacio continúa, Enter termina y Esc descarta. Con Also dictate the script with Scribe, Scribe de Zoom transcribe a la vez lo que dices y lo escribe en el guion (ver Dictar el guion con Scribe).
- Listen reproduce el bloque desde su inicio.
- Level lleva una toma ya grabada al nivel configurado.
Durante la grabación el audio del preview se silencia para que la toma anterior no se cuele en el micrófono.
Nivel de la voz
Al grabar, el medidor del subtítulo muestra los picos del micrófono en dBFS. La zona verde, entre −18 y −6 dBFS, es la ideal. Entre −6 y −3 dBFS el medidor pasa a ámbar, y por encima de −3 dBFS el borde del subtítulo se pone rojo con el aviso «Too loud!»: una palabra más fuerte podría saturar, y la saturación no se corrige después. Si algún tramo satura, el aviso lo cuenta hasta el final de la toma; si durante cuatro segundos nada llega a −30 dBFS, avisa de que la voz está muy baja.
Cada toma nueva se nivela al guardarla a −16 LUFS integrados, el nivel recomendado para voz en video online (YouTube reproduce a −14 y la televisión usa −23). FFmpeg mide la toma y le aplica una ganancia constante, con un pico real máximo de −1.5 dBTP y un filtro paso alto a 80 Hz. La grabación original se conserva en narration/.originals/. En Settings → Recording (Ctrl+,) puedes cambiar el nivel final, y avisa si eliges más de −14 LUFS o menos de −20, o desactivar la nivelación. Las tomas sin nivelar aparecen marcadas en el panel.
Whisper (opcional)
En lugar de pulsar Espacio, whisper.cpp puede detectar cuándo dices cada palabra. Instala whisper-cli y un modelo ggml-*.bin, e indícalos en Settings → Transcription (vacío busca whisper-cli en el PATH). Transcribe guarda en el JSON de la toma el tiempo de cada palabra; también puede hacerse automáticamente al guardar cada toma. Requiere FFmpeg. Para que una marca se resuelva por transcripción, su nombre debe ser la palabra o frase que dices; se ignoran mayúsculas, tildes y puntuación.
Cómo se resuelven las marcas
Cada marca se busca, en este orden: la marca pulsada al grabar (o escrita a mano en el JSON), la transcripción de Whisper y, por último, su posición proporcional dentro del texto del bloque. Las marcas se buscan después de la anterior, así que una palabra repetida corresponde a su siguiente aparición; pedir otra vez el mismo nombre devuelve el mismo tiempo. Una marca que no se encuentra emite un UserWarning y no espera.
Sin toma grabada, la duración se estima del texto a unas 150 palabras por minuto, de modo que puedes componer y revisar la escena antes de grabar.
El JSON es editable: mover un número retima la escena sin tocar el código.
{
"version": 1,
"markers": { "derivada": 2.4, "pendiente": 5.1 },
"words": [{ "text": "Hoy", "start": 0.12, "end": 0.3 }],
"holds": [],
"loudness": -16.0
}markers guarda segundos desde el inicio de la toma, words la transcripción, holds los segundos de cada pausa de una toma en vivo y loudness el nivel en LUFS al que se niveló.
API de narración
Los bloques de voz en off, sus marcas y la toma en vivo.
Voiceover, usable con with. Una toma grabada (.wav, .flac, .mp3, .m4a, .aac, .ogg u .opus) suena desde aquí y fija la duración del bloque; si no existe, la duración se estima del texto. Al salir del with se espera el resto de la toma. Claves inválidas o repetidas, volumen negativo o tomas ilegibles lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
key | str | obligatorio | Nombre de la toma: letras, dígitos, -, _ o .. Es el nombre de archivo dentro de narration/. |
text | str | None | None | Texto del bloque: teleprompter y estimación de tiempos. Sin él se usa la sección ## key del guion y, si no existe, las notes del segmento activo. |
volume | float | 1.0 | Ganancia lineal de la toma. |
from gaanim import Scene
scene = Scene()
scene.segment("derivada", notes="Hoy vemos la derivada y su pendiente")
titulo = scene.text("La derivada", role="title").move_to(0, 2.5)
curva = scene.geometry.circle(1.5)
with scene.voiceover("intro") as vo:
scene.play([titulo.animate.write()])
vo.wait_until("derivada")
scene.play([curva.animate.create()], duration=vo.until("pendiente"))
print(f"grabada: {vo.recorded} · {vo.duration:.1f} s · termina en {vo.end:.1f} s")
scene.render()grabada: False · 3.2 s · termina en 3.2 sValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
marker | str | obligatorio | Nombre de la marca: palabra o frase del guion, o el nombre pulsado al grabar. |
duration para que una animación termine justo en la marca.vo = scene.voiceover("cierre", text="Y eso es todo por hoy")
scene.play([logo.animate.fade_in()], duration=vo.until("todo"))
vo.finish()with al salir.False mientras la duración sea estimada).narration/script.md si existe. Las secciones con el nombre de una diapositiva son sus notas del orador y sustituyen a notes=. Llámala antes de los bloques que la usan; las diapositivas la leen la llames donde la llames. Un archivo ilegible o un ## vacío o repetido producen ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
path | str | obligatorio | Archivo Markdown; una ruta relativa se busca en la carpeta de assets, o junto al script si la escena no tiene assets. |
None — Los bloques que empiezan después y todas las diapositivas leen su texto de este guion.scene.narration_script("guion.md")
with scene.voiceover("intro") as vo:
vo.wait_until("derivada")stop() posterior espera lo que duró tu pausa, así que el timeline se reproduce de corrido en sincronía con tu voz. Sin toma, las paradas siguen siendo interactivas. Una segunda toma en vivo, una clave inválida o usada por una voz en off, o una toma ilegible producen ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
key | str | "live" | Nombre de la toma dentro de narration/. |
volume | float | 1.0 | Ganancia lineal de la toma. |
None — Declara la única toma en vivo de la escena.scene.live_take("clase")
scene.segment("intro", notes="Presentamos el problema")
scene.play([titulo.animate.write()])
scene.stop()
scene.segment("idea", notes="La idea clave es…")
scene.play([diagrama.animate.fade_in()])
scene.stop()
scene.render()