Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
bfarias-godaddy committed Sep 21, 2020
0 parents commit df56f1e
Show file tree
Hide file tree
Showing 49 changed files with 1,534 additions and 0 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/sync.yaml
@@ -0,0 +1,56 @@
# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches: [ master ]
# pull_request:
# branches: [ master ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Build
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
run: |
mkdir -p build
cd build
for txt in ../levels/*.txt ; do
node ../actions/generate_audio_files.js "$txt"
done
cp -r ../scaffold/. ./
node ../actions/build_manifest.js ../levels/*.txt
- name: Commit files
run: |
# bail if there are no changes
# git diff-index --quiet HEAD -- && exit
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add build/
git commit -m "Generate audio and rebuild website" -a
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifact
uses: actions/upload-artifact@v1.0.0
with:
# Artifact name
name: ${{ github.sha }}
# Directory containing files to upload
path: build
40 changes: 40 additions & 0 deletions CONTRIBUTING.md
@@ -0,0 +1,40 @@
# Design

## Text Corpus

All text used in the interface is configured by files in [`/levels`](/levels).

Files should be created for `1.txt` to `12.txt` for grade level texts.
These files should have lines of text to be spoken by the application at the appropriate level. E.G. with a file `1.txt`:

```txt
I am happy.
Birds Sing.
```

Would create 2 audio snippets and texts to be read at grade level 1 in the corresponding order: `I am happy.` then `Birds sing.`.

Any file that is not `NUMBER.txt` will be treated as a special ad-hoc file and audio for the first line of the file will be generated.

The only file used for this nature is [`help.txt`](/levels/help.txt)

## Changing the user interface

| File | Purpose |
| ---- | ---- |
| [`/scaffold/player.html`](/scaffold/index.html) | This file represents all the visual containers that the application uses. It does not contain styles. It does not contain interactivity for events such as clicking buttons. If you need to add a space to put text, add a button, or change the organization of containers you likely want to edit this file. |
| [`/scaffold/interactivity.js`](/scaffold/interactivity.js) | This file represents all behaviors that respond to user interface events. If you are looking to react to some event like the grade changing, you likely want to edit this file. NOTE: this file uses IE11 compatible syntax and thus most modern features of JS are not used, intentionally. |
| [`/scaffold/style.css`](/scaffold/style.css) | This file represents all styling of elements in `player.html`. If you are looking to change colors, padding, or layout you likely want to edit this file. NOTE: button images are not styled by this file, in order to change those colors use an SVG editor. |

## Automation

The app will be placed in [`/build`](/build) every time the repository is pushed to.
This is controlled by [the github action](.github/workflows/sync.yaml).
The action uses scripts in [`/actions`](/actions) to take the files from `/levels` and `/scaffold`.
The scripts run commands to generate the appropriate files for the application under `/build`.

Roughly:

1. The texts in `/levels` are compiled into a mapping of "text snippet."->"audio_NUMBER.mp3" in [`/build/text-to-mp3s.json`](/build/text-to-mp3s.json). If audio for a text does not exist it is created as a new audio file, but if it already exists no action is taken. NOTE: Audio files are not deleted if they do not have an entry in `text-to-mp3s.json`.
1. The files in `/levels` are sorted by if they are a grade level or ad-hoc text, then placed into [`/build/mp3s.json`](/build/mp3s.json). This is the file used to generate what a user sees and hears when running the application.
1. The files in `/scaffold` are copied to `/build`.
24 changes: 24 additions & 0 deletions LICENSE
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <https://unlicense.org>
59 changes: 59 additions & 0 deletions README.md
@@ -0,0 +1,59 @@
# Reading Tutor

## LICENSE

[Unlicense](https://spdx.org/licenses/Unlicense.html)

## Generating audio files to be used for the web

In order to run the Reading Tutor, all lines to be read aloud must be
created before the app is opened. This gives better audio results and
makes it so that more web browsers are able to run Reading Tutor.

Prerequisites:

* The `node` CLI must be installed
* The `aws` CLI must be installed

In order to generate the audio files it needs to be given what text needs
to be converted into audio. In order to do so, create a `.txt` where each
line is a sentence to be read by Reading Tutor. E.G. a `grade1.txt` file
might look like:

```txt
I am happy.
Dogs bark.
Birds sing.
```

Running:

```console
node generate_audio_files.js grade1.txt
```

Will instruct `generate_audio_files.js` to read all lines of `grade1.txt`
and generate audio accordingly.

## Build the web page for the application

In order to run the Reading Tutor it needs to list the texts for the web
page to show and in what order. This is related to but independent of the
list of available audio files.

In order to build the web page, the list of grades for the
web page will be used when you run:

```console
node build_web_page.js 1=grade1.txt 2=grade2.txt
```

Would generate the web page with 2 grade levels `1` and `2` with text from
the 2 files `grade1.txt` and `grade2.txt`. Grades that are not whole numbers, are less than 1, or if the list has missing increments will cause errors.

## Hosting the web application

On a static file server, place a copy of the `build/` directory after
generating the audio files and the web page. It will contain a file
`index.html` that can be opened to run the Reading Tutor within a web
browser.
67 changes: 67 additions & 0 deletions actions/build_manifest.js
@@ -0,0 +1,67 @@
const fs = require('fs');
const path = require('path');
const levelTextPaths = process.argv.slice(2);

if (levelTextPaths.length === 0) {
console.error('Must include at least 1 text file to process');
process.exit(1);
}

const existingAudio = new Map(
JSON.parse(
fs.readFileSync('./text-to-mp3s.json', 'utf8')
)
);
const outPath = 'mp3s.json';
const adHoc = {};
const levels = {};
const seenAudio = new Set();
for (const levelTextPath of levelTextPaths) {
const level = path.basename(levelTextPath, '.txt');
const levelText = fs.readFileSync(levelTextPath, 'utf8');
if (level === `${+level}`) {
const lines = levels[level] = [];
// grade level
for (const line of levelText.split(/\r?\n/g)) {
if (line.trim().length === 0) continue;
const audioPath = existingAudio.get(line);
if (audioPath) {
seenAudio.add(audioPath);
lines.push([line, audioPath]);
} else {
console.error(`couldn't find audio for ${
JSON.stringify(line)}, found in ${levelTextPath}
`.replace(/\s+/, ' '));
process.exit(1);
}
}
} else {
if (level === 'levels') {
console.error(
'File named levels.txt is not allowed, please rename file.'
);
process.exit(1);
}
const audioPath = existingAudio.get(levelText);
if (audioPath) {
seenAudio.add(audioPath);
adHoc[level] = [levelText, audioPath];
} else {
console.error(`couldn't find audio for ${
JSON.stringify(levelText.replace(/(?:\r?\n)+$/, ''))
}, be sure it was generated, if it was a multi-line file,
it needs to be changed to a single line.
`.replace(/\s+/, ' '));
process.exit(1);
}
}
}
fs.writeFileSync(
outPath,
JSON.stringify({
...adHoc,
levels
}),
'utf8'
);
process.stdout.write([...seenAudio].join('\n') + '\n');
109 changes: 109 additions & 0 deletions actions/generate_audio_files.js
@@ -0,0 +1,109 @@
'use strict';
/**
* @author Matthew Carlson <mj.carlson801@gmail.com>
*/
//
// usage: node generate_audio_files.js lines.txt
//
// each line of the .txt file represents an audio clip to be generated
//
// e.g. to create a set of 3 audio files:
//
// ---- test.txt ----
// Test
// this
// out.
// --------
//
// it would create 3 audio files with "Test", "this", and "out."
//
// it will store the mapping of text to audio in a JSON file "text-to-mp3s.json"
//
// it will generate audio files of the name audio_NNN.mp3 where "NNN" is
// replaced with a number
//
// it will not generate audio files if a mapping already exists
//
const fs = require("fs");
const { spawnSync } = require("child_process");

