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: 2 additions & 2 deletions .github/workflows/continuous-integration-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ jobs:
name: Test and Build
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
node-version: [20.x]
os: [ubuntu-latest, macOS-latest]
node-version: [22.x]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
Expand Down
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},
"license": "MIT",
"devDependencies": {
"sass-embedded": "^1.83.0",
"vuepress": "^2.0.0-rc.14"
},
"dependencies": {
Expand Down
69 changes: 69 additions & 0 deletions packages/helloao-cli/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,75 @@ export async function importApi(
}
}

/**
* A single chapter's worth of audio timing data to import.
*/
export interface AudioTimingRecord {
/**
* The ID of the translation.
*/
translationId: string;

/**
* The ID of the book.
*/
bookId: string;

/**
* The number of the chapter.
*/
chapterNumber: number;

/**
* The reader that the timings are for.
*/
reader: string;

/**
* The times (in seconds) that each verse starts, in verse order.
*/
verses: number[];
}

/**
* Imports chapter audio timing data from the given JSON file into the database in the
* current working directory.
*
* The file should contain an array of records: `{ translationId, bookId, chapterNumber, reader, verses }`
* where `verses` is an array of numbers (seconds) - one per verse in the chapter, in order.
* @param file The path to the JSON file to import.
* @param options The options.
*/
export async function importAudioTimings(
file: string,
options: ImportTranslationOptions
): Promise<void> {
const logger = log.getLogger();
const records: AudioTimingRecord[] = JSON.parse(
await readFile(file, 'utf-8')
);

const db = await database.getDb(options.db);
try {
const importTimings = db.transaction(() => {
for (let record of records) {
database.upsertChapterAudioTiming(
db,
record.translationId,
record.bookId,
record.chapterNumber,
record.reader,
record.verses
);
}
});
importTimings();
logger.log(`Imported ${records.length} audio timing records.`);
} finally {
db.close();
}
}

