recite

A passage from its display lines to one audio file with [start, end] and word timings per line.

recite takes a passage as the lines a reader sees and returns one audio file with timings for every line and word. This page builds it up in steps.

Pieces

Vāgdhenu renders text in pieces, and expects the two pādas (quarter-verses) of a half-verse as one piece, even when the book prints them on two lines. pieces(lines) groups the display lines accordingly: a piece ends after a word that contains a daṇḍa. In prose (prose=True) a piece also ends before it would pass max_syll (24) akṣaras, and in verse before a line that would take it past max_verse (44). Each piece is a list of (line_index, word) pairs, so word timings can be mapped back to their lines. A word that cleans to nothing, such as a daṇḍa or a verse number, stays in its piece and gets no time of its own. piece_text gives the cleaned text of a piece for rendering.


source

pieces

def pieces(
    lines, prose:bool=False, max_syll:int=24, max_verse:int=44
):

Group display lines into render pieces, each a list of (line_index, word).


source

word_text

def word_text(
    w
):
ls = ['नित्यानन्दकरी वराभयकरी सौन्दर्यरत्नाकरी', 'निर्धूताखिलघोरपावनकरी प्रत्यक्षमाहेश्वरी ।', 'प्रालेयाचलवंशपावनकरी काशीपुराधीश्वरी', 'भिक्षां देहि कृपावलम्बनकरी मातान्नपूर्णेश्वरी ॥ १ ॥']
ps = pieces(ls)
test_eq(len(ps), 2)
test_eq({i for i,_ in ps[0]}, {0, 1})
test_eq(ps[1][-1], (3, '॥'))
test_eq(len(pieces(['उपदेशं व्याख्यास्यामः॥'], prose=True)), 1)
assert all(sum(n_syll(word_text(w)) for _,w in p) <= 24 for p in pieces(['क '*60], prose=True))

source

piece_text

def piece_text(
    p
):

Lines from words

Word timings come back per piece. line_times(n, ps, words) turns them into one dict(s, e, w) per display line: the start of its first timed word, the end of its last, and the list of its words with their times. A line with no words gets None.


source

line_times

def line_times(
    n, ps, words
):

One dict(s, e, w) per line from the pieces’ word timings, or None for a line with no words.

ps = [[(0,'क'), (0,'ख'), (1,'ग')], [(1,'॥')]]
lt = line_times(3, ps, [[['क',0,100], ['ख',100,200], ['ग',300,400]], [['॥',400,400]]])
test_eq([lt[0]['s'], lt[0]['e'], lt[1]['s'], lt[1]['e']], [0, 200, 300, 400])
test_eq(lt[2], None)

Reciting sections

A section is a passage, such as a verse with its commentary, that becomes one stretch of audio. recite_section splits it into parts, each recited in a single voice, either chanted or read. By default the section is split into verses with verses, each ending on a line with a double daṇḍa, and each verse is scanned by ganapati to find its metre. ganapati applies the rules of the śloka, where Vāgdhenu’s own detector accepted a 32-syllable prose sentence as anuṣṭubh, and it recognises the āryā family, whose metres count syllable lengths instead of syllables.

part_voice then picks the voice. A metre with a reference in Vāgdhenu’s bank is chanted against that reference, another syllable-counted metre (a vṛtta) is chanted against vasantatilakā, and an āryā-family metre, or no metre at all, is read against the anuṣṭubh reference. You can give the parts yourself as parts=[dict(lines=[...], meter=...)], with line indices, and kind='chant' or kind='read' makes the whole section one voice. recite_part renders one part, keeping the best of tries seeds by Su-śrotā’s character error rate, and returns the timings of its lines. shift moves a set of timings later, so that each part’s timings count from the start of the whole recording.


source

part_voice

def part_voice(
    ch, meter
):

The voice (kind, ref) for a part in metre meter, where None means prose.


source

closes

def closes(
    l
):

source

verses

def verses(
    lines
):

Group line indices into verses, each ending on a line with a double daṇḍa.

test_eq(verses(['क ।', 'ख ॥ १ ॥', 'ग', 'घ']), [[0, 1], [2, 3]])

source

recite_part

def recite_part(
    ch, ear, lines, kind:NoneType=None, ref:NoneType=None, tries:int=3, max_cer:float=0.08
):