//Capture text file to use
let fileToUse = process.argv[2];
if (typeof fileToUse !== "string") {
console.error("Please pass in a file to parse");
process.exit(1);
}

//Read file and convert to strings
let content = fs.readFileSync(fileToUse, "utf8").toString();

//create Map from file, but make an empty one if it fails
let contentMap;
try {
let readJSON = fs.readFileSync("text-to-mp3s.json", "utf8").toString();
let parsedJSON = JSON.parse(readJSON);
contentMap = new Map(parsedJSON);
} catch (e) {
contentMap = new Map();
}

//read txt-to-mp3s.json and push to Map

//create counter for filename
let counter = contentMap.size + 1;

//create function that checks if filename is already a value in the Map
let mp3File;
function createFilename() {
mp3File = "audio_" + counter + ".mp3";
while (Array.from(contentMap.values()).includes(mp3File) ||
fs.existsSync(mp3File) === true
) {
counter++;
mp3File = "audio_" + counter + ".mp3";
}
}

//populate Map
for (let text of content.split(/\r?\n/)) {
if (text.trim().length === 0) continue;
//check if text already exists
if (contentMap.has(text)) {
// do nothing
} else {
//add to Map
createFilename();
let result = spawnSync("aws", [
"--region",
"us-east-1",
"polly",
"synthesize-speech",
"--engine",
"neural",
"--language",
"en-US",
"--output-format",
"mp3",
"--voice-id",
"Joanna",
"--text-type",
"text",
"--text",
text,
mp3File
]);
console.log(result.stdout.toString());
console.log(result.stderr.toString());
if (result.status !== 0) {
console.error("Failed to generate audio file");
process.exit(1);
}
contentMap.set(text, mp3File);
}
}

//convert Map contents to JSON
let data = JSON.stringify([...contentMap]);

//update 'text-to-mp3s.json.'
fs.writeFileSync("./text-to-mp3s.json", data, { encoding: "utf8" });
Binary file added build/audio_1.mp3
Binary file not shown.
Binary file added build/audio_2.mp3
Binary file not shown.
Binary file added build/audio_3.mp3
Binary file not shown.
Binary file added build/audio_4.mp3
Binary file not shown.
Binary file added build/audio_5.mp3
Binary file not shown.
Binary file added build/audio_6.mp3
Binary file not shown.
Binary file added build/favicon.ico
Binary file not shown.
Binary file added build/icon_192.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added build/icon_512.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build/img_decreaseGradeLevel.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build/img_help.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build/img_increaseGradeLevel.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build/img_nextSentence.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build/img_pointer.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build/img_previousSentence.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build/img_speak.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit df56f1e

Please sign in to comment.