core

Sanskrit recitation from text with Vāgdhenu: a chant voice for verse, a prose voice for gadya, and line timings.

This module loads Vāgdhenu and renders Sanskrit text with it. Vāgdhenu (code, weights, Apache-2.0) is a fine-tune of IndicF5, a text-to-speech model, with a BigVGAN-v2 vocoder, trained on one speaker’s chanting. Its source code, BigVGAN’s and IndicF5’s (the f5_tts package, which is not the f5-tts on PyPI) are not pip packages. So on first use fetch_src downloads each of them at a fixed commit, listed in SRCS, into the cache folder, and setup_paths puts them on sys.path. The weights come from the Hugging Face Hub. best_device picks CUDA if it is there, then Apple’s MPS, then the CPU.


source

fetch_src

def fetch_src(
    name
):

The path to the source code of name at its fixed commit, downloaded on first use.


source

setup_paths

def setup_paths():

Put Vāgdhenu’s src, BigVGAN and IndicF5 on sys.path.


source

best_device

def best_device():

The best available torch device: cuda, then mps, then cpu.

Text

The model reads plain Devanagari, so clean(s) removes Vedic accent marks and digits, and reduces each run of daṇḍas (। and ॥, with the verse number between them) to a single daṇḍa, keeping ॥ where there was one. Vāgdhenu has no svaras (Vedic pitch accents) and chants in the classical, laukika, style.

n_syll(s) counts the akṣaras, the syllables of Devanagari text: each independent vowel and each consonant not followed by a virāma. chunk_prose(text, max_syll) splits prose into pieces Vāgdhenu can render: first at daṇḍas, then between words, so that no piece has more than max_syll (24) akṣaras.


source

clean

def clean(
    s
):

Plain Devanagari for the model, without accents, digits or verse numbers between daṇḍas.

test_eq(clean('अग्निमी॑ळे पु॒रोहि॑तं ॥ १ ॥'), 'अग्निमीळे पुरोहितं ॥')
test_eq(clean('धर्मक्षेत्रे कुरुक्षेत्रे ।'), 'धर्मक्षेत्रे कुरुक्षेत्रे ।')

source

n_syll

def n_syll(
    s
):

The number of akṣaras in Devanagari s: independent vowels and consonants without a virāma.

test_eq(n_syll('कर्मण्येवाधिकारस्ते'), 8)

source

chunk_prose

def chunk_prose(
    text, max_syll:int=24
):

Split prose at daṇḍas, then between words, into pieces of at most max_syll akṣaras.

p = chunk_prose('अथातो धर्मजिज्ञासा । चोदनालक्षणोऽर्थो धर्मः । तस्य निमित्तपरीष्टिः ॥')
test_eq(p, ['अथातो धर्मजिज्ञासा', 'चोदनालक्षणोऽर्थो धर्मः', 'तस्य निमित्तपरीष्टिः'])
assert all(n_syll(c) <= 24 for c in chunk_prose('क '*80))

Sound

The raw render can sound thin, so polish(src, dst, fx) applies a named sound treatment to an audio file with ffmpeg. The treatments in FX were chosen by ear for listeners, not sound engineers. Each ends by normalising loudness to -16 LUFS, so every recording plays at the same volume, and none moves any sound in time: the echo adds decay after a syllable, never delay before it, so line and word timings stay valid. warm, the default, adds a little warmth below 250 Hz, softens hissing s sounds and adds the short echo of a small room. phone brings the words forward for small speakers. plain only normalises loudness.


source

polish

def polish(
    src, dst, fx:str='warm', kbps:int=96
):

Apply the sound treatment FX[fx] to audio file src and write it to dst.

test_eq(sorted(FX), ['phone', 'plain', 'warm'])
assert all(v.endswith(LN) for v in FX.values())

Recitation

A Recitation is what every render returns: the audio as a numpy array, its sample rate sr, one (start_ms, end_ms) span per piece in spans, the text of each piece in texts, and, once Su-śrotā has checked it, the character error rate cer. A piece is a line given to Chanter.chant or a chunk made by Chanter.read. ms is the length in milliseconds. save writes a wav file, or an mp3 through ffmpeg when the path ends in .mp3, optionally shifted pitch semitones and with an FX treatment; the timings stay valid. concat, or +, joins recitations with a silence between them and moves the later spans along to match.


source

Recitation

def Recitation(
    audio:numpy.ndarray, spans:list=<factory>, texts:list=<factory>, sr:int=24000, cer:float=None
)->None:

Recitation(audio: numpy.ndarray, spans: list = , texts: list = , sr: int = 24000, cer: float = None)


source

concat

def concat(
    recs, gap:float=0.8
):

Join recitations with gap seconds of silence between them, moving their spans to match.

a = Recitation(np.zeros(SR, np.float32), [(0, 900)], ['क'])
b = a + a
test_eq(b.ms, 2800)
test_eq(b.spans, [(0, 900), (1800, 2700)])

Chanter

Chanter() loads Vāgdhenu’s network, the vocoder and the bank of reference recordings once, and then renders as often as you like. Vāgdhenu sings a text in the style of a reference recording: the reference sets the pace and melody, and the timbre is always the fine-tuned voice. The bank holds one reference for each metre Vāgdhenu supports, plus gadya references for prose.

  • chant(lines) sings one verse given as a list of its lines, or as a string that it splits at newlines and daṇḍas. It finds the metre with ganapati through meter, renders against that metre’s reference, and uses vasantatilakā when nothing is detected. Pass meter= to choose one. The result has one span per line.
  • read(text) reads prose, split by chunk_prose, against the gadya reference by default, or against another reference given as ref. The result has one span per chunk.
  • render(pieces, ref) is what both call: it renders already cleaned pieces against one reference.
  • meters() lists the metres in the bank, and bank_ref(name) finds a metre’s reference under any spelling (IAST, plain ASCII or the recording’s file name), or returns None.

pace stretches every syllable; the default of 1.15 is unhurried, for chanting along. seed picks the random seed for a render; a different seed gives a slightly different performance. voice chooses between the two fine-tuned voices in VOICES. On Apple silicon the main network runs on MPS and BigVGAN on the CPU, because MPS has no transposed convolution with more than 65,536 output channels.


source

Chanter

def Chanter(
    voice:str='steer', device:NoneType=None, nfe:int=64, speed:float=0.9, gap:float=0.55, prose_gap:float=0.3,
    tail:float=0.18, fade:float=0.12, pad:float=0.5, pace:float=1.15
):

Renders Sanskrit verse and prose to audio with Vāgdhenu, with a time span per line.

Rendering needs the weights, about 1.5 GB downloaded on first use, and takes several times real time on Apple silicon, so these cells do not run in the tests.

ch = Chanter()
r = ch.chant(['वसुदेवसुतं देवं कंसचाणूरमर्दनम्', 'देवकीपरमानन्दं कृष्णं वन्दे जगद्गुरुम्'])
r.spans, r.save('/tmp/krishna.mp3')
ch.read('अथातो धर्मजिज्ञासा । चोदनालक्षणोऽर्थो धर्मः ।').save('/tmp/mimamsa.mp3')