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).
[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.
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.
Group display lines into render pieces, each a list of (line_index, word).
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))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.
One dict(s, e, w) per line from the pieces’ word timings, or None for a line with no words.
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.
The voice (kind, ref) for a part in metre meter, where None means prose.
Group line indices into verses, each ending on a line with a double daṇḍa.
Render lines in one voice, returning the Recitation and its kind, reference, error rate and line timings.
Recite one section’s lines, returning the Recitation and its parts, error rates and line timings.
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')])Recite sections, each dict(lines=..., kind=..., ref=...), as one Recitation with timings per section.
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).
Render each job in the JSON lines file jobs and append its timings to results.