Procedurally generates a radio weather report

Initial commit

sanin.dev 4a260f11

+27
.github/workflows/npm-audit.yml
··· 1 + name: NPM Audit Check 2 + 3 + on: 4 + push: 5 + branches: [ master ] 6 + pull_request: 7 + branches: [ master ] 8 + schedule: 9 + - cron: '15 16 * * 5' 10 + 11 + jobs: 12 + 13 + npm_audit: 14 + name: Check NPM audit 15 + runs-on: ubuntu-latest 16 + timeout-minutes: 20 17 + strategy: 18 + fail-fast: true 19 + permissions: 20 + contents: read 21 + 22 + steps: 23 + - name: Checkout repository 24 + uses: https://github.com/actions/checkout@v4 25 + 26 + - name: NPM Audit 27 + run: npm audit
+104
.gitignore
··· 1 + # Logs 2 + logs 3 + *.log 4 + npm-debug.log* 5 + yarn-debug.log* 6 + yarn-error.log* 7 + lerna-debug.log* 8 + 9 + # Diagnostic reports (https://nodejs.org/api/report.html) 10 + report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 + 12 + # Runtime data 13 + pids 14 + *.pid 15 + *.seed 16 + *.pid.lock 17 + 18 + # Directory for instrumented libs generated by jscoverage/JSCover 19 + lib-cov 20 + 21 + # Coverage directory used by tools like istanbul 22 + coverage 23 + *.lcov 24 + 25 + # nyc test coverage 26 + .nyc_output 27 + 28 + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 + .grunt 30 + 31 + # Bower dependency directory (https://bower.io/) 32 + bower_components 33 + 34 + # node-waf configuration 35 + .lock-wscript 36 + 37 + # Compiled binary addons (https://nodejs.org/api/addons.html) 38 + build/Release 39 + 40 + # Dependency directories 41 + node_modules/ 42 + jspm_packages/ 43 + 44 + # TypeScript v1 declaration files 45 + typings/ 46 + 47 + # TypeScript cache 48 + *.tsbuildinfo 49 + 50 + # Optional npm cache directory 51 + .npm 52 + 53 + # Optional eslint cache 54 + .eslintcache 55 + 56 + # Microbundle cache 57 + .rpt2_cache/ 58 + .rts2_cache_cjs/ 59 + .rts2_cache_es/ 60 + .rts2_cache_umd/ 61 + 62 + # Optional REPL history 63 + .node_repl_history 64 + 65 + # Output of 'npm pack' 66 + *.tgz 67 + 68 + # Yarn Integrity file 69 + .yarn-integrity 70 + 71 + # dotenv environment variables file 72 + .env 73 + .env.test 74 + 75 + # parcel-bundler cache (https://parceljs.org/) 76 + .cache 77 + 78 + # next.js build output 79 + .next 80 + 81 + # nuxt.js build output 82 + .nuxt 83 + 84 + # gatsby files 85 + .cache/ 86 + public 87 + 88 + # vuepress build output 89 + .vuepress/dist 90 + 91 + # Serverless directories 92 + .serverless/ 93 + 94 + # FuseBox cache 95 + .fusebox/ 96 + 97 + # DynamoDB Local files 98 + .dynamodb/ 99 + 100 + 101 + # custom .gitignore 102 + config/config.json5 103 + distribution/ 104 + .env
+12
README.md
··· 1 + # morning-report 2 + 3 + randomly generate a radio show-style weather report according to a flexible config file. 4 + 5 + ## Usage 6 + 7 + ``` 8 + npm install 9 + npm run build 10 + npm start 11 + ``` 12 +
+118
config/config.example.json5
··· 1 + { 2 + "programs": [ 3 + [ 4 + "intro music", 5 + "intro", 6 + "hi's and low's", 7 + "notable weather", 8 + "signoff" 9 + ] 10 + ], 11 + "segments": { 12 + "intro": [ 13 + "intro 1", 14 + "intro 2" 15 + ], 16 + "hi's and low's": [ 17 + "hilo 1", 18 + "hilo 2" 19 + ], 20 + "notable weather": [ 21 + "rain 1", 22 + "rain 2", 23 + "storm 1", 24 + "storm 2", 25 + "snow 1", 26 + "hail 1" 27 + ], 28 + "signoff": [ 29 + "signoff 1", 30 + "signoff 2" 31 + ] 32 + }, 33 + "sequences": { 34 + "intro music": { 35 + "tracks": [ 36 + "audio/intro_music.flac" 37 + ] 38 + }, 39 + "intro 1": { 40 + "tracks": [ 41 + "audio/intro_01.flac" 42 + ] 43 + }, 44 + "intro 2": { 45 + "tracks": [ 46 + "audio/intro_02.flac" 47 + ] 48 + }, 49 + "hilo 1": { 50 + "tracks": [ 51 + "audio/hi_01.flac", 52 + "%cory hi", 53 + "audio/lo_01.flac", 54 + "%cory lo" 55 + ] 56 + }, 57 + "hilo 2": { 58 + "tracks": [ 59 + "audio/hi_02.flac", 60 + "%cory hi", 61 + "audio/lo_02.flac", 62 + "%cory lo" 63 + ] 64 + }, 65 + "rain 1": { 66 + "condition": "weather == rain", 67 + "tracks": [ 68 + "audio/rain1.flac" 69 + ] 70 + }, 71 + "rain 2": { 72 + "condition": "weather == rain", 73 + "tracks": [ 74 + "audio/rain2.flac" 75 + ] 76 + }, 77 + "storm 1": { 78 + "condition": "weather == storm", 79 + "tracks": [ 80 + "audio/storm1.flac" 81 + ] 82 + }, 83 + "storm 2": { 84 + "condition": "weather == storm", 85 + "tracks": [ 86 + "audio/storm2.flac" 87 + ] 88 + }, 89 + "snow 1": { 90 + "condition": "weather == snow", 91 + "tracks": [ 92 + "audio/snow1.flac" 93 + ] 94 + }, 95 + "hail 1": { 96 + "condition": "weather == hail", 97 + "tracks": [ 98 + "audio/hail1.flac" 99 + ] 100 + }, 101 + "signoff 1": { 102 + "tracks": [ 103 + "audio/signoff_01.flac" 104 + ] 105 + }, 106 + "signoff 2": { 107 + "tracks": [ 108 + "audio/signoff_02.flac" 109 + ] 110 + } 111 + }, 112 + "voices": { 113 + "cory": { 114 + "directory": "audio/voice/cory/", 115 + "extension": "flac" 116 + } 117 + } 118 + }
+115
package-lock.json
··· 1 + { 2 + "name": "morning-report", 3 + "version": "0.0.1", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "morning-report", 9 + "version": "0.0.1", 10 + "license": "MIT", 11 + "dependencies": { 12 + "json5": "2.2.3", 13 + "openweathermap-ts": "1.2.10" 14 + }, 15 + "devDependencies": { 16 + "@types/node": "24.3.0", 17 + "typescript": "5.9.2" 18 + } 19 + }, 20 + "node_modules/@types/node": { 21 + "version": "24.3.0", 22 + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", 23 + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", 24 + "dev": true, 25 + "license": "MIT", 26 + "dependencies": { 27 + "undici-types": "~7.10.0" 28 + } 29 + }, 30 + "node_modules/json5": { 31 + "version": "2.2.3", 32 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", 33 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 34 + "license": "MIT", 35 + "bin": { 36 + "json5": "lib/cli.js" 37 + }, 38 + "engines": { 39 + "node": ">=6" 40 + } 41 + }, 42 + "node_modules/node-fetch": { 43 + "version": "2.7.0", 44 + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 45 + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 46 + "license": "MIT", 47 + "dependencies": { 48 + "whatwg-url": "^5.0.0" 49 + }, 50 + "engines": { 51 + "node": "4.x || >=6.0.0" 52 + }, 53 + "peerDependencies": { 54 + "encoding": "^0.1.0" 55 + }, 56 + "peerDependenciesMeta": { 57 + "encoding": { 58 + "optional": true 59 + } 60 + } 61 + }, 62 + "node_modules/openweathermap-ts": { 63 + "version": "1.2.10", 64 + "resolved": "https://registry.npmjs.org/openweathermap-ts/-/openweathermap-ts-1.2.10.tgz", 65 + "integrity": "sha512-Zckv2aXN8ENSeAeroces2jJciLWb6aLNXEmvG6pmF+BcIMw2kwRo6++/AKUNoU5suOp47UWA6lllDV0TNm//OA==", 66 + "license": "MIT", 67 + "dependencies": { 68 + "node-fetch": "^2.6.0" 69 + } 70 + }, 71 + "node_modules/tr46": { 72 + "version": "0.0.3", 73 + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 74 + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", 75 + "license": "MIT" 76 + }, 77 + "node_modules/typescript": { 78 + "version": "5.9.2", 79 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", 80 + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", 81 + "dev": true, 82 + "license": "Apache-2.0", 83 + "bin": { 84 + "tsc": "bin/tsc", 85 + "tsserver": "bin/tsserver" 86 + }, 87 + "engines": { 88 + "node": ">=14.17" 89 + } 90 + }, 91 + "node_modules/undici-types": { 92 + "version": "7.10.0", 93 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", 94 + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", 95 + "dev": true, 96 + "license": "MIT" 97 + }, 98 + "node_modules/webidl-conversions": { 99 + "version": "3.0.1", 100 + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 101 + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", 102 + "license": "BSD-2-Clause" 103 + }, 104 + "node_modules/whatwg-url": { 105 + "version": "5.0.0", 106 + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 107 + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 108 + "license": "MIT", 109 + "dependencies": { 110 + "tr46": "~0.0.3", 111 + "webidl-conversions": "^3.0.0" 112 + } 113 + } 114 + } 115 + }
+34
package.json
··· 1 + { 2 + "name": "morning-report", 3 + "version": "0.0.1", 4 + "description": "Procedurally generates a radio weather report", 5 + "keywords": [ 6 + "weather", 7 + "radio", 8 + "audio" 9 + ], 10 + "repository": { 11 + "type": "git", 12 + "url": "ssh://git@git.sanin.dev:3689/corysanin/morning-report.git" 13 + }, 14 + "license": "MIT", 15 + "author": { 16 + "name": "Cory Sanin", 17 + "email": "corysanin@outlook.com", 18 + "url": "https://sanin.dev/" 19 + }, 20 + "type": "module", 21 + "main": "distribution/index.js", 22 + "scripts": { 23 + "build": "npx tsc", 24 + "start": "node distribution/index.js" 25 + }, 26 + "dependencies": { 27 + "json5": "2.2.3", 28 + "openweathermap-ts": "1.2.10" 29 + }, 30 + "devDependencies": { 31 + "typescript": "5.9.2", 32 + "@types/node": "24.3.0" 33 + } 34 + }
+23
src/index.ts
··· 1 + import path from 'path'; 2 + import fsp from 'fs/promises'; 3 + import json5 from 'json5'; 4 + import Sequencer from './sequencer.js'; 5 + import type {Programs, Segments, Sequences} from './sequencer.js'; 6 + import type { Voices } from './voice.js'; 7 + 8 + 9 + interface Config { 10 + programs: Programs, 11 + segments: Segments, 12 + sequences: Sequences, 13 + voices: Voices 14 + } 15 + 16 + console.log('morning-report\nCory Sanin 2025\n'); 17 + 18 + const config: Config = json5.parse(await fsp.readFile(process.env['CONFIG'] || path.join('config', 'config.json5'), { encoding: 'utf-8' })); 19 + const sequence = Sequencer(config); 20 + console.log(sequence.join('\n')); 21 + 22 + 23 + export type { Config };
+54
src/sequencer.ts
··· 1 + import type { Config } from './index.js'; 2 + 3 + type SegmentName = string; 4 + type SequenceName = string; 5 + type Programs = SegmentName[][]; 6 + type Segments = { [segment: SegmentName]: SequenceName[] }; 7 + type Sequence = { 8 + condition?: string; 9 + tracks: string[]; 10 + } 11 + type Sequences = { [sequence: SequenceName]: Sequence }; 12 + 13 + let config: Config = null; 14 + 15 + function selectOne<T>(arr: T[]): T { 16 + return arr[Math.floor(Math.random() * arr.length)]; 17 + } 18 + 19 + function conditionIsMet(condition: string | undefined = undefined): boolean { 20 + if (typeof condition !== 'string') { 21 + return true; 22 + } 23 + // TODO: parse condition, return bool 24 + return false; 25 + } 26 + 27 + function processSequence(sequence: Sequence): string[] { 28 + const tracks = sequence.tracks; 29 + // TODO: process voice macros 30 + return tracks; 31 + } 32 + 33 + function processSegment(segment: SegmentName): string[] { 34 + if (!(segment in config.segments)) { 35 + return processSequence(config.sequences[segment]); 36 + } 37 + const potentialSequences: SequenceName[] = config.segments[segment].filter(s => conditionIsMet(config.sequences[s].condition)); 38 + if (potentialSequences.length === 0) { 39 + return []; 40 + } 41 + return processSequence(config.sequences[selectOne(potentialSequences)]); 42 + } 43 + 44 + function Sequencer(conf: Config): string[] { 45 + config = conf; 46 + const sequence: string[] = []; 47 + const program: SegmentName[] = selectOne(conf.programs); 48 + program.forEach(segment => sequence.push(...processSegment(segment))); 49 + return sequence; 50 + } 51 + 52 + export default Sequencer; 53 + export { Sequencer }; 54 + export type { SegmentName, SequenceName, Programs, Segments, Sequence, Sequences };
src/stitcher.ts

This is a binary file and will not be displayed.

+10
src/voice.ts
··· 1 + 2 + 3 + interface Voice { 4 + directory: string; 5 + extension: string; 6 + } 7 + 8 + type Voices = { [name: string]: Voice }; 9 + 10 + export type { Voice, Voices };
src/weather.ts

This is a binary file and will not be displayed.

+17
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "lib": ["ESNext"], 4 + "module": "NodeNext", 5 + "target": "ESNext", 6 + 7 + "esModuleInterop": true, 8 + "skipLibCheck": true, 9 + "moduleResolution": "node16", 10 + "sourceMap": true, 11 + "inlineSources": true, 12 + "rootDir": "./src", 13 + "outDir": "./distribution" 14 + }, 15 + "include": ["src/**/*"], 16 + "exclude": ["**/*.spec.ts"] 17 + }