export interface FetchTranslationsOptions {
/**
* Fetch all translations. If omitted, only undownloaded translations will be fetched.
Expand Down
13 changes: 13 additions & 0 deletions packages/helloao-cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
generateTranslationFiles,
generateTranslationsFiles,
importApi,
importAudioTimings,
importCommentaries,
importCommentary,
importTranslation,
Expand Down Expand Up @@ -413,6 +414,18 @@ async function start() {
});
});

program
.command('import-audio-timings <file>')
.description(
'Imports chapter audio timing data from the given JSON file into the database.\nThe file should contain an array of records: { translationId, bookId, chapterNumber, reader, verses } where verses is an array of numbers (seconds) - one per verse in the chapter, in order.'
)
.action(async (file: string, options: any) => {
await importAudioTimings(file, {
...program.opts(),
...options,
});
});

program
.command('upload-test-translation <input>')
.description(
Expand Down
90 changes: 90 additions & 0 deletions packages/helloao-cli/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,22 @@ export function insertTranslationContent(
UPDATE SET
url=excluded.url;`);

const chapterAudioTimingUpsert = db.prepare(`INSERT INTO ChapterAudioTiming(
translationId,
bookId,
number,
reader,
timingsJson
) VALUES (
@translationId,
@bookId,
@number,
@reader,
@timingsJson
) ON CONFLICT(translationId,bookId,number,reader) DO
UPDATE SET
timingsJson=excluded.timingsJson;`);

const insertChaptersAndVerses = db.transaction(() => {
for (let chapter of chapters) {
let verses: {
Expand Down Expand Up @@ -594,12 +610,67 @@ export function insertTranslationContent(
});
}
}

for (let reader in chapter.thisChapterAudioTimings) {
const verses = chapter.thisChapterAudioTimings[reader];
if (verses) {
chapterAudioTimingUpsert.run({
translationId: translation.id,
bookId: book.id,
number: chapter.chapter.number,
reader: reader,
timingsJson: JSON.stringify(verses),
});
}
}
}
});

insertChaptersAndVerses();
}

/**
* Inserts or updates the audio timing (per-verse start times, in seconds) for a chapter and reader.
* @param db The database to insert the timing into.
* @param translationId The ID of the translation.
* @param bookId The ID of the book.
* @param chapterNumber The number of the chapter.
* @param reader The reader that the timing is for.
* @param verses The times (in seconds) that each verse starts, in verse order.
*/
export function upsertChapterAudioTiming(
db: Database,
translationId: string,
bookId: string,
chapterNumber: number,
reader: string,
verses: number[]
) {
db.prepare(
`INSERT INTO ChapterAudioTiming(
translationId,
bookId,
number,
reader,
timingsJson
) VALUES (
@translationId,
@bookId,
@number,
@reader,
@timingsJson
) ON CONFLICT(translationId,bookId,number,reader) DO
UPDATE SET
timingsJson=excluded.timingsJson;`
).run({
translationId,
bookId,
number: chapterNumber,
reader,
timingsJson: JSON.stringify(verses),
});
}

/**
* Updates the hashes for the translations in the database.
* @param db The database to update the hashes in.
Expand Down Expand Up @@ -1766,6 +1837,14 @@ export async function* loadTranslationDatasets(
orderBy: [{ number: 'asc' }, { reader: 'asc' }],
});

const audioTimings = await db.chapterAudioTiming.findMany({
where: {
translationId: translation.id,
bookId: book.id,
},
orderBy: [{ number: 'asc' }, { reader: 'asc' }],
});

const bookChapters: TranslationBookChapter[] = chapters.map(
(chapter) => {
return {
Expand All @@ -1778,6 +1857,17 @@ export async function* loadTranslationDatasets(
acc[link.reader] = link.url;
return acc;
}, {} as any),
thisChapterAudioTimings: audioTimings
.filter(
(timing) =>
timing.number === chapter.number
)
.reduce((acc, timing) => {
acc[timing.reader] = JSON.parse(
timing.timingsJson
);
return acc;
}, {} as any),
};
}
);
Expand Down
5 changes: 5 additions & 0 deletions packages/helloao-cli/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,11 @@ export async function loadTranslationsFromDirectory(
chapter: chapterJson.chapter,
thisChapterAudioLinks:
chapterJson.thisChapterAudioLinks,
// Audio timings can't be reconstructed from the generated JSON files:
// thisChapterAudioTimings there is a map of reader -> URL, not the
// raw per-verse timing data. Re-importing timings requires the
// `import-audio-timings` CLI command instead.
thisChapterAudioTimings: {},
});
} else {
logger.warn(`Unknown chapter format: ${chapterFile}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "ChapterAudioTiming" (
"number" INTEGER NOT NULL,
"bookId" TEXT NOT NULL,
"translationId" TEXT NOT NULL,
"reader" TEXT NOT NULL,
"timingsJson" TEXT NOT NULL,

PRIMARY KEY ("translationId", "bookId", "number", "reader"),
CONSTRAINT "ChapterAudioTiming_translationId_bookId_fkey" FOREIGN KEY ("translationId", "bookId") REFERENCES "Book" ("translationId", "id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "ChapterAudioTiming_translationId_fkey" FOREIGN KEY ("translationId") REFERENCES "Translation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "ChapterAudioTiming_translationId_bookId_number_fkey" FOREIGN KEY ("translationId", "bookId", "number") REFERENCES "Chapter" ("translationId", "bookId", "number") ON DELETE RESTRICT ON UPDATE CASCADE
);
36 changes: 36 additions & 0 deletions packages/helloao-cli/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ApiCommentaryBooksSchema,
ApiDatasetBookChapterSchema,
ApiDatasetBooksSchema,
ApiTranslationBookChapterAudioTimingsSchema,
ApiTranslationBookChapterSchema,
ApiTranslationBooksSchema,
ApiTranslationCompleteBookSchema,
Expand Down Expand Up @@ -37,6 +38,11 @@ const chapter = z.number().positive().meta({
'The chapter number to get the content for. This should be a positive integer.',
});

const reader = z.string().meta({
description:
'The ID of the reader to get the audio timings for. For example, "hays" for the Hays reading of the Berean Standard Bible.',
});

export function createFreeUseBibleApiOpenApiDocument(): any {
return createDocument({
openapi: '3.1.0',
Expand Down Expand Up @@ -155,6 +161,36 @@ export function createFreeUseBibleApiOpenApiDocument(): any {
},
},
},
'/api/{translation}/{book}/{chapter}.{reader}.audioTimings.json':
{
get: {
operationId: 'getTranslationBookChapterAudioTimings',
description:
'Get the audio timings (per-verse start times, in seconds) for a specific chapter of a specific book for a specific translation and reader.',
requestParams: {
path: z.object({
translation,
book,
chapter,
reader,
}),
},
responses: {
'200': {
description: '200 OK',
content: {
'application/json': {
schema: ApiTranslationBookChapterAudioTimingsSchema,
},
},
},
'404': {
description:
'404 Not Found - The specified translation, book, chapter, or reader was not found.',
},
},
},
},
'/api/{translation}/complete.json': {
get: {
operationId: 'getTranslationComplete',
Expand Down
21 changes: 21 additions & 0 deletions packages/helloao-cli/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ model Translation {
verses ChapterVerse[]
footnotes ChapterFootnote[]
audioUrls ChapterAudioUrl[]
audioTimings ChapterAudioTiming[]
}

model Commentary {
Expand Down Expand Up @@ -149,6 +150,7 @@ model Book {
verses ChapterVerse[]
footnotes ChapterFootnote[]
audioUrls ChapterAudioUrl[]
audioTimings ChapterAudioTiming[]

@@id([translationId, id])
}
Expand Down Expand Up @@ -216,6 +218,7 @@ model Chapter {
verses ChapterVerse[]
footnotes ChapterFootnote[]
audioUrls ChapterAudioUrl[]
audioTimings ChapterAudioTiming[]

@@id([translationId, bookId, number])
}
Expand Down Expand Up @@ -256,6 +259,24 @@ model ChapterAudioUrl {
@@id([translationId, bookId, number, reader])
}

model ChapterAudioTiming {
number Int
bookId String
book Book @relation(fields: [translationId, bookId], references: [translationId, id])

translationId String
translation Translation @relation(fields: [translationId], references: [id])

chapter Chapter @relation(fields: [translationId, bookId, number], references: [translationId, bookId, number])

reader String

// JSON array of numbers: the time (in seconds) that each verse starts, in verse order.
timingsJson String

@@id([translationId, bookId, number, reader])
}

model ChapterVerse {
number Int

Expand Down
Loading
Loading