Render lines in one voice, returning the Recitation and its kind, reference, error rate and line timings.


source

recite_section

def recite_section(
    ch, ear, lines, kind:NoneType=None, ref:NoneType=None, parts:NoneType=None, gap:float=0.8, **kw
):

Recite one section’s lines, returning the Recitation and its parts, error rates and line timings.


source

shift

def shift(
    info, off
):

Move all the timings in info off milliseconds later.

The cell below checks this bookkeeping with a stand-in chanter and ear, without the models. A verse that scans is chanted, the prose line after it is read, and the second part’s timings start after the first part plus the gap. Parts given explicitly get the voice their metre calls for.

recite(ch, ear, sections) is the top-level call. Each section is a dict with lines and optionally kind, ref and parts. It recites every section, joins them with gap (1.2) seconds of silence, and returns the Recitation and a list with one dict per section: its kind (chant, read or mixed), its first part’s ref, its worst cer, its parts, and its lines timings.

from vachaka.core import Recitation
from vachaka.check import Ear
class FakeCh:
    gap, prose_gap = 0.5, 0.3
    def meter(self, lines): return 'anushtubh' if len(lines) == 2 else ''
    def bank_ref(self, name): return {'anuṣṭubh': 'anuṣṭubh', 'mālinī': 'mālinī'}.get(name)
    def render(self, ps, ref, seed, gap): return Recitation(np.zeros(len(ps)*24000, np.float32), [(i*1000, i*1000+900) for i in range(len(ps))], ps)
class FakeEar:
    def hear(self, audio, sr): return ''
    heard = Ear.heard   # best_of hears a render a piece at a time
    def words(self, rec, ws): return [[[w, s, e] for w in p] for (s,e),p in zip(rec.spans, ws)]
r, info = recite_section(FakeCh(), FakeEar(), ['धर्मक्षेत्रे कुरुक्षेत्रे समवेता युयुत्सवः ।', 'मामकाः पाण्डवाश्चैव किमकुर्वत सञ्जय ॥ १ ॥', 'इति प्रश्नः ॥'])
test_eq([p['kind'] for p in info['parts']], ['chant', 'read'])
test_eq(info['kind'], 'mixed')
test_eq(info['lines'][2]['s'], 2000 + 800)           # two 1 s pieces, then the gap
_, info = recite_section(FakeCh(), FakeEar(), ['क ख ग', 'घ ङ च ॥', 'छ ज झ ।'])
test_eq([p['kind'] for p in info['parts']], ['chant', 'read'])  # the line after the verse is its own part, and scans as nothing
ps = [dict(lines=[0, 1], meter='mālinī'), dict(lines=[2], meter='āryā'), dict(lines=[3], meter='śikhariṇī'), dict(lines=[4], meter=None)]
_, info = recite_section(FakeCh(), FakeEar(), ['क ।', 'ख ॥', 'ग ॥', 'घ ॥', 'ङ ।'], parts=ps)
test_eq([(p['kind'], p['ref']) for p in info['parts']], [('chant', 'mālinī'), ('read', 'anuṣṭubh'), ('chant', 'vasantatilaka'), ('read', 'anuṣṭubh')])

source

recite

def recite(
    ch, ear, sections, gap:float=1.2, **kw
):

Recite sections, each dict(lines=..., kind=..., ref=...), as one Recitation with timings per section.

CLI

vachaka-recite jobs.jsonl results.jsonl renders a file of jobs, one JSON object per line. Each job is dict(id, out, sections), where sections is what recite takes. It writes the audio to out (mp3 or wav) and appends dict(id, out, ms, sections) to the results file, with sections holding the timings. A job whose id is already in the results is skipped, so a stopped run carries on where it left off.

The options are --voice (steer), --tries (3) and --max_cer (0.08) for re-rendering, --pace (1.15), --pitch (1.0, so one semitone up by default) and --fx, the sound treatment from core.FX (warm, or none for the raw render).


source

recite_cli

def recite_cli(
    jobs:str, results:str, voice:str='steer', tries:int=3, max_cer:float=0.08, fx:str='warm', pace:float=1.15,
    pitch:float=1.0
):

Render each job in the JSON lines file jobs and append its timings to results.